@opengsd/gsd-core 1.9.0 → 1.10.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 (223) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +131 -34
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +503 -341
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +607 -63
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +106 -51
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -25
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -27,6 +27,9 @@
27
27
  * the additive, importable server surface a host (or the bin shim) drives.
28
28
  */
29
29
  'use strict';
30
+ var __importDefault = (this && this.__importDefault) || function (mod) {
31
+ return (mod && mod.__esModule) ? mod : { "default": mod };
32
+ };
30
33
  Object.defineProperty(exports, "__esModule", { value: true });
31
34
  exports.SERVER_NAME = exports.PROTOCOL_VERSION = void 0;
32
35
  exports.handleMessage = handleMessage;
@@ -36,9 +39,62 @@ const stateIo = require("./state-io.cjs");
36
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
40
  const shellCommandProjection = require("./shell-command-projection.cjs");
38
41
  const { dispatchGsdCommand } = shellCommandProjection;
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const node_path_1 = __importDefault(require("node:path"));
44
+ const mcp_catalog_cjs_1 = require("./mcp-catalog.cjs");
39
45
  exports.PROTOCOL_VERSION = '2024-11-05';
40
46
  exports.SERVER_NAME = 'gsd-core';
41
- const SERVER_VERSION = '1.7.0';
47
+ // #3072: resolve SERVER_VERSION from package.json (source tree) or the
48
+ // installed gsd-core/VERSION marker, WITHOUT a top-level `require(...)` —
49
+ // mirrors the established, already-precedented resolver in
50
+ // `runtime-artifact-conversion.cts`'s `gsdVersion()` (#1383): a top-level
51
+ // require would throw on any runtime whose root has no package.json/VERSION,
52
+ // and `scripts/sync-manifest-versions.cjs` is not a fit here — its
53
+ // `VERSIONED_MANIFESTS` list stamps a `version` FIELD into hand-authored JSON
54
+ // manifests (plugin.json, marketplace.json, vscode/package.json); a `.cts`
55
+ // source constant is not a JSON document that script can round-trip through
56
+ // `readJson`/`setByPath`, and teaching it to text-edit TypeScript source
57
+ // would be a much larger footprint than this lazy, cached, defensive read.
58
+ // Resolved once per process (never per-request) and cached; a failed lookup
59
+ // degrades to '0.0.0' (never `undefined`, never a crash) rather than making
60
+ // `initialize`'s `serverInfo.version` field absent or malformed.
61
+ const SEMVER_PREFIX = /^\d+\.\d+\.\d+/;
62
+ let cachedServerVersion;
63
+ function resolveServerVersion() {
64
+ if (cachedServerVersion !== undefined)
65
+ return cachedServerVersion;
66
+ let version = '0.0.0';
67
+ try {
68
+ const v = node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, '..', '..', 'VERSION'), 'utf8').trim();
69
+ if (SEMVER_PREFIX.test(v))
70
+ version = v;
71
+ }
72
+ catch {
73
+ /* not an installed tree (no gsd-core/VERSION) */
74
+ }
75
+ if (version === '0.0.0') {
76
+ try {
77
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy, defensive: a top-level require would throw on a runtime root with no package.json.
78
+ const pkg = require(node_path_1.default.join(__dirname, '..', '..', '..', 'package.json'));
79
+ if (pkg && typeof pkg.version === 'string' && SEMVER_PREFIX.test(pkg.version))
80
+ version = pkg.version;
81
+ }
82
+ catch {
83
+ /* runtime root has no package.json */
84
+ }
85
+ }
86
+ cachedServerVersion = version;
87
+ return version;
88
+ }
89
+ // Catalog is built once per process, lazily, and cached (design "Shape" /
90
+ // Gall's Law — no watching, no invalidation; Known limits: the package is
91
+ // immutable in every install mode we ship).
92
+ let cachedCatalog;
93
+ function getCatalog() {
94
+ if (cachedCatalog === undefined)
95
+ cachedCatalog = (0, mcp_catalog_cjs_1.buildCatalog)();
96
+ return cachedCatalog;
97
+ }
42
98
  // JSON-RPC 2.0 error codes.
43
99
  const PARSE_ERROR = -32700;
44
100
  const INVALID_REQUEST = -32600;
@@ -93,6 +149,32 @@ function okResponse(id, result) {
93
149
  function asString(v) {
94
150
  return typeof v === 'string' ? v : null;
95
151
  }
152
+ /** Extract the {@link REASON} carried by a `CatalogError`, if any (see `mcp-catalog.cts`). */
153
+ function catalogErrorReason(err) {
154
+ const reason = err && typeof err === 'object' ? err.reason : undefined;
155
+ return typeof reason === 'string' ? reason : undefined;
156
+ }
157
+ function catalogErrorMessage(err) {
158
+ return err instanceof Error ? err.message : String(err);
159
+ }
160
+ /**
161
+ * Map a `mcp-catalog.cts` `CatalogError.reason` to a JSON-RPC error code.
162
+ * `READ_FAILED` is a server-side IO/compose failure (`INTERNAL_ERROR`);
163
+ * every other reason (unknown uri/prompt/cursor/root, malformed/traversal
164
+ * uri, wrong-typed param) is a client-supplied-value problem (`INVALID_PARAMS`)
165
+ * — deliberately never `METHOD_NOT_FOUND`, so a refusal is never
166
+ * indistinguishable from the method simply not existing (test-matrix rows
167
+ * 33/35).
168
+ */
169
+ function catalogErrorCode(err) {
170
+ return catalogErrorReason(err) === mcp_catalog_cjs_1.REASON.READ_FAILED ? INTERNAL_ERROR : INVALID_PARAMS;
171
+ }
172
+ function wireResource(entry) {
173
+ return { uri: entry.uri, name: entry.name, title: entry.title, description: entry.description, mimeType: entry.mimeType };
174
+ }
175
+ function wirePrompt(entry) {
176
+ return { name: entry.name, title: entry.title, description: entry.description };
177
+ }
96
178
  function callTool(name, args, ctx) {
97
179
  const a = (args && typeof args === 'object' ? args : {});
98
180
  const cwd = asString(ctx.cwd) || process.cwd();
@@ -148,8 +230,12 @@ function handleMessage(request, ctx = {}) {
148
230
  case 'initialize':
149
231
  result = {
150
232
  protocolVersion: exports.PROTOCOL_VERSION,
151
- capabilities: { tools: {} },
152
- serverInfo: { name: exports.SERVER_NAME, version: SERVER_VERSION },
233
+ // #3072: resources/prompts declared alongside tools. Deliberately NO
234
+ // subscribe/listChanged — nothing ever sends those notifications
235
+ // (design row 2 / Hyrum's Law: advertising an unimplemented
236
+ // notification is a lie a host would act on).
237
+ capabilities: { tools: {}, resources: {}, prompts: {} },
238
+ serverInfo: { name: exports.SERVER_NAME, version: resolveServerVersion() },
153
239
  };
154
240
  break;
155
241
  case 'tools/list':
@@ -163,6 +249,52 @@ function handleMessage(request, ctx = {}) {
163
249
  result = callTool(toolName, params.arguments, ctx);
164
250
  break;
165
251
  }
252
+ case 'resources/list': {
253
+ const params = (request.params && typeof request.params === 'object' ? request.params : {});
254
+ try {
255
+ const cursor = typeof params.cursor === 'string' ? params.cursor : undefined;
256
+ const pageSize = typeof params.pageSize === 'number' ? params.pageSize : undefined;
257
+ const page = (0, mcp_catalog_cjs_1.listResources)(getCatalog(), { cursor, pageSize });
258
+ result = page.nextCursor === undefined
259
+ ? { resources: page.resources.map(wireResource) }
260
+ : { resources: page.resources.map(wireResource), nextCursor: page.nextCursor };
261
+ }
262
+ catch (e) {
263
+ return errorResponse(id, catalogErrorCode(e), catalogErrorMessage(e));
264
+ }
265
+ break;
266
+ }
267
+ case 'resources/read': {
268
+ const params = (request.params && typeof request.params === 'object' ? request.params : {});
269
+ try {
270
+ const read = (0, mcp_catalog_cjs_1.readResource)(getCatalog(), params.uri);
271
+ result = { contents: [{ uri: read.uri, mimeType: read.mimeType, text: read.text }] };
272
+ }
273
+ catch (e) {
274
+ return errorResponse(id, catalogErrorCode(e), catalogErrorMessage(e));
275
+ }
276
+ break;
277
+ }
278
+ case 'prompts/list':
279
+ result = {
280
+ prompts: [...getCatalog().prompts.values()]
281
+ .map(wirePrompt)
282
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
283
+ };
284
+ break;
285
+ case 'prompts/get': {
286
+ const params = (request.params && typeof request.params === 'object' ? request.params : {});
287
+ const name = asString(params.name);
288
+ if (!name)
289
+ return errorResponse(id, INVALID_PARAMS, 'prompts/get requires string "name".');
290
+ try {
291
+ result = (0, mcp_catalog_cjs_1.getPrompt)(getCatalog(), name, params.arguments);
292
+ }
293
+ catch (e) {
294
+ return errorResponse(id, catalogErrorCode(e), catalogErrorMessage(e));
295
+ }
296
+ break;
297
+ }
166
298
  default:
167
299
  if (isNotification)
168
300
  return null;
@@ -135,10 +135,15 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
135
135
  // Surface 1 — the checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
136
136
  // Use replace() + compare to avoid the test()+replace() global regex
137
137
  // lastIndex bug where test() advances state and replace() misses matches.
138
+ // (#2788 defect 2: the flip is CONDITIONAL — when a traceability row EXISTS
139
+ // for this ID but its Status write is rejected, the checkbox must NOT flip,
140
+ // so the two surfaces cannot silently diverge. The row-write outcome below
141
+ // gates whether the flip is kept.)
138
142
  const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
143
+ const beforeCheckbox = reqContent;
139
144
  const afterCheckbox = reqContent.replace(checkboxPattern, '$1x$2');
140
- const checkboxHit = afterCheckbox !== reqContent;
141
- if (checkboxHit)
145
+ const checkboxFlipped = afterCheckbox !== beforeCheckbox;
146
+ if (checkboxFlipped)
142
147
  reqContent = afterCheckbox;
143
148
  // Surface 2 — the traceability row: | <REQ-ID> | Phase N | Pending | → ... Complete |
144
149
  // via the markdown-table seam (ADR-2143 §7) — supersedes the prior ordinal
@@ -156,7 +161,11 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
156
161
  // updateTableCell call both probes the current value and writes.
157
162
  let tableHit = false;
158
163
  const tableUpdate = updateTraceabilityCell(reqContent, rowMatch, 'Status', (current) => {
159
- if (/^pending$/i.test(current.trim())) {
164
+ // #2788: accept `Gaps Found` as a forward input too — `revert-phase` (the
165
+ // documented gaps_found response) leaves a row stranded at Gaps Found with
166
+ // no inverse; a genuinely-satisfied requirement must be able to reach
167
+ // Complete again via mark-complete, or the milestone is blocked forever.
168
+ if (/^(pending|gaps found)$/i.test(current.trim())) {
160
169
  tableHit = true;
161
170
  return ' Complete ';
162
171
  }
@@ -165,6 +174,17 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
165
174
  if (tableUpdate.ok) {
166
175
  reqContent = tableUpdate.value;
167
176
  }
177
+ // #2788 defect 2: if a row EXISTS for this ID but its Status write was
178
+ // rejected (e.g. the row reads `Blocked`, which mark-complete does not
179
+ // accept), roll the checkbox back so the checkbox and the row cannot
180
+ // silently diverge. The checkbox and the row are two representations of the
181
+ // same fact; flipping one while the other rejects the write is the lie.
182
+ let checkboxHit = checkboxFlipped;
183
+ const rowExistsProbe = tableUpdate; // ok === a row matched (probes existence)
184
+ if (checkboxFlipped && rowExistsProbe.ok && !tableHit) {
185
+ reqContent = beforeCheckbox;
186
+ checkboxHit = false;
187
+ }
168
188
  // ADR-2143 §6 per-ID write-set entries: this ID's checkbox surface is
169
189
  // always tracked; the traceability surface is tracked only when the file
170
190
  // has a traceability table at all (same `hasTable` gate the existing
@@ -190,7 +210,14 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
190
210
  const hasRow = statusProbe.ok;
191
211
  const doneCheckbox = new RegExp(`-\\s*\\[x\\]\\s*\\*\\*${reqEscaped}\\*\\*`, 'i').test(reqContent);
192
212
  const doneTable = Boolean(hasRow && /^complete$/i.test(currentStatusCell.trim()));
193
- if (checkboxHit || tableHit) {
213
+ // #2788 defect 2: when a traceability table exists AND this ID has a row in
214
+ // it, `updated`/`marked_complete` must reflect the ROW moving, not a
215
+ // checkbox-only flip. Otherwise (`table_unmatched` — no row for this ID, or
216
+ // no table at all) the checkbox flip is a legitimate partial reconcile / the
217
+ // sole completion surface, so the #2140 OR semantics are preserved.
218
+ const rowExists = hasTable && hasRow;
219
+ const idUpdated = rowExists ? tableHit : (checkboxHit || tableHit);
220
+ if (idUpdated) {
194
221
  updated.push(reqId);
195
222
  }
196
223
  else if (doneTable || (doneCheckbox && !hasTable)) {
@@ -437,13 +464,25 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
437
464
  // Guard: prevent marking complete when ROADMAP still lists phases that have
438
465
  // no directory on disk (disk_status: no_directory). This catches the case
439
466
  // where the active milestone was erroneously marked complete before phases
440
- // were even started. Only fires when STATE.md confirms the current milestone
441
- // version matches what is being completed — no false positives on fresh
442
- // projects where phases haven't been scaffolded yet.
467
+ // were even started. The scan scopes the ROADMAP via the `version` argument
468
+ // (getMilestonePhaseFilter / extractCurrentMilestone above) and runs whenever
469
+ // --force is absent — a fresh project with no `### Phase N:` headings in the
470
+ // scoped slice yields an empty `noDirectoryPhases` and the guard is a no-op,
471
+ // so no STATE match is required to avoid false positives.
443
472
  // Pass --force to override this guard.
473
+ //
474
+ // #2946: the scan used to be nested inside `if (stateVersion && stateVersion
475
+ // === version)`, which silently disarmed the guard whenever STATE.md's
476
+ // `milestone:` field was desynced or absent — functionally an implicit
477
+ // --force on a one-way-door operation (ROADMAP/REQUIREMENTS archived, phase
478
+ // directories MOVED). The STATE field is not the source of truth for which
479
+ // phases belong to this milestone; the ROADMAP scoping is. The scan now runs
480
+ // unconditionally, and a present-but-mismatched STATE field emits a WARNING
481
+ // so the suspicious condition is visible rather than silent.
444
482
  if (!options.force) {
445
483
  try {
446
- // Only guard when STATE.md's milestone field matches the version being completed.
484
+ // Read STATE.md's milestone field only to detect a suspicious mismatch;
485
+ // it no longer gates the scan. (#2946)
447
486
  let stateVersion = null;
448
487
  try {
449
488
  const stateRaw = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
@@ -454,50 +493,66 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
454
493
  }
455
494
  }
456
495
  catch {
457
- /* skip */
496
+ /* skip — stateVersion stays null, scan still runs */
458
497
  }
459
- if (stateVersion && stateVersion === version) {
460
- const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
461
- const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
462
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
463
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
464
- const noDirectoryPhases = [];
465
- let pm;
466
- const phaseDirEntries = (() => {
467
- try {
468
- return node_fs_1.default
469
- .readdirSync(phasesDir, { withFileTypes: true })
470
- .filter((e) => e.isDirectory())
471
- .map((e) => e.name);
472
- }
473
- catch {
474
- return [];
475
- }
476
- })();
477
- while ((pm = phasePattern.exec(scopedContent)) !== null) {
478
- const phaseNum = pm[1];
479
- // Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
480
- // real phases — they legitimately have no directory and must not block
481
- // milestone completion. Mirrors the engine-wide sentinel convention
482
- // (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
483
- // the #1445 /^999/ progress filters). (#1580)
484
- const major = parseInt(phaseNum, 10);
485
- if (major === 0 || major === 999)
486
- continue;
487
- const normalized = normalizePhaseName(phaseNum);
488
- // A phase has disk_status: 'no_directory' when no phase directory
489
- // with a matching token exists on disk. Use the same phaseTokenMatches
490
- // helper that roadmap.analyze uses to avoid false positives on decimal
491
- // (2.1) and letter-suffix (12A) phase IDs.
492
- const hasDirectory = phaseDirEntries.some((d) => phaseTokenMatches(d, normalized));
493
- if (!hasDirectory) {
494
- noDirectoryPhases.push(phaseNum);
495
- }
498
+ if (stateVersion !== null && stateVersion !== version) {
499
+ // #2946: emit a WARNING so the suspicious STATE mismatch is visible
500
+ // rather than silently disarming the guard. Plain-text diagnostic on
501
+ // stderr, matching the existing [gsd-tools] WARNING convention
502
+ // (state.cts). A missing STATE.md `milestone:` field is not warned
503
+ // here — the scan still runs, and "no milestone declared" is a normal
504
+ // state for a fresh project, not a suspicious drift.
505
+ //
506
+ // `stateVersion` comes from a user-controlled file (STATE.md) and is
507
+ // not validated like the CLI `version` arg (ARCHIVE_VERSION_LABEL_RE).
508
+ // Sanitize before interpolating into stderr so ANSI escapes / control
509
+ // chars / secret-looking strings cannot be echoed verbatim into a CI
510
+ // log or terminal (CONTRIBUTING.md security: secret-looking values in
511
+ // stderr). `version` is already constrained to [A-Za-z0-9._-].
512
+ const safeStateVersion = stateVersion.replace(/[\x00-\x1f\x7f]/g, '?').slice(0, 80);
513
+ process.stderr.write(`[gsd-tools] WARNING: STATE.md milestone: "${safeStateVersion}" ≠ requested "${version}" — ` +
514
+ `running the unstarted-phase guard against the ROADMAP scoped for "${version}" anyway.\n`);
515
+ }
516
+ const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
517
+ const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
518
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
519
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
520
+ const noDirectoryPhases = [];
521
+ let pm;
522
+ const phaseDirEntries = (() => {
523
+ try {
524
+ return node_fs_1.default
525
+ .readdirSync(phasesDir, { withFileTypes: true })
526
+ .filter((e) => e.isDirectory())
527
+ .map((e) => e.name);
496
528
  }
497
- if (noDirectoryPhases.length > 0) {
498
- error(`Cannot mark milestone complete: ROADMAP lists ${noDirectoryPhases.length} unstarted phase(s) ` +
499
- `(e.g. Phase ${noDirectoryPhases[0]}). Re-run with --force to override.`);
529
+ catch {
530
+ return [];
500
531
  }
532
+ })();
533
+ while ((pm = phasePattern.exec(scopedContent)) !== null) {
534
+ const phaseNum = pm[1];
535
+ // Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
536
+ // real phases — they legitimately have no directory and must not block
537
+ // milestone completion. Mirrors the engine-wide sentinel convention
538
+ // (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
539
+ // the #1445 /^999/ progress filters). (#1580)
540
+ const major = parseInt(phaseNum, 10);
541
+ if (major === 0 || major === 999)
542
+ continue;
543
+ const normalized = normalizePhaseName(phaseNum);
544
+ // A phase has disk_status: 'no_directory' when no phase directory
545
+ // with a matching token exists on disk. Use the same phaseTokenMatches
546
+ // helper that roadmap.analyze uses to avoid false positives on decimal
547
+ // (2.1) and letter-suffix (12A) phase IDs.
548
+ const hasDirectory = phaseDirEntries.some((d) => phaseTokenMatches(d, normalized));
549
+ if (!hasDirectory) {
550
+ noDirectoryPhases.push(phaseNum);
551
+ }
552
+ }
553
+ if (noDirectoryPhases.length > 0) {
554
+ error(`Cannot mark milestone complete: ROADMAP lists ${noDirectoryPhases.length} unstarted phase(s) ` +
555
+ `(e.g. Phase ${noDirectoryPhases[0]}). Re-run with --force to override.`);
501
556
  }
502
557
  }
503
558
  catch (e) {
@@ -505,7 +560,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
505
560
  const message = e instanceof Error ? e.message : String(e);
506
561
  if (message && message.startsWith('Cannot mark milestone complete:'))
507
562
  throw e;
508
- // Phase scan failed or STATE version mismatch — allow completion to proceed.
563
+ // Phase scan failed (e.g. ROADMAP unreadable) — allow completion to proceed.
509
564
  }
510
565
  }
511
566
  // Gather stats from phases (scoped to current milestone only)
@@ -668,7 +723,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
668
723
  kind: 'milestoneComplete',
669
724
  version,
670
725
  nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
671
- }, { clock: clock_cjs_1.realClock, progressProvider: () => null, sourcePath: statePath });
726
+ }, { clock: clock_cjs_1.realClock, sourcePath: statePath });
672
727
  writeStateMd(statePath, result.content, cwd);
673
728
  }
674
729
  // Archive phase directories if requested
@@ -520,6 +520,65 @@ function phaseTokenMatches(dirName, normalized) {
520
520
  }
521
521
  return false;
522
522
  }
523
+ // ─── Canonical phase KEY surface (#2562) ─────────────────────────────────────
524
+ //
525
+ // A phase "key" is the padding-, case- and project-code-insensitive identity of
526
+ // a phase, for use as a Map/Set key when two independently-derived phase
527
+ // references (a ROADMAP table cell and a phase directory name, say) must be
528
+ // compared. Promoted here from a local pair in state.cts (#2445) so every
529
+ // consumer derives BOTH sides of a comparison from the SAME function — deriving
530
+ // one side with a bespoke regex is the #2562 defect class (a `01` table cell
531
+ // never matching a `1-slug` directory, silently zeroing a rollup).
532
+ /**
533
+ * Canonical key for an already-extracted phase TOKEN (`"5"`, `"05"`, `"005"`,
534
+ * `"12A"`, `"30.1"`, `"PROJ-05"`). Padding- and case-insensitive: every
535
+ * spelling of a number collapses to one key.
536
+ *
537
+ * Leading zeros are stripped per hyphen-separated segment BEFORE
538
+ * `normalizePhaseName` pads to the 2-digit convention. Padding alone is not a
539
+ * normalisation — `padStart(2)` is a no-op once the input is already ≥2
540
+ * characters, so `5` yielded `05` while `005` stayed `005` and the two never
541
+ * compared equal. The strip is deliberately confined to this key surface:
542
+ * `normalizePhaseName` itself is a RENDERING function whose verbatim treatment
543
+ * of wide IDs (`001.10`) is relied on by plan-ID capture and wave assignment.
544
+ * Arithmetic is avoided (`parseInt` would lose precision on a long digit run).
545
+ */
546
+ function phaseKeyFromToken(token) {
547
+ const stripped = String(token)
548
+ .split('-')
549
+ .map(segment => segment.replace(/^0+(?=\d)/, ''))
550
+ .join('-');
551
+ return normalizePhaseName(stripped).toUpperCase();
552
+ }
553
+ /**
554
+ * Canonical key for a phase DIRECTORY name (`"05-schedule-8"` → `"05"`,
555
+ * `"PROJ-5-x"` → `"05"`, `"30.1-follow-up"` → `"30.1"`).
556
+ */
557
+ function phaseKeyFromDir(dirName) {
558
+ return phaseKeyFromToken(extractPhaseToken(dirName));
559
+ }
560
+ /**
561
+ * Canonical key for a phase referenced in PROSE — a ROADMAP `## Progress` table
562
+ * cell (`"30. Schedule 8 rollout"`, `"**05.1 Follow-up**"`) or a STATE.md
563
+ * `Phase:` value. Markdown emphasis is stripped first so a bolded cell is not
564
+ * mistaken for a non-phase. Returns null when the value does not BEGIN with a
565
+ * phase token (`parsePhaseFromProse` anchoring, #2111).
566
+ */
567
+ function phaseKeyFromProse(value) {
568
+ if (value == null)
569
+ return null;
570
+ const { phase } = parsePhaseFromProse(String(value).replace(/[*_`~]/g, ''));
571
+ return phase === null ? null : phaseKeyFromToken(phase);
572
+ }
573
+ /**
574
+ * The PARENT phase key of a sub-phase key (`"30.1"` → `"30"`), or null for a
575
+ * top-level phase. A sub-phase directory inserted mid-milestone frequently has
576
+ * no ROADMAP row of its own and inherits its parent's milestone (#2562).
577
+ */
578
+ function parentPhaseKey(key) {
579
+ const dot = key.indexOf('.');
580
+ return dot === -1 ? null : key.slice(0, dot);
581
+ }
523
582
  // ─── #2121 canonical surface (ADR-2121) ──────────────────────────────────────
524
583
  /**
525
584
  * Parse a phase identifier from a STATE.md `Phase:` prose field VALUE — the text
@@ -662,6 +721,10 @@ module.exports = {
662
721
  comparePhaseNum,
663
722
  extractPhaseToken,
664
723
  phaseTokenMatches,
724
+ phaseKeyFromToken,
725
+ phaseKeyFromDir,
726
+ phaseKeyFromProse,
727
+ parentPhaseKey,
665
728
  parsePhaseFromProse,
666
729
  stripConfiguredProjectCodePrefix,
667
730
  isForeignPrefixedPhaseQuery,