@opengsd/gsd-core 1.16.0 → 1.18.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.
Files changed (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -2
  3. package/.opencode/plugins/gsd-core.js +251 -1
  4. package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
  5. package/GEMINI.md +1 -1
  6. package/agents/gsd-advisor-researcher.compact.md +1 -1
  7. package/agents/gsd-advisor-researcher.md +1 -1
  8. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  9. package/agents/gsd-assumptions-analyzer.md +1 -1
  10. package/agents/gsd-code-fixer.compact.md +1 -1
  11. package/agents/gsd-code-fixer.md +1 -1
  12. package/agents/gsd-code-reviewer.compact.md +1 -1
  13. package/agents/gsd-code-reviewer.md +1 -1
  14. package/agents/gsd-codebase-mapper.compact.md +1 -1
  15. package/agents/gsd-codebase-mapper.md +1 -1
  16. package/agents/gsd-debug-session-manager.compact.md +6 -0
  17. package/agents/gsd-debug-session-manager.md +6 -0
  18. package/agents/gsd-debugger.md +1 -1
  19. package/agents/gsd-doc-verifier.compact.md +4 -4
  20. package/agents/gsd-doc-verifier.md +4 -3
  21. package/agents/gsd-doc-writer.compact.md +23 -5
  22. package/agents/gsd-doc-writer.md +19 -6
  23. package/agents/gsd-eval-auditor.compact.md +1 -1
  24. package/agents/gsd-eval-auditor.md +1 -1
  25. package/agents/gsd-executor.md +7 -3
  26. package/agents/gsd-integration-checker.compact.md +1 -1
  27. package/agents/gsd-integration-checker.md +1 -1
  28. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  29. package/agents/gsd-nyquist-auditor.md +1 -1
  30. package/agents/gsd-phase-researcher.md +1 -1
  31. package/agents/gsd-plan-checker.md +2 -1
  32. package/agents/gsd-planner.md +5 -4
  33. package/agents/gsd-project-researcher.compact.md +1 -1
  34. package/agents/gsd-project-researcher.md +1 -1
  35. package/agents/gsd-research-synthesizer.compact.md +5 -5
  36. package/agents/gsd-research-synthesizer.md +5 -5
  37. package/agents/gsd-roadmapper.compact.md +1 -1
  38. package/agents/gsd-roadmapper.md +1 -1
  39. package/agents/gsd-security-auditor.compact.md +1 -1
  40. package/agents/gsd-security-auditor.md +1 -1
  41. package/agents/gsd-ui-auditor.compact.md +1 -1
  42. package/agents/gsd-ui-auditor.md +1 -1
  43. package/agents/gsd-ui-checker.compact.md +1 -1
  44. package/agents/gsd-ui-checker.md +1 -1
  45. package/agents/gsd-ui-researcher.compact.md +13 -1
  46. package/agents/gsd-ui-researcher.md +28 -1
  47. package/agents/gsd-verifier.md +1 -1
  48. package/bin/gsd-mcp-server.js +3 -1
  49. package/bin/install.js +398 -95
  50. package/commands/gsd/code-review.md +8 -0
  51. package/commands/gsd/pause-work.md +1 -1
  52. package/gsd-core/bin/gsd-tools.cjs +34 -12
  53. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  54. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  55. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  56. package/gsd-core/bin/lib/capability-registry.cjs +138 -80
  57. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  58. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  59. package/gsd-core/bin/lib/capability-validator.cjs +8 -5
  60. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  61. package/gsd-core/bin/lib/commands.cjs +33 -9
  62. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  63. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  64. package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
  65. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  66. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  67. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  68. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  69. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  70. package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
  71. package/gsd-core/bin/lib/graphify.cjs +3 -2
  72. package/gsd-core/bin/lib/init.cjs +43 -16
  73. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  74. package/gsd-core/bin/lib/install-engine.cjs +11 -0
  75. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  76. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  77. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  78. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  79. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  80. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  81. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  82. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  83. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  84. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  85. package/gsd-core/bin/lib/phase.cjs +141 -11
  86. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
  87. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  88. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  89. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  90. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  91. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
  92. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  93. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
  94. package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
  95. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  96. package/gsd-core/bin/lib/smart-entry.cjs +100 -53
  97. package/gsd-core/bin/lib/state-document.cjs +37 -3
  98. package/gsd-core/bin/lib/state-transition.cjs +98 -19
  99. package/gsd-core/bin/lib/state.cjs +544 -126
  100. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  101. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  102. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  103. package/gsd-core/bin/lib/update-context.cjs +18 -1
  104. package/gsd-core/bin/lib/verification.cjs +107 -17
  105. package/gsd-core/bin/lib/verify.cjs +36 -1
  106. package/gsd-core/references/agent-contracts.md +1 -1
  107. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  108. package/gsd-core/references/git-integration.md +3 -3
  109. package/gsd-core/references/planner-reversibility.md +18 -4
  110. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  111. package/gsd-core/references/tdd.md +4 -3
  112. package/gsd-core/references/verifier-phase-gates.md +8 -1
  113. package/gsd-core/workflows/add-backlog.md +10 -2
  114. package/gsd-core/workflows/add-phase.md +5 -1
  115. package/gsd-core/workflows/add-tests.md +5 -1
  116. package/gsd-core/workflows/add-todo.md +5 -1
  117. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  118. package/gsd-core/workflows/audit-milestone.md +21 -9
  119. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  120. package/gsd-core/workflows/autonomous.md +52 -64
  121. package/gsd-core/workflows/check-todos.md +5 -1
  122. package/gsd-core/workflows/code-review-fix.md +5 -1
  123. package/gsd-core/workflows/code-review.md +21 -2
  124. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  125. package/gsd-core/workflows/complete-milestone.md +25 -5
  126. package/gsd-core/workflows/debug.md +5 -1
  127. package/gsd-core/workflows/diagnose-issues.md +5 -1
  128. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  129. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  130. package/gsd-core/workflows/discuss-phase.md +5 -1
  131. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  132. package/gsd-core/workflows/docs-update.md +27 -5
  133. package/gsd-core/workflows/edit-phase.md +5 -1
  134. package/gsd-core/workflows/eval-review.md +11 -3
  135. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  136. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  137. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  138. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  139. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  140. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  141. package/gsd-core/workflows/execute-phase.md +40 -7
  142. package/gsd-core/workflows/execute-plan.md +8 -4
  143. package/gsd-core/workflows/extract-learnings.md +6 -2
  144. package/gsd-core/workflows/forensics.md +5 -1
  145. package/gsd-core/workflows/import.md +5 -1
  146. package/gsd-core/workflows/insert-phase.md +5 -1
  147. package/gsd-core/workflows/manager.md +10 -2
  148. package/gsd-core/workflows/map-codebase.md +5 -1
  149. package/gsd-core/workflows/milestone-summary.md +10 -2
  150. package/gsd-core/workflows/new-milestone.md +13 -5
  151. package/gsd-core/workflows/new-project.md +9 -5
  152. package/gsd-core/workflows/next.md +8 -4
  153. package/gsd-core/workflows/pause-work.md +3 -3
  154. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  155. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  156. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  157. package/gsd-core/workflows/plan-phase.md +41 -73
  158. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  159. package/gsd-core/workflows/plant-seed.md +8 -4
  160. package/gsd-core/workflows/progress.md +6 -2
  161. package/gsd-core/workflows/quick-batch.md +5 -1
  162. package/gsd-core/workflows/quick.md +22 -7
  163. package/gsd-core/workflows/remove-phase.md +5 -1
  164. package/gsd-core/workflows/resume-project.md +19 -11
  165. package/gsd-core/workflows/review.md +6 -2
  166. package/gsd-core/workflows/scan.md +5 -1
  167. package/gsd-core/workflows/secure-phase.md +6 -2
  168. package/gsd-core/workflows/settings.md +3 -3
  169. package/gsd-core/workflows/ship.md +13 -10
  170. package/gsd-core/workflows/thread.md +10 -2
  171. package/gsd-core/workflows/transition.md +5 -1
  172. package/gsd-core/workflows/ui-phase.md +5 -1
  173. package/gsd-core/workflows/ui-review.md +6 -2
  174. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  175. package/gsd-core/workflows/update.md +5 -1
  176. package/gsd-core/workflows/validate-phase.md +6 -2
  177. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  178. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  179. package/gsd-core/workflows/verify-work.md +11 -3
  180. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  181. package/hooks/dist/gsd-check-update.js +26 -20
  182. package/hooks/dist/gsd-context-monitor.js +14 -5
  183. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  184. package/hooks/dist/gsd-secret-read-guard.js +189 -15
  185. package/hooks/dist/gsd-statusline.js +321 -22
  186. package/hooks/dist/gsd-validate-commit.sh +27 -5
  187. package/hooks/gsd-agent-isolation-guard.js +3 -1
  188. package/hooks/gsd-check-update.js +26 -20
  189. package/hooks/gsd-context-monitor.js +14 -5
  190. package/hooks/gsd-cursor-subagent-start.js +2 -1
  191. package/hooks/gsd-secret-read-guard.js +189 -15
  192. package/hooks/gsd-statusline.js +321 -22
  193. package/hooks/gsd-validate-commit.sh +27 -5
  194. package/package.json +1 -1
  195. package/scripts/audit-workflow-script-paths.cjs +1 -1
  196. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  197. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  198. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  199. package/scripts/docs-guard-registry.cjs +2 -0
  200. package/scripts/gen-capability-registry.cjs +7 -1
  201. package/scripts/gen-loop-host-contract.cjs +4 -1
  202. package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
  203. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
  204. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  205. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  206. package/scripts/mutation-matrix.cjs +3 -1
  207. package/scripts/release-tarball-smoke.cjs +15 -7
  208. package/scripts/xtrace-phase-timing.cjs +205 -0
  209. package/skills/gsd-code-review/SKILL.md +8 -0
  210. package/skills/gsd-pause-work/SKILL.md +1 -1
  211. package/vscode/package.json +1 -1
@@ -43,6 +43,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
43
43
  const node_path_1 = __importDefault(require("node:path"));
44
44
  const node_child_process_1 = require("node:child_process");
45
45
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
46
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
46
47
  // #3696: the calendar-validity predicate moved to the STATE.md document module
47
48
  // so `state validate` can assert the same `last_activity` invariant this reader
48
49
  // already enforces (ADR-227). Two copies would let the two surfaces disagree
@@ -52,7 +53,7 @@ const ioMod = require("./io.cjs");
52
53
  const { output } = ioMod;
53
54
  // eslint-disable-next-line @typescript-eslint/no-require-imports
54
55
  const planningWorkspace = require("./planning-workspace.cjs");
55
- const { planningPaths } = planningWorkspace;
56
+ const { planningPaths, resolvePhaseIdConvention } = planningWorkspace;
56
57
  // eslint-disable-next-line @typescript-eslint/no-require-imports
57
58
  const frontmatter = require("./frontmatter.cjs");
58
59
  const { extractFrontmatter, frontmatterBlock } = frontmatter;
@@ -60,6 +61,9 @@ const { extractFrontmatter, frontmatterBlock } = frontmatter;
60
61
  const phaseLifecycle = require("./phase-lifecycle.cjs");
61
62
  const { deriveProgressFromRoadmap } = phaseLifecycle;
62
63
  // eslint-disable-next-line @typescript-eslint/no-require-imports
64
+ const roadmapParser = require("./roadmap-parser.cjs");
65
+ const { extractCurrentMilestone, scanMilestonePhaseIds } = roadmapParser;
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
63
67
  const stateDocument = require("./state-document.cjs");
64
68
  const { stateFieldValue } = stateDocument;
65
69
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -352,6 +356,7 @@ function detectSignals(cwd, now = Date.now) {
352
356
  stale_activity: false,
353
357
  roadmap_total_phases: null,
354
358
  roadmap_completed_phases: null,
359
+ milestone_phases: [],
355
360
  // No STATE.md (or unreadable) → no stamp to compare. Unknown, not fresh.
356
361
  state_commits_behind: null,
357
362
  state_commit_stale: null,
@@ -425,12 +430,18 @@ function detectSignals(cwd, now = Date.now) {
425
430
  // Progress table — isComplete falls back to the legacy comparison in that case.
426
431
  let roadmapTotalPhases = null;
427
432
  let roadmapCompletedPhases = null;
433
+ let milestonePhases = [];
428
434
  if (hasRoadmap) {
429
435
  try {
430
436
  const roadmapContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf8');
431
437
  const derived = deriveProgressFromRoadmap(roadmapContent);
432
438
  roadmapTotalPhases = derived.totalPhases;
433
439
  roadmapCompletedPhases = derived.completedPhases;
440
+ // #5228: the ordered phase list of the current milestone, from the same
441
+ // scan the milestone-scoped commands use.
442
+ milestonePhases = [
443
+ ...scanMilestonePhaseIds(extractCurrentMilestone(roadmapContent, cwd), resolvePhaseIdConvention(cwd)),
444
+ ].sort(comparePhaseNum);
434
445
  }
435
446
  catch {
436
447
  /* ROADMAP.md unreadable — leave null; isComplete falls back to legacy. */
@@ -457,6 +468,7 @@ function detectSignals(cwd, now = Date.now) {
457
468
  stale_activity: staleActivity,
458
469
  roadmap_total_phases: roadmapTotalPhases,
459
470
  roadmap_completed_phases: roadmapCompletedPhases,
471
+ milestone_phases: milestonePhases,
460
472
  state_commits_behind: freshness.commits_behind,
461
473
  state_commit_stale: freshness.commit_stale,
462
474
  };
@@ -488,10 +500,21 @@ function isComplete(s) {
488
500
  // Legacy path: STATE.md comparison. It only fires when ROADMAP.md is
489
501
  // absent or has no Progress table, and uses canonical phase-id ordering
490
502
  // so dotted phase ids are never coerced to JavaScript numbers.
491
- if (s.total_phases === null || s.current_phase === null)
492
- return false;
493
- if (comparePhaseNum(s.current_phase, s.total_phases) < 0)
503
+ if (s.current_phase === null)
494
504
  return false;
505
+ if (s.milestone_phases.length > 0) {
506
+ // #5228: compare against the milestone's LAST phase. The phase count is
507
+ // not a phase number once numbering continues from an earlier milestone.
508
+ const last = s.milestone_phases[s.milestone_phases.length - 1];
509
+ if (comparePhaseNum(s.current_phase, last) < 0)
510
+ return false;
511
+ }
512
+ else {
513
+ if (s.total_phases === null)
514
+ return false;
515
+ if (comparePhaseNum(s.current_phase, s.total_phases) < 0)
516
+ return false;
517
+ }
495
518
  }
496
519
  // Status regex: require milestone-level completion language. The pre-fix
497
520
  // regex matched any "shipped" / "done" substring (per-phase language).
@@ -558,52 +581,54 @@ function rec(id, label, command) {
558
581
  return action(id, label, command, true);
559
582
  }
560
583
  /** Per-situation ordered action list; recommended action first. */
561
- function actionsFor(situation, s) {
584
+ function actionsFor(situation, s, runtime = 'claude') {
585
+ // #4859: commands are printed verbatim, so name them the way this runtime registers them.
586
+ const cmd = (name) => (0, runtime_slash_cjs_1.formatGsdSlash)(name, runtime);
562
587
  const phaseN = s.current_phase ?? '';
563
588
  const execLabel = phaseN === '' ? 'Continue executing' : `Continue executing phase ${phaseN}`;
564
589
  switch (situation) {
565
590
  case 'no-project':
566
591
  return [
567
- rec('new-project', 'Start a new project', '/gsd:new-project'),
568
- action('map-codebase', 'Map an existing codebase', '/gsd:map-codebase'),
569
- action('quick', 'Quick task', '/gsd:quick'),
570
- action('help', 'Show help', '/gsd:help'),
592
+ rec('new-project', 'Start a new project', cmd('new-project')),
593
+ action('map-codebase', 'Map an existing codebase', cmd('map-codebase')),
594
+ action('quick', 'Quick task', cmd('quick')),
595
+ action('help', 'Show help', cmd('help')),
571
596
  ];
572
597
  case 'paused':
573
598
  return [
574
- rec('resume-work', 'Resume work', '/gsd:resume-work'),
575
- action('progress', 'Show progress', '/gsd:progress'),
576
- action('quick', 'Quick task', '/gsd:quick'),
577
- action('help', 'Show help', '/gsd:help'),
599
+ rec('resume-work', 'Resume work', cmd('resume-work')),
600
+ action('progress', 'Show progress', cmd('progress')),
601
+ action('quick', 'Quick task', cmd('quick')),
602
+ action('help', 'Show help', cmd('help')),
578
603
  ];
579
604
  case 'blocked':
580
605
  return [
581
- rec('debug', 'Debug the blocker', '/gsd:debug'),
582
- action('verify-work', 'Verify current work', '/gsd:verify-work'),
583
- action('capture', 'Capture a note', '/gsd:capture'),
584
- action('progress', 'Show progress', '/gsd:progress'),
606
+ rec('debug', 'Debug the blocker', cmd('debug')),
607
+ action('verify-work', 'Verify current work', cmd('verify-work')),
608
+ action('capture', 'Capture a note', cmd('capture')),
609
+ action('progress', 'Show progress', cmd('progress')),
585
610
  ];
586
611
  case 'verify-failed':
587
612
  return [
588
- rec('verify-work', 'Re-verify work', '/gsd:verify-work'),
589
- action('debug', 'Debug the failure', '/gsd:debug'),
590
- action('code-review', 'Review recent work', '/gsd:code-review'),
591
- action('progress', 'Show progress', '/gsd:progress'),
613
+ rec('verify-work', 'Re-verify work', cmd('verify-work')),
614
+ action('debug', 'Debug the failure', cmd('debug')),
615
+ action('code-review', 'Review recent work', cmd('code-review')),
616
+ action('progress', 'Show progress', cmd('progress')),
592
617
  ];
593
618
  case 'needs-first-phase':
594
619
  return [
595
- rec('discuss-phase', 'Discuss the first phase', '/gsd:discuss-phase'),
596
- action('plan-phase', 'Plan phase 1', '/gsd:plan-phase'),
597
- action('quick', 'Quick task', '/gsd:quick'),
598
- action('progress', 'Show progress', '/gsd:progress'),
620
+ rec('discuss-phase', 'Discuss the first phase', cmd('discuss-phase')),
621
+ action('plan-phase', 'Plan phase 1', cmd('plan-phase')),
622
+ action('quick', 'Quick task', cmd('quick')),
623
+ action('progress', 'Show progress', cmd('progress')),
599
624
  ];
600
625
  case 'planning':
601
626
  // Forward motion → delegate to the single gated engine (see 'executing').
602
627
  return [
603
- rec('progress-next', `Advance to the next step (plan phase ${phaseN || 1})`, '/gsd:progress --next'),
604
- action('plan-phase', `Plan phase ${phaseN || 1}`, '/gsd:plan-phase'),
605
- action('discuss-phase', 'Discuss before planning', '/gsd:discuss-phase'),
606
- action('progress', 'Show progress', '/gsd:progress'),
628
+ rec('progress-next', `Advance to the next step (plan phase ${phaseN || 1})`, cmd('progress --next')),
629
+ action('plan-phase', `Plan phase ${phaseN || 1}`, cmd('plan-phase')),
630
+ action('discuss-phase', 'Discuss before planning', cmd('discuss-phase')),
631
+ action('progress', 'Show progress', cmd('progress')),
607
632
  ];
608
633
  case 'executing':
609
634
  // In-project forward motion delegates to the single gated engine
@@ -612,44 +637,66 @@ function actionsFor(situation, s) {
612
637
  // /gsd:execute-phase here — that divergence is why the old flat /gsd-next
613
638
  // was removed (#3054). Direct execute stays as an explicit secondary choice.
614
639
  return [
615
- rec('progress-next', 'Advance to the next step', '/gsd:progress --next'),
616
- action('execute-phase', execLabel, '/gsd:execute-phase'),
617
- action('quick', 'Quick task', '/gsd:quick'),
618
- action('code-review', 'Review recent work', '/gsd:code-review'),
640
+ rec('progress-next', 'Advance to the next step', cmd('progress --next')),
641
+ action('execute-phase', execLabel, cmd('execute-phase')),
642
+ action('quick', 'Quick task', cmd('quick')),
643
+ action('code-review', 'Review recent work', cmd('code-review')),
619
644
  ];
620
645
  case 'verify-pending':
621
646
  // Forward motion → delegate to the single gated engine (see 'executing').
622
647
  return [
623
- rec('progress-next', 'Advance to the next step (verify)', '/gsd:progress --next'),
624
- action('verify-work', 'Verify work', '/gsd:verify-work'),
625
- action('code-review', 'Review recent work', '/gsd:code-review'),
626
- action('ship', 'Ship completed work', '/gsd:ship'),
648
+ rec('progress-next', 'Advance to the next step (verify)', cmd('progress --next')),
649
+ action('verify-work', 'Verify work', cmd('verify-work')),
650
+ action('code-review', 'Review recent work', cmd('code-review')),
651
+ action('ship', 'Ship completed work', cmd('ship')),
627
652
  ];
628
653
  case 'idle-stranded':
629
654
  return [
630
- rec('ship', 'Ship committed work', '/gsd:ship'),
631
- action('complete-milestone', 'Complete the milestone', '/gsd:complete-milestone'),
632
- action('progress', 'Show progress', '/gsd:progress'),
633
- action('capture', 'Capture a note', '/gsd:capture'),
655
+ rec('ship', 'Ship committed work', cmd('ship')),
656
+ action('complete-milestone', 'Complete the milestone', cmd('complete-milestone')),
657
+ action('progress', 'Show progress', cmd('progress')),
658
+ action('capture', 'Capture a note', cmd('capture')),
634
659
  ];
635
660
  case 'complete':
636
661
  return [
637
- rec('new-milestone', 'Start a new milestone', '/gsd:new-milestone'),
638
- action('extract-learnings', 'Extract learnings', '/gsd:extract-learnings'),
639
- action('quick', 'Quick task', '/gsd:quick'),
640
- action('progress', 'Show progress', '/gsd:progress'),
662
+ rec('new-milestone', 'Start a new milestone', cmd('new-milestone')),
663
+ action('extract-learnings', 'Extract learnings', cmd('extract-learnings')),
664
+ action('quick', 'Quick task', cmd('quick')),
665
+ action('progress', 'Show progress', cmd('progress')),
641
666
  ];
642
667
  case 'unknown':
643
668
  default:
644
669
  return [
645
- rec('progress', 'Show progress', '/gsd:progress'),
646
- action('progress-next', 'Advance to the next step', '/gsd:progress --next'),
647
- action('quick', 'Quick task', '/gsd:quick'),
648
- action('help', 'Show help', '/gsd:help'),
670
+ rec('progress', 'Show progress', cmd('progress')),
671
+ action('progress-next', 'Advance to the next step', cmd('progress --next')),
672
+ action('quick', 'Quick task', cmd('quick')),
673
+ action('help', 'Show help', cmd('help')),
649
674
  ];
650
675
  }
651
676
  }
652
677
  // ─── Summary line ─────────────────────────────────────────────────────────────
678
+ function phaseSummary(s) {
679
+ const phase = s.current_phase;
680
+ // Preserve the milestone ordinal introduced by #5228 when it is known.
681
+ const at = phase === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, phase) === 0);
682
+ if (at >= 0)
683
+ return `Phase ${at + 1} of ${s.milestone_phases.length}`;
684
+ const total = s.total_phases;
685
+ const legacy = `Phase ${phase ?? '?'} of ${total ?? '?'}`;
686
+ const roadmapTotal = s.roadmap_total_phases;
687
+ // Keep the recorded pair when roadmap counts are unavailable, or when the
688
+ // pair can describe a single milestone without changing its existing text.
689
+ if (phase === null || total === null || roadmapTotal === null || roadmapTotal <= 0
690
+ || (total === roadmapTotal && comparePhaseNum(phase, total) <= 0)) {
691
+ return legacy;
692
+ }
693
+ // When the recorded pair cannot safely describe this roadmap, label its
694
+ // counts separately rather than printing a mixed-scope ratio.
695
+ const roadmapDone = s.roadmap_completed_phases;
696
+ return roadmapDone === null
697
+ ? `Phase ${phase} (${roadmapTotal} roadmap phases)`
698
+ : `Phase ${phase} (${roadmapDone} of ${roadmapTotal} roadmap phases complete)`;
699
+ }
653
700
  function buildSummary(situation, s) {
654
701
  switch (situation) {
655
702
  case 'no-project':
@@ -663,7 +710,7 @@ function buildSummary(situation, s) {
663
710
  case 'needs-first-phase':
664
711
  return 'Project initialized — plan your first phase';
665
712
  case 'planning':
666
- return `Phase ${s.current_phase ?? '?'} of ${s.total_phases ?? '?'} — needs a plan`;
713
+ return `${phaseSummary(s)} — needs a plan`;
667
714
  case 'executing':
668
715
  return progressLine('executing', s);
669
716
  case 'verify-pending':
@@ -680,7 +727,7 @@ function buildSummary(situation, s) {
680
727
  function progressLine(tail, s) {
681
728
  const parts = [];
682
729
  if (s.current_phase !== null && s.total_phases !== null) {
683
- parts.push(`Phase ${s.current_phase} of ${s.total_phases}`);
730
+ parts.push(phaseSummary(s));
684
731
  }
685
732
  else if (s.current_phase !== null) {
686
733
  parts.push(`Phase ${s.current_phase}`);
@@ -699,7 +746,7 @@ function progressLine(tail, s) {
699
746
  function classifyProject(cwd, now = Date.now) {
700
747
  const signals = detectSignals(cwd, now);
701
748
  const situation = classify(signals);
702
- const actions = actionsFor(situation, signals);
749
+ const actions = actionsFor(situation, signals, (0, runtime_slash_cjs_1.resolveRuntime)(cwd));
703
750
  const recommended = actions.find((a) => a.recommended)?.id ?? 'progress';
704
751
  const summary = buildSummary(situation, signals);
705
752
  return { situation, recommended, summary, signals, actions };
@@ -718,6 +765,6 @@ function runSmartEntry(cwd, args, raw) {
718
765
  return;
719
766
  }
720
767
  // Human mode: a compact one-liner plus the recommended action.
721
- const human = `${result.summary}\nRecommended: ${result.recommended} → ${result.actions.find((a) => a.id === result.recommended)?.command ?? '/gsd:progress'}`;
768
+ const human = `${result.summary}\nRecommended: ${result.recommended} → ${result.actions.find((a) => a.id === result.recommended)?.command ?? (0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'progress')}`;
722
769
  output(null, true, human);
723
770
  }
@@ -14,6 +14,7 @@ exports.isUnfilledFieldValue = isUnfilledFieldValue;
14
14
  exports.leadingCalendarDate = leadingCalendarDate;
15
15
  exports.isRealCalendarDate = isRealCalendarDate;
16
16
  exports.stateFieldContinuation = stateFieldContinuation;
17
+ exports.fieldContinuationEnd = fieldContinuationEnd;
17
18
  exports.stateExtractField = stateExtractField;
18
19
  exports.stateFieldValue = stateFieldValue;
19
20
  exports.stateCurrentPositionSlice = stateCurrentPositionSlice;
@@ -378,7 +379,16 @@ function stateFieldContinuation(content, fieldName) {
378
379
  if (!rest.startsWith('\n'))
379
380
  return null; // end of file: nothing follows
380
381
  const lines = rest.slice(1).split('\n').map((line) => line.replace(/\r$/, ''));
381
- const continuation = [];
382
+ const continuation = lines.slice(0, continuationLineCount(lines)).map((line) => line.trim());
383
+ return continuation.length ? continuation.join(' ') : null;
384
+ }
385
+ /**
386
+ * How many of the lines directly below a single-line field belong to its value: the scan
387
+ * `stateFieldContinuation` reports on, shared so a writer that replaces the value removes exactly
388
+ * the lines a validator would call its continuation (#4945). `lines` are CR-free.
389
+ */
390
+ function continuationLineCount(lines) {
391
+ let count = 0;
382
392
  for (let i = 0; i < lines.length; i++) {
383
393
  const line = lines[i];
384
394
  if (!line.trim())
@@ -391,9 +401,33 @@ function stateFieldContinuation(content, fieldName) {
391
401
  // title, so stop before consuming it rather than after.
392
402
  if (i + 1 < lines.length && SETEXT_UNDERLINE_RE.test(lines[i + 1]))
393
403
  break;
394
- continuation.push(line.trim());
404
+ count++;
395
405
  }
396
- return continuation.length ? continuation.join(' ') : null;
406
+ return count;
407
+ }
408
+ /**
409
+ * The index just past a single-line field's value AND the continuation lines that wrap it
410
+ * (#4945). `valueEnd` is where the field's own line ends (before its line terminator). Replacing
411
+ * `content.slice(valueEnd, result)` with nothing removes every continuation line and the
412
+ * terminator in front of each, so the terminator after the last one stays. Returns `valueEnd`
413
+ * when nothing continues the value.
414
+ */
415
+ function fieldContinuationEnd(content, valueEnd) {
416
+ const tail = content.slice(valueEnd);
417
+ const first = /^\r?\n/.exec(tail);
418
+ if (!first)
419
+ return valueEnd;
420
+ const rawLines = tail.slice(first[0].length).split('\n');
421
+ const count = continuationLineCount(rawLines.map((line) => line.replace(/\r$/, '')));
422
+ if (count === 0)
423
+ return valueEnd;
424
+ let removed = first[0].length + (count - 1);
425
+ for (let i = 0; i < count; i++)
426
+ removed += rawLines[i].length;
427
+ // The terminator after the last continuation line stays; on a CRLF document that includes its CR.
428
+ if (rawLines[count - 1].endsWith('\r'))
429
+ removed -= 1;
430
+ return valueEnd + removed;
397
431
  }
398
432
  function stateExtractField(content, fieldName) {
399
433
  const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
@@ -417,7 +417,41 @@ function createStateWriteIntent(transaction, init = {}) {
417
417
  err.code = 'STATE_WRITE_INTENT_TRANSACTION_REQUIRED';
418
418
  throw err;
419
419
  }
420
- const assertions = Object.freeze((init.assertions ?? []).map((a) => Object.freeze({ field: a.field, requirement: a.requirement })));
420
+ // C2 (ADR-4629 §8.2): an unknown `requirement` is a construction error. §8.2
421
+ // judges only `'required'` assertions, so a mistyped value (`'Required'`)
422
+ // would otherwise be silently skipped: a fail-open verifier.
423
+ for (const a of init.assertions ?? []) {
424
+ if (a.requirement !== 'required' && a.requirement !== 'best-effort') {
425
+ const err = new Error(`createStateWriteIntent: assertion ${JSON.stringify(a.field)} has unknown requirement ` +
426
+ `${JSON.stringify(a.requirement)}; expected 'required' or 'best-effort' (ADR-4629 §8.1).`);
427
+ err.code = 'STATE_WRITE_INTENT_REQUIREMENT_INVALID';
428
+ throw err;
429
+ }
430
+ }
431
+ if (init.scope !== undefined && init.scope !== 'narrow' && init.scope !== 'broad') {
432
+ const err = new Error(`createStateWriteIntent: unknown scope ${JSON.stringify(init.scope)}; expected 'narrow' or 'broad' ` +
433
+ '(ADR-4629 §8.1).');
434
+ err.code = 'STATE_WRITE_INTENT_SCOPE_INVALID';
435
+ throw err;
436
+ }
437
+ // C2 (ADR-4629 §8.2): an unknown `target` is a construction error, never a
438
+ // silent fall-back to field semantics (a JS caller passing `'sections'`).
439
+ for (const a of init.assertions ?? []) {
440
+ if (a.target !== undefined && a.target !== 'field' && a.target !== 'section') {
441
+ const err = new Error(`createStateWriteIntent: assertion ${JSON.stringify(a.field)} has unknown target ` +
442
+ `${JSON.stringify(a.target)}; expected 'field' or 'section' (ADR-4629 §8.2).`);
443
+ err.code = 'STATE_WRITE_INTENT_TARGET_INVALID';
444
+ throw err;
445
+ }
446
+ }
447
+ // C2 (ADR-4629 §8.2): `target` / `value` are copied only when the caller set
448
+ // them, so a C1-shaped assertion stays exactly `{ field, requirement }`.
449
+ const assertions = Object.freeze((init.assertions ?? []).map((a) => Object.freeze({
450
+ field: a.field,
451
+ requirement: a.requirement,
452
+ ...(a.target !== undefined ? { target: a.target } : {}),
453
+ ...(a.value !== undefined ? { value: a.value } : {}),
454
+ })));
421
455
  return Object.freeze({
422
456
  ...transaction,
423
457
  assertions,
@@ -888,6 +922,8 @@ function transitionCore(content, intent, deps) {
888
922
  */
889
923
  function beginPhaseCore(content, intent, deps) {
890
924
  const updated = [];
925
+ // #4945: a re-entry whose earlier plans already have summaries starts at the first plan still to run.
926
+ const startPlan = intent.startPlan && intent.startPlan > 0 ? Math.floor(intent.startPlan) : 1;
891
927
  // #1255: body-field replacements operate on body only (frontmatter stripped),
892
928
  // not on the full content. The YAML `status:` key matches `^Status:\s*`
893
929
  // before the body pipe-table row if full content is passed.
@@ -943,7 +979,7 @@ function beginPhaseCore(content, intent, deps) {
943
979
  if (intent.phaseName) {
944
980
  tryField('Current Phase Name', intent.phaseName);
945
981
  }
946
- tryField('Current Plan', '1');
982
+ tryField('Current Plan', String(startPlan));
947
983
  if (intent.planCount) {
948
984
  tryField('Total Plans in Phase', String(intent.planCount));
949
985
  }
@@ -962,14 +998,21 @@ function beginPhaseCore(content, intent, deps) {
962
998
  // end-of-line (inert here since `.` never crosses line terminators
963
999
  // without `/s`, which is not set).
964
1000
  const focusPattern = /^([ \t]*\*\*Current focus:\*\*[ \t]*)(.*)$/im;
965
- if (focusPattern.test(body)) {
966
- body = body.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
1001
+ const focusMatch = focusPattern.exec(body);
1002
+ if (focusMatch) {
1003
+ // #4945: a value an author wrapped onto further lines is replaced whole, so its tail is not
1004
+ // left behind under the new value. The lines that count are the ones `state validate` already
1005
+ // reports as a field's continuation (stops at a blank line, a heading, a list item, a table
1006
+ // row, a fence or a sibling field). An empty first line has no value to continue.
1007
+ const valueEnd = focusMatch.index + focusMatch[0].length;
1008
+ const end = focusMatch[2].trim() === '' ? valueEnd : (0, state_document_cjs_2.fieldContinuationEnd)(body, valueEnd);
1009
+ body = body.slice(0, focusMatch.index) + focusMatch[1] + focusLabel + body.slice(end);
967
1010
  updated.push('Current focus');
968
1011
  }
969
1012
  // ## Current Position section mutation (#1104, #1365).
970
1013
  // `locateCurrentPosition` (fence-aware, tokenizeHeadings-based) locates
971
1014
  // the section; mirrors state.cts:2261-2324 byte-for-behaviour.
972
- body = mutateCurrentPositionFirstTime(body, intent, today, updated);
1015
+ body = mutateCurrentPositionFirstTime(body, { ...intent, startPlan }, today, updated);
973
1016
  }
974
1017
  else {
975
1018
  // Resume path: only update Last activity timestamp in Current Position
@@ -1073,7 +1116,7 @@ function mutateCurrentPositionFirstTime(body, intent, today, updated) {
1073
1116
  sectionBody = replaced;
1074
1117
  }
1075
1118
  // Plan line.
1076
- const planValue = `1 of ${intent.planCount || '?'}`;
1119
+ const planValue = `${intent.startPlan} of ${intent.planCount || '?'}`;
1077
1120
  if (/^Plan:/m.test(sectionBody)) {
1078
1121
  sectionBody = sectionBody.replace(/^Plan:.*$/m, `Plan: ${planValue}`);
1079
1122
  }
@@ -1575,9 +1618,30 @@ function advancePlanCore(content, deps) {
1575
1618
  data: { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans },
1576
1619
  };
1577
1620
  }
1578
- // ----------------------------------------------------------------------------
1579
- // completePhase — intent implementation (Phase 3)
1580
- // ----------------------------------------------------------------------------
1621
+ /**
1622
+ * #4824: Status / Current Plan / Stopped-at wording for the phase a
1623
+ * completion hands off to. The historical `Ready to plan` is only true for a
1624
+ * phase with no plans; a phase that already has PLANs routes to execution,
1625
+ * and one whose plans all have SUMMARYs awaits verification — never back to
1626
+ * `/gsd-plan-phase`. Which phase is selected stays #4078's rule.
1627
+ */
1628
+ function nextPhaseWording(disk) {
1629
+ switch (disk?.status) {
1630
+ case 'PLANNED':
1631
+ return { status: 'Ready to execute', currentPlan: 'Not started', stoppedAt: 'ready to execute' };
1632
+ case 'IN_PROGRESS':
1633
+ return { status: 'Ready to execute', currentPlan: String(disk.summaryCount + 1), stoppedAt: 'ready to execute' };
1634
+ case 'EXECUTED':
1635
+ case 'NEEDS_REVIEW':
1636
+ return {
1637
+ status: 'Phase complete — ready for verification',
1638
+ currentPlan: String(disk.planCount),
1639
+ stoppedAt: 'ready to verify',
1640
+ };
1641
+ default:
1642
+ return { status: 'Ready to plan', currentPlan: 'Not started', stoppedAt: 'ready to plan' };
1643
+ }
1644
+ }
1581
1645
  /**
1582
1646
  * Apply a `completePhase` transition to STATE.md content.
1583
1647
  *
@@ -1585,7 +1649,9 @@ function advancePlanCore(content, deps) {
1585
1649
  * (phase.cts) onto the substrate. Owns the field-classification-governed body
1586
1650
  * mutations: Current Phase (preserving the `of total` shape and phase name),
1587
1651
  * Current Phase Name, Status (`All phases complete` on the last phase, else
1588
- * `Ready to plan` per ADR-2207), Current Plan (`Not started`), Last Activity + Description,
1652
+ * `Ready to plan` per ADR-2207 — or, #4824, the next phase's disk-derived
1653
+ * wording when it is already planned or executed), Current Plan (`Not started`
1654
+ * unless the next phase is in flight), Last Activity + Description,
1589
1655
  * and the Completed/Total Phases + Progress percent block (re-derived from the
1590
1656
  * roadmap via the injected `roadmapProvider`).
1591
1657
  *
@@ -1602,6 +1668,7 @@ function advancePlanCore(content, deps) {
1602
1668
  function completePhaseCore(content, intent, deps) {
1603
1669
  const updated = [];
1604
1670
  const today = deps.clock.localToday();
1671
+ const nextWording = nextPhaseWording(intent.isLastPhase ? null : intent.nextPhaseDisk ?? null);
1605
1672
  // Consult the field-classification table for the frontmatter keys this
1606
1673
  // transition touches (same guard beginPhaseCore applies). A missing row is a
1607
1674
  // substrate defect — fail loudly rather than silently re-encoding policy.
@@ -1665,7 +1732,7 @@ function completePhaseCore(content, intent, deps) {
1665
1732
  // Status — `All phases complete` on the final phase (ADR-2207), otherwise
1666
1733
  // `Ready to plan`. Milestone termination (`<version> milestone complete`) is
1667
1734
  // owned solely by the milestone-close verb (milestoneCompleteCore).
1668
- const statusValue = intent.isLastPhase ? 'All phases complete' : 'Ready to plan';
1735
+ const statusValue = intent.isLastPhase ? 'All phases complete' : nextWording.status;
1669
1736
  const statusAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Status', null, statusValue);
1670
1737
  if (statusAfter !== body) {
1671
1738
  body = statusAfter;
@@ -1682,10 +1749,10 @@ function completePhaseCore(content, intent, deps) {
1682
1749
  let planAfter;
1683
1750
  const positionScope = (0, state_document_cjs_1.stateCurrentPositionSlice)(body);
1684
1751
  if (positionScope !== null) {
1685
- planAfter = (0, markdown_sectionizer_cjs_1.withSection)(body, (h) => (h.level === 2 || h.level === 3) && h.text.trim().toLowerCase() === 'current position', (sectionBody) => (0, state_document_cjs_1.stateReplaceFieldWithFallback)(sectionBody, 'Current Plan', 'Plan', 'Not started'), { levelBounded: true });
1752
+ planAfter = (0, markdown_sectionizer_cjs_1.withSection)(body, (h) => (h.level === 2 || h.level === 3) && h.text.trim().toLowerCase() === 'current position', (sectionBody) => (0, state_document_cjs_1.stateReplaceFieldWithFallback)(sectionBody, 'Current Plan', 'Plan', nextWording.currentPlan), { levelBounded: true });
1686
1753
  }
1687
1754
  else {
1688
- planAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Current Plan', 'Plan', 'Not started');
1755
+ planAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Current Plan', 'Plan', nextWording.currentPlan);
1689
1756
  }
1690
1757
  if (planAfter !== body) {
1691
1758
  body = planAfter;
@@ -1729,7 +1796,7 @@ function completePhaseCore(content, intent, deps) {
1729
1796
  // milestone termination wording stays owned by milestoneCompleteCore.
1730
1797
  const stoppedAtLine = intent.isLastPhase
1731
1798
  ? `Phase ${intent.phaseNum} complete — all phases complete`
1732
- : `Phase ${intent.phaseNum} complete${intent.nextPhaseNum ? `, ready to plan Phase ${intent.nextPhaseNum}` : ''}`;
1799
+ : `Phase ${intent.phaseNum} complete${intent.nextPhaseNum ? `, ${nextWording.stoppedAt} Phase ${intent.nextPhaseNum}` : ''}`;
1733
1800
  const stoppedAfter = (0, state_document_cjs_1.stateReplaceFieldInSession)(body, 'Stopped At', 'Stopped at', stoppedAtLine);
1734
1801
  if (stoppedAfter !== body) {
1735
1802
  body = stoppedAfter;
@@ -1740,15 +1807,27 @@ function completePhaseCore(content, intent, deps) {
1740
1807
  // Only runs when a Completed Phases field exists (the existing guard).
1741
1808
  const completedRaw = (0, state_document_cjs_1.stateExtractField)(body, 'Completed Phases');
1742
1809
  if (completedRaw !== null) {
1743
- let newCompleted = parseInt(completedRaw, 10);
1810
+ const recordedCompleted = parseInt(completedRaw, 10);
1811
+ let newCompleted = recordedCompleted;
1744
1812
  let derivedTotalPhases = null;
1745
1813
  const roadmapContent = deps.roadmapProvider ? deps.roadmapProvider() : null;
1746
1814
  if (roadmapContent) {
1747
1815
  const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
1748
- if (derived.completedPhases !== null)
1749
- newCompleted = derived.completedPhases;
1750
- if (derived.totalPhases !== null)
1751
- derivedTotalPhases = derived.totalPhases;
1816
+ // #4901: completed counters only ratchet up, like the frontmatter twin
1817
+ // (applyAuthoritativeProgressSubkeys, completedOnlyRaise). A derivation
1818
+ // that lands BELOW the recorded counter read a narrower scope than the
1819
+ // counter spans (e.g. the first `## Progress` section of a multi-milestone
1820
+ // roadmap), so its total is just as suspect: keep the recorded counter and
1821
+ // fall back to the recorded Total Phases for the percent below.
1822
+ const derivedLowered = derived.completedPhases !== null
1823
+ && Number.isFinite(recordedCompleted)
1824
+ && derived.completedPhases < recordedCompleted;
1825
+ if (!derivedLowered) {
1826
+ if (derived.completedPhases !== null)
1827
+ newCompleted = derived.completedPhases;
1828
+ if (derived.totalPhases !== null)
1829
+ derivedTotalPhases = derived.totalPhases;
1830
+ }
1752
1831
  }
1753
1832
  // #3057 B9: only mark 'Completed Phases' updated when the text actually
1754
1833
  // changed. `stateReplaceField` returns the full (re-)substituted content