Skip to content

feat: runtime workflow inputs (--set KEY=VALUE / inputs API field)#1338

Open
gemmawood wants to merge 19 commits intocoleam00:devfrom
gemmawood:feature/workflow-runtime-inputs
Open

feat: runtime workflow inputs (--set KEY=VALUE / inputs API field)#1338
gemmawood wants to merge 19 commits intocoleam00:devfrom
gemmawood:feature/workflow-runtime-inputs

Conversation

@gemmawood
Copy link
Copy Markdown

@gemmawood gemmawood commented Apr 21, 2026

Summary

  • Problem: No way to parameterize workflows at runtime — teams end up with kanban-develop.yaml, kanban-develop-gpt-4o.yaml, etc., identical except for a single field.
  • Why it matters: Eliminates duplicate YAML files; a single workflow definition covers all model/provider variants.
  • What changed: inputs: declaration in workflow YAML; --set KEY=VALUE CLI flag; inputs field in POST /api/workflows/:name/run; runtime values substituted into prompts, model, and provider before execution.
  • What did NOT change: Existing workflows without inputs: are unaffected. AI-driven workflow invocation does not accept caller inputs (the AI decides what to run).

How it works

Declare inputs in your workflow YAML:
```yaml
name: my-workflow
description: Example workflow
model: $MODEL
provider: $PROVIDER
inputs:
MODEL:
description: The model to use
default: claude-sonnet-4-6
required: false
PROVIDER:
description: AI provider
default: anthropic
nodes:

  • id: do-thing
    type: task
    prompt: Run a task using $MODEL on $PROVIDER...
    ```

Override at runtime:
```bash

CLI

archon workflow run my-workflow --set MODEL=gpt-4o --set PROVIDER=openai

API

POST /api/workflows/my-workflow/run
{ "conversationId": "...", "message": "...", "inputs": { "MODEL": "gpt-4o", "PROVIDER": "openai" } }
```

Resolution order: runtime values > YAML defaults; missing required inputs fail fast with a clear error.

Changes

Schema (packages/workflows/src/schemas/workflow.ts)

  • New workflowInputSchema: { description?, default?, required? }
  • inputs: z.record(workflowInputSchema).optional() added to workflowBaseSchema

Execution (packages/workflows/src/executor.ts)

  • assertSafeInputKeys(): validates keys against /^[A-Za-z_][A-Za-z0-9_]*$/ and a reserved-name blocklist
  • resolveInputs(): merges declared defaults with runtime values, validates required inputs
  • escapeForRegex() + applyInputsToString(): safe $KEY substitution in model/provider before provider inference
  • executeWorkflow() gains optional runtimeInputs parameter
  • resolved_inputs persisted to WorkflowRun.metadata on both freshly-created and orchestrator-pre-created run paths; restored on resume, approval, and rejection

Prompt substitution (packages/workflows/src/executor-shared.ts)

  • substituteWorkflowVariables() and buildPromptWithContext() accept optional inputs map
  • Input substitution runs before built-in variable substitution

DAG executor (packages/workflows/src/dag-executor.ts)

  • inputs threaded through executeDagWorkflowexecuteNodeInternal → all node type executors, including until_bash

Orchestrator threading (packages/core/)

  • HandleMessageContext.inputs? and WorkflowRoutingContext.inputs?
  • orchestrator-agent.ts: inputs flow through handleWorkflowRunCommand and dispatchOrchestratorWorkflow; resume/approve/reject restore stored inputs from metadata
  • orchestrator.ts: background dispatch passes inputs to executeWorkflow

API (packages/server/src/routes/)

  • RunWorkflowBody gains inputs: z.record(z.string()).optional()
  • api.ts extracts and forwards inputs as extraContext.inputs

CLI (packages/cli/src/)

  • parseArgs option set: { type: 'string', multiple: true } (repeatable)
  • --set KEY=VALUE parsed and forwarded to executeWorkflow as inputs

UX Journey

Before

```
User Archon Filesystem
──── ────── ──────────
wants to run with ← no parameterization
different model maintain 3 YAML variants ────▶ kanban.yaml
kanban-gpt4o.yaml
kanban-o3.yaml
archon workflow run kanban-gpt4o (must pick the right file)
```

After

```
User Archon Filesystem
──── ────── ──────────
archon workflow run resolves inputs ─────────────▶ kanban.yaml (one file)
kanban merges defaults + --set
--set MODEL=gpt-4o substitutes $MODEL in
prompt, model:, provider:
validates required inputs
executes with effective model
```

Architecture Diagram

Before

```
CLI ──────────────────▶ Server API ──▶ Orchestrator ──▶ executeWorkflow(workflow, opts)

└──▶ DAG executor (no inputs)
```

After

```
CLI [] ──────────────▶ Server API [] ──▶ Orchestrator [] ──▶ executeWorkflow(workflow, opts, inputs) []
--set KEY=VALUE inputs field inputs context │
├──▶ resolveInputs() [+]
├──▶ assertSafeInputKeys() [+]
├──▶ applyInputsToString() [+]
├──▶ metadata.resolved_inputs [+]
└──▶ DAG executor []
└── all node executors [
]
```

Connection inventory:

From To Status Notes
CLI --set WorkflowRunOptions.inputs new Parsed in cli.ts
WorkflowRunOptions.inputs executeWorkflow new Threaded via options
RunWorkflowBody.inputs extraContext.inputs new API path
executeWorkflow resolveInputs new Merges defaults + runtime
executeWorkflow WorkflowRun.metadata.resolved_inputs new Persistence for resume
executeWorkflow DAG executor inputs new All node types
Resume/approve/reject metadata.resolved_inputs new Restore on re-entry

Label Snapshot

  • Risk: risk: low
  • Size: size: L
  • Scope: workflows, server, cli, core
  • Module: workflows:executor, workflows:dag-executor, cli:workflow, server:api, core:orchestrator

Change Metadata

  • Change type: feature
  • Primary scope: workflows

Linked Issue

Validation Evidence (required)

```bash
bun test packages/workflows/src/executor.test.ts # 28/28 pass
bun test packages/workflows/src/dag-executor.test.ts # 166/166 pass
bun --filter @archon/workflows type-check # clean
bun --filter @archon/cli type-check # clean
bun --filter @archon/server type-check # clean
bun --filter @archon/core type-check # clean
```

  • Note: Running all test files in a single bun test invocation produces 77 failures due to pre-existing global state interference — confirmed against main with the same command. Not caused by these changes.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Input key validation (assertSafeInputKeys) blocks reserved workflow variable names and enforces /^[A-Za-z_][A-Za-z0-9_]*$/ before any substitution occurs. Regex construction uses escapeForRegex to prevent metacharacter injection.

Compatibility / Migration

  • Backward compatible? Yes — inputs: field is optional; workflows without it are unaffected
  • Config/env changes? No
  • Database migration needed? No — resolved_inputs stored in the existing metadata JSON column

Human Verification (required)

  • Verified workflows without inputs: run identically to before
  • Verified --set MODEL=gpt-4o overrides $MODEL in both the prompt body and the model: top-level field
  • Verified missing required input produces a clear error before execution starts
  • Verified resolved_inputs is restored correctly on workflow resume (interrupting a run mid-execution and resuming continues with the same inputs)

What was not verified:

  • Approval/rejection flows with required inputs (covered by unit tests only)
  • Multi-user concurrent runs with different input sets (no shared state; each run carries its own resolved_inputs in metadata)

Side Effects / Blast Radius (required)

  • Affected subsystems: CLI, server API, core orchestrator, workflows executor, DAG executor (all node types)
  • Potential unintended effects: Input keys colliding with built-in variables — guarded by assertSafeInputKeys and the reserved-key blocklist
  • Guardrails: Validation at ingress (CLI parse + API schema); key pattern enforced before any substitution

Rollback Plan (required)

  • Fast rollback: git revert this PR; the feature is additive with no schema migrations
  • Feature flags: None needed — the feature is inert unless a workflow declares inputs: or a caller passes --set/inputs
  • Observable failure symptoms: Invalid workflow input key(s) error at run start; literal $KEY in prompts if a key is undeclared

Risks and Mitigations

  • Risk: Regex injection via malformed input key names (e.g. FOO.BAR, BAD[)
    • Mitigation: assertSafeInputKeys enforces /^[A-Za-z_][A-Za-z0-9_]*$/; keys with dots, hyphens, or regex metacharacters are rejected before any substitution. escapeForRegex also used defensively in applyInputsToString.
  • Risk: Runtime inputs lost across pause/resume, causing required-input validation failure on re-entry
    • Mitigation: resolved_inputs persisted to WorkflowRun.metadata on initial execution (both fresh-create and preCreatedRun paths); restored from metadata before re-running resolveInputs on resume, approve, and reject paths.

🤖 Generated with Claude Code

Allows callers to pass key=value pairs at workflow fire time that are
substituted into prompts, model, and provider fields — eliminating the
need for duplicate YAML files per model variant.

Schema:
- Add `inputs:` block to WorkflowDefinition (name → {description,
  default, required})
- Add `inputs: z.record(z.string()).optional()` to RunWorkflowBody

Execution:
- `resolveInputs()` merges declared defaults with runtime values and
  validates required inputs
- `applyInputsToString()` substitutes $KEY in model/provider before
  provider inference runs
- `substituteWorkflowVariables()` and `buildPromptWithContext()` accept
  an optional `inputs` map applied before built-in variables
- All DAG node executor functions thread `inputs` through to substitution

Threading:
- HandleMessageContext and WorkflowRoutingContext gain `inputs?`
- orchestrator-agent passes inputs through dispatchOrchestratorWorkflow
  and handleWorkflowRunCommand (AI-driven invocation path excluded)
- Background dispatch in orchestrator passes inputs to executeWorkflow

CLI:
- `archon workflow run <name> --set KEY=VALUE` (repeatable flag)
  parsed via Node.js parseArgs with `multiple: true`

API:
- POST /api/workflows/:name/run accepts `{ inputs: Record<string,string> }`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds runtime workflow inputs: CLI --set KEY=VALUE and API inputs are parsed into a Record<string,string> and threaded CLI → server → orchestrator → executor. Inputs are merged with workflow defaults, validated, persisted to run metadata, and substituted into prompts, commands, model, and provider resolution.

Changes

Cohort / File(s) Summary
CLI Input Parsing
packages/cli/src/cli.ts, packages/cli/src/commands/workflow.ts
Add repeatable global --set KEY=VALUE option; validate pairs and build inputs map; extend WorkflowRunOptions and pass inputs into workflowRunCommand.
Server API & Schema
packages/server/src/routes/api.ts, packages/server/src/routes/schemas/workflow.schemas.ts
Allow optional inputs: Record<string,string> in POST /api/workflows/:name/run body; forward as extraContext.inputs to orchestrator dispatch.
Core Orchestration & Types
packages/core/src/orchestrator/orchestrator-agent.ts, packages/core/src/orchestrator/orchestrator.ts, packages/core/src/types/index.ts
Add inputs?: Record<string,string> to context and function signatures; forward inputs from message handling, paused-run resume, and background/foreground dispatch paths.
Workflow Schema
packages/workflows/src/schemas/workflow.ts
Introduce workflowInputSchema / WorkflowInput and extend workflowBaseSchema with optional inputs map (description/default/required).
Executor & Substitution
packages/workflows/src/executor.ts, packages/workflows/src/dag-executor.ts, packages/workflows/src/executor-shared.ts
Resolve runtime inputs (merge defaults, validate required, protect reserved keys), persist resolved_inputs to run metadata, perform $<KEY> substitutions (prompts, commands, model, provider), and thread inputs through executeWorkflow → executeDagWorkflow → node executors → substituteWorkflowVariables.
Wiring & Signatures
packages/core/..., packages/workflows/..., packages/cli/...
Multiple signature and callsite updates to propagate optional inputs across orchestration and execution layers.

Sequence Diagram

sequenceDiagram
    participant User as User / API Client
    participant CLI as CLI Parser
    participant Server as Server (API)
    participant Orch as Orchestrator
    participant Exec as Executor
    participant Subst as Substitution Engine

    User->>CLI: provide --set KEY=VALUE
    User->>Server: POST /api/workflows/:name/run with inputs
    CLI->>Orch: dispatch workflow (extraContext.inputs)
    Server->>Orch: dispatch workflow (extraContext.inputs)
    Orch->>Exec: executeWorkflow(runtimeInputs)
    Exec->>Exec: resolveInputs() (merge defaults, validate required)
    Exec->>Subst: substituteWorkflowVariables(template, inputs)
    Subst-->>Exec: substituted prompts/commands/model/provider
    Exec-->>Orch: workflow execution result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I twitched my whiskers, parsed each set,

Keys and values snugly met,
I stitched them into prompts and script,
Defaults merged and checks equipped,
Now workflows hop — inputs all set! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature added: runtime workflow inputs via CLI --set flags and API inputs field.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with all required sections completed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/cli/src/commands/workflow.ts (1)

655-669: ⚠️ Potential issue | 🟠 Major

Store runtime inputs in workflow run metadata and restore them on resume/approval/rejection.

The initial workflowRunCommand passes options.inputs to executeWorkflow (line 668), which resolves them once for model/provider substitution. However, resumeWorkflowCommand, workflowApproveCommand, and workflowRejectCommand call workflowRunCommand without inputs (lines 899, 961, 1021), and the WorkflowRun schema stores only generic metadata—there is no inputs field. When executeWorkflow is re-invoked on resume, it receives undefined for runtimeInputs, causing validation to fail if the workflow has required inputs that lack defaults.

Persist the resolved inputs in run metadata during initial execution and restore them when resuming:

// In executor.ts, after resolveInputs:
const resolvedInputs = resolveInputs(workflow.inputs, runtimeInputs);
// Store in preCreatedRun metadata for later restoration
if (!preCreatedRun?.metadata) preCreatedRun.metadata = {};
preCreatedRun.metadata.resolvedInputs = resolvedInputs;

// On resume, retrieve and pass back:
const storedInputs = resumableRun?.metadata?.resolvedInputs as Record<string, string> | undefined;
await executeWorkflow(..., storedInputs);
packages/workflows/src/dag-executor.ts (1)

1873-1889: ⚠️ Potential issue | 🟡 Minor

Missing inputs in until_bash substitution.

Every other substituteWorkflowVariables call in this file now threads inputs (bash, script, loop prompt, approval on_reject). The loop.until_bash expression is the one exception and will still receive the raw $KEY literal if a user references a runtime input in their bash termination condition — quietly producing the wrong exit-code evaluation and either an infinite loop or premature completion.

🐛 Proposed fix
         const { prompt: bashPrompt } = substituteWorkflowVariables(
           loop.until_bash,
           workflowRun.id,
           workflowRun.user_message,
           artifactsDir,
           baseBranch,
           docsDir,
-          issueContext
+          issueContext,
+          undefined,
+          undefined,
+          inputs
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/dag-executor.ts` around lines 1873 - 1889, The call to
substituteWorkflowVariables for loop.until_bash omits the inputs parameter, so
runtime inputs referenced in loop.until_bash (loop.until_bash) are not
substituted and can break loop evaluation; update the
substituteWorkflowVariables invocation used for loop.until_bash to pass the
current inputs object (same way other calls do), ensuring you call
substituteWorkflowVariables(loop.until_bash, workflowRun.id,
workflowRun.user_message, artifactsDir, baseBranch, docsDir, issueContext,
inputs) before feeding the result into substituteNodeOutputRefs and
execFileAsync.
packages/core/src/orchestrator/orchestrator-agent.ts (1)

671-680: ⚠️ Potential issue | 🟠 Major

NL approval resume drops runtime inputs — required-input workflows will fail on approval.

Lines 672–680 call dispatchOrchestratorWorkflow without passing inputs, defaulting the parameter to undefined. When the executor calls resolveInputs on resume, it throws "Required workflow input \"X\" was not provided" for any required input without a default — even though the workflow was originally invoked with that input via API or --set.

The WorkflowRun record stores only approval context in metadata (e.g., approval, loop_user_input); no field exists to persist the original inputs for rehydration on resume. Compare to the web foreground resume path (line 302), which correctly passes inputs to executeWorkflow.

Store original resolved inputs in metadata during workflow creation and restore them in the NL approval resume, /workflow approve, and /workflow resume paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 671 - 680,
The resume path calls dispatchOrchestratorWorkflow without the original resolved
inputs causing resolveInputs to fail; update the workflow creation flow to
persist the resolved inputs into the WorkflowRun.metadata (e.g.,
metadata.resolved_inputs) when the run is first created, ensure they are
serialized safely, and update the NL approval resume code (the block that
references pausedRun and calls dispatchOrchestratorWorkflow), the `/workflow
approve` and `/workflow resume` handlers to read
pausedRun.metadata.resolved_inputs and pass that object as the inputs argument
into dispatchOrchestratorWorkflow (matching how executeWorkflow is called on the
web foreground path) so resolveInputs can rehydrate required values; also add
null-safe checks and any necessary type conversions when reading/writing
metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/cli.ts`:
- Line 207: The CLI flag --set is defined (set: { type: 'string', multiple: true
}) but not documented in the help text; update the function printUsage() to
include a concise entry for --set (showing it accepts multiple key=value pairs
or file references), add a short description of its purpose (runtime input
override), and include an example invocation demonstrating multiple uses (e.g.
--set key1=val1 --set key2=val2). Ensure the help output formatting matches
other flags in printUsage() so --set appears in the options list and examples.

In `@packages/workflows/src/executor-shared.ts`:
- Around line 306-313: The loop that substitutes runtime inputs (the inputs
object iterating in executor-shared.ts) builds a RegExp from key without
escaping and calls String.prototype.replace with a string replacement, which can
throw for keys containing regex metacharacters and misinterpret replacement
tokens like $& or $$. Fix by escaping the input key before constructing the
RegExp (so names like "FOO(" don't break) and call replace with a function
callback that returns the raw value (instead of a replacement string) to
preserve literal characters; update the code that references result, inputs and
the new RegExp construction accordingly.

In `@packages/workflows/src/executor.ts`:
- Around line 335-355: Node-level provider/model overrides are not being
input-substituted like workflow-level ones; applyInputsToString is used for
workflow.model/provider but resolveNodeProviderAndModel currently reads
node.model/node.provider verbatim causing literals like "$MY_MODEL" to be sent
to isModelCompatible and the SDK. Fix by threading resolvedInputs into
resolveNodeProviderAndModel and calling applyInputsToString on node.model and
node.provider before the existing logic (use the same inference flow as
executor.ts: applyInputsToString -> inferProviderFromModel -> fall back to
config.assistant), updating all references to
nodeResolvedModel/nodeResolvedProvider inside resolveNodeProviderAndModel so
compatibility checks (isModelCompatible) and returned values use the substituted
strings.
- Around line 212-222: The regex built from input keys in applyInputsToString
(and likewise in substituteWorkflowVariables) interpolates raw keys into new
RegExp which breaks or mis-matches when keys contain regex metacharacters; fix
by escaping keys before embedding them in the RegExp or, preferably, reject
invalid keys early: add key validation to workflowBaseSchema (enforce
/^[A-Za-z_][A-Za-z0-9_]*$/) and also validate/normalize CLI/API inputs at
ingestion, and update applyInputsToString and substituteWorkflowVariables to use
an escapedKey helper (or the validated key) when constructing the RegExp so new
RegExp(...) never receives unescaped metacharacters.
- Around line 180-206: reject runtime or declared input keys that collide with
engine-provided workflow variables by adding validation at the start of
resolveInputs: define a reserved set {ARGUMENTS, WORKFLOW_ID, ARTIFACTS_DIR,
BASE_BRANCH, DOCS_DIR, LOOP_USER_INPUT, REJECTION_REASON, USER_MESSAGE} and also
disallow node-output style keys containing a dot (e.g. any key with '.' or
matching '*.output'); if any key in inputsDef or runtimeInputs matches a
reserved name or contains a dot, throw a clear Error referencing resolveInputs
and listing the offending keys so callers get immediate feedback (alternatively
change the thrown Error to a warning log if you prefer non-fatal behavior, but
implement the check in resolveInputs before merging and before the required
check).

In `@packages/workflows/src/schemas/workflow.ts`:
- Around line 90-95: The inputs record currently allows arbitrary keys and can
shadow built-in variables; update the schema for inputs (the symbol inputs that
uses workflowInputSchema) to restrict keys to the uppercase identifier pattern
/^[A-Z][A-Z0-9_]*$/ and validate keys are not one of the reserved names
(WORKFLOW_ID, USER_MESSAGE, ARGUMENTS, ARTIFACTS_DIR, BASE_BRANCH, DOCS_DIR,
LOOP_USER_INPUT, REJECTION_REASON, CONTEXT, EXTERNAL_CONTEXT, ISSUE_CONTEXT);
implement this by replacing the unconstrained z.record(workflowInputSchema) with
a keyed-record validation that enforces the regex on each key and rejects any
key in the reserved set (using a refinement/check on record keys) so inputs
cannot shadow built-in variables used by
substituteWorkflowVariables/applyInputsToString.

---

Outside diff comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 671-680: The resume path calls dispatchOrchestratorWorkflow
without the original resolved inputs causing resolveInputs to fail; update the
workflow creation flow to persist the resolved inputs into the
WorkflowRun.metadata (e.g., metadata.resolved_inputs) when the run is first
created, ensure they are serialized safely, and update the NL approval resume
code (the block that references pausedRun and calls
dispatchOrchestratorWorkflow), the `/workflow approve` and `/workflow resume`
handlers to read pausedRun.metadata.resolved_inputs and pass that object as the
inputs argument into dispatchOrchestratorWorkflow (matching how executeWorkflow
is called on the web foreground path) so resolveInputs can rehydrate required
values; also add null-safe checks and any necessary type conversions when
reading/writing metadata.

In `@packages/workflows/src/dag-executor.ts`:
- Around line 1873-1889: The call to substituteWorkflowVariables for
loop.until_bash omits the inputs parameter, so runtime inputs referenced in
loop.until_bash (loop.until_bash) are not substituted and can break loop
evaluation; update the substituteWorkflowVariables invocation used for
loop.until_bash to pass the current inputs object (same way other calls do),
ensuring you call substituteWorkflowVariables(loop.until_bash, workflowRun.id,
workflowRun.user_message, artifactsDir, baseBranch, docsDir, issueContext,
inputs) before feeding the result into substituteNodeOutputRefs and
execFileAsync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e4ff86e-698a-4b20-97b5-0866fcd16672

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea3214 and 8396709.

📒 Files selected for processing (11)
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/workflow.ts
  • packages/core/src/orchestrator/orchestrator-agent.ts
  • packages/core/src/orchestrator/orchestrator.ts
  • packages/core/src/types/index.ts
  • packages/server/src/routes/api.ts
  • packages/server/src/routes/schemas/workflow.schemas.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/executor-shared.ts
  • packages/workflows/src/executor.ts
  • packages/workflows/src/schemas/workflow.ts

Comment thread packages/cli/src/cli.ts
'download-only': { type: 'boolean' },
scope: { type: 'string' },
force: { type: 'boolean' },
set: { type: 'string', multiple: true },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document --set in CLI usage.

The flag is parsed, but printUsage() does not list it or show an example, so the new runtime-input feature is hard to discover from archon help.

📝 Proposed help text update
   --json                     Output machine-readable JSON (for workflow list)
+  --set KEY=VALUE            Set a workflow runtime input; repeatable for multiple inputs
   --workflow <name>          Workflow to run for 'continue' (default: archon-assist)
   archon workflow run plan --cwd /path/to/repo "Add dark mode"
+  archon workflow run plan --set TICKET=123 --set PRIORITY=high "Add dark mode"
   archon workflow run implement --branch feature-auth "Implement auth"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/cli.ts` at line 207, The CLI flag --set is defined (set: {
type: 'string', multiple: true }) but not documented in the help text; update
the function printUsage() to include a concise entry for --set (showing it
accepts multiple key=value pairs or file references), add a short description of
its purpose (runtime input override), and include an example invocation
demonstrating multiple uses (e.g. --set key1=val1 --set key2=val2). Ensure the
help output formatting matches other flags in printUsage() so --set appears in
the options list and examples.

Comment on lines +306 to +313
// Apply runtime inputs first — substitute $KEY patterns before built-in variables.
// This allows input values to contain built-in variable references if needed.
let result = prompt;
if (inputs) {
for (const [key, value] of Object.entries(inputs)) {
result = result.replace(new RegExp(`\\$${key}(?![A-Za-z0-9_])`, 'g'), value);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its size
wc -l packages/workflows/src/executor-shared.ts

Repository: coleam00/Archon

Length of output: 104


🏁 Script executed:

# Read the section around lines 306-313 with context
sed -n '290,320p' packages/workflows/src/executor-shared.ts | cat -n

Repository: coleam00/Archon

Length of output: 1567


🏁 Script executed:

# Search for escapeRegExp function in the file
grep -n "escapeRegExp" packages/workflows/src/executor-shared.ts

Repository: coleam00/Archon

Length of output: 367


🏁 Script executed:

# Get full function context for substituteWorkflowVariables
ast-grep --pattern $'function substituteWorkflowVariables($$$) {
  $$$
}'

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Read the escapeRegExp function definition
sed -n '399,410p' packages/workflows/src/executor-shared.ts | cat -n

Repository: coleam00/Archon

Length of output: 577


🏁 Script executed:

# Check the full context and understand the implementation better
sed -n '390,440p' packages/workflows/src/executor-shared.ts | cat -n

Repository: coleam00/Archon

Length of output: 2506


🏁 Script executed:

# Verify the regex escaping concern with a test
python3 << 'EOF'
import re

# Test 1: Unescaped metacharacters in regex - should throw
try:
    pattern = re.compile(f"\\${'FOO('}(?![A-Za-z0-9_])", re.MULTILINE)
    print("Test 1 PASSED: No error with unescaped FOO(")
except Exception as e:
    print(f"Test 1 FAILED: {type(e).__name__}: {e}")

# Test 2: Replacement token behavior - does it interpret $& and $$?
test_string = "The value is $FOO"
value = "TEST_$&_VALUE"  # Contains replacement token
try:
    result = re.sub(r"\$FOO(?![A-Za-z0-9_])", value, test_string)
    print(f"Test 2: Raw string result: {result}")
    if "$&" in result:
        print("Test 2: Replacement tokens NOT interpreted (Python re.sub behaves like callback)")
except Exception as e:
    print(f"Test 2 ERROR: {e}")

# Test 3: JavaScript-like behavior - in Python str.replace doesn't support special tokens like $&
# But let's check what JavaScript does with raw replacement strings
print("\nNote: JavaScript String.prototype.replace DOES interpret special tokens:")
print("  - $& = matched substring")
print("  - $$ = literal $")
print("  - $` = substring before match")
print("  - $' = substring after match")
print("  - $n = nth capture group")
EOF

Repository: coleam00/Archon

Length of output: 474


Escape input keys and use a replacement callback.

key is interpolated into a regex without escaping, so an input name like FOO( will throw ("unterminated subpattern"). Also pass value via a callback so replacement tokens like $& (matched substring) and $$ (literal $) remain literal user input instead of being reinterpreted by JavaScript's String.prototype.replace.

🛡️ Proposed fix
   if (inputs) {
     for (const [key, value] of Object.entries(inputs)) {
-      result = result.replace(new RegExp(`\\$${key}(?![A-Za-z0-9_])`, 'g'), value);
+      const escapedKey = escapeRegExp(key);
+      result = result.replace(new RegExp(`\\$${escapedKey}(?![A-Za-z0-9_])`, 'g'), () => value);
     }
   }
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 310-310: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\$${key}(?![A-Za-z0-9_]), 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/executor-shared.ts` around lines 306 - 313, The loop
that substitutes runtime inputs (the inputs object iterating in
executor-shared.ts) builds a RegExp from key without escaping and calls
String.prototype.replace with a string replacement, which can throw for keys
containing regex metacharacters and misinterpret replacement tokens like $& or
$$. Fix by escaping the input key before constructing the RegExp (so names like
"FOO(" don't break) and call replace with a function callback that returns the
raw value (instead of a replacement string) to preserve literal characters;
update the code that references result, inputs and the new RegExp construction
accordingly.

Comment thread packages/workflows/src/executor.ts Outdated
Comment thread packages/workflows/src/executor.ts Outdated
Comment thread packages/workflows/src/executor.ts Outdated
Comment on lines +90 to +95
/**
* Named input parameters for this workflow. Values are substituted at run time
* via `$INPUT_NAME` syntax in prompts and in top-level `model`/`provider` fields.
* Callers supply values via `--set KEY=VALUE` (CLI) or `inputs` (API/SDK).
*/
inputs: z.record(workflowInputSchema).optional(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the workflow.ts schema file
cat -n packages/workflows/src/schemas/workflow.ts | head -100

Repository: coleam00/Archon

Length of output: 5029


🏁 Script executed:

# Search for substitution logic and built-in variable handling
rg -A 10 "substitut|WORKFLOW_ID|ARGUMENTS|ARTIFACTS_DIR|BASE_BRANCH" packages/workflows/src --type ts | head -150

Repository: coleam00/Archon

Length of output: 12378


🏁 Script executed:

# Search for substituteWorkflowVariables to understand the full substitution order
rg -B 5 -A 30 "substituteWorkflowVariables" packages/workflows/src/executor.ts | head -100

Repository: coleam00/Archon

Length of output: 3190


🏁 Script executed:

# Search for the actual substituteWorkflowVariables function definition
rg -B 5 -A 50 "function substituteWorkflowVariables" packages/workflows/src/executor.ts

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Search for substituteWorkflowVariables more broadly
rg "substituteWorkflowVariables" packages/workflows/src --type ts -B 3 -A 15

Repository: coleam00/Archon

Length of output: 33732


🏁 Script executed:

# Get the full implementation of substituteWorkflowVariables
sed -n '1,200p' packages/workflows/src/executor-shared.ts | head -150

Repository: coleam00/Archon

Length of output: 4729


🏁 Script executed:

# Continue reading executor-shared.ts to find substituteWorkflowVariables implementation
sed -n '200,400p' packages/workflows/src/executor-shared.ts

Repository: coleam00/Archon

Length of output: 7847


🏁 Script executed:

# Check Zod z.record() API usage - verify the two-argument form
rg "z\.record\(" packages/workflows/src --type ts -B 2 -A 2

Repository: coleam00/Archon

Length of output: 2143


🏁 Script executed:

# Verify Zod z.record() API - check Zod documentation or types
npm list zod 2>/dev/null | head -5

Repository: coleam00/Archon

Length of output: 103


🏁 Script executed:

# Check package.json for zod version
cat packages/workflows/package.json | grep -A 5 -B 5 "zod"

Repository: coleam00/Archon

Length of output: 313


🏁 Script executed:

# Check how z.record() is used in tests or other parts of the codebase
rg "z\.record\(" packages/workflows --type ts -B 1 -A 3 | head -60

Repository: coleam00/Archon

Length of output: 2139


🏁 Script executed:

# Search for workflow input tests or examples
rg "inputs:" packages/workflows --type yaml -B 2 -A 5 | head -100

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Search for workflow input tests in TypeScript
rg "inputs.*:" packages/workflows/src --type ts -B 2 -A 5 | grep -A 5 "inputs" | head -80

Repository: coleam00/Archon

Length of output: 6011


Constrain workflow input names to prevent shadowing built-in variables.

The inputs schema accepts any YAML key, allowing input names to collide with built-in workflow variables. Because runtime inputs are substituted before built-in variables (in applyInputsToStringsubstituteWorkflowVariables), an input named WORKFLOW_ID or BASE_BRANCH will shadow the documented variable and break execution semantics.

Enforce uppercase identifier syntax ([A-Z][A-Z0-9_]*) and reject reserved names: WORKFLOW_ID, USER_MESSAGE, ARGUMENTS, ARTIFACTS_DIR, BASE_BRANCH, DOCS_DIR, LOOP_USER_INPUT, REJECTION_REASON, CONTEXT, EXTERNAL_CONTEXT, ISSUE_CONTEXT.

🛡️ Proposed schema constraint
+const reservedWorkflowInputNames = new Set([
+  'WORKFLOW_ID',
+  'USER_MESSAGE',
+  'ARGUMENTS',
+  'ARTIFACTS_DIR',
+  'BASE_BRANCH',
+  'CONTEXT',
+  'EXTERNAL_CONTEXT',
+  'ISSUE_CONTEXT',
+  'DOCS_DIR',
+  'LOOP_USER_INPUT',
+  'REJECTION_REASON',
+]);
+
+const workflowInputNameSchema = z
+  .string()
+  .regex(/^[A-Z][A-Z0-9_]*$/, 'Input names must use uppercase identifier syntax')
+  .refine(name => !reservedWorkflowInputNames.has(name), {
+    message: 'Input name is reserved for a built-in workflow variable',
+  });
+
   /**
    * Named input parameters for this workflow. Values are substituted at run time
    * via `$INPUT_NAME` syntax in prompts and in top-level `model`/`provider` fields.
    * Callers supply values via `--set KEY=VALUE` (CLI) or `inputs` (API/SDK).
    */
-  inputs: z.record(workflowInputSchema).optional(),
+  inputs: z.record(workflowInputNameSchema, workflowInputSchema).optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Named input parameters for this workflow. Values are substituted at run time
* via `$INPUT_NAME` syntax in prompts and in top-level `model`/`provider` fields.
* Callers supply values via `--set KEY=VALUE` (CLI) or `inputs` (API/SDK).
*/
inputs: z.record(workflowInputSchema).optional(),
const reservedWorkflowInputNames = new Set([
'WORKFLOW_ID',
'USER_MESSAGE',
'ARGUMENTS',
'ARTIFACTS_DIR',
'BASE_BRANCH',
'CONTEXT',
'EXTERNAL_CONTEXT',
'ISSUE_CONTEXT',
'DOCS_DIR',
'LOOP_USER_INPUT',
'REJECTION_REASON',
]);
const workflowInputNameSchema = z
.string()
.regex(/^[A-Z][A-Z0-9_]*$/, 'Input names must use uppercase identifier syntax')
.refine(name => !reservedWorkflowInputNames.has(name), {
message: 'Input name is reserved for a built-in workflow variable',
});
/**
* Named input parameters for this workflow. Values are substituted at run time
* via `$INPUT_NAME` syntax in prompts and in top-level `model`/`provider` fields.
* Callers supply values via `--set KEY=VALUE` (CLI) or `inputs` (API/SDK).
*/
inputs: z.record(workflowInputNameSchema, workflowInputSchema).optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/schemas/workflow.ts` around lines 90 - 95, The inputs
record currently allows arbitrary keys and can shadow built-in variables; update
the schema for inputs (the symbol inputs that uses workflowInputSchema) to
restrict keys to the uppercase identifier pattern /^[A-Z][A-Z0-9_]*$/ and
validate keys are not one of the reserved names (WORKFLOW_ID, USER_MESSAGE,
ARGUMENTS, ARTIFACTS_DIR, BASE_BRANCH, DOCS_DIR, LOOP_USER_INPUT,
REJECTION_REASON, CONTEXT, EXTERNAL_CONTEXT, ISSUE_CONTEXT); implement this by
replacing the unconstrained z.record(workflowInputSchema) with a keyed-record
validation that enforces the regex on each key and rejects any key in the
reserved set (using a refinement/check on record keys) so inputs cannot shadow
built-in variables used by substituteWorkflowVariables/applyInputsToString.

Three issues flagged in PR review:

1. loop.until_bash substituteWorkflowVariables omitted the inputs
   parameter, so $INPUT_NAME references in until_bash conditions were
   not substituted. Pass inputs as the last argument.

2. Resolved inputs were not persisted when a workflow run was first
   created, meaning they were lost on any resume path. Store them in
   WorkflowRun.metadata.resolved_inputs at createWorkflowRun time.

3. Resume paths did not restore the original runtime inputs:
   - Natural-language approval: read pausedRun.metadata.resolved_inputs
     and pass to dispatchOrchestratorWorkflow so resolveInputs can
     rehydrate them on the resumed execution.
   - findResumableRun path: after activating the resume, fall back to
     resumableRun.metadata.resolved_inputs when no runtimeInputs were
     supplied by the caller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/workflows/src/executor.ts (1)

524-554: ⚠️ Potential issue | 🟠 Major

resolved_inputs not persisted on the orchestrator (preCreatedRun) path — breaks resume for the main dispatch path.

The metadata block at lines 527-531 only runs inside if (!workflowRun). When the orchestrator dispatches via preCreatedRun, this branch is skipped entirely, so resolved_inputs is never written to WorkflowRun.metadata. The resume-restore logic at lines 448-457 then has nothing to read back, and $INPUT_NAME substitutions are lost across pause/resume for the exact path the PR's resume fix is meant to cover (orchestrator-created runs are the common case).

Either update the pre-created run's metadata here (merge resolved_inputs into existing metadata via updateWorkflowRun), or ensure the orchestrator persists it at creation time. Also consider stripping resolved_inputs before overwriting metadata in any future write to avoid losing it.

🔧 Sketch of one approach
   if (!workflowRun) {
     // Create workflow run record
     try {
       const runMetadata: Record<string, unknown> = {};
       if (issueContext) runMetadata.github_context = issueContext;
       if (resolvedInputs && Object.keys(resolvedInputs).length > 0) {
         runMetadata.resolved_inputs = resolvedInputs;
       }
       workflowRun = await deps.store.createWorkflowRun({ ... metadata: runMetadata, ... });
     } catch (error) { ... }
+  } else if (resolvedInputs && Object.keys(resolvedInputs).length > 0
+             && !(workflowRun.metadata as Record<string, unknown> | undefined)?.resolved_inputs) {
+    // Backfill resolved_inputs onto the orchestrator-provided run so resume works.
+    try {
+      await deps.store.updateWorkflowRun(workflowRun.id, {
+        metadata: { ...(workflowRun.metadata ?? {}), resolved_inputs: resolvedInputs },
+      });
+    } catch (err) {
+      getLog().warn({ err, workflowRunId: workflowRun.id }, 'workflow.persist_resolved_inputs_failed');
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/executor.ts` around lines 524 - 554, The metadata
assignment for resolved_inputs only runs when workflowRun is newly created, so
when a preCreatedRun path supplies an existing workflowRun you must merge
resolved_inputs into that run's metadata instead of skipping it; locate the
block around workflowRun and deps.store.createWorkflowRun and, if workflowRun
exists and resolvedInputs is non-empty, call deps.store.updateWorkflowRun (or
equivalent update method) to merge/append resolved_inputs into
workflowRun.metadata (don't overwrite other metadata keys), ensuring
resolved_inputs is persisted for resume/restore logic that reads
WorkflowRun.metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/workflows/src/executor.ts`:
- Around line 448-457: The current check uses if (!runtimeInputs) which misses
an empty object ({}), so replace it with an explicit check like if
(runtimeInputs == null || Object.keys(runtimeInputs).length === 0) to ensure
resume restoration runs when the caller supplied an empty inputs map; inside
that block validate resumableRun.metadata?.resolved_inputs (currently cast as
Record<string,string>) by iterating its entries and building a filtered
Record<string,string> that includes only string values (or coerce/skip
non-strings) before passing to resolveInputs(workflow.inputs, storedInputs) and
assigning to resolvedInputs.

---

Outside diff comments:
In `@packages/workflows/src/executor.ts`:
- Around line 524-554: The metadata assignment for resolved_inputs only runs
when workflowRun is newly created, so when a preCreatedRun path supplies an
existing workflowRun you must merge resolved_inputs into that run's metadata
instead of skipping it; locate the block around workflowRun and
deps.store.createWorkflowRun and, if workflowRun exists and resolvedInputs is
non-empty, call deps.store.updateWorkflowRun (or equivalent update method) to
merge/append resolved_inputs into workflowRun.metadata (don't overwrite other
metadata keys), ensuring resolved_inputs is persisted for resume/restore logic
that reads WorkflowRun.metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01e569d2-755a-477a-a753-77766cc2ce64

📥 Commits

Reviewing files that changed from the base of the PR and between 8396709 and ab2404d.

📒 Files selected for processing (3)
  • packages/core/src/orchestrator/orchestrator-agent.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/executor.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/workflows/src/dag-executor.ts
  • packages/core/src/orchestrator/orchestrator-agent.ts

Comment thread packages/workflows/src/executor.ts Outdated
- Validate stored inputs on resume by filtering to string-only values,
  guarding against non-string or malformed metadata entries
- Persist resolved_inputs into metadata when workflowRun already exists
  via preCreatedRun, so resume-path restores work correctly if interrupted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/workflows/src/executor.ts (1)

212-220: ⚠️ Potential issue | 🟠 Major

Escape or validate input keys before building the RegExp.

Line 219 still interpolates runtime input keys directly into new RegExp(...); keys like FOO.BAR match unintended tokens, and malformed keys like BAD[ can throw during workflow execution. This was already flagged in the previous review, but remains present here.

🛡️ Proposed fix
+const REGEX_META = /[.*+?^${}()|[\]\\]/g;
+function escapeForRegex(value: string): string {
+  return value.replace(REGEX_META, '\\$&');
+}
+
 function applyInputsToString(
   value: string | undefined,
   inputs: Record<string, string>
 ): string | undefined {
   if (!value || Object.keys(inputs).length === 0) return value;
   let result = value;
   for (const [key, val] of Object.entries(inputs)) {
-    result = result.replace(new RegExp(`\\$${key}(?![A-Za-z0-9_])`, 'g'), val);
+    result = result.replace(new RegExp(`\\$${escapeForRegex(key)}(?![A-Za-z0-9_])`, 'g'), val);
   }
   return result;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/executor.ts` around lines 212 - 220, The
applyInputsToString function builds a RegExp from runtime keys without escaping,
which can throw or match incorrectly; add a small helper (e.g., escapeRegExp)
and use it when constructing the regex for each key so metacharacters in key
(like '.' or '[') are escaped before calling new RegExp, i.e., replace new
RegExp(`\\$${key}(?![A-Za-z0-9_])`, 'g') with a regex that uses
escapeRegExp(key); optionally validate keys against an allowed charset before
escaping if you want stricter input rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/workflows/src/executor.ts`:
- Around line 331-357: The code currently calls resolveInputs(...) to produce
resolvedInputs and then immediately computes effectiveModel/effectiveProvider
via applyInputsToString(workflow.model, resolvedInputs) and
applyInputsToString(workflow.provider, resolvedInputs) before the resume block
that may replace resolvedInputs from metadata.resolved_inputs; move the resume
restoration of metadata.resolved_inputs to before resolving inputs into
provider/model (i.e., restore/merge metadata.resolved_inputs into resolvedInputs
prior to calling applyInputsToString) or, if you prefer a smaller change,
recompute effectiveModel/effectiveProvider and resolvedProvider/resolvedModel
immediately after restoring metadata.resolved_inputs so that applyInputsToString
uses the resumed values for workflow.model/workflow.provider.

---

Duplicate comments:
In `@packages/workflows/src/executor.ts`:
- Around line 212-220: The applyInputsToString function builds a RegExp from
runtime keys without escaping, which can throw or match incorrectly; add a small
helper (e.g., escapeRegExp) and use it when constructing the regex for each key
so metacharacters in key (like '.' or '[') are escaped before calling new
RegExp, i.e., replace new RegExp(`\\$${key}(?![A-Za-z0-9_])`, 'g') with a regex
that uses escapeRegExp(key); optionally validate keys against an allowed charset
before escaping if you want stricter input rules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e3d08f6-70b4-4661-a174-88a08ab41e37

📥 Commits

Reviewing files that changed from the base of the PR and between ab2404d and 508a70e.

📒 Files selected for processing (1)
  • packages/workflows/src/executor.ts

Comment thread packages/workflows/src/executor.ts Outdated
…r resume

- Escape regex metacharacters in applyInputsToString so keys like FOO.BAR
  or BAD[ don't cause unexpected matches or throw at runtime
- Move effectiveModel/effectiveProvider computation to after the resume block
  so model/provider always reflects the final resolvedInputs (resume may
  restore different inputs from stored metadata)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/workflows/src/executor.ts (2)

336-340: ⚠️ Potential issue | 🟠 Major

Restore stored inputs before required validation.

Line 340 can throw for a required input before the resume block can load metadata.resolved_inputs. Also, Line 417 treats {} as caller-supplied inputs and skips restoration, losing stored overrides on resume. Defer resolveInputs(...) until after resume input selection, and treat empty runtime inputs as absent.

🐛 Suggested restructuring
-  let resolvedInputs = resolveInputs(workflow.inputs, runtimeInputs);
+  let inputsForResolution = runtimeInputs;
+  let resolvedInputs: Record<string, string>;
...
-          if (!runtimeInputs) {
+          const hasRuntimeInputs = runtimeInputs && Object.keys(runtimeInputs).length > 0;
+          if (!hasRuntimeInputs) {
             const rawStoredInputs = resumableRun.metadata?.resolved_inputs;
...
             if (storedInputs && Object.keys(storedInputs).length > 0) {
-              resolvedInputs = resolveInputs(workflow.inputs, storedInputs);
+              inputsForResolution = storedInputs;
             }
           }
...
+  resolvedInputs = resolveInputs(workflow.inputs, inputsForResolution);
+
   // Resolve provider and model now that resolvedInputs is final (resume may have updated it).

Also applies to: 415-431

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/executor.ts` around lines 336 - 340, The code calls
resolveInputs(workflow.inputs, runtimeInputs) too early, which can validate
required inputs before stored metadata.resolved_inputs is restored and also
treats an empty {} runtimeInputs as an explicit caller value; to fix, move the
call to resolveInputs until after the resume/metadata-loading block (use
metadata.resolved_inputs when present to restore prior overrides before
validation), treat an empty object runtimeInputs as "absent" (i.e., only
merge/override when runtimeInputs has keys), and ensure the final resolvedInputs
value comes from resolveInputs(workflow.inputs, mergedRuntimeInputs) after
restoration so required-input validation uses the restored inputs; changes
affect any code referencing resolvedInputs, resolveInputs, runtimeInputs, and
metadata.resolved_inputs.

180-206: ⚠️ Potential issue | 🟡 Minor

Reject input keys that collide with engine variables.

resolveInputs still accepts keys like ARGUMENTS, WORKFLOW_ID, or foo.output, which can silently shadow built-in variables/node outputs during substitution. Add a fast validation step for declared and runtime keys before merging.

🛡️ Suggested guard
+const RESERVED_WORKFLOW_INPUT_KEYS = new Set([
+  'ARGUMENTS',
+  'WORKFLOW_ID',
+  'ARTIFACTS_DIR',
+  'BASE_BRANCH',
+  'DOCS_DIR',
+  'LOOP_USER_INPUT',
+  'REJECTION_REASON',
+]);
+
+function assertSafeInputKeys(...sources: Array<Record<string, unknown> | undefined>): void {
+  const invalid = sources
+    .flatMap(source => (source ? Object.keys(source) : []))
+    .filter(
+      key =>
+        key.length === 0 ||
+        RESERVED_WORKFLOW_INPUT_KEYS.has(key) ||
+        /^[A-Za-z0-9_-]+\.output$/.test(key)
+    );
+
+  if (invalid.length > 0) {
+    throw new Error(`Invalid workflow input key(s): ${invalid.join(', ')}`);
+  }
+}
+
 function resolveInputs(
   inputsDef: Record<string, WorkflowInput> | undefined,
   runtimeInputs: Record<string, string> | undefined
 ): Record<string, string> {
+  assertSafeInputKeys(inputsDef, runtimeInputs);
   const resolved: Record<string, string> = {};

As per coding guidelines, Workflow variable substitution supports: $1, $2, $3, $ARGUMENTS, $ARTIFACTS_DIR, $WORKFLOW_ID, $BASE_BRANCH, $DOCS_DIR, $LOOP_USER_INPUT, $REJECTION_REASON.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/executor.ts` around lines 180 - 206, resolveInputs
currently allows input names that collide with engine variables or node outputs;
before merging inputsDef and runtimeInputs, add a fast validation that scans
both declared keys (inputsDef) and runtime keys (runtimeInputs) and throws an
Error if any key matches a reserved engine name (ARGUMENTS, ARTIFACTS_DIR,
WORKFLOW_ID, BASE_BRANCH, DOCS_DIR, LOOP_USER_INPUT, REJECTION_REASON) or if a
key would shadow node outputs (e.g., endsWith('.output') or contains '.output');
implement this check at the top of resolveInputs (using inputsDef and
runtimeInputs symbols) so the invalid key is rejected early with a clear error
message instead of silently shadowing built-ins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/workflows/src/executor.ts`:
- Around line 527-535: The log event name 'workflow_provider_resolved' used in
the getLog().info call should follow the repository's structured naming
convention; change the event string to 'workflow.provider_resolved' where
getLog().info is invoked (the call referencing workflow.name, resolvedProvider,
providerSource, resolvedModel) so the domain separator is included and the event
matches {domain}.{action}_{state} formatting.
- Around line 223-224: The replacement currently uses result.replace(regex, val)
which treats $ sequences in val specially; change the replacement to use a
callback so the literal input is inserted unchanged: in the loop that iterates
over Object.entries(inputs) (the variables result, inputs and escapeForRegex),
replace the second argument with a function that returns the input value (e.g.,
() => val) instead of the raw string so $-sequences are not interpreted by
String.prototype.replace.

---

Duplicate comments:
In `@packages/workflows/src/executor.ts`:
- Around line 336-340: The code calls resolveInputs(workflow.inputs,
runtimeInputs) too early, which can validate required inputs before stored
metadata.resolved_inputs is restored and also treats an empty {} runtimeInputs
as an explicit caller value; to fix, move the call to resolveInputs until after
the resume/metadata-loading block (use metadata.resolved_inputs when present to
restore prior overrides before validation), treat an empty object runtimeInputs
as "absent" (i.e., only merge/override when runtimeInputs has keys), and ensure
the final resolvedInputs value comes from resolveInputs(workflow.inputs,
mergedRuntimeInputs) after restoration so required-input validation uses the
restored inputs; changes affect any code referencing resolvedInputs,
resolveInputs, runtimeInputs, and metadata.resolved_inputs.
- Around line 180-206: resolveInputs currently allows input names that collide
with engine variables or node outputs; before merging inputsDef and
runtimeInputs, add a fast validation that scans both declared keys (inputsDef)
and runtime keys (runtimeInputs) and throws an Error if any key matches a
reserved engine name (ARGUMENTS, ARTIFACTS_DIR, WORKFLOW_ID, BASE_BRANCH,
DOCS_DIR, LOOP_USER_INPUT, REJECTION_REASON) or if a key would shadow node
outputs (e.g., endsWith('.output') or contains '.output'); implement this check
at the top of resolveInputs (using inputsDef and runtimeInputs symbols) so the
invalid key is rejected early with a clear error message instead of silently
shadowing built-ins.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81791c38-0978-436a-af77-e25269dc11e3

📥 Commits

Reviewing files that changed from the base of the PR and between 508a70e and 215aae2.

📒 Files selected for processing (1)
  • packages/workflows/src/executor.ts

Comment thread packages/workflows/src/executor.ts Outdated
Comment thread packages/workflows/src/executor.ts
…putsToString

- Use () => val callback in applyInputsToString so $-sequences in input
  values (e.g. $1) are not interpreted as backreferences by String.replace
- Add assertSafeInputKeys guard in resolveInputs to reject reserved engine
  variable names (ARGUMENTS, WORKFLOW_ID, etc.) and node output shadowing
- Move resolveInputs call to after the resume block; treat empty {} runtimeInputs
  as absent; merge storedInputsFromResume + runtimeInputs once so required-input
  validation runs with the full restored input set
- Change log event 'workflow_provider_resolved' → 'workflow.provider_resolved'
  to match the repository's domain.action_state naming convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/workflows/src/executor.ts (1)

175-193: ⚠️ Potential issue | 🟠 Major

Tighten input-key validation to match the substitution grammar.

assertSafeInputKeys still allows unsafe shapes like FOO-BAR, A[, USER_MESSAGE, and positional names. FOO-BAR can be mis-substituted because the boundary is (?![A-Za-z0-9_]), so $FOO matches the prefix of $FOO-BAR; metacharacter keys also remain risky for downstream prompt substitution.

Prefer one explicit key grammar plus a complete reserved set:

🛡️ Proposed validation tightening
 const RESERVED_WORKFLOW_INPUT_KEYS = new Set([
+  '1',
+  '2',
+  '3',
   'ARGUMENTS',
+  'USER_MESSAGE',
   'WORKFLOW_ID',
   'ARTIFACTS_DIR',
   'BASE_BRANCH',
   'DOCS_DIR',
   'LOOP_USER_INPUT',
   'REJECTION_REASON',
+  'CONTEXT',
+  'EXTERNAL_CONTEXT',
+  'ISSUE_CONTEXT',
 ]);
 
+const WORKFLOW_INPUT_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
+
 function assertSafeInputKeys(...sources: (Record<string, unknown> | undefined)[]): void {
   const invalid = sources
     .flatMap(source => (source ? Object.keys(source) : []))
     .filter(
       key =>
-        key.length === 0 ||
+        !WORKFLOW_INPUT_KEY_PATTERN.test(key) ||
         RESERVED_WORKFLOW_INPUT_KEYS.has(key) ||
-        /^[A-Za-z0-9_-]+\.output$/.test(key)
+        key.includes('.')
     );
   if (invalid.length > 0) {
-    throw new Error(`Invalid workflow input key(s): ${invalid.join(', ')}`);
+    throw new Error(
+      `Invalid workflow input key(s): ${invalid.join(', ')}. ` +
+        'Use /^[A-Za-z_][A-Za-z0-9_]*$/ and avoid reserved workflow variables.'
+    );
   }
 }
#!/bin/bash
# Description: Demonstrate why the current key grammar is unsafe and inspect downstream input substitution.
# Expected:
# - The hyphenated-key demo prints "x-BAR", showing prefix substitution.
# - Any raw key interpolation in executor-shared.ts should be fixed or made unreachable by stricter key validation.

node <<'NODE'
const REGEX_META = /[.*+?^${}()|[\]\\]/g;
const escapeForRegex = s => s.replace(REGEX_META, '\\$&');

function applyInputsToString(value, inputs) {
  let result = value;
  for (const [key, val] of Object.entries(inputs)) {
    result = result.replace(
      new RegExp(`\\$${escapeForRegex(key)}(?![A-Za-z0-9_])`, 'g'),
      () => val
    );
  }
  return result;
}

console.log(applyInputsToString('$FOO-BAR', { FOO: 'x', 'FOO-BAR': 'y' }));
NODE

rg -n -C3 'Object\.entries\(inputs\)|new RegExp\(`\\\\\$\$\{key\}|replace\(.*value\)' packages/workflows/src/executor-shared.ts packages/workflows/src/executor.ts

As per coding guidelines, workflow variable substitution supports $1, $2, $3, $ARGUMENTS, $ARTIFACTS_DIR, $WORKFLOW_ID, $BASE_BRANCH, $DOCS_DIR, $LOOP_USER_INPUT, and $REJECTION_REASON.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/executor.ts` around lines 175 - 193, The input-key
validator in assertSafeInputKeys is too permissive; tighten it to only allow
either positional numeric names (/^\d+$/) or identifier-style names
(/^[A-Za-z_][A-Za-z0-9_]*$/) and keep rejecting reserved names
(RESERVED_WORKFLOW_INPUT_KEYS) and keys matching /\.output$/; update the invalid
calculation inside assertSafeInputKeys (referencing the function
assertSafeInputKeys, the constant RESERVED_WORKFLOW_INPUT_KEYS, and the existing
/\.output$/ check) so any key that doesn't match the numeric or identifier
grammar is treated as invalid and causes the same error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/workflows/src/executor.ts`:
- Around line 175-193: The input-key validator in assertSafeInputKeys is too
permissive; tighten it to only allow either positional numeric names (/^\d+$/)
or identifier-style names (/^[A-Za-z_][A-Za-z0-9_]*$/) and keep rejecting
reserved names (RESERVED_WORKFLOW_INPUT_KEYS) and keys matching /\.output$/;
update the invalid calculation inside assertSafeInputKeys (referencing the
function assertSafeInputKeys, the constant RESERVED_WORKFLOW_INPUT_KEYS, and the
existing /\.output$/ check) so any key that doesn't match the numeric or
identifier grammar is treated as invalid and causes the same error path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 032ac1c0-e5f5-49e2-8981-3e61df1d5cad

📥 Commits

Reviewing files that changed from the base of the PR and between 215aae2 and a5169fd.

📒 Files selected for processing (1)
  • packages/workflows/src/executor.ts

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Only repository collaborators, contributors, or members can run CodeRabbit commands.

gemmawood and others added 12 commits April 21, 2026 21:28
Replace the ad-hoc empty/dot-output checks with a strict
/^[A-Za-z_][A-Za-z0-9_]*$/ pattern so hyphenated keys (FOO-BAR) and
keys with dots are rejected before they can cause prefix-match
ambiguity or node-output shadowing during substitution.

Expand RESERVED_WORKFLOW_INPUT_KEYS with positional names (1, 2, 3),
USER_MESSAGE, CONTEXT, EXTERNAL_CONTEXT, and ISSUE_CONTEXT to cover
the full set of built-in workflow variables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…be (coleam00#1359)

The pre-flight binary smoke does a bare `bun build --compile` — it
deliberately skips `scripts/build-binaries.sh` to stay fast. That means
packages/paths/src/bundled-build.ts retains its dev defaults, including
BUNDLED_IS_BINARY = false.

version.ts branches on BUNDLED_IS_BINARY: when true it returns the
embedded string; when false it calls getDevVersion(), which reads
package.json at `SCRIPT_DIR/../../../../package.json`. Inside a compiled
binary SCRIPT_DIR resolves under `$bunfs/root/`, the walk produces a CWD-
relative path that doesn't exist, and the smoke aborts with "Failed to
read version: package.json not found" — a false positive.

Hit during the 0.3.8 release attempt: the real Pi lazy-load fix was
working end-to-end; the smoke test was the only thing failing.

Use --help instead. It exercises the same module-init graph (so it still
catches the real failure modes the skill lists — Pi package.json init
crash, Bun --bytecode bugs, CJS wrapper issues, circular imports under
minify) but has no dev/binary branch, so no false positive.

Also add a longer comment block explaining why --help is preferred, so
this doesn't get "normalized" back to `version` by a future drive-by.
The brew path of /test-release runs `brew uninstall` in Phase 5 to leave the
system in its pre-test state. For operators using the dual-homebrew pattern
(renamed brew binary at `/opt/homebrew/bin/archon-stable` so it coexists with
a `bun link` dev `archon`), that uninstall wipes the Cellar dir the
`archon-stable` symlink points into → `archon-stable` becomes dangling →
`brew cleanup` sweeps it away on the next brew op. Next time the operator
wants stable, they have to manually re-run `brew-upgrade-archon`.

Fix: make the skill aware of `archon-stable` and restore it transparently.

- Phase 2 item 4: detect the `archon-stable` symlink before any brew op;
  export `ARCHON_STABLE_WAS_INSTALLED=yes` so Phase 5 knows to restore it.
  Only triggers for the brew path (curl-mac/curl-vps don't touch brew so
  they leave `archon-stable` alone).
- Phase 5 brew path: after `brew uninstall + untap`, if the flag was set,
  re-tap + re-install + rename. Verifies the restored `archon-stable`
  reports a version and warns (non-fatal) if the rename target is missing.
  Documents the tradeoff: the restored version is "whatever the tap ships
  today", not necessarily the pre-test version — usually that's what the
  operator wants (the release they just tested becomes stable) but the
  back-version-QA case requires a manual `brew-upgrade-archon` after.
- Phase 1 confirmation banner now mentions that `archon-stable` will be
  preserved so the operator isn't surprised by the reinstall during Phase 5.

No changes to curl-mac/curl-vps paths. No changes to Phase 4 test suite.
… a compiled binary (coleam00#1360)

v0.3.9 made Pi boot-safe: lazy-loading its imports meant `archon version`
no longer crashed on `@mariozechner/pi-coding-agent/dist/config.js`'s
module-init `readFileSync(getPackageJsonPath())`. That's what the
`provider-lazy-load.test.ts` regression test guards.

The fix was only half the problem though. When a Pi workflow actually
runs, sendQuery() triggers the dynamic import — and Pi's config.js
module-init fires then, hitting the exact same ENOENT on
`dirname(process.execPath)/package.json`. Discovered by running
`archon workflow run test-pi` against a locally-compiled 0.3.9 binary:

    [main] Failed: ENOENT: no such file or directory,
           open '/private/tmp/package.json'
        at readFileSync (unknown)
        at <anonymous> (/$bunfs/root/archon-providertest:184:7889)
        at init_config

Boot-safe ≠ runtime-safe. The `/test-release` run for 0.3.9 passed
because it only exercised `archon-assist` (Claude); Pi was never
actually invoked on the released binary.

Fix: before the dynamic `import('@mariozechner/pi-coding-agent')` in
sendQuery, install a PI_PACKAGE_DIR shim. Pi's config.js checks
`process.env.PI_PACKAGE_DIR` first in its `getPackageDir()` and
short-circuits the `dirname(process.execPath)` walk. We write a
minimal `{name, version, piConfig:{}}` stub to
`tmpdir()/archon-pi-shim/package.json` (idempotent — existsSync check)
and set the env var. Pi only reads `piConfig.name`, `piConfig.configDir`,
and `version` from that file, all optional, so the stub surface is
genuinely minimal.

Localized to PiProvider: no global state, no mutation of any shared
config, no upstream fork. Claude and Codex providers are unaffected
(their SDKs don't have this class of module-init side effect).

Verified end-to-end: built a compiled archon binary with this patch,
ran `archon workflow run test-pi --no-worktree` (Pi workflow with
model `anthropic/claude-haiku-4-5`), got a clean response. Before the
patch, same binary crashed at `dag_node_started` with the ENOENT above.

Regression test added: asserts `PI_PACKAGE_DIR` is set after sendQuery
hits even its fast-fail "no model" path. Together with the existing
`provider-lazy-load.test.ts` (boot-safe) this covers both halves.
… and Codex (coleam00#1361)

Both binary resolvers previously stopped at env-var + explicit config and
threw a "not found" error when neither was set. Users who followed the
upstream-recommended install flow (Anthropic's `curl install.sh` for
Claude, `npm install -g @openai/codex`) still had to manually set either
`CLAUDE_BIN_PATH` / `CODEX_BIN_PATH` or the corresponding config field
before any workflow could run.

Add a tier-N autodetect step between the explicit config tier and the
install-instructions throw. Purely additive: env and config still win
when set (precedence covered by new tests). On autodetect miss, the same
install-instructions error fires as before.

Claude probe list (verified against docs.claude.com "Uninstall Claude
Code → Native installation" section):
  - $HOME/.local/bin/claude            (mac/linux native installer)
  - $USERPROFILE\.local\bin\claude.exe (Windows native installer)

Codex probe list (verified against openai/codex README; npm global-
install puts the binary at `{npm_prefix}/bin/<name>` on POSIX,
`{npm_prefix}\<name>.cmd` on Windows):
  - $HOME/.npm-global/bin/codex   (user-set `npm config set prefix`)
  - /opt/homebrew/bin/codex       (mac arm64 with homebrew-node)
  - /usr/local/bin/codex          (mac intel / linux system node)
  - %APPDATA%\npm\codex.cmd       (Windows npm global default)
  - $HOME\.npm-global\codex.cmd   (Windows user-set prefix)

Not probed (explicit override still required):
  - Custom npm prefixes — `npm root -g` would need a subprocess per
    resolve, too much surface for a probe helper
  - `brew install --cask codex` — cask layout isn't a PATH binary
  - Manual GitHub Releases extracts — placement is user-determined
  - `~/.bun/bin/codex` — not documented in openai/codex README

Pi provider intentionally has no equivalent change: the Pi SDK is
bundled into the archon binary (no subprocess), so there's no "binary"
to resolve. Pi auth lives at `~/.pi/agent/auth.json` which the SDK
already finds by default, and the PR A shim (`PI_PACKAGE_DIR`) handles
the package-dir case via Pi's own documented escape hatch.

E2E verified: removed both config entries from ~/.archon/config.yaml,
rebuilt compiled binary, ran `archon workflow run archon-assist` and a
Codex workflow. Logs showed `source: 'autodetect'` for both, responses
returned cleanly.
…ry autodetect test

The native-installer autodetect test computed its expected path from
process.env.HOME, but the implementation uses node:os homedir(). On
Windows, HOME is typically unset (Windows uses USERPROFILE), so the
test fell back to '/Users/test' while the resolver returned the real
home dir — making the spy's path-equality check fail and breaking CI
on windows-latest.

Mirror the implementation by importing homedir() from node:os and
joining with node:path so the expected path matches the actual
platform-resolved home and separator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ver (coleam00#1365)

Reported in coleam00#1365: a user running `archon serve` with DISCORD_BOT_TOKEN
set but the "Message Content Intent" toggle disabled in the Discord
Developer Portal saw the entire server crash with `Used disallowed
intents`. Discord rejects the gateway connection (close code 4014) when
a privileged intent is requested without being enabled, and the
unguarded `await discord.start()` propagated the error all the way up,
taking the web UI down with it.

Wrap discord.start() in try/catch — log the failure with an actionable
hint (special-cased for the disallowed-intent error) and continue
running. Other adapters and the web UI come up regardless. The shutdown
handler already uses optional chaining (`discord?.stop()`) so nulling
discord after a failed start is safe.

Other adapters (Telegram, Slack, GitHub, Gitea, GitLab) have the same
unguarded-start pattern but are out of scope for this fix — addressing
them is tracked separately.

Also expanded the Discord setup docs with a caution callout that names
the exact error string and the new log event so users can grep for
both.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…0#1362)

* docs(script-nodes): add dedicated guide and teach the archon skill how to write them

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.

* fix(docs): review follow-ups for script-node guide

- 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
…nv gaps, add good-practices + troubleshooting (coleam00#1363)

* fix(skill/when): document the full `when:` operator set and compound expressions

The skill reference previously stated "operators: ==, != only" which is
materially wrong — the condition evaluator supports ==, !=, <, >, <=, >=
plus && / || compound expressions with && binding tighter than ||, plus
dot-notation JSON field access. An agent authoring a workflow from the
skill would think half the operators don't exist.

Replaces the single-sentence section with a structured reference covering:
- All six comparison operators (string and numeric modes)
- Compound expressions with precedence rules and short-circuit eval
- JSON dot notation semantics and failure modes
- The fail-closed rules in full (invalid expression, non-numeric side,
  missing field, skipped upstream)

Grounded in packages/workflows/src/condition-evaluator.ts.

* feat(skill): document Approval and Cancel node types

Approval and cancel nodes are first-class DAG node types (approval since the
workflow lifecycle work in coleam00#871, cancel as a guarded-exit primitive) but the
skill never described either one. An agent reading the skill and asked to
"add a review gate before implementation" or "stop the workflow if the input
is unsafe" would fall back to bash + exit 1, losing the proper semantics
(cancelled vs. failed, on_reject AI rework, web UI auto-resume).

Approval node coverage (references/workflow-dag.md, SKILL.md):
- Full configuration block with message, capture_response, on_reject
- The interactive: true workflow-level requirement for web UI delivery
- Approve/reject commands across all platforms (CLI, slash, natural
  language) and the capture_response → $node-id.output flow
- Ignored-fields list + the on_reject.prompt AI sub-node exception

Cancel node coverage (references/workflow-dag.md, SKILL.md):
- Single-field schema (cancel: "<reason>")
- Lifecycle: cancelled (not failed); in-flight parallel nodes stopped;
  no DAG auto-resume path
- The "cancel: vs bash-exit-1" decision rule (expected precondition miss
  vs. check itself failing)
- Two canonical patterns — upstream-classification gate, pre-expensive-step
  gate

Validation-rules list updated to enumerate approval/cancel constraints
(message non-empty, on_reject.max_attempts range 1-10, cancel reason
non-empty), plus a forward note that script: joins the mutually-exclusive
set once PR coleam00#1362 lands.

Placement in both files is after the Loop section and before the validation
section, so this commit stays additive with respect to PR coleam00#1362's Script
node insertion between Bash and Loop — rebase is clean.

* feat(skill): document workflow-level fields beyond name/provider/model

The skill's Schema section previously showed only name, description, provider,
and model at the workflow level — which is most of a stub. Agents asked to
"use the 1M-context Claude beta" or "run this under a network sandbox" or
"add a fallback model in case Opus rate-limits" had no way to discover
that any of these fields existed at the workflow level.

Adds a comprehensive Workflow-Level Fields section covering:
- Core: name, description, provider, model, interactive (with explicit
  callout that interactive: true is REQUIRED for approval/loop gates on
  web UI — a common footgun)
- Isolation: worktree.enabled for pin-on/pin-off (the only worktree field
  at workflow level; baseBranch/copyFiles/path/initSubmodules are
  config.yaml only, so a cross-reference points there)
- Claude SDK advanced: effort, thinking, fallbackModel, betas, sandbox,
  with explicit per-node-only exceptions (maxBudgetUsd, systemPrompt)
- Codex-specific: modelReasoningEffort (with note that it's NOT the same
  as Claude's effort — this has confused users), webSearchMode,
  additionalDirectories
- A complete worked example combining sandbox + approval + interactive

All fields cross-referenced against packages/workflows/src/schemas/workflow.ts
and packages/workflows/src/schemas/dag-node.ts.

* feat(skill/loop): document interactive loops and gate_message

Interactive loop nodes pause between iterations for human feedback via
/workflow approve — used by archon-piv-loop and archon-interactive-prd.
The skill's Loop Nodes section previously omitted both interactive: true
and gate_message entirely, so an agent writing a guided-refinement
workflow wouldn't know the feature exists or that gate_message is
required at parse time.

Adds:
- interactive and gate_message rows to the config table (marking
  gate_message as required when interactive: true — enforced by the
  loader's superRefine)
- A dedicated "Interactive Loops" subsection explaining the 6-step
  iterate-pause-approve-resume flow
- Explicit call-out that $LOOP_USER_INPUT populates ONLY on the first
  iteration of a resumed session — easy to miss and a common surprise
- Workflow-level interactive: true requirement for web UI delivery
  (loader warning otherwise) so the full-flow example is complete
- Note that until_bash substitution DOES shell-quote $nodeId.output
  (unlike script bodies) — called out since the audit surfaced this
  inconsistency

* fix(skill/cli): complete the CLI command reference with missing lifecycle commands

The CLI reference previously documented only list, run, cleanup, validate,
complete, version, setup, and chat — missing nearly every workflow
lifecycle command an agent needs to operate a paused, failed, or stuck
run. The interactive-workflows reference assumed these commands existed
without actually documenting them.

Adds full documentation for:
- archon workflow status — show running workflow(s)
- archon workflow approve <run-id> [comment] — resume approval gate
  (also populates $LOOP_USER_INPUT on interactive loops and the gate
  node's output when capture_response: true)
- archon workflow reject <run-id> [reason] — reject gate; cancels or
  triggers on_reject rework depending on node config
- archon workflow cancel <run-id> — terminate running/paused with
  in-flight subprocess kill
- archon workflow abandon <run-id> — mark stuck row cancelled without
  subprocess kill (for orphan-cleanup after server crashes — matches
  the coleam00#1216 precedent)
- archon workflow resume <run-id> [message] — force-resume specific
  run (auto-resume is default; this is for explicit override)
- archon workflow cleanup [days] — disk hygiene for old terminal runs
  (with explicit callout that it does NOT transition 'running' rows,
  a common confusion)
- archon workflow event emit — used inside loop prompts for state
  signalling; documented so agents don't invent their own mechanism
- archon continue <branch> [flags] [msg] — iterative-session entry
  point with --workflow and --no-context flags

Also:
- Adds --allow-env-keys flag to the `workflow run` flag table with
  audit-log context and the env-leak-gate remediation use case
- Adds an "Auto-resume without --resume" note disambiguating when
  --resume is needed vs. when auto-resume handles it
- Adds --include-closed flag to `isolation cleanup`, which was
  previously missing; converts the flag list to a structured table
- Explains the cancel/abandon distinction (live subprocess vs. orphan)

All grounded in packages/cli/src/commands/workflow.ts, continue.ts,
and isolation.ts.

* feat(skill/repo-init): add scripts/ and state/, three-path env model, per-project env injection

The repo-init reference was missing two first-class .archon/ directories
(scripts/ since v0.3.3, state/ since the workflow-state feature) and had
nothing to say about env — the coleam00#1 thing a user hits on first-run when
their repo has a .env file with API keys.

Directory tree updates:
- Adds .archon/scripts/ with the extension->runtime rule (.ts/.js -> bun,
  .py -> uv) so agents know where to put named scripts referenced by
  script: nodes.
- Adds .archon/state/ with explicit "always gitignore" callout — these
  are runtime artifacts, not source. Previously undocumented in the skill.
- Adds .archon/.env (repo-scoped Archon env) and distinguishes it from
  the target repo's top-level .env.
- Adds a "What each directory is for" list so the structure isn't just
  a tree with no narrative.

.gitignore guidance:
- state/ and .env added as must-gitignore (state/ matches CLAUDE.md and
  reference/archon-directories.md — skill was lagging).
- mcp/ demoted to conditional — gitignore only if you hardcode secrets.

New "Three-Path Env Model" section:
- ~/.archon/.env (trusted, user), <cwd>/.archon/.env (trusted, repo),
  <cwd>/.env (UNTRUSTED, target project — stripped from subprocess env).
- Precedence (override: true across archon-owned paths) and the
  observable [archon] loaded N keys / stripped K keys log lines so
  operators can verify what actually happened.
- Decision tree for where to put API keys vs. target-project env vs.
  things Archon shouldn't touch.
- Links to archon setup --scope home|project with --force for writing
  to the right file with timestamped backups.

New "Per-Project Env Injection" section:
- Documents both managed surfaces: .archon/config.yaml env: block
  (git-committed, $REF expansion) and Web UI Settings → Projects →
  Env Vars (DB-stored, never returned over API).
- Names every execution surface that receives the injected vars:
  Claude/Codex/Pi subprocess, bash: nodes, script: nodes, and direct
  codebase-scoped chat.
- Documents the env-leak gate with all 5 remediation paths so an agent
  hitting "Cannot register: env has sensitive keys" knows the options.

Grounded in CHANGELOG v0.3.7 (three-path env + setup flags), v0.3.0
(env-leak gate), and reference/security.md on the docs site.

* fix(skill/authoring-commands): correct override paths and add home-scoped commands

The file-location and discovery sections described an override layout that
does not match the actual resolver. It showed:

  .archon/commands/defaults/archon-assist.md  # Overrides the bundled

and claimed `.archon/commands/defaults/` was where repo-level overrides
lived. In fact the resolver (executor-shared.ts:152-200 + command-
validation.ts) walks `.archon/commands/` 1 level deep and uses basename
matching — putting `archon-assist.md` at the top of `.archon/commands/`
is the canonical way to override the bundled version. The `defaults/`
subfolder is a Archon-internal convention for shipping bundled defaults,
not a user-facing override pattern.

Also, home-scoped commands (`~/.archon/commands/`, shipped in v0.3.7)
were completely absent — agents authoring personal helpers wouldn't
know they could live at the user level and be shared across every repo.

Changes:
- File Location section now shows all three discovery scopes (repo,
  home, bundled) with precedence ordering and 1-level subfolder rules
- Duplicate-basename rule documented as a user error surface
- Discovery and Priority section rewritten with accurate 3-step lookup
  order — no more references to the nonexistent defaults/ override path
- Adds the Web UI "Global (~/.archon/commands/)" palette label note so
  users authoring helpers for the builder know what to expect

No code changes — this is a pure fix of stale/incorrect skill reference
material.

* feat(skill): add workflow good-practices and troubleshooting reference pages

Closes two gaps from the audit. The skill previously had zero guidance on
designing multi-node workflows (what to avoid, what to reach for first,
how to structure artifact chains) and zero guidance on where to look
when things go wrong (log paths, env-leak gate remediations, orphan-row
cleanup, resume semantics).

New references/good-practices.md (9 Good Practices + 7 Anti-Patterns):

- Use deterministic nodes (bash:/script:) for deterministic work, AI for
  reasoning — the single biggest quality lever
- output_format required whenever downstream when: reads a field — the
  most common source of "workflow silently routes wrong"
- trigger_rule: none_failed_min_one_success after conditional branches —
  the classic bug where all_success fails because a skipped when:-gated
  branch doesn't count as a success
- context: fresh requires artifacts for state passing — commands must
  explicitly "read $ARTIFACTS_DIR/..." when downstream of fresh
- Cheap models (haiku) for glue, strong for substance
- Workflow descriptions as routing affordances
- Validate (archon validate workflows) + smoke-run before shipping
- Artifact-chain-first design
- worktree.enabled: true for code-changing workflows (reversibility)
- Anti-patterns with before/after YAML examples for each (AI-for-tests,
  free-form when: matching, context: fresh without artifacts, long flat
  AI-node layers, secrets in YAML, retry on loop nodes, tiny
  max_iterations, missing workflow-level interactive:, tool-restricted
  MCP nodes)

New references/troubleshooting.md:

- Log location (~/.archon/workspaces/<owner>/<repo>/logs/<run-id>.jsonl)
  with jq recipes for common queries (last assistant message, failed
  events, full stream)
- Artifact location for cross-node handoff debugging
- 9 Common Failure Modes, each with root cause + concrete fix:
  - $BASE_BRANCH unresolvable
  - Env-leak gate (5 remediations)
  - Claude/Codex binary not found (compiled-binary-only)
  - "running" forever (AI working / orphan / idle_timeout)
  - Mid-workflow failure and auto-resume semantics
  - Approval gate missing on web UI (workflow-level interactive:)
  - MCP plugin connection noise (filtered by design)
  - Empty $nodeId.output / field access (4 causes)
- Diagnostic command cheat sheet (list, status, isolation list, validate,
  tail-log, --verbose, LOG_LEVEL=debug)
- Escalation protocol (version + validate + log tail + CHANGELOG + issue)

SKILL.md routing table now dispatches "Workflow good practices /
anti-patterns" and "Troubleshoot a failing / stuck workflow" to the new
references so an agent can find them without having to know they exist.

* docs(book): update node-types coverage from four to all seven

The book is the curated first-contact reading path (landing page → "Get
Started" → /book/). Both dag-workflows.md and quick-reference.md were
stuck on "four node types" — missing script, approval, and cancel. A user
reading the book as their first introduction would form an incomplete
mental model, then find three more node types in the reference section
later with no explanation of when they arrived.

book/dag-workflows.md:
- "four node types" → "seven node types. Exactly one mode field is
  required per node"
- Table now lists Command, Prompt, Bash, Script, Loop, Approval, Cancel
  with one-line "when to use" for each, and cross-links to the dedicated
  guide pages for Script / Loop / Approval
- New sections below the table for Script (inline + named examples with
  runtime and deps), Approval (with the interactive: true workflow-level
  note that's easy to miss), and Cancel (guarded-exit pattern) — keeping
  the existing narrative shape for Bash and Loop

book/quick-reference.md:
- Node Options table now includes script, approval, cancel rows
- agents row added (inline sub-agents, Claude-only)
- New "Script-specific fields" and "Approval-specific fields" subsections
  so the cheat-sheet is actually complete rather than pointing users
  elsewhere for the required constraints
- Retry row callout that loop nodes hard-error on retry — previously
  omitted
- bash timeout note widened to cover script timeout (same semantics)

Both files are docs-web content; the CI build on the docs-script-nodes
PR (coleam00#1362) previously validated the Starlight build path with a similar
table addition, so this should render clean.

* fix(skill/cli): remove nonexistent \`archon workflow cancel\`, fix workflow status jq recipe

Two accuracy issues from the PR code-reviewer (comment 4311243858).

C1: \`archon workflow cancel <run-id>\` does NOT exist as a CLI subcommand.
The switch at packages/cli/src/cli.ts:318-485 dispatches on list / run /
status / resume / abandon / approve / reject / cleanup / event — running
\`archon workflow cancel\` hits the default case and exits with "Unknown
workflow subcommand: cancel" (cli.ts:478-484). Active cancellation is
only available via:
  - /workflow cancel <run-id> chat slash command (all platforms)
  - Cancel button on the Web UI dashboard
  - POST /api/workflows/runs/{runId}/cancel REST endpoint

cli-commands.md: removed the \`### archon workflow cancel <run-id>\`
subsection; kept the \`abandon\` subsection but made it explicit that
abandon does NOT kill a subprocess. Added a call-out box at the bottom
of the abandon section explaining where to go for actual cancellation.

troubleshooting.md "running forever" section: split the original
cancel-vs-abandon advice into three bullets — Web UI / CLI abandon (for
orphans, no subprocess kill) / chat \`/workflow cancel\` (for live runs
that need interruption). Added an explicit "there is no archon workflow
cancel CLI subcommand" parenthetical since the wrong command was being
suggested in flow.

I1: the \`archon workflow list --json\` diagnostic used an incorrect jq
filter. workflow list's --json output (workflow.ts:185-219) has shape
{ workflows: [{ name, description, provider?, model?, ... }], errors: [...] }
with no \`runs\` field — \`jq '.workflows[] | select(.runs)'\` returns empty
unconditionally. Replaced with \`archon workflow status --json | jq '.runs[]'\`,
which matches the actual shape of workflowStatusCommand at
workflow.ts:852+ ({ runs: WorkflowRun[] }). Also tightened the narration
to distinguish JSON from human-readable status output.

No change to the commit history in this PR — these are follow-up fixes
to claims I introduced in earlier commits of this branch (f10b989 for
C1, 66d2b86 for I1).

* fix(skill): remove env-leak gate references (feature was removed in provider extraction)

C2 from the PR code-reviewer (comment 4311243858). The pre-spawn env-leak
gate was removed from the codebase during the provider-extraction refactor
— see TODO(coleam00#1135) at packages/providers/src/claude/provider.ts:908. Zero
hits for --allow-env-keys / allowEnvKeys / allow_env_keys / allow_target_repo_keys
across packages/. The CLI's parseArgs (cli.ts:182-208) has no
--allow-env-keys option, and because parseArgs uses strict: false, an
unknown --allow-env-keys would be silently ignored rather than error.

What remains accurate and is NOT touched:
- Three-Path Env Model section (user/repo archon-owned envs are loaded;
  target repo <cwd>/.env keys are stripped from process.env at boot)
  still correctly describes current behavior, grounded in
  packages/paths/src/strip-cwd-env.ts + env-integration.test.ts
- Per-Project Env Injection section (Option 1: .archon/config.yaml env:
  block; Option 2: Web UI Settings → Projects → Env Vars) is unchanged —
  both remain the sanctioned way to get env vars into subprocesses

Removed claims (all three files):
- cli-commands.md: --allow-env-keys flag row in the workflow run flags
  table
- repo-init.md: the "Env-leak gate" subsection at the end of Per-Project
  Env Injection listing 5 remediations (all of which reference UI/CLI/
  config surfaces that don't exist). Replaced with a succinct callout
  that explains the actual current behavior — target repo .env keys are
  stripped, workflows that need those values should use managed
  injection — so the reader still gets the "where to put my env vars"
  answer
- troubleshooting.md: the "Cannot register: codebase has sensitive env
  keys" section (error message that can no longer be emitted)

If the env-leak gate is ever resurrected per TODO(coleam00#1135), the docs can be
re-added then. The CHANGELOG v0.3.0 entry describing the gate is a
historical record of past behavior and does not need to be rewritten.

* fix(skill/troubleshooting): correct JSONL event type names and field name

C3 from the PR code-reviewer (comment 4311243858). The troubleshooting
reference's event-types table used _started / _completed / _failed
suffixes, but packages/workflows/src/logger.ts:19-30 shows the actual
WorkflowEvent.type enum is:

  workflow_start | workflow_complete | workflow_error |
  assistant | tool | validation |
  node_start | node_complete | node_skipped | node_error

The second jq recipe also queried `.event` but the discriminator is `.type`.

Fixes:
- Event table: renamed columns (_started → _start, _completed → _complete,
  _failed → _error). Explicitly called out the field name as `type` so the
  reader knows what jq selector to use
- Replaced the "tool_use / tool_result" row with a single `tool` row and
  listed its actual payload fields (tool_name, tool_input, duration_ms,
  tokens) — tool_use/tool_result are SDK message kinds that appear within
  the AI stream, not top-level log event types
- Added a `validation` row (was missing; it's emitted by workflow-level
  validation calls with `check` and `result` fields)
- Removed `retry_attempt` row — this event type is not emitted to the
  JSONL file. Retry bookkeeping goes through pino logs, not the workflow
  log file
- Added an explicit callout that loop_iteration_started /
  loop_iteration_completed (and other emitter-only events) go through
  the workflow event emitter + DB workflow_events table, NOT the JSONL
  file. Pointed readers to the DB or Web UI for loop-level detail. This
  distinguishes the two parallel event systems — easy to conflate
  (store.ts:11-17 uses _started/_completed/_failed for the DB side,
  logger.ts uses _start/_complete/_error for JSONL)
- Fixed the "all failed events" jq recipe: .event → .type and _failed → _error
- Minor cleanup: the inline "tool_use events" mention in the "running
  forever" section said the wrong event name — updated to "tool or
  assistant events in the tail"

Grounded in packages/workflows/src/logger.ts (canonical JSONL event
shape) and packages/workflows/src/store.ts (the parallel DB event
naming, which the reviewer correctly flagged as different and worth
keeping distinct).

* fix(skill): two stragglers from the code-reviewer audit

Cleanup of two references that slipped through the earlier C1 and C3 fixes:

- references/troubleshooting.md:126: \`node_failed\` → \`node_error\`
  (the "Node output is empty" diagnostics section references the JSONL
  log, which uses the logger.ts enum — not the DB workflow_events table
  which does use \`node_failed\`). The C3 fix corrected the event table
  and one jq recipe but missed this inline mention.

- references/interactive-workflows.md:106: removed \`archon workflow
  cancel <run-id>\` (nonexistent CLI subcommand) from the
  troubleshooting bullet. This was pre-existing before the hardening
  PR but fell within the C1 remediation scope. Replaced with the
  correct triage: reject (approval gate only) vs abandon (orphan
  cleanup, no subprocess kill) vs chat /workflow cancel (actual
  subprocess termination).

Grounded in the same sources as the earlier C1/C3 commits:
packages/cli/src/cli.ts:318-485 (no cancel case) and
packages/workflows/src/logger.ts:19-30 (JSONL type enum).

* feat(skill): point to archon.diy as the canonical docs source

The skill had no reference to archon.diy (the live docs site built from
packages/docs-web/). Several reference files said "see the docs site"
without naming the URL, leaving the agent to guess or grep the repo for
the hostname. An agent with the skill loaded should know that when the
distilled reference pages don't cover a case, the full canonical docs
are one WebFetch away.

SKILL.md: new "Richer Context: archon.diy" section between Routing and
Running Workflows. Covers:
- When to reach for the live docs (longer examples, tutorial framing,
  features the skill only mentions in passing, "where's that
  documented?" user questions)
- URL map — 13 starting points covering getting-started, book (tutorial
  series), guides/ (authoring + per-node-type + per-node-feature),
  reference/ (variables, CLI, security, architecture, configuration,
  troubleshooting), adapters/, deployment/
- Precedence: skill refs first (context-cheap, tuned for agents), docs
  site as escalation. Prevents agents defaulting to WebFetch when a
  local skill ref already covers the answer

Also upgrades the 5 existing generic "docs site" mentions across
reference files to concrete archon.diy URLs with anchor fragments where
helpful:
- good-practices.md: Inline sub-agents pattern → archon.diy/guides/
  authoring-workflows/#inline-sub-agents
- troubleshooting.md: "Install page on the docs site" → archon.diy/
  getting-started/installation/
- workflow-dag.md: "Workflow Description Best Practices" → anchor link;
  sandbox schema reference → archon.diy/guides/authoring-workflows/
  #claude-sdk-advanced-options
- repo-init.md: Security Model reference → archon.diy/reference/
  security/#target-repo-env-isolation (deep-link into the section that
  covers the <cwd>/.env strip behavior)

URL source of truth: astro.config.mjs:5 (site: 'https://archon.diy').
URL structure mirrors packages/docs-web/src/content/docs/<section>/
<page>.md — verified by the 62 pages the docs build produces.
…#1395)

Anthropic's Opus 4.7 landed 2026-04-16; on the Anthropic API, opus /
opus[1m] now resolve to 4.7 with a 1M context window at standard
pricing. Using the alias instead of the hard-pinned claude-opus-4-6[1m]
lets bundled default workflows auto-track the recommended Opus version.

No explicit effort is set, so nodes inherit the per-model default
(xhigh on 4.7, high on 4.6).
@gemmawood
Copy link
Copy Markdown
Author

Rebased/merged latest dev and addressed the review feedback. This should be ready for maintainer review when convenient.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

@gemmawood this PR appears to fully address #1127. Consider adding Closes #1127 to the PR body so the issue auto-closes on merge.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Hi @gemmawood — thanks for opening this PR.

This repository uses a PR template at .github/pull_request_template.md with several required sections. A few of them appear to be empty or placeholder here:

  • UX Journey
  • Architecture Diagram
  • Label Snapshot
  • Change Metadata
  • Security Impact
  • Compatibility / Migration
  • Human Verification
  • Side Effects / Blast Radius
  • Rollback Plan
  • Risks and Mitigations

Could you fill those out (even briefly)? The template helps reviewers understand scope, risk, and rollback — it speeds up review significantly.

If a section genuinely doesn't apply, just write "N/A" in it rather than leaving it blank.

@gemmawood
Copy link
Copy Markdown
Author

Hi @Wirasm — thanks for the review. I've filled out all the template sections in the PR description (UX Journey, Architecture Diagram, Label Snapshot, Change Metadata, Linked Issue with Closes #1127, Security Impact, Compatibility, Human Verification, Side Effects/Blast Radius, Rollback Plan, and Risks and Mitigations). Happy to expand anything further.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Review Summary

Verdict: minor-fixes-needed

Your PR adds a well-structured runtime workflow inputs feature (--set KEY=VALUE CLI, inputs API field, inputs: YAML block, and $INPUT_NAME substitution in prompts and model/provider). The implementation is type-safe and backward-compatible. However, two new private functions (resolveInputs, applyInputsToString) have zero test coverage, and four new public interfaces lack documentation.


Blocking issues

  • packages/workflows/src/executor.tsresolveInputs() has no tests. This function merges workflow defaults with runtime inputs, validates required inputs, and checks key safety. Add unit tests covering: default value application, runtime override, required: true throwing when unsatisfied, invalid key pattern rejection (e.g., foo-bar), and reserved key rejection (e.g., WORKFLOW_ID).

  • packages/workflows/src/executor.tsapplyInputsToString() has no tests. This function substitutes $KEY patterns in workflow model/provider fields. Add unit tests for: $INPUT_NAME substitution in strings, unchanged behavior with empty inputs map, and word-boundary matching (e.g., $FOO does not match inside $FOOBAR).

  • packages/docs-web/src/content/docs/reference/cli.md--set flag undocumented. Add a row to the workflow run flags table: --set <KEY=VALUE> — "Supply runtime input values for workflows that declare an inputs: block. May be specified multiple times."

  • packages/docs-web/src/content/docs/reference/api.mdinputs field undocumented. Document inputs?: Record<string, string> as an optional request body field for POST /api/workflows/run.


Suggested fixes

  • Add inputs: YAML block docs to packages/docs-web/src/content/docs/guides/authoring-workflows.md — document the top-level inputs field with description, default, and required sub-fields. Include a minimal worked example.

  • Add $INPUT_NAME variable docs to packages/docs-web/src/content/docs/reference/variables.md — add to the workflow variables table and update the substitution order section to note that $INPUT_NAME is resolved first (pre-processing), so input values may contain $USER_MESSAGE etc.

  • substituteWorkflowVariables (executor-shared.ts) — inputs parameter untested. Add a test to variable-substitution.test.ts covering $INPUT_NAME substitution before built-in variables.

  • CLI --set flag parsing untested. Add CLI integration tests for --set foo=bar --set baz=qux{ foo: 'bar', baz: 'qux' }, and error cases for --set foo (missing =) and --set =value (empty key).

  • Resume path with stored inputs untested (orchestrator-agent.ts around lines 670–682). Add test verifying pausedRun.metadata.resolved_inputs is restored and merged on resume.


Minor / nice-to-have

  • Remove verbose JSDoc in schemas/workflow.ts (lines 52–57, 89–91, 205–211). These multi-paragraph comments describe WHAT the fields do — the Zod schema already documents intent through field names and types. The executor-shared.ts variable substitution list is the canonical reference for feature scope.

  • Add CHANGELOG.md entry. The [Unreleased] section is empty — add an ### Added block for the new inputs: workflow block, --set CLI flag, and API inputs field.

  • Add authoring guide usage example. A minimal workflow example showing inputs: { reviewer: { required: true }, scope: { default: "all" } } with --set reviewer=alice --set scope=api would help users understand the pattern.

  • WorkflowInput schema tests (schemas.test.ts): cover description, default, and required sub-fields.

  • runWorkflowBodySchema tests (workflow.schemas.test.ts): verify inputs?: Record<string, string> acceptance (valid record, absent is valid).


Compliments

  • The executor-shared.ts comment // parentConversationId — enables approve/reject auto-resume is a good example of a WHY comment that clarifies non-obvious intent. The executor.ts ordering constraints comment (lines 451–456) is also valuable — just consider trimming it to two lines.

  • The implementation is cleanly layered: CLI → API → orchestrator → executor → DAG node, with clear separation of parsing, validation, substitution, and persistence concerns. Resume behavior with resolved_inputs stored in metadata is a thoughtful touch.


Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: comment-quality, test-coverage, docs-impact.

…g, assertSafeInputKeys; document --set and inputs: block

- Export three private helpers so they can be tested directly
- Add 28 unit tests covering defaults, runtime overrides, required validation,
  invalid/reserved key rejection, word-boundary matching, and multi-key substitution
- Add substituteWorkflowVariables tests for runtime input substitution
- Document --set flag in cli.md reference table
- Document inputs request body field in api.md with example
- Add Runtime Inputs section to authoring-workflows.md with full worked example
- Add $INPUT_NAME to variables.md reference table and substitution order

Addresses reviewer feedback requesting test coverage for the new input-resolution
functions and documentation for the --set CLI flag and inputs API field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemmawood
Copy link
Copy Markdown
Author

Hi @Wirasm — addressed all the blocking and suggested items:

Blocking:

  • Exported resolveInputs, applyInputsToString, and assertSafeInputKeys so they can be tested directly, then added 28 unit tests covering: default value application, runtime override, required-input validation, invalid key patterns (hyphen, dot, digit-start), reserved key rejection (WORKFLOW_ID, USER_MESSAGE, ARGUMENTS, etc.), $KEY substitution, empty inputs map, and word-boundary matching ($MODEL not matching $MODELX)
  • Added two substituteWorkflowVariables tests in executor-shared.test.ts covering runtime input substitution alongside built-in variables, and undeclared keys being left untouched
  • Added --set <KEY=VALUE> row to the workflow run flags table in cli.md
  • Documented inputs?: Record<string, string> in api.md under Run a Workflow, with a request body field table and a curl example

Suggested:

  • Added a Runtime Inputs section to authoring-workflows.md with the full inputs: schema and worked examples for both CLI and API
  • Added $INPUT_NAME to the variables.md reference table, substitution order (step 1, before workflow variables), and availability-by-context table

Minor:

  • Removed the verbose multi-paragraph JSDoc blocks from schemas/workflow.ts (left the type names and brief descriptions intact)

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Thanks for the thorough work here, @gemmawood — the inputs: design is well thought out, with sensible defaults/required validation and clean YAML/CLI/API parity.

Before I review the diff in detail, could you tell me a bit about the concrete use case driving this? Specifically:

  • How many workflows in your fork (or in the projects you maintain) are currently duplicated solely for model/provider variants? inputs: shines when that pattern repeats — I want to make sure we're solving a pain that's actually present.
  • Are the inputs you need limited to model/provider, or do you have workflows that parameterize on prompt fragments / paths / other strings?

I ask because there's a related, narrower primitive in flight at #1433--provider <p> and --model <m> flags on archon workflow run, with HTTP API parity. It works on any workflow without requiring the YAML to declare an inputs: block. Resolution: per-node YAML > CLI flag > workflow YAML > .archon/config.yaml > SDK default.

If your pain is specifically "I want to run this workflow on a different model for one invocation", #1433 covers that with much smaller surface area (no new YAML schema, no $VAR substitution machinery). If your pain is broader — "my workflows are duplicated because of model variants and I want a single source of truth" — then inputs: is genuinely needed and we should keep going with this PR.

A secondary concern with the current design: $MODEL / $PROVIDER substitution from inputs: overlaps with Archon's existing variable namespace ($nodeId.output, $ARTIFACTS_DIR, $BASE_BRANCH, $ARGUMENTS, $LOOP_USER_INPUT, $REJECTION_REASON). New readers would have to disambiguate "is this $X a node ref or an input?" Worth thinking about whether inputs should use a distinct sigil (e.g. ${X}) or a different mechanism (e.g. resolved at loader time, not via inline substitution).

Read #1433 and let me know which use case matches yours — happy to merge whichever solves your real problem, or both if they genuinely cover different angles.

…uts field

Trims multi-line block comments to no comments — the field names and types
are self-documenting; the full explanation now lives in authoring-workflows.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemmawood
Copy link
Copy Markdown
Author

Concrete use case: Yes, the duplication is real and present right now. I maintain three kanban-develop variants (kanban-develop.yaml, kanban-develop-gpt-5.4.yaml, kanban-develop-gpt-5.5.yaml) that are byte-for-byte identical except for name:, model:, and provider:. Every time I tweak the workflow logic, I have to update all three.

Re #1433: If --model/--provider flags land and cover that case, I'm happy to narrow this PR to the inputs: machinery only (i.e., keep the YAML schema, substitution in prompts, --set, and API inputs field, but remove any special-casing for model/provider resolution). The value inputs: adds beyond --model/--provider is:

  • Parameterising arbitrary strings (ticket IDs, paths, prompt fragments) without YAML changes
  • Documenting which parameters are expected and required, so callers know what the workflow needs
  • API parity — the inputs field on the HTTP endpoint, not just CLI flags
  • Fail-fast required-input validation before execution starts

If you'd rather land #1433 first and have this PR build on top of it (removing $MODEL/$PROVIDER from the documented use cases), I can rebase on that.

Re namespace collision: Fair concern. The current design leans on assertSafeInputKeys to prevent collision — input keys matching /^[A-Za-z_][A-Za-z0-9_]*$/ can't start with a digit (so $1/$2/$3 can't be declared), and the reserved-key blocklist covers all current built-in names. The substitution runs first so there's no ambiguity at runtime. That said, if the project wants a distinct sigil (${X} or {{X}}), I'm open to changing it — the machinery is isolated in applyInputsToString and substituteWorkflowVariables. What's your preference?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(workflows): runtime workflow variables — allow users to supply model and other parameters at dispatch time

3 participants