@opengsd/gsd-core 1.9.1 → 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 (219) 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 +27 -3
  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 +453 -289
  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 +579 -66
  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 +75 -47
  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 -28
  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/prompt-budget.cjs +128 -165
  62. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  63. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  64. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  67. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  68. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  69. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  70. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  71. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  72. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  73. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  74. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  75. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  76. package/gsd-core/bin/lib/state.cjs +151 -62
  77. package/gsd-core/bin/lib/surface.cjs +12 -1
  78. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  79. package/gsd-core/bin/lib/uat.cjs +320 -21
  80. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  81. package/gsd-core/bin/lib/verification.cjs +29 -12
  82. package/gsd-core/bin/lib/verify.cjs +10 -2
  83. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  84. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  85. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  86. package/gsd-core/bin/lib/workstream.cjs +6 -0
  87. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  88. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  89. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  90. package/gsd-core/references/artifact-types.md +10 -3
  91. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  92. package/gsd-core/references/debugger-techniques.md +255 -0
  93. package/gsd-core/references/research-documentation-lookup.md +5 -3
  94. package/gsd-core/references/specless-probe-fallback.md +7 -6
  95. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  96. package/gsd-core/references/worktree-branch-check.md +2 -2
  97. package/gsd-core/templates/summary-complex.md +2 -0
  98. package/gsd-core/templates/summary-minimal.md +2 -0
  99. package/gsd-core/templates/summary-standard.md +2 -0
  100. package/gsd-core/templates/summary.md +2 -0
  101. package/gsd-core/workflows/audit-milestone.md +3 -0
  102. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  107. package/gsd-core/workflows/autonomous.md +32 -69
  108. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  109. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  110. package/gsd-core/workflows/code-review.md +42 -160
  111. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  112. package/gsd-core/workflows/complete-milestone.md +23 -81
  113. package/gsd-core/workflows/debug.md +9 -12
  114. package/gsd-core/workflows/diagnose-issues.md +22 -0
  115. package/gsd-core/workflows/discovery-phase.md +4 -4
  116. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  117. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  118. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  119. package/gsd-core/workflows/docs-update.md +8 -51
  120. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  121. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  122. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  124. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  126. package/gsd-core/workflows/execute-phase.md +65 -137
  127. package/gsd-core/workflows/execute-plan.md +1 -1
  128. package/gsd-core/workflows/help/modes/full.md +6 -1
  129. package/gsd-core/workflows/ingest-docs.md +2 -1
  130. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  131. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  132. package/gsd-core/workflows/new-milestone.md +21 -38
  133. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  134. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  135. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  136. package/gsd-core/workflows/new-project.md +13 -226
  137. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  138. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  139. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  140. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  142. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  143. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  144. package/gsd-core/workflows/plan-phase.md +49 -193
  145. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  146. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  147. package/gsd-core/workflows/progress.md +11 -153
  148. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  149. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  150. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  151. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  152. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  153. package/gsd-core/workflows/quick.md +20 -390
  154. package/gsd-core/workflows/resume-project.md +3 -0
  155. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  157. package/gsd-core/workflows/review.md +15 -8
  158. package/gsd-core/workflows/section-manifest.json +219 -0
  159. package/gsd-core/workflows/sketch.md +1 -1
  160. package/gsd-core/workflows/spec-phase.md +17 -14
  161. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  162. package/gsd-core/workflows/spike.md +50 -16
  163. package/gsd-core/workflows/sync-skills.md +49 -11
  164. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  165. package/gsd-core/workflows/transition.md +8 -21
  166. package/gsd-core/workflows/ui-phase.md +8 -7
  167. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  168. package/gsd-core/workflows/update.md +18 -7
  169. package/gsd-core/workflows/verify-phase.md +4 -7
  170. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  171. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  172. package/gsd-core/workflows/verify-work.md +8 -58
  173. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  174. package/hooks/dist/gsd-check-update-worker.js +14 -5
  175. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  176. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  177. package/hooks/dist/gsd-statusline.js +72 -6
  178. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  179. package/hooks/dist/gsd-write-guard.js +359 -0
  180. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  181. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  182. package/hooks/gsd-agent-isolation-guard.js +428 -0
  183. package/hooks/gsd-check-update-worker.js +14 -5
  184. package/hooks/gsd-cursor-subagent-start.js +532 -26
  185. package/hooks/gsd-read-injection-scanner.js +7 -0
  186. package/hooks/gsd-statusline.js +72 -6
  187. package/hooks/gsd-worktree-path-guard.js +2 -1
  188. package/hooks/gsd-write-guard.js +359 -0
  189. package/hooks/hooks.json +12 -0
  190. package/hooks/lib/isolation-sentinel.js +268 -0
  191. package/hooks/managed-hooks-registry.cjs +2 -0
  192. package/package.json +14 -5
  193. package/pi/gsd.cjs +57 -12
  194. package/scripts/build-hooks.js +9 -0
  195. package/scripts/changeset/lint.cjs +9 -2
  196. package/scripts/changeset/serialize.cjs +5 -1
  197. package/scripts/gen-capability-matrix.cjs +1 -1
  198. package/scripts/gen-context-index.cjs +448 -0
  199. package/scripts/gen-inventory-manifest.cjs +101 -1
  200. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  201. package/scripts/gen-section-manifest.cjs +638 -0
  202. package/scripts/generate-package-identity.cjs +4 -2
  203. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  204. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  205. package/scripts/lint-docs-command-form.cjs +195 -0
  206. package/scripts/lint-docs-required.cjs +9 -1
  207. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  208. package/scripts/lint-example-parser-parity.cjs +395 -0
  209. package/scripts/lint-test-file-count.allowlist.json +27 -1
  210. package/scripts/mutation-matrix.cjs +13 -0
  211. package/scripts/prompt-injection-scan.sh +27 -6
  212. package/scripts/run-tests.cjs +3 -2
  213. package/skills/gsd-autonomous/SKILL.md +1 -1
  214. package/skills/gsd-execute-phase/SKILL.md +1 -1
  215. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  216. package/skills/gsd-new-milestone/SKILL.md +1 -1
  217. package/skills/gsd-plan-phase/SKILL.md +2 -2
  218. package/vscode/package.json +1 -1
  219. 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;
@@ -464,13 +464,25 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
464
464
  // Guard: prevent marking complete when ROADMAP still lists phases that have
465
465
  // no directory on disk (disk_status: no_directory). This catches the case
466
466
  // where the active milestone was erroneously marked complete before phases
467
- // were even started. Only fires when STATE.md confirms the current milestone
468
- // version matches what is being completed — no false positives on fresh
469
- // 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.
470
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.
471
482
  if (!options.force) {
472
483
  try {
473
- // 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)
474
486
  let stateVersion = null;
475
487
  try {
476
488
  const stateRaw = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : null;
@@ -481,58 +493,74 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
481
493
  }
482
494
  }
483
495
  catch {
484
- /* skip */
496
+ /* skip — stateVersion stays null, scan still runs */
485
497
  }
486
- if (stateVersion && stateVersion === version) {
487
- const roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
488
- const scopedContent = extractCurrentMilestone(roadmapContent, cwd);
489
- // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
490
- const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
491
- const noDirectoryPhases = [];
492
- let pm;
493
- const phaseDirEntries = (() => {
494
- try {
495
- return node_fs_1.default
496
- .readdirSync(phasesDir, { withFileTypes: true })
497
- .filter((e) => e.isDirectory())
498
- .map((e) => e.name);
499
- }
500
- catch {
501
- return [];
502
- }
503
- })();
504
- while ((pm = phasePattern.exec(scopedContent)) !== null) {
505
- const phaseNum = pm[1];
506
- // Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
507
- // real phases — they legitimately have no directory and must not block
508
- // milestone completion. Mirrors the engine-wide sentinel convention
509
- // (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
510
- // the #1445 /^999/ progress filters). (#1580)
511
- const major = parseInt(phaseNum, 10);
512
- if (major === 0 || major === 999)
513
- continue;
514
- const normalized = normalizePhaseName(phaseNum);
515
- // A phase has disk_status: 'no_directory' when no phase directory
516
- // with a matching token exists on disk. Use the same phaseTokenMatches
517
- // helper that roadmap.analyze uses to avoid false positives on decimal
518
- // (2.1) and letter-suffix (12A) phase IDs.
519
- const hasDirectory = phaseDirEntries.some((d) => phaseTokenMatches(d, normalized));
520
- if (!hasDirectory) {
521
- noDirectoryPhases.push(phaseNum);
522
- }
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);
523
528
  }
524
- if (noDirectoryPhases.length > 0) {
525
- error(`Cannot mark milestone complete: ROADMAP lists ${noDirectoryPhases.length} unstarted phase(s) ` +
526
- `(e.g. Phase ${noDirectoryPhases[0]}). Re-run with --force to override.`);
529
+ catch {
530
+ return [];
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);
527
551
  }
528
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.`);
556
+ }
529
557
  }
530
558
  catch (e) {
531
559
  // If the error came from our guard, re-throw it; otherwise skip silently.
532
560
  const message = e instanceof Error ? e.message : String(e);
533
561
  if (message && message.startsWith('Cannot mark milestone complete:'))
534
562
  throw e;
535
- // Phase scan failed or STATE version mismatch — allow completion to proceed.
563
+ // Phase scan failed (e.g. ROADMAP unreadable) — allow completion to proceed.
536
564
  }
537
565
  }
538
566
  // Gather stats from phases (scoped to current milestone only)
@@ -695,7 +723,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
695
723
  kind: 'milestoneComplete',
696
724
  version,
697
725
  nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
698
- }, { clock: clock_cjs_1.realClock, progressProvider: () => null, sourcePath: statePath });
726
+ }, { clock: clock_cjs_1.realClock, sourcePath: statePath });
699
727
  writeStateMd(statePath, result.content, cwd);
700
728
  }
701
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,
@@ -30,7 +30,67 @@ const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPa
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const planningWorkspace = require("./planning-workspace.cjs");
32
32
  const { planningDir } = planningWorkspace;
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const frontmatterModule = require("./frontmatter.cjs");
35
+ const { extractFrontmatter } = frontmatterModule;
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const planDependencyGraphModule = require("./plan-dependency-graph.cjs");
38
+ const { computeHaltPropagation, buildSummaryFileIndex, isSummaryFileHalted } = planDependencyGraphModule;
39
+ /**
40
+ * #2830: parse a plan file's `depends_on` frontmatter. Returns [] — never
41
+ * throws — on a missing/unreadable/malformed plan or absent field, matching
42
+ * this primitive's existing fail-safe posture (a plan directory this
43
+ * primitive can otherwise read must never throw here).
44
+ */
45
+ function parsePlanDependsOn(phaseDir, planFile) {
46
+ try {
47
+ const planPath = node_path_1.default.join(phaseDir, planFile);
48
+ const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
49
+ const fm = extractFrontmatter(content, planPath);
50
+ const fmDeps = fm['depends_on'];
51
+ if (Array.isArray(fmDeps))
52
+ return fmDeps.map(String);
53
+ if (typeof fmDeps === 'string' && fmDeps.trim() !== '')
54
+ return [fmDeps];
55
+ return [];
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
33
61
  // ─── Phase search helpers ─────────────────────────────────────────────────────
62
+ /**
63
+ * #2855: single source of truth for resolving and enumerating a project's
64
+ * (or, when a workstream is active, that workstream's OWN) archived-milestone
65
+ * directories — `<planningDir(cwd)>/milestones/vX.Y-phases/`. Both
66
+ * `findPhaseInternal`'s archive fallback and `getArchivedPhaseDirs` used to
67
+ * carry independent copies of this resolve-then-enumerate logic, which is
68
+ * exactly the shape that let the original #2855 bug (hardcoded root path)
69
+ * exist in one copy and not the other. Sharing this seam means a future
70
+ * change to how the archive tree is located only needs to happen once.
71
+ * Most-recent-milestone-first order (reverse-sorted directory names).
72
+ * Never throws: an absent/unreadable milestones/ dir yields [].
73
+ */
74
+ function listArchiveVersionDirs(cwd) {
75
+ const milestonesDir = node_path_1.default.join(planningDir(cwd), 'milestones');
76
+ if (!node_fs_1.default.existsSync(milestonesDir))
77
+ return [];
78
+ try {
79
+ const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
80
+ return milestoneEntries
81
+ .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
82
+ .map(e => e.name)
83
+ .sort()
84
+ .reverse()
85
+ .map(archiveName => ({
86
+ version: archiveName.match(/^(v[\d.]+)-phases$/)[1],
87
+ archivePath: node_path_1.default.join(milestonesDir, archiveName),
88
+ }));
89
+ }
90
+ catch {
91
+ return [];
92
+ }
93
+ }
34
94
  function searchPhaseInDir(baseDir, relBase, normalized) {
35
95
  try {
36
96
  const dirs = readSubdirectories(baseDir, true);
@@ -55,6 +115,9 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
55
115
  has_verification: false,
56
116
  has_reviews: false,
57
117
  ambiguous_matches: matches,
118
+ halted_plans: [],
119
+ blocked_by: {},
120
+ runnable_plans: [],
58
121
  };
59
122
  }
60
123
  const match = matches[0];
@@ -76,6 +139,50 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
76
139
  const canonical = extractCanonicalPlanId(p);
77
140
  return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
78
141
  });
142
+ // #2830: reverse lookup from a completed plan's id (exact or canonical) to
143
+ // its actual summary filename. Shared builder (also used by phase.cts's
144
+ // cmdPhasePlanIndex) so the two can never disagree about which summary
145
+ // belongs to which plan.
146
+ const summaryFileByPlanId = buildSummaryFileIndex(summaries, extractCanonicalPlanId);
147
+ // #2830: this primitive previously never parsed depends_on at all — see
148
+ // src/plan-dependency-graph.cts's file header. Build the same
149
+ // PlanHaltNode[] shape phase.cts's cmdPhasePlanIndex builds (id resolution
150
+ // mirrors its planMap/canonicalToId pattern) and hand it to the ONE
151
+ // shared halt-propagation traversal so this reader and the wave-grouping
152
+ // reader can never diverge on the halt rule again.
153
+ const planIds = plans.map(p => p.replace('-PLAN.md', '').replace('PLAN.md', ''));
154
+ const planIdByLower = new Map(planIds.map(id => [id.toLowerCase(), id]));
155
+ const canonicalToPlanId = new Map(plans.map((p, i) => [extractCanonicalPlanId(p).toLowerCase(), planIds[i]]));
156
+ const haltNodes = plans.map((p, i) => {
157
+ const planId = planIds[i];
158
+ const canonical = extractCanonicalPlanId(p);
159
+ const summaryFile = summaryFileByPlanId.get(planId) ?? summaryFileByPlanId.get(canonical);
160
+ const halted = summaryFile !== undefined && isSummaryFileHalted(node_path_1.default.join(phaseDir, summaryFile));
161
+ const resolvedDependsOn = parsePlanDependsOn(phaseDir, p)
162
+ .map((dep) => {
163
+ const lower = dep.toLowerCase();
164
+ return planIdByLower.get(lower) ?? canonicalToPlanId.get(lower) ?? null;
165
+ })
166
+ .filter((id) => id !== null);
167
+ return { id: planId, resolvedDependsOn, halted };
168
+ });
169
+ const { blockedBy } = computeHaltPropagation(haltNodes);
170
+ const haltedPlans = plans.filter((_, i) => haltNodes[i].halted);
171
+ const incompletePlanSet = new Set(incompletePlans);
172
+ const blockedByFiles = {};
173
+ const runnablePlans = [];
174
+ for (let i = 0; i < plans.length; i++) {
175
+ const p = plans[i];
176
+ if (!incompletePlanSet.has(p))
177
+ continue;
178
+ const causes = blockedBy.get(planIds[i]) ?? [];
179
+ if (causes.length > 0) {
180
+ blockedByFiles[p] = causes;
181
+ }
182
+ else {
183
+ runnablePlans.push(p);
184
+ }
185
+ }
79
186
  return {
80
187
  found: true,
81
188
  directory: toPosixPath(node_path_1.default.join(relBase, match)),
@@ -89,6 +196,9 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
89
196
  has_context: hasContext,
90
197
  has_verification: hasVerification,
91
198
  has_reviews: hasReviews,
199
+ halted_plans: haltedPlans,
200
+ blocked_by: blockedByFiles,
201
+ runnable_plans: runnablePlans,
92
202
  };
93
203
  }
94
204
  catch {
@@ -104,59 +214,42 @@ function findPhaseInternal(cwd, phase) {
104
214
  const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
105
215
  if (current)
106
216
  return current;
107
- const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
108
- if (!node_fs_1.default.existsSync(milestonesDir))
109
- return null;
110
- try {
111
- const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
112
- const archiveDirs = milestoneEntries
113
- .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
114
- .map(e => e.name)
115
- .sort()
116
- .reverse();
117
- for (const archiveName of archiveDirs) {
118
- const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
119
- const version = versionMatch[1];
120
- const archivePath = node_path_1.default.join(milestonesDir, archiveName);
121
- const relBase = '.planning/milestones/' + archiveName;
122
- const result = searchPhaseInDir(archivePath, relBase, normalized);
123
- if (result) {
124
- result.archived = version;
125
- return result;
126
- }
217
+ // #2855: scope the archived-milestone fallback to the SAME workstream as the
218
+ // active-phase search above (planningDir(cwd) resolves GSD_WORKSTREAM/GSD_PROJECT
219
+ // the identical way both places), not the hardcoded project-root tree. Archived
220
+ // phases genuinely live under a workstream's own `.planning/workstreams/<ws>/
221
+ // milestones/` — that is where archivePhaseDirectories (milestone.cts) writes
222
+ // them via the same planningDir(cwd) resolution. Hardcoding root here let a
223
+ // pending workstream phase resolve to an unrelated workstream's (or a flat-mode
224
+ // project's) archived phase that merely shares a phase number. Shared with
225
+ // getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
226
+ for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
227
+ const relBase = toPosixPath(node_path_1.default.relative(cwd, archivePath));
228
+ const result = searchPhaseInDir(archivePath, relBase, normalized);
229
+ if (result) {
230
+ result.archived = version;
231
+ return result;
127
232
  }
128
233
  }
129
- catch { /* intentionally empty */ }
130
234
  return null;
131
235
  }
132
236
  function getArchivedPhaseDirs(cwd) {
133
- const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
237
+ // #2855: same workstream-scoped resolution as findPhaseInternal above, via
238
+ // the shared listArchiveVersionDirs helper. `phase.list --include-archived`
239
+ // (the primary non-init consumer) must not leak a different workstream's
240
+ // archive either.
134
241
  const results = [];
135
- if (!node_fs_1.default.existsSync(milestonesDir))
136
- return results;
137
- try {
138
- const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
139
- const phaseDirs = milestoneEntries
140
- .filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
141
- .map(e => e.name)
142
- .sort()
143
- .reverse();
144
- for (const archiveName of phaseDirs) {
145
- const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
146
- const version = versionMatch[1];
147
- const archivePath = node_path_1.default.join(milestonesDir, archiveName);
148
- const dirs = readSubdirectories(archivePath, true);
149
- for (const dir of dirs) {
150
- results.push({
151
- name: dir,
152
- milestone: version,
153
- basePath: node_path_1.default.join('.planning', 'milestones', archiveName),
154
- fullPath: node_path_1.default.join(archivePath, dir),
155
- });
156
- }
242
+ for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
243
+ const dirs = readSubdirectories(archivePath, true);
244
+ for (const dir of dirs) {
245
+ results.push({
246
+ name: dir,
247
+ milestone: version,
248
+ basePath: toPosixPath(node_path_1.default.relative(cwd, archivePath)),
249
+ fullPath: node_path_1.default.join(archivePath, dir),
250
+ });
157
251
  }
158
252
  }
159
- catch { /* intentionally empty */ }
160
253
  return results;
161
254
  }
162
255
  module.exports = {