diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index 40668ff58..fc9f98756 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -671,6 +671,26 @@ fi **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. + +```bash +if [ "$RUNTIME" = "claude" ] && [ "${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 + fi +fi +``` + Capture current HEAD before spawning (used for worktree branch check): ```bash EXPECTED_BASE=$(git rev-parse HEAD) diff --git a/tests/fix-1941-quick-worktree-stale-base.test.cjs b/tests/fix-1941-quick-worktree-stale-base.test.cjs new file mode 100644 index 000000000..0b59589dd --- /dev/null +++ b/tests/fix-1941-quick-worktree-stale-base.test.cjs @@ -0,0 +1,76 @@ +// allow-test-rule: source-text-is-the-product #1941 +// Workflow .md files are the installed AI instructions — their text IS what the runtime +// loads. Testing text content tests the deployed contract. Per CONTRIBUTING.md exception matrix. + +/** + * Regression tests for bug #1941: /gsd-quick worktree executor forks from a stale base — + * up to many commits behind, not just the one-commit gap #1265 already covers. + * + * Root cause: Claude Code's isolation="worktree" forks new worktrees from origin/HEAD, not + * the live local HEAD. When prior local commits (e.g. earlier quick tasks in the same + * session, or this task's own Step 5.6 pre-dispatch plan commit) advance 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. The fix ports the + * worktree.base-check auto-degrade pattern already used by execute-phase (#683/#1369) into + * quick.md's single-dispatch path. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'quick.md'); + +describe('quick: pre-dispatch worktree base re-check (#1941)', () => { + test('workflow file exists', () => { + assert.ok(fs.existsSync(WORKFLOW_PATH), 'workflows/quick.md should exist'); + }); + + test('Step 6 runs worktree.base-check before capturing EXPECTED_BASE', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const step6Idx = content.indexOf('**Step 6: Spawn executor**'); + const baseCheckIdx = content.indexOf('worktree.base-check', step6Idx); + const expectedBaseIdx = content.indexOf('EXPECTED_BASE=$(git rev-parse HEAD)', step6Idx); + assert.ok(step6Idx !== -1, '"Step 6: Spawn executor" must exist in quick.md'); + assert.ok(baseCheckIdx !== -1, 'worktree.base-check must be invoked within Step 6'); + assert.ok(expectedBaseIdx !== -1, 'EXPECTED_BASE capture must exist within Step 6'); + assert.ok( + baseCheckIdx < expectedBaseIdx, + 'worktree.base-check must run BEFORE EXPECTED_BASE is captured so the degrade decision reflects the most current local HEAD' + ); + }); + + test('degrade check references #1941 for traceability', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + assert.ok(content.includes('#1941'), 'quick.md must reference #1941'); + }); + + test('degrade check sets USE_WORKTREES=false when shouldDegrade is true', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const baseCheckIdx = content.indexOf('worktree.base-check'); + const block = content.slice(baseCheckIdx, baseCheckIdx + 600); + assert.ok( + block.includes('shouldDegrade') && block.includes('USE_WORKTREES=false'), + 'degrade check must override USE_WORKTREES=false when shouldDegrade is true' + ); + }); + + test('degrade check guards on RUNTIME=claude (worktree isolation is Claude Code-specific)', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const baseCheckIdx = content.indexOf('worktree.base-check'); + const block = content.slice(Math.max(0, baseCheckIdx - 200), baseCheckIdx + 200); + assert.ok( + block.includes('RUNTIME') && (block.includes('"claude"') || block.includes("'claude'")), + 'degrade check must guard on RUNTIME=claude' + ); + }); + + test('degrade check names origin/HEAD as the stale fork base', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const step6Idx = content.indexOf('**Step 6: Spawn executor**'); + const nextSection = content.indexOf('\n---', step6Idx); + const section = content.slice(step6Idx, nextSection === -1 ? undefined : nextSection); + assert.ok(section.includes('origin/HEAD'), 'Step 6 must name origin/HEAD as the stale fork base'); + }); +}); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index c5f07afdb..eb71f2dd8 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -1,92 +1,91 @@ { - "add-backlog.md": 7176, - "add-phase.md": 7247, - "add-tests.md": 16945, - "add-todo.md": 8996, - "ai-integration-phase.md": 14805, + "add-backlog.md": 7132, + "add-phase.md": 7203, + "add-tests.md": 16901, + "add-todo.md": 8952, + "ai-integration-phase.md": 14761, "analyze-dependencies.md": 3887, - "audit-fix.md": 11032, - "audit-milestone.md": 17681, - "audit-uat.md": 7469, - "autonomous.md": 42474, - "check-todos.md": 9475, - "cleanup.md": 9941, - "code-review-fix.md": 23934, - "code-review.md": 31646, - "complete-milestone.md": 31071, - "debug.md": 13549, - "diagnose-issues.md": 12864, + "audit-fix.md": 10988, + "audit-milestone.md": 17637, + "audit-uat.md": 7425, + "autonomous.md": 42430, + "check-todos.md": 9431, + "cleanup.md": 9897, + "code-review-fix.md": 23890, + "code-review.md": 31602, + "complete-milestone.md": 31027, + "debug.md": 13505, + "diagnose-issues.md": 12820, "discovery-phase.md": 8651, - "discuss-phase-assumptions.md": 27028, + "discuss-phase-assumptions.md": 26984, "discuss-phase-power.md": 11273, - "discuss-phase.md": 31986, - "do.md": 10112, - "docs-update.md": 55706, - "edit-phase.md": 12927, - "eval-review.md": 9967, - "execute-phase.md": 93528, - "execute-plan.md": 32655, - "explore.md": 10541, - "extract-learnings.md": 12893, + "discuss-phase.md": 31990, + "do.md": 10068, + "docs-update.md": 55662, + "edit-phase.md": 12883, + "eval-review.md": 9923, + "execute-phase.md": 93515, + "execute-plan.md": 32611, + "explore.md": 10497, + "extract-learnings.md": 12849, "fast.md": 4149, - "forensics.md": 12531, - "graduation.md": 11622, - "health.md": 11868, + "forensics.md": 12487, + "graduation.md": 11578, + "health.md": 11824, "help.md": 1722, - "import.md": 14648, + "import.md": 14604, "inbox.md": 14407, - "ingest-docs.md": 18380, - "insert-phase.md": 8987, + "ingest-docs.md": 18336, + "insert-phase.md": 8943, "list-phase-assumptions.md": 4305, - "list-seeds.md": 6987, - "list-workspaces.md": 5699, - "manager.md": 27302, - "map-codebase.md": 20833, - "milestone-summary.md": 11842, - "mvp-phase.md": 13626, - "new-milestone.md": 32888, - "new-project.md": 66182, - "new-workspace.md": 11298, - "next.md": 20138, + "list-seeds.md": 6943, + "list-workspaces.md": 5655, + "manager.md": 27258, + "map-codebase.md": 20789, + "milestone-summary.md": 11798, + "mvp-phase.md": 13582, + "new-milestone.md": 32844, + "new-project.md": 66138, + "new-workspace.md": 11254, + "next.md": 20094, "node-repair.md": 4173, "note.md": 6563, - "pause-work.md": 14441, - "plan-milestone-gaps.md": 11809, - "plan-phase.md": 90455, - "plan-review-convergence.md": 23512, - "plant-seed.md": 11785, - "pr-branch.md": 15963, - "profile-user.md": 21246, - "progress.md": 30599, - "quick.md": 49183, - "reapply-patches.md": 20312, - "remove-phase.md": 8513, - "remove-workspace.md": 7551, - "resume-project.md": 17270, - "review.md": 43356, - "scan.md": 7732, - "secure-phase.md": 13520, + "pause-work.md": 14397, + "plan-milestone-gaps.md": 11765, + "plan-phase.md": 89775, + "plan-review-convergence.md": 23468, + "plant-seed.md": 11741, + "pr-branch.md": 15919, + "profile-user.md": 21202, + "progress.md": 30555, + "quick.md": 50408, + "reapply-patches.md": 20393, + "remove-phase.md": 8469, + "remove-workspace.md": 7507, + "resume-project.md": 17226, + "review.md": 40956, + "scan.md": 7688, + "secure-phase.md": 13476, "session-report.md": 4044, - "settings-advanced.md": 39718, - "settings-integrations.md": 15892, - "settings.md": 33457, - "ship.md": 24691, - "sketch-wrap-up.md": 14267, - "sketch.md": 20004, - "smart-entry.md": 11124, - "spec-phase.md": 31945, - "spike-wrap-up.md": 15136, - "spike.md": 24561, - "stats.md": 6762, + "settings-advanced.md": 39646, + "settings-integrations.md": 15848, + "settings.md": 33413, + "ship.md": 24647, + "sketch-wrap-up.md": 14223, + "sketch.md": 19960, + "spec-phase.md": 31752, + "spike-wrap-up.md": 15092, + "spike.md": 24517, + "stats.md": 6718, "sync-skills.md": 6125, - "thread.md": 12508, - "transition.md": 22060, - "ui-phase.md": 15521, - "ui-review.md": 11216, - "ultraplan-phase.md": 10512, + "thread.md": 12464, + "transition.md": 22016, + "ui-phase.md": 15477, + "ui-review.md": 11172, + "ultraplan-phase.md": 10468, "undo.md": 10431, - "update.md": 20914, - "validate-phase.md": 10789, - "verify-phase.md": 40772, - "verify-work.md": 38267 + "update.md": 21053, + "validate-phase.md": 10745, + "verify-phase.md": 40728, + "verify-work.md": 35171 }