Conversation
…nd (fixes #1591) (#1634) * fix(orchestrator): move system context to cacheable systemPrompt.append (fixes #1591) Prompt caching was broken because the orchestrator embedded its static system context (project list, workflows, routing rules) in the prompt parameter, which changes every turn. This caused the Anthropic API to rebuild the cache prefix on each request (high cache_creation_input_tokens, zero cache_read_input_tokens). Move the static orchestrator context into systemPrompt.append, which extends the Claude Code preset and is part of the cacheable system prompt prefix. The prompt parameter now contains only per-turn dynamic content (workflow results, thread context, user message, issue context, files). Changes: - Add buildOrchestratorSystemAppend() to prompt-builder.ts - Simplify buildFullPrompt() to user-facing content only - Set requestOptions.systemPrompt with preset + append in handleMessage() - Widen systemPrompt type in AgentRequestOptions and NodeConfig to accept the SDK's full union type (string | string[] | preset object) - Update tests for new prompt construction path * fix(orchestrator): move system context to cacheable systemPrompt.append Fixes #1591 * refactor: extract SystemPromptInput type alias to prevent drift Address CodeRabbit review: consolidate the duplicated systemPrompt union type into a named type (SystemPromptPreset + SystemPromptInput) so both AgentRequestOptions and NodeConfig reference a single definition. * fix: restore file modes (0755 → 0644) from rebase churn Restore 812 files that had their mode bits changed from 100644 to 100755 during a rebase. No content changes — mode-only fix. Addresses S4 from review feedback.
…uccess (#1425) (#1662) * fix(providers,workflows): treat Claude SDK stop_sequence success as success (#1425) The Claude Agent SDK's SDKResultSuccess type declares is_error as boolean (not literal false). When a model terminates via a configured stop sequence the SDK sets is_error: true while keeping subtype: 'success' — its encoding of "non-default termination, but not a failure". The claude provider forwarded is_error verbatim into MessageChunk.isError, so three downstream consumers (dag-executor main path, dag-executor loop branch, orchestrator-agent direct chat) misclassified clean stop_sequence terminations as node failures and produced the contradictory user-hostile error "Node '<X>' failed: SDK returned success" — even though the AI had already completed correctly and written its output. Multi-user impact on v0.3.9 / v0.3.11 across review-classify and synthesis pipelines. Changes: - claude/provider.ts: normalise is_error + subtype: 'success' as a clean result. Don't propagate isError downstream; log a debug-level claude.result_success_stop_sequence event for observability instead. - dag-executor.ts (main path + loop branch): defense-in-depth guard so third-party IAgentProvider implementations that forward the SDK pair raw can't reintroduce the same false-failure. - provider/dag-executor/orchestrator-agent tests: regression tests covering the stop_sequence success path; guard test ensures real error subtypes (error_max_turns) still propagate. Fixes #1425 * fix(orchestrator,providers,workflows): address review feedback for #1425 - Apply errorSubtype !== 'success' guard at orchestrator-agent.ts handleStreamMode and handleBatchMode result branches (defense-in-depth, mirrors dag-executor). Without this, a third-party IAgentProvider that forwards the SDK pair raw would surface a spurious error on direct chat and drop conversation output. Adds matching regression test. - Rename log event claude.result_success_stop_sequence -> claude.result_success_validated per CodeRabbit; aligns with {domain}.{action}_{state} pino convention. - Lock the new debug log into the provider regression test. - Drop (#1425) issue refs from production comments (rot risk per CLAUDE.md). - Rewrite loop-branch guard comment to be self-contained instead of cross- referencing the main-path guard 1000+ lines away. - Add CHANGELOG entry under [Unreleased] -> Fixed.
…owed_tools (#1605) (#1661) When a DAG node has `skills:` but no `allowed_tools:`, the AgentDefinition wrapper defaulted tools to `['Skill']` only, stripping all native Claude Code tools (Read, Bash, Write, etc.). Fix: omit the `tools` field on AgentDefinition when `options.tools` is undefined, letting the SDK provide its full default tool set. When `allowed_tools` is explicitly set, Skill is still appended to the list.
The SSH-to-HTTPS converter in normalizeRepoUrl() and registerRepository() only matched `git@github.com:` literally. Custom SSH host aliases, GitHub Enterprise, Gitea, GitLab, and Bitbucket SSH URLs were left unchanged, which produced workspace paths containing literal `git@<host>:` segments. On Windows the colon makes mkdir fail with ENOTDIR; on Unix the owner extraction is malformed. Replace the literal-host check with an SCP-style regex `/^git@([^:]+):(.+)$/` at both call sites. The github.com case still converts identically; new hosts (custom aliases, GHE, GitLab, Bitbucket) now produce path-safe HTTPS URLs. Closes #1614.
…t.field works for Pi (#1654) * fix(workflows): persist structuredOutput on NodeOutput so $node.output.field works for Pi When a provider parses fence-wrapped or preamble-prefixed JSON onto the result chunk (Pi/Minimax via tryParseStructuredOutput), the executor captured it locally but never persisted it onto NodeOutput. Downstream consumers (substituteNodeOutputRefs, condition-evaluator) then JSON.parse(output)'d the original prose-prefixed text, which threw, and $node.output.field resolved to empty. This persists structuredOutput on NodeOutput (single-shot and loop-terminal-iteration success paths) and teaches both consumers to prefer the parsed object over re-parsing prose. Falls back to JSON.parse(output) when structuredOutput is absent so Claude/Codex output_format-encoded NodeOutput rows (and older rows written before this field existed) keep working. Cross-resume rehydration of structuredOutput from event_data is out of scope here; resumed runs that re-execute downstream nodes will fall through to the JSON.parse path, which matches existing behavior. Closes #1571 * test(workflows): docstring the structuredOutput makeOutput fixtures
…#1675) The gate node was the most fragile part of the workflow. It used Pi/Minimax to return a structured JSON verdict that the DAG branched on, but Pi intermittently wrapped the JSON in markdown fences or prefixed reasoning prose, breaking the condition-evaluator's field extraction. When that happened, every downstream review aspect was silently skipped and the workflow exited 0 with no review posted — indistinguishable from a successful run where the gate legitimately declined. In practice the gate added no signal for hand-picked PRs from the morning standup brief: across two full days of usage (~13 runs) the gate returned "review" every single time. The decline/needs_split/unclear branches were never exercised. Removing the gate eliminates the failure mode without losing any verdict the workflow has actually produced. Changes: - Remove gate, approve-decline, post-decline, approve-unclear nodes from maintainer-review-pr.yaml. - Rewire review-classify to depend on [fetch-pr, fetch-diff]. - Drop read-context (only the gate command consumed it). - Simplify record-review: hardcode gate_verdict to "review" for back-compat with the standup brief's marker logic; drop the one_success join. - Drop interactive: true (no more approval gate). - Update synthesize and report commands to remove gate-decision references. - Delete the now-orphan maintainer-review-gate.md command. If we later wire up automated review on every open PR (where the direction/scope decline would matter), reintroduce the gate then — and this time with a hardened parser or Claude provider on the gate node.
… when no project context (#1618) * fix(server,workflows,web): surface bundled defaults on /api/workflows when no project context (#1173) GET /api/workflows short-circuits to an empty array when there is no `cwd` query param and no registered codebases. The handler never reaches discovery, so bundled defaults are not surfaced and the UI renders a misleading "Add workflow definitions to .archon/workflows/" empty state on first run — even though the bundled YAML files are present on disk. This change: - Threads `cwd: string | null` through `discoverWorkflows` and `discoverWorkflowsWithConfig`. When `cwd` is `null` the discovery function loads bundled + home scopes and skips the project step cleanly (no path-join with an empty cwd, no read-error noise). - Removes the early-return in the GET handler. When no project context exists, it now calls `discoverWorkflowsWithConfig(null, ...)` so the response carries the bundled set instead of `[]`. - Distinguishes the empty-state copy in `WorkflowList` so the rare case where the list is genuinely empty reads correctly. With a project selected: "No workflows found in this project. Add workflow definitions to .archon/workflows/ in the project root." Without a project: "No workflows are available. Bundled defaults should appear here automatically; if they do not, check that `defaults.loadDefaultWorkflows` is enabled in your config." Tests cover both the API (new `falls back to null cwd when no cwd query and no codebases registered` case in `api.workflows.test.ts`) and the discovery layer (new `discoverWorkflows with null cwd` block in `loader.test.ts` asserting no project-source entries and no project-step read errors). * test(workflows): assert bundled defaults surface when cwd is null The second test in the null-cwd discovery group only verified that project-source workflows are absent. That assertion would still pass if the bundled-defaults loader silently regressed. Add an explicit `bundled` source-label assertion so the test catches that regression directly. * fix(workflows): address review on #1618 - api.md: document cwd-omitted behavior so the empty-state case is discoverable - workflow-discovery.ts: docstring explains loadDefaults default rather than just naming the skipped branch - api.workflows.test.ts: mockDiscoverWorkflows accepts string | null to match the wider signature - api.ts: drop trailing period inside the multi-line inline comment - CHANGELOG.md: add the #1173 Fixed entry under [Unreleased] - loader.test.ts: add a regression test that asserts loadConfig is not invoked when cwd is null * test(workflows): tighten null-cwd assertions + drop rot-prone refs Address Wirasm's polish review on #1618: - loader.test.ts: assert result.workflows.length === 0 in the loadDefaults:false case so the test no longer passes if bundled defaults are accidentally loaded. - loader.test.ts: drop the inline (issue #1173) reference and the workflow-discovery.ts file+line pointer from the two comments that risk rotting on future refactor. - api.workflows.test.ts: drop the inline (issue #1173) reference. - CHANGELOG: append positive framing to the #1173 line so the entry reads as "what works now" not just "what no longer breaks".
The pull_request_target event doesn't fire on type=ready_for_review unless explicitly listed. Add it so flipping a draft PR to ready triggers the marketplace auto-review. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(marketplace): add archon-idea-to-wo workflow Adds one entry to the marketplace registry for archon-idea-to-wo — an interactive 8-node workflow that turns a raw idea into BKM-format Work Orders through four AI phases with approval gates between each. Originally authored by @lamachine in PR #1647, where it was proposed as a bundled default. Repackaged as a standalone SHA-pinned external repo (coleam00/archon-idea-to-wo) so it can be published through the community marketplace without waiting for an Archon release. - Author: lamachine - Tags: planning, development - Source: coleam00/archon-idea-to-wo @ 3b0d5d82 (directory format) - archonVersionCompat: >=0.3.0 Closes #1647 * chore: re-trigger marketplace auto-review after ready-for-review PR was flipped to ready-for-review before the action's trigger list included ready_for_review (fixed in d8d5a35 on dev). Empty commit fires the synchronize event so the auto-review runs now that the draft gate is cleared. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…to_approve The 'gh pr review --approve' call fails with 'GitHub Actions is not permitted to approve pull requests' unless the repo has 'Allow GitHub Actions to create and approve pull requests' enabled in Settings. When the approve call fails, fall back to 'gh pr comment' so the PR author still gets a notification with the auto-review reasoning. The merge step still runs either way. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GitLab counterpart to archon-smart-pr-review. Adaptive code review of a GitLab MR — Haiku classifies which review agents are relevant, runs them in parallel, posts resolvable Discussion threads, auto-approves on 0 critical findings. Source: lraphael/archon-gitlab-workflows@55ca7349 Co-authored-by: Raphael Lechner <raphael.l@asix.pro>
# Conflicts: # packages/docs-web/src/data/marketplace.ts
Before this change the ai-review node only saw four things in its prompt: PR metadata, the entry diff, the schema validator's pass/fail summary, and the security scanner's severity+findings summary. The fetched workflow YAML and command files at the pinned SHA were saved to $ARTIFACTS_DIR/source/ but never surfaced to the AI, so Haiku ended up 'reviewing' the registry diff instead of the actual workflow. Add a bundle-source node that emits every fetched file (capped at 12k chars each to keep the prompt sane) and reference it in the ai-review prompt as the artifact under review. Rewrite the prompt instructions to make Haiku explicitly read the YAML + commands and name what it concluded about the workflow's behavior in 'reasoning'. Update the auto_merge comment template so it names what was actually reviewed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GitLab counterpart to archon-comprehensive-pr-review. Full code review of a GitLab MR — all 5 review agents (code-review, error-handling, test-coverage, comment-quality, docs-impact) run in parallel, posts resolvable Discussion threads, auto-approves on 0 critical findings. Source: lraphael/archon-gitlab-workflows@6e39b359 Co-authored-by: Raphael Lechner <raphael.l@asix.pro>
… instead of silent skip (#1673) (#1694) When a when: condition references $nodeId.output.field and the node's output text is not valid JSON, the condition evaluator now returns parsed:false so the DAG executor treats it as an error (instead of silently skipping downstream nodes and exiting 0). Additionally, strip common markdown fences (e.g. ```json blocks) from output text before attempting JSON.parse, handling the common Pi/Minimax pattern of wrapping JSON in fences.
…rkers (#1695) * Fix: extract ARCHON_STATE_JSON markers as standalone lines (#1674) The persist script extracted Claude's state-JSON block by substring-matching the BEGIN/END markers, which gave a false match whenever the marker string appeared inside the brief's prose or inside a JSON string value (e.g. a PR title narrating this very bug). PR #1676's switch to lastIndexOf made the self-referential case worse, since the last substring occurrence may now sit inside a JSON value. Match the markers only when they occupy an entire line (line-anchored ^...$ regex with the m flag) and pick the last END plus the last BEGIN before it, so duplicate-emission and substring-in-content both resolve correctly. Changes: - Replace indexOf/lastIndexOf substring matching in Tier 1 with line-anchored matchAll regex over BEGIN/END markers - Add tests for marker substring in brief prose, marker substring inside state JSON value, and combined duplicate-BEGIN + marker-in-prose case - Keep PR #1676's existing test cases (single block, duplicate BEGINs, JSON-wrapper fallback, no-valid-format exit 1) Fixes #1674 * fix(scripts): address review findings in maintainer-standup-persist - Add WARN diagnostic when all BEGIN markers appear after last END (previously silent fallthrough produced misleading terminal error) - Include 200-char candidate preview in JSON parse error message (error was unactionable in headless workflow without raw output) - Wrap mkdirSync/writeFileSync in try-catch with structured PERSIST FAILED message instead of raw Bun stack trace on disk errors - Fix brief assignment comment to explain WHY [0] vs last-pair asymmetry - Add test: BEGIN present but END absent (truncated output) exits 1 - Add test: prose preamble before first heading is stripped from brief - Add clarifying comment to Test 6 noting it is defence-in-depth, not a case that failed under the old indexOf approach * simplify: collapse multi-line comment blocks to single lines
* fix(web): add error handling for copy message button Handle navigator.clipboard.writeText() failures gracefully: - Add copyError state to track clipboard API errors - Show X icon with error color when copy fails - Reset error state after 2 seconds - Closes #1540 * docs(MessageBubble): add JSDoc comments for CodeRabbit coverage Add docstrings to MessageBubbleRaw component and copyMessage function to satisfy 80% docstring coverage requirement. Closes #1540 * refactor(MessageBubble): remove JSDoc comments per policy, add debug logging Per Wirasm review - remove redundant JSDoc blocks that restate code. The function names and types already convey what the code does. Add console.debug for clipboard errors for debugging support. Closes #1540
* fix(workflows): support editing global workflows * fix: address review feedback on global workflow editing - Document the `source` query parameter on PUT and DELETE in the API reference, including the 400 error for invalid values. - Document the new `source: "global"` response value for GET, with the three-tier auto-discovery order spelled out. - Add a DELETE test for `?source=global` to confirm the home-scoped file is removed (PUT already had coverage; DELETE was missing). - Add 400 "Invalid workflow source" validation tests for both PUT and DELETE so the enum rejection path is locked down. The blocking issue from the prior review — PUT/DELETE routes using cwdQuerySchema instead of workflowTargetQuerySchema — was already resolved on the branch via the rebase against current dev; both routes now correctly use workflowTargetQuerySchema. The comment-rot reference (`source:global` string in a comment) was dropped as part of the rebase conflict resolution where dev's home-scoped resolver block was kept. --------- Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
* feat(workflows): support Codex MCP nodes * fix: address MCP workflow review feedback * fix: address review feedback on Codex MCP support - Drop the broken `inferProviderFromModel` import from packages/cli/src/commands/workflow.ts. The module it referenced (@archon/workflows/model-validation) does not exist, which caused the CLI package's type-check to fail. Per CLAUDE.md, provider is resolved via an explicit chain (`node.provider ?? workflow.provider ?? config.assistant`) and model never influences provider selection — vendor SDKs add new model names faster than we can keep a mapping in sync, so the model-based inference fallback was the wrong primitive anyway. Removed the use site and added a comment anchoring the explicit-chain rule. - Drop "for Claude workflows" from the `dag.mcp_plugin_connection_suppressed` documentation in mcp-servers.md. The log event is provider-agnostic and the qualifier was misleading once Codex got MCP support too. - Add CHANGELOG.md entry under [Unreleased] for the MCP-for-Codex feature with a short note on the system-chunk surfacing behavior. --------- Co-authored-by: Kirill <borshyo@users.noreply.github.com> Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
…odes (#1651) * fix(workflows): pass user-controlled variables via env vars in bash nodes (#1585) Stop substituting user-controlled variables ($USER_MESSAGE, $ARGUMENTS, $LOOP_USER_INPUT, $REJECTION_REASON, $LOOP_PREV_OUTPUT, and context variables) into bash node script bodies before passing to `bash -c`. Instead, pass them as environment variables on the subprocess. This prevents shell injection when user messages contain metacharacters (backticks, $(), unbalanced quotes, etc.) that would otherwise be interpreted as shell syntax. The fix adds a `shellSafe` option to `substituteWorkflowVariables()` that skips user-controlled variable replacement. System-controlled variables ($WORKFLOW_ID, $ARTIFACTS_DIR, $BASE_BRANCH, $DOCS_DIR) are still substituted normally since they contain safe, known values. `substituteNodeOutputRefs()` with `escapedForBash=true` was already safe via `shellQuote()` — only the Pass 1 variables needed fixing. Closes #1585. Subsumes #1377. * fix: address CodeRabbit review — correct loop env semantics and harden test cleanup - LOOP_PREV_OUTPUT in until_bash now correctly references the previous iteration's output (not the current one) - Test uses try/finally for spy cleanup and asserts call count before dereferencing mock.calls * fix(clone): authenticate non-GitHub forge URLs via GITLAB_TOKEN / GITEA_TOKEN (fixes #1655) The clone handler only injected auth tokens for github.com URLs. Private repos on GitLab, Gitea, or Forgejo failed to clone via the Web UI URL form. Replace the github.com-specific string substitution with a forge-aware resolver that: - Looks up the correct env var per hostname (GH_TOKEN, GITLAB_TOKEN, GITEA_TOKEN) - Applies the correct auth URL scheme (oauth2: prefix for GitLab, bare token for GitHub/Gitea) - Handles self-hosted instances (gitlab.mycompany.com, gitea.myorg.com) Add 11 new tests: 5 integration (GitLab, self-hosted GitLab, Gitea, Forgejo, unknown forge) + 4 resolveForgeAuth unit tests + 2 updated existing tests. * fix: match forge auth on parsed hostname only (CodeRabbit security review) Replace substring matching on the full URL with hostname-based matching. This prevents token leakage when forge names appear in URL paths (e.g. https://evil.example.com/gitlab/mirror). - Exact hostname match for known forges (github.com, gitlab.com, gitea.com) - Label-based match for self-hosted instances (gitlab.mycompany.com) - Add 2 security regression tests for path-based injection vectors * fix: add timeout and error logging to until_bash execution Address review feedback from @Wirasm: - Add SUBPROCESS_DEFAULT_TIMEOUT to until_bash execFileAsync to prevent indefinite hangs (matching other bash invocations in this file) - Log non-ENOENT errors in until_bash catch block so syntax errors and permission issues are visible instead of silently swallowed * fix: address review feedback — loop env semantics, test cleanup, docs - Fix LOOP_USER_INPUT in until_bash: only populate on first iteration (consistent with LLM prompt substitution at line 1874) - Change afterAll → afterEach in multi-forge test cleanup (throw-safe) - Add GITLAB_TOKEN/GITEA_TOKEN cleanup to parent beforeEach - Add auth env vars section to variables.md reference - Add shell injection prevention note to script-nodes.md
…EA_TOKEN (fixes #1655) (#1658) * fix: address review feedback — hostname bug, docs, SSH support, test cleanup - Fix parsed.host → parsed.hostname to avoid port duplication in clone URLs - Generalize SSH→HTTPS conversion from GitHub-only to all forges (git@host:path) - Fix ForgeAuthEntry docstring (remove non-existent exact: true/false refs) - Add GITLAB_TOKEN/GITEA_TOKEN docs to quick-start, configuration, adapter pages - Add SSH URL + port duplication regression tests - Harden test env cleanup with beforeEach/afterEach, import afterEach from bun:test - Remove redundant per-test env cleanup (beforeEach/afterEach handles it) Addresses review by @Wirasm on #1658. * fix(clone): hostname boundary check, remove dead code, add bare URL test - Only match first hostname label for self-hosted forge detection, preventing token leakage to crafted hostnames like gitlab.example.com.gitlab.attacker.com - Remove dead gitea.com entry from FORGE_AUTH (handled by SELF_HOSTED_FORGE) - Add test for bare host/path URL form (github.com/owner/repo) - Move orphaned JSDoc comment above resolveForgeAuth function
* feat(pi): support extension-registered provider models via deferred resolution Extension providers (e.g. pi-provider-kiro) register their models on the ModelRegistry during session.bindExtensions(), not during the initial modelRegistry.find() call. The previous approach threw immediately when find() returned undefined, blocking extension models. New flow: 1. modelRegistry.find() checks static catalog + models.json (step 3) 2. If found: pass to createAgentSession, fail-fast auth as before 3. If not found: log info, skip auth (extension providers manage their own credentials), create session without model 4. After bindExtensions() (step 4g): retry modelRegistry.find() — now extension-registered models are discoverable — and call session.setModel() to switch to the resolved model 5. If still not found after bindExtensions(): throw with a hint about installing the provider extension with enableExtensions: true This preserves all existing behavior for built-in providers and models.json custom models while adding support for extension-registered providers like pi-provider-kiro (Kiro API — 20 free models including Claude, DeepSeek, Qwen, etc.). Changes: - provider.ts: deferred model resolution after bindExtensions(); conditional model arg to createAgentSession; conditional auth fail-fast; session.setModel() for extension-resolved models - provider.test.ts: add setModel mock; update model-not-found tests for two-phase find() behavior * test: add archon-test-pi example workflow for Kiro extension validation Simple two-node workflow that verifies the Pi provider works with extension-registered models (e.g. pi-provider-kiro). Node 1 asks a factual question, node 2 validates the answer. Tested with kiro/claude-sonnet-4-6 and kiro/minimax-m2-5. * docs: add Pi/Kiro prerequisites and setup to examples README * fix(pi): load models.json custom providers and surface SDK error messages - Switch ModelRegistry.inMemory() to ModelRegistry.create() so custom providers from ~/.pi/agent/models.json (ollama, LM Studio, etc.) resolve correctly. Both return the same mutable class supporting registerProvider() for extensions. - Surface Pi's AssistantMessage.errorMessage in the result chunk errors array so dag-executor shows actual errors instead of generic 'SDK returned error'. - Add ollama/qwen3.5 node to archon-test-pi workflow to validate both extension (kiro) and models.json (ollama) provider paths. * fix(pi): address Wirasm PR review — comment clarity, session cleanup, test mock alignment - event-bridge: document intentional design of yielding error chunks (isError:true propagates in chunk, not via throw); fix event name to pi.result_chunk_error; remove noisy model field from error log - provider: replace verbose multi-paragraph step-numbered comments with concise WHY explanations; fix stale ModelRegistry.inMemory() reference (code uses create()); add session.dispose() on model-not-found throw and setModel() failure to close the session cleanup gap - provider.test: align mock from ModelRegistry.inMemory → ModelRegistry.create to match actual provider code (was already broken on the branch); update test description and comments to describe behavior rather than step numbers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(pi): add happy-path test for deferred extension model resolution Verifies that when find() returns undefined on the first call (static catalog miss) and a model on the second call (after bindExtensions()), session.setModel() is invoked with the resolved model and no error is returned. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: jhegeman-ds <joel.hegeman@deepseas.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… cycles (#1530) * fix(workflows): preserve completed node state across DAG multi-resume cycles (#1520) Bug: getCompletedDagNodeOutputs only queried 'node_completed' events, but resumed runs emit 'node_skipped_prior_success' for already-completed nodes. On a second resume, previously completed nodes were re-executed because their skip events were invisible to the query. Fix: - Query both 'node_completed' and 'node_skipped_prior_success' event types in getCompletedDagNodeOutputs so multi-resume correctly identifies all previously completed nodes - Store original node_output in node_skipped_prior_success event data so the output is available for $nodeId.output substitution on next resume - Improve error messaging when all nodes fail: name the specific failed nodes and count skipped downstream nodes instead of the generic 'no successful nodes' message Closes #1520 * test: address review feedback — assert node_output, test failure message format - Assert node_output field in node_skipped_prior_success event test - Add test for empty-string fallback when node ID has undefined output - Add dedicated test verifying failure message names failing nodes - Update stale comment from 'no successful nodes' to new behavior - Add edge-case test for only node_skipped_prior_success rows (no node_completed)
…#1554) The Gitea, GitHub, and GitLab adapters constructed the canonical clone path manually as `getArchonWorkspacesPath() + owner + repo`, which nested the cloned repo at the project root. The CLI clone handler uses `getProjectSourcePath(owner, repo)` (returning `workspaces/owner/repo/source/`) and `ensureProjectStructure(owner, repo)` so that `worktrees/`, `artifacts/`, and `logs/` live as siblings of the cloned repo. The webhook adapters bypassed this, so the first webhook clone left the workspace in a layout the rest of the system did not expect, breaking worktree creation and command discovery. Replace the manual path construction with `getProjectSourcePath` and call `ensureProjectStructure` immediately before the clone in all three adapters. For GitLab nested namespaces (group/subgroup/repo), the leaf segment becomes the repo and the remaining path becomes the owner, which matches how the workspace tree treats nested groups. Closes #1547
…ut (#2173) Pi streams assistant text as many tiny text_delta events (often a few characters). The bridge mapped each to its own `assistant` chunk, and the DAG executor joins assistant chunks with "\n\n" in batch mode — shattering Pi output into "Се\n\nгод\n\nня" instead of "Сегодня". Claude and Codex each yield one chunk per complete text block, so the join is correct for them but wrong for Pi's char-level deltas. Coalesce consecutive text_delta chunks in bridgeSession into one block-level assistant chunk, flushed only at natural boundaries (turn start, text-block end, before any non-assistant chunk, end-of-stream, and on error). currentTurnText/assistantBuffer still see every delta, so streaming-tail detection and structured-output buffering are unchanged; the DAG executor and other providers are untouched. Closes #1814 Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…ed binary (#2174) * fix(providers): register Pi Bedrock backend so it loads in the compiled binary Pi lazy-loads every backend via dynamic import(). All backends use string-literal specifiers that Bun's --compile bundles, except Bedrock, whose loader routes through a computed-specifier indirection Bun can't resolve — so bedrock-converse-stream.js and @aws-sdk/client-bedrock-runtime never get embedded, and amazon-bedrock/* models fail in the binary with "Cannot find module './bedrock-converse-stream.js' from '/$bunfs/…'". Mirror Pi's own binary fix (earendil-works/pi#2350): register the Bedrock module override via setBedrockProviderModule() once per process, fed through the static @earendil-works/pi-ai/compat + /bedrock-provider subpaths that Bun does bundle. Called from sendQuery() (not module load) so the lazy-load invariant holds; unconditional so dev and binary stay in parity; a registration failure is swallowed with a WARN so non-Bedrock backends are unaffected. Closes #2154 * chore: pair the bedrock register log events per the logging convention --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…ts (#2171) * dx(providers): generate provider capability matrix from capabilities.ts Provider capability docs were hand-maintained across CLAUDE.md, the docs-web assistant guides, and the archon skill, and all three drifted from packages/providers/src/*/capabilities.ts (the 2026-07-14 audits and PR #2167 corrected the latest instances by hand). Nothing prevented recurrence. Add a generated-artifact + drift-gate, mirroring generate:pi-vendor-map: - scripts/generate-capability-matrix.ts renders a canonical matrix from the registry's capability constants (the same objects the dag-executor reads for its ignored-capability warnings) into a docs reference page, reference/provider-capabilities.md. - check:capability-matrix wired into `bun run validate` and CI; a capability change now fails CI until the page is regenerated. - a new ProviderCapabilities field fails the generator until it gets a matrix axis (totality guard), so the matrix can't silently omit one. Also fix a declared-but-unwired capability found in the sub-task audit: opencode declared hooks: true but has no nodeConfig.hooks translation site, so the dag-executor's ignored-capability warning was suppressed and a node's hooks: were dropped silently (a fail-fast violation). Set hooks: false and correct the ai-assistants doc row. agents: true is correct — verified wired via getOrderedAgents -> materializeAgents. Closes #2116 * docs(skill): correct OpenCode hooks claims in the archon skill tables The archon skill's provider tables were the third drift surface #2116 names, and the capabilities.ts hooks fix in the previous commit made their "OpenCode supports hooks" claims definitively wrong. Align them: - parameter-matrix.md Providers-at-a-Glance: OpenCode hooks -> ignored + warn - dag-advanced.md / SKILL.md / workflow-dag.md: hooks is Claude-only - point the skill's matrix at the canonical generated Provider Capability Matrix as the source of truth (issue proposal step 2) check:bundled-skill unaffected (file set unchanged; content is imported at build time). * docs: qualify OpenCode/Copilot agents as configured selection, not inline definitions --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
… paths (#2178) (#2179) * fix(workflows): align skill validation with execution resolver search paths (#2178) The validator was checking only .claude/skills/ directories while the execution resolver searches both .agents/skills/ and .claude/skills/ (with .agents/ preferred). This caused false validation warnings for skills that existed in .agents/skills/. Now the validator reuses the skillSearchRoots() function from @archon/providers to check all four search locations, ensuring consistency between validation and execution. Closes #2178 * test(workflows): cover skill validation across all resolver search roots Regression tests for #2178: the validator now accepts skills anywhere the runtime resolver looks (.agents/skills/ and .claude/skills/, project and user level) and warns only when a skill is found in none of them. HOME is pointed at a temp dir per test so real user-level skills can't leak in. Also drop the unused resolveSkillDirectories barrel export from @archon/providers — all consumers import it via relative paths; the validator only needs skillSearchRoots. --------- Co-authored-by: 王珩玮 <jtu_111@126.com> Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…etried) (#2181) * fix(workflows): classify session-limit/quota errors as FATAL so retry never burns attempts 'Claude session limit reached' node failures were classified UNKNOWN by classifyError — the never-retry FATAL guard in the DAG retry loop did not apply, so retry: blocks could re-attempt them back-to-back inside the same five-hour limit window where every attempt is doomed (on_error: all directly; on_error: transient via the 'exited with code' manifestation of the same limit). The synthesized 'Credit exhaustion detected' message had the same gap: the 'credit balance' FATAL pattern does not match it. Add 'session limit', 'usage limit reached', and 'credit exhaustion' to FATAL_PATTERNS. FATAL already takes precedence over TRANSIENT in classifyError, so limit messages that also mention rate limits stay FATAL, and the existing !isFatal guard now fails these nodes on first occurrence under every on_error mode. Telemetry errorClass becomes 'fatal' instead of 'unknown'. Tests cover both synthesized session-limit strings, the usage-limit and credit-exhaustion strings, FATAL-over-TRANSIENT precedence, and a drift guard asserting every detectCreditExhaustion output string classifies FATAL so future rewording cannot silently regress to UNKNOWN. Closes #2177 * test(workflows): consolidate session-limit FATAL tests, drop drift-guard duplicates --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…limits (#2175) * fix(providers): retry Claude 400 tool-use-concurrency errors as rate limits The Anthropic API's "400 due to tool use concurrency issues" error is transient — a retry after backoff almost always succeeds — but it was classified as non-retryable on both error paths: - Text path: add 'tool use concurrency' to RATE_LIMIT_PATTERNS so thrown subprocess errors carrying the message retry (the fix proposed in #1341). - Structural path (#1797, added after the issue was filed): a ClaudeApiResultError with this text arrives under a catch-all SDK code ('unknown'/'invalid_request' both classify to 'unknown'), which fails fast. Reclassify to rate_limit via a narrow text fallback that applies ONLY when the typed code resolves to the catch-all class — specific typed codes remain authoritative and are never overridden by text. Closes #1341 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(providers): codify untyped-transient text-match policy behind UNTYPED_TRANSIENT_PATTERNS Extract the tool-use-concurrency text match in classifyAndEnrichError into a named UNTYPED_TRANSIENT_PATTERNS constant with an explicit admission contract: entries apply only when the SDK's typed code resolved to the catch-all 'unknown' class, must name the upstream error, link an upstream typing request, and be removed once the SDK types them. Behavior unchanged. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
…nt (#2187) In the console AI Settings model pickers (Model Tiers / Aliases / Defaults), suggestion rows gave the cost/context hint layout priority: the hint span was shrink-0 while the model name carried min-w-0 truncate, so with justify-between the name collapsed to a couple of characters (e.g. 'a', 'ama…') on Pi's long hints. Stack the row instead (same shape as ArtifactPanel's list rows): the model name keeps the full row width on its own line, and the hint moves to a muted second line that truncates as the secondary element. Closes #2031 Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
The bundled archon-workflow-builder saves its generated YAML to the relative path .archon/workflows/<name>.yaml, but runs default to worktree isolation - so the file landed in a throwaway worktree and the user's repo stayed empty despite the 'Workflow saved!' report. Pin the workflow with worktree.enabled: false (same mechanism and rationale as archon-assist, #1546) so the save lands in the user's actual checkout. mutates_checkout stays unset: the workflow does write into the live checkout, so the path-lock guard must remain active. Regenerated the embedded bundle via bun run generate:bundled. Closes #1220 Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
#2188) * fix(console): show chat-dispatched run messages on the run detail page The getRun route deliberately splits the conversation pointer: CLI runs get conversation_platform_id, while web/chat-dispatched runs (parent conversation set) only get worker_platform_id. The console run primitive consumed only the former, so for web runs the messages query never fired and the run detail page showed "0 messages" without the agent's answer. Map worker_platform_id in the console run primitive and pick the message source via a new runMessageConversationId() helper that falls back to the worker conversation when conversation_platform_id is absent. CLI behavior unchanged; no server or OpenAPI change needed — the field was already exposed in the getRun response schema. Fixes #2048 * refactor(console): accept undefined in runMessageConversationId Self-review follow-up: widen the helper to Run | undefined so the RunDetailPage call site collapses to plain optional chaining (matching the file's existing style for not-yet-loaded detail), trim the duplicated split-pointer explanation from the field doc, and cover the undefined input in tests. --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…points (#2190) The create-with-message and web workflow-run routes fired title generation with no model, so the Codex provider fell through to the raw config default (gpt-5.3-codex) — which 400s on ChatGPT-plan accounts, leaving every title stuck on truncated message text and logging a provider error on each conversation start. Add resolveTitleRequest() to the orchestrator: it resolves the small tier (config tiers + per-user prefs when a userId is available, mirroring the chat-path fix from #1873) into a provider plus fully resolved request options, and never throws — on any failure it degrades to the legacy bare request. Both server call sites now route title generation through it. Fixes #1855 Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…2189) * fix(web): render console timestamps in the viewer's local timezone SQLite rows reach the console as naive UTC strings ("YYYY-MM-DD HH:MM:SS", no suffix) because the API serializers pass string timestamps through verbatim. The console's format helpers parsed them with bare new Date(), which treats suffix-less strings as browser-local time — so every clock, "Xm ago" label, and elapsed counter showed UTC digits as local time. Add an offset-aware ensureUtc guard (append Z and swap the space separator for T only when the string carries no Z or numeric offset) and route formatClock, relativeTime, elapsedSince, and formatRelativeToBaseline through it. Z-suffixed inputs (live SSE values, Date-serialized Postgres rows) pass through untouched, so nothing is double-shifted — and mixed naive/Z deltas now compute correctly. Closes #1990 * Extract toUtcMs helper for repeated epoch conversions * Pin non-UTC timezone in format tests so they can catch the regression --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…ocess exit (#2191) * fix(cli): scope workflow signal cleanup to the owned run and remove handlers on settle The SIGTERM/SIGINT cleanup registered by `workflow run` (and reached through the approve/reject/resume inline auto-resume) had three hazards that could destructively mark runs failed on process exit (#1123): - The handlers were anonymous `process.once` closures — impossible to remove, so they stayed live after executeWorkflow returned with the run legitimately paused at the next gate, and repeated workflowRunCommand calls in one process stacked handlers. - The cleanup failed whatever `getActiveWorkflowRun(conversation.id)` returned — a conversation-wide query (children match via parent_conversation_id) that could hit a run driven by another process, violating the no-autonomous-lifecycle-mutation principle. The cleanup now only ever fails THE run this process started or resumed: the id is preset from the resumable lookup on resume, and learned from the workflow_started emitter event for fresh runs (the progress subscription is now always registered; --quiet only gates rendering). Before failing, the handler re-reads the run status and leaves any non-running state alone — failWorkflowRun's own status='running' CAS closes the remaining read-then-write window. The handlers are named and deregistered in the finally once the run settles. A genuine mid-run interrupt still marks the run failed. In the engine, the three human-gate pause sites (approval, loop, loop_group) now tolerate losing the pause CAS to an external transition: instead of cascading into a spurious node failure, they log, skip the approval_pending emit, and let the between-layer status check halt the DAG cleanly. A pause failure while the run is still running rethrows unchanged, and the container write-back gate stays deliberately fail-closed. The approve/reject gate-resolution CAS (#2146) and paused-staging semantics (#2112) are untouched. Closes #1123 * refactor(workflows): emit approval_pending inside the gate-pause helper Review follow-up on the #1123 fix: the pause-then-conditionally-emit pairing appeared identically at all three gate sites, and every future caller would have had to remember to gate its emit on the return value to avoid a phantom approval_pending after a lost CAS. The helper now owns the emit (ApprovalContext already carries nodeId + message), so the invariant can't be gotten wrong at a new call site. Also collapses the trivial signal-handler wrappers to expression bodies. No behavior change — the emitter-observing tests pass unchanged. --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…ner start (#1981) * fix(docker): eliminate slow recursive chown in image build and container start (#1970) * review fixes: surface chown stderr on failure, tighten comments - fix_ownership now captures stderr from find/chown and emits it before exit 1, so a read-only or permission-denied volume is diagnosable instead of only the canned error line (still fails fast) - reword comments that referenced removed code (old chown -Rh semantics, 'same way we do /.archon') to state the invariant directly - trim the Dockerfile layer comment and entrypoint rationale to one-liners; keep the #1970 issue ref, drop timing snapshots - note why the find predicate uses -o, not -a - split a double-em-dash sentence in the docker deployment doc --------- Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
* fix(adapters): drop duplicate webhook deliveries at ingest (#1951) A duplicate delivery of the same comment passed every guard and queued a byte-identical second workflow run behind the first: the lock manager orders per-conversation messages but never dedups them. Dual repo+App webhook subscriptions (different delivery GUIDs for one comment), LB double-forwards, and redeliveries all hit this. Adds a bounded TTL first-seen cache in core and gates GitHub webhook processing on a logical idempotency key (comment id + updated_at, so edited comments still re-trigger), falling back to the X-GitHub-Delivery GUID when the payload lacks comment identity. Fails open when neither is available. * fix(adapters): require full comment identity for dedup key; deterministic TTL tests Keying on comment id alone (updated_at absent) would dedup an edit against the original within the TTL window — require both fields and use the delivery-GUID fallback otherwise. Dedup TTL tests now use an injected clock instead of wall-clock sleeps. * fix(core,adapters): name dedup eviction log per convention; document seen() ordering The eviction debug log used 'evicted_oldest_entries', which does not follow the {domain}.{action}_{state} event-name convention; rename it to 'delivery_dedup.entries_evicted'. Also document why the ingest dedup marks the key before downstream processing: the duplicates it guards against are dual subscriptions delivering the same comment near-simultaneously, so the key must be claimed before either delivery finishes or both would double-process. * test(server): extract GitHub webhook route and cover delivery-id forwarding Move the /webhooks/github handler into routes/webhooks.ts behind a narrow GithubWebhookTarget seam so it can be tested against a mocked adapter. The new route tests pin the X-GitHub-Delivery forwarding (present header, omitted header -> undefined), the missing-signature 400, and the fire-and-forget 200 on async processing failure. The adapter tests pass deliveryId directly, so only this seam catches broken header forwarding. * test(adapters,core): make dedup webhook tests hermetic; pin claim-at-ingest tradeoff The deliver() helper previously swallowed every error while first-seen deliveries ran into the unmocked Octokit path, making live GitHub API calls from the test suite. Stub the Octokit methods the webhook flow hits (repos.get, issues.listComments/createComment), stub handleMessage via spyOn, and drop the broad catch so unexpected failures fail the test. Assertions on the stubs verify the pipeline completes locally. Also: - regression test pinning that a redelivery within the TTL is dropped even when the first attempt failed transiently (the documented fire-and-forget tradeoff of claiming the key at ingest) - assert github.duplicate_delivery_dropped fires on the dedup path - rework the eviction-position test with real size pressure (maxEntries=2) - type the test helpers (TestClock, deliver return type) * docs(adapters,core): tighten dedup comments; document three-arg webhook flow - condense the dedup-key and claim-timing comment blocks to the load-bearing WHY (dual subscriptions deliver the same comment under different GUIDs; claim before downstream work or both pass) - trim the delivery-dedup file JSDoc to the rationale paragraph and drop the name-restating constant/field JSDocs - note in prune() that its front-of-map invariant relies on seen() short-circuiting on fresh entries - drop issue-number references from source comments (they live in the changelog/PR) - neutralize the WebhookEvent.comment field docs so the type is not coupled to the dedup consumer - architecture.md: three-arg handleWebhook + dedup step in the flow - github-app-setup.md: note that dual webhooks are deduplicated during PAT->App migration --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…low (#2192) WebUI-dispatched background workflows unconditionally resolved isolation whenever the conversation had a codebase, so a workflow declaring worktree.enabled: false still tried to create a worktree (and failed on local-path-only codebases). Mirror the CLI and foreground guards: skip validateAndResolveIsolation on explicit opt-out, run the workflow in the parent conversation's cwd, and emit workflow.worktree_disabled_by_policy so operators can tell live-checkout runs from worktree runs. Closes #1368 Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local> Co-authored-by: StuartJAtkinson <StuartJAtkinson@users.noreply.github.com>
…iles (#2194) Direction call forced by #1224 (Traefik --profile request): Caddy stays the single maintained reference proxy; alternative proxies/infra land as docs examples against the proxy contract (#2193). Adds the citable $deployment-recipes clause for future triage. Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…ense-in-depth (#1243) * refactor(server): use parseOwnerRepo helper in artifact route for defense-in-depth The artifact endpoint parsed codebase.name with split('/') and only checked nameParts.length < 2, duplicating and weakening the shared parseOwnerRepo helper already used by registerRepository. Replace with the helper that also rejects '..', '.', empty segments, and non-safe characters. Not an exploitable bug today — registration already goes through basename()/URL parsing which feed parseOwnerRepo — but collapses a future regression class if a new name-creation path is ever added. * refactor(server): extend parseOwnerRepo guard to the artifact-listing route The artifact-listing endpoint (GET /api/runs/:runId/artifacts) was added after PR #1243 was written and reintroduced the same inline codebase.name.split('/') pattern the PR removes from the file-serving route. Apply the shared parseOwnerRepo helper there too, keeping that site's existing rejection shape (200 with an empty files list). Traversal-shaped names are now rejected at the parse stage before any path is built; the downstream ARCHON_HOME containment check stays as a second layer. Folder projects (kind: 'folder') are unaffected: their plain display names never had an owner/repo shape, so both routes keep returning the same empty-list/404 responses they did before. Adds route tests for malformed names on both endpoints and locks in the folder-project behavior. Co-authored-by: shaun0927 <70629228+shaun0927@users.noreply.github.com> --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…grity (#1246) (#1251) * security(cli): add build-time embedded hash for web-dist tarball integrity (#1246) * test(cli): cover embedded, mismatch, and remote-fallback paths in downloadWebDist Export downloadWebDist with the embedded checksum as a defaulted parameter so tests can drive all three verification paths against a real tarball: embedded hash match (no checksums.txt fetch), embedded mismatch (hard fail before extraction), and empty constant (remote checksums.txt fallback). --------- Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
…1691) * fix(marketplace): guard fetch-source against missing sourceUrl/sha The marketplace-fetch-source script crashes with TypeError when sourceUrl is undefined, which happens when the CI workflow runs on PRs that don't add a marketplace entry (no sourceUrl in entry.json). - Make sourceUrl and sha optional in MarketplaceEntry interface - Add early return with exit(0) when either field is missing - Output empty {files, errors} result so downstream AI review node can proceed without source files instead of killing the workflow Fixes the Run marketplace auto-review CI failure on PRs #1685 and #1689. * test(marketplace): add test coverage for fetch-source guard block - Add marketplace-fetch-source.test.ts covering all guard paths: - missing sourceUrl only (stderr mentions sourceUrl, exit 0) - missing sha only (stderr mentions sha, exit 0) - missing both (stderr mentions both, exit 0) - both present (guard does not trigger, gh api errors expected) - missing entry.json (exit 1) - Trim inline comment to 2 lines per review feedback * test(marketplace): make fetch-source guard tests hermetic Replace the network-dependent 'both fields present' case with an intentionally unrecognized sourceUrl so the script stops deterministically at URL validation instead of reaching gh api. Trim WHAT-restating comments per review and add the missing trailing newline. --------- Co-authored-by: Hermes <hermes@local> Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…1711) * fix(server/workflows): resolve project workflows with .yml extension `GET /api/workflows/:name` and `DELETE /api/workflows/:name` constructed the on-disk filename as `${name}.yaml` and only tried that one extension. The discovery scanner in `workflow-discovery.ts` (the source for `GET /api/workflows`) accepts both `.yaml` and `.yml`, so a project that named its workflow `phase-0-spike.yml` would appear in the list endpoint but 404 on the detail endpoint and could not be deleted via the API. This is the same scope-mismatch class as the trailing-newline / case-sensitivity bugs that crop up whenever two endpoints derive paths independently — fix it once at the read site rather than forcing every author to rename files. Changes: - GET /api/workflows/:name now probes `${name}.yaml` then `${name}.yml` in each scope (project, home, default). Extracted as a small inline helper so the same logic isn't duplicated three times. - DELETE /api/workflows/:name probes both extensions before returning 404. - PUT (save) is unchanged: writes continue to use `.yaml` as the canonical extension for newly-created workflows. Existing `.yml` files remain readable and deletable. - Bundled defaults are still keyed on `${name}.yaml` (the suffix is a synthetic in-memory key there, not a filesystem path). Tests: - Added regression: `phase-0-spike.yml` in `<cwd>/.archon/workflows/` returns 200 with `source: project` and `filename: phase-0-spike.yml`. - All 36 tests in `api.workflows.test.ts` pass (was 35). - `tsc --noEmit` on `packages/server` is clean. * fix(server/workflows): delete both .yaml and .yml twins, cover .yml fallbacks - DELETE /api/workflows/:name now unlinks both extension variants before responding, so a twin file can't stay discoverable after a reported deletion; 404 only when neither existed - add tests: home-scope and source-build defaults .yml GET lookups, .yml-only and twin DELETE regressions, plus the source=global .yml DELETE - tighten the tryReadWorkflowAt comment to reference loadWorkflowsFromDir instead of a raw line number --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
* fix(web): URL-encode conversation IDs in PATCH/DELETE requests updateConversation and deleteConversation built request URLs with raw template interpolation, so platform conversation IDs containing slashes (GitHub/Gitea forge format: owner/repo#issue) split the path and missed the route, returning 404. sendMessage and getMessages already used encodeURIComponent — apply the same to PATCH/DELETE. Adds DELETE and PATCH regression tests at the route layer covering the encoded-slash + hash case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: cover client-side URL encoding of conversation IDs in PATCH/DELETE The existing regression tests exercise the server route's decoding, so they would still pass if the client-side encodeURIComponent fix were reverted. Add web API-client tests that spy on fetch and assert updateConversation/ deleteConversation hit /api/conversations/Solvation-BV%2FArchon%2342 with the right method (and title body for PATCH). Verified they fail against the unencoded URLs. Also extend the server PATCH/DELETE route tests with the Gitea ! separator and unknown-ID 404 cases for parity with the existing GET coverage. --------- Co-authored-by: LLMsolution <llmsolution@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
* fix(core): classify Claude usage-limit errors as rate limits Claude's 5-hour subscription cap surfaces as "You've hit your limit · resets <time>" (and "usage limit" when org overage is disabled). Neither matched the rate-limit branch in classifyAndFormatError, so users hit the generic "An unexpected error occurred" fallback instead of a clear message. Match both phrasings case-insensitively alongside "rate limit" and echo the reset time when present, e.g. "⚠️ AI usage limit reached (resets 4:50pm (UTC)). Please wait and try again." Applies to every platform. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(core/error-formatter): tighten comments per Wirasm review on PR #1761 Address the maintainer-review-pr suggestions on the upstream PR. No behavior change; @Wirasm's verdict was 'ready-to-merge' with these comment-quality nits: - Replace the 5-line block comment that restated what the regex does and embedded task-specific labels ("Claude subscription cap", "org-disabled- overage variant") with a single line stating intent: handles AI-provider rate-limit and usage-cap errors (both generic and Claude-specific formats). - Tighten the p.m.-truncation comment to a single line. The constraint it protects is genuinely non-obvious (anchor on · / stop at · or newline so abbreviated periods aren't cut) but it doesn't need two lines of prose. The third item Wirasm flagged (missing explicit `string` return type on classifyAndFormatError) is already present on the current PR head and needs no change. Validation: core type-check, 57 error-formatter tests pass, ESLint + Prettier clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(core/error-formatter): pin reset-time fallback and cover session-limit shapes - Turn the classification comment into a durable section label and note why broad substrings are safe (call sites only feed AI conversation-turn errors) - Constrain the no-separator resets fallback: drop any follow-on sentence so the workflow session-limit FATAL shape yields just the reset clause - Classify 'session limit' as a usage cap so it no longer falls through to the generic session branch and misleadingly suggests /reset - Pin the fallback with direct tests: standalone 'Resets in 5 minutes', reset-without-plural negative, multi-separator single-segment capture, p.m. preservation, and the exact #2181 FATAL shape --------- Co-authored-by: k4n4lm00n <191119700+k4n4lm00n@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
… routes (#2047) * fix(server): accept namespaced workflow names on HTTP launch and read routes Workflows can be namespaced one subfolder deep on disk (e.g. triage/foo), discovered at MAX_DISCOVERY_DEPTH = 1, and the CLI launches them by that name. The HTTP /run and GET routes validated the name with isValidCommandName, which rejects any '/', so namespaced workflows could only be run from the CLI (#2007). Add isValidWorkflowName: allows a single '/' by requiring every slash-separated segment to be a valid command name, so '..', '\', leading dots, and empty segments (leading/trailing/double slash) stay rejected and path traversal is unchanged. Wire it into the non-mutating /run and GET routes only; PUT and DELETE keep isValidCommandName since creating a subfolder on save is separate. * test(server): mock isValidWorkflowName in the /run validation test The POST /api/workflows/:name/run route now validates with isValidWorkflowName, but this test still mocked isValidCommandName to reject, so validation passed and the route returned 200 instead of 400. Mock the validator the route actually calls. * refactor(workflows): share MAX_DISCOVERY_DEPTH between validator and loader The workflow-name validator hard-coded a two-segment cap to mirror the loader's discovery depth, duplicating the one-subfolder contract. If the loader's MAX_DISCOVERY_DEPTH ever changed, the validator would silently drift and start rejecting discoverable names or accepting names the loader never finds. Move MAX_DISCOVERY_DEPTH into command-validation (the dependency-free leaf module) as the single source of truth, derive the segment cap from it, and import it back into workflow-discovery. The import direction keeps the leaf import-free, so the executor/dag-executor cycle this module breaks stays broken. * test(server): cover namespaced workflow name on the run endpoint The namespaced-name fix updated both GET /api/workflows/:name and POST /api/workflows/:name/run, but the new coverage only locked down the GET path. Since launchability over HTTP is the point of the fix, exercise a percent-encoded namespaced name on the run endpoint too: assert it is accepted and that the decoded `triage/review` reaches the orchestrator as `/workflow run triage/review ...`. The test installs the real validator logic so it goes red if the run route validates with isValidCommandName instead of isValidWorkflowName. * docs: correct MAX_DISCOVERY_DEPTH JSDoc and trim review-flagged comments Review follow-ups on the namespaced-name fix: - MAX_DISCOVERY_DEPTH's JSDoc claimed the constant had to live in command-validation to avoid re-introducing a module cycle, and pointed at a 'file note above' that doesn't exist. workflow-discovery already imports from this module, so there is no new edge and no cycle. Replaced with the real rationale: shared so discovery depth and the name validator can't drift. - Dropped the isValidWorkflowName JSDoc paragraph narrating HTTP/CLI callers; the first paragraph covers the contract. - Keyed the run-route regression test comment on the contract instead of 'the fix', and collapsed the describe-block preamble in command-validation.test.ts to one line. --------- Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
* Fix: align docker/build-push-action to @v6 in publish.yml (#127) Issue #127 (Dependabot bump for actions/setup-python 5.4.0 → 5.6.0) is superseded — actions/setup-python is no longer used in any workflow after the Archon overhaul. This commit addresses the spirit of the issue by fixing a related CI hygiene gap: docker/build-push-action was pinned at @v5 in publish.yml while test.yml already uses @v6. Changes: - Bump docker/build-push-action from @v5 to @v6 in .github/workflows/publish.yml Fixes #127 * ci: retrigger checks after undraft --------- Co-authored-by: waltg101 <145871366+waltg101@users.noreply.github.com> Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
…#1759) (#1789) * feat(workflows): add loop.command as exactly-one alternative to loop.prompt A loop node can now load its iteration prompt from a named command file (`loop.command: <name>`) instead of inlining it as `loop.prompt`. The schema enforces exactly one of the two. The loaded file is read once at node start and reused for every iteration; substitution semantics (`$LOOP_PREV_OUTPUT`, `$LOOP_USER_INPUT`, `$nodeId.output`, etc.) are unchanged. A bad reference fails the node with an actionable error before iteration 1. This mirrors the existing `prompt:` ⇄ `command:` relationship at the node level, so the longest/most-reusable loop prompts (Ralph-style implement loops) can live as Markdown files instead of being inlined in YAML. Refs specs/loop-command.md (#1759). * feat(workflows): validate loop.command resolves to a real command file Adds Level 3 (resource resolution) checks for loop.command parallel to existing command-node checks: invalid name, unresolved file, and "did you mean…" suggestions, all surfaced before a workflow runs and labelled with field 'loop.command'. Reuses availableCommands already computed at the top of the validator loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(web): regenerate API types so loop.command surfaces in DagNode Run after the workflow Zod schema gained an optional loop.command sibling to loop.prompt (items 1/2/4). Regenerated via `bun --filter @archon/web generate:types` against `bun run dev:server` on port 3090, then `bun x prettier --write` to keep the committed file in repo style and reduce the diff to the actual schema delta. Net effect: components['schemas']['DagNode']['loop'] now has prompt and command both optional, unblocking web-side consumers (next: canvas label for command-backed loops). * feat(web): label command-backed loop nodes on the canvas `resolveNodeDisplay` now branches inside the `'loop' in dn` block on `dn.loop?.command`: when a loop node carries `loop.command`, it returns `{ label: dn.loop.command, nodeType: 'loop' }` (no `promptText`), so the read-only builder canvas shows the command name as the node's label — mirroring how `command:` nodes display today. Inline-prompt loops keep their existing `{ label: 'Loop', promptText }` shape unchanged. `DagNodeComponent` needs no change: its `'loop'` case in `getContentPreview` reads `promptText?.split('\n')[0] ?? ''`, which is empty for command-backed loops (same effective preview as command nodes, whose label already lives in the header). The `LOOP` badge and loop stripe stay — only the label text changes — because the node is still semantically a loop. Closes the web-side acceptance criterion in specs/loop-command.md: "the builder canvas labels a command-backed loop by its command name." * test(workflows): loader cases for loop.command happy path, both/neither, unsafe name, ref-scan Lock the loader's loop.command behaviour with five cases inside 'describe(loop node parsing)': cleanly parses loop.command on its own, rejects both-present with an 'exactly one' message that names both fields, rejects neither-present with both alternatives named (so authors discover loop.command exists, not just the legacy loop.prompt), rejects '../escape' with 'invalid command name', and regression-guards the \$nodeId.output ref scanner so a command-backed loop neither crashes nor hides a sibling's reference to its output. Adds 5 tests (126/126 in loader.test.ts; was 121). Pairs with the schema + loader + executor + validator changes from earlier items in specs/loop-command.md. * test(workflows): validator cases for loop.command resource resolution Mirror the Level-3 command-node coverage for the new loop.command branch: repo-local hit, missing-target with suggestions, unsafe name guard, bundled-default fallback, and home-scope (ARCHON_HOME) resolution. Pure test addition — pins the behaviour landed in the validator change so a refactor cannot silently drop the defense-in-depth isValidCommandName check or the bundled/home resolution paths. * test(workflows): executor cases for loop.command runtime contract Pin the runtime behaviour of command-backed loop nodes with five tests in the existing `loop node execution` block: - read-once invariant: writes a command file, deletes it synchronously inside iter 1's mock generator, asserts iter 2 still runs from the in-memory template (no node_failed / loop_iteration_failed events). - fail-fast paths: missing target, empty target, and unsafe-name (../escape) each return before any sendQuery call and emit node_failed with the actionable diagnostic. The unsafe-name case bypasses the loop schema's superRefine via an "as unknown as DagNode" cast so the executor's defense-in-depth branch is exercised directly. - substitution: command-file body contains LOOP_PREV_OUTPUT and LOOP_USER_INPUT placeholders; iter 1 substitutes both to empty, iter 2 picks up iter 1's cleaned output for PREV while USER stays empty (non-interactive). Proves the loaded text flows through substituteWorkflowVariables identically to inline loop.prompt. Adds unlinkSync from 'fs' for the mid-generator deletion. bun test packages/workflows/src/dag-executor.test.ts now reports 250/250 (was 245, +5). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(workflows): describe loop.command alongside loop.prompt Adds a top-line note pointing readers at both authoring shapes, surfaces `command:` in the Configuration Fields YAML block with the "exactly one" rule inline, and inserts a `### command` subsection that covers repo/home/bundled resolution, command-name safety, load-once-on-node-start semantics, fail-fast on missing/empty/unreadable targets, and parity with inline `prompt` for variable substitution. Worked example mirrors the spec's `archon-ralph-implement` running scenario. Closes the last spec acceptance criterion for `loop.command`. * fix(workflows): trim loop.command at schema level Whitespace-padded values like " my-cmd " previously passed parse-time validation (the superRefine trimmed for isValidCommandName) but were stored untrimmed, so downstream loadCommandPrompt looked up the literal padded filename and failed at runtime with a confusing "not found" diagnostic. Normalize at the Zod schema (z.string().trim()) so the parsed value matches what resolution sees, and the existing parse-time errors remain the actionable surface. Caught by CodeRabbit on PR #1789. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(workflows): include command name in loop node_failed event payload The structured log on loop.command load failure already carries the failing command, but the workflow event written to the store did not — event-stream consumers (web UI, run inspectors, downstream automation) saw only the error string. Mirror the log context onto the event so both observability paths surface the same diagnostic. Caught by CodeRabbit on PR #1789. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(workflows): emit node_started for loop nodes to match other node executors executeLoopNode never emitted a node_started workflow event — neither on the success path (only the per-iteration loop_iteration_started fires, then node_completed at the end) nor on the new loop.command load-failure path. That breaks the project-wide event-pairing rule (CLAUDE.md: "Always pair _started with _completed or _failed") and was visible to event-stream consumers as a loop node that just appeared in node_failed without warning. Mirror executeBashNode and executeScriptNode: log dag_node_started, write the node_started workflow event (carrying the optional loop.command name in data so the start event captures the same context the failure event does), and emit the in-process WorkflowEmitterEvent. The outer DAG dispatcher already delegates the start event to each per-node executor, so no double emission. Caught by CodeRabbit on PR #1789. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(web): handle optional loop.prompt in console builder variant The loop.command rebase makes loop.prompt optional on the wire, but the console builder only models prompt-based loops. Coalesce to the empty default so the round-trip stays type-correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(workflows): loop-node lifecycle finalizer + read-once command snapshot across gate pauses Two review findings on the loop.command feature: - Every failed exit of executeLoopNode now returns through a single failLoopNode finalizer: one terminal log line, one persisted node_failed row (namespaced step_name), exactly one node_failed emitter event — covering command-load failure, provider-resolve failure, between-iteration and mid-stream cancellation, iteration throw, empty output, gate-send failure, and max-iterations exhaustion. Thrown exits stay paired by the runLayers dispatcher catch. The node_started pairing comment now states the actual contract, and the start/terminal logs use the dotted loop_node.* event names. - The command body is read ONCE per run/node: the interactive gate persists the loaded text as ApprovalContext.commandSnapshot and the resumed invocation reuses it, so a command file edited or deleted while the run sits paused can neither change nor break the loop's prompt. Prompt-source resolution also moved below the finalize-on-approve check so a bare approve never re-reads the file. pauseWorkflowRun null-resets the new field like every other optional approval sub-field (SQLite json_patch), and runs paused by older builds fall back to a fresh read. Tests: two-invocation approval-resume test that rewrites AND deletes the file while paused, exactly-one node_failed on exhaustion, node_started command metadata, and a command-backed loop inside a loop_group body with namespaced lifecycle events. Co-authored-by: marc0der <marc0der@users.noreply.github.com> * fix(web): preserve the loop prompt/command union through the builder round-trip A command-backed loop imported into the console builder previously collapsed to prompt: '' on export — silent data loss flagged as the blocking issue in review. The builder now models the engine's exactly-one rule end to end: - LoopNodeData carries an optional prompt/command pair with the one-of invariant; loopFromDag/loopToDag keep exactly the source the wire node carries (command never degrades to an empty prompt). - The inspector gains a prompt-source toggle (inline prompt vs command file); switching drops the other field so stale values cannot leak into the export. Node previews show the command name, like command nodes. - Structural validation enforces one-of + non-empty; content ref-scanning skips command-backed loops (file body is runtime-loaded, same posture as the engine loader). - A wire node carrying BOTH sources (not engine-producible) imports with an error-severity issue instead of a silent drop. Round-trip test: a { command } loop fixture must export byte-identical, with no prompt key introduced. Co-authored-by: marc0der <marc0der@users.noreply.github.com> * docs(workflows): present loop.prompt / loop.command as exactly-one alternatives - quick-reference: the Loop Node Options table now marks prompt and command as one-of alternatives (exactly one required) instead of prompt-required. - loop-nodes guide: the copyable configuration YAML no longer shows both keys together (the schema rejects that); the command alternative is a commented-out line with the one-of rule spelled out. The read-once paragraph now covers the pause snapshot (read once per RUN, resume reuses the persisted body), and the example comment describes resolution precedence instead of claiming one concrete path. - loader.test: the whitespace-trim comment now says parsing NORMALIZES the command name rather than rejecting it. Co-authored-by: marc0der <marc0der@users.noreply.github.com> * chore(web): regenerate api.generated.d.ts from the current OpenAPI spec Regenerated against the running dev server (bun --filter @archon/web generate:types + format), confirming the hand-merged loop prompt?/command? shape and picking up drift the checked-in file had accumulated: signal_completes on loop/loop_group, workflow-level container:, per-node pi:, and include/with. Co-authored-by: marc0der <marc0der@users.noreply.github.com> * test(workflows): drop specification-style narration from loop.command comments Review asked for hidden-mechanics comments only — the block referenced a nonexistent specs/loop-command.md and quoted acceptance criteria instead of stating what the tests pin down. Co-authored-by: marc0der <marc0der@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local> Co-authored-by: marc0der <marc0der@users.noreply.github.com>
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
added 2 commits
July 20, 2026 22:19
# Conflicts: # packages/docs-web/src/data/marketplace.ts
…serts clean teardown The smoke predates #2160: a container run that produces changes now ends paused at the approval-gated write-back with its container suspended and kept (resumable by contract). The job's leak assertion counted that as a leaked container. Reject the gate after the run — discarding the overlay and destroying the container — which is the Phase B semantic the assertions were written for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 0.6.0
Folder projects, opt-in Docker container isolation, three new workflow-composition primitives (
include:,loop_group,loop.command), the Archon Studio builder preview, and a large security + reliability batch spanning gates, providers, Windows, Docker, and the console.Added
_folder/<slug>/storage. Non-git local paths auto-register instead of erroring. (feat(codebases): folder projects — non-git workspaces + multi-repo roots #2055)include:workflow primitive — load-time inlining of another workflow's nodes as a flattened, namespaced sub-DAG. (feat(workflows): include: load-time workflow inlining primitive (#2121) #2129)loop_group:node — repeat a multi-node sub-DAG until a signal,until_bash, ormax_iterations. (feat(workflows): loop_group node — cross-node subgraph looping #2032)loop.command:— loop nodes can load their per-iteration prompt from a command file (exactly-one-of with inlineprompt:), with the body snapshotted per run so pause-time file edits can't change a running loop. (feat(workflows): let loop nodes load their prompt from a command file (loop.command:) #1759, feat(workflows): add loop.command for loading loop prompts from files (#1759) #1789)default_modelper user, written atomically with the provider;archon ai default <provider> [<model>]CLI + console support, plus a default-chat-model step inarchon setup. (feat(core/cli/console): per-user default chat model (default_model) + archon ai default <provider> [<model>] #2082, feat(cli): default chat model step in archon setup #2087)archon doctornow checks the Codex binary and the OpenCode embedded runtime. (feat(cli): archon doctor — add Codex binary and OpenCode runtime checks #2151)Security
$ARGUMENTS,$USER_MESSAGE,$CONTEXT,$LOOP_*, and$REJECTION_REASONare no longer interpolated into executable script source — read them viaprocess.env.X(bun) /os.environ['X'](python). Custom workflows referencing them in script bodies get an empty value plus a one-release migration warning.$nodeId.outputrefs are unchanged. (security(workflows): deliver user-controlled vars to script nodes via env, not source splice #2168)archon serveverifies downloads against a hash a compromised release cannot alter; dev installs keep the remote-checksum path. (security(cli): archon serve web-dist integrity relies on single-source GitHub Releases — no attestation #1246, security(cli): add build-time embedded hash for web-dist tarball integrity (#1246) #1251)parseOwnerRepohelper (defense-in-depth against traversal shapes). (refactor(server): use parseOwnerRepo helper in artifact route for defense-in-depth #1243)Changed
/reset. (fix(workflows): classify session-limit/quota errors as FATAL (never retried) #2181, fix(core): classify Claude usage-limit errors as rate limits #1761)CLAUDE_API_KEYis mirrored toANTHROPIC_API_KEYfor the Claude subprocess. Note: a host using subscription login that also carries a strayCLAUDE_API_KEYin.envnow bills to that key — explicitANTHROPIC_API_KEY, OAuth tokens, and per-user credentials still win. (fix(providers): mirror CLAUDE_API_KEY to ANTHROPIC_API_KEY for the Claude subprocess #1941)signal_completes, finalize-on-bare-approve, honest agent-steerable gate semantics, and cross-gate session continuity restored. (fix(workflows): interactive-loop completion — signal_completes, finalize-on-bare-approve, honest + agent-steerable gates #2126, fix(workflows): restore interactive-loop cross-gate session continuity (reverts #1923) #2046, Bug: interactive loop node does not terminate on the completion signal (VALIDATED) — re-runs the iteration on approve instead of finalizing #2074)$node.output.fieldon unknown nodes or undeclared fields fails loudly (including$LOOP_PREVrefs and after resume). (fix(workflows): fail loudly on unknown-node $id.output.field refs #2143, fix(workflows): fail loudly on unknown-node $LOOP_PREV.<id>.output.field refs #2165, fix(workflows): re-derive declaredFields on resume for strict $node.output.field access (#2091) #2093)Fixed
--detach), andbash/until_bashresolve the Git-Bash binary correctly instead of the WSL launcher (withARCHON_BASH_PATHoverride). (fix(reliability): stop Windows mid-run deaths (keep-awake + real --detach) #2063, bug(workflows): bash nodes silently fail on Windows whenbashresolves to WSL launcher (System32\bash.exe) — $VAR expansion broken in-cmode #1326, bash workflow nodes fail on Windows: execFile('bash') resolves to WSL bash and mangles multi-line scripts #1808, fix(workflows): resolve Windows bash binary correctly (#1326) — slim, supersedes #1470 #1779)chown; ownership is fixed only where wrong. (the chown appuser:appuser /app step in docker compose takes more 20 minutes to complete #1943, chown for appuser:appuser takes too long to finish #1970, fix(docker): eliminate slow recursive chown in image build and container start #1981)failed; CLIworkflow approveno longer marks paused runs failed on process exit; WebUI-dispatched runs honorworktree.enabled: false. (Gate approve/reject double-resolution guard is read-then-write — concurrent approves can duplicate gate events #2113, fix(workflows): close approve/reject double-resolution TOCTOU with a gate CAS (#2113) #2146, fix(workflows): gate approve/reject keeps the run paused instead of staging a fake 'failed' #2112, CLI workflow approve marks paused workflows as failed on process exit #1123, fix(cli): stop workflow approve from marking paused runs failed on process exit #2191, bug(core): dispatchBackgroundWorkflow ignores workflow worktree.enabled: false on WebUI runs #1368, fix(core): respect worktree.enabled: false in dispatchBackgroundWorkflow #2192)UNTYPED_TRANSIENT_PATTERNS). (fix(providers): throw on Claude API errors surfaced as text instead of completing them #2125, feat(providers): retry400 tool use concurrencyerrors from Claude SDK #1341, fix(providers): retry Claude 400 tool-use-concurrency errors as rate limits #2175)text_deltachunks coalesce (no more fragmented output), the Bedrock backend loads in compiled binaries, and extension-registered models resolve on later DAG nodes. (fix(providers): coalesce Pi text_delta chunks to stop fragmented streaming output #2173, fix(providers): register Pi Bedrock backend so it loads in the compiled binary #2174, fix(providers/pi): extension-registered models resolve on 2nd+ DAG node in a run #2111)smalltier instead of a hardcoded Codex model that 400s on ChatGPT-plan accounts. (Codex title-generator uses config-default gpt-5.3-codex, unsupported on ChatGPT-plan accounts → titles always fall back to truncated text #1855, fix(server): resolve small tier for title generation at server entry points #2190).ymlresolve on the by-name GET/DELETE routes (deleting removes both extension twins); namespaced workflow names (dir/name) work over the HTTP launch/read routes; conversation IDs containing//#URL-encode in PATCH/DELETE. (fix(server/workflows): resolve project workflows with .yml extension #1711, HTTP API cannot launch namespaced workflows: POST /api/workflows/{name}/run rejects names containing '/' #2007, fix(server): accept namespaced workflow names on HTTP launch and read routes #2047, fix(web): URL-encode conversation IDs in PATCH/DELETE requests #1657)retry:is honored on bash/script nodes; loop nodes wait for live background Agent tasks, keep a full audit trail, and check cancellation mid-stream;loop_groupbody lifecycle events are namespaced. (retry: on bash/script nodes is schema-valid but never executes — dispatch returns before the retry loop #2088, fix(workflows): honor retry: on bash/script nodes (#2088) #2096, fix(workflows): wait for live background Agent tasks before completing a node #2134, fix(workflows): follow-up to #2134 — loop-node background-task audit trail + live mid-stream cancel check (#2083) #2136, loop_group body lifecycle events use raw (un-namespaced) node ids — collides in observability streams #2090, fix(workflows): namespace loop_group body lifecycle events (#2090) #2098)sourceUrl/sha. (fix(marketplace): guard fetch-source against missing sourceUrl/sha #1691)/setprojectbinds by conversation DB id; project-scoped conversation cwd resolution;manage_runget on SQLite timestamps; failed runs abandonable via HTTP; resume resolves the covering codebase instead of re-registering the worktree; folder projects skip base-branch auto-detection; no-remote repos' logs/artifacts route under_local/<basename>; session ids no longer thread across provider boundaries; Codex receivessystemPromptby prompt-prepending;requires: [github]enforced on the CLI run path; unknownallowed_tools/denied_toolsnames warn at validation. (fix(core): /setproject writes to conversation DB id, not platform id #1937, [codex] Fix project-scoped conversation cwd resolution #1994, fix(core): manage_run get crash on SQLite string timestamps #2106, fix(server): allow abandoning failed runs via HTTP API #2140, fix(cli): resolve covering codebase on resume instead of re-registering the worktree #2141, fix(workflows): skip base-branch auto-detection on folder projects (#2159) #2164, fix(paths): route no-remote repos' logs/artifacts under _local/<basename> (#2132) #2150, fix(workflows): don't thread session ids across provider boundaries in DAG runs #2120, fix(providers/codex): deliver systemPrompt by prepending to the prompt #2118, fix(cli): enforce requires:[github] gate on the CLI run path (#2089) #2095, fix(workflows): warn on unknown allowed_tools/denied_tools names at validation time #2108)Merging this PR releases 0.6.0 to main.