Files
Paul O'Reilly a95197b162 add manual-workflow skill
Run a workflow template from a Claude Code session via the local control
plane. Handles topological wave execution, artifact passing, sentinel
resolution, and human review gates locally — no CP-side workflow
expansion required.

The pre-gathered "Available workflows" context uses python3 (not a
ls|sed pipe) so the Bash permission checker accepts it without needing
sed in allowed-tools — piped commands fail the check even when both
sides match an allowed pattern individually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:33:36 +12:00

175 lines
7.0 KiB
Markdown

---
name: manual-workflow
description: >
Run a workflow template from a Claude Code session using the control plane for task execution.
Handles artifact passing, sentinel resolution, and human review gates locally.
Usage: /manual-workflow <template-name> [param=value ...]
user_invocable: true
allowed-tools: Read, Glob, Grep, Bash(curl *), Bash(python3 *), Bash(sleep *), Bash(date *), Bash(cat *), Bash(ls *), Bash(kill *), Bash(kubectl port-forward *), AskUserQuestion, Write, Edit
---
# /manual-workflow Skill
<command-name>manual-workflow</command-name>
Run a workflow template through the control plane, managing artifact passing and human review gates from this Claude session.
## Arguments
`$ARGUMENTS` — Template name followed by optional `key=value` params.
Examples:
- `/manual-workflow spec-planning` — interactive param prompting
- `/manual-workflow spec-planning project_id=agent-runtimes task_description="Design artifact passing"`
## Pre-gathered context
### Available workflows
!`python3 -c "import glob,os; [print(os.path.basename(p).removesuffix('.yaml')) for p in sorted(glob.glob(os.path.expanduser('~/dev/claude/projects/agent-runtimes/workflows/*.yaml')))]"`
### Control plane access
!`curl -s --connect-timeout 2 http://localhost:8100/health 2>/dev/null || echo "CP_NOT_AVAILABLE"`
## Instructions
### Step 0: Parse arguments and validate
Parse `$ARGUMENTS` to extract the template name (first word) and any `key=value` params.
If no template name provided, list available workflows and ask the user to pick one.
### Step 1: Load and validate template
Read the workflow YAML from `~/dev/claude/projects/agent-runtimes/workflows/<name>.yaml`.
Parse it to extract: `params`, `nodes`, node `depends_on`, and `model_override` fields.
Display a summary:
```
Workflow: <name> v<version>
Description: <description>
Nodes: <count> (<node_ids>)
DAG: <visual showing phases>
```
### Step 2: Resolve parameters
For each required param not provided via arguments, ask the user with `AskUserQuestion`.
For each optional param, show the default and ask if they want to override.
For `model_a` / `model_b`: if not provided and the user doesn't override, set sensible defaults:
- `model_a` = the harness context that runs via MiniMax (use `minimax/v1` harness)
- `model_b` = the default Claude model (use `anthropic-cloud/v1` harness)
Or let the user specify model names and you'll map them to harnesses.
### Step 3: Ensure CP access
Check if `http://localhost:8100/health` responds. If not:
```bash
kubectl port-forward -n agent-runtimes svc/controlplane 8100:8100 &
```
Wait for health check to pass.
### Step 4: Execute in topological waves
Process nodes in dependency order. Nodes with no deps (or all deps satisfied) form a "wave" and run in parallel.
For each wave:
1. **Resolve sentinels** in each node's prompt: replace `<<ARTIFACT:node_id:key>>` with the collected output from that node. Wrap injected content in `[BEGIN ARTIFACT: node_id:key]` / `[END ARTIFACT: node_id:key]` markers.
2. **Render Jinja2** — replace `{{ param }}` references with resolved param values. For Jinja2 conditionals (`{% if %}`, `{% for %}`), render them with the params dict. Use Python:
```python
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
rendered = env.from_string(prompt).render(**params)
```
3. **Determine harness** for each node:
- If `model_override` resolves to a value containing "model_a" and model_a is minimax → use `minimax/v1`
- If `model_override` resolves to a value containing "model_b" or is empty → use `anthropic-cloud/v1`
- The harness controls which provider's auth the agent gets
4. **Submit tasks** to the CP via:
```bash
curl -s -X POST http://localhost:8100/tasks \
-H "Content-Type: application/json" \
-d '{"name": "<node_name>", "project_id": "<project_id>", "prompt": "<rendered_prompt>", "harness": "<harness>", "runtime": {"cli": "claude", "model": "<model_if_applicable>", "timeout": 3600}}'
```
5. **Monitor tasks** — poll every 30s:
```bash
curl -s http://localhost:8100/tasks/<task_id>
```
Display progress updates to the user: which nodes are running, which completed, duration.
6. **Collect outputs** — when a task succeeds, extract the assistant's output from the task logs:
```python
# Parse NDJSON logs, find assistant message content
for line in logs.split('\n'):
entry = json.loads(line)
if entry.get('type') == 'assistant':
content = entry['message']['content']
for block in content:
if block['type'] == 'text':
result += block['text']
```
Store the output as `artifacts[node_id]['output']` for sentinel resolution in downstream nodes.
7. **Handle failures** — if a task fails:
- Show the error and logs to the user
- Ask: "Retry this node, skip it, or abort the workflow?"
- On retry: resubmit with the same prompt
- On skip: mark the artifact as `[NODE FAILED — no output available]`
- On abort: stop the workflow
### Step 5: Human review gate
When the `escalate` node completes (or any node whose name contains "human" or "escalat"):
1. **Display the full escalation output** to the user
2. **Ask for decisions** on each "Decision needed: YES" item using `AskUserQuestion`
3. **Append the human decisions** to the escalation artifact:
```
## Human Decisions (from manual review)
### Decision 1: [topic]
**Choice:** [user's answer]
**Rationale:** [user's explanation if provided]
### Decision 2: [topic]
...
```
4. The updated artifact (original + human decisions) is then used for sentinel resolution in the `synthesize` node.
### Step 6: Final output
When the `synthesize` node completes:
1. Display a summary: total nodes, succeeded/failed, wall-clock time per phase
2. Write the final spec output to a file in the project:
- Ask the user where to save it (suggest `spec/<name>.md`)
- Write the file
3. Show the file path and suggest next steps: "Review the spec, then commit when ready."
### Error handling
- If the CP goes offline mid-workflow, pause and tell the user to fix it
- If a node times out (>3600s), treat as failure
- If ALL nodes in a wave fail, ask the user before continuing
- Never leave orphan tasks running — cancel them on abort
### Progress display
Keep the user informed with a compact status line after each check:
```
[Phase 2] spec_review_a: running (2m) | spec_review_b: running (1m45s) | security_review_a: succeeded (3m) | ...
```
### Important notes
- This skill runs the workflow LOCALLY from your Claude session — it does NOT use the CP's workflow expansion (Phase 2 artifact passing isn't implemented yet)
- Artifacts are passed by embedding the full text in downstream prompts — this works but means large artifacts consume context
- The harness mapping (model name → harness context) is specific to the agent-runtimes project's current provider setup
- Monitor the agent-monitor in another terminal for richer progress: `scripts/agent-monitor --filter "project=<project_id>" --filter "age<30m"`