@opengsd/gsd-core 1.18.0 → 1.20.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 (308) 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 +7 -7
  5. package/agents/gsd-code-fixer.md +13 -7
  6. package/agents/gsd-code-reviewer.compact.md +49 -8
  7. package/agents/gsd-code-reviewer.md +53 -14
  8. package/agents/gsd-executor.md +8 -4
  9. package/agents/gsd-verifier.md +1 -1
  10. package/bin/install.js +422 -103
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +179 -30
  13. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  14. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  15. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  16. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  17. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  18. package/gsd-core/bin/lib/capability-registry.cjs +376 -103
  19. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  20. package/gsd-core/bin/lib/capability-validator.cjs +32 -0
  21. package/gsd-core/bin/lib/commands.cjs +27 -17
  22. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  23. package/gsd-core/bin/lib/config.cjs +256 -58
  24. package/gsd-core/bin/lib/core-utils.cjs +45 -0
  25. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  26. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  27. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  28. package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
  29. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  30. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  33. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  34. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  35. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  36. package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
  37. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
  38. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  39. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  40. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  41. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  42. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  43. package/gsd-core/bin/lib/init.cjs +245 -68
  44. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  45. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  47. package/gsd-core/bin/lib/io.cjs +6 -0
  48. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  49. package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
  50. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  51. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  52. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  54. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  55. package/gsd-core/bin/lib/milestone.cjs +61 -51
  56. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  57. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  58. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  59. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  60. package/gsd-core/bin/lib/phase-id.cjs +629 -38
  61. package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
  62. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  63. package/gsd-core/bin/lib/phase.cjs +3066 -653
  64. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  65. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  66. package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
  67. package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
  68. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  69. package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
  70. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  71. package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
  72. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  73. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  74. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  75. package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
  76. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  77. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
  78. package/gsd-core/bin/lib/roadmap.cjs +383 -123
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  82. package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
  83. package/gsd-core/bin/lib/smart-entry.cjs +35 -11
  84. package/gsd-core/bin/lib/state-contract.cjs +39 -15
  85. package/gsd-core/bin/lib/state-document.cjs +344 -27
  86. package/gsd-core/bin/lib/state-transition.cjs +156 -32
  87. package/gsd-core/bin/lib/state.cjs +527 -186
  88. package/gsd-core/bin/lib/surface.cjs +9 -0
  89. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  90. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  91. package/gsd-core/bin/lib/template.cjs +2 -2
  92. package/gsd-core/bin/lib/validate.cjs +3 -2
  93. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  94. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  95. package/gsd-core/bin/lib/verification.cjs +96 -9
  96. package/gsd-core/bin/lib/verify.cjs +4 -2
  97. package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
  98. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  99. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  100. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  101. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  102. package/gsd-core/references/cicd-review-patterns.md +45 -0
  103. package/gsd-core/references/git-integration.md +11 -0
  104. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  105. package/gsd-core/references/necessity-review.md +48 -0
  106. package/gsd-core/references/planning-config.md +2 -0
  107. package/gsd-core/references/tdd.md +2 -2
  108. package/gsd-core/templates/DEBUG.md +16 -0
  109. package/gsd-core/templates/config.json +2 -1
  110. package/gsd-core/templates/verification-report.md +1 -1
  111. package/gsd-core/workflows/audit-fix.md +2 -1
  112. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  113. package/gsd-core/workflows/code-review-fix.md +46 -5
  114. package/gsd-core/workflows/code-review.md +64 -7
  115. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  116. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  117. package/gsd-core/workflows/complete-milestone.md +37 -150
  118. package/gsd-core/workflows/diagnose-issues.md +29 -5
  119. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  120. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  121. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  122. package/gsd-core/workflows/discuss-phase.md +6 -10
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  124. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  126. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  127. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  128. package/gsd-core/workflows/execute-phase.md +10 -6
  129. package/gsd-core/workflows/execute-plan.md +1 -1
  130. package/gsd-core/workflows/health.md +2 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  133. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  134. package/gsd-core/workflows/plan-phase.md +14 -14
  135. package/gsd-core/workflows/pr-branch.md +2 -0
  136. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  137. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  138. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  139. package/gsd-core/workflows/quick.md +1 -1
  140. package/gsd-core/workflows/reapply-patches.md +104 -0
  141. package/gsd-core/workflows/review.md +70 -154
  142. package/gsd-core/workflows/section-manifest.json +5 -0
  143. package/gsd-core/workflows/secure-phase.md +1 -1
  144. package/gsd-core/workflows/settings-advanced.md +57 -9
  145. package/gsd-core/workflows/settings.md +1 -0
  146. package/gsd-core/workflows/ship.md +15 -4
  147. package/gsd-core/workflows/spec-phase.md +3 -6
  148. package/gsd-core/workflows/verify-work.md +4 -3
  149. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  150. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  151. package/hooks/dist/gsd-graphify-update.sh +6 -0
  152. package/hooks/dist/gsd-path-export.js +95 -0
  153. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  154. package/hooks/dist/gsd-statusline.js +37 -3
  155. package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
  156. package/hooks/dist/gsd-workflow-guard.js +3 -2
  157. package/hooks/dist/gsd-worktree-path-guard.js +29 -2
  158. package/hooks/dist/lib/git-probe.js +16 -0
  159. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  160. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  161. package/hooks/gsd-agent-isolation-guard.js +11 -1
  162. package/hooks/gsd-cursor-subagent-start.js +6 -2
  163. package/hooks/gsd-graphify-update.sh +6 -0
  164. package/hooks/gsd-path-export.js +95 -0
  165. package/hooks/gsd-secret-read-guard.js +172 -39
  166. package/hooks/gsd-statusline.js +37 -3
  167. package/hooks/gsd-windsurf-pre-write.js +29 -2
  168. package/hooks/gsd-workflow-guard.js +3 -2
  169. package/hooks/gsd-worktree-path-guard.js +29 -2
  170. package/hooks/hooks.json +1 -0
  171. package/hooks/lib/git-probe.js +16 -0
  172. package/hooks/lib/isolation-sentinel.js +44 -0
  173. package/hooks/managed-hooks-registry.cjs +1 -0
  174. package/package.json +15 -8
  175. package/pi/gsd.cjs +1047 -10
  176. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  177. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  178. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  179. package/pi/package-agents/gsd-code-fixer.md +774 -0
  180. package/pi/package-agents/gsd-code-reviewer.md +441 -0
  181. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  182. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  183. package/pi/package-agents/gsd-debugger.md +1274 -0
  184. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  185. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  186. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  187. package/pi/package-agents/gsd-doc-writer.md +626 -0
  188. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  189. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  190. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  191. package/pi/package-agents/gsd-eval-planner.md +149 -0
  192. package/pi/package-agents/gsd-executor.md +898 -0
  193. package/pi/package-agents/gsd-framework-selector.md +159 -0
  194. package/pi/package-agents/gsd-integration-checker.md +476 -0
  195. package/pi/package-agents/gsd-intel-updater.md +338 -0
  196. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  197. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  198. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  199. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  200. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  201. package/pi/package-agents/gsd-planner.md +1003 -0
  202. package/pi/package-agents/gsd-project-researcher.md +611 -0
  203. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  204. package/pi/package-agents/gsd-roadmapper.md +779 -0
  205. package/pi/package-agents/gsd-security-auditor.md +171 -0
  206. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  207. package/pi/package-agents/gsd-ui-checker.md +417 -0
  208. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  209. package/pi/package-agents/gsd-user-profiler.md +174 -0
  210. package/pi/package-agents/gsd-verifier.md +963 -0
  211. package/pi/package-agents/manifest.json +39 -0
  212. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  213. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  214. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  215. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  216. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  217. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  218. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  219. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  220. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  221. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  222. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  223. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  224. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  225. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  226. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  227. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  228. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  229. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  230. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  231. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  232. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  233. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  234. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  235. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  236. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  237. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  238. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  239. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  240. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  241. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  242. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  243. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  244. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  245. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  246. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  247. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  248. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  249. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  250. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  251. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  252. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  253. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  254. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  255. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  256. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  257. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  258. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  259. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  260. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  261. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  262. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  263. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  264. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  265. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  266. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  267. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  268. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  269. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  270. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  271. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  272. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  273. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  274. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  275. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  276. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  277. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  278. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  279. package/pi/package-skills/gsd-update/SKILL.md +54 -0
  280. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  281. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  282. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  283. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  284. package/scripts/build-hooks.js +4 -0
  285. package/scripts/ci-next-health.cjs +246 -23
  286. package/scripts/command-contract-helpers.cjs +1 -1
  287. package/scripts/docs-guard-registry.cjs +22 -7
  288. package/scripts/gen-features.cjs +14 -2
  289. package/scripts/gen-health-docs.cjs +13 -1
  290. package/scripts/gen-pi-package-skills.cjs +217 -0
  291. package/scripts/lib/drift-scan.cjs +27 -0
  292. package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
  293. package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
  294. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  295. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  296. package/scripts/lint-retired-runtime-name.cjs +5 -0
  297. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  298. package/scripts/mutation-matrix.cjs +23 -1
  299. package/scripts/npm-audit-baseline.cjs +41 -7
  300. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  301. package/scripts/select-docs-guards.cjs +1 -1
  302. package/scripts/self-test-report-policy.cjs +350 -0
  303. package/scripts/sync-next-version.cjs +38 -6
  304. package/skills/gsd-update/SKILL.md +5 -2
  305. package/vscode/package.json +1 -1
  306. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  307. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  308. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -8,7 +8,8 @@
8
8
  * already answers:
9
9
  * - `planning-workspace.cjs`'s `planningPaths` for every `.planning/` path
10
10
  * (workstream-aware — never re-derived here).
11
- * - `phase-lifecycle.cjs`'s `locateProgressTable` (this issue's Edit 1) for
11
+ * - `roadmap-progress.cjs`'s `locateRoadmapProgress` (#5761; was
12
+ * `phase-lifecycle`'s `locateProgressTable`) for
12
13
  * "where is the Progress table" — the SAME table
13
14
  * `deriveProgressFromRoadmap` counts from, so `phases[]` and
14
15
  * `deriveProgressFromRoadmap`'s `totalPhases` can never disagree about
@@ -79,6 +80,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
79
80
  // documented `state.cjs -> state-contract.cjs -> smart-entry.cjs -> state.cjs`
80
81
  // require cycle the file header warns about.
81
82
  const phase_status_cjs_1 = require("./phase-status.cjs");
83
+ const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
82
84
  // ─── Public wire vocabulary ────────────────────────────────────────────────
83
85
  exports.STATE_CONTRACT_VERSION = '1.0.0';
84
86
  exports.STATE_CONTRACT_FLAVOR = 'core';
@@ -114,7 +116,7 @@ function owners() {
114
116
  /* eslint-disable @typescript-eslint/no-require-imports */
115
117
  const planningWorkspaceMod = require('./planning-workspace.cjs');
116
118
  const shellCommandProjectionMod = require('./shell-command-projection.cjs');
117
- const phaseLifecycleMod = require('./phase-lifecycle.cjs');
119
+ const roadmapProgressMod = require('./roadmap-progress.cjs');
118
120
  const phaseIdMod = require('./phase-id.cjs');
119
121
  const roadmapParserMod = require('./roadmap-parser.cjs');
120
122
  const markdownSectionizerMod = require('./markdown-sectionizer.cjs');
@@ -126,9 +128,10 @@ function owners() {
126
128
  planningPaths: planningWorkspaceMod.planningPaths,
127
129
  platformReadSync: shellCommandProjectionMod.platformReadSync,
128
130
  platformWriteSync: shellCommandProjectionMod.platformWriteSync,
129
- locateProgressTable: phaseLifecycleMod.locateProgressTable,
131
+ locateRoadmapProgress: roadmapProgressMod.locateRoadmapProgress,
130
132
  isSentinelPhaseId: phaseIdMod.isSentinelPhaseId,
131
133
  parsePhaseFromProse: phaseIdMod.parsePhaseFromProse,
134
+ resolvePhaseIdConvention: planningWorkspaceMod.resolvePhaseIdConvention,
132
135
  getMilestoneInfo: roadmapParserMod.getMilestoneInfo,
133
136
  collectSection: markdownSectionizerMod.collectSection,
134
137
  stateFieldValue: stateDocumentMod.stateFieldValue,
@@ -154,16 +157,31 @@ function buildMilestone(cwd) {
154
157
  }
155
158
  // ─── phases: Progress-table (primary) enumerator ───────────────────────────
156
159
  const PHASE_CELL_RE = /^(\d+(?:\.\d+)*)\s*[.:)–—-]?\s*(.*)$/;
157
- function phasesFromProgressTable(table) {
160
+ // #5626: under `milestone-prefixed` the phase id is the whole `M-NN` (`3-01`), so the number capture takes
161
+ // one optional `-NN` segment. Chosen by the convention value alone: every other convention keeps the
162
+ // dotted-only capture above, and the `\d+-\d+` shape stays an ordinary leading integer there.
163
+ const MILESTONE_PREFIXED = 'milestone-prefixed';
164
+ const PHASE_CELL_MILESTONE_RE = /^(\d+(?:-\d+)?(?:\.\d+)*)\s*[.:)–—-]?\s*(.*)$/;
165
+ /** The project's `phase_id_convention` (null when unset or unreadable), through its one owner. */
166
+ function phaseIdConventionOf(cwd) {
167
+ try {
168
+ return owners().resolvePhaseIdConvention(cwd);
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ function phasesFromProgressTable(table, convention) {
158
175
  const { isSentinelPhaseId } = owners();
159
176
  const phases = [];
177
+ const cellRe = convention === MILESTONE_PREFIXED ? PHASE_CELL_MILESTONE_RE : PHASE_CELL_RE;
160
178
  for (const row of table.rows) {
161
179
  const cell = (row['Phase'] ?? '').trim();
162
180
  if (!/^\d/.test(cell))
163
181
  continue; // stray note row, no leading digit
164
182
  if (isSentinelPhaseId(cell))
165
183
  continue; // phase 0 / 999.x sentinels
166
- const m = PHASE_CELL_RE.exec(cell);
184
+ const m = cellRe.exec(cell);
167
185
  const number = m ? m[1] : cell;
168
186
  const name = m && m[2].trim().length > 0 ? m[2].trim() : null;
169
187
  phases.push({ number, name, status: (0, phase_status_cjs_1.parseRoadmapStatusCell)(row['Status']) });
@@ -172,7 +190,11 @@ function phasesFromProgressTable(table) {
172
190
  }
173
191
  // ─── phases: `## Phases` checkbox fallback ─────────────────────────────────
174
192
  // `- [x] **Phase 1: Foundation**` / `- [ ] **Phase 2.1: Hardening**`.
175
- const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
193
+ // The entry prefix (marker, box, bold token) is the phase-entry grammar's (#5764): capture
194
+ // group 1 is the box character, 2 the number, 3 the name.
195
+ const PHASE_BULLET_RE = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase[ \\t]+(\\d+(?:\\.\\d+)*)[ \\t]*:?[ \\t]*([^*\\n]*)${phase_entry_grammar_cjs_1.PHASE_ENTRY_BOLD_CLOSE_SRC}`, 'g', { state: 'captured', bold: 'required' });
196
+ // #5626: the same bullet under `milestone-prefixed`: the number capture takes one optional `-NN` segment.
197
+ const PHASE_BULLET_MILESTONE_RE = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`Phase[ \\t]+(\\d+(?:-\\d+)?(?:\\.\\d+)*)[ \\t]*:?[ \\t]*([^*\\n]*)${phase_entry_grammar_cjs_1.PHASE_ENTRY_BOLD_CLOSE_SRC}`, 'g', { state: 'captured', bold: 'required' });
176
198
  /**
177
199
  * STATE.md's current phase, via the same frontmatter-then-body chain
178
200
  * `cmdStateCompletePhase` uses (`stateFieldValue` owns the #1760 fallback
@@ -180,7 +202,7 @@ const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?
180
202
  * phase as unknown so every unchecked bullet is PENDING rather than
181
203
  * IN_PROGRESS by a guess.
182
204
  */
183
- function currentPhaseFromState(statePath) {
205
+ function currentPhaseFromState(statePath, convention) {
184
206
  const { platformReadSync, extractFrontmatter, stripFrontmatter, stateFieldValue, parsePhaseFromProse } = owners();
185
207
  try {
186
208
  const raw = platformReadSync(statePath);
@@ -191,7 +213,7 @@ function currentPhaseFromState(statePath) {
191
213
  const { value } = stateFieldValue(fm, body, 'current_phase', 'Current Phase');
192
214
  if (!value)
193
215
  return null;
194
- return parsePhaseFromProse(value).phase;
216
+ return parsePhaseFromProse(value, convention).phase;
195
217
  }
196
218
  catch {
197
219
  return null;
@@ -207,15 +229,16 @@ function currentPhaseFromState(statePath) {
207
229
  * `**Phase N` anchor inside `PHASE_BULLET_RE` is the second layer of that
208
230
  * defence — a plan bullet's text never starts with `**Phase`.
209
231
  */
210
- function phasesFromChecklist(roadmapContent, cwd, statePath) {
232
+ function phasesFromChecklist(roadmapContent, cwd, statePath, convention) {
211
233
  const { collectSection, isSentinelPhaseId } = owners();
212
234
  const section = collectSection(roadmapContent, (h) => /^phases$/i.test(h.text.trim()));
213
235
  if (!section)
214
236
  return [];
215
- const currentPhase = currentPhaseFromState(statePath);
237
+ const currentPhase = currentPhaseFromState(statePath, convention);
216
238
  const phases = [];
217
239
  let match;
218
- const pattern = new RegExp(PHASE_BULLET_RE.source, PHASE_BULLET_RE.flags);
240
+ const bulletRe = convention === MILESTONE_PREFIXED ? PHASE_BULLET_MILESTONE_RE : PHASE_BULLET_RE;
241
+ const pattern = new RegExp(bulletRe.source, bulletRe.flags);
219
242
  while ((match = pattern.exec(section.body)) !== null) {
220
243
  const checked = match[1] === 'x' || match[1] === 'X';
221
244
  const number = match[2];
@@ -230,17 +253,18 @@ function phasesFromChecklist(roadmapContent, cwd, statePath) {
230
253
  return phases;
231
254
  }
232
255
  function buildPhases(cwd, paths) {
233
- const { platformReadSync, locateProgressTable } = owners();
256
+ const { platformReadSync, locateRoadmapProgress } = owners();
234
257
  try {
235
258
  let content = platformReadSync(paths.roadmap);
236
259
  if (content === null)
237
260
  return [];
238
261
  if (content.charCodeAt(0) === 0xfeff)
239
262
  content = content.slice(1); // strip leading BOM
240
- const table = locateProgressTable(content);
263
+ const convention = phaseIdConventionOf(cwd);
264
+ const { table } = locateRoadmapProgress(content);
241
265
  if (table)
242
- return phasesFromProgressTable(table);
243
- return phasesFromChecklist(content, cwd, paths.state);
266
+ return phasesFromProgressTable(table, convention);
267
+ return phasesFromChecklist(content, cwd, paths.state, convention);
244
268
  }
245
269
  catch {
246
270
  return [];
@@ -8,19 +8,28 @@
8
8
  * from the prior hand-written .cjs; only types are added.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.KNOWN_STATUS_PATTERNS = exports.KNOWN_TEMPLATE_DEFAULTS = exports.STATUS_ANCHORED_PATTERNS = exports.STATUS_EXACT_TOKENS = void 0;
11
+ exports.KNOWN_STATUS_PATTERNS = exports.KNOWN_TEMPLATE_DEFAULTS = exports.STATUS_ANCHORED_PATTERNS = exports.STATUS_EXACT_TOKENS = exports.FIELD_LINE_BREAK_RE = void 0;
12
12
  exports.toFiniteNumber = toFiniteNumber;
13
13
  exports.isUnfilledFieldValue = isUnfilledFieldValue;
14
14
  exports.leadingCalendarDate = leadingCalendarDate;
15
15
  exports.isRealCalendarDate = isRealCalendarDate;
16
16
  exports.stateFieldContinuation = stateFieldContinuation;
17
+ exports.editedLineContinuation = editedLineContinuation;
18
+ exports.readableEditedBoldLabel = readableEditedBoldLabel;
19
+ exports.markdownFencedBlocks = markdownFencedBlocks;
20
+ exports.maskFencedLines = maskFencedLines;
21
+ exports.writeOutsideFences = writeOutsideFences;
22
+ exports.readableBoldLabels = readableBoldLabels;
17
23
  exports.fieldContinuationEnd = fieldContinuationEnd;
24
+ exports.insertMissingSessionFields = insertMissingSessionFields;
18
25
  exports.stateExtractField = stateExtractField;
19
26
  exports.stateFieldValue = stateFieldValue;
20
27
  exports.stateCurrentPositionSlice = stateCurrentPositionSlice;
21
28
  exports.stateReplaceField = stateReplaceField;
22
29
  exports.stateReplaceFieldWithFallback = stateReplaceFieldWithFallback;
23
30
  exports.stateReplaceFieldInSession = stateReplaceFieldInSession;
31
+ exports.isSessionHeading = isSessionHeading;
32
+ exports.isSessionContinuityHeading = isSessionContinuityHeading;
24
33
  exports.normalizeStateStatus = normalizeStateStatus;
25
34
  exports.computeProgressPercent = computeProgressPercent;
26
35
  exports.shouldPreserveExistingProgress = shouldPreserveExistingProgress;
@@ -32,6 +41,9 @@ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
32
41
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
33
42
  const pattern_cjs_1 = require("./pattern.cjs");
34
43
  const planning_document_cjs_1 = require("./planning-document.cjs");
44
+ Object.defineProperty(exports, "FIELD_LINE_BREAK_RE", { enumerable: true, get: function () { return planning_document_cjs_1.FIELD_LINE_BREAK_RE; } });
45
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
46
+ const state_md_schema_cjs_1 = require("./state-md-schema.cjs");
35
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
36
48
  const planningScopeMod = require("./planning-scope.cjs");
37
49
  const { SCOPE } = planningScopeMod;
@@ -324,8 +336,19 @@ function isRealCalendarDate(year, month, day) {
324
336
  * produce false S009 fires on well-formed content: an indented code block, an
325
337
  * HTML block, and a setext underline (`===`, which the `[-*_]{3,}` rule does not
326
338
  * cover — it only knows `-`, `*` and `_`).
339
+ *
340
+ * #4998: a fence, an ATX heading (an empty `##` included), a blockquote or a
341
+ * thematic break opens with up to three leading spaces, as Markdown allows —
342
+ * such a line directly below a field was read as its continuation, so
343
+ * record-session skipped a field that does not wrap. Four spaces or a tab is
344
+ * the indented-code arm. Other indented structure (an indented bullet) stays a
345
+ * continuation on purpose: it fails toward skipping the field, never toward
346
+ * splitting a note from the field it annotates.
327
347
  */
328
- const MD_STRUCTURE_LINE_RE = /^(?:#{1,6}\s|\||>|```|~~~|[-*_]{3,}\s*$|=+\s*$|[-*+]\s|\d+[.)]\s|\[[^\]]+\]:|<|(?: {4}|\t))/;
348
+ // The backtick fence is spelled `\x60{3}`: a literal run of backticks in a
349
+ // regex literal opens a template string for the source scanners in
350
+ // scripts/ (lint-state-field-drift), which then misread the rest of the file.
351
+ const MD_STRUCTURE_LINE_RE = /^(?:\||>| {0,3}(?:#{1,6}(?:\s|$)|\x60{3}|~~~|>|[-*_]{3,}\s*$)|[-*_]{3,}\s*$|=+\s*$|[-*+]\s|\d+[.)]\s|\[[^\]]+\]:|<|(?: {4}|\t))/;
329
352
  /**
330
353
  * A setext heading's underline — `===` or `---` on its own line. The line ABOVE
331
354
  * one of these is a heading TITLE, which is indistinguishable from prose on its
@@ -334,7 +357,36 @@ const MD_STRUCTURE_LINE_RE = /^(?:#{1,6}\s|\||>|```|~~~|[-*_]{3,}\s*$|=+\s*$|[-*
334
357
  * continuation text (#3696 review round 2).
335
358
  */
336
359
  const SETEXT_UNDERLINE_RE = /^(?:=+|-+)\s*$/;
337
- const STATE_SIBLING_FIELD_LINE_RE = /^\*{0,2}[A-Za-z][A-Za-z0-9 _-]*\*{0,2}:{1,2}\*{0,2}(?:\s|$)/;
360
+ // #4998: a sibling field may be indented as the field above it may be
361
+ // (`stateExtractField`'s bold rung tolerates leading whitespace); up to three
362
+ // spaces, as for MD_STRUCTURE_LINE_RE — four is indented code either way. The
363
+ // label admits `.`, `(`, `)` and `/` as well (`Phase 3.1 status:`,
364
+ // `Next (planned):`): a sibling read as a continuation made record-session
365
+ // refuse a field that does not wrap.
366
+ const STATE_SIBLING_FIELD_LINE_RE = /^ {0,3}\*{0,2}[A-Za-z][A-Za-z0-9 _.()/-]*\*{0,2}:{1,2}\*{0,2}(?:\s|$)/;
367
+ /**
368
+ * #4998 review (trek-e 5638530738, Minor 2): the labels STATE.md defines —
369
+ * every body source the frontmatter sync reads, plus the session and position
370
+ * fields the template writes. Lower-cased.
371
+ */
372
+ const STATE_FIELD_LABELS = new Set([
373
+ ...Object.values(state_md_schema_cjs_1.STATE_FIELD_SCHEMA).flatMap((row) => row.bodySource ?? []),
374
+ 'Last session', 'Last Date', 'Resume file', 'Phase', 'Plan', 'Progress', 'Current focus',
375
+ ].map((label) => label.toLowerCase()));
376
+ /**
377
+ * A plain `words: text` line below a field is a sibling field or a wrapped
378
+ * tail (`records and verified: all good`); its shape cannot tell which. A
379
+ * line is a sibling beyond doubt only when its label is bold or one STATE.md
380
+ * defines. Each caller resolves the ambiguous rest toward its own safe side:
381
+ * the record-session wrap check (`editedLineContinuation`) reads it as a
382
+ * continuation and leaves the field whole; the validator and the writer that
383
+ * removes a continuation (`fieldContinuationEnd`) end the value there, so a
384
+ * sibling is never deleted.
385
+ */
386
+ function isUnambiguousSiblingLine(line) {
387
+ const m = /^ {0,3}(\*\*)?([A-Za-z][A-Za-z0-9 _.()/-]*?)(?:\*\*)?:{1,2}(?:\*\*)?(?:\s|$)/.exec(line);
388
+ return m !== null && (m[1] !== undefined || STATE_FIELD_LABELS.has(m[2].trim().toLowerCase()));
389
+ }
338
390
  /**
339
391
  * Return the prose that FOLLOWS a single-line field but plainly belongs to it —
340
392
  * i.e. the remainder `stateExtractField` silently drops when a writer emits a
@@ -360,6 +412,8 @@ const STATE_SIBLING_FIELD_LINE_RE = /^\*{0,2}[A-Za-z][A-Za-z0-9 _-]*\*{0,2}:{1,2
360
412
  * cannot wrap), or is followed by end-of-file, a blank line, Markdown
361
413
  * structure, or a sibling field.
362
414
  */
415
+ /** The line terminators the STATE.md readers (JS `m`-flag `^`/`$`) treat as a line boundary. */
416
+ const LINE_SEPARATOR_RE = /[\n\u2028\u2029]/;
363
417
  function stateFieldContinuation(content, fieldName) {
364
418
  const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
365
419
  // Same two single-line grammars stateExtractField uses, in the same order, so
@@ -371,15 +425,197 @@ function stateFieldContinuation(content, fieldName) {
371
425
  if (!match)
372
426
  return null;
373
427
  // `(.+)` stops at the line terminator, so the field's line ends where the
374
- // match does. JS `.` excludes \r as well as \n, so on a CRLF document the \r
375
- // sits just AFTER the match rather than inside it — hence the strip below
376
- // before testing for the newline.
377
- const afterValue = match.index + match[0].length;
428
+ // match does.
429
+ return continuationAfter(content, match.index + match[0].length);
430
+ }
431
+ /**
432
+ * #4998: where the line a single-line field write changed starts — found from
433
+ * the edit itself, the first byte where `before` and `after` differ, so it is
434
+ * always the occurrence the writer touched, whichever label grammar or
435
+ * occurrence it matched. The bytes before that point are common to both, so
436
+ * the offset holds in either string. Callers handle `before === after`.
437
+ */
438
+ function editedLineStart(before, after) {
439
+ const limit = Math.min(before.length, after.length);
440
+ let i = 0;
441
+ while (i < limit && before.charCodeAt(i) === after.charCodeAt(i))
442
+ i++;
443
+ // `lastIndexOf` clamps a negative start to 0, so i = 0 would find a
444
+ // newline AT byte 0 and misplace the edited line onto the next one.
445
+ return i === 0 ? 0 : before.lastIndexOf('\n', i - 1) + 1;
446
+ }
447
+ /**
448
+ * #4998: the continuation (as `stateFieldContinuation` defines it) of the line
449
+ * a single-line field write CHANGED (`editedLineStart`). `null` when nothing
450
+ * changed — an identical-value write leaves every byte, the continuation
451
+ * included, where it was — when the edited line is the last line, or when it
452
+ * is a pipe-table row (a table cell cannot wrap).
453
+ */
454
+ function editedLineContinuation(before, after) {
455
+ if (before === after)
456
+ return null;
457
+ const lineStart = editedLineStart(before, after);
458
+ const lineEnd = before.indexOf('\n', lineStart);
459
+ // The edited line only: trimming the rest of the document would let a blank
460
+ // edited line borrow a later table row's pipe.
461
+ const line = before.slice(lineStart, lineEnd === -1 ? before.length : lineEnd).replace(/\r$/, '');
462
+ if (line.trimStart().startsWith('|'))
463
+ return null;
464
+ // A line break INSIDE the edited line (a bare CR, U+2028, U+2029) already
465
+ // wraps the value for the `m`-flag readers, which stop there: the text after
466
+ // it is a continuation, exactly as a following LF line is (Codex review round 3).
467
+ const inner = line.search(planning_document_cjs_1.FIELD_LINE_BREAK_RE);
468
+ if (inner !== -1)
469
+ return line.slice(inner + 1).split(planning_document_cjs_1.FIELD_LINE_BREAK_RE).map((part) => part.trim()).join(' ').trim();
470
+ return lineEnd === -1 ? null : continuationAfter(before, lineEnd, false);
471
+ }
472
+ /**
473
+ * #4998: `after` with the line a single-line write changed respelled from
474
+ * `**Label**:` to `**Label:**` — the spelling `stateExtractField` reads — so a
475
+ * value `state record-session` just wrote reads back into the frontmatter
476
+ * sync. `stateReplaceField` keeps the author's spelling by contract (#5007),
477
+ * and that contract stands for every other caller: this is applied only by
478
+ * the one writer whose lines the sync must read back, and only to the one
479
+ * line it wrote. Widening the reader to `**Label**:` instead was tried and
480
+ * reverted — it made the sync read fenced examples. Any other line is
481
+ * untouched.
482
+ */
483
+ function readableEditedBoldLabel(before, after) {
484
+ if (before === after)
485
+ return after;
486
+ const lineStart = editedLineStart(before, after);
487
+ const lineEnd = after.indexOf('\n', lineStart);
488
+ const end = lineEnd === -1 ? after.length : lineEnd;
489
+ const line = after.slice(lineStart, end).replace(/^([ \t]*)\*\*([^*\r\n]+)\*\*:/, '$1**$2:**');
490
+ return after.slice(0, lineStart) + line + after.slice(end);
491
+ }
492
+ /**
493
+ * Index of the closing `---` of a closed frontmatter block at the top of a
494
+ * whole document, or -1 (always -1 outside `document` scope).
495
+ */
496
+ function lastFrontmatterLine(lines, scope) {
497
+ if (!scope.document)
498
+ return -1;
499
+ const text = lines.join('\n');
500
+ const fm = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(text);
501
+ return fm && fm.closed ? text.slice(0, fm.closingStart).split('\n').length - 1 : -1;
502
+ }
503
+ /**
504
+ * #4998: `scanFencedBlocks` over the Markdown of `lines` only. In a whole
505
+ * document the frontmatter block is YAML, where a ``` line is data (a block
506
+ * scalar), not a fence; its lines are blanked for the scan, so they never open
507
+ * a fence that shifts every boundary after them (Codex review). Line indices
508
+ * are those of `lines`.
509
+ */
510
+ function markdownFencedBlocks(lines, scope = {}) {
511
+ const last = lastFrontmatterLine(lines, scope);
512
+ return last === -1 ? (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines) : (0, markdown_sectionizer_cjs_1.scanFencedBlocks)(lines.map((line, i) => (i <= last ? '' : line)));
513
+ }
514
+ /**
515
+ * Per line of `lines`: whether a field reader or writer must not see it — a
516
+ * fenced block's line (delimiters included) or, in a whole document, a line
517
+ * of frontmatter between its `---` delimiters (YAML data that a Markdown
518
+ * scanner would otherwise read as a heading, a fence or a field).
519
+ */
520
+ function maskedLineFlags(lines, scope) {
521
+ const masked = lines.map(() => false);
522
+ for (const block of markdownFencedBlocks(lines, scope)) {
523
+ const end = block.closeLineIdx === -1 ? lines.length - 1 : block.closeLineIdx;
524
+ for (let i = block.openLineIdx; i <= end; i++)
525
+ masked[i] = true;
526
+ }
527
+ const last = lastFrontmatterLine(lines, scope);
528
+ for (let i = 1; i < last; i++)
529
+ masked[i] = true;
530
+ return masked;
531
+ }
532
+ /**
533
+ * #4998: `content` with every fenced line blanked to spaces — same length,
534
+ * same line terminators — for reading a field the way `writeOutsideFences`
535
+ * writes it: an example line in a fence is not a field. With
536
+ * `{ document: true }` the frontmatter's content lines are blanked too (its
537
+ * `---` delimiters kept), so scanners that do not know frontmatter — the
538
+ * heading tokenizer behind `collectSection`, `parsePlanningDoc` — see only
539
+ * the Markdown body (Codex review rounds 2 and 3).
540
+ */
541
+ function maskFencedLines(content, scope = {}) {
542
+ const lines = content.split('\n');
543
+ const masked = maskedLineFlags(lines, scope);
544
+ return lines.map((line, i) => (masked[i] ? line.replace(/[^\r]/g, ' ') : line)).join('\n');
545
+ }
546
+ /**
547
+ * #4998: apply a single-line field write (`stateReplaceField` and kin) to
548
+ * `content` as if its fenced blocks were absent, so a fenced example line is
549
+ * never the occurrence a write replaces — the same rule
550
+ * `insertMissingSessionFields` follows, through the same scanner. `write` runs
551
+ * against a masked copy (`maskFencedLines`); its one edit is then spliced into
552
+ * the real text at the same offsets. Returns what `write` returns for "no
553
+ * match" (`null`, or the input unchanged as `content`), and `null` if the
554
+ * edit cannot be carried back exactly (it touched a fenced line or changed
555
+ * the line structure) — the stateReplaceField no-match contract, never a
556
+ * partial write.
557
+ */
558
+ function writeOutsideFences(content, write, scope = {}) {
559
+ // Outside a whole document a leading `---` is a horizontal rule
560
+ // (`FenceScope`), but the field writers parse what they are given as a
561
+ // document, where it opens frontmatter and hides the fields below it: a
562
+ // section starting with a rule got a duplicate field inserted (Codex
563
+ // review). They are handed the text after a line break, which is removed again.
564
+ const run = (text) => {
565
+ if (scope.document)
566
+ return write(text);
567
+ const out = write(`\n${text}`);
568
+ return out !== null && out.startsWith('\n') ? out.slice(1) : null;
569
+ };
570
+ const masked = maskFencedLines(content, scope);
571
+ if (masked === content)
572
+ return run(content);
573
+ const result = run(masked);
574
+ if (result === null)
575
+ return null;
576
+ if (result === masked)
577
+ return content;
578
+ const limit = Math.min(masked.length, result.length);
579
+ let p = 0;
580
+ while (p < limit && masked.charCodeAt(p) === result.charCodeAt(p))
581
+ p++;
582
+ let s = 0;
583
+ while (s < limit - p && masked.charCodeAt(masked.length - 1 - s) === result.charCodeAt(result.length - 1 - s))
584
+ s++;
585
+ // The span the write replaced must lie on unfenced lines — an edit that
586
+ // touched a fenced line cannot be carried back, even when the masked and
587
+ // real bytes there happen to agree (a whitespace-only fenced line).
588
+ const lineOf = (offset) => content.slice(0, offset).split('\n').length - 1;
589
+ const fenced = maskedLineFlags(content.split('\n'), scope);
590
+ for (let line = lineOf(p); line <= lineOf(content.length - s); line++)
591
+ if (fenced[line])
592
+ return null;
593
+ const spliced = content.slice(0, p) + result.slice(p, result.length - s) + content.slice(content.length - s);
594
+ return maskFencedLines(spliced, scope) === result ? spliced : null;
595
+ }
596
+ /**
597
+ * #4998: `content` with every `**Label**:` line respelled `**Label:**`, for
598
+ * READING a field the way the writer finds it: `stateReplaceField` matches
599
+ * both spellings, `stateExtractField` only the second, so a `**Resume
600
+ * file**: plan.md` read as absent while the writer could still replace it.
601
+ * Never written back.
602
+ */
603
+ function readableBoldLabels(content) {
604
+ // allow-adhoc-markdown: a read-only copy for stateExtractField, never written back — not a document mutation
605
+ return content.replace(/^([ \t]*)\*\*([^*\r\n]+)\*\*:/gm, '$1**$2:**');
606
+ }
607
+ /** The continuation lines following the line that ends at `afterValue`. */
608
+ function continuationAfter(content, afterValue, ambiguousLabelEnds = true) {
609
+ // JS `.` excludes \r as well as \n, so on a CRLF document the \r sits just
610
+ // AFTER a `(.+)` match rather than inside it — hence the strip below before
611
+ // testing for the newline.
378
612
  const rest = content.slice(afterValue).replace(/^\r/, '');
379
- if (!rest.startsWith('\n'))
613
+ // #5460: U+2028 / U+2029 are line starts to the `m`-flag readers (and stop `(.+)`), so they end
614
+ // the value's line exactly as `\n` does.
615
+ if (!/^[\n\u2028\u2029]/.test(rest))
380
616
  return null; // end of file: nothing follows
381
- const lines = rest.slice(1).split('\n').map((line) => line.replace(/\r$/, ''));
382
- const continuation = lines.slice(0, continuationLineCount(lines)).map((line) => line.trim());
617
+ const lines = rest.slice(1).split(LINE_SEPARATOR_RE).map((line) => line.replace(/\r$/, ''));
618
+ const continuation = lines.slice(0, continuationLineCount(lines, ambiguousLabelEnds)).map((line) => line.trim());
383
619
  return continuation.length ? continuation.join(' ') : null;
384
620
  }
385
621
  /**
@@ -387,7 +623,7 @@ function stateFieldContinuation(content, fieldName) {
387
623
  * `stateFieldContinuation` reports on, shared so a writer that replaces the value removes exactly
388
624
  * the lines a validator would call its continuation (#4945). `lines` are CR-free.
389
625
  */
390
- function continuationLineCount(lines) {
626
+ function continuationLineCount(lines, ambiguousLabelEnds = true) {
391
627
  let count = 0;
392
628
  for (let i = 0; i < lines.length; i++) {
393
629
  const line = lines[i];
@@ -395,7 +631,9 @@ function continuationLineCount(lines) {
395
631
  break;
396
632
  if (MD_STRUCTURE_LINE_RE.test(line))
397
633
  break;
398
- if (STATE_SIBLING_FIELD_LINE_RE.test(line))
634
+ // A label line the shape cannot place is a sibling only where the caller
635
+ // fails safe that way (`isUnambiguousSiblingLine`).
636
+ if (STATE_SIBLING_FIELD_LINE_RE.test(line) && (ambiguousLabelEnds || isUnambiguousSiblingLine(line)))
399
637
  break;
400
638
  // Look ahead one line: a setext underline below makes THIS line a heading
401
639
  // title, so stop before consuming it rather than after.
@@ -414,10 +652,14 @@ function continuationLineCount(lines) {
414
652
  */
415
653
  function fieldContinuationEnd(content, valueEnd) {
416
654
  const tail = content.slice(valueEnd);
417
- const first = /^\r?\n/.exec(tail);
655
+ // #5460: the readers' `m`-flag `^` also starts a line after U+2028 / U+2029, so a continuation
656
+ // scan that split on `\n` alone swallowed `...tail<U+2028>Stopped at: x` as ONE continuation line
657
+ // and deleted the sibling field with it. Each separator is one UTF-16 unit, like `\n`, so the
658
+ // removal arithmetic below is unchanged.
659
+ const first = /^(?:\r?\n|[\u2028\u2029])/.exec(tail);
418
660
  if (!first)
419
661
  return valueEnd;
420
- const rawLines = tail.slice(first[0].length).split('\n');
662
+ const rawLines = tail.slice(first[0].length).split(LINE_SEPARATOR_RE);
421
663
  const count = continuationLineCount(rawLines.map((line) => line.replace(/\r$/, '')));
422
664
  if (count === 0)
423
665
  return valueEnd;
@@ -429,6 +671,63 @@ function fieldContinuationEnd(content, valueEnd) {
429
671
  removed -= 1;
430
672
  return valueEnd + removed;
431
673
  }
674
+ /**
675
+ * #4998: insert each needed field of `fields` — given in template order — into
676
+ * `body`, beside the sibling it follows in that order (below the sibling's own
677
+ * wrapped lines, never between them), else before the sibling it precedes,
678
+ * else at the top in bold. Plain beside a plain sibling, else bold, in the
679
+ * spellings `stateExtractField` reads back. Every existing line is kept, in
680
+ * order, and lines inside a fenced block are examples: never an anchor, never
681
+ * an insertion point. An inserted line ends as the body's lines do (CRLF when
682
+ * the body uses CRLF).
683
+ */
684
+ function insertMissingSessionFields(body, fields) {
685
+ const lines = body.split('\n');
686
+ const fenced = maskedLineFlags(lines, {});
687
+ const cr = body.includes('\r\n') ? '\r' : '';
688
+ // The spellings stateReplaceField writes: `**Label:**`, `**Label**:`, `Label:`.
689
+ const lineOf = (label) => {
690
+ const re = new RegExp(`^[ \\t]*(?:\\*\\*)?${(0, pattern_cjs_1.escapeRegex)(label)}(?::\\*\\*|\\*\\*:|:)(?:\\s|$)`, 'i');
691
+ return lines.findIndex((l, i) => !fenced[i] && re.test(l));
692
+ };
693
+ // Where field k goes: below the nearest earlier sibling present (past its
694
+ // wrapped lines, stopping before a fence the continuation scan does not
695
+ // recognize), else above the nearest later one, else the top.
696
+ const placement = (k) => {
697
+ for (let j = k - 1; j >= 0; j--) {
698
+ const anchor = lineOf(fields[j].label);
699
+ if (anchor === -1)
700
+ continue;
701
+ // An ambiguous `words: text` line is taken as the anchor's tail, as the
702
+ // wrap check takes it: inserting inside a wrapped value splits it
703
+ // (Codex review); inserting below a sibling only moves the new line.
704
+ const tailEnd = anchor + 1 + continuationLineCount(lines.slice(anchor + 1).map((l) => l.replace(/\r$/, '')), false);
705
+ let at = anchor + 1;
706
+ while (at < tailEnd && !fenced[at])
707
+ at++;
708
+ return { at, anchor };
709
+ }
710
+ for (let j = k + 1; j < fields.length; j++) {
711
+ const anchor = lineOf(fields[j].label);
712
+ if (anchor !== -1)
713
+ return { at: anchor, anchor };
714
+ }
715
+ return { at: 0, anchor: -1 };
716
+ };
717
+ fields.forEach(({ label, needed, value }, k) => {
718
+ if (!needed)
719
+ return;
720
+ const { at, anchor } = placement(k);
721
+ const bold = anchor === -1 || lines[anchor].trimStart().startsWith('**');
722
+ const line = (bold ? `**${label}:** ` : `${label}: `) + value;
723
+ // Appended after an unterminated last line: that line gains the CR.
724
+ if (at === lines.length)
725
+ lines[at - 1] += cr;
726
+ lines.splice(at, 0, at === lines.length ? line : line + cr);
727
+ fenced.splice(at, 0, false);
728
+ });
729
+ return lines.join('\n');
730
+ }
432
731
  function stateExtractField(content, fieldName) {
433
732
  const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
434
733
  // Bold line-start format: **FieldName:** value. Leading same-line whitespace
@@ -636,14 +935,13 @@ function stateReplaceField(content, fieldName, newValue) {
636
935
  // is the same, already-precedented widening as the "Depends on"
637
936
  // migration (src/phase.cts) — intentional, not scope creep.
638
937
  //
639
- // A parse failure, an absent/case-mismatched label, or a value containing
640
- // a line break (\r/\n — would forge sibling structure on splice, the same
641
- // hazard `setFieldValue` refuses unconditionally at #4917/ADR-4910
642
- // Decision 2 & 4; this local splice has no seam call to inherit that
643
- // refusal from, so it is re-checked here) falls through to the
938
+ // A parse failure or an absent/case-mismatched label falls through to the
644
939
  // plain/pipe-table branches below unchanged — the same silent
645
940
  // per-occurrence no-op-on-no-match contract the removed regex had for any
646
- // input it didn't match.
941
+ // input it didn't match. A value containing a line break
942
+ // (`FIELD_LINE_BREAK_RE` — would forge sibling structure on splice, the
943
+ // same hazard `setFieldValue` refuses at #4917/ADR-4910 Decision 2 & 4)
944
+ // is refused by every rung (#4998): the function returns null.
647
945
  const parsed = (0, planning_document_cjs_1.parsePlanningDoc)(content, 'STATE.md');
648
946
  if (parsed.ok) {
649
947
  let boldField = null;
@@ -667,11 +965,16 @@ function stateReplaceField(content, fieldName, newValue) {
667
965
  const newValueStr = `${newValue}`;
668
966
  const needsSeparator = newValueStr.length > 0 && !/[ \t]$/.test(spacingText);
669
967
  const writeValue = `${needsSeparator ? ' ' : ''}${newValueStr}`;
670
- if (!/[\r\n]/.test(writeValue)) {
968
+ if (!planning_document_cjs_1.FIELD_LINE_BREAK_RE.test(writeValue)) {
671
969
  return (content.slice(0, boldField.valueSpan.start) + writeValue + content.slice(boldField.trailingSpan.end));
672
970
  }
673
971
  }
674
972
  }
973
+ // #4998: the plain and table rungs refuse a line break too. Spliced in, it
974
+ // forges sibling lines exactly as the bold rung's refusal above prevents;
975
+ // falling through to them only moved the forgery one rung down.
976
+ if (planning_document_cjs_1.FIELD_LINE_BREAK_RE.test(`${newValue}`))
977
+ return null;
675
978
  // Plain line-start format: FieldName: value (same same-line confinement as above)
676
979
  const plainPattern = new RegExp(`(^${escaped}:[ \\t]*)(.*)`, 'im');
677
980
  if (plainPattern.test(content)) {
@@ -714,14 +1017,28 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
714
1017
  * Replace-only (no insertion): returns `content` unchanged when no session
715
1018
  * section exists or the field is absent from it, so a STATE.md layout without
716
1019
  * the line keeps its shape and the post-sync preservation pass decides the
717
- * frontmatter value (see #3374).
1020
+ * frontmatter value (see #3374). Lines inside a fenced block are examples and
1021
+ * are never the occurrence replaced (#4998, `writeOutsideFences`).
718
1022
  */
719
1023
  function stateReplaceFieldInSession(content, primary, fallback, value) {
720
- const isSession = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'session';
721
- const isSessionContinuity = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'session continuity';
722
- const hasCanonicalSession = (0, markdown_sectionizer_cjs_1.collectSection)(content, isSession, { levelBounded: true }) !== null;
723
- const target = hasCanonicalSession ? isSession : isSessionContinuity;
724
- return (0, markdown_sectionizer_cjs_1.withSection)(content, target, (sectionBody) => stateReplaceFieldWithFallback(sectionBody, primary, fallback, value));
1024
+ const hasCanonicalSession = (0, markdown_sectionizer_cjs_1.collectSection)(content, isSessionHeading, { levelBounded: true }) !== null;
1025
+ const target = hasCanonicalSession ? isSessionHeading : isSessionContinuityHeading;
1026
+ // #4998: a fenced example line is never the field written, as the session
1027
+ // readers (state.cts sessionFieldScope) never read one.
1028
+ return (0, markdown_sectionizer_cjs_1.withSection)(content, target, (sectionBody) => writeOutsideFences(sectionBody, (visible) => stateReplaceFieldWithFallback(visible, primary, fallback, value)) ?? sectionBody);
1029
+ }
1030
+ /**
1031
+ * The two session headings, as every session reader and writer recognises
1032
+ * them: one grammar (the `collectSection` tokenizer), so a heading the readers
1033
+ * take is never one a writer misses. #4998 review: `state record-session`
1034
+ * detected the section with its own regex, which rejected `## Session`, and
1035
+ * appended a duplicate section.
1036
+ */
1037
+ function isSessionHeading(h) {
1038
+ return h.level === 2 && h.text.trim().toLowerCase() === 'session';
1039
+ }
1040
+ function isSessionContinuityHeading(h) {
1041
+ return h.level === 2 && h.text.trim().toLowerCase() === 'session continuity';
725
1042
  }
726
1043
  /**
727
1044
  * #4186: the DECLARED raw-status vocabulary `normalizeStateStatus` recognizes.