diff --git a/.archon/maintainer-standup/direction.md b/.archon/maintainer-standup/direction.md index 89f9234fe2..3812d7ba1f 100644 --- a/.archon/maintainer-standup/direction.md +++ b/.archon/maintainer-standup/direction.md @@ -26,6 +26,8 @@ This file is **committed and shared by all maintainers**. Edit deliberately — - **Not a general-purpose chat UI.** Adapters are conversation surfaces for *workflow execution*, not standalone chat experiences. - **Not a replacement for the AI coding agent itself.** Archon orchestrates Claude Code / Codex / Pi — it doesn't reimplement them. - **Not opinionated about the dev environment.** No mandatory editor integrations, framework lock-in, or Docker requirement beyond what users opt into. +- **Not a deployment-infrastructure product.** Caddy is the single maintained reference reverse proxy (`--profile cloud`). Alternative proxies and infra recipes (Traefik, Nginx, k8s, ...) live in **docs** as community-maintained examples against the documented proxy contract (exposed port, health endpoint, `/internal/*` never proxied — see #2193), not as compose profiles Archon maintains: each maintained proxy config doubles a security-critical surface. Cite as `direction.md §deployment-recipes`. +- **Not a programming language.** The workflow YAML coordinates (gates, joins, retries, sessions, artifacts, reusable structure); `bash:`/`script:` nodes compute; prompts judge. PRs that add computation to the YAML surface conflict — see §workflow-language. ## Community providers @@ -45,6 +47,18 @@ Archon ships built-in providers for Claude (`@anthropic-ai/claude-agent-sdk`) an When citing this policy in a PR comment: `direction.md §community-providers`. +## Workflow language (YAML surface) + +The workflow YAML is a **coordination language**, not a programming language. Admissibility test for any new YAML surface feature (field, node type, expression capability): (1) does the *engine* need to see it to govern the run? (2) is it declarative data, not evaluation? (3) could a script node + existing wiring express it today? A feature that computes rather than coordinates is declined with a pointer to the escape hatch. Full rationale, case law, and the five failure smells: `packages/docs-web/src/content/docs/reference/workflow-language-constitution.md`. + +Triage clauses — cite as `direction.md §`: + +- **§when-grammar** — `when:` never grows incrementally (no parentheses, functions, string ops, arithmetic). Standing answer: compute the decision in a `script:` node, gate on `$node.output.field`. Only sanctioned growth is adopting CEL wholesale in one versioned change — never home-grown operators. +- **§load-time-composition** — composition/reuse features must fully resolve at load time (the executor runs a flat static DAG). Parameterization may carry **data**, never **structure**; dynamic/templated targets are declined. Runtime-resolved structure is a sub-run — a governance object with its own run record (#2121 Phase 2, co-designed with #1764) — not a language feature. +- **§workaround-triage** — repeated YAML structure or deterministic logic embedded in prompts is a signal, triaged into three buckets: missing *coordination* primitive → design it constitutionally; missing *pattern* → document the pattern (e.g. polyglot validate = detect-AI → execute-bash → fix-AI); disguised *computation* → point at script nodes. The workaround corpus decides language shape; the feature-request queue doesn't. +- **§schema-width** — new provider capabilities default into provider config or tier/alias presets, not new node fields; a node field is earned only by genuine per-node variance. Capability mismatches warn loudly, never silently no-op (`capabilities.ts` is the source of truth; docs derive from it — #2116). +- **§implicit-behavior** — a new implicit behavior (auto-anything) must be documented in the canonical behavior list, individually defeatable, and fail-safe — convenience alone never qualifies. + ## Open questions (no stance yet) These are direction calls we haven't made. PRs that touch these areas should surface the question for explicit decision rather than be silently accepted or rejected. The workflow may add to this list as new questions appear. diff --git a/.archon/scripts/__tests__/marketplace-fetch-source.test.ts b/.archon/scripts/__tests__/marketplace-fetch-source.test.ts new file mode 100644 index 0000000000..dd6da9a671 --- /dev/null +++ b/.archon/scripts/__tests__/marketplace-fetch-source.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'bun:test'; +import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { spawnSync } from 'node:child_process'; + +const SCRIPT = resolve(import.meta.dir, '../marketplace-fetch-source.ts'); + +interface FetchOutput { + files: string[]; + errors: string[]; +} + +function runFetch(entryJson: Record): { output: FetchOutput; stderr: string; exitCode: number } { + const artifactsDir = mkdtempSync(join(tmpdir(), 'fetch-test-')); + try { + mkdirSync(join(artifactsDir, 'source'), { recursive: true }); + writeFileSync(join(artifactsDir, 'entry.json'), JSON.stringify(entryJson)); + + const result = spawnSync('bun', [SCRIPT], { + env: { ...process.env, ARTIFACTS_DIR: artifactsDir }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const exitCode = result.status ?? 1; + const stdout = result.stdout?.toString() ?? ''; + const stderr = result.stderr?.toString() ?? ''; + + let output: FetchOutput = { files: [], errors: [] }; + if (stdout) { + try { + output = JSON.parse(stdout); + } catch {} + } + + return { output, stderr, exitCode }; + } finally { + rmSync(artifactsDir, { recursive: true, force: true }); + } +} + +describe('marketplace-fetch-source: guard for missing sourceUrl/sha', () => { + it('exits 0 with empty files when sourceUrl is missing', () => { + const { output, stderr, exitCode } = runFetch({ sha: 'abc123' }); + expect(exitCode).toBe(0); + expect(output.files).toHaveLength(0); + expect(stderr).toContain('sourceUrl'); + expect(output.errors.length).toBeGreaterThan(0); + expect(output.errors[0]).toContain('sourceUrl'); + }); + + it('exits 0 with empty files when sha is missing', () => { + const { output, stderr, exitCode } = runFetch({ sourceUrl: 'https://github.com/owner/repo/blob/main/path' }); + expect(exitCode).toBe(0); + expect(output.files).toHaveLength(0); + expect(stderr).toContain('sha'); + expect(output.errors.length).toBeGreaterThan(0); + expect(output.errors[0]).toContain('sha'); + }); + + it('exits 0 with empty files when both sourceUrl and sha are missing', () => { + const { output, stderr, exitCode } = runFetch({}); + expect(exitCode).toBe(0); + expect(output.files).toHaveLength(0); + expect(stderr).toContain('sourceUrl'); + expect(stderr).toContain('sha'); + expect(output.errors.length).toBeGreaterThan(0); + }); + + it('does not trigger guard when entry.json has both sourceUrl and sha', () => { + // Unrecognized URL makes the script stop deterministically at URL validation — no network. + const { stderr, exitCode } = runFetch({ + sourceUrl: 'https://example.com/not-a-github-url', + sha: 'abc123def456', + }); + expect(stderr).not.toContain('missing required field'); + expect(stderr).toContain('Unrecognized sourceUrl format'); + expect(exitCode).toBe(1); + }); +}); + +describe('marketplace-fetch-source: missing entry.json', () => { + it('exits 1 when entry.json is absent', () => { + const artifactsDir = mkdtempSync(join(tmpdir(), 'fetch-test-')); + try { + const result = spawnSync('bun', [SCRIPT], { + env: { ...process.env, ARTIFACTS_DIR: artifactsDir }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + expect(result.status ?? 1).toBe(1); + } finally { + rmSync(artifactsDir, { recursive: true, force: true }); + } + }); +}); diff --git a/.archon/scripts/marketplace-fetch-source.ts b/.archon/scripts/marketplace-fetch-source.ts index cd9affdc1c..774d21f05f 100644 --- a/.archon/scripts/marketplace-fetch-source.ts +++ b/.archon/scripts/marketplace-fetch-source.ts @@ -21,8 +21,8 @@ if (!existsSync(entryPath)) { } interface MarketplaceEntry { - sourceUrl: string; - sha: string; + sourceUrl?: string; + sha?: string; } const entry = JSON.parse(readFileSync(entryPath, 'utf8')) as MarketplaceEntry; @@ -34,6 +34,21 @@ mkdirSync(sourceDir, { recursive: true }); const errors: string[] = []; const files: string[] = []; +// Guard: sourceUrl/sha are required. If missing, output empty result so +// downstream nodes proceed gracefully (e.g., PR without a marketplace entry). +if (!sourceUrl || !sha) { + const missing: string[] = []; + if (!sourceUrl) missing.push('sourceUrl'); + if (!sha) missing.push('sha'); + const msg = `entry.json is missing required field(s): ${missing.join(', ')}. ` + + 'This PR likely does not add a marketplace entry with source files. ' + + 'Skipping source fetch.'; + process.stderr.write(msg + '\n'); + errors.push(msg); + console.log(JSON.stringify({ files, errors })); + process.exit(0); +} + // Parse GitHub blob/tree URL into owner/repo/path const blobMatch = sourceUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+)$/); const treeMatch = sourceUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/[^/]+\/(.+)$/); diff --git a/.archon/workflows/defaults/archon-fix-github-issue.yaml b/.archon/workflows/defaults/archon-fix-github-issue.yaml index 6118463cdf..89075765c5 100644 --- a/.archon/workflows/defaults/archon-fix-github-issue.yaml +++ b/.archon/workflows/defaults/archon-fix-github-issue.yaml @@ -39,10 +39,11 @@ nodes: - id: fetch-issue bash: | - # Strip quotes, whitespace, markdown backticks from AI output - ISSUE_NUM=$(echo "$extract-issue-number.output" | tr -d "'\"\`\n " | grep -oE '[0-9]+' | head -1) + # $extract-issue-number.output is injected pre-quoted by Archon — do NOT + # wrap it in double quotes (that corrupts the value; see issue #1884). + ISSUE_NUM=$(echo $extract-issue-number.output | grep -oE '[0-9]+' | head -1) if [ -z "$ISSUE_NUM" ]; then - echo "Failed to extract issue number from: $extract-issue-number.output" >&2 + echo "Failed to extract issue number from:" $extract-issue-number.output >&2 exit 1 fi gh issue view "$ISSUE_NUM" --json title,body,labels,comments,state,url,author diff --git a/.archon/workflows/defaults/archon-idea-to-pr.yaml b/.archon/workflows/defaults/archon-idea-to-pr.yaml index 38120b897b..8334885867 100644 --- a/.archon/workflows/defaults/archon-idea-to-pr.yaml +++ b/.archon/workflows/defaults/archon-idea-to-pr.yaml @@ -74,77 +74,14 @@ nodes: context: fresh # ═══════════════════════════════════════════════════════════════════ - # PHASE 6: CODE REVIEW - # ═══════════════════════════════════════════════════════════════════ - - - id: verify-pr-base - bash: | - set -euo pipefail - HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD) - PR_NUMBER=$(gh pr list --head "$HEAD_BRANCH" --state open --json number -q '.[0].number') - if [ -z "$PR_NUMBER" ]; then - echo "No open PR found for branch $HEAD_BRANCH" >&2 - exit 1 - fi - EXPECTED="$BASE_BRANCH" - ACTUAL=$(gh pr view "$PR_NUMBER" --json baseRefName -q '.baseRefName') - if [ "$ACTUAL" != "$EXPECTED" ]; then - echo "Base mismatch on PR #$PR_NUMBER: expected=$EXPECTED actual=$ACTUAL — re-targeting" >&2 - gh pr edit "$PR_NUMBER" --base "$EXPECTED" - else - echo "PR base verified: $EXPECTED" - fi - depends_on: [finalize-pr] - - - id: review-scope - command: archon-pr-review-scope - depends_on: [verify-pr-base] - context: fresh - - - id: sync - command: archon-sync-pr-with-main - depends_on: [review-scope] - context: fresh - - - id: code-review - command: archon-code-review-agent - depends_on: [sync] - context: fresh - - - id: error-handling - command: archon-error-handling-agent - depends_on: [sync] - context: fresh - - - id: test-coverage - command: archon-test-coverage-agent - depends_on: [sync] - context: fresh - - - id: comment-quality - command: archon-comment-quality-agent - depends_on: [sync] - context: fresh - - - id: docs-impact - command: archon-docs-impact-agent - depends_on: [sync] - context: fresh - - - id: synthesize - command: archon-synthesize-review - depends_on: [code-review, error-handling, test-coverage, comment-quality, docs-impact] - trigger_rule: one_success - context: fresh - - # ═══════════════════════════════════════════════════════════════════ - # PHASE 7: FIX REVIEW ISSUES + # PHASE 6-7: CODE REVIEW + FIX (shared building block) + # Inlines archon-review-block: verify-pr-base -> review-scope -> sync -> + # 5 parallel review agents -> synthesize -> implement-fixes. # ═══════════════════════════════════════════════════════════════════ - - id: implement-fixes - command: archon-implement-review-fixes - depends_on: [synthesize] - context: fresh + - id: review + include: archon-review-block + depends_on: [finalize-pr] # ═══════════════════════════════════════════════════════════════════ # PHASE 8: FINAL SUMMARY & FOLLOW-UP @@ -152,5 +89,5 @@ nodes: - id: workflow-summary command: archon-workflow-summary - depends_on: [implement-fixes] + depends_on: [review] context: fresh diff --git a/.archon/workflows/defaults/archon-issue-review-full.yaml b/.archon/workflows/defaults/archon-issue-review-full.yaml index 650f592e5a..1f03b1e182 100644 --- a/.archon/workflows/defaults/archon-issue-review-full.yaml +++ b/.archon/workflows/defaults/archon-issue-review-full.yaml @@ -30,83 +30,20 @@ nodes: context: fresh # ═══════════════════════════════════════════════════════════════════ - # PHASE 3: CODE REVIEW + # PHASE 3-4: CODE REVIEW + FIX (shared building block) + # Inlines archon-review-block: verify-pr-base -> review-scope -> sync -> + # 5 parallel review agents -> synthesize -> implement-fixes. # ═══════════════════════════════════════════════════════════════════ - - id: verify-pr-base - bash: | - set -euo pipefail - HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD) - PR_NUMBER=$(gh pr list --head "$HEAD_BRANCH" --state open --json number -q '.[0].number') - if [ -z "$PR_NUMBER" ]; then - echo "No open PR found for branch $HEAD_BRANCH" >&2 - exit 1 - fi - EXPECTED="$BASE_BRANCH" - ACTUAL=$(gh pr view "$PR_NUMBER" --json baseRefName -q '.baseRefName') - if [ "$ACTUAL" != "$EXPECTED" ]; then - echo "Base mismatch on PR #$PR_NUMBER: expected=$EXPECTED actual=$ACTUAL — re-targeting" >&2 - gh pr edit "$PR_NUMBER" --base "$EXPECTED" - else - echo "PR base verified: $EXPECTED" - fi + - id: review + include: archon-review-block depends_on: [implement] - - id: review-scope - command: archon-pr-review-scope - depends_on: [verify-pr-base] - context: fresh - - - id: sync - command: archon-sync-pr-with-main - depends_on: [review-scope] - context: fresh - - - id: code-review - command: archon-code-review-agent - depends_on: [sync] - context: fresh - - - id: error-handling - command: archon-error-handling-agent - depends_on: [sync] - context: fresh - - - id: test-coverage - command: archon-test-coverage-agent - depends_on: [sync] - context: fresh - - - id: comment-quality - command: archon-comment-quality-agent - depends_on: [sync] - context: fresh - - - id: docs-impact - command: archon-docs-impact-agent - depends_on: [sync] - context: fresh - - - id: synthesize - command: archon-synthesize-review - depends_on: [code-review, error-handling, test-coverage, comment-quality, docs-impact] - trigger_rule: one_success - context: fresh - - # ═══════════════════════════════════════════════════════════════════ - # PHASE 4: FIX REVIEW ISSUES - # ═══════════════════════════════════════════════════════════════════ - - - id: implement-fixes - command: archon-implement-review-fixes - depends_on: [synthesize] - context: fresh - # ═══════════════════════════════════════════════════════════════════ # PHASE 5: FINAL SUMMARY # ═══════════════════════════════════════════════════════════════════ - id: summary command: archon-workflow-summary - depends_on: [implement-fixes] + depends_on: [review] context: fresh diff --git a/.archon/workflows/defaults/archon-plan-to-pr.yaml b/.archon/workflows/defaults/archon-plan-to-pr.yaml index c0abe995e9..88791c517d 100644 --- a/.archon/workflows/defaults/archon-plan-to-pr.yaml +++ b/.archon/workflows/defaults/archon-plan-to-pr.yaml @@ -64,83 +64,20 @@ nodes: context: fresh # ═══════════════════════════════════════════════════════════════════ - # PHASE 6: CODE REVIEW + # PHASE 6-7: CODE REVIEW + FIX (shared building block) + # Inlines archon-review-block: verify-pr-base -> review-scope -> sync -> + # 5 parallel review agents -> synthesize -> implement-fixes. # ═══════════════════════════════════════════════════════════════════ - - id: verify-pr-base - bash: | - set -euo pipefail - HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD) - PR_NUMBER=$(gh pr list --head "$HEAD_BRANCH" --state open --json number -q '.[0].number') - if [ -z "$PR_NUMBER" ]; then - echo "No open PR found for branch $HEAD_BRANCH" >&2 - exit 1 - fi - EXPECTED="$BASE_BRANCH" - ACTUAL=$(gh pr view "$PR_NUMBER" --json baseRefName -q '.baseRefName') - if [ "$ACTUAL" != "$EXPECTED" ]; then - echo "Base mismatch on PR #$PR_NUMBER: expected=$EXPECTED actual=$ACTUAL — re-targeting" >&2 - gh pr edit "$PR_NUMBER" --base "$EXPECTED" - else - echo "PR base verified: $EXPECTED" - fi + - id: review + include: archon-review-block depends_on: [finalize-pr] - - id: review-scope - command: archon-pr-review-scope - depends_on: [verify-pr-base] - context: fresh - - - id: sync - command: archon-sync-pr-with-main - depends_on: [review-scope] - context: fresh - - - id: code-review - command: archon-code-review-agent - depends_on: [sync] - context: fresh - - - id: error-handling - command: archon-error-handling-agent - depends_on: [sync] - context: fresh - - - id: test-coverage - command: archon-test-coverage-agent - depends_on: [sync] - context: fresh - - - id: comment-quality - command: archon-comment-quality-agent - depends_on: [sync] - context: fresh - - - id: docs-impact - command: archon-docs-impact-agent - depends_on: [sync] - context: fresh - - - id: synthesize - command: archon-synthesize-review - depends_on: [code-review, error-handling, test-coverage, comment-quality, docs-impact] - trigger_rule: one_success - context: fresh - - # ═══════════════════════════════════════════════════════════════════ - # PHASE 7: FIX REVIEW ISSUES - # ═══════════════════════════════════════════════════════════════════ - - - id: implement-fixes - command: archon-implement-review-fixes - depends_on: [synthesize] - context: fresh - # ═══════════════════════════════════════════════════════════════════ # PHASE 8: FINAL SUMMARY & FOLLOW-UP # ═══════════════════════════════════════════════════════════════════ - id: workflow-summary command: archon-workflow-summary - depends_on: [implement-fixes] + depends_on: [review] context: fresh diff --git a/.archon/workflows/defaults/archon-review-block.yaml b/.archon/workflows/defaults/archon-review-block.yaml new file mode 100644 index 0000000000..706f2638bd --- /dev/null +++ b/.archon/workflows/defaults/archon-review-block.yaml @@ -0,0 +1,75 @@ +name: archon-review-block +description: | + Building block — included by other workflows via `include: archon-review-block`; + not intended for standalone runs. Provides the shared 9-node PR review sub-DAG: + verify PR base -> scope -> sync -> 5 parallel review agents -> synthesize -> implement fixes. + + Included by archon-idea-to-pr, archon-plan-to-pr, and archon-issue-review-full so the + review flow lives in exactly one file. The including workflow supplies the upstream + dependency (the node the review should run after); `$review.output` on the parent + resolves to this block's terminal node (implement-fixes). + +nodes: + - id: verify-pr-base + bash: | + set -euo pipefail + HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD) + PR_NUMBER=$(gh pr list --head "$HEAD_BRANCH" --state open --json number -q '.[0].number') + if [ -z "$PR_NUMBER" ]; then + echo "No open PR found for branch $HEAD_BRANCH" >&2 + exit 1 + fi + EXPECTED="$BASE_BRANCH" + ACTUAL=$(gh pr view "$PR_NUMBER" --json baseRefName -q '.baseRefName') + if [ "$ACTUAL" != "$EXPECTED" ]; then + echo "Base mismatch on PR #$PR_NUMBER: expected=$EXPECTED actual=$ACTUAL — re-targeting" >&2 + gh pr edit "$PR_NUMBER" --base "$EXPECTED" + else + echo "PR base verified: $EXPECTED" + fi + + - id: review-scope + command: archon-pr-review-scope + depends_on: [verify-pr-base] + context: fresh + + - id: sync + command: archon-sync-pr-with-main + depends_on: [review-scope] + context: fresh + + - id: code-review + command: archon-code-review-agent + depends_on: [sync] + context: fresh + + - id: error-handling + command: archon-error-handling-agent + depends_on: [sync] + context: fresh + + - id: test-coverage + command: archon-test-coverage-agent + depends_on: [sync] + context: fresh + + - id: comment-quality + command: archon-comment-quality-agent + depends_on: [sync] + context: fresh + + - id: docs-impact + command: archon-docs-impact-agent + depends_on: [sync] + context: fresh + + - id: synthesize + command: archon-synthesize-review + depends_on: [code-review, error-handling, test-coverage, comment-quality, docs-impact] + trigger_rule: one_success + context: fresh + + - id: implement-fixes + command: archon-implement-review-fixes + depends_on: [synthesize] + context: fresh diff --git a/.archon/workflows/defaults/archon-workflow-builder.yaml b/.archon/workflows/defaults/archon-workflow-builder.yaml index 66b75ddf06..49f129be0c 100644 --- a/.archon/workflows/defaults/archon-workflow-builder.yaml +++ b/.archon/workflows/defaults/archon-workflow-builder.yaml @@ -6,6 +6,14 @@ description: | Does: Scans codebase -> extracts intent (JSON) -> generates YAML -> validates -> saves. NOT for: Editing existing workflows or creating non-workflow files. +# Run in the live checkout, not in a fresh sub-worktree. Without this, every +# archon-workflow-builder invocation creates an isolated sub-worktree and +# writes the generated YAML there — the file never reaches the caller's +# .archon/workflows/, so the run reports success while the user's repo gains +# nothing. Closes #1220. +worktree: + enabled: false + nodes: - id: scan-codebase bash: | @@ -61,8 +69,8 @@ nodes: 5. Whether this should be a simple DAG or include a loop node Be specific and concrete. Each proposed node should have a clear type - (bash, prompt, command, script, loop, or approval) and a one-line - description of what it does. + (bash, prompt, command, script, loop, loop_group, approval, or cancel) and + a one-line description of what it does. model: small allowed_tools: [] output_format: @@ -116,7 +124,7 @@ nodes: nodes: - id: node-id-kebab-case - # Choose ONE of: prompt, bash, command, script, loop, approval + # Choose ONE of: prompt, bash, command, script, loop, loop_group, approval, cancel # --- prompt node (AI-executed) --- prompt: | @@ -151,11 +159,24 @@ nodes: max_iterations: 10 fresh_context: true # optional: reset context each iteration + # --- loop_group node (iterate a multi-node sub-DAG until done) --- + loop_group: + until: COMPLETION_SIGNAL + max_iterations: 5 + nodes: # sealed sub-DAG body, re-run each iteration + - id: body-step + prompt: | + Do one unit of work. Emit COMPLETION_SIGNAL when finished. + depends_on: [] + # --- approval node (human gate — pauses workflow) --- approval: message: "Review the plan above. Approve to continue." # capture_response: true # store reviewer comment as $.output + # --- cancel node (terminate the run with a reason; no AI) --- + cancel: "Reason the workflow was terminated" + # Common options for all node types: depends_on: [other-node-id] # DAG edges when: "$.output == 'value'" # conditional execution diff --git a/.archon/workflows/experimental/archon-release.yaml b/.archon/workflows/experimental/archon-release.yaml index 9be511c089..e1a758d738 100644 --- a/.archon/workflows/experimental/archon-release.yaml +++ b/.archon/workflows/experimental/archon-release.yaml @@ -916,7 +916,7 @@ nodes: - id: final-summary script: | // Defensive: this node runs with trigger_rule: all_done, so any upstream - // node may have been skipped or failed. Empty $node.output substitutions + // node may have been skipped or failed. Empty $.output substitutions // resolve to "" and would break JSON.parse if not guarded. const safeJson = (raw) => { const s = raw.trim(); diff --git a/.archon/workflows/test-workflows/e2e-codex-smoke.yaml b/.archon/workflows/test-workflows/e2e-codex-smoke.yaml index f24336b36e..cf22dc721d 100644 --- a/.archon/workflows/test-workflows/e2e-codex-smoke.yaml +++ b/.archon/workflows/test-workflows/e2e-codex-smoke.yaml @@ -3,7 +3,7 @@ name: e2e-codex-smoke description: "E2E smoke test for Codex provider. Runs a simple prompt + structured output node." provider: codex -model: gpt-5.2 +model: gpt-5.6-luna nodes: - id: simple diff --git a/.archon/workflows/test-workflows/e2e-container-smoke.yaml b/.archon/workflows/test-workflows/e2e-container-smoke.yaml new file mode 100644 index 0000000000..4f3d6fef59 --- /dev/null +++ b/.archon/workflows/test-workflows/e2e-container-smoke.yaml @@ -0,0 +1,60 @@ +# E2E smoke test — container isolation for folder projects (Phase B, #2153) +# Bash-node-only, NO AI credential. Runs against a folder project with --container: +# bun run cli workflow run e2e-container-smoke --folder --container --cwd "smoke" +# Proves the deterministic nodes exec INSIDE the managed runner container over an +# overlay of the folder root, and that an overlay write is visible in the merged +# view across separate exec calls within the run. +# +# The HOST-folder-unchanged and clean-teardown assertions live in the CI job +# (.github/workflows/e2e-smoke.yml): they can only be observed from the host after +# the container is gone, because Phase B discards the overlay on teardown. +name: e2e-container-smoke +description: "Container-isolation smoke. Asserts bash nodes run in-container (hostname == container id, overlay mounts present) and an overlay write is visible in the merged view. Host-unchanged + teardown are asserted by the CI job." + +nodes: + # Prove the deterministic node executed INSIDE the runner container, not on the host. + - id: assert-in-container + bash: | + # Docker sets an unset --hostname to the 12-char short container id and writes + # it to /etc/hostname (the `hostname` binary is absent from the slim base). + # On the host (CI runner / dev machine) the hostname never matches this shape, + # so a 12-hex value is positive proof the node exec'd inside the container. + hn="$(cat /etc/hostname 2>/dev/null || hostname)" + echo "hostname=$hn" + if ! printf '%s' "$hn" | grep -Eq '^[0-9a-f]{12}$'; then + echo "FAIL: hostname '$hn' is not a container short-id — node did not run in the container" + exit 1 + fi + # The overlay mount points only exist inside the archon-runner container. + if [ ! -d /mnt/lower ] || [ ! -d /mnt/upper ]; then + echo "FAIL: overlay mount points /mnt/lower + /mnt/upper missing — not the overlay container" + exit 1 + fi + echo "PASS: bash node ran inside the managed container (hostname == container id)" + + # Write a file into the workspace (folder root == overlay merged mount). + - id: overlay-write + bash: | + marker="container-smoke-overlay-marker.txt" + printf 'archon-overlay-smoke\n' > "$marker" + echo "wrote $(pwd)/$marker" + depends_on: [assert-in-container] + trigger_rule: all_success + + # A SEPARATE docker exec must see the write through the same overlay (merged view). + - id: overlay-read + bash: | + marker="container-smoke-overlay-marker.txt" + if [ ! -f "$marker" ]; then + echo "FAIL: $marker not visible in merged overlay view from a later node" + exit 1 + fi + content="$(cat "$marker")" + echo "merged-view content: $content" + if [ "$content" != "archon-overlay-smoke" ]; then + echo "FAIL: unexpected marker content '$content'" + exit 1 + fi + echo "PASS: overlay write visible in merged view across exec calls" + depends_on: [overlay-write] + trigger_rule: all_success diff --git a/.archon/workflows/test-workflows/e2e-mixed-providers.yaml b/.archon/workflows/test-workflows/e2e-mixed-providers.yaml index 9f5c408a37..2abcfbd23c 100644 --- a/.archon/workflows/test-workflows/e2e-mixed-providers.yaml +++ b/.archon/workflows/test-workflows/e2e-mixed-providers.yaml @@ -18,7 +18,7 @@ nodes: - id: codex-node prompt: "Say 'codex-ok' and nothing else." provider: codex - model: gpt-5.2 + model: gpt-5.6-luna idle_timeout: 30000 # 3. Assert both providers returned output diff --git a/.archon/workflows/test-workflows/e2e-structured-output.yaml b/.archon/workflows/test-workflows/e2e-structured-output.yaml index 8b0c3fbc00..0fc0bd063a 100644 --- a/.archon/workflows/test-workflows/e2e-structured-output.yaml +++ b/.archon/workflows/test-workflows/e2e-structured-output.yaml @@ -34,7 +34,7 @@ nodes: - id: use-fields depends_on: [classify] bash: | - # No surrounding quotes: $node.output.field is injected as a shell-safe + # No surrounding quotes: a $.output.field ref is injected as a shell-safe # single-quoted literal already (escapedForBash), so the value's quoting # is provided by the substitution. sentiment=$classify.output.sentiment diff --git a/.claude/docs/architecture-deep-dive.md b/.claude/docs/architecture-deep-dive.md index d5e542b59b..cd5f34ac0d 100644 --- a/.claude/docs/architecture-deep-dive.md +++ b/.claude/docs/architecture-deep-dive.md @@ -86,7 +86,7 @@ User message starts with /workflow IF isLoopWorkflow: → for i = 1..max_iterations: - → substituteVariables(prompt) → aiClient.sendQuery() + → substituteWorkflowVariables(prompt) → aiClient.sendQuery() → detectCompletionSignal(output, until) → break if found IF isStepWorkflow: diff --git a/.claude/docs/workflow-yaml-reference.md b/.claude/docs/workflow-yaml-reference.md index 70c88a22be..b96cab1b42 100644 --- a/.claude/docs/workflow-yaml-reference.md +++ b/.claude/docs/workflow-yaml-reference.md @@ -228,13 +228,11 @@ when: "$classify.output.complexity != 'trivial'" | `$PLAN` | Previous plan from session metadata | | `$IMPLEMENTATION_SUMMARY` | Previous execution summary | -### Positional Variables (command handler) +### User Message Variables | Variable | Replaced With | |----------|--------------| -| `$1` through `$9` | Positional arguments split from user message | -| `$ARGUMENTS` | All arguments joined | -| `\$` | Literal `$` (escape) | +| `$ARGUMENTS` / `$USER_MESSAGE` | The user's whole trigger message (positional `$1`–`$9` are not supported) | ### DAG Node Output References diff --git a/.claude/skills/archon/SKILL.md b/.claude/skills/archon/SKILL.md index c844ad0eb9..17e92ebbe2 100644 --- a/.claude/skills/archon/SKILL.md +++ b/.claude/skills/archon/SKILL.md @@ -185,8 +185,8 @@ Archon uses a single workflow format: **nodes** (DAG). Workflows are YAML files ```yaml name: my-workflow description: What this workflow does -provider: claude # Optional: 'claude' or 'codex' -model: sonnet # Optional: model override +provider: claude # Optional: 'claude', 'codex', 'pi', ... (default: from config) +model: medium # Optional: tier keyword (small|medium|large), @alias, or literal model nodes: - id: first-node command: my-command # Loads .archon/commands/my-command.md @@ -197,7 +197,7 @@ nodes: ### Node Types -Each node has exactly ONE of: `command`, `prompt`, `bash`, `script`, `loop`, `approval`, or `cancel`. +Each node has exactly ONE of: `command`, `prompt`, `bash`, `script`, `loop`, `loop_group`, `approval`, or `cancel`. **Command node** — runs a `.archon/commands/*.md` file: ```yaml @@ -247,6 +247,23 @@ Each node has exactly ONE of: `command`, `prompt`, `bash`, `script`, `loop`, `ap until_bash: "bun run test" # Optional: exit 0 = done ``` +**Loop group node** — repeats a multi-node sub-DAG body per iteration (implement → test → review as one repeated cycle): +```yaml +- id: fix-cycle + loop_group: + nodes: + - id: implement + prompt: "Implement the next fix. Last review: $LOOP_PREV.review.output" + - id: test + bash: "bun run test 2>&1 || true" + depends_on: [implement] + - id: review + prompt: "Review diff + tests: $test.output. All good? SHIP" + depends_on: [test] + until: SHIP + max_iterations: 6 +``` + **Approval node** — pauses the workflow for human review. Requires `interactive: true` at the workflow level for Web UI delivery: ```yaml interactive: true # workflow level — required for web UI @@ -300,13 +317,15 @@ For the full command authoring guide: Read `references/authoring-commands.md` | `$ARTIFACTS_DIR` | Pre-created directory for workflow artifacts | | `$BASE_BRANCH` | Base branch (auto-detected from git) | | `$WORKFLOW_ID` | Unique workflow run ID | -| `$nodeId.output` | Output from upstream node | +| `$nodeId.output` | Output from upstream node (`.field` access is strict — see variables ref) | +| `$LOOP_PREV_OUTPUT` / `$LOOP_PREV..output` | Previous loop / loop_group iteration output | +| `$LOOP_USER_INPUT` / `$REJECTION_REASON` | Human feedback at loop gates / approval rejections | Full variable reference: Read `references/variables.md` -### Advanced Features (Command/Prompt Nodes, Claude Only) +### Advanced Features (Command/Prompt Nodes) -`hooks` (tool interception), `mcp` (external tool servers), `skills` (domain knowledge injection), `output_format` (structured JSON output), `allowed_tools`/`denied_tools` (tool restrictions). +`output_format` (structured JSON output — all providers, schema-validated, node fails on miss), `hooks` (tool interception — Claude only), `mcp` (external tool servers — all providers except Pi), `skills` (per-node injection on Claude/Pi/OpenCode/Copilot; Codex via filesystem), `allowed_tools`/`denied_tools` (tool restrictions — all except Codex), `agents` (sub-agents — inline definitions on Claude; OpenCode/Copilot select CONFIGURED agents by name, not inline definitions), `persist_session` (cross-run AI memory), `output_type` (typed artifact sidecars). For details: Read `references/dag-advanced.md` diff --git a/.claude/skills/archon/examples/dag-workflow.yaml b/.claude/skills/archon/examples/dag-workflow.yaml index 50fcbdada1..0063786a11 100644 --- a/.claude/skills/archon/examples/dag-workflow.yaml +++ b/.claude/skills/archon/examples/dag-workflow.yaml @@ -72,7 +72,7 @@ nodes: Determine if this is a bug fix or a new feature. depends_on: [fetch-issue] - model: haiku + model: small # tier keyword — resolved from config; prefer over hardcoded model ids allowed_tools: [] output_format: type: object diff --git a/.claude/skills/archon/references/cli-commands.md b/.claude/skills/archon/references/cli-commands.md index 64283e7854..70fab88da1 100644 --- a/.claude/skills/archon/references/cli-commands.md +++ b/.claude/skills/archon/references/cli-commands.md @@ -35,12 +35,15 @@ archon workflow run archon-assist "Investigate the flaky test" --detach | `--no-worktree` | Skip isolation — run in the live checkout | | `--resume` | Resume the last failed run of this workflow at this cwd (skips completed nodes) | | `--detach` | Run in a detached background child and return immediately (find the run via `workflow runs`). Pair with `--json` for a structured ack. Child output goes to `~/.archon/logs/`. In the web console the run appears in the Workflow dock (listed by project) but may not update **live** until a refetch — it runs out-of-process and doesn't stream to the console's live event feed. | +| `--folder` | Register the current NON-git directory as a **folder project** and run in place (no worktree). For multi-repo roots and plain ops folders. Incompatible with `--branch`/`--from`; workflows pinned `worktree.enabled: true` are rejected on folder projects | | `--cwd ` | Working directory override | +| `--verbose` / `-v` | Debug logs + tool-level workflow progress events on stderr | **Flag conflicts** (errors): - `--branch` + `--no-worktree` - `--from` + `--no-worktree` - `--resume` + `--branch` +- `--branch`/`--from` on a folder project **Default behavior** (no flags): Auto-creates a worktree with branch name `{workflow-name}-{timestamp}`. @@ -139,12 +142,23 @@ archon workflow cleanup # Default: 7 days archon workflow cleanup 30 # Custom: 30 days ``` +### `archon workflow reset-sessions [--scope ] [--node ] [--yes] [--json]` + +Clear persisted per-node AI sessions (`persist_session` cross-run memory) for a workflow. Without `--scope`, wipes EVERY scope and requires `--yes`. `--node` narrows to one node's session. + +```bash +archon workflow reset-sessions standup-report --scope +archon workflow reset-sessions standup-report --node report --yes # all scopes, one node +``` + +Chat equivalent: `/workflow reset-sessions []` (auto-scoped to the current conversation). + ### `archon workflow event emit --run-id --type [--data ]` -Emit a workflow event to a running workflow. Used inside loop prompts to signal state (e.g. "checkpoint written") for observability. Rarely invoked from the shell directly. +Emit a workflow event into the run's audit log (`workflow_events`). **Write-only observability** — it does NOT steer the run: loop completion is decided solely by the `until` signal and `until_bash`, never by emitted events. Used inside loop prompts to record progress markers (e.g. "checkpoint written"). ```bash -archon workflow event emit --run-id abc123 --type checkpoint --data '{"step":"plan"}' +archon workflow event emit --run-id abc123 --type task_activity --data '{"step":"plan"}' ``` ### `archon continue [flags] [message]` @@ -241,6 +255,15 @@ archon version # Database: sqlite ``` +### `archon doctor` + +Verify the Archon setup: Claude binary resolution, Codex binary resolution (when Codex is configured or an OpenAI credential is connected), Pi auth, `gh` auth, OpenCode runtime SDK presence (with `--full`, or when OpenCode is the configured assistant), database connectivity, connected providers, workspace writability, bundled defaults, and adapter configuration (Slack/Telegram). Run this first when workflows fail with environment-shaped errors (binary not found, auth failures). + +```bash +archon doctor +archon doctor --full # also probe the OpenCode runtime SDK (module presence only — never boots the runtime) +``` + ### `archon setup [--spawn]` Interactive setup wizard for database, AI providers, and platform connections. diff --git a/.claude/skills/archon/references/dag-advanced.md b/.claude/skills/archon/references/dag-advanced.md index 63a83e9101..007e22a1b5 100644 --- a/.claude/skills/archon/references/dag-advanced.md +++ b/.claude/skills/archon/references/dag-advanced.md @@ -1,25 +1,29 @@ -# Advanced Features: Hooks, MCP, Skills, Retry +# Advanced Features: Hooks, MCP, Skills, Retry, Sessions, Typed Artifacts -These features are available on **command and prompt nodes** (hooks, MCP, skills, tool restrictions, `output_format`, `agents`, Claude SDK options) and **command, prompt, bash, and script nodes** (retry). Loop nodes do not support these features (`retry` on loop nodes is a hard error; others are silently ignored). Bash and script nodes silently ignore AI-specific fields (a loader warning lists the ignored fields). +Hooks, MCP, skills, tool restrictions, `output_format`, `agents`, and Claude SDK options apply to **command and prompt nodes** (including loop_group *body* nodes of those types). `retry` applies to command/prompt by default and to bash/script with an explicit block (see §Retry). Loop/loop_group nodes support none of these directly (`retry` there is a hard error; the rest are silently ignored) — except `model`/`provider`, which they forward to iterations. Bash and script nodes ignore AI-specific fields with a loader warning. ## Provider Compatibility -| Feature | Claude (per-node) | Codex (per-node) | Codex (global) | -|---------|-------------------|------------------|----------------| -| `hooks` | Supported | Ignored | Not available | -| `mcp` | Supported | Ignored | `~/.codex/config.toml` `[mcp_servers.*]` | -| `skills` | Supported | Ignored | `~/.agents/skills/` or `.agents/skills/` | -| `allowed_tools` / `denied_tools` | Supported | Ignored | `enabled_tools` / `disabled_tools` per MCP server in config.toml | -| `output_format` | Supported | Supported | — | -| `retry` | Supported | Supported | — | -| `model` / `provider` per-node | Supported | Supported | — | +| Feature | Claude (per-node) | Codex (per-node) | Pi (per-node) | Codex (global) | +|---------|-------------------|------------------|---------------|----------------| +| `hooks` | Supported | Ignored + warn | Not available | Not available | +| `mcp` | Supported | **Supported** (translated to `mcp_servers` config overrides) | Not available | `~/.codex/config.toml` `[mcp_servers.*]` | +| `skills` | Supported | Informational (auto-discovers from `.agents/skills/`) | Supported | `~/.agents/skills/` or `.agents/skills/` | +| `allowed_tools` / `denied_tools` | Supported | Ignored | **Supported** | `enabled_tools` / `disabled_tools` per MCP server in config.toml | +| `output_format` | Enforced | Enforced | Best-effort (validated + up to 3 re-asks) | — | +| `retry` | Supported | Supported | Supported | — | +| `model` / `provider` per-node | Supported | Supported | Supported | — | +| `effort` / `thinking` | Supported | Use `modelReasoningEffort` | Supported (maps to thinking level) | — | +| `agents` / `sandbox` / `maxBudgetUsd` / `fallbackModel` | Supported | No | No | — | + +Community providers beyond Pi: **OpenCode** supports per-node `mcp`, `skills`, `agents` (configured-agent selection by name — NOT Claude-style inline definitions), and tool restrictions (no `hooks` — Archon's Claude-shaped hook field has no OpenCode translation site; effort/thinking via `opencode.json`, not per-node); **Copilot** supports per-node `mcp`, `skills`, `agents`, tool restrictions, and effort/thinking (no hooks). Hooks are Claude-only. See the five-provider matrix in `parameter-matrix.md` §Providers at a Glance. `sandbox`/`maxBudgetUsd`/`fallbackModel` remain Claude-only. ### Claude vs Codex: How Each Gets MCP and Skills **Claude**: MCP servers and skills are configured **per-node** in the workflow YAML via `mcp:` and `skills:` fields. Each node can have different MCP servers and skills. -**Codex**: MCP servers and skills are configured **globally** — they apply to all Codex nodes in the workflow: -- **MCP servers**: Add to `~/.codex/config.toml` (or `.codex/config.toml` in the repo): +**Codex**: per-node `mcp:` works (Archon translates the JSON config into Codex `mcp_servers` overrides). Skills and instructions are filesystem-global: +- **MCP servers (global alternative)**: Add to `~/.codex/config.toml` (or `.codex/config.toml` in the repo): ```toml [mcp_servers.github] command = "npx" @@ -27,12 +31,10 @@ These features are available on **command and prompt nodes** (hooks, MCP, skills env = { GITHUB_TOKEN = "your-token" } ``` Manage with: `codex mcp add `, `codex mcp list` -- **Skills**: Place in `~/.agents/skills//SKILL.md` (user-level) or `.agents/skills//SKILL.md` (repo-level). Codex discovers them automatically. +- **Skills**: Place in `~/.agents/skills//SKILL.md` (user-level) or `.agents/skills//SKILL.md` (repo-level). Codex discovers them automatically; a node's `skills:` list is informational for Codex. - **Custom instructions**: Place in `~/.codex/AGENTS.md` (global) or `AGENTS.md` in the repo root. -The Codex CLI picks up all of these automatically because Archon inherits the full process environment when spawning the CLI. No Archon configuration needed — just set up the Codex CLI config once. - -**Hooks** have no Codex equivalent — they are a Claude-only SDK feature for intercepting tool calls. +**Hooks** have no Codex/Pi equivalent — they are a Claude-only SDK feature for intercepting tool calls. --- @@ -151,7 +153,7 @@ Use `allowed_tools`/`denied_tools` for hard restrictions. Use hooks when you wan ## MCP (Model Context Protocol) Servers -> Claude only. Codex nodes log a warning and ignore MCP configuration. +> Claude, Codex, OpenCode, and Copilot all accept per-node `mcp:` (translated to each SDK's server config). Only Pi lacks MCP — Pi nodes log a warning and ignore it. Connect external tool servers to individual nodes. @@ -239,7 +241,7 @@ Combine `mcp:` with `allowed_tools: []` for nodes that should ONLY use MCP tools ## Skills -> Claude only. Codex nodes log a warning and ignore skills. +> Claude, Pi, OpenCode, and Copilot support per-node `skills:` injection. Codex discovers skills from the filesystem (`.agents/skills/`) — a node's `skills:` list is informational there. Preload domain knowledge into a node via Claude Code skills. @@ -292,7 +294,9 @@ Skills provide **knowledge** (how to do something). MCP provides **capability** ## Retry Configuration -Available on command, prompt, and bash nodes. **Not supported on loop nodes** (hard error at load time). +Available on command and prompt nodes (default-on for transient errors), and on bash/script nodes **only with an explicit `retry:` block**. **Not supported on loop/loop_group nodes** (hard error at load time — the loop manages its own iteration). + +> **Version note (#2088):** on builds before the #2088 fix, `retry:` on bash/script nodes was accepted by the schema but never executed at runtime — only command/prompt nodes actually retried. Check `archon version` / CHANGELOG if a bash retry appears to be ignored. ```yaml - id: deploy @@ -303,6 +307,8 @@ Available on command, prompt, and bash nodes. **Not supported on loop nodes** (h on_error: all # 'transient' (default) or 'all' ``` +For deterministic bash/script failures (a script that exits 1 reproducibly), retrying is pointless — `retry:` there is for flaky externals (network fetches, rate-limited APIs). Classification detail: a bash/script failure message is formatted ` failed [exit N]: `, so the classifier runs on your script's **stderr text** — the `exited with code` TRANSIENT pattern in the table below targets AI-CLI crash messages and does NOT match a bash/script non-zero exit. A subprocess `timeout` DOES classify TRANSIENT. Practical rule: rely on the default `on_error: transient` when failures surface as timeouts/rate-limit text on stderr; use `on_error: all` when the flaky failure mode produces generic stderr. + ### Error Classification | Category | Examples | Retried? | @@ -316,16 +322,58 @@ FATAL patterns take priority over TRANSIENT patterns in the same error message. ### Two-Layer Retry Stack 1. **SDK-level** (automatic): Built-in retry for API errors (behavior managed by the Claude/Codex SDK) -2. **Node-level** (configurable via `retry:`): Wraps the entire SDK call. Default when `retry:` is omitted: 2 retries, 3000ms base delay, transient errors only +2. **Node-level** (configurable via `retry:`): Wraps the entire SDK call. Default when `retry:` is omitted: AI nodes get 2 retries, 3000ms base delay, transient errors only; bash/script nodes get a single attempt (no default retries) + +Retried AI attempts fork the session — a retry never corrupts the original session, and structured-output re-asks (a separate mechanism, up to 3 for best-effort providers) run in fresh sessions. ### Idle Timeout -Separate from retry — controls how long a node can be idle (no output) before being aborted: +Separate from retry — controls how long a node can be **silent** (no streamed output) before being aborted. It's a deadlock detector, not a work limiter: the timer resets on every message, so it only fires when the subprocess goes completely quiet. ```yaml - id: long-running command: full-analysis - idle_timeout: 600000 # 10 minutes (default: 5 minutes / 300000ms) + idle_timeout: 3600000 # 60 minutes (default: 30 minutes / 1800000ms) ``` -For bash nodes, use `timeout:` instead (controls total script execution time, default: 120000ms). +For bash/script nodes, use `timeout:` instead (controls total script execution time, default: 120000ms). + +--- + +## Session Persistence (`persist_session`) + +Persist a node's AI session **across runs** of the same workflow, so a later run's node resumes with the earlier conversation's context. This is cross-RUN memory — distinct from `context: shared` (within-run session threading between sequential nodes). + +```yaml +name: standup-report +persist_sessions: true # workflow-level default for all eligible nodes + +nodes: + - id: gather + bash: "git log --since=yesterday --oneline" + - id: report + prompt: "Yesterday's commits: $gather.output. Write the standup update, consistent with prior days." + depends_on: [gather] + persist_session: true # node-level (redundant here — workflow default covers it) +``` + +Mechanics: +- Only `command`/`prompt` nodes are eligible. Not bash/script/approval/cancel/loop/loop_group (and not loop_group *body* nodes — body sessions reset per iteration). +- Sessions are keyed by `(workflow name, node id, scope, provider)`. The scope is the **conversation** — chat threads each get their own memory; CLI runs share a per-invocation-context scope. +- Requires a provider with the `sessionResume` capability (Claude/Codex/Pi/OpenCode all have it). A `persist_session: true` node on a non-resumable provider fails at load or run time — never silently downgrades. +- `context: 'fresh'` on the node opts it back out. +- **Cold resume**: if the provider can't restore the session (transcript gone, server restart), the node still runs — fresh — with a warning, plus pointers to prior typed artifacts (see below) so the agent can re-read what it lost. It does not fail and does not re-run. +- Clear persisted memory with `archon workflow reset-sessions [--node ] [--scope ]` (chat: `/workflow reset-sessions []`, auto-scoped to the conversation). + +## Typed Output Artifacts (`output_type`) + +Any node can declare `output_type: