@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +17 -16
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +23 -13
- package/agents/gsd-planner.md +32 -188
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +12 -11
- package/bin/install.js +965 -1486
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +107 -1817
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +222 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +44 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +104 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +111 -51
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +53 -17
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +65 -28
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -7
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +11 -3
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +2293 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +25 -4
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +133 -29
- package/scripts/secret-scan.sh +3 -3
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
- 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,
|
|
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,
|