@opengsd/gsd-core 1.18.0 → 1.19.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 (257) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-code-fixer.compact.md +2 -2
  5. package/agents/gsd-code-fixer.md +4 -3
  6. package/agents/gsd-verifier.md +1 -1
  7. package/bin/install.js +422 -103
  8. package/gsd-core/bin/gsd-tools.cjs +106 -7
  9. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  10. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  11. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  12. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  13. package/gsd-core/bin/lib/capability-registry.cjs +156 -74
  14. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  15. package/gsd-core/bin/lib/capability-validator.cjs +17 -0
  16. package/gsd-core/bin/lib/config-loader.cjs +10 -0
  17. package/gsd-core/bin/lib/config.cjs +256 -58
  18. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  19. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  20. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  21. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  22. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  23. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  24. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  25. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  26. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  27. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  28. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  29. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  30. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  31. package/gsd-core/bin/lib/init.cjs +65 -1
  32. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  33. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  34. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  35. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  36. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  37. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  38. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  39. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  40. package/gsd-core/bin/lib/milestone.cjs +43 -1
  41. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  42. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  43. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  44. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  45. package/gsd-core/bin/lib/phase.cjs +115 -26
  46. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  47. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  48. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  49. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  50. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  51. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  53. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  54. package/gsd-core/bin/lib/roadmap.cjs +33 -25
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  58. package/gsd-core/bin/lib/shell-command-projection.cjs +121 -16
  59. package/gsd-core/bin/lib/smart-entry.cjs +13 -0
  60. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  61. package/gsd-core/bin/lib/state-document.cjs +12 -4
  62. package/gsd-core/bin/lib/state-transition.cjs +119 -24
  63. package/gsd-core/bin/lib/state.cjs +50 -28
  64. package/gsd-core/bin/lib/surface.cjs +9 -0
  65. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  66. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  67. package/gsd-core/bin/lib/verification.cjs +96 -9
  68. package/gsd-core/bin/lib/verify.cjs +2 -0
  69. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  72. package/gsd-core/references/git-integration.md +11 -0
  73. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  74. package/gsd-core/references/planning-config.md +2 -0
  75. package/gsd-core/references/tdd.md +2 -2
  76. package/gsd-core/templates/DEBUG.md +16 -0
  77. package/gsd-core/templates/config.json +2 -1
  78. package/gsd-core/templates/verification-report.md +1 -1
  79. package/gsd-core/workflows/audit-fix.md +2 -1
  80. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  81. package/gsd-core/workflows/code-review.md +61 -7
  82. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  83. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  84. package/gsd-core/workflows/complete-milestone.md +37 -150
  85. package/gsd-core/workflows/diagnose-issues.md +29 -5
  86. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  87. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  88. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  89. package/gsd-core/workflows/discuss-phase.md +6 -10
  90. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  91. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  92. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  93. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  94. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  95. package/gsd-core/workflows/execute-phase.md +9 -5
  96. package/gsd-core/workflows/health.md +2 -1
  97. package/gsd-core/workflows/import.md +1 -1
  98. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  99. package/gsd-core/workflows/plan-phase.md +13 -13
  100. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  101. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  102. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  103. package/gsd-core/workflows/quick.md +1 -1
  104. package/gsd-core/workflows/review.md +16 -13
  105. package/gsd-core/workflows/section-manifest.json +5 -0
  106. package/gsd-core/workflows/settings.md +1 -0
  107. package/gsd-core/workflows/ship.md +3 -4
  108. package/gsd-core/workflows/spec-phase.md +3 -6
  109. package/gsd-core/workflows/verify-work.md +4 -3
  110. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  111. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  112. package/hooks/dist/gsd-path-export.js +95 -0
  113. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  114. package/hooks/dist/gsd-statusline.js +35 -2
  115. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  116. package/hooks/dist/gsd-workflow-guard.js +1 -1
  117. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  118. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  119. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  120. package/hooks/gsd-agent-isolation-guard.js +11 -1
  121. package/hooks/gsd-cursor-subagent-start.js +6 -2
  122. package/hooks/gsd-path-export.js +95 -0
  123. package/hooks/gsd-secret-read-guard.js +172 -39
  124. package/hooks/gsd-statusline.js +35 -2
  125. package/hooks/gsd-windsurf-pre-write.js +28 -1
  126. package/hooks/gsd-workflow-guard.js +1 -1
  127. package/hooks/gsd-worktree-path-guard.js +28 -1
  128. package/hooks/hooks.json +1 -0
  129. package/hooks/lib/isolation-sentinel.js +44 -0
  130. package/hooks/managed-hooks-registry.cjs +1 -0
  131. package/package.json +13 -6
  132. package/pi/gsd.cjs +1047 -10
  133. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  134. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  135. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  136. package/pi/package-agents/gsd-code-fixer.md +769 -0
  137. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  138. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  139. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  140. package/pi/package-agents/gsd-debugger.md +1274 -0
  141. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  142. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  143. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  144. package/pi/package-agents/gsd-doc-writer.md +626 -0
  145. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  146. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  147. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  148. package/pi/package-agents/gsd-eval-planner.md +149 -0
  149. package/pi/package-agents/gsd-executor.md +894 -0
  150. package/pi/package-agents/gsd-framework-selector.md +159 -0
  151. package/pi/package-agents/gsd-integration-checker.md +476 -0
  152. package/pi/package-agents/gsd-intel-updater.md +338 -0
  153. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  154. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  155. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  156. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  157. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  158. package/pi/package-agents/gsd-planner.md +1003 -0
  159. package/pi/package-agents/gsd-project-researcher.md +611 -0
  160. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  161. package/pi/package-agents/gsd-roadmapper.md +779 -0
  162. package/pi/package-agents/gsd-security-auditor.md +171 -0
  163. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  164. package/pi/package-agents/gsd-ui-checker.md +417 -0
  165. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  166. package/pi/package-agents/gsd-user-profiler.md +174 -0
  167. package/pi/package-agents/gsd-verifier.md +963 -0
  168. package/pi/package-agents/manifest.json +39 -0
  169. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  170. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  171. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  172. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  173. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  174. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  175. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  176. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  177. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  178. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  179. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  180. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  181. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  182. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  183. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  184. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  185. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  186. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  187. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  188. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  189. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  190. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  191. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  192. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  193. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  194. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  195. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  196. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  197. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  198. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  199. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  200. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  201. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  202. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  203. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  204. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  205. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  206. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  207. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  208. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  209. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  210. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  211. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  212. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  213. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  214. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  215. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  216. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  217. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  218. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  219. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  220. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  221. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  222. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  223. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  224. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  225. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  226. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  227. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  228. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  229. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  230. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  231. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  232. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  233. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  234. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  235. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  236. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  237. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  238. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  239. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  240. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  241. package/scripts/build-hooks.js +4 -0
  242. package/scripts/command-contract-helpers.cjs +1 -1
  243. package/scripts/docs-guard-registry.cjs +20 -7
  244. package/scripts/gen-health-docs.cjs +13 -1
  245. package/scripts/gen-pi-package-skills.cjs +217 -0
  246. package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
  247. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
  248. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  249. package/scripts/lint-retired-runtime-name.cjs +5 -0
  250. package/scripts/npm-audit-baseline.cjs +41 -7
  251. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  252. package/scripts/select-docs-guards.cjs +1 -1
  253. package/scripts/self-test-report-policy.cjs +350 -0
  254. package/vscode/package.json +1 -1
  255. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  256. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  257. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -30,6 +30,7 @@ exports.applyPreserveWhenUnchanged = applyPreserveWhenUnchanged;
30
30
  exports.applyStatePreservation = applyStatePreservation;
31
31
  exports.transitionCore = transitionCore;
32
32
  exports.sliceCurrentPositionSection = sliceCurrentPositionSection;
33
+ exports.isStatePastPhase = isStatePastPhase;
33
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
35
  const frontmatter = require("./frontmatter.cjs");
35
36
  const state_document_cjs_1 = require("./state-document.cjs");
@@ -37,6 +38,9 @@ const state_document_cjs_2 = require("./state-document.cjs");
37
38
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
38
39
  const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
39
40
  const pattern_cjs_1 = require("./pattern.cjs");
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
42
+ const phaseIdMod = require("./phase-id.cjs");
43
+ const { comparePhaseNum } = phaseIdMod;
40
44
  // #4129: the completion-ratio kernel for the resync-arm ratchet's percent
41
45
  // (planning-scope's SCOPE — state-document's own dependency, no cycle here:
42
46
  // state-document never imports this module).
@@ -643,18 +647,24 @@ function mergeResyncProgressRatchet(curatedRecord, derivedRecord) {
643
647
  merged[key] = value;
644
648
  }
645
649
  }
646
- if ((0, state_document_cjs_2.toFiniteNumber)(derivedRecord.percent) !== null) {
647
- const recomputed = (0, state_document_cjs_2.computeProgressPercent)((0, state_document_cjs_2.toFiniteNumber)(merged.completed_plans), (0, state_document_cjs_2.toFiniteNumber)(merged.total_plans), (0, state_document_cjs_2.toFiniteNumber)(merged.completed_phases), (0, state_document_cjs_2.toFiniteNumber)(merged.total_phases), planningScopeMod.SCOPE.COMPLETE);
648
- // Same verbatim rule for percent: assign only when the recomputed value
649
- // numerically differs, so a string-spelled derived percent ("67") is not
650
- // churned into a number-spelled 67 (phantom-divergence noise, not a
651
- // change).
652
- if (recomputed !== null && recomputed !== (0, state_document_cjs_2.toFiniteNumber)(merged.percent)) {
653
- merged.percent = recomputed;
654
- }
655
- }
650
+ recomputeMergedProgressPercent(merged, derivedRecord);
656
651
  return merged;
657
652
  }
653
+ /**
654
+ * Make `merged.percent` the progress computation over the counters `merged` actually carries (#5619,
655
+ * #4129). Only when the derived side measured a percent: a derivation that withheld one (an incomplete
656
+ * scope, #1761/#3217) stays withheld, never invented. Same verbatim rule as the counters: assign only
657
+ * when the recomputed value numerically differs, so a string-spelled derived percent ("67") is not
658
+ * churned into a number-spelled 67 (phantom-divergence noise, not a change).
659
+ */
660
+ function recomputeMergedProgressPercent(merged, derivedRecord) {
661
+ if ((0, state_document_cjs_2.toFiniteNumber)(derivedRecord.percent) === null)
662
+ return;
663
+ const recomputed = (0, state_document_cjs_2.computeProgressPercent)((0, state_document_cjs_2.toFiniteNumber)(merged.completed_plans), (0, state_document_cjs_2.toFiniteNumber)(merged.total_plans), (0, state_document_cjs_2.toFiniteNumber)(merged.completed_phases), (0, state_document_cjs_2.toFiniteNumber)(merged.total_phases), planningScopeMod.SCOPE.COMPLETE);
664
+ if (recomputed !== null && recomputed !== (0, state_document_cjs_2.toFiniteNumber)(merged.percent)) {
665
+ merged.percent = recomputed;
666
+ }
667
+ }
658
668
  /**
659
669
  * Executor for `preservation: 'preserve-always'` (ADR-3408 §8.1). Only
660
670
  * `progress` carries this policy today. Preserves #3242/#1446/#2440/#2969
@@ -719,18 +729,23 @@ function applyPreserveAlways(field, cls, ctx) {
719
729
  // completed after the plan count grew) — ratcheting UP only, never
720
730
  // deriving downward (preserves the #3242 curated-progress protection
721
731
  // for cases unrelated to plan-count growth, e.g. a deleted SUMMARY).
722
- // percent also takes the derived value — the resync recomputed it from
723
- // disk counts, and a stale curated percent would be incoherent against
724
- // the ratcheted-up completed counts (e.g. 54/54 at 93%).
732
+ // percent is recomputed over the counters actually written (#5619): the
733
+ // disk-derived percent is only coherent with the DERIVED counters, and a
734
+ // stale curated percent would be incoherent against the ratcheted-up
735
+ // completed counts (e.g. 54/54 at 93%).
725
736
  const ratchetUpKeys = new Set(['completed_plans', 'completed_phases']);
726
737
  for (const [key, value] of Object.entries(curatedRecord)) {
727
738
  if (key === 'total_plans' || key === 'total_phases' || key === 'percent')
728
739
  continue;
729
740
  if (ratchetUpKeys.has(key)) {
730
- const derivedNum = typeof derivedRecord[key] === 'number' ? derivedRecord[key] : -Infinity;
731
- const curatedNum = typeof value === 'number' ? value : -Infinity;
741
+ // Frontmatter scalars arrive as STRINGS ("2", not 2): coerce through
742
+ // `toFiniteNumber`, never a `typeof === 'number'` test (#5619 — that
743
+ // test made both sides -Infinity and the ratchet unreachable).
744
+ const derivedNum = (0, state_document_cjs_2.toFiniteNumber)(derivedRecord[key]) ?? -Infinity;
745
+ const curatedNum = (0, state_document_cjs_2.toFiniteNumber)(value) ?? -Infinity;
732
746
  // Take the derived value only when it ratchets up (strictly
733
- // greater — #2969's `>` not `>=`); else keep curated.
747
+ // greater — #2969's `>` not `>=`); else keep curated, with its
748
+ // spelling, on a tie too.
734
749
  if (derivedNum > curatedNum)
735
750
  continue;
736
751
  merged[key] = value;
@@ -740,6 +755,7 @@ function applyPreserveAlways(field, cls, ctx) {
740
755
  }
741
756
  }
742
757
  }
758
+ recomputeMergedProgressPercent(merged, derivedRecord);
743
759
  next = merged;
744
760
  }
745
761
  else {
@@ -876,7 +892,36 @@ exports.STATE_MD_SECTIONS = {
876
892
  * Phases 2–7 add cases for the remaining 9 intent kinds. A missing case is
877
893
  * a compile-time error (the function would not return on that path).
878
894
  */
895
+ /**
896
+ * The transitions that rewrite single-line body fields (`Status:`, `Current focus:`, `Plan:` …) with
897
+ * plain line-anchored patterns. Those patterns match a fenced EXAMPLE of a field as readily as the
898
+ * live line (#5460), so these run over the document body with fenced blocks hidden.
899
+ * `milestone*`, `prune` and `rebuild` restructure sections or re-derive the document and are not
900
+ * field-by-field writers.
901
+ */
902
+ const FENCE_MASKED_TRANSITIONS = new Set([
903
+ 'beginPhase', 'advancePlan', 'completePhase', 'plannedPhase', 'patch', 'update', 'sync',
904
+ ]);
879
905
  function transitionCore(content, intent, deps) {
906
+ if (!FENCE_MASKED_TRANSITIONS.has(intent.kind))
907
+ return transitionCoreUnmasked(content, intent, deps);
908
+ // Only the body is masked: a fence opener inside a frontmatter block scalar must not swallow the
909
+ // closing `---` (the frontmatter is not where the live fields are written).
910
+ const body = stripFrontmatter(content);
911
+ if (!content.endsWith(body))
912
+ return transitionCoreUnmasked(content, intent, deps);
913
+ const prefix = content.slice(0, content.length - body.length);
914
+ const fenceMask = (0, markdown_sectionizer_cjs_1.maskFences)(body);
915
+ const result = transitionCoreUnmasked(prefix + fenceMask.masked, intent, deps);
916
+ const restored = fenceMask.restore(result.content);
917
+ // Cannot happen short of a writer bug (a transition dropped or duplicated a placeholder line); a
918
+ // refusal is the one outcome that cannot report a rewrite that did not land.
919
+ if (restored === null) {
920
+ throw new Error(`${intent.kind}: fenced code blocks could not be restored after the edit; refusing to write STATE.md`);
921
+ }
922
+ return { ...result, content: restored };
923
+ }
924
+ function transitionCoreUnmasked(content, intent, deps) {
880
925
  switch (intent.kind) {
881
926
  case 'beginPhase':
882
927
  return beginPhaseCore(content, intent, deps);
@@ -1104,7 +1149,9 @@ function mutateCurrentPositionFirstTime(body, intent, today, updated) {
1104
1149
  const span = locateCurrentPosition(body);
1105
1150
  if (span === null)
1106
1151
  return body;
1107
- let sectionBody = body.slice(span.start, span.end);
1152
+ // #5460: the section's fenced examples are hidden while the live lines are rewritten.
1153
+ const fenceMask = (0, markdown_sectionizer_cjs_1.maskFences)(body.slice(span.start, span.end));
1154
+ let sectionBody = fenceMask.masked;
1108
1155
  // Phase line — inline first, then pipe-table fallback (#1257).
1109
1156
  const phaseLabel = `${intent.phaseNumber}${intent.phaseName ? ` (${intent.phaseName})` : ''} — EXECUTING`;
1110
1157
  if (/^Phase:/m.test(sectionBody)) {
@@ -1146,8 +1193,11 @@ function mutateCurrentPositionFirstTime(body, intent, today, updated) {
1146
1193
  if (replaced !== null)
1147
1194
  sectionBody = replaced;
1148
1195
  }
1196
+ const restored = fenceMask.restore(sectionBody);
1197
+ if (restored === null)
1198
+ return body;
1149
1199
  updated.push('Current Position');
1150
- return body.slice(0, span.start) + sectionBody + body.slice(span.end);
1200
+ return body.slice(0, span.start) + restored + body.slice(span.end);
1151
1201
  }
1152
1202
  /**
1153
1203
  * Resume ## Current Position mutation: only update Last activity line
@@ -1158,7 +1208,8 @@ function mutateCurrentPositionResume(body, intent, today, updated) {
1158
1208
  const span = locateCurrentPosition(body);
1159
1209
  if (span === null)
1160
1210
  return body;
1161
- let sectionBody = body.slice(span.start, span.end);
1211
+ const fenceMask = (0, markdown_sectionizer_cjs_1.maskFences)(body.slice(span.start, span.end));
1212
+ let sectionBody = fenceMask.masked;
1162
1213
  const resumeActivity = `Last activity: ${today} — Phase ${intent.phaseNumber} execution resumed (wave continue)`;
1163
1214
  if (/^Last activity:/im.test(sectionBody)) {
1164
1215
  sectionBody = sectionBody.replace(/^Last activity:.*$/im, resumeActivity);
@@ -1173,7 +1224,10 @@ function mutateCurrentPositionResume(body, intent, today, updated) {
1173
1224
  updated.push('Last activity (resume)');
1174
1225
  }
1175
1226
  }
1176
- return body.slice(0, span.start) + sectionBody + body.slice(span.end);
1227
+ const restored = fenceMask.restore(sectionBody);
1228
+ if (restored === null)
1229
+ return body;
1230
+ return body.slice(0, span.start) + restored + body.slice(span.end);
1177
1231
  }
1178
1232
  const PLAN_SHAPE_N = /^(\d+)(?:\s.*)?$/;
1179
1233
  const PLAN_SHAPE_N_OF_M = /^(\d+)\s+of\s+(\d+)(?:\s.*)?$/;
@@ -1230,7 +1284,8 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
1230
1284
  const span = locateCurrentPosition(content);
1231
1285
  if (span === null)
1232
1286
  return content;
1233
- let sectionBody = content.slice(span.start, span.end);
1287
+ const fenceMask = (0, markdown_sectionizer_cjs_1.maskFences)(content.slice(span.start, span.end));
1288
+ let sectionBody = fenceMask.masked;
1234
1289
  let mutated = false;
1235
1290
  // #3395: Phase is always replaced when a caller passes it — system-derived,
1236
1291
  // not executor-authored (same rule as Plan below). plannedPhaseCore uses
@@ -1343,7 +1398,10 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
1343
1398
  }
1344
1399
  if (!mutated)
1345
1400
  return content;
1346
- return content.slice(0, span.start) + sectionBody + content.slice(span.end);
1401
+ const restored = fenceMask.restore(sectionBody);
1402
+ if (restored === null)
1403
+ return content;
1404
+ return content.slice(0, span.start) + restored + content.slice(span.end);
1347
1405
  }
1348
1406
  // ----------------------------------------------------------------------------
1349
1407
  // advancePlan — intent implementation (Phase 2)
@@ -1642,6 +1700,29 @@ function nextPhaseWording(disk) {
1642
1700
  return { status: 'Ready to plan', currentPlan: 'Not started', stoppedAt: 'ready to plan' };
1643
1701
  }
1644
1702
  }
1703
+ /**
1704
+ * #5165: is STATE.md already positioned PAST phase `phaseNum`? True when its
1705
+ * current phase (frontmatter `current_phase`, else the body `Current Phase` /
1706
+ * `Phase` field's leading phase token) compares greater than `phaseNum`.
1707
+ * Re-completing a phase STATE has already moved past must not rewind the
1708
+ * position; the caller combines this with "the phase is already complete in
1709
+ * ROADMAP.md" so a first-time out-of-order completion (#2028) still
1710
+ * transitions. An unparseable or absent current phase is "not past".
1711
+ */
1712
+ function isStatePastPhase(content, phaseNum, sourcePath) {
1713
+ const fm = extractFrontmatter(content, sourcePath);
1714
+ const fromFm = fm['current_phase'];
1715
+ let raw = typeof fromFm === 'string' || typeof fromFm === 'number' ? String(fromFm) : null;
1716
+ if (raw === null || raw.trim() === '') {
1717
+ const body = stripFrontmatter(content);
1718
+ raw = (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') || (0, state_document_cjs_1.stateExtractField)(body, 'Phase') || null;
1719
+ }
1720
+ // `Phase 4` / `phase 4 of 9` name phase 4 as surely as `4 of 9` does.
1721
+ const m = raw === null ? null : /^\s*(?:phase\s+)?(\d+(?:\.\d+)*)/i.exec(raw);
1722
+ if (!m)
1723
+ return false;
1724
+ return comparePhaseNum(m[1], String(phaseNum)) > 0;
1725
+ }
1645
1726
  /**
1646
1727
  * Apply a `completePhase` transition to STATE.md content.
1647
1728
  *
@@ -1669,6 +1750,12 @@ function completePhaseCore(content, intent, deps) {
1669
1750
  const updated = [];
1670
1751
  const today = deps.clock.localToday();
1671
1752
  const nextWording = nextPhaseWording(intent.isLastPhase ? null : intent.nextPhaseDisk ?? null);
1753
+ // #5165: the milestone the caller resolved (ROADMAP-bound identity first) wins;
1754
+ // else the milestone STATE asserts. It scopes the roadmap-derived counters
1755
+ // below to that milestone's Progress-table rows, the same milestone
1756
+ // `phase complete` scopes its frontmatter progress to.
1757
+ const currentMilestone = (0, phase_lifecycle_cjs_1.milestoneVersionOf)(intent.milestone)
1758
+ ?? (0, phase_lifecycle_cjs_1.milestoneVersionOf)(extractFrontmatter(content, deps.sourcePath)['milestone']);
1672
1759
  // Consult the field-classification table for the frontmatter keys this
1673
1760
  // transition touches (same guard beginPhaseCore applies). A missing row is a
1674
1761
  // substrate defect — fail loudly rather than silently re-encoding policy.
@@ -1812,7 +1899,7 @@ function completePhaseCore(content, intent, deps) {
1812
1899
  let derivedTotalPhases = null;
1813
1900
  const roadmapContent = deps.roadmapProvider ? deps.roadmapProvider() : null;
1814
1901
  if (roadmapContent) {
1815
- const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
1902
+ const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent, currentMilestone);
1816
1903
  // #4901: completed counters only ratchet up, like the frontmatter twin
1817
1904
  // (applyAuthoritativeProgressSubkeys, completedOnlyRaise). A derivation
1818
1905
  // that lands BELOW the recorded counter read a narrower scope than the
@@ -1933,8 +2020,16 @@ function plannedPhaseCore(content, intent, deps) {
1933
2020
  // Phase is system-derived, always replaced (Knuth invariant does not apply);
1934
2021
  // Status / Last activity stay template-aware.
1935
2022
  const beforePos = body;
2023
+ // #5369: with no --name the label reuses the curated frontmatter name (when it is a real one), so the
2024
+ // body line and the frontmatter agree on it (the #3834 reassert keeps that name in frontmatter). A
2025
+ // status fragment a pre-fix write left there is never copied back into the name slot.
2026
+ const curatedName = existingFm['current_phase_name'];
2027
+ const labelName = intent.phaseName
2028
+ ?? (typeof curatedName === 'string' && curatedName.trim() !== '' && !phaseIdMod.isPlannedPhaseStatusPhrase(curatedName)
2029
+ ? curatedName.trim()
2030
+ : null);
1936
2031
  body = mutateCurrentPositionForAdvance(body, {
1937
- phase: `${intent.phaseNumber}${intent.phaseName ? ` (${intent.phaseName})` : ''} — READY TO EXECUTE`,
2032
+ phase: `${intent.phaseNumber}${labelName ? ` (${labelName})` : ''} — READY TO EXECUTE`,
1938
2033
  status: 'Ready to execute',
1939
2034
  lastActivity: `${today} — Phase ${intent.phaseNumber} planning complete`,
1940
2035
  }, statusDefaults, lastActivityDefaults);
@@ -770,7 +770,7 @@ function cmdStateAdvancePlan(cwd, raw) {
770
770
  // silently absorbed.
771
771
  const body = stripFrontmatter(content);
772
772
  const positionScope = matchCurrentPositionSection(body) ?? body;
773
- const positionPhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(positionScope, 'Phase')).phase;
773
+ const positionPhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(positionScope, 'Phase'), resolvePhaseIdConvention(cwd)).phase;
774
774
  // #4093: a Current Position section with ZERO labeled fields has no
775
775
  // `Phase:` line either; frontmatter `current_phase` is the documented
776
776
  // survivor of body drift (the reporter's document still carried it, and
@@ -1283,7 +1283,7 @@ function cmdStateAddDecision(cwd, options, raw) {
1283
1283
  if (!phaseId) {
1284
1284
  const rawState = node_fs_1.default.readFileSync(statePath, 'utf-8');
1285
1285
  const fm = extractFrontmatter(rawState, statePath);
1286
- phaseId = resolveCurrentPhaseId(fm, stripFrontmatter(rawState)) ?? undefined;
1286
+ phaseId = resolveCurrentPhaseId(fm, stripFrontmatter(rawState), resolvePhaseIdConvention(cwd)) ?? undefined;
1287
1287
  }
1288
1288
  const entry = `- [Phase ${phaseId || '?'}]: ${summaryText}${rationaleText ? ` — ${rationaleText}` : ''}`;
1289
1289
  let _added = false;
@@ -1428,7 +1428,7 @@ function cmdStateAddRoadmapEvolution(cwd, options, raw) {
1428
1428
  if (!phaseId) {
1429
1429
  const rawState = node_fs_1.default.readFileSync(statePath, 'utf-8');
1430
1430
  const fm = extractFrontmatter(rawState, statePath);
1431
- phaseId = resolveCurrentPhaseId(fm, stripFrontmatter(rawState)) ?? undefined;
1431
+ phaseId = resolveCurrentPhaseId(fm, stripFrontmatter(rawState), resolvePhaseIdConvention(cwd)) ?? undefined;
1432
1432
  }
1433
1433
  const entry = `- Phase ${phaseId || '?'} ${actionText}${afterText}: ${flatNote}${urgentText}`;
1434
1434
  let duplicate = false;
@@ -1995,26 +1995,27 @@ function preferNewerLastActivity(existingFm, derivedFm) {
1995
1995
  derivedFm['last_activity'] = exRaw;
1996
1996
  }
1997
1997
  }
1998
- function parseProsePhaseField(value) {
1998
+ function parseProsePhaseField(value, convention) {
1999
1999
  // #2121 Phase 2 (#2125): delegate to the canonical anchored parser so this
2000
2000
  // module holds no independent prose phase-id regex. Drives #2111 — the
2001
2001
  // anchored parser returns { phase: null } for a "Milestone vX.Y complete"
2002
2002
  // body line (the old unanchored regex mined the minor-version digit, e.g.
2003
2003
  // v0.5 -> "5"), so syncStateFrontmatter's #905 guard preserves the real
2004
2004
  // current_phase instead of clobbering it.
2005
- return parsePhaseFromProse(value);
2005
+ // #5626: under `milestone-prefixed` the id is the whole `M-NN`; the convention comes from its owner.
2006
+ return parsePhaseFromProse(value, convention);
2006
2007
  }
2007
- function resolveStatePhase(fm, body) {
2008
+ function resolveStatePhase(fm, body, convention) {
2008
2009
  const currentPositionScope = matchCurrentPositionSection(body) ?? body;
2009
2010
  const frontmatterRaw = (0, state_document_cjs_1.stateFieldValue)(fm, body, 'current_phase', null).value;
2010
2011
  const legacyRaw = (0, state_document_cjs_1.stateFieldValue)(fm, currentPositionScope, null, 'Current Phase').value;
2011
2012
  const currentPositionRaw = (0, state_document_cjs_1.stateFieldValue)(fm, currentPositionScope, null, 'Phase').value;
2012
2013
  const sources = {
2013
- frontmatter: parseProsePhaseField(frontmatterRaw).phase,
2014
- legacy_current_phase: parseProsePhaseField(legacyRaw).phase,
2015
- current_position_phase: parseProsePhaseField(currentPositionRaw).phase,
2014
+ frontmatter: parseProsePhaseField(frontmatterRaw, convention).phase,
2015
+ legacy_current_phase: parseProsePhaseField(legacyRaw, convention).phase,
2016
+ current_position_phase: parseProsePhaseField(currentPositionRaw, convention).phase,
2016
2017
  };
2017
- const prosePhase = parseProsePhaseField(currentPositionRaw);
2018
+ const prosePhase = parseProsePhaseField(currentPositionRaw, convention);
2018
2019
  return {
2019
2020
  phase: sources.frontmatter ?? sources.legacy_current_phase ?? sources.current_position_phase,
2020
2021
  name: (0, state_document_cjs_1.stateFieldValue)(fm, body, 'current_phase_name', null).value
@@ -2069,9 +2070,9 @@ function resolveStatePhase(fm, body) {
2069
2070
  * Reconcile the two only by giving `resolveStatePhase` an explicit scope
2070
2071
  * parameter — never by pointing this at it and dropping the difference.
2071
2072
  */
2072
- function resolveCurrentPhaseId(fm, body) {
2073
+ function resolveCurrentPhaseId(fm, body, convention) {
2073
2074
  const positionSection = sliceCurrentPositionSection(body);
2074
- const prosePhase = positionSection !== null ? parseProsePhaseField((0, state_document_cjs_1.stateFieldValue)(fm, positionSection, null, 'Phase').value).phase : null;
2075
+ const prosePhase = positionSection !== null ? parseProsePhaseField((0, state_document_cjs_1.stateFieldValue)(fm, positionSection, null, 'Phase').value, convention).phase : null;
2075
2076
  return (0, state_document_cjs_1.stateFieldValue)(fm, body, 'current_phase', 'Current Phase').value ?? prosePhase;
2076
2077
  }
2077
2078
  function parseProseLastActivityField(value) {
@@ -2109,7 +2110,7 @@ function cmdStateSnapshot(cwd, raw) {
2109
2110
  // so it is scopeable exactly like Stopped At under ## Session. Fall back to
2110
2111
  // full-body search only when no ## Current Position section exists, so files
2111
2112
  // with no section heading keep their current behaviour.
2112
- const resolvedPhase = resolveStatePhase(fm, body);
2113
+ const resolvedPhase = resolveStatePhase(fm, body, resolvePhaseIdConvention(cwd));
2113
2114
  const currentPhase = resolvedPhase.phase;
2114
2115
  const currentPhaseName = resolvedPhase.name;
2115
2116
  const totalPhasesRaw = (0, state_document_cjs_1.stateFieldValue)(fm, body, 'total_phases', 'Total Phases').value;
@@ -2531,7 +2532,7 @@ milestoneExplicitlyNull) {
2531
2532
  // section overwrites current_phase here, and the next read surfaces it. Fall
2532
2533
  // back to full-body search when no ## Current Position section exists.
2533
2534
  const currentPositionScope = matchCurrentPositionSection(bodyContent) ?? bodyContent;
2534
- const prosePhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(currentPositionScope, 'Phase'));
2535
+ const prosePhase = parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(currentPositionScope, 'Phase'), cwd ? resolvePhaseIdConvention(cwd) : null);
2535
2536
  const currentPhase = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase') ?? prosePhase.phase;
2536
2537
  const currentPhaseName = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Phase Name') ?? prosePhase.name;
2537
2538
  const currentPlan = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Current Plan');
@@ -2799,8 +2800,14 @@ milestoneExplicitlyNull) {
2799
2800
  // — no second ROADMAP parser here. A ROADMAP without a canonical
2800
2801
  // `## Progress` table resolves no table → floor inert (disk count
2801
2802
  // stands), the owner's own answer to "what is countable".
2803
+ // #5165: the milestone-grouped `## Progress` table keeps every
2804
+ // milestone's rows and sits OUTSIDE any narrower roadmapScope, so the
2805
+ // floor names the milestone STATE asserts (assertedMilestoneVersion —
2806
+ // present on every resync path, unlike the optional storedMilestone
2807
+ // parameter) and the owner counts only that milestone's rows. No row
2808
+ // for it => nulls => the floor is inert and the disk count stands.
2802
2809
  const roadmapCompletedPhases = roadmapScope !== null && safeToUseRoadmapCount && !diskCountsWithheld
2803
- ? deriveProgressFromRoadmap(roadmapScope).completedPhases
2810
+ ? deriveProgressFromRoadmap(roadmapScope, assertedMilestoneVersion ?? storedMilestone).completedPhases
2804
2811
  : null;
2805
2812
  const flooredCompletedPhases = roadmapCompletedPhases !== null
2806
2813
  ? Math.max(diskCompletedPhases, roadmapCompletedPhases)
@@ -3227,6 +3234,8 @@ function applyAuthoritativeProgressSubkeys(fm, intent, opts) {
3227
3234
  for (const [subkey, value] of Object.entries(intent)) {
3228
3235
  if (typeof value !== 'number' || !Number.isFinite(value))
3229
3236
  continue;
3237
+ if (subkey === 'percent')
3238
+ continue; // composed below, never applied raw (#5165)
3230
3239
  if (!getFieldClassification(`progress.${subkey}`))
3231
3240
  continue;
3232
3241
  const isCompletedCounter = subkey === 'completed_phases' || subkey === 'completed_plans';
@@ -3244,10 +3253,18 @@ function applyAuthoritativeProgressSubkeys(fm, intent, opts) {
3244
3253
  }
3245
3254
  // percent: applied only when a completed counter moved (coherent with the
3246
3255
  // counters that just landed) or when no percent exists to contradict.
3256
+ // #5165 / ADR-4629 §8.5: `computeProgressPercent` is the one composition
3257
+ // owner (min(plan fraction, phase fraction)) that every resyncing write uses.
3258
+ // When the block carries the counters it needs, the percent is composed by
3259
+ // that owner over the counters just landed, so a later resyncing write
3260
+ // cannot move what this transition wrote. The intent's own percent (a
3261
+ // phase-only fraction) is only the fallback for a block without plan counts.
3247
3262
  const intentPercent = intent['percent'];
3248
3263
  if (typeof intentPercent === 'number' && Number.isFinite(intentPercent) && (completedMoved || (0, state_document_cjs_1.toFiniteNumber)(base['percent']) === null)) {
3249
- if (!Object.is(base['percent'], intentPercent)) {
3250
- base['percent'] = intentPercent;
3264
+ const composed = (0, state_document_cjs_1.computeProgressPercent)((0, state_document_cjs_1.toFiniteNumber)(base['completed_plans']), (0, state_document_cjs_1.toFiniteNumber)(base['total_plans']), (0, state_document_cjs_1.toFiniteNumber)(base['completed_phases']), (0, state_document_cjs_1.toFiniteNumber)(base['total_phases']), SCOPE.COMPLETE);
3265
+ const nextPercent = composed !== null ? composed : intentPercent;
3266
+ if (!Object.is(base['percent'], nextPercent)) {
3267
+ base['percent'] = nextPercent;
3251
3268
  changed = true;
3252
3269
  }
3253
3270
  }
@@ -3831,7 +3848,10 @@ function assertStatePreservationOptions(options, caller) {
3831
3848
  throw err;
3832
3849
  }
3833
3850
  }
3834
- function applyPostSyncPreservation(originalContent, transformedContent, syncedContent, statePath, options) {
3851
+ function applyPostSyncPreservation(originalContent, transformedContent, syncedContent, statePath, options,
3852
+ // #5626: the project's phase-id convention, resolved by the caller from its cwd (null: unset). Trailing and
3853
+ // optional so `options` stays the fifth parameter the options guard and its callers rely on.
3854
+ phaseIdConvention = null) {
3835
3855
  assertStatePreservationOptions(options, 'applyPostSyncPreservation');
3836
3856
  const { resync, authoritativeFm, deriveProgressKeys, divergedFields, explicitProgressField, preWriteState } = options;
3837
3857
  // Bug #1230: delta heuristic — snapshot pre-transform body source fields so
@@ -3888,7 +3908,7 @@ function applyPostSyncPreservation(originalContent, transformedContent, syncedCo
3888
3908
  const preCurrentPositionScope = matchCurrentPositionSection(preBody) ?? preBody;
3889
3909
  const preBodyCurrentPlan = (0, state_document_cjs_1.stateExtractField)(preBody, 'Current Plan');
3890
3910
  const preBodyCurrentPhase = (0, state_document_cjs_1.stateExtractField)(preBody, 'Current Phase')
3891
- ?? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(preCurrentPositionScope, 'Phase')).phase;
3911
+ ?? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(preCurrentPositionScope, 'Phase'), phaseIdConvention).phase;
3892
3912
  const preBodyPausedAt = (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Paused At');
3893
3913
  const preBodyLastActivityRaw = (0, state_document_cjs_1.stateExtractField)(preBody, 'Last Activity')
3894
3914
  ?? (0, state_document_cjs_1.stateExtractField)(preBody, 'Last activity');
@@ -3914,7 +3934,7 @@ function applyPostSyncPreservation(originalContent, transformedContent, syncedCo
3914
3934
  const postCurrentPositionScope = matchCurrentPositionSection(postBody) ?? postBody;
3915
3935
  const postBodyCurrentPlan = (0, state_document_cjs_1.stateExtractField)(postBody, 'Current Plan');
3916
3936
  const postBodyCurrentPhase = (0, state_document_cjs_1.stateExtractField)(postBody, 'Current Phase')
3917
- ?? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(postCurrentPositionScope, 'Phase')).phase;
3937
+ ?? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(postCurrentPositionScope, 'Phase'), phaseIdConvention).phase;
3918
3938
  const postBodyPausedAt = (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Paused At');
3919
3939
  const postBodyLastActivityRaw = (0, state_document_cjs_1.stateExtractField)(postBody, 'Last Activity')
3920
3940
  ?? (0, state_document_cjs_1.stateExtractField)(postBody, 'Last activity');
@@ -4131,7 +4151,7 @@ function applyPostSyncPreservation(originalContent, transformedContent, syncedCo
4131
4151
  function syncAndPreserveStateMd(originalContent, transformedContent, statePath, cwd, options) {
4132
4152
  assertStatePreservationOptions(options, 'syncAndPreserveStateMd');
4133
4153
  const synced = syncStateFrontmatter(transformedContent, cwd, options.authoritativeFm);
4134
- return applyPostSyncPreservation(originalContent, transformedContent, synced, statePath, options);
4154
+ return applyPostSyncPreservation(originalContent, transformedContent, synced, statePath, options, cwd ? resolvePhaseIdConvention(cwd) : null);
4135
4155
  }
4136
4156
  /**
4137
4157
  * Atomic read-modify-write for STATE.md.
@@ -4789,8 +4809,9 @@ function stateSectionScopeUnits(units, sectionTargets) {
4789
4809
  * matching `readStateTarget`'s long-standing special case.
4790
4810
  */
4791
4811
  function verifyStateWriteIntent(intent, preContent, postContent, statePath, bodyDeltas) {
4792
- const pre = (0, shell_command_projection_cjs_1.normalizeContent)(statePath, preContent).content;
4793
- const post = (0, shell_command_projection_cjs_1.normalizeContent)(statePath, postContent).content;
4812
+ // #5165: the write lands scoped to what it changed (platformWriteSync reads the file it replaces).
4813
+ const pre = (0, shell_command_projection_cjs_1.normalizeContent)(statePath, preContent, { prior: preContent }).content;
4814
+ const post = (0, shell_command_projection_cjs_1.normalizeContent)(statePath, postContent, { prior: preContent }).content;
4794
4815
  const preFm = extractFrontmatter(pre, statePath);
4795
4816
  const postFm = extractFrontmatter(post, statePath);
4796
4817
  const preBody = stripFrontmatter(pre);
@@ -4989,7 +5010,7 @@ function applyStateWriteIntent(statePath, intent, transformFn, cwd, options = {}
4989
5010
  return unwritten(verification);
4990
5011
  (0, shell_command_projection_cjs_1.platformWriteSync)(statePath, candidate);
4991
5012
  const reread = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
4992
- if (reread !== (0, shell_command_projection_cjs_1.normalizeContent)(statePath, candidate).content) {
5013
+ if (reread !== (0, shell_command_projection_cjs_1.normalizeContent)(statePath, candidate, { prior: pre }).content) {
4993
5014
  return Object.freeze({
4994
5015
  ok: false,
4995
5016
  reasons: Object.freeze([STATE_WRITE_INTENT_FAILURE.REREAD_MISMATCH]),
@@ -5055,7 +5076,7 @@ function cmdStateJson(cwd, raw) {
5055
5076
  const bodyPausedAt = (0, state_document_cjs_1.stateExtractField)(sessionScope, 'Paused At');
5056
5077
  const bodyPhaseSource = (0, state_document_cjs_1.stateExtractField)(body, 'Phase');
5057
5078
  const bodyCurrentPhase = (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase')
5058
- ?? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(positionScope, 'Phase')).phase;
5079
+ ?? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(positionScope, 'Phase'), resolvePhaseIdConvention(cwd)).phase;
5059
5080
  const bodyCurrentPlan = (0, state_document_cjs_1.stateExtractField)(body, 'Current Plan');
5060
5081
  const bodyStatus = (0, state_document_cjs_1.stateExtractField)(body, 'Status');
5061
5082
  // #3836: mirrors applyPostSyncPreservation's own derivation (state.cts
@@ -5527,7 +5548,8 @@ function cmdStatePlannedPhase(cwd, phaseNumber, phaseName, planCount, raw) {
5527
5548
  if (!intent.phaseName) {
5528
5549
  const preFm = extractFrontmatter(content, statePath);
5529
5550
  const curatedName = preFm['current_phase_name'];
5530
- if (typeof curatedName === 'string' && curatedName.trim().length > 0) {
5551
+ // #5369: a status fragment a pre-fix write left as the name is not curated; do not reassert it.
5552
+ if (typeof curatedName === 'string' && curatedName.trim().length > 0 && !phaseIdMod.isPlannedPhaseStatusPhrase(curatedName)) {
5531
5553
  rmwOptions.authoritativeFm = { current_phase_name: curatedName };
5532
5554
  }
5533
5555
  }
@@ -5756,7 +5778,7 @@ function cmdStateValidate(cwd, raw, opts = {}) {
5756
5778
  const { fm, body, scope: initialScope } = readStateFrontmatterScoped(content, statePath);
5757
5779
  const scope = initialScope;
5758
5780
  const status = (0, state_document_cjs_1.stateFieldValue)(fm, body, 'status', 'Status').value || '';
5759
- const resolvedPhase = resolveStatePhase(fm, body);
5781
+ const resolvedPhase = resolveStatePhase(fm, body, resolvePhaseIdConvention(cwd));
5760
5782
  const currentPhase = resolvedPhase.phase;
5761
5783
  const totalPlansRaw = (0, state_document_cjs_1.stateFieldValue)(fm, body, 'total_plans_in_phase', 'Total Plans in Phase').value;
5762
5784
  const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
@@ -6210,7 +6232,7 @@ function cmdStatePrune(cwd, options, raw) {
6210
6232
  const body = stripFrontmatter(rawState);
6211
6233
  // Prune needs an integer cutoff, so it parses the resolved id itself; a
6212
6234
  // non-numeric or absent id lands on 0 and prune bails, as before.
6213
- const currentPhase = parseInt(String(resolveCurrentPhaseId(fm, body)), 10) || 0;
6235
+ const currentPhase = parseInt(String(resolveCurrentPhaseId(fm, body, resolvePhaseIdConvention(cwd))), 10) || 0;
6214
6236
  const cutoff = currentPhase - keepRecent;
6215
6237
  if (cutoff <= 0) {
6216
6238
  emit({ pruned: false, reason: `Only ${currentPhase} phases — nothing to prune with --keep-recent ${keepRecent}` }, raw, 'false');
@@ -36,6 +36,7 @@ const node_os_1 = __importDefault(require("node:os"));
36
36
  const node_path_1 = __importDefault(require("node:path"));
37
37
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
38
38
  const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
39
+ const args_terminator_cjs_1 = require("./args-terminator.cjs");
39
40
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
41
  const installProfiles = require("./install-profiles.cjs");
41
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -322,6 +323,14 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
322
323
  if (node_path_1.default.resolve(runtimeConfigDir) !== node_path_1.default.resolve(layout.configDir)) {
323
324
  throw new TypeError('applySurface runtimeConfigDir must match layout.configDir');
324
325
  }
326
+ // #5637: re-staged commands and skills keep the typed-text terminator nonce the install wrote,
327
+ // when it wrote one; this process is not an installer, so nothing armed it.
328
+ // A directory with no nonce (an install from before this change) is re-staged canonical, and a
329
+ // nonce armed earlier in this process for another directory is not carried over.
330
+ if ((0, args_terminator_cjs_1.readInstalledNonce)(layout.configDir) !== null)
331
+ (0, args_terminator_cjs_1.armArgsNonce)(layout.configDir, true);
332
+ else
333
+ (0, args_terminator_cjs_1.disarmArgsNonce)();
325
334
  // #3712: the dest selection below prefers `kind.home` over layout.configDir and
326
335
  // then hands it to the destructive _syncGsdDir, so surface apply is a third
327
336
  // escape route into the developer's real home alongside install/uninstall.
@@ -20,10 +20,8 @@ const { output, error, ERROR_REASON } = ioMod;
20
20
  const planDocumentMod = require("./plan-document.cjs");
21
21
  const { parsePlanDocument } = planDocumentMod;
22
22
  // eslint-disable-next-line @typescript-eslint/no-require-imports
23
- const capabilityLoaderMod = require("./capability-loader.cjs");
24
- // eslint-disable-next-line @typescript-eslint/no-require-imports
25
23
  const taskContentResolutionMod = require("./task-content-resolution.cjs");
26
- const { resolveTaskContent, ResolverAmbiguousError, ResolverFailedError, ResolverTimeoutError, ResolverMalformedOutputError, } = taskContentResolutionMod;
24
+ const { resolveTaskContent, loadResolverCapabilities, isResolverFailure, } = taskContentResolutionMod;
27
25
  // ─── Implementation ───────────────────────────────────────────────────────────
28
26
  function isBehaviorAddingTaskContent(content) {
29
27
  const tddTrue = /\btdd\s*=\s*["']true["']/i.test(content);
@@ -60,19 +58,6 @@ function isBehaviorAddingTaskContent(content) {
60
58
  reason: isBehaviorAdding ? null : `Not behavior-adding: ${missing.join('; ')}`,
61
59
  };
62
60
  }
63
- /**
64
- * Default (production) capability loader for `resolve-content`: the merged
65
- * first-party + validated-installed-overlay registry (ADR-1244 D2), the
66
- * established runtime read path for "installed capabilities including
67
- * third-party" — as opposed to `capability-loader.cts`'s heavier build-time
68
- * validation entry points or the static `capability-registry.cjs` alone
69
- * (first-party only, would miss a third-party capability's
70
- * `taskContentResolver` declaration entirely).
71
- */
72
- function defaultLoadCapabilities(cwd) {
73
- const registry = capabilityLoaderMod.loadRegistry({ includeInstalled: true, cwd });
74
- return Object.values(registry.capabilities ?? {});
75
- }
76
61
  function parseResolveContentArgs(args) {
77
62
  let plan = null;
78
63
  let taskId = null;
@@ -93,12 +78,10 @@ function parseResolveContentArgs(args) {
93
78
  * (ADR-3646 Decision 2). Resolves one task's content from the external
94
79
  * tracker its `tracker-id` attribute names, via `task-content-resolution.cts`.
95
80
  *
96
- * HARD-HALT CONTRACT: a thrown `ResolverAmbiguousError` / `ResolverFailedError`
97
- * / `ResolverTimeoutError` / `ResolverMalformedOutputError` from
81
+ * HARD-HALT CONTRACT: a thrown resolver failure (`isResolverFailure`) from
98
82
  * `resolveTaskContent` is turned into this CLI's own non-zero exit via
99
83
  * `error()` — never swallowed into a `{resolved: false}` JSON answer. Any
100
- * other thrown error is not one of the four documented resolver-error
101
- * classes and is allowed to propagate uncaught.
84
+ * other thrown error is not a documented resolver-error class and is allowed to propagate uncaught.
102
85
  */
103
86
  function routeResolveContent({ args, cwd, raw }, deps = {}) {
104
87
  const usage = 'Usage: task resolve-content --plan <path> --task-id <tracker-id> --raw';
@@ -127,7 +110,7 @@ function routeResolveContent({ args, cwd, raw }, deps = {}) {
127
110
  error(`No task with tracker-id '${taskId}' found in plan: ${plan}`, ERROR_REASON.USAGE);
128
111
  return;
129
112
  }
130
- const loadCapabilities = deps.loadCapabilities ?? defaultLoadCapabilities;
113
+ const loadCapabilities = deps.loadCapabilities ?? loadResolverCapabilities;
131
114
  const capabilities = loadCapabilities(projectRoot);
132
115
  const resolveFn = deps.resolveTaskContentFn ?? resolveTaskContent;
133
116
  let result;
@@ -135,10 +118,7 @@ function routeResolveContent({ args, cwd, raw }, deps = {}) {
135
118
  result = resolveFn({ trackerId: task.trackerId, capabilities });
136
119
  }
137
120
  catch (err) {
138
- if (err instanceof ResolverAmbiguousError ||
139
- err instanceof ResolverFailedError ||
140
- err instanceof ResolverTimeoutError ||
141
- err instanceof ResolverMalformedOutputError) {
121
+ if (isResolverFailure(err)) {
142
122
  error(err.message, ERROR_REASON.UNKNOWN);
143
123
  return;
144
124
  }