diff --git a/.changeset/agile-geese-squeak.md b/.changeset/agile-geese-squeak.md new file mode 100644 index 000000000..e5c6e6c8e --- /dev/null +++ b/.changeset/agile-geese-squeak.md @@ -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) diff --git a/.changeset/eager-yaks-wander.md b/.changeset/eager-yaks-wander.md new file mode 100644 index 000000000..3988afe95 --- /dev/null +++ b/.changeset/eager-yaks-wander.md @@ -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 ` 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) diff --git a/agents/gsd-research-synthesizer.md b/agents/gsd-research-synthesizer.md index cb7c6be32..c95f4f281 100644 --- a/agents/gsd-research-synthesizer.md +++ b/agents/gsd-research-synthesizer.md @@ -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: diff --git a/commands/gsd/import.md b/commands/gsd/import.md index a87fa56a1..1aace3667 100644 --- a/commands/gsd/import.md +++ b/commands/gsd/import.md @@ -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 ` 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 ` to migrate a project at a different path. diff --git a/gsd-core/references/autonomous-smart-discuss.md b/gsd-core/references/autonomous-smart-discuss.md index 1e5554df7..357be94f0 100644 --- a/gsd-core/references/autonomous-smart-discuss.md +++ b/gsd-core/references/autonomous-smart-discuss.md @@ -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: diff --git a/gsd-core/references/decimal-phase-calculation.md b/gsd-core/references/decimal-phase-calculation.md index 995475c1a..1917bf682 100644 --- a/gsd-core/references/decimal-phase-calculation.md +++ b/gsd-core/references/decimal-phase-calculation.md @@ -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" ``` diff --git a/gsd-core/references/execute-phase-between-wave-reset.md b/gsd-core/references/execute-phase-between-wave-reset.md index 508ee5d51..c32c5ec02 100644 --- a/gsd-core/references/execute-phase-between-wave-reset.md +++ b/gsd-core/references/execute-phase-between-wave-reset.md @@ -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 diff --git a/gsd-core/references/git-integration.md b/gsd-core/references/git-integration.md index 846ffc06a..ad8771ce3 100644 --- a/gsd-core/references/git-integration.md +++ b/gsd-core/references/git-integration.md @@ -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/ ``` @@ -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/ ``` @@ -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 ``` diff --git a/gsd-core/references/git-planning-commit.md b/gsd-core/references/git-planning-commit.md index 97fdae8dd..06d097e16 100644 --- a/gsd-core/references/git-planning-commit.md +++ b/gsd-core/references/git-planning-commit.md @@ -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.` 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 diff --git a/gsd-core/references/model-profiles.md b/gsd-core/references/model-profiles.md index 5133aba90..9743ac5bc 100644 --- a/gsd-core/references/model-profiles.md +++ b/gsd-core/references/model-profiles.md @@ -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. diff --git a/gsd-core/references/phase-argument-parsing.md b/gsd-core/references/phase-argument-parsing.md index 4d6c60e71..dea3ba534 100644 --- a/gsd-core/references/phase-argument-parsing.md +++ b/gsd-core/references/phase-argument-parsing.md @@ -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) ``` diff --git a/gsd-core/references/planner-revision.md b/gsd-core/references/planner-revision.md index d1066f45b..af2cc2e06 100644 --- a/gsd-core/references/planner-revision.md +++ b/gsd-core/references/planner-revision.md @@ -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 diff --git a/gsd-core/references/planning-config.md b/gsd-core/references/planning-config.md index 5644834c5..6338fbfd7 100644 --- a/gsd-core/references/planning-config.md +++ b/gsd-core/references/planning-config.md @@ -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). --- diff --git a/gsd-core/references/runtime-aware-dispatch.md b/gsd-core/references/runtime-aware-dispatch.md index 3d55fad72..7c9ccfea1 100644 --- a/gsd-core/references/runtime-aware-dispatch.md +++ b/gsd-core/references/runtime-aware-dispatch.md @@ -19,7 +19,7 @@ preserves named-dispatch behavior on older GSD installs that lack the query. The persona rides `${AGENT_SKILLS_}` (Phase 3 / #2510) regardless of the resolved type — on non-Claude runtimes with no `agent_skills` config, -`gsd-tools query agent-skills ` returns the installed agent prompt as +`gsd_run query agent-skills ` 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. diff --git a/gsd-core/references/universal-anti-patterns.md b/gsd-core/references/universal-anti-patterns.md index 7a3cfe28c..9e35fe99f 100644 --- a/gsd-core/references/universal-anti-patterns.md +++ b/gsd-core/references/universal-anti-patterns.md @@ -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. diff --git a/gsd-core/references/verify-mvp-mode.md b/gsd-core/references/verify-mvp-mode.md index c876db847..e66194d82 100644 --- a/gsd-core/references/verify-mvp-mode.md +++ b/gsd-core/references/verify-mvp-mode.md @@ -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`). diff --git a/gsd-core/references/workstream-flag.md b/gsd-core/references/workstream-flag.md index 624945440..ac3515a33 100644 --- a/gsd-core/references/workstream-flag.md +++ b/gsd-core/references/workstream-flag.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 -gsd-tools query workstream.list -gsd-tools query workstream.status -gsd-tools query workstream.complete +gsd_run query workstream.create +gsd_run query workstream.list +gsd_run query workstream.status +gsd_run query workstream.complete ``` diff --git a/gsd-core/workflows/add-phase.md b/gsd-core/workflows/add-phase.md index 419cda316..d04dbf282 100644 --- a/gsd-core/workflows/add-phase.md +++ b/gsd-core/workflows/add-phase.md @@ -43,7 +43,7 @@ Exit. -**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 -- [ ] `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 diff --git a/gsd-core/workflows/complete-milestone.md b/gsd-core/workflows/complete-milestone.md index 70ed996cf..75ddf44c9 100644 --- a/gsd-core/workflows/complete-milestone.md +++ b/gsd-core/workflows/complete-milestone.md @@ -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:` 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. @@ -353,7 +353,7 @@ Key accomplishments for this milestone: -**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) diff --git a/gsd-core/workflows/discuss-phase/modes/text.md b/gsd-core/workflows/discuss-phase/modes/text.md index 84309d099..9208aae51 100644 --- a/gsd-core/workflows/discuss-phase/modes/text.md +++ b/gsd-core/workflows/discuss-phase/modes/text.md @@ -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. diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index cc34faee2..4f8bcceb6 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -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 ""`. +`workflow.cross_ai_command` via `gsd_run query config-set workflow.cross_ai_command ""`. **For each cross-AI plan (sequentially):** @@ -1530,7 +1530,7 @@ For 1M+ context models, consider: -- **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 diff --git a/gsd-core/workflows/execute-plan.md b/gsd-core/workflows/execute-plan.md index cda873c02..5c15ae19a 100644 --- a/gsd-core/workflows/execute-plan.md +++ b/gsd-core/workflows/execute-plan.md @@ -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 ``` @@ -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}" -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 \ diff --git a/gsd-core/workflows/insert-phase.md b/gsd-core/workflows/insert-phase.md index 30db7239d..88f32fd9c 100644 --- a/gsd-core/workflows/insert-phase.md +++ b/gsd-core/workflows/insert-phase.md @@ -47,7 +47,7 @@ Exit. -**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 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 diff --git a/gsd-core/workflows/manager.md b/gsd-core/workflows/manager.md index c4c46e026..a1c015119 100644 --- a/gsd-core/workflows/manager.md +++ b/gsd-core/workflows/manager.md @@ -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. diff --git a/gsd-core/workflows/plan-phase.md b/gsd-core/workflows/plan-phase.md index f3416856b..3b1dabe07 100644 --- a/gsd-core/workflows/plan-phase.md +++ b/gsd-core/workflows/plan-phase.md @@ -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. diff --git a/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md b/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md index 1a46967c1..fd1c18601 100644 --- a/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +++ b/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md @@ -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 ``, ``, ``, ``, ``, ``, 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. diff --git a/gsd-core/workflows/profile-user.md b/gsd-core/workflows/profile-user.md index 8458756d8..4e56a631f 100644 --- a/gsd-core/workflows/profile-user.md +++ b/gsd-core/workflows/profile-user.md @@ -1,7 +1,7 @@ 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. @@ -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 diff --git a/gsd-core/workflows/progress.md b/gsd-core/workflows/progress.md index 6fbc1bcfe..fb9c74da2 100644 --- a/gsd-core/workflows/progress.md +++ b/gsd-core/workflows/progress.md @@ -44,11 +44,11 @@ If missing both ROADMAP.md and PROJECT.md: suggest `/gsd:new-project`. -**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. @@ -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 diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index a85683b1b..623ba6706 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -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` diff --git a/gsd-core/workflows/remove-phase.md b/gsd-core/workflows/remove-phase.md index bdd24a8ee..c3c497d4d 100644 --- a/gsd-core/workflows/remove-phase.md +++ b/gsd-core/workflows/remove-phase.md @@ -78,7 +78,7 @@ Wait for confirmation. -**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 @@ -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 diff --git a/gsd-core/workflows/settings-advanced.md b/gsd-core/workflows/settings-advanced.md index c6b98de89..5818ab8ce 100644 --- a/gsd-core/workflows/settings-advanced.md +++ b/gsd-core/workflows/settings-advanced.md @@ -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 ` before proceeding to Step C. +`gsd_run query config-set runtime ` 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 ` so the central +Apply each selected value via `gsd_run query config-set ` 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. @@ -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 diff --git a/gsd-core/workflows/stats.md b/gsd-core/workflows/stats.md index 9d3a3f4cd..ffe1f6ccf 100644 --- a/gsd-core/workflows/stats.md +++ b/gsd-core/workflows/stats.md @@ -53,7 +53,7 @@ If no `.planning/` directory exists, inform the user to run `/gsd:new-project` f -**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) diff --git a/gsd-core/workflows/thread.md b/gsd-core/workflows/thread.md index 6fce00545..68ffeb71f 100644 --- a/gsd-core/workflows/thread.md +++ b/gsd-core/workflows/thread.md @@ -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 diff --git a/gsd-core/workflows/transition.md b/gsd-core/workflows/transition.md index afcc7e95c..6a9a71268 100644 --- a/gsd-core/workflows/transition.md +++ b/gsd-core/workflows/transition.md @@ -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. -**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 -**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** diff --git a/gsd-core/workflows/update.md b/gsd-core/workflows/update.md index 76805bede..569417430 100644 --- a/gsd-core/workflows/update.md +++ b/gsd-core/workflows/update.md @@ -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 diff --git a/gsd-core/workflows/verify-work.md b/gsd-core/workflows/verify-work.md index 13656de97..db89a3b43 100644 --- a/gsd-core/workflows/verify-work.md +++ b/gsd-core/workflows/verify-work.md @@ -635,7 +635,7 @@ All tests passed. Phase {phase} marked complete. 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 diff --git a/skills/gsd-import/SKILL.md b/skills/gsd-import/SKILL.md index 638148c4d..93405af2f 100644 --- a/skills/gsd-import/SKILL.md +++ b/skills/gsd-import/SKILL.md @@ -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 ` 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 ` to migrate a project at a different path. diff --git a/tests/emitted-drift-acks/3809-gsd-run-launcher-normalization.json b/tests/emitted-drift-acks/3809-gsd-run-launcher-normalization.json new file mode 100644 index 000000000..a7c42e26c --- /dev/null +++ b/tests/emitted-drift-acks/3809-gsd-run-launcher-normalization.json @@ -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" + } + } +} diff --git a/tests/no-bare-gsd-tools-command-position.test.cjs b/tests/no-bare-gsd-tools-command-position.test.cjs index a46930819..ff294e411 100644 --- a/tests/no-bare-gsd-tools-command-position.test.cjs +++ b/tests/no-bare-gsd-tools-command-position.test.cjs @@ -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 ` 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 diff --git a/tests/no-dead-sdk-refs.test.cjs b/tests/no-dead-sdk-refs.test.cjs index eaee21245..60fa09d64 100644 --- a/tests/no-dead-sdk-refs.test.cjs +++ b/tests/no-dead-sdk-refs.test.cjs @@ -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 +// /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 ` 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 /` 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 `/`) + `(? 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 / ` is resolvable and fine. `node ` 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 `.` 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 `', + 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 /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 ` 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}`); + } }); });