@opengsd/gsd-core 1.12.0 → 1.14.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 (455) 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 +12 -0
  4. package/agents/gsd-advisor-researcher.compact.md +85 -0
  5. package/agents/gsd-ai-researcher.compact.md +96 -0
  6. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  7. package/agents/gsd-code-fixer.compact.md +458 -0
  8. package/agents/gsd-code-fixer.md +5 -5
  9. package/agents/gsd-code-reviewer.compact.md +269 -0
  10. package/agents/gsd-code-reviewer.md +15 -3
  11. package/agents/gsd-codebase-mapper.compact.md +760 -0
  12. package/agents/gsd-debug-session-manager.compact.md +345 -0
  13. package/agents/gsd-doc-classifier.compact.md +192 -0
  14. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  15. package/agents/gsd-doc-verifier.compact.md +143 -0
  16. package/agents/gsd-doc-writer.compact.md +440 -0
  17. package/agents/gsd-dom-verifier.compact.md +138 -0
  18. package/agents/gsd-domain-researcher.compact.md +141 -0
  19. package/agents/gsd-eval-auditor.compact.md +160 -0
  20. package/agents/gsd-eval-planner.compact.md +137 -0
  21. package/agents/gsd-executor.md +63 -35
  22. package/agents/gsd-framework-selector.compact.md +82 -0
  23. package/agents/gsd-integration-checker.compact.md +245 -0
  24. package/agents/gsd-intel-updater.compact.md +226 -0
  25. package/agents/gsd-mempalace-curator.compact.md +45 -0
  26. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  27. package/agents/gsd-pattern-mapper.compact.md +275 -0
  28. package/agents/gsd-plan-checker.md +76 -57
  29. package/agents/gsd-planner.md +14 -0
  30. package/agents/gsd-project-researcher.compact.md +587 -0
  31. package/agents/gsd-research-synthesizer.compact.md +212 -0
  32. package/agents/gsd-roadmapper.compact.md +454 -0
  33. package/agents/gsd-roadmapper.md +13 -0
  34. package/agents/gsd-security-auditor.compact.md +162 -0
  35. package/agents/gsd-ui-auditor.compact.md +404 -0
  36. package/agents/gsd-ui-checker.compact.md +277 -0
  37. package/agents/gsd-ui-checker.md +19 -3
  38. package/agents/gsd-ui-researcher.compact.md +282 -0
  39. package/agents/gsd-ui-researcher.md +29 -0
  40. package/agents/gsd-user-profiler.compact.md +108 -0
  41. package/agents/gsd-verifier.md +23 -1
  42. package/bin/install.js +444 -134
  43. package/commands/gsd/cleanup.md +1 -0
  44. package/commands/gsd/code-review.md +2 -1
  45. package/commands/gsd/complete-milestone.md +1 -0
  46. package/commands/gsd/config.md +1 -0
  47. package/commands/gsd/debug.md +1 -0
  48. package/commands/gsd/execute-phase.md +1 -1
  49. package/commands/gsd/graphify.md +1 -0
  50. package/commands/gsd/health.md +1 -0
  51. package/commands/gsd/mempalace-capture.md +1 -0
  52. package/commands/gsd/mempalace-recall.md +1 -0
  53. package/commands/gsd/new-milestone.md +1 -0
  54. package/commands/gsd/new-project.md +1 -0
  55. package/commands/gsd/next.md +1 -0
  56. package/commands/gsd/ns-workflow.md +2 -1
  57. package/commands/gsd/pause-work.md +1 -0
  58. package/commands/gsd/phase.md +2 -1
  59. package/commands/gsd/pr-branch.md +1 -0
  60. package/commands/gsd/quick-batch.md +105 -0
  61. package/commands/gsd/resume-work.md +1 -0
  62. package/commands/gsd/review-backlog.md +1 -0
  63. package/commands/gsd/settings.md +2 -1
  64. package/commands/gsd/stats.md +1 -0
  65. package/commands/gsd/surface.md +18 -8
  66. package/commands/gsd/thread.md +1 -0
  67. package/commands/gsd/workspace.md +1 -0
  68. package/commands/gsd/workstreams.md +1 -0
  69. package/gsd-core/bin/check-latest-version.cjs +8 -3
  70. package/gsd-core/bin/gsd-tools.cjs +532 -174
  71. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  72. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  73. package/gsd-core/bin/lib/audit.cjs +39 -22
  74. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  75. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  76. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  77. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  78. package/gsd-core/bin/lib/capability-registry.cjs +528 -116
  79. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  80. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  81. package/gsd-core/bin/lib/capability-validator.cjs +134 -5
  82. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  83. package/gsd-core/bin/lib/check-command-router.cjs +198 -38
  84. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  85. package/gsd-core/bin/lib/clusters.cjs +1 -0
  86. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  87. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  88. package/gsd-core/bin/lib/commands.cjs +981 -79
  89. package/gsd-core/bin/lib/config-loader.cjs +4 -0
  90. package/gsd-core/bin/lib/config.cjs +153 -38
  91. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  92. package/gsd-core/bin/lib/coverage.cjs +1 -1
  93. package/gsd-core/bin/lib/decisions.cjs +343 -28
  94. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  95. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  96. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  97. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  98. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  99. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  100. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  101. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  102. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  103. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +87 -23
  104. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  105. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  106. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  107. package/gsd-core/bin/lib/init.cjs +539 -60
  108. package/gsd-core/bin/lib/install-engine.cjs +199 -14
  109. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  110. package/gsd-core/bin/lib/install-profiles.cjs +36 -14
  111. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  112. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  113. package/gsd-core/bin/lib/io.cjs +35 -0
  114. package/gsd-core/bin/lib/loop-resolver.cjs +64 -39
  115. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  116. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  117. package/gsd-core/bin/lib/milestone.cjs +41 -10
  118. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  119. package/gsd-core/bin/lib/phase-command-router.cjs +20 -7
  120. package/gsd-core/bin/lib/phase-id.cjs +412 -31
  121. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  122. package/gsd-core/bin/lib/phase.cjs +941 -98
  123. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +206 -30
  126. package/gsd-core/bin/lib/planning-workspace.cjs +153 -29
  127. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  128. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  129. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  130. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  131. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  132. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  133. package/gsd-core/bin/lib/research-store.cjs +11 -12
  134. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  135. package/gsd-core/bin/lib/review-lane-invocation.cjs +96 -1
  136. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  137. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  138. package/gsd-core/bin/lib/roadmap-parser.cjs +555 -41
  139. package/gsd-core/bin/lib/roadmap.cjs +292 -69
  140. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +260 -43
  141. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +28 -20
  142. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +294 -108
  143. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -47
  144. package/gsd-core/bin/lib/security.cjs +126 -7
  145. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  146. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  147. package/gsd-core/bin/lib/state-document.cjs +159 -32
  148. package/gsd-core/bin/lib/state-md-schema.cjs +44 -27
  149. package/gsd-core/bin/lib/state-transition.cjs +465 -62
  150. package/gsd-core/bin/lib/state.cjs +906 -151
  151. package/gsd-core/bin/lib/surface.cjs +83 -10
  152. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  153. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  154. package/gsd-core/bin/lib/uat.cjs +1420 -516
  155. package/gsd-core/bin/lib/update-context.cjs +36 -26
  156. package/gsd-core/bin/lib/validate.cjs +230 -12
  157. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  158. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  159. package/gsd-core/bin/lib/verification.cjs +316 -23
  160. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  161. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  162. package/gsd-core/bin/lib/verify.cjs +531 -36
  163. package/gsd-core/bin/lib/workstream-inventory.cjs +21 -2
  164. package/gsd-core/bin/lib/worktree-safety.cjs +21 -7
  165. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  166. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  167. package/gsd-core/bin/verify-reapply-patches.cjs +507 -81
  168. package/gsd-core/references/agent-contracts.md +3 -3
  169. package/gsd-core/references/compact-content-gate.md +66 -0
  170. package/gsd-core/references/edge-probe.md +17 -13
  171. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  172. package/gsd-core/references/execute-phase-response-language.md +6 -0
  173. package/gsd-core/references/executor-examples.md +42 -0
  174. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  175. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  176. package/gsd-core/references/model-profiles.md +12 -3
  177. package/gsd-core/references/mvp-concepts.md +2 -2
  178. package/gsd-core/references/plan-checker-examples.md +41 -0
  179. package/gsd-core/references/planner-antipatterns.md +25 -0
  180. package/gsd-core/references/planner-chunked.md +5 -1
  181. package/gsd-core/references/planner-coupling.md +42 -0
  182. package/gsd-core/references/planner-quick-batch.md +71 -0
  183. package/gsd-core/references/planner-reviews.md +47 -0
  184. package/gsd-core/references/planner-revision.md +75 -2
  185. package/gsd-core/references/planning-config.md +5 -1
  186. package/gsd-core/references/response-language-directive.md +9 -0
  187. package/gsd-core/references/revision-loop.md +118 -11
  188. package/gsd-core/references/tdd.md +17 -9
  189. package/gsd-core/references/thinking-models-planning.md +18 -2
  190. package/gsd-core/references/verification-patterns.md +17 -4
  191. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  192. package/gsd-core/references/worktree-path-safety.md +112 -2
  193. package/gsd-core/templates/README.md +7 -1
  194. package/gsd-core/templates/phase-prompt.md +4 -0
  195. package/gsd-core/templates/state.md +6 -3
  196. package/gsd-core/templates/summary.compact.md +212 -0
  197. package/gsd-core/templates/user-setup.compact.md +199 -0
  198. package/gsd-core/templates/user-setup.md +0 -9
  199. package/gsd-core/templates/verification-report.md +5 -0
  200. package/gsd-core/workflows/add-backlog.md +2 -0
  201. package/gsd-core/workflows/add-phase.md +2 -0
  202. package/gsd-core/workflows/add-tests.md +1 -1
  203. package/gsd-core/workflows/add-todo.md +4 -3
  204. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  205. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  206. package/gsd-core/workflows/audit-fix.md +2 -0
  207. package/gsd-core/workflows/audit-milestone.md +2 -0
  208. package/gsd-core/workflows/audit-uat.md +2 -0
  209. package/gsd-core/workflows/autonomous.md +15 -10
  210. package/gsd-core/workflows/check-todos.md +5 -3
  211. package/gsd-core/workflows/cleanup.md +4 -2
  212. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +22 -13
  213. package/gsd-core/workflows/code-review-fix.md +5 -3
  214. package/gsd-core/workflows/code-review.md +211 -43
  215. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  216. package/gsd-core/workflows/complete-milestone.md +40 -254
  217. package/gsd-core/workflows/debug.md +1 -1
  218. package/gsd-core/workflows/diagnose-issues.md +5 -1
  219. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  220. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  221. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  222. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  223. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  224. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  225. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  226. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  227. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  228. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  229. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  230. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  231. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  232. package/gsd-core/workflows/discuss-phase.md +1 -1
  233. package/gsd-core/workflows/do.md +43 -13
  234. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  235. package/gsd-core/workflows/docs-update.md +15 -156
  236. package/gsd-core/workflows/edit-phase.md +2 -0
  237. package/gsd-core/workflows/eval-review.md +1 -1
  238. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  239. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +20 -3
  240. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  241. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +24 -3
  242. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  243. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  245. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  246. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  247. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  248. package/gsd-core/workflows/execute-phase.md +78 -159
  249. package/gsd-core/workflows/execute-plan.md +28 -15
  250. package/gsd-core/workflows/explore.md +2 -0
  251. package/gsd-core/workflows/extract-learnings.md +2 -0
  252. package/gsd-core/workflows/fast.md +6 -0
  253. package/gsd-core/workflows/forensics.md +2 -0
  254. package/gsd-core/workflows/graduation.md +1 -1
  255. package/gsd-core/workflows/health.md +1 -1
  256. package/gsd-core/workflows/help/modes/brief.md +2 -0
  257. package/gsd-core/workflows/help/modes/default.md +2 -0
  258. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  259. package/gsd-core/workflows/help/modes/full.md +12 -0
  260. package/gsd-core/workflows/help/modes/topic.md +2 -0
  261. package/gsd-core/workflows/help.md +3 -1
  262. package/gsd-core/workflows/import.md +3 -3
  263. package/gsd-core/workflows/inbox.md +1 -1
  264. package/gsd-core/workflows/ingest-docs.md +1 -1
  265. package/gsd-core/workflows/insert-phase.md +2 -0
  266. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  267. package/gsd-core/workflows/list-seeds.md +2 -0
  268. package/gsd-core/workflows/list-workspaces.md +2 -0
  269. package/gsd-core/workflows/manager.md +3 -3
  270. package/gsd-core/workflows/map-codebase.md +52 -3
  271. package/gsd-core/workflows/milestone-summary.md +2 -0
  272. package/gsd-core/workflows/mvp-phase.md +1 -1
  273. package/gsd-core/workflows/new-milestone.md +55 -13
  274. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  275. package/gsd-core/workflows/new-project.md +37 -205
  276. package/gsd-core/workflows/new-workspace.md +1 -1
  277. package/gsd-core/workflows/next.md +2 -0
  278. package/gsd-core/workflows/node-repair.md +2 -0
  279. package/gsd-core/workflows/note.md +2 -0
  280. package/gsd-core/workflows/onboard.md +1 -1
  281. package/gsd-core/workflows/pause-work.md +19 -4
  282. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  283. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  284. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  285. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  286. package/gsd-core/workflows/plan-phase.md +144 -185
  287. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  288. package/gsd-core/workflows/plant-seed.md +1 -1
  289. package/gsd-core/workflows/pr-branch.md +30 -10
  290. package/gsd-core/workflows/profile-user.md +1 -1
  291. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  292. package/gsd-core/workflows/progress.md +25 -3
  293. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  294. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  295. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  296. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  297. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  298. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  299. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  300. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  301. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  302. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  303. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  304. package/gsd-core/workflows/quick-batch.md +203 -0
  305. package/gsd-core/workflows/quick.md +21 -4
  306. package/gsd-core/workflows/reapply-patches.md +79 -3
  307. package/gsd-core/workflows/remove-phase.md +2 -0
  308. package/gsd-core/workflows/remove-workspace.md +1 -1
  309. package/gsd-core/workflows/resume-project.md +6 -2
  310. package/gsd-core/workflows/review.md +215 -10
  311. package/gsd-core/workflows/scan.md +2 -0
  312. package/gsd-core/workflows/section-manifest.json +12 -0
  313. package/gsd-core/workflows/secure-phase.md +1 -1
  314. package/gsd-core/workflows/session-report.md +2 -0
  315. package/gsd-core/workflows/settings-advanced.md +2 -0
  316. package/gsd-core/workflows/settings-integrations.md +9 -8
  317. package/gsd-core/workflows/settings.md +19 -6
  318. package/gsd-core/workflows/ship.md +10 -10
  319. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  320. package/gsd-core/workflows/sketch.md +1 -1
  321. package/gsd-core/workflows/smart-entry.md +1 -1
  322. package/gsd-core/workflows/spec-phase.md +24 -19
  323. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  324. package/gsd-core/workflows/spike.md +1 -1
  325. package/gsd-core/workflows/stats.md +2 -0
  326. package/gsd-core/workflows/sync-skills.md +12 -4
  327. package/gsd-core/workflows/thread.md +2 -0
  328. package/gsd-core/workflows/transition.md +2 -0
  329. package/gsd-core/workflows/ui-phase.md +26 -5
  330. package/gsd-core/workflows/ui-review.md +1 -1
  331. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  332. package/gsd-core/workflows/undo.md +1 -1
  333. package/gsd-core/workflows/update.md +48 -43
  334. package/gsd-core/workflows/validate-phase.md +1 -1
  335. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  336. package/gsd-core/workflows/verify-work.md +68 -182
  337. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  338. package/hooks/dist/gsd-check-update-worker.js +19 -2
  339. package/hooks/dist/gsd-context-monitor.js +371 -27
  340. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  341. package/hooks/dist/gsd-node-runner.sh +1 -0
  342. package/hooks/dist/gsd-prompt-guard.js +30 -5
  343. package/hooks/dist/gsd-read-guard.js +2 -0
  344. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  345. package/hooks/dist/gsd-secret-read-guard.js +1105 -0
  346. package/hooks/dist/gsd-statusline.js +18 -10
  347. package/hooks/dist/gsd-validate-commit.sh +474 -7
  348. package/hooks/dist/gsd-workflow-guard.js +2 -1
  349. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  350. package/hooks/dist/gsd-write-guard.js +46 -1
  351. package/hooks/dist/lib/dispatch-identity.js +187 -0
  352. package/hooks/dist/lib/filename-classification.js +64 -0
  353. package/hooks/dist/lib/git-cmd.js +210 -1
  354. package/hooks/dist/lib/injection-patterns.js +36 -6
  355. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  356. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  357. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  358. package/hooks/gsd-agent-isolation-guard.js +42 -16
  359. package/hooks/gsd-check-update-worker.js +19 -2
  360. package/hooks/gsd-context-monitor.js +371 -27
  361. package/hooks/gsd-cursor-subagent-start.js +34 -14
  362. package/hooks/gsd-node-runner.sh +1 -0
  363. package/hooks/gsd-prompt-guard.js +30 -5
  364. package/hooks/gsd-read-guard.js +2 -0
  365. package/hooks/gsd-read-injection-scanner.js +5 -5
  366. package/hooks/gsd-secret-read-guard.js +1105 -0
  367. package/hooks/gsd-statusline.js +18 -10
  368. package/hooks/gsd-validate-commit.sh +474 -7
  369. package/hooks/gsd-workflow-guard.js +2 -1
  370. package/hooks/gsd-worktree-path-guard.js +25 -14
  371. package/hooks/gsd-write-guard.js +46 -1
  372. package/hooks/hooks.json +6 -0
  373. package/hooks/lib/dispatch-identity.js +187 -0
  374. package/hooks/lib/filename-classification.js +64 -0
  375. package/hooks/lib/git-cmd.js +210 -1
  376. package/hooks/lib/injection-patterns.js +36 -6
  377. package/hooks/lib/isolation-deny-reason.js +53 -1
  378. package/hooks/lib/isolation-sentinel.js +58 -19
  379. package/hooks/managed-hooks-registry.cjs +1 -0
  380. package/package.json +13 -9
  381. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  382. package/scripts/benchmark-compact-content.cjs +368 -0
  383. package/scripts/build-hooks.js +11 -4
  384. package/scripts/check-contract-drift.cjs +4 -1
  385. package/scripts/check-env.cjs +36 -8
  386. package/scripts/check-glossary-refs.cjs +25 -21
  387. package/scripts/ci-next-health.cjs +271 -0
  388. package/scripts/ci-prepare-test-scope.cjs +7 -7
  389. package/scripts/ci-test-scope.cjs +133 -20
  390. package/scripts/ci-timeout-report.cjs +1 -1
  391. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  392. package/scripts/docs-guard-registry.cjs +17 -2
  393. package/scripts/gen-adr-index.cjs +8 -2
  394. package/scripts/gen-inventory-manifest.cjs +12 -0
  395. package/scripts/gen-loop-host-contract.cjs +67 -15
  396. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  397. package/scripts/lib/drift-scan.cjs +1 -1
  398. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  399. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  400. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  401. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  402. package/scripts/lib/suite-detection.cjs +32 -0
  403. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  404. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  405. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  406. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  407. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +24 -2
  408. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  409. package/scripts/lint-phase-id-drift.cjs +465 -15
  410. package/scripts/lint-portable-grep.cjs +176 -0
  411. package/scripts/lint-response-language-coverage.cjs +530 -0
  412. package/scripts/lint-source-test-name-collision.cjs +1 -1
  413. package/scripts/lint-test-file-count.allowlist.json +4 -1
  414. package/scripts/lint-vendored-deps.cjs +128 -17
  415. package/scripts/lint-workflow-shellcheck-baseline.json +1112 -0
  416. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  417. package/scripts/npm-audit-baseline.cjs +376 -0
  418. package/scripts/prompt-injection-scan.sh +22 -0
  419. package/scripts/require-issue-link-policy.cjs +16 -1
  420. package/scripts/workflow-size.cjs +139 -0
  421. package/skills/gsd-cleanup/SKILL.md +1 -0
  422. package/skills/gsd-code-review/SKILL.md +2 -1
  423. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  424. package/skills/gsd-config/SKILL.md +1 -0
  425. package/skills/gsd-debug/SKILL.md +1 -0
  426. package/skills/gsd-execute-phase/SKILL.md +1 -1
  427. package/skills/gsd-graphify/SKILL.md +1 -0
  428. package/skills/gsd-health/SKILL.md +1 -0
  429. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  430. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  431. package/skills/gsd-new-milestone/SKILL.md +1 -0
  432. package/skills/gsd-new-project/SKILL.md +1 -0
  433. package/skills/gsd-next/SKILL.md +1 -0
  434. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  435. package/skills/gsd-pause-work/SKILL.md +1 -0
  436. package/skills/gsd-phase/SKILL.md +2 -1
  437. package/skills/gsd-pr-branch/SKILL.md +1 -0
  438. package/skills/gsd-quick-batch/SKILL.md +105 -0
  439. package/skills/gsd-resume-work/SKILL.md +1 -0
  440. package/skills/gsd-review-backlog/SKILL.md +1 -0
  441. package/skills/gsd-settings/SKILL.md +2 -1
  442. package/skills/gsd-stats/SKILL.md +1 -0
  443. package/skills/gsd-surface/SKILL.md +18 -8
  444. package/skills/gsd-thread/SKILL.md +1 -0
  445. package/skills/gsd-workspace/SKILL.md +1 -0
  446. package/skills/gsd-workstreams/SKILL.md +1 -0
  447. package/vscode/package.json +1 -1
  448. package/gsd-core/templates/claude-md.md +0 -145
  449. package/gsd-core/templates/codebase/concerns.md +0 -310
  450. package/gsd-core/templates/codebase/conventions.md +0 -307
  451. package/gsd-core/templates/codebase/integrations.md +0 -280
  452. package/gsd-core/templates/codebase/structure.md +0 -285
  453. package/gsd-core/templates/codebase/testing.md +0 -480
  454. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  455. package/gsd-core/templates/discovery.md +0 -146
@@ -16,6 +16,8 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.STATE_MD_SECTIONS = exports.FRONTMATTER_BODY_SOURCE = exports.FIELD_CLASSIFICATION = void 0;
19
+ exports.formatProgressMachineSegment = formatProgressMachineSegment;
20
+ exports.stateReplaceProgressPercent = stateReplaceProgressPercent;
19
21
  exports.beginFrontmatterReassembly = beginFrontmatterReassembly;
20
22
  exports.getFrontmatterBodySource = getFrontmatterBodySource;
21
23
  exports.frontmatterKeyForBodyField = frontmatterKeyForBodyField;
@@ -34,10 +36,72 @@ const state_document_cjs_2 = require("./state-document.cjs");
34
36
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
35
37
  const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
36
38
  const pattern_cjs_1 = require("./pattern.cjs");
39
+ // #4129: the completion-ratio kernel for the resync-arm ratchet's percent
40
+ // (planning-scope's SCOPE — state-document's own dependency, no cycle here:
41
+ // state-document never imports this module).
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const planningScopeMod = require("./planning-scope.cjs");
37
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
38
45
  const stateMdSchemaMod = require("./state-md-schema.cjs");
39
46
  const { STATE_FIELD_SCHEMA } = stateMdSchemaMod;
40
47
  const { extractFrontmatter, reconstructFrontmatter, stripFrontmatter, FRONTMATTER_UNPARSEABLE } = frontmatter;
48
+ function formatProgressMachineSegment(percent) {
49
+ // ADR-3180 Decision 7: rounding and the 100 ceiling belong to the
50
+ // completion-ratio kernel. The floor is added here because this helper is
51
+ // also fed persisted frontmatter values (hand-editable, unlike the
52
+ // count-shaped entries into that kernel). Bar and printed percent use the
53
+ // clamped value so the two halves of the segment can never disagree.
54
+ // #4294: the CELL count is the render kernel's — `renderProgressBar` holds a
55
+ // sub-100 percent one cell short of full, so `[██████████]` beside `95%`
56
+ // cannot recur here as a seventh inline copy of the rounding.
57
+ const clamped = Math.max(0, (0, phase_lifecycle_cjs_1.clampPercentFromFraction)(percent / 100));
58
+ return `[${(0, phase_lifecycle_cjs_1.renderProgressBar)(clamped, 10)}] ${clamped}%`;
59
+ }
60
+ // Consumers (a future STATE.md writer that bypasses all three reintroduces the
61
+ // #4213 divergence class): `cmdStateUpdateProgress` and `syncCore`'s progress
62
+ // intent (both in this module) plus the post-sync body reconciliation in
63
+ // `applyPostSyncPreservation` (src/state.cts). `cmdStateSync` never reaches
64
+ // that reconciliation — ADR-3408 §8.3: `state sync` lets the body win, so
65
+ // preservation must NOT run — which is why its correctness comes from
66
+ // `syncCore`'s call here.
67
+ function stateReplaceProgressPercent(content, percent) {
68
+ const body = stripFrontmatter(content);
69
+ // #2177: bold `**Progress:**` takes priority over the plain `^Progress:`
70
+ // form, so an earlier free-text line starting with `Progress:` cannot
71
+ // capture the rewrite ahead of the real status line.
72
+ //
73
+ // #4243 (follow-up to #4453, maintainer ruling 2026-09-07): the bold form
74
+ // is also ANCHORED to line start, with same-line leading whitespace only —
75
+ // the exact idiom #4453 applied to stateReplaceField's bold branch. The
76
+ // pre-fix pattern carried no `^` and no `m` flag, so a bold percent-ish
77
+ // label quoted MID-SENTENCE inside prose (an Accumulated Context bullet
78
+ // mentioning `**Progress:**`) captured the machine-segment rewrite and
79
+ // destroyed the rest of its line, silently, while the real Progress line
80
+ // stayed stale — every caller (cmdStateUpdateProgress, syncCore's percent
81
+ // arm, applyPostSyncPreservation) feeds the whole document. #2177's own
82
+ // recorded requirements are unaffected: the frontmatter is stripped before
83
+ // matching (its defect was the YAML `progress:` key shadowing the body
84
+ // line), the suffix-preserving machine-segment swap is untouched, and the
85
+ // bold-beats-plain priority now governs LINE-START forms. The leading class
86
+ // is `[ \t]*`, deliberately NOT `\s*` — `^\s*\*\*` can consume the newlines
87
+ // before the label into the match and drop them on rebuild (#4010's
88
+ // same-line confinement hazard). `$` is explicit-and-inert (`[^\r\n]*`
89
+ // never crosses line terminators) and documents that the match ends at
90
+ // end-of-line.
91
+ const boldProgressPattern = /^([ \t]*\*\*Progress:\*\*[ \t]*)([^\r\n]*)$/im;
92
+ const plainProgressPattern = /^(Progress:[ \t]*)([^\r\n]*)/im;
93
+ const pattern = boldProgressPattern.test(body)
94
+ ? boldProgressPattern
95
+ : plainProgressPattern.test(body)
96
+ ? plainProgressPattern
97
+ : null;
98
+ if (!pattern)
99
+ return null;
100
+ const machineSegment = /(?:\[[^\]\r\n]*\][ \t]*)?\d{1,3}%/;
101
+ const progress = formatProgressMachineSegment(percent);
102
+ const updatedBody = body.replace(pattern, (_match, prefix, value) => (`${prefix}${machineSegment.test(value) ? value.replace(machineSegment, progress) : progress}`));
103
+ return content.slice(0, content.length - body.length) + updatedBody;
104
+ }
41
105
  /**
42
106
  * ADR-3473 §8.1 (#3881, consequence 2 wiring): does `existingFm` carry the
43
107
  * `FRONTMATTER_UNPARSEABLE` marker `extractFrontmatter` sets when a
@@ -468,6 +532,72 @@ function preservedValuesEqual(a, b) {
468
532
  }
469
533
  return a === b;
470
534
  }
535
+ /**
536
+ * #4129: the resync-arm progress merge. A resyncing write whose scan MEASURED
537
+ * something no longer wholesale-replaces the curated block — the declared
538
+ * `progress-ratchet` mergeStrategy ("completed_plans/completed_phases only ever
539
+ * ratchet UP toward the derived value (#2969)", state-md-schema.cts) now holds
540
+ * on the write path too, matching what the read path (`shouldPreserveExistingProgress`)
541
+ * has always enforced. Rules, mirroring the `deriveProgressKeys` branch above:
542
+ *
543
+ * - total_plans / total_phases always take the derived value (#2440 — totals
544
+ * correct in BOTH directions).
545
+ * - completed_plans / completed_phases take the derived value only when it is
546
+ * strictly GREATER (#2969's `>` not `>=`); else the curated value survives
547
+ * (a hand-corrected or previously-correct counter can never be re-derived
548
+ * downward — the #4129 clobber).
549
+ * - any other key keeps the curated value (the existing branch's convention).
550
+ * - percent is RECOMPUTED from the merged counters through the single kernel
551
+ * (`computeProgressPercent`), because either side's stored percent was
552
+ * computed against that side's counters and the merged block may mix them
553
+ * (curated completed, derived totals). Recomputation runs ONLY when the
554
+ * derived block itself carried a percent — an upstream withhold
555
+ * (#1761 milestone-unbounded, #3217 scope) nulled percent deliberately and
556
+ * this merge must not resurrect it.
557
+ *
558
+ * Frontmatter scalars arrive as STRINGS ("2", not 2), so every comparison
559
+ * coerces through `toFiniteNumber` — never a `typeof === 'number'` test
560
+ * (scanMeasuredSomething's own convention).
561
+ */
562
+ function mergeResyncProgressRatchet(curatedRecord, derivedRecord) {
563
+ const merged = { ...derivedRecord };
564
+ for (const [key, value] of Object.entries(curatedRecord)) {
565
+ if (key === 'total_plans' || key === 'total_phases' || key === 'percent')
566
+ continue;
567
+ if (key === 'completed_plans' || key === 'completed_phases') {
568
+ const derivedNum = (0, state_document_cjs_2.toFiniteNumber)(derivedRecord[key]) ?? -Infinity;
569
+ const curatedNum = (0, state_document_cjs_2.toFiniteNumber)(value) ?? -Infinity;
570
+ // Ratchet up only (strictly greater, #2969); else keep curated.
571
+ if (derivedNum > curatedNum)
572
+ continue;
573
+ // Numerically EQUAL keeps the derived value VERBATIM. The two sides
574
+ // arrive in different scalar shapes (the re-parsed derived block
575
+ // carries string totals "2" while the curated snapshot carries numbers
576
+ // 2), and substituting the curated spelling over an equal derived one
577
+ // is a no-op in substance but a shape churn the §8.7 reporting loop
578
+ // would surface as a phantom `preserved-over-disagreeing-derived`
579
+ // warning (it diffs structurally). Only a curated counter that is
580
+ // STRICTLY greater replaces the derived value.
581
+ if (derivedNum === curatedNum)
582
+ continue;
583
+ merged[key] = value;
584
+ }
585
+ else {
586
+ merged[key] = value;
587
+ }
588
+ }
589
+ if ((0, state_document_cjs_2.toFiniteNumber)(derivedRecord.percent) !== null) {
590
+ const recomputed = (0, state_document_cjs_2.computeProgressPercent)((0, state_document_cjs_2.toFiniteNumber)(merged.completed_plans), (0, state_document_cjs_2.toFiniteNumber)(merged.total_plans), (0, state_document_cjs_2.toFiniteNumber)(merged.completed_phases), (0, state_document_cjs_2.toFiniteNumber)(merged.total_phases), planningScopeMod.SCOPE.COMPLETE);
591
+ // Same verbatim rule for percent: assign only when the recomputed value
592
+ // numerically differs, so a string-spelled derived percent ("67") is not
593
+ // churned into a number-spelled 67 (phantom-divergence noise, not a
594
+ // change).
595
+ if (recomputed !== null && recomputed !== (0, state_document_cjs_2.toFiniteNumber)(merged.percent)) {
596
+ merged.percent = recomputed;
597
+ }
598
+ }
599
+ return merged;
600
+ }
471
601
  /**
472
602
  * Executor for `preservation: 'preserve-always'` (ADR-3408 §8.1). Only
473
603
  * `progress` carries this policy today. Preserves #3242/#1446/#2440/#2969
@@ -483,25 +613,39 @@ function applyPreserveAlways(field, cls, ctx) {
483
613
  const derived = ctx.postFm[field];
484
614
  const derivedMeasured = scanMeasuredSomething(cls, derived);
485
615
  const curatedMeasured = scanMeasuredSomething(cls, curated);
486
- // On a resyncing write the fresh derivation is authoritative — UNLESS it
487
- // measured nothing while the curated block did (#3756), AND the caller did
488
- // not explicitly name a progress-affecting field this write. The
489
- // unmeasured-scan guard exists to stop an INCIDENTAL resync (e.g. `state
490
- // add-decision`, whose `resync` defaults true for reasons that have
491
- // nothing to do with `progress`) from dropping a real curated block when a
492
- // milestone-scoped disk scan measures nothing (#3756's archived-milestone
493
- // case). It must not also block a write the user pointed AT `progress` on
494
- // purpose: `preserve-always`'s own contract is "never overwrite unless the
495
- // caller explicitly names this field" (FIELD_CLASSIFICATION doc comment),
496
- // and `state update Progress` / `state patch Progress=...` are exactly
497
- // that naming — the resync they trigger must win even when the disk scan
498
- // it also drives (e.g. because there are no phase dirs at all) reads as
499
- // "unmeasured" (tests/frontmatter.test.cjs: "state.update \"Progress\"
500
- // resyncs progress frontmatter from the updated body", pre-existing, #3242).
501
- if (ctx.resync && (derivedMeasured || !curatedMeasured || ctx.explicitProgressField))
616
+ // On a resyncing write the fresh derivation is authoritative in two cases
617
+ // (#3756 / ADR-3473 §8.6, unchanged): when the caller EXPLICITLY named a
618
+ // progress-affecting field (`preserve-always`'s contract is "never
619
+ // overwrite unless the caller explicitly names this field" — `state update
620
+ // Progress` is exactly that naming, pre-existing #3242 behavior), and when
621
+ // the derivation measured something the curated block did not (an
622
+ // unmeasured CURATED block is not worth protecting). The unmeasured-DERIVED
623
+ // guard also stands: an incidental resync (e.g. `state add-decision`, whose
624
+ // `resync` defaults true for reasons that have nothing to do with
625
+ // `progress`) that measured nothing must not drop a real curated block
626
+ // (#3756's archived-milestone case) — that falls through to the wholesale
627
+ // restore below.
628
+ //
629
+ // #4129 narrows the remaining arm. A resyncing write whose scan MEASURED
630
+ // something while the curated block is also real previously wholesale-
631
+ // replaced the curated block with the derived one — no monotonic guard, so
632
+ // any under-counting derivation (a stale-dated verification, #2348) silently
633
+ // reverted every hand-correction and every correct value an earlier write
634
+ // had persisted, while the read path (`shouldPreserveExistingProgress`)
635
+ // kept reporting the higher stored counters. That arm now falls through to
636
+ // `mergeResyncProgressRatchet` — the declared `progress-ratchet`
637
+ // mergeStrategy, finally enforced on the write path: totals derived both
638
+ // directions (#2440), completed counters up-only (#2969), percent
639
+ // recomputed from the merged counters.
640
+ if (ctx.resync && (ctx.explicitProgressField || (derivedMeasured && !curatedMeasured)))
502
641
  return;
503
642
  let next;
504
- if (cls.mergeStrategy === 'progress-ratchet' && ctx.deriveProgressKeys && derived && derivedMeasured) {
643
+ if (cls.mergeStrategy === 'progress-ratchet' && ctx.resync && derived && derivedMeasured && curatedMeasured) {
644
+ // #4129 resync arm — see mergeResyncProgressRatchet's doc. Reached only
645
+ // after the early-out above, so curatedMeasured is guaranteed true here.
646
+ next = mergeResyncProgressRatchet(curated, (derived ?? {}));
647
+ }
648
+ else if (cls.mergeStrategy === 'progress-ratchet' && ctx.deriveProgressKeys && derived && derivedMeasured) {
505
649
  // #2440: total_plans and total_phases always take the derived (post-sync)
506
650
  // value even under !resync. This is used by cmdStatePlannedPhase where
507
651
  // total_plans must correct upward after plans are added. For body-only
@@ -784,7 +928,17 @@ function beginPhaseCore(content, intent, deps) {
784
928
  const focusLabel = intent.phaseName
785
929
  ? `Phase ${intent.phaseNumber} — ${intent.phaseName}`
786
930
  : `Phase ${intent.phaseNumber}`;
787
- const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
931
+ // #4469: anchored to line start with same-line whitespace only, mirroring
932
+ // #4243/PR #4453's fix to stateReplaceField's bold branch. The pre-fix
933
+ // pattern carried no `^`/`m` and used `\s*` (crosses newlines), so a bold
934
+ // `**Current focus:**` quoted mid-sentence elsewhere in the body (e.g. an
935
+ // Accumulated Context bullet) matched first and had the rest of its line
936
+ // silently overwritten with the new focus label -- the same #4010
937
+ // data-loss class. `[ \t]*` (not `\s*`) avoids consuming the newlines
938
+ // before the label into the match; `$` documents the match ends at
939
+ // end-of-line (inert here since `.` never crosses line terminators
940
+ // without `/s`, which is not set).
941
+ const focusPattern = /^([ \t]*\*\*Current focus:\*\*[ \t]*)(.*)$/im;
788
942
  if (focusPattern.test(body)) {
789
943
  body = body.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
790
944
  updated.push('Current focus');
@@ -955,6 +1109,46 @@ function mutateCurrentPositionResume(body, intent, today, updated) {
955
1109
  }
956
1110
  return body.slice(0, span.start) + sectionBody + body.slice(span.end);
957
1111
  }
1112
+ const PLAN_SHAPE_N = /^(\d+)(?:\s.*)?$/;
1113
+ const PLAN_SHAPE_N_OF_M = /^(\d+)\s+of\s+(\d+)(?:\s.*)?$/;
1114
+ /**
1115
+ * Parse a decimal group into a plan number, or `null` if it is not a value we
1116
+ * are willing to do arithmetic on.
1117
+ *
1118
+ * `parseInt` is deliberately not used on the raw field: it truncates (`"2 of 5"`
1119
+ * -> 2), accepts a sign (`"+2"`), and silently loses precision past
1120
+ * `Number.MAX_SAFE_INTEGER`, where the number we report and the string we write
1121
+ * back stop agreeing. The grammars above already exclude signs and trailing
1122
+ * text, so the only remaining hazard is magnitude.
1123
+ */
1124
+ function planNumberFrom(digits) {
1125
+ const n = Number(digits);
1126
+ return Number.isSafeInteger(n) ? n : null;
1127
+ }
1128
+ /**
1129
+ * Advance the leading integer of a written plan value, preserving everything
1130
+ * the author wrote around it: the zero-padding width ("04" -> "05") and any
1131
+ * trailing remainder ("2 of 99" -> "3 of 99", and the `\r` of a CRLF file).
1132
+ *
1133
+ * The three parse branches disagree about the field NAME and about whether a
1134
+ * total is carried inline, but they agree completely about this: only the
1135
+ * leading digits are the plan number, and nothing else on the line belongs to
1136
+ * this transition. Writing `String(newPlan)` instead — as the legacy branch
1137
+ * did — discards the author's text on a branch nobody was reading.
1138
+ *
1139
+ * padStart never truncates, so 09 -> 10 widens rather than clipping.
1140
+ */
1141
+ function bumpLeadingNumber(raw, next) {
1142
+ const digits = /^\d+/.exec(raw);
1143
+ // Total rather than pass-through. `raw.replace(/^\d+/, …)` returns the input
1144
+ // unchanged when there are no leading digits, so `+2` advanced in `data` and
1145
+ // wrote the file untouched — the command reported progress it had not made
1146
+ // and could be re-run forever. The grammars make that unreachable today;
1147
+ // returning null keeps it unreachable if a fourth branch is ever added.
1148
+ if (!digits)
1149
+ return null;
1150
+ return raw.replace(/^\d+/, () => String(next).padStart(digits[0].length, '0'));
1151
+ }
958
1152
  /**
959
1153
  * Update fields within the ## Current Position section for advancePlan.
960
1154
  * Mirrors `updateCurrentPositionFields` (state.cts:496) byte-for-behaviour:
@@ -1006,19 +1200,80 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
1006
1200
  mutated = true;
1007
1201
  }
1008
1202
  }
1009
- if (fields.plan) {
1203
+ if (fields.plan || fields.currentPlan) {
1010
1204
  // Plan is always replaced — system-derived, not executor-authored.
1011
- if (/^Plan:/m.test(sectionBody)) {
1012
- sectionBody = sectionBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
1013
- mutated = true;
1014
- }
1015
- else {
1016
- const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Plan', fields.plan);
1205
+ //
1206
+ // Which NAME to write is decided by what the SECTION carries, not by which
1207
+ // header field the value was read from. Mirroring the header was wrong in
1208
+ // both directions: a legacy header with a `Current Plan:` section line left
1209
+ // the section a plan behind, and a hybrid header with a `Plan:` section line
1210
+ // mutated nothing at all. The invariant is per-name — every site spelled
1211
+ // `Current Plan` gets the `Current Plan` value, every `Plan` site gets the
1212
+ // `Plan` value — so both are passed in and each is written where its own
1213
+ // name appears.
1214
+ //
1215
+ // Title-Case LITERALS reach both the regex and stateReplaceField
1216
+ // (ADR-3408 §8.3(b)): a literal cannot collide with a lowercase/snake_case
1217
+ // frontmatter key, whatever the caller passed.
1218
+ //
1219
+ // The replacements go through a replacer FUNCTION, never a replacement
1220
+ // string. `fields.plan` is derived from file content, and `String.replace`
1221
+ // expands `$&`, `` $` `` and `$'` in a replacement string — a STATE.md
1222
+ // carrying `Current Plan: 04 of 06 $&` would splice part of itself into the
1223
+ // document. `stateReplaceField` already uses a function for this reason;
1224
+ // these arms now agree with it.
1225
+ // Each name is written INDEPENDENTLY, and each falls back on its own.
1226
+ //
1227
+ // Two defects lived in the previous shape, both of which produced the
1228
+ // split-brain document this arm exists to prevent:
1229
+ //
1230
+ // - The fallback was guarded by `!mutated`, and `mutated` is FUNCTION-wide
1231
+ // — already set by the `phase`/`status`/`lastActivity` arms above, which
1232
+ // `advancePlanCore` always populates. A section spelled `**Current
1233
+ // Plan:**` (bold) or as a pipe-table row therefore skipped its fallback
1234
+ // because an UNRELATED field had been refreshed, and the section stayed a
1235
+ // plan behind the header.
1236
+ // - The fallback then picked ONE name by ternary. In the legacy shape both
1237
+ // values are populated, so it always chose `Current Plan` and a
1238
+ // `**Plan:**` section line — which base did write — got nothing.
1239
+ //
1240
+ // `planWritten` is local, so nothing outside this arm can satisfy its guard.
1241
+ let planWritten = false;
1242
+ const writePlanField = (name, value) => {
1243
+ if (!value)
1244
+ return;
1245
+ // Plain `Name:` line first. Title-Case LITERALS reach both the regex and
1246
+ // stateReplaceField (ADR-3408 §8.3(b)), and the replacement goes through a
1247
+ // replacer FUNCTION so a `$&` / `` $` `` / `$'` in the author's text is not
1248
+ // expanded into the document.
1249
+ if (name === 'Current Plan') {
1250
+ if (/^Current Plan:/m.test(sectionBody)) {
1251
+ sectionBody = sectionBody.replace(/^Current Plan:.*$/m, () => `Current Plan: ${value}`);
1252
+ planWritten = true;
1253
+ return;
1254
+ }
1255
+ const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Current Plan', value);
1256
+ if (replaced !== null) {
1257
+ sectionBody = replaced;
1258
+ planWritten = true;
1259
+ }
1260
+ return;
1261
+ }
1262
+ if (/^Plan:/m.test(sectionBody)) {
1263
+ sectionBody = sectionBody.replace(/^Plan:.*$/m, () => `Plan: ${value}`);
1264
+ planWritten = true;
1265
+ return;
1266
+ }
1267
+ const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Plan', value);
1017
1268
  if (replaced !== null) {
1018
1269
  sectionBody = replaced;
1019
- mutated = true;
1270
+ planWritten = true;
1020
1271
  }
1021
- }
1272
+ };
1273
+ writePlanField('Current Plan', fields.currentPlan);
1274
+ writePlanField('Plan', fields.plan);
1275
+ if (planWritten)
1276
+ mutated = true;
1022
1277
  }
1023
1278
  if (!mutated)
1024
1279
  return content;
@@ -1030,8 +1285,10 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
1030
1285
  /**
1031
1286
  * Apply an `advancePlan` transition to STATE.md content.
1032
1287
  *
1033
- * Parses Current Plan / Total Plans (legacy separate fields or compound
1034
- * "Plan: X of Y" format), increments the plan number, updates body fields
1288
+ * Parses Current Plan / Total Plans in any of three shapes — the legacy
1289
+ * separate fields, the compound "Plan: X of Y", or the hybrid
1290
+ * "Current Plan: X of Y" (legacy name, compound value, no Total Plans
1291
+ * sibling) — increments the plan number, updates body fields
1035
1292
  * and the ## Current Position section. When currentPlan >= totalPlans,
1036
1293
  * takes the phase-complete branch (sets Status to "Phase complete — ready
1037
1294
  * for verification") instead of advancing.
@@ -1077,30 +1334,105 @@ function advancePlanCore(content, deps) {
1077
1334
  };
1078
1335
  }
1079
1336
  }
1080
- // Parse plan number — legacy first, then compound.
1337
+ // Parse plan number — legacy pair first, then the hybrid, then compound.
1338
+ //
1339
+ // These branches decide ONE thing: which numbers the advance is computed
1340
+ // from. They deliberately do not record which FIELD supplied them, because
1341
+ // the write path no longer asks — every spelling is written back from its own
1342
+ // raw text (#3791 review round 6, B1/M1). An earlier revision tracked a
1343
+ // `planSourceField`/`planRawValue` pair here and then wrote the OTHER
1344
+ // spelling from this one's numbers, which is precisely how a field ended up
1345
+ // holding a value nothing had derived for it.
1081
1346
  const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
1082
1347
  const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1083
1348
  const planField = (0, state_document_cjs_1.stateExtractField)(content, 'Plan');
1084
- let currentPlan;
1085
- let totalPlans;
1086
- let useCompoundFormat = false;
1087
- if (legacyPlan && legacyTotal) {
1088
- currentPlan = parseInt(legacyPlan, 10);
1089
- totalPlans = parseInt(legacyTotal, 10);
1090
- }
1091
- else if (planField) {
1092
- currentPlan = parseInt(planField, 10);
1093
- const ofMatch = planField.match(/of\s+(\d+)/);
1094
- totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
1095
- useCompoundFormat = true;
1096
- }
1097
- else {
1098
- currentPlan = NaN;
1099
- totalPlans = NaN;
1100
- }
1101
- if (isNaN(currentPlan) || isNaN(totalPlans)) {
1349
+ // Every branch below reads its numbers out of an ANCHORED match's capture
1350
+ // groups. Nothing here calls parseInt on a raw field value, so a value the
1351
+ // grammar does not fully describe cannot half-parse into a plausible number.
1352
+ const legacyNMatch = legacyPlan ? PLAN_SHAPE_N.exec(legacyPlan) : null;
1353
+ const legacyNofMMatch = legacyPlan ? PLAN_SHAPE_N_OF_M.exec(legacyPlan) : null;
1354
+ const totalNMatch = legacyTotal ? PLAN_SHAPE_N.exec(legacyTotal) : null;
1355
+ const planNofMMatch = planField ? PLAN_SHAPE_N_OF_M.exec(planField) : null;
1356
+ const planNMatch = planField ? PLAN_SHAPE_N.exec(planField) : null;
1357
+ let parsedCurrent = null;
1358
+ let parsedTotal = null;
1359
+ if (legacyPlan && legacyTotal && (legacyNMatch || legacyNofMMatch) && totalNMatch) {
1360
+ // Legacy pair wins whenever both fields are present and both are readable,
1361
+ // even if the Current Plan value also carries an "of M" — the explicit
1362
+ // sibling field is the stated intent, so it supplies the total.
1363
+ parsedCurrent = planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1]);
1364
+ parsedTotal = planNumberFrom(totalNMatch[1]);
1365
+ }
1366
+ else if (legacyNofMMatch) {
1367
+ // Hybrid: legacy field name, compound value, no readable Total Plans
1368
+ // sibling. Written by hand (and by agents) often enough to be worth
1369
+ // reading — #3784.
1370
+ parsedCurrent = planNumberFrom(legacyNofMMatch[1]);
1371
+ parsedTotal = planNumberFrom(legacyNofMMatch[2]);
1372
+ }
1373
+ else if (planNofMMatch) {
1374
+ parsedCurrent = planNumberFrom(planNofMMatch[1]);
1375
+ parsedTotal = planNumberFrom(planNofMMatch[2]);
1376
+ }
1377
+ // No branch for a bare `Plan: N` paired with a `Total Plans in Phase: M`
1378
+ // sibling and no `Current Plan` at all (#3791 review round 6, M2). A revision
1379
+ // of this PR accepted it; base did not (its `else if (planField)` arm had no
1380
+ // `of M` match and errored via NaN), and it is out of #3784's scope, which is
1381
+ // the hybrid `Current Plan: N of M`. It cannot be given the schema-row +
1382
+ // forcing-test coupling the other shapes have, either: `Plan` is body-only,
1383
+ // `buildStateFrontmatter` never reads it into frontmatter, so there is no
1384
+ // `current_*` key to hang a row on. An accepted shape with no schema row and
1385
+ // no forcing test is exactly the drift this diff is otherwise built to
1386
+ // prevent, so the shape is refused and named in the error instead.
1387
+ if (parsedCurrent === null || parsedTotal === null) {
1102
1388
  return { content: reassemble(body), updated: [], data: { error: true } };
1103
1389
  }
1390
+ const currentPlan = parsedCurrent;
1391
+ const totalPlans = parsedTotal;
1392
+ // Each SPELLING's own plan number, read from its own value (#3791 review
1393
+ // round 6, B1/M1). The parse above picks ONE field to advance FROM; these are
1394
+ // what each field independently claims, and they are the only honest basis
1395
+ // for writing that field back.
1396
+ const legacyOwnCurrent = legacyNMatch || legacyNofMMatch
1397
+ ? planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1])
1398
+ : null;
1399
+ const planOwnCurrent = planNofMMatch || planNMatch
1400
+ ? planNumberFrom((planNofMMatch ?? planNMatch)[1])
1401
+ : null;
1402
+ // A document carrying BOTH spellings with DIFFERENT plan numbers disagrees
1403
+ // with itself, and no rule here can say which half is right. Refuse.
1404
+ //
1405
+ // This is the #3807 posture one field over: name the conflict, let the caller
1406
+ // resolve it, never pick. The alternative shipped in an earlier revision of
1407
+ // this PR and was the round-6 Blocker — with `Plan` as the parse source, the
1408
+ // write path re-stamped `Current Plan`'s value with the number it had just
1409
+ // derived from `Plan`, so `Current Plan: 7` beside `Plan: 2 of 5` silently
1410
+ // became `Current Plan: 3`. A number with no relationship to the field it was
1411
+ // written into, no error, no diagnostic.
1412
+ //
1413
+ // Placed BEFORE the phase-complete branch deliberately. Guarding only the
1414
+ // normal advance leaves `Current Plan: 7` beside `Plan: 5 of 5` writing a
1415
+ // terminal "Phase complete — ready for verification" into a document whose
1416
+ // two spellings never agreed on where execution was.
1417
+ //
1418
+ // Differing TOTALS are NOT a disagreement about position and are preserved,
1419
+ // not resolved: `Current Plan: 2` / `Total Plans in Phase: 5` beside
1420
+ // `Plan: 2 of 9` advances to `3` and `3 of 9`. Reconciling the two totals
1421
+ // would be this transition inventing an answer to a question nobody asked it.
1422
+ if (legacyOwnCurrent !== null && planOwnCurrent !== null && legacyOwnCurrent !== planOwnCurrent) {
1423
+ return {
1424
+ content: reassemble(body),
1425
+ updated: [],
1426
+ data: {
1427
+ error: true,
1428
+ reason: 'ambiguous_plan_position',
1429
+ plan_candidates: [
1430
+ `Current Plan: ${legacyPlan}`,
1431
+ `Plan: ${planField}`,
1432
+ ],
1433
+ },
1434
+ };
1435
+ }
1104
1436
  const updated = [];
1105
1437
  const statusDefaults = state_document_cjs_2.KNOWN_TEMPLATE_DEFAULTS['Status'];
1106
1438
  const lastActivityDefaults = state_document_cjs_2.KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
@@ -1122,24 +1454,98 @@ function advancePlanCore(content, deps) {
1122
1454
  }
1123
1455
  // Normal advance branch.
1124
1456
  const newPlan = currentPlan + 1;
1125
- let planDisplayValue;
1126
- if (useCompoundFormat) {
1127
- planDisplayValue = planField.replace(/^\d+/, String(newPlan));
1457
+ // The value each SPELLING should carry after the advance. A document may hold
1458
+ // both names (a `Current Plan:` header and a `Plan:` line in the section, or
1459
+ // the reverse), and each has always rendered differently — the legacy field
1460
+ // holds a bare/padded number while the section's `Plan:` line holds the
1461
+ // compound `N of M`.
1462
+ //
1463
+ // Each is advanced from ITS OWN raw text, never from the other's numbers
1464
+ // (#3791 review round 6, B1/M1). `bumpLeadingNumber` replaces only the leading
1465
+ // digits, so the field's zero-padding width, its own ` of M` and any trailing
1466
+ // annotation all survive — which is what the changeset claims, and what the
1467
+ // previous revision did only for whichever field happened to be the parse
1468
+ // source. The other field it re-stamped from numbers that were never its own.
1469
+ const advanceOwn = (raw, own) => {
1470
+ if (raw === null)
1471
+ return undefined;
1472
+ // Present but unreadable (`Plan: TBD`). Leave it exactly as authored: this
1473
+ // transition cannot advance what it cannot read, and writing a derived
1474
+ // number over it is the fabrication B1 was filed for. Stale-and-untouched is
1475
+ // honest; refusing the whole document because an unrelated line is
1476
+ // unreadable would be a narrowing #3784 does not license.
1477
+ if (own === null)
1478
+ return undefined;
1479
+ return bumpLeadingNumber(raw, newPlan) ?? undefined;
1480
+ };
1481
+ // Title-Case LITERALS to stateReplaceField (ADR-3408 §8.3(b)): a literal
1482
+ // cannot collide with a lowercase/snake_case frontmatter key, so it is safe
1483
+ // regardless of how the content argument was derived. `body` here is in fact
1484
+ // `stripFrontmatter(content)`, but the write-path drift guard does not do
1485
+ // dataflow tracking (by design), and satisfying its invariant by construction
1486
+ // is better than asking a reader to re-derive that it holds.
1487
+ // One value per SPELLING, then write both names everywhere they appear.
1488
+ //
1489
+ // `Current Plan` — whatever the author wrote, advanced in place: padding
1490
+ // and any ` of M` preserved.
1491
+ // `Plan` — likewise, so its OWN total survives. `Plan: 2 of 9`
1492
+ // beside a `Total Plans in Phase: 5` advances to
1493
+ // `3 of 9`, not `3 of 5`: the two totals disagreeing is
1494
+ // the document's business, not this transition's to
1495
+ // reconcile.
1496
+ //
1497
+ // The two are deliberately different strings for the legacy shape, which is
1498
+ // why this is a per-name value rather than one shared display value. Writing
1499
+ // only the name the value was PARSED from is what left the other name stale:
1500
+ // a `**Plan:** 2 of 6` header beside a `Current Plan:` line advanced one and
1501
+ // not the other, in whichever direction the precedence happened to fall.
1502
+ //
1503
+ // Each write is a no-op when that name is absent (`stateReplaceField` returns
1504
+ // null), so a document carrying only one spelling is unaffected — and
1505
+ // `undefined` means "present but not advanceable", which is left untouched
1506
+ // rather than overwritten.
1507
+ const currentPlanDisplayValue = advanceOwn(legacyPlan, legacyOwnCurrent);
1508
+ const planDisplayValue = planField === null
1509
+ // No top-level `Plan:` field to advance, but the `## Current Position`
1510
+ // section may still carry a `Plan:` line in a shape `stateExtractField`
1511
+ // does not read. There is no raw text here to preserve, so it gets the
1512
+ // compound rendering that line has always carried.
1513
+ ? `${newPlan} of ${totalPlans}`
1514
+ : advanceOwn(planField, planOwnCurrent);
1515
+ if (currentPlanDisplayValue !== undefined) {
1516
+ body = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Plan', currentPlanDisplayValue) || body;
1517
+ }
1518
+ // Only touch `Plan` when the document actually declares one. Writing it
1519
+ // unconditionally meant a `stateReplaceField` whose first match could be any
1520
+ // `Plan:` line anywhere in the body — including prose outside
1521
+ // `## Current Position` that was never a field. `planField` is the read of
1522
+ // that same field from the top of this function, so the write is scoped to a
1523
+ // document that has one.
1524
+ if (planField !== null && planDisplayValue !== undefined) {
1128
1525
  body = (0, state_document_cjs_1.stateReplaceField)(body, 'Plan', planDisplayValue) || body;
1129
1526
  }
1130
- else {
1131
- planDisplayValue = `${newPlan} of ${totalPlans}`;
1132
- body = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Plan', String(newPlan)) || body;
1133
- }
1134
1527
  body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Status', statusDefaults, 'Ready to execute') || body;
1135
1528
  body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last Activity', lastActivityDefaults, today) || body;
1136
1529
  body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last activity', lastActivityDefaults, today) || body;
1137
1530
  body = mutateCurrentPositionForAdvance(body, {
1138
1531
  status: 'Ready to execute',
1139
1532
  lastActivity: today,
1533
+ // Both spellings, each with its own value. The section writes whichever
1534
+ // name it actually carries; passing only the header's name is what left
1535
+ // two sites disagreeing about where execution is.
1140
1536
  plan: planDisplayValue,
1537
+ currentPlan: currentPlanDisplayValue,
1141
1538
  }, statusDefaults, lastActivityDefaults);
1142
- updated.push('Current Plan', 'Status', 'Last Activity', 'Current Position');
1539
+ // Report `Current Plan` only when it actually moved. The write above is
1540
+ // conditional now — a `Current Plan:` that is present but unreadable is left
1541
+ // as authored — so an unconditional push here would report progress this
1542
+ // transition had not made, which is the same sin `bumpLeadingNumber` returns
1543
+ // null to avoid. `reconcileReportedFields` at the `state.cts` caller would
1544
+ // catch it against the persisted bytes, but `transitionCore`'s own `updated`
1545
+ // is consumed directly too and has to be true on its own.
1546
+ if (currentPlanDisplayValue !== undefined)
1547
+ updated.push('Current Plan');
1548
+ updated.push('Status', 'Last Activity', 'Current Position');
1143
1549
  return {
1144
1550
  content: reassemble(body),
1145
1551
  updated,
@@ -2004,13 +2410,10 @@ function syncCore(content, intent, deps) {
2004
2410
  if (currentProgress) {
2005
2411
  const currentPercent = parseInt(currentProgress.replace(/[^\d]/g, ''), 10);
2006
2412
  if (currentPercent !== intent.percent) {
2007
- const barWidth = 10;
2008
- const filled = Math.round((intent.percent / 100) * barWidth);
2009
- const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
2010
- const progressStr = `[${bar}] ${intent.percent}%`;
2011
- changes.push(`Progress: ${currentProgress} -> ${progressStr}`);
2012
- const result = (0, state_document_cjs_1.stateReplaceField)(modified, 'Progress', progressStr);
2413
+ const result = stateReplaceProgressPercent(modified, intent.percent);
2013
2414
  if (result) {
2415
+ const progressStr = formatProgressMachineSegment(intent.percent);
2416
+ changes.push(`Progress: ${currentProgress} -> ${progressStr}`);
2014
2417
  modified = result;
2015
2418
  updated.push('Progress');
2016
2419
  }