Skip to content

Commit 5ed38dc

Browse files
Wirasmjoelsb
andauthored
feat(isolation,workflows): worktree location + per-workflow isolation 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>
1 parent 7be4d0a commit 5ed38dc

19 files changed

Lines changed: 748 additions & 171 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# E2E smoke test — workflow-level worktree.enabled: false
2+
# Verifies: when a workflow pins worktree.enabled: false, runs happen in the
3+
# live repo checkout (no worktree created, cwd == repo root). Zero AI calls.
4+
name: e2e-worktree-disabled
5+
description: "Pinned-isolation-off smoke. Asserts cwd is the repo root rather than a worktree path, regardless of how the workflow is invoked."
6+
7+
worktree:
8+
enabled: false
9+
10+
nodes:
11+
# Print cwd so the operator can eyeball it, and capture for the assertion node.
12+
- id: print-cwd
13+
bash: "pwd"
14+
15+
# Assertion: cwd must NOT contain '/.archon/workspaces/' — if it does, the
16+
# policy was ignored and a worktree was created anyway. We also assert the
17+
# cwd ends with a git repo (has a .git directory or file visible).
18+
- id: assert-live-checkout
19+
bash: |
20+
cwd="$(pwd)"
21+
echo "assert-live-checkout cwd=$cwd"
22+
case "$cwd" in
23+
*/.archon/workspaces/*/worktrees/*)
24+
echo "FAIL: workflow ran inside a worktree ($cwd) despite worktree.enabled: false"
25+
exit 1
26+
;;
27+
esac
28+
if [ ! -e "$cwd/.git" ]; then
29+
echo "FAIL: cwd $cwd is not a git checkout root (.git missing)"
30+
exit 1
31+
fi
32+
echo "PASS: ran in live checkout (no worktree created by policy)"
33+
depends_on: [print-cwd]
34+
trigger_rule: all_success

.archon/workflows/repo-triage.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ description: >-
88
runs; safe to re-run; idempotent.
99
interactive: false
1010

11+
# Read-only triage runs directly in the live checkout. Creating a worktree
12+
# every run would be wasted work (nothing is mutated) and would scatter stale
13+
# branches under ~/.archon/workspaces/<owner>/<repo>/worktrees/.
14+
worktree:
15+
enabled: false
16+
1117
nodes:
1218
# ---------------------------------------------------------------------------
1319
# Issue triage — runs concurrently with pr-link (no depends_on between them).

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- **`'global'` variant on `WorkflowSource`** — workflows at `~/.archon/workflows/` and commands at `~/.archon/commands/` now render with a distinct source label (no longer coerced to `'project'`). Web UI badges updated.
1616
- **`getHomeWorkflowsPath()`, `getHomeCommandsPath()`, `getHomeScriptsPath()`, `getLegacyHomeWorkflowsPath()`** helpers in `@archon/paths`, exported for both internal discovery and external callers that want to target the home scope directly.
1717
- **`discoverScriptsForCwd(cwd)`** in `@archon/workflows/script-discovery` — merges home-scoped + repo-scoped scripts with repo winning on name collisions. Used by the DAG executor and validator; callers no longer need to know about the two-scope shape.
18+
- **Workflow-level worktree policy (`worktree.enabled` in workflow YAML).** A workflow can now pin whether its runs use isolation regardless of how they were invoked: `worktree.enabled: false` always runs in the live checkout (CLI `--branch` / `--from` hard-error; web/chat/orchestrator short-circuits `validateAndResolveIsolation`), `worktree.enabled: true` requires isolation (CLI `--no-worktree` hard-errors). Omit the block to let the caller decide (current default). First consumer: `.archon/workflows/repo-triage.yaml` pinned to `enabled: false` since it's read-only.
19+
- **Per-project worktree path (`worktree.path` in `.archon/config.yaml`).** Opt-in repo-relative directory (e.g. `.worktrees`) where Archon places worktrees for that repo, instead of the default `~/.archon/workspaces/<owner>/<repo>/worktrees/`. Co-locates worktrees with the project so they appear in the IDE file tree. Validated as a safe relative path (no absolute, no `..`); malformed values fail loudly at worktree creation. Users opting in are responsible for `.gitignore`ing the directory themselves — no automatic file mutation. Credits @joelsb for surfacing the need in #1117.
1820
- **Three-path env model with operator-visible log lines.** The CLI and server now load env vars from `~/.archon/.env` (user scope) and `<cwd>/.archon/.env` (repo scope, overrides user) at boot, both with `override: true`. A new `[archon] loaded N keys from <path>` line is emitted per source (only when N > 0). `[archon] stripped N keys from <cwd> (...)` now also prints when stripCwdEnv removes target-repo env keys, replacing the misleading `[dotenv@17.3.1] injecting env (0) from .env` preamble that always reported 0. The `quiet: true` flag suppresses dotenv's own output. (#1302)
1921
- **`archon setup --scope home|project` and `--force` flags.** Default is `--scope home` (writes `~/.archon/.env`). `--scope project` targets `<cwd>/.archon/.env` instead. `--force` overwrites the target wholesale rather than merging; a timestamped backup is still written. (#1303)
2022
- **Merge-only setup writes with timestamped backups.** `archon setup` now reads the existing target file, preserves non-empty values, carries user-added custom keys forward, and writes a `<target>.archon-backup-<ISO-ts>` before every rewrite. Fixes silent PostgreSQL→SQLite downgrade and silent token loss on re-run. (#1303)

packages/cli/src/commands/workflow.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,146 @@ describe('workflowRunCommand', () => {
865865
expect(createCallsAfter).toBe(createCallsBefore);
866866
});
867867

868+
// -------------------------------------------------------------------------
869+
// Workflow-level `worktree.enabled` policy
870+
// -------------------------------------------------------------------------
871+
872+
it('skips isolation when workflow YAML pins worktree.enabled: false', async () => {
873+
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
874+
const { executeWorkflow } = await import('@archon/workflows/executor');
875+
const conversationDb = await import('@archon/core/db/conversations');
876+
const codebaseDb = await import('@archon/core/db/codebases');
877+
const isolation = await import('@archon/isolation');
878+
879+
const getIsolationProviderMock = isolation.getIsolationProvider as ReturnType<typeof mock>;
880+
const providerBefore = getIsolationProviderMock.mock.results.at(-1)?.value as
881+
| { create: ReturnType<typeof mock> }
882+
| undefined;
883+
const createCallsBefore = providerBefore?.create.mock.calls.length ?? 0;
884+
885+
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
886+
workflows: [
887+
makeTestWorkflowWithSource({
888+
name: 'triage',
889+
description: 'Read-only triage',
890+
worktree: { enabled: false },
891+
}),
892+
],
893+
errors: [],
894+
});
895+
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
896+
id: 'conv-123',
897+
});
898+
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce({
899+
id: 'cb-123',
900+
default_cwd: '/test/path',
901+
});
902+
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
903+
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
904+
success: true,
905+
workflowRunId: 'run-123',
906+
});
907+
908+
// No flags — policy alone should disable isolation
909+
await workflowRunCommand('/test/path', 'triage', 'go', {});
910+
911+
const providerAfter = getIsolationProviderMock.mock.results.at(-1)?.value as
912+
| { create: ReturnType<typeof mock> }
913+
| undefined;
914+
const createCallsAfter = providerAfter?.create.mock.calls.length ?? 0;
915+
expect(createCallsAfter).toBe(createCallsBefore);
916+
});
917+
918+
it('throws when workflow pins worktree.enabled: false but caller passes --branch', async () => {
919+
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
920+
921+
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
922+
workflows: [
923+
makeTestWorkflowWithSource({
924+
name: 'triage',
925+
description: 'Read-only triage',
926+
worktree: { enabled: false },
927+
}),
928+
],
929+
errors: [],
930+
});
931+
932+
await expect(
933+
workflowRunCommand('/test/path', 'triage', 'go', { branchName: 'feat-x' })
934+
).rejects.toThrow(/worktree\.enabled: false/);
935+
});
936+
937+
it('throws when workflow pins worktree.enabled: false but caller passes --from', async () => {
938+
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
939+
940+
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
941+
workflows: [
942+
makeTestWorkflowWithSource({
943+
name: 'triage',
944+
description: 'Read-only triage',
945+
worktree: { enabled: false },
946+
}),
947+
],
948+
errors: [],
949+
});
950+
951+
await expect(
952+
workflowRunCommand('/test/path', 'triage', 'go', { fromBranch: 'dev' })
953+
).rejects.toThrow(/worktree\.enabled: false/);
954+
});
955+
956+
it('accepts worktree.enabled: false + --no-worktree as redundant (no error)', async () => {
957+
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
958+
const { executeWorkflow } = await import('@archon/workflows/executor');
959+
const conversationDb = await import('@archon/core/db/conversations');
960+
const codebaseDb = await import('@archon/core/db/codebases');
961+
962+
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
963+
workflows: [
964+
makeTestWorkflowWithSource({
965+
name: 'triage',
966+
description: 'Read-only triage',
967+
worktree: { enabled: false },
968+
}),
969+
],
970+
errors: [],
971+
});
972+
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
973+
id: 'conv-123',
974+
});
975+
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce({
976+
id: 'cb-123',
977+
default_cwd: '/test/path',
978+
});
979+
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
980+
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
981+
success: true,
982+
workflowRunId: 'run-123',
983+
});
984+
985+
// Should not throw — redundant, not contradictory
986+
await workflowRunCommand('/test/path', 'triage', 'go', { noWorktree: true });
987+
});
988+
989+
it('throws when workflow pins worktree.enabled: true but caller passes --no-worktree', async () => {
990+
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
991+
992+
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
993+
workflows: [
994+
makeTestWorkflowWithSource({
995+
name: 'build',
996+
description: 'Requires a worktree',
997+
worktree: { enabled: true },
998+
}),
999+
],
1000+
errors: [],
1001+
});
1002+
1003+
await expect(
1004+
workflowRunCommand('/test/path', 'build', 'go', { noWorktree: true })
1005+
).rejects.toThrow(/worktree\.enabled: true/);
1006+
});
1007+
8681008
it('throws when isolation cannot be created due to missing codebase', async () => {
8691009
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
8701010
const conversationDb = await import('@archon/core/db/conversations');

packages/cli/src/commands/workflow.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,37 @@ export async function workflowRunCommand(
261261
);
262262
}
263263

264+
// Reconcile workflow-level worktree policy with invocation flags.
265+
// The workflow YAML's `worktree.enabled` pins isolation regardless of caller —
266+
// a mismatch between policy and flags is a user error we surface loudly
267+
// rather than silently applying one side and ignoring the other.
268+
const pinnedEnabled = workflow.worktree?.enabled;
269+
if (pinnedEnabled === false) {
270+
if (options.branchName !== undefined) {
271+
throw new Error(
272+
`Workflow '${workflow.name}' sets worktree.enabled: false (runs in live checkout).\n` +
273+
' --branch requires an isolated worktree.\n' +
274+
" Drop --branch or change the workflow's worktree.enabled."
275+
);
276+
}
277+
if (options.fromBranch !== undefined) {
278+
throw new Error(
279+
`Workflow '${workflow.name}' sets worktree.enabled: false (runs in live checkout).\n` +
280+
' --from/--from-branch only applies when a worktree is created.\n' +
281+
" Drop --from or change the workflow's worktree.enabled."
282+
);
283+
}
284+
// --no-worktree is redundant but not contradictory — silently accept.
285+
} else if (pinnedEnabled === true) {
286+
if (options.noWorktree) {
287+
throw new Error(
288+
`Workflow '${workflow.name}' sets worktree.enabled: true (requires a worktree).\n` +
289+
' --no-worktree conflicts with the workflow policy.\n' +
290+
" Drop --no-worktree or change the workflow's worktree.enabled."
291+
);
292+
}
293+
}
294+
264295
console.log(`Running workflow: ${workflowName}`);
265296
console.log(`Working directory: ${cwd}`);
266297
console.log('');
@@ -403,8 +434,14 @@ export async function workflowRunCommand(
403434
console.log('');
404435
}
405436

406-
// Default to worktree isolation unless --no-worktree or --resume
407-
const wantsIsolation = !options.resume && !options.noWorktree;
437+
// Default to worktree isolation unless --no-worktree or --resume.
438+
// Workflow YAML `worktree.enabled` pins the decision — mismatches with CLI
439+
// flags are rejected above, so by this point the policy (if set) and flags
440+
// agree. `--resume` reuses an existing worktree and takes precedence over
441+
// the pinned policy to avoid disturbing a paused run.
442+
const flagWantsIsolation = !options.resume && !options.noWorktree;
443+
const wantsIsolation =
444+
!options.resume && pinnedEnabled !== undefined ? pinnedEnabled : flagWantsIsolation;
408445

409446
if (wantsIsolation && codebase) {
410447
// Auto-generate branch identifier from workflow name + timestamp when --branch not provided

packages/core/src/config/config-types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,29 @@ export interface RepoConfig {
176176
* @default true
177177
*/
178178
initSubmodules?: boolean;
179+
180+
/**
181+
* Per-project worktree directory (relative to repo root). When set,
182+
* worktrees are created at `<repoRoot>/<path>/<branch>` instead of under
183+
* `~/.archon/worktrees/` or the workspaces layout.
184+
*
185+
* Opt-in — co-locates worktrees with the repo so they appear in the IDE
186+
* file tree. The user is responsible for adding the directory to their
187+
* `.gitignore` (no automatic file mutation).
188+
*
189+
* Path resolution precedence (highest to lowest):
190+
* 1. this `worktree.path` (repo-local)
191+
* 2. global `paths.worktrees` (absolute override in `~/.archon/config.yaml`)
192+
* 3. auto-detected project-scoped (`~/.archon/workspaces/owner/repo/...`)
193+
* 4. default global (`~/.archon/worktrees/`)
194+
*
195+
* Must be a safe relative path: no leading `/`, no `..` segments. Absolute
196+
* or escaping values fail loudly at worktree creation (Fail Fast — no silent
197+
* fallback).
198+
*
199+
* @example '.worktrees'
200+
*/
201+
path?: string;
179202
};
180203

181204
/**

packages/core/src/orchestrator/orchestrator-agent.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -228,31 +228,43 @@ async function dispatchOrchestratorWorkflow(
228228
codebase_id: codebase.id,
229229
});
230230

231-
// Validate and resolve isolation
231+
// Validate and resolve isolation.
232+
// A workflow with `worktree.enabled: false` short-circuits the resolver entirely
233+
// and runs in the live checkout — no worktree creation, no env row. This is the
234+
// declarative equivalent of CLI `--no-worktree` for workflows that should always
235+
// run live (e.g. read-only triage, docs generation on the main checkout).
232236
let cwd: string;
233-
try {
234-
const result = await validateAndResolveIsolation(
235-
{ ...conversation, codebase_id: codebase.id },
236-
codebase,
237-
platform,
238-
conversationId,
239-
isolationHints
237+
if (workflow.worktree?.enabled === false) {
238+
getLog().info(
239+
{ workflowName: workflow.name, conversationId, codebaseId: codebase.id },
240+
'workflow.worktree_disabled_by_policy'
240241
);
241-
cwd = result.cwd;
242-
} catch (error) {
243-
if (error instanceof IsolationBlockedError) {
244-
getLog().warn(
245-
{
246-
reason: error.reason,
247-
conversationId,
248-
codebaseId: codebase.id,
249-
workflowName: workflow.name,
250-
},
251-
'isolation_blocked'
242+
cwd = codebase.default_cwd;
243+
} else {
244+
try {
245+
const result = await validateAndResolveIsolation(
246+
{ ...conversation, codebase_id: codebase.id },
247+
codebase,
248+
platform,
249+
conversationId,
250+
isolationHints
252251
);
253-
return;
252+
cwd = result.cwd;
253+
} catch (error) {
254+
if (error instanceof IsolationBlockedError) {
255+
getLog().warn(
256+
{
257+
reason: error.reason,
258+
conversationId,
259+
codebaseId: codebase.id,
260+
workflowName: workflow.name,
261+
},
262+
'isolation_blocked'
263+
);
264+
return;
265+
}
266+
throw error;
254267
}
255-
throw error;
256268
}
257269

258270
// Dispatch workflow

packages/docs-web/src/content/docs/guides/authoring-workflows.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ model: sonnet
120120
modelReasoningEffort: medium # Codex only
121121
webSearchMode: live # Codex only
122122
interactive: true # Web only: run in foreground instead of background
123+
worktree: # Optional: pin isolation behavior regardless of caller
124+
enabled: false # false = always run in the live checkout (CLI --no-worktree
125+
# and web both honor it). Use for read-only workflows
126+
# like triage/reporting. true = must use a worktree;
127+
# CLI --no-worktree hard-errors. Omit to let the
128+
# caller decide (current default = worktree).
123129
124130
# Required for DAG-based
125131
nodes:

packages/docs-web/src/content/docs/reference/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ worktree:
127127
- .vscode # Copy entire directory
128128
initSubmodules: true # Optional: default true — auto-detects .gitmodules and runs
129129
# `git submodule update --init --recursive`. Set false to opt out.
130+
path: .worktrees # Optional: co-locate worktrees with the repo at
131+
# <repoRoot>/.worktrees/<branch> instead of under
132+
# ~/.archon/workspaces/<owner>/<repo>/worktrees/.
133+
# Must be relative; no absolute, no `..` segments.
130134

131135
# Documentation directory
132136
docs:
@@ -180,6 +184,8 @@ This is useful when you maintain coding style or identity preferences in `~/.cla
180184

181185
**Docs path behavior:** The `docs.path` setting controls where the `$DOCS_DIR` variable points. When not configured, `$DOCS_DIR` defaults to `docs/`. Unlike `$BASE_BRANCH`, this variable always has a safe default and never throws an error. Configure it when your documentation lives outside the standard `docs/` directory (e.g., `packages/docs-web/src/content/docs`).
182186

187+
**Worktree path behavior:** By default, every repo's worktrees live under `~/.archon/workspaces/<owner>/<repo>/worktrees/<branch>` — outside the repo, invisible to the IDE. Set `worktree.path` to opt in to a **repo-local** layout instead: worktrees are created at `<repoRoot>/<worktree.path>/<branch>` so they show up in the file tree and editor workspace. A common choice is `.worktrees`. Because worktrees now live inside the repository tree, you should add the directory to your `.gitignore` (Archon does not modify user-owned files). The configured path must be relative to the repo root; absolute paths and paths containing `..` segments fail loudly at worktree creation rather than silently falling back.
188+
183189
## Environment Variables
184190

185191
Environment variables override all other configuration. They are organized by category below.

0 commit comments

Comments
 (0)