@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6

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 (113) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +20 -0
  4. package/agents/gsd-doc-classifier.md +105 -0
  5. package/agents/gsd-doc-synthesizer.md +61 -0
  6. package/agents/gsd-ui-checker.md +30 -0
  7. package/agents/gsd-ui-researcher.md +1 -0
  8. package/bin/install.js +1568 -622
  9. package/gsd-core/bin/gsd-tools.cjs +40 -1
  10. package/gsd-core/bin/lib/api-coverage.cjs +466 -0
  11. package/gsd-core/bin/lib/audit.cjs +6 -3
  12. package/gsd-core/bin/lib/capability-loader.cjs +11 -9
  13. package/gsd-core/bin/lib/capability-registry.cjs +761 -84
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/capability-writer.cjs +10 -1
  16. package/gsd-core/bin/lib/check-command-router.cjs +242 -3
  17. package/gsd-core/bin/lib/commands.cjs +7 -5
  18. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  19. package/gsd-core/bin/lib/config.cjs +96 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +4 -1
  21. package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
  22. package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
  23. package/gsd-core/bin/lib/host-integration.cjs +45 -4
  24. package/gsd-core/bin/lib/init.cjs +76 -39
  25. package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
  26. package/gsd-core/bin/lib/install-engine.cjs +228 -18
  27. package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
  28. package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
  29. package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
  30. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  31. package/gsd-core/bin/lib/milestone.cjs +3 -3
  32. package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
  33. package/gsd-core/bin/lib/phase-id.cjs +132 -3
  34. package/gsd-core/bin/lib/phase.cjs +78 -16
  35. package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
  36. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  37. package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
  38. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
  39. package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
  40. package/gsd-core/bin/lib/roadmap.cjs +42 -56
  41. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
  42. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
  43. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
  44. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  45. package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
  46. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
  47. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  48. package/gsd-core/bin/lib/security.cjs +6 -36
  49. package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
  50. package/gsd-core/bin/lib/spec-section.cjs +111 -0
  51. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  52. package/gsd-core/bin/lib/state-transition.cjs +1 -1
  53. package/gsd-core/bin/lib/state.cjs +24 -24
  54. package/gsd-core/bin/lib/surface.cjs +40 -6
  55. package/gsd-core/bin/lib/uat.cjs +4 -1
  56. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  57. package/gsd-core/bin/lib/validate.cjs +15 -6
  58. package/gsd-core/bin/lib/verify.cjs +33 -37
  59. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  60. package/gsd-core/bin/shared/model-catalog.json +14 -9
  61. package/gsd-core/references/api-coverage.md +104 -0
  62. package/gsd-core/references/model-profiles.md +2 -2
  63. package/gsd-core/references/planning-config.md +2 -0
  64. package/gsd-core/references/specless-probe-fallback.md +172 -0
  65. package/gsd-core/references/ui-consideration-probe.md +73 -0
  66. package/gsd-core/templates/UI-SPEC.md +25 -0
  67. package/gsd-core/templates/VALIDATION.md +2 -0
  68. package/gsd-core/templates/config.json +2 -1
  69. package/gsd-core/workflows/audit-fix.md +9 -1
  70. package/gsd-core/workflows/audit-milestone.md +7 -4
  71. package/gsd-core/workflows/code-review-fix.md +7 -3
  72. package/gsd-core/workflows/code-review.md +4 -1
  73. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
  74. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
  75. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
  76. package/gsd-core/workflows/execute-phase.md +1 -25
  77. package/gsd-core/workflows/plan-phase.md +37 -2
  78. package/gsd-core/workflows/quick.md +2 -2
  79. package/gsd-core/workflows/review.md +59 -13
  80. package/gsd-core/workflows/settings-advanced.md +12 -9
  81. package/gsd-core/workflows/settings.md +2 -2
  82. package/gsd-core/workflows/ui-phase.md +146 -1
  83. package/gsd-core/workflows/validate-phase.md +2 -2
  84. package/gsd-core/workflows/verify-phase.md +3 -2
  85. package/gsd-core/workflows/verify-work.md +38 -0
  86. package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
  87. package/hooks/dist/gsd-cursor-stop.js +48 -0
  88. package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
  89. package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
  90. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  91. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  92. package/hooks/dist/managed-hooks-registry.cjs +6 -0
  93. package/hooks/gsd-cursor-pre-tool.js +76 -0
  94. package/hooks/gsd-cursor-stop.js +48 -0
  95. package/hooks/gsd-cursor-subagent-start.js +50 -0
  96. package/hooks/gsd-cursor-subagent-stop.js +40 -0
  97. package/hooks/gsd-windsurf-pre-command.js +275 -0
  98. package/hooks/gsd-windsurf-pre-write.js +132 -0
  99. package/hooks/managed-hooks-registry.cjs +6 -0
  100. package/package.json +9 -4
  101. package/pi/gsd.cjs +354 -0
  102. package/scripts/build-hooks.js +8 -1
  103. package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
  104. package/scripts/gen-registry.cjs +128 -0
  105. package/scripts/lint-phase-id-drift.cjs +150 -0
  106. package/scripts/lint-test-file-count.allowlist.json +2 -1
  107. package/scripts/registry-schema.cjs +565 -0
  108. package/scripts/run-tests.cjs +21 -1
  109. package/scripts/validate-registry.cjs +117 -0
  110. package/vscode/browser.js +197 -0
  111. package/vscode/extension.js +383 -0
  112. package/vscode/host-binding.js +113 -0
  113. package/vscode/package.json +96 -0
@@ -0,0 +1,104 @@
1
+ # API Coverage Gate (Full Coverage by Default — Opt Out, Never Opt In)
2
+
3
+ > Reference for the `api-coverage` gate on the `ai-integration` capability (#1562).
4
+ > Config key: `workflow.api_coverage_gate` (default `true`). Gate point: `verify:pre`.
5
+
6
+ ## The problem this closes
7
+
8
+ "We integrated the API" too often silently means "we integrated whatever the
9
+ first use case exercised." Every un-built capability is then an invisible hole,
10
+ discovered later by a user who reasonably expected it to work. The phase sealed
11
+ green because its tasks completed — nobody *decided* the gaps were acceptable,
12
+ because nobody *enumerated* them.
13
+
14
+ This gate makes the API surface **visible and decided** before the phase can
15
+ seal. Full coverage is the default starting position; the coverage matrix is the
16
+ *subtraction record*. Every gap is an explicit, reasoned opt-out rather than a
17
+ surprise.
18
+
19
+ ## When it fires
20
+
21
+ The gate runs at `verify:pre` (before `/gsd:verify-work` begins UAT). A phase is
22
+ treated as an external-API integration when **either**:
23
+
24
+ 1. a `COVERAGE.md` matrix is present in the phase directory (the planner produced
25
+ one at `plan:pre`), **or**
26
+ 2. the phase scope shows a strong external-API-integration signal (an integration
27
+ verb co-occurring with an external-API noun, or an explicit `<Service>
28
+ API|SDK|REST|GraphQL` surface) and no matrix yet exists.
29
+
30
+ Non-API phases (refactors, bug fixes, internal-only work, features that merely
31
+ *mention* an existing internal API) do **not** fire the gate — the trigger
32
+ requires a compound signal, so a bare word like "api" in "the public API of
33
+ UserController" is intentionally ignored.
34
+
35
+ ## The two touch points
36
+
37
+ 1. **Plan time (`plan:pre`).** A contribution to the planner prompts it to run
38
+ the deterministic detector over the phase scope and, when an integration is
39
+ detected, produce `COVERAGE.md`. See
40
+ `capabilities/ai-integration/fragments/api-coverage-plan-pre.md`.
41
+ 2. **Seal time (`verify:pre`).** The blocking `api-coverage.verify-pre` gate
42
+ runs `check api-coverage.verify-pre <phase-dir>` and blocks unless a valid
43
+ matrix exists (or no integration is detected).
44
+
45
+ ## The coverage matrix format
46
+
47
+ Canonical form — a markdown table (human-editable, diff-friendly):
48
+
49
+ ```markdown
50
+ # API Coverage — <service>
51
+
52
+ > Full coverage by default. Opt-outs are explicit, reasoned decisions.
53
+
54
+ | capability | decision | reason |
55
+ |---|---|---|
56
+ | search | INTEGRATE | |
57
+ | playlists | INTEGRATE | |
58
+ | skip | OPT-OUT | not needed yet — tracked for follow-up phase |
59
+ ```
60
+
61
+ - **`INTEGRATE` is the default.** Every capability starts as INTEGRATE.
62
+ - **Every `OPT-OUT` MUST carry a one-line reason** (`not needed`, `not needed
63
+ yet`, `explicitly out of scope`, …). An opt-out without a reason is an
64
+ un-decided hole — the exact failure mode this gate exists to close.
65
+ - A fenced ` ```coverage ` JSON block (`[{"capability":…,"decision":…,"reason":…}]`)
66
+ is also accepted for machine-generated matrices.
67
+
68
+ Rules enforced at seal time: the matrix must be non-empty; every capability name
69
+ must be non-empty and unique; every decision must be `INTEGRATE` or `OPT-OUT`;
70
+ every `OPT-OUT` must have a reason. Violations block the seal with a precise
71
+ error.
72
+
73
+ ## A second integration against the same need
74
+
75
+ A second platform for an existing capability (e.g. adding YouTube alongside
76
+ Spotify for media playback) starts from the **same full-coverage baseline** as
77
+ the first. Do not carry over the first integration's opt-outs silently —
78
+ re-decide each capability for the new surface, so a first-class/fallback
79
+ asymmetry cannot accumulate into a later user-facing bug.
80
+
81
+ ## The matrix persists
82
+
83
+ `COVERAGE.md` is a phase artifact. A future phase that extends the same
84
+ integration starts from the recorded surface and decisions rather than from
85
+ zero — the matrix is the durable subtraction record.
86
+
87
+ ## Tuning
88
+
89
+ - **Disable entirely:** set `workflow.api_coverage_gate: false` in
90
+ `.planning/config.json` (the gate unregisters from `verify:pre`).
91
+ - **Widen the trigger vocabulary:** the detector accepts `--verbs` / `--nouns`
92
+ overrides (see `capabilities/ai-integration/fragments/api-coverage-plan-pre.md`).
93
+ The default vocabulary is additive-only.
94
+
95
+ ## Detector CLI
96
+
97
+ ```bash
98
+ echo "$PHASE_SCOPE" | node gsd-core/bin/lib/api-coverage.cjs --json
99
+ # exit 0 = integration detected, 1 = none, 2 = startup error
100
+ ```
101
+
102
+ The detector is a pure function (`detectApiIntegration` → `{ detected, signals,
103
+ terms }`) shared by the plan-time prompt and the seal-time gate, so the
104
+ low-false-positive guarantee is testable rather than a judgment call.
@@ -45,10 +45,10 @@ Model profiles control which Claude model each GSD agent uses. This allows balan
45
45
  | Phase type | Agents |
46
46
  |---|---|
47
47
  | `planning` | gsd-planner, gsd-roadmapper, gsd-pattern-mapper |
48
- | `discuss` | (reserved — no subagent today) |
48
+ | `discuss` | `gsd-assumptions-analyzer` |
49
49
  | `research` | gsd-phase-researcher, gsd-project-researcher, gsd-research-synthesizer, gsd-codebase-mapper, gsd-ui-researcher |
50
50
  | `execution` | gsd-executor, gsd-debugger, gsd-doc-writer |
51
- | `verification` | gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier |
51
+ | `verification` | gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier, gsd-code-reviewer |
52
52
  | `completion` | (reserved — no subagent today) |
53
53
 
54
54
  ### Resolution precedence (highest to lowest)
@@ -256,6 +256,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
256
256
  | `workflow.node_repair` | boolean | `true` | `true`, `false` | Attempt automatic repair of failed plan nodes |
257
257
  | `workflow.node_repair_budget` | number | `2` | Any positive integer | Max repair retries per failed node |
258
258
  | `workflow.ai_integration_phase` | boolean | `true` | `true`, `false` | Run /gsd:ai-integration-phase before planning AI system phases |
259
+ | `workflow.api_coverage_gate` | boolean | `true` | `true`, `false` | Require an explicit API-coverage decision (full-by-default, opt-out-not-opt-in) before a phase that integrates an external API/SDK/service can seal. At plan:pre prompts a COVERAGE.md matrix; at verify:pre a blocking gate fails the seal unless the matrix exists with every non-integrated capability an explicit, reasoned opt-out (#1562) |
259
260
  | `workflow.ui_phase` | boolean | `true` | `true`, `false` | Generate UI-SPEC.md for frontend phases |
260
261
  | `workflow.ui_safety_gate` | boolean | `true` | `true`, `false` | Require safety gate approval for UI changes |
261
262
  | `workflow.text_mode` | boolean | `false` | `true`, `false` | Use plain-text numbered lists instead of AskUserQuestion menus |
@@ -269,6 +270,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
269
270
  | `workflow.mvp_mode` | boolean | `false` | `true`, `false` | Persist the MVP-mode flag in config so every phase defaults to MVP framing without requiring `--mvp` on the CLI. Resolved via the chain: `--mvp` CLI flag → ROADMAP.md `**Mode:** mvp` field → this config value → `false`. When `true`, the planner, executor, verifier, and discovery surfaces (progress, stats, graphify) all treat the phase as an MVP vertical slice (UI → API → DB) of one user-visible capability. |
270
271
  | `workflow.context_guard_mode` | string | `"warn"` | `"auto"`, `"warn"`, `"off"` | Context exhaustion guard mode for `execute-phase`. Before each wave, the orchestrator self-assesses context pressure using degradation signals from `context-budget.md`. `"warn"` (default): emit a warning and recommend `/gsd:pause-work` when POOR tier is detected. `"auto"`: automatically invoke `/gsd:pause-work` before the next wave when POOR tier is detected. `"off"`: disable the guard. The guard is heuristic — no programmatic context-% API exists. |
271
272
  | `workflow.plan_chunked` | boolean | `false` | `true`, `false` | Enable chunked planning mode. When `true`, the plan-phase orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3–5 min each). Each plan is committed individually for crash resilience. Particularly useful on Windows where long-lived Tasks may hang on stdio. Also activated by the `--chunked` flag. |
273
+ | `workflow.specless_probe_fallback` | boolean | `true` | `true`, `false` | Gate the SPEC-less probe fallback in `plan-phase`. When `true` (default), a phase that did not supply a `## Edge Coverage` / `## Prohibitions` SPEC section (header absent or present-but-empty) runs the existing probe protocol — the deterministic `edge-probe.cjs` for edges and an in-planner LLM recall pass for prohibitions — and authors the resulting predicates into PLAN.md `must_haves` (section-level precedence: a SPEC-supplied section is never re-run or overwritten). When `false`, the fallback is skipped but the skip is recorded: plan-phase emits a visible "probe fallback disabled" marker, never a silent skip. |
272
274
  | `workflow.code_review_command` | string\|null | `null` | Any shell command | External code-review command integrated into `/gsd:ship`. The diff is piped to the command via stdin; the command must output JSON with a `verdict` field (`"APPROVED"` or `"REVISE"`). Non-zero exit or `"REVISE"` verdict blocks the ship workflow. When unset, the built-in review flow runs. Example: `my-review-tool --review`. |
273
275
  | `workflow.inline_plan_threshold` | number | `2` | `0`–`10` | Plans with ≤N tasks execute inline instead of spawning a subagent |
274
276
  | `workflow.code_review` | boolean | `true` | `true`, `false` | Enable built-in code review step in the ship workflow |
@@ -0,0 +1,172 @@
1
+ # Spec-less Probe Fallback — protocol
2
+
3
+ Lazy-loaded by `workflows/plan-phase.md` step 7.95 (the gate) and the `<specless_probe_fallback>`
4
+ planner block. When a phase SPEC did NOT supply `## Edge Coverage` / `## Prohibitions`, plan-phase
5
+ runs the same probe protocol the SPEC path uses and authors the predicates into PLAN.md `must_haves`
6
+ (ADR-857 Phase 6 — the *else branch* of the `<downstream_consumer>` SPEC-conditional lift). This is
7
+ core workflow-body substrate — NOT the `PLAN_PRE_HOOKS_JSON contribution into planner` capability rail
8
+ (D-03). Section absence is detected by the shared `spec-section` helper in the gate; this file holds the
9
+ *run-the-probe* half so the capped plan-phase.md stays lean (#717/#1074 budget).
10
+
11
+ ## 0. Gate — toggle + per-section absence (run first, in the orchestrator)
12
+
13
+ Reads the default-ON toggle and computes `EDGE_ABSENT` / `PROHIB_ABSENT` via the shared `spec-section`
14
+ helper; records a VISIBLE skip when disabled or when the phase has no requirement IDs (never a silent
15
+ skip, never a hard-fail). Sets `SPECLESS_FALLBACK`, `EDGE_ABSENT`, `PROHIB_ABSENT`, and
16
+ `SPECLESS_FALLBACK_DISABLED` for §A and the planner prompt.
17
+
18
+ ```bash
19
+ # Toggle defaults ON (D-04 / RAIL-05): any value other than literal "false" enables.
20
+ SPECLESS_CFG=$(gsd_run query config-get workflow.specless_probe_fallback 2>/dev/null || echo "true")
21
+ SPECLESS_FALLBACK=true; [[ "$SPECLESS_CFG" == "false" ]] && SPECLESS_FALLBACK=false
22
+
23
+ # Per-section absence (D-05 / RAIL-03): "not supplied" = header absent OR present-but-empty. The
24
+ # shared, tested `spec-section` helper (src/spec-section.cts -> bin/lib/spec-section.cjs) is the SINGLE
25
+ # source of truth for the canonical SPEC headings (suffix-tolerant) and table-row counting, replacing
26
+ # ad-hoc awk (contract pinned by tests/spec-section.test.cjs). Resolve via the edge-probe install-dir
27
+ # idiom; build only in a source checkout, else fail loud (never silently mis-detect).
28
+ _GSD_RT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
29
+ _gsd_lib() { for _d in "$_GSD_RT/gsd-core/bin/lib" "$_GSD_RT/bin/lib" "$_GSD_RT/.claude/bin/lib" "$HOME/.claude/gsd-core/bin/lib" "$HOME/.claude/bin/lib"; do [ -f "$_d/$1" ] && { echo "$_d/$1"; return; }; done; }
30
+ SPEC_SECTION_JS=$(_gsd_lib spec-section.cjs)
31
+ if [ -z "$SPEC_SECTION_JS" ] && [ -f "$_GSD_RT/tsconfig.build.json" ] && [ -f "$_GSD_RT/src/spec-section.cts" ]; then
32
+ npm --prefix "$_GSD_RT" run build:lib 2>/dev/null || true; SPEC_SECTION_JS=$(_gsd_lib spec-section.cjs)
33
+ fi
34
+ [ -n "$SPEC_SECTION_JS" ] || { echo "ERROR: spec-section.cjs not found - reinstall GSD or run build:lib." >&2; exit 1; }
35
+ # supplied => header present AND >=1 row; missing $SPEC_FILE => supplied:false => fallback fires.
36
+ EDGE_ABSENT=1; node "$SPEC_SECTION_JS" "$SPEC_FILE" edges 2>/dev/null | grep -q '"supplied":true' && EDGE_ABSENT=0
37
+ PROHIB_ABSENT=1; node "$SPEC_SECTION_JS" "$SPEC_FILE" prohibitions 2>/dev/null | grep -q '"supplied":true' && PROHIB_ABSENT=0
38
+
39
+ # Disabled path - record the skip VISIBLY, never silently (RAIL-05 / PROH-4); the note rides into the
40
+ # planner prompt (Step 8) so the plan records that no probe predicates were generated.
41
+ SPECLESS_FALLBACK_DISABLED=""
42
+ if [[ "$SPECLESS_FALLBACK" != "true" ]]; then
43
+ echo "WARNING: probe fallback disabled (workflow.specless_probe_fallback=false); skip recorded, not silent." >&2
44
+ SPECLESS_FALLBACK_DISABLED="probe fallback disabled (workflow.specless_probe_fallback=false): no probe-derived predicates generated for SPEC-absent sections this run."
45
+ fi
46
+
47
+ # Nothing-to-probe guard: the fallback derives predicates from requirement TEXT, so zero requirement
48
+ # IDs => nothing to probe => skip VISIBLY (like the disabled path), NOT a hard-fail. Prevents a
49
+ # no-SPEC + no-requirements phase from aborting under the default-ON fallback. The orchestrator
50
+ # substitutes {phase_req_ids}; empty/whitespace/TBD => no requirements. (A still-literal token is
51
+ # non-empty, so an unsubstituted run correctly hits the reference's fail-loud guard instead.)
52
+ SPECLESS_REQ_IDS="{phase_req_ids}"
53
+ if [[ "$SPECLESS_FALLBACK" == "true" ]] && { [ -z "${SPECLESS_REQ_IDS// /}" ] || [ "${SPECLESS_REQ_IDS}" = "TBD" ]; }; then
54
+ echo "info: spec-less probe fallback: phase has no requirement IDs - nothing to probe; skipping (visible skip)." >&2
55
+ SPECLESS_FALLBACK=false
56
+ SPECLESS_FALLBACK_DISABLED="spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)."
57
+ fi
58
+ ```
59
+
60
+ ## A. Edge probe (deterministic) — run when `SPECLESS_FALLBACK=true` AND `EDGE_ABSENT=1`
61
+
62
+ Mirrors spec-phase Step 5.5 verbatim; the ONLY divergence (D-02) is sourcing `$REQS_JSON` from the
63
+ phase requirement IDs (`{phase_req_ids}`) instead of a SPEC interview. Leave `$COVERAGE` empty when
64
+ `EDGE_ABSENT=0` — a SPEC-supplied section is never re-run (section-level precedence).
65
+
66
+ ```bash
67
+ # Resolve the compiled edge-probe.cjs against the GSD install dir via RUNTIME_DIR (#448) — NOT the
68
+ # consuming project's git root — falling back to git toplevel / $HOME/.claude (spec-phase.md:198 idiom).
69
+ _GSD_RT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
70
+ EDGE_PROBE_JS=$(for _c in \
71
+ "$_GSD_RT/gsd-core/bin/lib/edge-probe.cjs" "$_GSD_RT/bin/lib/edge-probe.cjs" \
72
+ "$_GSD_RT/.claude/bin/lib/edge-probe.cjs" "$HOME/.claude/gsd-core/bin/lib/edge-probe.cjs" \
73
+ "$HOME/.claude/bin/lib/edge-probe.cjs"; do [ -f "$_c" ] && { echo "$_c"; break; }; done)
74
+ # Build ONLY inside a verified GSD source checkout; --prefix pins npm so we never trigger the
75
+ # consuming project's build:lib. Never silent-skip (RR-04) — fail loud if unresolvable.
76
+ if [ -z "$EDGE_PROBE_JS" ]; then
77
+ if [ -f "$_GSD_RT/tsconfig.build.json" ] && [ -f "$_GSD_RT/src/edge-probe.cts" ]; then
78
+ npm --prefix "$_GSD_RT" run build:lib 2>/dev/null || true
79
+ EDGE_PROBE_JS=$(for _c in \
80
+ "$_GSD_RT/gsd-core/bin/lib/edge-probe.cjs" "$_GSD_RT/bin/lib/edge-probe.cjs" \
81
+ "$_GSD_RT/.claude/bin/lib/edge-probe.cjs" "$HOME/.claude/gsd-core/bin/lib/edge-probe.cjs" \
82
+ "$HOME/.claude/bin/lib/edge-probe.cjs"; do [ -f "$_c" ] && { echo "$_c"; break; }; done)
83
+ fi
84
+ [ -n "$EDGE_PROBE_JS" ] || { echo "ERROR: edge-probe.cjs not found — reinstall GSD or run \`npm run build:lib\`." >&2; exit 1; }
85
+ fi
86
+
87
+ # THE ONE DIVERGENCE (D-02): source requirements from THIS phase. Populate the heredoc from
88
+ # {phase_req_ids}, pulling each requirement's text from REQUIREMENTS.md: {"id","text","shapes"?}.
89
+ # mktemp suffix trick is BSD/GNU portable (#1520).
90
+ REQS_JSON=$(mktemp "${TMPDIR:-/tmp}/edge-probe-reqs-XXXXXX") && mv "$REQS_JSON" "${REQS_JSON}.json" && REQS_JSON="${REQS_JSON}.json" || exit 1
91
+ cat > "$REQS_JSON" <<'JSON'
92
+ [
93
+ { "id": "R1", "text": "<replace: requirement text from REQUIREMENTS.md for each {phase_req_ids} id>" }
94
+ ]
95
+ JSON
96
+ # Guard — fail loud on empty/invalid array OR a still-present `<replace:>` placeholder (forgotten
97
+ # substitution would yield a bogus report). Never a silent no-op.
98
+ if ! node -e 'const a=require(process.argv[1]);if(!Array.isArray(a)||a.length===0)process.exit(1);if(a.some(r=>typeof r.text!=="string"||!r.text.trim()||r.text.includes("<replace:")))process.exit(1)' "$REQS_JSON" 2>/dev/null; then
99
+ echo "ERROR: edge-probe requirements JSON is empty/invalid or still holds the <replace: …> placeholder — populate \$REQS_JSON from {phase_req_ids} before running." >&2
100
+ exit 1
101
+ fi
102
+ # Invoke + CAPTURE, exit-checked (engine FAILS CLOSED exit 2 on bad shape; a bare COVERAGE=$(node …)
103
+ # would swallow it and fall through to prose re-derivation = fail-OPEN).
104
+ if ! COVERAGE=$(node "$EDGE_PROBE_JS" "$REQS_JSON"); then
105
+ rm -f "$REQS_JSON"
106
+ echo "ERROR: edge-probe engine failed (invalid shapes or bad input) — fix the requirement(s); never proceed with empty coverage." >&2
107
+ exit 1
108
+ fi
109
+ rm -f "$REQS_JSON"
110
+ # Exit-0-but-garbage guard: report must parse as JSON with { items[], coverage{} }.
111
+ if ! printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let r;try{r=JSON.parse(s)}catch{process.exit(1)}if(!r||!Array.isArray(r.items)||typeof r.coverage!=="object"||r.coverage===null)process.exit(1)})'; then
112
+ echo "ERROR: edge-probe produced an unparseable/malformed coverage report — refusing to proceed." >&2
113
+ exit 1
114
+ fi
115
+ # Zero-applicable guard: surface a likely classification miss loudly (spec-phase 5.5:277 shape).
116
+ APPLICABLE=$(printf '%s' "$COVERAGE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let n=0;try{n=JSON.parse(s).coverage.applicable}catch{n=0}process.stdout.write(String(n))})')
117
+ if [ "$APPLICABLE" = "0" ]; then
118
+ echo "WARNING: edge-probe proposed ZERO applicable edges across all phase requirements — likely a classification miss, not a genuinely edge-free phase. Do NOT silently write an empty fallback Edge Coverage." >&2
119
+ fi
120
+ ```
121
+
122
+ **Edge `--auto` resolution rules (reuse spec-phase 5.5 verbatim, D-06):** auto-`covered` where a
123
+ defensible acceptance criterion can be written (→ a plain `must_haves.truths` string); else
124
+ auto-`backstop` → author it as a **structured flat-scalar marker** `{ statement: <the check>,
125
+ verification: backstop }` in `must_haves.truths`, NOT a prose note (the verifier branches
126
+ deterministically on the `verification: backstop` field; a parenthetical is unparseable — the #1110
127
+ fragility; flat scalar `verification:` key, never a nested object, ADR-550 #1278). A `backstop` truth
128
+ the verifier cannot confirm with explicit evidence abstains → `human_needed` (reason
129
+ `insufficient_spec`), never a silent pass (#1154; `references/honest-verifier.md`). **Never
130
+ auto-dismiss** (a wrong dismissal is the exact silent failure this eliminates). An `unclassified` row
131
+ stays **`unresolved`** (#1110) — never auto-`backstop`ped — and is surfaced to the planner as a flagged
132
+ assumption. Pass `$COVERAGE` (+ the gate's `$SPECLESS_FALLBACK_DISABLED` note) into the gsd-planner
133
+ prompt (Step 8). When `EDGE_ABSENT=0`, `$COVERAGE` is empty and this does not run.
134
+
135
+ ## B. Prohibition recall (LLM prose pass) — run when `PROHIB_ABSENT=1`
136
+
137
+ There is NO compiled prohibition engine and NO `node` invocation (ADR-550 D7b) — the gsd-planner runs
138
+ this in-prompt. Full two-stage protocol, canon-referral rule, and status×verification schema live in
139
+ `~/.claude/gsd-core/references/prohibition-probe.md` (do not inline it). Summary:
140
+
141
+ - **Stage 1 — Recall (adversarial).** Per requirement: *"What could this feature silently become that
142
+ the author would NOT want, but the spec does not forbid?"* Over-produce (~10 raw must-NOT candidates).
143
+ - **Stage 2 — Precision.** DROP routine-engineering (normal correctness/hygiene — owned by the edge
144
+ probe or code review); KEEP values / safety / ethics (~2–3 survive).
145
+ - **Canon-referral drop (ADR-550 D6).** A kept candidate that is canon security/compliance (OWASP /
146
+ prototype-pollution / path-traversal / injection / GDPR / generic fairness) is NOT minted — emit a
147
+ one-line breadcrumb and DROP it.
148
+
149
+ **Fallback `--auto` divergence (D-06 / RAIL-04 / PROH-1):** author each kept prohibition as
150
+ **flagged-unverified with NO wired-check descriptor**. NEVER write `check_kind` / `check_target` /
151
+ `check_rule` / `check_violation_fixture` / `check_clean_fixture` — there is no human to wire/verify a
152
+ check, and a descriptor-less item is what keeps it fail-closed (it disposes
153
+ `{status:'unverified', flagged:true}` downstream via the reused `dispositionForProhibition`). **Never
154
+ auto-dismiss**; never fabricate a check path. Surface any `unresolved` prohibition as a flagged
155
+ assumption — never a silent drop.
156
+
157
+ ## C. Authoring (the `<downstream_consumer>` else-branch)
158
+
159
+ Author the fallback report into `must_haves` with the SAME lift the SPEC path uses — only the source
160
+ changes (the fallback report, not the SPEC):
161
+
162
+ - **Edges →** every `covered` edge's acceptance criterion → `must_haves.truths` as a plain string;
163
+ every `backstop` edge → `must_haves.truths` as a structured `{ statement, verification: backstop }`
164
+ marker (NOT prose; #1110/#1278), which abstains → `human_needed` at verify time when unconfirmed
165
+ (#1154); every `unresolved`/`unclassified` row → an explicit flagged assumption (never a silent drop).
166
+ - **Prohibitions →** every kept prohibition → the `must_haves.prohibitions:` sibling block (NOT
167
+ `truths`, ADR-550 D3) via the single `projectProhibitions` serializer (Hyrum — no second
168
+ serializer), authored **descriptor-less** (no `check_*` scalar) so each disposes flagged-unverified.
169
+ - **Section-level precedence:** a SPEC-supplied section is never re-run or overwritten — exactly one
170
+ producer per section.
171
+ - **No-silent-drop equality:** for each section, (# probe-surfaced items) == (# authored into
172
+ `must_haves` + # surfaced as flagged assumptions).
@@ -0,0 +1,73 @@
1
+ # UI-Consideration Probe — Spec-Completeness Reference
2
+
3
+ The **third** adapter of the shared `probe-core` resolution model (ADR-550 Decision 7), on
4
+ the **UI element/state axis**. It surfaces the shape-rooted UI *state* considerations a
5
+ UI-SPEC must resolve before a dimension may PASS — the visual analog of the requirement-side
6
+ [edge-probe](./edge-probe.md), reusing its exact lifecycle, validators, and plan-phase lift
7
+ (see edge-probe.md for the shared status×verification model — this doc does not re-argue it).
8
+
9
+ **Axis boundary (this is a MIXED axis).** This compiled taxonomy covers ONLY the finite,
10
+ project-independent shape-rooted content/robustness states. The **open**, domain/UX-dependent
11
+ considerations — real-time/offline/optimistic-UI, deep accessibility (WCAG breadth),
12
+ internationalization / RTL depth, and emerging interaction paradigms — are open-ended and are
13
+ prose-owned in the companion [domain-probes.md](./domain-probes.md) technology/UX bank, NOT
14
+ here. Forcing them into a closed compiled taxonomy is the wrong model.
15
+
16
+ ## Inputs
17
+
18
+ A list of UI elements, each a `{ id, text, elements? }` record where `text` is the
19
+ researcher-authored description and `elements` is an optional author-supplied override of the
20
+ element classification. The six element kinds are: `form`, `list-collection`, `nav`, `media`,
21
+ `interactive-control`, `static-content`. When `elements` is absent, a heuristic classifier
22
+ proposes kinds from the prose (propose-then-confirm) — the author may correct the kind.
23
+
24
+ ## Taxonomy (8 categories)
25
+
26
+ Closed and small by design: the finite, project-independent content/robustness states every
27
+ UI surface must account for. Growth toward open UX topics happens in `domain-probes.md`, not by
28
+ bloating this closed core.
29
+
30
+ | id | name | applies to element kinds | consideration question |
31
+ |----|------|--------------------------|------------------------|
32
+ | empty | Empty / no data | form, list-collection, media | What is shown when there is no data — zero items, an unfilled form, or absent media? |
33
+ | loading | Loading / in-flight | form, list-collection, media, nav | What is shown while data or content is still loading (skeleton, spinner, progressive reveal)? |
34
+ | error | Error / failure | form, list-collection, media, nav | What is shown when the load or submit fails (message, retry affordance, partial fallback)? |
35
+ | populated | Populated / happy path | list-collection, media | What does the normal populated (happy-path) state look like at a typical volume of content? |
36
+ | partial | Partial / incomplete | form, list-collection | What is shown for partial or incomplete data — some fields or rows present, others missing? |
37
+ | overflow | Overflow / truncation | list-collection, nav, static-content | What happens when content exceeds its container — scroll, clip, wrap, or truncate? |
38
+ | zero-one-many | Zero / one / many | list-collection | How does the layout read at zero, one, and many items (singular vs plural copy, spacing)? |
39
+ | long-text | Long text | form, static-content, interactive-control, nav | What happens with unusually long text — truncation, wrapping, ellipsis, or reflow? |
40
+
41
+ ## Relevance filter + resolution states
42
+
43
+ The probe reuses the edge-probe rails verbatim (ADR-550 Decision 7 — see
44
+ [edge-probe.md](./edge-probe.md#relevance-filter--resolution-states) for the full model):
45
+
46
+ 1. **Relevance filter first.** Classify each element's kind(s), then raise only the categories
47
+ whose `applies to element kinds` intersect. A static label is never asked about loading or
48
+ empty state — that is what makes an unresolved consideration meaningful.
49
+ 2. **Dismissal requires a reason string.** Silence is not a resolution; the reason is the audit
50
+ trail.
51
+ 3. **Zero-classification surfaces one `unclassified` candidate (#1110).** An element whose prose
52
+ matched no kind cue yields exactly one soft `unclassified — review manually` item
53
+ (`category: "unclassified"`, `status: "unresolved"`) — never a silent drop, never a guessed
54
+ kind. `unclassified` is a review signal, **not** a ninth taxonomy category; an explicit
55
+ `elements: []` opt-out stays silent.
56
+
57
+ Each raised consideration carries the shared two orthogonal axes — `status`
58
+ (`resolved | dismissed | unresolved`) and, when resolved, a `verification` tier
59
+ (`explicit | backstop`). A `backstop` consideration lifts into `must_haves.truths` and, at
60
+ verify time, is confirmed only by explicit evidence (a wired held-out/property test) or routes
61
+ to `insufficient_spec → human_needed` — never a silent pass (the honest-verifier disposition,
62
+ #1154). See [honest-verifier.md](./honest-verifier.md).
63
+
64
+ ## Closed / open boundary
65
+
66
+ The **8 ids above are the closed, compiled subset** — finite and project-independent, so a
67
+ compiled taxonomy is legitimate (the same property that makes edge-probe's data-shape taxonomy
68
+ closed). The **open subset is prose-owned in [domain-probes.md](./domain-probes.md)**:
69
+ real-time/offline/optimistic-UI, deep accessibility (WCAG breadth), i18n / RTL depth, and
70
+ emerging interaction paradigms (gesture/voice/reduced-motion/print) are open-ended and
71
+ cue-triggered — they do not belong in this closed taxonomy. This probe **complements** the
72
+ `gsd-ui-checker` six quality dimensions (it adds a state-coverage axis); it does not change the
73
+ BLOCK/FLAG/PASS enum or the dimensions themselves.
@@ -79,6 +79,31 @@ Accent reserved for: {explicit list — never "all interactive elements"}
79
79
 
80
80
  ---
81
81
 
82
+ ## UI Considerations
83
+
84
+ > Populated by the ui-phase UI-consideration probe (Step 9.5) and lifted by plan-phase's
85
+ > `## UI Considerations` lift rule via the identical rule as SPEC `## Edge Coverage`. Shape-rooted UI *state*
86
+ > coverage (empty / loading / error / populated / partial / overflow / zero-one-many / long-text).
87
+ > Empty-state and error-state COPY live in `## Copywriting Contract` above — this section covers
88
+ > state coverage and REFERENCES those rows rather than restating the copy (de-dup).
89
+
90
+ Applicable state considerations resolved: {N covered, M backstop, K unresolved — or "none applicable"}
91
+
92
+ | Category | Element(s) | Status | Resolution / Reason |
93
+ |----------|------------|--------|---------------------|
94
+ | {empty} | {list-collection} | ✅ covered | {concrete truth string — e.g. "Empty results render the documented 'No results' copy"} |
95
+ | {long-text} | {static-content} | 🧪 backstop | {held-out/visual UI-state test — lifts as `{ statement, verification: backstop }`} |
96
+ | {overflow} | {list-collection} | ⚠ unresolved | {planner treats as assumption} |
97
+
98
+ <!-- Status vocabulary (locked by probe-core projectTruths):
99
+ ✅ covered → a plain truth string lifted into must_haves.truths
100
+ 🧪 backstop → a flat scalar { statement, verification: backstop }; at verify time, no explicit
101
+ evidence → insufficient_spec → human_needed (never a silent pass, #1154)
102
+ ⚠ unresolved → an explicit planner assumption (surfaced, never silently dropped)
103
+ Rows are REPLACED (not appended) on a probe re-run — idempotent. -->
104
+
105
+ ---
106
+
82
107
  ## Registry Safety
83
108
 
84
109
  | Registry | Blocks Used | Safety Gate |
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  phase: {N}
3
3
  slug: {phase-slug}
4
+ # status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
5
+ # audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
4
6
  status: draft
5
7
  nyquist_compliant: false
6
8
  wave_0_complete: false
@@ -18,7 +18,8 @@
18
18
  "plan_bounce_passes": 2,
19
19
  "cross_ai_execution": false,
20
20
  "cross_ai_command": "",
21
- "cross_ai_timeout": 300
21
+ "cross_ai_timeout": 300,
22
+ "test_gate_timeout": 600
22
23
  },
23
24
  "ship": {
24
25
  "pr_body_sections": []
@@ -124,7 +124,15 @@ if [ -z "$AUDIT_TEST_CMD" ]; then
124
124
  AUDIT_TEST_CMD="true"
125
125
  fi
126
126
  fi
127
- eval "$AUDIT_TEST_CMD" 2>&1 | tail -20
127
+ # #1857: normalize to one-shot (defeat vitest/jest watch mode) + bound with a
128
+ # timeout so a watch-mode runner cannot hang the audit gate indefinitely.
129
+ AUDIT_TEST_CMD=$(gsd_run query normalize-test-command "$AUDIT_TEST_CMD" --cwd . 2>/dev/null || echo "$AUDIT_TEST_CMD")
130
+ TEST_GATE_TIMEOUT=$(gsd_run query config-get workflow.test_gate_timeout 2>/dev/null || echo "600")
131
+ timeout "$TEST_GATE_TIMEOUT" bash -c "$AUDIT_TEST_CMD" 2>&1 | tail -20
132
+ AUDIT_TEST_EXIT=${PIPESTATUS[0]}
133
+ if [ "$AUDIT_TEST_EXIT" -eq 124 ]; then
134
+ echo "✗ Audit test gate timed out after ${TEST_GATE_TIMEOUT}s — likely stuck in watch/dev mode (e.g. vitest without 'run'). Run tests one-shot (e.g. 'vitest run') or raise workflow.test_gate_timeout."
135
+ fi
128
136
  ```
129
137
 
130
138
  **c. If tests pass** — commit atomically:
@@ -153,17 +153,20 @@ Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` a
153
153
 
154
154
  If no active validate-phase step hook exists: skip entirely.
155
155
 
156
- For each phase directory, check `*-VALIDATION.md`. If exists, parse frontmatter (`nyquist_compliant`, `wave_0_complete`).
156
+ For each phase directory, check `*-VALIDATION.md`. If exists, parse frontmatter (`status`, `nyquist_compliant`, `wave_0_complete`).
157
157
 
158
158
  Classify per phase:
159
159
 
160
160
  | Status | Condition |
161
161
  |--------|-----------|
162
- | COMPLIANT | `nyquist_compliant: true` and all tasks green |
163
- | PARTIAL | VALIDATION.md exists, `nyquist_compliant: false` or red/pending |
162
+ | COMPLIANT | `status: validated` and `nyquist_compliant: true` and all tasks green |
163
+ | PARTIAL | `status: validated` and (`nyquist_compliant: false` or red/pending) |
164
+ | NOT-VALIDATED | `status: draft` (or absent) — validate-phase has not yet reconciled this file (#2117) |
164
165
  | MISSING | No VALIDATION.md |
165
166
 
166
- Add to audit YAML: `nyquist: { compliant_phases, partial_phases, missing_phases, overall }`
167
+ > **NOT-VALIDATED vs PARTIAL (#2117):** A phase reads `status: draft` when it was seeded by plan-phase but never reconciled by validate-phase, OR when its `VALIDATION.md` predates the `status` field (files written before #2117 stay `draft` whether or not validation ran). In both cases `nyquist_compliant` is not authoritative, so this is a coverage TODO ("run validate-phase") — not a compliance failure. Re-running validate-phase promotes the file to `status: validated` and yields the real COMPLIANT/PARTIAL verdict. Only `status: validated` + `nyquist_compliant: false` is a genuine PARTIAL.
168
+
169
+ Add to audit YAML: `nyquist: { compliant_phases, partial_phases, not_validated_phases, missing_phases, overall }`
167
170
 
168
171
  Discovery only — never auto-calls `/gsd:validate-phase`.
169
172
 
@@ -23,6 +23,10 @@ INIT=$(gsd_run query init.phase-op "${PHASE_ARG}")
23
23
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
24
24
  AGENT_SKILLS_FIXER=$(gsd_run query agent-skills gsd-code-fixer)
25
25
  AGENT_SKILLS_REVIEWER=$(gsd_run query agent-skills gsd-code-reviewer)
26
+ # #2072: resolve the routed models so model_overrides / models.<phaseType> are honored
27
+ # (gsd-code-reviewer → "verification", gsd-code-fixer → "execution"); thread them below.
28
+ REVIEWER_MODEL=$(gsd_run query resolve-model gsd-code-reviewer --raw)
29
+ FIXER_MODEL=$(gsd_run query resolve-model gsd-code-fixer --raw)
26
30
  ```
27
31
 
28
32
  Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
@@ -194,7 +198,7 @@ echo "Fix scope: ${FIX_SCOPE}"
194
198
  Use Agent() to spawn agent:
195
199
 
196
200
  ```text
197
- Agent(subagent_type="gsd-code-fixer", prompt="
201
+ Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt="
198
202
  <files_to_read>
199
203
  ${REVIEW_PATH}
200
204
  </files_to_read>
@@ -277,7 +281,7 @@ if [ "$AUTO_MODE" = "true" ]; then
277
281
 
278
282
  # Spawn gsd-code-reviewer agent to re-review (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
279
283
  # (This overwrites REVIEW_PATH with latest review state)
280
- Agent(subagent_type="gsd-code-reviewer", prompt="
284
+ Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt="
281
285
  <config>
282
286
  depth: ${REVIEW_DEPTH}
283
287
  phase_dir: ${PHASE_DIR}
@@ -311,7 +315,7 @@ ${AGENT_SKILLS_REVIEWER}")
311
315
  # Still has issues — spawn fixer again (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
312
316
  echo "Issues remain. Applying fixes for iteration ${ITERATION}..."
313
317
 
314
- Agent(subagent_type="gsd-code-fixer", prompt="
318
+ Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt="
315
319
  <files_to_read>
316
320
  ${REVIEW_PATH}
317
321
  </files_to_read>
@@ -22,6 +22,9 @@ PHASE_ARG="${1}"
22
22
  INIT=$(gsd_run query init.phase-op "${PHASE_ARG}")
23
23
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
24
24
  AGENT_SKILLS_REVIEWER=$(gsd_run query agent-skills gsd-code-reviewer)
25
+ # #2072: resolve the routed model so model_overrides / models.verification are honored
26
+ # (the resolver maps gsd-code-reviewer → phaseType "verification"); thread it below.
27
+ REVIEWER_MODEL=$(gsd_run query resolve-model gsd-code-reviewer --raw)
25
28
  ```
26
29
 
27
30
  Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
@@ -482,7 +485,7 @@ Spawn the gsd-code-reviewer agent:
482
485
  Print: `◆ Spawning code reviewer... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
483
486
 
484
487
  ```
485
- Agent(subagent_type="gsd-code-reviewer", prompt="
488
+ Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt="
486
489
  <files_to_read>
487
490
  ${FILES_TO_READ}
488
491
  </files_to_read>
@@ -68,6 +68,9 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars
68
68
  INIT=$(gsd_run query init.phase-op "${PHASE}")
69
69
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
70
70
  AGENT_SKILLS_ANALYZER=$(gsd_run query agent-skills gsd-assumptions-analyzer)
71
+ # #2072: resolve the routed model so model_overrides / models.discuss are honored
72
+ # (the resolver maps gsd-assumptions-analyzer → phaseType "discuss"); thread it below.
73
+ ANALYZER_MODEL=$(gsd_run query resolve-model gsd-assumptions-analyzer --raw)
71
74
  ```
72
75
 
73
76
  Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`,
@@ -255,7 +258,7 @@ If no USER-PROFILE.md: calibration_tier = "standard"
255
258
  **Spawn Explore subagent** (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)**:**
256
259
 
257
260
  ```
258
- Agent(subagent_type="gsd-assumptions-analyzer", prompt="""
261
+ Agent(subagent_type="gsd-assumptions-analyzer", model="{ANALYZER_MODEL}", prompt="""
259
262
  Analyze the codebase for Phase {PHASE}: {phase_name}.
260
263
 
261
264
  Phase goal: {roadmap_description}
@@ -94,14 +94,18 @@ if [ -z "$TEST_CMD" ]; then
94
94
  echo "⚠ No test runner detected — skipping post-merge test gate"
95
95
  fi
96
96
  fi
97
- # Run test suite with 5-minute timeout
97
+ # #1857: normalize to a one-shot form (defeat vitest/jest watch mode) via the
98
+ # same shared normalize-test-command helper the regression gate uses, then bound
99
+ # with the configured timeout so a watch-mode runner cannot hang the gate.
100
+ TEST_CMD=$(gsd_run query normalize-test-command "$TEST_CMD" --cwd . 2>/dev/null || echo "$TEST_CMD")
101
+ TEST_GATE_TIMEOUT=$(gsd_run query config-get workflow.test_gate_timeout 2>/dev/null || echo "600")
98
102
  TEST_EXIT=0
99
- timeout 300 bash -c "$TEST_CMD" 2>&1
103
+ timeout "$TEST_GATE_TIMEOUT" bash -c "$TEST_CMD" 2>&1
100
104
  TEST_EXIT=$?
101
105
  if [ "${TEST_EXIT}" -eq 0 ]; then
102
106
  echo "✓ Post-merge test gate passed — no cross-plan conflicts"
103
107
  elif [ "${TEST_EXIT}" -eq 124 ]; then
104
- echo "⚠ Post-merge test gate timed out after 5 minutes"
108
+ echo "⚠ POST-MERGE TEST GATE TIMED OUT after ${TEST_GATE_TIMEOUT}s — the runner did not exit, likely stuck in watch/dev mode (e.g. vitest without 'run'). Verify tests with a one-shot command (e.g. 'vitest run') or raise workflow.test_gate_timeout."
105
109
  else
106
110
  echo "✗ Post-merge test gate failed (exit code ${TEST_EXIT})"
107
111
  WAVE_FAILURE_COUNT=$((WAVE_FAILURE_COUNT + 1))
@@ -110,7 +114,7 @@ fi
110
114
 
111
115
  **If `TEST_EXIT` is 0 (pass):** `✓ Post-merge test gate: {N} tests passed — no cross-plan conflicts` → continue to orchestrator tracking update.
112
116
 
113
- **If `TEST_EXIT` is 124 (timeout):** Log warning, treat as non-blocking, continue. Tests may need a longer budget or manual run.
117
+ **If `TEST_EXIT` is 124 (timeout):** The runner did not exit within the budget — surface the printed message clearly (watch/dev mode is the likely cause; #1857). Treated as non-blocking (a genuinely long suite may just need a larger `workflow.test_gate_timeout`), but it is NEVER silently ignored — the watch-mode cause is named so the user can fix it (one-shot command / `workflow.test_command` / larger timeout).
114
118
 
115
119
  **If `TEST_EXIT` is non-zero (test failure):** Increment `WAVE_FAILURE_COUNT` to track
116
120
  cumulative failures across waves. Subsequent waves should report: