@opengsd/gsd-core 1.7.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 (261) 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 +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -56,6 +56,11 @@ const uatPredicate = require("./uat-predicate.cjs");
56
56
  const { evaluateUatPassed } = uatPredicate;
57
57
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
58
58
  const verificationMod = require("./verification.cjs");
59
+ // #2572: the artifact↔disk core behind the `verify-summary` verb. `verify.cts`
60
+ // has no transitive import path back to `phase.cts`, so this edge introduces no
61
+ // cycle (the reverse edge, `state.cts → verify.cjs`, would).
62
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- verify.cjs is an export= CommonJS module
63
+ const verifyMod = require("./verify.cjs");
59
64
  const { readVerificationStatus } = verificationMod;
60
65
  const { planningDir, withPlanningLock, listAvailableWorkstreams, getActiveWorkstream } = planningWorkspace;
61
66
  const { extractFrontmatter } = frontmatterMod;
@@ -106,6 +111,19 @@ function updateTraceabilityCell(text, match, column, newValue) {
106
111
  return result;
107
112
  return { ok: true, value: before + result.value + after };
108
113
  }
114
+ /**
115
+ * Extract the MAJOR version segment from a version-ish string: "v1", "v1.3",
116
+ * "V1.0", and "1.0" all yield "1"; "v2" yields "2". Used (#2334 BLOCKER fix)
117
+ * to compare a `## v<N> ...` REQUIREMENTS.md heading against the current
118
+ * milestone's version at MAJOR-version granularity only — "v1" heading vs
119
+ * milestone "v1.3" is the SAME major version and must not be treated as a
120
+ * version mismatch. Returns null when `raw` has no leading digit run (not a
121
+ * version-shaped string), which the caller treats as "cannot resolve".
122
+ */
123
+ function extractMajorVersion(raw) {
124
+ const m = raw.trim().match(/^v?(\d+)/i);
125
+ return m ? m[1] : null;
126
+ }
109
127
  function describeNonCanonicalPlans(dirFiles, matchedFiles) {
110
128
  const matched = new Set(matchedFiles);
111
129
  const offenders = dirFiles.filter((f) => looksLikePlanFile(f) && !matched.has(f));
@@ -127,8 +145,13 @@ function extractCanonicalPlanId(filename) {
127
145
  // or a single-digit-plus-letter id ("3A"); a *bare* single digit is a slug word,
128
146
  // so "46-6-rs-…" is not paired into a "46-6" id while "3A-01" stays intact.
129
147
  const tokenRe = /^(?:\d{2,}[A-Z]?|\d[A-Z])(?:\.\d+)*$/i;
148
+ // #2232: the PAIRED plan component is a zero-padded continuation segment
149
+ // (exactly 2 digits), so a ≥3-digit slug word (a year) is not paired into a
150
+ // bogus "14-2026" id. The leading phase component keeps tokenRe's unbounded
151
+ // \d{2,} — phase numbers ≥100 are legitimate; only continuations are capped.
152
+ const planTokenRe = new RegExp(`^(?:${phaseIdMod.PHASE_CONTINUATION_SEGMENT_SOURCE}[A-Z]?|\\d[A-Z])(?:\\.\\d+)*$`, 'i');
130
153
  const phaseIdx = parts.findIndex((p) => tokenRe.test(p));
131
- if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
154
+ if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && planTokenRe.test(parts[phaseIdx + 1])) {
132
155
  return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
133
156
  }
134
157
  return base;
@@ -494,7 +517,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
494
517
  const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
495
518
  const planPath = node_path_1.default.join(phaseDir, planFile);
496
519
  const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
497
- const fm = extractFrontmatter(content);
520
+ // Pass planPath so a truncated PLAN.md names the file in the #1882 diagnostic.
521
+ const fm = extractFrontmatter(content, planPath);
498
522
  const xmlTasks = content.match(/<task[\s>]/gi) || [];
499
523
  const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
500
524
  const taskCount = xmlTasks.length || mdTasks.length;
@@ -603,6 +627,29 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
603
627
  result['warnings'] = warnings;
604
628
  output(result, raw);
605
629
  }
630
+ // #2390 — phase.add title-shape heuristic. A description at or under this many
631
+ // characters, and with no sentence-ending punctuation followed by more text,
632
+ // reads as a short Title. Anything longer or multi-sentence reads as a Goal,
633
+ // not a Title. phase.add still writes the phase verbatim (it never mangles
634
+ // ROADMAP.md), but when the description looks goal-shaped the JSON result
635
+ // gains a `warning` key naming the gap, so the caller — or the orchestrating
636
+ // add-phase workflow — can split title vs. goal instead of the whole paragraph
637
+ // landing silently in the `### Phase N:` header.
638
+ const PHASE_ADD_TITLE_MAX_LEN = 80;
639
+ const PHASE_ADD_MULTI_SENTENCE_RE = /[.!?]['")\]]?\s+\S/;
640
+ function describeGoalShapedTitle(description) {
641
+ const trimmed = description.trim();
642
+ const tooLong = trimmed.length > PHASE_ADD_TITLE_MAX_LEN;
643
+ const multiSentence = PHASE_ADD_MULTI_SENTENCE_RE.test(trimmed);
644
+ if (!tooLong && !multiSentence)
645
+ return null;
646
+ const reasons = [
647
+ tooLong ? `${trimmed.length} chars (over the ${PHASE_ADD_TITLE_MAX_LEN}-char title threshold)` : null,
648
+ multiSentence ? 'multiple sentences' : null,
649
+ ].filter(Boolean).join(', ');
650
+ return (`description looks goal-shaped, not title-shaped (${reasons}). It was written verbatim ` +
651
+ `as the phase title; consider a short title with the detail moved to **Goal:**.`);
652
+ }
606
653
  function cmdPhaseAdd(cwd, description, raw, customId) {
607
654
  if (!description) {
608
655
  error('description required for phase add');
@@ -690,6 +737,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
690
737
  (0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, updatedContent);
691
738
  return { newPhaseId: _newPhaseId, dirName: _dirName };
692
739
  });
740
+ const titleWarning = describeGoalShapedTitle(description);
693
741
  const result = {
694
742
  phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
695
743
  padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
@@ -698,7 +746,9 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
698
746
  directory: toPosixPath(node_path_1.default.join(node_path_1.default.relative(cwd, planningDir(cwd)), 'phases', dirName)),
699
747
  naming_mode: config.phase_naming,
700
748
  };
701
- output(result, raw, result.padded);
749
+ if (titleWarning)
750
+ result['warning'] = titleWarning;
751
+ output(result, raw, result['padded']);
702
752
  }
703
753
  function cmdPhaseAddBatch(cwd, descriptions, raw) {
704
754
  if (!Array.isArray(descriptions) || descriptions.length === 0) {
@@ -1358,13 +1408,14 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1358
1408
  warnings.push(`${file}: has diagnosed gaps`);
1359
1409
  }
1360
1410
  for (const file of phaseFiles.filter((f) => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
1361
- const content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseFullDir, file), 'utf-8');
1411
+ const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
1412
+ const content = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
1362
1413
  // #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false positives
1363
1414
  // from historical metadata in the file body (e.g. `previous_status: gaps_found`).
1364
1415
  // A full-text regex like /status: gaps_found/ matches the substring inside
1365
1416
  // `previous_status: gaps_found`, producing spurious warnings even when the
1366
1417
  // current frontmatter status is `passed`.
1367
- const verFm = extractFrontmatter(content);
1418
+ const verFm = extractFrontmatter(content, verificationFilePath);
1368
1419
  // Normalise to lower-case so `status: Passed` (title-case) is not missed.
1369
1420
  const verStatus = typeof verFm['status'] === 'string' ? verFm['status'].trim().toLowerCase() : '';
1370
1421
  if (verStatus === 'human_needed')
@@ -1380,11 +1431,51 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1380
1431
  * mechanism). A readdirSync/readFileSync failure here just means fewer
1381
1432
  * warnings are surfaced this run, not a blocked or corrupted completion. */
1382
1433
  }
1434
+ // #2572: artifact↔disk advisory for the SUMMARYs of the phase being completed.
1435
+ //
1436
+ // A SUMMARY asserts "I created these files". Nothing checked that claim for
1437
+ // phase summaries — the `verify-summary` verb has existed since the beginning
1438
+ // but was only ever pointed at `.planning/research/SUMMARY.md`. An interrupted
1439
+ // or over-reported phase therefore counted toward 100% silently.
1440
+ //
1441
+ // Joins the same ADVISORY channel as the pre-scan above: findings land in
1442
+ // `warnings[]` (rendered by execute-phase.md's "If has_warnings is true"
1443
+ // step), never in the completion GATE (readVerificationStatus below).
1444
+ // Completion is never blocked.
1445
+ //
1446
+ // `checkCommits: false` — only the file-existence half is surfaced here, so
1447
+ // the `git cat-file` probes would be spawned and their result discarded. The
1448
+ // hash pattern is a loose `\b[0-9a-f]{7,40}\b` that matches any hex-shaped
1449
+ // token in prose, too noisy to put in front of a user even as a warning.
1450
+ //
1451
+ // `Infinity` — report every referenced file, not the CLI verb's default first
1452
+ // two, so a phase that lists twelve files and landed three says so. The verb
1453
+ // keeps its 2-file default; only this caller opts out of the cap.
1454
+ try {
1455
+ const phaseDirRel = phaseInfo['directory'];
1456
+ // `summaries` arrives pre-sorted from the phase locator, so warning order is
1457
+ // deterministic across platforms rather than readdir-dependent.
1458
+ const summaryNames = phaseInfo['summaries'] || [];
1459
+ for (const summaryName of summaryNames) {
1460
+ const v = verifyMod.verifySummaryCore(cwd, `${phaseDirRel}/${summaryName}`, Infinity, { checkCommits: false });
1461
+ const missing = v.checks.files_created.missing;
1462
+ if (missing.length > 0) {
1463
+ warnings.push(`${summaryName}: references ${missing.length} file(s) not on disk: ${missing.join(', ')}`);
1464
+ }
1465
+ }
1466
+ }
1467
+ catch {
1468
+ /* best-effort, same posture as the #2245 pre-scan above: an unreadable
1469
+ * SUMMARY means one fewer advisory this run, never a blocked completion. */
1470
+ }
1383
1471
  let nextPhaseNum = null;
1384
1472
  let nextPhaseName = null;
1385
1473
  let isLastPhase = true;
1386
1474
  const verificationBlocked = withPlanningLock(cwd, () => {
1387
- const verificationStatus = readVerificationStatus(phaseFullDir);
1475
+ // #2617: pass the project's runtime so the blocked-completion error below
1476
+ // suggests the command surface this runtime actually installs
1477
+ // ($gsd-… on Codex) rather than a hard-coded Claude-style string.
1478
+ const verificationStatus = readVerificationStatus(phaseFullDir, { runtime: (0, runtime_slash_cjs_1.resolveRuntime)(cwd) });
1388
1479
  if (verificationStatus.status !== 'passed') {
1389
1480
  return verificationStatus;
1390
1481
  }
@@ -1553,13 +1644,43 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1553
1644
  const reqMatch = sectionText.match(/\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]+)/i);
1554
1645
  const originalReqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
1555
1646
  let reqContent = originalReqContent;
1647
+ // #2316: `citedReqIds` — the REQ-IDs ROADMAP's own **Requirements:**
1648
+ // line for this phase actually cites — is hoisted out of the
1649
+ // `if (reqMatch)` block (previously scoped only inside it) so the
1650
+ // ghost-ID cross-check below (~#2316-1) can consult it. `TBD` is the
1651
+ // literal placeholder `phase.add`/`-batch`/`-insert` seed
1652
+ // (`**Requirements**: TBD`, src/phase.cts:833,920,1078) — never a
1653
+ // real REQ-ID, so it is filtered out wherever a cited-ID list feeds
1654
+ // a warning (#2316-7 boundary).
1655
+ const isPlaceholderReqId = (id) => id.toUpperCase() === 'TBD';
1656
+ let citedReqIds = [];
1657
+ // #2316-1: Traceability-row writes that matched NO row (ghost or
1658
+ // otherwise) — the `if (reqUpdate.ok)` below previously had no
1659
+ // `else`, discarding this fact silently instead of surfacing it.
1660
+ const traceabilityWriteMisses = [];
1556
1661
  if (reqMatch) {
1557
- const reqIds = reqMatch[1]
1662
+ // #2334 HIGH 3: filter the tokenized capture to the REQ-ID SHAPE —
1663
+ // the SAME shape bodyReqIds (`\*\*([A-Z][A-Z0-9]*-\d+)\*\*`, below)
1664
+ // and tableReqIds (`([A-Z][A-Z0-9]*-\d+)`, below) already require —
1665
+ // so the ghost-ID / unregistered comparisons stay shape-symmetric.
1666
+ // Without this, `[^\n]+` split on `[,\s]+` turned EVERY word after
1667
+ // the ID list into a "cited REQ-ID": the shipped
1668
+ // `templates/roadmap.md:32` line
1669
+ // `**Requirements**: [REQ-01, REQ-02] <!-- brackets optional, ... -->`
1670
+ // warned to register `<!--`, `brackets`, `optional`, `-->`, etc., and
1671
+ // `**Requirements:** None` warned to register the literal word
1672
+ // `None`. This subsumes the `TBD` placeholder special-case (`TBD`
1673
+ // does not match the REQ-ID shape either); `isPlaceholderReqId` is
1674
+ // kept below as a defensive no-op for any caller that still hands
1675
+ // it a raw token.
1676
+ const REQ_ID_SHAPE_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
1677
+ citedReqIds = reqMatch[1]
1558
1678
  .replace(/[\[\]]/g, '')
1559
1679
  .split(/[,\s]+/)
1560
1680
  .map((r) => r.trim())
1561
- .filter(Boolean);
1562
- for (const reqId of reqIds) {
1681
+ .filter(Boolean)
1682
+ .filter((r) => REQ_ID_SHAPE_RE.test(r));
1683
+ for (const reqId of citedReqIds) {
1563
1684
  const reqEscaped = escapeRegex(reqId);
1564
1685
  reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'), '$1x$2');
1565
1686
  // Traceability row: | <REQ-ID> | Phase N | Pending|In Progress | ->
@@ -1578,14 +1699,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1578
1699
  // Complete" gate is folded into the newValue callback so one
1579
1700
  // updateTableCell call both probes and writes.
1580
1701
  const reqUpdate = updateTraceabilityCell(reqContent, reqRowMatch, 'Status', (current) => /^(?:pending|in progress)$/i.test(current.trim()) ? ' Complete ' : current);
1581
- if (reqUpdate.ok)
1702
+ if (reqUpdate.ok) {
1582
1703
  reqContent = reqUpdate.value;
1704
+ }
1705
+ else if (!isPlaceholderReqId(reqId)) {
1706
+ traceabilityWriteMisses.push(reqId);
1707
+ }
1583
1708
  }
1584
1709
  }
1585
1710
  // #1159 (Defect B): collect requirement IDs only from ACTIVE sections.
1586
1711
  // Requirements under headings whose text contains "deferred", "backlog",
1587
- // "future", or "v2" (case-insensitive) are explicitly out of current scope
1588
- // and must not be flagged as missing from the Traceability table.
1712
+ // "future", or an OFF-milestone `v<N>` (case-insensitive) are explicitly
1713
+ // out of current scope and must not be flagged as missing from the
1714
+ // Traceability table.
1589
1715
  //
1590
1716
  // Strategy: walk lines, track heading depth, and toggle a "deferred" flag
1591
1717
  // when a heading matching the pattern is encountered. A sub-heading (higher
@@ -1593,7 +1719,47 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1593
1719
  // opens a same-or-shallower heading that does NOT match the pattern.
1594
1720
  // Lines inside fenced code blocks (``` or ~~~) are treated as content, not
1595
1721
  // headings, to avoid false deferred-section detection from code examples.
1596
- const DEFERRED_HEADING_RE = /\b(?:deferred|backlog|future|v\d+)\b/i;
1722
+ //
1723
+ // #2334 BLOCKER fix (regresses closed bug #1159 against GSD's OWN
1724
+ // shipped template): #2316-4a dropped the bare `v\d+` alternative
1725
+ // entirely to stop it over-matching an ACTIVE heading like "## v1
1726
+ // Requirements" — but the shipped `templates/requirements.md:35`
1727
+ // scaffold ships `## v2 Requirements` / "Deferred to future release"
1728
+ // as its ONLY deferred marker, and `v\d+` was the ONLY alternative
1729
+ // that ever matched a bare version heading (the deferred-ness lives
1730
+ // in body prose, not the heading text). Dropping it regressed #1159
1731
+ // for every project scaffolded from the shipped template.
1732
+ //
1733
+ // Fix: make the `v<N>` alternative MILESTONE-AWARE instead of
1734
+ // deleting it. A `## v<N> ...` heading is deferred ONLY when `<N>`
1735
+ // (MAJOR version only — "v1" vs milestone "v1.3" is the SAME major
1736
+ // version) does not match the CURRENT milestone's major version,
1737
+ // resolved via `stateExtractField` against STATE.md's `milestone:`
1738
+ // frontmatter field (the same seam `getMilestoneInfo`/state.cts's
1739
+ // frontmatter builder already use — no bespoke frontmatter parsing).
1740
+ // "## v1 Requirements" while the milestone is v1.x is the ACTIVE
1741
+ // milestone's own section (#2316's original ask) and must NOT be
1742
+ // swallowed; "## v2 Requirements" while the milestone is v1.x is a
1743
+ // genuinely future milestone (#1159's ask, and the literal shipped-
1744
+ // template shape) and MUST stay suppressed. `deferred`/`backlog`/
1745
+ // `future` are unaffected by milestone resolution — a genuinely
1746
+ // deferred heading always spells one of those words too (see
1747
+ // #2316-5 regression guard: "## Deferred v2 Requirements", "##
1748
+ // Future Backlog", "## Deferred", "## Backlog", "## Future").
1749
+ //
1750
+ // Fail-safe: when the milestone version cannot be resolved at all
1751
+ // (no STATE.md, or no `milestone:` field), fall back to the OLD
1752
+ // pre-#2316-4a behavior and treat every `v\d+` heading as deferred.
1753
+ // A false "deferred" here only ever SUPPRESSES a warning — strictly
1754
+ // safer than spamming a warning on every v\d+-headed scaffold when
1755
+ // we cannot tell whether it names the active milestone.
1756
+ const DEFERRED_KEYWORD_RE = /\b(?:deferred|backlog|future)\b/i;
1757
+ const HEADING_VERSION_RE = /\bv(\d+)(?:\.\d+)*\b/i;
1758
+ const stateRawForMilestone = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
1759
+ const currentMilestoneRaw = stateRawForMilestone
1760
+ ? stateExtractField(stateRawForMilestone, 'milestone')
1761
+ : null;
1762
+ const currentMilestoneMajor = currentMilestoneRaw ? extractMajorVersion(currentMilestoneRaw) : null;
1597
1763
  const bodyReqIds = [];
1598
1764
  // deferredDepth: the heading level that opened the current deferred block,
1599
1765
  // or 0 when we are in an active section.
@@ -1617,11 +1783,21 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1617
1783
  }
1618
1784
  // Heading at same level or shallower than current deferred opener,
1619
1785
  // or no active deferred block yet.
1620
- if (DEFERRED_HEADING_RE.test(text)) {
1786
+ if (DEFERRED_KEYWORD_RE.test(text)) {
1621
1787
  deferredDepth = depth; // enter a deferred block
1622
1788
  }
1623
1789
  else {
1624
- deferredDepth = 0; // back in an active section
1790
+ const versionMatch = text.match(HEADING_VERSION_RE);
1791
+ if (versionMatch) {
1792
+ const headingMajor = versionMatch[1];
1793
+ deferredDepth =
1794
+ currentMilestoneMajor === null || headingMajor !== currentMilestoneMajor
1795
+ ? depth // unresolved milestone (fail-safe) or off-milestone version -> deferred
1796
+ : 0; // same major version as the current milestone -> active
1797
+ }
1798
+ else {
1799
+ deferredDepth = 0; // back in an active section
1800
+ }
1625
1801
  }
1626
1802
  continue;
1627
1803
  }
@@ -1654,8 +1830,68 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1654
1830
  if (unregistered.length > 0) {
1655
1831
  warnings.push(`REQUIREMENTS.md: ${unregistered.length} REQ-ID(s) found in body but missing from Traceability table: ${unregistered.join(', ')} — add them manually to keep traceability in sync`);
1656
1832
  }
1833
+ // #2316-1: ghost REQ-IDs — cited by ROADMAP's own **Requirements:**
1834
+ // line for this phase, but registered NOWHERE in REQUIREMENTS.md
1835
+ // (neither its body nor its Traceability table). The `unregistered`
1836
+ // check above only ever compares REQUIREMENTS.md's own body against
1837
+ // its own Traceability table; it never consults `citedReqIds`, so an
1838
+ // ID that ROADMAP cites but REQUIREMENTS.md never defines at all was
1839
+ // previously invisible to every guard. `TBD` (the phase.add/-batch/
1840
+ // -insert placeholder) is excluded — see #2316-7 boundary.
1841
+ //
1842
+ // #2334 HIGH 2: classify "ghost" by PROBING THE ACTUAL WRITE
1843
+ // SURFACES this same function just wrote to (:1947 checkbox,
1844
+ // :1967 Traceability row) — case-insensitively — mirroring
1845
+ // milestone.cts's `notFound`/`hasRow`/`doneCheckbox` classification
1846
+ // (src/milestone.cts:117-141,209-215), instead of set-differencing
1847
+ // `bodyReqIds` (deferred-filtered, case-sensitive, bold-only) and
1848
+ // `tableReqIds` (case-sensitive) against `citedReqIds`. Those two
1849
+ // indexes can disagree with the writes: an ID under a `##
1850
+ // Deferred` heading gets its checkbox ticked by the write loop
1851
+ // above but is deliberately EXCLUDED from `bodyReqIds` by the
1852
+ // deferred-heading filter (#1159), so the old set-diff reported it
1853
+ // as an unregistered ghost in the SAME response that just ticked
1854
+ // its checkbox; a case-mismatched citation (`known-01` vs
1855
+ // `**KNOWN-01**`) lands its write via the writes' case-insensitive
1856
+ // regexes but failed the old set-diff's case-SENSITIVE
1857
+ // `Array.includes`/`Set.has`. An ID whose checkbox OR Traceability
1858
+ // row actually matched is registered — not a ghost — regardless of
1859
+ // which section (deferred or not) it lives under.
1860
+ const reqIsRegisteredAnywhere = (id) => {
1861
+ const reqEscaped = escapeRegex(id);
1862
+ // Surface 1 — checkbox, EITHER state (`[ ]` or `[x]`), case-
1863
+ // insensitive: existence check, not the write's space-only match.
1864
+ if (new RegExp(`-\\s*\\[[ xX]\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'i').test(reqContent)) {
1865
+ return true;
1866
+ }
1867
+ // Surface 2 — Traceability row exists at all (any Status value),
1868
+ // via the SAME no-op-probe-through-updateTraceabilityCell
1869
+ // technique milestone.cts's `hasRow` uses (:210-214): a case-
1870
+ // insensitive first-cell match, regardless of current Status.
1871
+ const rowProbeMatch = (row) => (Object.values(row)[0] ?? '').trim().toLowerCase() === id.toLowerCase();
1872
+ return updateTraceabilityCell(reqContent, rowProbeMatch, 'Status', (current) => current).ok;
1873
+ };
1874
+ const ghostReqIds = citedReqIds.filter((id) => !isPlaceholderReqId(id) && !reqIsRegisteredAnywhere(id));
1875
+ if (ghostReqIds.length > 0) {
1876
+ warnings.push(`ROADMAP Phase ${phaseNum} cites REQ-ID(s) not registered anywhere in REQUIREMENTS.md (neither body nor Traceability table): ${ghostReqIds.join(', ')} — add them to REQUIREMENTS.md or correct the ROADMAP citation`);
1877
+ }
1878
+ // #2316-1 cont.: a cited ID whose Traceability-row write matched no
1879
+ // row for a reason OTHER than being a ghost (e.g. a malformed table)
1880
+ // still deserves a warning instead of a silent discard — but skip
1881
+ // IDs already reported above as ghosts to avoid a duplicate message
1882
+ // for the same root cause.
1883
+ const traceabilityWriteFailures = traceabilityWriteMisses.filter((id) => !ghostReqIds.includes(id));
1884
+ if (traceabilityWriteFailures.length > 0) {
1885
+ warnings.push(`REQUIREMENTS.md: Traceability row write skipped for REQ-ID(s) cited by ROADMAP (no matching row found): ${traceabilityWriteFailures.join(', ')}`);
1886
+ }
1657
1887
  writes.push({ filePath: reqPath, before: originalReqContent, after: reqContent });
1658
- requirementsUpdated = true;
1888
+ // #2316-3: `requirements_updated` must reflect whether REQUIREMENTS.md
1889
+ // content actually CHANGED, not merely that the file existed in the
1890
+ // transaction — mirrors the `writes.push({filePath,before,after})`
1891
+ // diff-tracking pattern used for the ROADMAP write above. A phase
1892
+ // whose citations match nothing (ghost REQ-IDs only) must report
1893
+ // `false`, not a bare "the file was present" `true`.
1894
+ requirementsUpdated = reqContent !== originalReqContent;
1659
1895
  }
1660
1896
  }
1661
1897
  try {
@@ -1802,10 +2038,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1802
2038
  clock: clock_cjs_1.realClock,
1803
2039
  progressProvider: () => null, // completePhase derives progress from the roadmap, not disk
1804
2040
  roadmapProvider: () => roadmapContent,
2041
+ sourcePath: statePath,
1805
2042
  });
1806
2043
  stateContent = completeResult.content;
1807
2044
  stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
1808
- stateContent = syncStateFrontmatter(stateContent, cwd);
2045
+ // #2736: the transition holds the next phase's exact display name in
2046
+ // the intent; pass it as authoritative so the sync's prose
2047
+ // re-derivation cannot rewrite current_phase_name to the name's own
2048
+ // parenthetical (`Closer-ruling measurement (D1a)` → `D1a`).
2049
+ stateContent = syncStateFrontmatter(stateContent, cwd, nextPhaseDisplayName ? { current_phase_name: nextPhaseDisplayName } : undefined);
1809
2050
  writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
1810
2051
  }
1811
2052
  writePlanningFileSet(writes);
@@ -3,7 +3,7 @@
3
3
  * ADR-22 Drift-Guard Decision Module
4
4
  *
5
5
  * Implements the authority ladder and severity classification table from
6
- * ADR-22 (docs/adr/0022-source-grounding-drift-guard.md).
6
+ * ADR-22 (docs/adr/22-plan-drift-guard.md).
7
7
  *
8
8
  * Design constraints:
9
9
  * - Pure module: no I/O, no require() calls, no side effects.
@@ -12,10 +12,62 @@ const node_path_1 = require("node:path");
12
12
  // eslint-disable-next-line @typescript-eslint/no-require-imports
13
13
  const coreUtils = require("./core-utils.cjs");
14
14
  const { countMatchedSummaries } = coreUtils;
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const frontmatterMod = require("./frontmatter.cjs");
17
+ const { extractFrontmatter } = frontmatterMod;
15
18
  // Excluded derivative files
16
19
  const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
17
20
  const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
18
21
  const PLAN_REVIEW_RE = /-PLAN-REVIEW\.md$/i;
22
+ // #2349: a plan's frontmatter always sits at byte 0 and closes well before the
23
+ // body, so only a bounded prefix is ever needed to read the `status` marker.
24
+ // Capping the read keeps scanPhasePlans — which loops over every phase directory
25
+ // on hot paths (state sync/validate, roadmap progress) — from slurping a
26
+ // pathologically large committed plan file into memory just to inspect one key.
27
+ const PLAN_FRONTMATTER_READ_CAP = 64 * 1024;
28
+ /**
29
+ * #2349: a plan whose frontmatter declares `status: superseded` was deliberately
30
+ * reassigned or never executed — its work moved to a later plan, so it can never
31
+ * gain a matching `*-SUMMARY.md`. Like a retired phase (#1514, one level up), such
32
+ * a plan must be excluded from BOTH the plan and summary counts; otherwise a phase
33
+ * with a deliberately-unexecuted plan reads `completed: false` forever, pinning the
34
+ * milestone below 100%. Reading only the frontmatter `status` key is the same seam
35
+ * verify.cts / phase.cts already use for plan metadata; a plan without the marker is
36
+ * counted exactly as before.
37
+ *
38
+ * This is the only path in scanPhasePlans that opens file *contents* (the rest is
39
+ * filename matching), so it is hardened accordingly: `statSync().isFile()` rejects
40
+ * anything that is not a regular file — a directory, socket, or a symlink resolving
41
+ * to a device such as `/dev/zero` (a git-committable DoS vector; cf. #2378/#2383) —
42
+ * BEFORE any open, and the read is bounded to a fixed prefix. Fail-safe throughout:
43
+ * a non-regular or unreadable plan is treated as a normal (counted) plan, never
44
+ * silently dropped.
45
+ */
46
+ function isPlanSuperseded(planFullPath) {
47
+ let content;
48
+ try {
49
+ const st = (0, node_fs_1.statSync)(planFullPath); // follows symlinks → resolves to the target's real type
50
+ if (!st.isFile())
51
+ return false;
52
+ const length = Math.min(st.size, PLAN_FRONTMATTER_READ_CAP);
53
+ if (length === 0)
54
+ return false;
55
+ const fd = (0, node_fs_1.openSync)(planFullPath, 'r');
56
+ try {
57
+ const buf = Buffer.allocUnsafe(length);
58
+ const bytesRead = (0, node_fs_1.readSync)(fd, buf, 0, length, 0);
59
+ content = buf.toString('utf8', 0, bytesRead);
60
+ }
61
+ finally {
62
+ (0, node_fs_1.closeSync)(fd);
63
+ }
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ const status = extractFrontmatter(content, planFullPath)['status'];
69
+ return typeof status === 'string' && status.trim().toLowerCase() === 'superseded';
70
+ }
19
71
  function isRootPlanFile(fileName) {
20
72
  if (PLAN_OUTLINE_RE.test(fileName))
21
73
  return false;
@@ -75,7 +127,16 @@ function scanPhasePlans(phaseDir) {
75
127
  }
76
128
  catch { /* ignore unreadable nested layout */ }
77
129
  }
78
- const planFiles = rootPlanFiles.concat(nestedPlanFiles);
130
+ const allPlanFiles = rootPlanFiles.concat(nestedPlanFiles);
131
+ // #2349: drop plans explicitly marked `status: superseded` from the plan set
132
+ // BEFORE counting, so they inflate neither the denominator (planCount) nor,
133
+ // via countMatchedSummaries below, the numerator (summaryCount). Plans without
134
+ // the marker are untouched, so behaviour is byte-for-behaviour identical for
135
+ // every existing phase — only a phase carrying the new marker changes.
136
+ const supersededPlanFiles = allPlanFiles.filter((f) => isPlanSuperseded((0, node_path_1.join)(phaseDir, f)));
137
+ const planFiles = supersededPlanFiles.length === 0
138
+ ? allPlanFiles
139
+ : allPlanFiles.filter((f) => !supersededPlanFiles.includes(f));
79
140
  const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
80
141
  const planCount = planFiles.length;
81
142
  // Count only summaries that are the PLAN→SUMMARY partner of an existing plan
@@ -87,7 +148,14 @@ function scanPhasePlans(phaseDir) {
87
148
  return {
88
149
  planCount,
89
150
  summaryCount,
90
- completed: planCount > 0 && summaryCount >= planCount,
151
+ // #2349: gate completion on whether the phase had ANY plans on disk
152
+ // (allPlanFiles), NOT on the post-exclusion planCount. A phase whose plans
153
+ // were ALL marked superseded has planCount 0, but it is NOT an unplanned
154
+ // empty phase — there is simply no remaining work, so it must read complete
155
+ // (0 >= 0) rather than being pinned below 100% forever, which is the very
156
+ // failure this fix removes. A genuinely empty phase (no plans authored)
157
+ // still has allPlanFiles.length 0 and stays not-completed, exactly as before.
158
+ completed: allPlanFiles.length > 0 && summaryCount >= planCount,
91
159
  hasNestedPlans,
92
160
  planFiles,
93
161
  summaryFiles,
@@ -369,8 +369,15 @@ function findContextMdIn(absDirOrFiles) {
369
369
  return 'CONTEXT.md';
370
370
  return files.find((f) => f.endsWith('-CONTEXT.md')) ?? null;
371
371
  }
372
- catch {
373
- return null;
372
+ catch (err) {
373
+ // #1883: distinguish genuine absence from a permission/I-O failure. ENOENT
374
+ // ("nothing there") keeps the long-standing null contract the callers rely
375
+ // on; every other error (EACCES, EIO, …) is a real read failure that must
376
+ // propagate — otherwise an unreadable phase dir is silently reported as
377
+ // "no CONTEXT.md" and the discuss/plan gates wrongly skip context.
378
+ if (err.code === 'ENOENT')
379
+ return null;
380
+ throw err;
374
381
  }
375
382
  }
376
383
  module.exports = {
@@ -920,24 +920,50 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
920
920
  '<!-- GSD:profile-end -->',
921
921
  ];
922
922
  const sectionContent = sectionLines.join('\n');
923
+ // #2565: resolve target through effective runtime policy instead of
924
+ // hardcoded .claude/CLAUDE.md. Mirrors the #3163 fix applied to
925
+ // cmdGenerateClaudeMd above; that fix diverged when it didn't propagate
926
+ // here, leaving /gsd-profile-user writing Claude files on Codex installs.
927
+ // - Project scope: getProjectInstructionFile(runtime) is the single source
928
+ // of truth (AGENTS.md for codex/opencode/kilo/kimi/unknown; GEMINI.md for
929
+ // antigravity; .github/copilot-instructions.md for copilot).
930
+ // - Global scope: ~/.<config-home>/<instruction-basename>, derived from
931
+ // getGlobalConfigDir + basename(getProjectInstructionFile), so codex lands
932
+ // at ~/.codex/AGENTS.md. Claude global is preserved byte-for-byte (no
933
+ // env-var drift beyond the prior hardcoded path).
934
+ // - GSD_RUNTIME env var takes precedence over config.runtime (mirrors the
935
+ // #3163 env-precedence contract). Non-claude always wins over a stale
936
+ // claude_md_path (#3163 rationale: an AGENTS-native project must never
937
+ // write to CLAUDE.md even if a prior Claude setup left claude_md_path).
938
+ let config = {};
939
+ try {
940
+ config = loadConfig(cwd);
941
+ }
942
+ catch { /* use defaults */ }
943
+ const effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime']);
944
+ const isClaudeRuntime = !effectiveRuntime || effectiveRuntime === 'claude';
923
945
  let targetPath;
924
946
  if (options.global) {
925
- targetPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'CLAUDE.md');
947
+ if (isClaudeRuntime) {
948
+ targetPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'CLAUDE.md');
949
+ }
950
+ else {
951
+ targetPath = node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)(effectiveRuntime), node_path_1.default.basename((0, runtime_name_policy_cjs_1.getProjectInstructionFile)(effectiveRuntime)));
952
+ }
926
953
  }
927
954
  else if (options.output) {
928
955
  targetPath = node_path_1.default.isAbsolute(options.output) ? options.output : node_path_1.default.join(cwd, options.output);
929
956
  }
930
957
  else {
931
- // Read claude_md_path from config; #1098 default is ./.claude/CLAUDE.md
958
+ // Read claude_md_path from config; #1098 default is .claude/CLAUDE.md
932
959
  // (kept consistent with cmdGenerateClaudeMd so the profile section and the
933
960
  // managed sections land in the same file on a config-less project).
934
- let configClaudeMdPath = './.claude/CLAUDE.md';
935
- try {
936
- const config = loadConfig(cwd);
937
- if (config['claude_md_path'])
938
- configClaudeMdPath = config['claude_md_path'];
961
+ let configClaudeMdPath = '.claude/CLAUDE.md';
962
+ if (config['claude_md_path'])
963
+ configClaudeMdPath = config['claude_md_path'];
964
+ if (!isClaudeRuntime) {
965
+ configClaudeMdPath = (0, runtime_name_policy_cjs_1.getProjectInstructionFile)(effectiveRuntime);
939
966
  }
940
- catch { /* use default */ }
941
967
  targetPath = node_path_1.default.isAbsolute(configClaudeMdPath) ? configClaudeMdPath : node_path_1.default.join(cwd, configClaudeMdPath);
942
968
  }
943
969
  let action;