feat: per-project worktree.path config option#1117
feat: per-project worktree.path config option#1117joelsb wants to merge 3 commits intocoleam00:devfrom
Conversation
Allows repositories to configure a custom worktree directory relative to the repo root (e.g. `.worktrees`) via `.archon/config.yaml`: ```yaml worktree: path: .worktrees ``` When set, worktrees are created at `<repoRoot>/<path>/<branchName>` instead of the global `~/.archon/worktrees/` directory. This keeps worktrees co-located with the project and visible in the IDE. The per-project path takes highest priority, overriding both the project-scoped workspaces path and the legacy global path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds per-repository worktree directory support via a new optional Changes
Sequence Diagram(s)sequenceDiagram
participant Caller as Isolation Request
participant Provider as WorktreeProvider
participant Config as Config Loader
participant FS as FileSystem
participant Git as Worktree Tool
Caller->>Provider: create(request)
Provider->>Config: loadConfig(canonicalRepoPath)
Config-->>Provider: WorktreeCreateConfig | null
Provider->>Provider: getWorktreePath(request, branchName, config)
alt config.path exists
Note right of Provider: use <repoRoot>/<config.path>/<branch>
else
Note right of Provider: fallback to project-scoped or legacy path
end
Provider->>Provider: worktreePath
Provider->>Provider: createWorktree(request, worktreePath, branchName, preloadedConfig)
alt config.path is set
Provider->>FS: mkdirAsync(join(repoRoot, config.path))
else
Provider->>FS: ensure base-layout directories
end
FS-->>Provider: dirs ready
Provider->>Git: create worktree at worktreePath
Git-->>Provider: warnings[]
Provider-->>Caller: { warnings }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Exposes the per-project worktree.path through MergedConfig so tools, the web UI, and API endpoints can read and display both the global paths.worktrees and the per-project worktreePath override. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/config/config-loader.ts`:
- Around line 389-395: The code calls repo.worktree.path.trim() after checking
!== undefined which will throw if path is non-string (e.g., null); update the
branch in config-loader.ts to first assert the type is string (e.g., typeof
repo.worktree.path === 'string') before calling .trim(), set result.worktreePath
when trimmed is non-empty, and otherwise either log the whitespace-warning using
getLog().warn or throw a clear error for unsupported non-string values (per
guidelines) so the unsafe state is rejected explicitly; reference symbols:
repo.worktree.path, result.worktreePath, and getLog().warn.
🪄 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: ec9164cc-04c1-40ff-be9f-4d9ec639025e
📒 Files selected for processing (2)
packages/core/src/config/config-loader.tspackages/core/src/config/config-types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/core/src/config/config-types.ts
| if (repo.worktree?.path !== undefined) { | ||
| const trimmed = repo.worktree.path.trim(); | ||
| if (trimmed) { | ||
| result.worktreePath = trimmed; | ||
| } else { | ||
| getLog().warn({ rawValue: repo.worktree.path }, 'config.worktree_path_whitespace_ignored'); | ||
| } |
There was a problem hiding this comment.
Guard repo.worktree.path type before calling .trim().
This branch can throw if YAML contains a non-string value (e.g., path: null), because !== undefined still passes and .trim() is invoked on a non-string.
Suggested fix
// Propagate per-project worktree path for isolation providers
if (repo.worktree?.path !== undefined) {
- const trimmed = repo.worktree.path.trim();
+ if (typeof repo.worktree.path !== 'string') {
+ throw new Error('Invalid .archon/config.yaml: worktree.path must be a string');
+ }
+ const trimmed = repo.worktree.path.trim();
if (trimmed) {
result.worktreePath = trimmed;
} else {
getLog().warn({ rawValue: repo.worktree.path }, 'config.worktree_path_whitespace_ignored');
}
}As per coding guidelines, “Throw early with a clear error for unsupported or unsafe states” and “keep unsupported paths explicit (error out) rather than adding partial fake support.”
📝 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.
| if (repo.worktree?.path !== undefined) { | |
| const trimmed = repo.worktree.path.trim(); | |
| if (trimmed) { | |
| result.worktreePath = trimmed; | |
| } else { | |
| getLog().warn({ rawValue: repo.worktree.path }, 'config.worktree_path_whitespace_ignored'); | |
| } | |
| if (repo.worktree?.path !== undefined) { | |
| if (typeof repo.worktree.path !== 'string') { | |
| throw new Error('Invalid .archon/config.yaml: worktree.path must be a string'); | |
| } | |
| const trimmed = repo.worktree.path.trim(); | |
| if (trimmed) { | |
| result.worktreePath = trimmed; | |
| } else { | |
| getLog().warn({ rawValue: repo.worktree.path }, 'config.worktree_path_whitespace_ignored'); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/src/config/config-loader.ts` around lines 389 - 395, The code
calls repo.worktree.path.trim() after checking !== undefined which will throw if
path is non-string (e.g., null); update the branch in config-loader.ts to first
assert the type is string (e.g., typeof repo.worktree.path === 'string') before
calling .trim(), set result.worktreePath when trimmed is non-empty, and
otherwise either log the whitespace-warning using getLog().warn or throw a clear
error for unsupported non-string values (per guidelines) so the unsafe state is
rejected explicitly; reference symbols: repo.worktree.path, result.worktreePath,
and getLog().warn.
|
Hey @joelsb — thanks for this! The motivation is real (worktrees in The primitiveThe config field is a free-form relative path string, but the actual semantics is "a directory inside this repo". That mismatch leaves a few foot-guns the PR doesn't address:
The actual user need is "put them somewhere under the repo so I can see them in my IDE". Two cheaper-to-validate primitives that express that intent more precisely:
If you want to keep the full The side effectsA few things that aren't quite right yet: 1. Asymmetric config-load error handling. try { earlyConfig = await this.loadConfig(...); earlyConfigLoaded = true; }
catch { /* swallowed */ }The early load silently swallows errors and falls back to defaults; the later load in 2. The PR replaces 3. The PR plumbs 4. Implicit A user opting into 5. Worktree-inside-repo recursion hazard (worth thinking about, not necessarily blocking). When the worktree lives at
Probably not a blocker, but worth a note in the doc/changelog so users opt in with eyes open. Suggested cleanup checklist
Happy to review the next iteration. Thanks again for digging into this one! |
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>
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>
… policy (#1310) * feat(isolation): per-project worktree.path + collapse to two layouts Adds an opt-in `worktree.path` to .archon/config.yaml so a repo can co-locate worktrees with its own checkout (`<repoRoot>/<path>/<branch>`) instead of the default `~/.archon/workspaces/<owner>/<repo>/worktrees/<branch>`. Requested in joelsb's #1117. Primitive changes (clean up the graveyard rather than add parallel code paths): - Collapse worktree layouts from three to two. The old "legacy global" layout (`~/.archon/worktrees/<owner>/<repo>/<branch>`) is gone — every repo resolves to the workspace-scoped layout (`~/.archon/workspaces/<owner>/<repo>/worktrees/<branch>`), whether it was archon-cloned or locally registered. `extractOwnerRepo()` on the repo path is the stable identity fallback. Ends the divergence where workspace-cloned and local repos had visibly different worktree trees. - `getWorktreeBase()` in @archon/git now returns `{ base, layout }` and accepts an optional `{ repoLocal }` override. The layout value replaces the old `isProjectScopedWorktreeBase()` classification at the call sites (`isProjectScopedWorktreeBase` stays exported as deprecated back-compat). - `WorktreeCreateConfig.path` carries the validated override from repo config. `resolveRepoLocalOverride()` fails loudly on absolute paths, `..` escapes, and resolve-escape edge cases (Fail Fast — no silent default fallback when the config is syntactically wrong). - `WorktreeProvider.create()` now loads repo config exactly once and threads it through `getWorktreePath()` + `createWorktree()`. Replaces the prior swallow-then-retry pattern flagged on #1117. `generateEnvId()` is gone — envId is assigned directly from the resolved path (the invariant was already documented on `destroy(envId)`). Tests (packages/git + packages/isolation): - Update the pre-existing `getWorktreeBase` / `isProjectScopedWorktreeBase` suite for the new two-layout return shape and precedence. - Add 8 tests for `worktree.path`: default fallthrough, empty/whitespace ignored, override wins for workspace-scoped repos, rejects absolute, rejects `../` escapes (three variants), accepts nested relative paths. Docs: add `worktree.path` to the repo config reference with explicit precedence and the `.gitignore` responsibility note. Co-authored-by: Joel Bastos <joelsb2001@gmail.com> * feat(workflows): per-workflow worktree.enabled policy Introduces a declarative top-level `worktree:` block on a workflow so authors can pin isolation behavior regardless of invocation surface. Solves the case where read-only workflows (e.g. `repo-triage`) should always run in the live checkout, without every CLI/web/scheduled-trigger caller having to remember to set the right flag. Schema (packages/workflows/src/schemas/workflow.ts + loader.ts): - New optional `worktree.enabled: boolean` on `workflowBaseSchema`. Loader parses with the same warn-and-ignore discipline used for `interactive` and `modelReasoningEffort` — invalid shapes log and drop rather than killing workflow discovery. Policy reconciliation (packages/cli/src/commands/workflow.ts): - Three hard-error cases when YAML policy contradicts invocation flags: • `enabled: false` + `--branch` (worktree required by flag, forbidden by policy) • `enabled: false` + `--from` (start-point only meaningful with worktree) • `enabled: true` + `--no-worktree` (policy requires worktree, flag forbids it) - `enabled: false` + `--no-worktree` is redundant, accepted silently. - `--resume` ignores the pinned policy (it reuses the existing run's worktree even when policy would disable — avoids disturbing a paused run). Orchestrator wiring (packages/core/src/orchestrator/orchestrator-agent.ts): - `dispatchOrchestratorWorkflow` short-circuits `validateAndResolveIsolation` when `workflow.worktree?.enabled === false` and runs directly in `codebase.default_cwd`. Web chat/slack/telegram callers have no flag equivalent to `--no-worktree`, so the YAML field is their only control. - Logged as `workflow.worktree_disabled_by_policy` for operator visibility. First consumer (.archon/workflows/repo-triage.yaml): - `worktree: { enabled: false }` — triage reads issues/PRs and writes gh labels; no code mutations, no reason to spin up a worktree per run. Tests: - Loader: parses `worktree.enabled: true|false`, omits block when absent. - CLI: four new integration tests for the reconciliation matrix (skip when policy false, three hard-error cases, redundant `--no-worktree` accepted, `--no-worktree` + `enabled: true` rejected). Docs: authoring-workflows.md gets the new top-level field in the schema example with a comment explaining the precedence and the `enabled: true|false` semantics. * fix(isolation): use path.sep for repo-containment check on Windows resolveRepoLocalOverride was hardcoding '/' as the separator in the startsWith check, so on Windows (where `resolve()` returns backslash paths like `D:\Users\dev\Projects\myapp`) every otherwise-valid relative `worktree.path` was rejected with "resolves outside the repo root". Fixed by importing `path.sep` and using it in the sentinel. Fixes the 3 Windows CI failures in `worktree.path repo-local override`. --------- Co-authored-by: Joel Bastos <joelsb2001@gmail.com>
Summary
worktree.pathto repo-level.archon/config.yaml— a relative path from repo root where worktrees should be created<repoRoot>/<path>/<branchName>instead of~/.archon/worktrees/Motivation
When working with Archon on a project, worktrees are created in
~/.archon/worktrees/by default — invisible to the IDE and far from the project. This PR allows repos to opt in to keeping worktrees co-located:Worktrees then appear at
myproject/.worktrees/archon/task-fix-bug— visible in the IDE file tree and easy to navigate.Changes
packages/isolation/src/types.tspath?: stringtoWorktreeCreateConfigpackages/core/src/config/config-types.tspath?: stringtoRepoConfig.worktreepackages/isolation/src/providers/worktree.tsgetWorktreePath()checksconfig.pathfirst;create()loads config early;createWorktree()handles custom path mkdirpackages/isolation/src/providers/worktree.test.tsTest plan
worktree.path: .worktreesin a repo's.archon/config.yamland runarchon workflow run— worktree should appear under.worktrees/🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores
.worktreesdirectories.Tests