* fix(#705): route hardcoded $HOME gsd-tools invocations in agents/commands through gsd_run The hardcoded `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" <cmd>` form (fixed for workflows in #621/#637) survived in agent/command surfaces and misresolves on global/shim-only installs. Route every agent-executed invocation through the resolved `gsd_run` launcher in gsd-phase-researcher, gsd-planner (load_graph_context extracted to a shared reference to stay under the planner size budget), import, and graphify. Add a regression guard over agents/ + commands/ + gsd-core/references/ bash blocks. User-facing display messages and docs are intentionally left untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#705): use repo changeset fragment format (type: Fixed, pr: 707) The hand-written fragment used the standard changesets package format (package: bump) which lacks the type:/pr: frontmatter the repo's docs-required lint consumes (fail_malformed_fragment / missing_type). Regenerated via scripts/changeset/new.cjs. 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:
5
.changeset/sturdy-lemurs-roar.md
Normal file
5
.changeset/sturdy-lemurs-roar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 707
|
||||
---
|
||||
**`/gsd:graphify`, `/gsd:import`, and planning agents now resolve `gsd-tools` on global/shim-only installs** — agent and command surfaces that invoked a hardcoded `$HOME/.claude/...gsd-tools.cjs` path now route through the resolved `gsd_run` launcher, so the step no longer reports the tool "not found" when there is no project-local runtime.
|
||||
@@ -632,7 +632,8 @@ ls .planning/graphs/graph.json 2>/dev/null
|
||||
If graph.json exists, check freshness:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify status
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run graphify status
|
||||
```
|
||||
|
||||
If the status response has `stale: true`, note for later: "Graph is {age_hours}h old -- treat semantic relationships as approximate." Include this annotation inline with any graph context injected below.
|
||||
@@ -640,7 +641,7 @@ If the status response has `stale: true`, note for later: "Graph is {age_hours}h
|
||||
Query the graph for each major capability in the phase scope (2-3 queries per D-05, discovery-focused):
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify query "<capability-keyword>" --budget 1500
|
||||
gsd_run graphify query "<capability-keyword>" --budget 1500
|
||||
```
|
||||
|
||||
Derive query terms from the phase goal and requirement descriptions. Examples:
|
||||
|
||||
@@ -818,39 +818,10 @@ If exists, load relevant documents by phase type:
|
||||
</step>
|
||||
|
||||
<step name="load_graph_context">
|
||||
Check for knowledge graph:
|
||||
|
||||
```bash
|
||||
ls .planning/graphs/graph.json 2>/dev/null
|
||||
```
|
||||
|
||||
If graph.json exists, check freshness:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify status
|
||||
```
|
||||
|
||||
If the status response has `stale: true`, note for later: "Graph is {age_hours}h old -- treat semantic relationships as approximate." Include this annotation inline with any graph context injected below.
|
||||
|
||||
Query the graph for phase-relevant dependency context (single query per D-06):
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify query "<phase-goal-keyword>" --budget 2000
|
||||
```
|
||||
|
||||
(graphify is not exposed on `gsd-tools query` yet; use `gsd-tools.cjs` for graphify only.)
|
||||
|
||||
Use the keyword that best captures the phase goal. Examples:
|
||||
- Phase "User Authentication" -> query term "auth"
|
||||
- Phase "Payment Integration" -> query term "payment"
|
||||
- Phase "Database Migration" -> query term "migration"
|
||||
|
||||
If the query returns nodes and edges, incorporate as dependency context for planning:
|
||||
- Which modules/files are semantically related to this phase's domain
|
||||
- Which subsystems may be affected by changes in this phase
|
||||
- Cross-document relationships that inform task ordering and wave structure
|
||||
|
||||
If no results or graph.json absent, continue without graph context.
|
||||
Read `gsd-core/references/planner-load-graph-context.md` and execute it. It checks for a
|
||||
knowledge graph and, if `.planning/graphs/graph.json` exists, reads freshness and
|
||||
phase-relevant dependency context via the `gsd_run` launcher and incorporates the results
|
||||
into planning. If the graph is absent, skip and continue without graph context.
|
||||
</step>
|
||||
|
||||
<step name="identify_phase">
|
||||
|
||||
@@ -79,7 +79,8 @@ Modes:
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node $HOME/.claude/gsd-core/bin/gsd-tools.cjs graphify query <term>
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run graphify query <term>
|
||||
```
|
||||
|
||||
Parse the JSON output and display results:
|
||||
@@ -95,7 +96,8 @@ Parse the JSON output and display results:
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node $HOME/.claude/gsd-core/bin/gsd-tools.cjs graphify status
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run graphify status
|
||||
```
|
||||
|
||||
Parse the JSON output and display:
|
||||
@@ -119,7 +121,8 @@ Surface both so the agent can choose.
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node $HOME/.claude/gsd-core/bin/gsd-tools.cjs graphify diff
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run graphify diff
|
||||
```
|
||||
|
||||
Parse the JSON output and display:
|
||||
@@ -137,7 +140,8 @@ If no snapshot exists, suggest running `build` twice (first to create, second to
|
||||
Run the pre-flight check first:
|
||||
|
||||
```bash
|
||||
node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify build
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run graphify build
|
||||
```
|
||||
|
||||
Parse the JSON output:
|
||||
@@ -156,12 +160,13 @@ GSD > Building knowledge graph...
|
||||
Run the build, copy artifacts, write the diff snapshot, and report the summary in a single foreground Bash call so the whole pipeline survives to completion. Use a `timeout` of `600000` ms (10 minutes), which covers the `graphify.build_timeout` ceiling (default 300 s) with margin:
|
||||
|
||||
```bash
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
graphify update . \
|
||||
&& cp graphify-out/graph.json .planning/graphs/graph.json \
|
||||
&& { [ -f graphify-out/graph.html ] && cp graphify-out/graph.html .planning/graphs/graph.html || true; } \
|
||||
&& cp graphify-out/GRAPH_REPORT.md .planning/graphs/GRAPH_REPORT.md \
|
||||
&& node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify build snapshot \
|
||||
&& node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" graphify status
|
||||
&& gsd_run graphify build snapshot \
|
||||
&& gsd_run graphify status
|
||||
```
|
||||
|
||||
Do NOT pass `run_in_background: true`. Typical builds complete in 15-60 seconds and the entire chain must run foreground.
|
||||
|
||||
@@ -33,8 +33,12 @@ $ARGUMENTS
|
||||
|
||||
<process>
|
||||
If `--from-gsd2` is in $ARGUMENTS:
|
||||
Run: `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" from-gsd2`
|
||||
Pass `--path <dir>` if provided. Present the migration result to the user.
|
||||
Run the reverse-migration (append `--path <dir>` if provided):
|
||||
```bash
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run from-gsd2
|
||||
```
|
||||
Present the migration result to the user.
|
||||
Stop here (do not run the standard import workflow).
|
||||
|
||||
Otherwise, execute the import workflow end-to-end.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-06-02",
|
||||
"generated": "2026-06-05",
|
||||
"families": {
|
||||
"agents": [
|
||||
"gsd-advisor-researcher",
|
||||
@@ -227,6 +227,7 @@
|
||||
"planner-graphify-auto-update.md",
|
||||
"planner-human-verify-mode.md",
|
||||
"planner-interface-context.md",
|
||||
"planner-load-graph-context.md",
|
||||
"planner-mvp-mode.md",
|
||||
"planner-reviews.md",
|
||||
"planner-revision.md",
|
||||
|
||||
@@ -264,7 +264,7 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that
|
||||
|
||||
---
|
||||
|
||||
## References (63 shipped)
|
||||
## References (64 shipped)
|
||||
|
||||
Full roster at `gsd-core/references/*.md`. References are shared knowledge documents that workflows and agents `@-reference`. The groupings below match [`docs/ARCHITECTURE.md`](ARCHITECTURE.md#references-gsd-corereferencesmd) — core, workflow, thinking-model clusters, and the modular planner decomposition.
|
||||
|
||||
@@ -358,6 +358,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
|
||||
| `planner-human-verify-mode.md` | Rules for `workflow.human_verify_mode = end-of-phase`: suppress `checkpoint:human-verify` task emission and route deferred items via `<verify><human-check>`. |
|
||||
| `planner-graphify-auto-update.md` | How `load_graph_context` surfaces `.last-build-status.json` auto-update state (running / failed / stale head) alongside the existing staleness annotation. Opt-in via `graphify.auto_update` (#3347). |
|
||||
| `planner-interface-context.md` | Interface context rules for executors — how to extract key interfaces/types/exports from existing code and document new interfaces that downstream plans will consume. |
|
||||
| `planner-load-graph-context.md` | Planner's load_graph_context step: knowledge-graph freshness + dependency-context query via the gsd_run launcher (extracted from gsd-planner.md). |
|
||||
| `skeleton-template.md` | SKELETON.md template emitted for new-project Walking Skeleton (Phase 1 + `--mvp`). |
|
||||
| `user-story-template.md` | User story format for MVP planning — "As a / I want to / So that" structured fields. |
|
||||
| `spidr-splitting.md` | SPIDR splitting decomposition rules for handling large user stories in MVP mode. |
|
||||
|
||||
36
gsd-core/references/planner-load-graph-context.md
Normal file
36
gsd-core/references/planner-load-graph-context.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Planner — Load Graph Context
|
||||
|
||||
> Loaded by `gsd-planner` at the `load_graph_context` step.
|
||||
|
||||
Check for knowledge graph:
|
||||
|
||||
```bash
|
||||
ls .planning/graphs/graph.json 2>/dev/null
|
||||
```
|
||||
|
||||
If graph.json exists, check freshness:
|
||||
|
||||
```bash
|
||||
_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 command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/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
|
||||
gsd_run graphify status
|
||||
```
|
||||
|
||||
If the status response has `stale: true`, note for later: "Graph is {age_hours}h old -- treat semantic relationships as approximate." Include this annotation inline with any graph context injected below.
|
||||
|
||||
Query the graph for phase-relevant dependency context (single query per D-06):
|
||||
|
||||
```bash
|
||||
gsd_run graphify query "<phase-goal-keyword>" --budget 2000
|
||||
```
|
||||
|
||||
Use the keyword that best captures the phase goal. Examples:
|
||||
- Phase "User Authentication" -> query term "auth"
|
||||
- Phase "Payment Integration" -> query term "payment"
|
||||
- Phase "Database Migration" -> query term "migration"
|
||||
|
||||
If the query returns nodes and edges, incorporate as dependency context for planning:
|
||||
- Which modules/files are semantically related to this phase's domain
|
||||
- Which subsystems may be affected by changes in this phase
|
||||
- Cross-document relationships that inform task ordering and wave structure
|
||||
|
||||
If no results or graph.json absent, continue without graph context.
|
||||
@@ -41,8 +41,9 @@ const GRAPHIFY_MD = path.join(__dirname, '..', 'commands', 'gsd', 'graphify.md')
|
||||
*/
|
||||
function extractStep3Block() {
|
||||
const content = fs.readFileSync(GRAPHIFY_MD, 'utf-8');
|
||||
// Capture from the `graphify update .` line through the next closing ``` fence.
|
||||
const match = content.match(/```bash\r?\n(graphify update \.[^\0]*?)```/);
|
||||
// Capture the full body of the ```bash fence that CONTAINS `graphify update .`
|
||||
// (including any leading preamble line), without crossing into other fences.
|
||||
const match = content.match(/```bash\r?\n((?:(?!```)[\s\S])*?graphify update \.(?:(?!```)[\s\S])*?)\r?\n```/);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -469,8 +469,8 @@ describe('regressions', () => {
|
||||
'a bash code block must invoke `graphify update .`'
|
||||
);
|
||||
assert.ok(
|
||||
bashBlocks.some(b => /gsd-tools\.cjs["']?\s+graphify build snapshot/.test(b.content)),
|
||||
'a bash code block must invoke `gsd-tools.cjs graphify build snapshot`'
|
||||
bashBlocks.some(b => /gsd_run\s+graphify build snapshot/.test(b.content)),
|
||||
'a bash code block must invoke `gsd_run graphify build snapshot`'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
121
tests/no-hardcoded-home-gsd-tools.test.cjs
Normal file
121
tests/no-hardcoded-home-gsd-tools.test.cjs
Normal file
@@ -0,0 +1,121 @@
|
||||
// allow-test-rule: source-text-is-the-product
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SCAN_DIRS = ['agents', 'commands', path.join('gsd-core', 'references')];
|
||||
|
||||
// Fix #3: broaden to catch backtick-delimited and split-quoted forms.
|
||||
// Matches: node <optional-quote/backtick> ($HOME|${HOME}|~) <any non-newline chars> gsd-tools.cjs
|
||||
// PREAMBLE_SKIP_RE is still applied before this to exclude preamble lines.
|
||||
const HARDCODED_RE = /node\s+[`"']?(?:\$HOME|\$\{HOME\}|~)[^\n]*?gsd-tools\.cjs/;
|
||||
const PREAMBLE_SKIP_RE = /_GSD_SHIM_NAME|GSD_TOOLS=|\[ -f/;
|
||||
|
||||
function collectMdFiles(dir) {
|
||||
const results = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...collectMdFiles(full));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function extractBashBlocks(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
const blocks = [];
|
||||
let inBash = false;
|
||||
let blockLines = [];
|
||||
let blockStart = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!inBash) {
|
||||
// Fix #1: match bash/sh as the language tag regardless of any trailing info string.
|
||||
if (/^```(?:bash|sh)(?:\s.*)?$/.test(line)) {
|
||||
inBash = true;
|
||||
blockLines = [];
|
||||
blockStart = i + 1;
|
||||
}
|
||||
// Plain ``` fences (no language) are ignored
|
||||
} else {
|
||||
if (/^```\s*$/.test(line)) {
|
||||
blocks.push({ lines: blockLines, startLine: blockStart });
|
||||
inBash = false;
|
||||
blockLines = [];
|
||||
} else {
|
||||
blockLines.push({ text: line, lineNum: i + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix #2: if file ends while still inside a bash block, push the accumulated block.
|
||||
if (inBash && blockLines.length > 0) {
|
||||
blocks.push({ lines: blockLines, startLine: blockStart });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Fix #4: scan returns per-directory block counts so we can assert each dir contributed.
|
||||
function scanAll() {
|
||||
const violations = [];
|
||||
const perDirCounts = {};
|
||||
|
||||
for (const dir of SCAN_DIRS) {
|
||||
const absDir = path.join(ROOT, dir);
|
||||
perDirCounts[dir] = { exists: fs.existsSync(absDir), bashBlockCount: 0 };
|
||||
if (!perDirCounts[dir].exists) continue;
|
||||
const files = collectMdFiles(absDir);
|
||||
for (const file of files) {
|
||||
const blocks = extractBashBlocks(file);
|
||||
perDirCounts[dir].bashBlockCount += blocks.length;
|
||||
const relPath = path.relative(ROOT, file).replace(/\\/g, '/');
|
||||
for (const block of blocks) {
|
||||
for (const { text, lineNum } of block.lines) {
|
||||
if (HARDCODED_RE.test(text) && !PREAMBLE_SKIP_RE.test(text)) {
|
||||
violations.push(`${relPath}:${lineNum}: ${text.trim()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { violations, perDirCounts };
|
||||
}
|
||||
|
||||
test('no hardcoded $HOME gsd-tools.cjs in bash blocks of agents/, commands/, and gsd-core/references/', () => {
|
||||
const { violations } = scanAll();
|
||||
assert.equal(
|
||||
violations.length,
|
||||
0,
|
||||
`Found ${violations.length} hardcoded invocation(s):\n${violations.join('\n')}`
|
||||
);
|
||||
});
|
||||
|
||||
// Fix #4: per-directory floor — each scan dir must exist and contribute >= 1 bash block.
|
||||
test('each scan dir (agents/, commands/, gsd-core/references/) exists and contains at least one bash block', () => {
|
||||
const { perDirCounts } = scanAll();
|
||||
const failures = [];
|
||||
for (const [dir, { exists, bashBlockCount }] of Object.entries(perDirCounts)) {
|
||||
if (!exists) {
|
||||
failures.push(` ${dir}/: directory does not exist`);
|
||||
} else if (bashBlockCount < 1) {
|
||||
failures.push(` ${dir}/: exists but contains 0 bash blocks`);
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
failures.length,
|
||||
0,
|
||||
`Per-directory bash-block floor failed:\n${failures.join('\n')}\nCheck that each scan dir is non-empty and contains bash fences.`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user