@opengsd/gsd-core 1.5.0-rc.2 → 1.5.0-rc.3
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/agents/gsd-mempalace-curator.md +47 -0
- package/bin/install.js +25 -7
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +110 -92
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +31 -18
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +1 -2
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/init.cjs +8 -5
- package/gsd-core/bin/lib/install-profiles.cjs +2 -1
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +71 -11
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +221 -35
- package/gsd-core/bin/lib/verify.cjs +19 -4
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +82 -143
- package/gsd-core/workflows/help/modes/full.md +3 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/plan-phase.md +89 -221
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +9 -6
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/ship.md +57 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-work.md +27 -11
- package/package.json +3 -2
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/gen-capability-registry.cjs +359 -48
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +8 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- package/scripts/run-tests.cjs +74 -14
|
@@ -653,7 +653,21 @@ function cmdWriteProfile(cwd, options, raw) {
|
|
|
653
653
|
}
|
|
654
654
|
let outputPath = options.output;
|
|
655
655
|
if (!outputPath) {
|
|
656
|
-
|
|
656
|
+
// #1114: resolve the ACTIVE runtime's config home so the profile is written
|
|
657
|
+
// where the runtime's own workflows look for it. Previously this hardcoded the
|
|
658
|
+
// Claude home, so a Codex run wrote the profile under the Claude config dir
|
|
659
|
+
// while Codex advisor-mode (installed under the Codex home) checked the Codex
|
|
660
|
+
// dir and never found it. Mirrors cmdGenerateDevPreferences' runtime resolution.
|
|
661
|
+
let effectiveRuntime = 'claude';
|
|
662
|
+
try {
|
|
663
|
+
const config = loadConfig(cwd);
|
|
664
|
+
effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime'], 'claude') || 'claude';
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], 'claude') || 'claude';
|
|
668
|
+
}
|
|
669
|
+
// path.join (not a string literal) keeps the cline-install leaked-path lint quiet.
|
|
670
|
+
outputPath = node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)(effectiveRuntime), 'gsd-core', 'USER-PROFILE.md');
|
|
657
671
|
}
|
|
658
672
|
else if (!node_path_1.default.isAbsolute(outputPath)) {
|
|
659
673
|
outputPath = node_path_1.default.join(cwd, outputPath);
|
|
@@ -911,8 +925,10 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
|
|
|
911
925
|
targetPath = node_path_1.default.isAbsolute(options.output) ? options.output : node_path_1.default.join(cwd, options.output);
|
|
912
926
|
}
|
|
913
927
|
else {
|
|
914
|
-
// Read claude_md_path from config
|
|
915
|
-
|
|
928
|
+
// Read claude_md_path from config; #1098 default is ./.claude/CLAUDE.md
|
|
929
|
+
// (kept consistent with cmdGenerateClaudeMd so the profile section and the
|
|
930
|
+
// managed sections land in the same file on a config-less project).
|
|
931
|
+
let configClaudeMdPath = './.claude/CLAUDE.md';
|
|
916
932
|
try {
|
|
917
933
|
const config = loadConfig(cwd);
|
|
918
934
|
if (config['claude_md_path'])
|
|
@@ -986,7 +1002,12 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
986
1002
|
}
|
|
987
1003
|
}
|
|
988
1004
|
let assemblyConfig = {};
|
|
989
|
-
|
|
1005
|
+
// #1098: default the Claude-family instruction file to the project-scoped
|
|
1006
|
+
// `.claude/CLAUDE.md` (a valid auto-loaded memory location) rather than a
|
|
1007
|
+
// repo-root `CLAUDE.md`, so generated GSD content does not land next to — or
|
|
1008
|
+
// pollute — a hand-crafted repo-root CLAUDE.md. An explicit `claude_md_path`
|
|
1009
|
+
// config value or `--output` still wins.
|
|
1010
|
+
let configClaudeMdPath = './.claude/CLAUDE.md';
|
|
990
1011
|
try {
|
|
991
1012
|
const config = loadConfig(cwd);
|
|
992
1013
|
if (config['claude_md_path'])
|
|
@@ -1037,6 +1058,26 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1037
1058
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.dirname(outputPath));
|
|
1038
1059
|
(0, shell_command_projection_cjs_1.platformWriteSync)(outputPath, existingContent);
|
|
1039
1060
|
}
|
|
1061
|
+
else if (!/<!-- GSD:[a-z]+-start/.test(existingContent) && !options.force) {
|
|
1062
|
+
// #1098: the target instruction file already exists and contains NO GSD
|
|
1063
|
+
// section markers — it is a hand-crafted CLAUDE.md/AGENTS.md, not a
|
|
1064
|
+
// GSD-managed one. Do NOT clobber it with generated project documentation
|
|
1065
|
+
// (broad project detail belongs in PROJECT.md / REQUIREMENTS.md, which GSD
|
|
1066
|
+
// already owns). Leave the file untouched and report a skip; `--force`
|
|
1067
|
+
// overwrites intentionally.
|
|
1068
|
+
output({
|
|
1069
|
+
claude_md_path: outputPath,
|
|
1070
|
+
action: 'skipped',
|
|
1071
|
+
reason: 'existing instruction file has no GSD markers (hand-crafted); not overwriting. Pass --force to overwrite.',
|
|
1072
|
+
sections_generated: [],
|
|
1073
|
+
sections_fallback: [],
|
|
1074
|
+
sections_skipped: MANAGED_SECTIONS,
|
|
1075
|
+
sections_total: MANAGED_SECTIONS.length,
|
|
1076
|
+
profile_status: 'skipped',
|
|
1077
|
+
message: `Left existing ${node_path_1.default.basename(outputPath)} untouched (no GSD markers found). Broad project context lives in PROJECT.md / REQUIREMENTS.md; pass --force to overwrite this file with GSD-managed sections.`,
|
|
1078
|
+
}, raw, undefined);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1040
1081
|
else {
|
|
1041
1082
|
action = 'updated';
|
|
1042
1083
|
let fileContent = existingContent;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Profile-pipeline command router — CLI dispatcher for gsd-tools profiling commands.
|
|
4
|
+
*
|
|
5
|
+
* ADR-857 phase 6 / ADR-959: profile-pipeline capability command cutover.
|
|
6
|
+
* Extracted from hardcoded case arms in gsd-tools.cjs (lines 1324-1410).
|
|
7
|
+
* Dispatch path: default → dispatchCapabilityCommand →
|
|
8
|
+
* require(profile-pipeline-command-router.cjs) → route<X>.
|
|
9
|
+
*
|
|
10
|
+
* Router signature: { args, cwd, raw, error } — identical to existing routers.
|
|
11
|
+
* Test seams: _pipeline / _output inject mock modules; _core injects mock core.
|
|
12
|
+
*
|
|
13
|
+
* Async note: cmdExtractMessages and cmdProfileSample are async functions.
|
|
14
|
+
* dispatchCapabilityCommand (gsd-tools.cjs:366-371) explicitly errors if a
|
|
15
|
+
* router returns a Promise. Therefore these router functions call the async
|
|
16
|
+
* function WITHOUT await and WITHOUT returning the Promise. The async functions
|
|
17
|
+
* end with output() or process.exit() so the process terminates correctly once
|
|
18
|
+
* the event loop drains. Unhandled rejections are caught by the .catch() wrapper
|
|
19
|
+
* to surface errors via the error() callback.
|
|
20
|
+
*/
|
|
21
|
+
const { ERROR_REASON } = require('./io.cjs');
|
|
22
|
+
|
|
23
|
+
// ─── Pipeline phase commands ───────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function routeScanSessions({ args, cwd, raw, error, _pipeline }) {
|
|
26
|
+
void cwd; void error;
|
|
27
|
+
const p = _pipeline ?? require('./profile-pipeline.cjs');
|
|
28
|
+
const pathIdx = args.indexOf('--path');
|
|
29
|
+
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
|
|
30
|
+
const verboseFlag = args.includes('--verbose');
|
|
31
|
+
const jsonFlag = args.includes('--json');
|
|
32
|
+
// cmdScanSessions is synchronous — call directly.
|
|
33
|
+
p.cmdScanSessions(sessionsPath, { verbose: verboseFlag, json: jsonFlag }, raw);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function routeExtractMessages({ args, cwd, raw, error, _pipeline }) {
|
|
37
|
+
const p = _pipeline ?? require('./profile-pipeline.cjs');
|
|
38
|
+
const sessionIdx = args.indexOf('--session');
|
|
39
|
+
const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : null;
|
|
40
|
+
const limitIdx = args.indexOf('--limit');
|
|
41
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : null;
|
|
42
|
+
const pathIdx = args.indexOf('--path');
|
|
43
|
+
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
|
|
44
|
+
// args[0] = 'extract-messages' (family name), args[1] = project positional
|
|
45
|
+
const projectArg = args[1];
|
|
46
|
+
if (!projectArg || projectArg.startsWith('--')) {
|
|
47
|
+
error('Usage: gsd-tools extract-messages <project> [--session <id>] [--limit N] [--path <dir>]\nRun scan-sessions first to see available projects.', ERROR_REASON.USAGE);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// cmdExtractMessages is async — do NOT return the Promise.
|
|
51
|
+
// The function ends with output() or process.exit(); the event loop will drain.
|
|
52
|
+
void cwd;
|
|
53
|
+
p.cmdExtractMessages(projectArg, { sessionId, limit }, raw, sessionsPath)
|
|
54
|
+
.catch(e => { error(e && e.message ? e.message : String(e)); });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function routeProfileSample({ args, cwd, raw, error, _pipeline }) {
|
|
58
|
+
void cwd; void error;
|
|
59
|
+
const p = _pipeline ?? require('./profile-pipeline.cjs');
|
|
60
|
+
const pathIdx = args.indexOf('--path');
|
|
61
|
+
const sessionsPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
|
|
62
|
+
const limitIdx = args.indexOf('--limit');
|
|
63
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 150;
|
|
64
|
+
const maxPerIdx = args.indexOf('--max-per-project');
|
|
65
|
+
const maxPerProject = maxPerIdx !== -1 ? parseInt(args[maxPerIdx + 1], 10) : null;
|
|
66
|
+
const maxCharsIdx = args.indexOf('--max-chars');
|
|
67
|
+
const maxChars = maxCharsIdx !== -1 ? parseInt(args[maxCharsIdx + 1], 10) : 500;
|
|
68
|
+
// cmdProfileSample is async — do NOT return the Promise.
|
|
69
|
+
p.cmdProfileSample(sessionsPath, { limit, maxPerProject, maxChars }, raw)
|
|
70
|
+
.catch(e => { error(e && e.message ? e.message : String(e)); });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Output phase commands ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function routeWriteProfile({ args, cwd, raw, error, _output }) {
|
|
76
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
77
|
+
const inputIdx = args.indexOf('--input');
|
|
78
|
+
const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
|
|
79
|
+
if (!inputPath) {
|
|
80
|
+
error('--input <analysis-json-path> is required', ERROR_REASON.USAGE);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const outputIdx = args.indexOf('--output');
|
|
84
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
85
|
+
o.cmdWriteProfile(cwd, { input: inputPath, output: outputPath }, raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function routeProfileQuestionnaire({ args, cwd, raw, error, _output }) {
|
|
89
|
+
void cwd; void error;
|
|
90
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
91
|
+
const answersIdx = args.indexOf('--answers');
|
|
92
|
+
const answers = answersIdx !== -1 ? args[answersIdx + 1] : null;
|
|
93
|
+
o.cmdProfileQuestionnaire({ answers }, raw);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function routeGenerateDevPreferences({ args, cwd, raw, error, _output }) {
|
|
97
|
+
void error;
|
|
98
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
99
|
+
const analysisIdx = args.indexOf('--analysis');
|
|
100
|
+
const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
|
|
101
|
+
const outputIdx = args.indexOf('--output');
|
|
102
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
103
|
+
const stackIdx = args.indexOf('--stack');
|
|
104
|
+
const stack = stackIdx !== -1 ? args[stackIdx + 1] : null;
|
|
105
|
+
o.cmdGenerateDevPreferences(cwd, { analysis: analysisPath, output: outputPath, stack }, raw);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function routeGenerateClaudeProfile({ args, cwd, raw, error, _output }) {
|
|
109
|
+
void error;
|
|
110
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
111
|
+
const analysisIdx = args.indexOf('--analysis');
|
|
112
|
+
const analysisPath = analysisIdx !== -1 ? args[analysisIdx + 1] : null;
|
|
113
|
+
const outputIdx = args.indexOf('--output');
|
|
114
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
115
|
+
const globalFlag = args.includes('--global');
|
|
116
|
+
o.cmdGenerateClaudeProfile(cwd, { analysis: analysisPath, output: outputPath, global: globalFlag }, raw);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function routeGenerateClaudeMd({ args, cwd, raw, error, _output }) {
|
|
120
|
+
void error;
|
|
121
|
+
const o = _output ?? require('./profile-output.cjs');
|
|
122
|
+
const outputIdx = args.indexOf('--output');
|
|
123
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
|
|
124
|
+
const autoFlag = args.includes('--auto');
|
|
125
|
+
const forceFlag = args.includes('--force');
|
|
126
|
+
o.cmdGenerateClaudeMd(cwd, { output: outputPath, auto: autoFlag, force: forceFlag }, raw);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
routeScanSessions,
|
|
131
|
+
routeExtractMessages,
|
|
132
|
+
routeProfileSample,
|
|
133
|
+
routeWriteProfile,
|
|
134
|
+
routeProfileQuestionnaire,
|
|
135
|
+
routeGenerateDevPreferences,
|
|
136
|
+
routeGenerateClaudeProfile,
|
|
137
|
+
routeGenerateClaudeMd,
|
|
138
|
+
};
|
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
};
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
14
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
16
|
const core = require("./core.cjs");
|
|
16
17
|
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, output, error, findPhaseInternal, phaseTokenMatches } = core;
|
|
@@ -386,7 +387,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
386
387
|
}
|
|
387
388
|
const isComplete = summaryCount >= planCount;
|
|
388
389
|
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
|
389
|
-
const today =
|
|
390
|
+
const today = clock_cjs_1.realClock.today();
|
|
390
391
|
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
391
392
|
output({ updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
|
|
392
393
|
return;
|
|
@@ -397,25 +398,37 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
397
398
|
const phasePattern = phaseMarkdownRegexSource(phaseNum);
|
|
398
399
|
// Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables)
|
|
399
400
|
const tableRowPattern = new RegExp(`^(\\|\\s*${phasePattern}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, 'im');
|
|
400
|
-
const dateField = isComplete ? ` ${today} ` : ' ';
|
|
401
401
|
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
402
402
|
const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
|
|
403
|
+
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
|
403
404
|
if (cells.length === 5) {
|
|
404
405
|
// 5-col: Phase | Milestone | Plans | Status | Completed
|
|
405
406
|
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
406
407
|
cells[3] = ` ${status.padEnd(11)}`;
|
|
407
|
-
|
|
408
|
+
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
|
409
|
+
const existingDate5 = cells[4].trim();
|
|
410
|
+
cells[4] = isComplete
|
|
411
|
+
? (dateShape.test(existingDate5) ? cells[4] : ` ${today} `)
|
|
412
|
+
: ' ';
|
|
408
413
|
}
|
|
409
414
|
else if (cells.length === 4) {
|
|
410
415
|
// 4-col: Phase | Plans | Status | Completed
|
|
411
416
|
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
412
417
|
cells[2] = ` ${status.padEnd(11)}`;
|
|
413
|
-
|
|
418
|
+
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
|
419
|
+
const existingDate4 = cells[3].trim();
|
|
420
|
+
cells[3] = isComplete
|
|
421
|
+
? (dateShape.test(existingDate4) ? cells[3] : ` ${today} `)
|
|
422
|
+
: ' ';
|
|
414
423
|
}
|
|
415
424
|
return '|' + cells.join('|') + '|';
|
|
416
425
|
});
|
|
417
|
-
// Update plan count in phase detail section
|
|
418
|
-
|
|
426
|
+
// Update plan count in phase detail section.
|
|
427
|
+
// Three recognised forms (all tolerated; canonical template uses the first):
|
|
428
|
+
// `**Plans**: N plans` — bold word + outer colon (gsd-core/templates/roadmap.md)
|
|
429
|
+
// `**Plans:** N plans` — bold "Plans:" (colon inside bold)
|
|
430
|
+
// `Plans: N plans` — plain text header
|
|
431
|
+
const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)\\s*)[^\\n]+`, 'i');
|
|
419
432
|
const planCountText = isComplete
|
|
420
433
|
? `${summaryCount}/${planCount} plans complete`
|
|
421
434
|
: `${summaryCount}/${planCount} plans executed`;
|
|
@@ -434,6 +447,80 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
434
447
|
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
435
448
|
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
436
449
|
}
|
|
450
|
+
// Compute the active (post-</details>) region offset ONCE. Both the
|
|
451
|
+
// missing-plan DETECTION and the row INSERTION must use the same active
|
|
452
|
+
// region string so that a plan row that exists only in an archived <details>
|
|
453
|
+
// block is not counted as "already present" in the active milestone section.
|
|
454
|
+
// (Finding 1 code-review: detection was previously running against the full
|
|
455
|
+
// roadmapContent, causing archived rows to suppress active-section inserts.)
|
|
456
|
+
const lastDetailsClose = roadmapContent.lastIndexOf('</details>');
|
|
457
|
+
const activeRegion = lastDetailsClose === -1
|
|
458
|
+
? roadmapContent
|
|
459
|
+
: roadmapContent.slice(lastDetailsClose + '</details>'.length);
|
|
460
|
+
// Compute which plan files are MISSING a checkbox row in the ACTIVE region.
|
|
461
|
+
// This handles three cases:
|
|
462
|
+
// (a) Fresh template — no rows at all: all plans are missing.
|
|
463
|
+
// (b) Partial gap — some rows exist, others don't: only the absent ones.
|
|
464
|
+
// (c) All rows present — nothing to insert (idempotent).
|
|
465
|
+
//
|
|
466
|
+
// Detection is scoped to the active region so a plan that appears in an
|
|
467
|
+
// archived <details> block is still correctly detected as missing from the
|
|
468
|
+
// active milestone section.
|
|
469
|
+
const missingPlans = phaseInfo.plans.filter((planFile) => {
|
|
470
|
+
const planEscaped = escapeRegex(planFile);
|
|
471
|
+
return !new RegExp(`-\\s*\\[[x ]\\]\\s*(?:\\*\\*)?${planEscaped}`, 'i').test(activeRegion);
|
|
472
|
+
});
|
|
473
|
+
if (missingPlans.length > 0) {
|
|
474
|
+
// Insert missing plan checklist rows (#1163). We prefer to anchor to the
|
|
475
|
+
// bare `Plans:` checklist header (canonical template form) and fall back to
|
|
476
|
+
// the bold `**Plans**:`/`**Plans:**` summary line only when no bare header
|
|
477
|
+
// is present. Using two separate patterns avoids the lazy-quantifier trap
|
|
478
|
+
// where a single alternation would stop at the first matching alternative
|
|
479
|
+
// (the bold summary) before reaching the checklist header.
|
|
480
|
+
//
|
|
481
|
+
// Canonical template (gsd-core/templates/roadmap.md) uses BOTH lines:
|
|
482
|
+
// **Plans**: N plans ← summary (colon outside bold)
|
|
483
|
+
// Plans: ← checklist header (PREFERRED insertion anchor)
|
|
484
|
+
// Rows must land after `Plans:`, not between the summary and the header.
|
|
485
|
+
//
|
|
486
|
+
// Pattern A: anchor to bare `Plans:` header (preferred).
|
|
487
|
+
// Pattern B: fallback to bold summary when no bare header exists.
|
|
488
|
+
const insertRowsPatternA = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:^|\\n)(?:Plans:)[^\\n]*)`, 'i');
|
|
489
|
+
const insertRowsPatternB = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*)[^\\n]*)`, 'i');
|
|
490
|
+
const sortedMissing = [...missingPlans].sort();
|
|
491
|
+
const newRows = sortedMissing.map(p => `- [ ] ${p}`).join('\n');
|
|
492
|
+
const inserter = (match) => `${match}\n${newRows}`;
|
|
493
|
+
// Scope insertion to the active (post-</details>) milestone region to
|
|
494
|
+
// prevent duplicate phase headings in archived sections from receiving rows.
|
|
495
|
+
// replaceInCurrentMilestone only accepts a string replacement, so we
|
|
496
|
+
// perform the scoped replace manually here (same strategy as that helper).
|
|
497
|
+
// Note: lastDetailsClose was computed above (shared with detection).
|
|
498
|
+
const scopedReplace = (src, pat) => src.replace(pat, inserter);
|
|
499
|
+
let withRows;
|
|
500
|
+
if (lastDetailsClose === -1) {
|
|
501
|
+
// activeRegion === roadmapContent when there are no </details> blocks.
|
|
502
|
+
const regionA = scopedReplace(activeRegion, insertRowsPatternA);
|
|
503
|
+
withRows = regionA !== activeRegion ? regionA : scopedReplace(activeRegion, insertRowsPatternB);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const beforeDetails = roadmapContent.slice(0, lastDetailsClose + '</details>'.length);
|
|
507
|
+
const regionA = scopedReplace(activeRegion, insertRowsPatternA);
|
|
508
|
+
const afterWithRows = regionA !== activeRegion ? regionA : scopedReplace(activeRegion, insertRowsPatternB);
|
|
509
|
+
withRows = beforeDetails + afterWithRows;
|
|
510
|
+
}
|
|
511
|
+
if (withRows !== roadmapContent) {
|
|
512
|
+
roadmapContent = withRows;
|
|
513
|
+
// Mark any newly-inserted rows that already have summaries as complete
|
|
514
|
+
for (const summaryFile of phaseInfo.summaries) {
|
|
515
|
+
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
516
|
+
if (!planId)
|
|
517
|
+
continue;
|
|
518
|
+
const planEscaped = escapeRegex(planId);
|
|
519
|
+
const planCheckboxPattern = new RegExp(`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i');
|
|
520
|
+
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
437
524
|
(0, shell_command_projection_cjs_1.platformWriteSync)(roadmapPath, roadmapContent);
|
|
438
525
|
});
|
|
439
526
|
output({
|
|
@@ -620,7 +707,10 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|
|
620
707
|
}
|
|
621
708
|
}
|
|
622
709
|
const newListBlock = annotatedLines.join('\n') + '\n';
|
|
623
|
-
|
|
710
|
+
// #1103: when `(?:^|\n)` consumed a leading `\n` (mid-string match), re-emit it
|
|
711
|
+
// so the line preceding the Plans: header is not fused onto it.
|
|
712
|
+
const leadingNewline = plansBlockMatch[0].startsWith('\n') ? '\n' : '';
|
|
713
|
+
const newPhaseSection = phaseSection.replace(plansBlockMatch[0], leadingNewline + plansHeader + newListBlock);
|
|
624
714
|
const nextContent = content.slice(0, phaseStart) + newPhaseSection + content.slice(phaseEnd);
|
|
625
715
|
if (nextContent === content)
|
|
626
716
|
return;
|