Conversation
) CLI-launched workflows were visible in the Web UI chat but displayed as plain text only — no WorkflowProgressCard or WorkflowResultCard. The CLI adapter already handled both metadata fields; the sendMessage calls were simply missing from workflowRunCommand. Changes: - Send workflowDispatch message before executeWorkflow (mirrors orchestrator.ts) - Send workflowResult message after successful completion with summary - Wrap result message in try/catch with warn log (same pattern as orchestrator) Fixes #1017 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…for PR #1052 - Wrap dispatch sendMessage in try/catch (matches result card pattern) to prevent UI notification failures from blocking workflow execution - Update dispatch comment to accurately describe structural similarity to orchestrator while noting synchronous CLI semantics and that workerConversationId === conversationId in the CLI path - Add note to result card comment about paused-path exclusion - Add 4 integration tests for workflowRunCommand: dispatch ordering and metadata shape, result card with summary, no result card without summary, and non-throwing DB failure on result persist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Template literals automatically coerce numbers to strings; wrapping with String() is redundant. Removed from formatAge, formatDuration, and all console.log calls in workflow.ts. Also compacted a two-line object spread in workflowStatusCommand to a single line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deduplicate JSON branch in workflowStatusCommand by computing the output array once with a single console.log call, removing the duplicated verbose/non-verbose conditional branches. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply review finding: rename flat log event names to use the
cli.{action}_{state} convention matching the rest of the file.
- workflow_dispatch_surface_failed → cli.workflow_dispatch_surface_failed
- workflow_output_surface_failed → cli.workflow_result_surface_failed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aces (#1035) The cmd.exe fallback in spawnWindowsTerminal() used shell: true, which caused Bun/Node to flatten args into a single string without proper quoting. Paths with spaces were split at whitespace, breaking the /D argument to start. Changes: - Remove shell: true from cmd.exe fallback spawn options - Remove shell?: boolean from trySpawn options type (no callers need it) Fixes #1035 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During SSE streaming, tool calls always appeared below all text because onText appended to the existing message even when it already had tool calls. The server-side persistence already segments at this boundary. Mirror that rule in the client's onText handler: when the last streaming message has tool calls, seal it and start a new message for incoming text. Fixes #1054 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After a workflow completes, the AI had no awareness of results when answering follow-up questions. This adds a "Recent Workflow Results" section to the orchestrator prompt by querying persisted workflow_result messages from the conversation. Changes: - Add getRecentWorkflowResultMessages() to db/messages.ts - Add WorkflowResultContext type and formatWorkflowContextSection() to prompt-builder.ts - Extend buildFullPrompt() with optional workflowContext parameter - Fetch and inject workflow context in handleMessage() before prompt building Fixes #1055 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add console.error logging to silent .catch on SSE reconnect re-fetch (ChatInterface.tsx:~544) so production failures are visible in logs - Extract onText setMessages reducer to chat-message-reducer.ts as a pure function (applyOnText) with 14 unit tests covering all 6 segmentation rules including the new tool-call boundary (issue #1054) - Refactor ChatInterface.onText to delegate to applyOnText Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Parallelize checksums + tarball fetch in serve.ts (removes waterfall latency) - Remove redundant existsSync before readFileSync in update-check.ts (catch already handles ENOENT) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…flowResultCard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CRITICAL: fix metadata filter in getRecentWorkflowResultMessages to check for workflowResult key presence instead of category (which is never persisted to DB); feature was completely non-functional on every call - HIGH: guard JSON.parse(msg.metadata) with typeof check to handle PostgreSQL JSONB columns returned as objects (not strings) by node-postgres - MEDIUM: add structured warn log inside inner metadata parse catch block - LOW: use SELECT id, content, metadata instead of SELECT * in new DB query - LOW: update comments in messages.ts and prompt-builder.ts for accuracy - Tests: add formatWorkflowContextSection unit tests (pure function coverage) - Tests: add getRecentWorkflowResultMessages tests (dialect switch + contract) - Tests: add getDatabaseType mock to messages.test.ts connection mock - Tests: add ../db/messages mock and formatWorkflowContextSection to prompt-builder mock in orchestrator-agent.test.ts - Tests: add handleMessage workflow context injection behavioral tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents "Cannot read properties of undefined (reading 'status')" crash when navigating between chat and workflow execution views during race conditions where run data may be transiently undefined. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename the core AI provider interface and all related types, classes, factory functions, and directory from clients/ to providers/. Rename map: - IAssistantClient → IAgentProvider - ClaudeClient → ClaudeProvider - CodexClient → CodexProvider - getAssistantClient → getAgentProvider - AssistantRequestOptions → AgentRequestOptions - IWorkflowAssistantClient → IWorkflowAgentProvider - AssistantClientFactory → AgentProviderFactory - WorkflowAssistantOptions → WorkflowAgentOptions - packages/core/src/clients/ → packages/core/src/providers/ NOT renamed (user-facing/DB-stored): assistant config key, DEFAULT_AI_ASSISTANT env var, ai_assistant_type DB column. No behavioral changes — purely naming.
…nd docs - AssistantDefaults → ProviderDefaults, ClaudeAssistantDefaults → ClaudeProviderDefaults - Logger domains: client.claude → provider.claude, client.codex → provider.codex - Fix stale JSDoc, error messages, and references in architecture docs, CHANGELOG, testing rules
- ProviderDefaults → CodexProviderDefaults (symmetric with ClaudeProviderDefaults) - Fix stale "AI client" comments in orchestrator-agent.ts and orchestrator.test.ts - Remove dead createMockAgentProvider in test/mocks/streaming.ts (zero importers, wrong method names) - Fix irregular whitespace in .claude/rules/workflows.md
…entprovider refactor: rename IAssistantClient to IAgentProvider
docs: consolidate Claude guidance into CLAUDE.md
…1137) * refactor: extract providers from @archon/core into @archon/providers Move Claude and Codex provider implementations, factory, and SDK dependencies into a new @archon/providers package. This establishes a clean boundary: providers own SDK translation, core owns business logic. Key changes: - New @archon/providers package with zero-dep contract layer (types.ts) - @archon/workflows imports from @archon/providers/types — no mirror types - dag-executor delegates option building to providers via nodeConfig - IAgentProvider gains getCapabilities() for provider-agnostic warnings - @archon/core no longer depends on SDK packages directly - UnknownProviderError standardizes error shape across all surfaces Zero user-facing changes — same providers, same config, same behavior. * refactor: remove config type duplication and backward-compat re-exports Address review findings: - Move ClaudeProviderDefaults and CodexProviderDefaults to the @archon/providers/types contract layer as the single source of truth. @archon/core/config/config-types.ts now imports from there. - Remove provider re-exports from @archon/core (index.ts and types/). Consumers should import from @archon/providers directly. - Update @archon/server to depend on @archon/providers for MessageChunk. * refactor: move structured output validation into providers Each provider now normalizes its own structured output semantics: - Claude already yields structuredOutput from the SDK's native field - Codex now parses inline agent_message text as JSON when outputFormat is set, populating structuredOutput on the result chunk This eliminates the last provider === 'codex' branch from dag-executor, making it fully provider-agnostic. The dag-executor checks structuredOutput uniformly regardless of provider. Also removes the ClaudeCodexProviderDefaults deprecated alias — all consumers now use ClaudeProviderDefaults directly. * fix: address PR review — restore warnings, fix loop options, cleanup Critical fixes: - Restore MCP missing env vars user-facing warning (was silently dropped) - Restore Haiku + MCP tool search warning - Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort, thinking, betas, sandbox were silently lost for loop nodes) - Add TODO(#1135) comments documenting env-leak gate gap Cleanup: - Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage) - Remove 26 unnecessary eslint-disable comments from test files - Trim internal helpers from providers barrel (withFirstMessageTimeout, getProcessUid, loadMcpConfig, buildSDKHooksFromYAML) - Add @archon/providers dep to CLI package.json - Fix 8 stale documentation paths pointing to deleted core/src/providers/ - Add E2E smoke test workflows for both Claude and Codex providers * fix: forward provider system warnings to users in dag-executor The dag-executor only forwarded system chunks starting with "MCP server connection failed:" — all other provider warnings (missing env vars, Haiku+MCP, structured output issues) were logged but never reached the user. Now forwards all system chunks starting with⚠️ (the prefix providers use for user-actionable warnings). * fix: add providers package to Dockerfile and fix CI module resolution - Add packages/providers/ to all three Dockerfile stages (deps, production package.json copy, production source copy) - Replace wildcard export map (./*) with explicit subpath entries to fix module resolution in CI (bun workspace linking) * chore: update bun.lock for providers package exports
* fix: make env-integration test cross-platform (Windows CI) Check for Windows env var equivalents (Path instead of PATH, USERPROFILE instead of HOME) in scenario 3 assertions. Closes #1128 * fix: Windows PATH/HOME casing in provider subprocess env test Same cross-platform fix for ClaudeProvider test — spread objects lose Windows case-insensitive behavior (Path vs PATH, USERPROFILE vs HOME).
…ies (#1162) * refactor: decompose provider sendQuery() into explicit helper boundaries (#1139) sendQuery() in both Claude and Codex providers was a monolith mixing SDK option building, nodeConfig translation, stream normalization, and error classification. This makes it hard to safely extend for Phase 2 provider extensibility. Decompose both providers into focused internal helpers: Claude: - buildBaseClaudeOptions: SDK option construction - buildToolCaptureHooks: PostToolUse/PostToolUseFailure hook setup - applyNodeConfig: workflow nodeConfig → SDK translation + structured warnings - streamClaudeMessages: raw SDK event → MessageChunk normalization - classifyAndEnrichError: error classification with retry decisions Codex: - buildTurnOptions: per-turn option construction (output schema, abort) - streamCodexEvents: raw SDK event → MessageChunk normalization - classifyAndEnrichCodexError: error classification with retry decisions Also introduces ProviderWarning { code, message } replacing raw string warnings for machine-readable provider translation warnings. Adds 43 focused unit tests covering the extracted helpers directly. Fixes #1139 * fix: export ToolResultEntry type used in public buildBaseClaudeOptions API * fix: unexport internal helpers to prevent API surface leakage, fix retry state bug Review findings: 1. Internal helpers were exported and reachable through package.json subpath exports (./claude/provider, ./codex/provider), widening the public API. All new helpers are now file-local — the only public exports remain ClaudeProvider, CodexProvider, loadMcpConfig, buildSDKHooksFromYAML, withFirstMessageTimeout, getProcessUid. 2. Codex streamState (lastTodoListSignature) was shared across retry attempts, causing todo-list dedup to suppress output on retry. Now creates fresh state per attempt. Removed direct helper test imports — existing sendQuery e2e tests (51 Claude + 42 Codex) cover all behavior paths. * fix: address review findings — abort handling, retry bugs, error swallowing Fixes from CodeRabbit + multi-agent review: 1. classifyAndEnrichError preserves first-event timeout diagnostic instead of collapsing it into generic "Query aborted" (the timeout aborts the controller, but the original error carries the #1067 breadcrumb) 2. nodeConfigWarnings emitted once before retry loop, not per attempt 3. buildSubprocessEnv() called once before retry loop (was re-logging auth mode and rebuilding { ...process.env } per attempt) 4. Abort signal listener registered once with forwarding to current controller (was accumulating per-retry listeners) 5. PostToolUse hook wrapped in try/catch (JSON.stringify can throw on circular refs — was asymmetric with PostToolUseFailure which had it) 6. Codex streamCodexEvents throws on abort instead of silent break (callers were getting truncated stream with no result/error) 7. Both providers store enrichedError (not raw error) for retry exhaustion — preserves stderr context in final throw 8. Log is_error result events at error level in Claude stream normalizer * test: add black-box behavioral tests for sendQuery decomposition fixes Restore test coverage for the specific fixes from the decomposition review, exercised through sendQuery (black-box) since helpers are file-local: Claude (6 tests): - Timeout error preserved (not collapsed into "Query aborted") - nodeConfig warnings emitted once even when retries occur - Abort signal cancels across retries via single forwarding listener - Enriched error (with stderr) thrown at retry exhaustion - PostToolUse hook handles circular reference without crashing - is_error result events logged at error level Codex (3 tests): - Abort signal throws instead of silently truncating stream - Enriched error thrown at retry exhaustion - Todo-list dedup state resets between retry attempts
- e2e-all-nodes: exercises bash, prompt, script (bun), structured output, model override (haiku), effort control, and $nodeId.output refs - e2e-mixed-providers: tests Claude + Codex in the same workflow with cross-provider output references - echo-args.js: simple script node test helper
) Remove the entire env-leak scanning/consent infrastructure: scanner, allow_env_keys DB column usage, allow_target_repo_keys config, PATCH consent route, --allow-env-keys CLI flag, and UI consent toggle. The env-leak gate was the wrong primitive. Target repo .env protection is already structural: - stripCwdEnv() at boot removes Bun-auto-loaded CWD .env keys - Archon loads its own env sources afterward (~/.archon/.env) - process.env is clean before any subprocess spawns - Managed env injection (config.yaml env: + DB vars) is unchanged No scanning, no consent, no blocking. Any repo can be registered and used. Subprocesses receive the already-clean process.env.
* Implement managed execution env propagation * Address managed env review feedback
…ss (#1185) * refactor: extract provider metadata seam for Phase 2 registry readiness - Add static capability constants (capabilities.ts) for Claude and Codex - Export getProviderCapabilities() from @archon/providers for capability queries without provider instantiation - Add inferProviderFromModel() to model-validation.ts, replacing three copy-pasted inline inference blocks in executor.ts and dag-executor.ts - Replace throwaway provider instantiation in dag-executor with static capability lookup (getProviderCapabilities) - Add orchestrator warning when env vars are configured but provider doesn't support envInjection * refactor: address LOW findings from code review - Remove CLAUDE_CAPABILITIES/CODEX_CAPABILITIES from public index (YAGNI — callers should use getProviderCapabilities(), not raw constants) - Remove dead _deps parameter from resolveNodeProviderAndModel and its two call-sites (no longer needed after static capability lookup refactor) - Update factory.ts module JSDoc to mention both exported functions - Add edge-case tests for getProviderCapabilities: empty string and case-sensitive throws (parity with existing getAgentProvider tests) - Add test for inferProviderFromModel with empty string (returns default, documenting the falsy-string shortcut)
* feat: replace hardcoded provider factory with typed registry system Replace the built-in-only factory switch with a typed ProviderRegistration registry where entries carry metadata (displayName, capabilities, isModelCompatible) alongside the factory function. This enables community providers to register without modifying core code. - Add ProviderRegistration and ProviderInfo types to contract layer - Create registry.ts with register/get/list/clear API, delete factory.ts - Bootstrap registerBuiltinProviders() at server and CLI entrypoints - Widen provider unions from 'claude' | 'codex' to string across schemas, config types, deps, executors, and API validation - Replace hardcoded model-validation with registry-driven isModelCompatible and inferProviderFromModel (built-in only inference) - Add GET /api/providers endpoint returning registry metadata - Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector, WorkflowBuilder, SettingsPage) via useProviders hook - Dynamic provider selection in CLI setup command - Registry test suite covering full lifecycle * feat: generalize assistant config and tighten registry validation - Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer - Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults - Introduce AssistantDefaults/AssistantDefaultsConfig intersection types that combine ProviderDefaultsMap with typed built-in entries - Replace hardcoded claude/codex config merging with generic mergeAssistantDefaults() that iterates all provider entries - Replace hardcoded toSafeConfig projection with generic toSafeAssistantDefaults() that strips server-internal fields - Validate provider strings at all config-entry surfaces: env override, global config, repo config all throw on unknown providers - Validate provider on PATCH /api/config/assistants (400 on unknown) - Move validator.ts from hardcoded Codex checks to capability-driven warnings using registry getProviderCapabilities() - Remove resolveProvider() default to 'claude' — returns undefined when no provider is set, skipping capability warnings for unresolved nodes - Widen config API schemas to generic Record<string, ProviderDefaults> - Rewrite SettingsPage to iterate providers dynamically with built-in specific UI for Claude/Codex and generic JSON view for community - Extract bootstrap to provider-bootstrap modules in CLI and server - Remove all as Record<...> casts from dag-executor, executor, orchestrator — clean indexing via ProviderDefaultsMap intersection * fix: remove remaining hardcoded provider assumptions and regenerate types - Replace hardcoded 'claude' defaults in CLI setup with registry lookup (getRegisteredProviders().find(p => p.builtIn)?.id) - Replace hardcoded 'claude' default in clone.ts folder detection with registry-driven fallback - Update config YAML comment from "claude or codex" to "registered provider" - Make bootstrap test assertions use toContain instead of exact toEqual so they don't break when community providers are registered - Widen validator.test.ts helper from 'claude' | 'codex' to string - Remove unnecessary type casts in NodeInspector, WorkflowBuilder, SettingsPage now that generated types use string - Regenerate api.generated.d.ts from updated OpenAPI spec — all provider fields are now string instead of 'claude' | 'codex' union * fix: address PR review findings — consistency, tests, docs Critical fixes: - isModelCompatible now throws on unknown providers (fail-fast parity with getProviderCapabilities) instead of silently returning true - Schema provider fields use z.string().trim().min(1) to reject whitespace-only values - validator.ts resolveProvider accepts defaultProvider param so capability warnings fire for config-inherited providers - PATCH /api/config/assistants validates assistants keys against registry (rejects unknown provider IDs in the map) YAGNI cleanup: - Delete provider-bootstrap.ts wrappers in CLI and server — call registerBuiltinProviders() directly - Remove no-op .map(provider => provider) in SettingsPage Test coverage: - Add GET /api/providers endpoint tests (shape, projection, capabilities) - Add config-loader throw-path tests for unknown providers in env var, global config, and repo config - Add isModelCompatible throw test for unknown providers Docs: - CLAUDE.md: factory.ts → registry.ts in directory tree, add GET /api/providers to API endpoints section - .env.example: update DEFAULT_AI_ASSISTANT comment - docs-web configuration reference: update provider constraint docs UI: - Settings default-assistant dropdown uses allProviderEntries fallback (no longer silently empty on API failure) - clearRegistry marked @internal in JSDoc * fix: use registry defaults in getDefaults/registerProject, document type design - getDefaults() initializes assistant defaults from registered providers instead of hardcoding { claude: {}, codex: {} } - getDefaults() uses first registered built-in as default assistant instead of hardcoding 'claude' - handleRegisterProject uses config.assistant instead of hardcoded 'claude' for new codebase ai_assistant_type - Document AssistantDefaults/AssistantDefaultsConfig intersection types: built-in keys are typed for parseClaudeConfig/parseCodexConfig type safety; community providers use the generic [string] index - Document WorkflowConfig.assistants intersection type with same rationale * docs: update stale provider references to reflect registry system - architecture.md: DB schema comment now says 'registered provider' - first-workflow.md: provider field accepts any registered provider - quick-reference.md: provider type changed from enum to string - authoring-workflows.md: provider type changed from enum to string - title-generator.ts: @param doc updated from 'claude or codex' to generic provider identifier * docs: fix remaining stale provider references in quick-reference and authoring guide - quick-reference.md: per-node provider type changed from enum to string - quick-reference.md: model mismatch guidance updated for registry pattern - authoring-workflows.md: provider comment says 'any registered provider'
…ion (#1198) * fix: prevent worktree isolation bypass via prompt and git-level adoption (#1193, #1188) Three fixes for workflows operating on wrong branches: - archon-implement prompt: replace ambiguous branch table with decision tree that trusts the worktree isolation system, uses $BASE_BRANCH explicitly, and instructs AI to never switch branches - WorktreeProvider.findExisting: verify worktree's parent repo matches the request before adopting, preventing cross-clone adoption - WorktreeProvider.createNewBranch: reset stale orphan branches to the intended start-point instead of silently inheriting old commits Fixes #1193 Relates to #1188 * fix: address PR review — strict worktree verification, align sibling prompts Address CodeRabbit + self-review findings on #1198: Code fixes: - findExisting now throws on cross-checkout or unverifiable state instead of returning null, avoiding a confusing cascade through createNewBranch - verifyWorktreeOwnership handles .git errors precisely: ENOENT/EACCES/EIO throw a fail-fast error; EISDIR (full checkout at path) throws a clear "not a worktree" error; unmatched gitdir (submodule, malformed) throws - Path comparison uses resolve() to normalize trailing slashes - Added classifyIsolationError patterns so new errors produce actionable user messages Test fixes: - mockClear readFile/rm in afterEach - New tests: cross-checkout throws, EISDIR throws, EACCES throws, submodule pointer throws, trailing-slash normalization, branch -f reset failure propagates without retry - Updated existing tests that relied on permissive adoption to provide valid matching gitdir Prompt fixes (sweep of all default commands): - archon-implement.md: clarify "never switch branches" applies to worktree context; non-worktree branch creation still allowed - archon-fix-issue.md + archon-implement-issue.md: aligned decision tree with archon-implement pattern; use $BASE_BRANCH instead of MAIN/MASTER - archon-plan-setup.md: converted table to ordered decision tree with IN WORKTREE? first; removed ambiguous "already on correct feature branch" row
* fix: extend worktree ownership guard to resolver adoption paths (#1183, #1188) PR #1198 guarded WorktreeProvider.findExisting(), but IsolationResolver has three earlier adoption paths that bypass the provider layer: - findReusable (DB lookup by workflow identity) - findLinkedIssueEnv (cross-reference via linked issues) - tryBranchAdoption (PR branch discovery) Two clones of the same remote share codebase_id (identity is derived from owner/repo). Without these guards, clone B silently adopts clone A's worktree via any of the three paths. Changes: - Extract verifyWorktreeOwnership from WorktreeProvider (private) to @archon/git/src/worktree.ts as an exported function, sitting next to getCanonicalRepoPath which parses the same .git file format - Call the shared function from all three resolver paths; throw on cross-clone mismatch (DB rows are preserved — they legitimately belong to the other clone) - Compute canonicalRepoPath once at the top of resolve() - Six new tests in resolver.test.ts covering each guarded path's cross-checkout and same-clone behaviors Fixes #1183 Fixes #1188 (part 1 — cross-checkout; part 2 parallel collision deferred to follow-up alongside #1036) * fix: address PR review — polish, observability, secondary gap, docs Addresses the multi-agent review on #1206: Code fixes: - worktree.adoption_refused_cross_checkout log event renamed to match CLAUDE.md {domain}.{action}_{state} convention - verifyWorktreeOwnership now preserves err.code and err via { cause } when wrapping fs errors, so classifyIsolationError is robust to Node message format changes - Structured fields (codebaseId, canonicalRepoPath) added to all cross-clone rejection logs for incident debugging - Wrap getCanonicalRepoPath at top of resolve() with classified error instead of letting it propagate as an unclassified crash - Extract assertWorktreeOwnership helper on IsolationResolver — centralizes warn-then-rethrow contract, removes duplication - Dedupe toWorktreePath(existing.working_path) calls in resolver paths - Add code comment on findLinkedIssueEnv explaining why throw-on-first is intentional (user decision — surfaces anomaly instead of masking) Secondary gap closed: - WorktreeProvider.findExisting PR-branch adoption path (findWorktreeByBranch) now also verifies ownership — same class of bug as the main path, just via a different lookup Tests: - 8 new unit tests for verifyWorktreeOwnership in @archon/git (matching pointer, different clone, EISDIR/ENOENT errno preservation, submodule pointer, corrupted .git, trailing-slash normalization, cause chain) - tryBranchAdoption cross-clone test now asserts store.create was never called (symmetry with paths 1+2 asserting updateStatus) - New test for cross-clone rejection in the PR-branch-adoption secondary path in worktree.test.ts Docs: - CHANGELOG.md Unreleased entry for the cross-clone fix series - troubleshooting.md "Worktree Belongs to a Different Clone" section documenting all four new error patterns with resolution steps and pointer to #1192 for the architectural fix * fix(git): use raw .git pointer in cross-clone error message verifyWorktreeOwnership previously called path.resolve() on the gitdir path before embedding it in the error message. On Windows, resolve() prepends a drive letter to a POSIX-style path (e.g., /other/clone → C:\other\clone), which: 1. Misled users by showing a path that doesn't match what's actually in their .git file 2. Broke a Windows-only test asserting the error contains the literal /other/clone path Compare on resolved paths (correct — normalizes trailing slashes and relative components for the equality check) but display the raw match in the error message (recognizable to the user).
* feat(workflows): inline sub-agent definitions on DAG nodes Add `agents:` node field letting workflow YAML define Claude Agent SDK sub-agents inline, keyed by kebab-case ID. The main agent can spawn them via the Task tool — useful for map-reduce patterns where a cheap model briefs items and a stronger model reduces. Authors no longer need standalone `.claude/agents/*.md` files for workflow-scoped helpers; the definitions live with the workflow. Claude only. Codex and community providers without the capability emit a capability warning and ignore the field. Merges with the internal `dag-node-skills` wrapper when `skills:` is also set — user-defined agents win on ID collision. * fix(workflows): address PR #1276 review feedback Critical: - Re-export agentDefinitionSchema + AgentDefinition from schemas/index.ts (matches the "schemas/index.ts re-exports all" convention). Important: - Surface user-override of internal 'dag-node-skills' wrapper: warn-level provider log + platform message to the user when agents: redefines the reserved ID alongside skills:. User-wins behavior preserved (by design) but silent capability removal is now observable. - Add validator test coverage for the agents-capability warning (codex node with agents: → warning; claude node → no warning; no-agents field → no warning). - Strengthen NodeConfig.agents duplicate-type comment explaining the intentional circular-dep avoidance and pointing at the Zod schema as authoritative source. Actual extraction is follow-up work. Simplifications: - Drop redundant typeof check in validator (schema already enforces). - Drop unreachable Object.keys(...).length > 0 check in dag-executor. - Drop rot-prone "(out of v1 scope)" parenthetical. - Drop WHAT-only comment on AGENT_ID_REGEX. - Tighten AGENT_ID_REGEX to reject trailing/double hyphens (/^[a-z0-9]+(-[a-z0-9]+)*$/). Tests: - parseWorkflow strips agents on script: and loop: nodes (parallel to the existing bash: coverage). - provider emits warn log on dag-node-skills collision; no warn on non-colliding inline agents. Docs: - Renumber authoring-workflows Summary section (12b → 13; bump 13-19). - Add Pi capability-table row for inline agents (❌, Claude-only). - Add when-to-use guidance (agents: vs .claude/agents/*.md) in the new "Inline sub-agents" section. - Cross-link skills.md Related → inline-sub-agents. - CHANGELOG [Unreleased] Added entry for #1276.
…1296) Four defensive fixes to the Pi community provider to match the Claude/Codex contract and eliminate silent error swallowing. 1. envInjection now actually wired (capability was declared but unused) Pi's SDK has no top-level `env` option on createAgentSession, so per-project env vars were being dropped. Routes requestOptions.env through a BashSpawnHook that merges caller env over the inherited baseline (caller wins, matching Claude/Codex semantics). When env is present with no allow/deny, resolvePiTools now explicitly returns Pi's 4 default tools so the pre-constructed default bashTool is replaced with an env-aware one. 2. AsyncQueue no longer leaks on consumer abort. Added close() that drains pending waiters with { done: true } so iterate() exits instead of hanging forever when the producer's finally fires before the next push. bridgeSession calls queue.close() in its finally block. 3. buildResultChunk no longer reports silent success when agent_end fires with no assistant message. Now returns { isError: true, errorSubtype: 'missing_assistant_message' } and logs a warn event so broken Pi sessions don't masquerade as clean completions. 4. session-resolver no longer swallows arbitrary errors from SessionManager.list(). Narrowed the catch to ENOENT/ENOTDIR (the only "session dir doesn't exist yet" signals); permission errors, parse failures, and other unexpected errors now propagate. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…ing (#1297) Pi's SDK has no native JSON-schema mode (unlike Claude's outputFormat / Codex's outputSchema). Previously Pi declared structuredOutput: false and any workflow using output_format silently degraded — the node ran, the transcript was treated as free text, and downstream $nodeId.output.field refs resolved to empty strings. 8 bundled/repo workflows across 10 nodes were affected (archon-create-issue, archon-fix-github-issue, archon-smart-pr-review, archon-workflow-builder, archon-validate-pr, etc.). This PR closes the gap via prompt engineering + post-parse: 1. When requestOptions.outputFormat is present, the provider appends a "respond with ONLY a JSON object matching this schema" instruction plus JSON.stringify(schema) to the prompt before calling session.prompt(). 2. bridgeSession accepts an optional jsonSchema param. When set, it buffers every assistant text_delta and — on the terminal result chunk — parses the buffer via tryParseStructuredOutput (trims whitespace, strips ```json / ``` fences, JSON.parse). On success, attaches structuredOutput to the result chunk (matching Claude's shape). On failure, emits a warn event and leaves structuredOutput undefined so the executor's existing dag.structured_output_missing path handles it. 3. Flipped PI_CAPABILITIES.structuredOutput to true. Unlike Claude/Codex this is best-effort, not SDK-enforced — reliable on GPT-5, Claude, Gemini 2.x, recent Qwen Coder, DeepSeek V3, less reliable on smaller or older models that ignore JSON-only instructions. Tests added (14 total): - tryParseStructuredOutput: clean JSON, fenced, bare fences, arrays, whitespace, empty, prose-wrapped (fails), malformed, inner backticks - augmentPromptForJsonSchema via provider integration: schema appended, prompt unchanged when absent - End-to-end: clean JSON → structuredOutput parsed; fenced JSON parses; prose-wrapped → no structuredOutput + no crash; no outputFormat → never sets structuredOutput even if assistant happens to emit JSON Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Adds `assistants.pi.enableExtensions` (default false) to `.archon/config.yaml`. When true, Pi's `noExtensions` guard is lifted so the session loads tools and lifecycle hooks from `~/.pi/agent/extensions/`, packages installed via `pi install npm:<pkg>`, and the workflow's cwd `.pi/` directory — opening up the community extension ecosystem at https://shittycodingagent.ai/packages. Default stays suppressed to preserve the "Archon is source of truth" trust boundary: enabling this loads arbitrary JS under the Archon server's OS permissions, including whatever extension code the target repo happens to ship. Operators opt in explicitly, per-host. Skills, prompt templates, themes, and context files remain suppressed even when extensions are enabled — only the extensions gate opens. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…ght (#1079) Starlight removes the `starlight-theme` localStorage key when the user selects "auto" mode. The old init script checked that key, so every navigation or refresh re-forced dark theme. Use a separate `archon-theme-init` sentinel that persists across theme changes. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…iku sub-agents (#1293) * feat(workflows): add repo-triage — 6-node periodic maintenance workflow Adds .archon/workflows/repo-triage.yaml: a self-contained periodic maintenance workflow that uses inline sub-agents (Claude SDK agents: field introduced in #1276) for map-reduce across open issues and PRs. Six DAG nodes, three-layer topology: - Layer 1 (parallel): triage-issues, link-prs, closed-pr-dedup-check, stale-nudge - Layer 2: closed-dedup-check (reads triage-issues state) - Layer 3: digest (synthesises all prior nodes + writes markdown) Capabilities per node: - triage-issues: delegates labeling to on-disk triage-agent; inline brief-gen Haiku for duplicate detection; 3-day auto-close clock for unanswered duplicate warnings - link-prs: conservative PR ↔ issue cross-refs via inline pr-issue- matcher Haiku, Sonnet re-verifies fully-addresses claims before suggesting Closes #X; auto-nudges on low-quality PR template fill with first-run grandfather guard (snapshot-only, no nudge spam) - closed-dedup-check: cross-matches open issues against recently- closed ones via inline closed-brief-gen Haiku; same 3-day clock - closed-pr-dedup-check: flags open PRs duplicating recently-closed PRs via inline pr-brief-gen Haiku; comment-only, never closes PRs - stale-nudge: 60-day inactivity pings (configurable); no auto-close - digest: synthesises per-node outputs + reads state files to emit $ARTIFACTS_DIR/digest.md with clickable GitHub comment links Env-gated rollout knobs: - DRY_RUN=1 (read-only; prints [DRY] lines, no gh/state mutations) - SKIP_PR_LINK=1, SKIP_CLOSED_DEDUP=1, SKIP_CLOSED_PR_DEDUP=1, SKIP_STALE_NUDGE=1 - STALE_DAYS=N (stale-nudge window; default 60) Cross-run state under .archon/state/ (gitignored): - triage-state.json briefs + pendingDedupComments - closed-dedup-state.json closedBriefs + closedMatchComments - closed-pr-dedup-state.json openBriefs + closedBriefs + matches - pr-state.json linkedPrs + commentIds + templateAdherence - stale-nudge-state.json nudged (with updatedAtAtNudge for re-nudge) Every bot comment: - @-tags the target human (reporter for issues, author for PRs) - Tracks comment ID in state for traceability - Is idempotent — re-runs skip existing comments Intended use: invoke periodically (`archon workflow run repo-triage --no-worktree`) once a scheduler lands; live state persists across runs so previously-flagged items reconcile correctly. .gitignore: adds .archon/state/ for cross-run memory files. * feat(workflows/repo-triage): post digest to Slack when SLACK_WEBHOOK is set Extends the digest node with an optional Slack-post step after the canonical digest.md artifact is written. Uses Slack incoming webhook (no bot token required beyond the incoming-webhook scope). Behavior: - SLACK_WEBHOOK unset → skipped silently with a one-line note - DRY_RUN=1 → prints full payload, does not curl - Otherwise → POSTs a compact (<3500 char) mrkdwn-formatted summary containing headline numbers, this-run comment index (clickable GitHub URLs), pending items, and a path reference to digest.md - curl failure or non-ok Slack response is logged but does not fail the node — digest.md on disk remains authoritative - Intermediate Slack text written to $ARTIFACTS_DIR/digest-slack.txt for traceability; payload JSON assembled via jq and written to $ARTIFACTS_DIR/slack-payload.json before curl posts it Slack mrkdwn conversion rules baked into the prompt (no tables, link shape <url|text>, single-asterisk bold) so Sonnet emits a variant that renders cleanly in Slack rather than being sent raw. The webhook URL is read from the operator's environment (Archon auto-loads ~/.archon/.env on CLI startup — put SLACK_WEBHOOK=... there). * fix(workflows/repo-triage): address PR #1293 review feedback Critical (3): - `gh issue close --reason "not planned"` (space, not underscore) — the CLI expects lowercase with a space; `not_planned` fails at runtime. Fixed in both auto-close paths (triage-issues step 8, closed-dedup- check step 7). - link-prs step 7 state save was sparse `{ sha, processedAt, related, fullyAddresses }`, overwriting `commentIds` / `templateNudgedAt` / `templateAdherence`. Changed to explicit merge that spreads existing entry first so per-run captured fields survive. - Corrupt-JSON state files previously treated as first-run default (silent `pendingDedupComments` reset → 3-day clock restarts forever). All five state-load sites now abort loudly on JSON.parse throw; ENOENT/empty continue to default-shape. Important (7): - Sub-agents (`brief-gen`, `closed-brief-gen`, `pr-brief-gen`, `pr-issue-matcher`) emit `ERROR: <reason>` on gh failures rather than partial/fabricated JSON. Orchestrator detects the sentinel, logs the failed ID + first 200 chars of raw response, tracks in a failed-list, and aborts the cluster/match pass if ≥50% of items failed (avoids acting on bad data). - `pr-brief-gen` now sets `diffTruncated: true` when the 30k-char diff cap hits; link-prs verify pass downgrades any `fully-addresses` claim to `related` when either side's brief was truncated. - 3-day auto-close validates `postedAt` parses as ISO-8601 before the elapsed-time comparison; corrupt timestamps are logged and skipped, never acted on. - `gh issue close` failure path no longer drops state — sets `closeAttemptFailed: true` on the entry for next-run retry. Only drops on exit 0. - `closed-pr-dedup-check` idempotency check (`gh pr view --json comments`) now aborts the post on fetch failure rather than falling through — prevents double-posts on gh hiccups. - `triage-agent` label pass has preflight `test -f` check for `.claude/agents/triage-agent.md`; skips the pass with a clear log if the file is missing rather than firing Task calls that fail obscurely. - `brief-gen` template-adherence wording flipped from "Ignore … as 'filled'" (ambiguous, read as affirmative) to explicit "A section counts as MISSING when …", matching the `pr-issue-matcher` phrasing. Minor: - `stale-nudge` idempotency check uses substring "has been quiet for" instead of a prefix check that never matched (posted body starts with @<author>). - `closed-dedup-check` distinguishes "upstream crashed" (missing/corrupt triage-state.json, or `lastRunAt == null`) from "legitimately quiet day" (state present, briefs empty) — different log lines. - Slack curl adds `-w "\nHTTP_STATUS:%{http_code}"` + `2>&1` so TLS / 4xx / 5xx errors are visible in captured output. - `stateReason` values from `gh issue view --json stateReason` are UPPERCASE (`COMPLETED`, `NOT_PLANNED`); documented and instruct sub-agent to normalize to lowercase for consistency. Docs: - CLAUDE.md repo-level `.archon/` tree now lists `state/`. - archon-directories.md tree adds `state/` + `scripts/` (both were missing) with purpose descriptions. Deferred (worth doing as a follow-up, not blocking): - DRY/SKIP preamble duplication (~30-50 lines across 5 nodes). - Explicit `BASELINE_IS_EMPTY` capture in link-prs (current derived check works but is a load-bearing model instruction). - Digest `WARNING` prefix block when upstream nodes are missing outputs — today's "(output unavailable)" sub-line is functional. - Pre-existing README workflow-count (17 → 20) and table gaps — not caused by this PR.
, #1303) (#1304) * feat(paths/cli/setup): unify env load + write on three-path model (#1302, #1303) Key env handling on directory ownership rather than filename. `.archon/` (at `~/` or `<cwd>/`) is archon-owned; anything else is the user's. - `<repo>/.env` — stripped at boot (guard kept), never loaded, never written - `<repo>/.archon/.env` — loaded at repo scope (wins over home), writable via `archon setup --scope project` - `~/.archon/.env` — loaded at home scope, writable via `--scope home` (default) Read side (#1302): - New `@archon/paths/env-loader` with `loadArchonEnv(cwd)` shared by CLI and server entry points. Loads both archon-owned files with `override: true`; repo scope wins. - Replaced `[dotenv@17.3.1] injecting env (0) from .env` (always lied about stripped keys) with `[archon] stripped N keys from <cwd> (...)` and `[archon] loaded N keys from <path>` lines, emitted only when N > 0. `quiet: true` passed to dotenv to silence its own output. - `stripCwdEnv` unchanged in semantics — still the only source that deletes keys from `process.env`; now logs what it did. Write side (#1303): - `archon setup` never writes to `<repo>/.env`. Writing there was incoherent because `stripCwdEnv` deletes those keys on every run. - New `--scope home|project` (default home) targets exactly one archon-owned file. New `--force` overrides the merge; backup still written. - Merge-only by default: existing non-empty values win, user-added custom keys survive, `<path>.archon-backup-<ISO-ts>` written before every rewrite. Fixes silent PostgreSQL→SQLite downgrade and silent token loss in Add mode. - One-time migration note emitted when `<cwd>/.env` exists at setup start. Tests: new `env-loader.test.ts` (6), extended `strip-cwd-env.test.ts` (+4 for the log line), extended `setup.test.ts` (+10 for scope/merge/backup/force/ repo-untouched), extended `cli.test.ts` (+5 for flag parsing). Docs: configuration.md, cli.md, security.md, cli-internals.md, setup skill — all updated to the three-path model. * fix(cli/setup): address PR review — scope/path/secret-handling edge cases - cli: resolve --scope project to git repo root so running setup from a subdir writes to <repo-root>/.archon/.env (what loadArchonEnv reads at boot), not <subdir>/.archon/.env. Fail fast with a useful message when --scope project is used outside a git repo. - setup: resolveScopedEnvPath() now delegates to @archon/paths helpers (getArchonEnvPath / getRepoArchonEnvPath) so Docker's /.archon home, ARCHON_HOME overrides, and the "undefined" literal guard all behave identically between the loader and the writer. - setup: wrap the writeScopedEnv call in try/catch so an fs exception (permission denied, read-only FS, backup copy failure) stops the clack spinner cleanly and emits an actionable error instead of a raw stack trace after the user has completed the entire wizard. - setup: checkExistingConfig(envPath?) — scope-aware existing-config read. Add/Update/Fresh now reflects the actual write target, not an unconditional ~/.archon/.env. - setup: serializeEnv escapes \r (was only \n) so values with bare CR or CRLF round-trip through dotenv.parse without corruption. Regression test added. - setup: merge path treats whitespace-only existing values (' ') as empty, so a copy-paste stray space doesn't silently defeat the wizard update for that key forever. Regression test added. - setup: 0o600 mode on the written env file AND on backup copies — writeFileSync+copyFileSync default to 0o666 & ~umask, which can leave secrets group/world-readable on a permissive umask. - docs/cli.md + setup skill: appendix sections that still described the pre-#1303 two-file symlink model now reflect the three-path model. * fix(paths/env-loader): Windows-safe assertion for home-scope load line The test asserted the log line contained `from ~/`, which is opportunistic tilde-shortening that only happens when the tmpdir lives under `homedir()`. On Windows CI the tmpdir is on `D:\\` while homedir is `C:\\Users\\...`, so the path renders absolute and the `~/` never appears. Match on the count and the archon-home tmpdir segment instead — robust on both Unix tilde-short paths and Windows absolute paths.
Machine-local runtime state from the Claude Code scheduler (pid + sessionId + acquisition timestamp). Should not be shared across machines.
…1299) * feat(providers/pi): interactive flag binds UIContext for extensions Adds `interactive: true` opt-in to Pi provider (in `.archon/config.yaml` under `assistants.pi`) that binds a minimal `ExtensionUIContext` stub to each session. Without this, Pi's `ExtensionRunner.hasUI()` reports false, causing extensions like `@plannotator/pi-extension` to silently auto-approve every plan instead of opening their browser review UI. Semantics: clamped to `enableExtensions: true` — no extensions loaded means nothing would consume `hasUI`, so `interactive` alone is silently dropped. Stub forwards `notify()` to Archon's event stream; interactive dialogs (select/confirm/input/editor/custom) resolve to undefined/false; TUI-only setters (widgets/headers/footers/themes) no-op. Theme access throws with a clear diagnostic — Pi's theme singleton is coupled to its own `Symbol.for()` registry which Archon doesn't own. Trust boundary: only binds when the operator has explicitly enabled both flags. Extensions gated on `ctx.hasUI` (plannotator and similar) get a functional UI context; extensions that reach for TUI features still fail loudly rather than rendering garbage. Includes smoke-test workflow documenting the integration surface. End-to-end plannotator UI rendering requires plan-mode activation (Pi `--plan` CLI flag or `/plannotator` TUI slash command) which is out of reach for programmatic Archon sessions — manual test only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(providers/pi): end-to-end interactive extension UI Three fixes that together get plannotator's browser review UI to actually render from an Archon workflow and reach the reviewer's browser. 1. Call resourceLoader.reload() when enableExtensions is true. createAgentSession's internal reload is gated on `!resourceLoader`, so caller-supplied loaders must reload themselves. Without this, getExtensions() returns the empty default, no ExtensionRunner is built, and session.extensionRunner.setFlagValue() silently no-ops. 2. Set PLANNOTATOR_REMOTE=1 in interactive mode. plannotator-browser.ts only calls ctx.ui.notify(url) when openBrowser() returns { isRemote: true }; otherwise it spawns xdg-open/start on the Archon server host — invisible to the user and untestable from bash asserts. From the workflow runner's POV every Archon execution IS remote; flipping the heuristic routes the URL through notify(), which the ExtensionUIContext stub forwards into the event stream. Respect explicit operator overrides. 3. notify() emits as assistant chunks, not system chunks. The DAG executor's system-chunk filter only forwards warnings/MCP prefixes, and only assistant chunks accumulate into $nodeId.output. Emitting as assistant makes the URL available both in the user's stream and in downstream bash/script nodes via output substitution. Plus: extensionFlags config pass-through (equivalent to `pi --plan` on the CLI) applied via ExtensionRunner.setFlagValue() BEFORE bindExtensions fires session_start, so extensions reading flags in their startup handler actually see them. Also bind extensions with an empty binding when enableExtensions is on but interactive is off, so session_start still fires for flag-driven but UI-less extensions. Smoke test (.archon/workflows/e2e-plannotator-smoke.yaml) uses openai-codex/gpt-5.4-mini (ChatGPT Plus OAuth compatible) and bumps idle_timeout to 600000ms so plannotator's server survives while a human approves in the browser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(providers/pi): keep Archon extension-agnostic Remove the plannotator-specific PLANNOTATOR_REMOTE=1 env var write from the Pi provider. Archon's provider layer shouldn't know about any specific extension's internals. Document the env var in the plannotator smoke test instead — operators who use plannotator set it via their shell or per-codebase env config. Workflow smoke test updated with: - Instructions for setting PLANNOTATOR_REMOTE=1 externally - Simpler assertion (URL emission only) — validated in a real reject-revise-approve run: reviewer annotated, clicked Send Feedback, Pi received the feedback as a tool result, revised the plan (added aria-label and WCAG contrast per the annotation), resubmitted, and reviewer approved. Plannotator's tool result signals approval but doesn't return the plan text, so the bash assertion now only checks that the review URL reached the stream (not that plan content flowed into \$nodeId.output — it can't). - Known-limitation note documenting the tool-result shape so downstream workflow authors know to Write the plan separately if they need it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(providers/pi): keep e2e-plannotator-smoke workflow local-only The smoke test is plannotator-specific (calls plannotator_submit_plan, expects PLAN.md on disk, requires PLANNOTATOR_REMOTE=1) and is better kept out of the PR while the extension-agnostic infra lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * style(providers/pi): trim verbose inline comments Collapse multi-paragraph SDK explanations to 1-2 line "why" notes across provider.ts, types.ts, ui-context-stub.ts, and event-bridge.ts. No behavior change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(providers/pi): wire assistants.pi.env + theme-proxy identity Two end-to-end fixes discovered while exercising the combined plannotator + @pi-agents/loop smoke flow: - PiProviderDefaults gains an optional `env` map; parsePiConfig picks it up and the provider applies it to process.env at session start (shell env wins, no override). Needed so extensions like plannotator can read PLANNOTATOR_REMOTE=1 from config.yaml without requiring a shell export before `archon workflow run`. - ui-context-stub theme proxy returns identity decorators instead of throwing on unknown methods. Styled strings flow into no-op setStatus/setWidget sinks anyway, so the throw was blocking plannotator_submit_plan after HTTP approval with no benefit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(providers/pi): flush notify() chunks immediately in batch mode Batch-mode adapters (CLI) accumulate assistant chunks and only flush on node completion. That broke plannotator's review-URL flow: Pi's notify() emitted the URL as an assistant chunk, but the user needed the URL to POST /api/approve — which is what unblocks the node in the first place. Adds an optional `flush` flag on assistant MessageChunks. notify() sets it, and the DAG executor drains pending batched content before surfacing the flushed chunk so ordering is preserved. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: mention Pi alongside Claude and Codex in README + top-level docs The AI assistants docs page already covers Pi in depth, but the README architecture diagram + docs table, overview "Further Reading" section, and local-deployment .env comment still listed only Claude/Codex. Left feature-specific mentions alone where Pi genuinely lacks support (e.g. structured output — Claude + Codex only). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: note Pi structured output (best-effort) in matrix + workflow docs Pi gained structured output support via prompt augmentation + JSON extraction (see packages/providers/src/community/pi/capabilities.ts). Unlike Claude/Codex, which use SDK-enforced JSON mode, Pi appends the schema to the prompt and parses JSON out of the result text (bare or fenced). Updates four stale references that still said Claude/Codex-only: - ai-assistants.md capabilities matrix - authoring-workflows.md (YAML example + field table) - workflow-dag.md skill reference - CLAUDE.md DAG-format node description Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(providers/pi): default extensions + interactive to on Extensions (community packages like @plannotator/pi-extension and user-authored ones) are a core reason users pick Pi. Defaulting enableExtensions and interactive to false previously silenced installed extensions with no signal, leading to "did my extension even load?" confusion. Opt out in .archon/config.yaml when you want the prior behavior: assistants: pi: enableExtensions: false # skip extension discovery entirely # interactive: false # load extensions, but no UI bridge Docs gain a new "Extensions (on by default)" section in getting-started/ai-assistants.md that documents the three config surfaces (extensionFlags, env, workflow-level interactive) and uses plannotator as a concrete walk-through example. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
CLAUDE.md is the primary entry point for agents working in this repo, but it only mentioned Pi once — buried in a DAG-node capability parenthetical. Add Pi to the directory tree, Package Split blurb, and AI Agent Providers list so Pi is discoverable without relying on the docs site or git log. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ack (#1033) * fix(db): throw on corrupt commands JSON instead of silent empty fallback (#967) getCodebaseCommands() silently returned {} when the commands column contained corrupt JSON. Callers had no way to distinguish 'no commands' from 'unreadable data', violating fail-fast principles. Now throws a descriptive error with the codebase ID and a recovery hint. The error is still logged for observability before throwing. Adds two test cases: corrupt JSON throws, valid JSON string parses. * fix: include parse error in log for better diagnostics
All 15 worktree git-subprocess timeouts in WorktreeProvider were hardcoded at 30000ms. Repos with heavy post-checkout hooks (lint, dependency install, submodule init) routinely exceed that budget and fail worktree creation. Consolidate them onto a single GIT_OPERATION_TIMEOUT_MS constant at 5 min. Generous enough to cover reported cases while still catching genuine hangs (credential prompts in non-TTY, stalled fetches). Chosen over the config-key approach in #1029 to avoid adding permanent .archon/config.yaml surface for a problem a raised default solves cleanly. If 5 min turns out to also be too tight for real-world use, we'll revisit. Closes #1119 Supersedes #1029 Co-authored-by: Shay Elmualem <12733941+norbinsh@users.noreply.github.com>
… drop globalSearchPath (closes #1136) (#1315) * feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath Collapses the awkward `~/.archon/.archon/workflows/` convention to a direct `~/.archon/workflows/` child (matching `workspaces/`, `archon.db`, etc.), adds home-scoped commands and scripts with the same loading story, and kills the opt-in `globalSearchPath` parameter so every call site gets home-scope for free. Closes #1136 (supersedes @jonasvanderhaegen's tactical fix — the bug was the primitive itself: an easy-to-forget parameter that five of six call sites on dev dropped). Primitive changes: - Home paths are direct children of `~/.archon/`. New helpers in `@archon/paths`: `getHomeWorkflowsPath()`, `getHomeCommandsPath()`, `getHomeScriptsPath()`, and `getLegacyHomeWorkflowsPath()` (detection-only for migration). - `discoverWorkflowsWithConfig(cwd, loadConfig)` reads home-scope internally. The old `{ globalSearchPath }` option is removed. Chat command handler, Web UI workflow picker, orchestrator resolve path — all inherit home-scope for free without maintainer patches at every new site. - `discoverScriptsForCwd(cwd)` merges home + repo scripts (repo wins on name collision). dag-executor and validator use it; the hardcoded `resolve(cwd, '.archon', 'scripts')` single-scope path is gone. - Command resolution is now walked-by-basename in each scope. `loadCommand` and `resolveCommand` walk 1 subfolder deep and match by `.md` basename, so `.archon/commands/triage/review.md` resolves as `review` — closes the latent bug where subfolder commands were listed but unresolvable. - All three (`workflows/`, `commands/`, `scripts/`) enforce a 1-level subfolder cap (matches the existing `defaults/` convention). Deeper nesting is silently skipped. - `WorkflowSource` gains `'global'` alongside `'bundled'` and `'project'`. Web UI node palette shows a dedicated "Global (~/.archon/commands/)" section; badges updated. Migration (clean cut — no fallback read): - First use after upgrade: if `~/.archon/.archon/workflows/` exists, Archon logs a one-time WARN per process with the exact `mv` command: `mv ~/.archon/.archon/workflows ~/.archon/workflows && rmdir ~/.archon/.archon` The legacy path is NOT read — users migrate manually. Rollback caveat noted in CHANGELOG. Tests: - `@archon/paths/archon-paths.test.ts`: new helper tests (default HOME, ARCHON_HOME override, Docker), plus regression guards for the double-`.archon/` path. - `@archon/workflows/loader.test.ts`: home-scoped workflows, precedence, subfolder 1-depth cap, legacy-path deprecation warning fires exactly once per process. - `@archon/workflows/validator.test.ts`: home-scoped commands + subfolder resolution. - `@archon/workflows/script-discovery.test.ts`: depth cap + merge semantics (repo wins, home-missing tolerance). - Existing CLI + orchestrator tests updated to drop `globalSearchPath` assertions. E2E smoke (verified locally, before cleanup): - `.archon/workflows/e2e-home-scope.yaml` + scratch repo at /tmp - Home-scoped workflow discovered from an unrelated git repo - Home-scoped script (`~/.archon/scripts/*.ts`) executes inside a script node - 1-level subfolder workflow (`~/.archon/workflows/triage/*.yaml`) listed - Legacy path warning fires with actionable `mv` command; workflows there are NOT loaded Docs: `CLAUDE.md`, `docs-web/guides/global-workflows.md` (full rewrite for three-type scope + subfolder convention + migration), `docs-web/reference/ configuration.md` (directory tree), `docs-web/reference/cli.md`, `docs-web/guides/authoring-workflows.md`. Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com> * test(script-discovery): normalize path separators in mocks for Windows The 4 new tests in `scanScriptDir depth cap` and `discoverScriptsForCwd — merge repo + home with repo winning` compared incoming mock paths with hardcoded forward-slash strings (`if (path === '/scripts/triage')`). On Windows, `path.join('/scripts', 'triage')` produces `\scripts\triage`, so those branches never matched, readdir returned `[]`, and the tests failed. Added a `norm()` helper at module scope and wrapped the incoming `path` argument in every `mockImplementation` before comparing. Stored paths go through `normalizeSep()` in production code, so the existing equality assertions on `script.path` remain OS-independent. Fixes Windows CI job `test (windows-latest)` on PR #1315. * address review feedback: home-scope error handling, depth cap, and tests Critical fixes: - api.ts: add `maxDepth: 1` to all 3 findMarkdownFilesRecursive calls in GET /api/commands (bundled/home/project). Without this the UI palette surfaced commands from deep subfolders that the executor (capped at 1) could not resolve — silent "command not found" at runtime. - validator.ts: wrap home-scope findMarkdownFilesRecursive and resolveCommandInDir calls in try/catch so EACCES/EPERM on ~/.archon/commands/ doesn't crash the validator with a raw filesystem error. ENOENT still returns [] via the underlying helper. Error handling fixes: - workflow-discovery.ts: maybeWarnLegacyHomePath now sets the "warned-once" flag eagerly before `await access()`, so concurrent discovery calls (server startup with parallel codebase resolution) can't double-warn. Non-ENOENT probe errors (EACCES/EPERM) now log at WARN instead of DEBUG so permission issues on the legacy dir are visible in default operation. - dag-executor.ts: wrap discoverScriptsForCwd in its own try/catch so an EACCES on ~/.archon/scripts/ routes through safeSendMessage / logNodeError with a dedicated "failed to discover scripts" message instead of being mis-attributed by the outer catch's "permission denied (check cwd permissions)" branch. Tests: - load-command-prompt.test.ts (new): 6 tests covering the executor's command resolution hot path — home-scope resolves when repo misses, repo shadows home, 1-level subfolder resolvable by basename, 2-level rejected, not-found, empty-file. Runs in its own bun test batch. - archon-paths.test.ts: add getHomeScriptsPath describe block to match the existing getHomeCommandsPath / getHomeWorkflowsPath coverage. Comment clarity: - workflow-discovery.ts: MAX_DISCOVERY_DEPTH comment now leads with the actual value (1) before describing what 0 would mean. - script-discovery.ts: copy the "routing ambiguity" rationale from MAX_DISCOVERY_DEPTH to MAX_SCRIPT_DISCOVERY_DEPTH. Cleanup: - Remove .archon/workflows/e2e-home-scope.yaml — one-off smoke test that would ship permanently in every project's workflow list. Equivalent coverage exists in loader.test.ts. Addresses all blocking and important feedback from the multi-agent review on PR #1315. --------- Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
… policy (#1310) * feat(isolation): per-project worktree.path + collapse to two layouts Adds an opt-in `worktree.path` to .archon/config.yaml so a repo can co-locate worktrees with its own checkout (`<repoRoot>/<path>/<branch>`) instead of the default `~/.archon/workspaces/<owner>/<repo>/worktrees/<branch>`. Requested in joelsb's #1117. Primitive changes (clean up the graveyard rather than add parallel code paths): - Collapse worktree layouts from three to two. The old "legacy global" layout (`~/.archon/worktrees/<owner>/<repo>/<branch>`) is gone — every repo resolves to the workspace-scoped layout (`~/.archon/workspaces/<owner>/<repo>/worktrees/<branch>`), whether it was archon-cloned or locally registered. `extractOwnerRepo()` on the repo path is the stable identity fallback. Ends the divergence where workspace-cloned and local repos had visibly different worktree trees. - `getWorktreeBase()` in @archon/git now returns `{ base, layout }` and accepts an optional `{ repoLocal }` override. The layout value replaces the old `isProjectScopedWorktreeBase()` classification at the call sites (`isProjectScopedWorktreeBase` stays exported as deprecated back-compat). - `WorktreeCreateConfig.path` carries the validated override from repo config. `resolveRepoLocalOverride()` fails loudly on absolute paths, `..` escapes, and resolve-escape edge cases (Fail Fast — no silent default fallback when the config is syntactically wrong). - `WorktreeProvider.create()` now loads repo config exactly once and threads it through `getWorktreePath()` + `createWorktree()`. Replaces the prior swallow-then-retry pattern flagged on #1117. `generateEnvId()` is gone — envId is assigned directly from the resolved path (the invariant was already documented on `destroy(envId)`). Tests (packages/git + packages/isolation): - Update the pre-existing `getWorktreeBase` / `isProjectScopedWorktreeBase` suite for the new two-layout return shape and precedence. - Add 8 tests for `worktree.path`: default fallthrough, empty/whitespace ignored, override wins for workspace-scoped repos, rejects absolute, rejects `../` escapes (three variants), accepts nested relative paths. Docs: add `worktree.path` to the repo config reference with explicit precedence and the `.gitignore` responsibility note. Co-authored-by: Joel Bastos <joelsb2001@gmail.com> * feat(workflows): per-workflow worktree.enabled policy Introduces a declarative top-level `worktree:` block on a workflow so authors can pin isolation behavior regardless of invocation surface. Solves the case where read-only workflows (e.g. `repo-triage`) should always run in the live checkout, without every CLI/web/scheduled-trigger caller having to remember to set the right flag. Schema (packages/workflows/src/schemas/workflow.ts + loader.ts): - New optional `worktree.enabled: boolean` on `workflowBaseSchema`. Loader parses with the same warn-and-ignore discipline used for `interactive` and `modelReasoningEffort` — invalid shapes log and drop rather than killing workflow discovery. Policy reconciliation (packages/cli/src/commands/workflow.ts): - Three hard-error cases when YAML policy contradicts invocation flags: • `enabled: false` + `--branch` (worktree required by flag, forbidden by policy) • `enabled: false` + `--from` (start-point only meaningful with worktree) • `enabled: true` + `--no-worktree` (policy requires worktree, flag forbids it) - `enabled: false` + `--no-worktree` is redundant, accepted silently. - `--resume` ignores the pinned policy (it reuses the existing run's worktree even when policy would disable — avoids disturbing a paused run). Orchestrator wiring (packages/core/src/orchestrator/orchestrator-agent.ts): - `dispatchOrchestratorWorkflow` short-circuits `validateAndResolveIsolation` when `workflow.worktree?.enabled === false` and runs directly in `codebase.default_cwd`. Web chat/slack/telegram callers have no flag equivalent to `--no-worktree`, so the YAML field is their only control. - Logged as `workflow.worktree_disabled_by_policy` for operator visibility. First consumer (.archon/workflows/repo-triage.yaml): - `worktree: { enabled: false }` — triage reads issues/PRs and writes gh labels; no code mutations, no reason to spin up a worktree per run. Tests: - Loader: parses `worktree.enabled: true|false`, omits block when absent. - CLI: four new integration tests for the reconciliation matrix (skip when policy false, three hard-error cases, redundant `--no-worktree` accepted, `--no-worktree` + `enabled: true` rejected). Docs: authoring-workflows.md gets the new top-level field in the schema example with a comment explaining the precedence and the `enabled: true|false` semantics. * fix(isolation): use path.sep for repo-containment check on Windows resolveRepoLocalOverride was hardcoding '/' as the separator in the startsWith check, so on Windows (where `resolve()` returns backslash paths like `D:\Users\dev\Projects\myapp`) every otherwise-valid relative `worktree.path` was rejected with "resolves outside the repo root". Fixed by importing `path.sep` and using it in the sentinel. Fixes the 3 Windows CI failures in `worktree.path repo-local override`. --------- Co-authored-by: Joel Bastos <joelsb2001@gmail.com>
) The Settings page's Platform Connections section hardcoded every platform except Web to 'Not configured', so users couldn't tell whether their Slack/ Telegram/Discord/GitHub/Gitea/GitLab adapters had actually started. - Server: /api/health now returns an activePlatforms array populated live as each adapter's start() resolves. Passed into registerApiRoutes so the reference stays mutable — Telegram starts after the HTTP listener is already accepting requests, so a snapshot would miss it. - Web: SettingsPage.PlatformConnectionsSection now reads activePlatforms from /api/health and looks each platform up in a Set. Also adds Gitea and GitLab to the list (they already ship as adapters). Closes #1031 Co-authored-by: Lior Franko <liorfr@dreamgroup.com>
#1328) Three related fixes around the `worktree.copyFiles` primitive: 1. Remove the `.env.example -> .env` rename example from reference/configuration.md and getting-started/overview.md. The `->` parser was removed in #739 (2026-03-19) because it caused the stale-credentials production bug in #228 — but the docs kept advertising it. A user writing `.env.example -> .env` today gets `parseCopyFileEntry` returning `{source: '.env.example -> .env', destination: '.env.example -> .env'}`, stat() fails with ENOENT, and the copy silently no-ops at debug level. 2. Replace the single-line "Default behavior: .archon/ is always copied" note with a proper "Worktree file copying" subsection that explains: - Why this exists (git worktree add = tracked files only; gitignored workflow inputs need this hook) - The `.archon/` default (no config needed for the common case) - Common entries: .env, .vscode/, .claude/, plans/, reports/, data fixtures - Semantics: source=destination, ENOENT silently skipped, per-entry error isolation, path-traversal rejected - Interaction with `worktree.path` (both layouts get the same treatment) 3. Update the overview example to drop the `.env.example + .env` pair (which implied rename semantics) in favor of `.env + plans/`, and call out that `.archon/` is auto-copied so users don't list it. No code changes. `bun run format:check` and `bun run lint` green.
When a workflow node defines hooks (PreToolUse/PostToolUse) in YAML but
no hooks exist yet on the options object, applyNodeConfig crashes with
"undefined is not an object" because it tries to assign properties on
the undefined options.hooks.
Initialize options.hooks to {} before the merge loop.
Reproduces with: archon workflow run archon-architect (which uses
per-node hooks extensively).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…) (#1184) * fix: detect completion signal in any XML tag, not just <promise> (#1126) Loop nodes with `until:` reported max_iterations_reached when the AI wrapped the completion signal in XML tags other than `<promise>` (e.g., `<COMPLETE>ALL_CLEAN</COMPLETE>`). The three existing regex patterns all missed this format, causing the loop to exhaust iterations and fail. Changes: - Add generic XML-wrapped signal pattern to `detectCompletionSignal` - Extend `stripCompletionTags` to strip matched XML-wrapped signals from output - Pass `loop.until` to `stripCompletionTags` call site in dag-executor - Add unit tests for detection and stripping of XML-wrapped signals - Add integration test for loop completing on final iteration with XML tags Fixes #1126 * fix: address review findings for completion signal detection - Update detectCompletionSignal JSDoc to document all three detection formats - Update stripCompletionTags JSDoc to mention the `until` parameter - Remove superfluous `m` flag from xmlWrappedPattern (no anchors, no effect) - Document that XML tag names are matched independently (intentional permissiveness) - Add test: detects signal in mismatched XML tags (permissive behavior) - Add test: strips both <promise> and XML-tagged signal in same chunk - Add assertion in DAG integration test that raw XML tags don't appear in sent messages * simplify: reduce complexity in changed files * fix: require matching XML tag names in completion-signal detection Follow-up to the initial broadening in this PR. The first version of the regex accepted mismatched open/close tags (e.g. `<COMPLETE>X</done>`) which was a small false-positive surface when the AI interleaves tags in prose. Tightens both detectCompletionSignal and stripCompletionTags to capture the tag name and enforce it on the close via \1 backreference. Case-insensitivity on the tag name is preserved. Test updates: - Flip the "permissive mismatch" case to assert strict rejection with a comment explaining the guard. - Add a case-insensitive matching case to lock that behavior in. No behavior change for workflows that use matching tags (the overwhelming common case) or for <promise>...</promise>. Behavior change is limited to the narrow "open tag and close tag disagree" case, which only happens when the AI is confused — in which case we'd rather report max_iterations_reached and let the author inspect than silently call the loop complete.
* fix(web): allow deleting nodes from Workflow Builder (#971) Three independent gaps prevented users from deleting nodes added to the Workflow Builder canvas: dropped nodes were never auto-selected so keyboard shortcuts silently no-oped, no right-click context menu existed, and the Delete Node button was buried in the Advanced tab (hidden below the viewport for Prompt/Command, completely absent for Bash since bash nodes have no Advanced tab). Fixes #971. * fix(web): push undo snapshot before adding nodes on canvas Call onPushSnapshot() before setNodes() in both onDrop and quick-add handlers so that node additions are captured by undo/redo history. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): address PR #1113 review feedback - Hold nodes/edges in refs so handleNodeDeleteById and onPushSnapshot can't capture stale pre-drop state (fixes undo-stack correctness). - Clamp context-menu x/y to viewport so right-click near edges stays fully on-screen. - Drop non-conformant role=menu/menuitem from the single-item context menu; rely on the native button for accessibility. - Extend isInputTarget() to cover ARIA combobox/textbox/searchbox so Backspace in Radix/shadcn widgets never nukes a node. - Extract handleBuilderKeydown as a pure function and add tests covering the Delete/Backspace + isInputTarget invariant. - Remove issue-number references from code comments per CLAUDE.md. - Document the new delete affordances in the Workflow Builder docs. - Inline context-menu dismissal, rename pointer handler, drop unused deps in keyboardActions useMemo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#1155) * fix(workflows): make adversarial init sed portable on macOS * chore: regenerate bundled-defaults after adversarial-dev sed fix Sync generated bundle with the new temp-file sed pattern in archon-adversarial-dev.yaml so check:bundled passes and binary distributions ship the macOS-safe version. --------- Co-authored-by: laplace young <yangqk12@whu.edu.cn> Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
…#1327) * fix(workflows): filter user-plugin MCP noise out of workflow warnings Before this change, the dag-executor surfaced every entry in the Claude SDK's "MCP server connection failed: …" system message to the user. That message includes user-level plugin MCPs inherited from ~/.claude/ (e.g. `telegram`) that fail to connect in the headless workflow subprocess — they're non-actionable noise for the workflow author. Fix: - Pre-compute the set of workflow-configured MCP server names per node by parsing the `mcp:` config file once at the start of executeNodeInternal. No caller-facing API change; no duplication of the provider's env-var expansion logic (we only need the keys). - Split the system-message handler: the `MCP server connection failed:` path now surfaces only the subset of failing names that match the node's configured set; user-plugin failures are debug-logged as `dag.mcp_plugin_connection_suppressed`. The `⚠️ ` branch is unchanged. Supersedes #1134 (closed as stale — the Windows HOME fix in that PR was already shipped via #1302, and the claude.ts enabledPlugins change targeted a file that has since moved into @archon/providers). Credits @MrFadiAi for identifying and reporting the underlying issue. Co-authored-by: Fadi Ai <MrFadiAi@users.noreply.github.com> * fix(workflows): preserve MCP failure status in filtered message + observability Address review feedback on PR #1327: - parseMcpFailureServerNames now returns {name, segment} entries so the forwarded "MCP server connection failed: ..." message preserves the per-server status detail (e.g. "(timeout)", "(disconnected)") that the bare-name reconstruction was dropping. - loadConfiguredMcpServerNames now debug-logs read failures as dag.mcp_filter_config_read_failed instead of swallowing them silently. A transient EMFILE/EBUSY at filter time would otherwise silently reclassify a real workflow-MCP failure as plugin noise. - Add 4 integration tests through executeDagWorkflow covering the mixed workflow/plugin split, all-plugin suppression, no-mcp:-config nodes, and the unchanged⚠️ warning path. - Drop a WHAT comment above configuredMcpNames and a temporal phrase ("before this filter landed") that would rot on merge. - Document the filtering boundary in guides/mcp-servers.md and add a troubleshooting row for users debugging silently-suppressed plugin MCPs. --------- Co-authored-by: Fadi Ai <MrFadiAi@users.noreply.github.com>
…1330) axios <1.15.0 can be coerced to bypass NO_PROXY rules via hostname normalization, enabling SSRF in the right network shape. Archon pulls axios transitively through @slack/bolt (^1.12.0) and @slack/web-api (^1.13.5); before this change bun.lock resolved axios@1.13.6 — within the vulnerable range. Adding "axios": "^1.15.0" to the root package.json overrides bumps the transitive resolution to axios@1.15.1 (latest compatible 1.x). Both Slack range specs accept it without API surface changes — no downstream code touches axios directly. Supersedes #1153. Credits @stefans71 for identifying and reporting the vulnerability; their PR was stale on the lockfile (0.3.5 → 0.3.6 drift on dev), so this is a fresh one-line re-do on current dev. Closes #1053. Co-authored-by: Stefans71 <stefans71@users.noreply.github.com>
…"not a git repo" (#1332) * fix(cli): surface stale-workspace registration error instead of fake "not a git repo" When workflowRunCommand auto-registers an unregistered repo, a stale ~/.archon/workspaces/<owner>/<repo>/source symlink (pointing to an old checkout) causes createProjectSourceSymlink() in @archon/paths to throw: Source symlink at <linkPath> already points to <existing>, expected <target> The CLI caught that in a try/catch, logged it at warn level, continued with `codebase = null`, and then the isolation / resume branches hit their "codebase missing" fallback and threw the generic: Cannot create worktree: not in a git repository. That message is false — the repo is valid; the Archon workspace entry is stale. It sends users down the wrong diagnostic path (checking git config, permissions, etc.) instead of pointing at the workspace dir. Fix: preserve the registration error on a new `codebaseRegistrationError` local, and at both fallback sites (resume + worktree-creation) check it before the generic "not a git repo" branch. When set, throw a truthful: Cannot {create worktree,resume}: repository registration failed. Error: <original message> Hint: Remove the stale workspace entry at <dir> and retry, or use --no-worktree to skip isolation. The hint's exact path comes from a small parser that extracts the workspace directory from the known "Source symlink at …" format; when the message shape doesn't match (future error text changes), the parser returns null and we fall back to a generic "check registration under <archon-home>/workspaces" hint — safe degradation. Regression test in workflow.test.ts asserts the new error message and negatively asserts the old "not in a git repository" string is gone. Supersedes #1157 — that PR was draft + CONFLICTING against current dev, and also mentioned Windows test-compat changes that weren't in the diff (pruned scope). This is a fresh re-do focused strictly on #1146. Closes #1146. Co-authored-by: Bortlesboat <Bortlesboat@users.noreply.github.com> * review: add resume-path test, null-fallback test, update troubleshooting docs Addresses multi-agent review feedback on this PR: - Add regression test for the --resume fallback site (the worktree-create site was already covered; the resume site had identical wiring but zero test coverage). - Add test for the unrecognized-error-shape branch of buildRegistrationFailureError so the generic workspace hint is pinned (prevents accidental inversion of the stale-entry vs generic-hint ternary). - Update the troubleshooting page to key on the new "Cannot create worktree: repository registration failed." message. Users hitting the new error won't find the page under the old heading, and the "In the future..." note is obsolete now that the error itself contains the cleanup path. - Trim both new docblocks: keep the load-bearing cross-package error string contract in extractStaleWorkspaceEntry, drop narration of what the code already shows. Drop the "Before this helper existed..." paragraph from buildRegistrationFailureError — that's CHANGELOG material. Drop PR-reference suffix from the test section divider. * review: guard getArchonHome in hint + export parser for direct tests Two follow-up fixes to the multi-agent review commit (f32f002): CodeRabbit finding — unguarded getArchonHome() in the fallback hint. If getArchonHome() ever throws (misconfigured env vars, permission issues on the resolution path), the registration-failure Error would never get constructed: we'd throw a secondary home-resolution error that masks the root cause. Wrap the fallback branch in try/catch — prefer losing the exact path in the hint over replacing the actionable registration error. A safe generic hint ("Check your Archon workspace registration and retry") takes over when getArchonHome() throws. The original error.message is always embedded verbatim in the re-thrown Error. S2 — export extractStaleWorkspaceEntry for direct table tests. The parser is where the cross-package string contract with @archon/paths actually lives; direct tests against it are cheaper than end-to-end CLI tests and pin the edge cases: - POSIX path with forward slashes (typical unix user) - Windows path with backslashes (verifies Math.max(lastIndexOf / , lastIndexOf \)) - Unrelated error message (no prefix) → null - Prefix matches but delimiter missing → null - Source path without any separator → null (guards against returning empty string, which would produce a nonsense "Remove the stale workspace entry at " hint) - Empty string → null Six new cases in the test file. The claim of Windows support in the PR description is now actually verified. * fix(test): make generic-hint assertion path-separator agnostic Windows test runner (CI) hit: Expected to contain: "Check your Archon workspace registration under /home/test/.archon/workspaces" Received: "... under \home\test\.archon\workspaces and retry, ..." path.join normalizes to `\` on Windows and `/` on POSIX. The test hardcoded forward slashes in the expected substring. Split into two separator-agnostic asserts: the prefix up to "under", then `/workspaces\b/` regex for the final path segment. Behavior doesn't change — the hint still gets the full path.join'd workspaces dir on either platform. --------- Co-authored-by: Bortlesboat <Bortlesboat@users.noreply.github.com>
…th-reason dialog (#1329) * fix(server,web,workflows): web approval gates auto-resume + reject-with-reason dialog Fixes three tightly-coupled bugs that made web approval gates unusable: 1. orchestrator-agent did not pass parentConversationId to executeWorkflow for any web-dispatched foreground / interactive / resumable run. Without that field, findResumableRunByParentConversation (the machinery the CLI relies on for resume) couldn't find the paused run from the same conversation on a follow-up message, and the approve/reject API handlers had no conversation to dispatch back to. 2. POST /api/workflows/runs/:runId/{approve,reject} recorded the decision and returned "Send a message to continue the workflow." — the workflow never actually resumed. Added tryAutoResumeAfterGate() that mirrors what workflowApproveCommand / workflowRejectCommand already do on the CLI: look up the parent conversation, dispatch `/workflow run <name> <userMessage>` back through dispatchToOrchestrator. Failures are non-fatal — the user can still send a manual message as a fallback. 3. The during-streaming cancel-check in dag-executor aborted any streaming node whenever the run status left 'running', including the legitimate transition to 'paused' that an approval node performs. A concurrent AI node in the same DAG layer now tolerates 'paused' and finishes its own stream; only truly terminal / unknown states (null, cancelled, failed, completed) abort the in-flight stream. Web UI: ConfirmRunActionDialog gains an optional reasonInput prop (label + placeholder) that renders a textarea and passes the trimmed value to onConfirm. WorkflowRunCard (dashboard) and WorkflowProgressCard (chat) both use it for Reject now — the chat card was still on window.confirm, which was both inconsistent with the dashboard and couldn't collect a reason. The trimmed reason threads through to $REJECTION_REASON in the workflow's on_reject prompt. Supersedes #1147. @jonasvanderhaegen surfaced the root cause and shape of the fix; that PR was 87 commits stale and pre-dated the reject-UX upgrade (#1261 area), so this is a fresh re-do on current dev. Tests: - packages/server/src/routes/api.workflow-runs.test.ts — 5 new cases: approve with parent dispatches; approve without parent returns "Send a message"; approve with deleted parent conversation skips safely; reject dispatches on-reject flows; reject that cancels (no on_reject) does NOT dispatch. - packages/core/src/orchestrator/orchestrator.test.ts — updated the two synthesizedPrompt-dispatch tests for the new executeWorkflow arity. Closes #1131. Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com> * fix: address multi-agent review findings for web approval auto-resume C1 (critical) — cross-adapter misrouting guard tryAutoResumeAfterGate now checks parentConv.platform_type === 'web' before dispatching. Non-web parents (Slack/Telegram/GitHub/Discord) being approved from the dashboard skip auto-resume rather than dispatching a Slack thread_ts or Telegram chat_id through the web adapter's lock manager. C2 (critical) — fire-and-forget dispatch replaced with await void dispatchToOrchestrator() meant the "Resuming workflow." response fired before async work completed, and the outer try/catch couldn't observe dispatch failures. Changed to await; response now accurately reflects dispatch outcome. I1 — replaced logPrefix string-template (which produced 3-segment api.workflow_*.dispatched event names violating {domain}.{action}_{state}) with literal event names per action, branched inside the helper. Accepts action: 'approve' | 'reject' instead. I2 — corrected misleading "foreground/interactive" qualifier in the approve-endpoint comment; background web dispatches also set parent_conversation_id via the pre-created run, so they auto-resume too. I3 — extracted shouldContinueStreamingForStatus() as a small exported policy and added 7 unit tests covering running/paused/null/cancelled/ failed/completed/unknown. Full-integration coverage of the paused- tolerance invariant would require manipulating the 10s CANCEL_CHECK_INTERVAL_MS, which is flaky-prone; unit test of the policy function captures the same invariant deterministically. I4 — updated approval-nodes.md and authoring-workflows.md to reflect that Web UI approve/reject now auto-resumes (no "send a follow-up message" copy), documented the reject-with-reason dialog and $REJECTION_REASON flow, and called out the cross-platform caveat. S1 — rewrote streaming status check as positive shouldContinue safe-list via the extracted policy function, matching the inline comment. S2 — inlined handleReject on the dashboard rather than squeezing rejectWorkflowRun through runAction with a closure; keeps runAction narrow for the single-arg lifecycle actions. S5 — new regression test covering the non-web-parent skip path (slack-platform parent → dispatch skipped → response falls back to "Send a message to continue"). S6 — removed stale reference to runAction in ConfirmRunActionDialog's onConfirm JSDoc (no longer accurate now that WorkflowProgressCard calls the dialog without runAction). S7 — fixed misleading "user can resume manually by sending any message" docstring (resume is triggered by re-running the workflow command, not by an arbitrary message). Skipped as out-of-scope: S3 — cancelWorkflowRun rowCount check (pre-existing defect; separate PR) S4 — tightening expect.anything() to UUID regex (deferred) S8 — 12-positional-arg executeWorkflow → options-bag refactor (tracked follow-up) bun run validate green locally; 68 tests in api.workflow-runs.test.ts (up from 67), 173 in dag-executor.test.ts (up from 166). * review: close I1/I2/I3/I4/I6 — paused tolerance in loop + emitter, resume test, useId I1 (loop inter-iteration check) — dag-executor.ts:1715 Used `!== 'running'` in the loop node's between-iteration status check. A sibling approval node pausing the run in the same topological layer would abort the loop mid-iteration with "Loop node '<id>' stopped at iteration N (paused)". Switched to the shared shouldContinueStreamingForStatus helper so paused is tolerated — same semantics the streaming check got. Extended inline comment explains the sibling-layer concurrency reason. I2 (skipIfStatusChanged emitter unregister) — dag-executor.ts:2886 At DAG-finalization writes the helper correctly skipped writing on any non-running state (paused included — don't mark a paused run complete), but it *also* called getWorkflowEventEmitter().unregisterRun() which broke SSE observability for a run that's still live (waiting for user approval). Split the two responsibilities: skip the write for all non-running states, but only unregister the emitter for terminal states (cancelled / deleted / completed / failed). `paused` keeps the emitter registered so resume stays visible on the dashboard. I3 (foreground_resume_detected branch untested) — orchestrator-agent.test.ts That branch was modified as part of the original fix (added parentConversationId as 11th positional arg) but no existing test configured mockFindResumableRunByParentConversation to return non-null. A positional mistake (e.g. accidentally swapping issueContext and parentConversationId) would silently break auto-resume with no failing test. New regression test configures the mock, asserts both the cwd comes from the resumable run's working_path AND parentConversationId is passed correctly at position 10. I4 (null-parent log level) — api.ts tryAutoResumeAfterGate `getConversationById` returning null is a data-integrity signal (the parent conversation was deleted while the run was paused) — worth surfacing at info level so operators notice, not hiding at debug. Missing platform_conversation_id on an existing row would be an unusual DB state and stays at debug. Added `parentDeleted: boolean` to the log context so the two cases are distinguishable in observability. I6 (hardcoded DOM id) — ConfirmRunActionDialog.tsx `id="confirm-run-action-reason"` collided when multiple dialog instances share the same page (Radix portals mitigate in practice but the code was fragile). Switched to React.useId() so each instance gets a unique id — htmlFor/id wiring preserved. S11 (arity-only assertion) — orchestrator-agent.test.ts:1092 area The interactive-workflow-on-web test asserted mockExecuteWorkflow was called, but nothing about the args. Added a specific assertion that position 10 (parentConversationId) equals 'conv-1' (the caller conversation id) — pins the wiring that I1/I2 depend on being correct. Deferred (from review S1-S10, I5, I7): - S1 (ExecuteWorkflowOptions bag) — tracked as standalone follow-up; 12 positional args with 2 adjacent optionals is a real maintenance hazard but the refactor deserves its own PR. - S7 (WHY comment on non-web else branch) — review text says the branch "correctly omits" parentConversationId but the code passes it; the combination with the web-parent guard in tryAutoResumeAfterGate is intentional. Not adding a justify-what-we-don't-do comment. - S2/S3/S4/S5/S8/S9/S10 — pure polish (event-map ternary, platformConvId inlining, shared constant for REJECTION_REASON_INPUT, onChange arrow shorthand, discriminated union, docblock trim, suffix comment drop) - I5 (soften "Resuming workflow." to "— check the dashboard for progress") — users clicking from the dashboard are already on the dashboard; the current text is accurate (enqueue completed) and concise. - I7 (test dispatch-throws path) — covered implicitly by the try/catch branch of tryAutoResumeAfterGate returning false; a direct test would require mocking handleMessage to throw and would couple to dispatchToOrchestrator internals. bun run validate green; 189 dag-executor tests, 98 orchestrator-agent tests, 68 api.workflow-runs tests — all the new cases pass. --------- Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
|
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 |
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.3.7
Pi community provider, home-scoped workflows/commands/scripts, worktree policy, Web UI approval-gate auto-resume, three-path env model, and a breaking change to Claude Code binary resolution for compiled binary users.
Added
@mariozechner/pi-coding-agent). First community provider under the Phase 2 registry (builtIn: false). One adapter exposes ~20 LLM backends (Anthropic, OpenAI, Google, Groq, Mistral, Cerebras, xAI, OpenRouter, Hugging Face, and more) via a<pi-provider-id>/<model-id>model format. (feat(providers): add Pi community provider (@mariozechner/pi-coding-agent) #1270)agents:). Define Claude Agent SDKAgentDefinitions directly in workflow YAML. The main agent can spawn them in parallel via theTasktool. Claude only. (feat(workflows): inline sub-agent definitions on DAG nodes #1276)~/.archon/commands/— personal command helpers reusable across every repo.~/.archon/scripts/— personal Bun/uv scripts reusable across every repo.'global'variant onWorkflowSource— home-scoped workflows/commands now render with a distinct source label.getHomeWorkflowsPath(),getHomeCommandsPath(),getHomeScriptsPath(),getLegacyHomeWorkflowsPath()helpers in@archon/paths.discoverScriptsForCwd(cwd)merging home-scoped + repo-scoped scripts with repo winning on name collisions.worktree.enabledin workflow YAML). Pin whether runs use isolation regardless of invocation.worktree.pathin.archon/config.yaml). Co-locate worktrees with the project. (feat: per-project worktree.path config option #1117)~/.archon/.envand<cwd>/.archon/.envat boot, with new[archon] loaded N keys from <path>log lines. (CLI silently strips repo-local .env vars; logs say(0) from .envinstead ofstripped N keys#1302)archon setup --scope home|projectand--forceflags. Scope targets~/.archon/.envor<cwd>/.archon/.env. (bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303)<target>.archon-backup-<ISO-ts>before rewrite. (bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303)getArchonEnvPath()andgetRepoArchonEnvPath(cwd)plus a new@archon/paths/env-loadersubpath.registerCommunityProviders()aggregator in@archon/providers— adding a new community provider is a single-line edit.contributing/adding-a-community-provider.mdguide — walkthrough of the Phase 2 registry pattern using Pi.CLAUDE_BIN_PATHenv var +assistants.claude.claudeBinaryPathconfig option — highest-precedence overrides for the Claude Code binary path. (feat: add claudeBinaryPath config option for custom Claude Code install paths #1176)Changed
CLAUDE_BIN_PATHorassistants.claude.claudeBinaryPath. Dev mode and Docker unaffected. (bug: compiled binary cannot invoke Claude Code — SDK cli.js path hardcoded to CI runner filesystem #1210, Claude Code process exited with code 1 via webui #1087)~/.archon/workflows/(was~/.archon/.archon/workflows/). Emits a one-time WARN with migration command if the old path is detected; old path is no longer read.globalSearchPathoption.discoverWorkflows()anddiscoverWorkflowsWithConfig()consult~/.archon/workflows/automatically — every call site gets home-scoped discovery for free. (fix: pass globalSearchPath to command handler workflow discovery #1136)Fixed
$REJECTION_REASON. (Interactive workflows cannot resume after approval gate (parent_conversation_id is NULL) #1131, fix: web UI approval/reject auto-resume for workflow gates #1147)failOrphanedRuns()per the "No Autonomous Lifecycle Mutation Across Process Boundaries" principle. (bug: server startup marks actively-running workflows as failed via failOrphanedRuns() #1216)MCP server connection failed: <plugin>noise no longer surfaces in workflow runs. Dag-executor filters to only the servers the workflow configured; user-level Claude plugin MCP failures are debug-logged instead. (fix: Windows HOME resolution and MCP plugin warning suppression #1134)archon setupno longer writes to<repo>/.env. Writes target exactly one archon-owned file (home or project scope), merge-by-default, with timestamped backups. (bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303)<cwd>/.archon/.envpath + archon-owned log lines make load state observable and recoverable. (CLI silently strips repo-local .env vars; logs say(0) from .envinstead ofstripped N keys#1302)axiosto^1.15.0via rootoverridesto clear CVE-2025-62718 (NO_PROXY bypass → potential SSRF). Credits @stefans71. (fix: override axios to ^1.15.0 for CVE-2025-62718 #1153, Vulnerability in Archon project #1053)cli.codebase_auto_registration_failedwith misleading secondary error #1146).gitpointer matches. Credits @halindrome. (bug(implement): archon-implement reuses pre-existing branch from unrelated task, implementing wrong fix #1193, Git-level worktree adoption bypasses cross-checkout guard and allows parallel collision #1188, Worktree reuse matches environments from different local clones of the same remote #1183, fix: prevent worktree isolation bypass via prompt and git-level adoption #1198, fix: extend worktree ownership guard to resolver adoption paths #1206)Removed
globalSearchPathoption fromdiscoverWorkflows()/discoverWorkflowsWithConfig(). Home-scoped discovery is now automatic.@anthropic-ai/claude-agent-sdk/embedimport — the Bun asset-embedding path that failed silently when Bun couldn't produce a usable virtual FS path. Replaced by explicit binary-path resolution. (bug: compiled binary cannot invoke Claude Code — SDK cli.js path hardcoded to CI runner filesystem #1210, Claude Code process exited with code 1 via webui #1087)Merging this PR releases 0.3.7 to main.