@opengsd/gsd-core 1.8.0 → 1.9.0

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 (174) 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 +31 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +186 -55
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +849 -2
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +5 -5
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +57 -5
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  49. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  50. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  51. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  53. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  54. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  56. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  57. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  58. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  59. package/gsd-core/bin/lib/state-document.cjs +164 -20
  60. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  61. package/gsd-core/bin/lib/state.cjs +141 -21
  62. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  63. package/gsd-core/bin/lib/uat.cjs +9 -7
  64. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  65. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  66. package/gsd-core/bin/lib/validate.cjs +32 -0
  67. package/gsd-core/bin/lib/verification.cjs +51 -14
  68. package/gsd-core/bin/lib/verify.cjs +128 -20
  69. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  72. package/gsd-core/bin/shared/model-catalog.json +5 -0
  73. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  74. package/gsd-core/references/context-budget.md +40 -0
  75. package/gsd-core/references/gate-prompts.md +6 -3
  76. package/gsd-core/references/model-profile-resolution.md +64 -13
  77. package/gsd-core/references/offer-next.md +88 -0
  78. package/gsd-core/references/planning-config.md +2 -1
  79. package/gsd-core/references/reviewer-instances.md +28 -21
  80. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  81. package/gsd-core/references/ui-consideration-probe.md +2 -2
  82. package/gsd-core/references/worktree-branch-check.md +4 -4
  83. package/gsd-core/templates/summary-minimal.md +4 -0
  84. package/gsd-core/templates/summary-standard.md +4 -0
  85. package/gsd-core/templates/summary.md +7 -0
  86. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  87. package/gsd-core/workflows/audit-fix.md +4 -0
  88. package/gsd-core/workflows/audit-milestone.md +8 -0
  89. package/gsd-core/workflows/autonomous.md +19 -15
  90. package/gsd-core/workflows/check-todos.md +2 -2
  91. package/gsd-core/workflows/code-review-fix.md +14 -6
  92. package/gsd-core/workflows/code-review.md +76 -19
  93. package/gsd-core/workflows/debug.md +10 -2
  94. package/gsd-core/workflows/diagnose-issues.md +4 -0
  95. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  96. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  97. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  98. package/gsd-core/workflows/discuss-phase.md +2 -2
  99. package/gsd-core/workflows/docs-update.md +8 -0
  100. package/gsd-core/workflows/eval-review.md +1 -1
  101. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  102. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  103. package/gsd-core/workflows/execute-phase.md +85 -115
  104. package/gsd-core/workflows/execute-plan.md +5 -4
  105. package/gsd-core/workflows/explore.md +4 -0
  106. package/gsd-core/workflows/extract-learnings.md +21 -0
  107. package/gsd-core/workflows/help/modes/full.md +3 -3
  108. package/gsd-core/workflows/import.md +4 -1
  109. package/gsd-core/workflows/ingest-docs.md +4 -0
  110. package/gsd-core/workflows/map-codebase.md +13 -6
  111. package/gsd-core/workflows/new-milestone.md +10 -2
  112. package/gsd-core/workflows/new-project.md +11 -4
  113. package/gsd-core/workflows/next.md +5 -2
  114. package/gsd-core/workflows/plan-phase.md +42 -46
  115. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  116. package/gsd-core/workflows/progress.md +1 -1
  117. package/gsd-core/workflows/quick.md +14 -3
  118. package/gsd-core/workflows/review.md +146 -575
  119. package/gsd-core/workflows/scan.md +9 -1
  120. package/gsd-core/workflows/secure-phase.md +10 -2
  121. package/gsd-core/workflows/ship.md +41 -11
  122. package/gsd-core/workflows/smart-entry.md +1 -1
  123. package/gsd-core/workflows/ui-phase.md +8 -1
  124. package/gsd-core/workflows/ui-review.md +8 -1
  125. package/gsd-core/workflows/update.md +104 -5
  126. package/gsd-core/workflows/validate-phase.md +10 -2
  127. package/gsd-core/workflows/verify-work.md +8 -1
  128. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  129. package/hooks/dist/gsd-cursor-stop.js +6 -2
  130. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  131. package/hooks/dist/gsd-graphify-update.sh +9 -0
  132. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  133. package/hooks/dist/gsd-prompt-guard.js +101 -2
  134. package/hooks/dist/gsd-read-guard.js +100 -2
  135. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  136. package/hooks/dist/gsd-statusline.js +9 -6
  137. package/hooks/dist/gsd-workflow-guard.js +110 -6
  138. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  139. package/hooks/dist/lib/cursor-workspace.js +74 -0
  140. package/hooks/gsd-cursor-session-start.js +6 -2
  141. package/hooks/gsd-cursor-stop.js +6 -2
  142. package/hooks/gsd-cursor-subagent-start.js +6 -2
  143. package/hooks/gsd-graphify-update.sh +9 -0
  144. package/hooks/gsd-phase-boundary.sh +14 -2
  145. package/hooks/gsd-prompt-guard.js +101 -2
  146. package/hooks/gsd-read-guard.js +100 -2
  147. package/hooks/gsd-read-injection-scanner.js +109 -2
  148. package/hooks/gsd-statusline.js +9 -6
  149. package/hooks/gsd-workflow-guard.js +110 -6
  150. package/hooks/gsd-worktree-path-guard.js +132 -8
  151. package/hooks/lib/cursor-workspace.js +74 -0
  152. package/package.json +7 -7
  153. package/pi/gsd.cjs +26 -1
  154. package/scripts/check-coverage-gate.cjs +51 -0
  155. package/scripts/check-glossary-refs.cjs +24 -0
  156. package/scripts/ci-test-scope.cjs +67 -17
  157. package/scripts/gen-adr-index.cjs +6 -4
  158. package/scripts/gen-capability-matrix.cjs +26 -2
  159. package/scripts/gen-capability-registry.cjs +132 -34
  160. package/scripts/gen-emitted-baseline.cjs +145 -0
  161. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  162. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  163. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  164. package/scripts/lint-resolution-provenance.cjs +9 -0
  165. package/scripts/mutation-matrix.cjs +4 -0
  166. package/scripts/prompt-injection-scan.sh +6 -0
  167. package/scripts/registry-schema.cjs +57 -8
  168. package/scripts/release-notes/conventional-title.cjs +19 -1
  169. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  170. package/scripts/workflow-size.cjs +16 -8
  171. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  172. package/vscode/package.json +1 -1
  173. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  174. package/scripts/update-size-baseline.cjs +0 -68
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Unusable Input Diagnostic — the out-of-band half of ADR-1411's
4
+ * "corrupt is not absent" amendment (epic #1879).
5
+ *
6
+ * ADR-1411 splits the amendment's mechanism in two. Where a read already returns a
7
+ * provenance envelope, the cause is named *in-band* — that is `ConfigResolution.reason`
8
+ * (#1880, shipped). Where a read returns a bare sentinel or a plausible default it cannot
9
+ * extend, the return value is preserved exactly and the cause is surfaced *out-of-band*,
10
+ * as a deduplicated diagnostic on stderr. This module owns that second mechanism.
11
+ *
12
+ * It exists as a shared seam rather than a pattern copied per site because four call sites
13
+ * across four modules need identical behaviour (#1882 frontmatter, #1881 roadmap-parser,
14
+ * #1883 planning-workspace/verify, #1884 planning lock). Four hand-rolled copies of one
15
+ * behaviour is `DEFECT.GENERATIVE-FIX` by construction; one seam with a frozen reason set
16
+ * is the documented cure.
17
+ *
18
+ * Two contracts this module must not break:
19
+ *
20
+ * - **Unconditional.** ADR-1411 diverges deliberately from ADR-227's never-implemented
21
+ * `GSD_DEBUG` opt-in: "an opt-in nobody sets is indistinguishable from the silence
22
+ * #1879 is about". There is no config gate here, by design.
23
+ * - **Never throws.** Callers are leaf readers that promised a total function. A failed
24
+ * stderr write (closed stream, EPIPE) must not turn a silent degradation into a crash.
25
+ */
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ const node_crypto_1 = __importDefault(require("node:crypto"));
30
+ // ─── Reason vocabulary ────────────────────────────────────────────────────────
31
+ /**
32
+ * Frozen so tests assert a typed surface instead of diagnostic prose
33
+ * (CONTRIBUTING.md — Prohibited: Raw Text Matching on Test Outputs).
34
+ *
35
+ * Adding a reason is three coordinated changes, matching the repo's `REASON`-enum
36
+ * convention: the entry here, the emitting call site, and the test that locks
37
+ * `Object.keys(UNUSABLE_REASON).sort()`. Each epic-#1879 phase adds only its own —
38
+ * pre-declaring the later phases' reasons would be speculative generality and would
39
+ * leave values no call site emits.
40
+ */
41
+ const UNUSABLE_REASON = Object.freeze({
42
+ /**
43
+ * A file opened a `---` frontmatter fence at byte 0, carried at least one parseable
44
+ * key, and never closed the fence — a truncated or half-written file, NOT a file that
45
+ * legitimately has no frontmatter. (#1882)
46
+ */
47
+ FRONTMATTER_UNTERMINATED: 'frontmatter_unterminated',
48
+ /**
49
+ * A ROADMAP.md exists but could not be read (EACCES/EIO/…). Distinct from a project that
50
+ * simply has no ROADMAP yet: absence returns the same sentinel, silently. (#1881)
51
+ */
52
+ ROADMAP_UNREADABLE: 'roadmap_unreadable',
53
+ });
54
+ /** One human-readable clause per reason. Prose lives here, never in a test assertion. */
55
+ const REASON_PROSE = Object.freeze({
56
+ [UNUSABLE_REASON.FRONTMATTER_UNTERMINATED]: 'frontmatter opens with "---" but never closes; metadata was NOT applied',
57
+ [UNUSABLE_REASON.ROADMAP_UNREADABLE]: 'ROADMAP.md exists but could not be read; phase and milestone lookups fell back to defaults',
58
+ });
59
+ // ─── Dedup state ──────────────────────────────────────────────────────────────
60
+ /**
61
+ * Process-lifetime dedup set. Mirrors `config-loader.cjs`'s `_warnedUnknownConfigKeys`
62
+ * guard, which ADR-1411 names as the precedent to reuse.
63
+ */
64
+ const _warnedUnusableInputs = new Set();
65
+ /**
66
+ * Count of diagnostics actually WRITTEN, which is not the same as the size of the dedup set:
67
+ * one emission records every key the input could later be identified by, so set size counts
68
+ * identities while this counts events. Tests assert on this because the behavioural claim is
69
+ * "how many diagnostics did the operator see", not "how many keys are interned".
70
+ */
71
+ let _unusableInputEmissions = 0;
72
+ /**
73
+ * ASCII control characters (including NUL) are stripped from any path before it is used
74
+ * as a key component or written to a terminal. Two reasons, both real:
75
+ *
76
+ * - the key separator is NUL, so a `sourcePath` containing NUL could otherwise forge a
77
+ * collision with a different (path, reason) pair and suppress a genuine second failure;
78
+ * - a path carrying ANSI escapes would be replayed verbatim into the operator's terminal.
79
+ */
80
+ const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
81
+ /**
82
+ * Strip control characters. Deliberately does NOT normalize path separators.
83
+ *
84
+ * An earlier revision folded backslashes to `/` unconditionally, reasoning that `C:\a\b.md`
85
+ * and `C:/a/b.md` are one file and should not report twice. That is true on Windows, and
86
+ * false — destructively — everywhere else: `\` is a legal filename character on Linux and
87
+ * macOS, so `/repo/weird\name/PLAN.md` and `/repo/weird/name/PLAN.md` are two genuinely
88
+ * different files that collapsed to one key, and the second one's diagnostic was silently
89
+ * swallowed. ADR-1411 forbids exactly that ("keying too coarsely suppresses a genuine second
90
+ * failure in a different file"), and this repo targets Linux/macOS/Windows alike.
91
+ *
92
+ * The trade is now explicit and one-directional: two spellings of one Windows path may
93
+ * report twice (mild noise), but two distinct files can never silence each other (lost
94
+ * signal). Dropping a real diagnostic is the strictly worse failure.
95
+ */
96
+ function sanitizeSource(source) {
97
+ return source.replace(CONTROL_CHARS, '');
98
+ }
99
+ /**
100
+ * Identify the offending input. A path is preferred because it is what an operator can act
101
+ * on. When the caller has only an in-memory string, fall back to a short content digest so
102
+ * that *different* bad inputs still produce *different* keys.
103
+ *
104
+ * The leading `p`/`d` tag is what keeps the two namespaces disjoint. Without it a caller
105
+ * whose file is literally named `<unnamed:8efa5269728e7271>` would key identically to a
106
+ * path-less caller whose content happens to hash to that digest — no brute force required,
107
+ * since the digest of any predictable content (a shared template, known boilerplate) can
108
+ * simply be computed and used as a filename to pre-seed suppression. Because control
109
+ * characters — including NUL — are stripped from `source`, a caller-supplied path can never
110
+ * contain the separator and so can never forge a key in the other namespace either.
111
+ *
112
+ * The digest is computed only on the emission path, which is rare, so it never costs
113
+ * anything on a healthy read.
114
+ */
115
+ function sourceKey(source, content) {
116
+ if (typeof source === 'string' && source.trim() !== '') {
117
+ return `p\u0000${sanitizeSource(source)}`;
118
+ }
119
+ const digest = node_crypto_1.default.createHash('sha256').update(content ?? '').digest('hex').slice(0, 16);
120
+ return `d\u0000${digest}`;
121
+ }
122
+ /** Human-facing name for the offending input, derived from the same key. */
123
+ function displaySource(key) {
124
+ return key.startsWith('p\u0000') ? key.slice(2) : `<unnamed:${key.slice(2)}>`;
125
+ }
126
+ /**
127
+ * Emit a deduplicated diagnostic naming an input that exists but cannot be used.
128
+ *
129
+ * The key is `<normalized source>\0<reason>`. ADR-1411 requires the resolved path AND the
130
+ * distinguishing cause — keying on the path alone would let a second, different fault on
131
+ * the same file go unreported; keying on the message prose would couple the guard to
132
+ * wording.
133
+ *
134
+ * @returns `true` when this call actually wrote a diagnostic, `false` when it was
135
+ * deduplicated. Returning the decision is what lets tests assert emission *counts* on a
136
+ * typed surface rather than scraping stderr.
137
+ */
138
+ function warnUnusableInput({ reason, source, content }) {
139
+ // Defensive: an unknown reason must not emit a diagnostic with `undefined` in it.
140
+ const prose = Object.prototype.hasOwnProperty.call(REASON_PROSE, reason)
141
+ ? REASON_PROSE[reason]
142
+ : null;
143
+ if (prose === null)
144
+ return false;
145
+ // The guarantee, stated precisely, because it is not symmetric:
146
+ //
147
+ // * a file reported BY NAME is reported at most once, and
148
+ // * an anonymous re-parse of content already reported by name stays silent, and
149
+ // * two DIFFERENT files always both report, even when their truncated bytes are identical.
150
+ //
151
+ // The asymmetry is the anonymous-FIRST ordering (a path-less parse, then a named parse of the
152
+ // same content), which emits twice. That is a deliberate limit, not an oversight. A path-less
153
+ // caller cannot identify its file, so suppressing the later named report would also suppress a
154
+ // genuine second failure in a DIFFERENT file whenever two files share byte-identical truncated
155
+ // content — the over-coarse keying ADR-1411 explicitly forbids. Between a duplicate line and a
156
+ // swallowed diagnostic the ADR ranks the swallow worse, so the duplicate is accepted; and the
157
+ // second line is the more useful of the two, because it carries the filename.
158
+ //
159
+ // Mechanically: check ONLY the key matching what this caller actually knows, but record every
160
+ // key the input could later be identified by.
161
+ const identity = sourceKey(source, content);
162
+ const keys = [`${identity}\u0000${reason}`];
163
+ if (typeof content === 'string' && identity.startsWith('p\u0000')) {
164
+ keys.push(`${sourceKey(undefined, content)}\u0000${reason}`);
165
+ }
166
+ if (_warnedUnusableInputs.has(keys[0]))
167
+ return false;
168
+ for (const k of keys)
169
+ _warnedUnusableInputs.add(k);
170
+ try {
171
+ process.stderr.write(`gsd: warning — ${displaySource(identity)}: ${prose}. (#1879)\n`);
172
+ // Counted only after a write that actually completed. Incrementing before the try counted
173
+ // attempts, so on a broken stderr the counter claimed a diagnostic had reached the operator
174
+ // when nothing had — a seam documented as "written" reporting something else.
175
+ _unusableInputEmissions += 1;
176
+ }
177
+ catch {
178
+ /* a closed or broken stderr must never escalate a degraded read into a crash */
179
+ }
180
+ return true;
181
+ }
182
+ // ─── Test seams ───────────────────────────────────────────────────────────────
183
+ /**
184
+ * Clear the dedup state between cases.
185
+ *
186
+ * This exists because the set is process-global: without it, the second test to use a key
187
+ * silently observes the first test's suppression. #2674 is the cautionary precedent — a
188
+ * reset helper that cleared two of three sets was a silent no-op for the very suite that
189
+ * existed to test it, and the cases only passed because each happened to pick a key no
190
+ * other case reused.
191
+ */
192
+ function _resetUnusableInputWarningsForTests() {
193
+ _warnedUnusableInputs.clear();
194
+ _unusableInputEmissions = 0;
195
+ }
196
+ /** Number of diagnostics written — the typed surface tests assert on instead of stderr prose. */
197
+ function _unusableInputEmissionCountForTests() {
198
+ return _unusableInputEmissions;
199
+ }
200
+ /** Size of the dedup set (identities interned, not events). Retained for key-shape assertions. */
201
+ function _unusableInputWarningCountForTests() {
202
+ return _warnedUnusableInputs.size;
203
+ }
204
+ /** Test seam: the sanitized form of a source, so control-char handling is asserted on a
205
+ * returned value instead of by scraping what reached stderr. */
206
+ function _sanitizeSourceForTests(source) {
207
+ return sanitizeSource(source);
208
+ }
209
+ module.exports = {
210
+ UNUSABLE_REASON,
211
+ _sanitizeSourceForTests,
212
+ warnUnusableInput,
213
+ _resetUnusableInputWarningsForTests,
214
+ _unusableInputWarningCountForTests,
215
+ _unusableInputEmissionCountForTests,
216
+ };
@@ -37,6 +37,7 @@ exports.canonicalPlanStem = canonicalPlanStem;
37
37
  exports.phaseVariants = phaseVariants;
38
38
  exports.buildRoadmapPhaseVariants = buildRoadmapPhaseVariants;
39
39
  exports.buildNotStartedPhaseVariants = buildNotStartedPhaseVariants;
40
+ exports.textEncodingError = textEncodingError;
40
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports
41
42
  const phaseIdMod = require("./phase-id.cjs");
42
43
  const { OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, PHASE_NUMBER_TOKEN_SOURCE, PHASE_CONTINUATION_SEGMENT_SOURCE, } = phaseIdMod;
@@ -137,3 +138,34 @@ function buildNotStartedPhaseVariants(roadmapContent) {
137
138
  }
138
139
  return notStartedPhases;
139
140
  }
141
+ /**
142
+ * Detect binary corruption (embedded NUL bytes) in a text artifact's bytes.
143
+ *
144
+ * #2701: the plan/summary/verification/state validators must FAIL LOUD on a
145
+ * NUL-corrupted file instead of reporting `valid: true`. A NUL byte is the
146
+ * unambiguous signal — UTF-8 text never contains 0x00 — and a file carrying one
147
+ * is binary-classified by `file(1)`, then silently OMITTED from recursive /
148
+ * binary-skipping search results (`rg -l`, `grep -rI`, exit 0), so the corruption
149
+ * reads downstream as "file absent" rather than "file corrupt." The error message
150
+ * names that consequence so the next investigator is not misdirected.
151
+ *
152
+ * This is a pure, opt-in check called explicitly by each validator at its own
153
+ * entry point. It is deliberately NOT placed inside the shared `platformReadSync`
154
+ * read primitive (which dozens of best-effort, tolerant reads flow through and
155
+ * which must not start hard-failing on encoding). It does NOT strip, sanitize, or
156
+ * repair the NUL bytes — corruption is a signal of an upstream authoring-tool bug
157
+ * and must stay visible.
158
+ *
159
+ * @param buf the file bytes (Buffer or string; a string is searched char-wise)
160
+ * @param relPath a path/label for the diagnostic message
161
+ * @returns an error string when NUL is found, or `null` when the bytes are clean text
162
+ */
163
+ function textEncodingError(buf, relPath) {
164
+ const nul = typeof buf === 'string' ? buf.indexOf('\0') : buf.indexOf(0x00);
165
+ if (nul === -1)
166
+ return null;
167
+ return (`${relPath}: file contains NUL bytes (first at offset ${nul}). ` +
168
+ 'Artifact files must be UTF-8 text. A NUL-corrupted file is binary-classified ' +
169
+ 'and silently skipped by recursive / binary-skipping search tools (rg, grep -I), ' +
170
+ 'so downstream verification reports its contents as missing rather than corrupt.');
171
+ }
@@ -41,6 +41,7 @@ const frontmatterMod = require("./frontmatter.cjs");
41
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
42
42
  const scanPhasePlans = require("./plan-scan.cjs");
43
43
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
44
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
44
45
  const { output, error } = io;
45
46
  const { extractPhaseToken } = phaseId;
46
47
  const { extractFrontmatter } = frontmatterMod;
@@ -61,6 +62,12 @@ const VERIFIER_STATUSES = ['passed', 'gaps_found', 'human_needed'];
61
62
  *
62
63
  * For 'gaps_found', next_command is built at call time in readVerificationStatus
63
64
  * by substituting the phase number — it is NOT stored as a function in the table.
65
+ *
66
+ * #2617: `next_command` here holds a BARE command name (`execute-phase`), never a
67
+ * prefixed one. Every return path projects it through `formatGsdSlash` with the
68
+ * caller's runtime, so Codex sees `$gsd-execute-phase` and slash-hyphen runtimes
69
+ * see `/gsd-execute-phase`. Storing a prefixed literal is what leaked the
70
+ * hard-coded (and deprecated) `/gsd:` colon form to every runtime.
64
71
  */
65
72
  const VERIFICATION_ROUTING_TABLE = {
66
73
  passed: {
@@ -77,7 +84,12 @@ const VERIFICATION_ROUTING_TABLE = {
77
84
  human_needed: {
78
85
  status: 'human_needed',
79
86
  next_action: "Human verification required. Complete the manual tests in the phase's *-UAT.md, then re-run the verify step until status is passed.",
80
- next_command: '',
87
+ // #2617: was '' — next_action told the user to "re-run the verify step" but
88
+ // named no command, while init.cts's parallel projector emitted
89
+ // `verify-work <N>` for this same state. The two surfaces disagreed on
90
+ // whether a next command existed at all; init's answer was the useful one,
91
+ // and init now delegates here rather than re-deriving it.
92
+ next_command: 'verify-work',
81
93
  },
82
94
  stale: {
83
95
  status: 'stale',
@@ -89,16 +101,30 @@ const VERIFICATION_ROUTING_TABLE = {
89
101
  missing: {
90
102
  status: 'missing',
91
103
  next_action: 'No verification report found — the verify step never completed. Re-run execute-phase.',
92
- next_command: '/gsd:execute-phase',
104
+ next_command: 'execute-phase',
93
105
  },
94
106
  // INTERNAL SENTINEL: constructed when the file has a status value not in
95
107
  // VERIFIER_STATUSES. Never emitted by the verifier.
96
108
  unknown: {
97
109
  status: 'unknown',
98
110
  next_action: '', // filled in dynamically with the raw value
99
- next_command: '/gsd:execute-phase',
111
+ next_command: 'execute-phase',
100
112
  },
101
113
  };
114
+ /**
115
+ * Project a BARE command name (plus optional argument tail) into the surface the
116
+ * given runtime actually installs (#2617).
117
+ *
118
+ * `formatGsdSlash` owns the per-runtime shape (`$gsd-<cmd>` for shell-var
119
+ * runtimes like Codex, `/gsd-<cmd>` otherwise) and is idempotent, so passing an
120
+ * already-prefixed string is safe. An empty command stays empty — "no next
121
+ * command" must not become a bare prefix.
122
+ */
123
+ function projectNextCommand(bare, runtime, tail = '') {
124
+ if (!bare)
125
+ return '';
126
+ return `${(0, runtime_slash_cjs_1.formatGsdSlash)(bare, runtime)}${tail}`;
127
+ }
102
128
  /** Normalize separators to posix (git emits `/`; callers may pass `\` on Windows). */
103
129
  function toPosix(p) {
104
130
  return p.replace(/\\/g, '/');
@@ -177,12 +203,12 @@ function defaultPhaseCleanCommitTimesMs(phaseDir, files, execGitFn = shell_comma
177
203
  * Used for two early-return paths: no *-VERIFICATION.md file found, and
178
204
  * file present but no parseable frontmatter status.
179
205
  */
180
- function missingResult() {
206
+ function missingResult(runtime, phaseArg) {
181
207
  const route = VERIFICATION_ROUTING_TABLE['missing'];
182
208
  return {
183
209
  status: route.status,
184
210
  next_action: route.next_action,
185
- next_command: route.next_command,
211
+ next_command: projectNextCommand(route.next_command, runtime, phaseArg),
186
212
  };
187
213
  }
188
214
  function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phaseCleanCommitTimesMs = defaultPhaseCleanCommitTimesMs) {
@@ -241,14 +267,25 @@ function findStaleVerificationSummary(phaseDir, fsImpl = node_fs_1.default, phas
241
267
  *
242
268
  * @param phaseDir - Absolute path to the phase directory.
243
269
  * @param opts - Options. `opts.fs` allows test injection (defaults to node:fs).
270
+ * `opts.runtime` selects the command surface `next_command` is
271
+ * projected into (#2617).
244
272
  */
245
273
  function readVerificationStatus(phaseDir, opts = {}) {
246
274
  const fsImpl = opts.fs ?? node_fs_1.default;
247
275
  const phaseCleanCommitTimesMs = opts.phaseCleanCommitTimesMs ?? defaultPhaseCleanCommitTimesMs;
276
+ const runtime = opts.runtime ?? 'claude';
248
277
  // Phase token for the gaps_found command
249
278
  const baseName = node_path_1.default.basename(phaseDir);
250
279
  const phaseToken = extractPhaseToken(baseName);
251
- const phaseNumber = phaseToken.length > 0 ? phaseToken : baseName;
280
+ const derivedPhaseNumber = phaseToken.length > 0 ? phaseToken : baseName;
281
+ // #2617: the phase number becomes a COMMAND ARGUMENT, so it is appended only
282
+ // when it is unambiguously one. extractPhaseToken also returns project-code
283
+ // forms (`PROJ-07`), which are indistinguishable by shape from an ordinary
284
+ // directory name — `gsd-651-parent` yields `gsd-651` — and emitting
285
+ // `execute-phase gsd-651` is worse than emitting no argument at all. Callers
286
+ // that already know the number (init) pass it explicitly and always get it.
287
+ const phaseArgSource = opts.phaseNumber ?? (/^\d+(\.\d+)*$/.test(derivedPhaseNumber) ? derivedPhaseNumber : '');
288
+ const phaseArg = phaseArgSource ? ` ${phaseArgSource}` : '';
252
289
  // 1. Find *-VERIFICATION.md
253
290
  let verificationFile = null;
254
291
  try {
@@ -261,7 +298,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
261
298
  verificationFile = null;
262
299
  }
263
300
  if (!verificationFile) {
264
- return missingResult();
301
+ return missingResult(runtime, phaseArg);
265
302
  }
266
303
  // 2. Read and parse frontmatter using the shared parser.
267
304
  // extractFrontmatter anchors at byte 0, so body `status:` lines are ignored.
@@ -269,7 +306,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
269
306
  let rawStatus = null;
270
307
  try {
271
308
  const content = fsImpl.readFileSync(filePath, 'utf-8');
272
- const fm = extractFrontmatter(content);
309
+ const fm = extractFrontmatter(content, filePath);
273
310
  const statusVal = fm['status'];
274
311
  // status is always a scalar string in a well-formed VERIFICATION.md frontmatter;
275
312
  // only accept string values — arrays and objects are not valid status values.
@@ -282,7 +319,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
282
319
  rawStatus = null;
283
320
  }
284
321
  if (!rawStatus) {
285
- return missingResult();
322
+ return missingResult(runtime, phaseArg);
286
323
  }
287
324
  // gaps_found takes priority over stale — gap closure is the correct next
288
325
  // step regardless of whether summaries are newer than the verification file.
@@ -291,7 +328,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
291
328
  return {
292
329
  status: entry.status,
293
330
  next_action: entry.next_action,
294
- next_command: `/gsd:plan-phase ${phaseNumber} --gaps`,
331
+ next_command: projectNextCommand('plan-phase', runtime, `${phaseArg} --gaps`),
295
332
  };
296
333
  }
297
334
  const staleVerification = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs);
@@ -300,7 +337,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
300
337
  return {
301
338
  status: entry.status,
302
339
  next_action: entry.next_action,
303
- next_command: `/gsd:verify-work ${phaseNumber}`,
340
+ next_command: projectNextCommand('verify-work', runtime, phaseArg),
304
341
  };
305
342
  }
306
343
  // 3. Route — exclude internal sentinels from raw-file lookup (they are
@@ -314,7 +351,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
314
351
  return {
315
352
  status: entry.status,
316
353
  next_action: entry.next_action,
317
- next_command: entry.next_command,
354
+ next_command: projectNextCommand(entry.next_command, runtime, phaseArg),
318
355
  };
319
356
  }
320
357
  // Unknown value
@@ -322,7 +359,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
322
359
  return {
323
360
  status: unknownRoute.status,
324
361
  next_action: `Unexpected verification status '${rawStatus}'. Re-run execute-phase verification.`,
325
- next_command: unknownRoute.next_command,
362
+ next_command: projectNextCommand(unknownRoute.next_command, runtime, phaseArg),
326
363
  };
327
364
  }
328
365
  /**
@@ -339,7 +376,7 @@ function cmdVerificationStatus(cwd, phaseDirArg, raw) {
339
376
  return;
340
377
  }
341
378
  const phaseDir = node_path_1.default.resolve(cwd, phaseDirArg);
342
- const result = readVerificationStatus(phaseDir);
379
+ const result = readVerificationStatus(phaseDir, { runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd) });
343
380
  output(result, raw);
344
381
  }
345
382
  module.exports = {