From 2815720aad0686c6f40927c4e4b557f383ac6129 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 2 Jun 2026 23:02:25 -0400 Subject: [PATCH] fix(#621): route plan-phase post-planning-gaps through gsd_run launcher (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#621): route plan-phase post-planning-gaps through gsd_run launcher The post-planning-gaps step in gsd-core/workflows/plan-phase.md invoked gsd-tools via a hardcoded `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs"` path twice on one line — for the gap-analysis call and its nested init.plan-phase phase_req_ids query — bypassing the gsd_run launcher that every other call in the workflow uses. On non-default install/runtime layouts (relocated/global installs, non-Claude runtimes) the hardcoded path does not resolve, so the assistant reported the gap-analysis tool as "not found" and fell back to a frontmatter-only coverage check even when a working install existed. #3668 fixed this class earlier in the file but missed this block. Route both invocations through gsd_run, matching the rest of the workflow. No hardcoded $HOME gsd-tools path remains in plan-phase.md. Co-Authored-By: Claude Opus 4.8 * chore(#621): add changeset for plan-phase gsd_run gap-analysis fix Co-Authored-By: Claude Opus 4.8 * test(#621): update bug-2851 §13e guard for the gsd_run gap-analysis form The #621 fix migrates plan-phase.md's post-planning-gaps gap-analysis call from the hardcoded node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" form to the gsd_run launcher (the canonical resolvable form every other call in the file uses). bug-2851's §13e subtest pinned that line to the absolute-$HOME form and now asserts stale behavior. Update the §13e assertion to require `gsd_run gap-analysis` (still rejecting a regression to the hardcoded $HOME path), retitle it, and note the migration in the file header. The generic bare-`gsd-tools` sweeper test is unchanged — gsd_run is a resolvable launcher, not a bare gsd-tools call. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/curious-lynx-travel.md | 5 ++ gsd-core/workflows/plan-phase.md | 2 +- .../bug-2851-workflow-bare-gsd-tools.test.cjs | 10 ++- ...1-plan-phase-gap-analysis-gsd-run.test.cjs | 84 +++++++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 .changeset/curious-lynx-travel.md create mode 100644 tests/bug-621-plan-phase-gap-analysis-gsd-run.test.cjs diff --git a/.changeset/curious-lynx-travel.md b/.changeset/curious-lynx-travel.md new file mode 100644 index 000000000..c70ad03ad --- /dev/null +++ b/.changeset/curious-lynx-travel.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 635 +--- +**`/gsd-plan-phase` gap-analysis no longer reports the tool as "not found" on non-default installs** — the post-planning-gaps step now resolves `gsd-tools` through the workflow's `gsd_run` launcher instead of a hardcoded `$HOME/.claude/...` path, so it works under relocated/global installs and non-Claude runtimes instead of silently falling back to a frontmatter-only coverage check. diff --git a/gsd-core/workflows/plan-phase.md b/gsd-core/workflows/plan-phase.md index 8ae790a72..63b08be16 100644 --- a/gsd-core/workflows/plan-phase.md +++ b/gsd-core/workflows/plan-phase.md @@ -1628,7 +1628,7 @@ one place before execution begins. POST_PLANNING_GAPS=$(gsd_run query config-get workflow.post_planning_gaps --default true 2>/dev/null || echo true) if [ "$POST_PLANNING_GAPS" = "true" ]; then # Scope to this phase's mapped REQ-IDs (#447); null/TBD skips the requirements comparison (CONTEXT.md decisions still reported), mirroring §13. - node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" gap-analysis --phase-dir "${PHASE_DIR}" --phase-req-ids "$(node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" query init.plan-phase "$PHASE" --pick phase_req_ids 2>/dev/null || echo TBD)" + gsd_run gap-analysis --phase-dir "${PHASE_DIR}" --phase-req-ids "$(gsd_run query init.plan-phase "$PHASE" --pick phase_req_ids 2>/dev/null || echo TBD)" fi ``` diff --git a/tests/bug-2851-workflow-bare-gsd-tools.test.cjs b/tests/bug-2851-workflow-bare-gsd-tools.test.cjs index da2f790c7..d75ad980f 100644 --- a/tests/bug-2851-workflow-bare-gsd-tools.test.cjs +++ b/tests/bug-2851-workflow-bare-gsd-tools.test.cjs @@ -7,6 +7,10 @@ * * node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" [args] * + * As of #621, the §13e gap-analysis call uses the `gsd_run` launcher (the + * canonical resolvable form) instead of the absolute-$HOME path above. + * Both forms are resolvable; `gsd_run` is now the preferred canonical form. + * * Some workflow markdown files leaked the bare `gsd-tools ` form, * which fails with `command not found` at runtime. * @@ -143,7 +147,7 @@ describe('bug-2851: workflow files must not call bare `gsd-tools` (#2245 sweep r ); }); - test('plan-phase.md §13e gap-analysis uses canonical absolute-path invocation', () => { + test('plan-phase.md §13e gap-analysis uses the gsd_run launcher (resolvable invocation, #621)', () => { const planPhase = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf-8'); const blocks = extractShellBlocks(planPhase); let foundGapAnalysisCall = false; @@ -153,8 +157,8 @@ describe('bug-2851: workflow files must not call bare `gsd-tools` (#2245 sweep r foundGapAnalysisCall = true; assert.match( line, - /\bnode\s+["']?\$HOME\/\.claude\/gsd-core\/bin\/gsd-tools\.cjs["']?\s+gap-analysis\b/, - `gap-analysis call must use canonical absolute-path invocation, got: ${line.trim()}`, + /\bgsd_run\s+gap-analysis\b/, + `gap-analysis must use the gsd_run launcher (not a hardcoded $HOME path), got: ${line.trim()}`, ); } } diff --git a/tests/bug-621-plan-phase-gap-analysis-gsd-run.test.cjs b/tests/bug-621-plan-phase-gap-analysis-gsd-run.test.cjs new file mode 100644 index 000000000..f99e6c3f4 --- /dev/null +++ b/tests/bug-621-plan-phase-gap-analysis-gsd-run.test.cjs @@ -0,0 +1,84 @@ +// allow-test-rule: source-text-is-the-product +// The post-planning-gaps gap-analysis invocation is deployed workflow text the +// runtime executes; the contract is that it routes through the gsd_run launcher, +// not a hardcoded $HOME path (#621). + +/** + * Regression test for #621: plan-phase gap-analysis must route through gsd_run + * + * Prior to the fix, line 1631 of plan-phase.md hardcoded: + * node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" gap-analysis ... + * twice on the same line, breaking non-default install layouts. + * + * After the fix, both invocations route through gsd_run (the launcher defined + * at line ~34 of the same file that resolves gsd-tools.cjs against + * RUNTIME_DIR / git-toplevel / PATH / $HOME in order). + */ + +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', + 'plan-phase.md' +); + +// ─── Fixture ────────────────────────────────────────────────────────────────── + +const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8'); + +// ─── #621 regression: gap-analysis routes through gsd_run ──────────────────── + +describe('plan-phase workflow: post-planning-gaps gap-analysis uses gsd_run launcher (#621)', () => { + test('gap-analysis call routes through gsd_run, not hardcoded node path', () => { + assert.ok( + workflow.includes('gsd_run gap-analysis'), + 'workflow must invoke gap-analysis via gsd_run, not a hardcoded node path' + ); + }); + + test('inner phase_req_ids query also routes through gsd_run', () => { + assert.ok( + workflow.includes('gsd_run query init.plan-phase'), + 'workflow must invoke the inner phase_req_ids query via gsd_run launcher' + ); + }); + + test('no hardcoded node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" invocations remain (#621)', () => { + const hardcodedCount = ( + workflow.match(/node "\$HOME\/\.claude\/gsd-core\/bin\/gsd-tools\.cjs"/g) || [] + ).length; + assert.strictEqual( + hardcodedCount, + 0, + [ + '#621 regression: workflow must not contain any hardcoded', + 'node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" invocations;', + `found ${hardcodedCount}`, + ].join(' ') + ); + }); + + test('post-planning-gaps block still gates on workflow.post_planning_gaps and preserves required args', () => { + const hasGate = workflow.includes('workflow.post_planning_gaps'); + const hasPhaseDir = workflow.includes('--phase-dir "${PHASE_DIR}"'); + const hasPickArg = workflow.includes('--pick phase_req_ids'); + assert.ok( + hasGate, + 'workflow must still gate the gap-analysis step on workflow.post_planning_gaps config key' + ); + assert.ok( + hasPhaseDir, + 'gap-analysis invocation must still pass --phase-dir "${PHASE_DIR}"' + ); + assert.ok( + hasPickArg, + 'inner query must still pass --pick phase_req_ids to extract phase requirement IDs' + ); + }); +});