Skip to content

Commit 357c650

Browse files
committed
Refactor and enhance Shellflow codebase
- Updated `dry_run` and `timeout_global` fields in `shellflow_models.py` for better clarity and functionality. - Improved variable parsing in `variables.py`, including changes to error handling and regex matching. - Adjusted test assertions in `test_annotations.py`, `test_config.py`, and `test_doctor.py` for consistency in string formatting. - Enhanced test coverage in `test_helpers.py`, `test_hooks.py`, and `test_macros.py` with updated assertions and improved readability. - Added comprehensive regression tests in `test_scotty_absorption_regressions.py` to cover new features and ensure stability. - Fixed various minor issues across tests to maintain consistency and improve reliability.
1 parent 14ccec4 commit 357c650

26 files changed

Lines changed: 1989 additions & 613 deletions

README.md

Lines changed: 241 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ ShellFlow is a minimal shell script orchestrator for mixed local and remote exec
1717
## What It Does
1818

1919
- Split a shell script into `@LOCAL` and `@REMOTE` execution blocks.
20-
- Run each block fail-fast, in order.
21-
- Reuse the shared prelude before the first marker for every block.
20+
- Run blocks sequentially by default, or run annotated groups with `--mode parallel`.
21+
- Freeze uppercase prelude assignments once so values such as `BUILD_ID=$(date +%s)` stay consistent across local and remote blocks.
22+
- Declare script parameters with `# @option` and pass them as CLI flags or agent schema values.
23+
- Run focused parts of a playbook with `# @TASK` and `shellflow run --task`.
24+
- Reuse command snippets with `# @MACRO` and `# @HELPER`.
25+
- Add local lifecycle hooks for setup, cleanup, success, and failure handling.
2226
- Pass the previous block output forward as `SHELLFLOW_LAST_OUTPUT`.
2327
- Export named scalar values from a block into later block environments.
2428
- Emit either a final JSON report or streaming JSON Lines events for agents.
2529
- Support bounded `@TIMEOUT` and `@RETRY` directives without embedding workflow logic.
26-
- Provide non-interactive, dry-run, and audit-log modes for automated execution.
27-
- Resolve remote targets from `~/.ssh/config` or a custom SSH config path.
30+
- Provide non-interactive, dry-run, audit-log, doctor, and agent-run modes for automated execution.
31+
- Resolve remote targets from inline `@SERVER` definitions, `~/.ssh/config`, or a custom SSH config path.
2832

2933
## Quick Start
3034

@@ -79,31 +83,41 @@ uv pip install -e .
7983
shellflow --version
8084
```
8185

82-
## Script Format
86+
## Playbook Format
8387

84-
Shellflow recognizes two markers:
88+
Shellflow playbooks are ordinary shell scripts plus comment markers. Marker names are shown uppercase here and should be written that way for readability; the parser accepts marker names case-insensitively.
89+
90+
### Block markers
8591

8692
- `# @LOCAL`
8793
- `# @REMOTE <ssh-host>`
8894

89-
Shellflow also recognizes bounded block directives at the top of a block body:
95+
`<ssh-host>` must match an inline `@SERVER` definition or a `Host` entry in your SSH config. Shellflow then connects using the configured `host`, `HostName`, `User`, `Port`, and identity key values.
96+
97+
### Block directives
98+
99+
Block directives must appear immediately after the `# @LOCAL` or `# @REMOTE <ssh-host>` marker, before the first command in that block.
90100

91101
- `# @TIMEOUT <seconds>`
92102
- `# @RETRY <count>`
93103
- `# @EXPORT NAME=stdout|stderr|output|exit_code`
94-
- `# @SHELL <shell>` - Specify the shell to use (e.g., `zsh`, `bash`)
104+
- `# @SHELL <shell>` - specify `bash`, `zsh`, or `sh`.
105+
- `# @PARALLEL [group]` - mark this block for grouped parallel execution.
95106

96-
`<ssh-host>` must match a `Host` entry in your SSH config. Shellflow then connects using that SSH host definition, which means the actual machine can be resolved through the configured `HostName`, `User`, `Port`, and `IdentityFile` values.
107+
`@PARALLEL` may appear immediately before a block marker or as a block directive. It applies only to that one block. Consecutive parallel blocks are grouped together when you run with `--mode parallel`.
97108

98109
Example:
99110

100111
```bash
101112
#!/bin/bash
102113
set -euo pipefail
103114

115+
# @option release-name=
116+
# @option branch=main
117+
104118
# @LOCAL
105119
# @EXPORT VERSION=stdout
106-
echo "runs locally"
120+
echo "$RELEASE_NAME-$BRANCH"
107121

108122
# @REMOTE sui
109123
uname -a
@@ -113,6 +127,163 @@ echo "remote output: $SHELLFLOW_LAST_OUTPUT"
113127
echo "version = $VERSION"
114128
```
115129

130+
### Dynamic options
131+
132+
Declare script parameters near the top of the file:
133+
134+
```bash
135+
# @option staging
136+
# @option branch=main
137+
# @option release-name=
138+
```
139+
140+
- `# @option staging` is boolean. Passing `--staging` sets `STAGING=1`.
141+
- `# @option branch=main` has a default. Passing `--branch develop` sets `BRANCH=develop`.
142+
- `# @option release-name=` is required. Pass `--release-name v1` or set `RELEASE_NAME` in the environment.
143+
- Option names become uppercase environment variables with dashes converted to underscores.
144+
145+
Run it:
146+
147+
```bash
148+
shellflow run deploy.sh --branch develop --release-name v2026.05.01 --staging
149+
```
150+
151+
### Preamble freeze
152+
153+
Lines before the first block marker are the shared prelude. Uppercase assignments in the prelude are evaluated once locally and then exported into every block with a frozen value:
154+
155+
```bash
156+
BUILD_ID=$(date +%s)
157+
158+
# @LOCAL
159+
echo "$BUILD_ID"
160+
161+
# @REMOTE sui
162+
echo "$BUILD_ID"
163+
```
164+
165+
Both blocks receive the same `BUILD_ID`. Non-assignment prelude lines, such as `set -euo pipefail` and helper functions, are still prepended to each block.
166+
167+
Keep the prelude declarative. Avoid one-time side effects such as `cd`, `rm`, or deployment commands before the first marker.
168+
169+
### Tasks and macros
170+
171+
Use `# @TASK <name>` to label blocks and `--task` to run only that task:
172+
173+
```bash
174+
# @TASK build
175+
# @LOCAL
176+
echo "build"
177+
178+
# @TASK deploy
179+
# @REMOTE sui
180+
echo "deploy"
181+
```
182+
183+
```bash
184+
shellflow run deploy.sh --task build
185+
```
186+
187+
Use a single-line macro to define a task flow:
188+
189+
```bash
190+
# @MACRO release build deploy smoke-test
191+
192+
# @TASK build
193+
# @LOCAL
194+
echo "build"
195+
196+
# @TASK deploy
197+
# @REMOTE sui
198+
echo "deploy"
199+
200+
# @TASK smoke-test
201+
# @LOCAL
202+
echo "smoke"
203+
```
204+
205+
```bash
206+
shellflow run deploy.sh --task release
207+
```
208+
209+
Macros can also expand command snippets inside a block:
210+
211+
```bash
212+
# @MACRO print_env
213+
# env | sort
214+
# @ENDMACRO
215+
216+
# @LOCAL
217+
print_env
218+
```
219+
220+
### Helpers
221+
222+
Helpers are reusable command snippets. They are expanded when a block contains only the helper name on a line:
223+
224+
```bash
225+
# @HELPER backup_db
226+
# pg_dump "$DATABASE_URL" > backup.sql
227+
# @ENDHELPER
228+
229+
# @LOCAL
230+
backup_db
231+
```
232+
233+
### Lifecycle hooks
234+
235+
Hooks run locally and share Shellflow's execution context:
236+
237+
- `PRE` - once before all main blocks.
238+
- `BEFORE` - before each main block.
239+
- `AFTER` - after each main block.
240+
- `SUCCESS` - after all main blocks succeed.
241+
- `ERROR` - after a hook or main block fails.
242+
- `FINISHED` - at the end, whether the run succeeds or fails.
243+
244+
`POST` is accepted as an alias for `AFTER`; `FINALLY` is accepted as an alias for `FINISHED`.
245+
246+
```bash
247+
# @HOOK PRE
248+
# echo "prepare"
249+
# @ENDHOOK
250+
251+
# @HOOK ERROR
252+
# echo "rollback or collect diagnostics"
253+
# @ENDHOOK
254+
255+
# @HOOK FINISHED
256+
# echo "cleanup"
257+
# @ENDHOOK
258+
```
259+
260+
### Parallel groups
261+
262+
Mark each block that should join the parallel group:
263+
264+
```bash
265+
# @PARALLEL web
266+
# @REMOTE web-1
267+
systemctl restart nginx
268+
269+
# @PARALLEL web
270+
# @REMOTE web-2
271+
systemctl restart nginx
272+
273+
# @LOCAL
274+
echo "runs after the parallel group"
275+
```
276+
277+
Run with:
278+
279+
```bash
280+
shellflow run restart.sh --mode parallel
281+
```
282+
283+
Without `--mode parallel`, blocks run sequentially even if annotated.
284+
285+
### Remote shells and tracing
286+
116287
Using `@SHELL` for remote servers with non-bash default shells:
117288

118289
Shellflow starts remote shells in login mode. For remote `zsh` and `bash` blocks, Shellflow also bootstraps `~/.zshrc` or `~/.bashrc` quietly before running your commands so tools initialized there, such as `mise`, remain available in non-interactive automation even if the rc file exits non-zero.
@@ -131,8 +302,25 @@ compdef
131302
ls -la
132303
```
133304

305+
Remote verbose tracing uses the shell's `DEBUG` trap and executes the block as one native script. That preserves multi-line Bash and zsh constructs such as `if/else/fi`, `for` loops, and function definitions.
306+
134307
## SSH Configuration
135308

309+
You can define remote hosts inline:
310+
311+
```bash
312+
# @SERVER sui
313+
# host: 192.168.1.100
314+
# user: deploy
315+
# port: 22
316+
# key: ~/.ssh/id_ed25519
317+
318+
# @REMOTE sui
319+
hostname
320+
```
321+
322+
Inline server definitions are useful for portable playbooks. The `host` field is required; `user`, `port`, and `key` are optional.
323+
136324
Example `~/.ssh/config` entry:
137325

138326
```sshconfig
@@ -152,7 +340,7 @@ hostname
152340

153341
This is intentional:
154342

155-
- Shellflow accepts configured SSH host names, not arbitrary free-form targets.
343+
- Shellflow accepts configured SSH host aliases, not arbitrary comma-separated or free-form targets.
156344
- Unknown remote targets fail early with a clear error before spawning `ssh`.
157345
- You can override the default config path with `--ssh-config`.
158346

@@ -199,6 +387,8 @@ echo "prelude is active"
199387
echo "prelude is also active here"
200388
```
201389

390+
Uppercase assignments in the prelude are special: Shellflow evaluates them once locally, freezes the values, and exports them into every block. That keeps release IDs, timestamps, and option-derived values stable across local and remote execution.
391+
202392
## Agent-Native Usage
203393

204394
Shellflow is designed to be the execution substrate for an outer agent, not an embedded planner.
@@ -208,47 +398,66 @@ Shellflow is designed to be the execution substrate for an outer agent, not an e
208398
- Use `--no-input` for CI or agent runs where interactive prompts must fail deterministically.
209399
- Use `--dry-run` to preview planned execution without running commands.
210400
- Use `--audit-log <path>` to mirror the structured event stream into a redacted JSONL file.
401+
- Use `agent-run --json-input` when an agent already has the script body and option values in memory.
211402

212403
Recommended agent flow:
213404

214405
1. Generate or select a plain shell script with `@LOCAL` and `@REMOTE` markers.
215-
2. Add bounded directives only where needed: `@TIMEOUT`, `@RETRY`, and `@EXPORT`.
216-
3. Run with `--json` or `--jsonl`.
217-
4. Let the outer agent decide whether to retry, branch, or stop based on Shellflow's structured result.
406+
2. Read `# @option` declarations and provide required values.
407+
3. Add bounded directives only where needed: `@TIMEOUT`, `@RETRY`, and `@EXPORT`.
408+
4. Run with `--json` or `--jsonl`.
409+
5. Let the outer agent decide whether to retry, branch, or stop based on Shellflow's structured result.
410+
411+
Agent-run input:
412+
413+
```bash
414+
shellflow agent-run --json-input '{
415+
"script": "# @option release-name=\n# @LOCAL\necho \"$RELEASE_NAME\"\n",
416+
"options": {"release-name": "v2026.05.01"},
417+
"dry_run": false
418+
}'
419+
```
218420

219421
Shellflow intentionally does not provide:
220422

221423
- Conditional directives such as `@IF stdout_contains=...`
222424
- A workflow DSL or embedded ReAct loop
223425
- Heuristic destructive-command detection
426+
- Multi-host comma expansion inside one `@REMOTE` marker
224427

225428
Those decisions belong in the outer agent or automation layer.
226429

227-
### Agent-Native Logging Optimizations
430+
### Agent-Native Reporting
228431

229-
Shellflow includes several logging optimizations specifically designed for LLM agent consumption:
432+
Shellflow's structured modes are designed for LLM agent consumption:
230433

231-
- **ANSI Escape Sequence Stripping**: Automatically removes color codes, cursor movements, and other terminal control sequences that would consume unnecessary tokens in LLM context windows.
434+
- **Stable run and block identifiers**: JSON and JSONL output include `run_id`, one-based block indexes, and stable `block-N` identifiers.
232435

233-
- **Command-Level Granularity**: Uses `set -x` with custom `PS4` prompts to provide per-command execution traces, allowing agents to pinpoint exactly which command failed in a multi-command block.
436+
- **Separated stdout and stderr**: Block reports keep stdout, stderr, combined output, exit code, failure kind, retries, timeouts, and exported values explicit.
234437

235-
- **Stream Debouncing**: Implements intelligent buffering for progress bars and streaming output, preventing token waste from rapid `\r` updates while ensuring important output isn't lost.
438+
- **Command-level remote tracing**: Remote verbose execution uses shell `DEBUG` traps to report the command about to run without breaking multi-line shell syntax.
236439

237-
- **Semantic Pipeline**: Transforms raw terminal output into structured, agent-friendly data with clean text and preserved timing information.
440+
- **Audit-safe exports**: Audit logs redact exported values whose names look secret-like, such as `TOKEN`, `SECRET`, or `PASSWORD`.
238441

239-
These optimizations ensure that when agents process Shellflow's JSONL output, they receive clean, actionable data without the visual formatting artifacts that make terminal output human-friendly but LLM-confusing.
442+
- **Bounded verbose output**: `--output-lines` limits verbose per-command log tails while preserving full block output in structured results.
240443

241444
## CLI
242445

243446
```text
244447
shellflow run <script>
245448
shellflow run <script> --verbose
449+
shellflow run <script> --output-lines 50
246450
shellflow run <script> --json
247451
shellflow run <script> --jsonl
248452
shellflow run <script> --no-input
249453
shellflow run <script> --dry-run
454+
shellflow run <script> --mode parallel
455+
shellflow run <script> --task <task-or-macro>
250456
shellflow run <script> --audit-log ./audit.jsonl --jsonl
251457
shellflow run <script> --ssh-config ./ssh_config
458+
shellflow run <script> --release-name v1 --branch main
459+
shellflow agent-run --json-input '{"script":"# @LOCAL\necho hi\n"}'
460+
shellflow doctor [script]
252461
shellflow --version
253462
```
254463

@@ -262,8 +471,12 @@ shellflow run playbooks/hello.sh --jsonl --no-input
262471
shellflow run playbooks/hello.sh --dry-run --jsonl
263472
shellflow run playbooks/hello.sh --audit-log ./audit.jsonl --jsonl
264473
shellflow run playbooks/hello.sh --ssh-config ~/.ssh/config.work
474+
shellflow run playbooks/deploy.sh --task release --mode parallel --json
475+
shellflow doctor playbooks/deploy.sh --ssh-config ~/.ssh/config.work
265476
```
266477

478+
Run `shellflow run --help`, `shellflow agent-run --help`, or `shellflow doctor --help` for the exact command options supported by the installed version.
479+
267480
## Development
268481

269482
Useful commands:
@@ -327,6 +540,13 @@ The release workflow then runs verification, builds distributions with `uv build
327540
```text
328541
shellflow/
329542
├── src/shellflow.py
543+
├── src/advanced_modes.py
544+
├── src/config.py
545+
├── src/doctor.py
546+
├── src/helpers.py
547+
├── src/hooks.py
548+
├── src/macros.py
549+
├── src/variables.py
330550
├── tests/
331551
├── features/
332552
├── playbooks/

0 commit comments

Comments
 (0)