Skip to content

feat(isolation,workflows): worktree location + per-workflow isolation policy#1310

Merged
Wirasm merged 3 commits intodevfrom
feat/worktree-path-and-enabled-policy
Apr 20, 2026
Merged

feat(isolation,workflows): worktree location + per-workflow isolation policy#1310
Wirasm merged 3 commits intodevfrom
feat/worktree-path-and-enabled-policy

Conversation

@Wirasm
Copy link
Copy Markdown
Collaborator

@Wirasm Wirasm commented Apr 20, 2026

Summary

  • Problem (WHERE): Worktrees were scattered across three layouts depending on how the repo was registered — project-scoped (workspace-cloned) or legacy global (locally-registered) — and there was no way to co-locate them with the repo for IDE visibility. Silent divergence surface. Requested in feat: per-project worktree.path config option #1117 (@joelsb).
  • Problem (WHETHER): Read-only workflows like repo-triage needed every caller (CLI/web/chat/scheduled trigger) to remember to opt out of worktree creation. No declarative way for a workflow to pin "I always run live."
  • What changed:
    1. Collapsed worktree layouts from three to two — workspace-scoped (always, for every repo) or repo-local (opt-in via worktree.path). The legacy ~/.archon/worktrees/<owner>/<repo>/ fallback is removed; every repo resolves to ~/.archon/workspaces/<owner>/<repo>/worktrees/ by default regardless of how it was registered.
    2. Added per-project worktree.path to .archon/config.yaml — validated repo-relative path, opt-in co-location (e.g. .worktrees lives at <repoRoot>/.worktrees/<branch>).
    3. Added per-workflow worktree.enabled: true | false to workflow YAML — pins isolation regardless of invocation surface. Hard-errors on conflict with CLI flags. First consumer: .archon/workflows/repo-triage.yaml (enabled: false).
  • What did not change (scope boundary): No changes to the CLI flag surface (--no-worktree, --branch, --from still work); no new env vars; no migration of already-created worktrees (existing paths remain reachable through stored working_path); no changes to isolation adapters outside WorktreeProvider; no web UI changes beyond what generated types produce.

UX Journey

Before

  User            .archon/config.yaml        WorktreeProvider                       Worktree location
  ────            ───────────────────        ────────────────                       ─────────────────
  archon-cloned    (no worktree.path)    →   project-scoped layout               →  ~/.archon/workspaces/owner/repo/worktrees/<branch>
  local-register   (no worktree.path)    →   [~] legacy global layout            →  ~/.archon/worktrees/owner/repo/<branch>
                                             (two paths for "basically the same")

  repo-triage.yaml (no policy)           +   caller must remember --no-worktree  →  worktree or live depending on caller

  foo.yaml worktree.path: ...            →   [X] no support                      →  ignored

After

  User            .archon/config.yaml        WorktreeProvider                       Worktree location
  ────            ───────────────────        ────────────────                       ─────────────────
  any repo         (no worktree.path)    →   workspace-scoped (unified)          →  ~/.archon/workspaces/owner/repo/worktrees/<branch>
  any repo         *worktree.path: X*    →   **repo-local**                      →  <repoRoot>/X/<branch>          ← new, opt-in

  repo-triage.yaml worktree.enabled: false →  policy pinned, CLI flags must agree →  live checkout, every time

  foo.yaml worktree.enabled: true         →  CLI --no-worktree → hard error      →  always isolated

Architecture Diagram

Before

getWorktreeBase() ──▶ one of three returns (string only)
                        ├─ project-scoped path            (workspace-cloned repos)
                        ├─ legacy global ~/.archon/worktrees/   (local-registered)
                        └─ (no override support)
isProjectScopedWorktreeBase() ── separate classifier (duplicates logic)

WorktreeProvider.getWorktreePath() ── classifies + appends owner/repo + branch
WorktreeProvider.createWorktree()  ── separately loads config (swallow-and-retry), re-classifies

workflow YAML ── no isolation policy field
CLI  ── only way to opt out: --no-worktree on every invocation
orchestrator ── always calls validateAndResolveIsolation()

After

getWorktreeBase(repoPath, codebaseName?, override?) ──▶ { base, layout }
                        ├─ 'repo-local'         (when override.repoLocal set)    [+]
                        └─ 'workspace-scoped'   (default for every repo)          [~]
resolveOwnerRepo() ── new internal helper: uniform owner/repo identity
isProjectScopedWorktreeBase() ── thin wrapper on layout  (deprecated)             [~]

WorktreeProvider.getWorktreePath(request, branch, config?)
 └─ validates config.path → passes to getWorktreeBase → appends branch           [~]
WorktreeProvider.createWorktree(..., preloadedConfig)
 └─ single config load at create() boundary, no retry                            [~]
resolveRepoLocalOverride() ── Fail-Fast validation of worktree.path              [+]

workflow YAML top-level `worktree: { enabled: bool }`                            [+]
CLI ── reconciles policy with --branch/--no-worktree/--from (hard error)         [~]
orchestrator ── short-circuits when worktree.enabled === false                   [~]

Connection inventory:

From To Status Notes
getWorktreeBase() return type { base, layout } modified Was string
WorktreeBaseOverride.repoLocal getWorktreeBase() new Opt-in repo-local
WorktreeCreateConfig.path RepoConfigLoaderWorktreeProvider new Sourced from .archon/config.yaml worktree.path
WorkflowBase.worktree.enabled workflow.ts schema + loader new Parses with warn-and-ignore for bad types
workflow.worktree.enabled CLI reconciliation new Hard error on flag conflict
workflow.worktree.enabled dispatchOrchestratorWorkflow new Short-circuits isolation resolver when false
extractOwnerRepo(repoPath) resolveOwnerRepo() fallback modified Now fires for any local-registered repo (uniform path)
~/.archon/worktrees/ fallback removed Legacy global layout gone
generateEnvId() removed Dead after envId = worktreePath

Label Snapshot

  • Risk: risk: medium (changes worktree base resolution semantics for locally-registered repos — existing worktrees remain accessible via stored paths, but new worktrees for such repos go to workspace-scoped locations)
  • Size: size: M
  • Scope: isolation, workflows, cli, core, docs
  • Module: isolation:worktree-provider, git:worktree-base, workflows:schema, workflows:loader, cli:workflow-cmd, core:orchestrator-agent, core:config-types

Change Metadata

  • Change type: feature
  • Primary scope: multi

Linked Issue

Validation Evidence (required)

bun run validate          # ✅ check:bundled + type-check + lint + format + test (all 10 packages)

Targeted suites:

  • @archon/git — 142 tests pass (getWorktreeBase return-shape migration + 2 new repo-local override tests + isProjectScopedWorktreeBase deprecation notes)
  • @archon/isolation — 244+ tests pass; 8 new tests for worktree.path: custom path, empty/whitespace ignored, null/undefined fallback, override-wins-over-workspace, rejects absolute path, rejects .. escapes (3 variants), accepts nested relative
  • @archon/workflows — 149+ tests pass; 3 new loader tests for worktree.enabled: true | false | omitted
  • @archon/cli — 84 tests pass (was 79); 5 new reconciliation tests for policy vs flag combinations

E2E smoke (run manually from repo root):

bun run cli validate workflows e2e-worktree-disabled  # ✅ ok
bun run cli workflow run e2e-worktree-disabled "smoke"  # ✅ PASS: ran in live checkout (no worktree created by policy)
bun run cli workflow run e2e-worktree-disabled "smoke" --branch feat-x  # ✅ rejected: "worktree.enabled: false ... --branch requires an isolated worktree"
bun run cli workflow run e2e-deterministic "smoke" --no-worktree  # ✅ PASS (existing smoke, no regression)

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? Yes — narrow. Repo-local worktrees live inside the user's repo (previously always outside under ~/.archon/). Users opting in via worktree.path are implicitly trusting Archon to write into their repo tree; we validate the config value rejects ../absolute paths so a hostile or typo'd value can't escape the repo root. No automatic .gitignore mutation — users manage that themselves.

Compatibility / Migration

  • Backward compatible? Mostly yes.
    • Repos without worktree.path in their config continue to get worktrees under ~/.archon/workspaces/owner/repo/worktrees/. This was already the case for workspace-cloned repos.
    • Behavior change for locally-registered repos: new worktrees now go to ~/.archon/workspaces/owner/repo/worktrees/ instead of ~/.archon/worktrees/owner/repo/. Existing worktrees under the old path are still reachable via their stored working_path (no DB migration needed); Archon just won't create new ones there.
  • Config/env changes? Yes — two new optional fields: worktree.path (repo config) and worktree.enabled (workflow YAML).
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios:
    • bun run validate passes locally (type-check, lint, format, tests, bundled defaults).
    • E2E e2e-worktree-disabled runs and produces PASS: ran in live checkout (no worktree created by policy). Verified via pwd assertion inside a bash node.
    • E2E e2e-deterministic --no-worktree still passes (existing smoke, no regression).
    • CLI conflict: archon workflow run e2e-worktree-disabled "smoke" --branch feat-x exits non-zero with the expected policy-mismatch message.
  • Edge cases checked:
    • Malformed worktree.path values (absolute, .., nested ../) — all rejected with actionable messages.
    • worktree.enabled + --resume — policy is ignored on resume (documented in code comment).
    • Workflow loader warn-and-ignore for non-boolean worktree.enabled values.
  • What was not verified:
    • Web UI regeneration via bun --filter @archon/web generate:types — requires the dev server running. Not strictly necessary: web HealthResponse/WorkflowDefinition types do not yet reference the new optional fields, and backend continues to validate on submission. Will regenerate separately when touching other web-visible API surface.
    • Live run against a Claude-SDK workflow pinned to worktree.enabled: false. The CLI path is exercised by the e2e smoke + tests; the orchestrator path is covered by the new guard + existing mocks, but not by a live end-to-end run.

Side Effects / Blast Radius (required)

  • Affected subsystems: WorktreeProvider (layout + config load + envId assignment), @archon/git/worktree (primitive signature + tests), core/config (RepoConfig.worktree.path field), workflow loader + schema (worktree.enabled), CLI workflow command (reconciliation), orchestrator dispatch (short-circuit), .archon/workflows/repo-triage.yaml (first consumer).
  • Potential unintended effects:
    • New worktrees for locally-registered repos land in the workspace-scoped area instead of ~/.archon/worktrees/. Flagged above under Migration.
    • Config parse errors in .archon/config.yaml worktree.path now surface at worktree creation instead of defaulting silently. This is intentional (Fail Fast / CLAUDE.md), but it does mean a typo breaks runs rather than being ignored.
  • Guardrails: existing 'worktree.*_failed' structured log events fire on any error from resolveRepoLocalOverride(). New 'workflow.worktree_disabled_by_policy' log event makes the orchestrator short-circuit observable.

Rollback Plan (required)

  • Fast rollback: git revert the two commits. Both are cohesive and self-contained.
  • Feature flags: N/A — behavior opt-in is already via .archon/config.yaml / workflow YAML, so not setting those fields returns you to default behavior.
  • Observable failure symptoms: worktrees appearing in unexpected locations, CLI throwing .archon/config.yaml worktree.path must be … errors at run time, repo-triage creating a worktree instead of running live.

Risks and Mitigations

  • Risk: Locally-registered repos that had worktrees in ~/.archon/worktrees/owner/repo/<branch> will land new worktrees in ~/.archon/workspaces/owner/repo/worktrees/<branch>. Existing worktrees remain accessible (stored working_path is absolute), but archon isolation list may present a mixed tree for a transition period.
    • Mitigation: documented in CHANGELOG. No destructive change — old worktrees aren't touched. Users who want everything in one place can archon isolation cleanup the old ones.
  • Risk: A repo opting in to worktree.path: .worktrees without .gitignoreing the directory will see "untracked files" when running git status.
    • Mitigation: documented in configuration.md. Explicit non-goal of this PR — Archon doesn't mutate user-owned files.
  • Risk: Workflow-level worktree.enabled: true is a no-op ceiling on surfaces that don't have a --no-worktree equivalent (web UI, most chat adapters). Explicit in docs.

Credit to @joelsb for the repo-local worktree idea in #1117. Commit 1 carries a Co-authored-by: trailer.

Summary by CodeRabbit

  • New Features

    • Workflows can pin isolation with worktree.enabled (including forcing live-checkout)
    • Repositories can opt into a repo-relative worktree directory via worktree.path
    • CLI enforces/validates worktree-related flags and rejects conflicting flag combinations
    • Worktree layout behavior clarified to distinguish repo-local vs workspace-scoped layouts
  • Documentation

    • Updated workflow authoring guide, configuration reference, and CHANGELOG with worktree options and validation rules
  • Tests

    • Added unit and end-to-end tests covering worktree policies, path validation, and loader parsing

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds workflow-level worktree.enabled to pin isolation behavior, a repo-level worktree.path config for repo-local worktree placement, updates worktree base/layout modeling, and enforces CLI/orchestrator validation and control-flow to honor pinned policy. Tests and docs updated.

Changes

Cohort / File(s) Summary
Workflows / examples
\.archon/workflows/e2e-worktree-disabled.yaml, \.archon/workflows/repo-triage.yaml
New e2e smoke workflow and updated repo-triage workflow pinned with worktree.enabled: false.
Workflow schema & loader
packages/workflows/src/schemas/workflow.ts, packages/workflows/src/loader.ts, packages/workflows/src/loader.test.ts
Added worktree schema with optional enabled:boolean, loader parsing/validation, and tests for true/false/absent cases.
CLI command & tests
packages/cli/src/commands/workflow.ts, packages/cli/src/commands/workflow.test.ts
Reconciles workflow worktree.enabled with CLI flags, errors on incompatible flag combinations, and adjusts isolation decision logic; tests added/updated.
Orchestrator
packages/core/src/orchestrator/orchestrator-agent.ts
Short-circuits isolation resolution when workflow pins worktree.enabled: false, logs policy event, and sets cwd to live checkout; preserves prior isolation error handling.
Worktree base & git helpers
packages/git/src/worktree.ts, packages/git/src/index.ts, packages/git/src/git.test.ts
Removed legacy global fallback; introduced WorktreeLayout and WorktreeBaseOverride; getWorktreeBase() returns { base, layout }; tests updated to the two-layout model.
Isolation provider & types
packages/isolation/src/providers/worktree.ts, packages/isolation/src/providers/worktree.test.ts, packages/isolation/src/types.ts, packages/isolation/src/factory.ts
Added worktree.path validation (resolveRepoLocalOverride), refactored provider to load repo config once, compute repo-local override, and create worktrees accordingly; WorktreeCreateConfig gained optional path.
Core config types
packages/core/src/config/config-types.ts
Added optional path?: string to RepoConfig.worktree.
Docs & changelog
packages/docs-web/.../authoring-workflows.md, packages/docs-web/.../configuration.md, CHANGELOG.md
Documented worktree.enabled semantics and worktree.path behavior/validation; changelog updated.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI
  participant Orch as Orchestrator
  participant Prov as IsolationProvider
  participant Git as GitHelpers
  participant Config as RepoConfig

  CLI->>Orch: request run(workflow YAML, CLI flags)
  Orch->>Config: inspect workflow.worktree
  alt workflow.worktree?.enabled === false
    Orch->>Orch: log workflow.worktree_disabled_by_policy
    Orch->>Git: set cwd = codebase.default_cwd
    Orch-->>CLI: run in live checkout
  else
    Orch->>Prov: validateAndResolveIsolation(request)
    Prov->>Config: load repo config (once)
    Prov->>Git: getWorktreeBase(repoPath, codebaseName, override)
    Git-->>Prov: return { base, layout }
    Prov->>Prov: create/find worktree under resolved base
    Prov-->>Orch: return resolved cwd
    Orch-->>CLI: run at resolved cwd
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped through YAML, schema, and tree,

worktree.enabled told me where to be.
Repo-local paths now peek and play,
No hidden home-dir burrows away.
A carrot for config — hooray! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the two main features: worktree location consolidation/repo-local option and per-workflow isolation policy.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with detailed sections on problem, UX journey, architecture, validation, security, compatibility, and rollback.
Linked Issues check ✅ Passed The PR substantially fulfills all coding objectives from #1117: adds worktree.path config field, validates against absolute/escaping paths, implements priority override in WorktreeProvider, and provides comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes align with stated PR objectives. Worktree provider refactoring, config/schema updates, and CLI/orchestrator reconciliation are within scope. E2E workflows and tests support the primary features without unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/worktree-path-and-enabled-policy

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 force-pushed the feat/worktree-path-and-enabled-policy branch from 3246c23 to 7d054c8 Compare April 20, 2026 13:48
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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/docs-web/src/content/docs/reference/configuration.md (1)

83-87: ⚠️ Potential issue | 🟡 Minor

Remove or clarify the stale paths.worktrees example.

Line 86 still advertises ~/.archon/worktrees, but Line 182 says new worktrees default to the workspace-scoped layout. This conflicts with the PR’s removal of the legacy global worktree fallback and can mislead users about where new worktrees are created.

📝 Proposed docs adjustment
 # Custom paths (usually not needed)
 paths:
   workspaces: ~/.archon/workspaces
-  worktrees: ~/.archon/worktrees

Also applies to: 182-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs-web/src/content/docs/reference/configuration.md` around lines
83 - 87, The docs show a stale example for the paths.worktrees key (alongside
paths.workspaces); remove or clarify that paths.worktrees is legacy and no
longer used so it doesn't imply a global worktree location; instead update the
example to either omit paths.worktrees entirely or add a short note stating that
new worktrees default to the workspace-scoped layout (no global
~/.archon/worktrees fallback) so readers know where worktrees are actually
created.
packages/isolation/src/providers/worktree.ts (1)

726-730: ⚠️ Potential issue | 🟡 Minor

Preserve the single config-load boundary when config is absent.

Line 729 passes the already-loaded null config, but Line 825 treats null the same as “not provided” and calls loadConfig() again. That contradicts the new create-boundary invariant and can make absent config behave differently between the first and second read.

Proposed fix
   private async copyConfiguredFiles(
     canonicalRepoPath: string,
     worktreePath: string,
-    worktreeConfig?: { baseBranch?: string; copyFiles?: string[] } | null
+    worktreeConfig?: WorktreeCreateConfig | null
   ): Promise<{ configLoadFailed: boolean }> {
@@
-    if (worktreeConfig) {
-      userCopyFiles = worktreeConfig.copyFiles ?? [];
-    } else {
+    if (worktreeConfig !== undefined) {
+      userCopyFiles = worktreeConfig?.copyFiles ?? [];
+    } else {
       // Config not provided - try loading it
       try {
         const loadedConfig = await this.loadConfig(canonicalRepoPath);

Also applies to: 814-842

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/isolation/src/providers/worktree.ts` around lines 726 - 730, The
code currently treats a returned null worktreeConfig the same as “not provided”
and calls loadConfig() again later, breaking the create-boundary invariant;
update the logic in the caller (the block using copyConfiguredFiles and later
code that calls loadConfig()) to distinguish explicit null from undefined:
accept the returned worktreeConfig value as the authoritative result (including
null meaning “explicitly absent”) and only call loadConfig() when the config
variable is strictly undefined (i.e., not provided), ensuring functions
copyConfiguredFiles, configLoadFailed and subsequent code do not re-load when
worktreeConfig === null.
🧹 Nitpick comments (2)
packages/core/src/orchestrator/orchestrator-agent.ts (1)

237-242: Use the standard structured log event suffix.

workflow.worktree_disabled_by_policy does not follow the repo’s {domain}.{action}_{state} convention. A name like workflow.worktree_policy_validated keeps the policy audit event while matching the accepted state suffix.

🪵 Suggested event rename
     getLog().info(
       { workflowName: workflow.name, conversationId, codebaseId: codebase.id },
-      'workflow.worktree_disabled_by_policy'
+      'workflow.worktree_policy_validated'
     );

As per coding guidelines, use Pino structured logging with createLogger() from @archon/paths; follow event naming format {domain}.{action}_{state} (states: _started, _completed, _failed, _validated, _rejected); always pair _started with _completed or _failed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 237 - 242,
The structured log event string "workflow.worktree_disabled_by_policy" does not
follow the repo convention; update the getLog().info call in the
workflow.worktree?.enabled check to emit a standard `{domain}.{action}_{state}`
event (e.g. "workflow.worktree_policy_validated") while preserving the same
metadata ({ workflowName: workflow.name, conversationId, codebaseId: codebase.id
}) and behavior (setting cwd = codebase.default_cwd); ensure the new
`_validated` suffix is used for this policy-audit event and keep pairing
conventions elsewhere (use `_started`/`_completed` or `_failed` pairs where
applicable).
packages/workflows/src/loader.ts (1)

346-346: Avoid duplicating the workflow worktree shape.

Line 346 redefines the worktree policy shape locally. Use the existing schema-derived workflow contract so future worktree fields do not drift between the loader and schema.

♻️ Proposed refactor
-    let worktreePolicy: { enabled?: boolean } | undefined;
+    let worktreePolicy: WorkflowDefinition['worktree'] | undefined;

As per coding guidelines, "Always derive types from Zod schemas using z.infer<typeof schema> instead of writing parallel hand-crafted interfaces".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/loader.ts` at line 346, The local type declaration for
worktreePolicy (the variable worktreePolicy) duplicates the workflow worktree
shape; instead derive its type from the existing Zod workflow schema by
replacing the ad-hoc type with z.infer<typeof <WorkflowSchema>>['worktree'] (or
the exact exported schema name used in your codebase), import that schema where
loader.ts defines worktreePolicy, and use that inferred type so the loader stays
in sync with the schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.archon/workflows/e2e-worktree-disabled.yaml:
- Around line 22-31: The current checks only catch workspace-scoped worktrees
and treat any .git path as a real repo root; update the logic to also detect
repo-local worktrees by (1) extending the worktree detection to test if
"$cwd/.git" is a file whose contents start with "gitdir:" (indicating a gitfile
pointing to a worktree) or if the resolved git dir contains a "worktrees"
directory, and (2) fail when either case is true; replace or augment the
existing [ ! -e "$cwd/.git" ] check with a test that reads "$cwd/.git" and
rejects when it's a gitfile (or when git --git-dir="$cwd" rev-parse --git-dir
resolves outside the cwd), referencing the shell pattern matching and the
"$cwd/.git" existence check in the diff to locate where to change the code.

In @.archon/workflows/repo-triage.yaml:
- Around line 11-15: The comment claiming this workflow is “Read-only” is
inaccurate; update the comment near the worktree.enabled setting
(worktree.enabled: false) to clarify that while source files are not mutated,
the workflow does persist Archon state in the live checkout, writes artifacts,
and may interact with GitHub (comments/issue updates/closures); change the
wording to something like “does not mutate source files; state and artifacts are
intentionally persisted in the live checkout and the workflow may perform
GitHub-side actions” so readers aren’t misled.

In `@packages/cli/src/commands/workflow.test.ts`:
- Around line 881-917: The test incorrectly compares the .create call counts on
the last provider instance (providerBefore/providerAfter) which can false-pass
because getIsolationProvider() returns fresh providers; instead snapshot the
number of times getIsolationProvider was called: capture
getIsolationProviderMock.mock.calls.length before invoking workflowRunCommand
and assert it is unchanged after, replacing the providerBefore/providerAfter and
createCallsBefore/createCallsAfter checks; reference
getIsolationProvider/getIsolationProviderMock and workflowRunCommand to locate
where to change the assertion.

In `@packages/core/src/config/config-types.ts`:
- Around line 180-201: The config docs for worktree.path still reference
obsolete global fallbacks (e.g., global paths.worktrees and default
~/.archon/worktrees) — update the JSDoc above the worktree.path (the path?:
string field) to remove those fallback items and reflect the new precedence:
only repo-local worktree.path and workspace-scoped layouts remain; keep the
“must be a safe relative path” requirement and the example. Ensure the
precedence list and descriptive sentences mention only worktree.path
(repo-local) and the workspace-scoped behavior, and delete any mention of global
paths.worktrees or ~/.archon/worktrees.

In `@packages/git/src/worktree.ts`:
- Around line 65-70: The current workspace detection uses startsWith on repoPath
against workspacesPath (variables workspacesPath and repoPath in
getArchonWorkspacesPath usage) which misclassifies sibling directories; change
to compute the canonicalized relative path using
path.relative(path.resolve(workspacesPath), path.resolve(repoPath)) and only
treat it as a child when that relative value does not start with '..' and is not
equal to '' (i.e., ensure boundary containment), then split that relative path
into parts to derive owner/repo (update the logic inside the block that builds
relative and parts).

In `@packages/isolation/src/providers/worktree.test.ts`:
- Around line 2530-2540: The test currently checks only forward-slash `..`
escapes; update the test invoking provider.getWorktreePath (using
provider.generateBranchName and baseRequest) to also assert that Windows-style
backslash traversal patterns (e.g. '..\\worktrees', '..\\', and mixed
'nested\\..\\..\\escape') throw the same /must stay within the repo/ error so
backslash-based escaping is rejected on Windows while preserving Linux CI
behavior.

In `@packages/isolation/src/providers/worktree.ts`:
- Around line 94-100: The containment check using resolve(...) and
startsWith(repoRootResolved + '/') is platform- and symlink-unsafe; replace the
startsWith logic with path.relative(repoRootResolved, resolved) and ensure the
relative path does not begin with '..' (or is not equal to '..' segments) to
correctly validate containment on Windows and POSIX in the code around the
resolved/repoRootResolved variables in providers/worktree.ts; additionally,
after creating the worktree base (where the code creates the directory for the
worktree), call fs.realpath() (or fs.promises.realpath) on that created path and
verify its realpath is contained within the repo realpath (using path.relative
again) to detect and reject symlink escapes before using the directory.

In `@packages/isolation/src/types.ts`:
- Around line 251-263: Update the JSDoc for the `path?: string` field to clarify
precedence under the two-layout model: state that this repo-local relative path
overrides the workspace-scoped default (not a legacy global `~/.archon/...`
layout), and mention validation is still enforced in
`WorktreeProvider.getWorktreePath()`; keep the example and rules about safe
relative paths but replace “global defaults” wording with “workspace-scoped
default” (or equivalent) to avoid implying a removed global layout participates
in resolution.

In `@packages/workflows/src/schemas/workflow.ts`:
- Around line 39-48: The workflowWorktreePolicySchema currently allows unknown
keys (Zod's default strips extras); update the z.object definition for
workflowWorktreePolicySchema to reject unknown keys by appending .strict() to
the object schema so typos like "enable" or extra properties cause a validation
error; locate the workflowWorktreePolicySchema symbol in this file and change
its definition to use z.object({...}).strict() and run/adjust any callers/tests
that expect permissive parsing.

---

Outside diff comments:
In `@packages/docs-web/src/content/docs/reference/configuration.md`:
- Around line 83-87: The docs show a stale example for the paths.worktrees key
(alongside paths.workspaces); remove or clarify that paths.worktrees is legacy
and no longer used so it doesn't imply a global worktree location; instead
update the example to either omit paths.worktrees entirely or add a short note
stating that new worktrees default to the workspace-scoped layout (no global
~/.archon/worktrees fallback) so readers know where worktrees are actually
created.

In `@packages/isolation/src/providers/worktree.ts`:
- Around line 726-730: The code currently treats a returned null worktreeConfig
the same as “not provided” and calls loadConfig() again later, breaking the
create-boundary invariant; update the logic in the caller (the block using
copyConfiguredFiles and later code that calls loadConfig()) to distinguish
explicit null from undefined: accept the returned worktreeConfig value as the
authoritative result (including null meaning “explicitly absent”) and only call
loadConfig() when the config variable is strictly undefined (i.e., not
provided), ensuring functions copyConfiguredFiles, configLoadFailed and
subsequent code do not re-load when worktreeConfig === null.

---

Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 237-242: The structured log event string
"workflow.worktree_disabled_by_policy" does not follow the repo convention;
update the getLog().info call in the workflow.worktree?.enabled check to emit a
standard `{domain}.{action}_{state}` event (e.g.
"workflow.worktree_policy_validated") while preserving the same metadata ({
workflowName: workflow.name, conversationId, codebaseId: codebase.id }) and
behavior (setting cwd = codebase.default_cwd); ensure the new `_validated`
suffix is used for this policy-audit event and keep pairing conventions
elsewhere (use `_started`/`_completed` or `_failed` pairs where applicable).

In `@packages/workflows/src/loader.ts`:
- Line 346: The local type declaration for worktreePolicy (the variable
worktreePolicy) duplicates the workflow worktree shape; instead derive its type
from the existing Zod workflow schema by replacing the ad-hoc type with
z.infer<typeof <WorkflowSchema>>['worktree'] (or the exact exported schema name
used in your codebase), import that schema where loader.ts defines
worktreePolicy, and use that inferred type so the loader stays in sync with the
schema.
🪄 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: 44e6b46c-3ebc-4cdf-8202-c9a63d119150

📥 Commits

Reviewing files that changed from the base of the PR and between 39a05b7 and 7d054c8.

📒 Files selected for processing (19)
  • .archon/workflows/e2e-worktree-disabled.yaml
  • .archon/workflows/repo-triage.yaml
  • CHANGELOG.md
  • packages/cli/src/commands/workflow.test.ts
  • packages/cli/src/commands/workflow.ts
  • packages/core/src/config/config-types.ts
  • packages/core/src/orchestrator/orchestrator-agent.ts
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/docs-web/src/content/docs/reference/configuration.md
  • packages/git/src/git.test.ts
  • packages/git/src/index.ts
  • packages/git/src/worktree.ts
  • packages/isolation/src/factory.ts
  • packages/isolation/src/providers/worktree.test.ts
  • packages/isolation/src/providers/worktree.ts
  • packages/isolation/src/types.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/loader.ts
  • packages/workflows/src/schemas/workflow.ts

Comment on lines +22 to +31
case "$cwd" in
*/.archon/workspaces/*/worktrees/*)
echo "FAIL: workflow ran inside a worktree ($cwd) despite worktree.enabled: false"
exit 1
;;
esac
if [ ! -e "$cwd/.git" ]; then
echo "FAIL: cwd $cwd is not a git checkout root (.git missing)"
exit 1
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Detect repo-local worktrees too.

The current assertion only catches workspace-scoped paths. If policy enforcement regresses with worktree.path: .worktrees, this workflow can run inside a repo-local worktree and still pass because .git exists as a gitfile.

🧪 Proposed assertion hardening
       case "$cwd" in
         */.archon/workspaces/*/worktrees/*)
           echo "FAIL: workflow ran inside a worktree ($cwd) despite worktree.enabled: false"
           exit 1
           ;;
       esac
       if [ ! -e "$cwd/.git" ]; then
         echo "FAIL: cwd $cwd is not a git checkout root (.git missing)"
         exit 1
       fi
+      if [ -f "$cwd/.git" ] && grep -q '/worktrees/' "$cwd/.git"; then
+        echo "FAIL: workflow ran inside a git worktree ($cwd) despite worktree.enabled: false"
+        exit 1
+      fi
       echo "PASS: ran in live checkout (no worktree created by policy)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "$cwd" in
*/.archon/workspaces/*/worktrees/*)
echo "FAIL: workflow ran inside a worktree ($cwd) despite worktree.enabled: false"
exit 1
;;
esac
if [ ! -e "$cwd/.git" ]; then
echo "FAIL: cwd $cwd is not a git checkout root (.git missing)"
exit 1
fi
case "$cwd" in
*/.archon/workspaces/*/worktrees/*)
echo "FAIL: workflow ran inside a worktree ($cwd) despite worktree.enabled: false"
exit 1
;;
esac
if [ ! -e "$cwd/.git" ]; then
echo "FAIL: cwd $cwd is not a git checkout root (.git missing)"
exit 1
fi
if [ -f "$cwd/.git" ] && grep -q '/worktrees/' "$cwd/.git"; then
echo "FAIL: workflow ran inside a git worktree ($cwd) despite worktree.enabled: false"
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/workflows/e2e-worktree-disabled.yaml around lines 22 - 31, The
current checks only catch workspace-scoped worktrees and treat any .git path as
a real repo root; update the logic to also detect repo-local worktrees by (1)
extending the worktree detection to test if "$cwd/.git" is a file whose contents
start with "gitdir:" (indicating a gitfile pointing to a worktree) or if the
resolved git dir contains a "worktrees" directory, and (2) fail when either case
is true; replace or augment the existing [ ! -e "$cwd/.git" ] check with a test
that reads "$cwd/.git" and rejects when it's a gitfile (or when git
--git-dir="$cwd" rev-parse --git-dir resolves outside the cwd), referencing the
shell pattern matching and the "$cwd/.git" existence check in the diff to locate
where to change the code.

Comment on lines +11 to +15
# Read-only triage runs directly in the live checkout. Creating a worktree
# every run would be wasted work (nothing is mutated) and would scatter stale
# branches under ~/.archon/workspaces/<owner>/<repo>/worktrees/.
worktree:
enabled: false
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify that this workflow still mutates state and GitHub.

worktree.enabled: false is fine here, but the comment says this is “Read-only” and that “nothing is mutated.” The workflow writes .archon/state/*, writes artifacts, and can comment/close issues. Consider narrowing the claim to “does not mutate source files” and call out that state is intentionally persisted in the live checkout.

📝 Suggested wording
-# Read-only triage runs directly in the live checkout. Creating a worktree
-# every run would be wasted work (nothing is mutated) and would scatter stale
-# branches under ~/.archon/workspaces/<owner>/<repo>/worktrees/.
+# Repo maintenance runs directly in the live checkout: it does not modify source
+# files, but it intentionally persists `.archon/state/*` and may comment/close
+# issues via GitHub. Creating a worktree every run would hide that state and
+# scatter stale branches under ~/.archon/workspaces/<owner>/<repo>/worktrees/.
 worktree:
   enabled: false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.archon/workflows/repo-triage.yaml around lines 11 - 15, The comment
claiming this workflow is “Read-only” is inaccurate; update the comment near the
worktree.enabled setting (worktree.enabled: false) to clarify that while source
files are not mutated, the workflow does persist Archon state in the live
checkout, writes artifacts, and may interact with GitHub (comments/issue
updates/closures); change the wording to something like “does not mutate source
files; state and artifacts are intentionally persisted in the live checkout and
the workflow may perform GitHub-side actions” so readers aren’t misled.

Comment on lines +881 to +917
const getIsolationProviderMock = isolation.getIsolationProvider as ReturnType<typeof mock>;
const providerBefore = getIsolationProviderMock.mock.results.at(-1)?.value as
| { create: ReturnType<typeof mock> }
| undefined;
const createCallsBefore = providerBefore?.create.mock.calls.length ?? 0;

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'triage',
description: 'Read-only triage',
worktree: { enabled: false },
}),
],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'cb-123',
default_cwd: '/test/path',
});
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
success: true,
workflowRunId: 'run-123',
});

// No flags — policy alone should disable isolation
await workflowRunCommand('/test/path', 'triage', 'go', {});

const providerAfter = getIsolationProviderMock.mock.results.at(-1)?.value as
| { create: ReturnType<typeof mock> }
| undefined;
const createCallsAfter = providerAfter?.create.mock.calls.length ?? 0;
expect(createCallsAfter).toBe(createCallsBefore);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the provider boundary is not entered.

This compares create calls on whichever provider object was last returned by a process-global mock. Because getIsolationProvider() returns a fresh provider each call, this can false-pass when previous tests leave matching call counts. Snapshot getIsolationProvider calls instead.

🧪 Proposed test hardening
     const getIsolationProviderMock = isolation.getIsolationProvider as ReturnType<typeof mock>;
-    const providerBefore = getIsolationProviderMock.mock.results.at(-1)?.value as
-      | { create: ReturnType<typeof mock> }
-      | undefined;
-    const createCallsBefore = providerBefore?.create.mock.calls.length ?? 0;
+    const providerCallsBefore = getIsolationProviderMock.mock.calls.length;
 
     (discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
       workflows: [
@@
     // No flags — policy alone should disable isolation
     await workflowRunCommand('/test/path', 'triage', 'go', {});
 
-    const providerAfter = getIsolationProviderMock.mock.results.at(-1)?.value as
-      | { create: ReturnType<typeof mock> }
-      | undefined;
-    const createCallsAfter = providerAfter?.create.mock.calls.length ?? 0;
-    expect(createCallsAfter).toBe(createCallsBefore);
+    expect(getIsolationProviderMock.mock.calls.length).toBe(providerCallsBefore);

As per coding guidelines, "Prefer reproducible commands and locked dependency behavior in CI-sensitive paths; keep tests deterministic with no flaky timing or network dependence without guardrails".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getIsolationProviderMock = isolation.getIsolationProvider as ReturnType<typeof mock>;
const providerBefore = getIsolationProviderMock.mock.results.at(-1)?.value as
| { create: ReturnType<typeof mock> }
| undefined;
const createCallsBefore = providerBefore?.create.mock.calls.length ?? 0;
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'triage',
description: 'Read-only triage',
worktree: { enabled: false },
}),
],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'cb-123',
default_cwd: '/test/path',
});
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
success: true,
workflowRunId: 'run-123',
});
// No flags — policy alone should disable isolation
await workflowRunCommand('/test/path', 'triage', 'go', {});
const providerAfter = getIsolationProviderMock.mock.results.at(-1)?.value as
| { create: ReturnType<typeof mock> }
| undefined;
const createCallsAfter = providerAfter?.create.mock.calls.length ?? 0;
expect(createCallsAfter).toBe(createCallsBefore);
const getIsolationProviderMock = isolation.getIsolationProvider as ReturnType<typeof mock>;
const providerCallsBefore = getIsolationProviderMock.mock.calls.length;
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'triage',
description: 'Read-only triage',
worktree: { enabled: false },
}),
],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'cb-123',
default_cwd: '/test/path',
});
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
success: true,
workflowRunId: 'run-123',
});
// No flags — policy alone should disable isolation
await workflowRunCommand('/test/path', 'triage', 'go', {});
expect(getIsolationProviderMock.mock.calls.length).toBe(providerCallsBefore);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/workflow.test.ts` around lines 881 - 917, The test
incorrectly compares the .create call counts on the last provider instance
(providerBefore/providerAfter) which can false-pass because
getIsolationProvider() returns fresh providers; instead snapshot the number of
times getIsolationProvider was called: capture
getIsolationProviderMock.mock.calls.length before invoking workflowRunCommand
and assert it is unchanged after, replacing the providerBefore/providerAfter and
createCallsBefore/createCallsAfter checks; reference
getIsolationProvider/getIsolationProviderMock and workflowRunCommand to locate
where to change the assertion.

Comment on lines +180 to +201
/**
* Per-project worktree directory (relative to repo root). When set,
* worktrees are created at `<repoRoot>/<path>/<branch>` instead of under
* `~/.archon/worktrees/` or the workspaces layout.
*
* Opt-in — co-locates worktrees with the repo so they appear in the IDE
* file tree. The user is responsible for adding the directory to their
* `.gitignore` (no automatic file mutation).
*
* Path resolution precedence (highest to lowest):
* 1. this `worktree.path` (repo-local)
* 2. global `paths.worktrees` (absolute override in `~/.archon/config.yaml`)
* 3. auto-detected project-scoped (`~/.archon/workspaces/owner/repo/...`)
* 4. default global (`~/.archon/worktrees/`)
*
* Must be a safe relative path: no leading `/`, no `..` segments. Absolute
* or escaping values fail loudly at worktree creation (Fail Fast — no silent
* fallback).
*
* @example '.worktrees'
*/
path?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove the obsolete global worktree precedence from this public config doc.

This comment still documents global paths.worktrees and the default ~/.archon/worktrees/ layout, but the PR summary says those fallback layouts were removed. Public type docs should describe only worktree.path → repo-local, otherwise workspace-scoped.

📝 Suggested wording
-     * worktrees are created at `<repoRoot>/<path>/<branch>` instead of under
-     * `~/.archon/worktrees/` or the workspaces layout.
+     * worktrees are created at `<repoRoot>/<path>/<branch>` instead of under
+     * the workspace-scoped layout.

@@
-     * Path resolution precedence (highest to lowest):
-     *   1. this `worktree.path` (repo-local)
-     *   2. global `paths.worktrees` (absolute override in `~/.archon/config.yaml`)
-     *   3. auto-detected project-scoped (`~/.archon/workspaces/owner/repo/...`)
-     *   4. default global (`~/.archon/worktrees/`)
+     * Path resolution precedence:
+     *   1. this `worktree.path` (repo-local)
+     *   2. workspace-scoped (`~/.archon/workspaces/<owner>/<repo>/worktrees/`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Per-project worktree directory (relative to repo root). When set,
* worktrees are created at `<repoRoot>/<path>/<branch>` instead of under
* `~/.archon/worktrees/` or the workspaces layout.
*
* Opt-in co-locates worktrees with the repo so they appear in the IDE
* file tree. The user is responsible for adding the directory to their
* `.gitignore` (no automatic file mutation).
*
* Path resolution precedence (highest to lowest):
* 1. this `worktree.path` (repo-local)
* 2. global `paths.worktrees` (absolute override in `~/.archon/config.yaml`)
* 3. auto-detected project-scoped (`~/.archon/workspaces/owner/repo/...`)
* 4. default global (`~/.archon/worktrees/`)
*
* Must be a safe relative path: no leading `/`, no `..` segments. Absolute
* or escaping values fail loudly at worktree creation (Fail Fast no silent
* fallback).
*
* @example '.worktrees'
*/
path?: string;
/**
* Per-project worktree directory (relative to repo root). When set,
* worktrees are created at `<repoRoot>/<path>/<branch>` instead of under
* the workspace-scoped layout.
*
* Opt-in co-locates worktrees with the repo so they appear in the IDE
* file tree. The user is responsible for adding the directory to their
* `.gitignore` (no automatic file mutation).
*
* Path resolution precedence:
* 1. this `worktree.path` (repo-local)
* 2. workspace-scoped (`~/.archon/workspaces/<owner>/<repo>/worktrees/`)
*
* Must be a safe relative path: no leading `/`, no `..` segments. Absolute
* or escaping values fail loudly at worktree creation (Fail Fast no silent
* fallback).
*
* `@example` '.worktrees'
*/
path?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/config/config-types.ts` around lines 180 - 201, The config
docs for worktree.path still reference obsolete global fallbacks (e.g., global
paths.worktrees and default ~/.archon/worktrees) — update the JSDoc above the
worktree.path (the path?: string field) to remove those fallback items and
reflect the new precedence: only repo-local worktree.path and workspace-scoped
layouts remain; keep the “must be a safe relative path” requirement and the
example. Ensure the precedence list and descriptive sentences mention only
worktree.path (repo-local) and the workspace-scoped behavior, and delete any
mention of global paths.worktrees or ~/.archon/worktrees.

Comment on lines 65 to +70
const workspacesPath = getArchonWorkspacesPath();
if (repoPath.startsWith(workspacesPath)) {
const relative = repoPath.substring(workspacesPath.length + 1);
const parts = relative.split(/[/\\]/).filter(p => p.length > 0);
if (parts.length >= 2) {
return getProjectWorktreesPath(parts[0], parts[1]);
return { owner: parts[0], repo: parts[1] };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node - <<'NODE'
const workspacesPath = '/home/me/.archon/workspaces';
const repoPath = '/home/me/.archon/workspaces-backup/acme/widget/source';

const relative = repoPath.substring(workspacesPath.length + 1);
const parts = relative.split(/[/\\]/).filter(Boolean);

console.log({ startsWith: repoPath.startsWith(workspacesPath), derived: parts.slice(0, 2) });
// Expected with current logic: startsWith is true even though repoPath is a sibling, not a child.
NODE

Repository: coleam00/Archon

Length of output: 110


🏁 Script executed:

cat -n packages/git/src/worktree.ts | sed -n '50,80p'

Repository: coleam00/Archon

Length of output: 1491


🏁 Script executed:

cat -n packages/git/src/worktree.ts | head -100

Repository: coleam00/Archon

Length of output: 4873


Use path-boundary containment for workspace detection.

Line 66 uses repoPath.startsWith(workspacesPath) without boundary checking, causing sibling paths such as ~/.archon/workspaces-backup/acme/widget/source to be incorrectly classified as workspace children and derive wrong owner/repo values (e.g., { owner: 'backup', repo: 'acme' } instead of the actual path segments). Use relative(resolve(workspacesPath), resolve(repoPath)) with a boundary check instead.

Proposed fix
-import { join, resolve } from 'path';
+import { isAbsolute, join, relative, resolve, sep } from 'path';
@@
-  const workspacesPath = getArchonWorkspacesPath();
-  if (repoPath.startsWith(workspacesPath)) {
-    const relative = repoPath.substring(workspacesPath.length + 1);
-    const parts = relative.split(/[/\\]/).filter(p => p.length > 0);
+  const workspacesPath = resolve(getArchonWorkspacesPath());
+  const resolvedRepoPath = resolve(repoPath);
+  const relativeToWorkspaces = relative(workspacesPath, resolvedRepoPath);
+  if (
+    relativeToWorkspaces &&
+    relativeToWorkspaces !== '..' &&
+    !relativeToWorkspaces.startsWith(`..${sep}`) &&
+    !isAbsolute(relativeToWorkspaces)
+  ) {
+    const parts = relativeToWorkspaces.split(/[/\\]/).filter(p => p.length > 0);
     if (parts.length >= 2) {
       return { owner: parts[0], repo: parts[1] };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/git/src/worktree.ts` around lines 65 - 70, The current workspace
detection uses startsWith on repoPath against workspacesPath (variables
workspacesPath and repoPath in getArchonWorkspacesPath usage) which
misclassifies sibling directories; change to compute the canonicalized relative
path using path.relative(path.resolve(workspacesPath), path.resolve(repoPath))
and only treat it as a child when that relative value does not start with '..'
and is not equal to '' (i.e., ensure boundary containment), then split that
relative path into parts to derive owner/repo (update the logic inside the block
that builds relative and parts).

Comment on lines +2530 to +2540
test('rejects a worktree.path that escapes the repo root via `..`', () => {
const branch = provider.generateBranchName(baseRequest);
expect(() => provider.getWorktreePath(baseRequest, branch, { path: '../worktrees' })).toThrow(
/must stay within the repo/
);
expect(() => provider.getWorktreePath(baseRequest, branch, { path: '..' })).toThrow(
/must stay within the repo/
);
expect(() =>
provider.getWorktreePath(baseRequest, branch, { path: 'nested/../../escape' })
).toThrow(/must stay within the repo/);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cover backslash traversal in worktree.path validation.

These tests only cover /-separated .. escapes. Since this suite already validates Windows-style path handling, add ..\\... cases so repo-local worktrees cannot escape the repo root on Windows while Linux CI still passes.

🧪 Proposed additional cases
       expect(() =>
         provider.getWorktreePath(baseRequest, branch, { path: 'nested/../../escape' })
       ).toThrow(/must stay within the repo/);
+      expect(() =>
+        provider.getWorktreePath(baseRequest, branch, { path: '..\\worktrees' })
+      ).toThrow(/must stay within the repo/);
+      expect(() =>
+        provider.getWorktreePath(baseRequest, branch, { path: 'nested\\..\\..\\escape' })
+      ).toThrow(/must stay within the repo/);
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('rejects a worktree.path that escapes the repo root via `..`', () => {
const branch = provider.generateBranchName(baseRequest);
expect(() => provider.getWorktreePath(baseRequest, branch, { path: '../worktrees' })).toThrow(
/must stay within the repo/
);
expect(() => provider.getWorktreePath(baseRequest, branch, { path: '..' })).toThrow(
/must stay within the repo/
);
expect(() =>
provider.getWorktreePath(baseRequest, branch, { path: 'nested/../../escape' })
).toThrow(/must stay within the repo/);
test('rejects a worktree.path that escapes the repo root via `..`', () => {
const branch = provider.generateBranchName(baseRequest);
expect(() => provider.getWorktreePath(baseRequest, branch, { path: '../worktrees' })).toThrow(
/must stay within the repo/
);
expect(() => provider.getWorktreePath(baseRequest, branch, { path: '..' })).toThrow(
/must stay within the repo/
);
expect(() =>
provider.getWorktreePath(baseRequest, branch, { path: 'nested/../../escape' })
).toThrow(/must stay within the repo/);
expect(() =>
provider.getWorktreePath(baseRequest, branch, { path: '..\\worktrees' })
).toThrow(/must stay within the repo/);
expect(() =>
provider.getWorktreePath(baseRequest, branch, { path: 'nested\\..\\..\\escape' })
).toThrow(/must stay within the repo/);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/isolation/src/providers/worktree.test.ts` around lines 2530 - 2540,
The test currently checks only forward-slash `..` escapes; update the test
invoking provider.getWorktreePath (using provider.generateBranchName and
baseRequest) to also assert that Windows-style backslash traversal patterns
(e.g. '..\\worktrees', '..\\', and mixed 'nested\\..\\..\\escape') throw the
same /must stay within the repo/ error so backslash-based escaping is rejected
on Windows while preserving Linux CI behavior.

Comment thread packages/isolation/src/providers/worktree.ts
Comment on lines +251 to +263
/**
* Per-project relative path (from repo root) where worktrees should be created.
* When set, worktrees live at `<repoRoot>/<path>/<branch>` with `repo-local` layout.
* Highest priority in path resolution — overrides project-scoped and global defaults.
*
* Must be a safe relative path: no leading `/`, no `..` segments, non-empty after trim.
* Validation is enforced in `WorktreeProvider.getWorktreePath()` (fails fast with a
* clear error rather than silently falling back).
*
* Sourced from `.archon/config.yaml > worktree.path` in the repo.
* @example '.worktrees'
*/
path?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the precedence wording to match the two-layout model.

The new path field is correct, but “global defaults” reads like the removed legacy ~/.archon/worktrees/... layout still participates in resolution. This should describe only repo-local overriding the workspace-scoped default.

📝 Suggested wording
-   * Highest priority in path resolution — overrides project-scoped and global defaults.
+   * Highest priority in path resolution — overrides the workspace-scoped default.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Per-project relative path (from repo root) where worktrees should be created.
* When set, worktrees live at `<repoRoot>/<path>/<branch>` with `repo-local` layout.
* Highest priority in path resolution overrides project-scoped and global defaults.
*
* Must be a safe relative path: no leading `/`, no `..` segments, non-empty after trim.
* Validation is enforced in `WorktreeProvider.getWorktreePath()` (fails fast with a
* clear error rather than silently falling back).
*
* Sourced from `.archon/config.yaml > worktree.path` in the repo.
* @example '.worktrees'
*/
path?: string;
/**
* Per-project relative path (from repo root) where worktrees should be created.
* When set, worktrees live at `<repoRoot>/<path>/<branch>` with `repo-local` layout.
* Highest priority in path resolution overrides the workspace-scoped default.
*
* Must be a safe relative path: no leading `/`, no `..` segments, non-empty after trim.
* Validation is enforced in `WorktreeProvider.getWorktreePath()` (fails fast with a
* clear error rather than silently falling back).
*
* Sourced from `.archon/config.yaml > worktree.path` in the repo.
* `@example` '.worktrees'
*/
path?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/isolation/src/types.ts` around lines 251 - 263, Update the JSDoc for
the `path?: string` field to clarify precedence under the two-layout model:
state that this repo-local relative path overrides the workspace-scoped default
(not a legacy global `~/.archon/...` layout), and mention validation is still
enforced in `WorktreeProvider.getWorktreePath()`; keep the example and rules
about safe relative paths but replace “global defaults” wording with
“workspace-scoped default” (or equivalent) to avoid implying a removed global
layout participates in resolution.

Comment on lines +39 to +48
export const workflowWorktreePolicySchema = z.object({
/**
* Pin worktree isolation on or off for this workflow.
* - `true` — always run inside a worktree; CLI `--no-worktree` hard-errors
* - `false` — always run in the live checkout; CLI `--branch` / `--from`
* hard-error, orchestrator skips isolation resolution
* - omitted — caller decides (current default = worktree for most types)
*/
enabled: z.boolean().optional(),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Zod object .strict unknown keys behavior

💡 Result:

In Zod, object schemas have three behaviors for unknown (extra) keys by default, .passthrough, .strict, and the default .strip: - Default z.object({}): Strips unknown keys from the output, ignoring them silently. Example: z.object({name: z.string}).parse({name: 'test', extra: true}) returns {name: 'test'}. - .strict or z.strictObject({}): Throws a ZodError with "unrecognized_keys" if any unknown keys are present in the input. Example: z.strictObject({name: z.string}).parse({name: 'test', extra: true}) throws an error listing the extra keys. - .passthrough or z.looseObject({}): Allows unknown keys to pass through to the output unchanged. This behavior is consistent across Zod v3 and v4 (current as of 2026). In v4, use z.strictObject(shape) directly instead of chaining .strict on z.object, though both work. Strict mode is shallow (top-level keys only); nested objects use their own rules. Example code: import { z } from 'zod'; const strictSchema = z.strictObject({ name: z.string, }); strictSchema.parse({ name: 'Alice' }); // OK strictSchema.parse({ name: 'Alice', age: 30 }); // Throws: Unrecognized key(s) in object: 'age'

Citations:


🏁 Script executed:

cat -n packages/workflows/src/schemas/workflow.ts | head -80

Repository: coleam00/Archon

Length of output: 3969


🏁 Script executed:

rg "workflowWorktreePolicySchema" -A 5 -B 5 packages/workflows/src/

Repository: coleam00/Archon

Length of output: 3017


🏁 Script executed:

rg "\.strict\(\)" packages/workflows/src/schemas/

Repository: coleam00/Archon

Length of output: 230


Add .strict() to reject unknown worktree policy keys.

The schema at lines 39–48 uses the default Zod object behavior, which silently strips unknown keys. This means a typo like worktree: { enable: true } parses as an empty policy {} and falls back to caller defaults, defeating the workflow's explicit isolation intent.

Per the Fail Fast + Explicit Errors guideline, unknown keys should be rejected:

Proposed fix
 export const workflowWorktreePolicySchema = z.object({
   /**
    * Pin worktree isolation on or off for this workflow.
@@
    */
   enabled: z.boolean().optional(),
-});
+}).strict();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/schemas/workflow.ts` around lines 39 - 48, The
workflowWorktreePolicySchema currently allows unknown keys (Zod's default strips
extras); update the z.object definition for workflowWorktreePolicySchema to
reject unknown keys by appending .strict() to the object schema so typos like
"enable" or extra properties cause a validation error; locate the
workflowWorktreePolicySchema symbol in this file and change its definition to
use z.object({...}).strict() and run/adjust any callers/tests that expect
permissive parsing.

@Wirasm
Copy link
Copy Markdown
Collaborator Author

Wirasm commented Apr 20, 2026

PR Review Summary (multi-agent)

Ran 7 specialist agents: code-reviewer, docs-impact, pr-test-analyzer, silent-failure-hunter, type-design-analyzer, comment-analyzer, code-simplifier. Aggregated below.

Critical Issues (1)

Agent Issue Location
code-reviewer resolveRepoLocalOverride hardcodes + '/' in the escape check. On Windows path.resolve() uses \, so repoRootResolved + '/' can never prefix a resolved path — every valid worktree.path throws on Windows. Use path.sep. packages/isolation/src/providers/worktree.ts:98

Important Issues (5)

Agent Issue Location
code-reviewer / comment-analyzer / simplifier / docs-impact Newly-added JSDoc lists a 4-tier precedence (worktree.pathpaths.worktrees → workspace-scoped → ~/.archon/worktrees/). Steps 2 and 4 are fictional on this PR — getWorktreeBase() no longer consumes paths.worktrees, and the legacy global is removed. Replace with the real 2-tier list. packages/core/src/config/config-types.ts (RepoConfig.worktree.path)
pr-test-analyzer Orchestrator short-circuit for worktree.enabled === false has zero unit tests. This is the only enforcement point on web / Slack / Telegram / GitHub — a refactor that flips === false to !enabled or moves the guard regresses silently. CLI e2e doesn't cover the orchestrator path. packages/core/src/orchestrator/orchestrator-agent.ts:237
silent-failure-hunter / pr-test-analyzer --resume silently bypasses worktree.enabled: true. The conflict-check block only rejects enabled:true + --no-worktree; enabled:true + --resume falls through. The PR body documents resume-ignores-policy for enabled:false only; enabled:true is not. No regression guard test either. packages/cli/src/commands/workflow.ts:264-294, 437-444
silent-failure-hunter Typo'd keys in the worktree: block are silently swallowed (e.g. enabeld: false). The loader extracts only enabled by name — no unknown-key warning. For a security-adjacent property, this violates the Fail-Fast guidance in CLAUDE.md. Emit unknown_worktree_keys_ignored warn with the bad keys. packages/workflows/src/loader.ts:346-362
docs-impact Stale references to the removed legacy layout across three docs: (a) architecture.md:637-646 still lists LEGACY: ~/.archon/worktrees/... as a named layout with numbered resolution, (b) archon-directories.md:33,42,87-89 lists the legacy dir as active with "Legacy fallback" label, (c) CHANGELOG entry doesn't mention the legacy removal or the migration nuance for locally-registered repos. packages/docs-web/src/content/docs/reference/architecture.md, archon-directories.md, CHANGELOG.md

Suggestions (14)

Agent Suggestion Location
simplifier Inline flagWantsIsolation; !options.resume && (pinnedEnabled ?? !options.noWorktree) is equivalent and drops the redundant !resume term. packages/cli/src/commands/workflow.ts:434-445
type-design / simplifier Loader bypasses workflowWorktreePolicySchema and re-implements the shape check manually. Call safeParse to keep the schema as the single parse authority (matches modelReasoningEffortSchema pattern in the same file). packages/workflows/src/loader.ts:346-362
pr-test-analyzer Missing tests for non-boolean worktree.enabled ("false", 1) — documented warn-and-ignore path has no coverage. interactive has parallel precedent. packages/workflows/src/loader.test.ts
pr-test-analyzer Path-traversal edge cases not covered: ./foo, bare . (would put worktrees at repo root), trailing slash. packages/isolation/src/providers/worktree.test.ts
pr-test-analyzer Interaction untested: worktree.path set in config + workflow worktree.enabled: false — path should be cleanly ignored on short-circuit. n/a (add test)
pr-test-analyzer mkdirAsync call with repo-local base not asserted by any create() test — a wrong-base regression would pass existing mocks. packages/isolation/src/providers/worktree.test.ts
silent-failure-hunter copyConfiguredFiles treats a valid null worktreeConfig as "not provided" and triggers a redundant second loadConfig(). PR's "single config load" comment is inaccurate for this branch. Use !== undefined check. packages/isolation/src/providers/worktree.ts:828-841
silent-failure-hunter workflow.worktree_disabled_by_policy log missing workflowRunId for correlation (minor observability gap — runId doesn't exist yet at that point, consider threading later). packages/core/src/orchestrator/orchestrator-agent.ts:238-241
type-design Consider logging layout at the resolution point in WorktreeProvider — it's currently destructured away at both call sites. packages/isolation/src/providers/worktree.ts:587
type-design RepoConfig.worktree and WorktreeCreateConfig have parallel fields bridged by structural typing. A _AssertCompat compile-time check would catch future drift. packages/core/src/config/config-types.ts, packages/isolation/src/types.ts
comment-analyzer Test section header // Per-repo ... — #1117 successor will rot; drop the issue reference. packages/isolation/src/providers/worktree.test.ts:838
comment-analyzer // Create new worktree (re-uses the already-loaded repoConfig — no double load) is partially inaccurate — resolveRepoLocalOverride is still called twice per create. Trim to "re-uses the already-loaded repoConfig". packages/isolation/src/providers/worktree.ts:1059-1060
docs-impact Workflow authoring guide missing worktree.enabled in the schema YAML example and summary bullet list. Reference page missing worktree.path entry and the "Worktree path behavior" prose paragraph. packages/docs-web/src/content/docs/guides/authoring-workflows.md, reference/configuration.md
simplifier Tautological tests: isProjectScopedWorktreeBase is deprecated and always returns true under the new model — multi-case tests verify a constant. Collapse to one assertion. packages/git/src/git.test.ts

Strengths

  • Return-shape migration of getWorktreeBase(){ base, layout } is complete across every call site.
  • resolveRepoLocalOverride validation is robust on POSIX (rejects absolute, .. prefix, foo/../.. escape) with actionable error messages.
  • Fail-fast discipline holds for config load + path validation (no swallow-and-retry remains in the create() boundary).
  • CLI reconciliation covers all five flag/policy combinations, including the non-error enabled:false + --no-worktree redundant accept.
  • verifyWorktreeOwnership cross-clone guard on both adoption paths is a solid defensive addition.
  • Type derivation via z.infer is consistent — no parallel hand-crafted interfaces introduced.
  • Short-circuit is observable via structured workflow.worktree_disabled_by_policy log event.
  • WorktreeLayout string union + paired { base, layout } return prevents mismatched base/layout computation by construction.

Verdict

NEEDS FIXES — one cross-platform correctness bug blocks merge on Windows; four important items (stale JSDoc, untested orchestrator short-circuit, undocumented resume bypass, silent typo swallow) should be addressed before merge. Everything else is follow-up-grade.

Recommended Actions

  1. Fix Windows path separator in resolveRepoLocalOverride (+ '/'+ sep).
  2. Correct the 4-tier precedence docblock in config-types.ts to match the two-layout reality.
  3. Add orchestrator unit test for worktree.enabled: false bypass.
  4. Either reject or document+log the enabled:true + --resume case; add a regression-guard test.
  5. Emit unknown_worktree_keys_ignored warn in the loader's worktree block.
  6. Clean up the three stale docs pages (architecture, archon-directories, CHANGELOG nuance).
  7. Consider the schema-safeParse refactor and the flagWantsIsolation inline as polish passes.

Wirasm and others added 3 commits April 20, 2026 21:47
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>
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.
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`.
@Wirasm Wirasm force-pushed the feat/worktree-path-and-enabled-policy branch from 0bb458b to cd5c465 Compare April 20, 2026 18:48
@Wirasm Wirasm merged commit 5ed38dc into dev Apr 20, 2026
3 of 4 checks passed
@Wirasm Wirasm deleted the feat/worktree-path-and-enabled-policy branch April 20, 2026 18:54
Wirasm added a commit that referenced this pull request Apr 22, 2026
)

The two "Smoke-test Claude binary-path resolver" steps in
.github/workflows/release.yml run `archon workflow run archon-assist
"hello"` against a fresh `git init` temp repo with no origin. As of
#1310's worktree-policy changes, default isolation auto-syncs the
worktree with origin before creating it, which fails with
"neither origin/HEAD nor origin/main exist" — hit before Claude's
resolver is even reached, so the test assertions ("Claude Code not
found", "CLAUDE_BIN_PATH") never match and the linux-x64 build
aborts the whole release matrix.

The tests exercise the Claude resolver path, not worktree setup, so
--no-worktree is the correct fix: it short-circuits
validateAndResolveIsolation and skips the origin sync entirely.
Matches the documented usage in CLAUDE.md
(`archon workflow run quick-fix --no-worktree "Fix typo"`).

Surfaced while cutting v0.3.8 — the release CI failed deterministically
on both builds. Binaries themselves are fine (the v0.3.7 Pi-lazy-load
fix works; local pre-flight passed on --help). v0.3.8's GitHub Release
has been deleted so `releases/latest` falls back to v0.3.6; next
release will be v0.3.9 with this fix.
@Wirasm Wirasm mentioned this pull request Apr 22, 2026
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.

1 participant