Skip to content

fix(core): respect worktree.baseBranch in cleanup service default branch resolution#1423

Open
kagura-agent wants to merge 16 commits intocoleam00:devfrom
kagura-agent:fix/cleanup-respect-baseBranch-config
Open

fix(core): respect worktree.baseBranch in cleanup service default branch resolution#1423
kagura-agent wants to merge 16 commits intocoleam00:devfrom
kagura-agent:fix/cleanup-respect-baseBranch-config

Conversation

@kagura-agent
Copy link
Copy Markdown
Contributor

@kagura-agent kagura-agent commented Apr 26, 2026

Problem

The cleanup service called getDefaultBranch() directly in three places (merged-branch detection, status breakdown, scheduled cleanup) without consulting the configured worktree.baseBranch from .archon/config.yaml. This caused repeated env_cleanup_error log entries on every startup for repositories where the default branch is not main and origin/HEAD is not set (e.g. repos using master).

The error message itself advises users to set worktree.baseBranch — but the cleanup service never reads it, making the suggestion ineffective.

Fix

Add a resolveMainBranch() helper in cleanup-service.ts that:

  1. Reads worktree.baseBranch from .archon/config.yaml (via Bun.YAML.parse)
  2. Falls back to getDefaultBranch() auto-detection when no config exists or the field is unset

Replace all three getDefaultBranch() call sites in cleanup-service with resolveMainBranch().

Note: Other callers like executor.ts and workflow.ts already respect this config — this aligns the cleanup service with the existing pattern.

Changes

  • packages/core/src/services/cleanup-service.ts: Add resolveMainBranch(), replace 3 call sites
  • packages/core/src/services/cleanup-service.test.ts: Add 2 tests (config override + fallback)

Validation

  • All 44 tests pass (bun test packages/core/src/services/cleanup-service.test.ts)
  • Lint-staged passes (eslint + prettier)
  • No new type-check errors introduced

Closes #1419

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: cleanupMergedWorktrees, getWorktreeStatusBreakdown, runScheduledCleanup — all three getDefaultBranch() call sites in cleanup-service now go through resolveMainBranch()
  • Potential unintended effects: If .archon/config.yaml has an invalid worktree.baseBranch value (e.g. a branch that doesn't exist), the cleanup service will use it and isBranchMerged will fail to find merged branches — but this is the same behavior as other callers (executor.ts, workflow.ts) that already read this config
  • Guardrails/monitoring for early detection: The catch block in resolveMainBranch() treats config load failure as non-fatal and falls back to getDefaultBranch(). Existing env_cleanup_error log entries will disappear for repos with baseBranch configured (the desired outcome)

Rollback Plan (required)

  • Fast rollback command/path: Revert this commit — the three call sites revert from resolveMainBranch() back to getDefaultBranch(), restoring the previous behavior
  • Feature flags or config toggles: The existing worktree.baseBranch config key acts as an implicit toggle — if the field is unset or the config file is absent, behavior is identical to before this PR
  • Observable failure symptoms: If rollback is needed, repos using master (or any non-main default branch) without origin/HEAD set will again produce env_cleanup_error log entries on every startup

Summary by CodeRabbit

  • New Features

    • Cleanup and merge-detection now respect a repository-configured base branch for determining the main branch, while automatically falling back to the previous default-branch behavior when no valid configuration exists.
  • Tests

    • Added coverage validating base-branch resolution and fallback behavior (including invalid or missing configs) to ensure merge checks and merged-worktree cleanup use the configured or default branch as appropriate.

github-actions Bot and others added 14 commits April 22, 2026 11:26
…be (coleam00#1359)

The pre-flight binary smoke does a bare `bun build --compile` — it
deliberately skips `scripts/build-binaries.sh` to stay fast. That means
packages/paths/src/bundled-build.ts retains its dev defaults, including
BUNDLED_IS_BINARY = false.

version.ts branches on BUNDLED_IS_BINARY: when true it returns the
embedded string; when false it calls getDevVersion(), which reads
package.json at `SCRIPT_DIR/../../../../package.json`. Inside a compiled
binary SCRIPT_DIR resolves under `$bunfs/root/`, the walk produces a CWD-
relative path that doesn't exist, and the smoke aborts with "Failed to
read version: package.json not found" — a false positive.

Hit during the 0.3.8 release attempt: the real Pi lazy-load fix was
working end-to-end; the smoke test was the only thing failing.

Use --help instead. It exercises the same module-init graph (so it still
catches the real failure modes the skill lists — Pi package.json init
crash, Bun --bytecode bugs, CJS wrapper issues, circular imports under
minify) but has no dev/binary branch, so no false positive.

Also add a longer comment block explaining why --help is preferred, so
this doesn't get "normalized" back to `version` by a future drive-by.
The brew path of /test-release runs `brew uninstall` in Phase 5 to leave the
system in its pre-test state. For operators using the dual-homebrew pattern
(renamed brew binary at `/opt/homebrew/bin/archon-stable` so it coexists with
a `bun link` dev `archon`), that uninstall wipes the Cellar dir the
`archon-stable` symlink points into → `archon-stable` becomes dangling →
`brew cleanup` sweeps it away on the next brew op. Next time the operator
wants stable, they have to manually re-run `brew-upgrade-archon`.

Fix: make the skill aware of `archon-stable` and restore it transparently.

- Phase 2 item 4: detect the `archon-stable` symlink before any brew op;
  export `ARCHON_STABLE_WAS_INSTALLED=yes` so Phase 5 knows to restore it.
  Only triggers for the brew path (curl-mac/curl-vps don't touch brew so
  they leave `archon-stable` alone).
- Phase 5 brew path: after `brew uninstall + untap`, if the flag was set,
  re-tap + re-install + rename. Verifies the restored `archon-stable`
  reports a version and warns (non-fatal) if the rename target is missing.
  Documents the tradeoff: the restored version is "whatever the tap ships
  today", not necessarily the pre-test version — usually that's what the
  operator wants (the release they just tested becomes stable) but the
  back-version-QA case requires a manual `brew-upgrade-archon` after.
- Phase 1 confirmation banner now mentions that `archon-stable` will be
  preserved so the operator isn't surprised by the reinstall during Phase 5.

No changes to curl-mac/curl-vps paths. No changes to Phase 4 test suite.
… a compiled binary (coleam00#1360)

v0.3.9 made Pi boot-safe: lazy-loading its imports meant `archon version`
no longer crashed on `@mariozechner/pi-coding-agent/dist/config.js`'s
module-init `readFileSync(getPackageJsonPath())`. That's what the
`provider-lazy-load.test.ts` regression test guards.

The fix was only half the problem though. When a Pi workflow actually
runs, sendQuery() triggers the dynamic import — and Pi's config.js
module-init fires then, hitting the exact same ENOENT on
`dirname(process.execPath)/package.json`. Discovered by running
`archon workflow run test-pi` against a locally-compiled 0.3.9 binary:

    [main] Failed: ENOENT: no such file or directory,
           open '/private/tmp/package.json'
        at readFileSync (unknown)
        at <anonymous> (/$bunfs/root/archon-providertest:184:7889)
        at init_config

Boot-safe ≠ runtime-safe. The `/test-release` run for 0.3.9 passed
because it only exercised `archon-assist` (Claude); Pi was never
actually invoked on the released binary.

Fix: before the dynamic `import('@mariozechner/pi-coding-agent')` in
sendQuery, install a PI_PACKAGE_DIR shim. Pi's config.js checks
`process.env.PI_PACKAGE_DIR` first in its `getPackageDir()` and
short-circuits the `dirname(process.execPath)` walk. We write a
minimal `{name, version, piConfig:{}}` stub to
`tmpdir()/archon-pi-shim/package.json` (idempotent — existsSync check)
and set the env var. Pi only reads `piConfig.name`, `piConfig.configDir`,
and `version` from that file, all optional, so the stub surface is
genuinely minimal.

Localized to PiProvider: no global state, no mutation of any shared
config, no upstream fork. Claude and Codex providers are unaffected
(their SDKs don't have this class of module-init side effect).

Verified end-to-end: built a compiled archon binary with this patch,
ran `archon workflow run test-pi --no-worktree` (Pi workflow with
model `anthropic/claude-haiku-4-5`), got a clean response. Before the
patch, same binary crashed at `dag_node_started` with the ENOENT above.

Regression test added: asserts `PI_PACKAGE_DIR` is set after sendQuery
hits even its fast-fail "no model" path. Together with the existing
`provider-lazy-load.test.ts` (boot-safe) this covers both halves.
… and Codex (coleam00#1361)

Both binary resolvers previously stopped at env-var + explicit config and
threw a "not found" error when neither was set. Users who followed the
upstream-recommended install flow (Anthropic's `curl install.sh` for
Claude, `npm install -g @openai/codex`) still had to manually set either
`CLAUDE_BIN_PATH` / `CODEX_BIN_PATH` or the corresponding config field
before any workflow could run.

Add a tier-N autodetect step between the explicit config tier and the
install-instructions throw. Purely additive: env and config still win
when set (precedence covered by new tests). On autodetect miss, the same
install-instructions error fires as before.

Claude probe list (verified against docs.claude.com "Uninstall Claude
Code → Native installation" section):
  - $HOME/.local/bin/claude            (mac/linux native installer)
  - $USERPROFILE\.local\bin\claude.exe (Windows native installer)

Codex probe list (verified against openai/codex README; npm global-
install puts the binary at `{npm_prefix}/bin/<name>` on POSIX,
`{npm_prefix}\<name>.cmd` on Windows):
  - $HOME/.npm-global/bin/codex   (user-set `npm config set prefix`)
  - /opt/homebrew/bin/codex       (mac arm64 with homebrew-node)
  - /usr/local/bin/codex          (mac intel / linux system node)
  - %APPDATA%\npm\codex.cmd       (Windows npm global default)
  - $HOME\.npm-global\codex.cmd   (Windows user-set prefix)

Not probed (explicit override still required):
  - Custom npm prefixes — `npm root -g` would need a subprocess per
    resolve, too much surface for a probe helper
  - `brew install --cask codex` — cask layout isn't a PATH binary
  - Manual GitHub Releases extracts — placement is user-determined
  - `~/.bun/bin/codex` — not documented in openai/codex README

Pi provider intentionally has no equivalent change: the Pi SDK is
bundled into the archon binary (no subprocess), so there's no "binary"
to resolve. Pi auth lives at `~/.pi/agent/auth.json` which the SDK
already finds by default, and the PR A shim (`PI_PACKAGE_DIR`) handles
the package-dir case via Pi's own documented escape hatch.

E2E verified: removed both config entries from ~/.archon/config.yaml,
rebuilt compiled binary, ran `archon workflow run archon-assist` and a
Codex workflow. Logs showed `source: 'autodetect'` for both, responses
returned cleanly.
…ry autodetect test

The native-installer autodetect test computed its expected path from
process.env.HOME, but the implementation uses node:os homedir(). On
Windows, HOME is typically unset (Windows uses USERPROFILE), so the
test fell back to '/Users/test' while the resolver returned the real
home dir — making the spy's path-equality check fail and breaking CI
on windows-latest.

Mirror the implementation by importing homedir() from node:os and
joining with node:path so the expected path matches the actual
platform-resolved home and separator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ver (coleam00#1365)

Reported in coleam00#1365: a user running `archon serve` with DISCORD_BOT_TOKEN
set but the "Message Content Intent" toggle disabled in the Discord
Developer Portal saw the entire server crash with `Used disallowed
intents`. Discord rejects the gateway connection (close code 4014) when
a privileged intent is requested without being enabled, and the
unguarded `await discord.start()` propagated the error all the way up,
taking the web UI down with it.

Wrap discord.start() in try/catch — log the failure with an actionable
hint (special-cased for the disallowed-intent error) and continue
running. Other adapters and the web UI come up regardless. The shutdown
handler already uses optional chaining (`discord?.stop()`) so nulling
discord after a failed start is safe.

Other adapters (Telegram, Slack, GitHub, Gitea, GitLab) have the same
unguarded-start pattern but are out of scope for this fix — addressing
them is tracked separately.

Also expanded the Discord setup docs with a caution callout that names
the exact error string and the new log event so users can grep for
both.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…0#1362)

* docs(script-nodes): add dedicated guide and teach the archon skill how to write them

Script nodes (script:) have been a first-class DAG node type since v0.3.3 but
were documented only as one-liners in CLAUDE.md and a CI smoke test. Claude
Code reading the archon skill would see "Four Node Types: command, prompt,
bash, loop" and reach for bash+node/python one-liners instead of a proper
script node — losing bun's --no-env-file isolation, uv's --with dependency
pins, and the .archon/scripts/ reuse story.

- New packages/docs-web/src/content/docs/guides/script-nodes.md mirroring the
  structure of loop-nodes.md / approval-nodes.md: schema, inline vs named
  dispatch, runtime/deps semantics, scripts directory precedence (repo > home),
  extension-runtime mapping, env isolation, stdout/stderr contract, patterns,
  and the explicit list of ignored AI fields.
- guides/authoring-workflows.md and guides/index.md updated so the new guide is
  discoverable from both the node-types table and the guides landing page.
- reference/variables.md calls out the no-shell-quote difference between
  bash: and script: substitution — a subtle correctness trap when adapting a
  bash pattern into a script node.
- Sidebar order bumped +1 on hooks/mcp-servers/skills/global-workflows/
  remotion-workflow to slot script-nodes at order 5 next to the other
  node-type guides.

- .claude/skills/archon/SKILL.md: replaces stale "Four Node Types" (which
  also silently omitted approval and cancel) with the accurate seven, with a
  script-node code block showing both inline and named patterns.
- references/workflow-dag.md: full Script Node section covering dispatch,
  resolution, deps, stdout contract, and the list of AI-only fields that are
  ignored; validation-rules list updated.
- references/dag-advanced.md and references/variables.md: retry-support line
  corrected; no-shell-quote note added.
- examples/dag-workflow.yaml: added an extract-labels TypeScript script node
  and updated the header comment.

* fix(docs): review follow-ups for script-node guide

- skills example: extract-labels was reading process.env.ISSUE_JSON which is
  never set; use String.raw`$fetch-issue.output` so the upstream bash node's
  JSON is actually consumed
- guides/script-nodes.md + skills/workflow-dag.md: idle_timeout is accepted
  but ignored on script (and bash) nodes — executeScriptNode only reads
  node.timeout. Clarify that script/bash use `timeout`, not idle_timeout
- archon-workflow-builder.yaml: prompt enumerated only bash/prompt/command/loop,
  so the AI builder could never propose script or approval nodes. Add both
  (plus examples + rule about script output not being shell-quoted) and
  regenerate bundled defaults
- book/dag-workflows.md + book/quick-reference.md + adapters/web.md: fill in
  the node-type references that were missing script, approval, and cancel.
  adapters/web.md also overclaimed "loop" in the palette — NodePalette.tsx
  only drags command/prompt/bash, so note that the other kinds are YAML-only
…nv gaps, add good-practices + troubleshooting (coleam00#1363)

* fix(skill/when): document the full `when:` operator set and compound expressions

The skill reference previously stated "operators: ==, != only" which is
materially wrong — the condition evaluator supports ==, !=, <, >, <=, >=
plus && / || compound expressions with && binding tighter than ||, plus
dot-notation JSON field access. An agent authoring a workflow from the
skill would think half the operators don't exist.

Replaces the single-sentence section with a structured reference covering:
- All six comparison operators (string and numeric modes)
- Compound expressions with precedence rules and short-circuit eval
- JSON dot notation semantics and failure modes
- The fail-closed rules in full (invalid expression, non-numeric side,
  missing field, skipped upstream)

Grounded in packages/workflows/src/condition-evaluator.ts.

* feat(skill): document Approval and Cancel node types

Approval and cancel nodes are first-class DAG node types (approval since the
workflow lifecycle work in coleam00#871, cancel as a guarded-exit primitive) but the
skill never described either one. An agent reading the skill and asked to
"add a review gate before implementation" or "stop the workflow if the input
is unsafe" would fall back to bash + exit 1, losing the proper semantics
(cancelled vs. failed, on_reject AI rework, web UI auto-resume).

Approval node coverage (references/workflow-dag.md, SKILL.md):
- Full configuration block with message, capture_response, on_reject
- The interactive: true workflow-level requirement for web UI delivery
- Approve/reject commands across all platforms (CLI, slash, natural
  language) and the capture_response → $node-id.output flow
- Ignored-fields list + the on_reject.prompt AI sub-node exception

Cancel node coverage (references/workflow-dag.md, SKILL.md):
- Single-field schema (cancel: "<reason>")
- Lifecycle: cancelled (not failed); in-flight parallel nodes stopped;
  no DAG auto-resume path
- The "cancel: vs bash-exit-1" decision rule (expected precondition miss
  vs. check itself failing)
- Two canonical patterns — upstream-classification gate, pre-expensive-step
  gate

Validation-rules list updated to enumerate approval/cancel constraints
(message non-empty, on_reject.max_attempts range 1-10, cancel reason
non-empty), plus a forward note that script: joins the mutually-exclusive
set once PR coleam00#1362 lands.

Placement in both files is after the Loop section and before the validation
section, so this commit stays additive with respect to PR coleam00#1362's Script
node insertion between Bash and Loop — rebase is clean.

* feat(skill): document workflow-level fields beyond name/provider/model

The skill's Schema section previously showed only name, description, provider,
and model at the workflow level — which is most of a stub. Agents asked to
"use the 1M-context Claude beta" or "run this under a network sandbox" or
"add a fallback model in case Opus rate-limits" had no way to discover
that any of these fields existed at the workflow level.

Adds a comprehensive Workflow-Level Fields section covering:
- Core: name, description, provider, model, interactive (with explicit
  callout that interactive: true is REQUIRED for approval/loop gates on
  web UI — a common footgun)
- Isolation: worktree.enabled for pin-on/pin-off (the only worktree field
  at workflow level; baseBranch/copyFiles/path/initSubmodules are
  config.yaml only, so a cross-reference points there)
- Claude SDK advanced: effort, thinking, fallbackModel, betas, sandbox,
  with explicit per-node-only exceptions (maxBudgetUsd, systemPrompt)
- Codex-specific: modelReasoningEffort (with note that it's NOT the same
  as Claude's effort — this has confused users), webSearchMode,
  additionalDirectories
- A complete worked example combining sandbox + approval + interactive

All fields cross-referenced against packages/workflows/src/schemas/workflow.ts
and packages/workflows/src/schemas/dag-node.ts.

* feat(skill/loop): document interactive loops and gate_message

Interactive loop nodes pause between iterations for human feedback via
/workflow approve — used by archon-piv-loop and archon-interactive-prd.
The skill's Loop Nodes section previously omitted both interactive: true
and gate_message entirely, so an agent writing a guided-refinement
workflow wouldn't know the feature exists or that gate_message is
required at parse time.

Adds:
- interactive and gate_message rows to the config table (marking
  gate_message as required when interactive: true — enforced by the
  loader's superRefine)
- A dedicated "Interactive Loops" subsection explaining the 6-step
  iterate-pause-approve-resume flow
- Explicit call-out that $LOOP_USER_INPUT populates ONLY on the first
  iteration of a resumed session — easy to miss and a common surprise
- Workflow-level interactive: true requirement for web UI delivery
  (loader warning otherwise) so the full-flow example is complete
- Note that until_bash substitution DOES shell-quote $nodeId.output
  (unlike script bodies) — called out since the audit surfaced this
  inconsistency

* fix(skill/cli): complete the CLI command reference with missing lifecycle commands

The CLI reference previously documented only list, run, cleanup, validate,
complete, version, setup, and chat — missing nearly every workflow
lifecycle command an agent needs to operate a paused, failed, or stuck
run. The interactive-workflows reference assumed these commands existed
without actually documenting them.

Adds full documentation for:
- archon workflow status — show running workflow(s)
- archon workflow approve <run-id> [comment] — resume approval gate
  (also populates $LOOP_USER_INPUT on interactive loops and the gate
  node's output when capture_response: true)
- archon workflow reject <run-id> [reason] — reject gate; cancels or
  triggers on_reject rework depending on node config
- archon workflow cancel <run-id> — terminate running/paused with
  in-flight subprocess kill
- archon workflow abandon <run-id> — mark stuck row cancelled without
  subprocess kill (for orphan-cleanup after server crashes — matches
  the coleam00#1216 precedent)
- archon workflow resume <run-id> [message] — force-resume specific
  run (auto-resume is default; this is for explicit override)
- archon workflow cleanup [days] — disk hygiene for old terminal runs
  (with explicit callout that it does NOT transition 'running' rows,
  a common confusion)
- archon workflow event emit — used inside loop prompts for state
  signalling; documented so agents don't invent their own mechanism
- archon continue <branch> [flags] [msg] — iterative-session entry
  point with --workflow and --no-context flags

Also:
- Adds --allow-env-keys flag to the `workflow run` flag table with
  audit-log context and the env-leak-gate remediation use case
- Adds an "Auto-resume without --resume" note disambiguating when
  --resume is needed vs. when auto-resume handles it
- Adds --include-closed flag to `isolation cleanup`, which was
  previously missing; converts the flag list to a structured table
- Explains the cancel/abandon distinction (live subprocess vs. orphan)

All grounded in packages/cli/src/commands/workflow.ts, continue.ts,
and isolation.ts.

* feat(skill/repo-init): add scripts/ and state/, three-path env model, per-project env injection

The repo-init reference was missing two first-class .archon/ directories
(scripts/ since v0.3.3, state/ since the workflow-state feature) and had
nothing to say about env — the coleam00#1 thing a user hits on first-run when
their repo has a .env file with API keys.

Directory tree updates:
- Adds .archon/scripts/ with the extension->runtime rule (.ts/.js -> bun,
  .py -> uv) so agents know where to put named scripts referenced by
  script: nodes.
- Adds .archon/state/ with explicit "always gitignore" callout — these
  are runtime artifacts, not source. Previously undocumented in the skill.
- Adds .archon/.env (repo-scoped Archon env) and distinguishes it from
  the target repo's top-level .env.
- Adds a "What each directory is for" list so the structure isn't just
  a tree with no narrative.

.gitignore guidance:
- state/ and .env added as must-gitignore (state/ matches CLAUDE.md and
  reference/archon-directories.md — skill was lagging).
- mcp/ demoted to conditional — gitignore only if you hardcode secrets.

New "Three-Path Env Model" section:
- ~/.archon/.env (trusted, user), <cwd>/.archon/.env (trusted, repo),
  <cwd>/.env (UNTRUSTED, target project — stripped from subprocess env).
- Precedence (override: true across archon-owned paths) and the
  observable [archon] loaded N keys / stripped K keys log lines so
  operators can verify what actually happened.
- Decision tree for where to put API keys vs. target-project env vs.
  things Archon shouldn't touch.
- Links to archon setup --scope home|project with --force for writing
  to the right file with timestamped backups.

New "Per-Project Env Injection" section:
- Documents both managed surfaces: .archon/config.yaml env: block
  (git-committed, $REF expansion) and Web UI Settings → Projects →
  Env Vars (DB-stored, never returned over API).
- Names every execution surface that receives the injected vars:
  Claude/Codex/Pi subprocess, bash: nodes, script: nodes, and direct
  codebase-scoped chat.
- Documents the env-leak gate with all 5 remediation paths so an agent
  hitting "Cannot register: env has sensitive keys" knows the options.

Grounded in CHANGELOG v0.3.7 (three-path env + setup flags), v0.3.0
(env-leak gate), and reference/security.md on the docs site.

* fix(skill/authoring-commands): correct override paths and add home-scoped commands

The file-location and discovery sections described an override layout that
does not match the actual resolver. It showed:

  .archon/commands/defaults/archon-assist.md  # Overrides the bundled

and claimed `.archon/commands/defaults/` was where repo-level overrides
lived. In fact the resolver (executor-shared.ts:152-200 + command-
validation.ts) walks `.archon/commands/` 1 level deep and uses basename
matching — putting `archon-assist.md` at the top of `.archon/commands/`
is the canonical way to override the bundled version. The `defaults/`
subfolder is a Archon-internal convention for shipping bundled defaults,
not a user-facing override pattern.

Also, home-scoped commands (`~/.archon/commands/`, shipped in v0.3.7)
were completely absent — agents authoring personal helpers wouldn't
know they could live at the user level and be shared across every repo.

Changes:
- File Location section now shows all three discovery scopes (repo,
  home, bundled) with precedence ordering and 1-level subfolder rules
- Duplicate-basename rule documented as a user error surface
- Discovery and Priority section rewritten with accurate 3-step lookup
  order — no more references to the nonexistent defaults/ override path
- Adds the Web UI "Global (~/.archon/commands/)" palette label note so
  users authoring helpers for the builder know what to expect

No code changes — this is a pure fix of stale/incorrect skill reference
material.

* feat(skill): add workflow good-practices and troubleshooting reference pages

Closes two gaps from the audit. The skill previously had zero guidance on
designing multi-node workflows (what to avoid, what to reach for first,
how to structure artifact chains) and zero guidance on where to look
when things go wrong (log paths, env-leak gate remediations, orphan-row
cleanup, resume semantics).

New references/good-practices.md (9 Good Practices + 7 Anti-Patterns):

- Use deterministic nodes (bash:/script:) for deterministic work, AI for
  reasoning — the single biggest quality lever
- output_format required whenever downstream when: reads a field — the
  most common source of "workflow silently routes wrong"
- trigger_rule: none_failed_min_one_success after conditional branches —
  the classic bug where all_success fails because a skipped when:-gated
  branch doesn't count as a success
- context: fresh requires artifacts for state passing — commands must
  explicitly "read $ARTIFACTS_DIR/..." when downstream of fresh
- Cheap models (haiku) for glue, strong for substance
- Workflow descriptions as routing affordances
- Validate (archon validate workflows) + smoke-run before shipping
- Artifact-chain-first design
- worktree.enabled: true for code-changing workflows (reversibility)
- Anti-patterns with before/after YAML examples for each (AI-for-tests,
  free-form when: matching, context: fresh without artifacts, long flat
  AI-node layers, secrets in YAML, retry on loop nodes, tiny
  max_iterations, missing workflow-level interactive:, tool-restricted
  MCP nodes)

New references/troubleshooting.md:

- Log location (~/.archon/workspaces/<owner>/<repo>/logs/<run-id>.jsonl)
  with jq recipes for common queries (last assistant message, failed
  events, full stream)
- Artifact location for cross-node handoff debugging
- 9 Common Failure Modes, each with root cause + concrete fix:
  - $BASE_BRANCH unresolvable
  - Env-leak gate (5 remediations)
  - Claude/Codex binary not found (compiled-binary-only)
  - "running" forever (AI working / orphan / idle_timeout)
  - Mid-workflow failure and auto-resume semantics
  - Approval gate missing on web UI (workflow-level interactive:)
  - MCP plugin connection noise (filtered by design)
  - Empty $nodeId.output / field access (4 causes)
- Diagnostic command cheat sheet (list, status, isolation list, validate,
  tail-log, --verbose, LOG_LEVEL=debug)
- Escalation protocol (version + validate + log tail + CHANGELOG + issue)

SKILL.md routing table now dispatches "Workflow good practices /
anti-patterns" and "Troubleshoot a failing / stuck workflow" to the new
references so an agent can find them without having to know they exist.

* docs(book): update node-types coverage from four to all seven

The book is the curated first-contact reading path (landing page → "Get
Started" → /book/). Both dag-workflows.md and quick-reference.md were
stuck on "four node types" — missing script, approval, and cancel. A user
reading the book as their first introduction would form an incomplete
mental model, then find three more node types in the reference section
later with no explanation of when they arrived.

book/dag-workflows.md:
- "four node types" → "seven node types. Exactly one mode field is
  required per node"
- Table now lists Command, Prompt, Bash, Script, Loop, Approval, Cancel
  with one-line "when to use" for each, and cross-links to the dedicated
  guide pages for Script / Loop / Approval
- New sections below the table for Script (inline + named examples with
  runtime and deps), Approval (with the interactive: true workflow-level
  note that's easy to miss), and Cancel (guarded-exit pattern) — keeping
  the existing narrative shape for Bash and Loop

book/quick-reference.md:
- Node Options table now includes script, approval, cancel rows
- agents row added (inline sub-agents, Claude-only)
- New "Script-specific fields" and "Approval-specific fields" subsections
  so the cheat-sheet is actually complete rather than pointing users
  elsewhere for the required constraints
- Retry row callout that loop nodes hard-error on retry — previously
  omitted
- bash timeout note widened to cover script timeout (same semantics)

Both files are docs-web content; the CI build on the docs-script-nodes
PR (coleam00#1362) previously validated the Starlight build path with a similar
table addition, so this should render clean.

* fix(skill/cli): remove nonexistent \`archon workflow cancel\`, fix workflow status jq recipe

Two accuracy issues from the PR code-reviewer (comment 4311243858).

C1: \`archon workflow cancel <run-id>\` does NOT exist as a CLI subcommand.
The switch at packages/cli/src/cli.ts:318-485 dispatches on list / run /
status / resume / abandon / approve / reject / cleanup / event — running
\`archon workflow cancel\` hits the default case and exits with "Unknown
workflow subcommand: cancel" (cli.ts:478-484). Active cancellation is
only available via:
  - /workflow cancel <run-id> chat slash command (all platforms)
  - Cancel button on the Web UI dashboard
  - POST /api/workflows/runs/{runId}/cancel REST endpoint

cli-commands.md: removed the \`### archon workflow cancel <run-id>\`
subsection; kept the \`abandon\` subsection but made it explicit that
abandon does NOT kill a subprocess. Added a call-out box at the bottom
of the abandon section explaining where to go for actual cancellation.

troubleshooting.md "running forever" section: split the original
cancel-vs-abandon advice into three bullets — Web UI / CLI abandon (for
orphans, no subprocess kill) / chat \`/workflow cancel\` (for live runs
that need interruption). Added an explicit "there is no archon workflow
cancel CLI subcommand" parenthetical since the wrong command was being
suggested in flow.

I1: the \`archon workflow list --json\` diagnostic used an incorrect jq
filter. workflow list's --json output (workflow.ts:185-219) has shape
{ workflows: [{ name, description, provider?, model?, ... }], errors: [...] }
with no \`runs\` field — \`jq '.workflows[] | select(.runs)'\` returns empty
unconditionally. Replaced with \`archon workflow status --json | jq '.runs[]'\`,
which matches the actual shape of workflowStatusCommand at
workflow.ts:852+ ({ runs: WorkflowRun[] }). Also tightened the narration
to distinguish JSON from human-readable status output.

No change to the commit history in this PR — these are follow-up fixes
to claims I introduced in earlier commits of this branch (f10b989 for
C1, 66d2b86 for I1).

* fix(skill): remove env-leak gate references (feature was removed in provider extraction)

C2 from the PR code-reviewer (comment 4311243858). The pre-spawn env-leak
gate was removed from the codebase during the provider-extraction refactor
— see TODO(coleam00#1135) at packages/providers/src/claude/provider.ts:908. Zero
hits for --allow-env-keys / allowEnvKeys / allow_env_keys / allow_target_repo_keys
across packages/. The CLI's parseArgs (cli.ts:182-208) has no
--allow-env-keys option, and because parseArgs uses strict: false, an
unknown --allow-env-keys would be silently ignored rather than error.

What remains accurate and is NOT touched:
- Three-Path Env Model section (user/repo archon-owned envs are loaded;
  target repo <cwd>/.env keys are stripped from process.env at boot)
  still correctly describes current behavior, grounded in
  packages/paths/src/strip-cwd-env.ts + env-integration.test.ts
- Per-Project Env Injection section (Option 1: .archon/config.yaml env:
  block; Option 2: Web UI Settings → Projects → Env Vars) is unchanged —
  both remain the sanctioned way to get env vars into subprocesses

Removed claims (all three files):
- cli-commands.md: --allow-env-keys flag row in the workflow run flags
  table
- repo-init.md: the "Env-leak gate" subsection at the end of Per-Project
  Env Injection listing 5 remediations (all of which reference UI/CLI/
  config surfaces that don't exist). Replaced with a succinct callout
  that explains the actual current behavior — target repo .env keys are
  stripped, workflows that need those values should use managed
  injection — so the reader still gets the "where to put my env vars"
  answer
- troubleshooting.md: the "Cannot register: codebase has sensitive env
  keys" section (error message that can no longer be emitted)

If the env-leak gate is ever resurrected per TODO(coleam00#1135), the docs can be
re-added then. The CHANGELOG v0.3.0 entry describing the gate is a
historical record of past behavior and does not need to be rewritten.

* fix(skill/troubleshooting): correct JSONL event type names and field name

C3 from the PR code-reviewer (comment 4311243858). The troubleshooting
reference's event-types table used _started / _completed / _failed
suffixes, but packages/workflows/src/logger.ts:19-30 shows the actual
WorkflowEvent.type enum is:

  workflow_start | workflow_complete | workflow_error |
  assistant | tool | validation |
  node_start | node_complete | node_skipped | node_error

The second jq recipe also queried `.event` but the discriminator is `.type`.

Fixes:
- Event table: renamed columns (_started → _start, _completed → _complete,
  _failed → _error). Explicitly called out the field name as `type` so the
  reader knows what jq selector to use
- Replaced the "tool_use / tool_result" row with a single `tool` row and
  listed its actual payload fields (tool_name, tool_input, duration_ms,
  tokens) — tool_use/tool_result are SDK message kinds that appear within
  the AI stream, not top-level log event types
- Added a `validation` row (was missing; it's emitted by workflow-level
  validation calls with `check` and `result` fields)
- Removed `retry_attempt` row — this event type is not emitted to the
  JSONL file. Retry bookkeeping goes through pino logs, not the workflow
  log file
- Added an explicit callout that loop_iteration_started /
  loop_iteration_completed (and other emitter-only events) go through
  the workflow event emitter + DB workflow_events table, NOT the JSONL
  file. Pointed readers to the DB or Web UI for loop-level detail. This
  distinguishes the two parallel event systems — easy to conflate
  (store.ts:11-17 uses _started/_completed/_failed for the DB side,
  logger.ts uses _start/_complete/_error for JSONL)
- Fixed the "all failed events" jq recipe: .event → .type and _failed → _error
- Minor cleanup: the inline "tool_use events" mention in the "running
  forever" section said the wrong event name — updated to "tool or
  assistant events in the tail"

Grounded in packages/workflows/src/logger.ts (canonical JSONL event
shape) and packages/workflows/src/store.ts (the parallel DB event
naming, which the reviewer correctly flagged as different and worth
keeping distinct).

* fix(skill): two stragglers from the code-reviewer audit

Cleanup of two references that slipped through the earlier C1 and C3 fixes:

- references/troubleshooting.md:126: \`node_failed\` → \`node_error\`
  (the "Node output is empty" diagnostics section references the JSONL
  log, which uses the logger.ts enum — not the DB workflow_events table
  which does use \`node_failed\`). The C3 fix corrected the event table
  and one jq recipe but missed this inline mention.

- references/interactive-workflows.md:106: removed \`archon workflow
  cancel <run-id>\` (nonexistent CLI subcommand) from the
  troubleshooting bullet. This was pre-existing before the hardening
  PR but fell within the C1 remediation scope. Replaced with the
  correct triage: reject (approval gate only) vs abandon (orphan
  cleanup, no subprocess kill) vs chat /workflow cancel (actual
  subprocess termination).

Grounded in the same sources as the earlier C1/C3 commits:
packages/cli/src/cli.ts:318-485 (no cancel case) and
packages/workflows/src/logger.ts:19-30 (JSONL type enum).

* feat(skill): point to archon.diy as the canonical docs source

The skill had no reference to archon.diy (the live docs site built from
packages/docs-web/). Several reference files said "see the docs site"
without naming the URL, leaving the agent to guess or grep the repo for
the hostname. An agent with the skill loaded should know that when the
distilled reference pages don't cover a case, the full canonical docs
are one WebFetch away.

SKILL.md: new "Richer Context: archon.diy" section between Routing and
Running Workflows. Covers:
- When to reach for the live docs (longer examples, tutorial framing,
  features the skill only mentions in passing, "where's that
  documented?" user questions)
- URL map — 13 starting points covering getting-started, book (tutorial
  series), guides/ (authoring + per-node-type + per-node-feature),
  reference/ (variables, CLI, security, architecture, configuration,
  troubleshooting), adapters/, deployment/
- Precedence: skill refs first (context-cheap, tuned for agents), docs
  site as escalation. Prevents agents defaulting to WebFetch when a
  local skill ref already covers the answer

Also upgrades the 5 existing generic "docs site" mentions across
reference files to concrete archon.diy URLs with anchor fragments where
helpful:
- good-practices.md: Inline sub-agents pattern → archon.diy/guides/
  authoring-workflows/#inline-sub-agents
- troubleshooting.md: "Install page on the docs site" → archon.diy/
  getting-started/installation/
- workflow-dag.md: "Workflow Description Best Practices" → anchor link;
  sandbox schema reference → archon.diy/guides/authoring-workflows/
  #claude-sdk-advanced-options
- repo-init.md: Security Model reference → archon.diy/reference/
  security/#target-repo-env-isolation (deep-link into the section that
  covers the <cwd>/.env strip behavior)

URL source of truth: astro.config.mjs:5 (site: 'https://archon.diy').
URL structure mirrors packages/docs-web/src/content/docs/<section>/
<page>.md — verified by the 62 pages the docs build produces.
…#1395)

Anthropic's Opus 4.7 landed 2026-04-16; on the Anthropic API, opus /
opus[1m] now resolve to 4.7 with a 1M context window at standard
pricing. Using the alias instead of the hard-pinned claude-opus-4-6[1m]
lets bundled default workflows auto-track the recommended Opus version.

No explicit effort is set, so nodes inherit the per-model default
(xhigh on 4.7, high on 4.6).
…m00#1398)

* fix(workflow): migrate piv-loop plan handoff to $ARTIFACTS_DIR (coleam00#1380)

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

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

Fixes coleam00#1380

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

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

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

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

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

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

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

---------

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

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

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

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

Also registers the new reference in SKILL.md routing table.
…nch resolution

The cleanup service called getDefaultBranch() directly for merge detection,
ignoring worktree.baseBranch from .archon/config.yaml. This caused repeated
env_cleanup_error on startup for repos where the default branch is not main
and origin/HEAD is not set (e.g. repos using master).

Add resolveMainBranch() helper that reads worktree.baseBranch from the repo
config before falling back to git auto-detection. Replace all three
getDefaultBranch() call sites in cleanup-service.ts.

Fixes coleam00#1419
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

Adds an async helper to resolve a repository’s main branch by reading .archon/config.yaml (worktree.baseBranch, normalized via toBranchName) and falling back to getDefaultBranch on error or absence. Updates cleanup-service call sites to use this resolver and adds tests for configured and fallback behaviors.

Changes

Cohort / File(s) Summary
Main branch resolution & call-site updates
packages/core/src/services/cleanup-service.ts
Adds resolveMainBranch which loads repo config and returns worktree.baseBranch (trimmed + toBranchName) if available; otherwise falls back to getDefaultBranch. Replaces direct getDefaultBranch calls in runScheduledCleanup, getWorktreeStatusBreakdown, and cleanupMergedWorktrees.
Tests: cleanup service
packages/core/src/services/cleanup-service.test.ts
Adds tests asserting that when worktree.baseBranch is a valid string, getDefaultBranch is not called and merge checks use the configured branch; adds coverage for fallback cases (invalid YAML, non-string, null) ensuring getDefaultBranch is invoked once and used for merge checks.

Sequence Diagram(s)

sequenceDiagram
    participant Cleanup as CleanupService
    participant Config as ConfigLoader
    participant Git as GitUtils
    Cleanup->>Config: loadRepoConfig(repoPath)
    alt config.worktree.baseBranch exists
        Config-->>Cleanup: baseBranch
        Cleanup->>Git: toBranchName(trim(baseBranch))
        Git-->>Cleanup: normalizedBranch
        Cleanup->>Cleanup: use normalizedBranch for merge checks / cleanup
    else config missing or parse error
        Config-->>Cleanup: null / error
        Cleanup->>Git: getDefaultBranch(repoPath)
        Git-->>Cleanup: defaultBranch
        Cleanup->>Cleanup: use defaultBranch for merge checks / cleanup
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I peeked in config with a twitch and a hop,

Found baseBranch hiding — I gave it a chop,
Trimmed and named it, then danced in the night,
Now cleanup knows left from right. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: respecting worktree.baseBranch in the cleanup service's default branch resolution logic.
Description check ✅ Passed The PR description comprehensively covers the problem, solution, validation evidence, side effects, and rollback plan, closely following the required template structure with all critical sections filled.
Linked Issues check ✅ Passed The code changes fully address issue #1419: resolveMainBranch() reads worktree.baseBranch from config and falls back to getDefaultBranch(); three call sites are updated; tests verify both config override and fallback behavior.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the cleanup service's base branch resolution: new resolveMainBranch() helper, three updated call sites, and corresponding test coverage with no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/core/src/services/cleanup-service.test.ts (1)

1463-1507: Strengthen the override test by also asserting getDefaultBranch is not called.

The PR objective for #1419 explicitly calls for "a unit test verifying getDefaultBranch is not called when the config provides baseBranch" — i.e., the whole point is skipping git auto-detection (which is the source of the startup error spam). Today the test only asserts the value passed to isBranchMerged. A loud regression — e.g., someone refactors resolveMainBranch to call getDefaultBranch first and override afterward — would still pass.

Also: mergeResult is captured but never asserted on; either drop the binding or assert against it.

♻️ Suggested additional assertions
         const mergeResult = await cleanupMergedWorktrees('codebase-1', tempDir);

         // isBranchMerged should have been called with 'master' (from config)
         const branchMergeCalls = mockIsBranchMerged.mock.calls.filter(
           (call: unknown[]) => call[0] === tempDir
         );
         expect(branchMergeCalls).toHaveLength(1);
         expect(branchMergeCalls[0][2]).toBe('master');
+        // Critical: git auto-detection must be bypassed when config provides baseBranch
+        expect(mockGetDefaultBranch).not.toHaveBeenCalledWith(tempDir);
+        expect(mergeResult.removed).toContain('feature-x');
       } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/services/cleanup-service.test.ts` around lines 1463 - 1507,
The test for cleanupMergedWorktrees currently verifies isBranchMerged gets
'master' from the config but doesn't assert that git auto-detection was skipped;
update the test to additionally assert that the getDefaultBranch mock (or the
mocked function used by resolveMainBranch/git detection) was not called
(expect(mockGetDefaultBranch).not.toHaveBeenCalled()) to ensure auto-detection
is bypassed, and either remove the unused mergeResult binding or add an
assertion on mergeResult (e.g., expected shape/status) to avoid unused-variable
noise; reference the test's use of cleanupMergedWorktrees, mockIsBranchMerged,
and the mocked getDefaultBranch function when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/services/cleanup-service.test.ts`:
- Around line 1509-1516: The test currently short-circuits because
mockListByCodebase returns an empty array; update the test so mockListByCodebase
resolves to a single environment entry (so the for-loop executes) and clear
mockGetDefaultBranch before calling cleanupMergedWorktrees to avoid prior-test
leakage (use mockGetDefaultBranch.mockClear() or resetMocks). Then assert
mockGetDefaultBranch was called exactly once with '/nonexistent/repo' and assert
that isBranchMerged (or the consumer that checks merge status) was invoked with
the fallback branch value ('main') to verify the resolver→consumer chain
(reference cleanupMergedWorktrees, mockListByCodebase, mockGetDefaultBranch, and
isBranchMerged).
- Around line 1462-1518: The new describe('resolveMainBranch config override',
...) is incorrectly nested inside the describe('cleanupStaleWorktrees', ...)
block but tests exercise cleanupMergedWorktrees; move that entire describe block
so it is a sibling (or inside) the describe('cleanupMergedWorktrees', ...) block
and not inside cleanupStaleWorktrees, and add a beforeEach inside that describe
to clear/reset the mocks it depends on (mockIsBranchMerged,
mockGetDefaultBranch, mockGetPrState, mockIsPatchEquivalent, mockListByCodebase)
using jest.clearAllMocks() or individually mockClear/mockReset before each test
so one-shot returns and call history cannot leak. Ensure tests still call
cleanupMergedWorktrees and keep assertions unchanged.

In `@packages/core/src/services/cleanup-service.ts`:
- Around line 41-54: The resolveMainBranch function currently re-implements YAML
loading and uses unsafe type assertions; change it to call
loadRepoConfig(repoPath) and, if loadRepoConfig returns a baseBranch string,
return toBranchName(result.baseBranch) (fall back to getDefaultBranch(repoPath)
otherwise). Replace the double-cast approach by checking typeof raw === 'string'
when validating baseBranch (use the already-normalized baseBranch from
loadRepoConfig), and in the catch block add a debug/process logger entry that
includes the caught error to aid diagnosing malformed or missing
.archon/config.yaml files.

---

Nitpick comments:
In `@packages/core/src/services/cleanup-service.test.ts`:
- Around line 1463-1507: The test for cleanupMergedWorktrees currently verifies
isBranchMerged gets 'master' from the config but doesn't assert that git
auto-detection was skipped; update the test to additionally assert that the
getDefaultBranch mock (or the mocked function used by resolveMainBranch/git
detection) was not called (expect(mockGetDefaultBranch).not.toHaveBeenCalled())
to ensure auto-detection is bypassed, and either remove the unused mergeResult
binding or add an assertion on mergeResult (e.g., expected shape/status) to
avoid unused-variable noise; reference the test's use of cleanupMergedWorktrees,
mockIsBranchMerged, and the mocked getDefaultBranch function when making these
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c923c077-9402-4b3a-a652-bced306872b2

📥 Commits

Reviewing files that changed from the base of the PR and between 9122673 and 4d05a4f.

📒 Files selected for processing (2)
  • packages/core/src/services/cleanup-service.test.ts
  • packages/core/src/services/cleanup-service.ts

Comment thread packages/core/src/services/cleanup-service.test.ts Outdated
Comment thread packages/core/src/services/cleanup-service.test.ts Outdated
Comment on lines +41 to +54
async function resolveMainBranch(repoPath: RepoPath): Promise<BranchName> {
try {
const content = await readFile(join(repoPath, '.archon', 'config.yaml'), 'utf-8');
const config = Bun.YAML.parse(content) as Record<string, unknown> | null;
const worktree = (config as Record<string, Record<string, unknown>> | null)?.worktree;
const configured = (worktree?.baseBranch as string | undefined)?.trim();
if (configured) {
return toBranchName(configured);
}
} catch {
// Config load failure (missing file, parse error) is non-fatal — fall through to auto-detection
}
return getDefaultBranch(repoPath);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether loadRepoConfig already exists and exposes worktree.baseBranch
ast-grep --pattern 'export function loadRepoConfig($$$) { $$$ }'
ast-grep --pattern 'export async function loadRepoConfig($$$) { $$$ }'
rg -nP --type=ts -C3 '\bloadRepoConfig\b'
# Also confirm baseBranch normalization is centralized
rg -nP --type=ts -C2 '\bbaseBranch\b' -g '!**/*.test.ts' -g '!**/dist/**'

Repository: coleam00/Archon

Length of output: 38421


Refactor to use loadRepoConfig for consistent config handling.

The codebase already has loadRepoConfig() (in packages/core/src/config/config-loader.ts) which reads, parses, and normalizes worktree.baseBranch from .archon/config.yaml. Re-implementing YAML reading and extraction here creates a parallel codepath that can drift during schema evolution. Use loadRepoConfig(repoPath) and access result.baseBranch instead, which is already normalized with .trim() applied.

Additionally:

  1. Replace the double type assertion with a typeof raw === 'string' check for defensive runtime validation
  2. Enhance the bare catch block with a debug log to aid diagnosis of misconfigured .archon/config.yaml files
Suggested fix
 async function resolveMainBranch(repoPath: RepoPath): Promise<BranchName> {
-  try {
-    const content = await readFile(join(repoPath, '.archon', 'config.yaml'), 'utf-8');
-    const config = Bun.YAML.parse(content) as Record<string, unknown> | null;
-    const worktree = (config as Record<string, Record<string, unknown>> | null)?.worktree;
-    const configured = (worktree?.baseBranch as string | undefined)?.trim();
+  const config = await loadRepoConfig(repoPath);
+  const configured = config.baseBranch;
+  if (configured) {
+    try {
+      return toBranchName(configured);
+    } catch (err) {
+      getLog().debug({ err, repoPath }, 'repo_config_base_branch_parse_failed');
+    }
+  }
-    if (configured) {
-      return toBranchName(configured);
-    }
-  } catch (err) {
-    // Config load failure (missing file, parse error) is non-fatal — fall through to auto-detection
-    getLog().debug({ err, repoPath }, 'repo_config_load_failed');
-  }
   return getDefaultBranch(repoPath);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/services/cleanup-service.ts` around lines 41 - 54, The
resolveMainBranch function currently re-implements YAML loading and uses unsafe
type assertions; change it to call loadRepoConfig(repoPath) and, if
loadRepoConfig returns a baseBranch string, return
toBranchName(result.baseBranch) (fall back to getDefaultBranch(repoPath)
otherwise). Replace the double-cast approach by checking typeof raw === 'string'
when validating baseBranch (use the already-normalized baseBranch from
loadRepoConfig), and in the catch block add a debug/process logger entry that
includes the caught error to aid diagnosing malformed or missing
.archon/config.yaml files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered this, but loadRepoConfig() imports config-loader.ts which has a top-level import from '@archon/providers'. This triggers a module resolution failure in the cleanup-service test suite because those tests don't (and shouldn't need to) mock the entire providers registry.

The inline YAML read is intentionally self-contained to avoid pulling in the config-loader dependency chain. Happy to revisit if config-loader gets decoupled from @archon/providers in the future.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kagura-agent, that's a clear and valid constraint — pulling config-loader.ts into cleanup-service.ts would drag @archon/providers into the test dependency graph, which is a real cost. The self-contained inline read is the right call here.

The refactoring suggestion is withdrawn. Two smaller hardening points from the original comment still apply independently, if you'd like them:

  1. typeof guard instead of double cast — replaces the as Record<string, Record<string, unknown>> | null re-cast with a runtime check:

    const worktree = config !== null && typeof config === 'object' && 'worktree' in config
      ? (config as Record<string, unknown>).worktree
      : undefined;
    const raw = worktree !== null && typeof worktree === 'object' && 'baseBranch' in (worktree as object)
      ? (worktree as Record<string, unknown>).baseBranch
      : undefined;
    const configured = typeof raw === 'string' ? raw.trim() : undefined;
  2. Debug log in catch — helps diagnose genuinely malformed .archon/config.yaml files without being noisy for the common "file doesn't exist" case:

    } catch (err) {
      getLog().debug({ err, repoPath }, 'resolve_main_branch_config_load_failed');
    }

Neither is blocking. Happy to close out the comment as-is if you'd prefer to keep the current form.


✏️ Learnings added
Learnt from: kagura-agent
Repo: coleam00/Archon PR: 1423
File: packages/core/src/services/cleanup-service.ts:41-54
Timestamp: 2026-04-26T09:16:35.436Z
Learning: In `packages/core/src/services/cleanup-service.ts`, `resolveMainBranch` intentionally uses an inline `readFile` + `Bun.YAML.parse` instead of `loadRepoConfig()` from `packages/core/src/config/config-loader.ts`. The reason is that `config-loader.ts` has a top-level `import from 'archon/providers'`, which causes module resolution failures in the cleanup-service test suite (tests do not mock the full providers registry). The self-contained YAML read avoids the `archon/providers` dependency chain in tests.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/git/src/**/*.{ts,tsx} : `archon/git` handles git operations - worktrees, branches, repos, exec wrappers; depends only on archon/paths; use `execFileAsync` for direct git calls (not `exec`)

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/cli/src/**/*.{ts,tsx} : CLI isolation defaults: auto-creates worktree with generated branch name (isolation by default); `--branch` flag for explicit branch name; `--no-worktree` opts out of isolation (runs in live checkout)

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/paths/src/**/*.{ts,tsx} : `archon/paths` provides path resolution utilities, Pino logger factory, web dist cache path (`getWebDistDir`), and CWD env stripper (`stripCwdEnv`, `strip-cwd-env-boot`); has no archon/* deps; only `pino` and `dotenv` allowed as external deps

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to **/*.{ts,tsx} : Use `archon/git` functions for git operations; use `execFileAsync` (not `exec`) when calling git directly

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/workflows/src/defaults/**/*.{ts,tsx,yaml} : Bundled defaults are in `.archon/commands/defaults/` and `.archon/workflows/defaults/`; binary builds embed at compile time via `packages/workflows/src/defaults/bundled-defaults.generated.ts`; source builds load from filesystem; merged with repo-specific (repo overrides defaults by name)

Learnt from: truck0321
Repo: coleam00/Archon PR: 1104
File: .archon/scripts/forge-cli.ts:15-16
Timestamp: 2026-04-15T12:56:38.364Z
Learning: `.archon/scripts/forge-cli.ts` is an intentionally standalone Bun script with zero dependencies by design. It runs in workflow bash nodes via `bun` execution outside the monorepo package graph, so importing `archon/git` or any other monorepo package would require bundling and is explicitly not desired. Do not suggest importing monorepo packages into this script.

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to **/*.{ts,tsx} : Import workflow engine types/functions from direct subpaths, not main package export: `from 'archon/workflows/deps'`, `from 'archon/workflows/store'`, `from 'archon/workflows/executor'`, etc.

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/workflows/src/**/*.{ts,tsx} : `archon/workflows` is the workflow engine - loader, router, executor, DAG, logger, bundled defaults; depends only on archon/git + archon/paths + archon/providers/types + hono/zod-openapi + zod; DB/AI/config injected via `WorkflowDeps`

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to **/*.{ts,tsx} : Use `import * as conversationDb from 'archon/core/db/conversations'` and `import * as git from 'archon/git'` for namespace imports from well-organized submodules

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to **/*.{ts,tsx} : Import workflow engine types/functions from direct subpaths (e.g., `from 'archon/workflows/deps'`, `from 'archon/workflows/store'`, `from 'archon/workflows/executor'`) rather than the main package export

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/workflows/src/workflow-discovery.ts : Migration from pre-0.x `~/.archon/.archon/workflows/`: Archon emits a one-time WARN with exact `mv` command and does NOT load from old location. Move with: `mv ~/.archon/.archon/workflows ~/.archon/workflows && rmdir ~/.archon/.archon`

Learnt from: Wirasm
Repo: coleam00/Archon PR: 514
File: archon-ui-main/src/services/crawlProgressService.ts:35-39
Timestamp: 2025-08-28T13:07:24.810Z
Learning: The crawlProgressService.ts in the Archon codebase should be deprecated in favor of the existing useCrawlProgressPolling hook from usePolling.ts, which already includes ETag support, 304 handling, tab visibility detection, and proper React lifecycle integration. This consolidation reduces code duplication and improves performance.

Learnt from: CR
Repo: coleam00/Archon PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-20T18:47:37.611Z
Learning: Applies to packages/workflows/src/**/*.{ts,tsx} : Provider is inherited from `.archon/config.yaml` unless explicitly set; per-node `provider` and `model` overrides are supported; model validation ensures provider/model compatibility at load time

- Move describe block to top-level (was incorrectly nested inside cleanupStaleWorktrees)
- Add proper beforeEach with mock reset to prevent test order leakage
- Strengthen override test to assert getDefaultBranch NOT called
- Strengthen fallback test with a real environment entry to verify full chain
@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Hi @kagura-agent — thanks for opening this PR.

This repository uses a PR template at .github/pull_request_template.md with several required sections. A few of them appear to be empty or placeholder here:

  • Side Effects / Blast Radius
  • Rollback Plan

Could you fill those out (even briefly)? The template helps reviewers understand scope, risk, and rollback — it speeds up review significantly.

If a section genuinely doesn't apply, just write "N/A" in it rather than leaving it blank.

@kagura-agent
Copy link
Copy Markdown
Contributor Author

Thanks @Wirasm — added the Side Effects / Blast Radius and Rollback Plan sections!

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 28, 2026

Review Summary

Verdict: minor-fixes-needed

Your fix is tight and well-scoped — resolveMainBranch is a clean addition, and the three call sites are updated consistently. The type safety and test structure are solid. A few items need attention before merge, but nothing blocking.

Suggested fixes

  • cleanup-service.ts:34-52 — The catch block is completely silent. loadRepoConfig() in config-loader.ts handles the same scenario (missing file, parse error) and logs before falling through — your new function should do the same. At minimum add getLog().debug(...) for expected failures. Better yet, prefer Option A — delegate to loadRepoConfig() directly:

    async function resolveMainBranch(repoPath: RepoPath): Promise<BranchName> {
      const repoConfig = await loadRepoConfig(repoPath);
      if (repoConfig.worktree?.baseBranch?.trim()) {
        return toBranchName(repoConfig.worktree.baseBranch.trim());
      }
      return getDefaultBranch(repoPath);
    }

    This eliminates the duplicate YAML parsing and gets proper error handling for free.

  • cleanup-service.ts:37-55 — Add test cases for the error fallback paths that aren't currently exercised:

    // Invalid YAML syntax in config
    test('falls back on YAML parse error', async () => {
      await writeFile(configPath, 'worktree:\n  baseBranch: [invalid');
      await cleanupMergedWorktrees(repoPath, mockLog, mockExecutor);
      expect(mockGetDefaultBranch).toHaveBeenCalledWith(repoPath);
    });
    
    // baseBranch is a non-string type
    test('falls back when baseBranch is not a string', async () => {
      await writeFile(configPath, 'worktree:\n  baseBranch: !!null null');
      await cleanupMergedWorktrees(repoPath, mockLog, mockExecutor);
      expect(mockGetDefaultBranch).toHaveBeenCalledWith(repoPath);
    });
  • cleanup-service.ts:329 & :483 (runScheduledCleanup, getWorktreeStatusBreakdown) — These two call sites for resolveMainBranch aren't directly tested. They're covered transitively, but adding an assertion that mockGetDefaultBranch is NOT called (or IS called with correct args) in these contexts would catch silent regressions.

Minor / nice-to-have

  • cleanup-service.ts:37-40 — The JSDoc on resolveMainBranch restates what the function name already says. Consider removing it — the function body makes the fallback order clear without it.

Compliments

  • The catch block comment at line 52-53 is exactly right — it captures the why of silent fallback (// Config load failure is non-fatal) without restating what the code does. This is the standard for Archon comments.
  • The test suite's approach of using a real temp directory with a real YAML file is the correct choice for config-loading tests, and the try/finally cleanup is properly handled.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality.

@kagura-agent
Copy link
Copy Markdown
Contributor Author

Thanks for the review @Wirasm! Just a quick note — the docker-entrypoint.sh comment style suggestion doesn't apply to this PR, as it only touches TypeScript files (cleanup-service.ts and cleanup-service.test.ts) where // is the standard comment syntax. No other blocking or suggested changes, so this should be ready to go. Let me know if I missed anything!

…e assertions

- Add getLog().debug() to resolveMainBranch catch block for visibility
- Add tests for YAML parse error fallback to getDefaultBranch
- Add tests for non-string baseBranch (number) fallback
- Add tests for null baseBranch fallback
- Add assertions verifying resolveMainBranch in runScheduledCleanup
- Add assertions verifying resolveMainBranch in getWorktreeStatusBreakdown
@kagura-agent
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review! Addressed all items:

  • Added getLog().debug(...) logging to the resolveMainBranch catch block for visibility on config load failures
  • Added tests for YAML parse error fallback and non-string baseBranch fallback (number type triggers TypeError on .trim(), null falls through)
  • Added assertions verifying resolveMainBranch is called correctly in both runScheduledCleanup and getWorktreeStatusBreakdown call sites

Re: Option A (delegating to loadRepoConfig) — I considered this but config-loader.ts imports from @archon/providers at the top level, which causes module resolution failures in cleanup-service.test.ts. The inline Bun.YAML.parse approach avoids this coupling while still providing the same fallback behavior. Happy to discuss if you see a better path!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/services/cleanup-service.test.ts (1)

1692-1708: Make the “no config exists” fallback test deterministic.

Using '/nonexistent/repo' (Line 1697 / Line 1703) is environment-dependent. Prefer a fresh temp dir with no .archon/config.yaml to avoid host-specific false failures.

♻️ Proposed stabilization
   test('falls back to getDefaultBranch when no config exists', async () => {
-    mockListByCodebase.mockResolvedValueOnce([
-      {
-        id: 'env-fallback',
-        branch_name: 'feature-y',
-        working_path: '/nonexistent/repo/worktrees/feature-y',
-        status: 'active',
-      },
-    ]);
-    mockGetDefaultBranch.mockResolvedValueOnce('main');
-
-    await cleanupMergedWorktrees('codebase-1', '/nonexistent/repo');
-
-    expect(mockGetDefaultBranch).toHaveBeenCalledTimes(1);
-    expect(mockGetDefaultBranch).toHaveBeenCalledWith('/nonexistent/repo');
-    expect(mockIsBranchMerged.mock.calls[0][2]).toBe('main');
+    const { mkdtemp, rm } = await import('fs/promises');
+    const { join } = await import('path');
+    const { tmpdir } = await import('os');
+    const tempDir = await mkdtemp(join(tmpdir(), 'archon-cleanup-test-'));
+    try {
+      mockListByCodebase.mockResolvedValueOnce([
+        {
+          id: 'env-fallback',
+          branch_name: 'feature-y',
+          working_path: join(tempDir, 'worktrees', 'feature-y'),
+          status: 'active',
+        },
+      ]);
+      mockGetDefaultBranch.mockResolvedValueOnce('main');
+
+      await cleanupMergedWorktrees('codebase-1', tempDir);
+
+      expect(mockGetDefaultBranch).toHaveBeenCalledTimes(1);
+      expect(mockGetDefaultBranch).toHaveBeenCalledWith(tempDir);
+      expect(mockIsBranchMerged.mock.calls[0][2]).toBe('main');
+    } finally {
+      await rm(tempDir, { recursive: true, force: true });
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/services/cleanup-service.test.ts` around lines 1692 - 1708,
The test uses a host-dependent path '/nonexistent/repo'; make it deterministic
by creating an isolated temporary directory with no .archon/config.yaml and pass
that dir to cleanupMergedWorktrees instead. Replace the hardcoded
'/nonexistent/repo' in this test with a freshly-created temp dir (e.g., using
fs.mkdtemp / os.tmpdir) and ensure the temp dir is empty (or explicitly remove
any .archon/config.yaml) before calling cleanupMergedWorktrees('codebase-1',
tempDir), then clean up the temp dir after the test; keep existing assertions
for mockGetDefaultBranch and mockIsBranchMerged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/services/cleanup-service.test.ts`:
- Around line 1692-1708: The test uses a host-dependent path
'/nonexistent/repo'; make it deterministic by creating an isolated temporary
directory with no .archon/config.yaml and pass that dir to
cleanupMergedWorktrees instead. Replace the hardcoded '/nonexistent/repo' in
this test with a freshly-created temp dir (e.g., using fs.mkdtemp / os.tmpdir)
and ensure the temp dir is empty (or explicitly remove any .archon/config.yaml)
before calling cleanupMergedWorktrees('codebase-1', tempDir), then clean up the
temp dir after the test; keep existing assertions for mockGetDefaultBranch and
mockIsBranchMerged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 183fe10d-fdbe-4f79-8287-4363eba2dad1

📥 Commits

Reviewing files that changed from the base of the PR and between c5e41ba and f456031.

📒 Files selected for processing (2)
  • packages/core/src/services/cleanup-service.test.ts
  • packages/core/src/services/cleanup-service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/services/cleanup-service.ts

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.

worktree.baseBranch in .archon/config.yaml is documented but never read — causes repeated env_cleanup_error on startup

3 participants