flowsh-cli 0.8.3__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/PKG-INFO +25 -17
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/README.md +24 -16
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/pyproject.toml +3 -2
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/src/flowsh_cli/__init__.py +2 -2
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/src/flowsh_cli/cli.py +26 -4
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/src/flowsh_cli/models.py +48 -3
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/src/flowsh_cli/render.py +92 -23
- flowsh_cli-0.9.0/src/flowsh_cli/skill.py +244 -0
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/src/flowsh_cli/__main__.py +0 -0
- {flowsh_cli-0.8.3 → flowsh_cli-0.9.0}/src/flowsh_cli/examples.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowsh-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Generate Bash harness scripts from workflow YAML files.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
|
|
17
17
|
# flowsh-cli
|
|
18
18
|
|
|
19
|
-
`flowsh-cli` turns workflow YAML into executable
|
|
19
|
+
`flowsh-cli` turns workflow YAML into executable shell scripts.
|
|
20
20
|
|
|
21
21
|
Use it when you want a small, reproducible way to encode a workflow once and rerun it locally or in CI.
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ uvx flowsh-cli path/to/workflows.yml
|
|
|
28
28
|
|
|
29
29
|
That reads the workflow YAML at the path you provide and writes harness scripts to the current working directory.
|
|
30
30
|
|
|
31
|
-
In this repository, the example workflow file lives at
|
|
31
|
+
In this repository, the example workflow file lives at `workflows.yml`.
|
|
32
32
|
|
|
33
33
|
Useful flags:
|
|
34
34
|
|
|
@@ -68,18 +68,23 @@ workflows:
|
|
|
68
68
|
expandPrompt: true
|
|
69
69
|
prompt: |
|
|
70
70
|
Review issue ${ISSUE_NUMBER} and summarize the repository state.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
- type: parallel
|
|
72
|
+
steps:
|
|
73
|
+
- type: bash
|
|
74
|
+
run: echo "worker A"
|
|
75
|
+
- type: bash
|
|
76
|
+
run: echo "worker B"
|
|
77
|
+
- type: for
|
|
78
|
+
in: ITEMS
|
|
79
|
+
item: ITEM
|
|
80
|
+
steps:
|
|
81
|
+
- type: bash
|
|
82
|
+
run: echo "$ITEM"
|
|
83
|
+
- type: while
|
|
84
|
+
condition: '[ -n "$(ls doc/plan/steps/planned/ 2>/dev/null)" ]'
|
|
85
|
+
steps:
|
|
86
|
+
- type: bash
|
|
87
|
+
run: echo "keep looping until the queue is empty"
|
|
83
88
|
```
|
|
84
89
|
|
|
85
90
|
Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` in the current working directory.
|
|
@@ -90,8 +95,9 @@ Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` i
|
|
|
90
95
|
|---|---|---|
|
|
91
96
|
| `vars` | Execute shell commands and export their stdout into shell variables | Variable names must be uppercase shell identifiers. |
|
|
92
97
|
| `bash` | Run shell commands | Runs with `bash -euo pipefail`. |
|
|
93
|
-
| `agent` | Call
|
|
98
|
+
| `agent` | Call the agent runtime | Supports `agent`, `model`, `command`, `capture`, `expandPrompt`, and `dangerouslySkipPermissions`. |
|
|
94
99
|
| `for` | Iterate over newline-delimited values from a previous `vars` step | Flat iteration only; nested `for` steps are not supported. |
|
|
100
|
+
| `while` | Re-evaluate a Bash condition before each iteration | Use for dynamic queues or other stateful loops that must keep discovering new work. |
|
|
95
101
|
| `parallel` | Run child steps concurrently | Children run as separate branches and the parent waits for all of them. |
|
|
96
102
|
|
|
97
103
|
## Agent Behavior
|
|
@@ -100,7 +106,9 @@ Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` i
|
|
|
100
106
|
|
|
101
107
|
`expandPrompt: true` does plain text replacement only. It does not evaluate shell expressions like `$(...)`, backticks, or globs.
|
|
102
108
|
|
|
103
|
-
Set `
|
|
109
|
+
Set `capture: VARIABLE_NAME` on an `agent` step when you want the full agent output stored in a shell variable for later `vars` or `bash` steps.
|
|
110
|
+
|
|
111
|
+
Set `dangerouslySkipPermissions: true` only when you want the generated harness to pass `--dangerously-skip-permissions` to the agent runtime. The YAML alias `dangerously-skip-permissions` is also accepted.
|
|
104
112
|
|
|
105
113
|
## Validation And Safety
|
|
106
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# flowsh-cli
|
|
2
2
|
|
|
3
|
-
`flowsh-cli` turns workflow YAML into executable
|
|
3
|
+
`flowsh-cli` turns workflow YAML into executable shell scripts.
|
|
4
4
|
|
|
5
5
|
Use it when you want a small, reproducible way to encode a workflow once and rerun it locally or in CI.
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ uvx flowsh-cli path/to/workflows.yml
|
|
|
12
12
|
|
|
13
13
|
That reads the workflow YAML at the path you provide and writes harness scripts to the current working directory.
|
|
14
14
|
|
|
15
|
-
In this repository, the example workflow file lives at
|
|
15
|
+
In this repository, the example workflow file lives at `workflows.yml`.
|
|
16
16
|
|
|
17
17
|
Useful flags:
|
|
18
18
|
|
|
@@ -52,18 +52,23 @@ workflows:
|
|
|
52
52
|
expandPrompt: true
|
|
53
53
|
prompt: |
|
|
54
54
|
Review issue ${ISSUE_NUMBER} and summarize the repository state.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
- type: parallel
|
|
56
|
+
steps:
|
|
57
|
+
- type: bash
|
|
58
|
+
run: echo "worker A"
|
|
59
|
+
- type: bash
|
|
60
|
+
run: echo "worker B"
|
|
61
|
+
- type: for
|
|
62
|
+
in: ITEMS
|
|
63
|
+
item: ITEM
|
|
64
|
+
steps:
|
|
65
|
+
- type: bash
|
|
66
|
+
run: echo "$ITEM"
|
|
67
|
+
- type: while
|
|
68
|
+
condition: '[ -n "$(ls doc/plan/steps/planned/ 2>/dev/null)" ]'
|
|
69
|
+
steps:
|
|
70
|
+
- type: bash
|
|
71
|
+
run: echo "keep looping until the queue is empty"
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` in the current working directory.
|
|
@@ -74,8 +79,9 @@ Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` i
|
|
|
74
79
|
|---|---|---|
|
|
75
80
|
| `vars` | Execute shell commands and export their stdout into shell variables | Variable names must be uppercase shell identifiers. |
|
|
76
81
|
| `bash` | Run shell commands | Runs with `bash -euo pipefail`. |
|
|
77
|
-
| `agent` | Call
|
|
82
|
+
| `agent` | Call the agent runtime | Supports `agent`, `model`, `command`, `capture`, `expandPrompt`, and `dangerouslySkipPermissions`. |
|
|
78
83
|
| `for` | Iterate over newline-delimited values from a previous `vars` step | Flat iteration only; nested `for` steps are not supported. |
|
|
84
|
+
| `while` | Re-evaluate a Bash condition before each iteration | Use for dynamic queues or other stateful loops that must keep discovering new work. |
|
|
79
85
|
| `parallel` | Run child steps concurrently | Children run as separate branches and the parent waits for all of them. |
|
|
80
86
|
|
|
81
87
|
## Agent Behavior
|
|
@@ -84,7 +90,9 @@ Harness paths are derived from workflow ids. `wf_example` becomes `example.sh` i
|
|
|
84
90
|
|
|
85
91
|
`expandPrompt: true` does plain text replacement only. It does not evaluate shell expressions like `$(...)`, backticks, or globs.
|
|
86
92
|
|
|
87
|
-
Set `
|
|
93
|
+
Set `capture: VARIABLE_NAME` on an `agent` step when you want the full agent output stored in a shell variable for later `vars` or `bash` steps.
|
|
94
|
+
|
|
95
|
+
Set `dangerouslySkipPermissions: true` only when you want the generated harness to pass `--dangerously-skip-permissions` to the agent runtime. The YAML alias `dangerously-skip-permissions` is also accepted.
|
|
88
96
|
|
|
89
97
|
## Validation And Safety
|
|
90
98
|
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flowsh-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Generate Bash harness scripts from workflow YAML files."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -29,7 +29,7 @@ flowsh-cli = "flowsh_cli.cli:main"
|
|
|
29
29
|
|
|
30
30
|
[dependency-groups]
|
|
31
31
|
dev = [
|
|
32
|
-
"pytest>=
|
|
32
|
+
"pytest>=9.0.3,<10",
|
|
33
33
|
"ruff>=0.14,<0.15",
|
|
34
34
|
]
|
|
35
35
|
|
|
@@ -45,3 +45,4 @@ select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
|
45
45
|
|
|
46
46
|
[tool.ruff.lint.per-file-ignores]
|
|
47
47
|
"tests/test_workflow_to_harness.py" = ["E501"]
|
|
48
|
+
"src/flowsh_cli/skill.py" = ["E501"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""flowsh-cli: generate
|
|
1
|
+
"""flowsh-cli: generate shell scripts from workflow YAML files."""
|
|
2
2
|
|
|
3
3
|
from flowsh_cli.models import Workflow, WorkflowParseError, parse_workflows
|
|
4
4
|
from flowsh_cli.render import harness_path, render_harness
|
|
5
5
|
|
|
6
|
-
__version__ = "0.
|
|
6
|
+
__version__ = "0.9.0"
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
9
9
|
"Workflow",
|
|
@@ -14,11 +14,15 @@ from flowsh_cli import __version__
|
|
|
14
14
|
from flowsh_cli.examples import example_yaml, examples_index
|
|
15
15
|
from flowsh_cli.models import Workflow, WorkflowParseError, parse_workflows, workflow_schema_yaml
|
|
16
16
|
from flowsh_cli.render import harness_path, render_harness
|
|
17
|
+
from flowsh_cli.skill import skill_text
|
|
18
|
+
|
|
19
|
+
DEFAULT_WORKFLOW_YAML = Path("workflows.yml")
|
|
20
|
+
WORKFLOW_YAML_ARGUMENT = typer.Argument(DEFAULT_WORKFLOW_YAML, help="Path to workflow.yml")
|
|
17
21
|
|
|
18
22
|
app = typer.Typer(
|
|
19
23
|
add_completion=False,
|
|
20
24
|
context_settings={"terminal_width": 120, "max_content_width": 120},
|
|
21
|
-
help="Generate reproducible
|
|
25
|
+
help="Generate reproducible shell scripts from workflow YAML files.",
|
|
22
26
|
pretty_exceptions_enable=False,
|
|
23
27
|
)
|
|
24
28
|
|
|
@@ -32,9 +36,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
32
36
|
return 0
|
|
33
37
|
|
|
34
38
|
|
|
35
|
-
@app.command(help="Generate reproducible
|
|
39
|
+
@app.command(help="Generate reproducible shell scripts from workflow YAML files.")
|
|
36
40
|
def generate(
|
|
37
|
-
workflow_yaml:
|
|
41
|
+
workflow_yaml: Path = WORKFLOW_YAML_ARGUMENT,
|
|
38
42
|
workflow: Annotated[
|
|
39
43
|
str | None,
|
|
40
44
|
typer.Option(
|
|
@@ -90,6 +94,15 @@ def generate(
|
|
|
90
94
|
is_eager=True,
|
|
91
95
|
),
|
|
92
96
|
] = None,
|
|
97
|
+
skill: Annotated[
|
|
98
|
+
bool,
|
|
99
|
+
typer.Option(
|
|
100
|
+
"--skill",
|
|
101
|
+
callback=lambda value: print_skill(value),
|
|
102
|
+
help="Show the flowsh-cli skill description and exit.",
|
|
103
|
+
is_eager=True,
|
|
104
|
+
),
|
|
105
|
+
] = False,
|
|
93
106
|
output: Annotated[
|
|
94
107
|
Path | None,
|
|
95
108
|
typer.Option(
|
|
@@ -99,12 +112,13 @@ def generate(
|
|
|
99
112
|
),
|
|
100
113
|
] = None,
|
|
101
114
|
) -> None:
|
|
102
|
-
"""Generate
|
|
115
|
+
"""Generate shell scripts from workflow YAML."""
|
|
103
116
|
|
|
104
117
|
_ = version
|
|
105
118
|
_ = schema
|
|
106
119
|
_ = examples
|
|
107
120
|
_ = example
|
|
121
|
+
_ = skill
|
|
108
122
|
|
|
109
123
|
try:
|
|
110
124
|
workflows = parse_workflows(workflow_yaml)
|
|
@@ -173,6 +187,14 @@ def print_example(value: str | None) -> None:
|
|
|
173
187
|
raise typer.Exit
|
|
174
188
|
|
|
175
189
|
|
|
190
|
+
def print_skill(value: bool) -> None:
|
|
191
|
+
if not value:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
print(skill_text(), end="")
|
|
195
|
+
raise typer.Exit
|
|
196
|
+
|
|
197
|
+
|
|
176
198
|
def write_harnesses(
|
|
177
199
|
workflows: list[Workflow], *, dry_run: bool, force: bool, output_path: Path | None = None
|
|
178
200
|
) -> None:
|
|
@@ -94,6 +94,10 @@ class AgentStep(BaseStep):
|
|
|
94
94
|
agent: str | None = None
|
|
95
95
|
model: str | None = None
|
|
96
96
|
command: str | None = None
|
|
97
|
+
capture: str | None = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="Name of a shell variable that receives the full agent output for later steps.",
|
|
100
|
+
)
|
|
97
101
|
dangerouslySkipPermissions: bool = Field(
|
|
98
102
|
default=False,
|
|
99
103
|
validation_alias=AliasChoices(
|
|
@@ -112,6 +116,16 @@ class AgentStep(BaseStep):
|
|
|
112
116
|
"are replaced. All other shell syntax in the prompt is safe to use."
|
|
113
117
|
),
|
|
114
118
|
)
|
|
119
|
+
expandFields: bool = Field(
|
|
120
|
+
default=False,
|
|
121
|
+
description=(
|
|
122
|
+
"When true, substitutes ${VAR} and $VAR tokens from vars steps into the model, "
|
|
123
|
+
"agent, and command fields at runtime, using the same safe plain-text replacement "
|
|
124
|
+
"as expandPrompt. Also implies prompt expansion (equivalent to expandPrompt: true) "
|
|
125
|
+
"so that expandFields: true alone expands model, agent, command, and prompt. "
|
|
126
|
+
"Setting both expandPrompt and expandFields is allowed; both simply apply."
|
|
127
|
+
),
|
|
128
|
+
)
|
|
115
129
|
|
|
116
130
|
@model_validator(mode="before")
|
|
117
131
|
@classmethod
|
|
@@ -138,8 +152,15 @@ class AgentStep(BaseStep):
|
|
|
138
152
|
@field_validator("agent")
|
|
139
153
|
@classmethod
|
|
140
154
|
def validate_agent(cls, value: str | None) -> str | None:
|
|
141
|
-
if value is not None and not re.fullmatch(r"[A-Za-z0-9_-]+", value):
|
|
142
|
-
raise ValueError("must match ^[A-Za-z0-9_-]+$")
|
|
155
|
+
if value is not None and not re.fullmatch(r"[A-Za-z0-9_${}-]+", value):
|
|
156
|
+
raise ValueError("must match ^[A-Za-z0-9_${}-]+$")
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
@field_validator("capture")
|
|
160
|
+
@classmethod
|
|
161
|
+
def validate_capture(cls, value: str | None) -> str | None:
|
|
162
|
+
if value is not None and not re.fullmatch(r"[A-Z_][A-Z0-9_]*", value):
|
|
163
|
+
raise ValueError("must match ^[A-Z_][A-Z0-9_]*$")
|
|
143
164
|
return value
|
|
144
165
|
|
|
145
166
|
|
|
@@ -196,18 +217,42 @@ class ForStep(BaseStep):
|
|
|
196
217
|
return value
|
|
197
218
|
|
|
198
219
|
|
|
220
|
+
class WhileStep(BaseStep):
|
|
221
|
+
type: Literal["while"]
|
|
222
|
+
condition: str
|
|
223
|
+
steps: list[Step] = Field(min_length=1)
|
|
224
|
+
|
|
225
|
+
@field_validator("condition")
|
|
226
|
+
@classmethod
|
|
227
|
+
def validate_condition(cls, value: str) -> str:
|
|
228
|
+
if value.strip() == "":
|
|
229
|
+
raise ValueError("must not be empty")
|
|
230
|
+
if has_unsafe_control_characters(value):
|
|
231
|
+
raise ValueError("must not contain unsafe control characters")
|
|
232
|
+
return value
|
|
233
|
+
|
|
234
|
+
@field_validator("steps")
|
|
235
|
+
@classmethod
|
|
236
|
+
def reject_nested_while_steps(cls, value: list[Step]) -> list[Step]:
|
|
237
|
+
for step in value:
|
|
238
|
+
if isinstance(step, WhileStep):
|
|
239
|
+
raise ValueError("nested while steps are not supported")
|
|
240
|
+
return value
|
|
241
|
+
|
|
242
|
+
|
|
199
243
|
class ParallelStep(BaseStep):
|
|
200
244
|
type: Literal["parallel"]
|
|
201
245
|
steps: list[Step] = Field(min_length=1)
|
|
202
246
|
|
|
203
247
|
|
|
204
248
|
Step = Annotated[
|
|
205
|
-
VarsStep | BashStep | AgentStep | ForStep | ParallelStep,
|
|
249
|
+
VarsStep | BashStep | AgentStep | ForStep | WhileStep | ParallelStep,
|
|
206
250
|
Field(discriminator="type"),
|
|
207
251
|
]
|
|
208
252
|
|
|
209
253
|
|
|
210
254
|
ForStep.model_rebuild()
|
|
255
|
+
WhileStep.model_rebuild()
|
|
211
256
|
ParallelStep.model_rebuild()
|
|
212
257
|
|
|
213
258
|
|
|
@@ -10,6 +10,7 @@ from flowsh_cli.models import (
|
|
|
10
10
|
ParallelStep,
|
|
11
11
|
Step,
|
|
12
12
|
VarsStep,
|
|
13
|
+
WhileStep,
|
|
13
14
|
Workflow,
|
|
14
15
|
WorkflowParam,
|
|
15
16
|
)
|
|
@@ -121,19 +122,26 @@ def render_harness(workflow: Workflow) -> str:
|
|
|
121
122
|
"catch() {",
|
|
122
123
|
' local step_name="$1"',
|
|
123
124
|
' local exit_code="$2"',
|
|
124
|
-
'
|
|
125
|
+
' local step_type="${3:-}"',
|
|
126
|
+
' local hint=""',
|
|
127
|
+
' if [[ "$step_type" == "vars" && "$exit_code" == "127" ]]; then',
|
|
128
|
+
' hint=" — vars values are shell commands, not literal strings;'
|
|
129
|
+
' check workflow definition"',
|
|
130
|
+
" fi",
|
|
131
|
+
' log ERROR "Step failed: ${step_name} [${step_type}] (exit=${exit_code})${hint}"',
|
|
125
132
|
"}",
|
|
126
133
|
"",
|
|
127
134
|
section("run_step() - dry-run and failure handling; streams output via tee"),
|
|
128
135
|
"run_step() {",
|
|
129
136
|
' local step_name="$1"',
|
|
137
|
+
' local step_type="${2:-}"',
|
|
130
138
|
"",
|
|
131
139
|
' if [[ "$DRY_RUN" == true ]]; then',
|
|
132
|
-
' log INFO "[DRY-RUN] would run: ${step_name}"',
|
|
140
|
+
' log INFO "[DRY-RUN] would run: ${step_name} [${step_type}]"',
|
|
133
141
|
" return 0",
|
|
134
142
|
" fi",
|
|
135
143
|
"",
|
|
136
|
-
' log INFO "Running step: ${step_name}"',
|
|
144
|
+
' log INFO "Running step: ${step_name} [${step_type}]"',
|
|
137
145
|
"",
|
|
138
146
|
" set +e",
|
|
139
147
|
' if ( : >> "$LOG_FILE" ) 2>/dev/null; then',
|
|
@@ -146,7 +154,7 @@ def render_harness(workflow: Workflow) -> str:
|
|
|
146
154
|
" set -e",
|
|
147
155
|
"",
|
|
148
156
|
" if [[ $status -ne 0 ]]; then",
|
|
149
|
-
' catch "$step_name" "$status"',
|
|
157
|
+
' catch "$step_name" "$status" "$step_type"',
|
|
150
158
|
" fi",
|
|
151
159
|
' return "$status"',
|
|
152
160
|
"}",
|
|
@@ -154,13 +162,14 @@ def render_harness(workflow: Workflow) -> str:
|
|
|
154
162
|
section("run_stateful_step() - dry-run and failure handling without subshells"),
|
|
155
163
|
"run_stateful_step() {",
|
|
156
164
|
' local step_name="$1"',
|
|
165
|
+
' local step_type="${2:-}"',
|
|
157
166
|
"",
|
|
158
167
|
' if [[ "$DRY_RUN" == true ]]; then',
|
|
159
|
-
' log INFO "[DRY-RUN] would run: ${step_name}"',
|
|
168
|
+
' log INFO "[DRY-RUN] would run: ${step_name} [${step_type}]"',
|
|
160
169
|
" return 0",
|
|
161
170
|
" fi",
|
|
162
171
|
"",
|
|
163
|
-
' log INFO "Running step: ${step_name}"',
|
|
172
|
+
' log INFO "Running step: ${step_name} [${step_type}]"',
|
|
164
173
|
"",
|
|
165
174
|
" set +e",
|
|
166
175
|
' "$step_name"',
|
|
@@ -168,18 +177,19 @@ def render_harness(workflow: Workflow) -> str:
|
|
|
168
177
|
" set -e",
|
|
169
178
|
"",
|
|
170
179
|
" if [[ $status -ne 0 ]]; then",
|
|
171
|
-
' catch "$step_name" "$status"',
|
|
180
|
+
' catch "$step_name" "$status" "$step_type"',
|
|
172
181
|
" fi",
|
|
173
182
|
' return "$status"',
|
|
174
183
|
"}",
|
|
175
184
|
"",
|
|
176
|
-
section("run_agent() - prompt handling and
|
|
185
|
+
section("run_agent() - prompt handling and CLI invocation"),
|
|
177
186
|
"run_agent() {",
|
|
178
187
|
' local prompt="$1"',
|
|
179
188
|
' local agent="${2:-}"',
|
|
180
189
|
' local model="${3:-}"',
|
|
181
190
|
' local command="${4:-}"',
|
|
182
|
-
' local
|
|
191
|
+
' local capture="${5:-}"',
|
|
192
|
+
' local dangerously_skip_permissions="${6:-false}"',
|
|
183
193
|
"",
|
|
184
194
|
" local cmd=(opencode run --format json)",
|
|
185
195
|
' if [[ -n "$agent" ]]; then',
|
|
@@ -205,7 +215,17 @@ def render_harness(workflow: Workflow) -> str:
|
|
|
205
215
|
" return 127",
|
|
206
216
|
" fi",
|
|
207
217
|
"",
|
|
208
|
-
'
|
|
218
|
+
' if [[ -n "$capture" ]]; then',
|
|
219
|
+
" local output",
|
|
220
|
+
" local status=0",
|
|
221
|
+
' output="$("${cmd[@]}" -- "$prompt")"',
|
|
222
|
+
" status=$?",
|
|
223
|
+
" printf '%s\\n' \"$output\"",
|
|
224
|
+
' printf -v "$capture" \'%s\' "$output"',
|
|
225
|
+
' return "$status"',
|
|
226
|
+
" else",
|
|
227
|
+
' "${cmd[@]}" -- "$prompt"',
|
|
228
|
+
" fi",
|
|
209
229
|
"}",
|
|
210
230
|
"",
|
|
211
231
|
section(f"Starting workflow: {workflow.name}"),
|
|
@@ -248,9 +268,36 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
|
|
|
248
268
|
body_lines = [
|
|
249
269
|
f" while IFS= read -r {step.item}; do",
|
|
250
270
|
f" export {step.item}",
|
|
251
|
-
*[
|
|
271
|
+
*[
|
|
272
|
+
f" run_step {fn} {inner_step.type}"
|
|
273
|
+
for fn, inner_step in zip(inner_fns, step.steps, strict=True)
|
|
274
|
+
],
|
|
252
275
|
f' done <<< "${{{step.in_}}}"',
|
|
253
276
|
]
|
|
277
|
+
elif isinstance(step, WhileStep):
|
|
278
|
+
child_fns: list[str] = []
|
|
279
|
+
for i, child in enumerate(step.steps, start=1):
|
|
280
|
+
child_title = child.name or default_step_title(i, child)
|
|
281
|
+
child_fn = step_function_name(i, child.name, used_function_names)
|
|
282
|
+
child_fns.append(child_fn)
|
|
283
|
+
prefix_lines.append(section(f"While child {i} ({child.type}): {child_title}"))
|
|
284
|
+
prefix_lines.append(f"{child_fn}() {{")
|
|
285
|
+
prefix_lines.extend(_render_step_body(child, child_title))
|
|
286
|
+
prefix_lines.append("}")
|
|
287
|
+
prefix_lines.append("")
|
|
288
|
+
|
|
289
|
+
body_lines = []
|
|
290
|
+
if step.when is not None:
|
|
291
|
+
body_lines.append(f" if ! ({step.when}); then")
|
|
292
|
+
body_lines.append(f" log INFO {bash_quote(f'Step skipped (when): {title}')}")
|
|
293
|
+
body_lines.append(" return 0")
|
|
294
|
+
body_lines.append(" fi")
|
|
295
|
+
body_lines.append("")
|
|
296
|
+
|
|
297
|
+
body_lines.append(f" while ({step.condition}); do")
|
|
298
|
+
for child_fn in child_fns:
|
|
299
|
+
body_lines.append(f" run_step {child_fn} || return $?")
|
|
300
|
+
body_lines.append(" done")
|
|
254
301
|
elif isinstance(step, ParallelStep):
|
|
255
302
|
child_fns: list[str] = []
|
|
256
303
|
for i, child in enumerate(step.steps, start=1):
|
|
@@ -273,7 +320,7 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
|
|
|
273
320
|
else:
|
|
274
321
|
body_lines = _render_step_body(step, title)
|
|
275
322
|
|
|
276
|
-
runner = "run_stateful_step" if isinstance(step, (VarsStep, ForStep)) else "run_step"
|
|
323
|
+
runner = "run_stateful_step" if isinstance(step, (VarsStep, ForStep, WhileStep)) else "run_step"
|
|
277
324
|
outer_lines = [
|
|
278
325
|
section(f"Step {index} ({step.type}): {title}"),
|
|
279
326
|
f"{function_name}() {{",
|
|
@@ -283,13 +330,32 @@ def render_step(index: int, step: Step, used_function_names: set[str] | None = N
|
|
|
283
330
|
outer_lines.extend(
|
|
284
331
|
[
|
|
285
332
|
"}",
|
|
286
|
-
f"{runner} {function_name}",
|
|
333
|
+
f"{runner} {function_name} {step.type}",
|
|
287
334
|
"",
|
|
288
335
|
]
|
|
289
336
|
)
|
|
290
337
|
return prefix_lines + outer_lines
|
|
291
338
|
|
|
292
339
|
|
|
340
|
+
def _find_var_tokens(text: str) -> list[str]:
|
|
341
|
+
"""Return unique ${VAR}/$VAR tokens (matching [A-Z_][A-Z0-9_]*) in order of first appearance."""
|
|
342
|
+
braced = re.findall(r"\$\{([A-Z_][A-Z0-9_]*)\}", text)
|
|
343
|
+
bare = re.findall(r"\$([A-Z_][A-Z0-9_]*)(?!\w)", text)
|
|
344
|
+
seen: dict[str, None] = {}
|
|
345
|
+
for var in braced + bare:
|
|
346
|
+
seen[var] = None
|
|
347
|
+
return list(seen)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _expand_var_lines(local_name: str, tokens: list[str]) -> list[str]:
|
|
351
|
+
"""Return bash lines that expand ${VAR}/$VAR literal tokens in `local_name` at runtime."""
|
|
352
|
+
lines: list[str] = []
|
|
353
|
+
for var in tokens:
|
|
354
|
+
lines.append(f' _p=\'${{{var}}}\'; {local_name}="${{{local_name}//"$_p"/"${var}"}}"')
|
|
355
|
+
lines.append(f' _p=\'${var}\'; {local_name}="${{{local_name}//"$_p"/"${var}"}}"')
|
|
356
|
+
return lines
|
|
357
|
+
|
|
358
|
+
|
|
293
359
|
def _render_step_body(step: Step, title: str) -> list[str]:
|
|
294
360
|
"""Return indented body lines for a step function (no wrapper, no run_step call)."""
|
|
295
361
|
lines: list[str] = []
|
|
@@ -338,23 +404,24 @@ def _render_step_body(step: Step, title: str) -> list[str]:
|
|
|
338
404
|
" )",
|
|
339
405
|
]
|
|
340
406
|
)
|
|
341
|
-
if step.expandPrompt:
|
|
407
|
+
if step.expandPrompt or step.expandFields:
|
|
342
408
|
_prompt_no_code = re.sub(r"```.*?```", "", step.prompt, flags=re.DOTALL)
|
|
343
|
-
|
|
344
|
-
bare = re.findall(r"\$([A-Z_][A-Z0-9_]*)(?!\w)", _prompt_no_code)
|
|
345
|
-
seen: dict[str, None] = {}
|
|
346
|
-
for var in braced + bare:
|
|
347
|
-
seen[var] = None
|
|
348
|
-
for var in seen:
|
|
349
|
-
lines.append(f' _p=\'${{{var}}}\'; prompt="${{prompt//"$_p"/"${var}"}}"')
|
|
350
|
-
lines.append(f' _p=\'${var}\'; prompt="${{prompt//"$_p"/"${var}"}}"')
|
|
409
|
+
lines.extend(_expand_var_lines("prompt", _find_var_tokens(_prompt_no_code)))
|
|
351
410
|
lines.append(f" local agent={bash_quote(step.agent or '')}")
|
|
411
|
+
if step.expandFields and step.agent:
|
|
412
|
+
lines.extend(_expand_var_lines("agent", _find_var_tokens(step.agent)))
|
|
352
413
|
lines.append(f" local model={bash_quote(step.model or '')}")
|
|
414
|
+
if step.expandFields and step.model:
|
|
415
|
+
lines.extend(_expand_var_lines("model", _find_var_tokens(step.model)))
|
|
353
416
|
lines.append(f" local command={bash_quote(step.command or '')}")
|
|
417
|
+
if step.expandFields and step.command:
|
|
418
|
+
lines.extend(_expand_var_lines("command", _find_var_tokens(step.command)))
|
|
419
|
+
lines.append(f" local capture={bash_quote(step.capture or '')}")
|
|
354
420
|
dangerous_skip_permissions = "true" if step.dangerouslySkipPermissions else "false"
|
|
355
421
|
lines.append(f" local dangerously_skip_permissions={dangerous_skip_permissions}")
|
|
356
422
|
lines.append(
|
|
357
|
-
' run_agent "$prompt" "$agent" "$model"
|
|
423
|
+
' run_agent "$prompt" "$agent" "$model" '
|
|
424
|
+
'"$command" "$capture" "$dangerously_skip_permissions"'
|
|
358
425
|
)
|
|
359
426
|
elif isinstance(step, ForStep):
|
|
360
427
|
raise AssertionError("nested for steps are not supported")
|
|
@@ -393,6 +460,8 @@ def default_step_title(index: int, step: Step) -> str:
|
|
|
393
460
|
return truncate_one_line(step.prompt)
|
|
394
461
|
if isinstance(step, ForStep):
|
|
395
462
|
return f"for {step.item} in {step.in_}"
|
|
463
|
+
if isinstance(step, WhileStep):
|
|
464
|
+
return f"while {truncate_one_line(step.condition)}"
|
|
396
465
|
if isinstance(step, ParallelStep):
|
|
397
466
|
return f"parallel ({len(step.steps)} steps)"
|
|
398
467
|
return f"step {index}"
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
SKILL_MARKDOWN = """\
|
|
4
|
+
---
|
|
5
|
+
name: flowsh-cli
|
|
6
|
+
description: Generate executable shell harness scripts from workflow definitions. Use when creating, modifying, validating, reviewing, troubleshooting, or generating workflow-to-shell automation, especially for orchestrating bash and agent calls.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Workflow-to-Shell Generation with flowsh-cli
|
|
10
|
+
|
|
11
|
+
Use this skill whenever working with workflow definitions that produce executable Bash or shell scripts.
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
|
|
15
|
+
flowsh-cli converts workflow definitions into executable shell harness scripts.
|
|
16
|
+
|
|
17
|
+
Workflow definitions are the source of truth.
|
|
18
|
+
|
|
19
|
+
Generated shell scripts are build artifacts derived from those workflow definitions.
|
|
20
|
+
|
|
21
|
+
The CLI parses, validates, and generates in a single operation. Validation is therefore an essential part of workflow generation.
|
|
22
|
+
|
|
23
|
+
A workflow is considered correct when:
|
|
24
|
+
|
|
25
|
+
* It conforms to the current schema.
|
|
26
|
+
* Validation succeeds.
|
|
27
|
+
* Generated shell output matches the intended behavior.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Mandatory Operating Procedure
|
|
32
|
+
|
|
33
|
+
## Rule 1: Always Consult the Current Schema First
|
|
34
|
+
|
|
35
|
+
Before creating, modifying, reviewing, troubleshooting, or generating workflows:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uvx flowsh-cli --schema
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The schema is the authoritative specification.
|
|
42
|
+
|
|
43
|
+
Do not rely on:
|
|
44
|
+
|
|
45
|
+
* Memory
|
|
46
|
+
* Existing workflow files
|
|
47
|
+
* Previous examples
|
|
48
|
+
* Assumptions about supported fields
|
|
49
|
+
|
|
50
|
+
If a workflow conflicts with the schema, the schema is authoritative.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Rule 2: Validate Through Generation
|
|
55
|
+
|
|
56
|
+
flowsh-cli validates workflows as part of generation.
|
|
57
|
+
|
|
58
|
+
Use:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uvx flowsh-cli [workflow-name].yml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Validation failures must be resolved before reasoning about generated shell output.
|
|
65
|
+
|
|
66
|
+
Do not assume a workflow is valid simply because it appears similar to previous examples.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Rule 3: Always Verify After Changes
|
|
71
|
+
|
|
72
|
+
After modifying a workflow:
|
|
73
|
+
|
|
74
|
+
1. Retrieve the current schema.
|
|
75
|
+
2. Run a dry-run generation.
|
|
76
|
+
3. Review any validation errors or warnings.
|
|
77
|
+
4. Review the generated output.
|
|
78
|
+
5. Confirm generated shell behavior matches intent.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uvx flowsh-cli [workflow-name].yml --dry-run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A workflow modification is not complete until a successful dry-run has been reviewed.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Rule 4: Make Minimal Changes
|
|
91
|
+
|
|
92
|
+
When modifying workflows:
|
|
93
|
+
|
|
94
|
+
* Prefer the smallest possible change.
|
|
95
|
+
* Avoid unrelated refactoring.
|
|
96
|
+
* Preserve existing behavior unless explicitly requested.
|
|
97
|
+
* Keep workflow structure stable.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
# Workflow Review Priorities
|
|
102
|
+
|
|
103
|
+
When reviewing workflows:
|
|
104
|
+
|
|
105
|
+
1. Schema compliance.
|
|
106
|
+
2. Successful validation.
|
|
107
|
+
3. Workflow correctness.
|
|
108
|
+
4. Generated shell correctness.
|
|
109
|
+
5. Shell safety.
|
|
110
|
+
6. Maintainability.
|
|
111
|
+
|
|
112
|
+
Generated shell behavior should only be reviewed after validation succeeds.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# Common Commands
|
|
117
|
+
|
|
118
|
+
Display help:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uvx flowsh-cli --help
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Display the current schema:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uvx flowsh-cli --schema
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Generate all workflows:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uvx flowsh-cli [workflow-name].yml
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Generate a specific workflow:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uvx flowsh-cli [workflow-name].yml --workflow wf_example
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Preview generation without writing files:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uvx flowsh-cli [workflow-name].yml --dry-run
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Overwrite existing generated files:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uvx flowsh-cli [workflow-name].yml --force
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# Folder Structure & File Management
|
|
157
|
+
|
|
158
|
+
By convention, organize workflow files and generated harness scripts under a `.harness/` folder:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
project/
|
|
162
|
+
└── .harness/
|
|
163
|
+
├── [workflow-name].yml # Workflow file
|
|
164
|
+
└── [harness-name].sh # Generated harness script
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Convention:**
|
|
168
|
+
- Store workflow definitions under `[workflow-name].yml` files
|
|
169
|
+
- Generated harness scripts are produced in `.harness/` directory with names matching `[harness-name].sh`
|
|
170
|
+
- Generated artifacts are build products—regenerate them as needed from source workflows
|
|
171
|
+
- Use `.gitignore` to exclude generated `.sh` files if desired, or commit them for reproducibility
|
|
172
|
+
|
|
173
|
+
**Generation command:**
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
uvx flowsh-cli .harness/[workflow-name].yml
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This generates harnesses in `.harness/` alongside the source workflow definitions.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
# Example Workflow
|
|
184
|
+
|
|
185
|
+
Always verify against the current schema before creating production workflows.
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
workflows:
|
|
189
|
+
- id: wf_example
|
|
190
|
+
name: Example Workflow
|
|
191
|
+
|
|
192
|
+
steps:
|
|
193
|
+
- type: vars
|
|
194
|
+
values:
|
|
195
|
+
TODAY: date -u +%F
|
|
196
|
+
|
|
197
|
+
- type: bash
|
|
198
|
+
run: |
|
|
199
|
+
echo "$TODAY"
|
|
200
|
+
|
|
201
|
+
- type: agent
|
|
202
|
+
prompt: |
|
|
203
|
+
Summarize the output.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
# Troubleshooting Procedure
|
|
209
|
+
|
|
210
|
+
If generation fails:
|
|
211
|
+
|
|
212
|
+
1. Retrieve the latest schema.
|
|
213
|
+
2. Review validation errors.
|
|
214
|
+
3. Correct the workflow definition.
|
|
215
|
+
4. Run a dry-run.
|
|
216
|
+
5. Review generated output.
|
|
217
|
+
6. Apply the smallest possible fix.
|
|
218
|
+
7. Repeat until validation and generation succeed.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
# Expected Behavior
|
|
223
|
+
|
|
224
|
+
Always:
|
|
225
|
+
|
|
226
|
+
* Consult the latest schema first.
|
|
227
|
+
* Treat the schema as authoritative.
|
|
228
|
+
* Use flowsh-cli validation before evaluating generated output.
|
|
229
|
+
* Run a dry-run after workflow modifications.
|
|
230
|
+
* Review generated shell behavior before declaring success.
|
|
231
|
+
* Prefer minimal changes.
|
|
232
|
+
|
|
233
|
+
Never:
|
|
234
|
+
|
|
235
|
+
* Assume schema details from memory.
|
|
236
|
+
* Skip schema inspection.
|
|
237
|
+
* Ignore validation errors.
|
|
238
|
+
* Skip dry-run verification after changes.
|
|
239
|
+
* Declare success without reviewing generated output.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def skill_text() -> str:
|
|
244
|
+
return SKILL_MARKDOWN
|
|
File without changes
|
|
File without changes
|