* fix(#2652): gate quick/diagnose dispatch on dispatch.isolation, not runtime name quick.md and diagnose-issues.md kept the pre-#2584 `RUNTIME != "claude"` worktree gate, so every non-Claude runtime failed closed regardless of the capability it negotiated — including Codex, which declares orchestrator-worktree. Route both through the negotiated dispatch.isolation seam via a new shared reference, and migrate the two execute-phase reference fragments that carried the same runtime-name gate. - new gsd-core/references/dispatch-isolation-gate.md: canonical ISOLATION resolution, harness-flag resolution, single-agent degrade rule - quick.md / diagnose-issues.md read the gate; dispatch uses the {harnessFlag} placeholder rather than a hardcoded isolation="worktree" - execute-phase-wave-guard.md / execute-phase-between-wave-reset.md: migrate [ "$RUNTIME" = "claude" ] -> [ "$ISOLATION" = "harness-worktree" ] - every degrade site now clears BOTH USE_WORKTREES and ISOLATION; clearing one dispatched an isolated agent with no base guard and no manifest - parity guard in host-integration.test.cjs scans workflows AND references and matches six reintroduction shapes - migrate four tests that pinned the pre-#2584 runtime-name contract Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#2652): use the /gsd:<cmd> namespace in the isolation degrade messages The degrade warnings cited /gsd-execute-phase, the retired hyphen form that slash-command-namespace.test.cjs rejects in Claude-facing source. Same length, so the quick.md size budget is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(#2652): add changeset for PR #2728 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#2652): normalize dispatch-site paths to forward slashes for Windows path.relative() returns backslash-separated paths on Windows, so the #2652 dispatch-site parity test compared "gsd-core\workflows\quick.md" against the hardcoded forward-slash literal "gsd-core/workflows/quick.md" and failed on every windows-latest CI lane. Normalize with .replace(/\\/g, '/'), matching the existing convention used elsewhere in this suite (e.g. tests/branch-no-track-guard.test.cjs:37). * test(#2652): restore the size-growth acknowledgment The rebase dropped tests/emitted-drift-ack.json. #2757/#2758 fixed the ATTRIBUTION axis, but the SIZE-GROWTH axis is independent: diagnose-issues.md (+2086) and quick.md (+230) still need an ack naming them and saying why. Verified: 65/66 without it (both files named), 66/66 with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#2652): convert execute-plan.md Pattern A onto the dispatch-isolation gate Pattern A hardcoded `isolation="worktree"` — Claude Code's own literal — gated only on `workflow.use_worktrees`, with no capability negotiation at all. It is the same defect #2652 fixes at the other four sites, just a different shape: the file contains no RUNTIME variable, so the new detector correctly does not flag it. Concrete break: a Codex user who follows this PR's own newly-documented pattern and sets `workflow.use_worktrees: true` to get isolated dispatch via /gsd:quick then runs a plan through /gsd-execute-plan Pattern A, and hits an unconverted path — either an Agent() call erroring on an unrecognized parameter or silent unisolated execution, depending on host tolerance. Pattern A is a single-agent dispatch site through the host's own subagent tool, so it takes the same treatment as quick.md and diagnose-issues.md: resolve ISOLATION/HARNESS_FLAG through the canonical reference, degrade to sequential on orchestrator-worktree hosts, and substitute the host's declared {harnessFlag} instead of Claude Code's literal. while the area was open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(#2652): add the INVENTORY row for dispatch-isolation-gate.md, refresh CONTEXT Two bookkeeping gaps flagged in review: INVENTORY.md had no row for the new gsd-core/references/dispatch-isolation-gate.md. INVENTORY-MANIFEST.json was regenerated correctly and its --check only diffs a live directory scan against the committed manifest, so CI passed regardless — but gen-inventory-manifest.cjs's own stderr guidance says to add the matching INVENTORY.md row. This is the repo's named "Inventory Drift" pattern. Placed with the dispatch/isolation cluster (worktree-branch-check, runtime-aware-dispatch) rather than alphabetically, matching how that table is grouped. CONTEXT.md's Host-Integration Interface entry still described dispatch.isolation as "declared and negotiated but not yet consumed by any scheduler — Phase 1 of #2584". That was already stale before this PR (execute-phase graduated in Phase 3) and more so now with three single-agent dispatch sites consuming it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(#2652): detect reversed-operand runtime gates; add a permutation property All five reintroduction regexes assumed $RUNTIME on the LEFT of the comparison, so `[ "claude" != "$RUNTIME" ]` — the same gate written backwards — evaded every one of them. Verified against the old patterns before fixing: all four reversed shapes (single bracket, double bracket, test builtin, JS template) scored EVADED. Each comparison shape is now generated in both operand orders from a single template, so a shape cannot be added in one order and forgotten in the other. The mutation table gains the four reversed cases. Also adds the fast-check property review suggested in place of the hand-rolled cases: it generates the cross product of the axes an author actually varies — bracket form, operator, operand order, quoting, spacing, runtime id — so a permutation the hand-written patterns miss surfaces here rather than in production. The 11 explicit cases stay as named regression anchors. execute-plan.md joins the scan's required-identities list now that it is a converted dispatch site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(#2652): acknowledge the execute-plan.md size growth The Pattern A conversion adds 811 bytes to an emitted workflow. Per #2719 the size axis needs its own acknowledgment, independent of attribution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(#2652): repin the execute-plan.md PROSE_ALLOWLIST line after the rebase The #2751 command-position gate pins its prose exemptions by line number. This branch inserts the dispatch-isolation resolution above the `validated downstream by gsd-tools uat classify-coverage` sentence, moving it from execute-plan.md:387 to :397 — which fired the gate twice for one displacement (an un-allowlisted mention at 397, a stale entry at 387). The prose itself is unchanged from next; only the pin moves. Fixes #2652 * fix(#2652): gate the #2649 base-check on ISOLATION in diagnose-issues.md The rebase onto next merged #2649's pre-dispatch base-check textually, but its degrade flipped USE_WORKTREES after ISOLATION was already resolved, so the degrade never reached the dispatch decision. Gate the block on ISOLATION = "harness-worktree" and degrade ISOLATION itself, the same pairing quick.md already uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#2652): key quick.md post-dispatch bookkeeping on ISOLATION, not the Claude literal Review Blocker: the manifest append (l.822), worktree merge-back (l.825), and its skip clause (l.839) all conditioned on the literal isolation="worktree" — Claude Code's own rendering of {harnessFlag}. Cursor renders --worktree, so a newly-unblocked isolated Cursor run created a worktree whose committed work was never merged back and never cleaned up, silently. All three now key on ISOLATION = "harness-worktree" at dispatch. The existing parity detector cannot catch this class (its ISOLATION_TOKEN treats the literal as a legitimate marker), so this adds a dedicated literal-condition detector with a discrimination proof against both pre-fix sentences, a benign-mention control, and a positive pin on all three re-keyed conditions. Verified fail-first against the pre-fix quick.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#2652): scope the use_worktrees=false install stamp to isolation=none runtimes `_stampNonClaudeRuntimeDefaults` rewrote every non-Claude runtime's `workflow.use_worktrees` read to `--default false`. That default resolved before `gsd_run query dispatch-isolation` was ever consulted, so the five runtimes that declare worktree support — cursor (harness-worktree) and codex/opencode/kimi/kimi-code (orchestrator-worktree) — got ISOLATION=none regardless of what they negotiated. The gate this PR migrates dispatch onto was therefore still deciding isolation by runtime name, one layer down. The stamp's #1521 premise was that worktree isolation *was* Claude Code's isolation="worktree" spawn parameter, which no other host honored. #2584 replaced that premise with the negotiated capability. The stamp is now scoped to runtimes whose negotiated isolation really is `none`, where the default it writes is the outcome the resolver reaches anyway. `_negotiatedDispatchIsolation` mirrors routeDispatchIsolation's resolution against the same registry — closed vocabulary, a harness-worktree host must declare its flag, an orchestrator-worktree host must carry a descriptor that resolves — and fails closed to `none` on anything else, so an undeclared or unknown runtime keeps today's behavior. Two #1515 tests pinned the superseded premise for codex and are re-pointed at the new contract rather than deleted: the safety property they protect is now held by the isolation gate's fail-closed resolution, not by a name-scoped install-time default. Verified fail-first — all five assertions red against the pre-fix source, green after. * test(#2652): acknowledge the emitted ripple and re-point the end-to-end stamp proof Scoping the use_worktrees stamp changes emitted output, and two gates caught it. `gsd-core/workflows/execute-phase.md` now differs at emit time for the five hosts that declare worktree support (cursor harness-worktree; codex, opencode, kimi, kimi-code orchestrator-worktree) — the source file is byte-identical, only the stamp is gone. Acknowledged in this PR's fragment. `tests/install.test.cjs`'s real-install assertion pinned the superseded premise end-to-end, asserting codex receives `--default false`. Re-pointed rather than deleted, matching the two unit tests: it now proves codex keeps the unstamped `true` read. A second arm installs windsurf — which declares isolation `none` — and asserts the false stamp is still applied there, so the change cannot silently degrade into "never stamp" without a test noticing. The ack entry collides with `2658-trae-instruction-file-path.json`, which is fully spent (merged via #2925, so all 25 of its entries are present at base and gate nothing) and is pruned for the same reason and by the same rule as the spent `2649-*` fragment this PR already removed. #2566 prunes the same file for the same collision on `new-project.md`; a delete/delete merges cleanly either way, and the base-side cleanup would make both unnecessary. * fix(#2652): re-record the sentinel when a dispatch site degrades isolation Review Blocker B1/B2/B3. Every isolation degrade in a dispatch site is decided in shell, where routeDispatchIsolation cannot see it. That resolver persists whatever it resolved to the run-scoped sentinel as an unconditional side effect (#3045), so a degrade that only reassigns $ISOLATION leaves the sentinel asserting harness-worktree while the dispatch correctly omits the harness flag. The shipped PreToolUse guard reads the sentinel at the instant of the Agent() call and denies that mismatch with exit 2 — the work does not run unisolated, it does not run at all. Latent on this branch and lands on rebase, since8f75e275(#3045) is not yet in the merge-base. Four sites now push the final shell-computed value through the same single write path with --force-isolation, matching the idiom #3045 established in executor-isolation-dispatch.md: - quick.md, after the #1941 base-check degrade - diagnose-issues.md, after the config-gate degrades and after #2649's - execute-plan.md Pattern A, before spawning - references/dispatch-isolation-gate.md, both degrade paths, plus a new "Re-record after every degrade" section — the canonical file taught the defect, so fixing only the call sites would leave the source of truth wrong Tests assert the RECORDED value, not $ISOLATION. Asserting the local variable is what let this class through: $ISOLATION was already `none` at every site and the defect was entirely in what reached the sentinel. Each workflow's own degrade block is executed under a gsd_run stub that captures the write, with a fail-first proof that the pre-fix shape records nothing (while $ISOLATION reads `none` in both), plus a coverage guard so a new degrade site cannot skip it. Also corrects the drift-ack rationale (review Minor 5): @-references are eagerly inlined, so extracting the gate does not reduce loaded context. The reason to extract is single-sourcing across five dispatch sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(#2652): satisfy the new CRLF-portability and cleanup lint rules in host-integration.test.cjs next's local/no-crlf-fragile-split and no-raw-rmsync-in-tests rules now cover the fenced-block regexes, log-line split, and temp-dir removal this suite added: bash-fence matchers and line counting accept \r\n, and the raw fs.rmSync becomes helpers.cleanup (Windows-EBUSY retry budget). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#2652): restore next's 2658 ack fragment, minus the one colliding key trek-e (PR #2728, 2026-08-07): the branch deleted tests/emitted-drift-acks/2658-trae-instruction-file-path.json wholesale while next had modified it. That was correct against the 08-03 base, where the fragment was fully spent; it is wrong against next @1d208e5a, which still carries 23 live entries. next's copy is restored byte-identical except for the single key that genuinely collides with this PR's own fragment, gsd-core/workflows/execute-phase.md. Both acks name that path for different deltas -- 2658's is the trae CLAUDE.md replacement-target rewrite, ours is the emit-time _stampNonClaudeRuntimeDefaults ripple from review round 3. Per the ack-lifecycle law (#2789), an entry already at the base is spent and inert, so this PR's entry is the live one and 2658's is dropped. This follows the guidance given on #2566 in the 08-06 round: "Regenerate rather than delete -- the collision is one entry." Verified: lint-emitted-drift-ack ok (0 problems, 357 keys, no cross-source duplicates); emitted-attribution 170/170 with GSD_EMITTED_BASE=upstream/next; host-integration 222/222; runtime-converters 130/130. * fix(#2652): bound the degrade-harness spawn and close the round-6 majors B1 (CI red, ours): tests/host-integration.test.cjs spawned bash with no timeout, violating local/no-unbounded-spawn. `next` deleted the allowlist outright (#3148), so the merge-commit run flags it even though this branch still carries the file's grandfathered entry. Bounded at 15s, with a named failure on timeout/signal rather than an opaque `exited null`. M1: add a parity test between `_negotiatedDispatchIsolation` (install time) and `routeDispatchIsolation` (dispatch time). Both read the same capability registry and the same `resolveOrchestratorExec`, but duplicate the DECISION on top of them across two surfaces with no call edge between them, so neither symbol appears in the other's impact graph and nothing static can catch them drifting apart. The resolver leg drives the real gsd-tools CLI per registered runtime, both ways it is really called: with `--cwd-target` (the executor spawn, which resolves the orchestrator descriptor — the same question install time asks) and without it (the `Resolve ISOLATION` call every dispatch site makes first, which does not). The second leg is what catches an orchestrator host whose descriptor stops resolving: the install would stamp `use_worktrees=false` while the workflow gate still reported `orchestrator-worktree`. M2: add install-level Cursor coverage. A real `--cursor` install, then the gate blocks that install emitted, run against the gsd-tools that install emitted, with the runtime declared through `.planning/config.json` — the tier `resolveRuntime` actually reads — and any ambient GSD_RUNTIME blanked, so the install has to reach the right resolver on its own. It then performs the documented `{harnessFlag}` substitution against the `Agent()` call that install emitted and asserts on the rendered dispatch: exactly one emitted Agent() call carries the slot, it is the gsd-executor / gsd-debugger dispatch rather than some other call in the same file, and rendering it yields `--worktree` with no residual placeholder and no `isolation="worktree"`. This is artifact-level — it proves the emitted wiring, not a live Cursor host invocation. Asserting the shell variable alone would have stayed green if the placeholder were deleted from the emitted dispatch, or drifted onto the reviewer call beside it. M3: diagnose-issues.md inlined a reordered copy of the reference this PR introduces as the single source of truth. It now reads the reference the same way quick.md and execute-plan.md do; the drift-ack entry is corrected to describe what the file actually contains, and to name the four files that reference the gate rather than claiming five. M4: CONTEXT.md still called `resolveOrchestratorExec` UNCONSUMED in the same paragraph this PR edits. It has been consumed since #2584 Phase 3 — routed through `query dispatch-isolation --json` and process-spawned by executor-isolation-dispatch.md — and #2652 adds a second consumer. Every new assertion verified fail-first against a real mutation: cursor's negotiated isolation (breaks the target-bound parity leg), the no-target orchestrator branch in routeDispatchIsolation (breaks the gate parity leg), cursor's harnessIsolationFlag (breaks the resolved value), deleting `{harnessFlag}` from quick.md's emitted Agent() call (breaks the slot), and moving it onto the code-reviewer dispatch (breaks the wrong-call guard). Refs #2652 * fix(#2652): serialize unisolated diagnosis, scope the execute-plan gate to dispatching patterns Round-7 review findings (independent cross-AI pass over the whole PR against the current base). BLOCKER — diagnose-issues.md announced sequential mode and then fanned out. The `orchestrator-worktree` degrade sets ISOLATION=none and prints "debug agents run sequentially on the main working tree", but the spawn step still said "All agents spawn in single message (parallel execution)". On Codex, OpenCode and Kimi that dispatched N unisolated debuggers concurrently against the primary checkout — the exact outcome the degrade exists to prevent, and reachable only because this PR removed the FATAL that used to stop those hosts earlier. Fan-out is now keyed on ISOLATION: parallel only when each agent has its own worktree, one at a time otherwise. BLOCKER — execute-plan.md Pattern B could not dispatch at all on Claude or Cursor. The gate recorded `harness-worktree` to the #3045 sentinel, but only Pattern A carries `{harnessFlag}`; Pattern B's segment executors carry none, and `hooks/gsd-agent-isolation-guard.js` blocks precisely that mismatch with exit 2. Segments are unisolated BY DESIGN — each continues on the working tree the previous one left behind, so per-agent worktrees would break the sequence — so Pattern B now records `none` before its first dispatch and dispatches without the flag. MAJOR — the same gate ran before routing was chosen, so an isolation-`none` host with `use_worktrees=true` hit the fail-closed FATAL even when routing would have selected Pattern C, which is fully inline and dispatches nothing. Resolution now happens after the pattern is known, and Pattern C skips it. MAJOR — tests/host-integration.test.cjs fed `fs.readFileSync` output straight to bash. The `\r?\n` fence regex guards only the delimiter, leaving embedded CR on every line of the captured body — DEFECT.WINDOWS-CRLF-TEST-PORTABILITY, which helpers.cjs documents by name. Now reads through `readFileNormalized`. MAJOR — the "every dispatch-site degrade block re-records" test hand-listed three files, so its name was a claim its scan could not support. The scan is now derived from the workflow/reference tree (SCAN_ROOTS/collectMarkdown hoisted to module scope so there is one definition, not two). Verified fail-first against execute-plan.md — a file the previous scan never opened. The two wave fragments are exempt because they delegate the re-record to per-plan-worktree-gate.md via USE_WORKTREES_FOR_PLAN; that delegation is now ASSERTED, so deleting the delegate fails this test instead of widening a hole silently. MINOR — the changeset claimed the FATAL was gone for "non-Claude runtimes" full stop. Narrowed: isolation-`none` hosts still fail closed when worktrees are explicitly enabled, which is the contract rather than the defect. Two further findings were investigated and rejected, with evidence: - Raw `spawnSync` vs `tests/helpers/process-seam.cjs`: the seam exposes runNode/runGit/runHook and cannot express the `bash -c` harness these tests need; `installAndRead` in this same file is byte-identical to the base and still uses raw spawnSync with an explicit timeout, which is the form the lint sanctions. Migrating only the new call sites would split the file's convention for no safety gain. - `pending-migration-to-typed-ir` on the runtime-converters parity test: the annotation and the rendered-text loop both exist at the merge-base under #3090. This PR extends an already-tracked test rather than adding a new one under a category CONTRIBUTING closes to new tests. Refs #2652 * test(#2652): re-point the execute-plan prose allowlist at its shifted line `PROSE_ALLOWLIST` in tests/no-bare-gsd-tools-command-position.test.cjs keys entries by LINE NUMBER. The previous commit added the post-routing isolation block to execute-plan.md, which pushed the `validated downstream by gsd-tools uat classify-coverage` prose mention from line 397 to 414. That broke the guard in both directions at once: the entry at 397 went stale, and the real mention at 414 became an unallowlisted offender. Caught by CI (7 red jobs, all shard 3/3 plus ubuntu-22) rather than locally, because I verified only the suites I believed the change touched. Any edit to a workflow .md shifts line numbers, and this repo carries line-keyed allowlists — so a workflow edit needs the full suite, not a subset. Refs #2652 * test(#2652): route the new subprocesses through the process seam Retracting a rejection I made on the record. In the round-6 response I argued these call sites could keep a hand-rolled `spawnSync` because the seam exposes only runNode/runGit/runHook and cannot express `bash -c`, and because `installAndRead` in the same file uses that shape. The first half was true and irrelevant, the second half is not a licence: CONTRIBUTING is unambiguous — "Anything that shells out goes through tests/helpers/process-seam.cjs — never a hand-rolled spawnSync/execFileSync in your suite", and "Never use try/finally inside test bodies." `runHook` already documents `interpreter: 'bash'` for running a shell script, so writing the harness to a file complies without extending the seam. I had the rule and the seam's own documentation in front of me and reasoned around both. Converted: - host-integration.test.cjs degrade harness: spawnSync('bash', ['-c', …]) → runHook(scriptFile, [], { interpreter: 'bash' }). - install.test.cjs cursor gate: `which bash` probe → process.platform; the installer spawn → runNode(…, { env: installSpawnEnv({HOME, USERPROFILE}) }), which also blanks ambient GSD_HOME/runtime-location vars that could otherwise make capability discovery host-dependent; the emitted-gate spawn → runHook(gateScript, [], { interpreter: 'bash' }). - Three try/finally test bodies → t.after(). Class-norm timeouts: tests/helpers/timeouts.cjs arrived with this branch's latest base merge, so the literals written earlier (15000/120000/60000) now duplicate PROBE_TIMEOUT_MS and INSTALL_TIMEOUT_MS. Imported instead — that module exists because INSTALL_TIMEOUT_MS had already drifted 60s→120s once after a real bench ETIMEDOUT. Deliberately NOT converted: `installAndRead`'s spawnSync, which is byte-identical to the merge-base and predates this PR — converting shared scaffolding is an unrelated change. Verified equivalent, not assumed: argv/cwd/env/encoding/timeout and every assertion are preserved; t.after() still cleans up on the assertion-failure path the try/finally covered; and the cursor test still resolves Cursor under a hostile ambient GSD_RUNTIME=claude. Refs #2652 * fix(#2652): replace the falsified use_worktrees doc row; distinguish an unresolvable gate from a declared none Round-8 review findings. BLOCKER — docs/CONFIGURATION.md's `Non-Claude note` asserted three things this PR overturns: that worktree isolation "no other runtime honors" (Cursor declares harness-worktree with `--worktree`, and this PR's own install test asserts that flag reaching the emitted Agent() slot), that non-Claude installs default the key to `false`, and that forcing `true` always fails closed. Replaced with the capability-based description, and the `#1515, #1521` citation dropped — those are the two issues whose premise this PR removes. The reviewer flagged that the fix is merge-order dependent, because #2531 rewrites the same row and its replacement text is written in anticipation of this PR landing. Rather than pick an order, BOTH sides are now order-independent: #2531's "Current default … until #2652" paragraph becomes a plain troubleshooting note, and this row states the capability rule without asserting a stamp state. Whichever merges first, the row is correct; the second merge is a textual conflict at worst. MINOR — the gate reported a capability verdict the tool never returned. `ISOLATION=$(… || echo "none")` made a shim-resolution failure, a non-zero exit and an empty stdout indistinguishable from a declared `none`, so a transient query failure aborted /gsd:quick on Claude Code with "runtime 'claude' declares no executor-isolation primitive" — false. The gate now tracks ISOLATION_RESOLVED separately: both paths still fail closed, but only a real verdict claims the host declares nothing; the unresolved branch says it could not resolve and points at the shim. Fixed in the canonical reference so every dispatch site inherits it. MINOR — quick.md:527 cited #2649 for its own degrade; that is #1941, and #2649 is the diagnose-issues/execute-plan gate. Corrected, and the distinction stated so the next reader does not chase it. MINOR — quick.md:413 (manifest init) and :429 (worktree_branch_check embed) still branched on USE_WORKTREES while dispatch, manifest-append, merge-back and the skip clause had all moved to ISOLATION. Safe only by coincidence — both now key on ISOLATION. MINOR — the diff removes a second drift-ack entry (the execute-phase.md key from 2658-trae-instruction-file-path.json), forced by the same duplicate-key lint rule as the 2649 removal. Disclosed in the PR comment; the earlier disclosure covered only one of the two. Verified: lint:ci green; 300/300 across host-integration, fix-1941-quick-worktree-stale-base, execute-phase-wave and workflow-guard. Refs #2652 * test(#2652): anchor the emitted-gate finder on the heading, not the assignment `b89c3fbf` added a finder that located the gate's `Resolve ISOLATION` block by the literal `ISOLATION=$(gsd_run query dispatch-isolation --raw`. `f3bccf21` then split that assignment into `_ISOLATION_RAW`/`ISOLATION_RESOLVED` so a shim failure stops masquerading as a declared `none` — and the finder stopped matching. The test did not report the drift it exists to catch; it reported "emitted dispatch-isolation-gate.md has no Resolve ISOLATION bash block" and went red, and stayed red because the earlier full-suite run was read from a truncated log. Anchored on the heading instead. The workflows tell a dispatch site to run the `Resolve ISOLATION` and `Resolve the harness flag` blocks BY NAME, so the heading is the contract and the body is free to change under it. Verified: 413/413 in tests/install.test.cjs. The test still bites — mutating the gate's `ISOLATION="$_ISOLATION_RAW"` to `ISOLATION=none` turns it red (the emitted gate then resolves cursor to none and exits 1 instead of printing harness-worktree), and reverting restores green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(#2652): wire the canonical resolver at the one dispatch site that still inlined the old shape Codex review of the whole PR on the new base found one Major, and it was real. executor-isolation-dispatch.md declares references/dispatch-isolation-gate.md canonical at line 10, then kept the OLDER resolver inline: `|| echo "none"`, no ISOLATION_RESOLVED. So the one site that resolves isolation for the wave path collapsed a shim failure into a declared `none` and aborted with "runtime '$RUNTIME' declares no executor-isolation primitive" — false for a Claude or Cursor user whose resolver merely failed to answer. Still fail-closed, so not an unsafe-dispatch hole, but the correction this PR is about was unwired at the site that matters most. Replaced with the gate's exact shape: capture the raw value, track ISOLATION_RESOLVED, and emit the "could not resolve" FATAL when no verdict was learned. Added a regression test in the #2652 dispatch-site parity suite: every file that ASSIGNS from `gsd_run query dispatch-isolation --raw` must carry ISOLATION_RESOLVED, must not use the collapsing form, and must have a distinct unresolved message. Nothing covered this before — install.test.cjs checks the emitted REFERENCE, not each site's own inline copy, which is exactly how the two drifted apart. The test's first draft also flagged quick.md, diagnose-issues.md and execute-plan.md. That was a false positive worth recording: those three @-reference the gate and only make `--force-isolation` re-record calls, which carry no verdict. The predicate now matches an assignment from the resolver, not any mention of it, so it flags sites that can actually be wrong. Verified by mutation: restoring the collapsing line reds the new test. Validated: lint:ci clean; full suite shows the same 7 known failures as the pre-change baseline — #1160 _resolveManifest and the #3053 quick_id host-timezone tests (both reproduce on pristine next @33fca50d), plus helpers-cleanup "outside os.tmpdir()", which fails only in a worktree. * test(#2652): close two vacuous-pass holes in the new inline-resolver guard Codex cleared the push and flagged the guard test itself. Both holes were real. SCAN_ROOTS already yields references/dispatch-isolation-gate.md, and the test appended it a second time, so the candidate list was [executor, gate, gate] and `length >= 2` was satisfiable by the gate alone. If the executor site had dropped out of the predicate — the exact regression the test exists to catch — it would still have passed. Paths are deduped and the assertion now pins the two expected inliner identities instead of a count. The collapse detector keyed on `ISOLATION=$(…)`, so `_ISOLATION_RAW=$(… || echo "none")` restored the identical defect while satisfying every other assertion (ISOLATION_RESOLVED still appears in the file). Codex mutation-probed exactly that and it passed. The pattern now matches any assignment target and any `|| … echo` tail. Verified: that mutation now reds the test. Test-only change; the workflow bash is byte-identical to the commit the full suite ran green against. lint:ci clean, host-integration 223/223. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
40 KiB
With --full flag: enables the complete quality pipeline — discussion + research + plan-checking + verification. One flag for everything.
With --validate flag: enables plan-checking (max 2 iterations) and post-execution verification only. Use when you want quality guarantees without discussion or research.
With --discuss flag: lightweight discussion phase before planning. Surfaces assumptions, clarifies gray areas, captures decisions in CONTEXT.md so the planner treats them as locked.
With --research flag: spawns a focused research agent before planning. Investigates implementation approaches, library options, and pitfalls. Use when you're unsure how to approach a task.
Granular flags are composable: --discuss --research --validate gives the same result as --full.
<required_reading> Read all files referenced by the invoking prompt's execution_context before starting. </required_reading>
<available_agent_types> Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
- gsd-phase-researcher — Researches technical approaches for a phase
- gsd-planner — Creates detailed plans from phase scope
- gsd-plan-checker — Reviews plan quality before execution
- gsd-executor — Executes plan tasks, commits, creates SUMMARY.md
- gsd-verifier — Verifies phase completion, checks quality gates
- gsd-code-reviewer — Reviews source files for bugs, security issues, and code quality </available_agent_types>
Parse $ARGUMENTS for:
--fullflag → store$FULL_MODE=true,$DISCUSS_MODE=true,$RESEARCH_MODE=true,$VALIDATE_MODE=true--validateflag → store$VALIDATE_MODE=true--discussflag → store$DISCUSS_MODE=true--researchflag → store$RESEARCH_MODE=true- Remaining text → use as
$DESCRIPTIONif non-empty
After parsing, normalize: if $DISCUSS_MODE and $RESEARCH_MODE and $VALIDATE_MODE are all true, set $FULL_MODE=true. This ensures --discuss --research --validate is treated identically to --full.
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --default "" 2>/dev/null || echo "")
If response_language is set: All user-facing questions, prompts, and explanations in this workflow MUST be presented in {response_language}. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated.
If $DESCRIPTION is empty after parsing, prompt user interactively:
Text mode (workflow.text_mode: true in config or --text flag): Set TEXT_MODE=true if --text is present in $ARGUMENTS OR text_mode from init JSON is true. When TEXT_MODE is active, replace every AskUserQuestion call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where AskUserQuestion is not available.
AskUserQuestion(
header: "Quick Task",
question: "What do you want to do?",
followUp: null
)
Store response as $DESCRIPTION.
If still empty, re-prompt: "Please provide a task description."
Display banner based on active flags:
If $FULL_MODE (all phases enabled — --full or all granular flags):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (FULL)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion + research + plan checking + verification enabled
If $DISCUSS_MODE and $VALIDATE_MODE (no research):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS + VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion + plan checking + verification enabled
If $DISCUSS_MODE and $RESEARCH_MODE (no validate):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS + RESEARCH)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion + research enabled
If $RESEARCH_MODE and $VALIDATE_MODE (no discuss):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (RESEARCH + VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Research + plan checking + verification enabled
If $DISCUSS_MODE only:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion phase enabled — surfacing gray areas before planning
If $RESEARCH_MODE only:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (RESEARCH)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Research phase enabled — investigating approaches before planning
If $VALIDATE_MODE only:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Plan checking + verification enabled
Step 2: Initialize
DISCUSS_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--discuss([[:space:]]|$) ]]; then DISCUSS_PARAM="--discuss"; fi
RESEARCH_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--research([[:space:]]|$) ]]; then RESEARCH_PARAM="--research"; fi
VALIDATE_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--validate([[:space:]]|$) ]]; then VALIDATE_PARAM="--validate"; fi
FULL_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--full([[:space:]]|$) ]]; then FULL_PARAM="--full"; fi
INIT=$(gsd_run query init.quick "$DESCRIPTION" $DISCUSS_PARAM $RESEARCH_PARAM $VALIDATE_PARAM $FULL_PARAM)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_PLANNER=$(gsd_run query agent-skills gsd-planner)
AGENT_SKILLS_EXECUTOR=$(gsd_run query agent-skills gsd-executor)
AGENT_SKILLS_CHECKER=$(gsd_run query agent-skills gsd-plan-checker)
AGENT_SKILLS_VERIFIER=$(gsd_run query agent-skills gsd-verifier)
Parse JSON for: planner_model, executor_model, checker_model, verifier_model, reviewer_model, commit_docs, branch_name, quick_id, slug, date, timestamp, quick_dir, task_dir, roadmap_exists, planning_exists, response_language.
init.quick does not emit dedicated state_path/project_path fields, so derive them from the already-absolute quick_dir (#2376 — files handed to a spawned subagent must resolve regardless of that subagent's own cwd):
STATE_PATH="$(dirname "${quick_dir}")/STATE.md"
PROJECT_PATH="$(dirname "${quick_dir}")/PROJECT.md"
USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")
RUNTIME=$(gsd_run query config-get runtime --default claude --raw 2>/dev/null || echo "claude")
Resolve isolation now (#2584/#2652). Read @gsd-core/references/dispatch-isolation-gate.md
and run its Resolve ISOLATION, Single-agent dispatch sites, and Resolve the harness flag
blocks in order; they set ISOLATION/HARNESS_FLAG via query dispatch-isolation.
ISOLATION — not RUNTIME — gates every worktree decision below. Substitute {harnessFlag}
in Step 6's Agent() with $HARNESS_FLAG+comma when ISOLATION = "harness-worktree", else
empty. {harnessFlag}
is a template placeholder, not a shell variable.
If USE_WORKTREES is not "false", run a startup orphan sweep before spawning any executors. This reaps locked worktrees whose lock-owner process is dead, whose branch is merged into the default branch, and whose lock file mtime is older than 5 minutes. Running it at startup prevents accumulation of orphaned worktrees from prior sessions that exited without cleanup (#3707).
if [ "$USE_WORKTREES" != "false" ]; then
gsd_run query worktree.reap-orphans 2>/dev/null || true
fi
If the project uses git submodules, worktree isolation is unsafe only when the quick task touches a submodule path. The previous behavior unconditionally disabled worktree isolation whenever .gitmodules existed, which penalised every quick task in a submodule project even when the task was nowhere near a submodule. Parse submodule paths from .gitmodules so the executor can act on actual submodule paths rather than the mere file's existence:
# Parse submodule paths from .gitmodules once (empty if no .gitmodules).
# SUBMODULE_PATHS is a newline-separated list of repo-relative paths used as
# a fail-loud commit-time guard inside the quick-task executor — if the
# executor stages any path that falls inside SUBMODULE_PATHS, it must abort
# the commit and surface the conflict rather than silently corrupting the
# submodule state.
if [ -f .gitmodules ]; then
SUBMODULE_PATHS=$(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null | awk '{print $2}')
else
SUBMODULE_PATHS=""
fi
Quick mode does not have a pre-declared files_modified list (the task is freeform), so use a fail-loud guard at commit time: when the executor stages files for the quick-task commit, if any staged path falls inside a SUBMODULE_PATHS entry, abort with a clear error explaining that worktree-isolated commits cannot safely span submodule boundaries — the user can re-run with workflow.use_worktrees=false to fall back to sequential execution on the main tree. If SUBMODULE_PATHS is empty (no .gitmodules in the repo), worktree isolation proceeds normally.
If roadmap_exists is false: Error — Quick mode requires an active project with ROADMAP.md. Run /gsd:new-project first.
Quick tasks can run mid-phase - validation only checks ROADMAP.md exists, not phase status.
Step 2.5: Handle quick-task branching
If branch_name is empty/null: Skip and continue on the current branch.
If branch_name is set: Check out the quick-task branch before any planning commits.
The new branch must fork off the project's default branch (origin/HEAD), not
off whatever HEAD happens to be checked out — otherwise consecutive quick tasks
compound on top of each other and stay unpushed (#2916). If $branch_name
already exists locally, reuse it as-is so resumed work is not rebased.
DEFAULT_BRANCH=$(gsd_run query git.base-branch 2>/dev/null \
|| git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||' \
|| echo main)
if git show-ref --verify --quiet "refs/heads/$branch_name"; then
git switch "$branch_name" \
|| { echo "ERROR: Could not switch to existing quick-task branch '$branch_name'." >&2; exit 1; }
else
# Fetch the default branch so origin/$DEFAULT_BRANCH is current. If the fetch
# fails (offline, no remote, auth failure) AND we have no local copy of
# origin/$DEFAULT_BRANCH to fall back on, abort — creating the branch off
# arbitrary HEAD is exactly the bug #2916 fixed.
if ! git fetch --quiet origin "$DEFAULT_BRANCH"; then
if ! git show-ref --verify --quiet "refs/remotes/origin/$DEFAULT_BRANCH"; then
echo "ERROR: Could not fetch origin/$DEFAULT_BRANCH and no local copy exists. Refusing to create '$branch_name' off the current HEAD (#2916). Resolve the remote/network issue and retry." >&2
exit 1
fi
echo "WARNING: git fetch origin $DEFAULT_BRANCH failed; using the local copy of origin/$DEFAULT_BRANCH as base." >&2
fi
if [ -n "$(git status --porcelain)" ]; then
echo "WARNING: Uncommitted changes present. Carrying them onto the new quick-task branch — they will be branched off origin/$DEFAULT_BRANCH (not the previous-task HEAD)."
else
# Best-effort: fast-forward the local default branch so subsequent local
# work sees the latest tip. Failure here is non-fatal because we always
# create the new branch directly from origin/$DEFAULT_BRANCH below.
git switch --quiet "$DEFAULT_BRANCH" 2>/dev/null \
&& git merge --ff-only --quiet "origin/$DEFAULT_BRANCH" 2>/dev/null \
|| true
fi
# Pin the new branch to origin/$DEFAULT_BRANCH so the start point is
# deterministic regardless of which branch we are currently on (#2916).
# On success HEAD is exactly at origin/$DEFAULT_BRANCH, so a post-creation
# merge-base / "ahead-of" guard would be unreachable — the explicit base
# argument here is the single source of correctness for #2916.
# --no-track: with the default branch.autoSetupMerge=true, checkout -b from a
# remote-tracking ref wires branch.<name>.merge to refs/heads/$DEFAULT_BRANCH
# (origin/master), so a GUI sync pushes quick-task commits straight onto
# origin/$DEFAULT_BRANCH, bypassing PR review (#2498).
git checkout -b "$branch_name" "origin/$DEFAULT_BRANCH" --no-track \
|| { echo "ERROR: Could not create '$branch_name' from origin/$DEFAULT_BRANCH (#2916)." >&2; exit 1; }
fi
All quick-task commits for this run stay on that branch. User handles merge/rebase afterward.
Step 3: Create task directory
mkdir -p "${task_dir}"
Step 4: Create quick task directory
Create the directory for this quick task:
QUICK_DIR="${task_dir}"
mkdir -p "$QUICK_DIR"
Report to user:
Creating quick task ${quick_id}: ${DESCRIPTION}
Directory: ${QUICK_DIR}
Store $QUICK_DIR for use in orchestration.
If section_manifest is null or "discussion-phase" is in its included list: read and execute gsd-core/workflows/quick/steps/discussion-phase.md. Otherwise skip — do not read the file.
If section_manifest is null or "research-phase" is in its included list: read and execute gsd-core/workflows/quick/steps/research-phase.md. Otherwise skip — do not read the file.
Step 5: Spawn planner (quick mode)
If $VALIDATE_MODE: Use quick-full mode with stricter constraints.
If NOT $VALIDATE_MODE: Use standard quick mode.
Display: ◆ Spawning planner... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
Agent(
prompt="
<planning_context>
**Mode:** ${VALIDATE_MODE ? 'quick-full' : 'quick'}
**Directory:** ${QUICK_DIR}
**Description:** ${DESCRIPTION}
<files_to_read>
- ${STATE_PATH} (Project State)
- ./CLAUDE.md or ./.claude/CLAUDE.md (if exists — follow project-specific guidelines)
${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — locked, do not revisit)' : ''}
${RESEARCH_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-RESEARCH.md (Research findings — use to inform implementation choices)' : ''}
</files_to_read>
${AGENT_SKILLS_PLANNER}
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules
</planning_context>
<constraints>
- Create a SINGLE plan with 1-3 focused tasks
- Quick tasks should be atomic and self-contained
${RESEARCH_MODE ? '- Research findings are available — use them to inform library/pattern choices' : '- No research phase'}
${VALIDATE_MODE ? '- Target ~40% context usage (structured for verification)' : '- Target ~30% context usage (simple, focused)'}
${VALIDATE_MODE ? '- MUST generate `must_haves` in plan frontmatter (truths, artifacts, key_links)' : ''}
${VALIDATE_MODE ? '- Each task MUST have `files`, `action`, `verify`, `done` fields' : ''}
</constraints>
<output>
Write plan to: ${QUICK_DIR}/${quick_id}-PLAN.md
Return: ## PLANNING COMPLETE with plan path
</output>
",
subagent_type="gsd-planner",
model="{planner_model}",
description="Quick plan: ${DESCRIPTION}"
)
ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
After planner returns:
- Verify plan exists at
${QUICK_DIR}/${quick_id}-PLAN.md - Extract plan count (typically 1 for quick tasks)
- Report: "Plan created: ${QUICK_DIR}/${quick_id}-PLAN.md"
If plan not found, error: "Planner failed to create ${quick_id}-PLAN.md"
If section_manifest is null or "plan-checker-loop" is in its included list: read and execute gsd-core/workflows/quick/steps/plan-checker-loop.md. Otherwise skip — do not read the file.
If section_manifest is null or "worktree-pre-dispatch-commit" is in its included list: read and execute gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md. Otherwise skip — do not read the file.
Step 6: Spawn executor
Auto-degrade to sequential if HEAD has diverged from the worktree fork base (#1941, mirrors
execute-phase's #683/#1369 guard). Claude Code's isolation="worktree" forks new worktrees from
origin/HEAD, not the live local HEAD. If a prior quick task in this session (or the Step 5.6
pre-dispatch plan commit above) advanced local HEAD without an intervening git push,
origin/HEAD stays pinned to a stale ancestor and the executor's worktree_branch_check guard
halts with a base-mismatch fatal — potentially many commits behind, not just one. Run this check
immediately before capturing EXPECTED_BASE so it reflects the most current local state.
if [ "$ISOLATION" = "harness-worktree" ] && [ "${USE_WORKTREES:-true}" != "false" ]; then
_QUICK_SHOULD_DEGRADE=$(gsd_run query worktree.base-check --pick shouldDegrade 2>/dev/null || true)
if [ "$_QUICK_SHOULD_DEGRADE" = "true" ]; then
_QUICK_DEGRADE_MSG=$(gsd_run query worktree.base-check --pick message 2>/dev/null || true)
[ -n "$_QUICK_DEGRADE_MSG" ] && printf '%s\n' "$_QUICK_DEGRADE_MSG" >&2
echo "⚠ [#1941] Worktree fork base diverged from orchestrator HEAD — auto-degrading to sequential mode for this quick task to avoid a base-mismatch halt." >&2
USE_WORKTREES=false
ISOLATION=none
fi
fi
# Re-resolve (and, as a side effect, re-persist) now that the base-check
# auto-degrade above may have changed $ISOLATION since the Step 2 gate's
# `dispatch-isolation` call (#3045). That first call recorded the NATURALLY
# resolved mode into the run-scoped sentinel the isolation guard hooks read
# (hooks/gsd-agent-isolation-guard.js, hooks/gsd-cursor-subagent-start.js via
# hooks/lib/isolation-sentinel.js). The degrade above is decided HERE, in
# shell — the resolver cannot see it — so without this the sentinel still
# asserts `harness-worktree` while the dispatch below correctly omits the
# harness flag, and the guard denies the dispatch with exit 2. `--force-isolation`
# pushes the FINAL, shell-computed value through that SAME single write path
# (`none` also clears the stored harnessFlag, since none applies to sequential
# dispatch). Best-effort: a write failure here must never fail the task — the
# guards' own sentinel-absent fallback is safe, just less precise.
gsd_run query dispatch-isolation --raw --force-isolation "$ISOLATION" >/dev/null 2>&1 || true
Capture current HEAD before spawning (used for worktree branch check):
EXPECTED_BASE=$(git rev-parse HEAD)
if [ "$ISOLATION" = "harness-worktree" ]; then # keyed on ISOLATION like every other dispatch-coupled branch (#2652)
# BSD/macOS mktemp only randomizes XXXXXX when it is the final path component, so make a
# suffixless temp then append the extension — portable across BSD + GNU (#1520).
QUICK_WORKTREE_MANIFEST=$(mktemp "${TMPDIR:-/tmp}/gsd-quick-worktree-XXXXXX") && mv "$QUICK_WORKTREE_MANIFEST" "${QUICK_WORKTREE_MANIFEST}.json" && QUICK_WORKTREE_MANIFEST="${QUICK_WORKTREE_MANIFEST}.json" || exit 1
printf '{"worktrees":[]}\n' > "$QUICK_WORKTREE_MANIFEST"
export QUICK_WORKTREE_MANIFEST
fi
Spawn gsd-executor with plan reference:
Agent(
prompt="
Execute quick task ${quick_id}.
${ISOLATION === "harness-worktree" ? `
<worktree_branch_check>
ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispatch, read \`gsd-core/references/worktree-branch-check.md\`, substitute \`{EXPECTED_BASE}\` with the base SHA captured above (${EXPECTED_BASE}), substitute \`{EXPECTED_BASE_ALTERNATE}\` with \`${QUICK_PLAN_PARENT}\` when it differs from \`${EXPECTED_BASE}\` (otherwise empty), and replace this note with that fragment's \`<worktree_branch_check>\` block so the dispatched prompt carries the runnable guard verbatim — do not pass this instruction through in its place.
</worktree_branch_check>
FIRST ACTION after the worktree branch check: ensure the quick PLAN.md exists at a worktree-rooted relative path before any Read/Edit/Write path can be primed. If \`${QUICK_DIR}/${quick_id}-PLAN.md\` is absent, materialize it from the shared git object store:
\`\`\`bash
QUICK_PLAN_COMMIT="${QUICK_PLAN_COMMIT}"
QUICK_PLAN_PATH="${QUICK_DIR}/${quick_id}-PLAN.md"
if [ ! -f "$QUICK_PLAN_PATH" ]; then
mkdir -p "$(dirname "$QUICK_PLAN_PATH")"
git show "${QUICK_PLAN_COMMIT}:${QUICK_PLAN_PATH}" > "$QUICK_PLAN_PATH" || {
echo "FATAL: unable to materialize quick plan from ${QUICK_PLAN_COMMIT}:${QUICK_PLAN_PATH}; refusing to continue." >&2
exit 42
}
fi
\`\`\`
` : ''}
<files_to_read>
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan)
- ${STATE_PATH} (Project state)
- ./CLAUDE.md or ./.claude/CLAUDE.md (Project instructions, if exists)
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
</files_to_read>
${AGENT_SKILLS_EXECUTOR}
<submodule_commit_guard>
SUBMODULE_PATHS for this project: ${SUBMODULE_PATHS}
If SUBMODULE_PATHS is non-empty, you MUST run this fail-loud guard immediately
before EVERY git commit you create during this quick task (after \`git add\`,
before \`git commit\`). Quick mode does not have a pre-declared files_modified
list, so the guard runs at commit time:
\`\`\`bash
SUBMODULE_PATHS=\"${SUBMODULE_PATHS}\"
if [ -n \"\$SUBMODULE_PATHS\" ]; then
STAGED=\$(git diff --cached --name-only)
for sm_raw in \$SUBMODULE_PATHS; do
sm=\"\${sm_raw#./}\"
sm=\"\${sm%/}\"
[ -z \"\$sm\" ] && continue
for f_raw in \$STAGED; do
f=\"\${f_raw#./}\"
f=\"\${f%/}\"
case \"\$f\" in
\"\$sm\"|\"\$sm\"/*)
echo \"ABORT: staged path \$f_raw falls inside submodule \$sm — worktree-isolated commits cannot safely span submodule boundaries. Re-run with workflow.use_worktrees=false.\" >&2
exit 1 ;;
esac
done
done
fi
\`\`\`
If the guard aborts, do NOT attempt the commit, do NOT remove the staged files,
and do NOT continue subsequent tasks. Surface the abort message in your
SUMMARY.md and stop — the user must rerun with worktrees disabled.
</submodule_commit_guard>
<constraints>
- Execute all tasks in the plan
- Commit each task atomically (code changes only)
- Run the <submodule_commit_guard> bash block before every \`git commit\` if SUBMODULE_PATHS is non-empty
- Create summary at: ${QUICK_DIR}/${quick_id}-SUMMARY.md with `status: complete` in SUMMARY frontmatter (required so the audit-open milestone-close scanner recognises the task as done, not [unknown])
- Do NOT commit docs artifacts (SUMMARY.md, STATE.md, PLAN.md) — the orchestrator handles the docs commit in Step 8
- Do NOT update ROADMAP.md (quick tasks are separate from planned phases)
</constraints>
",
subagent_type="gsd-executor",
model="{executor_model}",
{harnessFlag}
description="Execute: ${DESCRIPTION}"
)
ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
If the executor ran isolated (ISOLATION = "harness-worktree" at dispatch), append its returned {agent_id, worktree_path, branch, expected_base, allowed_bases} metadata to QUICK_WORKTREE_MANIFEST before cleanup. Set expected_base to ${EXPECTED_BASE} and allowed_bases to ["${EXPECTED_BASE}", "${QUICK_PLAN_PARENT}"] with duplicates removed. If any required field is unavailable, stop and ask for recovery; do not discover global worktrees.
After executor returns:
-
Worktree cleanup: If the executor ran isolated (
ISOLATION = "harness-worktree"at dispatch), merge the worktree branch back and clean up:QUICK_WORKTREE_MANIFEST=${QUICK_WORKTREE_MANIFEST:-$WAVE_WORKTREE_MANIFEST} [ -n "${QUICK_WORKTREE_MANIFEST:-}" ] && [ -f "$QUICK_WORKTREE_MANIFEST" ] || { echo "BLOCKED: missing QUICK_WORKTREE_MANIFEST; refusing broad worktree cleanup (#3384)." >&2 exit 1 } # Prefer the bounded cleanup helper. It verifies branch identity, expected # base, deletion diffs, merge result, and worktree removal before branch # deletion. If it blocks, resolve the reported manifest entry and rerun. # Fail closed: SDK refusal (safety guard #3174/#3384) must surface — do not swallow exit 1. gsd_run query worktree.cleanup-wave --manifest "$QUICK_WORKTREE_MANIFEST" || exit 1If
ISOLATIONwas not"harness-worktree"at dispatch (including a #1941 base-check degrade — that is this file's degrade; #2649 is thediagnose-issues.md/execute-plan.mdone), skip this step.ISOLATED-RUN RECOVERY — FAIL SAFE (#1292): When an isolated (worktree) run is rejected — the user declines to merge it, the orchestrator surfaces recovery guidance for a blocked/halted plan, or the run over-reached the requested scope — the worktree-isolation contract MUST hold through recovery. Do NOT propose continuing on
main/the primary checkout as the default or recommended recovery path. Default to a safe halt and offer: (a) re-attempt in a fresh, narrowly-scoped worktree, or (b) inspect or discard the rejected worktree without merging. Any path that edits the primary checkout requires an explicit, clearly-labeled confirmation from the user first — editingmaindirectly is never the proposed or default option for a run the user configured to be isolated. -
Verify summary exists at
${QUICK_DIR}/${quick_id}-SUMMARY.md -
Extract commit hash from executor output
-
Report completion status
Known Claude Code bug (classifyHandoffIfNeeded): If executor reports "failed" with error classifyHandoffIfNeeded is not defined, this is a Claude Code runtime bug — not a real failure. Check if summary file exists and git log shows commits. If so, treat as successful.
If summary not found, error: "Executor failed to create ${quick_id}-SUMMARY.md"
Note: For quick tasks producing multiple plans (rare), spawn executors in parallel waves per execute-phase patterns.
Step 6.25: Code review (auto)
Skip this step entirely if $FULL_MODE is false.
Capability gate:
EXECUTE_POST_HOOKS_JSON=$(gsd_run loop render-hooks execute:post --raw)
Resolve active step hooks from EXECUTE_POST_HOOKS_JSON where kind == "step" and ref.skill == "code-review".
If no active code-review step hook exists, skip with message "Code review skipped (code-review capability inactive)".
Scope files from executor's commits:
# Find the diff base: last commit before quick task started
# Use git log to find commits referencing the quick task id, then take the parent of the oldest
QUICK_COMMITS=$(git log --oneline --format="%H" --grep="${quick_id}" 2>/dev/null)
if [ -n "$QUICK_COMMITS" ]; then
DIFF_BASE=$(echo "$QUICK_COMMITS" | tail -1)^
# Verify parent exists (guard against first commit in repo)
git rev-parse "${DIFF_BASE}" >/dev/null 2>&1 || DIFF_BASE=$(echo "$QUICK_COMMITS" | tail -1)
else
# No commits found for this quick task — skip review
DIFF_BASE=""
fi
if [ -n "$DIFF_BASE" ]; then
CHANGED_FILES=$(git diff --name-only "${DIFF_BASE}..HEAD" -- . ':!.planning' 2>/dev/null | tr '\n' ' ')
else
CHANGED_FILES=""
fi
If CHANGED_FILES is empty, skip with "No source files changed — skipping code review."
Invoke review:
Agent(
prompt="Review these files for bugs, security issues, and code quality.
Files: ${CHANGED_FILES}
Output: ${QUICK_DIR}/${quick_id}-REVIEW.md
Depth: quick",
subagent_type="gsd-code-reviewer",
model="{reviewer_model}"
)
ORCHESTRATOR RULE — CODEX RUNTIME: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
If review produces findings, display advisory message. Error handling: Failures are non-blocking — catch and proceed.
If section_manifest is null or "quick-verification" is in its included list: read and execute gsd-core/workflows/quick/steps/quick-verification.md. Otherwise skip — do not read the file.
Step 7: Update STATE.md
Update STATE.md with quick task completion record.
7a. Check if "Quick Tasks Completed" section exists:
Read STATE.md and check for ### Quick Tasks Completed section.
7b. If section doesn't exist, create it:
Insert after ### Blockers/Concerns section:
If $VALIDATE_MODE:
### Quick Tasks Completed
| # | Description | Date | Commit | Status | Directory |
|---|-------------|------|--------|--------|-----------|
If NOT $VALIDATE_MODE:
### Quick Tasks Completed
| # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------|
Note: If the table already exists, match its existing column format. If adding --validate (or --full) to a project that already has quick tasks without a Status column, add the Status column to the header and separator rows, and leave Status empty for the new row's predecessors.
7c. Append new row to table:
Use date from init:
If $VALIDATE_MODE (or table has Status column):
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | ${VERIFICATION_STATUS} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
If NOT $VALIDATE_MODE (and table has no Status column):
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
For a schema-safe append outside this workflow (e.g. from fast.md), gsd_run quick-tasks-append --task <text> performs the equivalent write via the shared, schema-backed appendQuickTaskRow helper (#2133, ADR-2143 §3/§7).
7d. Update "Last activity" line:
Use date from init:
Last activity: ${date} - Completed quick task ${quick_id}: ${DESCRIPTION}
Use Edit tool to make these changes atomically
Step 8: Final commit and completion
Stage and commit quick task artifacts. This step MUST always run — even if the executor already committed some files (e.g. when running without worktree isolation). The gsd-tools.cjs query commit command (or legacy gsd-tools.cjs commit) handles already-committed files gracefully.
Build file list:
${QUICK_DIR}/${quick_id}-PLAN.md${QUICK_DIR}/${quick_id}-SUMMARY.md.planning/STATE.md- If
$DISCUSS_MODEand context file exists:${QUICK_DIR}/${quick_id}-CONTEXT.md - If
$RESEARCH_MODEand research file exists:${QUICK_DIR}/${quick_id}-RESEARCH.md - If
$VALIDATE_MODEand verification file exists:${QUICK_DIR}/${quick_id}-VERIFICATION.md - If
${QUICK_DIR}/${quick_id}-deferred-items.mdexists:${QUICK_DIR}/${quick_id}-deferred-items.md
# Explicitly stage all artifacts before commit — PLAN.md may be untracked
# if the executor ran without worktree isolation and committed docs early
# Filter .planning/ files from staging if commit_docs is disabled (#1783)
COMMIT_DOCS=$(gsd_run query config-get commit_docs 2>/dev/null || echo "true")
if [ "$COMMIT_DOCS" = "false" ]; then
file_list_filtered=$(echo "${file_list}" | tr ' ' '\n' | grep -v '^\.planning/' | tr '\n' ' ')
git add ${file_list_filtered} 2>/dev/null
else
git add ${file_list} 2>/dev/null
fi
gsd_run query commit "docs(quick-${quick_id}): ${DESCRIPTION}" --files ${file_list}
Get final commit hash:
commit_hash=$(git rev-parse --short HEAD)
Display completion output:
If $VALIDATE_MODE:
---
GSD > QUICK TASK COMPLETE (VALIDATED)
Quick Task ${quick_id}: ${DESCRIPTION}
${RESEARCH_MODE ? 'Research: ' + QUICK_DIR + '/' + quick_id + '-RESEARCH.md' : ''}
Summary: ${QUICK_DIR}/${quick_id}-SUMMARY.md
Verification: ${QUICK_DIR}/${quick_id}-VERIFICATION.md (${VERIFICATION_STATUS})
Commit: ${commit_hash}
---
Ready for next task: /gsd:quick ${GSD_WS}
If NOT $VALIDATE_MODE:
---
GSD > QUICK TASK COMPLETE
Quick Task ${quick_id}: ${DESCRIPTION}
${RESEARCH_MODE ? 'Research: ' + QUICK_DIR + '/' + quick_id + '-RESEARCH.md' : ''}
Summary: ${QUICK_DIR}/${quick_id}-SUMMARY.md
Commit: ${commit_hash}
---
Ready for next task: /gsd:quick ${GSD_WS}
<success_criteria>
- ROADMAP.md validation passes
- User provides task description
--full,--validate,--discuss, and--researchflags parsed from arguments when present--fullsets all booleans ($FULL_MODE,$DISCUSS_MODE,$RESEARCH_MODE,$VALIDATE_MODE)- Slug generated (lowercase, hyphens, max 40 chars)
- Quick ID generated (YYMMDD-xxx format, 2s Base36 precision)
- Directory created at
.planning/quick/YYMMDD-xxx-slug/ - (--discuss) Gray areas identified and presented, decisions captured in
${quick_id}-CONTEXT.md - (--research) Research agent spawned,
${quick_id}-RESEARCH.mdcreated ${quick_id}-PLAN.mdcreated by planner (honors CONTEXT.md decisions when --discuss, uses RESEARCH.md findings when --research)- (--validate) Plan checker validates plan, revision loop capped at 2
${quick_id}-SUMMARY.mdcreated by executor- (--validate)
${quick_id}-VERIFICATION.mdcreated by verifier - STATE.md updated with quick task row (Status column when --validate)
- Artifacts committed </success_criteria>