@opengsd/gsd-core 1.4.4 → 1.5.0-rc.2

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 (197) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/agents/gsd-code-fixer.md +3 -2
  4. package/agents/gsd-debug-session-manager.md +2 -1
  5. package/agents/gsd-debugger.md +4 -3
  6. package/agents/gsd-executor.md +17 -16
  7. package/agents/gsd-intel-updater.md +38 -41
  8. package/agents/gsd-phase-researcher.md +8 -8
  9. package/agents/gsd-plan-checker.md +23 -13
  10. package/agents/gsd-planner.md +32 -188
  11. package/agents/gsd-project-researcher.md +5 -4
  12. package/agents/gsd-research-synthesizer.md +2 -1
  13. package/agents/gsd-ui-researcher.md +2 -1
  14. package/agents/gsd-verifier.md +12 -11
  15. package/bin/install.js +965 -1486
  16. package/commands/gsd/autonomous.md +5 -1
  17. package/commands/gsd/ns-manage.md +8 -1
  18. package/commands/gsd/ns-project.md +5 -0
  19. package/commands/gsd/ns-review.md +4 -1
  20. package/commands/gsd/ns-workflow.md +7 -1
  21. package/commands/gsd/plan-review-convergence.md +5 -4
  22. package/commands/gsd/surface.md +12 -5
  23. package/gemini-extension.json +1 -1
  24. package/gsd-core/bin/gsd-tools.cjs +198 -101
  25. package/gsd-core/bin/gsd_run +20 -0
  26. package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
  27. package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
  28. package/gsd-core/bin/lib/capability-state.cjs +336 -0
  29. package/gsd-core/bin/lib/check-command-router.cjs +133 -2
  30. package/gsd-core/bin/lib/cli-exit.cjs +22 -3
  31. package/gsd-core/bin/lib/config-loader.cjs +716 -0
  32. package/gsd-core/bin/lib/configuration.cjs +4 -34
  33. package/gsd-core/bin/lib/core-utils.cjs +198 -0
  34. package/gsd-core/bin/lib/core.cjs +107 -1817
  35. package/gsd-core/bin/lib/edge-probe.cjs +173 -0
  36. package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
  37. package/gsd-core/bin/lib/federated-config.cjs +182 -0
  38. package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
  39. package/gsd-core/bin/lib/init.cjs +58 -12
  40. package/gsd-core/bin/lib/install-profiles.cjs +157 -3
  41. package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
  42. package/gsd-core/bin/lib/intel.cjs +3 -3
  43. package/gsd-core/bin/lib/io.cjs +222 -0
  44. package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
  45. package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
  46. package/gsd-core/bin/lib/model-resolver.cjs +426 -0
  47. package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
  48. package/gsd-core/bin/lib/phase-id.cjs +215 -0
  49. package/gsd-core/bin/lib/phase-locator.cjs +148 -0
  50. package/gsd-core/bin/lib/phase.cjs +17 -0
  51. package/gsd-core/bin/lib/probe-core.cjs +257 -0
  52. package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
  54. package/gsd-core/bin/lib/roadmap.cjs +44 -1
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
  56. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
  57. package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
  58. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
  59. package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
  60. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  61. package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
  62. package/gsd-core/bin/lib/state-document.cjs +8 -0
  63. package/gsd-core/bin/lib/state.cjs +114 -2
  64. package/gsd-core/bin/lib/surface.cjs +66 -14
  65. package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
  66. package/gsd-core/bin/lib/update-context.cjs +4 -1
  67. package/gsd-core/bin/lib/verify.cjs +104 -1
  68. package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
  69. package/gsd-core/bin/shared/model-catalog.json +11 -6
  70. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
  71. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
  72. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
  73. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
  74. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
  75. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
  76. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
  77. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
  78. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
  79. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
  80. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
  81. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
  82. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
  83. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
  84. package/gsd-core/references/edge-probe.md +261 -0
  85. package/gsd-core/references/planner-antipatterns.md +41 -0
  86. package/gsd-core/references/planner-guidance.md +186 -0
  87. package/gsd-core/references/planner-reviews.md +5 -2
  88. package/gsd-core/templates/phase-prompt.md +7 -7
  89. package/gsd-core/templates/project.md +19 -2
  90. package/gsd-core/templates/spec.md +12 -0
  91. package/gsd-core/templates/summary-complex.md +1 -0
  92. package/gsd-core/templates/summary-minimal.md +1 -0
  93. package/gsd-core/templates/summary-standard.md +1 -0
  94. package/gsd-core/templates/summary.md +1 -0
  95. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  96. package/gsd-core/workflows/add-backlog.md +1 -1
  97. package/gsd-core/workflows/add-phase.md +1 -1
  98. package/gsd-core/workflows/add-tests.md +1 -1
  99. package/gsd-core/workflows/add-todo.md +1 -1
  100. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  101. package/gsd-core/workflows/audit-fix.md +1 -1
  102. package/gsd-core/workflows/audit-milestone.md +1 -1
  103. package/gsd-core/workflows/audit-uat.md +1 -1
  104. package/gsd-core/workflows/autonomous.md +111 -51
  105. package/gsd-core/workflows/check-todos.md +1 -1
  106. package/gsd-core/workflows/cleanup.md +1 -1
  107. package/gsd-core/workflows/code-review-fix.md +6 -4
  108. package/gsd-core/workflows/code-review.md +53 -17
  109. package/gsd-core/workflows/complete-milestone.md +11 -5
  110. package/gsd-core/workflows/debug.md +1 -1
  111. package/gsd-core/workflows/diagnose-issues.md +1 -1
  112. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  113. package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
  114. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  115. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  116. package/gsd-core/workflows/discuss-phase.md +8 -1
  117. package/gsd-core/workflows/do.md +1 -1
  118. package/gsd-core/workflows/docs-update.md +1 -1
  119. package/gsd-core/workflows/edit-phase.md +1 -1
  120. package/gsd-core/workflows/eval-review.md +4 -1
  121. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  122. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
  123. package/gsd-core/workflows/execute-phase.md +8 -1
  124. package/gsd-core/workflows/execute-plan.md +1 -1
  125. package/gsd-core/workflows/explore.md +1 -1
  126. package/gsd-core/workflows/extract-learnings.md +1 -1
  127. package/gsd-core/workflows/forensics.md +1 -1
  128. package/gsd-core/workflows/graduation.md +1 -1
  129. package/gsd-core/workflows/health.md +1 -1
  130. package/gsd-core/workflows/help/modes/full.md +1 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/ingest-docs.md +1 -1
  133. package/gsd-core/workflows/insert-phase.md +1 -1
  134. package/gsd-core/workflows/list-workspaces.md +1 -1
  135. package/gsd-core/workflows/manager.md +1 -1
  136. package/gsd-core/workflows/map-codebase.md +1 -1
  137. package/gsd-core/workflows/milestone-summary.md +1 -1
  138. package/gsd-core/workflows/mvp-phase.md +1 -1
  139. package/gsd-core/workflows/new-milestone.md +9 -1
  140. package/gsd-core/workflows/new-project.md +9 -1
  141. package/gsd-core/workflows/new-workspace.md +1 -1
  142. package/gsd-core/workflows/next.md +1 -1
  143. package/gsd-core/workflows/pause-work.md +1 -1
  144. package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
  145. package/gsd-core/workflows/plan-phase.md +65 -28
  146. package/gsd-core/workflows/plan-review-convergence.md +60 -33
  147. package/gsd-core/workflows/plant-seed.md +1 -1
  148. package/gsd-core/workflows/profile-user.md +1 -1
  149. package/gsd-core/workflows/progress.md +1 -1
  150. package/gsd-core/workflows/quick.md +2 -2
  151. package/gsd-core/workflows/remove-phase.md +1 -1
  152. package/gsd-core/workflows/remove-workspace.md +1 -1
  153. package/gsd-core/workflows/resume-project.md +1 -1
  154. package/gsd-core/workflows/review.md +1 -1
  155. package/gsd-core/workflows/scan.md +1 -1
  156. package/gsd-core/workflows/secure-phase.md +1 -1
  157. package/gsd-core/workflows/settings-advanced.md +7 -7
  158. package/gsd-core/workflows/settings-integrations.md +1 -1
  159. package/gsd-core/workflows/settings.md +2 -2
  160. package/gsd-core/workflows/ship.md +8 -1
  161. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  162. package/gsd-core/workflows/sketch.md +1 -1
  163. package/gsd-core/workflows/spec-phase.md +130 -1
  164. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  165. package/gsd-core/workflows/spike.md +1 -1
  166. package/gsd-core/workflows/stats.md +1 -1
  167. package/gsd-core/workflows/thread.md +1 -1
  168. package/gsd-core/workflows/transition.md +1 -1
  169. package/gsd-core/workflows/ui-phase.md +1 -1
  170. package/gsd-core/workflows/ui-review.md +1 -1
  171. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  172. package/gsd-core/workflows/update.md +2 -2
  173. package/gsd-core/workflows/validate-phase.md +1 -1
  174. package/gsd-core/workflows/verify-phase.md +1 -1
  175. package/gsd-core/workflows/verify-work.md +8 -1
  176. package/package.json +11 -3
  177. package/scripts/base64-scan.sh +1 -1
  178. package/scripts/changeset/cli.cjs +8 -1
  179. package/scripts/changeset/lint.cjs +38 -2
  180. package/scripts/ci-test-scope.cjs +21 -10
  181. package/scripts/gen-capability-registry.cjs +2293 -0
  182. package/scripts/gen-loop-host-contract.cjs +471 -0
  183. package/scripts/lib/allowlist-ratchet.cjs +101 -1
  184. package/scripts/lint-regression-test-names.allowlist.json +269 -0
  185. package/scripts/lint-regression-test-names.cjs +117 -0
  186. package/scripts/lint-test-file-count.allowlist.json +25 -4
  187. package/scripts/lint-windows-test-portability.cjs +178 -0
  188. package/scripts/prompt-injection-scan.sh +4 -4
  189. package/scripts/research-profiles.cjs +10 -10
  190. package/scripts/run-tests.cjs +133 -29
  191. package/scripts/secret-scan.sh +3 -3
  192. package/scripts/sync-next-version.cjs +133 -0
  193. package/scripts/sync-runtime-launcher.cjs +21 -5
  194. package/scripts/update-size-baseline.cjs +68 -0
  195. package/scripts/workflow-policy.cjs +42 -9
  196. package/scripts/workflow-size.cjs +90 -0
  197. package/scripts/run-cross-platform-tests.cjs +0 -67
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ /**
3
+ * Roadmap Parser — ROADMAP.md parsing helpers
4
+ *
5
+ * ADR-857 rollout phase 2b: extracted from core.cts (issue #870).
6
+ * Owns shipped-milestone slicing, current-milestone extraction,
7
+ * milestone/phase lookups, and milestone-phase filtering.
8
+ * Behaviour is preserved byte-for-behaviour from the prior location;
9
+ * only the module boundary moved. core.cjs re-exports every symbol here
10
+ * under its own `export =` object so existing consumers are unaffected.
11
+ *
12
+ * New imports should pull roadmap-parser helpers from roadmap-parser.cjs directly.
13
+ *
14
+ * Dependencies (leaf modules only — no core.cjs, no loadConfig):
15
+ * - node:fs / node:path (stdlib)
16
+ * - ./phase-id.cjs (escapeRegex, phaseMarkdownRegexSource)
17
+ * - ./planning-workspace.cjs (planningDir)
18
+ * - ./shell-command-projection.cjs (platformReadSync)
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ const node_fs_1 = __importDefault(require("node:fs"));
24
+ const node_path_1 = __importDefault(require("node:path"));
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const phaseIdModule = require("./phase-id.cjs");
27
+ const { escapeRegex, phaseMarkdownRegexSource } = phaseIdModule;
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
29
+ const planningWorkspace = require("./planning-workspace.cjs");
30
+ const { planningDir } = planningWorkspace;
31
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
32
+ // ─── Roadmap milestone scoping ───────────────────────────────────────────────
33
+ /**
34
+ * Strip shipped milestone content wrapped in <details> blocks.
35
+ */
36
+ function stripShippedMilestones(content) {
37
+ return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
38
+ }
39
+ /**
40
+ * Extract the current milestone section from ROADMAP.md by positive lookup.
41
+ */
42
+ function extractCurrentMilestone(content, cwd) {
43
+ if (!cwd)
44
+ return stripShippedMilestones(content);
45
+ let version = null;
46
+ try {
47
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
48
+ const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
49
+ if (stateRaw !== null) {
50
+ const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
51
+ if (milestoneMatch) {
52
+ version = milestoneMatch[1].trim();
53
+ }
54
+ }
55
+ }
56
+ catch { /* ignore */ }
57
+ if (!version) {
58
+ const inProgressMatch = content.match(/(?:🚧|🔄)\s*\*\*v(\d+\.\d+)\s/);
59
+ if (inProgressMatch) {
60
+ version = 'v' + inProgressMatch[1];
61
+ }
62
+ }
63
+ if (!version)
64
+ return stripShippedMilestones(content);
65
+ const escapedVersion = escapeRegex(version);
66
+ const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}\\b[^\\n]*)`, 'gmi');
67
+ const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${escapedVersion}[^<]*)<\\/summary>`, 'i');
68
+ const headingMatches = [...content.matchAll(sectionPattern)];
69
+ if (headingMatches.length === 0) {
70
+ const summaryMatch = content.match(summaryPattern);
71
+ if (summaryMatch) {
72
+ const summaryIdx = content.indexOf(summaryMatch[0]);
73
+ const beforeSummary = content.slice(0, summaryIdx);
74
+ const detailsOpenIdx = beforeSummary.lastIndexOf('<details');
75
+ if (detailsOpenIdx !== -1) {
76
+ const afterDetails = content.slice(detailsOpenIdx);
77
+ const closingMatch = afterDetails.match(/<\/details>/i);
78
+ const detailsEnd = closingMatch
79
+ ? detailsOpenIdx + (closingMatch.index ?? 0) + '</details>'.length
80
+ : content.length;
81
+ const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
82
+ const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
83
+ const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
84
+ const preamble = content.slice(0, preambleCutoff)
85
+ .replace(/<details>[\s\S]*?<\/details>/gi, '')
86
+ .replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
87
+ .replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
88
+ return preamble + content.slice(detailsOpenIdx, detailsEnd);
89
+ }
90
+ }
91
+ return stripShippedMilestones(content);
92
+ }
93
+ const allMatches = headingMatches;
94
+ const closedMarkerPattern = /\b(?:CLOSED|ARCHIVED|ABANDONED|SHIPPED|FAILED)\b|✅|🗄/i;
95
+ const activeMarkerPattern = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progress|🚧|🔄/i;
96
+ const isClosed = (h) => closedMarkerPattern.test(h) && !activeMarkerPattern.test(h);
97
+ const firstMatch = allMatches[0];
98
+ const selected = allMatches.find((m) => !isClosed(m[1])) || firstMatch;
99
+ const sectionStart = selected.index;
100
+ const computeSectionEnd = (headingText, headingStart) => {
101
+ const level = (headingText.match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
102
+ const rest = content.slice(headingStart + headingText.length);
103
+ const stopPattern = new RegExp(`^#{1,${level}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
104
+ let end = content.length;
105
+ let fc = null;
106
+ let fl = 0;
107
+ let off = 0;
108
+ for (const line of rest.split('\n')) {
109
+ const fm = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
110
+ if (fm) {
111
+ const ch = fm[1][0];
112
+ const ln = fm[1].length;
113
+ const trailing = fm[2] || '';
114
+ if (!fc) {
115
+ fc = ch;
116
+ fl = ln;
117
+ }
118
+ else if (ch === fc && ln >= fl && /^\s*$/.test(trailing)) {
119
+ fc = null;
120
+ fl = 0;
121
+ }
122
+ }
123
+ else if (!fc && stopPattern.test(line)) {
124
+ end = headingStart + headingText.length + off;
125
+ break;
126
+ }
127
+ off += line.length + 1;
128
+ }
129
+ return end;
130
+ };
131
+ const sectionEnd = computeSectionEnd(selected[0], sectionStart);
132
+ const anyMilestonePattern = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧)/im;
133
+ const firstMilestoneMatch = content.match(anyMilestonePattern);
134
+ const preambleCutoff = firstMilestoneMatch
135
+ ? firstMilestoneMatch.index
136
+ : firstMatch.index;
137
+ const beforeMilestones = content.slice(0, preambleCutoff);
138
+ const currentSection = content.slice(sectionStart, sectionEnd);
139
+ // Multi-milestone roadmaps split each added milestone across two version-bearing
140
+ // headings: a `## Phases` checklist subsection (early) and a dedicated
141
+ // `## Milestone … (Phase Details)` section (late) holding the `### Phase N:`
142
+ // detail headers. The scope window above stops at the next version-bearing
143
+ // heading — the current milestone's OWN Phase Details heading — leaving those
144
+ // detail headers outside `currentSection`. Append that section so phase
145
+ // resolution and counting see the current milestone's phases. Anchor the lookup
146
+ // to the SELECTED heading's specific version token (boundary-aware, so a
147
+ // `v3.0` state does not match a `v3.0-A` sub-milestone) so sibling milestones
148
+ // that share a version prefix do not cross-pollinate. (#730)
149
+ const selectedVersionToken = selected[1].match(/v\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*/i)?.[0];
150
+ const detailsVersionBoundary = selectedVersionToken
151
+ ? new RegExp(`${escapeRegex(selectedVersionToken)}(?![\\w.-])`, 'i')
152
+ : null;
153
+ let detailsSection = '';
154
+ const detailsMatch = allMatches.find((m) => /\(Phase\s+Details\)/i.test(m[1]) &&
155
+ !isClosed(m[1]) &&
156
+ (!detailsVersionBoundary || detailsVersionBoundary.test(m[1])) &&
157
+ (m.index ?? 0) >= sectionEnd);
158
+ if (detailsMatch) {
159
+ const detailsStart = detailsMatch.index ?? 0;
160
+ detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
161
+ }
162
+ const preamble = beforeMilestones
163
+ .replace(/<details>[\s\S]*?<\/details>/gi, '')
164
+ .replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
165
+ .replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
166
+ return detailsSection
167
+ ? preamble + currentSection + '\n' + detailsSection
168
+ : preamble + currentSection;
169
+ }
170
+ /**
171
+ * Replace a pattern only in the current milestone section of ROADMAP.md.
172
+ */
173
+ function replaceInCurrentMilestone(content, pattern, replacement) {
174
+ const lastDetailsClose = content.lastIndexOf('</details>');
175
+ if (lastDetailsClose === -1) {
176
+ return content.replace(pattern, replacement);
177
+ }
178
+ const offset = lastDetailsClose + '</details>'.length;
179
+ const before = content.slice(0, offset);
180
+ const after = content.slice(offset);
181
+ return before + after.replace(pattern, replacement);
182
+ }
183
+ function getRoadmapPhaseInternal(cwd, phaseNum) {
184
+ if (!phaseNum)
185
+ return null;
186
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
187
+ if (!node_fs_1.default.existsSync(roadmapPath))
188
+ return null;
189
+ try {
190
+ const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
191
+ if (roadmapRaw === null)
192
+ throw new Error('missing');
193
+ const content = extractCurrentMilestone(roadmapRaw, cwd);
194
+ const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
195
+ const headerMatch = content.match(phasePattern);
196
+ if (!headerMatch)
197
+ return null;
198
+ const phaseName = headerMatch[1].trim();
199
+ const headerIndex = headerMatch.index;
200
+ const restOfContent = content.slice(headerIndex);
201
+ const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i);
202
+ const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
203
+ const section = content.slice(headerIndex, sectionEnd).trim();
204
+ const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
205
+ const goal = goalMatch ? goalMatch[1].trim() : null;
206
+ return {
207
+ found: true,
208
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
209
+ phase_number: String(phaseNum),
210
+ phase_name: phaseName,
211
+ goal,
212
+ section,
213
+ };
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
219
+ function getMilestoneInfo(cwd) {
220
+ try {
221
+ const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
222
+ if (roadmap === null)
223
+ throw new Error('missing');
224
+ let stateVersion = null;
225
+ if (cwd) {
226
+ try {
227
+ const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
228
+ const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
229
+ if (stateRaw !== null) {
230
+ const m = stateRaw.match(/^milestone:\s*(.+)/m);
231
+ if (m)
232
+ stateVersion = m[1].trim();
233
+ }
234
+ }
235
+ catch { /* intentionally empty */ }
236
+ }
237
+ if (stateVersion) {
238
+ const escapedVer = escapeRegex(stateVersion);
239
+ const headingMatch = roadmap.match(new RegExp(`##[^\\n]*${escapedVer}[:\\s]+([^\\n(]+)`, 'i'));
240
+ if (headingMatch) {
241
+ if (!headingMatch[0].includes('✅')) {
242
+ return { version: stateVersion, name: headingMatch[1].trim() };
243
+ }
244
+ }
245
+ else {
246
+ const listMatch = roadmap.match(new RegExp(`🚧\\s*\\*?\\*?${escapedVer}\\s+([^*\\n]+)`, 'i'));
247
+ if (listMatch) {
248
+ return { version: stateVersion, name: listMatch[1].trim() };
249
+ }
250
+ return { version: stateVersion, name: 'milestone' };
251
+ }
252
+ }
253
+ const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
254
+ if (inProgressMatch) {
255
+ return {
256
+ version: 'v' + inProgressMatch[1],
257
+ name: inProgressMatch[2].trim(),
258
+ };
259
+ }
260
+ const cleaned = stripShippedMilestones(roadmap);
261
+ const headingMatch = cleaned.match(/## (?!.*✅).*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/);
262
+ if (headingMatch) {
263
+ return {
264
+ version: 'v' + headingMatch[1],
265
+ name: headingMatch[2].trim(),
266
+ };
267
+ }
268
+ const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/);
269
+ return {
270
+ version: versionMatch ? versionMatch[0] : 'v1.0',
271
+ name: 'milestone',
272
+ };
273
+ }
274
+ catch {
275
+ return { version: 'v1.0', name: 'milestone' };
276
+ }
277
+ }
278
+ // ─── Fence-aware text helper ──────────────────────────────────────────────────
279
+ /**
280
+ * Return a copy of `text` with every line that lies inside a fenced code block
281
+ * replaced by an empty string, using the same fence semantics as
282
+ * `computeSectionEnd` (backtick/tilde, ≥3 chars, indent ≤3 spaces, toggle;
283
+ * an unclosed fence treats remaining content as fenced).
284
+ */
285
+ function stripFencedLines(text) {
286
+ let fenceChar = null;
287
+ let fenceLen = 0;
288
+ const lines = text.split('\n');
289
+ const result = [];
290
+ for (const line of lines) {
291
+ const fm = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
292
+ if (fm) {
293
+ const ch = fm[1][0];
294
+ const ln = fm[1].length;
295
+ const trailing = fm[2] || '';
296
+ if (!fenceChar) {
297
+ fenceChar = ch;
298
+ fenceLen = ln;
299
+ // The fence-open line itself is not a content line — blank it.
300
+ result.push('');
301
+ }
302
+ else if (ch === fenceChar && ln >= fenceLen && /^\s*$/.test(trailing)) {
303
+ fenceChar = null;
304
+ fenceLen = 0;
305
+ // The fence-close line — blank it.
306
+ result.push('');
307
+ }
308
+ else {
309
+ // A fence marker that doesn't close the current fence (different char or shorter) — keep treating as fenced content.
310
+ result.push(fenceChar ? '' : line);
311
+ }
312
+ }
313
+ else {
314
+ result.push(fenceChar ? '' : line);
315
+ }
316
+ }
317
+ return result.join('\n');
318
+ }
319
+ /**
320
+ * Returns a filter function that checks whether a phase directory belongs
321
+ * to the current milestone based on ROADMAP.md phase headings.
322
+ */
323
+ function getMilestonePhaseFilter(cwd, versionOverride) {
324
+ const milestonePhaseNums = new Set();
325
+ let missingExplicitVersion = false;
326
+ try {
327
+ const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
328
+ const roadmapContent = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
329
+ if (roadmapContent === null)
330
+ throw new Error('missing');
331
+ let roadmap = extractCurrentMilestone(roadmapContent, cwd);
332
+ const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
333
+ const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
334
+ if (!hasVersionedMilestonesGlobal && hasPhaseHeadings) {
335
+ console.warn('[gsd] Deprecated: free-form ROADMAP.md detected (no versioned milestone headings). ' +
336
+ 'Set phase_id_convention in config.json to suppress this warning.');
337
+ }
338
+ if (versionOverride) {
339
+ const escapedVersion = escapeRegex(versionOverride);
340
+ const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}[^\\n]*)`, 'mi');
341
+ let sectionMatch = roadmapContent.match(sectionPattern);
342
+ if (!sectionMatch) {
343
+ const summaryPat = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i');
344
+ const summaryHit = roadmapContent.match(summaryPat);
345
+ if (summaryHit) {
346
+ const beforeSummary = roadmapContent.slice(0, summaryHit.index);
347
+ const detailsIdx = beforeSummary.lastIndexOf('<details');
348
+ if (detailsIdx !== -1) {
349
+ sectionMatch = null;
350
+ }
351
+ }
352
+ }
353
+ if (!sectionMatch) {
354
+ const hasVersionedMilestones = /^#{1,3}\s+(?!Phase\s+\S).*v\d+\.\d+/mi.test(roadmapContent);
355
+ const versionInSummary = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i').test(roadmapContent);
356
+ if (hasVersionedMilestones && !versionInSummary) {
357
+ roadmap = '';
358
+ missingExplicitVersion = true;
359
+ }
360
+ }
361
+ else {
362
+ const sectionStart = sectionMatch.index;
363
+ const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
364
+ const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
365
+ const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
366
+ let sectionEnd = roadmapContent.length;
367
+ let fenceChar = null;
368
+ let fenceLen = 0;
369
+ let charOffset = 0;
370
+ for (const line of restContent.split('\n')) {
371
+ const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
372
+ if (fenceMatch) {
373
+ const char = fenceMatch[1][0];
374
+ const len = fenceMatch[1].length;
375
+ const trailing = fenceMatch[2] || '';
376
+ if (!fenceChar) {
377
+ fenceChar = char;
378
+ fenceLen = len;
379
+ }
380
+ else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
381
+ fenceChar = null;
382
+ fenceLen = 0;
383
+ }
384
+ }
385
+ else if (!fenceChar && nextMilestonePattern.test(line)) {
386
+ sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
387
+ break;
388
+ }
389
+ charOffset += line.length + 1;
390
+ }
391
+ const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
392
+ roadmap = currentSection;
393
+ }
394
+ }
395
+ const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
396
+ const roadmapUnfenced = stripFencedLines(roadmap);
397
+ let m;
398
+ while ((m = phasePattern.exec(roadmapUnfenced)) !== null) {
399
+ milestonePhaseNums.add(m[1]);
400
+ }
401
+ }
402
+ catch { /* intentionally empty */ }
403
+ if (milestonePhaseNums.size === 0) {
404
+ const passAll = (() => true);
405
+ passAll.phaseCount = 0;
406
+ passAll.missingExplicitVersion = missingExplicitVersion;
407
+ return passAll;
408
+ }
409
+ const normalized = new Set([...milestonePhaseNums].map(n => n.split('-').map(seg => (seg.replace(/^0+(?=\d)/, '') || '0')).join('-').toLowerCase()));
410
+ function normalizePhaseIdSegments(id) {
411
+ return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
412
+ }
413
+ const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
414
+ const numericRe = roadmapUsesHyphenedIds
415
+ ? /^0*(\d+(?:-0*\d+)*[A-Za-z]?(?:\.\d+)*)/
416
+ : /^0*(\d+[A-Za-z]?(?:\.\d+)*)/;
417
+ function isDirInMilestone(dirName) {
418
+ const m2 = dirName.match(numericRe);
419
+ if (m2 && normalized.has(normalizePhaseIdSegments(m2[1]).toLowerCase()))
420
+ return true;
421
+ const customMatch = dirName.match(/^([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)/);
422
+ if (customMatch && normalized.has(customMatch[1].toLowerCase()))
423
+ return true;
424
+ const stripped = dirName.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
425
+ if (stripped !== dirName) {
426
+ const sm = stripped.match(numericRe);
427
+ if (sm && normalized.has(normalizePhaseIdSegments(sm[1]).toLowerCase()))
428
+ return true;
429
+ }
430
+ return false;
431
+ }
432
+ isDirInMilestone.phaseCount = milestonePhaseNums.size;
433
+ isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
434
+ return isDirInMilestone;
435
+ }
436
+ module.exports = {
437
+ stripShippedMilestones,
438
+ extractCurrentMilestone,
439
+ replaceInCurrentMilestone,
440
+ getRoadmapPhaseInternal,
441
+ getMilestoneInfo,
442
+ getMilestonePhaseFilter,
443
+ };
@@ -13,7 +13,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  // eslint-disable-next-line @typescript-eslint/no-require-imports
15
15
  const core = require("./core.cjs");
16
- const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches } = core;
16
+ const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, output, error, findPhaseInternal, phaseTokenMatches } = core;
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const roadmapParserModule = require("./roadmap-parser.cjs");
19
+ const { stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = roadmapParserModule;
17
20
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
22
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -132,6 +135,45 @@ function searchPhaseInContent(content, escapedPhase, phaseNum) {
132
135
  section,
133
136
  };
134
137
  }
138
+ // ─── getRoadmapPhaseWithFallback ──────────────────────────────────────────────
139
+ /**
140
+ * Two-pass phase lookup that mirrors cmdRoadmapGetPhase's resolution strategy.
141
+ *
142
+ * Pass 1: current-milestone slice (extractCurrentMilestone).
143
+ * Pass 2: full roadmap content (stripShippedMilestones) — covers cross-milestone
144
+ * and older frontend phases that are no longer in the current milestone slice.
145
+ *
146
+ * Returns the phase section string if found, null if ROADMAP.md is missing,
147
+ * or throws if ROADMAP.md read fails.
148
+ *
149
+ * Used by check-command-router (computeUiPlanGate) so ui-plan-gate uses the SAME
150
+ * phase resolution as `roadmap.get-phase` — not a milestone-only subset.
151
+ */
152
+ function getRoadmapPhaseWithFallback(cwd, phaseNum) {
153
+ const roadmapPath = planningPaths(cwd).roadmap;
154
+ if (!node_fs_1.default.existsSync(roadmapPath))
155
+ return null;
156
+ const rawContent = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
157
+ const milestoneContent = extractCurrentMilestone(rawContent, cwd);
158
+ const fullContent = stripShippedMilestones(rawContent);
159
+ const exactSource = phaseMarkdownRegexSourceExact(phaseNum);
160
+ if (exactSource) {
161
+ const exactMilestone = searchPhaseInContent(milestoneContent, exactSource, phaseNum);
162
+ if (exactMilestone && !exactMilestone.error)
163
+ return exactMilestone.section ?? null;
164
+ const exactFull = searchPhaseInContent(fullContent, exactSource, phaseNum);
165
+ if (exactFull && !exactFull.error)
166
+ return exactFull.section ?? null;
167
+ }
168
+ const escapedPhase = phaseMarkdownRegexSource(phaseNum);
169
+ const milestoneResult = searchPhaseInContent(milestoneContent, escapedPhase, phaseNum);
170
+ const result = (milestoneResult && !milestoneResult.error)
171
+ ? milestoneResult
172
+ : searchPhaseInContent(fullContent, escapedPhase, phaseNum) || milestoneResult;
173
+ if (!result || result.error)
174
+ return null;
175
+ return result.section ?? null;
176
+ }
135
177
  // ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
136
178
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
137
179
  const roadmapPath = planningPaths(cwd).roadmap;
@@ -594,6 +636,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
594
636
  }
595
637
  module.exports = {
596
638
  cmdRoadmapGetPhase,
639
+ getRoadmapPhaseWithFallback,
597
640
  cmdRoadmapAnalyze,
598
641
  cmdRoadmapUpdatePlanProgress,
599
642
  cmdRoadmapAnnotateDependencies,