fix(#4881): trust worktree.baseRef:"head" in harness mode; a WorktreeCreate hook withholds it and only a measured fork base restores it (#4921)
* fix(#4881): trust worktree.baseRef:"head" in harness mode and keep the #4868 observation as what restores it under a WorktreeCreate hook The pre-dispatch base check still derived its harness-mode verdict from the retired #48 premise that the harness never reads worktree.baseRef: with "head" set and HEAD diverged from origin/HEAD it degraded every wave with baseref-head-ignored-by-harness. #4868 inserted an observation of a clean prior harness worktree at HEAD ahead of that comparison, but an execute-phase run never has one at the moment it checks — the base-check runs before any dispatch, a degraded wave creates no worktrees, and a wave that did run in worktrees has them removed and HEAD moved before the next check — so the common case was unchanged (#4881 repro states 1 and 4). Re-scope of the closed #4752 onto current next, with #4868 kept: - branch a trusts "head" in both isolation modes, the way the harness is measured to behave (#4588: three settings layers, three OSes), and the spawn-time exit-42 guard stays the observation-based backstop; - a Claude Code WorktreeCreate hook in any settings file the check reads, or a file that does not parse, withholds that trust — the hook creates the worktree without applying the setting — and the inferred comparison runs, degrading with baseref-head-bypassed-by-hook; - the #4868 observation (b2) now sits behind branch a: it is reached only when "head" was not trusted outright, and on a hook host it is what restores the trust — a hook that forks from HEAD leaves exactly that evidence, one that forks elsewhere never does. Its per-HEAD cache is unchanged. It is skipped under an explicit --observed-fork-base, which outranks an inference from a prior worktree; - --observed-fork-base <sha> threads a measured fork base through the evaluation (strict full-hex, TypeError otherwise). Tests: the #4868 rows are unchanged and still reachable (they run with the setting unset); the #4752 rows re-land, with the one exit-128 row updated to the degrade #4734 pinned since; a new #4881 block pins the start-of-run state (baseref-head, git never consulted), the hook + observation composition in both directions, and that an explicit observation skips the probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPXQQPzHGintWtbhBoQCRS * docs(#4881): rewrite the eight surfaces that still state the harness ignores worktree.baseRef:"head" Every prose surface #4868 left untouched still asserted the retired #48 premise as verified fact, starting with the step file the orchestrator reads. Each now describes the measured behaviour, the WorktreeCreate-hook exception, the --observed-fork-base input, and the #4868 observation as what lifts the hook degrade; docs/CLI-TOOLS.md gains the fork-from-head-observed reason row #4868 did not document. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPXQQPzHGintWtbhBoQCRS * chore(#4881): set changeset fragment pr to 4921 * fix(#4881): withhold the #4868 observation under the hook interlock The hook interlock this PR added withheld the worktree.baseRef:"head" trust but still let b2's prior-worktree observation restore it, and that observation cannot be attributed to the hook. The evidence is a clean agent worktree sitting at the orchestrator HEAD; nothing on disk records which creator left it there, so one the plain harness created BEFORE a WorktreeCreate hook was configured — with HEAD unmoved since — reads as evidence for the hook. It was the one fail-open branch in a mechanism documented as fail-closed. Keying the observation cache by hook configuration does not close it. observeHarnessForkFromHead has two legs: a HEAD-keyed cache and a live probe over .claude/worktrees/agent-*. A hook-keyed cache simply misses, and the miss falls through to the probe, which re-finds the same stale worktree and re-confirms. The probe takes no hook input at all. So the observation is not consulted under the interlock rather than re-keyed: on a hook host the only admissible positive signal is an explicit --observed-fork-base measurement of the dispatch in hand, and absent one the inferred comparison runs and a mismatch degrades with baseref-head-bypassed-by-hook, leaving the spawn-time exit-42 guard as the backstop. Scoped to the case branch a. declined to trust: "head" set AND a hook (or an unparseable layer) in the harness's path. With no "head" setting b2 is #4868's own arm and is unchanged, hook or not — re-scoping that trust is a separate question this PR does not open, and a test pins the boundary. Cost, stated: a hook host with "head" set, on a branch diverged from origin/HEAD and passing no observation, now runs sequentially. It still runs parallel when HEAD matches origin/HEAD. No workflow threads an observation today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ * refactor(#4881): drop the now-unused forkRef message-builder parameter buildMsgBaserefHeadIgnored stopped reading forkRef when the message was made mode-neutral, and the parameter was retained with `void forkRef;` for symmetry with its two sibling builders, which do read it. Symmetry is not reason enough to keep a dead parameter on a module-private function with one caller, so drop it (#4921 review). Behaviour is unchanged; the message text is pinned by an existing full-string assertion, which is what covers the only real risk here — transposing the two remaining arguments at the call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ * test(#4881): exercise both FULL_SHA_RE alternatives at their boundaries The invalid-observation list pinned 39 and 41 hex around the 40-hex SHA-1 arm but left the 64-hex SHA-256 arm's own +/-1 boundary unexercised, which the repo's boundary-coverage convention asks for (#4921 review). Adds 63, 65 and a 64-length non-hex string. The regex already rejected all three -- this is coverage of correct behaviour, not a fix -- so it carries no negative control against a pre-fix base. That it is not vacuous was shown instead by widening the arm to {63,65}, under which the row fails by name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ * docs(#4881): finish the surface sweep the hook interlock owes Self-found by this round's own pre-push adversarial review, over six passes. Eight sites, four classes. FIVE were prose still asserting the stance the interlock overturned, which reads as live canon to anyone arriving cold: docs/CLI-TOOLS.md, docs/CONFIGURATION.md and gsd-core/references/planning-config.md each still said the hook degrade is lifted "unless/until a clean prior harness worktree is observed"; observeHarnessForkFromHead's own header still said a qualifying worktree "can only exist if the harness forked from HEAD"; and a test name still called the observation "required on a hook host" when it is now inadmissible there. My own sweep had grepped for "restores"/"lifts" and missed every one -- the ordinary failure of a grep, which returns what you thought to search for. The SIXTH is the same class one step worse: gsd-core/workflows/execute-plan.md still said flatly that Claude Code's isolation="worktree" "forks from origin/HEAD, not live local HEAD" -- in a paragraph THIS PR already edits, a few sentences after the clause it corrected. A tombstone makes only its own line clean; adjoining text asserting the dead stance is the other half of the same defect. Now qualified on the setting, with the hook exception named. The SEVENTH is a proof-strength overstatement that predates this PR, with a driven counterexample: a worktree created from an older base and since `git checkout --detach`ed onto HEAD is clean, sits at HEAD, and satisfies the probe identically, so "can only exist" was false. The worktree's own reflog does retain that original checkout -- the information is not lost, the probe simply does not consult it. The EIGHTH is that the header described only one of the function's two legs. A cache hit returns the prior conclusion without reading any worktree, so "the probe reads a worktree's present state" was true of the live probe and false of the cache. The header now separates them, and names the cache's blindness as a third reason the observation is inadmissible under a hook. Gaps seven and eight are inherited from #4868 and accepted there for the no-hook case. Nothing about the mechanism changes here; only what the header claims for it. No behavioural change -- comments, prose, and one test's registered name. Emitted-Drift-Ack-Growth: execute-plan.md — the Pattern A paragraph gained a qualifying clause: it stated flatly that Claude Code forks from origin/HEAD, which is the premise this PR retires, a few sentences after the clause the PR had already corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuT9wTyrMaxjAPevqeH4YZ --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -1287,19 +1287,26 @@ Diagnose and configure the worktree fork base used by Claude Code's `isolation="
|
||||
# Returns JSON: { shouldDegrade, reason, message, headSha, forkRef, forkSha }
|
||||
node gsd-tools.cjs worktree base-check
|
||||
|
||||
# Same check, but against the fork base a worktree this host created was
|
||||
# actually observed to have (git rev-parse HEAD inside it, before any commit).
|
||||
node gsd-tools.cjs worktree base-check --observed-fork-base <sha>
|
||||
|
||||
# Write worktree.baseRef:"head" into .claude/settings.local.json (no-clobber).
|
||||
# Returns JSON: { changed, skipped, previous, baseRef, file }
|
||||
node gsd-tools.cjs worktree set-baseref
|
||||
```
|
||||
|
||||
**`worktree base-check`** reads `worktree.baseRef` from a three-layer cascade — `.claude/settings.local.json`, then `.claude/settings.json`, then the user/global `settings.json` under `CLAUDE_CONFIG_DIR` (or `~/.claude`) — and compares the current `HEAD` SHA against `origin/HEAD`. Project-level settings take precedence over the user/global layer, so a machine-wide `worktree.baseRef:"head"` set via `/config` is honored when no project override exists. The `shouldDegrade` field is `true` when the execute-phase orchestrator will fall back to sequential execution. `--mode` declares who creates the isolated worktree (#3659): `harness-worktree` (the default — the runtime harness forks it and does **not** read project-settings `baseRef`, #48) or `orchestrator-worktree` (GSD itself runs `git worktree add` with an explicit start-point and honors `"head"`); invalid values fail closed with an error. Possible `reason` values:
|
||||
**`worktree base-check`** reads `worktree.baseRef` from a three-layer cascade — `.claude/settings.local.json`, then `.claude/settings.json`, then the user/global `settings.json` under `CLAUDE_CONFIG_DIR` (or `~/.claude`) — and compares the current `HEAD` SHA against `origin/HEAD`. Project-level settings take precedence over the user/global layer, so a machine-wide `worktree.baseRef:"head"` set via `/config` is honored when no project override exists. The `shouldDegrade` field is `true` when the execute-phase orchestrator will fall back to sequential execution. `--mode` declares who creates the isolated worktree (#3659): `harness-worktree` (the default — the runtime harness forks it) or `orchestrator-worktree` (GSD itself runs `git worktree add` with an explicit start-point). `worktree.baseRef:"head"` is honored by the orchestrator by construction and by the Claude Code harness as measured from all three settings layers (#4588; the #48 finding that the harness did not read the setting predates upstream claude-code#54940); Cursor, the other `harness-worktree` host, is unmeasured. With `"head"` set the check does not compare in either mode unless `--observed-fork-base` is given (below), with one further exception under `harness-worktree` and no observation: when a Claude Code `WorktreeCreate` hook is configured in any of the same three settings files, or one of them does not parse, the hook creates the agent worktree and Claude Code does not apply `worktree.baseRef` to it, so the check compares `HEAD` against `origin/HEAD` as if the setting were absent (#4588). The #4868 prior-worktree observation is **not** consulted on that path: a worktree sitting at `HEAD` records nothing about which creator made it, so one the plain harness left there before the hook was configured would read as evidence for the hook (#4881). Pass `--observed-fork-base` for a trusted verdict on a hook host. Hooks from managed policy settings, a `--settings` file, plugins, agent frontmatter or SDK registrations are not visible to the check; the exit-42 guard remains the backstop for those. `--observed-fork-base <sha>` supplies the fork base a worktree created for a dispatch was actually measured to have (`git rev-parse HEAD` inside it, before any commit — a full 40- or 64-hex sha, case-insensitive; abbreviations are refused because the comparison is exact); when given, it replaces the `origin/HEAD` inference as the fork side of the comparison and `"head"` no longer short-circuits — the verdict then reports a measurement, and a mismatch under `"head"` means the worktree was not forked from HEAD despite the setting. Invalid values for either flag fail closed with an error. Possible `reason` values:
|
||||
|
||||
| `reason` | `shouldDegrade` | Meaning |
|
||||
|---|---|---|
|
||||
| `baseref-head` | `false` | `worktree.baseRef:"head"` is set and `--mode orchestrator-worktree` declares GSD-managed worktrees — the fork base is the orchestrator HEAD by construction |
|
||||
| `baseref-head-ignored-by-harness` | `true` | `worktree.baseRef:"head"` is set but HEAD differs from `origin/HEAD` in harness (default) mode — the harness does not read the setting (#48), so the run degrades to sequential (#3659) |
|
||||
| `head-matches-fork` | `false` | HEAD and `origin/HEAD` are the same commit |
|
||||
| `head-diverged-from-fork` | `true` | Branch is ahead of or diverged from `origin/HEAD` |
|
||||
| `baseref-head-bypassed-by-hook` | `true` | `worktree.baseRef:"head"` is set under `harness-worktree` with no fork base observed, but a Claude Code `WorktreeCreate` hook is configured in one of the three settings files (or one of them does not parse, so a hook cannot be ruled out), and `HEAD` differs from the inferred fork base. The hook creates the agent worktree and Claude Code does not apply `worktree.baseRef` to it, so the setting is not trusted; `message` names the file (#4588). The #4868 prior-worktree observation does **not** lift it: a worktree sitting at `HEAD` carries no record of which creator left it there, so one the plain harness created before the hook was configured is indistinguishable from one the hook created (#4881). Only `--observed-fork-base` restores a trusted verdict here. The fallback comparison is not a measurement of the hook: when `HEAD` matches the inferred fork base the check does not degrade, and the exit-42 guard remains the backstop. `--observed-fork-base` gives a measured verdict |
|
||||
| `baseref-head` | `false` | `worktree.baseRef:"head"` is set and no fork base was observed (and, under `harness-worktree`, no `WorktreeCreate` hook was found in the settings files) — the fork base is the orchestrator HEAD in either mode (by construction under `orchestrator-worktree`; under `harness-worktree` as measured on Claude Code, #4588 — Cursor is unmeasured). The spawn-time `worktree-branch-check` exit-42 guard remains the backstop on a host that does not honor it |
|
||||
| `baseref-head-ignored-by-harness` | `true` | `worktree.baseRef:"head"` is set but the fork base passed via `--observed-fork-base` differs from HEAD — the worktree was not forked from HEAD despite the setting (in either mode), so the run degrades to sequential (#3659, #4588) |
|
||||
| `observed-fork-matches-head` | `false` | The fork base passed via `--observed-fork-base` equals HEAD (#4588) |
|
||||
| `fork-from-head-observed` | `false` | Under `harness-worktree` with no `--observed-fork-base`, a clean linked worktree under `.claude/worktrees/agent-*` sits at the current `HEAD` — positive evidence that this host's worktree creator forks from `HEAD` (#4868). Reached only when `"head"` is absent or not `"head"` — with the setting trusted the check returns `baseref-head` first, and under a `WorktreeCreate` hook with `"head"` set the observation is withheld rather than consulted, because it cannot be attributed to the hook (#4881). Cached per `HEAD` in `.gsd/harness-fork-probe.json`; a dirty worktree, one at another commit, none at all, or a git failure is inconclusive and falls through |
|
||||
| `head-matches-fork` | `false` | HEAD and the inferred fork base (`origin/HEAD`, or its symbolic-ref fallback such as `origin/next` — reported in `forkRef`) are the same commit |
|
||||
| `head-diverged-from-fork` | `true` | Branch is ahead of or diverged from the fork base — the inferred `origin/HEAD` (or its fallback), or the `--observed-fork-base` value when one was given (`forkRef: "observed"`) |
|
||||
| `fork-ref-unknown` | `true` | `origin/HEAD` could not be resolved |
|
||||
| `no-head` | `true` for exit 128, `false` for exit 0 with empty stdout | Exit 128 is git's definitive "no resolvable HEAD here" answer — not a git repository, or a repository with no commits; no harness worktree can be created, so the check degrades to sequential (#4734), with a `message` explaining why. Exit 0 with empty stdout is ambiguous (git completed without a definitive answer) and stays non-degrading (`headAbsenceVerified` distinguishes the two: `true` / `false`) |
|
||||
| `head-unresolvable` | `true` | `git rev-parse HEAD` did not return a definitive answer (timed out, `git` missing, or any other non-128 failure) — fails closed rather than being treated as `no-head` |
|
||||
|
||||
@@ -566,7 +566,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `worktree.baseRef` | string | (unset) | Controls which ref the worktree-based parallel executor uses as the base when creating new phase/wave worktrees. When unset, the executor bases new worktrees on the repository default branch (`origin/HEAD`); if the current branch has diverged, execute-phase auto-degrades to sequential execution rather than halting (as of v1.4.0). Set to `"head"` to base new worktrees on the local `HEAD` instead. **Where it applies (#48/#3659):** honored on runtimes where GSD itself creates the worktrees (Codex, OpenCode, Kimi, Kimi Code) — there it restores wave-based parallel execution on diverged branches. On harness-isolated runtimes (Claude Code, Cursor) the harness does **not** read this setting (verified 5/5 in #48; upstream claude-code#44965): the base check compares against the real fork base regardless and auto-degrades to sequential execution before dispatch when `HEAD` has diverged, so the exit-42 halt is a last-resort backstop rather than the only guard. See [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). |
|
||||
| `worktree.baseRef` | string | (unset) | Controls which ref the worktree-based parallel executor uses as the base when creating new phase/wave worktrees. When unset, the executor bases new worktrees on the repository default branch (`origin/HEAD`); if the current branch has diverged, execute-phase auto-degrades to sequential execution rather than halting (as of v1.4.0). Set to `"head"` to base new worktrees on the local `HEAD` instead — this restores wave-based parallel execution on diverged branches. **Where it applies (#3659/#4588):** honored on runtimes where GSD itself creates the worktrees (Codex, OpenCode, Kimi, Kimi Code) by construction, and on Claude Code by its harness — measured from the project-local, project-shared and user/global layers on macOS, Windows and Linux (#4588). Cursor also declares harness-created worktrees but has not been measured; there the check trusts the setting the same way and the exit-42 guard is the backstop. (#48 had found the harness did not read the setting; that was fixed upstream in claude-code#54940, and until #4588 the base check still assumed it, degrading every wave on an unmerged branch regardless of the setting.) With `"head"` set the pre-dispatch base check trusts it and does not compare, with two exceptions: a supplied `--observed-fork-base` is compared against HEAD instead, and on a harness-created run with no observation a Claude Code `WorktreeCreate` hook in one of those three settings files — or one that does not parse — withholds the trust and the check compares against `origin/HEAD` (#4588). The #4868 prior-worktree observation does not lift that: nothing on a worktree records which creator made it, so one the plain harness left at `HEAD` before the hook was configured would read as evidence for the hook — pass `--observed-fork-base` for a trusted verdict there (#4881). The spawn-time exit-42 guard in each executor remains the observation-based backstop on a host that does not honor the setting. See [Fix the worktree base-mismatch (exit 42) error](how-to/fix-worktree-base-mismatch.md). |
|
||||
|
||||
### Executor isolation per runtime
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ When you run `/gsd-execute-phase` or `/gsd-quick` on a branch that is ahead of t
|
||||
```
|
||||
⚠ Worktree base mismatch: HEAD (abc12345) differs from origin/HEAD (def67890).
|
||||
Running this phase sequentially on the main working tree. Parallel worktrees
|
||||
return once HEAD is merged/pushed so origin/HEAD matches it.
|
||||
(worktree.baseRef:"head" applies only where GSD itself creates the worktree —
|
||||
the runtime harness does not read it; #48, #3659.)
|
||||
return once HEAD is merged/pushed so origin/HEAD matches it, or set
|
||||
worktree.baseRef:"head" to fork worktrees from HEAD instead (honored by
|
||||
GSD-created worktrees and by the Claude Code harness; #683, #4588).
|
||||
```
|
||||
|
||||
The phase or quick task runs to completion sequentially; nothing is blocked. This is the runtime mitigation (`/gsd-execute-phase`: #683/#1369; `/gsd-quick`: #1941).
|
||||
@@ -34,7 +34,7 @@ All worktree-isolated executors halt immediately. Zero progress is made.
|
||||
|
||||
## Why this happens
|
||||
|
||||
Claude Code's `isolation="worktree"` forks executor worktrees from the repository's default branch (`origin/HEAD`), not from your current `HEAD`. When your branch contains commits that `origin/HEAD` does not have — plan files, new source files, anything added since the branch diverged — those files are absent inside each worktree. GSD's `worktree-branch-check` safety guard correctly refuses to act on a worktree that does not match the orchestrator's state, and exits with code 42.
|
||||
Unless `worktree.baseRef` is set to `"head"`, Claude Code's `isolation="worktree"` forks executor worktrees from the repository's default branch (`origin/HEAD`), not from your current `HEAD`. When your branch contains commits that `origin/HEAD` does not have — plan files, new source files, anything added since the branch diverged — those files are absent inside each worktree. GSD's `worktree-branch-check` safety guard correctly refuses to act on a worktree that does not match the orchestrator's state, and exits with code 42.
|
||||
|
||||
This is the guard working as designed: it prevents silent data loss or phantom edits in the wrong tree. The error is a branch-state condition, not an OS-specific or hardware issue.
|
||||
|
||||
@@ -52,20 +52,23 @@ Use this option when:
|
||||
|
||||
---
|
||||
|
||||
## Option 2 — Where `worktree.baseRef: "head"` actually applies (runtimes where GSD creates the worktrees)
|
||||
## Option 2 — Set `worktree.baseRef: "head"` (restores parallel execution on a diverged branch)
|
||||
|
||||
**What this setting can and cannot do (#48, #3659):** on runtimes whose own harness creates isolated
|
||||
worktrees (Claude Code's `Agent(isolation="worktree")`), the harness forks from the repository
|
||||
default branch and **does not read project-settings `baseRef`** — verified 5/5 in #48; tracked
|
||||
upstream at claude-code#44965/#43535. On those runtimes, setting `baseRef:"head"` does **not**
|
||||
restore parallel worktrees on a diverged branch, and since #3659 it no longer silences the
|
||||
pre-dispatch check: GSD compares `HEAD` against the real fork base and auto-degrades to sequential
|
||||
execution before dispatch instead of letting every executor die at the exit-42 guard.
|
||||
**What this setting does (#3659, #4588):** it makes new worktrees fork from your current `HEAD`
|
||||
instead of `origin/HEAD`, so the files your branch added are present inside each executor
|
||||
worktree and the wave can run in parallel. It is honored by GSD-created worktrees by construction
|
||||
and, as measured, by Claude Code's harness:
|
||||
|
||||
The setting **is** honored where GSD itself runs `git worktree add <path> <start-point>` — the
|
||||
orchestrator-managed isolation used on runtimes with a headless exec surface (Codex, OpenCode,
|
||||
Kimi, Kimi Code). There `baseRef:"head"` really does fork from your current `HEAD`, the check
|
||||
suppresses on it (`reason: "baseref-head"`), and parallel execution works on any branch.
|
||||
- **Runtimes where GSD itself runs `git worktree add <path> <start-point>`** (Codex, OpenCode,
|
||||
Kimi, Kimi Code) — by construction; the check suppresses on it (`reason: "baseref-head"`).
|
||||
- **Claude Code's `Agent(isolation="worktree")`** — because the harness reads the setting and
|
||||
forks from `HEAD`. This was measured on current Claude Code from the project-local,
|
||||
project-shared and user/global settings layers on macOS, Windows and Linux (#4588). Cursor,
|
||||
the other runtime whose harness creates the worktree, has not been measured — see below. An earlier finding that the harness did *not* read it (#48,
|
||||
verified against the Claude Code of the time; upstream claude-code#44965) was fixed upstream in
|
||||
claude-code#54940, but until #4588 GSD's pre-dispatch check still assumed it and degraded every
|
||||
wave on an unmerged branch regardless of the setting — the `baseref-head-ignored-by-harness`
|
||||
reason you may have seen on older installs.
|
||||
|
||||
Run the convenience command from your project root:
|
||||
|
||||
@@ -75,18 +78,51 @@ node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree set-baseref
|
||||
|
||||
This writes `worktree.baseRef: "head"` into `.claude/settings.local.json` in your project root. It is no-clobber: if you already have an explicit `baseRef` set to something else, it leaves your value in place and tells you.
|
||||
|
||||
To verify the result on a harness-isolated runtime (Claude Code, Cursor), pass the dispatch mode so
|
||||
the check evaluates the base the harness will actually use:
|
||||
To verify the result, pass the dispatch mode the runtime uses:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree base-check --mode harness-worktree
|
||||
```
|
||||
|
||||
The output is JSON. On a diverged branch expect `shouldDegrade: true` with
|
||||
`reason: "baseref-head-ignored-by-harness"` — GSD will run the phase sequentially; parallel
|
||||
worktrees return once `HEAD` is merged/pushed so `origin/HEAD` matches it. On a GSD-managed runtime,
|
||||
`--mode orchestrator-worktree` returns `shouldDegrade: false` with `reason: "baseref-head"` and
|
||||
parallel worktrees work on any branch.
|
||||
The output is JSON. With the setting in place expect `shouldDegrade: false` with
|
||||
`reason: "baseref-head"` in either mode — the check does not compare `HEAD` against
|
||||
`origin/HEAD` when the fork base is `HEAD` by configuration. Without the setting, on a diverged
|
||||
branch, expect `shouldDegrade: true` with `reason: "head-diverged-from-fork"`.
|
||||
|
||||
**If you configure a Claude Code `WorktreeCreate` hook**, the setting does not reach the worktrees
|
||||
Claude Code dispatches: the hook creates them from the directory it emits, and Claude Code does not
|
||||
apply `worktree.baseRef` on that path. The check looks for such a hook in the same three settings
|
||||
files it reads `worktree.baseRef` from. When it finds one, or when one of those files does not
|
||||
parse, it compares `HEAD` against `origin/HEAD` as if the setting were absent, and a mismatch
|
||||
returns `shouldDegrade: true` with `reason: "baseref-head-bypassed-by-hook"` and a message naming
|
||||
the file (#4588). That fallback is the comparison the check makes without the setting, and it does
|
||||
not know where your hook forks either: when `HEAD` matches `origin/HEAD` the check does not degrade,
|
||||
and the exit-42 guard below still catches a hook that forks elsewhere. A hook cannot prove itself by
|
||||
leaving a worktree behind: the #4868 observation looks for a clean agent worktree under
|
||||
`.claude/worktrees/` sitting at the current `HEAD`, but nothing on disk records *which* creator left
|
||||
it there, so one the plain harness created before you configured the hook — with `HEAD` unmoved
|
||||
since — would read as evidence for the hook. The check therefore does not consult that observation
|
||||
once a hook is in the path (#4881). To get a trusted verdict, pass the commit a hook-created
|
||||
worktree starts at as `--observed-fork-base` (described next), or remove the hook. Hooks from managed policy settings, a `--settings` file, plugins, agent frontmatter or SDK
|
||||
registrations are not visible to the check; there the exit-42 guard below is the backstop.
|
||||
|
||||
If you have a real measurement of what a worktree on this host forked from (`git rev-parse HEAD`
|
||||
inside a freshly created isolated worktree, before any commit — the full sha, not an abbreviation),
|
||||
pass it and the check evaluates that instead of inferring:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree base-check --observed-fork-base <sha>
|
||||
```
|
||||
|
||||
A match returns `reason: "observed-fork-matches-head"`; a mismatch with `"head"` set returns
|
||||
`reason: "baseref-head-ignored-by-harness"` — that worktree was not forked from HEAD despite the
|
||||
setting — and the run degrades to sequential as before.
|
||||
|
||||
**Measured on Claude Code only.** Cursor also declares harness-created worktrees; whether it honors
|
||||
`worktree.baseRef` has not been measured. There the pre-dispatch check trusts the setting the same
|
||||
way, and a host that does not honor it is caught by the exit-42 guard below. If you are on Cursor
|
||||
and see exit 42 with the setting in place, the SHA the halted executor prints is exactly the
|
||||
observation to pass here, and worth reporting.
|
||||
|
||||
Alternatively, set the value by hand in `.claude/settings.local.json`:
|
||||
|
||||
@@ -98,11 +134,11 @@ Alternatively, set the value by hand in `.claude/settings.local.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Fresh installs and upgrades of GSD Core both set `worktree.baseRef:"head"` automatically in `.claude/settings.local.json` (no-clobber) when `workflow.use_worktrees` is enabled (the default). This remains useful for GSD-managed runtimes and harmless elsewhere — post-#3659 it never silences the check on harness-managed ones.
|
||||
**Note:** Fresh installs and upgrades of GSD Core both set `worktree.baseRef:"head"` automatically in `.claude/settings.local.json` (no-clobber) when `workflow.use_worktrees` is enabled (the default). If you see the degrade warning on a fresh install, check that the key is still present — a hand-edited `settings.local.json` is the usual reason it is not.
|
||||
|
||||
Use this option when:
|
||||
|
||||
- Your runtime uses GSD-managed worktrees (Codex, OpenCode, Kimi, Kimi Code) and you regularly work on long-lived or milestone branches
|
||||
- You regularly work on long-lived or milestone branches, on any worktree-capable runtime
|
||||
- You want parallel phase execution there (faster, lower context-window pressure)
|
||||
|
||||
---
|
||||
@@ -135,7 +171,7 @@ See also: [`workflow.use_worktrees`](../CONFIGURATION.md#workflow-toggles) in th
|
||||
|
||||
## The exit-42 backstop
|
||||
|
||||
The `worktree-branch-check` guard (exit 42) remains active in all execution modes as a safety backstop. It fires only when an executor worktree's branch does not match the expected orchestrator state — a condition that should not arise once you have applied one of the options above. If you continue to see exit 42 after setting `worktree.baseRef: "head"`, run `/gsd-forensics` to investigate.
|
||||
The `worktree-branch-check` guard (exit 42) remains active in all execution modes as a safety backstop. It fires only when an executor worktree's branch does not match the expected orchestrator state — a condition that should not arise once you have applied one of the options above. It is also the observation-based check behind Option 2: with `worktree.baseRef: "head"` set, the pre-dispatch check trusts the setting and does not compare, so a host that does *not* honor it is caught here — the mismatched executors of that dispatch halt (every one of them, in a concurrent wave), and nothing is merged. If you continue to see exit 42 after setting `worktree.baseRef: "head"`, your runtime is forking from somewhere other than `HEAD`; run `/gsd-forensics` to investigate, and pass the SHA the halted executor printed as `--observed-fork-base` to `worktree base-check` to see the verdict GSD would reach with that measurement.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user