@opengsd/gsd-core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -24,6 +24,9 @@ const { planningDir } = planningWorkspace;
|
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
25
|
const configLoaderMod = require("./config-loader.cjs");
|
|
26
26
|
const { loadConfig } = configLoaderMod;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const cliExitMod = require("./cli-exit.cjs");
|
|
29
|
+
const { ExitError } = cliExitMod;
|
|
27
30
|
// ─── W021 Implementation ──────────────────────────────────────────────────────
|
|
28
31
|
/**
|
|
29
32
|
* Check each phase entry in a milestone-prefixed ROADMAP.md for W021 violations.
|
|
@@ -110,12 +113,40 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
|
|
|
110
113
|
'annotate-dependencies': () => roadmap.cmdRoadmapAnnotateDependencies(cwd, args[2], raw),
|
|
111
114
|
'validate': () => {
|
|
112
115
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
113
|
-
|
|
116
|
+
const warnings = [];
|
|
117
|
+
// #2978: structural validation. A verb named "validate" that cannot
|
|
118
|
+
// produce a negative result provides false assurance. Before the
|
|
119
|
+
// opt-in milestone-prefix check, verify the file is structurally a
|
|
120
|
+
// roadmap at all.
|
|
121
|
+
let roadmapContent;
|
|
114
122
|
try {
|
|
115
123
|
roadmapContent = node_fs_1.default.readFileSync(roadmapPath, 'utf8');
|
|
116
124
|
}
|
|
117
125
|
catch {
|
|
118
|
-
// ROADMAP.md missing —
|
|
126
|
+
// ROADMAP.md missing — not silent success.
|
|
127
|
+
warnings.push({ code: 'V001', message: 'ROADMAP.md not found or unreadable' });
|
|
128
|
+
const result = { warnings };
|
|
129
|
+
process.stdout.write(raw ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
130
|
+
throw new ExitError(1);
|
|
131
|
+
}
|
|
132
|
+
// Empty or whitespace-only.
|
|
133
|
+
if (roadmapContent.trim() === '') {
|
|
134
|
+
warnings.push({ code: 'V002', message: 'ROADMAP.md is empty' });
|
|
135
|
+
}
|
|
136
|
+
// Malformed frontmatter — a `---` opener with no matching closer.
|
|
137
|
+
// Tolerate a leading BOM (#3057) before the fence.
|
|
138
|
+
const contentAfterBom = roadmapContent.replace(/^\uFEFF/, '');
|
|
139
|
+
if (contentAfterBom.startsWith('---')) {
|
|
140
|
+
const closeMatch = contentAfterBom.slice(3).match(/\r?\n---\s*(\r?\n|$)/);
|
|
141
|
+
if (!closeMatch) {
|
|
142
|
+
warnings.push({ code: 'V003', message: 'ROADMAP.md frontmatter is malformed (unterminated --- fence)' });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// No recognizable phase structure — at least one `### Phase N:` heading.
|
|
146
|
+
// Mirrors the phase-heading pattern used across roadmap-parser.cts.
|
|
147
|
+
const hasPhaseEntry = /^#{2,4}\s*Phase\s+\S/im.test(roadmapContent);
|
|
148
|
+
if (!hasPhaseEntry && !warnings.some((w) => w.code === 'V002')) {
|
|
149
|
+
warnings.push({ code: 'V004', message: 'ROADMAP.md contains no recognizable phase entries (no "### Phase N:" headings)' });
|
|
119
150
|
}
|
|
120
151
|
// W021 only fires when phase_id_convention is explicitly 'milestone-prefixed'.
|
|
121
152
|
// Authoritative source: .planning/config.json (set by the upgrade command).
|
|
@@ -141,14 +172,16 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
|
|
|
141
172
|
}
|
|
142
173
|
}
|
|
143
174
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
175
|
+
if (convention === 'milestone-prefixed') {
|
|
176
|
+
warnings.push(...checkW021(roadmapContent));
|
|
177
|
+
}
|
|
147
178
|
const result = { warnings };
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
179
|
+
process.stdout.write(raw ? JSON.stringify(result) : JSON.stringify(result, null, 2));
|
|
180
|
+
// #2978: exit non-zero on any warning, per the documented contract
|
|
181
|
+
// ("exits non-zero on any error or warning").
|
|
182
|
+
if (warnings.length > 0) {
|
|
183
|
+
throw new ExitError(1);
|
|
184
|
+
}
|
|
152
185
|
},
|
|
153
186
|
'upgrade': () => {
|
|
154
187
|
const dryRun = !args.includes('--apply');
|
|
@@ -36,21 +36,76 @@ const unusableInputMod = require("./unusable-input.cjs");
|
|
|
36
36
|
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
37
37
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
38
38
|
// ─── Roadmap milestone scoping ───────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Markers that classify a MILESTONE HEADING (or `<summary>`) as closed/shipped
|
|
41
|
+
* versus still active. Hoisted to module scope in #2562 — three call sites
|
|
42
|
+
* (`extractCurrentMilestone`, `currentMilestoneRawRanges`,
|
|
43
|
+
* `isMilestoneShippedInRoadmap`) previously kept byte-identical copies.
|
|
44
|
+
*/
|
|
45
|
+
const MILESTONE_CLOSED_MARKER_PATTERN = /\b(?:CLOSED|ARCHIVED|ABANDONED|SHIPPED|FAILED)\b|✅|🗄/i;
|
|
46
|
+
const MILESTONE_ACTIVE_MARKER_PATTERN = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progress|🚧|🔄/i;
|
|
47
|
+
function isClosedMilestoneHeading(headingText) {
|
|
48
|
+
return MILESTONE_CLOSED_MARKER_PATTERN.test(headingText) && !MILESTONE_ACTIVE_MARKER_PATTERN.test(headingText);
|
|
49
|
+
}
|
|
39
50
|
/**
|
|
40
51
|
* Strip shipped milestone content wrapped in <details> blocks.
|
|
41
52
|
*/
|
|
42
53
|
function stripShippedMilestones(content) {
|
|
43
54
|
return (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(content, 'details');
|
|
44
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* #2562: is the milestone `version` marked SHIPPED by the ROADMAP itself?
|
|
58
|
+
*
|
|
59
|
+
* Scoped deliberately narrowly, because a false positive here reproduces the
|
|
60
|
+
* exact symptom #2562 reports ("milestone complete" while phases are unstarted):
|
|
61
|
+
*
|
|
62
|
+
* - Only a MILESTONE HEADING (`^#{1,3}` that is not a `Phase N:` heading) or a
|
|
63
|
+
* `<summary>` line can carry the signal. A bullet or checklist item that
|
|
64
|
+
* merely NAMES the version (`- [x] 03-01: ship the v2.0 login endpoint ✅`)
|
|
65
|
+
* is prose about a phase, not a milestone verdict, and is ignored.
|
|
66
|
+
* - The version token is boundary-matched with `(?![\w.-])` (mirrors the #730
|
|
67
|
+
* sub-milestone boundary at `extractCurrentMilestone`), so `v2.0` does not
|
|
68
|
+
* match inside `v2.0.1` — `\b` alone would, since `.` is a non-word char.
|
|
69
|
+
* - Shipped/active classification reuses the same marker patterns the milestone
|
|
70
|
+
* sectioniser uses, so an in-progress marker on the line always wins.
|
|
71
|
+
*
|
|
72
|
+
* Both patterns are anchored and use only complementary character classes
|
|
73
|
+
* (`[^\n]`, `[^<]`, `[^>]`) with no overlapping alternation, so matching stays
|
|
74
|
+
* linear in the ROADMAP's length — an untrusted ROADMAP cannot drive backtracking.
|
|
75
|
+
*/
|
|
76
|
+
function isMilestoneShippedInRoadmap(content, version) {
|
|
77
|
+
const boundedVersion = `${escapeRegex(version)}(?![\\w.-])`;
|
|
78
|
+
const candidates = [
|
|
79
|
+
// A milestone heading: `## v2.0 Launch — ✅ SHIPPED`.
|
|
80
|
+
new RegExp(`^#{1,3}[^\\S\\n]+(?!Phase\\s+\\S)[^\\n]*${boundedVersion}[^\\n]*$`, 'gmi'),
|
|
81
|
+
// A collapsed shipped block's own summary: `<summary>✅ v2.0 … SHIPPED</summary>`.
|
|
82
|
+
new RegExp(`<summary[^>]*>[^<]*${boundedVersion}[^<]*<\\/summary>`, 'gi'),
|
|
83
|
+
];
|
|
84
|
+
for (const pattern of candidates) {
|
|
85
|
+
for (const match of content.matchAll(pattern)) {
|
|
86
|
+
if (isClosedMilestoneHeading(match[0]))
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
45
92
|
/**
|
|
46
93
|
* Extract the current milestone section from ROADMAP.md by positive lookup.
|
|
94
|
+
*
|
|
95
|
+
* @param content - ROADMAP.md content.
|
|
96
|
+
* @param cwd - Project working directory, used to read the companion STATE.md
|
|
97
|
+
* for the current `milestone:` version.
|
|
98
|
+
* @param ws - #2562: workstream name, so the companion STATE.md is read from
|
|
99
|
+
* `.planning/workstreams/<ws>/` instead of the project root. Omitted (the
|
|
100
|
+
* default) preserves the prior `planningDir(cwd)` resolution exactly,
|
|
101
|
+
* including its `GSD_WORKSTREAM` env fallback.
|
|
47
102
|
*/
|
|
48
|
-
function extractCurrentMilestone(content, cwd) {
|
|
103
|
+
function extractCurrentMilestone(content, cwd, ws) {
|
|
49
104
|
if (!cwd)
|
|
50
105
|
return stripShippedMilestones(content);
|
|
51
106
|
let version = null;
|
|
52
107
|
try {
|
|
53
|
-
const statePath = node_path_1.default.join(planningDir(cwd), 'STATE.md');
|
|
108
|
+
const statePath = node_path_1.default.join(planningDir(cwd, ws), 'STATE.md');
|
|
54
109
|
const stateRaw = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
55
110
|
if (stateRaw !== null) {
|
|
56
111
|
const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
|
|
@@ -97,9 +152,7 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
97
152
|
return stripShippedMilestones(content);
|
|
98
153
|
}
|
|
99
154
|
const allMatches = headingMatches;
|
|
100
|
-
const
|
|
101
|
-
const activeMarkerPattern = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progress|🚧|🔄/i;
|
|
102
|
-
const isClosed = (h) => closedMarkerPattern.test(h) && !activeMarkerPattern.test(h);
|
|
155
|
+
const isClosed = isClosedMilestoneHeading;
|
|
103
156
|
const firstMatch = allMatches[0];
|
|
104
157
|
const selected = allMatches.find((m) => !isClosed(m[1])) || firstMatch;
|
|
105
158
|
const sectionStart = selected.index;
|
|
@@ -156,26 +209,36 @@ function extractCurrentMilestone(content, cwd) {
|
|
|
156
209
|
const detailsStart = detailsMatch.index ?? 0;
|
|
157
210
|
detailsSection = content.slice(detailsStart, computeSectionEnd(detailsMatch[0], detailsStart));
|
|
158
211
|
}
|
|
212
|
+
// #2947: the preamble strip removes `### Phase N:` detail headings from the
|
|
213
|
+
// pre-milestone region so they don't duplicate the ones inside the selected
|
|
214
|
+
// milestone section. But when the phase list lives under a non-version-bearing
|
|
215
|
+
// `## Phases` heading (the shipped greenfield template's own shape) and the
|
|
216
|
+
// selected version-bearing heading is a LATER progress/notes sub-heading with
|
|
217
|
+
// NO phase details of its own, stripping the preamble phases silently drops
|
|
218
|
+
// every phase (phase_count: 0, exit 0). Only strip preamble phase details when
|
|
219
|
+
// the selected milestone section actually contains its own — otherwise the
|
|
220
|
+
// preamble phases ARE this milestone's phases and must be preserved.
|
|
221
|
+
const currentSectionHasPhaseDetails = /^#{2,4}\s*Phase\s+\S/im.test(currentSection);
|
|
159
222
|
const preamble = (0, markdown_sectionizer_cjs_1.stripTaggedBlocks)(beforeMilestones, 'details')
|
|
160
223
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
161
|
-
.replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim
|
|
224
|
+
.replace(currentSectionHasPhaseDetails ? /^#{2,4}\s*Phase\s+[\w][\w.-]*(?:\s*\([^)\n]{0,200}\))?\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim : /$/, '')
|
|
162
225
|
.replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
|
|
163
226
|
return detailsSection
|
|
164
227
|
? preamble + currentSection + '\n' + detailsSection
|
|
165
228
|
: preamble + currentSection;
|
|
166
229
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Replace a pattern only in the current milestone section of ROADMAP.md.
|
|
169
|
-
*/
|
|
170
230
|
function replaceInCurrentMilestone(content, pattern, replacement) {
|
|
231
|
+
const apply = (src) => typeof replacement === 'function'
|
|
232
|
+
? src.replace(pattern, replacement)
|
|
233
|
+
: src.replace(pattern, replacement);
|
|
171
234
|
const lastDetailsClose = content.lastIndexOf('</details>');
|
|
172
235
|
if (lastDetailsClose === -1) {
|
|
173
|
-
return content
|
|
236
|
+
return apply(content);
|
|
174
237
|
}
|
|
175
238
|
const offset = lastDetailsClose + '</details>'.length;
|
|
176
239
|
const before = content.slice(0, offset);
|
|
177
240
|
const after = content.slice(offset);
|
|
178
|
-
return before + after
|
|
241
|
+
return before + apply(after);
|
|
179
242
|
}
|
|
180
243
|
/**
|
|
181
244
|
* Resolve a single phase's detail-section heading (`### Phase N: …`, any level
|
|
@@ -452,16 +515,24 @@ function getMilestoneInfo(cwd) {
|
|
|
452
515
|
* free-form ROADMAPs that lack versioned milestone headings. When absent or
|
|
453
516
|
* any other value, the warning is suppressed — legacy/default projects must
|
|
454
517
|
* never see spurious warnings.
|
|
518
|
+
* @param ws - #2562: workstream name, so the ROADMAP/STATE pair is read from
|
|
519
|
+
* `.planning/workstreams/<ws>/` instead of the project root. Required by any
|
|
520
|
+
* caller that iterates workstreams (it cannot set `GSD_WORKSTREAM` per
|
|
521
|
+
* iteration). Omitted (the default) preserves the prior `planningDir(cwd)`
|
|
522
|
+
* resolution exactly, including its `GSD_WORKSTREAM` env fallback — every
|
|
523
|
+
* pre-#2562 call site is unaffected.
|
|
455
524
|
*/
|
|
456
|
-
function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
525
|
+
function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention, ws) {
|
|
457
526
|
const milestonePhaseNums = new Set();
|
|
458
527
|
let missingExplicitVersion = false;
|
|
528
|
+
let versionScoped = false;
|
|
529
|
+
let versionSectionFound = false;
|
|
459
530
|
try {
|
|
460
|
-
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
531
|
+
const roadmapPath = node_path_1.default.join(planningDir(cwd, ws), 'ROADMAP.md');
|
|
461
532
|
const roadmapContent = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
|
|
462
533
|
if (roadmapContent === null)
|
|
463
534
|
throw new Error('missing');
|
|
464
|
-
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
535
|
+
let roadmap = extractCurrentMilestone(roadmapContent, cwd, ws);
|
|
465
536
|
const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
|
466
537
|
const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
|
|
467
538
|
if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
|
|
@@ -493,6 +564,8 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
493
564
|
}
|
|
494
565
|
}
|
|
495
566
|
else {
|
|
567
|
+
versionScoped = true;
|
|
568
|
+
versionSectionFound = true;
|
|
496
569
|
const sectionStart = sectionMatch.index;
|
|
497
570
|
const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
|
|
498
571
|
const afterHeading = sectionStart + sectionMatch[0].length;
|
|
@@ -552,12 +625,20 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
552
625
|
const passAll = (() => true);
|
|
553
626
|
passAll.phaseCount = 0;
|
|
554
627
|
passAll.missingExplicitVersion = missingExplicitVersion;
|
|
628
|
+
passAll.versionScoped = false;
|
|
629
|
+
// #2562: preserved through the pass-all degrade precisely BECAUSE
|
|
630
|
+
// `versionScoped` is reset here — this is the only surviving evidence that
|
|
631
|
+
// the current milestone exists in the ROADMAP and simply has no phases yet.
|
|
632
|
+
passAll.versionSectionFound = versionSectionFound;
|
|
555
633
|
return passAll;
|
|
556
634
|
}
|
|
557
|
-
const normalized = new Set([...milestonePhaseNums].map(n => n.split('-').map(seg => (seg.replace(/^0+(?=\d)/, '') || '0')).join('-').toLowerCase()));
|
|
558
635
|
function normalizePhaseIdSegments(id) {
|
|
559
636
|
return id.split('-').map(seg => seg.replace(/^0+(?=\d)/, '') || '0').join('-');
|
|
560
637
|
}
|
|
638
|
+
// #2562: derive BOTH sides of every membership comparison from
|
|
639
|
+
// normalizePhaseIdSegments. This set previously inlined a byte-identical
|
|
640
|
+
// second copy of that logic — the drift-prone shape this issue is about.
|
|
641
|
+
const normalized = new Set([...milestonePhaseNums].map(n => normalizePhaseIdSegments(n).toLowerCase()));
|
|
561
642
|
const roadmapUsesHyphenedIds = [...normalized].some(n => n.includes('-'));
|
|
562
643
|
// #2043: milestone-prefixed sub-phase components must be zero-padded — so a
|
|
563
644
|
// single-digit slug word after the phase
|
|
@@ -587,6 +668,8 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
|
|
|
587
668
|
}
|
|
588
669
|
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
589
670
|
isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
|
|
671
|
+
isDirInMilestone.versionScoped = versionScoped;
|
|
672
|
+
isDirInMilestone.versionSectionFound = versionSectionFound;
|
|
590
673
|
return isDirInMilestone;
|
|
591
674
|
}
|
|
592
675
|
/**
|
|
@@ -628,9 +711,7 @@ function currentMilestoneRawRanges(content, cwd) {
|
|
|
628
711
|
const headingMatches = [...content.matchAll(sectionPattern)];
|
|
629
712
|
if (headingMatches.length === 0)
|
|
630
713
|
return null;
|
|
631
|
-
const
|
|
632
|
-
const activeMarkerPattern = /\b(?:STARTED|ACTIVE|WIP)\b|in\s+progress|🚧|🔄/i;
|
|
633
|
-
const isClosed = (h) => closedMarkerPattern.test(h) && !activeMarkerPattern.test(h);
|
|
714
|
+
const isClosed = isClosedMilestoneHeading;
|
|
634
715
|
const firstMatch = headingMatches[0];
|
|
635
716
|
const selected = headingMatches.find((m) => !isClosed(m[1])) || firstMatch;
|
|
636
717
|
const sectionStart = selected.index ?? 0;
|
|
@@ -671,6 +752,7 @@ function currentMilestoneRawRanges(content, cwd) {
|
|
|
671
752
|
module.exports = {
|
|
672
753
|
stripShippedMilestones,
|
|
673
754
|
extractCurrentMilestone,
|
|
755
|
+
isMilestoneShippedInRoadmap,
|
|
674
756
|
replaceInCurrentMilestone,
|
|
675
757
|
getRoadmapPhaseInternal,
|
|
676
758
|
getMilestoneInfo,
|
|
@@ -265,7 +265,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
265
265
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
266
266
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
267
267
|
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
268
|
-
|
|
268
|
+
// #3036: widen the id capture to accept non-numeric-leading ids (e.g. B7, P0.3-2)
|
|
269
|
+
// that get-phase/execute-phase already resolve. An optional leading letter prefix
|
|
270
|
+
// ([A-Za-z]?) covers letter-prefixed ids without breaking numeric-leading ones.
|
|
271
|
+
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
272
|
+
const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
|
|
269
273
|
const phases = [];
|
|
270
274
|
let match;
|
|
271
275
|
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
|
|
@@ -297,8 +301,9 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
297
301
|
const sectionStart = match.index;
|
|
298
302
|
const restOfContent = content.slice(sectionStart);
|
|
299
303
|
// #3691: `\d` → `\d[\d.]*` so decimal phase headings (e.g. `### Phase 02.3:`) are
|
|
300
|
-
// recognised as section boundaries.
|
|
301
|
-
|
|
304
|
+
// recognised as section boundaries. #3036: `[A-Za-z]?\d` so non-numeric-leading ids
|
|
305
|
+
// (e.g. B7) are also recognised.
|
|
306
|
+
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+[A-Za-z]?\d[\d.-]*/i);
|
|
302
307
|
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
303
308
|
const section = content.slice(sectionStart, sectionEnd);
|
|
304
309
|
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
@@ -389,7 +394,9 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
389
394
|
// IDs (e.g. `1-01`) match the detail-heading scanner above; otherwise they truncate
|
|
390
395
|
// at the dash (`1-01` -> `1`) and every such phase reports a phantom missing detail.
|
|
391
396
|
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
392
|
-
|
|
397
|
+
// #3036: widen to accept non-numeric-leading ids (same widening as the detail-heading pattern above).
|
|
398
|
+
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
|
|
399
|
+
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)/gi;
|
|
393
400
|
const checklistPhases = new Set();
|
|
394
401
|
let checklistMatch;
|
|
395
402
|
while ((checklistMatch = checklistPattern.exec(content)) !== null) {
|
|
@@ -468,8 +475,13 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
468
475
|
// cmdPhaseComplete's gate (phase.cts:1436). Previously the checkbox fired the
|
|
469
476
|
// moment the last plan summary landed — before gsd-verifier had verified.
|
|
470
477
|
const phaseDir = node_path_1.default.join(cwd, phaseInfo.directory);
|
|
471
|
-
const
|
|
478
|
+
const verificationResult = readVerificationStatus(phaseDir);
|
|
479
|
+
const verificationPassed = verificationResult.status === 'passed';
|
|
472
480
|
const isComplete = summaryCount >= planCount && verificationPassed;
|
|
481
|
+
// #3057 B3: routing above is unchanged (an indeterminate staleness check
|
|
482
|
+
// still routes as if nothing were stale) — this only makes the fact visible
|
|
483
|
+
// to whatever reads this command's JSON output.
|
|
484
|
+
const verificationStaleCheckIndeterminate = verificationResult.staleCheckIndeterminate === true;
|
|
473
485
|
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
|
474
486
|
const today = clock_cjs_1.realClock.localToday();
|
|
475
487
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
@@ -526,11 +538,28 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
526
538
|
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
527
539
|
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
528
540
|
// `Plans: N plans` — plain text header
|
|
529
|
-
|
|
541
|
+
//
|
|
542
|
+
// #2853: the verb owns the count token ONLY — it must not destroy hand-written
|
|
543
|
+
// prose a human placed after the count (e.g. "(11-16 are gap closure ...)").
|
|
544
|
+
// Group $1 = phase header → `Plans:` label + trailing whitespace (unchanged).
|
|
545
|
+
// Group $2 = the existing count token to replace: matches `N/N plans complete`,
|
|
546
|
+
// `N/N plans executed`, or the bare template `N plans` form. Group $3 = whatever
|
|
547
|
+
// else is on the line (`[^\r\n]*`, so CRLF `\r` is preserved).
|
|
548
|
+
//
|
|
549
|
+
// Trailing text is preserved ONLY when a real count token ($2) was present —
|
|
550
|
+
// i.e. an annotation a human wrote after a real count. When $2 is absent the
|
|
551
|
+
// line is the fresh-template bracketed placeholder (`[Number of plans…]`) or
|
|
552
|
+
// other freeform guidance, not user prose: the count replaces the whole token,
|
|
553
|
+
// preserving the pre-#2853 clean-output behaviour on the template path.
|
|
554
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}(?=[:\\s])(?:(?!\\n#{1,4}\\s)[\\s\\S])*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)(\\d+\\s*\\/\\s*\\d+\\s+plans(?:\\s+(?:complete|executed))?|\\d+\\s+plans)?([^\\r\\n]*)`, 'i');
|
|
530
555
|
const planCountText = isComplete
|
|
531
556
|
? `${summaryCount}/${planCount} plans complete`
|
|
532
557
|
: `${summaryCount}/${planCount} plans executed`;
|
|
533
|
-
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern,
|
|
558
|
+
roadmapContent = replaceInCurrentMilestone(roadmapContent, planCountPattern, (_match, label, existingCount, trailing) => {
|
|
559
|
+
// Preserve trailing text only when a real count preceded it.
|
|
560
|
+
const suffix = existingCount ? trailing : '';
|
|
561
|
+
return `${label}${planCountText}${suffix}`;
|
|
562
|
+
});
|
|
534
563
|
// If complete: check checkbox
|
|
535
564
|
if (isComplete) {
|
|
536
565
|
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phasePattern}${OPTIONAL_PHASE_TAG_SOURCE}[:\\s][^\\n]*)`, 'i');
|
|
@@ -628,6 +657,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
628
657
|
summary_count: summaryCount,
|
|
629
658
|
status,
|
|
630
659
|
complete: isComplete,
|
|
660
|
+
verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
|
|
631
661
|
}, raw, `${summaryCount}/${planCount} ${status}`);
|
|
632
662
|
}
|
|
633
663
|
// ─── cmdRoadmapAnnotateDependencies ───────────────────────────────────────────
|