* test(#3809): generalize dead-ref guard into a rule table (failing first)
The #2020 guard hardcoded `sdk/(src|dist|handlers)/` — the three dead paths
that had caused that storm. That proved those three paths were gone and said
nothing about the class, so #3809 reproduced the identical Windows find.exe
storm under a different token and the guard could not see it.
Replaces the single regex with a rule table over the same runtime-loaded
markdown surface, adds `commands/` to the scan set (previously uncovered),
and adds rule B: the runtime shim filename must never appear in command
position, because it is not a PATH command and an agent that meets it falls
back to locating the file.
Rule B's matcher is deliberately lenient — the launcher's own resolver
assignment, `node <path>/<shim>` calls, bare paths, and prose that names the
file all stay unflagged, each pinned by a negative-space row.
This commit is expected to FAIL: 50 offenders across 23 files remain in the
tree. The remediation lands next.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#3809): route every workflow call through the gsd_run launcher
50 places across 23 runtime-loaded workflow, agent, reference, and command
files instructed the agent to run the runtime shim by filename. That filename
is not on PATH under any name -- package.json ships gsd-core, gsd-tools,
gsd_run and gsd-mcp-server -- so the call exited 127, the file-shaped token
sent the agent looking for the file, and on Git Bash for Windows the resulting
`find /` walked the entire drive (7268 CPU-seconds in the report) until
somebody killed it by hand.
CONTEXT.md -> Runtime Launcher Module already makes gsd_run the single entry
point: "Canonical space-safe shell preamble (`gsd_run`) used by every workflow
bash block to invoke the GSD runtime CLI." These sites predate that rule --
they trace to 0e6907050 (docs(#195): migrate workflow markdown off gsd-sdk
query), which swapped one non-PATH token for another.
Two further instances of the same class surfaced during remediation and are
fixed here rather than left for later:
- references/model-profiles.md prescribed `node <shim> effort sync` with no
path at all; node resolves a bare filename against cwd, so it fails the
same way.
- references/universal-anti-patterns.md rule 25 instructed every agent to
"use <shim>" when shelling out. That rule did not contain the defect, it
prescribed it repo-wide.
Five "(or legacy <shim>)" parentheticals left dangling by the substitution are
removed; after the rewrite they offered the non-resolving form as an
alternative.
The guard from the previous commit now passes. Its node-prefix exemption was
tightened to require a path separator, which is what exposed model-profiles.
Fixes #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#3809): key the guard on the CLI's whole verb roster, not observed usage
Review found the first cut of rule B repeating the very mistake it exists to
prevent. Its verb set held query, commit and effort -- the verbs that happened
to appear in the tree -- so it could not see `<shim> phase add`,
`<shim> state load`, `<shim> verify ...` or twenty-odd other real single-word
subcommands. A guard that only recognises yesterday's offenders is not a guard.
The set is now the CLI's full advertised roster, unioned from the usage banner
and HOST_COMMAND_ROUTERS (which carries verification, planning, uat, stats,
todo and windows, all absent from the banner).
Widening it immediately caught a live offender the first pass had missed:
references/planning-config.md prescribed `node <shim> worktree set-baseref`
with no path. Fixed here.
Also drops the "a hyphen or a dot means subcommand" heuristic, which was
unsound for prose -- it flagged `built-in` and `v1.2`. Detection now keys
entirely on the roster, testing the first dot-segment so that phase.add and
state.patch still match while prose does not. Both false positives are pinned
as negative-space rows.
Guard verified against the pre-fix tree at origin/next: 52 offenders across 25
files, and 0 after this branch's remediation.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#3809): derive the verb roster from the router; repair launcher parity
Standards review caught the guard repeating the defect it exists to prevent.
Its verb list was a hand-copied literal -- and worse, transcribed from an
INSTALLED older binary, so it was missing 22 verbs this tree actually ships
(websearch, windows, state-snapshot, context-predicates and the dispatch-*
family among them). gsd-tools.cjs already carries three hand-maintained
rosters whose drift is a named defect pinned by the parity test in
tests/commands.test.cjs; a hand-copied fourth was that same defect wearing a
guard's clothes.
The roster is now derived from HOST_COMMAND_ROUTERS + TOP_LEVEL_USAGE, lazily
and memoised, with `query` supplemented explicitly -- it dispatches through
the routing hub ahead of the host-router table, so it appears in neither
export, yet 45 of the 50 offenders used it. A parity test pins the derivation.
Two regressions this branch introduced, both caught by the remote runner:
- runtime-launcher-parity: rewriting a comment in gsd-research-synthesizer.md
put a `gsd_run` token at line 65 while the canonical preamble sits at 158,
breaking "exactly ONE preamble, before the first gsd_run call". The comment
is descriptive and needs no command token at all; it now names none.
- The #2751 guard's PROSE_ALLOWLIST entry for that same line went stale once
the line stopped carrying a bare mention. Pruned, exactly as that guard's
own stale-entry test instructs.
Also corrects git-planning-commit.md, where the first pass rewrote only the
trailing "legacy" clause and left the sentence reading backwards.
Note the #2751 guard and this one are complementary, not duplicates: its regex
requires whitespace immediately after `gsd-tools`, so it cannot match the
`.cjs` form, and this one only matches the `.cjs` form.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#2751): extend the bare-command guard to references/ and commands/
The #2751 guard has only ever scanned agents/ and gsd-core/workflows/. Two
runtime-loaded directories were never in its scan set, and 47 bare
`gsd-tools <verb>` calls had accumulated there unseen -- the same defect that
guard exists to catch, in the rooms it never entered.
- gsd-core/references/: 37 calls, all rewritten to gsd_run. references are
fragments inlined into a parent that defines the launcher, which is why 21
of the 22 files already using gsd_run carry no local preamble.
- commands/gsd/: 10 operative calls rewritten. The remaining 10 are
descriptive prose ("resolved inside the workflow via ...") and are
allowlisted with reasons, bringing PROSE_ALLOWLIST to 15.
commands/ also came under launcher propagation. sync-runtime-launcher.cjs
walked only WORKFLOWS_DIR and AGENTS_DIR, so every preamble under commands/
was a hand-pasted copy nothing propagated and no test checked -- graphify.md
had accumulated five. It now walks COMMANDS_DIR too, which collapses those
five to the canonical one-per-file, and runtime-launcher-parity gains a
(B-commands) arm mirroring (B-agents) exactly so the placement stays honest.
The parity arm keys on shell blocks, so commands/gsd/workstreams.md and
config.md -- which name gsd_run only in inline backtick prose -- are exempt,
as they should be. gsd_run is itself a shipped npm bin, so those inline
instructions resolve from PATH exactly as the gsd-tools form they replace did.
skills/ is deliberately NOT added to either guard's scan set: it is generated
from commands/ and pinned by lint:generated-sync, so guarding the source
guards both, and scanning the mirror would double-report every future
offender. Regenerated here.
Refs #2751, #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(#3809): acknowledge the one emitted file this change grows
The emitted-attribution gate failed on the previous sha: gsd-research-synthesizer.md
grew 3 bytes (13847 -> 13850) with no acknowledgment. The substitution SHRANK the
other 19 emitted files, which is why the growth arm was not expected to fire at all.
The 3 bytes are unavoidable. Line 65 is a descriptive comment inside a fenced block;
naming any command there puts a gsd_run token ahead of the file's canonical preamble
at line 158, which runtime-launcher-parity's (B-agents) arm correctly rejects. So the
comment names no command and says where the config is actually loaded instead, which
reads longer than the token it replaced.
Acks only the path the gate reported, per the fragment rules.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* revert(#2751): drop the commands/ half — three contracts pin it in place
The remote runner refuted the commands/ extension outright. Reverting it and
keeping the references/ conversion, which passed.
What broke, all of it caused by bringing commands/ under launcher propagation:
- graphify.md's five per-block preambles are LOAD-BEARING, not accumulated
drift. tests/graphify-visualization.test.cjs extracts individual Step-3
shell chains and executes them standalone, so each fenced block needs its
own definition of gsd_run. Collapsing them to the canonical one-per-file
produced `bash: gsd_run: command not found`, exit 127, across four tests.
The "define once per file" contract holds for workflows and agents because
nothing extracts their blocks in isolation; commands/ is not like that.
- explore.md broke "the preamble that DEFINES gsd_run must appear before the
first USE of gsd_run anywhere in the file".
- tests/gsd-tools-path-refs.test.cjs (#1766) ASSERTS that
commands/gsd/workstreams.md contains the literal string
`gsd-tools query workstream.list`. Rewriting it to gsd_run contradicts a
test that pins the opposite, so the two guards disagree about that file by
construction.
So commands/ is not a scan-set widening. It needs those contracts reconciled
first, and that is its own change. SCAN_DIRS keeps gsd-core/references/ and
drops commands/, the ten commands/ allowlist entries go with it (back to 5),
and the reasoning is recorded in the guard itself so the next person does not
rediscover it by burning a matrix run.
commands/gsd/import.md keeps its #3809 fix — that one is the .cjs form this
PR exists to remove, and it is untouched by any of the above.
Refs #2751, #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* revert(#3809): restore explore.md's Step 1 preamble placement
Running the launcher sync script processed workflows/ and agents/ too, not
just the commands/ directory the run was aimed at, and it MOVED
gsd-core/workflows/explore.md's preamble from Step 1 down to Step 3.
The script inserts into the first bash block that USES gsd_run. explore.md's
Step 1 block only DEFINES it, and that placement is deliberate -- the file
says so on the line above: "Placed in Step 1 rather than Step 3 so declining
the research offer cannot leave Step 5's commit call unbootstrapped."
tests/explore-command.test.cjs pins it.
explore.md carried no #3809 offender, so reverting it costs this fix nothing.
This was collateral from invoking the sync script at all, not from the
COMMANDS_DIR change, which is why the earlier commands/ revert did not catch it.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#3809): backfill PR number into changeset fragments
pr:0 -> pr:3815 for both fragments now that the PR exists.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#3809): drop the hand-rolled regex escaper CodeQL flagged
CodeQL raised js/incomplete-sanitization (HIGH) on the guard's pattern build:
`SHIM.replace(/\./g, '\\.')` escapes the dot and nothing else, so it does not
escape backslashes. It blocked PR #3815.
The repo already bans this shape -- local/no-adhoc-regex-escape exists exactly
to stop hand-rolled escapers, with the canonical one in src/pattern.cts. Rather
than reach for that helper, the pattern now carries no escaping logic at all:
SHIM is a compile-time constant whose only metacharacter is the dot, so the
regex source is spelled out literally. The generated source string is
byte-identical to what the replace() produced, verified before and after --
0 offenders on this tree, 52 against origin/next, unchanged.
A drift pin asserts SHIM_PATTERN still matches SHIM exactly, and that the dot
is escaped rather than acting as a wildcard, so the two cannot separate.
Refs #3809
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/agile-geese-squeak.md
Normal file
5
.changeset/agile-geese-squeak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3815
|
||||
---
|
||||
**Workflows no longer send AI runtimes hunting the filesystem for the GSD shim** — 50 places across 23 runtime-loaded workflow, agent, reference, and command files told the agent to run `gsd-tools.cjs` by filename, which is not on PATH under any name. The agent got "command not found", fell back to locating the file, and on Git Bash for Windows `find /` walked the entire drive until someone killed it. Every one now calls the canonical `gsd_run` launcher. (#3809)
|
||||
5
.changeset/eager-yaks-wander.md
Normal file
5
.changeset/eager-yaks-wander.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3815
|
||||
---
|
||||
**`gsd-core/references/` is now covered by the bare-command guard** — the #2751 guard only ever scanned `agents/` and `gsd-core/workflows/`, so 37 bare `gsd-tools <verb>` calls sat unguarded in a directory it never looked at. They now call the canonical `gsd_run` launcher, and the guard scans references too. (#2751)
|
||||
@@ -62,7 +62,7 @@ cat .planning/research/FEATURES.md
|
||||
cat .planning/research/ARCHITECTURE.md
|
||||
cat .planning/research/PITFALLS.md
|
||||
|
||||
# Planning config loaded via gsd-tools query (or gsd-tools.cjs) in commit step
|
||||
# Planning config is loaded by the commit step below, after the launcher preamble
|
||||
```
|
||||
|
||||
Parse each file to extract:
|
||||
|
||||
@@ -17,7 +17,7 @@ allowed-tools:
|
||||
Import external plan files into the GSD planning system with conflict detection against PROJECT.md decisions.
|
||||
|
||||
- **--from**: Import an external plan file, detect conflicts, write as GSD PLAN.md, validate via gsd-plan-checker.
|
||||
- **--from-gsd2**: Reverse-migrate a GSD-2 project (`.gsd/` directory) back to GSD v1 (`.planning/`) format. Runs `gsd-tools.cjs from-gsd2`. Pass `--path <dir>` to migrate a project at a different path.
|
||||
- **--from-gsd2**: Reverse-migrate a GSD-2 project (`.gsd/` directory) back to GSD v1 (`.planning/`) format. Runs `gsd_run from-gsd2`. Pass `--path <dir>` to migrate a project at a different path.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -5,7 +5,7 @@ Smart discuss is the autonomous-optimized variant of `gsd-discuss-phase`. It pro
|
||||
**Inputs:** `PHASE_NUM` from execute_phase. Run init to get phase paths:
|
||||
|
||||
```bash
|
||||
PHASE_STATE=$(gsd-tools query init.phase-op ${PHASE_NUM})
|
||||
PHASE_STATE=$(gsd_run query init.phase-op ${PHASE_NUM})
|
||||
```
|
||||
|
||||
Parse from JSON: `phase_dir`, `phase_slug`, `padded_phase`, `phase_name`.
|
||||
@@ -94,7 +94,7 @@ Read the 3-5 most relevant files to understand existing patterns.
|
||||
**Get phase details:**
|
||||
|
||||
```bash
|
||||
DETAIL=$(gsd-tools query roadmap.get-phase ${PHASE_NUM})
|
||||
DETAIL=$(gsd_run query roadmap.get-phase ${PHASE_NUM})
|
||||
```
|
||||
|
||||
Extract `goal`, `requirements`, `success_criteria` from the JSON response.
|
||||
@@ -266,7 +266,7 @@ Write the file.
|
||||
**Commit:**
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "docs(${PADDED_PHASE}): smart discuss context" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
|
||||
gsd_run query commit "docs(${PADDED_PHASE}): smart discuss context" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
|
||||
```
|
||||
|
||||
Display confirmation:
|
||||
|
||||
@@ -6,7 +6,7 @@ Calculate the next decimal phase number for urgent insertions.
|
||||
|
||||
```bash
|
||||
# Get next decimal phase after phase 6
|
||||
gsd-tools query phase.next-decimal 6
|
||||
gsd_run query phase.next-decimal 6
|
||||
```
|
||||
|
||||
Output:
|
||||
@@ -32,13 +32,13 @@ With existing decimals:
|
||||
## Extract Values
|
||||
|
||||
```bash
|
||||
DECIMAL_PHASE=$(gsd-tools query phase.next-decimal "${AFTER_PHASE}" --pick next)
|
||||
BASE_PHASE=$(gsd-tools query phase.next-decimal "${AFTER_PHASE}" --pick base_phase)
|
||||
DECIMAL_PHASE=$(gsd_run query phase.next-decimal "${AFTER_PHASE}" --pick next)
|
||||
BASE_PHASE=$(gsd_run query phase.next-decimal "${AFTER_PHASE}" --pick base_phase)
|
||||
```
|
||||
|
||||
Or with --raw flag:
|
||||
```bash
|
||||
DECIMAL_PHASE=$(gsd-tools query phase.next-decimal "${AFTER_PHASE}" --raw)
|
||||
DECIMAL_PHASE=$(gsd_run query phase.next-decimal "${AFTER_PHASE}" --raw)
|
||||
# Returns just: 06.1
|
||||
```
|
||||
|
||||
@@ -56,7 +56,7 @@ DECIMAL_PHASE=$(gsd-tools query phase.next-decimal "${AFTER_PHASE}" --raw)
|
||||
Decimal phase directories use the full decimal number:
|
||||
|
||||
```bash
|
||||
SLUG=$(gsd-tools query generate-slug "$DESCRIPTION" --raw)
|
||||
SLUG=$(gsd_run query generate-slug "$DESCRIPTION" --raw)
|
||||
PHASE_DIR=".planning/phases/${DECIMAL_PHASE}-${SLUG}"
|
||||
mkdir -p "$PHASE_DIR"
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
7b. **Pre-wave dependency check (waves 2+ only):**
|
||||
Before wave N+1, run `gsd-tools.cjs query verify.key-links {phase_dir}/{plan}-PLAN.md` for each upcoming plan.
|
||||
Before wave N+1, run `gsd_run query verify.key-links {phase_dir}/{plan}-PLAN.md` for each upcoming plan.
|
||||
If any PRIOR-wave artifact link fails, present:
|
||||
- `## Cross-Plan Wiring Gap` with plan/link/from/pattern rows
|
||||
- Options: investigate+fix before continue, or continue with cascade risk
|
||||
|
||||
@@ -51,7 +51,7 @@ Phases:
|
||||
What to commit:
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "docs: initialize [project-name] ([N] phases)" --files .planning/
|
||||
gsd_run query commit "docs: initialize [project-name] ([N] phases)" --files .planning/
|
||||
```
|
||||
|
||||
</format>
|
||||
@@ -136,7 +136,7 @@ SUMMARY: .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md
|
||||
What to commit:
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
|
||||
gsd_run query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
|
||||
```
|
||||
|
||||
**Note:** Code files NOT included - already committed per-task.
|
||||
@@ -156,7 +156,7 @@ Current: [task name]
|
||||
What to commit:
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
|
||||
gsd_run query commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
|
||||
```
|
||||
|
||||
</format>
|
||||
@@ -279,7 +279,7 @@ Set `commit_docs: false` so planning docs stay local and are not committed to an
|
||||
|
||||
1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. On subsequent runs, `loadConfig` auto-syncs the `sub_repos` list with the filesystem — adding newly created repos and removing deleted ones. This means `config.json` may be rewritten automatically when repos change on disk.
|
||||
2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo).
|
||||
3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging.
|
||||
3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd_run commit-to-subrepo`. File paths are made relative to the sub-repo root before staging.
|
||||
4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination.
|
||||
|
||||
### Commit Routing
|
||||
@@ -287,7 +287,7 @@ Set `commit_docs: false` so planning docs stay local and are not committed to an
|
||||
Instead of the standard `commit` command, use `commit-to-subrepo` when `sub_repos` is configured:
|
||||
|
||||
```bash
|
||||
gsd-tools query commit-to-subrepo "feat(02-01): add user API" \
|
||||
gsd_run query commit-to-subrepo "feat(02-01): add user API" \
|
||||
--files backend/src/api/users.ts backend/src/types/user.ts frontend/src/components/UserForm.tsx
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Git Planning Commit
|
||||
|
||||
Commit planning artifacts via `gsd-tools query commit`, which checks `commit_docs` config and gitignore status (same behavior as legacy `gsd-tools.cjs commit`).
|
||||
Commit planning artifacts via `gsd_run query commit`, which checks `commit_docs` config and gitignore status.
|
||||
|
||||
## Commit via CLI
|
||||
|
||||
@@ -9,7 +9,7 @@ Pass the message first, then file paths via `--files`. Both `commit` and `commit
|
||||
Always use this for `.planning/` files — it handles `commit_docs` and gitignore checks automatically:
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md
|
||||
gsd_run query commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md
|
||||
```
|
||||
|
||||
The CLI will return `skipped` (with reason) if `commit_docs` is `false`, `.planning/` is gitignored, or a per-phase `phase_commit_docs.<phase-id>` override resolves `false` for the phase being committed. No manual conditional checks needed.
|
||||
@@ -19,7 +19,7 @@ The CLI will return `skipped` (with reason) if `commit_docs` is `false`, `.plann
|
||||
To fold `.planning/` file changes into the previous commit:
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "" --files .planning/codebase/*.md --amend
|
||||
gsd_run query commit "" --files .planning/codebase/*.md --amend
|
||||
```
|
||||
|
||||
## Commit Message Patterns
|
||||
|
||||
@@ -228,7 +228,7 @@ the next spawn. `effort` (claude runtime) has its own cascade
|
||||
install time into the `effort:` frontmatter key of
|
||||
`~/.claude/agents/gsd-*.md` — Claude Code's Agent tool has no per-spawn
|
||||
effort parameter, so per-agent frontmatter is the only channel. An effort
|
||||
config change has no effect until `node gsd-tools.cjs effort sync --apply`
|
||||
config change has no effect until `gsd_run effort sync --apply`
|
||||
re-syncs the agent files. Codex agents instead pin
|
||||
`model_reasoning_effort` in `~/.codex/agents/*.toml` at install time.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ From `$ARGUMENTS`:
|
||||
The `find-phase` command handles normalization and validation in one step:
|
||||
|
||||
```bash
|
||||
PHASE_INFO=$(gsd-tools query find-phase "${PHASE}")
|
||||
PHASE_INFO=$(gsd_run query find-phase "${PHASE}")
|
||||
```
|
||||
|
||||
Returns JSON with:
|
||||
@@ -45,7 +45,7 @@ fi
|
||||
Use `roadmap get-phase` to validate phase exists:
|
||||
|
||||
```bash
|
||||
PHASE_CHECK=$(gsd-tools query roadmap.get-phase "${PHASE}" --pick found)
|
||||
PHASE_CHECK=$(gsd_run query roadmap.get-phase "${PHASE}" --pick found)
|
||||
if [ "$PHASE_CHECK" = "false" ]; then
|
||||
echo "ERROR: Phase ${PHASE} not found in roadmap"
|
||||
exit 1
|
||||
@@ -57,5 +57,5 @@ fi
|
||||
Use `find-phase` for directory lookup:
|
||||
|
||||
```bash
|
||||
PHASE_DIR=$(gsd-tools query find-phase "${PHASE}" --raw)
|
||||
PHASE_DIR=$(gsd_run query find-phase "${PHASE}" --raw)
|
||||
```
|
||||
|
||||
@@ -55,7 +55,7 @@ Group by plan, dimension, severity.
|
||||
### Step 6: Commit
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "fix($PHASE): revise plans based on checker feedback" --files .planning/phases/$PHASE-*/$PHASE-*-PLAN.md
|
||||
gsd_run query commit "fix($PHASE): revise plans based on checker feedback" --files .planning/phases/$PHASE-*/$PHASE-*-PLAN.md
|
||||
```
|
||||
|
||||
### Step 7: Return Revision Summary
|
||||
|
||||
@@ -63,15 +63,15 @@ Configuration options for `.planning/` directory behavior.
|
||||
|
||||
```bash
|
||||
# Commit with automatic commit_docs + gitignore checks:
|
||||
gsd-tools query commit "docs: update state" --files .planning/STATE.md
|
||||
gsd_run query commit "docs: update state" --files .planning/STATE.md
|
||||
|
||||
# Load config via state load (returns JSON):
|
||||
INIT=$(gsd-tools query state.load)
|
||||
INIT=$(gsd_run query state.load)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# commit_docs is available in the JSON output
|
||||
|
||||
# Or use init commands which include commit_docs:
|
||||
INIT=$(gsd-tools query init.execute-phase "1")
|
||||
INIT=$(gsd_run query init.execute-phase "1")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# commit_docs is included in all init command outputs
|
||||
```
|
||||
@@ -83,7 +83,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
**Commit via CLI (handles checks automatically):**
|
||||
|
||||
```bash
|
||||
gsd-tools query commit "docs: update state" --files .planning/STATE.md
|
||||
gsd_run query commit "docs: update state" --files .planning/STATE.md
|
||||
```
|
||||
|
||||
The CLI checks `commit_docs` config and gitignore status internally — no manual conditionals needed.
|
||||
@@ -171,14 +171,14 @@ To use uncommitted mode:
|
||||
|
||||
Use `init execute-phase` which returns all config as JSON:
|
||||
```bash
|
||||
INIT=$(gsd-tools query init.execute-phase "1")
|
||||
INIT=$(gsd_run query init.execute-phase "1")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# JSON output includes: branching_strategy, phase_branch_template, milestone_branch_template
|
||||
```
|
||||
|
||||
Or use `state load` for the config values:
|
||||
```bash
|
||||
INIT=$(gsd-tools query state.load)
|
||||
INIT=$(gsd_run query state.load)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# Parse branching_strategy, phase_branch_template, milestone_branch_template from JSON
|
||||
```
|
||||
@@ -401,7 +401,7 @@ Several config fields affect each other or trigger special behavior:
|
||||
|
||||
8. **`sub_repos` auto-sync** -- On every config load, GSD scans for child directories with `.git` and updates the `sub_repos` array if the filesystem has changed. Legacy `multiRepo: true` is automatically migrated to a detected `sub_repos` array.
|
||||
|
||||
9. **`workflow.use_worktrees` and branch divergence** -- When `use_worktrees` is `true` (default), executor worktrees are forked from `origin/HEAD` -- by the host's own harness on `dispatch.isolation: harness-worktree` runtimes (Claude Code, Cursor), or by GSD itself on `orchestrator-worktree` runtimes (Codex, OpenCode, Kimi, Kimi Code). The divergence behavior below is identical either way, because the fork base is a property of the repository rather than of whoever creates the worktree. If your current branch has commits that `origin/HEAD` does not (for example an unmerged milestone or feature branch), GSD automatically degrades to sequential execution for that run and prints a one-line `⚠ Worktree base mismatch` warning. To restore parallel execution permanently, set `worktree.baseRef:"head"` in `.claude/settings.local.json` (run `node gsd-tools.cjs worktree set-baseref`). This makes the harness fork worktrees from the live HEAD instead of `origin/HEAD`. Both fresh installs and upgrades of GSD Core set this automatically (no-clobber) when `use_worktrees` is enabled; you can also run the command manually at any time. Setting `workflow.use_worktrees: false` is the alternative if worktrees are not needed at all. On a runtime whose declared `dispatch.isolation` is `none`, an explicit `true` is a config the execution workflows fail closed on; `/gsd:health` reports it as warning `W025` and `/gsd:settings` offers to repair it (#2486).
|
||||
9. **`workflow.use_worktrees` and branch divergence** -- When `use_worktrees` is `true` (default), executor worktrees are forked from `origin/HEAD` -- by the host's own harness on `dispatch.isolation: harness-worktree` runtimes (Claude Code, Cursor), or by GSD itself on `orchestrator-worktree` runtimes (Codex, OpenCode, Kimi, Kimi Code). The divergence behavior below is identical either way, because the fork base is a property of the repository rather than of whoever creates the worktree. If your current branch has commits that `origin/HEAD` does not (for example an unmerged milestone or feature branch), GSD automatically degrades to sequential execution for that run and prints a one-line `⚠ Worktree base mismatch` warning. To restore parallel execution permanently, set `worktree.baseRef:"head"` in `.claude/settings.local.json` (run `gsd_run worktree set-baseref`). This makes the harness fork worktrees from the live HEAD instead of `origin/HEAD`. Both fresh installs and upgrades of GSD Core set this automatically (no-clobber) when `use_worktrees` is enabled; you can also run the command manually at any time. Setting `workflow.use_worktrees: false` is the alternative if worktrees are not needed at all. On a runtime whose declared `dispatch.isolation` is `none`, an explicit `true` is a config the execution workflows fail closed on; `/gsd:health` reports it as warning `W025` and `/gsd:settings` offers to repair it (#2486).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ preserves named-dispatch behavior on older GSD installs that lack the query.
|
||||
|
||||
The persona rides `${AGENT_SKILLS_<ROLE>}` (Phase 3 / #2510) regardless of the
|
||||
resolved type — on non-Claude runtimes with no `agent_skills` config,
|
||||
`gsd-tools query agent-skills <role>` returns the installed agent prompt as
|
||||
`gsd_run query agent-skills <role>` returns the installed agent prompt as
|
||||
the block. So a coder dispatch with the planner persona injected gives kimi-code
|
||||
the planner's behavior in the coder built-in's process.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Reference: `gsd-core/references/questioning.md` for the full anti-pattern list.
|
||||
|
||||
## State Management Anti-Patterns
|
||||
|
||||
15. **No direct Write/Edit to STATE.md or ROADMAP.md for mutations.** Always use `gsd-tools query` for registered state/roadmap handlers (e.g. `state.update`, `state.advance-plan`, `roadmap.update-plan-progress`), or legacy `node …/gsd-tools.cjs` for CLI-only commands. Direct Write tool usage bypasses safe update logic and is unsafe in multi-session environments. Exception: first-time creation of STATE.md from template is allowed.
|
||||
15. **No direct Write/Edit to STATE.md or ROADMAP.md for mutations.** Always use `gsd_run query` for registered state/roadmap handlers (e.g. `state.update`, `state.advance-plan`, `roadmap.update-plan-progress`), or legacy `node …/gsd-tools.cjs` for CLI-only commands. Direct Write tool usage bypasses safe update logic and is unsafe in multi-session environments. Exception: first-time creation of STATE.md from template is allowed.
|
||||
|
||||
## Behavioral Rules
|
||||
|
||||
@@ -53,7 +53,7 @@ Reference: `gsd-core/references/questioning.md` for the full anti-pattern list.
|
||||
## GSD-Specific Rules
|
||||
|
||||
24. **Do not** check for `mode === 'auto'` or `mode === 'autonomous'` -- GSD uses `yolo` config flag. Check `yolo: true` for autonomous mode, absence or `false` for interactive mode.
|
||||
25. **Prefer `gsd-tools query`** for orchestration when a handler exists; when shelling out to the legacy CLI, use **`gsd-tools.cjs`** (not `gsd-tools.js` or any other filename) — GSD ships the programmatic API as CommonJS for Node.js CLI compatibility.
|
||||
25. **Prefer `gsd_run query`** for orchestration when a handler exists; when shelling out to the legacy CLI, go through the same `gsd_run` launcher rather than naming the shim file. The shim is not on PATH under any name ending in `.cjs`, and an agent that meets the bare filename falls back to searching the filesystem for it — on Git Bash for Windows that is a full-drive `find.exe` traversal (#3809). `gsd_run` resolves the CommonJS shim itself across every runtime home.
|
||||
26. **Plan files MUST follow `{padded_phase}-{NN}-PLAN.md` pattern** (e.g., `01-01-PLAN.md`). Never use `PLAN-01.md`, `plan-01.md`, or any other variation -- gsd-tools detection depends on this exact pattern.
|
||||
27. **Do not start executing the next plan before writing the SUMMARY.md for the current plan** -- downstream plans may reference it via `@` includes.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ The user-flow form mirrors what a real user does: open, fill, click, see. No HTT
|
||||
## When this framing applies
|
||||
|
||||
The framing fires when:
|
||||
- The phase under verification has `**Mode:** mvp` in ROADMAP.md (parsed via `gsd-tools query roadmap.get-phase --pick mode`).
|
||||
- The phase under verification has `**Mode:** mvp` in ROADMAP.md (parsed via `gsd_run query roadmap.get-phase --pick mode`).
|
||||
- AND the phase has a user-story-formatted goal (set by `/gsd mvp-phase` per Phase 2): "As a [user role], I want to [capability], so that [outcome]."
|
||||
|
||||
If the phase has `mode: mvp` but the goal is NOT in user-story format, the verifier surfaces this as a discrepancy and asks the user to run `/gsd mvp-phase` to reformat the goal — same pattern as the planner agent under MVP_MODE (per `gsd-core/references/planner-mvp-mode.md`).
|
||||
|
||||
@@ -109,19 +109,19 @@ This ensures workstream scope chains automatically through the workflow:
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# All gsd-tools query commands accept --ws
|
||||
gsd-tools query state.json --ws feature-a
|
||||
gsd-tools query find-phase 3 --ws feature-b
|
||||
# All gsd_run query commands accept --ws
|
||||
gsd_run query state.json --ws feature-a
|
||||
gsd_run query find-phase 3 --ws feature-b
|
||||
|
||||
# Session-local switching without --ws on every command
|
||||
GSD_SESSION_KEY=my-terminal-a gsd-tools query workstream.set feature-a
|
||||
GSD_SESSION_KEY=my-terminal-a gsd-tools query state.json
|
||||
GSD_SESSION_KEY=my-terminal-b gsd-tools query workstream.set feature-b
|
||||
GSD_SESSION_KEY=my-terminal-b gsd-tools query state.json
|
||||
GSD_SESSION_KEY=my-terminal-a gsd_run query workstream.set feature-a
|
||||
GSD_SESSION_KEY=my-terminal-a gsd_run query state.json
|
||||
GSD_SESSION_KEY=my-terminal-b gsd_run query workstream.set feature-b
|
||||
GSD_SESSION_KEY=my-terminal-b gsd_run query state.json
|
||||
|
||||
# Workstream CRUD
|
||||
gsd-tools query workstream.create <name>
|
||||
gsd-tools query workstream.list
|
||||
gsd-tools query workstream.status <name>
|
||||
gsd-tools query workstream.complete <name>
|
||||
gsd_run query workstream.create <name>
|
||||
gsd_run query workstream.list
|
||||
gsd_run query workstream.status <name>
|
||||
gsd_run query workstream.complete <name>
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ Exit.
|
||||
</step>
|
||||
|
||||
<step name="add_phase">
|
||||
**Delegate the phase addition to `gsd-tools.cjs query phase.add`:**
|
||||
**Delegate the phase addition to `gsd_run query phase.add`:**
|
||||
|
||||
```bash
|
||||
RESULT=$(gsd_run query phase.add "${description}")
|
||||
@@ -107,7 +107,7 @@ Roadmap updated: .planning/ROADMAP.md
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `gsd-tools.cjs query phase.add` executed successfully
|
||||
- [ ] `gsd_run query phase.add` executed successfully
|
||||
- [ ] Phase directory created
|
||||
- [ ] Roadmap updated with new phase entry
|
||||
- [ ] STATE.md updated with roadmap evolution note
|
||||
|
||||
@@ -61,7 +61,7 @@ These items are open. Choose an action:
|
||||
```
|
||||
|
||||
If user chooses [A] (Acknowledge):
|
||||
1. Re-run `gsd-tools.cjs query audit-open --json` to get structured data.
|
||||
1. Re-run `gsd_run query audit-open --json` to get structured data.
|
||||
2. Acknowledge every open item through the `audit-open acknowledge` CLI writer — this is what actually suppresses each item starting at the NEXT `audit-open` scan; the STATE.md table in step 3 is a disclosure record only, it is no longer the suppression mechanism. Every acknowledge call's exit status is accumulated (`ACK_FAILURES`); the step HALTS before closing if any failed — a refusal (`unsupported_heading_shape`, `ambiguous`, `not_found`, missing file, etc.) must never be silently discarded and let the close proceed as if everything were suppressed. `AUDIT_JSON` uses the same `@file:` large-payload sentinel handling `INIT_MANAGER` uses in `verify_readiness` below — `io.output` swaps any JSON payload over 50000 chars for a `@file:<path>` marker, and feeding that literal string to `jq` would silently make every loop body below iterate zero times:
|
||||
```bash
|
||||
AUDIT_JSON=$(gsd_run query audit-open --json)
|
||||
@@ -162,8 +162,8 @@ If user chooses [A] (Acknowledge):
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
`todos` is the only category the scanner caps (5 entries per scan, with a remainder count for the rest). Re-run `gsd-tools.cjs query audit-open --json` (through the same `@file:` handling above) and repeat the `todos` block until it reports no `todos` items — every other category always returns its full open set in one pass.
|
||||
3. Re-run `gsd-tools.cjs query audit-open --json` once more and write the items just acknowledged as new rows to STATE.md under `## Deferred Items` — append to the existing table (creating the section if absent) rather than overwriting it, preserving rows recorded at earlier milestone closes:
|
||||
`todos` is the only category the scanner caps (5 entries per scan, with a remainder count for the rest). Re-run `gsd_run query audit-open --json` (through the same `@file:` handling above) and repeat the `todos` block until it reports no `todos` items — every other category always returns its full open set in one pass.
|
||||
3. Re-run `gsd_run query audit-open --json` once more and write the items just acknowledged as new rows to STATE.md under `## Deferred Items` — append to the existing table (creating the section if absent) rather than overwriting it, preserving rows recorded at earlier milestone closes:
|
||||
```markdown
|
||||
## Deferred Items
|
||||
|
||||
@@ -189,7 +189,7 @@ Acknowledging is verdict-preserving and self-invalidating: it never rewrites the
|
||||
|
||||
If output shows all clear (no open items): set `closeout_type=verified_closeout`. If the audit JSON's `acknowledged.total` is `0`, print `All artifact types clear.` and proceed. Otherwise the close is clean only because `{acknowledged.total}` item(s) acknowledged at an earlier milestone close are still being suppressed, not because everything was fixed this time — print `All artifact types clear ({acknowledged.total} previously acknowledged item(s) still suppressed — see STATE.md Deferred Items).` and record `Known verification overrides: 0 newly acknowledged, {acknowledged.total} carried forward from a prior close (see STATE.md Deferred Items)` in the MILESTONES.md entry before proceeding.
|
||||
|
||||
SECURITY: Audit JSON output is structured data from the `audit-open` query handler (same JSON contract as legacy `gsd-tools.cjs audit-open`) — validated and sanitized at source. The `audit-open acknowledge` writer is the only path that sets the `audit_acknowledged` suppression marker — it snapshots each artifact's current state itself from the identifiers passed on the command line, so this workflow never hand-authors the marker. When writing the STATE.md disclosure table, item identifiers, statuses, and deferred-item text are sanitized via `sanitizeForDisplay()` before inclusion. Never inject raw user-supplied content into STATE.md without sanitization.
|
||||
SECURITY: Audit JSON output is structured data from the `audit-open` query handler (same JSON contract as legacy `gsd_run audit-open`) — validated and sanitized at source. The `audit-open acknowledge` writer is the only path that sets the `audit_acknowledged` suppression marker — it snapshots each artifact's current state itself from the identifiers passed on the command line, so this workflow never hand-authors the marker. When writing the STATE.md disclosure table, item identifiers, statuses, and deferred-item text are sanitized via `sanitizeForDisplay()` before inclusion. Never inject raw user-supplied content into STATE.md without sanitization.
|
||||
</step>
|
||||
|
||||
<step name="verify_readiness">
|
||||
@@ -353,7 +353,7 @@ Key accomplishments for this milestone:
|
||||
|
||||
<step name="create_milestone_entry">
|
||||
|
||||
**Note:** MILESTONES.md entry is now created automatically by `gsd-tools.cjs query milestone.complete` in the archive_milestone step. The entry includes version, date, phase/plan/task counts, and accomplishments extracted from SUMMARY.md files.
|
||||
**Note:** MILESTONES.md entry is now created automatically by `gsd_run query milestone.complete` in the archive_milestone step. The entry includes version, date, phase/plan/task counts, and accomplishments extracted from SUMMARY.md files.
|
||||
|
||||
If additional details are needed (e.g., user-provided "Delivered" summary, git range, LOC stats), add them manually after the CLI creates the base entry.
|
||||
|
||||
@@ -517,7 +517,7 @@ AskUserQuestion: "Archive completed quick tasks into this milestone too?" with o
|
||||
|
||||
If "Yes": set `ARCHIVE_QUICK_FLAG="--archive-quick"`. If "Skip" (or `.planning/quick/` is empty): set `ARCHIVE_QUICK_FLAG=""`.
|
||||
|
||||
**Delegate archival to `gsd-tools.cjs query milestone.complete`:**
|
||||
**Delegate archival to `gsd_run query milestone.complete`:**
|
||||
|
||||
```bash
|
||||
ARCHIVE=$(gsd_run query milestone.complete "v[X.Y]" --name "[Milestone Name]" $ARCHIVE_QUICK_FLAG)
|
||||
|
||||
@@ -18,7 +18,7 @@ Claude App cannot forward TUI menu selections back to the host.
|
||||
|
||||
- Per-session: pass `--text` flag to any command (e.g.,
|
||||
`/gsd:discuss-phase --text`)
|
||||
- Per-project: `gsd-tools.cjs query config-set workflow.text_mode true`
|
||||
- Per-project: `gsd_run query config-set workflow.text_mode true`
|
||||
|
||||
Text mode applies to ALL workflows in the session, not just discuss-phase.
|
||||
|
||||
|
||||
@@ -431,7 +431,7 @@ CROSS_AI_TIMEOUT=$(gsd_run query config-get workflow.cross_ai_timeout 2>/dev/nul
|
||||
**If no plans are marked for cross-AI:** Skip to execute_waves.
|
||||
|
||||
**If plans are marked but `cross_ai_command` is empty:** Error — tell user to set
|
||||
`workflow.cross_ai_command` via `gsd-tools.cjs query config-set workflow.cross_ai_command "<command>"`.
|
||||
`workflow.cross_ai_command` via `gsd_run query config-set workflow.cross_ai_command "<command>"`.
|
||||
|
||||
**For each cross-AI plan (sequentially):**
|
||||
|
||||
@@ -1530,7 +1530,7 @@ For 1M+ context models, consider:
|
||||
</context_efficiency>
|
||||
|
||||
<failure_handling>
|
||||
- **Quota / rate-limit (any runtime — #3095):** Agent return body contains a sentinel like `usage limit`, `rate limit`, `429`, `too many requests`, `RESOURCE_EXHAUSTED`, `usage_limit_reached`. Route via `gsd-tools.cjs query agent.classify-failure` → `class: "quota-exceeded"`. Do not offer retry-now; the right action is wait-for-reset and resume.
|
||||
- **Quota / rate-limit (any runtime — #3095):** Agent return body contains a sentinel like `usage limit`, `rate limit`, `429`, `too many requests`, `RESOURCE_EXHAUSTED`, `usage_limit_reached`. Route via `gsd_run query agent.classify-failure` → `class: "quota-exceeded"`. Do not offer retry-now; the right action is wait-for-reset and resume.
|
||||
- **classifyHandoffIfNeeded false failure:** Agent reports "failed" but error is `classifyHandoffIfNeeded is not defined` → Claude Code bug, not GSD. Spot-check (SUMMARY exists, commits present) → if pass, treat as success
|
||||
- **Agent fails mid-plan:** Missing SUMMARY.md → report, ask user how to proceed
|
||||
- **Dependency chain breaks:** Wave 1 fails → Wave 2 dependents likely fail → user chooses attempt or skip
|
||||
|
||||
@@ -68,7 +68,7 @@ Find first PLAN without matching SUMMARY. Decimal phases supported (`01.1-hotfix
|
||||
|
||||
```bash
|
||||
PHASE=$(echo "$PLAN_PATH" | grep -oE '[0-9]+(\.[0-9]+)?-[0-9]+')
|
||||
# config settings can be fetched via gsd-tools.cjs query config-get if needed
|
||||
# config settings can be fetched via gsd_run query config-get if needed
|
||||
```
|
||||
|
||||
<if mode="yolo">
|
||||
@@ -428,7 +428,7 @@ Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready f
|
||||
handles STATE.md/ROADMAP.md updates centrally after merging worktrees to avoid
|
||||
merge conflicts).
|
||||
|
||||
Update STATE.md using gsd-tools.cjs query (or legacy gsd-tools) state mutations:
|
||||
Update STATE.md using gsd_run query (or legacy gsd-tools) state mutations:
|
||||
|
||||
```bash
|
||||
# Auto-detect parallel mode: .git is a file in worktrees, a directory in main repo
|
||||
@@ -465,7 +465,7 @@ gsd_run query state.add-blocker --text-file "${BLOCKER_TEXT_FILE}"
|
||||
</step>
|
||||
|
||||
<step name="update_session_continuity">
|
||||
Update session info using gsd-tools.cjs query (or legacy gsd-tools):
|
||||
Update session info using gsd_run query (or legacy gsd-tools):
|
||||
|
||||
```bash
|
||||
gsd_run query state.record-session \
|
||||
|
||||
@@ -47,7 +47,7 @@ Exit.
|
||||
</step>
|
||||
|
||||
<step name="insert_phase">
|
||||
**Delegate the phase insertion to `gsd-tools.cjs query phase.insert`:**
|
||||
**Delegate the phase insertion to `gsd_run query phase.insert`:**
|
||||
|
||||
```bash
|
||||
RESULT=$(gsd_run query phase.insert "${after_phase}" "${description}")
|
||||
@@ -143,10 +143,10 @@ Project state updated: .planning/STATE.md
|
||||
<success_criteria>
|
||||
Phase insertion is complete when:
|
||||
|
||||
- [ ] `gsd-tools.cjs query phase.insert` executed successfully
|
||||
- [ ] `gsd_run query phase.insert` executed successfully
|
||||
- [ ] Phase directory created
|
||||
- [ ] Roadmap updated with new phase entry (includes "(INSERTED)" marker)
|
||||
- [ ] `gsd-tools.cjs query state.add-roadmap-evolution ...` returned `{ added: true }` or `{ added: false, reason: "duplicate" }`
|
||||
- [ ] `gsd-tools.cjs query state.patch` returned matched next-phase pointer field(s)
|
||||
- [ ] `gsd_run query state.add-roadmap-evolution ...` returned `{ added: true }` or `{ added: false, reason: "duplicate" }`
|
||||
- [ ] `gsd_run query state.patch` returned matched next-phase pointer field(s)
|
||||
- [ ] User informed of next steps and dependency implications
|
||||
</success_criteria>
|
||||
|
||||
@@ -33,7 +33,7 @@ Parse JSON for: `milestone_version`, `milestone_name`, `phase_count`, `completed
|
||||
- `manager_flags.plan` — appended to plan agent init command
|
||||
- `manager_flags.execute` — appended to execute agent init command
|
||||
|
||||
These are empty strings by default. Set via: `gsd-tools.cjs query config-set manager.flags.discuss "--auto --analyze"`
|
||||
These are empty strings by default. Set via: `gsd_run query config-set manager.flags.discuss "--auto --analyze"`
|
||||
|
||||
**If error:** Display the error message and exit.
|
||||
|
||||
|
||||
@@ -602,7 +602,7 @@ VALIDATION_EXISTS=$(ls "${PHASE_DIR}"/*-VALIDATION.md 2>/dev/null | head -1)
|
||||
If missing and Nyquist is still enabled/applicable — ask user:
|
||||
1. Re-run: `/gsd:plan-phase {PHASE} --research ${GSD_WS}`
|
||||
2. Disable Nyquist with the exact command:
|
||||
`gsd-tools.cjs query config-set workflow.nyquist_validation false`
|
||||
`gsd_run query config-set workflow.nyquist_validation false`
|
||||
3. Continue anyway (plans fail Dimension 8)
|
||||
|
||||
Proceed to Step 7.8 (or Step 8 if pattern mapper is disabled) only if user selects 2 or 3.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
3. Status gate: reject `superseded`/`rejected`/`deprecated`; warn on `proposed`; missing status defaults to `accepted`.
|
||||
4. Empty-decisions fallback: if all parsed ADRs have zero `decisions[]`, emit `ADR ingest produced no locked decisions; fall back to discuss-phase for this phase.` and exit with `/gsd:discuss-phase {N}` guidance.
|
||||
5. Generate CONTEXT.md using `<domain>`, `<decisions>`, `<canonical_refs>`, `<specifics>`, `<deferred>`, `<scope_fence>`, map `consequences_positive[]` to Success Criteria and `consequences_negative[]` to Risk Summary, and include `**Source:** ADR Ingest Express Path ({INGEST_PATH})`.
|
||||
6. Commit with `gsd-tools.cjs query commit "docs(${padded_phase}): generate context from ADR ingest" --files "${phase_dir}/${padded_phase}-CONTEXT.md"` and set `context_content`; continue to step 5.
|
||||
6. Commit with `gsd_run query commit "docs(${padded_phase}): generate context from ADR ingest" --files "${phase_dir}/${padded_phase}-CONTEXT.md"` and set `context_content`; continue to step 5.
|
||||
|
||||
**Effect:** This bypasses step 4 (Load CONTEXT.md) since CONTEXT.md was synthesized from ADR input.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<purpose>
|
||||
Orchestrate the full developer profiling flow: consent, session analysis (or questionnaire fallback), profile generation, result display, and artifact creation.
|
||||
|
||||
This workflow wires Phase 1 (session pipeline) and Phase 2 (profiling engine) into a cohesive user-facing experience. All heavy lifting is done by existing `gsd-tools.cjs query` handlers (with legacy `gsd-tools.cjs` parity where needed) and the gsd-user-profiler agent -- this workflow orchestrates the sequence, handles branching, and provides the UX.
|
||||
This workflow wires Phase 1 (session pipeline) and Phase 2 (profiling engine) into a cohesive user-facing experience. All heavy lifting is done by existing `gsd_run query` handlers and the gsd-user-profiler agent -- this workflow orchestrates the sequence, handles branching, and provides the UX.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -386,7 +386,7 @@ gsd_run query generate-claude-profile --analysis "$ANALYSIS_PATH" --global --jso
|
||||
|
||||
Display: "✓ Added profile section to $HOME/.claude/CLAUDE.md"
|
||||
|
||||
**Error handling:** If any `gsd-tools.cjs query` or gsd-tools.cjs call fails, display the error message and use AskUserQuestion to offer "Retry" or "Skip this artifact". On retry, re-run the command. On skip, continue to next artifact.
|
||||
**Error handling:** If any `gsd_run query` call fails, display the error message and use AskUserQuestion to offer "Retry" or "Skip this artifact". On retry, re-run the command. On skip, continue to next artifact.
|
||||
|
||||
---
|
||||
|
||||
@@ -461,7 +461,7 @@ rm -f "$ANALYSIS_PATH" 2>/dev/null
|
||||
- [ ] Profile written to USER-PROFILE.md via write-profile subcommand
|
||||
- [ ] Result display shows report card table and highlight reel with evidence
|
||||
- [ ] Artifact selection uses multiSelect with all options pre-selected
|
||||
- [ ] Artifacts generated sequentially via gsd-tools.cjs query (or gsd-tools.cjs) subcommands
|
||||
- [ ] Artifacts generated sequentially via `gsd_run query` subcommands
|
||||
- [ ] Refresh diff shows changed dimensions when --refresh was used
|
||||
- [ ] Temp files cleaned up on completion
|
||||
</success_criteria>
|
||||
|
||||
@@ -44,11 +44,11 @@ If missing both ROADMAP.md and PROJECT.md: suggest `/gsd:new-project`.
|
||||
</step>
|
||||
|
||||
<step name="load">
|
||||
**Use structured extraction from `gsd-tools.cjs query` (or legacy gsd-tools.cjs):**
|
||||
**Use structured extraction from `gsd_run query`:**
|
||||
|
||||
Instead of reading full files, use targeted tools to get only the data needed for the report:
|
||||
- `ROADMAP=$(gsd-tools.cjs query roadmap.analyze)`
|
||||
- `STATE=$(gsd-tools.cjs query state-snapshot)`
|
||||
- `ROADMAP=$(gsd_run query roadmap.analyze)`
|
||||
- `STATE=$(gsd_run query state-snapshot)`
|
||||
|
||||
This minimizes orchestrator context usage.
|
||||
</step>
|
||||
@@ -96,7 +96,7 @@ Use this instead of manually reading/parsing ROADMAP.md.
|
||||
> blocks are a secondary config aid that may be significantly stale — do NOT use the
|
||||
> CLAUDE.md project description as a source for any progress report field.
|
||||
|
||||
**Generate progress bar from `gsd-tools.cjs query progress` / `progress.json`, then present rich status report:**
|
||||
**Generate progress bar from `gsd_run query progress` / `progress.json`, then present rich status report:**
|
||||
|
||||
```bash
|
||||
# Get formatted progress bar
|
||||
|
||||
@@ -647,7 +647,7 @@ Use Edit tool to make these changes atomically
|
||||
|
||||
**Step 8: Final commit and completion**
|
||||
|
||||
Stage and commit quick task artifacts. This step MUST always run — even if the executor already committed some files (e.g. when running without worktree isolation). The `gsd-tools.cjs query commit` command (or legacy `gsd-tools.cjs` commit) handles already-committed files gracefully.
|
||||
Stage and commit quick task artifacts. This step MUST always run — even if the executor already committed some files (e.g. when running without worktree isolation). The `gsd_run query commit` command handles already-committed files gracefully.
|
||||
|
||||
Build file list:
|
||||
- `${QUICK_DIR}/${quick_id}-PLAN.md`
|
||||
|
||||
@@ -78,7 +78,7 @@ Wait for confirmation.
|
||||
</step>
|
||||
|
||||
<step name="execute_removal">
|
||||
**Delegate the entire removal operation to `gsd-tools.cjs query phase.remove`:**
|
||||
**Delegate the entire removal operation to `gsd_run query phase.remove`:**
|
||||
|
||||
```bash
|
||||
RESULT=$(gsd_run query phase.remove "${target}")
|
||||
@@ -141,7 +141,7 @@ Would you like to:
|
||||
|
||||
- Don't remove completed phases (have SUMMARY.md files) without --force
|
||||
- Don't remove current or past phases
|
||||
- Don't manually renumber — use `gsd-tools.cjs query phase.remove` which handles all renumbering
|
||||
- Don't manually renumber — use `gsd_run query phase.remove` which handles all renumbering
|
||||
- Don't add "removed phase" notes to STATE.md — git commit is the record
|
||||
- Don't modify completed phase directories
|
||||
</anti_patterns>
|
||||
@@ -150,7 +150,7 @@ Would you like to:
|
||||
Phase removal is complete when:
|
||||
|
||||
- [ ] Target phase validated as future/unstarted
|
||||
- [ ] `gsd-tools.cjs query phase.remove` executed successfully
|
||||
- [ ] `gsd_run query phase.remove` executed successfully
|
||||
- [ ] Changes committed with descriptive message
|
||||
- [ ] User informed of changes
|
||||
</success_criteria>
|
||||
|
||||
@@ -434,7 +434,7 @@ AskUserQuestion([
|
||||
|
||||
If "Other (Group B or custom)" is selected, prompt the user to enter the runtime name as a free-text string.
|
||||
If the selected runtime differs from the stored `runtime` key, update `runtime` via
|
||||
`gsd-tools.cjs query config-set runtime <value>` before proceeding to Step C.
|
||||
`gsd_run query config-set runtime <value>` before proceeding to Step C.
|
||||
|
||||
**Step C — Configure tier overrides for the selected runtime:**
|
||||
|
||||
@@ -494,7 +494,7 @@ change.
|
||||
Merge the new settings into the existing config at `$GSD_CONFIG_PATH`. This merge is the
|
||||
core correctness invariant: **preserve every unrelated key** — do not clobber siblings.
|
||||
|
||||
Apply each selected value via `gsd-tools.cjs query config-set <key> <value>` so the central
|
||||
Apply each selected value via `gsd_run query config-set <key> <value>` so the central
|
||||
validator (`isValidConfigKey`) accepts the write and the deep-merge preserves unrelated
|
||||
keys and sibling sub-objects.
|
||||
|
||||
@@ -567,7 +567,7 @@ anything not listed in Sections 1–8 MUST survive the update):
|
||||
```
|
||||
|
||||
Never emit a full overwrite of the file that omits keys the user did not touch. Always
|
||||
route each write through `gsd-tools.cjs query config-set` so sibling preservation is handled by
|
||||
route each write through `gsd_run query config-set` so sibling preservation is handled by
|
||||
the central setter.
|
||||
</step>
|
||||
|
||||
@@ -807,7 +807,7 @@ UI/AI phase gates), use /gsd:settings.
|
||||
- [ ] Numeric inputs validated — non-numeric rejected and re-prompted
|
||||
- [ ] Branch-template inputs validated — non-default must contain a placeholder
|
||||
- [ ] Null-allowed fields accept an empty input as a clear
|
||||
- [ ] Writes routed through `gsd-tools.cjs query config-set` so unrelated keys are preserved
|
||||
- [ ] Writes routed through `gsd_run query config-set` so unrelated keys are preserved
|
||||
- [ ] Section 7 shows current runtime and built-in tier table
|
||||
- [ ] Group B runtimes display "(no built-in default — your runtime handles model selection)"
|
||||
- [ ] Override set/clear/keep paths all work correctly for each tier
|
||||
|
||||
@@ -53,7 +53,7 @@ If no `.planning/` directory exists, inform the user to run `/gsd:new-project` f
|
||||
</step>
|
||||
|
||||
<step name="mvp_summary">
|
||||
**MVP phase summary.** Read all phases via `gsd-tools.cjs query roadmap.analyze` (Phase 1's `cmdRoadmapAnalyze` surfaces a `mode` field per phase). Count phases by mode:
|
||||
**MVP phase summary.** Read all phases via `gsd_run query roadmap.analyze` (Phase 1's `cmdRoadmapAnalyze` surfaces a `mode` field per phase). Count phases by mode:
|
||||
|
||||
```bash
|
||||
ANALYZE=$(gsd_run query roadmap.analyze)
|
||||
|
||||
@@ -221,6 +221,6 @@ updated: {today ISO date}
|
||||
- Slugs from $ARGUMENTS are sanitized before use in file paths: only [a-z0-9-] allowed, max 60 chars, reject ".." and "/"
|
||||
- File names from readdir/ls are sanitized before display: strip non-printable chars and ANSI sequences
|
||||
- Artifact content (thread titles, goal sections, next steps) rendered as plain text only — never executed or passed to agent prompts without DATA_START/DATA_END boundaries
|
||||
- Status fields read via gsd-tools.cjs query frontmatter.get — never eval'd or shell-expanded
|
||||
- The generate-slug call for new threads runs through gsd-tools.cjs query (or gsd-tools) which sanitizes input — keep that pattern
|
||||
- Status fields read via gsd_run query frontmatter.get — never eval'd or shell-expanded
|
||||
- The generate-slug call for new threads runs through gsd_run query (or gsd-tools) which sanitizes input — keep that pattern
|
||||
</security_notes>
|
||||
|
||||
@@ -131,7 +131,7 @@ Resolve before transition. Review: `/gsd:audit-uat`
|
||||
```
|
||||
|
||||
This preliminary check blocks obviously unresolved verification early, ahead
|
||||
of the authoritative gate below. `gsd-tools.cjs query phase.complete` (in
|
||||
of the authoritative gate below. `gsd_run query phase.complete` (in
|
||||
`update_roadmap_and_state`) remains the authoritative stale-aware gate and
|
||||
fail-closes unless canonical verification status is `passed`.
|
||||
|
||||
@@ -197,7 +197,7 @@ If found, delete them — phase is complete, handoffs are stale.
|
||||
|
||||
<step name="update_roadmap_and_state">
|
||||
|
||||
**Delegate ROADMAP.md and STATE.md updates to `gsd-tools.cjs query phase.complete`:**
|
||||
**Delegate ROADMAP.md and STATE.md updates to `gsd_run query phase.complete`:**
|
||||
|
||||
```bash
|
||||
TRANSITION=$(gsd_run query phase.complete "${current_phase}")
|
||||
@@ -333,7 +333,7 @@ This step is fully delegated to `graduation.md`. It handles guard checks (featur
|
||||
|
||||
<step name="update_current_position_after_transition">
|
||||
|
||||
**Note:** Basic position updates (Current Phase, Status, Current Plan, Last Activity) were already handled by `gsd-tools.cjs query phase.complete` in the update_roadmap_and_state step.
|
||||
**Note:** Basic position updates (Current Phase, Status, Current Plan, Last Activity) were already handled by `gsd_run query phase.complete` in the update_roadmap_and_state step.
|
||||
|
||||
Verify the updates are correct by reading STATE.md. If the progress bar needs updating, use:
|
||||
|
||||
@@ -437,7 +437,7 @@ Resume file: None
|
||||
|
||||
**MANDATORY: Verify milestone status before presenting next steps.**
|
||||
|
||||
**Use the transition result from `gsd-tools.cjs query phase.complete`:**
|
||||
**Use the transition result from `gsd_run query phase.complete`:**
|
||||
|
||||
The `is_last_phase` field from the phase complete result tells you directly:
|
||||
- `is_last_phase: false` → More phases remain → Go to **Route A**
|
||||
|
||||
@@ -296,8 +296,8 @@ installer does not know about and will delete during the wipe.
|
||||
**Do not use bash path-stripping (`${filepath#$RUNTIME_DIR/}`) or `node -e require()`
|
||||
inline** — those patterns fail when `$RUNTIME_DIR` is unset and the stripped
|
||||
relative path may not match manifest key format, which causes CUSTOM_COUNT=0
|
||||
even when custom files exist (bug #1997). Use `gsd-tools.cjs query detect-custom-files`
|
||||
or the bundled `gsd-tools.cjs detect-custom-files` path — both resolve paths
|
||||
even when custom files exist (bug #1997). Use `gsd_run query detect-custom-files`
|
||||
or the bundled `gsd_run detect-custom-files` path — both resolve paths
|
||||
reliably with Node.js `path.relative()`.
|
||||
|
||||
First, resolve the config directory (`RUNTIME_DIR`) from the install scope
|
||||
|
||||
@@ -635,7 +635,7 @@ All tests passed. Phase {phase} marked complete.
|
||||
<step name="scan_phase_artifacts">
|
||||
Run phase artifact scan to surface any open items before marking phase verified:
|
||||
|
||||
`audit-open` is CJS-only until registered on `gsd-tools.cjs query`:
|
||||
`audit-open` is CJS-only until registered on `gsd_run query`:
|
||||
|
||||
```bash
|
||||
gsd_run query audit-open --json
|
||||
|
||||
@@ -18,7 +18,7 @@ allowed-tools:
|
||||
Import external plan files into the GSD planning system with conflict detection against PROJECT.md decisions.
|
||||
|
||||
- **--from**: Import an external plan file, detect conflicts, write as GSD PLAN.md, validate via gsd-plan-checker.
|
||||
- **--from-gsd2**: Reverse-migrate a GSD-2 project (`.gsd/` directory) back to GSD v1 (`.planning/`) format. Runs `gsd-tools.cjs from-gsd2`. Pass `--path <dir>` to migrate a project at a different path.
|
||||
- **--from-gsd2**: Reverse-migrate a GSD-2 project (`.gsd/` directory) back to GSD v1 (`.planning/`) format. Runs `gsd_run from-gsd2`. Pass `--path <dir>` to migrate a project at a different path.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$comment": "Growth ack (#2914 fragment). Reason: #3809 routes every command-position shim reference in runtime-loaded markdown through the canonical gsd_run launcher. The substitution SHRANK 19 emitted files; this is the only one that grew. Its line 65 is a descriptive comment inside a fenced block, and rewriting it to name a command at all would place a gsd_run token ahead of the file's canonical preamble at line 158, which runtime-launcher-parity's (B-agents) arm correctly rejects. The comment therefore names no command and explains where the config is actually loaded instead, costing 3 bytes. gsd-research-synthesizer.md 13847 -> 13850 LF bytes (+3).",
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"gsd-research-synthesizer.md": {
|
||||
"reason": "descriptive comment reworded to name no command, so the file's first gsd_run token stays behind its canonical preamble (runtime-launcher-parity B-agents); +3 bytes"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,28 @@ const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const ROUTER_PATH = path.join(ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
|
||||
const SCAN_DIRS = ['agents', path.join('gsd-core', 'workflows')];
|
||||
// #3809 widened this from agents/ + workflows/ to include gsd-core/references/,
|
||||
// which was carrying 37 bare calls the guard simply never looked at.
|
||||
//
|
||||
// commands/ is deliberately NOT here, and that is a finding rather than an
|
||||
// oversight. Its files cannot use the shared launcher the way workflows and agents
|
||||
// do: tests/graphify-visualization.test.cjs extracts individual Step-3 shell chains
|
||||
// and runs them standalone, so each fenced block needs its OWN preamble —
|
||||
// graphify.md carries five on purpose, and collapsing them to one produces
|
||||
// `gsd_run: command not found` (exit 127). tests/gsd-tools-path-refs.test.cjs
|
||||
// (#1766) separately pins commands/gsd/workstreams.md to the literal string
|
||||
// `gsd-tools query workstream.list`. Bringing commands/ under this guard therefore
|
||||
// needs those two contracts reconciled first; it is not a scan-set widening.
|
||||
//
|
||||
// skills/ is absent for a different reason: it is generated from commands/ by
|
||||
// scripts/gen-plugin-skills.cjs and pinned by lint:generated-sync, so guarding the
|
||||
// source guards both, and scanning the generated mirror would double-report every
|
||||
// future offender.
|
||||
const SCAN_DIRS = [
|
||||
'agents',
|
||||
path.join('gsd-core', 'workflows'),
|
||||
path.join('gsd-core', 'references'),
|
||||
];
|
||||
|
||||
// Derive the verb set the bare-call guard matches against. Most top-level
|
||||
// verbs live in the host-command router table as `'verb': routeHandler` entries
|
||||
@@ -87,7 +108,6 @@ const PROSE_ALLOWLIST = [
|
||||
{ file: 'agents/gsd-roadmapper.md', line: 642, reason: 'parenthetical "e.g." naming SDK queries a user *could* run; not an agent instruction' },
|
||||
{ file: 'agents/gsd-intel-updater.md', line: 40, reason: 'cross-platform note names the `gsd-tools intel <subcommand>` CLI surface descriptively ("CLI invocations go through..."); not an agent instruction' },
|
||||
{ file: 'gsd-core/workflows/execute-plan.md', line: 415, reason: 'describes the downstream SDK validation step (`validated downstream by ...`); names the mechanism, does not instruct the agent to type it' },
|
||||
{ file: 'agents/gsd-research-synthesizer.md', line: 65, reason: 'a code comment inside a fenced block explaining what the commit step loads (`# Planning config loaded via gsd-tools query ...`); descriptive, not an invocation — and explicitly names gsd-tools.cjs as the alternative' },
|
||||
];
|
||||
|
||||
// Resolver-snippet definition lines / probes that must never be flagged. A line
|
||||
|
||||
@@ -1,27 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
// Regression guard for #2020 — dead SDK file references (sdk/src/..., sdk/dist/...)
|
||||
// in runtime-loaded markdown cause AI runtimes to `find` them; on Git Bash for
|
||||
// Windows `find /` traverses the whole drive (14h+, orphaned find.exe, 4M+ handles).
|
||||
// The SDK package was retired (ADR-0174), so these paths never resolve.
|
||||
// Guard: runtime-loaded markdown must not carry a reference an AI runtime will try to
|
||||
// LOCATE on the filesystem. When a runtime meets a file-shaped token it cannot resolve,
|
||||
// it falls back to searching for it — and on Git Bash for Windows `find /` maps to the
|
||||
// drive root, so `find.exe` traverses the whole disk (orphaned processes, handle leak,
|
||||
// a pegged core until someone reaps it by hand).
|
||||
//
|
||||
// Scans the markdown a runtime loads + tries to locate references in
|
||||
// (agents/, workflows/, references/) and fails on any sdk/(src|dist|handlers)
|
||||
// file-path reference. Code-comment mentions in *.cjs (historical prose, not
|
||||
// locatable file refs) are out of scope.
|
||||
// The guard is a RULE TABLE, deliberately, because the first version of it was not.
|
||||
//
|
||||
// #2020 shipped a guard hardcoded to `sdk/(src|dist|handlers)/` — the three dead paths
|
||||
// that had caused the storm. That is an instance fix wearing a regression test: it
|
||||
// proved those three paths were gone and said nothing about the class. Seven weeks
|
||||
// later #3809 reproduced the identical storm under a different token, and the guard
|
||||
// was structurally incapable of seeing it. Adding a rule here must stay a one-entry
|
||||
// change, so the next recurrence is a table row rather than a third incident.
|
||||
//
|
||||
// Scope: the markdown a runtime actually loads and resolves references against —
|
||||
// agents/, gsd-core/workflows/, gsd-core/references/, commands/. Prose mentions inside
|
||||
// *.cjs sources are out of scope: nothing tries to locate those.
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fc = require('fast-check');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
// Runtime-loaded markdown surfaces (the issue is about references a runtime
|
||||
// tries to LOCATE as files — agents/workflows/references, not source comments).
|
||||
const SCAN_DIRS = ['agents', 'gsd-core/workflows', 'gsd-core/references'];
|
||||
// A dead SDK file-path reference: sdk/src|sdk/dist|sdk/handlers followed by a path.
|
||||
const SCAN_DIRS = ['agents', 'gsd-core/workflows', 'gsd-core/references', 'commands'];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule A (#2020) — a dead SDK file-path reference. The SDK package was retired
|
||||
// by ADR-0174, so these paths never resolve and a runtime will hunt for them.
|
||||
// ---------------------------------------------------------------------------
|
||||
const DEAD_SDK_REF = /sdk\/(?:src|dist|handlers)\//;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule B (#3809) — the runtime shim named in COMMAND position.
|
||||
//
|
||||
// The shim filename is not a command on any platform. package.json `bin` ships
|
||||
// `gsd-core`, `gsd-tools`, `gsd_run`, `gsd-mcp-server`; the .cjs file exists only at
|
||||
// <runtime-root>/gsd-core/bin/. CONTEXT.md -> Runtime Launcher Module makes `gsd_run`
|
||||
// the single sanctioned entry point: "Canonical space-safe shell preamble (`gsd_run`)
|
||||
// used by every workflow bash block to invoke the GSD runtime CLI."
|
||||
//
|
||||
// So a workflow that says `<shim> query phase.add` instructs the agent to run something
|
||||
// that exits 127, after which the file-shaped token sends it looking for the file.
|
||||
//
|
||||
// What separates an INVOCATION from the four legitimate ways this filename appears is
|
||||
// the token that follows it. Being lenient here is the entire point — the guard must
|
||||
// not flag the launcher's own resolver, a real `node <path>/<shim>` call, a bare path,
|
||||
// or prose that simply names the file. See the negative-space rows below, each of which
|
||||
// is a form that exists in the tree today and must keep working.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Built at runtime so this line is not itself an invocation the guard would flag.
|
||||
const SHIM = ['gsd-tools', '.cjs'].join('');
|
||||
|
||||
// The pattern is spelled out rather than escaped from SHIM at build time. A
|
||||
// `SHIM.replace(/\./g, '\\.')` here is a hand-rolled escaper: it handles the dot and
|
||||
// nothing else, which CodeQL flags as js/incomplete-sanitization (it does not escape
|
||||
// backslashes) and which local/no-adhoc-regex-escape bans outright — the canonical
|
||||
// escaper lives in src/pattern.cts. Since SHIM is a compile-time constant whose only
|
||||
// metacharacter is the dot, the honest fix is to carry no escaping logic at all.
|
||||
// SHIM_PATTERN and SHIM are pinned to each other by a test below so they cannot drift.
|
||||
const SHIM_PATTERN = 'gsd-tools\\.cjs';
|
||||
const SHIM_RE = new RegExp(
|
||||
// not preceded by a path separator, word char, or hyphen (excludes `<dir>/<shim>`)
|
||||
`(?<![\\w./\\\\-])${SHIM_PATTERN}` +
|
||||
// at least one space or tab, then the following token
|
||||
'[ \\t]+([a-z][a-z0-9.-]*)',
|
||||
'g',
|
||||
);
|
||||
|
||||
// The verb roster is DERIVED from gsd-tools.cjs's own exports, never hand-copied.
|
||||
//
|
||||
// That file already carries three hand-maintained rosters — TOP_LEVEL_USAGE,
|
||||
// HOST_COMMAND_ROUTERS and SKIP_ROOT_RESOLUTION — whose divergence is a named repo
|
||||
// defect (DEFECT.GENERATIVE-FIX), pinned by the parity test in tests/commands.test.cjs.
|
||||
// A hand-copied fourth roster here would be that same defect wearing a guard's clothes,
|
||||
// and it already was: the first cut of this list was transcribed from an INSTALLED
|
||||
// older binary and silently missed 22 verbs this tree ships, including `websearch`,
|
||||
// `windows` and `state-snapshot`. Deriving costs one require and cannot drift.
|
||||
//
|
||||
// The require is lazy and memoised: tests/helpers.cjs defers its own built-lib require
|
||||
// for the same reason, so an unbuilt tree fails one test with an actionable message
|
||||
// instead of crashing the file before a single test() registers.
|
||||
let _cliVerbs = null;
|
||||
function cliVerbs() {
|
||||
if (_cliVerbs) return _cliVerbs;
|
||||
const { HOST_COMMAND_ROUTERS, TOP_LEVEL_USAGE } = require('../gsd-core/bin/gsd-tools.cjs');
|
||||
const listed = TOP_LEVEL_USAGE.match(/Commands: ([\s\S]*?)\n\nGlobal flags:/);
|
||||
assert.ok(listed, 'TOP_LEVEL_USAGE must contain a "Commands: ...\\n\\nGlobal flags:" block');
|
||||
_cliVerbs = new Set([
|
||||
...Object.keys(HOST_COMMAND_ROUTERS),
|
||||
...listed[1].split(',').map((s) => s.trim()).filter(Boolean),
|
||||
// `query` dispatches through the Command Routing Hub ahead of the host-router
|
||||
// table, so it appears in neither export — yet it is the form 45 of the 50 #3809
|
||||
// offenders used. Verified live in this tree: bare `query` is a usage error while
|
||||
// `query state-snapshot` dispatches and returns JSON.
|
||||
'query',
|
||||
]);
|
||||
return _cliVerbs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcommand tokens invoked on the bare shim in one line of markdown.
|
||||
* Returns [] for every legitimate form. Pure — no filesystem access.
|
||||
*/
|
||||
function findShimInvocations(line) {
|
||||
// The canonical launcher's own single source of truth assigns the filename.
|
||||
if (line.includes('_GSD_SHIM_NAME=')) return [];
|
||||
|
||||
const found = [];
|
||||
let m;
|
||||
SHIM_RE.lastIndex = 0;
|
||||
while ((m = SHIM_RE.exec(line)) !== null) {
|
||||
// `node <path>/<shim> <verb>` is resolvable and fine. `node <shim> <verb>` is NOT —
|
||||
// node resolves a bare filename against cwd, so it fails exactly like the bare form.
|
||||
// The exemption therefore requires a real path separator before the shim.
|
||||
if (/\bnode[ \t]+["']?[^ \t"']*[/\\]$/.test(line.slice(0, m.index))) continue;
|
||||
// A dotted subcommand is always `<family>.<verb>` and the family is itself a roster
|
||||
// verb, so testing the first segment covers `phase.add` and `state.patch` without
|
||||
// resorting to "contains a dot", which also matches prose like `v1.2`.
|
||||
const token = m[1];
|
||||
if (cliVerbs().has(token.split('.')[0])) {
|
||||
found.push(token);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
const RULES = [
|
||||
{
|
||||
id: '#2020',
|
||||
label: 'dead SDK file reference',
|
||||
remedy: 'the SDK package was retired (ADR-0174) — point at a live path',
|
||||
scan: (line) => (DEAD_SDK_REF.test(line) ? ['sdk/'] : []),
|
||||
},
|
||||
{
|
||||
id: '#3809',
|
||||
label: `bare \`${SHIM}\` invocation`,
|
||||
remedy: 'call the canonical launcher instead: `gsd_run <subcommand>`',
|
||||
scan: findShimInvocations,
|
||||
},
|
||||
];
|
||||
|
||||
function walkMd(dir, out = []) {
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
||||
@@ -34,21 +157,159 @@ function walkMd(dir, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
describe('#2020 — no dead SDK file references in runtime-loaded markdown', () => {
|
||||
/** Offenders for one rule across every runtime-loaded markdown file. */
|
||||
function scanTree(rule) {
|
||||
const offenders = [];
|
||||
for (const rel of SCAN_DIRS) {
|
||||
const absDir = path.join(ROOT, rel);
|
||||
for (const file of walkMd(absDir)) {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (const file of walkMd(path.join(ROOT, rel))) {
|
||||
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
||||
lines.forEach((line, i) => {
|
||||
if (DEAD_SDK_REF.test(line)) offenders.push(`${path.relative(ROOT, file)}:${i + 1}`);
|
||||
for (const hit of rule.scan(line)) {
|
||||
offenders.push(`${path.relative(ROOT, file)}:${i + 1} (${hit})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return offenders;
|
||||
}
|
||||
|
||||
test('agents/workflows/references contain no sdk/src|sdk/dist|sdk/handlers references', () => {
|
||||
assert.deepEqual(offenders, [],
|
||||
`Dead SDK file references found (runtimes \`find\` these → #2020 Windows find.exe storm):\n${offenders.join('\n')}`);
|
||||
describe('runtime-loaded markdown carries no unresolvable reference', () => {
|
||||
for (const rule of RULES) {
|
||||
test(`${rule.id} — no ${rule.label} in ${SCAN_DIRS.join(', ')}`, () => {
|
||||
const offenders = scanTree(rule);
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`${rule.id}: ${offenders.length} ${rule.label}(s) found. Runtimes resolve these by ` +
|
||||
`filesystem search — on Git Bash for Windows that is a full-drive find.exe storm.\n` +
|
||||
`Remedy: ${rule.remedy}.\n${offenders.join('\n')}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('#3809 — what counts as a bare shim invocation', () => {
|
||||
// Positive space: forms that send an agent hunting for the file.
|
||||
const INVOCATIONS = [
|
||||
['inline code in prose', `**Delegate the phase addition to \`${SHIM} query phase.add\`:**`, 'query'],
|
||||
['command substitution', `- \`ROADMAP=$(${SHIM} query roadmap.analyze)\``, 'query'],
|
||||
['dotted subcommand', `\`${SHIM} query state.add-roadmap-evolution ...\``, 'query'],
|
||||
['hyphenated subcommand', `Use \`${SHIM} detect-custom-files\``, 'detect-custom-files'],
|
||||
['bare verb, no backticks', `# config settings can be fetched via ${SHIM} query config-get`, 'query'],
|
||||
['commit verb', `via \`${SHIM} commit-to-subrepo\`. File paths are relative`, 'commit-to-subrepo'],
|
||||
];
|
||||
|
||||
for (const [name, line, expected] of INVOCATIONS) {
|
||||
test(`flags ${name}`, () => {
|
||||
assert.deepEqual(findShimInvocations(line), [expected]);
|
||||
});
|
||||
test(`flags ${name} with a CRLF line ending`, () => {
|
||||
// A trailing \r must not defeat the match — this repo has a documented
|
||||
// bug class of regexes that only ever saw \n.
|
||||
assert.deepEqual(findShimInvocations(`${line}\r`), [expected]);
|
||||
});
|
||||
}
|
||||
|
||||
// Negative space: every legitimate way the filename appears in the tree today.
|
||||
// Each row is a real line; flagging any of them would be an over-broad fix.
|
||||
const LEGITIMATE = [
|
||||
['the launcher resolver assignment', `_GSD_SHIM_NAME="${SHIM}"; _GSD_RUNTIME_ROOT="\${RUNTIME_DIR:-$(pwd)}"`],
|
||||
['a node-prefixed invocation', ` node <config-dir>/gsd-core/bin/${SHIM} restore-custom-files \\`],
|
||||
['a quoted node-prefixed invocation', `node "$GSD_DIR/gsd-core/bin/${SHIM}" query commit`],
|
||||
['a qualified path with no subcommand', ` "$PREFERRED_CONFIG_DIR/gsd-core/bin/${SHIM}" \\`],
|
||||
['prose naming the file', `# Resolve ${SHIM} WITHOUT yet knowing GSD_DIR. The running workflow lives`],
|
||||
['prose whose next token is an English word', `# shim-only install (${SHIM} present, \`gsd-tools\` not on PATH) the bare call exits`],
|
||||
['prose with a lowercase English word after', `the ${SHIM} file lives under gsd-core/bin`],
|
||||
['the filename at end of line', `authoritative tool for THIS install is ${SHIM}`],
|
||||
['prose with a hyphenated English word after', `the ${SHIM} built-in helper does X`],
|
||||
['prose with a version number after', `the ${SHIM} v1.2 release notes`],
|
||||
];
|
||||
|
||||
for (const [name, line] of LEGITIMATE) {
|
||||
test(`ignores ${name}`, () => {
|
||||
assert.deepEqual(findShimInvocations(line), []);
|
||||
});
|
||||
}
|
||||
|
||||
// `node <shim>` with no directory is NOT exempt: node resolves a bare filename
|
||||
// against cwd, so it fails exactly like the bare form (found at
|
||||
// gsd-core/references/model-profiles.md:231).
|
||||
// The verb roster is the whole advertised command surface, not the subset that happens
|
||||
// to appear in the tree — a bare verb nobody has written yet must still be caught.
|
||||
for (const verb of ['phase', 'state', 'verify', 'roadmap', 'milestone', 'worktree']) {
|
||||
test(`flags the bare verb \`${verb}\`, which appears nowhere in the tree today`, () => {
|
||||
assert.deepEqual(findShimInvocations(`run \`${SHIM} ${verb} list\``), [verb]);
|
||||
});
|
||||
}
|
||||
|
||||
test('flags a node-prefixed shim that carries no path', () => {
|
||||
assert.deepEqual(findShimInvocations(`\`node ${SHIM} effort sync --apply\``), ['effort']);
|
||||
});
|
||||
|
||||
// Boundary: the separator between the filename and the subcommand.
|
||||
test('zero separating spaces is not an invocation (limit-1)', () => {
|
||||
assert.deepEqual(findShimInvocations(`\`${SHIM}query\``), []);
|
||||
});
|
||||
test('exactly one separating space is an invocation (limit)', () => {
|
||||
assert.deepEqual(findShimInvocations(`\`${SHIM} query\``), ['query']);
|
||||
});
|
||||
test('more than one separating space is still an invocation (limit+1)', () => {
|
||||
assert.deepEqual(findShimInvocations(`\`${SHIM} query\``), ['query']);
|
||||
assert.deepEqual(findShimInvocations(`\`${SHIM}\tquery\``), ['query']);
|
||||
});
|
||||
|
||||
// Properties — the matcher is a parser, so pin its two directional invariants.
|
||||
test('property: a path-qualified or node-prefixed shim is never flagged', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.constantFrom('node ', 'node "', "node '", '/', './', '../', 'gsd-core/bin/', '$DIR/'),
|
||||
fc.constantFrom('query', 'commit', 'phase.add', 'audit-open', 'from-gsd2'),
|
||||
(prefix, verb) => {
|
||||
const line = prefix.endsWith('/')
|
||||
? ` ${prefix}${SHIM} ${verb}`
|
||||
: ` ${prefix}gsd-core/bin/${SHIM} ${verb}`;
|
||||
assert.deepEqual(findShimInvocations(line), []);
|
||||
},
|
||||
),
|
||||
{ numRuns: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
test('property: a bare shim followed by a subcommand is always flagged', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.constantFrom('', '`', '$(', '- ', 'run ', '**via '),
|
||||
fc.constantFrom('query', 'commit', 'phase.add', 'audit-open', 'from-gsd2', 'commit-to-subrepo'),
|
||||
fc.integer({ min: 1, max: 4 }),
|
||||
(prefix, verb, spaces) => {
|
||||
const line = `${prefix}${SHIM}${' '.repeat(spaces)}${verb}`;
|
||||
assert.deepEqual(findShimInvocations(line), [verb]);
|
||||
},
|
||||
),
|
||||
{ numRuns: 300 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#3809 — the verb roster is derived, not copied', () => {
|
||||
test('SHIM_PATTERN matches SHIM exactly, so the two cannot drift', () => {
|
||||
assert.equal(new RegExp(`^${SHIM_PATTERN}$`).test(SHIM), true,
|
||||
`SHIM_PATTERN (${SHIM_PATTERN}) no longer matches SHIM (${SHIM})`);
|
||||
// And prove the pattern's dot is escaped rather than a wildcard.
|
||||
assert.equal(new RegExp(`^${SHIM_PATTERN}$`).test('gsd-toolsXcjs'), false,
|
||||
'the dot in SHIM_PATTERN must be escaped, not a wildcard');
|
||||
});
|
||||
|
||||
test('covers every command gsd-tools.cjs actually dispatches', () => {
|
||||
const { HOST_COMMAND_ROUTERS } = require('../gsd-core/bin/gsd-tools.cjs');
|
||||
const missing = Object.keys(HOST_COMMAND_ROUTERS).filter((v) => !cliVerbs().has(v));
|
||||
assert.deepEqual(missing, [], `verb(s) dispatched by gsd-tools.cjs but invisible to this guard: ${missing.join(', ')}`);
|
||||
});
|
||||
|
||||
test('recognises verbs that no hand-copied list had', () => {
|
||||
// These ship in this tree but were absent from the hand-copied first cut.
|
||||
for (const verb of ['websearch', 'windows', 'state-snapshot', 'context-predicates']) {
|
||||
assert.equal(cliVerbs().has(verb), true, `roster is missing ${verb}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user