Skip to content

Release 0.3.7#1352

Merged
Wirasm merged 103 commits intomainfrom
dev
Apr 22, 2026
Merged

Release 0.3.7#1352
Wirasm merged 103 commits intomainfrom
dev

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Apr 22, 2026

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

  • Pi community provider (@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)
  • Inline sub-agent definitions on DAG nodes (agents:). Define Claude Agent SDK AgentDefinitions directly in workflow YAML. The main agent can spawn them in parallel via the Task tool. Claude only. (feat(workflows): inline sub-agent definitions on DAG nodes #1276)
  • Home-scoped commands at ~/.archon/commands/ — personal command helpers reusable across every repo.
  • Home-scoped scripts at ~/.archon/scripts/ — personal Bun/uv scripts reusable across every repo.
  • 1-level subfolder support for workflows, commands, and scripts. Files resolve one folder deep by name or filename.
  • 'global' variant on WorkflowSource — 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.
  • Workflow-level worktree policy (worktree.enabled in workflow YAML). Pin whether runs use isolation regardless of invocation.
  • Per-project worktree path (worktree.path in .archon/config.yaml). Co-locate worktrees with the project. (feat: per-project worktree.path config option #1117)
  • Three-path env model with operator-visible log lines. CLI and server now load from ~/.archon/.env and <cwd>/.archon/.env at boot, with new [archon] loaded N keys from <path> log lines. (CLI silently strips repo-local .env vars; logs say (0) from .env instead of stripped N keys #1302)
  • archon setup --scope home|project and --force flags. Scope targets ~/.archon/.env or <cwd>/.archon/.env. (bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303)
  • Merge-only setup writes with timestamped backups. Preserves existing values + user-added custom keys; writes <target>.archon-backup-<ISO-ts> before rewrite. (bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303)
  • getArchonEnvPath() and getRepoArchonEnvPath(cwd) plus a new @archon/paths/env-loader subpath.
  • registerCommunityProviders() aggregator in @archon/providers — adding a new community provider is a single-line edit.
  • contributing/adding-a-community-provider.md guide — walkthrough of the Phase 2 registry pattern using Pi.
  • CLAUDE_BIN_PATH env var + assistants.claude.claudeBinaryPath config option — highest-precedence overrides for the Claude Code binary path. (feat: add claudeBinaryPath config option for custom Claude Code install paths #1176)
  • Release-workflow Claude subprocess smoke test — release CI now installs Claude Code and exercises the resolver + subprocess spawn.

Changed

  • Claude Code binary resolution (breaking for compiled binary users): Archon no longer embeds the Claude Code SDK into compiled binaries. In compiled builds, install Claude Code separately and point Archon at it via CLAUDE_BIN_PATH or assistants.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)
  • Home-scoped workflow location moved to ~/.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.
  • Workflow discovery no longer takes a globalSearchPath option. discoverWorkflows() and discoverWorkflowsWithConfig() consult ~/.archon/workflows/ automatically — every call site gets home-scoped discovery for free. (fix: pass globalSearchPath to command handler workflow discovery #1136)
  • Dashboard nav tab shows numeric running-workflows count instead of a binary pulse dot.
  • Workflow run destructive actions (Abandon, Cancel, Delete, Reject) now use a confirmation dialog matching the codebase-delete UX.

Fixed

Removed


Merging this PR releases 0.3.7 to main.

coleam00 and others added 30 commits April 10, 2026 09:46
)

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).
Wirasm and others added 28 commits April 19, 2026 09:16
* 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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

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

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a8593db-b6d4-44af-809d-59c40048079f

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

Use the checkbox below for a quick retry:

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

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

❤️ Share

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

@Wirasm Wirasm merged commit 48c81d3 into main Apr 22, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.