Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .archon/workflows/e2e-worktree-disabled.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# E2E smoke test — workflow-level worktree.enabled: false
# Verifies: when a workflow pins worktree.enabled: false, runs happen in the
# live repo checkout (no worktree created, cwd == repo root). Zero AI calls.
name: e2e-worktree-disabled
description: "Pinned-isolation-off smoke. Asserts cwd is the repo root rather than a worktree path, regardless of how the workflow is invoked."

worktree:
enabled: false

nodes:
# Print cwd so the operator can eyeball it, and capture for the assertion node.
- id: print-cwd
bash: "pwd"

# Assertion: cwd must NOT contain '/.archon/workspaces/' — if it does, the
# policy was ignored and a worktree was created anyway. We also assert the
# cwd ends with a git repo (has a .git directory or file visible).
- id: assert-live-checkout
bash: |
cwd="$(pwd)"
echo "assert-live-checkout cwd=$cwd"
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
Comment on lines +22 to +31
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.

echo "PASS: ran in live checkout (no worktree created by policy)"
depends_on: [print-cwd]
trigger_rule: all_success
6 changes: 6 additions & 0 deletions .archon/workflows/repo-triage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ description: >-
runs; safe to re-run; idempotent.
interactive: false

# 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
Comment on lines +11 to +15
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.


nodes:
# ---------------------------------------------------------------------------
# Issue triage — runs concurrently with pr-link (no depends_on between them).
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`'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.
- **`getHomeWorkflowsPath()`, `getHomeCommandsPath()`, `getHomeScriptsPath()`, `getLegacyHomeWorkflowsPath()`** helpers in `@archon/paths`, exported for both internal discovery and external callers that want to target the home scope directly.
- **`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.
- **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.
- **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.
- **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)
- **`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)
- **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)
Expand Down
140 changes: 140 additions & 0 deletions packages/cli/src/commands/workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,146 @@ describe('workflowRunCommand', () => {
expect(createCallsAfter).toBe(createCallsBefore);
});

// -------------------------------------------------------------------------
// Workflow-level `worktree.enabled` policy
// -------------------------------------------------------------------------

it('skips isolation when workflow YAML pins worktree.enabled: false', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const { executeWorkflow } = await import('@archon/workflows/executor');
const conversationDb = await import('@archon/core/db/conversations');
const codebaseDb = await import('@archon/core/db/codebases');
const isolation = await import('@archon/isolation');

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);
Comment on lines +879 to +915
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.

});

it('throws when workflow pins worktree.enabled: false but caller passes --branch', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'triage',
description: 'Read-only triage',
worktree: { enabled: false },
}),
],
errors: [],
});

await expect(
workflowRunCommand('/test/path', 'triage', 'go', { branchName: 'feat-x' })
).rejects.toThrow(/worktree\.enabled: false/);
});

it('throws when workflow pins worktree.enabled: false but caller passes --from', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'triage',
description: 'Read-only triage',
worktree: { enabled: false },
}),
],
errors: [],
});

await expect(
workflowRunCommand('/test/path', 'triage', 'go', { fromBranch: 'dev' })
).rejects.toThrow(/worktree\.enabled: false/);
});

it('accepts worktree.enabled: false + --no-worktree as redundant (no error)', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const { executeWorkflow } = await import('@archon/workflows/executor');
const conversationDb = await import('@archon/core/db/conversations');
const codebaseDb = await import('@archon/core/db/codebases');

(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',
});

// Should not throw — redundant, not contradictory
await workflowRunCommand('/test/path', 'triage', 'go', { noWorktree: true });
});

it('throws when workflow pins worktree.enabled: true but caller passes --no-worktree', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'build',
description: 'Requires a worktree',
worktree: { enabled: true },
}),
],
errors: [],
});

await expect(
workflowRunCommand('/test/path', 'build', 'go', { noWorktree: true })
).rejects.toThrow(/worktree\.enabled: true/);
});

it('throws when isolation cannot be created due to missing codebase', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const conversationDb = await import('@archon/core/db/conversations');
Expand Down
41 changes: 39 additions & 2 deletions packages/cli/src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,37 @@ export async function workflowRunCommand(
);
}

// Reconcile workflow-level worktree policy with invocation flags.
// The workflow YAML's `worktree.enabled` pins isolation regardless of caller —
// a mismatch between policy and flags is a user error we surface loudly
// rather than silently applying one side and ignoring the other.
const pinnedEnabled = workflow.worktree?.enabled;
if (pinnedEnabled === false) {
if (options.branchName !== undefined) {
throw new Error(
`Workflow '${workflow.name}' sets worktree.enabled: false (runs in live checkout).\n` +
' --branch requires an isolated worktree.\n' +
" Drop --branch or change the workflow's worktree.enabled."
);
}
if (options.fromBranch !== undefined) {
throw new Error(
`Workflow '${workflow.name}' sets worktree.enabled: false (runs in live checkout).\n` +
' --from/--from-branch only applies when a worktree is created.\n' +
" Drop --from or change the workflow's worktree.enabled."
);
}
// --no-worktree is redundant but not contradictory — silently accept.
} else if (pinnedEnabled === true) {
if (options.noWorktree) {
throw new Error(
`Workflow '${workflow.name}' sets worktree.enabled: true (requires a worktree).\n` +
' --no-worktree conflicts with the workflow policy.\n' +
" Drop --no-worktree or change the workflow's worktree.enabled."
);
}
}

console.log(`Running workflow: ${workflowName}`);
console.log(`Working directory: ${cwd}`);
console.log('');
Expand Down Expand Up @@ -403,8 +434,14 @@ export async function workflowRunCommand(
console.log('');
}

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

if (wantsIsolation && codebase) {
// Auto-generate branch identifier from workflow name + timestamp when --branch not provided
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/config/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ export interface RepoConfig {
* @default true
*/
initSubmodules?: boolean;

/**
* 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;
Comment on lines +180 to +201
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.

};

/**
Expand Down
54 changes: 33 additions & 21 deletions packages/core/src/orchestrator/orchestrator-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,31 +228,43 @@ async function dispatchOrchestratorWorkflow(
codebase_id: codebase.id,
});

// Validate and resolve isolation
// Validate and resolve isolation.
// A workflow with `worktree.enabled: false` short-circuits the resolver entirely
// and runs in the live checkout — no worktree creation, no env row. This is the
// declarative equivalent of CLI `--no-worktree` for workflows that should always
// run live (e.g. read-only triage, docs generation on the main checkout).
let cwd: string;
try {
const result = await validateAndResolveIsolation(
{ ...conversation, codebase_id: codebase.id },
codebase,
platform,
conversationId,
isolationHints
if (workflow.worktree?.enabled === false) {
getLog().info(
{ workflowName: workflow.name, conversationId, codebaseId: codebase.id },
'workflow.worktree_disabled_by_policy'
);
cwd = result.cwd;
} catch (error) {
if (error instanceof IsolationBlockedError) {
getLog().warn(
{
reason: error.reason,
conversationId,
codebaseId: codebase.id,
workflowName: workflow.name,
},
'isolation_blocked'
cwd = codebase.default_cwd;
} else {
try {
const result = await validateAndResolveIsolation(
{ ...conversation, codebase_id: codebase.id },
codebase,
platform,
conversationId,
isolationHints
);
return;
cwd = result.cwd;
} catch (error) {
if (error instanceof IsolationBlockedError) {
getLog().warn(
{
reason: error.reason,
conversationId,
codebaseId: codebase.id,
workflowName: workflow.name,
},
'isolation_blocked'
);
return;
}
throw error;
}
throw error;
}

// Dispatch workflow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ model: sonnet
modelReasoningEffort: medium # Codex only
webSearchMode: live # Codex only
interactive: true # Web only: run in foreground instead of background
worktree: # Optional: pin isolation behavior regardless of caller
enabled: false # false = always run in the live checkout (CLI --no-worktree
# and web both honor it). Use for read-only workflows
# like triage/reporting. true = must use a worktree;
# CLI --no-worktree hard-errors. Omit to let the
# caller decide (current default = worktree).

# Required for DAG-based
nodes:
Expand Down
6 changes: 6 additions & 0 deletions packages/docs-web/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ worktree:
- .vscode # Copy entire directory
initSubmodules: true # Optional: default true — auto-detects .gitmodules and runs
# `git submodule update --init --recursive`. Set false to opt out.
path: .worktrees # Optional: co-locate worktrees with the repo at
# <repoRoot>/.worktrees/<branch> instead of under
# ~/.archon/workspaces/<owner>/<repo>/worktrees/.
# Must be relative; no absolute, no `..` segments.

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

**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`).

**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.

## Environment Variables

Environment variables override all other configuration. They are organized by category below.
Expand Down
Loading
Loading