From 9f37c2afd330aef4d39116f54627ced71ea41116 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Wed, 22 Apr 2026 16:47:06 +0300 Subject: [PATCH 1/2] docs(script-nodes): add dedicated guide and teach the archon skill how to write them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script nodes (script:) have been a first-class DAG node type since v0.3.3 but were documented only as one-liners in CLAUDE.md and a CI smoke test. Claude Code reading the archon skill would see "Four Node Types: command, prompt, bash, loop" and reach for bash+node/python one-liners instead of a proper script node — losing bun's --no-env-file isolation, uv's --with dependency pins, and the .archon/scripts/ reuse story. - New packages/docs-web/src/content/docs/guides/script-nodes.md mirroring the structure of loop-nodes.md / approval-nodes.md: schema, inline vs named dispatch, runtime/deps semantics, scripts directory precedence (repo > home), extension-runtime mapping, env isolation, stdout/stderr contract, patterns, and the explicit list of ignored AI fields. - guides/authoring-workflows.md and guides/index.md updated so the new guide is discoverable from both the node-types table and the guides landing page. - reference/variables.md calls out the no-shell-quote difference between bash: and script: substitution — a subtle correctness trap when adapting a bash pattern into a script node. - Sidebar order bumped +1 on hooks/mcp-servers/skills/global-workflows/ remotion-workflow to slot script-nodes at order 5 next to the other node-type guides. - .claude/skills/archon/SKILL.md: replaces stale "Four Node Types" (which also silently omitted approval and cancel) with the accurate seven, with a script-node code block showing both inline and named patterns. - references/workflow-dag.md: full Script Node section covering dispatch, resolution, deps, stdout contract, and the list of AI-only fields that are ignored; validation-rules list updated. - references/dag-advanced.md and references/variables.md: retry-support line corrected; no-shell-quote note added. - examples/dag-workflow.yaml: added an extract-labels TypeScript script node and updated the header comment. --- .claude/skills/archon/SKILL.md | 22 +- .../skills/archon/examples/dag-workflow.yaml | 23 +- .../skills/archon/references/dag-advanced.md | 2 +- .claude/skills/archon/references/variables.md | 1 + .../skills/archon/references/workflow-dag.md | 56 ++- .../docs/guides/authoring-workflows.md | 1 + .../content/docs/guides/global-workflows.md | 2 +- .../docs-web/src/content/docs/guides/hooks.md | 2 +- .../docs-web/src/content/docs/guides/index.md | 1 + .../src/content/docs/guides/mcp-servers.md | 2 +- .../content/docs/guides/remotion-workflow.md | 2 +- .../src/content/docs/guides/script-nodes.md | 332 ++++++++++++++++++ .../src/content/docs/guides/skills.md | 2 +- .../src/content/docs/reference/variables.md | 8 +- 14 files changed, 439 insertions(+), 17 deletions(-) create mode 100644 packages/docs-web/src/content/docs/guides/script-nodes.md diff --git a/.claude/skills/archon/SKILL.md b/.claude/skills/archon/SKILL.md index f36e7391b8..7f126c9bac 100644 --- a/.claude/skills/archon/SKILL.md +++ b/.claude/skills/archon/SKILL.md @@ -152,9 +152,9 @@ nodes: depends_on: [first-node] ``` -### Four Node Types +### Node Types -Each node has exactly ONE of: `command`, `prompt`, `bash`, or `loop`. +Each node has exactly ONE of: `command`, `prompt`, `bash`, `script`, `loop`, `approval`, or `cancel`. **Command node** — runs a `.archon/commands/*.md` file: ```yaml @@ -177,6 +177,22 @@ Each node has exactly ONE of: `command`, `prompt`, `bash`, or `loop`. timeout: 15000 ``` +**Script node** — TypeScript/JavaScript (via `bun`) or Python (via `uv`), no AI, stdout captured as output: +```yaml +- id: transform + script: | + const raw = process.argv.slice(2).join(' ') || '{}'; + console.log(JSON.stringify({ parsed: JSON.parse(raw) })); + runtime: bun # 'bun' (.ts/.js) or 'uv' (.py) — REQUIRED + timeout: 30000 # Optional, ms, default 120000 + +# Or reference a named script from .archon/scripts/ or ~/.archon/scripts/ +- id: analyze + script: analyze-metrics # loads .archon/scripts/analyze-metrics.py + runtime: uv + deps: ["pandas>=2.0"] # Optional, uv only — 'uv run --with ' +``` + **Loop node** — iterates AI prompt until completion: ```yaml - id: implement @@ -230,7 +246,7 @@ For details: Read `references/dag-advanced.md` ### Example Files -- `examples/dag-workflow.yaml` — workflow with conditions, bash nodes, structured output +- `examples/dag-workflow.yaml` — workflow with conditions, bash + script + loop nodes, structured output - `examples/command-template.md` — Command file skeleton with all variables --- diff --git a/.claude/skills/archon/examples/dag-workflow.yaml b/.claude/skills/archon/examples/dag-workflow.yaml index 5e15f4c77c..924ac70d25 100644 --- a/.claude/skills/archon/examples/dag-workflow.yaml +++ b/.claude/skills/archon/examples/dag-workflow.yaml @@ -1,7 +1,8 @@ -# Example: Workflow with all four node types +# Example: Workflow demonstrating multiple node types # -# Demonstrates: bash nodes, structured output, when: conditions, -# trigger_rule, per-node model, context: fresh, loop nodes, and output substitution. +# Demonstrates: bash nodes, script nodes (TypeScript via bun), structured output, +# when: conditions, trigger_rule, per-node model, context: fresh, loop nodes, +# and output substitution. # # IMPORTANT: This is a reference example. Design your actual workflow # around the user's specific needs — the number of nodes, their types, @@ -42,6 +43,22 @@ nodes: fi timeout: 5000 + # ── SCRIPT NODE: TypeScript (bun runtime), no AI, stdout captured as output ── + # Deterministic parsing the shell would mangle — extracts labels cleanly as JSON. + - id: extract-labels + script: | + const raw = process.env.ISSUE_JSON ?? '{}'; + try { + const issue = JSON.parse(raw); + const labels = (issue.labels ?? []).map((l) => l.name); + console.log(JSON.stringify({ labels, count: labels.length })); + } catch { + console.log(JSON.stringify({ labels: [], count: 0 })); + } + runtime: bun + depends_on: [fetch-issue] + timeout: 10000 + # ── PROMPT NODE: Inline AI prompt with structured output ── - id: classify prompt: | diff --git a/.claude/skills/archon/references/dag-advanced.md b/.claude/skills/archon/references/dag-advanced.md index 4add35d8f7..63a83e9101 100644 --- a/.claude/skills/archon/references/dag-advanced.md +++ b/.claude/skills/archon/references/dag-advanced.md @@ -1,6 +1,6 @@ # Advanced Features: Hooks, MCP, Skills, Retry -These features are available on **command and prompt nodes** (hooks, MCP, skills, tool restrictions) and **command, prompt, and bash nodes** (retry, output_format). Loop nodes do not support these features (`retry` on loop nodes is a hard error; others are silently ignored). +These features are available on **command and prompt nodes** (hooks, MCP, skills, tool restrictions, `output_format`, `agents`, Claude SDK options) and **command, prompt, bash, and script nodes** (retry). Loop nodes do not support these features (`retry` on loop nodes is a hard error; others are silently ignored). Bash and script nodes silently ignore AI-specific fields (a loader warning lists the ignored fields). ## Provider Compatibility diff --git a/.claude/skills/archon/references/variables.md b/.claude/skills/archon/references/variables.md index 8f3d2dc57f..0275aa7d91 100644 --- a/.claude/skills/archon/references/variables.md +++ b/.claude/skills/archon/references/variables.md @@ -26,6 +26,7 @@ All variables are available in all workflows. The only exception is `$nodeId.out - **Command files** (`.archon/commands/*.md`) — all variables except `$nodeId.output` - **Inline `prompt:` fields** — in DAG prompt nodes and loop node prompts - **`bash:` scripts in DAG nodes** — `$nodeId.output` references are automatically shell-quoted (single-quoted with `'` escaped) +- **`script:` bodies in DAG nodes** — same substitution as bash, but `$nodeId.output` values are **NOT** shell-quoted. Parse with `JSON.parse` / `json.loads` rather than interpolating into shell syntax ## Substitution Order diff --git a/.claude/skills/archon/references/workflow-dag.md b/.claude/skills/archon/references/workflow-dag.md index eefb380646..1856b9445b 100644 --- a/.claude/skills/archon/references/workflow-dag.md +++ b/.claude/skills/archon/references/workflow-dag.md @@ -20,9 +20,9 @@ nodes: depends_on: [other-node] # Node IDs that must complete first ``` -## Four Node Types (Mutually Exclusive) +## Node Types (Mutually Exclusive) -Each node must have exactly ONE of these fields: +Each node must have exactly ONE of these fields: `command`, `prompt`, `bash`, `script`, `loop`, `approval`, or `cancel`. ### Command Node Runs a command file from `.archon/commands/`: @@ -54,6 +54,54 @@ Runs a shell script without AI: - **stderr** forwarded as warning, does not fail the node - No AI invoked — AI-specific fields are ignored - Use `timeout:` (milliseconds) for execution time limit +- `$nodeId.output` substitutions are **auto shell-quoted** (safe to embed) + +### Script Node +Runs TypeScript/JavaScript (via `bun`) or Python (via `uv`) without AI. Same stdout/stderr contract as bash nodes. + +**Inline script (TypeScript):** +```yaml +- id: parse + script: | + const raw = process.argv.slice(2).join(' ') || '{}'; + const data = JSON.parse(raw); + console.log(JSON.stringify({ items: data.items?.length ?? 0 })); + runtime: bun # REQUIRED: 'bun' or 'uv' + timeout: 30000 # ms, default: 120000 +``` + +**Inline script (Python) with uv dependencies:** +```yaml +- id: fetch + script: | + import httpx, json + r = httpx.get("https://api.github.com/repos/anthropics/anthropic-cookbook") + print(json.dumps({ "stars": r.json()["stargazers_count"] })) + runtime: uv + deps: ["httpx>=0.27"] # Optional — 'uv run --with '. Ignored for bun. +``` + +**Named script from `.archon/scripts/`:** +```yaml +- id: analyze + script: analyze-metrics # Resolves .archon/scripts/analyze-metrics.py + runtime: uv # Must match file extension (.ts/.js → bun, .py → uv) + deps: ["pandas>=2.0"] +``` + +- **Inline vs named**: a `script` value is treated as inline code if it contains a newline or any shell metacharacter (space, or any of: `;` `(` `)` `{` `}` `&` `|` `<` `>` `$` `` ` `` `"` `'`). Otherwise it's a named-script lookup (bare identifier). +- **Named script resolution**: `/.archon/scripts/` (wins) → `~/.archon/scripts/`. 1-level subfolder grouping allowed. Extension determines runtime (`.ts`/`.js` → `bun`, `.py` → `uv`) and MUST match the declared `runtime:` +- **Dispatch**: + - `bun` + inline → `bun --no-env-file -e ''` + - `bun` + named → `bun --no-env-file run ` + - `uv` + inline → `uv run [--with dep ...] python -c ''` + - `uv` + named → `uv run [--with dep ...] ` +- **`deps`** is uv-only. Bun auto-installs on import; `deps` with `runtime: bun` emits a validator warning +- **stdout** captured as `$nodeId.output` (trailing newline trimmed) +- **stderr** forwarded as warning, does NOT fail the node. Non-zero exit DOES fail it. +- **`bun --no-env-file`** prevents target repo `.env` from leaking into the subprocess +- `$nodeId.output` substitutions are **NOT shell-quoted** in script bodies — parse with `JSON.parse` / `json.loads`, don't interpolate into shell syntax +- AI-specific fields (`model`, `provider`, `hooks`, `mcp`, `skills`, `output_format`, `allowed_tools`, `denied_tools`, `agents`, `effort`, `thinking`, `maxBudgetUsd`, `systemPrompt`, `fallbackModel`, `betas`, `sandbox`) emit a loader warning and are ignored ### Loop Node Iterates an AI prompt until a completion signal or max iterations: @@ -302,7 +350,9 @@ Use `--json` for machine-readable output. Use `archon validate commands ` - All `depends_on` reference existing IDs - No cycles - `$nodeId.output` refs in `when:`, `prompt:`, `loop.prompt:` must point to known IDs -- Exactly one of `command`, `prompt`, `bash`, `loop` per node +- Exactly one of `command`, `prompt`, `bash`, `script`, `loop`, `approval`, `cancel` per node +- Script nodes require `runtime: bun` or `runtime: uv` +- Named scripts must exist in `.archon/scripts/` or `~/.archon/scripts/` with extension matching declared runtime - `retry` on loop node = hard error - `steps:` format rejected (deprecated — use `nodes:` only) diff --git a/packages/docs-web/src/content/docs/guides/authoring-workflows.md b/packages/docs-web/src/content/docs/guides/authoring-workflows.md index 0fbc282640..a4bc85fafd 100644 --- a/packages/docs-web/src/content/docs/guides/authoring-workflows.md +++ b/packages/docs-web/src/content/docs/guides/authoring-workflows.md @@ -174,6 +174,7 @@ nodes: | `command` | string | Command name to load from `.archon/commands/` | | `prompt` | string | Inline prompt string | | `bash` | string | Shell script (no AI). Stdout captured as `$nodeId.output`. Optional `timeout` (ms, default 120000) | +| `script` | string | TypeScript/JavaScript (via `bun`) or Python (via `uv`) — inline code or named reference to `.archon/scripts/`. Stdout captured as `$nodeId.output`. Requires `runtime: bun` or `runtime: uv`. Optional `deps` (uv only) and `timeout` (ms, default 120000). See [Script Nodes](/guides/script-nodes/) | | `loop` | object | Iterative AI prompt until completion signal. See [Loop Nodes](/guides/loop-nodes/) | | `approval` | object | Pauses workflow for human review. See [Approval Nodes](/guides/approval-nodes/) | | `cancel` | string | Terminates the workflow run with a reason string. Uses existing cancellation plumbing — in-flight parallel nodes are stopped | diff --git a/packages/docs-web/src/content/docs/guides/global-workflows.md b/packages/docs-web/src/content/docs/guides/global-workflows.md index 282881e312..a4651ba0ec 100644 --- a/packages/docs-web/src/content/docs/guides/global-workflows.md +++ b/packages/docs-web/src/content/docs/guides/global-workflows.md @@ -6,7 +6,7 @@ area: workflows audience: [user] status: current sidebar: - order: 8 + order: 9 --- Workflows placed in `~/.archon/workflows/`, commands in `~/.archon/commands/`, and scripts in `~/.archon/scripts/` are loaded globally -- they appear in every project and can be invoked from any repository. Workflows and commands carry the `source: 'global'` label in the Web UI node palette; scripts resolve under the same repo-wins-over-home precedence. diff --git a/packages/docs-web/src/content/docs/guides/hooks.md b/packages/docs-web/src/content/docs/guides/hooks.md index 3e6928ae21..201e60c3cb 100644 --- a/packages/docs-web/src/content/docs/guides/hooks.md +++ b/packages/docs-web/src/content/docs/guides/hooks.md @@ -6,7 +6,7 @@ area: workflows audience: [user] status: current sidebar: - order: 5 + order: 6 --- DAG workflow nodes support a `hooks` field that attaches Claude Agent SDK hooks diff --git a/packages/docs-web/src/content/docs/guides/index.md b/packages/docs-web/src/content/docs/guides/index.md index 0d53209fb6..f3cce0d69e 100644 --- a/packages/docs-web/src/content/docs/guides/index.md +++ b/packages/docs-web/src/content/docs/guides/index.md @@ -20,6 +20,7 @@ How-to guides for building and running AI coding workflows with Archon. - [Loop Nodes](/guides/loop-nodes/) — Iterative AI execution with completion conditions and deterministic exit checks - [Approval Nodes](/guides/approval-nodes/) — Human review gates with optional AI rework on rejection +- [Script Nodes](/guides/script-nodes/) — TypeScript/JavaScript (bun) or Python (uv) as a deterministic DAG node, without AI ## Node Features (Claude only) diff --git a/packages/docs-web/src/content/docs/guides/mcp-servers.md b/packages/docs-web/src/content/docs/guides/mcp-servers.md index 46474477e2..c777964d75 100644 --- a/packages/docs-web/src/content/docs/guides/mcp-servers.md +++ b/packages/docs-web/src/content/docs/guides/mcp-servers.md @@ -6,7 +6,7 @@ area: workflows audience: [user] status: current sidebar: - order: 6 + order: 7 --- DAG workflow nodes support a `mcp` field that attaches MCP (Model Context Protocol) diff --git a/packages/docs-web/src/content/docs/guides/remotion-workflow.md b/packages/docs-web/src/content/docs/guides/remotion-workflow.md index d68831be91..666b1ad916 100644 --- a/packages/docs-web/src/content/docs/guides/remotion-workflow.md +++ b/packages/docs-web/src/content/docs/guides/remotion-workflow.md @@ -6,7 +6,7 @@ area: workflows audience: [user] status: current sidebar: - order: 9 + order: 10 --- The `archon-remotion-generate` workflow uses AI to create Remotion video compositions. diff --git a/packages/docs-web/src/content/docs/guides/script-nodes.md b/packages/docs-web/src/content/docs/guides/script-nodes.md new file mode 100644 index 0000000000..f023d2adba --- /dev/null +++ b/packages/docs-web/src/content/docs/guides/script-nodes.md @@ -0,0 +1,332 @@ +--- +title: Script Nodes +description: Run TypeScript, JavaScript, or Python code as a DAG node without invoking an AI agent. +category: guides +area: workflows +audience: [user] +status: current +sidebar: + order: 5 +--- + +DAG workflow nodes support a `script` field that runs a TypeScript, JavaScript, +or Python snippet as part of the workflow. No AI agent is invoked — the script +runs via the `bun` or `uv` runtime, `stdout` is captured as the node's output, +and the result is available downstream as `$nodeId.output`. + +Use script nodes for deterministic work that needs a real programming language: +parsing JSON, transforming data between upstream AI nodes, calling HTTP APIs +with typed clients, or computing values that a shell one-liner would mangle. +If a plain shell command is enough, use a [`bash:` node](/guides/authoring-workflows/#node-fields) +instead. + +## Quick Start + +### Inline TypeScript (bun) + +```yaml +nodes: + - id: parse + script: | + const data = { count: 42, label: "ok" }; + console.log(JSON.stringify(data)); + runtime: bun +``` + +### Inline Python (uv) + +```yaml +nodes: + - id: compute + script: | + import json, statistics + values = [1, 2, 3, 4, 5] + print(json.dumps({ "mean": statistics.mean(values) })) + runtime: uv +``` + +### Named script from `.archon/scripts/` + +```yaml +nodes: + - id: fetch-pages + script: fetch-github-pages # resolves .archon/scripts/fetch-github-pages.ts + runtime: bun + timeout: 60000 +``` + +The file `.archon/scripts/fetch-github-pages.ts` is loaded and executed with +`bun --no-env-file run `. + +## How It Works + +1. **Substitute variables.** `$ARGUMENTS`, `$WORKFLOW_ID`, `$ARTIFACTS_DIR`, + `$BASE_BRANCH`, `$DOCS_DIR`, and upstream `$nodeId.output` references are + substituted into the `script` text before execution. +2. **Detect inline vs named.** If the `script` value contains a newline or any + shell metacharacter (see [Inline vs Named Scripts](#inline-vs-named-scripts) + below), it's treated as inline code. Otherwise it's treated as a named-script + reference. +3. **Dispatch.** + - `runtime: bun` + inline → `bun --no-env-file -e ''` + - `runtime: bun` + named → `bun --no-env-file run ` + - `runtime: uv` + inline → `uv run [--with dep ...] python -c ''` + - `runtime: uv` + named → `uv run [--with dep ...] ` +4. **Capture.** `stdout` (with the trailing newline stripped) becomes + `$nodeId.output`. `stderr` is logged as a warning and posted to the + conversation but does **not** fail the node. A non-zero exit code fails it. + +## YAML Schema + +```yaml +- id: node-name + script: # required, non-empty + runtime: bun | uv # required + deps: ["httpx", "pydantic>=2"] # optional, uv-only (see below) + timeout: 60000 # optional ms, default 120000 + depends_on: [upstream] # optional + when: "$upstream.output != ''" # optional + trigger_rule: all_success # optional (default) + retry: # optional; same shape as bash/AI nodes + max_attempts: 3 + on_error: transient +``` + +### Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `script` | string | Yes | Inline code, or the basename (no extension) of a file in `.archon/scripts/` or `~/.archon/scripts/` | +| `runtime` | `'bun'` \| `'uv'` | Yes | Which runtime executes the script. Must match the file extension for named scripts | +| `deps` | string[] | No | Python dependencies to install for this run. **uv only** — ignored with a warning for `bun` | +| `timeout` | number (ms) | No | Hard kill after this many milliseconds. Default: `120000` (2 min) | + +Standard DAG fields (`id`, `depends_on`, `when`, `trigger_rule`, `retry`, +`idle_timeout`) all work. AI-specific fields (`model`, `provider`, `context`, +`output_format`, `allowed_tools`, `denied_tools`, `hooks`, `mcp`, `skills`, +`agents`, `effort`, `thinking`, `maxBudgetUsd`, `systemPrompt`, `fallbackModel`, +`betas`, `sandbox`) are accepted by the parser but emit a loader warning and +are ignored at runtime — no AI is invoked. + +## Inline vs Named Scripts + +The executor decides mode from the `script` string itself. A value is treated +as **inline code** if it contains a newline or any shell metacharacter; otherwise +it's a **named script** lookup. + +- **Metacharacters that trigger inline mode:** space, `;` `(` `)` `{` `}` `&` + `|` `<` `>` `$` `` ` `` `"` `'` +- **Inline examples:** `"const x = 1; console.log(x)"`, multi-line blocks, any + snippet with a space +- **Named examples:** `fetch-pages`, `analyze_metrics`, `triage-fmt` — bare + identifiers with no whitespace or shell syntax + +If you want an inline snippet that happens to be syntactically a single +identifier, add a trailing comment or newline to force inline mode. + +### Named Script Resolution + +Named scripts are discovered from, in precedence order: + +1. `/.archon/scripts/` — repo-local +2. `~/.archon/scripts/` — home-scoped (shared across every repo) + +Each directory is walked one subfolder deep (e.g. `.archon/scripts/triage/foo.ts` +resolves as `foo`). Deeper nesting is ignored. On a same-name collision the +repo-local entry wins silently — see [Global Workflows](/guides/global-workflows/) +for the shared precedence rules. + +### Extension ↔ Runtime Mapping + +Named scripts derive their runtime from the file extension: + +| Extension | Runtime | +|-----------|---------| +| `.ts`, `.js` | `bun` | +| `.py` | `uv` | + +The `runtime:` declared on the node **must match the file's extension** — the +validator rejects `runtime: uv` pointing at a `.ts` file, and vice versa. For +inline scripts, you can use any language that the chosen runtime supports. + +## Dependencies (uv only) + +`deps` is a pass-through to `uv run --with `, which installs packages into +a per-run ephemeral environment: + +```yaml +- id: scrape + script: | + import httpx + r = httpx.get("https://api.github.com/repos/anthropics/anthropic-cookbook") + print(r.text) + runtime: uv + deps: ["httpx>=0.27"] +``` + +- **Version pinning** — any PEP 508 specifier works (`pkg==1.2.3`, `pkg>=2,<3`). +- **Bun ignores `deps`** — Bun auto-installs imported packages on first run, so + the validator emits a warning if you set `deps` with `runtime: bun`. Remove + the field, or switch to `uv` if you need explicit dependency management. +- **No persistent environment** — each run is isolated; there is no `requirements.txt` + or lockfile to maintain. + +## Output and Data Flow + +`stdout` (trimmed of its trailing newline) becomes `$nodeId.output`. Print JSON +if you want downstream nodes to access structured fields with +`$nodeId.output.field` — the workflow engine tries to parse the output as JSON +for field access in `when:` conditions and prompt substitution. + +```yaml +- id: classify + script: | + const input = process.argv.slice(2).join(' '); + const severity = input.includes('crash') ? 'high' : 'low'; + console.log(JSON.stringify({ severity, length: input.length })); + runtime: bun + +- id: investigate + command: investigate-bug + depends_on: [classify] + when: "$classify.output.severity == 'high'" +``` + +### Variable Substitution in Scripts + +Variables are substituted into the `script` text **as raw strings, without +shell quoting** — unlike `bash:` nodes, where `$nodeId.output` values are +auto-quoted. Treat substituted values as untrusted input and parse them with +language features, not by interpolating into shell syntax. + +For **named scripts**, variables are not passed automatically. Read them from +the environment (`process.env.USER_MESSAGE`, `os.environ['USER_MESSAGE']`) +or accept them via stdin. For **inline scripts**, substituted variables are +literally embedded into the code string at execution time. + +## Environment and Isolation + +Script subprocesses receive `process.env` merged with any codebase-scoped env +vars you've configured via the Web UI (Settings → Projects → Env Vars) or the +`env:` block in `.archon/config.yaml`. This is the same injection surface used +by Claude, Codex, and bash nodes. + +**Target repo `.env` isolation:** the Bun subprocess is invoked with +`--no-env-file`, so variables in the target repo's `.env` do **not** leak into +the script. Archon-managed env (from `~/.archon/.env` and `/.archon/.env`) +passes through normally. `uv`-launched Python subprocesses do not auto-load +`.env` at all. See [Security Model](/reference/security/#target-repo-env-isolation) +for the full story. + +## Validation + +`archon validate workflows ` checks script nodes for: + +- **Script file exists** — for named scripts, the basename must exist in + `.archon/scripts/` or `~/.archon/scripts/` with a matching extension for + the declared runtime. Missing files fail validation with a hint showing + the expected path. +- **Runtime available on PATH** — `bun` or `uv` must be installed. Missing + runtimes emit a warning with the official install command: + - `curl -fsSL https://bun.sh/install | bash` + - `curl -LsSf https://astral.sh/uv/install.sh | sh` +- **`deps` with `runtime: bun`** — warns that `deps` is a no-op under Bun. + +Runtime availability is cached per-process — the check spawns `which bun` / +`which uv` once and memoizes the result. + +## Patterns + +### Transform AI output before the next node + +Use a script node as a deterministic adapter between two AI nodes. The script +parses the upstream classifier's JSON, filters, and forwards a clean payload: + +```yaml +- id: classify + prompt: "Classify: $ARGUMENTS" + allowed_tools: [] + output_format: + type: object + properties: + items: + type: array + items: { type: object } + +- id: filter + script: | + const upstream = JSON.parse(process.env.UPSTREAM ?? '{}'); + const high = (upstream.items ?? []).filter(i => i.severity === 'high'); + console.log(JSON.stringify(high)); + runtime: bun + depends_on: [classify] + +- id: triage + command: triage-high-severity + depends_on: [filter] + when: "$filter.output != '[]'" +``` + +*(Note: to actually populate `UPSTREAM` you'd inline-substitute +`$classify.output` into the script body. The example above illustrates the +shape.)* + +### Reusable helper in `~/.archon/scripts/` + +A helper you want available in every repo — say, a triage summary formatter — +lives at `~/.archon/scripts/triage-fmt.ts`: + +```typescript +// ~/.archon/scripts/triage-fmt.ts +const raw = process.argv.slice(2).join(' ') || '{}'; +const data = JSON.parse(raw); +const lines = data.issues?.map((i: { id: string; title: string }) => + `- [${i.id}] ${i.title}` +).join('\n') ?? ''; +console.log(lines || 'no issues'); +``` + +Then reference it by name from any repo's workflow: + +```yaml +- id: format + script: triage-fmt + runtime: bun + depends_on: [gather] +``` + +### Python with scientific dependencies + +```yaml +- id: analyze + script: | + import json, sys + import pandas as pd + data = json.loads(sys.argv[1]) if len(sys.argv) > 1 else [] + df = pd.DataFrame(data) + print(df.describe().to_json()) + runtime: uv + deps: ["pandas>=2.0"] + depends_on: [collect] +``` + +## What Does NOT Work + +- **AI-only features** — `hooks`, `mcp`, `skills`, `allowed_tools`, + `denied_tools`, `agents`, `model`, `provider`, `output_format`, `effort`, + `thinking`, `maxBudgetUsd`, `systemPrompt`, `fallbackModel`, `betas`, and + `sandbox` are all ignored at runtime. The loader emits a warning listing + the ignored fields. +- **Interactive prompts** — the script runs headlessly; any `stdin` read will + see EOF immediately. +- **Runtimes other than `bun` and `uv`** — rejected at parse time. +- **Cancelling mid-execution** — script subprocesses are killed on workflow + cancel, but there's no cooperative cancellation signal. Design scripts to + complete quickly or fail fast. + +## See Also + +- [Authoring Workflows](/guides/authoring-workflows/) — full workflow reference +- [Global Workflows, Commands, and Scripts](/guides/global-workflows/) — home-scoped `~/.archon/scripts/` +- [Security Model](/reference/security/#target-repo-env-isolation) — env isolation details +- [Variables Reference](/reference/variables/) — substitution rules diff --git a/packages/docs-web/src/content/docs/guides/skills.md b/packages/docs-web/src/content/docs/guides/skills.md index d27262ffac..f64b6def3d 100644 --- a/packages/docs-web/src/content/docs/guides/skills.md +++ b/packages/docs-web/src/content/docs/guides/skills.md @@ -6,7 +6,7 @@ area: workflows audience: [user] status: current sidebar: - order: 7 + order: 8 --- DAG workflow nodes support a `skills` field that preloads named skills into the diff --git a/packages/docs-web/src/content/docs/reference/variables.md b/packages/docs-web/src/content/docs/reference/variables.md index f32779cb6c..127ab8d653 100644 --- a/packages/docs-web/src/content/docs/reference/variables.md +++ b/packages/docs-web/src/content/docs/reference/variables.md @@ -8,11 +8,11 @@ sidebar: order: 5 --- -Archon substitutes variables in command files, inline prompts, and bash scripts before execution. There are three categories of variables: workflow variables (substituted by the workflow engine), positional arguments (substituted by the command handler), and node output references (DAG workflows only). +Archon substitutes variables in command files, inline prompts, bash scripts, and `script:` node bodies before execution. There are three categories of variables: workflow variables (substituted by the workflow engine), positional arguments (substituted by the command handler), and node output references (DAG workflows only). ## Workflow Variables -These variables are substituted by the workflow executor in all node types (`command:`, `prompt:`, `bash:`, `loop:`). +These variables are substituted by the workflow executor in all node types (`command:`, `prompt:`, `bash:`, `script:`, `loop:`). | Variable | Resolves to | Notes | |----------|-------------|-------| @@ -64,6 +64,10 @@ In DAG workflows, nodes can reference the output of any completed upstream node. | `$nodeId.output` | Full output string of the referenced node | The node must be a declared dependency (in `depends_on`) | | `$nodeId.output.field` | A specific JSON field from the node's output | Requires the upstream node to use `output_format` for structured JSON | +### Shell Quoting in `bash:` vs `script:` + +`$nodeId.output` values are **auto shell-quoted** (single-quoted, with embedded `'` escaped) when substituted into `bash:` scripts, so the value is always safe to embed in a shell command. They are **not** shell-quoted when substituted into `script:` bodies — the raw value is embedded as-is. For script nodes, treat substituted values as untrusted input and parse them with language features (e.g. `JSON.parse`), not by interpolating into shell syntax. + ### Example ```yaml From 0d656969beb08ddb281836866c5d859a0c49dd5e Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Thu, 23 Apr 2026 15:28:57 +0300 Subject: [PATCH 2/2] fix(docs): review follow-ups for script-node guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - skills example: extract-labels was reading process.env.ISSUE_JSON which is never set; use String.raw`$fetch-issue.output` so the upstream bash node's JSON is actually consumed - guides/script-nodes.md + skills/workflow-dag.md: idle_timeout is accepted but ignored on script (and bash) nodes — executeScriptNode only reads node.timeout. Clarify that script/bash use `timeout`, not idle_timeout - archon-workflow-builder.yaml: prompt enumerated only bash/prompt/command/loop, so the AI builder could never propose script or approval nodes. Add both (plus examples + rule about script output not being shell-quoted) and regenerate bundled defaults - book/dag-workflows.md + book/quick-reference.md + adapters/web.md: fill in the node-type references that were missing script, approval, and cancel. adapters/web.md also overclaimed "loop" in the palette — NodePalette.tsx only drags command/prompt/bash, so note that the other kinds are YAML-only --- .../defaults/archon-workflow-builder.yaml | 39 ++++++++++++++----- .../skills/archon/examples/dag-workflow.yaml | 7 +++- .../skills/archon/references/workflow-dag.md | 2 +- .../docs-web/src/content/docs/adapters/web.md | 2 +- .../src/content/docs/book/dag-workflows.md | 5 ++- .../src/content/docs/book/quick-reference.md | 3 ++ .../src/content/docs/guides/script-nodes.md | 13 ++++--- .../defaults/bundled-defaults.generated.ts | 2 +- 8 files changed, 52 insertions(+), 21 deletions(-) diff --git a/.archon/workflows/defaults/archon-workflow-builder.yaml b/.archon/workflows/defaults/archon-workflow-builder.yaml index a311b8d970..66ce915de1 100644 --- a/.archon/workflows/defaults/archon-workflow-builder.yaml +++ b/.archon/workflows/defaults/archon-workflow-builder.yaml @@ -61,7 +61,8 @@ nodes: 5. Whether this should be a simple DAG or include a loop node Be specific and concrete. Each proposed node should have a clear type - (bash, prompt, command, or loop) and a one-line description of what it does. + (bash, prompt, command, script, loop, or approval) and a one-line + description of what it does. model: haiku allowed_tools: [] output_format: @@ -115,7 +116,7 @@ nodes: nodes: - id: node-id-kebab-case - # Choose ONE of: prompt, bash, command, loop + # Choose ONE of: prompt, bash, command, script, loop, approval # --- prompt node (AI-executed) --- prompt: | @@ -131,6 +132,17 @@ nodes: # --- command node (references a .archon/commands/ file) --- command: command-name + # --- script node (TypeScript via bun, or Python via uv — no AI, stdout = $.output) --- + # Use for deterministic data transforms the shell would mangle (JSON parsing, etc.) + script: | + const raw = String.raw`$other-node.output`; + const data = JSON.parse(raw); + console.log(JSON.stringify({ count: data.items.length })); + runtime: bun # required: 'bun' (.ts/.js) or 'uv' (.py) + # deps: [requests] # uv only + # Or reference a named script in .archon/scripts/: + # script: extract-labels # no extension; bun resolves .ts/.js, uv resolves .py + # --- loop node (iterative AI execution) --- loop: prompt: | @@ -139,17 +151,22 @@ nodes: max_iterations: 10 fresh_context: true # optional: reset context each iteration + # --- approval node (human gate — pauses workflow) --- + approval: + message: "Review the plan above. Approve to continue." + # capture_response: true # store reviewer comment as $.output + # Common options for all node types: depends_on: [other-node-id] # DAG edges when: "$.output == 'value'" # conditional execution trigger_rule: all_success # all_success | one_success | all_done - timeout: 120000 # ms, for bash nodes + timeout: 120000 # ms, for bash and script nodes ``` ## Variable Reference - `$ARGUMENTS` — user's input text - `$ARTIFACTS_DIR` — pre-created directory for workflow artifacts - - `$.output` — stdout from a bash node or AI response from a prompt node + - `$.output` — stdout from a bash/script node or AI response from a prompt node - `$.output.field` — JSON field from a node with output_format - `$BASE_BRANCH` — base git branch @@ -158,12 +175,14 @@ nodes: 2. The `description:` MUST follow the "Use when / Triggers / Does / NOT for" pattern 3. Every node MUST have a unique kebab-case `id` 4. Use `depends_on` to define execution order - 5. Use `bash` nodes for deterministic operations (file checks, git commands, installs) - 6. Use `prompt` nodes for AI reasoning tasks - 7. Use `output_format` on prompt nodes when downstream nodes need structured data - 8. Use `allowed_tools: []` on classification/analysis nodes that don't need tools - 9. Use `denied_tools: [Edit, Bash]` when a node should only use Write (not edit existing files) - 10. Prefer `model: haiku` for simple classification tasks to save cost + 5. Use `bash` nodes for deterministic shell operations (file checks, git commands, installs) + 6. Use `script` nodes for typed data transforms (TypeScript JSON parsing, Python with deps) — stdout is captured as output, stderr is forwarded as a warning. $nodeId.output is NOT shell-quoted in script bodies — parse with JSON.parse / json.loads, not shell interpolation + 7. Use `prompt` nodes for AI reasoning tasks + 8. Use `approval` nodes to pause for human review at risky gates (plan→execute boundary, destructive actions) + 9. Use `output_format` on prompt nodes when downstream nodes need structured data + 10. Use `allowed_tools: []` on classification/analysis nodes that don't need tools + 11. Use `denied_tools: [Edit, Bash]` when a node should only use Write (not edit existing files) + 12. Prefer `model: haiku` for simple classification tasks to save cost ## Output diff --git a/.claude/skills/archon/examples/dag-workflow.yaml b/.claude/skills/archon/examples/dag-workflow.yaml index 924ac70d25..676e08161e 100644 --- a/.claude/skills/archon/examples/dag-workflow.yaml +++ b/.claude/skills/archon/examples/dag-workflow.yaml @@ -45,9 +45,14 @@ nodes: # ── SCRIPT NODE: TypeScript (bun runtime), no AI, stdout captured as output ── # Deterministic parsing the shell would mangle — extracts labels cleanly as JSON. + # + # NOTE: `$fetch-issue.output` is substituted *raw* into the script body (no shell + # quoting — see reference/variables.md). Wrapping it in a String.raw template + # preserves backslashes and newlines in the JSON payload without needing any + # escaping. Safe here because gh issue view --json emits clean JSON. - id: extract-labels script: | - const raw = process.env.ISSUE_JSON ?? '{}'; + const raw = String.raw`$fetch-issue.output`; try { const issue = JSON.parse(raw); const labels = (issue.labels ?? []).map((l) => l.name); diff --git a/.claude/skills/archon/references/workflow-dag.md b/.claude/skills/archon/references/workflow-dag.md index 1856b9445b..5132e0dab6 100644 --- a/.claude/skills/archon/references/workflow-dag.md +++ b/.claude/skills/archon/references/workflow-dag.md @@ -131,7 +131,7 @@ All node types share these fields: | `depends_on` | string[] | `[]` | Node IDs that must settle before this node runs | | `when` | string | — | Condition expression. Node **skipped** when false | | `trigger_rule` | string | `all_success` | Join semantics for multiple dependencies | -| `idle_timeout` | number (ms) | 300000 | Per-node idle timeout. On loop nodes, applies per-iteration | +| `idle_timeout` | number (ms) | 300000 | Idle timeout for AI streaming (`command`, `prompt`) and per-iteration idle for `loop`. Accepted but ignored on `bash` and `script` — use `timeout` there | **Command, prompt, and bash nodes** (silently ignored on loop nodes, except `retry` which is a hard error): diff --git a/packages/docs-web/src/content/docs/adapters/web.md b/packages/docs-web/src/content/docs/adapters/web.md index 0025ca0219..bb5e43ba91 100644 --- a/packages/docs-web/src/content/docs/adapters/web.md +++ b/packages/docs-web/src/content/docs/adapters/web.md @@ -166,7 +166,7 @@ Click on a workflow run (from the dashboard or progress card) to open the execut The Workflow Builder at `/workflows/builder` provides a visual editor for creating and modifying workflow YAML files. Features include: - **DAG canvas** -- Drag-and-drop nodes to build your workflow graph visually -- **Node palette** -- Add command, prompt, bash, and loop nodes from a sidebar library +- **Node palette** -- Drag command, prompt, and bash nodes from a sidebar library. Additional node types (`script`, `loop`, `approval`, `cancel`) are editable via the Code / Split view - **Node inspector** -- Click a node to configure its properties (command, prompt text, dependencies, model overrides, hooks, MCP servers, etc.) in a tabbed panel - **View modes** -- Toggle between Visual, Split, and Code views. Split mode shows the canvas and YAML side by side. - **Command picker** -- Browse available commands when configuring command nodes diff --git a/packages/docs-web/src/content/docs/book/dag-workflows.md b/packages/docs-web/src/content/docs/book/dag-workflows.md index 2a66702584..93bf766872 100644 --- a/packages/docs-web/src/content/docs/book/dag-workflows.md +++ b/packages/docs-web/src/content/docs/book/dag-workflows.md @@ -230,14 +230,17 @@ The classify-and-route example uses `none_failed_min_one_success` on `implement` ## Node Types -Archon supports four node types: +Archon supports seven node types: | Type | Syntax | When to use | |------|--------|-------------| | **Command** | `command: my-command` | Load a command from `.archon/commands/my-command.md`. The standard choice. | | **Prompt** | `prompt: "inline instructions..."` | Quick, one-off instructions that don't need a reusable command file. | | **Bash** | `bash: "shell command"` | Run a shell script without AI. Stdout is captured as `$nodeId.output`. Deterministic operations only. | +| **Script** | `script: "..." runtime: bun\|uv` | TypeScript (via bun) or Python (via uv) — deterministic typed transforms where bash would need fragile quoting. Stdout is captured as `$nodeId.output`. See [Script Nodes](/guides/script-nodes/). | | **Loop** | `loop: { prompt: "...", until: SIGNAL }` | Repeat an AI prompt until a completion signal appears in the output. See [Loop Nodes](/guides/loop-nodes/). | +| **Approval** | `approval: { message: "..." }` | Pause the run for human review before continuing. See [Approval Nodes](/guides/approval-nodes/). | +| **Cancel** | `cancel: "reason string"` | Terminate the run with a reason (useful as a `when:`-gated branch for safety checks). | **Command** is the most common. Use it for anything you'll reuse across workflows. diff --git a/packages/docs-web/src/content/docs/book/quick-reference.md b/packages/docs-web/src/content/docs/book/quick-reference.md index ae37659f7a..2c3123acdd 100644 --- a/packages/docs-web/src/content/docs/book/quick-reference.md +++ b/packages/docs-web/src/content/docs/book/quick-reference.md @@ -124,7 +124,10 @@ All nodes share these base fields: | `command` | One of | string | Name of a command file in `.archon/commands/` | | `prompt` | One of | string | Inline AI instructions | | `bash` | One of | string | Shell script (runs without AI; stdout captured as `$nodeId.output`) | +| `script` | One of | string | TypeScript/JS (via bun) or Python (via uv); requires `runtime:` (`bun` or `uv`); optional `deps:` (uv only) and `timeout:` (ms). Stdout captured as `$nodeId.output`. See [Script Nodes](/guides/script-nodes/) | | `loop` | One of | object | Loop configuration (see Loop Options below) | +| `approval` | One of | object | Human-review gate; pauses the run until approved or rejected. See [Approval Nodes](/guides/approval-nodes/) | +| `cancel` | One of | string | Terminates the run with the given reason string | | `depends_on` | No | string[] | Node IDs that must complete before this node runs | | `when` | No | string | Condition expression; node is skipped if false | | `trigger_rule` | No | string | Join semantics when multiple upstreams exist (see Trigger Rules) | diff --git a/packages/docs-web/src/content/docs/guides/script-nodes.md b/packages/docs-web/src/content/docs/guides/script-nodes.md index f023d2adba..73a0ad9fbe 100644 --- a/packages/docs-web/src/content/docs/guides/script-nodes.md +++ b/packages/docs-web/src/content/docs/guides/script-nodes.md @@ -101,12 +101,13 @@ The file `.archon/scripts/fetch-github-pages.ts` is loaded and executed with | `deps` | string[] | No | Python dependencies to install for this run. **uv only** — ignored with a warning for `bun` | | `timeout` | number (ms) | No | Hard kill after this many milliseconds. Default: `120000` (2 min) | -Standard DAG fields (`id`, `depends_on`, `when`, `trigger_rule`, `retry`, -`idle_timeout`) all work. AI-specific fields (`model`, `provider`, `context`, -`output_format`, `allowed_tools`, `denied_tools`, `hooks`, `mcp`, `skills`, -`agents`, `effort`, `thinking`, `maxBudgetUsd`, `systemPrompt`, `fallbackModel`, -`betas`, `sandbox`) are accepted by the parser but emit a loader warning and -are ignored at runtime — no AI is invoked. +Standard DAG fields (`id`, `depends_on`, `when`, `trigger_rule`, `retry`) all +work. AI-specific fields (`model`, `provider`, `context`, `output_format`, +`allowed_tools`, `denied_tools`, `hooks`, `mcp`, `skills`, `agents`, `effort`, +`thinking`, `maxBudgetUsd`, `systemPrompt`, `fallbackModel`, `betas`, `sandbox`) +are accepted by the parser but emit a loader warning and are ignored at runtime +— no AI is invoked. `idle_timeout` is also accepted but ignored: script nodes +run as one-shot subprocesses, so use `timeout` (hard kill after N ms) instead. ## Inline vs Named Scripts diff --git a/packages/workflows/src/defaults/bundled-defaults.generated.ts b/packages/workflows/src/defaults/bundled-defaults.generated.ts index cd430f3d5a..074bac9046 100644 --- a/packages/workflows/src/defaults/bundled-defaults.generated.ts +++ b/packages/workflows/src/defaults/bundled-defaults.generated.ts @@ -74,5 +74,5 @@ export const BUNDLED_WORKFLOWS: Record = { "archon-smart-pr-review": "name: archon-smart-pr-review\ndescription: |\n Use when: User wants a smart, efficient PR review that adapts to PR complexity.\n Triggers: \"smart review\", \"review this PR\", \"review PR #123\", \"efficient review\",\n \"smart PR review\", \"quick review\".\n Does: Gathers PR scope -> classifies complexity -> routes to only relevant review agents ->\n synthesizes findings -> auto-fixes CRITICAL/HIGH issues.\n NOT for: When you explicitly want ALL review agents (use archon-comprehensive-pr-review instead).\n\n Unlike the comprehensive review, this workflow classifies the PR first and only runs\n the review agents that are relevant. A 3-line typo fix skips test-coverage and docs-impact.\n\nnodes:\n - id: scope\n command: archon-pr-review-scope\n\n - id: sync\n command: archon-sync-pr-with-main\n depends_on: [scope]\n\n - id: classify\n prompt: |\n You are a PR complexity classifier. Analyze the PR scope below and determine\n which review agents should run.\n\n ## PR Scope\n $scope.output\n\n ## Rules\n - **Code review**: Always run unless the diff is empty or only touches non-code files\n (e.g. README-only, config-only, or .yaml-only changes).\n - **Error handling**: Run if the diff touches code with try/catch, error handling,\n async/await, or adds new failure paths.\n - **Test coverage**: Run if the diff touches source code (not just tests, docs, or config).\n - **Comment quality**: Run if the diff adds or modifies comments, docstrings, JSDoc,\n or significant documentation within code files.\n - **Docs impact**: Run if the diff adds/removes/renames public APIs, commands, CLI flags,\n environment variables, or user-facing features.\n\n Classify the PR complexity:\n - **trivial**: Typo fixes, formatting, single-line changes, version bumps\n - **small**: 1-3 files, straightforward logic, no architectural changes\n - **medium**: 4-10 files, moderate logic changes, some cross-cutting concerns\n - **large**: 10+ files, architectural changes, new subsystems, complex refactors\n\n Provide your reasoning for each decision.\n depends_on: [scope]\n model: haiku\n allowed_tools: []\n output_format:\n type: object\n properties:\n run_code_review:\n type: string\n enum: [\"true\", \"false\"]\n run_error_handling:\n type: string\n enum: [\"true\", \"false\"]\n run_test_coverage:\n type: string\n enum: [\"true\", \"false\"]\n run_comment_quality:\n type: string\n enum: [\"true\", \"false\"]\n run_docs_impact:\n type: string\n enum: [\"true\", \"false\"]\n complexity:\n type: string\n enum: [\"trivial\", \"small\", \"medium\", \"large\"]\n reasoning:\n type: string\n required:\n - run_code_review\n - run_error_handling\n - run_test_coverage\n - run_comment_quality\n - run_docs_impact\n - complexity\n - reasoning\n\n - id: code-review\n command: archon-code-review-agent\n depends_on: [classify, sync]\n when: \"$classify.output.run_code_review == 'true'\"\n\n - id: error-handling\n command: archon-error-handling-agent\n depends_on: [classify, sync]\n when: \"$classify.output.run_error_handling == 'true'\"\n\n - id: test-coverage\n command: archon-test-coverage-agent\n depends_on: [classify, sync]\n when: \"$classify.output.run_test_coverage == 'true'\"\n\n - id: comment-quality\n command: archon-comment-quality-agent\n depends_on: [classify, sync]\n when: \"$classify.output.run_comment_quality == 'true'\"\n\n - id: docs-impact\n command: archon-docs-impact-agent\n depends_on: [classify, sync]\n when: \"$classify.output.run_docs_impact == 'true'\"\n\n - id: synthesize\n command: archon-synthesize-review\n depends_on: [code-review, error-handling, test-coverage, comment-quality, docs-impact]\n trigger_rule: one_success\n\n - id: implement-fixes\n command: archon-implement-review-fixes\n depends_on: [synthesize]\n\n # Optional: push notification when review completes.\n # To enable, create .archon/mcp/ntfy.json — see docs/mcp-servers.md\n - id: check-ntfy\n bash: \"test -f .archon/mcp/ntfy.json && echo 'true' || echo 'false'\"\n depends_on: [implement-fixes]\n\n - id: notify\n depends_on: [check-ntfy, synthesize, implement-fixes]\n when: \"$check-ntfy.output == 'true'\"\n trigger_rule: all_success\n mcp: .archon/mcp/ntfy.json\n allowed_tools: []\n prompt: |\n Send a push notification summarizing the PR review results.\n\n Review synthesis:\n $synthesize.output\n\n Fix results:\n $implement-fixes.output\n\n Send with:\n - title: \"PR Review Complete\"\n - message: 1-2 sentence summary — verdict and issue count. Short enough for a lock screen.\n - priority: 3 if ready to merge, 4 if needs fixes, 5 if critical issues remain\n", "archon-test-loop-dag": "name: archon-test-loop-dag\ndescription: |\n Use when: User explicitly says \"test-loop-dag\" or \"run test-loop-dag\".\n IMPORTANT: This is a DAG workflow with a loop node that iterates until completion.\n NOT for: General testing questions or debugging.\n Does: Initializes a counter, iterates until it reaches 3, then reports completion.\n\nnodes:\n - id: setup\n bash: |\n echo \"0\" > .archon/test-loop-dag-counter.txt\n echo \"Counter initialized to 0\"\n\n - id: loop-counter\n depends_on: [setup]\n loop:\n prompt: |\n You are testing the loop node functionality within a DAG workflow.\n\n ## Your Task\n\n 1. Read the file `.archon/test-loop-dag-counter.txt`\n 2. Parse the current counter value\n 3. Increment it by 1\n 4. Write the new value back to the file\n 5. Report the current iteration\n\n ## User Intent\n\n $USER_MESSAGE\n\n ## Completion Criteria\n\n - If the counter reaches 3 or higher, output: COMPLETE\n - Otherwise, just report your progress and end normally\n\n ## Important\n\n Be concise. Just do the task and report the counter value.\n until: COMPLETE\n max_iterations: 5\n fresh_context: false\n\n - id: report\n depends_on: [loop-counter]\n prompt: |\n The loop counter test has completed. The loop node output was:\n\n $loop-counter.output\n\n Read `.archon/test-loop-dag-counter.txt` and confirm the final counter value.\n Report: \"Test loop DAG completed successfully. Final counter: {value}\"\n", "archon-validate-pr": "name: archon-validate-pr\ndescription: |\n Use when: User wants a thorough PR validation that tests both main (bug present) and feature branch (bug fixed).\n Triggers: \"validate PR\", \"validate pr #123\", \"test this PR\", \"verify PR\", \"full PR validation\",\n \"validate pull request\", \"test PR end-to-end\".\n Does: Fetches PR info -> finds free ports -> parallel code review (main vs feature) ->\n E2E test on main (reproduce bug) -> E2E test on feature (verify fix) -> final verdict report.\n NOT for: Quick code-only reviews (use archon-smart-pr-review), fixing issues, general exploration.\n\n This workflow is designed for running in parallel — each instance finds its own free ports\n to avoid conflicts. Produces artifacts in $ARTIFACTS_DIR/ and posts a validation report.\n\nprovider: claude\nmodel: opus\n\nnodes:\n # ═══════════════════════════════════════════════════════════════\n # PHASE 1: SETUP — Fetch PR info and allocate ports\n # ═══════════════════════════════════════════════════════════════\n\n - id: fetch-pr\n bash: |\n # Extract PR number from arguments\n PR_NUMBER=$(echo \"$ARGUMENTS\" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+' | head -1)\n # Fallback: extract first number if no URL path found (e.g., \"validate PR 42\")\n if [ -z \"$PR_NUMBER\" ]; then\n PR_NUMBER=$(echo \"$ARGUMENTS\" | grep -oE '[0-9]+' | head -1)\n fi\n if [ -z \"$PR_NUMBER\" ]; then\n # Try getting PR from current branch\n PR_NUMBER=$(gh pr view --json number -q '.number' 2>/dev/null)\n fi\n\n if [ -z \"$PR_NUMBER\" ]; then\n echo \"ERROR: No PR number found in arguments: $ARGUMENTS\"\n exit 1\n fi\n\n echo \"$PR_NUMBER\" > \"$ARTIFACTS_DIR/.pr-number\"\n\n # Fetch full PR details\n gh pr view \"$PR_NUMBER\" --json number,title,body,url,headRefName,baseRefName,files,additions,deletions,changedFiles,state,author,labels,isDraft\n\n - id: find-ports\n bash: |\n # Use Bun to let the OS pick truly free ports (cross-platform: Linux, macOS, Windows)\n BACKEND_PORT=$(bun -e \"const s = Bun.serve({port: 0, fetch: () => new Response('')}); console.log(s.port); s.stop()\")\n FRONTEND_PORT=$(bun -e \"const s = Bun.serve({port: 0, fetch: () => new Response('')}); console.log(s.port); s.stop()\")\n\n echo \"$BACKEND_PORT\" > \"$ARTIFACTS_DIR/.backend-port\"\n echo \"$FRONTEND_PORT\" > \"$ARTIFACTS_DIR/.frontend-port\"\n\n echo \"BACKEND_PORT=$BACKEND_PORT\"\n echo \"FRONTEND_PORT=$FRONTEND_PORT\"\n\n - id: resolve-paths\n bash: |\n # Resolve canonical repo path (main branch) vs worktree path (feature branch)\n CANONICAL_REPO=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\\.git$||')\n WORKTREE_PATH=$(pwd)\n FEATURE_BRANCH=$(git branch --show-current)\n\n # Get PR branch info\n PR_NUMBER=$(cat \"$ARTIFACTS_DIR/.pr-number\")\n PR_HEAD=$(gh pr view \"$PR_NUMBER\" --json headRefName -q '.headRefName')\n PR_BASE=$(gh pr view \"$PR_NUMBER\" --json baseRefName -q '.baseRefName')\n\n echo \"$CANONICAL_REPO\" > \"$ARTIFACTS_DIR/.canonical-repo\"\n echo \"$WORKTREE_PATH\" > \"$ARTIFACTS_DIR/.worktree-path\"\n echo \"$FEATURE_BRANCH\" > \"$ARTIFACTS_DIR/.feature-branch\"\n echo \"$PR_HEAD\" > \"$ARTIFACTS_DIR/.pr-head\"\n echo \"$PR_BASE\" > \"$ARTIFACTS_DIR/.pr-base\"\n\n echo \"CANONICAL_REPO=$CANONICAL_REPO\"\n echo \"WORKTREE_PATH=$WORKTREE_PATH\"\n echo \"FEATURE_BRANCH=$FEATURE_BRANCH\"\n echo \"PR_HEAD=$PR_HEAD\"\n echo \"PR_BASE=$PR_BASE\"\n depends_on: [fetch-pr]\n\n # ═══════════════════════════════════════════════════════════════\n # PHASE 2: CODE REVIEW — Parallel analysis of main vs feature\n # ═══════════════════════════════════════════════════════════════\n\n - id: code-review-main\n command: archon-validate-pr-code-review-main\n depends_on: [fetch-pr, resolve-paths]\n context: fresh\n\n - id: code-review-feature\n command: archon-validate-pr-code-review-feature\n depends_on: [fetch-pr, resolve-paths, code-review-main]\n context: fresh\n\n # ═══════════════════════════════════════════════════════════════\n # PHASE 3: E2E TESTING — Sequential (after code reviews finish)\n # ═══════════════════════════════════════════════════════════════\n\n - id: classify-testability\n prompt: |\n You are a PR testability classifier. Determine whether this PR's changes can be\n validated via browser E2E testing, or if it requires code-review-only validation.\n\n ## PR Details\n\n $fetch-pr.output\n\n ## Rules\n\n - **e2e_testable**: Changes affect the Web UI (components, hooks, styles, API routes\n that serve the frontend, SSE streaming, layout, user-visible behavior). These can be\n validated by starting Archon and using agent-browser to interact with the UI.\n - **code_review_only**: Changes are purely backend logic, CLI-only, workflow engine,\n database schemas, git operations, build tooling, tests, documentation, or other\n non-UI code. No visual validation possible.\n\n Consider: even if a change is backend, if it affects what the frontend displays\n (e.g., API response format changes, SSE event changes), it IS e2e_testable.\n depends_on: [fetch-pr]\n model: haiku\n allowed_tools: []\n output_format:\n type: object\n properties:\n testable:\n type: string\n enum: [\"e2e_testable\", \"code_review_only\"]\n reasoning:\n type: string\n test_plan:\n type: string\n required: [testable, reasoning, test_plan]\n\n - id: e2e-test-main\n command: archon-validate-pr-e2e-main\n depends_on: [classify-testability, find-ports, resolve-paths, code-review-main, code-review-feature]\n when: \"$classify-testability.output.testable == 'e2e_testable'\"\n context: fresh\n idle_timeout: 1800000\n\n - id: e2e-test-feature\n command: archon-validate-pr-e2e-feature\n depends_on: [e2e-test-main, find-ports, resolve-paths]\n when: \"$classify-testability.output.testable == 'e2e_testable'\"\n context: fresh\n idle_timeout: 1800000\n\n # ═══════════════════════════════════════════════════════════════\n # PHASE 4: FINAL REPORT — Synthesize all findings\n # ═══════════════════════════════════════════════════════════════\n\n - id: cleanup-processes\n bash: |\n # Safety net: kill any orphaned processes from E2E testing\n # This runs after E2E nodes complete (or timeout/fail) to prevent process accumulation\n BACKEND_PORT=$(cat \"$ARTIFACTS_DIR/.backend-port\" 2>/dev/null | tr -d '\\n')\n FRONTEND_PORT=$(cat \"$ARTIFACTS_DIR/.frontend-port\" 2>/dev/null | tr -d '\\n')\n\n if [ -z \"$BACKEND_PORT\" ] || [ -z \"$FRONTEND_PORT\" ]; then\n echo \"No port files found — skipping cleanup\"\n exit 0\n fi\n\n echo \"Cleaning up ports $BACKEND_PORT and $FRONTEND_PORT...\"\n\n # Kill by all recorded PID files\n for pidfile in \"$ARTIFACTS_DIR\"/.e2e-*-pid; do\n if [ -f \"$pidfile\" ]; then\n PID=$(cat \"$pidfile\" | tr -d '\\n')\n echo \"Killing PID $PID from $pidfile\"\n kill \"$PID\" 2>/dev/null || taskkill //F //T //PID \"$PID\" 2>/dev/null || true\n fi\n done\n\n # Kill by port (cross-platform fallback)\n for PORT in $BACKEND_PORT $FRONTEND_PORT; do\n fuser -k \"$PORT/tcp\" 2>/dev/null || true\n lsof -ti:\"$PORT\" 2>/dev/null | xargs kill -9 2>/dev/null || true\n netstat -ano 2>/dev/null | grep \":$PORT \" | grep LISTENING | awk '{print $5}' | sort -u | while read pid; do\n taskkill //F //T //PID \"$pid\" 2>/dev/null || true\n done\n done\n\n # pkill fallback: catch processes that escaped PID/port cleanup\n pkill -f \"PORT=$BACKEND_PORT.*bun\" 2>/dev/null || true\n pkill -f \"vite.*port.*$FRONTEND_PORT\" 2>/dev/null || true\n\n # Close this workflow's browser session only (scoped by session ID)\n BROWSER_SESSION=$(cat \"$ARTIFACTS_DIR/.browser-session\" 2>/dev/null | tr -d '\\n')\n if [ -n \"$BROWSER_SESSION\" ]; then\n agent-browser --session \"$BROWSER_SESSION\" close 2>/dev/null || true\n fi\n\n # Remove main E2E worktree if it still exists (safety net)\n CANONICAL_REPO=$(cat \"$ARTIFACTS_DIR/.canonical-repo\" 2>/dev/null | tr -d '\\n')\n MAIN_E2E_PATH=$(cat \"$ARTIFACTS_DIR/.e2e-main-worktree\" 2>/dev/null | tr -d '\\n')\n if [ -n \"$MAIN_E2E_PATH\" ] && [ -n \"$CANONICAL_REPO\" ] && [ -d \"$MAIN_E2E_PATH\" ]; then\n echo \"Removing leftover main E2E worktree: $MAIN_E2E_PATH\"\n git -C \"$CANONICAL_REPO\" worktree remove \"$MAIN_E2E_PATH\" --force 2>/dev/null || rm -rf \"$MAIN_E2E_PATH\"\n fi\n\n sleep 1\n echo \"Process cleanup complete\"\n depends_on: [e2e-test-main, e2e-test-feature]\n trigger_rule: all_done\n\n - id: final-report\n command: archon-validate-pr-report\n depends_on: [code-review-main, code-review-feature, e2e-test-main, e2e-test-feature, classify-testability, cleanup-processes]\n trigger_rule: all_done\n context: fresh\n", - "archon-workflow-builder": "name: archon-workflow-builder\ndescription: |\n Use when: User wants to create a new custom workflow for their project.\n Triggers: \"build me a workflow\", \"create a workflow\", \"generate a workflow\",\n \"new workflow\", \"make a workflow for\", \"workflow builder\".\n Does: Scans codebase -> extracts intent (JSON) -> generates YAML -> validates -> saves.\n NOT for: Editing existing workflows or creating non-workflow files.\n\nnodes:\n - id: scan-codebase\n bash: |\n echo \"=== Existing Commands ===\"\n if [ -d \".archon/commands\" ]; then\n find .archon/commands -type f -name \"*.md\" 2>/dev/null | head -30\n else\n echo \"(no .archon/commands/ directory)\"\n fi\n\n echo \"\"\n echo \"=== Existing Workflows ===\"\n if [ -d \".archon/workflows\" ]; then\n find .archon/workflows -type f \\( -name \"*.yaml\" -o -name \"*.yml\" \\) 2>/dev/null | head -30\n else\n echo \"(no .archon/workflows/ directory)\"\n fi\n\n echo \"\"\n echo \"=== Package Info ===\"\n if [ -f \"package.json\" ]; then\n grep -E '\"name\"|\"scripts\"' package.json | head -10\n else\n echo \"(no package.json)\"\n fi\n\n echo \"\"\n echo \"=== Project Context (CLAUDE.md first 50 lines) ===\"\n if [ -f \"CLAUDE.md\" ]; then\n head -50 CLAUDE.md\n else\n echo \"(no CLAUDE.md)\"\n fi\n\n - id: extract-intent\n prompt: |\n You are a workflow design classifier. Given a user's description of what they want\n a workflow to do, extract structured intent.\n\n ## User's Request\n $ARGUMENTS\n\n ## Codebase Context\n $scan-codebase.output\n\n ## Instructions\n\n Analyze the user's request and the existing codebase to determine:\n 1. A kebab-case workflow name (e.g., \"lint-and-test\", \"deploy-staging\")\n 2. A description following the Archon pattern (Use when / Triggers / Does / NOT for)\n 3. Trigger phrases the router should match\n 4. A list of proposed nodes with their types and purposes\n 5. Whether this should be a simple DAG or include a loop node\n\n Be specific and concrete. Each proposed node should have a clear type\n (bash, prompt, command, or loop) and a one-line description of what it does.\n model: haiku\n allowed_tools: []\n output_format:\n type: object\n properties:\n workflow_name:\n type: string\n description:\n type: string\n trigger_phrases:\n type: string\n proposed_nodes:\n type: string\n execution_mode:\n type: string\n enum: [\"dag\", \"loop\"]\n required: [workflow_name, description, trigger_phrases, proposed_nodes, execution_mode]\n depends_on: [scan-codebase]\n\n - id: generate-yaml\n prompt: |\n You are an Archon workflow author. Generate a complete, valid workflow YAML file\n based on the structured intent provided.\n\n ## Intent\n - **Name**: $extract-intent.output.workflow_name\n - **Description**: $extract-intent.output.description\n - **Trigger Phrases**: $extract-intent.output.trigger_phrases\n - **Proposed Nodes**: $extract-intent.output.proposed_nodes\n - **Execution Mode**: $extract-intent.output.execution_mode\n\n ## Original User Request\n $ARGUMENTS\n\n ## Archon Workflow YAML Schema Reference\n\n A workflow YAML file has this structure:\n\n ```yaml\n name: workflow-name\n description: |\n Use when: ...\n Triggers: ...\n Does: ...\n NOT for: ...\n\n # Optional top-level settings:\n # provider: claude (or codex)\n # model: sonnet (or haiku, opus, etc.)\n # interactive: true (forces foreground execution in web UI)\n\n nodes:\n - id: node-id-kebab-case\n # Choose ONE of: prompt, bash, command, loop\n\n # --- prompt node (AI-executed) ---\n prompt: |\n Instructions for the AI...\n # Optional: model, allowed_tools, denied_tools, output_format, context, idle_timeout\n\n # --- bash node (shell script, no AI, stdout = $.output) ---\n bash: |\n #!/bin/bash\n set -e\n echo \"result\"\n\n # --- command node (references a .archon/commands/ file) ---\n command: command-name\n\n # --- loop node (iterative AI execution) ---\n loop:\n prompt: |\n Instructions repeated each iteration...\n until: COMPLETION_SIGNAL\n max_iterations: 10\n fresh_context: true # optional: reset context each iteration\n\n # Common options for all node types:\n depends_on: [other-node-id] # DAG edges\n when: \"$.output == 'value'\" # conditional execution\n trigger_rule: all_success # all_success | one_success | all_done\n timeout: 120000 # ms, for bash nodes\n ```\n\n ## Variable Reference\n - `$ARGUMENTS` — user's input text\n - `$ARTIFACTS_DIR` — pre-created directory for workflow artifacts\n - `$.output` — stdout from a bash node or AI response from a prompt node\n - `$.output.field` — JSON field from a node with output_format\n - `$BASE_BRANCH` — base git branch\n\n ## Rules\n 1. The `name:` field MUST match: $extract-intent.output.workflow_name\n 2. The `description:` MUST follow the \"Use when / Triggers / Does / NOT for\" pattern\n 3. Every node MUST have a unique kebab-case `id`\n 4. Use `depends_on` to define execution order\n 5. Use `bash` nodes for deterministic operations (file checks, git commands, installs)\n 6. Use `prompt` nodes for AI reasoning tasks\n 7. Use `output_format` on prompt nodes when downstream nodes need structured data\n 8. Use `allowed_tools: []` on classification/analysis nodes that don't need tools\n 9. Use `denied_tools: [Edit, Bash]` when a node should only use Write (not edit existing files)\n 10. Prefer `model: haiku` for simple classification tasks to save cost\n\n ## Output\n\n Write the complete workflow YAML to: `$ARTIFACTS_DIR/generated-workflow.yaml`\n\n Use the Write tool. Do NOT use Edit or Bash. The file must be valid YAML and follow\n all the patterns above.\n denied_tools: [Edit, Bash]\n depends_on: [extract-intent]\n\n - id: validate-yaml\n bash: |\n FILE=\"$ARTIFACTS_DIR/generated-workflow.yaml\"\n\n if [ ! -f \"$FILE\" ]; then\n echo \"ERROR: generated-workflow.yaml not found at $FILE\"\n exit 1\n fi\n\n if [ ! -s \"$FILE\" ]; then\n echo \"ERROR: generated-workflow.yaml is empty\"\n exit 1\n fi\n\n if ! grep -q \"^name:\" \"$FILE\"; then\n echo \"ERROR: missing 'name:' field\"\n exit 1\n fi\n\n if ! grep -q \"^nodes:\" \"$FILE\"; then\n echo \"ERROR: missing 'nodes:' field\"\n exit 1\n fi\n\n echo \"VALID\"\n depends_on: [generate-yaml]\n\n - id: save-or-report\n prompt: |\n You are a workflow installer. Save the generated workflow and report to the user.\n\n ## Workflow Details\n - **Name**: $extract-intent.output.workflow_name\n - **Trigger Phrases**: $extract-intent.output.trigger_phrases\n\n ## Instructions\n\n 1. Read the generated workflow from `$ARTIFACTS_DIR/generated-workflow.yaml`\n 2. Create the directory `.archon/workflows/` if it doesn't exist (use Bash: `mkdir -p .archon/workflows/`)\n 3. Save the workflow to `.archon/workflows/$extract-intent.output.workflow_name.yaml`\n Use the Write tool to write the file.\n 4. Report to the user:\n - Workflow name and file location\n - Trigger phrases that will invoke it\n - How to run it: `bun run cli workflow run $extract-intent.output.workflow_name \"your input\"`\n - How to test it: `bun run cli validate workflows $extract-intent.output.workflow_name`\n depends_on: [validate-yaml]\n", + "archon-workflow-builder": "name: archon-workflow-builder\ndescription: |\n Use when: User wants to create a new custom workflow for their project.\n Triggers: \"build me a workflow\", \"create a workflow\", \"generate a workflow\",\n \"new workflow\", \"make a workflow for\", \"workflow builder\".\n Does: Scans codebase -> extracts intent (JSON) -> generates YAML -> validates -> saves.\n NOT for: Editing existing workflows or creating non-workflow files.\n\nnodes:\n - id: scan-codebase\n bash: |\n echo \"=== Existing Commands ===\"\n if [ -d \".archon/commands\" ]; then\n find .archon/commands -type f -name \"*.md\" 2>/dev/null | head -30\n else\n echo \"(no .archon/commands/ directory)\"\n fi\n\n echo \"\"\n echo \"=== Existing Workflows ===\"\n if [ -d \".archon/workflows\" ]; then\n find .archon/workflows -type f \\( -name \"*.yaml\" -o -name \"*.yml\" \\) 2>/dev/null | head -30\n else\n echo \"(no .archon/workflows/ directory)\"\n fi\n\n echo \"\"\n echo \"=== Package Info ===\"\n if [ -f \"package.json\" ]; then\n grep -E '\"name\"|\"scripts\"' package.json | head -10\n else\n echo \"(no package.json)\"\n fi\n\n echo \"\"\n echo \"=== Project Context (CLAUDE.md first 50 lines) ===\"\n if [ -f \"CLAUDE.md\" ]; then\n head -50 CLAUDE.md\n else\n echo \"(no CLAUDE.md)\"\n fi\n\n - id: extract-intent\n prompt: |\n You are a workflow design classifier. Given a user's description of what they want\n a workflow to do, extract structured intent.\n\n ## User's Request\n $ARGUMENTS\n\n ## Codebase Context\n $scan-codebase.output\n\n ## Instructions\n\n Analyze the user's request and the existing codebase to determine:\n 1. A kebab-case workflow name (e.g., \"lint-and-test\", \"deploy-staging\")\n 2. A description following the Archon pattern (Use when / Triggers / Does / NOT for)\n 3. Trigger phrases the router should match\n 4. A list of proposed nodes with their types and purposes\n 5. Whether this should be a simple DAG or include a loop node\n\n Be specific and concrete. Each proposed node should have a clear type\n (bash, prompt, command, script, loop, or approval) and a one-line\n description of what it does.\n model: haiku\n allowed_tools: []\n output_format:\n type: object\n properties:\n workflow_name:\n type: string\n description:\n type: string\n trigger_phrases:\n type: string\n proposed_nodes:\n type: string\n execution_mode:\n type: string\n enum: [\"dag\", \"loop\"]\n required: [workflow_name, description, trigger_phrases, proposed_nodes, execution_mode]\n depends_on: [scan-codebase]\n\n - id: generate-yaml\n prompt: |\n You are an Archon workflow author. Generate a complete, valid workflow YAML file\n based on the structured intent provided.\n\n ## Intent\n - **Name**: $extract-intent.output.workflow_name\n - **Description**: $extract-intent.output.description\n - **Trigger Phrases**: $extract-intent.output.trigger_phrases\n - **Proposed Nodes**: $extract-intent.output.proposed_nodes\n - **Execution Mode**: $extract-intent.output.execution_mode\n\n ## Original User Request\n $ARGUMENTS\n\n ## Archon Workflow YAML Schema Reference\n\n A workflow YAML file has this structure:\n\n ```yaml\n name: workflow-name\n description: |\n Use when: ...\n Triggers: ...\n Does: ...\n NOT for: ...\n\n # Optional top-level settings:\n # provider: claude (or codex)\n # model: sonnet (or haiku, opus, etc.)\n # interactive: true (forces foreground execution in web UI)\n\n nodes:\n - id: node-id-kebab-case\n # Choose ONE of: prompt, bash, command, script, loop, approval\n\n # --- prompt node (AI-executed) ---\n prompt: |\n Instructions for the AI...\n # Optional: model, allowed_tools, denied_tools, output_format, context, idle_timeout\n\n # --- bash node (shell script, no AI, stdout = $.output) ---\n bash: |\n #!/bin/bash\n set -e\n echo \"result\"\n\n # --- command node (references a .archon/commands/ file) ---\n command: command-name\n\n # --- script node (TypeScript via bun, or Python via uv — no AI, stdout = $.output) ---\n # Use for deterministic data transforms the shell would mangle (JSON parsing, etc.)\n script: |\n const raw = String.raw`$other-node.output`;\n const data = JSON.parse(raw);\n console.log(JSON.stringify({ count: data.items.length }));\n runtime: bun # required: 'bun' (.ts/.js) or 'uv' (.py)\n # deps: [requests] # uv only\n # Or reference a named script in .archon/scripts/:\n # script: extract-labels # no extension; bun resolves .ts/.js, uv resolves .py\n\n # --- loop node (iterative AI execution) ---\n loop:\n prompt: |\n Instructions repeated each iteration...\n until: COMPLETION_SIGNAL\n max_iterations: 10\n fresh_context: true # optional: reset context each iteration\n\n # --- approval node (human gate — pauses workflow) ---\n approval:\n message: \"Review the plan above. Approve to continue.\"\n # capture_response: true # store reviewer comment as $.output\n\n # Common options for all node types:\n depends_on: [other-node-id] # DAG edges\n when: \"$.output == 'value'\" # conditional execution\n trigger_rule: all_success # all_success | one_success | all_done\n timeout: 120000 # ms, for bash and script nodes\n ```\n\n ## Variable Reference\n - `$ARGUMENTS` — user's input text\n - `$ARTIFACTS_DIR` — pre-created directory for workflow artifacts\n - `$.output` — stdout from a bash/script node or AI response from a prompt node\n - `$.output.field` — JSON field from a node with output_format\n - `$BASE_BRANCH` — base git branch\n\n ## Rules\n 1. The `name:` field MUST match: $extract-intent.output.workflow_name\n 2. The `description:` MUST follow the \"Use when / Triggers / Does / NOT for\" pattern\n 3. Every node MUST have a unique kebab-case `id`\n 4. Use `depends_on` to define execution order\n 5. Use `bash` nodes for deterministic shell operations (file checks, git commands, installs)\n 6. Use `script` nodes for typed data transforms (TypeScript JSON parsing, Python with deps) — stdout is captured as output, stderr is forwarded as a warning. $nodeId.output is NOT shell-quoted in script bodies — parse with JSON.parse / json.loads, not shell interpolation\n 7. Use `prompt` nodes for AI reasoning tasks\n 8. Use `approval` nodes to pause for human review at risky gates (plan→execute boundary, destructive actions)\n 9. Use `output_format` on prompt nodes when downstream nodes need structured data\n 10. Use `allowed_tools: []` on classification/analysis nodes that don't need tools\n 11. Use `denied_tools: [Edit, Bash]` when a node should only use Write (not edit existing files)\n 12. Prefer `model: haiku` for simple classification tasks to save cost\n\n ## Output\n\n Write the complete workflow YAML to: `$ARTIFACTS_DIR/generated-workflow.yaml`\n\n Use the Write tool. Do NOT use Edit or Bash. The file must be valid YAML and follow\n all the patterns above.\n denied_tools: [Edit, Bash]\n depends_on: [extract-intent]\n\n - id: validate-yaml\n bash: |\n FILE=\"$ARTIFACTS_DIR/generated-workflow.yaml\"\n\n if [ ! -f \"$FILE\" ]; then\n echo \"ERROR: generated-workflow.yaml not found at $FILE\"\n exit 1\n fi\n\n if [ ! -s \"$FILE\" ]; then\n echo \"ERROR: generated-workflow.yaml is empty\"\n exit 1\n fi\n\n if ! grep -q \"^name:\" \"$FILE\"; then\n echo \"ERROR: missing 'name:' field\"\n exit 1\n fi\n\n if ! grep -q \"^nodes:\" \"$FILE\"; then\n echo \"ERROR: missing 'nodes:' field\"\n exit 1\n fi\n\n echo \"VALID\"\n depends_on: [generate-yaml]\n\n - id: save-or-report\n prompt: |\n You are a workflow installer. Save the generated workflow and report to the user.\n\n ## Workflow Details\n - **Name**: $extract-intent.output.workflow_name\n - **Trigger Phrases**: $extract-intent.output.trigger_phrases\n\n ## Instructions\n\n 1. Read the generated workflow from `$ARTIFACTS_DIR/generated-workflow.yaml`\n 2. Create the directory `.archon/workflows/` if it doesn't exist (use Bash: `mkdir -p .archon/workflows/`)\n 3. Save the workflow to `.archon/workflows/$extract-intent.output.workflow_name.yaml`\n Use the Write tool to write the file.\n 4. Report to the user:\n - Workflow name and file location\n - Trigger phrases that will invoke it\n - How to run it: `bun run cli workflow run $extract-intent.output.workflow_name \"your input\"`\n - How to test it: `bun run cli validate workflows $extract-intent.output.workflow_name`\n depends_on: [validate-yaml]\n", };