Skip to content

Release 0.3.11#1653

Merged
Wirasm merged 81 commits into
mainfrom
dev
May 12, 2026
Merged

Release 0.3.11#1653
Wirasm merged 81 commits into
mainfrom
dev

Conversation

@Wirasm

@Wirasm Wirasm commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Release 0.3.11

Workflow marketplace, expanded setup wizard, and broad Pi/workflow engine fixes.

Added

Changed

Fixed


Merging this PR releases 0.3.11 to main.

github-actions Bot and others added 30 commits April 22, 2026 11:26
…be (#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 (#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 (#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 (#1365)

Reported in #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>
* 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 (#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 #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 #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 #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 #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 #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 (#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(#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(#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.
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).
* fix(workflow): migrate piv-loop plan handoff to $ARTIFACTS_DIR (#1380)

The create-plan node used a relative path (.claude/archon/plans/{slug}.plan.md)
that the AI agent would sometimes write to a different location, breaking all
downstream nodes that glob for the plan file. Migrated all plan/progress file
references to $ARTIFACTS_DIR/plan.md and $ARTIFACTS_DIR/progress.txt, matching
the pattern used by archon-fix-github-issue and other workflows.

Changes:
- Replace slug-based plan path with $ARTIFACTS_DIR/plan.md in create-plan node
- Replace ls -t glob discovery with direct $ARTIFACTS_DIR/plan.md reads in
  refine-plan, code-review, and fix-feedback nodes
- Replace empty-string guard with file-existence check in implement-setup bash
- Migrate progress.txt references in implement loop to $ARTIFACTS_DIR/
- Add explicit plan/progress paths in finalize node
- Regenerated bundled-defaults.generated.ts

Fixes #1380

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(workflow): address review findings in archon-piv-loop

- Rename 'Step 2: Write the Plan' to 'Step 2: Plan File Location' to
  eliminate the duplicate heading that collided with Step 3's identical
  title in the create-plan node
- Guard implement-setup against a 0-task plan file: exit 1 with a
  clear error when no '### Task N:' sections are found, preventing a
  silent no-op implement loop
- Remove 2>/dev/null from code-review commit so pre-commit hook failures
  and other stderr are visible to the agent instead of silently swallowed
- Replace '|| true' on git push in finalize with an explicit WARNING echo
  so push failures (auth, upstream conflict, no remote) surface to the
  agent rather than being silently ignored
- Regenerate bundled-defaults.generated.ts

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

* chore(workflows): regenerate bundled defaults to match opus[1m] alias

The bundle was stale relative to the YAML sources after #1395 merged —
check:bundled was failing CI. Regenerated; no YAML edits.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cutor (#1403)

PIV Task 1: Adds three new tests in a dedicated describe block
'executeDagWorkflow -- final status derivation' covering the anyFailed
branch (dag-executor.ts ~line 2956) that previously had no direct test:
- one success + one independent failure calls failWorkflowRun (not completeWorkflowRun)
- multiple successes + one failure calls failWorkflowRun (not completeWorkflowRun)
- trigger_rule: none_failed skips dependent node but anyFailed still marks run failed

Fixes #1381.
New reference for the archon skill: a single-glance lookup of which
parameter works on which node type, an intent-based "how do I..." table,
a consolidated silent-failure catalog, and an inline agents: section
(previously only referenced via archon.diy).

Purpose is complementary, not duplicative:
- workflow-dag.md remains the authoring guide
- dag-advanced.md remains the hooks/MCP/skills/retry deep-dive
- good-practices.md remains the patterns and anti-patterns
- parameter-matrix.md is the grep-this-first lookup when you know the
  outcome you want but not which field gets you there

Also registers the new reference in SKILL.md routing table.
Add explicit references to .github/PULL_REQUEST_TEMPLATE.md in both
CONTRIBUTING.md and CLAUDE.md, plus a reminder to link issues with
Closes/Fixes/Resolves so they auto-close on merge. Repo-triage runs
were flagging dozens of partially-filled or unlinked PRs each cycle.
…riage (#1428)

* feat(workflows): add maintainer-standup workflow for daily PR/issue triage

Daily morning briefing that pulls origin/dev, triages all open PRs and assigned
issues against direction.md, and surfaces progress vs. the previous run. Designed
for live-checkout use (worktree.enabled: false) so it can read its own state.

Layout under .archon/maintainer-standup/:
  - direction.md (committed) — project north-star: what Archon IS / IS NOT.
    Drives PR P4 polite-decline classification with cited clauses.
  - README.md / profile.md.example — setup docs and template for new maintainers.
  - profile.md, state.json, briefs/YYYY-MM-DD.md — gitignored, per-maintainer.

Engine:
  - 3 parallel gather scripts in .archon/scripts/maintainer-standup-*.ts
    (git-status, gh-data, read-context) — bun runtime, JSON stdout.
  - Synthesis node: command file with output_format schema for
    { brief_markdown, next_state }.
  - Persist node: tiny inline bun script writes both to disk.

Run-to-run continuity: state.json carries observed_prs/issues snapshots, so the
next run can detect what merged, what closed, what the maintainer shipped, and
which carry-over items aged past N days.

Also adds .archon/** to the ESLint global ignore list (matches the existing
.claude/skills/** pattern) since .archon/ is user content and not part of any
tsconfig project.

* fix(maintainer-standup): address CodeRabbit review on #1428

- gh-data: bump --limit 100 → 1000 on all_open_prs and warn loudly when
  the cap is hit; preserves the observed_prs invariant the next-run
  "resolved since last run" diff depends on. (CodeRabbit critical)
- maintainer-standup.md: clarify P1 CI signal — the gathered payload only
  carries mergeStateStatus, not statusCheckRollup; for borderline P1s,
  drill in via `gh pr checks <n>`. (CodeRabbit minor)
- workflow.yaml persist: write briefs under local YYYY-MM-DD (sv-SE
  locale) instead of UTC ISO date, so an evening run doesn't file
  tomorrow's brief and break recent_briefs lookups. (CodeRabbit minor)
- workflow.yaml persist: wrap state/brief writes in try/catch; on
  failure dump brief_markdown and next_state to stderr so a 5-minute
  Sonnet synthesis isn't lost to a transient disk error. (CodeRabbit minor)
- gh-data + git-status: switch from execSync (shell-string) to
  execFileSync (argv array) for git/gh invocations. Defense-in-depth
  against shell metacharacters in values that pass through (esp. the
  gh_handle from profile.md). (CodeRabbit nitpick)
Add optional `tags: string[]` to `workflowBaseSchema`. Explicit values take precedence over keyword inference; `tags: []` suppresses inference end-to-end; omitting the field falls back to inference (backwards compatible). Non-array values warn-and-ignore matching the sibling `worktree`/`additionalDirectories` patterns.
…ows under maintainer/ (#1430)

* feat(workflows): add maintainer-review-pr and group maintainer workflows under .archon/workflows/maintainer/

Adds the maintainer-review-pr workflow — a Pi/Minimax-based PR triage
flow that gates on direction alignment, scope focus, and PR-template
quality before doing any deep review. If the gate clears, runs the
five review aspects (code/error-handling/test-coverage/comment-quality/
docs-impact) as parallel Archon nodes and auto-posts a synthesized
review comment. If the gate fails (direction conflict, multiple
concerns, sprawling scope), drafts a polite-decline comment and pauses
for the maintainer's approval before posting.

Reorganizes the existing maintainer-standup workflow into the same
subfolder so all maintainer-facing workflows live together. Subfolder
grouping is supported by the workflow loader (1 level deep, resolution
by filename).

What lands:

- .archon/workflows/maintainer/maintainer-standup.yaml (moved from
  .archon/workflows/maintainer-standup.yaml)
- .archon/workflows/maintainer/maintainer-review-pr.yaml (new)
- .archon/commands/maintainer-review-{gate,code-review,error-handling,
  test-coverage,comment-quality,docs-impact,synthesize,report}.md (new,
  Pi-tuned variants of the existing review-agent commands so they avoid
  Claude-only Task / sub-agent patterns)

Pi/Minimax integration:

- Uses provider: pi, model: minimax/MiniMax-M2.7 — verified via the
  e2e-minimax-smoke test that Pi correctly routes to Minimax (session
  jsonl confirms provider=minimax) and that Pi's best-effort
  output_format parser handles the gate's nested schema.
- Two test runs landed real comments: a direction-decline on PR #1335
  and a deep-review on PR #1369. Both were posted to GitHub via the
  workflow's gh pr comment node.

* chore(workflows): also group repo-triage under .archon/workflows/maintainer/

repo-triage is the third maintainer-facing workflow alongside maintainer-standup and maintainer-review-pr; group it in the same subfolder for consistency. Subfolder resolution is by filename so the workflow name is unchanged.
…r unmapped providers (#1284)

Closes #1096.

- Switch Pi provider model lookup from pi-ai's getModel() (static catalog
  only) to ModelRegistry.create(authStorage).find() so user-configured
  custom models in ~/.pi/agent/models.json (LM Studio, ollama, llamacpp,
  custom OpenAI-compatible endpoints) are discoverable.
- Remove the local lookupPiModel helper.
- For env-var-mapped providers (anthropic, openai, etc.) still throw
  with a pi /login hint when credentials are missing. For unmapped
  providers, log pi.auth_missing at info and continue so local models
  that don't need credentials work without ceremony.
- Surface modelRegistry.getError() in the not-found message and emit
  pi.model_not_found so users debugging custom-provider configs see the
  real cause (e.g. missing baseUrl in models.json).
- Guard AuthStorage.create() and ModelRegistry.create() with try/catch
  so a malformed ~/.pi/agent/auth.json surfaces with Pi-framed context
  instead of a raw SDK stack trace.
- Document the credential-free path for local providers in ai-assistants.md.

Co-authored-by: Matt Chapman <Matt@NinjitsuWeb.com>
…add e2e-minimax-smoke (#1431)

* chore(workflows): group all smoke-test workflows under .archon/workflows/test-workflows/

Move the 7 existing e2e-*.yaml smoke tests plus the new e2e-minimax-smoke
test into a dedicated subfolder. Subfolder grouping is supported by the
workflow loader (1 level deep, resolution by filename) so workflow names
are unchanged. Mirrors the .archon/workflows/maintainer/ split landing
in #1430.

Also adds e2e-minimax-smoke.yaml — a sanity check that Pi correctly
routes to Minimax M2.7 via the user's local pi auth, and that Pi's
best-effort output_format parser handles a small nested schema. Asserts
routing by reading the most recent Pi session jsonl rather than asking
the model to self-identify (LLMs are unreliable narrators about their
own identity, especially when Pi's system prompt mentions other
providers as defaults).

* fix(e2e-minimax-smoke): address CodeRabbit review on #1431

- Widen find window from -mmin -3 to -mmin -10. The smoke's three Pi
  nodes plus the assert can collectively run several minutes on slow
  networks; 3 minutes was tight enough to false-FAIL on a healthy run.
  (CodeRabbit minor)
- Drop non-deterministic `head -1` over `find` output. find doesn't
  guarantee any order; on a tie, the wrong file would be picked. Now
  iterates all matching sessions and breaks on first one carrying the
  routing signal — any match is sufficient evidence. (CodeRabbit minor)
- Replace single-regex `'"provider":"minimax".*"modelId":"MiniMax-M2.7"'`
  with two separate greps joined by `&&`. JSON field order isn't part of
  Pi's contract; a future Pi release reordering `provider` and `modelId`
  in the model_change event would silently false-FAIL the original
  pattern. The new check is order-independent. (CodeRabbit major)
Six findings, two majors and four minors/nitpicks:

- gate.md L17 vs L77: resolved conflicting input-source instructions.
  Body claimed "all inline, no extra fetch" while a later phase
  permitted reading PULL_REQUEST_TEMPLATE.md. Now: explicit "one
  allowed extra read" callout in Phase 1 + matching wording in Gate C.
  (CodeRabbit major)

- gate.md fenced blocks: added missing language identifiers (text/json/
  markdown) to satisfy markdownlint MD040. (CodeRabbit minor)

- gate.md L155 + read-context.ts: deterministic clock. The 3-day deadline
  was anchored to prior_state.last_run_at, which can be stale and produce
  past-dated deadlines. Moved both today and deadline_3d into the
  read-context.ts output (computed via sv-SE locale → ISO date in local
  time) and instructed the gate to use $read-context.output.deadline_3d
  directly. LLMs are unreliable at calendar arithmetic; this avoids it
  entirely. (CodeRabbit major)

- maintainer-review-pr.yaml fetch-diff: dropped 2>/dev/null on gh pr diff
  so auth / network / deleted-PR failures fail the node instead of
  feeding an empty diff to the gate. Empty-but-successful diff (PR has
  no changes) is now an explicit marker the gate can detect. (CodeRabbit
  minor)

- maintainer-review-pr.yaml approve-unclear: added capture_response: true
  so the maintainer's approve comment flows to the report node. Reject
  reasoning is already captured by Archon's run record. (CodeRabbit
  minor)

- maintainer-review-pr.yaml post-decline + report.md: the gh pr edit
  --add-label call previously swallowed all errors with || true and the
  report still claimed the label was applied. Now writes applied/skipped
  to $ARTIFACTS_DIR/.label-applied + the gh stderr to .label-error so
  the report can describe the actual outcome. (CodeRabbit nitpick)
…ume (#1435)

* fix(workflows): approval gate bypass after reject-with-redraft on resume

When an approval node was rejected with on_reject.prompt, the synthetic
PromptNode built to run the on_reject prompt reused the approval gate's
own node ID. executeNodeInternal then wrote a node_completed event with
that ID, causing getCompletedDagNodeOutputs to treat the gate as already
completed on the next resume — bypassing the human gate entirely.

Fix: give the synthetic node the ID `${node.id}:on_reject` so its
node_completed event has a distinct step_name that won't match the
approval gate slot in priorCompletedNodes.

Adds a regression test asserting no node_completed event with the
approval gate's ID is written during on_reject execution.

Fixes #1429

* test(workflows): add positive assertion and SSE side-effect comment for on_reject synthetic node

Add complementary positive assertion to the regression test to verify that
node_completed is written exactly once with step_name 'review:on_reject',
ensuring future refactors that suppress the event entirely would be caught.

Add inline comment in executeApprovalNode documenting the known SSE side-effect:
node_started/node_completed events with nodeId='review:on_reject' flow through
the SSE pipeline into the web UI, resulting in a transient phantom node in the
execution view. This is cosmetic-only — the human gate contract is preserved.

* simplify: reduce duplicate cast pattern in on_reject test assertions
…e checkout (#1438)

* feat(workflows): add mutates_checkout field to skip path-lock for concurrent runs

Add `mutates_checkout: boolean` (optional, default true) to the workflow
schema. When set to false, the executor skips the path-exclusive lock
that serializes all runs on the same working path, allowing N concurrent
runs on the same live checkout.

The primary use case is `maintainer-review-pr`, which reads shared state
but writes only to per-run artifact paths and GitHub PR comments — two
parallel reviews of different PRs should not fail with "Workflow already
active on this path".

Changes:
- `schemas/workflow.ts`: add optional `mutates_checkout` field
- `loader.ts`: parse and propagate the field (warn-and-ignore on invalid values)
- `executor.ts`: wrap path-lock guard in `if (workflow.mutates_checkout !== false)`
- `executor.test.ts`: two new tests in the concurrent-run guard suite
- `maintainer-review-pr.yaml`: opt in with `mutates_checkout: false`

* test(workflows): add loader tests for mutates_checkout parsing

- Add 5 tests covering false, true, omitted, and invalid (string "yes") values
- Invalid non-boolean values are silently dropped with warn — now explicitly tested
- Remove the // end mutates_checkout guard trailing comment (no precedent in file)
- Clarify loader comment: "parse/warn pattern" not "warn-and-ignore pattern" to avoid implying the return style matches interactive

* simplify: collapse nodeType/aiFields pair into single nonAiNode object in parseDagNode
…es (#1434)

* docs: replace String.raw with direct assignment in script node examples

String.raw`$nodeId.output` fails silently when substituted output contains
a backtick, terminating the template literal early and producing cryptic parse
errors. JSON is valid JS expression syntax, so direct assignment is safe for
all valid JSON values including those with backticks.

- Replace String.raw pattern in dag-workflow.yaml example
- Replace String.raw pattern in archon-workflow-builder.yaml template
- Add CAUTION bullet in workflow-dag.md Script Node section
- Add Silent Failures item #14 in parameter-matrix.md
- Add Starlight caution aside in script-nodes.md
- Extend script bodies bullet in variables.md
- Regenerate bundled-defaults.generated.ts

Fixes #1427

* docs: fix Rule 6 in generate-yaml prompt to distinguish bun vs uv patterns

Rule 6 still referenced JSON.parse after the example was updated to direct
assignment, creating a contradiction for the AI code generator. Update the
prose to explicitly distinguish TypeScript/bun (direct assignment) from
Python/uv (json.loads), matching the updated embedded example.
…s/experimental/

Move two repo-scoped workflows that were sitting untracked at the workflow
root into a dedicated subfolder. Subfolder grouping is supported by the
loader (1 level deep, resolution by filename), so workflow names are
unchanged and the /release skill still resolves archon-release correctly.

Files moved:
- archon-fix-github-issue-experimental.yaml — Path-A variant of the
  issue-fix workflow used today to land #1434, #1435, #1438.
- archon-release.yaml — the live release workflow used by the /release
  skill end-to-end (validate -> binary smoke -> version bump -> changelog
  -> approval -> commit -> PR -> tag -> Homebrew formula update).
…des (#1387)

executeBashNode previously only merged explicit envVars on top of
process.env. The three well-known workflow directories (artifactsDir,
logDir, baseBranch) were passed as function parameters and used for
compile-time substitution of $ARTIFACTS_DIR / $LOG_DIR / $BASE_BRANCH
in the script body, but were never added to the subprocess environment.

As a result, any script that relied on shell-runtime expansion — e.g.
JSON_FILE="${ARTIFACTS_DIR}/foo.output.json" inside a heredoc, an
inherited helper script, or a `bash -c` subshell — saw the variable
unset and silently fell back to its default (typically an empty string
or "."), writing artifacts to the workflow cwd instead of the nominal
artifacts directory.

Always build subprocessEnv from process.env plus the three well-known
directories, then allow explicit envVars to override. Compile-time
substitution behavior is unchanged; existing scripts that do not
reference these variables are unaffected; user-supplied envVars still
win on conflict.
…1426)

* fix(workflow): substitute \$nodeId.output refs in approval messages

Approval node messages were emitted as raw strings, bypassing the
substituteNodeOutputRefs() pass that prompt/bash/loop/cancel nodes
all run. This made interactive workflows like atlas-onboard show
literal "\$gather-context.output.repo_name" placeholders to humans
at HITL gates, leaving them unable to know what they were approving.

Fix: rendered the approval.message through substituteNodeOutputRefs
once at the top of the standard approval gate path, then used the
resolved string in all 4 emission sites (safeSendMessage,
createWorkflowEvent, pauseWorkflowRun, event-emitter).

Test: new dag-executor.test case wires a structured-output upstream
node into an approval node and asserts pauseWorkflowRun receives the
substituted message ("Repo: hcr-els | App: CCELS | Port: 3012")
rather than the literal placeholders.

Repro: any workflow with an approval node whose message references
\$nodeId.output[.field]. Observed in the wild on atlas-onboard's
confirm-context HITL gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(workflow): extend approval-substitution test to cover all 4 emission sites

Per CodeRabbit review: the original test only verified pauseWorkflowRun
received the substituted message, but the fix touches 4 emission sites.
A future regression at safeSendMessage / createWorkflowEvent / event-emitter
would silently leave the test passing while users still saw raw $node.output
placeholders.

Adds two additional assertions:
- platform.sendMessage prompt contains substituted message + does NOT
  contain literal $gather-context.output placeholders
- The persisted approval_requested workflow event's data.message is
  substituted

Event-emitter assertion deferred (no existing pattern for spying on the
global emitter in this test file). Two of three secondary surfaces
covered closes the practical regression risk — both are user-visible
(chat prompt + audit-log event); the emitter is internal only.

Test count: 7 pass / 22 expect() (was 18). Full suite 193 pass / 353
expect() — no regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#1367)

* feat(workflows): expose $LOOP_PREV_OUTPUT in loop node prompts (#1286)

Adds a new substitution variable that carries the previous loop iteration's
cleaned output into the next iteration's prompt. Empty on iteration 1; the
prior iteration's output (after stripCompletionTags) on iteration 2+.

Why: fresh_context: true loops have no way to reference what the previous
pass produced or why it failed without dragging the full session forward.
$LOOP_PREV_OUTPUT closes that gap with zero session-cost — same trust
boundary as $nodeId.output, no new external surface.

Changes:
- packages/workflows/src/executor-shared.ts: substituteWorkflowVariables
  accepts a 10th positional loopPrevOutput arg and substitutes
  $LOOP_PREV_OUTPUT (defaults to '').
- packages/workflows/src/dag-executor.ts: executeLoopNode passes
  lastIterationOutput on iteration 2+ (and explicit '' on iteration 1 /
  the first iteration of an interactive resume, since lastIterationOutput
  is a per-call variable that does not survive resume metadata).
- Unit tests: 3 new cases in executor-shared.test.ts.
- Integration tests: 2 new cases in dag-executor.test.ts verifying the
  prompt sent to the AI on iter 1 vs iter 2, and that the value reflects
  cleaned output (no <promise> tags).
- Docs: variables.md, loop-nodes.md (new "Retry-on-failure" pattern),
  CLAUDE.md variable reference.

Backward compatibility: prompts that don't reference $LOOP_PREV_OUTPUT are
unaffected. All 843 workflow tests + type-check + lint + format:check +
bun run validate pass locally.

* docs: address coderabbit review on variables/loop-nodes

- variables.md: include $LOOP_PREV_OUTPUT in substitution-order list and
  availability table to match the new variable row at line 30
- loop-nodes.md: document the interactive-resume exception where the first
  iteration after an approval-gate resume still receives an empty
  $LOOP_PREV_OUTPUT regardless of iteration number (per dag-executor.ts
  L1781-1783 where i === startIteration always clears prev output)

* docs(changelog): add Unreleased entry for $LOOP_PREV_OUTPUT (#1367 review)

* test(loop): add resume-from-approval integration test for $LOOP_PREV_OUTPUT (#1367 review)

Per maintainer-review-pr suggestion (Wirasm): two-call integration test
covering the resume-from-approval scenario.

  - Call 1: fresh interactive loop pauses at the gate after iteration 1 and
    asserts $LOOP_PREV_OUTPUT substitutes to empty on iter 1 (no prior
    output) plus the gate pause is recorded.
  - Call 2: resumed run with metadata.approval populated. The first
    resumed iteration must substitute $LOOP_PREV_OUTPUT to '', NOT to the
    paused run's iter-1 output (which lived in a different process and is
    not persisted). $LOOP_USER_INPUT still flows through as normal.

Locks the documented invariant at dag-executor.ts:1769-1772.

---------

Co-authored-by: voidborne-d <DottyEstradalco@allergist.com>
…1457)

The brief was missing a key signal — when contributors reply on PRs or
issues, the maintainer wouldn't see it explicitly. Empirically reviewed
PR replies were buried under aggregate updatedAt timestamps with no
indication of WHO replied or WHAT they said.

This adds a new "Replies waiting on you" section to the daily brief,
sourced from two paginated GitHub API calls scoped by since=last_run_at:

  - /repos/{o}/{r}/issues/comments  PR + issue conversation comments
  - /repos/{o}/{r}/pulls/comments   inline code-review comments

Filters applied:
  - Skip the maintainer's own comments (gh_handle from profile.md)
  - Skip GitHub bot accounts (login ending in [bot]) — coderabbitai,
    chatgpt-codex-connector, dependabot, etc. They post a constant
    churn of automated review tooling that drowns out human replies;
    the maintainer wants the latter.

Output is grouped by PR/issue number with kind classification:
  - issue              comment on a non-PR issue
  - pr_conversation    PR conversation-level comment
  - pr_review          inline code-review comment (most actionable —
                       usually needs a code-level response, so kind
                       upgrades to pr_review whenever review comments
                       arrive on a PR that also has conversation ones)

Sorted by recency (newest reply first). Synthesizer reads
gh-data.output.replies_since_last_run and renders a section.

Verified on a backdated state.json (last_run_at = yesterday morning):
22 human replies on 22 PRs/issues, bot noise filtered (32 → 22 after
the [bot] filter). Surfaces exactly the contributor responses to
yesterday's review comments and direction questions.
The maintainer-standup brief had no signal for "I already triaged that
PR via maintainer-review-pr 2 days ago" — it just kept listing reviewed
PRs in P1-P4 with no acknowledgement of prior work. Result: maintainer
ends up re-skimming the same PR several mornings in a row.

This adds a shared persistent state file at:

  .archon/maintainer-standup/reviewed-prs.json (gitignored, per-maintainer)

shape:

  {
    "1338": {
      "reviewed_at": "2026-04-27T16:34:57Z",
      "gate_verdict": "review",     // review | decline | needs_split | unclear
      "run_id": "..."
    },
    ...
  }

Three pieces:

1. WRITER — new `record-review` script node in maintainer-review-pr.yaml,
   runs after whichever branch fired (post-review / post-decline /
   approve-unclear) with trigger_rule: one_success. Inline bun script;
   reads $gate.output.verdict, $ARTIFACTS_DIR/.pr-number, and
   $WORKFLOW_ID; appends/upserts the entry. report node now depends on
   record-review so the state write happens before the run completes.

2. READER — read-context.ts loads reviewed-prs.json into a new
   reviewed_prs field on the standup gather output. Same pattern as
   prior_state and recent_briefs.

3. SURFACE — maintainer-standup command file gets a Phase 2h rule:
   when listing PRs in P1-P4 / Polite-decline sections, append:
     - "✓ reviewed Nd ago" for review-branch entries
     - "✓ declined Nd ago" for decline / needs_split branches
     - "✓ triaged Nd ago (unclear)" for unclear branch
   and a STALENESS marker — compare reviewed_at to PR's updatedAt; if
   contributor pushed since the prior review, append
   "⚠ contributor pushed since" so the maintainer knows the prior pass
   may need to be re-run.

Plus a one-shot backfill script:

  .archon/scripts/maintainer-standup-backfill-reviews.ts

Scans the maintainer's gh comments in the last 7 days, pattern-matches
"## Review Summary" / direction-clause-citation / split-up wording, and
populates reviewed-prs.json. Idempotent; existing entries (from real
workflow runs) take precedence over backfilled ones (the writer-node
record is more authoritative than a body-pattern guess). Uses 64MB
maxBuffer on the gh exec because --paginate over 7 days of an active
repo's comments easily exceeds Node's default 1MB.

Backfill verified: 363 comments scanned, 18 matched, 17 unique PRs
populated — exactly the 17 PRs we reviewed via the workflow yesterday.

The new state file is gitignored alongside the existing per-maintainer
files (profile.md, state.json, briefs/).
…1460)

Both SDKs were ~30 patch releases behind. Validation suite passes
(type-check, lint, format, tests across all 10 packages) without code
changes. The only sustained Claude SDK behavior change in the range —
v0.2.111's options.env overlay/replace flap, since reverted to overlay —
is a no-op for Archon, which already passes { ...process.env } as the
SDK env.
Wirasm and others added 27 commits May 4, 2026 15:45
Adds repo-triage-minimax.yaml — a Pi/Minimax M2.7 port of the existing
repo-triage workflow. Identical DAG shape, state files, and guardrails;
all four LLM-orchestrated nodes rewritten to do per-issue/per-PR work
inline since Pi has no Task tool / inline sub-agents.

Differences from the Claude variant:
- Drops the four `agents:` blocks (brief-gen, closed-brief-gen,
  pr-brief-gen, pr-issue-matcher) — Pi can't invoke them.
- Removes Task from `allowed_tools` for every prompt node.
- Each "BRIEF/MATCH PASS" reads the gh JSON dump once and processes
  every entry inline; JSON shapes that the deleted sub-agents returned
  are now spec'd in the orchestrator prompt.
- Skips the LABEL pass — labelling requires Claude's Task tool to
  invoke the on-disk triage-agent sub-agent. Run repo-triage.yaml
  (the Claude variant) when label-coverage matters.

Validates clean: 1 valid, 0 errors, 0 warnings.
)

* fix(workflows): prevent zombie workflow runs from hung Pi cleanup (#1561)

When a Pi/Minimax DAG node received an SDK error result, the executor
threw inside the for-await loop and bridgeSession's finally hung on
`await promptPromise.catch(...)` because session.dispose() does not
settle session.prompt(). Bun's event loop drained with no remaining
I/O sources and the process exited before the catch and DB finalization
could run, leaving the workflow run stuck in 'running' indefinitely.

Changes:
- Wrap the post-dispose prompt-promise await in Promise.race against a
  10s timeout so cleanup always completes
- Add a defensive finally backstop in executeWorkflow that flips any
  still-'running' run to 'failed' before returning
- Add regression test for the cleanup timeout
- Update store mocks in executor tests to include getWorkflowRunStatus

Fixes #1561

* fix: address review findings from PR #1563

- Clear the cleanup setTimeout after Promise.race resolves to prevent
  a dangling timer from holding the event loop open for up to 10s in
  fast CLI Pi workflows
- Add debug log inside promptPromise.catch for Pi SDK cleanup-phase
  rejections so secondary errors surface without polluting output
- Add warn log when the executor finally backstop fires so zombie
  corrections are visible in logs (not just the DB record)
- Add executor finally backstop tests covering the 'running' and
  'completed' status branches
- Add fast-path test asserting bridgeSession cleanup completes in <1s
  when prompt() settles promptly (guards the clearTimeout fix)

* simplify: use unknown instead of Error in backstop catch handler

* fix(pi): drop blocking await on prompt promise; cleanup is non-blocking

The previous fix wrapped `await promptPromise.catch(...)` in a 10s timeout
race to prevent the cleanup from hanging when Pi's session.prompt() never
settles after dispose(). That solved #1561 but introduced a magic number
and unnecessary work.

The await itself was the bug. The queue is closed before the await runs,
and the .then() handlers attached to promptPromise only push to that
queue — closed pushes are no-ops. There's nothing the cleanup is actually
waiting for. Whether prompt() resolves in 1ms or never, no observable
behavior changes for the caller.

Drop the await entirely. Attach .catch() fire-and-forget so a stray async
rejection (the .then() handlers should preclude this, but belt-and-
suspenders) doesn't bubble up as an unhandled-rejection process exit.
Cleanup now returns in <200ms regardless of Pi's prompt() behavior.

Tests:
- "cleanup does not block when prompt() hangs forever" — asserts <200ms
  (vs. the previous ~10s tolerance)
- "late prompt() rejection does not become unhandled" — regression for
  the .catch() belt
…wn (#1529)

* fix(orchestrator): create ~/.archon/workspaces before AI provider spawn

On a fresh install, ~/.archon/workspaces doesn't exist yet. The
orchestrator passes that path as cwd to the AI provider, which calls
spawn() — which raises ENOENT. The error is then misclassified as
"binary not found" in the friendly-error path, surfacing as an
incorrect "Claude binary not found" message.

Add ensureArchonWorkspacesPath() in @archon/paths that mkdir -p's the
directory and returns the path. Use it at the orchestrator's spawn-cwd
site so the directory is guaranteed to exist before spawn().

Other call sites of getArchonWorkspacesPath() (workflow discovery,
path-prefix comparisons) only consume the path string and don't need
the directory to exist; they keep using the pure getter.

Closes #1528

* test(orchestrator): assert ensureArchonWorkspacesPath is called

Capture the @archon/paths mock as a named variable and assert it was
called in the syncWorkspace handleMessage path. Without this, the test
suite passes even if orchestrator-agent.ts:824 reverts to the
non-ensuring getArchonWorkspacesPath() variant — exactly the regression
that surfaced as 'Claude Code native binary not found' in #1528.
…ion (#1575)

GitHub stays the primary forge; Gitea and GitLab are community-supported
targets via the existing community adapters at
packages/adapters/src/community/forge/. Long-term home for outbound forge
operations (PR/issue/review CRUD) is the same per-forge adapter that
already handles inbound webhooks today.

Closes the open direction question that has been blocking PR #1104
(forge-agnostic workflow commands) for ~8 days.

The architectural cleanup — migrating PR #1104's interim forge-cli.ts
into the existing community adapters — is tracked separately as #1574
and is not a precondition for merging #1104.
…de failures (#1572)

* fix(pi): surface SDK error messages and cap concurrency to stop cascade failures (#1569)

Parallel Pi/Minimax workflow batches degrade as concurrent session count rises:
the first few succeed, the rest cascade-fail with empty review findings.
Operators see only "SDK returned error" because the underlying message is
discarded, which also prevents the executor's transient-retry path from
classifying 429/overload errors and retrying them.

Changes:
- buildResultChunk now surfaces AssistantMessage.errorMessage in the result
  chunk's errors[] array so dag-executor's errorsDetail is populated and the
  thrown error message is pattern-matchable.
- PiProvider gains a module-level counting semaphore configurable via
  assistants.pi.maxConcurrent in .archon/config.yaml. Pi has no SDK-level
  throttling (unlike Claude), so this is the right place to gate concurrent
  upstream calls.
- TRANSIENT_PATTERNS now includes '529' and 'overloaded' so Anthropic's
  service-overload responses route to the existing retry-with-backoff path.
- Tests cover errors[] population and maxConcurrent parsing.

Fixes #1569

* fix(pi): address review findings — semaphore race, silent failure, test gaps

- Snapshot piSemaphore into const sem before first await so the finally
  block only releases a slot this call actually acquired (prevents spurious
  over-release when concurrent calls have mixed maxConcurrent config)
- Add warn log in serializeToolResult catch so serialization failures are
  visible rather than silently falling back to String(result)
- Fix maxConcurrent JSDoc to say "Omit for unlimited" — 0 is not a valid
  sentinel (parsePiConfig drops it silently); remove the misleading hint
- Extend errorMessage-absent test to also cover errorMessage: '' (both
  falsy cases excluded from errors[], now both documented)
- Add combined maxConcurrent + model fields test to config.test.ts
- Add classifyError tests covering 429, 529, overloaded, FATAL priority,
  and UNKNOWN classification for executor-shared.ts
- Add semaphore smoke tests to provider.test.ts verifying initialization
  log and that the absent-maxConcurrent path skips initialization

* simplify: remove redundant String() coercion, fix orphaned JSDoc, early-return in release()
…: false) — closes #1546 (#1555)

Co-authored-by: Zolto <zolto@zhome.local>
* Pi provider: load user settings files as session baseline

Previously, the Pi provider created an empty SettingsManager.inMemory() on every query, ignoring the user's Pi settings files entirely. This meant user preferences in ~/.pi/agent/settings.json (retry counts, transport, compaction strategy, thinking budgets, default model, etc.) and per-repo overrides in <cwd>/.pi/settings.json had no effect on Archon-driven sessions.

Co-authored-by: Copilot <copilot@github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* test(settings): add shallow merge behavior for object values in settings

* fix(docs): correct spelling of "behavior" in Pi settings documentation

* fix(tests): reset mock settings manager implementations in PiProvider tests

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…1566)

* feat(cli): setup overhaul + archon doctor + complete bundled skill (#1494)

Make binary installs the official primary path. Setup wizard becomes
a dead-simple "AI + skippable adapters" flow, the embedded skill ships
all 21 files, and `archon doctor` provides a canonical "is my setup OK?"
checklist.

Changes:
- bundled-skill.ts: embed all 21 .claude/skills/archon/ files (was 18,
  missing good-practices.md, parameter-matrix.md, troubleshooting.md)
- scripts/check-bundled-skill.ts: CI guard that fails when bundled-skill.ts
  drifts from .claude/skills/archon/; wired into `bun run validate`
- setup.ts: remove database prompt (SQLite implicit), remove Discord,
  validate Claude binary via spawn test, probe `gh auth status` and
  optionally run `gh auth login`, add Telegram security note + empty-
  allowlist warning, append update instructions and offer to run
  `archon doctor` at the end
- doctor.ts: new `archon doctor` command — green/red checklist for
  Claude binary, gh auth, database, workspace writability, bundled
  defaults, and adapter token pings (best-effort)
- cli.ts: register `doctor` and add to noGitCommands
- doctor.test.ts: spyOn-based tests for individual check functions
- setup.test.ts: drop SetupConfig.database and platforms.discord
  references, anchor DATABASE_URL assertion to line start

Closes #1494

* fix: address review findings from PR #1566

- Fix checkWorkspaceWritable false negative: separate try/catch for rmSync
  so a cleanup failure never reports the directory as unwritable
- Fix collectGitHubConfig empty catch: distinguish gh-not-installed (ENOENT)
  from gh-not-authenticated with actionable error messages
- Check spawnSync('gh', ['auth', 'login']) return value and warn if spawn fails
- Fix three stacked JSDoc blocks in setup.ts: restore orphaned JSDoc to
  collectClaudeBinaryPath and collectClaudeAuth, keep only correct block
  above probeClaudeBinarySpawns
- Fix doctorCommand allSettled rejection handler: use String(s.reason) for
  non-Error rejections instead of producing "undefined" output; add log.error
- Fix checkSlack/checkTelegram catch label: drop "network error: " prefix
  so JSON parse failures aren't mischaracterized
- Fix "Skip silently" wording in doctor.ts: visible output contradicted "silently"
- Fix doctor.test.ts module comment: BUNDLED_IS_BINARY is not spied, clarify
- Add checkBundledDefaults test: passes in dev mode without mocks
- Update CLAUDE.md: fix validate description "five" → "six" checks, add
  check:bundled-skill; add archon doctor to CLI section
- Update docs-web cli.md: add doctor command section after setup
- Add comment to check-bundled-skill.ts documenting substring check limitation

* simplify: trim doctor.ts JSDoc and remove redundant existsSync check

- Remove 12-line file JSDoc that restated the obvious; keep only the
  non-obvious setup-invocation note
- Drop existsSync() guard before execFileAsync() — TOCTOU-prone and
  redundant since the catch block already produces a clear error
- Collapse verbose allSettled comment to one line

* feat(setup): bootstrap project-scoped .archon/config.yaml on skill install

`archon setup` previously copied the Claude skill into the user's project
but did not create a `.archon/` directory or a project config there. A
fresh user who wanted any project-scoped override (per-project assistant
defaults, custom docs path, etc.) had to `mkdir -p .archon` and create
config.yaml by hand — exactly the friction the v0.3.11 setup overhaul
was meant to remove.

Add `bootstrapProjectConfig(projectPath)` and call it after the skill
install. Writes a commented-out starter `.archon/config.yaml` with
example keys and a link to the configuration reference. Idempotent on
re-run (skips if the file already exists). Workflows/commands/scripts
subdirectories are intentionally not created — empty dirs would clutter
users' trees and the loaders handle their absence cleanly.

The wizard's final summary now shows the created config path so the
user knows where to put per-project overrides.

Tests:
- creates `.archon/config.yaml` when missing
- creates the `.archon` directory if absent
- is idempotent — leaves an existing user config untouched
- returns failed state without throwing on unwritable target

* fix: address multi-agent review findings from PR #1566

Production correctness:
- setup.ts: gh auth login now checks .status !== 0 so a non-zero exit
  (cancelled OAuth, failed callback) is surfaced instead of silently
  succeeding.
- setup.ts: probeClaudeBinarySpawns returns {ok, reason}; the warning
  now includes the actual spawn error (ENOENT/timeout/permissions).
- setup.ts: bootstrapProjectConfig uses writeFileSync flag 'wx' and
  catches EEXIST, eliminating the TOCTOU window between existsSync
  and the write.
- doctor.ts: split checkDatabase into module-load vs query try-catches
  so a missing @archon/core stops masquerading as "Database not
  reachable". Both branches now log structured errors.
- doctor.ts: checkWorkspaceWritable rmSync catch logs warn so repeated
  delete failures leave a diagnostic trace.
- cli.ts: doctor case uses a static import to match every other peer
  command.

Tests:
- checkClaudeBinary covers all four branches via an injected isBinary
  parameter (skip / no-path / spawn-pass / spawn-fail).
- checkDatabase covers sqlite + postgres pass, query failure, and
  module-load failure via injectable loadDeps.
- checkSlack / checkTelegram cover pass / fail / network-error->skip
  via spyOn(globalThis, 'fetch').
- checkGhAuth gains an explicit GH_TOKEN-only branch.
- doctorCommand asserts the exit-code contract (0 on all-pass, 1 on
  any fail, thrown checks counted) and Promise.allSettled non-short-
  circuit, via injectable check thunks.

Docs:
- cli.md: doctor added to the no-git command list.
- CLAUDE.md: validate now mentions check:bundled-skill alongside
  check:bundled.
- overview.md: archon doctor row added to the CLI command table.
- troubleshooting.md: points users at archon doctor after setup.
* feat(docs): add public roadmap page at /roadmap

Adds a visual roadmap page to the docs site showing what's shipped,
in progress, next, and planned — with GitHub issue links, version
badges, and a secondary tier for longer-horizon items.

- New `src/data/roadmap.ts` — typed data (RoadmapItem, statusConfig) for
  14 items across 4 statuses and 2 tiers
- New `src/pages/roadmap.astro` — standalone dark page with vertical
  timeline for active items, compact grid for secondary planned items,
  and a 2-col shipped section; no Starlight layout dependency
- `astro.config.mjs` — adds Roadmap link to sidebar
- `index.md` — adds Roadmap action button to hero

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

* fix(docs): remove broken /workflows/ nav link from roadmap page

The nav bar linked to /workflows/ which doesn't exist in the docs site,
producing a 404. Removed the dead link per review findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Leex <thomas@thirty3.de>
* fix(workflows): add provider: claude to opus[1m] nodes — closes #1610

Without an explicit `provider:` annotation, the DAG executor falls back to
`config.assistant` (default: `codex` for some users). Codex silently ignores
`opus[1m]`, completes in ~3 s with empty output, and downstream nodes receive
nothing. The three bundled workflow files that pair `model: opus[1m]` with no
`provider:` are now annotated with `provider: claude`.

Also fixes three pre-existing Windows CI issues: path-separator bug in
`check-bundled-skill.ts` (backslash vs forward slash), newline truncation in
`dag-executor.test.ts` script-failure test, and `IS_SANDBOX=1` guard skip in
`provider.test.ts` root-UID test. Regenerates `bundled-defaults.generated.ts`.

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

* fix(review): address review findings for PR #1622

- Add regression tests for provider resolution (#1610): assert that a
  node with no provider: routes to workflowProvider, and that explicit
  provider: claude overrides workflowProvider even when set to 'codex'
- Add bundled opus invariant test: every default workflow node with an
  opus model must have provider: claude at node or workflow level
- Fix stale comment in dag-executor.ts:315 — null is caught by the
  typeof check above, not by this branch
- Docs: add provider/model independence warning to authoring-workflows.md

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

* simplify: replace for loop with .some() in isVersionRequest

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…1609)

* Add Pi as AI provider option in setup wizard (#1607)

The `archon setup` wizard previously hardcoded `[claude, codex]` in its AI
multiselect. Pi is a registered community provider but was unreachable
through the wizard, so users had to ctrl-c and hand-edit `~/.archon/.env`
plus `~/.archon/config.yaml` to use it.

Changes:
- Add Pi (community) option to the `collectAIConfig` multiselect, with a
  per-backend prompt covering 9 LLM backends and an optional API key.
- Write the chosen backend API key to `.env` under its canonical name
  (e.g. `ANTHROPIC_API_KEY`) and the model ref to `~/.archon/config.yaml`
  under `assistants.pi.model`. Skip the YAML write if `pi:` already exists
  or show a manual paste-in note when `assistants:` is present without `pi:`.
- Add a Pi module-load verification spinner mirroring the Claude binary
  spawn-test, with a continue/abort prompt on failure.
- Extend `archon doctor` with `checkPi` (skip when not configured, pass
  on auth.json or API key env var, fail when default but no auth).
- Update default-assistant selection to handle Pi alongside Claude/Codex.
- Mention the wizard path in the Pi docs page.
- Tests cover Pi-only, Claude+Pi, no-API-key, `checkExistingConfig` Pi
  detection, all `checkPi` branches, and `checkPiModule` ok/fail.

Fixes #1607

* fix(cli): address Pi provider review findings in doctor and setup

- Add `probeAuthJsonExists` wrapper to doctor.ts so the existsSync call
  can be properly spied in tests (ESM named-import rebinding limitation,
  consistent with `probeFileExists` pattern in setup.ts)

- Fix `checkPi` false positive: shared keys like ANTHROPIC_API_KEY no
  longer count as Pi evidence unless DEFAULT_AI_ASSISTANT=pi; Claude-only
  users no longer see a spurious "pass" or "Pi: Configured" status

- Fix `writeHomePiModelConfig` to use `pathsGetArchonHome` from
  @archon/paths instead of the local `getArchonHome()`, so Docker
  environments (/.archon) are handled correctly

- Replace `includes('pi:')` substring check with `/^\s*pi\s*:/m` regex
  to prevent false positives from substrings like `api:`

- Fix `log.warn` → `log.warning` for consistency with rest of setup.ts

- Add `err.code` to the Pi model config write-failure warning message

- Update `checkPiModule` docstring to avoid "mirrors Claude binary check
  pattern" phrasing that misled maintainers

- Update `writeHomePiModelConfig` branch-1 docstring from "user-edited"
  to "idempotent" to accurately describe the skip condition

- Add `writeHomePiModelConfig` test suite covering all three branches,
  quote escaping, and the api:/pi: regex fix

- Update checkPi tests to spy on `probeAuthJsonExists` via doctorModule
  instead of fsModule.existsSync; add regression tests for M2 false positive

- Add `# Pi Authentication` section header assertion to mixed Claude+Pi
  generateEnvContent test

* simplify: remove redundant hasApiKey in checkPi

* fix: address code review findings for PR #1609

Fixed:
- checkPiModule docstring: remove inaccurate 'doctor-step role' analogy
- checkPiModule catch: use instanceof Error guard + structured Pino log
- Move checkPiModule tests from doctor.test.ts to setup.test.ts (convention)
- hasPi detection: add clarifying comment explaining API-key-only intent
- collectPiConfig: remove redundant typeof guard after isCancel narrows to string
- writeHomePiModelConfig catch: add structured err field to Pino log
- cli.md: add Pi auth to archon doctor checklist description

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

* simplify: remove redundant aliases in setup.ts

- Remove `rawValue`/`value` alias in serializeEnv (no transformation)
- Remove `skillTarget`/`skillTargetRaw` alias (use raw directly)
- Simplify single-provider default selection to use selectedProviders array

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

---------

Co-authored-by: Cole Medin <cole@dynamous.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…tup (#1608)

* fix(cli): suppress boot stderr and Pino info logs in archon doctor/setup (#1606)

`archon doctor` and `archon setup` were leaking `[archon] loaded N keys` boot
lines and Pino `info` JSON events into their human-readable ○/✓ checklist
output. Gate the loaded-keys lines behind ARCHON_VERBOSE_BOOT=1 or LOG_LEVEL=
debug/trace, and default Pino to `warn` for the two interactive commands
unless `--verbose` is passed.

Changes:
- packages/paths/src/env-loader.ts: add isVerboseBoot() helper; only emit
  `[archon] loaded` lines when verbose-boot or LOG_LEVEL=debug/trace is set
- packages/cli/src/cli.ts: default Pino to `warn` for `setup` and `doctor`
  unless `--verbose` is passed (lazy logger init means this takes effect
  before any module logger is created)
- packages/paths/src/env-loader.test.ts: existing loaded-line tests opt in
  via ARCHON_VERBOSE_BOOT=1; add coverage for default suppression and
  LOG_LEVEL=debug

Fixes #1606

* fix: address review findings from PR #1608

- Condense multi-line cli.ts comment to one line (CLAUDE.md compliance)
- Respect LOG_LEVEL=debug/trace even for interactive commands (setup/doctor)
- Collapse isVerboseBoot() JSDoc to single-line comment
- Scope consoleErrorSpy to single test that uses it (was global fixture)
- Add LOG_LEVEL=trace test for isVerboseBoot() coverage
- Add ARCHON_VERBOSE_BOOT=true (non-'1') test to document strict-equality
- Document ARCHON_VERBOSE_BOOT in configuration.md env var table
- Update configuration.md operator log lines block to reflect gating
- Update cli.md env startup steps 2/3 with verbosity gate note
- Update cli-internals.md boot flow diagram with verbosity gate note
- Add CHANGELOG.md [Unreleased] entry for #1606
- Fix .env.example --quiet level label (error → warn)

* simplify: export isVerboseBoot and eliminate logLevelIsVerbose duplicate in cli
…stall (#1624)

* feat(docs-web): add marketplace data registry

PIV Task 1: Create marketplace.ts with MarketplaceEntry interface,
tagConfig, VALID_HOSTS, and 4 seed entries pinned to SHA 69b2c89.

* feat(docs-web): add marketplace catalog, detail pages, and JSON endpoint

PIV Tasks 2-5: Standalone Astro catalog page with client-side filter,
dynamic detail pages via getStaticPaths, workflows.json endpoint,
and sidebar entry in astro.config.mjs.

* feat(cli): add workflow search and install marketplace commands

PIV Tasks 6-7: Add workflowSearchCommand (fetches workflows.json,
filters by query) and workflowInstallCommand (downloads YAML at
pinned SHA, writes to .archon/workflows/). Wire in cli.ts with
early-exit for search (no git required) and install case in switch.

* feat: add marketplace lint script, GitHub Action, and contributing guide

PIV Tasks 8-10: Bun lint script validates slug uniqueness, host
allowlist, and SHA+file existence. GitHub Action runs on PRs
touching marketplace.ts. CONTRIBUTING.md documents submission process.

* fix(cli): validate marketplace slug and entry fields before install

- Validate slug against ^[a-z0-9-]+$ before path construction to prevent
  path traversal when ARCHON_MARKETPLACE_URL points to an untrusted server
- Validate required fields (slug, sourceUrl, tags) on each marketplace entry
  in fetchMarketplace() to surface clear errors on malformed responses

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

* feat(cli): support directory-based marketplace installs

Changes:
- workflowInstallCommand now handles both blob (single-file) and tree (directory) URLs
- Directory installs fetch GitHub Contents API listing and install files by convention
  (commands/ → .archon/commands/, scripts/ → .archon/scripts/, etc.)
- Main workflow identified by slug-matching filename or sole .yaml in directory root
- Lint script validates directory entries via GitHub Contents API instead of raw URL
- Updated MarketplaceEntry.sourceUrl comment to reflect file-or-directory semantics
- CONTRIBUTING.md documents both single-file and directory submission formats

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): validate path components from GitHub API in directory install

The directory install code used `subdir.name` and `file.name` from the
GitHub Contents API directly in path joins. Add an `isSafePathComponent`
guard that rejects `.`, `..`, and any name containing path separators or
non-portable characters before using it. Same defense-in-depth pattern as
the existing slug validation in `workflowInstallCommand`.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add marketplace-fetch-source script

PIV Task 1: Bun script that downloads marketplace entry source files
at pinned SHA via GitHub Contents API, preserving directory structure.

* feat: add marketplace-validate-schema script

PIV Task 2: Bun script that validates all .yaml files in source artifacts against the Archon workflow schema using parseWorkflow.

* feat: add marketplace-security-scan script

PIV Task 3: deterministic regex/heuristic scanner with 9 categories
(rce, exfil, reverse_shell, cred_leak, obfuscation, unsafe_permissions,
path_escape, shell_exec, suspicious_network). Reads $ARTIFACTS_DIR/source/
recursively, outputs JSON with severity + findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add marketplace-security-scan tests and fixtures

PIV Task 4: 9 malicious fixtures (one per scanner category), 3 benign
fixtures (zero false positives), empty-dir test. All 13 tests pass.

* feat: add marketplace-pr-review-and-merge workflow

PIV Task 5: DAG workflow with 9 nodes — fetch PR metadata, verify scope,
parse entry, fetch source, validate schema + security scan (parallel),
AI review, decide, and act (post GitHub review).

* feat: add GitHub Actions trigger for marketplace auto-review

PIV Task 6: Triggers on PRs touching marketplace.ts, runs the
marketplace-pr-review-and-merge workflow via CLI.

* feat: add auto-merge for clean submissions and pull_request_target trigger

Changes:
- Switch GH Action from pull_request to pull_request_target for fork PR secret access
- Add ANTHROPIC_API_KEY and contents:write permission to GH Action
- Add auto_merge decision: clean PRs (scan none + AI approval) are auto-merged
- Update ai-review prompt and output_format with auto_merge recommendation
- Update decide script with 4-value decision matrix (no trust gating)
- Add auto_merge case arm to act node with squash merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… subprocesses (#1640)

The bash node executor sets ARTIFACTS_DIR LOG_DIR and BASE_BRANCH on the subprocess (dag-executor.ts:1320-1326), but the script node executor only conditionally forwards caller-supplied variables and never sets these three. Scripts that read ARTIFACTS_DIR from the process environment (e.g. the marketplace-pr-review-and-merge workflow parse-entry node) hit ENOENT in CI because the variable is undefined and resolve with empty string falls back to cwd. Mirror the bash-node executor pattern so script nodes receive the same runtime variables unconditionally.
…libc Claude binary (#1641)

Two bugs surfaced when the workflow first ran against a real submission PR:

1. parse-entry regex assumed single-line 'sourceUrl: <value>'. Prettier wraps long values to a new line, and each line in the diff carries a leading '+' that our regex's \s* cannot skip. Strip the '+' prefix from added lines before regex matching.

2. Claude Agent SDK loaded the linux-x64-musl native variant on glibc Ubuntu runners and failed at the missing binary. Mirror the docker-entrypoint fix (PR #1521): after install, locate the glibc binary under node_modules and export CLAUDE_BIN_PATH so the SDK resolver picks it instead of musl.
…tive path (#1642)

Bun's run-script context for scripts at .archon/scripts/ doesn't reliably honor the @archon/workflows/loader subpath export when invoked in CI via bun --no-env-file run <abs-path>. Module resolution finds node_modules at the repo root but fails on the subpath. Switch to a direct relative-path import to bypass the resolution gap. Other marketplace scripts (fetch-source, security-scan) don't hit this because they only use node: built-ins.
…e can route invalid submissions (#1643)

validate-schema was exiting 1 when any submitted workflow YAML failed schema validation. The DAG executor treats non-zero exit as node failure, which short-circuits the downstream ai-review / decide / act nodes via trigger_rule. Result: a marketplace PR with an invalid workflow gets no review comment because the workflow crashes before act can post one. Exit 0 always — the JSON output already includes 'valid: false', and decide routes that to 'request_changes' with an actionable comment.
…hema (#1644)

parseWorkflow logs warnings via Pino at default stdout. validate-schema's stdout is consumed by the decide node via variable substitution into a TypeScript expression, so any non-JSON log line breaks the substituted script. Set log level to fatal before parseWorkflow runs to suppress warnings cleanly. Long-term, the platform logger should write to stderr instead of stdout for CLI tool ergonomics, but that's a broader cross-cutting change.
… register providers (#1645)

Two false-positive sources in validate-schema:

1. Provider registry empty when parseWorkflow runs standalone. The CLI normally populates it at startup; this script must call registerBuiltinProviders + registerCommunityProviders or every workflow with provider: claude gets rejected with Unknown provider claude. Registered.

2. Non-workflow YAMLs were being parsed as workflows. Directory submissions ship brand.yaml, config.yaml, template scaffolds alongside the workflow file. parseWorkflow rejects these for missing required workflow fields. Filter to YAMLs with a top-level nodes: block before calling parseWorkflow.

Tested locally against PR #1639 submission: now returns valid: true with only video-generic.yaml validated, vs the previous 4 false-positive errors.
* feat(marketplace): add video-generic workflow

Adds a Remotion-based video generation workflow to the marketplace as
the first directory-format entry from an external repo. The workflow
turns a freeform prompt (URL, GitHub repo, release notes, topic) into
a voiced + animated Remotion video, with three HITL approval gates
for spec, script, and live preview.

Source: leex279/remotion-video-test/.archon @ 4dac83c2 (directory).

This is also the first end-to-end smoke test of the directory-install
path (yaml at .archon/ root, with commands/ scripts/ templates/ siblings
fanning out into the user's .archon/ on install).

* chore: retrigger auto-review after dag-executor env-var fix in dev

* chore: retrigger auto-review after multi-line parse + glibc binary fix

* chore: retrigger auto-review after workspace import fix

* chore: retrigger auto-review after validate-schema exit fix + credit top-up

* chore: retrigger auto-review after pino-log silencing fix

* chore: retrigger auto-review after validate-schema filter/provider fix
…1532)

* fix(core,web): show newest messages instead of oldest on hydration (#1531)

Two defects combine to silently lose recent messages in long conversations:

1. listMessages() returns the oldest N messages (ORDER BY ASC LIMIT 200).
   In conversations with >200 messages, the newest messages disappear
   from the UI after refresh — the exact opposite of every chat UI's
   expectation.

2. The stuck-placeholder recovery path in ChatInterface replaces React
   message state with the server fetch, dropping any live SSE-only messages
   (not just system messages) that haven't been persisted yet. This causes
   mid-session message loss without any user-visible trigger.

Fix:
- Change listMessages() to ORDER BY DESC then reverse, so the newest
  N messages are returned in chronological order
- Broaden stuck-placeholder recovery to merge by message ID instead of
  only preserving system messages — any client-only message (SSE-streamed,
  system status) not in the hydrated set is kept and interleaved by
  timestamp

Closes #1531

* fix: filter transient placeholders from stuck-placeholder recovery merge

Address CodeRabbit review: tighten the client-only filter to exclude
optimistic user rows and empty thinking placeholders. Only preserve
system messages and assistant messages with meaningful content (content,
error, workflowDispatch, workflowResult, or toolCalls).
…late multi-chunk AI commands before parsing (#1581)

* fix: improve command accumulation handling for multi-chunk messages

* fix: enhance command parsing logic to prevent premature command completion

Co-authored-by: Copilot <copilot@github.com>

* fix: enhance INVOKE_WORKFLOW_FULL_RE regex to properly handle --prompt accumulation in multi-chunk messages

Co-authored-by: Copilot <copilot@github.com>

* fix: update command chunk handling to prevent path corruption in multi-chunk messages

Co-authored-by: Copilot <copilot@github.com>

* fix: add mock for listCodebases to enhance test coverage

* Fix Pi provider silent failures: markdown bold wrapping and streaming truncation

Fixes two failure modes where the Pi provider silently drops /register-project and /invoke-workflow commands, leaving projects unregistered with no error shown to the user.

Mode A — Markdown bold wrapping (orchestrator-agent.ts)

Pi occasionally emits commands wrapped in markdown bold: **/register-project Name "/path"**. The existing prefix and full-command regexes require lines to start with /, so these were never detected.

Adds normalizeCommandText() which strips leading/trailing * characters from lines whose first non-asterisk character is /. Applied to all six detection and parse sites: isCommandFullyParsed, handleStreamMode, handleBatchMode, parseOrchestratorCommands (3 uses), and handleProjectRegistrationResult.

Mode B — Streaming truncation (event-bridge.ts)

Pi's text_delta events sometimes stop delivering characters mid-command (e.g. /register-project arrives but  SaberEngine "/path" never does as a delta). The complete text is present in agent_end.messages but was previously discarded.

Adds extractLastAssistantText() to read the fully-assembled text from the agent_end transcript. In bridgeSession, tracks the per-turn streamed length, and if the assembled text is strictly longer and starts with what was streamed, emits the missing suffix as a corrective assistant chunk before the result chunk — making it available to the orchestrator's accumulator.

Tests

5 new tests in orchestrator-agent.test.ts covering bold/italic stripping for both commands, including multiline responses and quoted paths
5 new tests in event-bridge.test.ts covering: truncation detected and emitted, no false emission when complete, mismatch guard, per-turn reset on turn_start, and assistantBuffer inclusion when wantsStructured

* fix: improve command normalization to handle leading/trailing whitespace

* fix: improve command registration handling to avoid false positives in text extraction

* fix: enhance command accumulation logic to prevent token loss and improve error handling

* fix: improve command registration handling to prevent silent drops of valid commands

---------

Co-authored-by: Copilot <copilot@github.com>
…1405)

* fix(server): GET /api/workflows/:name missed home-scoped workflows

The single-workflow endpoint searched only project-scope, bundled, and
filesystem defaults — skipping `~/.archon/workflows/` entirely. This
made global workflows invisible to the web UI builder (which loads via
this endpoint) even though `GET /api/workflows` (list) surfaced them
correctly after PR #1315.

Adds a home-scope lookup between project and bundled tiers, matching
`discoverWorkflowsWithConfig`. Uses `source: 'global'` (already in the
`WorkflowSource` union).

Reproduces the bug: create a workflow in `~/.archon/workflows/`, then
open `/workflows/builder?edit=<name>` → 404 "Workflow not found".
After this fix: loads correctly with `source: "global"`.

Related to #1138.

* test(server): cover home-scope lookup + project/home precedence

Two new tests for GET /api/workflows/:name:

- home-scope hit: file in ~/.archon/workflows/, no project match
  → returns source: 'global'
- shadow: same filename in both project and home
  → project wins, home is not even attempted

Uses ARCHON_HOME env var to redirect home lookups to a tmpdir during
the test. Restores the prior value in finally so test order doesn't
leak state.

* test(server): harden shadow test — assert home readFile is never called

Address CodeRabbit nitpick on #1405: `parseWorkflow` is globally mocked,
so asserting `body.source === 'project'` alone cannot catch a regression
that reads both files before deciding. Add a `spyOn(fs.readFile)` that
fails if the home path was ever opened.

Restores the spy in finally so other tests aren't affected.

* review(server): address Wirasm feedback on PR #1405

- Shorten home-scope fallback comment to point at `discoverWorkflowsWithConfig`
  as the canonical discovery anchor; drop historical "previously skipped" and
  web-UI builder noise.
- Drop "(project scope)" / bundled-defaults parentheticals — both restate WHAT
  the code already says.
- Add 500-path test: malformed home-scoped YAML returns 500 with
  "Home workflow file is invalid" error message.

---------

Co-authored-by: Raphael Lechner <raphael.l@asix.pro>
…esumableRun (closes #1392) (#1646)

* fix(workflows): make resume explicit via prepareResumedRun / hydrateResumableRun (closes #1392)

Previously `executeWorkflow` called `findResumableRun(workflow.name, cwd)`
unconditionally and silently auto-resumed the most recent failed run for
the same `(workflow_name, cwd)` pair. The `--resume` CLI flag was
documented as opt-in but had no causal effect on this path — it only
steered worktree selection. Net effect: cached node outputs from a prior
failed run (e.g. an `extract-pr-number` returning "1634") bled into the
next invocation of the same workflow at the same path.

This commit moves the resume decision out of the executor and into the
caller. The executor takes a `preCreatedRun` and optional
`priorCompletedNodes` via a new options bag; it never queries the store
for a prior run. Two helpers in `@archon/workflows/executor` cover the
two real lookup shapes:

- `hydrateResumableRun(deps, candidate)` — caller already has the run
  row (CLI `--resume` post worktree-path resolution; orchestrator
  foreground-resume via `findResumableRunByParentConversation`).
- `prepareResumedRun(deps, workflow, cwd)` — canonical
  `(workflow, cwd)` lookup-and-hydrate.

Both throw on DB errors instead of the previous "warn and degrade to
fresh" silent fallback, matching the Fail Fast principle.

`executeWorkflow`'s trailing positional args (`codebaseId`,
`issueContext`, `isolationContext`, `parentConversationId`,
`preCreatedRun`, plus the new `priorCompletedNodes`) consolidate into a
single `ExecuteWorkflowOptions` bag — fixing the
`undefined, undefined, undefined, undefined, true` smell at every
caller. 7 required positionals + 1 opts; future additions go to opts.

Orphan-cleanup code in the executor deletes outright: with caller-side
resume there is no separate `preCreatedRun` to orphan when resume takes
over.

Supersedes #1396 (which gated the same internal lookup behind a flag
without addressing the positional explosion or the conceptual
responsibility leak).

Closes #1392

* fix(workflows): address review feedback on explicit-resume PR

Server resume endpoint: wire POST /api/workflows/runs/:runId/resume to
dispatch via the orchestrator (parent web conversation) instead of
returning a stale "re-run to auto-resume" message that no longer
matches reality. CLI-created runs and non-web parents return a clear
400 pointing at `archon workflow resume <id>`.

Executor type design: drop the YAGNI `prepareResumedRun` helper (no
production callers), tighten `ExecuteWorkflowOptions` with a
discriminated union so `priorCompletedNodes` without `preCreatedRun`
is a type error, and rename `hydrateResumableRun`'s return key from
`run` to `preCreatedRun` so callers can spread directly.

Orchestrator foreground-resume: when hydration returns null (prior
run had nothing worth resuming) surface a user-visible notice and
fall through to a fresh run on the same worktree, instead of
throwing a >100-char error that `classifyAndFormatError` would
swallow into the generic `/reset` advice.

CLI resume: wrap `hydrateResumableRun` in try/catch with context so
DB errors don't surface as raw stack traces.

Docs: update three stale locations describing resume as automatic
on re-invocation (book/dag-workflows.md, guides/authoring-workflows.md,
guides/approval-nodes.md).

Tests + comments: rewrite resume endpoint tests to cover the four
new branches (404, 400 no-parent, 400 non-web, 200 web dispatch);
add orchestrator test for the new hydrate→null fall-through; fix
the off-by-one arg-index comment in executor.test.ts; drop
issue-number references and position-leaky comments per
comment-analyzer feedback.
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ca60bc19-9aa9-4d99-9466-fb842942c3cd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

@Wirasm Wirasm merged commit 3f258e6 into main May 12, 2026
10 of 13 checks passed
buihongduc132 pushed a commit to buihongduc132/Archon that referenced this pull request Jun 3, 2026
- Reset to upstream v0.3.11 (Release 0.3.11 coleam00#1653)
- Re-applied local patches: Pi provider noSkills, config-loader, types
- Fixed isModelCompatible as optional in ProviderRegistration
- Type-check clean on all packages
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.