fix(#621): route plan-phase post-planning-gaps through gsd_run launcher (#635)

* 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 <noreply@anthropic.com>

* chore(#621): add changeset for plan-phase gsd_run gap-analysis fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-02 23:02:25 -04:00
committed by GitHub
parent de7d6add15
commit 2815720aad
4 changed files with 97 additions and 4 deletions

View File

@@ -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.

View File

@@ -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
```

View File

@@ -7,6 +7,10 @@
*
* node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" <subcommand> [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 <subcommand>` 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()}`,
);
}
}

View File

@@ -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'
);
});
});