@opengsd/gsd-core 1.3.0 → 1.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/agents/gsd-advisor-researcher.md +1 -20
  2. package/agents/gsd-ai-researcher.md +1 -20
  3. package/agents/gsd-domain-researcher.md +1 -20
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-phase-researcher.md +92 -166
  6. package/agents/gsd-planner.md +9 -36
  7. package/agents/gsd-project-researcher.md +62 -141
  8. package/agents/gsd-ui-researcher.md +2 -21
  9. package/agents/gsd-verifier.md +8 -2
  10. package/bin/install.js +85 -4
  11. package/commands/gsd/graphify.md +11 -6
  12. package/commands/gsd/import.md +6 -2
  13. package/commands/gsd/plan-phase.md +2 -2
  14. package/gsd-core/bin/check-latest-version.cjs +3 -2
  15. package/gsd-core/bin/gsd-tools.cjs +238 -32
  16. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  17. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  18. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  19. package/gsd-core/bin/lib/commands.cjs +5 -4
  20. package/gsd-core/bin/lib/config.cjs +28 -4
  21. package/gsd-core/bin/lib/core.cjs +72 -28
  22. package/gsd-core/bin/lib/graphify.cjs +2 -2
  23. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  24. package/gsd-core/bin/lib/init.cjs +19 -3
  25. package/gsd-core/bin/lib/installer-migrations.cjs +61 -22
  26. package/gsd-core/bin/lib/intel.cjs +3 -20
  27. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  28. package/gsd-core/bin/lib/phase.cjs +3 -3
  29. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  30. package/gsd-core/bin/lib/research-store.cjs +167 -0
  31. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  32. package/gsd-core/bin/lib/security.cjs +73 -0
  33. package/gsd-core/bin/lib/shell-command-projection.cjs +3 -0
  34. package/gsd-core/bin/lib/validate.cjs +2 -2
  35. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  36. package/gsd-core/bin/lib/verification.cjs +193 -0
  37. package/gsd-core/bin/lib/verify.cjs +2 -2
  38. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  39. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  40. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  41. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  42. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  43. package/gsd-core/references/planner-load-graph-context.md +36 -0
  44. package/gsd-core/references/planning-config.md +3 -1
  45. package/gsd-core/references/research-documentation-lookup.md +29 -0
  46. package/gsd-core/references/research-philosophy.md +29 -0
  47. package/gsd-core/references/research-verification-protocol.md +27 -0
  48. package/gsd-core/workflows/execute-phase.md +19 -8
  49. package/gsd-core/workflows/help/modes/full.md +2 -2
  50. package/gsd-core/workflows/ingest-docs.md +3 -2
  51. package/gsd-core/workflows/plan-phase.md +14 -10
  52. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  53. package/gsd-core/workflows/review.md +22 -5
  54. package/gsd-core/workflows/ship.md +5 -8
  55. package/gsd-core/workflows/spec-phase.md +2 -1
  56. package/gsd-core/workflows/update.md +2 -1
  57. package/hooks/dist/gsd-context-monitor.js +1 -1
  58. package/hooks/dist/gsd-workflow-guard.js +1 -0
  59. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  60. package/hooks/gsd-context-monitor.js +1 -1
  61. package/hooks/gsd-workflow-guard.js +1 -0
  62. package/hooks/gsd-worktree-path-guard.js +1 -1
  63. package/package.json +4 -1
  64. package/scripts/affected-tests-lib.cjs +3 -2
  65. package/scripts/changeset/cli.cjs +183 -28
  66. package/scripts/changeset/lint.cjs +5 -4
  67. package/scripts/changeset/new.cjs +4 -4
  68. package/scripts/check-alias-drift.cjs +77 -71
  69. package/scripts/check-env.cjs +185 -179
  70. package/scripts/check-npm-integrity.cjs +115 -109
  71. package/scripts/ci-guard-runner.cjs +11 -5
  72. package/scripts/ci-prepare-test-scope.cjs +27 -22
  73. package/scripts/ci-rebase-check.cjs +46 -45
  74. package/scripts/ci-test-scope.cjs +6 -4
  75. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  76. package/scripts/gen-inventory-manifest.cjs +38 -32
  77. package/scripts/gen-research-agents.cjs +276 -0
  78. package/scripts/lib/cli-exit.cjs +56 -0
  79. package/scripts/lint-command-contract.cjs +28 -22
  80. package/scripts/lint-descriptions.cjs +32 -28
  81. package/scripts/lint-docs-required.cjs +4 -4
  82. package/scripts/lint-legacy-dir-name.cjs +56 -52
  83. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  84. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  85. package/scripts/lint-skill-deps.cjs +31 -26
  86. package/scripts/lint-test-file-count.allowlist.json +1 -0
  87. package/scripts/lint-test-file-count.cjs +5 -4
  88. package/scripts/mutation-matrix.cjs +6 -3
  89. package/scripts/prompt-injection-scan.sh +1 -1
  90. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  91. package/scripts/release-tarball-smoke.cjs +6 -4
  92. package/scripts/research-profiles.cjs +149 -0
  93. package/scripts/run-affected-tests.cjs +2 -1
  94. package/scripts/run-cross-platform-tests.cjs +11 -7
  95. package/scripts/run-tests.cjs +8 -7
  96. package/scripts/strip-prose-atrefs.cjs +1 -1
  97. package/scripts/sync-runtime-launcher.cjs +0 -3
  98. package/scripts/verify-npm-publish.cjs +14 -26
@@ -86,7 +86,7 @@ Create detailed execution plan for a specific phase.
86
86
 
87
87
  - `--skip-research` — bypass the research subagent
88
88
  - `--research-phase <N>` — research-only mode. Spawns the research agent for phase `<N>`, writes `RESEARCH.md`, then exits before the planner runs. Useful for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops. Replaces the deleted `gsd-research-phase` standalone command (#3042).
89
- - Modifiers: `--research` forces refresh (re-spawn researcher, no prompt). `--view` prints existing `RESEARCH.md` to stdout without spawning. With neither, prompts `update / view / skip` if `RESEARCH.md` already exists.
89
+ - Modifiers: `--research` forces refresh (re-spawn researcher). `--view` prints existing `RESEARCH.md` to stdout without spawning. With neither, auto-uses an existing `RESEARCH.md` (one-line notice, then clean exit).
90
90
  - `--gaps` — focus only on closing gaps from a prior plan-check
91
91
  - `--skip-verify` — skip the post-plan verifier loop
92
92
  - `--ingest <path-or-glob>` — pre-ingest external ADRs/PRDs/SPECs before planning (see *PRD Express Path* below)
@@ -100,7 +100,7 @@ Create detailed execution plan for a specific phase.
100
100
  - Multiple plans per phase supported (XX-01, XX-02, etc.)
101
101
 
102
102
  Usage: `/gsd:plan-phase 1`
103
- Usage: `/gsd:plan-phase --research-phase 2` — research only on phase 2 (prompts if `RESEARCH.md` exists)
103
+ Usage: `/gsd:plan-phase --research-phase 2` — research only on phase 2 (auto-uses existing `RESEARCH.md`, no prompt)
104
104
  Usage: `/gsd:plan-phase --research-phase 2 --view` — print existing `RESEARCH.md`, no spawn
105
105
  Usage: `/gsd:plan-phase --research-phase 2 --research` — force-refresh, no prompt
106
106
  Result: Creates `.planning/phases/01-foundation/01-01-PLAN.md`
@@ -52,7 +52,8 @@ If `PATH_NOT_FOUND` or `MANIFEST_NOT_FOUND`: display error and exit.
52
52
  Run the init query:
53
53
 
54
54
  ```bash
55
- INIT=$(node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" init ingest-docs)
55
+ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
56
+ INIT=$(gsd_run init ingest-docs)
56
57
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
57
58
  ```
58
59
 
@@ -295,7 +296,7 @@ Preview the merge diff to the user and gate via approve-revise-abort before writ
295
296
  Commit the ingest results:
296
297
 
297
298
  ```bash
298
- node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" commit \
299
+ gsd_run commit \
299
300
  "docs: ingest {N} docs from {SCAN_PATH} (#2387)" --files \
300
301
  .planning/PROJECT.md \
301
302
  .planning/REQUIREMENTS.md \
@@ -32,7 +32,8 @@ Load all context in one call (paths only to minimize orchestrator context):
32
32
 
33
33
  ```bash
34
34
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
35
- INIT=$(gsd_run query init.plan-phase "$PHASE")
35
+ GRAN_PARAM=""; if [[ "$ARGUMENTS" =~ (^|[[:space:]])--granularity[[:space:]]+([^[:space:]-][^[:space:]]*) ]]; then GRAN_PARAM="--granularity ${BASH_REMATCH[2]}"; fi
36
+ INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM)
36
37
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
37
38
  AGENT_SKILLS_RESEARCHER=$(gsd_run query agent-skills gsd-phase-researcher)
38
39
  AGENT_SKILLS_PLANNER=$(gsd_run query agent-skills gsd-planner)
@@ -46,7 +47,7 @@ When `TDD_MODE` is `true`, the planner agent is instructed to apply `type: tdd`
46
47
 
47
48
  When `CONTEXT_WINDOW >= 500000`, the planner prompt includes the 3 most recent prior phase CONTEXT.md and SUMMARY.md files PLUS any phases explicitly listed in the current phase's `Depends on:` field in ROADMAP.md. Explicit dependencies always load regardless of recency (e.g., Phase 7 declaring `Depends on: Phase 2` always sees Phase 2's context). Bounded recency keeps the planner's context budget focused on recent work.
48
49
 
49
- Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `text_mode`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `phase_status` (#3569), `planning_exists`, `roadmap_exists`, `phase_req_ids`, `response_language`.
50
+ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `text_mode`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `phase_status` (#3569), `planning_exists`, `roadmap_exists`, `phase_req_ids`, `response_language`, `granularity`.
50
51
 
51
52
  **If `response_language` is set:** Include `response_language: {value}` in all spawned subagent prompts so any user-facing output stays in the configured language.
52
53
 
@@ -99,7 +100,7 @@ The gate fires only on `Complete`. `Executed` and `Needs Review` are not gated
99
100
 
100
101
  ## 2. Parse and Normalize Arguments
101
102
 
102
- Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--research-phase <N>`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--ingest <path-or-glob>`, `--ingest-format <auto|nygard|madr|narrative>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`, `--mvp`, `--tdd`, `--force` (override closed-phase gate, see §1.5)).
103
+ Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--research-phase <N>`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--ingest <path-or-glob>`, `--ingest-format <auto|nygard|madr|narrative>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`, `--mvp`, `--tdd`, `--granularity <coarse|standard|fine>`, `--force` (override closed-phase gate, see §1.5)).
103
104
 
104
105
  **`--research-phase <N>` — research-only mode (#3042 + #3044).** When this flag is present, parse `<N>` as the phase number (overrides any positional phase argument), set `RESEARCH_ONLY=true`, and treat the rest of this workflow as a research-dispatch only — the planner spawn (step 8), plan-checker, verification, gaps, bounce, and post-planning-gaps blocks all skip on `RESEARCH_ONLY`. Use this for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops. Replaces the deleted `/gsd-research-phase` command.
105
106
 
@@ -120,6 +121,8 @@ if $RESEARCH_ONLY && [[ "$ARGUMENTS" =~ (^|[[:space:]])--view([[:space:]]|$) ]];
120
121
  fi
121
122
  ```
122
123
 
124
+ **`--granularity <coarse|standard|fine>` — CLI override (#703).** When present, this value is the resolved granularity passed to the planner — it wins over any per-phase `granularities.<type>` config, top-level `granularity` config, or project defaults. The init JSON always includes a `granularity` field reflecting the resolved value; read it from there. Invalid values (anything other than `coarse`, `standard`, `fine`) cause an error at the CLI boundary.
125
+
123
126
  Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from init JSON is `true`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for Claude Code remote sessions (`/rc` mode) where TUI menus don't work through the Claude App.
124
127
 
125
128
  **MVP_MODE resolution.** Resolve `MVP_MODE` once via the centralized `phase.mvp-mode` query verb. Precedence (first hit wins): CLI flag → ROADMAP.md `**Mode:** mvp` → `workflow.mvp_mode` config → false. The verb is the single source of truth — do not re-implement the chain.
@@ -143,7 +146,7 @@ fi
143
146
  ```
144
147
 
145
148
  When `WALKING_SKELETON=true`:
146
- - Planner is instructed to produce `SKELETON.md` in the phase directory alongside `PLAN.md`. The template lives at `@~/.claude/gsd-core/references/skeleton-template.md`.
149
+ - Planner is instructed to produce `SKELETON.md` in the phase directory alongside `PLAN.md`. The template lives at `~/.claude/gsd-core/references/skeleton-template.md` — the planner reads it when producing SKELETON.md (lazy; not loaded on non-skeleton runs).
147
150
  - The plan must scaffold project + routing + one real DB read/write + one real UI interaction + dev deployment — the thinnest possible end-to-end working slice.
148
151
 
149
152
  **Interaction with `--prd <filepath>`.** `--mvp` and `--prd` compose. The PRD express path (Step 3.5) creates `CONTEXT.md` from the PRD file and continues to research; the Walking Skeleton gate fires independently from the conditions above. When both are active on Phase 1 of a new project, the planner receives `WALKING_SKELETON=true` and PRD-derived context simultaneously — the PRD informs *what the skeleton should prove*. No precedence is needed; the two signals are orthogonal. See [`references/mvp-concepts.md`](../references/mvp-concepts.md) for the broader interaction map.
@@ -417,15 +420,15 @@ Pass `ai_spec_path` and `framework_line` to planner in step 7 so it can referenc
417
420
 
418
421
  **Skip if:** `--gaps` flag or `--skip-research` flag or `--reviews` flag.
419
422
 
420
- ### 5.0. Research-Only Modifiers (`--view`, `--research`, prompt)
423
+ ### 5.0. Research-Only Modifiers (`--view`, `--research`)
421
424
 
422
425
  **Skip if:** `RESEARCH_ONLY` is `false`.
423
426
 
424
427
  Three branches in research-only mode (`--research-phase <N>`):
425
428
 
426
- 1. **`--view`** (or user picks "View" in the prompt below): print `RESEARCH.md` to stdout, no spawn, exit. If `RESEARCH.md` is missing, error with: `--view requires an existing RESEARCH.md; drop --view to spawn the researcher.`
429
+ 1. **`--view`**: print `RESEARCH.md` to stdout, no spawn, exit. If `RESEARCH.md` is missing, error with: `--view requires an existing RESEARCH.md; drop --view to spawn the researcher.`
427
430
  2. **`--research`** (force-refresh): re-spawn researcher unconditionally — fall through to "Spawn gsd-phase-researcher" below.
428
- 3. **Neither flag AND `has_research=true`:** emit `RESEARCH.md already exists for Phase ${PHASE}.` and prompt the user with three choices: `1. Update — re-spawn researcher and refresh RESEARCH.md`, `2. View — print existing RESEARCH.md and exit (no spawn)`, `3. Skip — exit without spawning or printing`. Map "Update" → fall through to spawn, "View" → set `VIEW_ONLY=true` and emit RESEARCH.md as in (1), "Skip" → exit cleanly. Mirrors the deleted `/gsd-research-phase` standalone's existing-artifact menu (#3042 parity).
431
+ 3. **Neither flag AND `has_research=true`:** auto-use the existing research and exit cleanly — do not prompt, do not re-spawn. Emit `RESEARCH.md already exists for Phase ${PHASE}, using it. To force-refresh, re-invoke with --research; to print, re-invoke with --view. Path: ${research_path}` then exit. The explicit-flag escape hatches cover any deviation; this matches §5.1's promptless auto-use of existing research, removing the §5.0/§5.1 inconsistency (#159).
429
432
 
430
433
  ```bash
431
434
  if [[ "$VIEW_ONLY" == "true" ]]; then
@@ -933,12 +936,13 @@ Each TDD plan gets one feature with RED/GREEN/REFACTOR gate sequence.
933
936
  </tdd_mode_active>
934
937
  ` : ''}
935
938
 
936
- **MVP_MODE:** ${MVP_MODE} (when true, follow vertical-slice rules from `@~/.claude/gsd-core/references/planner-mvp-mode.md`; when false, ignore MVP guidance entirely.)
937
- **WALKING_SKELETON:** ${WALKING_SKELETON} (when true, the first deliverable must be a Walking Skeleton — produce SKELETON.md alongside PLAN.md.)
939
+ **MVP_MODE:** ${MVP_MODE} (when true, follow vertical-slice rules from `~/.claude/gsd-core/references/planner-mvp-mode.md`; when false, ignore MVP guidance entirely.)
940
+ **WALKING_SKELETON:** ${WALKING_SKELETON} (when true, the first deliverable must be a Walking Skeleton — Read the template at `~/.claude/gsd-core/references/skeleton-template.md` and produce SKELETON.md alongside PLAN.md.)
941
+ **Granularity:** {granularity}
938
942
 
939
943
  ${MVP_MODE === 'true' ? `
940
944
  <mvp_mode_active>
941
- **MVP Mode is ENABLED.** Follow vertical-slice planning rules from @~/.claude/gsd-core/references/planner-mvp-mode.md. Each plan must deliver a complete vertical slice — thin end-to-end functionality rather than horizontal layers.
945
+ **MVP Mode is ENABLED.** Read `~/.claude/gsd-core/references/planner-mvp-mode.md` now and follow its vertical-slice planning rules. Each plan must deliver a complete vertical slice — thin end-to-end functionality rather than horizontal layers.
942
946
  </mvp_mode_active>
943
947
  ` : ''}
944
948
  </planning_context>
@@ -63,7 +63,7 @@ Then re-run: /gsd:plan-review-convergence {PHASE}
63
63
  ## 2. Initialize
64
64
 
65
65
  ```bash
66
- INIT=$(node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" init plan-phase "$PHASE")
66
+ INIT=$(gsd_run init plan-phase "$PHASE")
67
67
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
68
68
  ```
69
69
 
@@ -76,7 +76,7 @@ Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from in
76
76
  ## 3. Validate Phase + Pre-flight Gate
77
77
 
78
78
  ```bash
79
- PHASE_INFO=$(node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" roadmap get-phase "${PHASE}")
79
+ PHASE_INFO=$(gsd_run roadmap get-phase "${PHASE}")
80
80
  ```
81
81
 
82
82
  **If `found` is false:** Error with available phases. Exit.
@@ -230,7 +230,7 @@ fi
230
230
  **If HIGH_COUNT == 0 (converged):**
231
231
 
232
232
  ```bash
233
- node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" state planned-phase --phase "${PHASE}" --name "${phase_name}" --plans "${PLAN_COUNT}"
233
+ gsd_run state planned-phase --phase "${PHASE}" --name "${phase_name}" --plans "${PLAN_COUNT}"
234
234
  ```
235
235
 
236
236
  Display:
@@ -22,7 +22,7 @@ command -v codex >/dev/null 2>&1 && echo "codex:available" || echo "codex:missin
22
22
  command -v coderabbit >/dev/null 2>&1 && echo "coderabbit:available" || echo "coderabbit:missing"
23
23
  command -v opencode >/dev/null 2>&1 && echo "opencode:available" || echo "opencode:missing"
24
24
  command -v qwen >/dev/null 2>&1 && echo "qwen:available" || echo "qwen:missing"
25
- command -v cursor >/dev/null 2>&1 && echo "cursor:available" || echo "cursor:missing"
25
+ command -v cursor-agent >/dev/null 2>&1 && echo "cursor:available" || echo "cursor:missing"
26
26
  command -v agy >/dev/null 2>&1 && echo "antigravity:available" || echo "antigravity:missing"
27
27
 
28
28
  # Check local model servers (OpenAI-compatible HTTP API — no CLI binary required)
@@ -284,9 +284,15 @@ fi
284
284
 
285
285
  **Cursor:**
286
286
  ```bash
287
- cat /tmp/gsd-review-prompt-{phase}.md | cursor agent -p --mode ask --trust 2>/dev/null > /tmp/gsd-review-cursor-{phase}.md
287
+ # cursor-agent is a SEPARATE binary from the `cursor` IDE launcher; print mode (-p) takes the
288
+ # prompt as an ARGUMENT, not stdin. A full review prompt can exceed the OS argument limit, so
289
+ # reference the prompt file by path rather than inlining it. Capture stderr so a failure is
290
+ # diagnosable instead of a silent empty result.
291
+ CURSOR_PROMPT_ARG="Read the file at /tmp/gsd-review-prompt-{phase}.md in full and carry out the review request it contains. Output only the resulting markdown review. Do not edit any files."
292
+ cursor-agent -p --mode ask --trust --output-format text "$CURSOR_PROMPT_ARG" 2>/tmp/gsd-review-cursor-{phase}.err > /tmp/gsd-review-cursor-{phase}.md
288
293
  if [ ! -s /tmp/gsd-review-cursor-{phase}.md ]; then
289
- echo "Cursor review failed or returned empty output." > /tmp/gsd-review-cursor-{phase}.md
294
+ echo "Cursor review failed or returned empty output. stderr:" > /tmp/gsd-review-cursor-{phase}.md
295
+ cat /tmp/gsd-review-cursor-{phase}.err >> /tmp/gsd-review-cursor-{phase}.md
290
296
  fi
291
297
  ```
292
298
 
@@ -350,8 +356,19 @@ if [ -f "$_AGY_CACHE" ]; then
350
356
  fi
351
357
  fi
352
358
 
353
- # Step 1 — primary invocation: stdout works on macOS, Linux, and WSL
354
- agy -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-antigravity-{phase}.md
359
+ # Step 1 — primary invocation: stdout works on macOS, Linux, and WSL.
360
+ # Bound the run with agy's OWN `--print-timeout` (issue #687). On a large,
361
+ # file-path-rich prompt agy's agentic Cascade can loop on its code_search/grep
362
+ # steps and never converge; `--print-timeout` is agy's native cap for print mode
363
+ # (defaults to 5m — see maintainer note above), so we pass it explicitly to let a
364
+ # stalled run self-terminate through the tool's own mechanism. A non-zero exit
365
+ # (timeout or crash) discards any partial output so the Step 2 transcript fallback
366
+ # / Step 3 stub take over.
367
+ agy --print-timeout 300s -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-antigravity-{phase}.md
368
+ _AGY_RC=$?
369
+ if [ "$_AGY_RC" -ne 0 ]; then
370
+ : > /tmp/gsd-review-antigravity-{phase}.md
371
+ fi
355
372
 
356
373
  # Step 2 — transcript fallback: catches Windows agy -p stdout bug (and any future stdout-silent edge cases).
357
374
  # Reads only lines appended AFTER the pre-flight watermark. If agy failed before writing a new response,
@@ -41,15 +41,12 @@ Verify the work is ready to ship:
41
41
 
42
42
  1. **Verification passed?**
43
43
  ```bash
44
- VERIFICATION_FILE=$(ls ${PHASE_DIR}/*-VERIFICATION.md 2>/dev/null | head -1)
45
- STATUS=$(sed -n '/^---$/,/^---$/p' "${VERIFICATION_FILE}" 2>/dev/null | grep -m1 "^status:" | cut -d: -f2 | tr -d ' ')
44
+ VERIFICATION=$(gsd_run query verification.status "${PHASE_DIR}" 2>/dev/null)
45
+ STATUS=$(printf '%s' "$VERIFICATION" | jq -r '.status' 2>/dev/null || echo "")
46
+ NEXT_ACTION=$(printf '%s' "$VERIFICATION" | jq -r '.next_action' 2>/dev/null || echo "")
47
+ NEXT_COMMAND=$(printf '%s' "$VERIFICATION" | jq -r '.next_command' 2>/dev/null || echo "")
46
48
  ```
47
- The verifier emits exactly `passed`, `gaps_found`, or `human_needed` (see the status table in `execute-phase.md`); only `passed` may ship. Route on `${STATUS}` — on any non-`passed` value, block with `PHASE_VERIFICATION_INCOMPLETE` and state the matching next action:
48
- - `passed` → verification complete; continue to the next preflight check.
49
- - `gaps_found` → run `/gsd:plan-phase ${PHASE_NUMBER} --gaps` to plan the fixes, then re-run `/gsd:execute-phase` before shipping.
50
- - `human_needed` → complete the manual tests in `${PHASE_DIR}/*-UAT.md`, then re-run the verify step until status is `passed`.
51
- - empty (no `*-VERIFICATION.md`) → the verify step never completed; re-run `/gsd:execute-phase`.
52
- - any other value → unexpected status `${STATUS}`; re-run `/gsd:execute-phase` verification.
49
+ Only `passed` may ship. If `$STATUS` is `passed`, verification is complete — continue to the next preflight check. Any other value (including `gaps_found`, `human_needed`, `missing`, and `unknown`) blocks with `PHASE_VERIFICATION_INCOMPLETE`: present `$NEXT_ACTION` to the user and, when `$NEXT_COMMAND` is non-empty, show it as the command to run next. The query already handles missing files and unexpected values, so no per-status arm is needed.
53
50
 
54
51
  2. **Clean working tree?**
55
52
  ```bash
@@ -56,7 +56,8 @@ Rotate through these perspectives — each naturally surfaces different blindspo
56
56
  ## Step 1: Initialize
57
57
 
58
58
  ```bash
59
- INIT=$(node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" init phase-op "${PHASE}")
59
+ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
60
+ INIT=$(gsd_run init phase-op "${PHASE}")
60
61
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
61
62
  ```
62
63
 
@@ -174,7 +174,6 @@ EXTRACT_JSON=$(node "$GSD_DIR/gsd-core/scripts/changeset/cli.cjs" extract \
174
174
  --changelog "$CHANGELOG_TMP" \
175
175
  --json 2>/dev/null)
176
176
  EXTRACT_EXIT=$?
177
- rm -f "$CHANGELOG_TMP"
178
177
 
179
178
  if [ "$EXTRACT_EXIT" -eq 2 ]; then
180
179
  # Exit 2 = no releases in range (e.g. versions are equal or changelog is sparse)
@@ -188,6 +187,8 @@ else
188
187
  --to "$LATEST_VERSION" \
189
188
  --changelog "$CHANGELOG_TMP" 2>/dev/null || echo "(changelog unavailable)")
190
189
  fi
190
+ # Clean up temp changelog now that both extract runs are done
191
+ rm -f "$CHANGELOG_TMP"
191
192
  ```
192
193
 
193
194
  3. Display preview and ask for confirmation, using `$CHANGELOG_PREVIEW` from the extract step above:
@@ -150,7 +150,7 @@ process.stdin.on('end', () => {
150
150
  spawn(
151
151
  process.execPath,
152
152
  [gsdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
153
- { cwd, detached: true, stdio: 'ignore' }
153
+ { cwd, detached: true, stdio: 'ignore', windowsHide: true }
154
154
  ).unref();
155
155
  warnData.criticalRecorded = true;
156
156
  // Persist the sentinel so subsequent debounce cycles don't re-fire
@@ -61,6 +61,7 @@ function currentBranch(cwd) {
61
61
  cwd,
62
62
  encoding: 'utf8',
63
63
  stdio: ['ignore', 'pipe', 'ignore'],
64
+ windowsHide: true,
64
65
  });
65
66
  if (result.status !== 0) return '';
66
67
  return result.stdout.trim();
@@ -18,7 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { spawnSync } = require('child_process');
20
20
 
21
- const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 };
21
+ const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, windowsHide: true };
22
22
 
23
23
  function git(args, cwd) {
24
24
  return spawnSync('git', args, { ...SPAWNOPT, cwd });
@@ -150,7 +150,7 @@ process.stdin.on('end', () => {
150
150
  spawn(
151
151
  process.execPath,
152
152
  [gsdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
153
- { cwd, detached: true, stdio: 'ignore' }
153
+ { cwd, detached: true, stdio: 'ignore', windowsHide: true }
154
154
  ).unref();
155
155
  warnData.criticalRecorded = true;
156
156
  // Persist the sentinel so subsequent debounce cycles don't re-fire
@@ -61,6 +61,7 @@ function currentBranch(cwd) {
61
61
  cwd,
62
62
  encoding: 'utf8',
63
63
  stdio: ['ignore', 'pipe', 'ignore'],
64
+ windowsHide: true,
64
65
  });
65
66
  if (result.status !== 0) return '';
66
67
  return result.stdout.trim();
@@ -18,7 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { spawnSync } = require('child_process');
20
20
 
21
- const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 };
21
+ const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, windowsHide: true };
22
22
 
23
23
  function git(args, cwd) {
24
24
  return spawnSync('git', args, { ...SPAWNOPT, cwd });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.3.0",
3
+ "version": "1.4.0-rc.1",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "bin": {
6
6
  "gsd-core": "bin/install.js",
@@ -62,6 +62,9 @@
62
62
  "typescript": "^6.0.3",
63
63
  "typescript-eslint": "^8.60.0"
64
64
  },
65
+ "overrides": {
66
+ "qs": ">=6.15.2"
67
+ },
65
68
  "optionalDependencies": {
66
69
  "fallow": "^2.70.0"
67
70
  },
@@ -4,6 +4,7 @@ const { execFileSync } = require('node:child_process');
4
4
  const { readdirSync, readFileSync, existsSync } = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
7
+ const { ExitError } = require('./lib/cli-exit.cjs');
7
8
  const { suiteOf } = require('./run-tests.cjs');
8
9
 
9
10
  const CRITICAL_PATHS = [
@@ -409,7 +410,7 @@ function runNodeTestFiles(repoRoot, files) {
409
410
  if (firstFailure === 0) firstFailure = code;
410
411
  }
411
412
  }
412
- if (firstFailure !== 0) process.exit(firstFailure);
413
+ if (firstFailure !== 0) throw new ExitError(firstFailure);
413
414
  }
414
415
 
415
416
  function runSuite(repoRoot, suite) {
@@ -442,7 +443,7 @@ function resolveBaseRef() {
442
443
  * security), so every concrete match that pickAffectedTests put into `selected`
443
444
  * belongs to one of those suites and will be exercised by running all three.
444
445
  */
445
- function resolveRunPlan({ changedFiles, selected, widenRequired, criticalPath, noChanges }) {
446
+ function resolveRunPlan({ changedFiles: _changedFiles, selected, widenRequired, criticalPath, noChanges }) {
446
447
  if (noChanges) {
447
448
  return { mode: 'suite', suite: 'unit' };
448
449
  }