@opengsd/gsd-core 1.14.0 → 1.16.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 (551) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -15,6 +15,10 @@
15
15
  * anchors/alias refusal, the #3257 comment channel, the #1882 truncation
16
16
  * probe, null-byte preservation and object-list flattening for the existing
17
17
  * string-shaped value contract — is layered on top, in one place, below.
18
+ *
19
+ * #5105: the WRITER — `spliceFrontmatter` and its layout, comment-classification, parse-budget
20
+ * and read-back machinery — lives in `frontmatter-splice.cts`, split out by module ownership.
21
+ * This module re-exports its public names unchanged and requires it lazily (`spliceModule`).
18
22
  */
19
23
  var __importDefault = (this && this.__importDefault) || function (mod) {
20
24
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -27,6 +31,8 @@ const { output, error } = ioMod;
27
31
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
28
32
  const validate_cjs_1 = require("./validate.cjs");
29
33
  const text_lines_cjs_1 = require("./text-lines.cjs");
34
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
35
+ const pattern_cjs_1 = require("./pattern.cjs");
30
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
37
  const unusableInputMod = require("./unusable-input.cjs");
32
38
  const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
@@ -40,7 +46,7 @@ const js_yaml_cjs_1 = require("./vendor/js-yaml.cjs");
40
46
  * which is the documented behavior `tests/fixtures/adversarial/frontmatter/
41
47
  * duplicate-keys.md` pins.
42
48
  */
43
- const YAML_LOAD_OPTS = { schema: js_yaml_cjs_1.FAILSAFE_SCHEMA, json: true };
49
+ const YAML_LOAD_OPTS = Object.freeze({ schema: js_yaml_cjs_1.FAILSAFE_SCHEMA, json: true });
44
50
  /**
45
51
  * How many parsed keys an unterminated region must yield before it is reported as a
46
52
  * truncated frontmatter rather than left alone as ordinary Markdown. See the rationale on
@@ -88,8 +94,36 @@ function isFrontmatterShaped(region) {
88
94
  * are attached to the top-level key that follows them; comments after the last
89
95
  * key go to `trailing`. Only set when a comment is actually seen, so comment-less
90
96
  * frontmatter parses byte-identically to before.
97
+ *
98
+ * `leading` (and `inline`) are keyed by `commentPathKey` of the key path the comment belongs
99
+ * to — `["status"]` for a top-level key, `["progress","total"]` for a nested one — so a
100
+ * top-level key literally named `a.b` never shares an entry with sub-key `b` of map `a`.
101
+ * `inline` holds a comment that sits on its key's own line (`a: 1 # note`), verbatim from
102
+ * the whitespace before its `#`; only `spliceFrontmatter` sets it, for the one key it
103
+ * regenerates (see `segmentComments`).
91
104
  */
92
105
  const FULL_LINE_COMMENTS = Symbol('fullLineComments');
106
+ /**
107
+ * The comment-channel entry name of a key path: the JSON array of its key segments, which no
108
+ * two different paths share whatever characters a key holds (found while implementing #5105 —
109
+ * a dot-joined path read a top-level key named `a.b` as sub-key `b` of map `a`).
110
+ */
111
+ function commentPathKey(segments) {
112
+ return JSON.stringify(segments);
113
+ }
114
+ /**
115
+ * The mapping key a line opens and the line's indentation, or null for a line that opens no
116
+ * key (a list item, a flow or scalar continuation, a comment). The key is read exactly as
117
+ * `segmentKeyOf` reads a top-level one — a quoted key unescaped, a plain key ending at the
118
+ * first `:` followed by whitespace — at any indentation. At indent > 0 the no-space bare-key
119
+ * fallback is disabled (see `segmentKeyOf`'s docblock): a nested continuation line like
120
+ * ` https://x` is not misread as opening key `https`.
121
+ */
122
+ function channelKeyLine(line) {
123
+ const indent = /^\s*/.exec(line)?.[0].length ?? 0;
124
+ const k = segmentKeyOf(line.slice(indent), indent);
125
+ return k ? { indent, key: k.key } : null;
126
+ }
93
127
  /**
94
128
  * ADR-3473 §8.1 §0.3 (#3881, consequence 2): a Symbol-keyed marker carried on the `{}`
95
129
  * `extractFrontmatter` returns when the region failed to parse (malformed YAML, or a refused
@@ -273,7 +307,7 @@ function restoreNullBytesDeep(value) {
273
307
  * produced four different strings for those four spellings (ADR-3473 40-design.md §0.1). No
274
308
  * adapter over a tree can recover a distinction the tree does not carry, so this renders a single
275
309
  * canonical string per object-list item instead, keeping the existing value SHAPE (an array of
276
- * strings) that `sliceTopLevelFrontmatterSegments`, the `[object Object]` guard and
310
+ * strings) that `sliceFrontmatterLayout`, the `[object Object]` guard and
277
311
  * `noOpObjectListSetError` all depend on. Choosing structured (non-string) values is fork (b) —
278
312
  * out of scope for this phase.
279
313
  */
@@ -343,25 +377,22 @@ function extractCommentChannel(yaml, orderedKeys) {
343
377
  const lines = (0, text_lines_cjs_1.splitLines)(yaml);
344
378
  // #3742: pending full-line comments carry their indentation so an INDENTED
345
379
  // comment (` # note` above a nested key) can attach to the nested key that
346
- // follows it — recorded under a dotted path key (`progress.total_phases`)
347
- // that reconstructFrontmatter re-emits at the same nesting depth. Column-0
348
- // comments keep the exact pre-#3742 behavior (top-level key attachment).
380
+ // follows it — recorded under its key path (`["progress","total_phases"]`,
381
+ // see `commentPathKey`) that reconstructFrontmatter re-emits at the same
382
+ // nesting depth. Column-0 comments keep the exact pre-#3742 behavior
383
+ // (top-level key attachment).
349
384
  let pending = [];
350
385
  let channel;
351
386
  let keyIdx = 0;
352
- // Stack of enclosing mapping keys with their indentation, for dotted-path
387
+ // Stack of enclosing mapping keys with their indentation, for key-path
353
388
  // construction on nested key lines. Only indented keys push here.
354
389
  const pathStack = [];
355
- const attach = (pathKey, comments) => {
390
+ const attach = (segments, comments) => {
356
391
  if (!channel)
357
392
  channel = { leading: Object.create(null), trailing: [] };
358
- // Null-prototype `leading` (post-#3881-review, finding 3): the path key is
359
- // derived from arbitrary user-authored YAML keys — `constructor`,
360
- // `__proto__`, `toString`, `valueOf`, `hasOwnProperty` all round-trip
361
- // through here. On an ordinary `{}` those resolve to inherited
362
- // Object.prototype members; the null prototype makes every lookup an
363
- // own-property-or-undefined read.
364
- channel.leading[pathKey] = comments.map((c) => c.line);
393
+ // Null-prototype `leading` (post-#3881-review, finding 3): every lookup is an
394
+ // own-property-or-undefined read, whatever key text a user-authored path holds.
395
+ channel.leading[commentPathKey(segments)] = comments.map((c) => c.line);
365
396
  };
366
397
  for (const line of lines) {
367
398
  if (line.trim() === '')
@@ -371,23 +402,18 @@ function extractCommentChannel(yaml, orderedKeys) {
371
402
  pending.push({ indent: commentMatch[1].length, line });
372
403
  continue;
373
404
  }
374
- // A list item (`- foo: bar`) is not a mapping key: its `- ` prefix would
375
- // otherwise register as a key named `- foo` and corrupt the path stack
376
- // (#3742 review). List items fall through to the pending-drop below.
377
- const isListItem = /^\s*-\s/.test(line);
378
- const keyLineMatch = isListItem
379
- ? null
380
- : /^(\s*)(?:"([^"]+)"|'([^']+)'|([^:\s][^:]*)):(?:\s|$)/.exec(line);
381
- if (keyLineMatch) {
382
- const indent = keyLineMatch[1].length;
383
- const key = keyLineMatch[2] ?? keyLineMatch[3] ?? keyLineMatch[4];
405
+ // A list item (`- foo: bar`) is not a mapping key (#3742 review): `channelKeyLine`
406
+ // reads no key from it, so it falls through to the pending-drop below.
407
+ const keyLine = channelKeyLine(line);
408
+ if (keyLine) {
409
+ const { indent, key } = keyLine;
384
410
  if (indent === 0) {
385
411
  // Top-level: keep the pre-#3742 orderedKeys walk — the comment
386
412
  // attaches only to the next EXPECTED top-level key.
387
413
  if (keyIdx < orderedKeys.length && key === orderedKeys[keyIdx]) {
388
414
  const col0 = pending.filter((c) => c.indent === 0);
389
415
  if (col0.length)
390
- attach(key, col0);
416
+ attach([key], col0);
391
417
  keyIdx++;
392
418
  // A top-level mapping key opens a nesting context for the indented
393
419
  // keys that follow it (#3742 dotted-path attachment).
@@ -399,14 +425,14 @@ function extractCommentChannel(yaml, orderedKeys) {
399
425
  }
400
426
  else {
401
427
  // Nested key line: a pending comment at the SAME indentation attaches
402
- // to this key under its dotted path. Deeper/misaligned pending
428
+ // to this key under its key path. Deeper/misaligned pending
403
429
  // comments were not leading this key — drop them, matching the
404
430
  // top-level rule's "attach only when a key follows" discipline.
405
431
  while (pathStack.length > 0 && pathStack[pathStack.length - 1].indent >= indent)
406
432
  pathStack.pop();
407
433
  const sameIndent = pending.filter((c) => c.indent === indent);
408
434
  if (sameIndent.length && key.length > 0) {
409
- attach([...pathStack.map((e) => e.key), key].join('.'), sameIndent);
435
+ attach([...pathStack.map((e) => e.key), key], sameIndent);
410
436
  }
411
437
  pathStack.push({ indent, key });
412
438
  pending = [];
@@ -675,28 +701,79 @@ function countTopLevelKeyShapedLines(region) {
675
701
  * the same bytes the offsets were computed against.
676
702
  */
677
703
  function frontmatterRegion(content) {
678
- // #2977: tolerate a single leading UTF-8 BOM (U+FEFF), which Windows tooling
679
- // (PowerShell `>`/`Out-File` on PS 5.1, several editors) writes by default.
680
- // Without this strip, the byte-0 `startsWith('---')` fence check below fails
681
- // on the BOM and the whole parse collapses — every frontmatter field silently
682
- // disappears, and the engine proceeds as though the file had no frontmatter
683
- // at all. The BOM is a single codepoint; stripping it here restores byte-0
684
- // alignment. Scope: BOM only. Arbitrary non-BOM content before the fence
685
- // (leading whitespace/blank line/comment) is a separate product-intent
686
- // decision (tolerate vs diagnose) left to a future change.
687
- if (content.charCodeAt(0) === 0xFEFF)
688
- content = content.slice(1);
689
- // Match frontmatter only at byte 0 — a `---` block later in the document body
690
- // (YAML examples, horizontal rules) must never be treated as frontmatter.
691
- const headerEnd = content.startsWith('---\r\n') ? 5 : content.startsWith('---\n') ? 4 : -1;
692
- if (headerEnd === -1)
704
+ // The fence rules — BOM tolerance (#2977), the byte-0 opening fence, the whole-line closing
705
+ // fence, an adjacent empty block — live in `locateFrontmatterFence`, the one owner every
706
+ // fence consumer reads (found while implementing #5105: four copies of this answer disagreed).
707
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
708
+ if (!fence)
693
709
  return null;
694
- const closingLineStart = content.indexOf('\n---', headerEnd);
695
- if (closingLineStart === -1) {
696
- return { region: content.slice(headerEnd), terminated: false, content };
710
+ const stripped = content.slice(fence.bom.length);
711
+ if (!fence.closed) {
712
+ return { region: content.slice(fence.openEnd), terminated: false, content: stripped };
697
713
  }
698
- const yamlEnd = content[closingLineStart - 1] === '\r' ? closingLineStart - 1 : closingLineStart;
699
- return { region: content.slice(headerEnd, yamlEnd), terminated: true, content };
714
+ return { region: content.slice(fence.openEnd, fence.bodyEnd), terminated: true, content: stripped };
715
+ }
716
+ /**
717
+ * The closed frontmatter BLOCK of a document for a writer: `bom` (the leading BOM, or ''),
718
+ * `block` (from the opening `---` through the closing `---`, both fences included, no line
719
+ * ending after the closing fence) and `rest` (everything after it), so
720
+ * `bom + block + rest === content`. Read from `locateFrontmatterFence`, so a writer and every
721
+ * reader agree on where the block is — BOM, CRLF and an empty block included (found while
722
+ * implementing #5105: two private fence regexes disagreed with the reader on a BOM document
723
+ * and on a block holding only a blank line). Null when there is no block or it is unterminated.
724
+ */
725
+ function frontmatterBlock(content) {
726
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
727
+ if (!fence || !fence.closed)
728
+ return null;
729
+ return {
730
+ bom: fence.bom,
731
+ block: content.slice(fence.bom.length, fence.closingFenceEnd),
732
+ rest: content.slice(fence.closingFenceEnd),
733
+ };
734
+ }
735
+ /**
736
+ * One top-level frontmatter key's block as RAW TEXT: the rest of the key's line plus every
737
+ * following blank or indented line (its nested value), or '' when the closed frontmatter block
738
+ * has no such key. The block is the one `frontmatterRegion` finds — the same fence every reader
739
+ * agrees on — and, unlike `rawFrontmatterField`, the text is returned WITHOUT parsing the YAML, so
740
+ * it survives frontmatter the parser refuses (a `--- x` line inside the block, say). For a caller
741
+ * that scans a block's text for a citation rather than reading its value — the decision-coverage
742
+ * gate's `must_haves` / `truths` / `objective` scan (#5139, moved here from the router).
743
+ * `key` is matched LITERALLY (regex-escaped), so no caller-supplied key can change the pattern.
744
+ */
745
+ function frontmatterKeyBlockText(content, key) {
746
+ const found = frontmatterRegion(content);
747
+ if (!found || !found.terminated)
748
+ return '';
749
+ const match = found.region.match(new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}\\s*:(.*)$`, 'm'));
750
+ if (!match)
751
+ return '';
752
+ const startIdx = (match.index || 0) + match[0].length;
753
+ const rest = found.region.slice(startIdx + 1).split(/\r?\n/);
754
+ const block = [match[1] || ''];
755
+ for (const line of rest) {
756
+ if (line === '' || /^\s/.test(line))
757
+ block.push(line);
758
+ else
759
+ break;
760
+ }
761
+ return block.join('\n');
762
+ }
763
+ /**
764
+ * True when the closed frontmatter block has a line `<key>:<spaces><value><spaces>` — the
765
+ * multiline test `^<key>:\s*<value>\s*$` over the fence owner's region, key and value matched
766
+ * LITERALLY (regex-escaped). The pattern is deliberately the one the `check tdd-review-checkpoint`
767
+ * gate always ran (`^type:\s*tdd\s*$`), so its detection is byte-equivalent to the old regex
768
+ * whatever the block holds (a duplicate key, a value on the next line, CRLF): the block is raw
769
+ * text, never parsed, so it classifies even when the YAML parser refuses the frontmatter. An
770
+ * unterminated or absent block is false. #5139.
771
+ */
772
+ function frontmatterKeyHasValue(content, key, value) {
773
+ const found = frontmatterRegion(content);
774
+ if (!found || !found.terminated)
775
+ return false;
776
+ return new RegExp(`^${(0, pattern_cjs_1.escapeRegex)(key)}:\\s*${(0, pattern_cjs_1.escapeRegex)(value)}\\s*$`, 'm').test(found.region);
700
777
  }
701
778
  function extractFrontmatter(content, sourcePath) {
702
779
  // Fence location (BOM strip, byte-0 rule, CR handling) lives in
@@ -758,6 +835,22 @@ function extractFrontmatter(content, sourcePath) {
758
835
  * "nothing to iterate" cases the caller should not have to tell apart.
759
836
  */
760
837
  function frontmatterListEntries(content, key) {
838
+ const field = rawFrontmatterField(content, key);
839
+ // `Array.isArray` narrows an `unknown` to `any[]`, and returning that
840
+ // unchecked is how `any` escapes a guarded parser into every caller. The
841
+ // element type genuinely IS unknown here — that is the point of this
842
+ // function — so say so.
843
+ if (!field || !Array.isArray(field.value))
844
+ return null;
845
+ return field.value;
846
+ }
847
+ /**
848
+ * One top-level frontmatter key's value VERBATIM — before the display flattening
849
+ * `extractFrontmatter` applies — off the same guarded parse path (`frontmatterRegion`, the
850
+ * anchor/alias and sentinel guards, the ambiguous-colon repair). Null when the document has
851
+ * no closed, parseable frontmatter mapping or the key is not an own key of it.
852
+ */
853
+ function rawFrontmatterField(content, key) {
761
854
  const found = frontmatterRegion(content);
762
855
  if (!found || !found.terminated)
763
856
  return null;
@@ -774,14 +867,9 @@ function frontmatterListEntries(content, key) {
774
867
  }
775
868
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
776
869
  return null;
777
- const value = raw[key];
778
- // `Array.isArray` narrows an `unknown` to `any[]`, and returning that
779
- // unchecked is how `any` escapes a guarded parser into every caller. The
780
- // element type genuinely IS unknown here — that is the point of this
781
- // function — so say so.
782
- if (!Array.isArray(value))
870
+ if (!Object.prototype.hasOwnProperty.call(raw, key))
783
871
  return null;
784
- return value;
872
+ return { value: raw[key] };
785
873
  }
786
874
  /**
787
875
  * Escape a string for emission inside a YAML double-quoted scalar (#1779). ADR-3473 §8.1
@@ -923,100 +1011,128 @@ function agentScalarNeedsDoubleQuoting(s) {
923
1011
  function generalScalarNeedsNumericQuoting(s) {
924
1012
  return YAML_NUMERIC_RE.test(s) && !/^\d+$/.test(s);
925
1013
  }
1014
+ /**
1015
+ * A list item `reconstructFrontmatter` may write inside an inline `[a, b]` list: a string
1016
+ * that reads back as itself there. A flow indicator (`,[]{}`), a `: ` or ` #` (a flow
1017
+ * mapping pair, a comment) or anything that needs quoting as a plain scalar would split,
1018
+ * truncate or re-type the item, so such a list is written in block form instead, where
1019
+ * `blockSequenceItem` quotes the item (found while implementing #5105).
1020
+ */
1021
+ function isPlainFlowSequenceItem(item) {
1022
+ return typeof item === 'string' && !/[,[\]{}]|:(?:\s|$)|\s#/.test(item) && !scalarNeedsDoubleQuoting(item);
1023
+ }
1024
+ /** One block-sequence item as `reconstructFrontmatter` writes it: quoted whenever bare would misread. */
1025
+ function blockSequenceItem(item) {
1026
+ // A null item reads back as '' (see `readBackProjection`), so it is written as one.
1027
+ if (item === null || item === undefined)
1028
+ return '""';
1029
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
1030
+ if (typeof item !== 'string')
1031
+ return String(item);
1032
+ return item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item) ? `"${escapeDoubleQuotedScalar(item)}"` : item;
1033
+ }
1034
+ /** A nested mapping's scalar value as `reconstructFrontmatter` writes it: quoted whenever bare would misread. */
1035
+ function nestedScalar(value) {
1036
+ const sv = String(value);
1037
+ return sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) || generalScalarNeedsNumericQuoting(sv) ? `"${escapeDoubleQuotedScalar(sv)}"` : sv;
1038
+ }
926
1039
  function reconstructFrontmatter(obj) {
927
1040
  const lines = [];
928
1041
  // #3257: read the full-line-comment channel (set by parseGuardedYamlRegion when comments
929
1042
  // were present). Object.entries skips the Symbol key, so the data loop is unchanged.
930
1043
  const commentChannel = obj[FULL_LINE_COMMENTS];
1044
+ // A key's leading full-line comments and its inline comment, by exact key path.
1045
+ const leadingOf = (segments) => commentChannel?.leading[commentPathKey(segments)];
1046
+ const inlineOf = (segments) => commentChannel?.inline?.[commentPathKey(segments)] ?? '';
931
1047
  for (const [key, value] of Object.entries(obj)) {
932
1048
  if (value === null || value === undefined)
933
1049
  continue;
934
1050
  // #3257: re-emit this key's leading full-line comments before the key itself.
935
- const leading = commentChannel?.leading[key];
1051
+ const leading = leadingOf([key]);
936
1052
  if (leading)
937
1053
  for (const c of leading)
938
1054
  lines.push(c);
1055
+ const inline = inlineOf([key]);
939
1056
  if (Array.isArray(value)) {
940
1057
  if (value.length === 0) {
941
- lines.push(`${key}: []`);
1058
+ lines.push(`${key}: []${inline}`);
942
1059
  }
943
- else if (value.every(v => typeof v === 'string') && value.length <= 3 && (value).join(', ').length < 60) {
944
- lines.push(`${key}: [${(value).join(', ')}]`);
1060
+ else if (value.every(isPlainFlowSequenceItem) && value.length <= 3 && (value).join(', ').length < 60) {
1061
+ lines.push(`${key}: [${(value).join(', ')}]${inline}`);
945
1062
  }
946
1063
  else {
947
- lines.push(`${key}:`);
1064
+ lines.push(`${key}:${inline}`);
948
1065
  for (const item of value) {
949
- lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuotedScalar(item)}"` : item}`);
1066
+ lines.push(` - ${blockSequenceItem(item)}`);
950
1067
  }
951
1068
  }
952
1069
  }
953
1070
  else if (typeof value === 'object') {
954
- lines.push(`${key}:`);
1071
+ lines.push(`${key}:${inline}`);
955
1072
  for (const [subkey, subval] of Object.entries(value)) {
956
1073
  if (subval === null || subval === undefined)
957
1074
  continue;
958
1075
  // #3742: re-emit a nested key's leading full-line comments (channel
959
- // path key `parent.subkey`) at the subkey's own indentation.
960
- const nestedLeading = commentChannel?.leading[`${key}.${subkey}`];
1076
+ // path `[parent, subkey]`) at the subkey's own indentation.
1077
+ const nestedLeading = leadingOf([key, subkey]);
961
1078
  if (nestedLeading)
962
1079
  for (const c of nestedLeading)
963
1080
  lines.push(` ${c.trimStart()}`);
1081
+ const subInline = inlineOf([key, subkey]);
964
1082
  if (Array.isArray(subval)) {
965
1083
  if (subval.length === 0) {
966
- lines.push(` ${subkey}: []`);
1084
+ lines.push(` ${subkey}: []${subInline}`);
967
1085
  }
968
- else if (subval.every((v) => typeof v === 'string') && subval.length <= 3 && (subval).join(', ').length < 60) {
969
- lines.push(` ${subkey}: [${(subval).join(', ')}]`);
1086
+ else if (subval.every(isPlainFlowSequenceItem) && subval.length <= 3 && (subval).join(', ').length < 60) {
1087
+ lines.push(` ${subkey}: [${(subval).join(', ')}]${subInline}`);
970
1088
  }
971
1089
  else {
972
- lines.push(` ${subkey}:`);
1090
+ lines.push(` ${subkey}:${subInline}`);
973
1091
  for (const item of subval) {
974
- lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuotedScalar(item)}"` : item}`);
1092
+ lines.push(` - ${blockSequenceItem(item)}`);
975
1093
  }
976
1094
  }
977
1095
  }
978
1096
  else if (typeof subval === 'object') {
979
- lines.push(` ${subkey}:`);
1097
+ lines.push(` ${subkey}:${subInline}`);
980
1098
  for (const [subsubkey, subsubval] of Object.entries(subval)) {
981
1099
  if (subsubval === null || subsubval === undefined)
982
1100
  continue;
983
1101
  // #3742: same nested-comment re-emission one level deeper
984
- // (`parent.sub.subsub`).
985
- const deepLeading = commentChannel?.leading[`${key}.${subkey}.${subsubkey}`];
1102
+ // (`[parent, sub, subsub]`).
1103
+ const deepLeading = leadingOf([key, subkey, subsubkey]);
986
1104
  if (deepLeading)
987
1105
  for (const c of deepLeading)
988
1106
  lines.push(` ${c.trimStart()}`);
1107
+ const deepInline = inlineOf([key, subkey, subsubkey]);
989
1108
  if (Array.isArray(subsubval)) {
990
1109
  if (subsubval.length === 0) {
991
- lines.push(` ${subsubkey}: []`);
1110
+ lines.push(` ${subsubkey}: []${deepInline}`);
992
1111
  }
993
1112
  else {
994
- lines.push(` ${subsubkey}:`);
1113
+ lines.push(` ${subsubkey}:${deepInline}`);
995
1114
  for (const item of subsubval) {
996
- lines.push(` - ${item}`);
1115
+ lines.push(` - ${blockSequenceItem(item)}`);
997
1116
  }
998
1117
  }
999
1118
  }
1000
1119
  else {
1001
- // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
1002
- lines.push(` ${subsubkey}: ${subsubval}`);
1120
+ lines.push(` ${subsubkey}: ${nestedScalar(subsubval)}${deepInline}`);
1003
1121
  }
1004
1122
  }
1005
1123
  }
1006
1124
  else {
1007
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
1008
- const sv = String(subval);
1009
- lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) || generalScalarNeedsNumericQuoting(sv) ? `"${escapeDoubleQuotedScalar(sv)}"` : sv}`);
1125
+ lines.push(` ${subkey}: ${nestedScalar(subval)}${subInline}`);
1010
1126
  }
1011
1127
  }
1012
1128
  }
1013
1129
  else {
1014
1130
  const sv = String(value);
1015
1131
  if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{') || scalarNeedsDoubleQuoting(sv) || generalScalarNeedsNumericQuoting(sv)) {
1016
- lines.push(`${key}: "${escapeDoubleQuotedScalar(sv)}"`);
1132
+ lines.push(`${key}: "${escapeDoubleQuotedScalar(sv)}"${inline}`);
1017
1133
  }
1018
1134
  else {
1019
- lines.push(`${key}: ${sv}`);
1135
+ lines.push(`${key}: ${sv}${inline}`);
1020
1136
  }
1021
1137
  }
1022
1138
  }
@@ -1042,8 +1158,8 @@ function propagateCommentChannel(source, target) {
1042
1158
  return;
1043
1159
  // #3742: two changes, both about a target that is a PARTIAL rebuild.
1044
1160
  //
1045
- // (a) Root-segment membership: a comment keyed by a dotted path
1046
- // (`progress.total_plans`) survives while its root section survives —
1161
+ // (a) Root-segment membership: a comment keyed by a nested path
1162
+ // (`["progress","total_plans"]`) survives while its root section survives —
1047
1163
  // requiring the full path to resolve inside `target` would drop every
1048
1164
  // nested comment the moment the rebuild reconstructed the section
1049
1165
  // object (a fresh object with the same leaf keys still matches at
@@ -1054,8 +1170,10 @@ function propagateCommentChannel(source, target) {
1054
1170
  // concatenate (source first, mirroring document order when the source
1055
1171
  // is the earlier snapshot).
1056
1172
  const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
1173
+ // The root is the path's first segment (`commentPathKey`), never text before a `.` — a
1174
+ // top-level key named `a.b` is its own root (found while implementing #5105).
1057
1175
  const rootAlive = (k) => {
1058
- const root = k.split('.')[0];
1176
+ const root = JSON.parse(k)[0];
1059
1177
  return hasOwn(target, root);
1060
1178
  };
1061
1179
  // Null-prototype `leading` (post-#3881-review, finding 3) — same rationale as
@@ -1086,130 +1204,64 @@ function propagateCommentChannel(source, target) {
1086
1204
  target[FULL_LINE_COMMENTS] = merged;
1087
1205
  }
1088
1206
  }
1207
+ /** Column-0 quoted key: `"…":` (JSON-style escapes) or `'…':` (`''` escapes a quote). */
1208
+ const QUOTED_SEGMENT_KEY_RE = /^(?:"((?:[^"\\]|\\.)*)"|'((?:[^']|'')*)')[ \t]*:/;
1089
1209
  /**
1090
- * Slice a frontmatter YAML body into per-top-level-key raw text segments. Each segment
1091
- * runs from a column-0 `key:` line through the line before the next column-0 key (or the
1092
- * end), capturing all nested indented content. Used by `spliceFrontmatter` for per-key
1093
- * identity preservation (#1572): a structurally-unchanged key keeps its original raw
1094
- * text, so the lossy `reconstructFrontmatter` never touches object-lists the caller did
1095
- * not modify (e.g. must_haves.artifacts / .prohibitions).
1210
+ * First character of a plain (unquoted) top-level key. A comment, an indented line, a
1211
+ * quote and a YAML indicator (`-`/`?`/`:` followed by whitespace or end of line, or any
1212
+ * of `,[]{}&*!|>%@` and backtick) never start one — those lines are never a key line, so
1213
+ * a key the parser reads from them (an explicit `? k` key, a flow mapping) has no segment
1214
+ * and `spliceFrontmatter` refuses rather than duplicating it.
1096
1215
  */
1097
- function sliceTopLevelFrontmatterSegments(yaml) {
1098
- const lines = (0, text_lines_cjs_1.splitLines)(yaml);
1099
- const segments = [];
1100
- let current = null;
1101
- for (const line of lines) {
1102
- // A column-0 `key:` (no leading whitespace) starts a new top-level segment.
1103
- if (/^[A-Za-z0-9_-]+:/.test(line)) {
1104
- if (current)
1105
- segments.push({ key: current.key, raw: current.raw.join('\n') });
1106
- const keyName = line.match(/^([A-Za-z0-9_-]+):/)[1];
1107
- current = { key: keyName, raw: [line] };
1108
- }
1109
- else if (current) {
1110
- current.raw.push(line);
1111
- }
1112
- // Stray lines before the first top-level key (rare in frontmatter) are dropped.
1113
- }
1114
- if (current)
1115
- segments.push({ key: current.key, raw: current.raw.join('\n') });
1116
- return segments;
1117
- }
1216
+ const PLAIN_KEY_START_RE = /^(?:[^\s#,[\]{}&*!|>'"%@`\-?:]|[-?:](?=[^\s]))/;
1118
1217
  /**
1119
- * Regenerate one frontmatter key's serialization, fail-closed if the lossy
1120
- * `reconstructFrontmatter` cannot represent the value (#1572 codex review). Object-list
1121
- * items (e.g. must_haves.artifacts `{path, provides}` maps) serialize as the literal
1122
- * string "[object Object]"; rather than silently emit that and destroy the data, refuse
1123
- * so the caller (cmdFrontmatterSet/Merge) errors out WITHOUT writing — directing the
1124
- * user to edit the file directly. The reported #1572 case (mutating an UNRELATED field)
1125
- * is unaffected: unchanged keys preserve their original raw text and never reach here.
1218
+ * Where the key of a column-0 line ends, agreeing with the YAML parser: the key is
1219
+ * everything before the FIRST `:` followed by whitespace or end of line (so `a:b: 1` is
1220
+ * key `a:b` and `http://x: 1` is key `http://x`, exactly as js-yaml reads them). Only when
1221
+ * no such colon exists does the no-space `key:value` spelling (`updated:2026-01-01`) fall
1222
+ * back to the bare-ASCII key before the first `:`. Returns null for a line that is not a
1223
+ * top-level key line.
1224
+ *
1225
+ * `indent` is the caller's indentation of `line` (0 for the genuinely column-0 callers —
1226
+ * `sliceFrontmatterLayout` — which never see a nonzero value since a real indented line
1227
+ * already fails `PLAIN_KEY_START_RE` there). `channelKeyLine` (#5105 follow-up) calls this
1228
+ * on an INDENT-STRIPPED nested line instead, to read nested keys like `total_phases: 5` —
1229
+ * but the no-space bare-ASCII fallback exists only for the top-level `key:value` shorthand a
1230
+ * document author actually writes; on a nested continuation line (a URL, a path, or any
1231
+ * other value line of a multi-line scalar that happens to contain an ASCII word immediately
1232
+ * followed by `:`, e.g. `https://x`) it misreads the line as opening a key. Restricting the
1233
+ * fallback to `indent === 0` keeps the top-level shorthand working while a nested line only
1234
+ * ever opens a key when the colon is unambiguously followed by whitespace or end of line.
1126
1235
  */
1127
- function regenerateFrontmatterKey(key, value) {
1128
- const rendered = reconstructFrontmatter({ [key]: value });
1129
- if (/\[object Object\]/.test(rendered)) {
1130
- throw new Error(`frontmatter: cannot faithfully serialize key "${key}" — it contains a nested object-list ` +
1131
- `(e.g. must_haves.artifacts) the frontmatter writer cannot represent, and serializing it would ` +
1132
- `emit "[object Object]". Edit the file directly instead of using frontmatter set/merge.`);
1133
- }
1134
- return rendered;
1135
- }
1136
- function spliceFrontmatter(content, newObj) {
1137
- const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
1138
- if (match) {
1139
- const fmBlock = match[0];
1140
- // Whole-document no-op guard: a true no-op returns content verbatim (byte-exact,
1141
- // including any formatting the lossy serializer would normalize).
1142
- try {
1143
- if (frontmatterDeepEqual(extractFrontmatter(content), newObj)) {
1144
- return content;
1145
- }
1146
- }
1147
- catch {
1148
- /* fall through to regeneration on any comparison hiccup */
1149
- }
1150
- // Per-key identity preservation (#1572). `reconstructFrontmatter` is a deliberately
1151
- // lossy serializer — it cannot faithfully re-emit nested object-list items (e.g.
1152
- // must_haves.artifacts / .prohibitions, whose items are `{ path, provides }` /
1153
- // `{ statement, status }` maps; `extractFrontmatter` flattens those to scalar
1154
- // strings, so a round-trip drops `provides:` and collapses the list to a malformed
1155
- // inline array). For any top-level key whose value is STRUCTURALLY UNCHANGED between
1156
- // the original parse and `newObj`, preserve that key's ORIGINAL raw text verbatim;
1157
- // regenerate only keys that actually changed. This generalizes the whole-document
1158
- // no-op guard above to per-key fidelity, so mutating `wave` no longer destroys an
1159
- // unrelated `must_haves` block. Keys absent from the original (genuinely new) are
1160
- // regenerated and appended; keys absent from `newObj` are preserved (never silently
1161
- // deleted by a set/merge).
1162
- const fmLines = (0, text_lines_cjs_1.splitLines)(fmBlock);
1163
- const inner = fmLines.slice(1, -1).join('\n'); // drop the opening `---` and closing `---`
1164
- let originalParsed;
1165
- try {
1166
- originalParsed = extractFrontmatter(fmBlock);
1167
- }
1168
- catch {
1169
- originalParsed = {};
1170
- }
1171
- const segments = sliceTopLevelFrontmatterSegments(inner);
1172
- const emitted = [];
1173
- const seen = new Set();
1174
- for (const seg of segments) {
1175
- seen.add(seg.key);
1176
- if (Object.prototype.hasOwnProperty.call(newObj, seg.key)) {
1177
- // Key is in newObj: preserve original raw text if structurally unchanged,
1178
- // otherwise regenerate. The key SET is defined by newObj — keys that were in
1179
- // the original but are absent from newObj are intentionally dropped (the real
1180
- // cmdSet/cmdMerge flow always passes the full merged object, so this only
1181
- // matters for direct unit callers and matches spliceFrontmatter's contract:
1182
- // the result frontmatter IS newObj).
1183
- if (frontmatterDeepEqual(newObj[seg.key], originalParsed[seg.key])) {
1184
- emitted.push(seg.raw); // unchanged → preserve original raw text verbatim
1185
- }
1186
- else {
1187
- emitted.push(regenerateFrontmatterKey(seg.key, newObj[seg.key])); // changed → regenerate (fail-closed on object-lists)
1188
- }
1236
+ function segmentKeyOf(line, indent = 0) {
1237
+ const q = QUOTED_SEGMENT_KEY_RE.exec(line);
1238
+ if (q) {
1239
+ const valueStart = q[0].length;
1240
+ if (q[1] !== undefined) {
1241
+ try {
1242
+ return { key: JSON.parse(`"${q[1]}"`), valueStart };
1189
1243
  }
1190
- // else: key absent from newObj → drop (not emitted).
1191
- }
1192
- // Append genuinely-new keys not present in the original frontmatter.
1193
- for (const k of Object.keys(newObj)) {
1194
- if (!seen.has(k)) {
1195
- emitted.push(regenerateFrontmatterKey(k, newObj[k]));
1244
+ catch {
1245
+ return { key: q[1], valueStart };
1196
1246
  }
1197
1247
  }
1198
- const yamlStr = emitted.join('\n');
1199
- return `---\n${yamlStr}\n---` + content.slice(fmBlock.length);
1200
- }
1201
- // No existing frontmatter — generate from scratch, fail-closed on unrepresentable values.
1202
- const yamlStr = reconstructFrontmatter(newObj);
1203
- if (/\[object Object\]/.test(yamlStr)) {
1204
- throw new Error('frontmatter: cannot faithfully serialize the requested frontmatter — it contains a nested ' +
1205
- 'object-list (e.g. must_haves.artifacts) the writer cannot represent. Edit the file directly.');
1248
+ return { key: q[2].replace(/''/g, "'"), valueStart };
1206
1249
  }
1207
- return `---\n${yamlStr}\n---\n\n` + content;
1250
+ if (!PLAIN_KEY_START_RE.test(line))
1251
+ return null;
1252
+ const spaced = /:(?:[ \t]|$)/.exec(line);
1253
+ if (spaced)
1254
+ return { key: line.slice(0, spaced.index).trimEnd(), valueStart: spaced.index + 1 };
1255
+ if (indent > 0)
1256
+ return null;
1257
+ const bare = /^([A-Za-z0-9_-]+):/.exec(line);
1258
+ return bare ? { key: bare[1], valueStart: bare[0].length } : null;
1208
1259
  }
1209
1260
  /**
1210
1261
  * Structural deep-equality for two parsed frontmatter objects. Order-sensitive for arrays
1211
- * (YAML lists are ordered), key-order-insensitive for objects. Used only by `spliceFrontmatter`
1212
- * to recognize a no-op write-back; intentionally narrow (handles the string / string[] /
1262
+ * (YAML lists are ordered), key-order-insensitive for objects. Used by the writer
1263
+ * (`frontmatter-splice.cts`, through `spliceSeam`) to recognize a no-op write-back, and by
1264
+ * `objectListFieldWouldLoseData` below; intentionally narrow (handles the string / string[] /
1213
1265
  * nested-object shapes `extractFrontmatter` produces).
1214
1266
  */
1215
1267
  function frontmatterDeepEqual(a, b) {
@@ -1285,10 +1337,12 @@ function normalizeMustHavesItem(item) {
1285
1337
  * `.planning/` must_haves blocks are untrusted input exactly like the rest of frontmatter.
1286
1338
  */
1287
1339
  function parseMustHavesBlock(content, blockName) {
1288
- const fmMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
1289
- if (!fmMatch)
1340
+ // Located through the one fence owner, so a BOM document's must_haves are read like an
1341
+ // LF or CRLF one's (found while implementing #5105).
1342
+ const found = frontmatterRegion(content);
1343
+ if (!found || !found.terminated)
1290
1344
  return [];
1291
- const yaml = fmMatch[1];
1345
+ const yaml = found.region;
1292
1346
  let parsed;
1293
1347
  try {
1294
1348
  refuseAnchorsAndAliases(yaml);
@@ -1315,6 +1369,16 @@ function parseMustHavesBlock(content, blockName) {
1315
1369
  }
1316
1370
  return block.map(normalizeMustHavesItem);
1317
1371
  }
1372
+ /**
1373
+ * The frontmatter WRITER (`frontmatter-splice.cts`, split out of this module by #5105), required
1374
+ * LAZILY: that module requires this one at load time for the parser it builds on, so requiring
1375
+ * it here at load time would hand one of the two a half-built export object. Called only from
1376
+ * the re-export getters and the set/merge commands below — never while this module loads.
1377
+ */
1378
+ function spliceModule() {
1379
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1380
+ return require('./frontmatter-splice.cjs');
1381
+ }
1318
1382
  // ─── Frontmatter CRUD commands ────────────────────────────────────────────────
1319
1383
  // Shared base for 'plan' and 'plan-gap-closure' below — a plain array reference (not
1320
1384
  // FRONTMATTER_SCHEMAS.plan.required) because the object literal that defines
@@ -1364,14 +1428,29 @@ const FRONTMATTER_SCHEMAS = {
1364
1428
  *
1365
1429
  * Canonical home for this primitive (#2143 audit dedup): previously
1366
1430
  * duplicated byte-identically in both `state.cts` and `state-transition.cts`.
1431
+ *
1432
+ * Each block is the one `locateFrontmatterFence` finds — the same block every reader and
1433
+ * writer sees — and the whitespace after its closing fence goes with it. Found while
1434
+ * implementing #5105: the previous regex needed a line ending before the closing `---` that
1435
+ * the opening fence's own line ending could not supply, so it could not see an adjacent empty
1436
+ * block (`---\n---\n`) and stripped through the first `---` in the BODY instead; it also
1437
+ * closed on any line merely starting with `---`, which no reader of the block agreed with.
1438
+ *
1439
+ * Whitespace BEFORE the opening fence is skipped here, and only here: readers see no block in
1440
+ * such a document, but this is the writer's strip step (`state update` strips the old block and
1441
+ * writes a new one), and not skipping it would stack a second block above the stale one. The
1442
+ * whitespace goes only when a closed block follows it; otherwise `content` is returned as is.
1367
1443
  */
1368
1444
  function stripFrontmatter(content, opts = {}) {
1369
1445
  let result = content;
1370
- while (true) {
1371
- const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
1372
- if (stripped === result)
1446
+ const unindented = content.replace(/^\s+/, '');
1447
+ if (unindented !== content && (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(unindented)?.closed)
1448
+ result = unindented;
1449
+ for (;;) {
1450
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(result);
1451
+ if (!fence || !fence.closed)
1373
1452
  break;
1374
- result = stripped;
1453
+ result = result.slice(fence.closingFenceEnd).replace(/^\s*/, '');
1375
1454
  if (opts.once)
1376
1455
  break;
1377
1456
  }
@@ -1394,6 +1473,14 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
1394
1473
  // Pass the resolved path so a truncated file is named in the diagnostic and deduplicated
1395
1474
  // per file rather than per content digest (#1882, ADR-1411 wiring clause).
1396
1475
  const fm = extractFrontmatter(content, fullPath);
1476
+ // #4806: an unparseable frontmatter block is a distinct outcome — the file
1477
+ // HAS a frontmatter block but its YAML failed to parse. Reporting
1478
+ // "Field not found" tells the caller the key is absent, which is
1479
+ // indistinguishable from a file that genuinely lacks it.
1480
+ if (fm[FRONTMATTER_UNPARSEABLE] === true) {
1481
+ output({ error: 'Frontmatter is not parseable YAML — fix the syntax error in the frontmatter block', path: filePath }, raw, undefined);
1482
+ return;
1483
+ }
1397
1484
  if (field) {
1398
1485
  const value = fm[field];
1399
1486
  if (value === undefined) {
@@ -1406,6 +1493,43 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
1406
1493
  output(fm, raw, undefined);
1407
1494
  }
1408
1495
  }
1496
+ /**
1497
+ * A field name is one line of a YAML key: a line break, NUL or other control character in it
1498
+ * is never an intended key name. The one check `frontmatter set` and `frontmatter merge`
1499
+ * share (found while implementing #5105).
1500
+ */
1501
+ function rejectControlCharacterFieldName(field) {
1502
+ if (/[\u0000-\u001f\u007f]/.test(field)) {
1503
+ error('field name contains a control character (a line break, tab, NUL or other C0/DEL character) — use a plain key name');
1504
+ }
1505
+ }
1506
+ /**
1507
+ * Write one field as an own data property. `fm[field] =` and `Object.assign` treat a field
1508
+ * named `__proto__` as the prototype setter, so the key was never written while the command
1509
+ * reported success (found while implementing #5105).
1510
+ */
1511
+ function setOwnField(fm, field, value) {
1512
+ Object.defineProperty(fm, field, { value, writable: true, enumerable: true, configurable: true });
1513
+ }
1514
+ /**
1515
+ * `spliceFrontmatter` for the set/merge commands: a write refusal (`FrontmatterWriteRefusedError`
1516
+ * — unparseable block, unreconcilable keys, a block that would not read back, a comment that
1517
+ * would be lost, a block too complex to classify within the parse budget) is reported as `{ error, code, path }` and nothing is written — the same
1518
+ * shape `cmdFrontmatterGet` uses for an unparseable block. Returns null when refused; any
1519
+ * other error propagates as before.
1520
+ */
1521
+ function spliceOrReportRefusal(content, fm, filePath, raw) {
1522
+ const { spliceFrontmatter, isFrontmatterWriteRefusal } = spliceModule();
1523
+ try {
1524
+ return spliceFrontmatter(content, fm);
1525
+ }
1526
+ catch (err) {
1527
+ if (!isFrontmatterWriteRefusal(err))
1528
+ throw err;
1529
+ output({ error: err.message, code: err.code, path: filePath }, raw, undefined);
1530
+ return null;
1531
+ }
1532
+ }
1409
1533
  function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
1410
1534
  if (!filePath || !field || value === undefined) {
1411
1535
  error('file, field, and value required');
@@ -1414,6 +1538,7 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
1414
1538
  if (filePath.includes('\0')) {
1415
1539
  error('file path contains null bytes');
1416
1540
  }
1541
+ rejectControlCharacterFieldName(field);
1417
1542
  const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
1418
1543
  if (!node_fs_1.default.existsSync(fullPath)) {
1419
1544
  output({ error: 'File not found', path: filePath }, raw, undefined);
@@ -1438,8 +1563,10 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
1438
1563
  output({ error: lossyErr, field }, raw, undefined);
1439
1564
  return;
1440
1565
  }
1441
- fm[field] = parsedValue;
1442
- const newContent = spliceFrontmatter(content, fm);
1566
+ setOwnField(fm, field, parsedValue);
1567
+ const newContent = spliceOrReportRefusal(content, fm, filePath, raw);
1568
+ if (newContent === null)
1569
+ return;
1443
1570
  // #1660: a no-op set (newContent unchanged) with a dict-valued field means the lossy
1444
1571
  // frontmatter parser made the new value's projection equal the original's — the change
1445
1572
  // did not apply (bites object-list fields like must_haves). Detection lives in the pure
@@ -1480,15 +1607,31 @@ function noOpObjectListSetError(originalContent, newContent, parsedValue) {
1480
1607
  * no-op guard, sails through `regenerateFrontmatterKey` (which only refuses when the NEW value
1481
1608
  * itself contains a live JS object), and silently writes a version with `provides` gone.
1482
1609
  *
1483
- * This is the general form of the same "cannot faithfully round-trip" contract: a field is
1484
- * lossy exactly when regenerating its OWN already-parsed value fails to reproduce its own raw
1485
- * source text byte-for-byte (proof, not a guess, that this key's original shape does not
1486
- * survive parse → reconstruct). When that is true AND the caller is genuinely changing the
1487
- * field (not merely re-supplying an equal value, which `frontmatterDeepEqual` already lets
1488
- * through), the set is refused — matching `regenerateFrontmatterKey`'s own fail-closed
1489
- * philosophy for the mirror-image case (new value carries a nested object outright).
1610
+ * A field is lossy exactly when the parse a caller sees FLATTENED it: its verbatim YAML value
1611
+ * (`rawFrontmatterField`) holds a list item that is itself a mapping or a list, which
1612
+ * `extractFrontmatter` hands back as one display string, so any replacement a caller builds
1613
+ * from that view silently drops the item's structure. When that is true AND the caller is
1614
+ * genuinely changing the field (not merely re-supplying an equal value), the set is refused —
1615
+ * matching `regenerateFrontmatterKey`'s own fail-closed philosophy for the mirror-image case
1616
+ * (new value carries a nested object outright).
1617
+ *
1618
+ * Found while implementing #5105: this used to compare the field's regenerated text against
1619
+ * its source text, which refused every field merely written in another style — a quoted
1620
+ * scalar (`title: 'x'`), a trailing comment, a block list, a multi-line value — although the
1621
+ * parse had lost nothing. What the caller replaces is its own business; only data the caller
1622
+ * could not see is protected here, and whether the NEW value reads back is `spliceFrontmatter`'s
1623
+ * read-back check.
1490
1624
  */
1625
+ function holdsFlattenedListItem(value) {
1626
+ if (Array.isArray(value))
1627
+ return value.some((item) => (item !== null && typeof item === 'object') || holdsFlattenedListItem(item));
1628
+ if (value !== null && typeof value === 'object')
1629
+ return Object.values(value).some(holdsFlattenedListItem);
1630
+ return false;
1631
+ }
1632
+ /** See `holdsFlattenedListItem`: the #1660 lossy object-list refusal for one changed field. */
1491
1633
  function objectListFieldWouldLoseData(content, field, newValue) {
1634
+ const { regenerateFrontmatterKey, readBackProjection } = spliceModule();
1492
1635
  // A NEW value that itself carries a live nested object (rather than an already-flattened
1493
1636
  // string) is the mirror-image case `regenerateFrontmatterKey` already refuses on its own
1494
1637
  // (the "[object Object]" guard, via spliceFrontmatter) — leave that path's existing throw
@@ -1508,25 +1651,13 @@ function objectListFieldWouldLoseData(content, field, newValue) {
1508
1651
  }
1509
1652
  if (!Object.prototype.hasOwnProperty.call(originalParsed, field))
1510
1653
  return null;
1511
- const originalValue = originalParsed[field];
1512
- if (frontmatterDeepEqual(newValue, originalValue))
1513
- return null;
1514
- const fmMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
1515
- if (!fmMatch)
1516
- return null;
1517
- const original = sliceTopLevelFrontmatterSegments(fmMatch[1]).find((s) => s.key === field);
1518
- if (!original)
1654
+ if (frontmatterDeepEqual(readBackProjection(newValue), originalParsed[field]))
1519
1655
  return null;
1520
- let regeneratedOriginal;
1521
- try {
1522
- regeneratedOriginal = regenerateFrontmatterKey(field, originalValue);
1523
- }
1524
- catch {
1525
- return `frontmatter set refused — the existing "${field}" field contains a nested object-list ` +
1526
- `(e.g. must_haves.artifacts) the frontmatter writer cannot faithfully represent, and this change ` +
1527
- `would silently discard data. Edit the file directly instead of using frontmatter set/merge.`;
1528
- }
1529
- if (regeneratedOriginal.trim() === original.raw.trim())
1656
+ // The verbatim value is read through the one fence owner (`frontmatterRegion`), so a BOM
1657
+ // document is refused exactly like an LF or CRLF one, and any key spelling (bare, quoted,
1658
+ // Unicode) is found by its parsed key (found while implementing #5105).
1659
+ const original = rawFrontmatterField(content, field);
1660
+ if (!original || !holdsFlattenedListItem(original.value))
1530
1661
  return null;
1531
1662
  return `frontmatter set refused — the existing "${field}" field cannot be faithfully round-tripped by ` +
1532
1663
  `the frontmatter writer (its structure would be flattened and data, such as a nested object-list ` +
@@ -1553,8 +1684,29 @@ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
1553
1684
  error('Invalid JSON for --data');
1554
1685
  return;
1555
1686
  }
1556
- Object.assign(fm, mergeData);
1557
- const newContent = spliceFrontmatter(content, fm);
1687
+ // Only a JSON object names fields: an array or a string would spread into index-named
1688
+ // keys (`0: q`) and `null` crashed (found while implementing #5105).
1689
+ if (mergeData === null || typeof mergeData !== 'object' || Array.isArray(mergeData)) {
1690
+ error('--data must be a JSON object of field names to values');
1691
+ return;
1692
+ }
1693
+ for (const key of Object.keys(mergeData))
1694
+ rejectControlCharacterFieldName(key);
1695
+ // #1660 parity with `cmdFrontmatterSet`: a merged key that would flatten a lossy object-list
1696
+ // field is refused before anything is written, reported in set's `{ error, field }` shape
1697
+ // (found while implementing #5105).
1698
+ for (const [key, value] of Object.entries(mergeData)) {
1699
+ const lossyErr = objectListFieldWouldLoseData(content, key, value);
1700
+ if (lossyErr) {
1701
+ output({ error: lossyErr, field: key }, raw, undefined);
1702
+ return;
1703
+ }
1704
+ }
1705
+ for (const [key, value] of Object.entries(mergeData))
1706
+ setOwnField(fm, key, value);
1707
+ const newContent = spliceOrReportRefusal(content, fm, filePath, raw);
1708
+ if (newContent === null)
1709
+ return;
1558
1710
  (0, shell_command_projection_cjs_1.platformWriteSync)(fullPath, newContent);
1559
1711
  output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
1560
1712
  }
@@ -1632,7 +1784,17 @@ module.exports = {
1632
1784
  // the alias so the prohibition schema round-trip and any future caller can use the canonical name.
1633
1785
  parseFrontmatter: extractFrontmatter,
1634
1786
  reconstructFrontmatter,
1635
- spliceFrontmatter,
1787
+ // The writer lives in `frontmatter-splice.cts` (#5105); its public names are re-exported
1788
+ // here, unchanged, so no caller's import moves. Getters, because that module requires this
1789
+ // one at load time — see `spliceModule`.
1790
+ get spliceFrontmatter() { return spliceModule().spliceFrontmatter; },
1791
+ // The one owner of "may a writer splice this frontmatter block?" — callers catch
1792
+ // `isFrontmatterWriteRefusal(err)` and surface `err.message`/`err.code`.
1793
+ get FrontmatterWriteRefusedError() { return spliceModule().FrontmatterWriteRefusedError; },
1794
+ get isFrontmatterWriteRefusal() { return spliceModule().isFrontmatterWriteRefusal; },
1795
+ // The parse allowance past which `spliceFrontmatter` refuses with FRONTMATTER_TOO_COMPLEX —
1796
+ // exported so its boundary can be exercised exactly.
1797
+ get SPLICE_PARSE_BUDGET_CHARS() { return spliceModule().SPLICE_PARSE_BUDGET_CHARS; },
1636
1798
  stripFrontmatter,
1637
1799
  noOpObjectListSetError,
1638
1800
  parseMustHavesBlock,
@@ -1640,6 +1802,16 @@ module.exports = {
1640
1802
  // branch on an entry's `status:`/`resolution:` rather than print it. Off the
1641
1803
  // same parse path as `extractFrontmatter`, minus only the display flattening.
1642
1804
  frontmatterListEntries,
1805
+ // #5139: one top-level key's value VERBATIM (before display flattening), off the same guarded
1806
+ // parse path — the decision-coverage gates read `must_haves`/`truths`/`objective` and the
1807
+ // SUMMARY `files_modified` list through it instead of a hand-rolled `^key:` scan / regex.
1808
+ rawFrontmatterField,
1809
+ // #5139: one key's block as unparsed text, for a citation scan that must survive frontmatter
1810
+ // the YAML parser refuses.
1811
+ frontmatterKeyBlockText,
1812
+ // #5139: `^<key>:\s*<value>\s*$` over the fence owner's region, both escaped — the tdd gate's
1813
+ // `type: tdd` test, equivalent to the regex it replaced.
1814
+ frontmatterKeyHasValue,
1643
1815
  // #3850: the display rendering itself, so a caller deriving a name from those
1644
1816
  // objects produces the byte-identical string `extractFrontmatter` would have.
1645
1817
  flattenObjectListItem,
@@ -1649,4 +1821,24 @@ module.exports = {
1649
1821
  cmdFrontmatterMerge,
1650
1822
  cmdFrontmatterValidate,
1651
1823
  propagateCommentChannel,
1824
+ // #4917 / ADR-4910 Decision 1: additive-only export so `planning-document.cts`
1825
+ // can COMPOSE this seam's fence-detection grammar (byte-0 rule, BOM strip,
1826
+ // CR handling) instead of reimplementing it. No behavior change — same
1827
+ // function `extractFrontmatter`/`frontmatterListEntries` already call.
1828
+ frontmatterRegion,
1829
+ // The closed block for a writer (`bom + block + rest === content`), composing
1830
+ // `frontmatterRegion` — `uat.cts` locates its `updated:` edits through it.
1831
+ frontmatterBlock,
1832
+ // #5105: the reader internals the writer (`frontmatter-splice.cts`) builds on. Not public
1833
+ // API — one bag, so the reader's public surface does not grow by eight names.
1834
+ spliceSeam: Object.freeze({
1835
+ FULL_LINE_COMMENTS,
1836
+ YAML_LOAD_OPTS,
1837
+ commentPathKey,
1838
+ channelKeyLine,
1839
+ segmentKeyOf,
1840
+ escapeNullBytesForParse,
1841
+ unparseableResult,
1842
+ frontmatterDeepEqual,
1843
+ }),
1652
1844
  };