@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.
Files changed (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-mempalace-curator.md +47 -0
  3. package/bin/install.js +25 -7
  4. package/commands/gsd/mempalace-capture.md +71 -0
  5. package/commands/gsd/mempalace-recall.md +102 -0
  6. package/commands/gsd/ns-context.md +4 -2
  7. package/gemini-extension.json +1 -1
  8. package/gsd-core/bin/gsd-tools.cjs +110 -92
  9. package/gsd-core/bin/lib/capability-activation.cjs +86 -0
  10. package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
  11. package/gsd-core/bin/lib/capability-state.cjs +31 -18
  12. package/gsd-core/bin/lib/check-command-router.cjs +328 -1
  13. package/gsd-core/bin/lib/clusters.cjs +2 -0
  14. package/gsd-core/bin/lib/command-roster.cjs +19 -0
  15. package/gsd-core/bin/lib/commands.cjs +33 -10
  16. package/gsd-core/bin/lib/config-loader.cjs +7 -8
  17. package/gsd-core/bin/lib/config-schema.cjs +32 -3
  18. package/gsd-core/bin/lib/config.cjs +1 -2
  19. package/gsd-core/bin/lib/core.cjs +5 -2
  20. package/gsd-core/bin/lib/init.cjs +8 -5
  21. package/gsd-core/bin/lib/install-profiles.cjs +2 -1
  22. package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
  23. package/gsd-core/bin/lib/model-resolver.cjs +47 -5
  24. package/gsd-core/bin/lib/phase.cjs +71 -11
  25. package/gsd-core/bin/lib/profile-output.cjs +45 -4
  26. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
  27. package/gsd-core/bin/lib/roadmap.cjs +97 -7
  28. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1700 -0
  29. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +20 -28
  30. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
  31. package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
  32. package/gsd-core/bin/lib/state-command-router.cjs +15 -3
  33. package/gsd-core/bin/lib/state-document.cjs +46 -1
  34. package/gsd-core/bin/lib/state.cjs +221 -35
  35. package/gsd-core/bin/lib/verify.cjs +19 -4
  36. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
  37. package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
  38. package/gsd-core/bin/shared/model-catalog.json +1 -0
  39. package/gsd-core/references/loop-hook-dispatch.md +61 -0
  40. package/gsd-core/templates/config.json +1 -1
  41. package/gsd-core/workflows/audit-milestone.md +5 -3
  42. package/gsd-core/workflows/autonomous.md +10 -5
  43. package/gsd-core/workflows/code-review-fix.md +9 -7
  44. package/gsd-core/workflows/code-review.md +8 -6
  45. package/gsd-core/workflows/discuss-phase.md +14 -0
  46. package/gsd-core/workflows/execute-phase.md +82 -143
  47. package/gsd-core/workflows/help/modes/full.md +3 -1
  48. package/gsd-core/workflows/new-project.md +3 -3
  49. package/gsd-core/workflows/plan-phase.md +89 -221
  50. package/gsd-core/workflows/profile-user.md +3 -1
  51. package/gsd-core/workflows/progress.md +58 -1
  52. package/gsd-core/workflows/quick.md +9 -6
  53. package/gsd-core/workflows/review.md +19 -2
  54. package/gsd-core/workflows/secure-phase.md +4 -2
  55. package/gsd-core/workflows/settings-advanced.md +2 -0
  56. package/gsd-core/workflows/ship.md +57 -0
  57. package/gsd-core/workflows/validate-phase.md +4 -2
  58. package/gsd-core/workflows/verify-work.md +27 -11
  59. package/package.json +3 -2
  60. package/scripts/affected-tests-lib.cjs +16 -4
  61. package/scripts/gen-capability-registry.cjs +359 -48
  62. package/scripts/gen-inventory-manifest.cjs +1 -4
  63. package/scripts/gen-loop-host-contract.cjs +55 -0
  64. package/scripts/issue-version-gate.cjs +140 -0
  65. package/scripts/lint-allow-test-rule-refs.allowlist.json +326 -0
  66. package/scripts/lint-allow-test-rule-refs.cjs +162 -0
  67. package/scripts/lint-test-file-count.allowlist.json +8 -0
  68. package/scripts/release-tarball-smoke.cjs +7 -1
  69. 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
- outputPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'gsd-core', 'USER-PROFILE.md');
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, default to ./CLAUDE.md
915
- let configClaudeMdPath = './CLAUDE.md';
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
- let configClaudeMdPath = './CLAUDE.md';
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 = new Date().toISOString().split('T')[0];
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
- cells[4] = dateField;
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
- cells[3] = dateField;
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
- const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, 'i');
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
- const newPhaseSection = phaseSection.replace(plansBlockMatch[0], plansHeader + newListBlock);
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;