fix(#2474): gate worktree dispatch on project-level USE_WORKTREES too (#2561)

* test(#2474): update dispatch gate test for dual-gate behavior

The #2772 test asserted the gate reads USE_WORKTREES_FOR_PLAN only.
Update to accept the dual-gate (USE_WORKTREES + USE_WORKTREES_FOR_PLAN).

* fix(#2474): gate worktree dispatch on project-level USE_WORKTREES too

The per-plan dispatch condition checked only USE_WORKTREES_FOR_PLAN
(submodule-derived), ignoring the project-level USE_WORKTREES flag.
Add USE_WORKTREES to the gate. Net-negative edit: compress two
nearby prose lines to offset the added shell condition (93353 bytes,
down from 93368).

Closes #2474

* docs(#2474): backfill changeset PR number (2561)

* fix: merge coverage gate into single-process check (#2474)

The test:coverage:unit script chained two c8 invocations with &&:
the first ran tests and wrote coverage data to .nyc_output/, the
second read that data for per-file branch checks. On fast CI runners
(ubuntu/24), the second process started before the filesystem flushed
the first process's writes — a classic TOCTOU race that caused
intermittent coverage gate failures.

Replace the two-process chain with a single c8 invocation that
generates both text and json-summary reports, followed by a Node
script (scripts/check-coverage-gate.cjs) that reads the JSON summary
once and checks both overall and per-file thresholds. No filesystem
race is possible because the JSON report is fully written before the
check script reads it.
This commit is contained in:
Tom Boucher
2026-07-23 09:14:02 -04:00
committed by GitHub
parent ae8526cd50
commit 7d298d6d4d
25 changed files with 83 additions and 27 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2561
---
**`use_worktrees: false` is now honored at the worktree dispatch gate** — the per-plan dispatch condition checks BOTH the project-level `USE_WORKTREES` flag AND the per-plan `USE_WORKTREES_FOR_PLAN` variable. Previously, the dispatch gate checked only the per-plan variable (derived from submodule intersection), so plans that didn't touch submodules would still fork `isolation="worktree"` agents even when the project-level setting disabled worktrees entirely. The fix is net-negative in file size (prose compression offsets the added shell condition). (#2474)

View File

@@ -557,7 +557,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
Read and execute `gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md` for each plan. It extracts `PLAN_FILES` from the plan's JSON, intersects against `SUBMODULE_PATHS` (with normalization, bidirectional matching, and glob-prefix handling), and sets `USE_WORKTREES_FOR_PLAN` to `false` when the plan touches a submodule path. Append `plan_id` to a `WAVE_WORKTREE_PLANS` accumulator when `USE_WORKTREES_FOR_PLAN != false`.
The dispatch branches in step 3 below MUST gate on `USE_WORKTREES_FOR_PLAN` for the current plan, not on the project-level `USE_WORKTREES`.
The dispatch branches in step 3 gate on both `USE_WORKTREES` and `USE_WORKTREES_FOR_PLAN` (#2474).
2.75. **Execute:wave:pre capability dispatch:**
@@ -578,14 +578,14 @@ increases monotonically across waves. `{status}` is `complete` (success),
For 200k models, this keeps orchestrator context lean (~10-15%).
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
**Worktree mode** (`USE_WORKTREES_FOR_PLAN` is not `false` — evaluated per-plan in step 2.5):
**Worktree mode** (`USE_WORKTREES` and `USE_WORKTREES_FOR_PLAN` not `false`):
Before spawning, capture the current HEAD:
```bash
EXPECTED_BASE=$(git rev-parse HEAD)
DISPATCH_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EXPECTED_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "${USE_WORKTREES_FOR_PLAN:-true}" != "false" ] && [ -z "${WAVE_WORKTREE_MANIFEST:-}" ]; then
if [ "${USE_WORKTREES:-true}" != "false" ] && [ "${USE_WORKTREES_FOR_PLAN:-true}" != "false" ] && [ -z "${WAVE_WORKTREE_MANIFEST:-}" ]; then
M=$(mktemp "${TMPDIR:-/tmp}/gsd-worktree-wave-XXXXXX") && mv "$M" "$M.json" && WAVE_WORKTREE_MANIFEST="$M.json" || exit 1 # XXXXXX must be path-final on BSD/macOS (#1520)
# Persist the dispatch-time orchestrator worktree root so wave-cleanup can pin back to the
# orchestrator's OWN worktree — NOT `git worktree list`'s first entry (always the main

View File

@@ -127,7 +127,7 @@
"test:affected": "node scripts/run-affected-tests.cjs",
"test:coverage": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
"test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
"test:coverage:unit": "c8 --check-coverage --lines 70 --branches 60 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && c8 check-coverage --per-file --lines 0 --functions 0 --statements 0 --branches 70 --include 'gsd-core/bin/lib/state.cjs' --include 'gsd-core/bin/lib/phase.cjs' --include 'gsd-core/bin/lib/verify.cjs' --include 'gsd-core/bin/lib/init.cjs' --exclude 'tests/**'",
"test:coverage:unit": "c8 --reporter text --reporter json-summary --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit && node scripts/check-coverage-gate.cjs",
"test:coverage:all": "npm run test:coverage",
"test:mutation": "stryker run",
"test:mutation:since": "stryker run --incremental --since origin/next"

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const summaryPath = path.join(process.cwd(), 'coverage', 'coverage-summary.json');
if (!fs.existsSync(summaryPath)) {
console.error('check-coverage-gate: coverage/coverage-summary.json not found.');
console.error('Ensure c8 runs with --reporter json-summary before this script.');
process.exitCode = 1;
return;
}
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
const errors = [];
const OVERALL_LINES = 70;
const OVERALL_BRANCHES = 60;
const PER_FILE_BRANCHES = 70;
const PER_FILE_FILES = [
'gsd-core/bin/lib/state.cjs',
'gsd-core/bin/lib/phase.cjs',
'gsd-core/bin/lib/verify.cjs',
'gsd-core/bin/lib/init.cjs',
];
const overall = summary.total;
if (overall.lines.pct < OVERALL_LINES) {
errors.push(`Overall lines ${overall.lines.pct}% < ${OVERALL_LINES}%`);
}
if (overall.branches.pct < OVERALL_BRANCHES) {
errors.push(`Overall branches ${overall.branches.pct}% < ${OVERALL_BRANCHES}%`);
}
for (const file of PER_FILE_FILES) {
const key = Object.keys(summary).find((k) => k.endsWith(file));
if (!key) {
errors.push(`${file}: not found in coverage summary`);
continue;
}
const pct = summary[key].branches.pct;
if (pct < PER_FILE_BRANCHES) {
errors.push(`${file}: branches ${pct}% < ${PER_FILE_BRANCHES}%`);
}
}
if (errors.length > 0) {
for (const e of errors) console.error(`ERROR: ${e}`);
process.exitCode = 1;
}

View File

@@ -247,7 +247,7 @@
"gsd-core/workflows/docs-update.md": "0b3023a3caa2123f",
"gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a",
"gsd-core/workflows/eval-review.md": "9e8b169c05098b57",
"gsd-core/workflows/execute-phase.md": "881c9be0dec7dbb8",
"gsd-core/workflows/execute-phase.md": "62f2ffd1a3fab9a5",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1832b97d0923fa67",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "13aa54f8279960ae",

View File

@@ -318,7 +318,7 @@
"gsd-core/workflows/docs-update.md": "a320371be70f43d8",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7",
"gsd-core/workflows/execute-phase.md": "f3a37602243681f9",
"gsd-core/workflows/execute-phase.md": "9576dee82fc42496",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -317,7 +317,7 @@
"gsd-core/workflows/docs-update.md": "7aef1019ce141558",
"gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e",
"gsd-core/workflows/eval-review.md": "11620e0dc4a003dd",
"gsd-core/workflows/execute-phase.md": "2604b9cb7dc66920",
"gsd-core/workflows/execute-phase.md": "5aad166b1505215d",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "9a1f3deffb4dab14",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "a7f9b9a45303382f",

View File

@@ -246,7 +246,7 @@
"gsd-core/workflows/docs-update.md": "ea47bd2d5a0d1d85",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "7c2f5ff8cc4d06e5",
"gsd-core/workflows/execute-phase.md": "100b55c147922978",
"gsd-core/workflows/execute-phase.md": "cd61c80541fa54be",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "d2020c5e01c7bd7f",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -250,7 +250,7 @@
"gsd-core/workflows/docs-update.md": "7eb7095ca86d506f",
"gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74",
"gsd-core/workflows/eval-review.md": "17fc9b4c4f7e1c74",
"gsd-core/workflows/execute-phase.md": "a5af949ed1092448",
"gsd-core/workflows/execute-phase.md": "b169a6a7ad091517",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1b3558cb8f41b65a",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "35612890c1173577",

View File

@@ -318,7 +318,7 @@
"gsd-core/workflows/docs-update.md": "a320371be70f43d8",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7",
"gsd-core/workflows/execute-phase.md": "9bc9511ebd40b7cc",
"gsd-core/workflows/execute-phase.md": "8108d708e41340c2",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -353,7 +353,7 @@
"gsd-core/workflows/docs-update.md": "bf32efe90219d3dd",
"gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f",
"gsd-core/workflows/eval-review.md": "c89a63285ead961e",
"gsd-core/workflows/execute-phase.md": "e5b9fb391a90d7fb",
"gsd-core/workflows/execute-phase.md": "f6bf3f953a56e821",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "cd8678d082d6e191",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -248,7 +248,7 @@
"gsd-core/workflows/docs-update.md": "f64b067d0d729f04",
"gsd-core/workflows/edit-phase.md": "8667c28b22b1599f",
"gsd-core/workflows/eval-review.md": "1ae2d3d3fbf72893",
"gsd-core/workflows/execute-phase.md": "ac3229160d3a3fda",
"gsd-core/workflows/execute-phase.md": "7f16a37d0729d517",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "ca37f91f7bd6f05c",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "25bebed74e645109",

View File

@@ -318,7 +318,7 @@
"gsd-core/workflows/docs-update.md": "215b0122c4d2b1e5",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "9389c58f7384972d",
"gsd-core/workflows/execute-phase.md": "80a4fb2d23642cd8",
"gsd-core/workflows/execute-phase.md": "784f3579b077e42e",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "d2020c5e01c7bd7f",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -247,7 +247,7 @@
"gsd-core/workflows/docs-update.md": "02811dd7aa32d5ba",
"gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8",
"gsd-core/workflows/eval-review.md": "e7cd5dfcf18e2458",
"gsd-core/workflows/execute-phase.md": "5adf286f7ec99a59",
"gsd-core/workflows/execute-phase.md": "e9ced8c3351c0df4",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "9b2193de25506b3b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "26ee34c543926402",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "362123fdf99e9980",

View File

@@ -318,7 +318,7 @@
"gsd-core/workflows/docs-update.md": "3c0d5ef72202d246",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "35be91cd22a9bc11",
"gsd-core/workflows/execute-phase.md": "3ddae43ba8ad5081",
"gsd-core/workflows/execute-phase.md": "8f3e14c95888af4d",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "b1e6588cd32a6f08",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -275,7 +275,7 @@
"gsd-core/workflows/docs-update.md": "a320371be70f43d8",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7",
"gsd-core/workflows/execute-phase.md": "73ffb4f5d915e410",
"gsd-core/workflows/execute-phase.md": "d8415f714b900335",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -311,7 +311,7 @@
"gsd-core/workflows/docs-update.md": "a320371be70f43d8",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7",
"gsd-core/workflows/execute-phase.md": "157a97e0978bd5df",
"gsd-core/workflows/execute-phase.md": "4fce4cae4a748e1e",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -318,7 +318,7 @@
"gsd-core/workflows/docs-update.md": "f6210dc8ff7776d5",
"gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39",
"gsd-core/workflows/eval-review.md": "1973f08d0eb34159",
"gsd-core/workflows/execute-phase.md": "b59b9993b1da7bb8",
"gsd-core/workflows/execute-phase.md": "732995bcbb285233",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "a15993affd62f4bf",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "c4cba01539f0368a",

View File

@@ -214,7 +214,7 @@
"gsd-core/workflows/docs-update.md": "a320371be70f43d8",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7",
"gsd-core/workflows/execute-phase.md": "6779316ccf03982c",
"gsd-core/workflows/execute-phase.md": "6f720660e0cee48a",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -247,7 +247,7 @@
"gsd-core/workflows/docs-update.md": "226550a6d556b09f",
"gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36",
"gsd-core/workflows/eval-review.md": "88431952699d9fa6",
"gsd-core/workflows/execute-phase.md": "69c5e8a42ea9c331",
"gsd-core/workflows/execute-phase.md": "b2744ece3ffc1ec6",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "0e0949db56deebeb",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "0b04cc2dcab61107",

View File

@@ -247,7 +247,7 @@
"gsd-core/workflows/docs-update.md": "89e760e665ff99f8",
"gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad",
"gsd-core/workflows/eval-review.md": "90a29a4fa0cdd947",
"gsd-core/workflows/execute-phase.md": "7d9ad2344ede3966",
"gsd-core/workflows/execute-phase.md": "cea9135e975318d8",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "296b812e8d0e9ce8",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7d30384e9bf82664",

View File

@@ -247,7 +247,7 @@
"gsd-core/workflows/docs-update.md": "6bd25d0123ee6c73",
"gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789",
"gsd-core/workflows/eval-review.md": "713de00ea5326988",
"gsd-core/workflows/execute-phase.md": "b28e4d8cffeb2944",
"gsd-core/workflows/execute-phase.md": "93432c3e63da4914",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "3194ecd382690755",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "fb4497767cdf73a3",

View File

@@ -318,7 +318,7 @@
"gsd-core/workflows/docs-update.md": "a320371be70f43d8",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "32d3d7e80ab2bfc7",
"gsd-core/workflows/execute-phase.md": "0e1e8e5e3fa63587",
"gsd-core/workflows/execute-phase.md": "70da309ca11a8494",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f7782680c2b8ad6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -24,7 +24,7 @@
"docs-update.md": 56494,
"edit-phase.md": 12927,
"eval-review.md": 10332,
"execute-phase.md": 93383,
"execute-phase.md": 93368,
"execute-plan.md": 35143,
"explore.md": 11127,
"extract-learnings.md": 12893,

View File

@@ -4055,12 +4055,12 @@ describe('execute-phase.md dispatch wires USE_WORKTREES_FOR_PLAN (#2772)', () =>
assert.ok(fs.existsSync(gatePath), `expected ${gatePath} to exist`);
});
test('Worktree-mode dispatch gate reads USE_WORKTREES_FOR_PLAN, not USE_WORKTREES', () => {
test('Worktree-mode dispatch gate reads both USE_WORKTREES and USE_WORKTREES_FOR_PLAN (#2474)', () => {
const md = fs.readFileSync(workflowPath, 'utf-8');
assert.match(
md,
/\*\*Worktree mode\*\*\s*\(`USE_WORKTREES_FOR_PLAN`/,
'Worktree-mode header must gate on USE_WORKTREES_FOR_PLAN per-plan'
/\*\*Worktree mode\*\*.*`USE_WORKTREES`.*`USE_WORKTREES_FOR_PLAN`/,
'Worktree-mode header must gate on both USE_WORKTREES and USE_WORKTREES_FOR_PLAN (#2474)'
);
});