@opengsd/gsd-core 1.12.0 → 1.13.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 (286) 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-executor.md +63 -35
  5. package/agents/gsd-plan-checker.md +76 -57
  6. package/agents/gsd-planner.md +14 -0
  7. package/agents/gsd-ui-checker.md +19 -3
  8. package/agents/gsd-ui-researcher.md +29 -0
  9. package/agents/gsd-verifier.md +23 -1
  10. package/bin/install.js +239 -67
  11. package/commands/gsd/execute-phase.md +1 -1
  12. package/commands/gsd/ns-workflow.md +2 -1
  13. package/commands/gsd/phase.md +1 -1
  14. package/commands/gsd/quick-batch.md +105 -0
  15. package/commands/gsd/surface.md +18 -8
  16. package/gsd-core/bin/gsd-tools.cjs +195 -50
  17. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +514 -114
  19. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  20. package/gsd-core/bin/lib/capability-validator.cjs +120 -4
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  22. package/gsd-core/bin/lib/check-command-router.cjs +85 -2
  23. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  24. package/gsd-core/bin/lib/clusters.cjs +1 -0
  25. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  26. package/gsd-core/bin/lib/commands.cjs +337 -13
  27. package/gsd-core/bin/lib/config-loader.cjs +3 -0
  28. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  29. package/gsd-core/bin/lib/decisions.cjs +213 -1
  30. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  31. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  32. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  33. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  34. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  35. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  36. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  37. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  38. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  39. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  40. package/gsd-core/bin/lib/init.cjs +132 -15
  41. package/gsd-core/bin/lib/install-engine.cjs +184 -12
  42. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  43. package/gsd-core/bin/lib/install-profiles.cjs +22 -14
  44. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  45. package/gsd-core/bin/lib/io.cjs +35 -0
  46. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  47. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  48. package/gsd-core/bin/lib/milestone.cjs +22 -2
  49. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  50. package/gsd-core/bin/lib/phase-id.cjs +251 -9
  51. package/gsd-core/bin/lib/phase.cjs +774 -35
  52. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  53. package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
  54. package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
  55. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  56. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  57. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  58. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  59. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  60. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  61. package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
  62. package/gsd-core/bin/lib/roadmap.cjs +187 -58
  63. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
  64. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  65. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
  66. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
  67. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  68. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  69. package/gsd-core/bin/lib/state-document.cjs +30 -5
  70. package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
  71. package/gsd-core/bin/lib/state-transition.cjs +333 -44
  72. package/gsd-core/bin/lib/state.cjs +684 -125
  73. package/gsd-core/bin/lib/surface.cjs +23 -8
  74. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  75. package/gsd-core/bin/lib/uat.cjs +1419 -515
  76. package/gsd-core/bin/lib/update-context.cjs +6 -2
  77. package/gsd-core/bin/lib/validate.cjs +230 -12
  78. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  79. package/gsd-core/bin/lib/verification.cjs +273 -12
  80. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  81. package/gsd-core/bin/lib/verify.cjs +346 -16
  82. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
  84. package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
  85. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  86. package/gsd-core/references/agent-contracts.md +3 -3
  87. package/gsd-core/references/edge-probe.md +17 -13
  88. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  89. package/gsd-core/references/execute-phase-response-language.md +6 -0
  90. package/gsd-core/references/executor-examples.md +42 -0
  91. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  92. package/gsd-core/references/mvp-concepts.md +2 -2
  93. package/gsd-core/references/plan-checker-examples.md +41 -0
  94. package/gsd-core/references/planner-antipatterns.md +25 -0
  95. package/gsd-core/references/planner-chunked.md +5 -1
  96. package/gsd-core/references/planner-coupling.md +42 -0
  97. package/gsd-core/references/planner-quick-batch.md +71 -0
  98. package/gsd-core/references/planner-reviews.md +47 -0
  99. package/gsd-core/references/planner-revision.md +75 -2
  100. package/gsd-core/references/planning-config.md +2 -1
  101. package/gsd-core/references/response-language-directive.md +9 -0
  102. package/gsd-core/references/revision-loop.md +118 -11
  103. package/gsd-core/references/tdd.md +14 -9
  104. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  105. package/gsd-core/templates/phase-prompt.md +4 -0
  106. package/gsd-core/templates/verification-report.md +5 -0
  107. package/gsd-core/workflows/add-backlog.md +2 -0
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +1 -1
  110. package/gsd-core/workflows/add-todo.md +1 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  112. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  113. package/gsd-core/workflows/audit-fix.md +2 -0
  114. package/gsd-core/workflows/audit-milestone.md +2 -0
  115. package/gsd-core/workflows/audit-uat.md +2 -0
  116. package/gsd-core/workflows/autonomous.md +2 -0
  117. package/gsd-core/workflows/check-todos.md +1 -1
  118. package/gsd-core/workflows/cleanup.md +1 -1
  119. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
  120. package/gsd-core/workflows/code-review-fix.md +2 -0
  121. package/gsd-core/workflows/code-review.md +73 -31
  122. package/gsd-core/workflows/complete-milestone.md +13 -4
  123. package/gsd-core/workflows/debug.md +1 -1
  124. package/gsd-core/workflows/diagnose-issues.md +5 -1
  125. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  126. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  127. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  128. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  129. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  130. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  131. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  132. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  133. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  134. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  135. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  136. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  137. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  138. package/gsd-core/workflows/discuss-phase.md +1 -1
  139. package/gsd-core/workflows/do.md +43 -13
  140. package/gsd-core/workflows/docs-update.md +1 -1
  141. package/gsd-core/workflows/edit-phase.md +2 -0
  142. package/gsd-core/workflows/eval-review.md +1 -1
  143. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
  144. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
  145. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  146. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  147. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  148. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  149. package/gsd-core/workflows/execute-phase.md +32 -14
  150. package/gsd-core/workflows/execute-plan.md +8 -8
  151. package/gsd-core/workflows/explore.md +2 -0
  152. package/gsd-core/workflows/extract-learnings.md +2 -0
  153. package/gsd-core/workflows/fast.md +6 -0
  154. package/gsd-core/workflows/forensics.md +2 -0
  155. package/gsd-core/workflows/graduation.md +1 -1
  156. package/gsd-core/workflows/health.md +1 -1
  157. package/gsd-core/workflows/help/modes/brief.md +2 -0
  158. package/gsd-core/workflows/help/modes/default.md +2 -0
  159. package/gsd-core/workflows/help/modes/full.md +12 -0
  160. package/gsd-core/workflows/help/modes/topic.md +2 -0
  161. package/gsd-core/workflows/help.md +2 -0
  162. package/gsd-core/workflows/import.md +3 -3
  163. package/gsd-core/workflows/inbox.md +1 -1
  164. package/gsd-core/workflows/ingest-docs.md +1 -1
  165. package/gsd-core/workflows/insert-phase.md +2 -0
  166. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  167. package/gsd-core/workflows/list-seeds.md +2 -0
  168. package/gsd-core/workflows/list-workspaces.md +2 -0
  169. package/gsd-core/workflows/manager.md +3 -3
  170. package/gsd-core/workflows/map-codebase.md +2 -0
  171. package/gsd-core/workflows/milestone-summary.md +2 -0
  172. package/gsd-core/workflows/mvp-phase.md +1 -1
  173. package/gsd-core/workflows/new-milestone.md +1 -1
  174. package/gsd-core/workflows/new-project.md +5 -3
  175. package/gsd-core/workflows/new-workspace.md +1 -1
  176. package/gsd-core/workflows/next.md +2 -0
  177. package/gsd-core/workflows/node-repair.md +2 -0
  178. package/gsd-core/workflows/note.md +2 -0
  179. package/gsd-core/workflows/onboard.md +1 -1
  180. package/gsd-core/workflows/pause-work.md +19 -4
  181. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  182. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  183. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  184. package/gsd-core/workflows/plan-phase.md +130 -12
  185. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  186. package/gsd-core/workflows/plant-seed.md +1 -1
  187. package/gsd-core/workflows/pr-branch.md +11 -3
  188. package/gsd-core/workflows/profile-user.md +1 -1
  189. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  190. package/gsd-core/workflows/progress.md +25 -3
  191. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  192. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  193. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  194. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  195. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  196. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  197. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  198. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  199. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  200. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  201. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  202. package/gsd-core/workflows/quick-batch.md +203 -0
  203. package/gsd-core/workflows/quick.md +13 -3
  204. package/gsd-core/workflows/reapply-patches.md +2 -0
  205. package/gsd-core/workflows/remove-phase.md +2 -0
  206. package/gsd-core/workflows/remove-workspace.md +1 -1
  207. package/gsd-core/workflows/resume-project.md +6 -2
  208. package/gsd-core/workflows/review.md +215 -10
  209. package/gsd-core/workflows/scan.md +2 -0
  210. package/gsd-core/workflows/section-manifest.json +12 -0
  211. package/gsd-core/workflows/secure-phase.md +1 -1
  212. package/gsd-core/workflows/session-report.md +2 -0
  213. package/gsd-core/workflows/settings-advanced.md +2 -0
  214. package/gsd-core/workflows/settings-integrations.md +9 -8
  215. package/gsd-core/workflows/settings.md +1 -1
  216. package/gsd-core/workflows/ship.md +10 -10
  217. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  218. package/gsd-core/workflows/sketch.md +1 -1
  219. package/gsd-core/workflows/smart-entry.md +1 -1
  220. package/gsd-core/workflows/spec-phase.md +24 -19
  221. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  222. package/gsd-core/workflows/spike.md +1 -1
  223. package/gsd-core/workflows/stats.md +2 -0
  224. package/gsd-core/workflows/sync-skills.md +12 -4
  225. package/gsd-core/workflows/thread.md +2 -0
  226. package/gsd-core/workflows/transition.md +2 -0
  227. package/gsd-core/workflows/ui-phase.md +26 -5
  228. package/gsd-core/workflows/ui-review.md +1 -1
  229. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  230. package/gsd-core/workflows/undo.md +1 -1
  231. package/gsd-core/workflows/update.md +41 -38
  232. package/gsd-core/workflows/validate-phase.md +1 -1
  233. package/gsd-core/workflows/verify-work.md +49 -3
  234. package/hooks/dist/gsd-check-update-worker.js +19 -2
  235. package/hooks/dist/gsd-context-monitor.js +283 -12
  236. package/hooks/dist/gsd-node-runner.sh +1 -0
  237. package/hooks/dist/gsd-prompt-guard.js +30 -5
  238. package/hooks/dist/gsd-read-guard.js +2 -0
  239. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  240. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  241. package/hooks/dist/gsd-statusline.js +7 -3
  242. package/hooks/dist/gsd-validate-commit.sh +444 -7
  243. package/hooks/dist/gsd-workflow-guard.js +2 -1
  244. package/hooks/dist/lib/git-cmd.js +210 -1
  245. package/hooks/dist/lib/injection-patterns.js +36 -6
  246. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  247. package/hooks/gsd-check-update-worker.js +19 -2
  248. package/hooks/gsd-context-monitor.js +283 -12
  249. package/hooks/gsd-node-runner.sh +1 -0
  250. package/hooks/gsd-prompt-guard.js +30 -5
  251. package/hooks/gsd-read-guard.js +2 -0
  252. package/hooks/gsd-read-injection-scanner.js +5 -5
  253. package/hooks/gsd-secret-read-guard.js +1079 -0
  254. package/hooks/gsd-statusline.js +7 -3
  255. package/hooks/gsd-validate-commit.sh +444 -7
  256. package/hooks/gsd-workflow-guard.js +2 -1
  257. package/hooks/hooks.json +6 -0
  258. package/hooks/lib/git-cmd.js +210 -1
  259. package/hooks/lib/injection-patterns.js +36 -6
  260. package/hooks/managed-hooks-registry.cjs +1 -0
  261. package/package.json +5 -5
  262. package/scripts/build-hooks.js +11 -4
  263. package/scripts/ci-test-scope.cjs +7 -0
  264. package/scripts/docs-guard-registry.cjs +10 -0
  265. package/scripts/gen-loop-host-contract.cjs +67 -15
  266. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  267. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  268. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  269. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  270. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
  271. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  272. package/scripts/lint-phase-id-drift.cjs +133 -8
  273. package/scripts/lint-portable-grep.cjs +176 -0
  274. package/scripts/lint-response-language-coverage.cjs +524 -0
  275. package/scripts/lint-test-file-count.allowlist.json +3 -1
  276. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  277. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  278. package/scripts/npm-audit-baseline.cjs +376 -0
  279. package/scripts/prompt-injection-scan.sh +8 -0
  280. package/scripts/require-issue-link-policy.cjs +16 -1
  281. package/skills/gsd-execute-phase/SKILL.md +1 -1
  282. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  283. package/skills/gsd-phase/SKILL.md +1 -1
  284. package/skills/gsd-quick-batch/SKILL.md +105 -0
  285. package/skills/gsd-surface/SKILL.md +18 -8
  286. package/vscode/package.json +1 -1
@@ -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;
@@ -38,6 +40,44 @@ const pattern_cjs_1 = require("./pattern.cjs");
38
40
  const stateMdSchemaMod = require("./state-md-schema.cjs");
39
41
  const { STATE_FIELD_SCHEMA } = stateMdSchemaMod;
40
42
  const { extractFrontmatter, reconstructFrontmatter, stripFrontmatter, FRONTMATTER_UNPARSEABLE } = frontmatter;
43
+ function formatProgressMachineSegment(percent) {
44
+ // ADR-3180 Decision 7: rounding and the 100 ceiling belong to the
45
+ // completion-ratio kernel. The floor is added here because this helper is
46
+ // also fed persisted frontmatter values (hand-editable, unlike the
47
+ // count-shaped entries into that kernel), and `'░'.repeat` throws on a
48
+ // negative count. Bar and printed percent use the clamped value so the two
49
+ // halves of the segment can never disagree.
50
+ const clamped = Math.max(0, (0, phase_lifecycle_cjs_1.clampPercentFromFraction)(percent / 100));
51
+ const filled = Math.round(clamped / 10);
52
+ return `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}] ${clamped}%`;
53
+ }
54
+ // Consumers (a future STATE.md writer that bypasses all three reintroduces the
55
+ // #4213 divergence class): `cmdStateUpdateProgress` and `syncCore`'s progress
56
+ // intent (both in this module) plus the post-sync body reconciliation in
57
+ // `applyPostSyncPreservation` (src/state.cts). `cmdStateSync` never reaches
58
+ // that reconciliation — ADR-3408 §8.3: `state sync` lets the body win, so
59
+ // preservation must NOT run — which is why its correctness comes from
60
+ // `syncCore`'s call here.
61
+ function stateReplaceProgressPercent(content, percent) {
62
+ const body = stripFrontmatter(content);
63
+ // #2177: bold `**Progress:**` anywhere in the body wins outright; the plain
64
+ // `^Progress:` form is the fallback only when no bold line exists, so an
65
+ // earlier free-text line starting with `Progress:` cannot capture the
66
+ // rewrite ahead of the real status line.
67
+ const boldProgressPattern = /(\*\*Progress:\*\*[ \t]*)([^\r\n]*)/i;
68
+ const plainProgressPattern = /^(Progress:[ \t]*)([^\r\n]*)/im;
69
+ const pattern = boldProgressPattern.test(body)
70
+ ? boldProgressPattern
71
+ : plainProgressPattern.test(body)
72
+ ? plainProgressPattern
73
+ : null;
74
+ if (!pattern)
75
+ return null;
76
+ const machineSegment = /(?:\[[^\]\r\n]*\][ \t]*)?\d{1,3}%/;
77
+ const progress = formatProgressMachineSegment(percent);
78
+ const updatedBody = body.replace(pattern, (_match, prefix, value) => (`${prefix}${machineSegment.test(value) ? value.replace(machineSegment, progress) : progress}`));
79
+ return content.slice(0, content.length - body.length) + updatedBody;
80
+ }
41
81
  /**
42
82
  * ADR-3473 §8.1 (#3881, consequence 2 wiring): does `existingFm` carry the
43
83
  * `FRONTMATTER_UNPARSEABLE` marker `extractFrontmatter` sets when a
@@ -955,6 +995,46 @@ function mutateCurrentPositionResume(body, intent, today, updated) {
955
995
  }
956
996
  return body.slice(0, span.start) + sectionBody + body.slice(span.end);
957
997
  }
998
+ const PLAN_SHAPE_N = /^(\d+)(?:\s.*)?$/;
999
+ const PLAN_SHAPE_N_OF_M = /^(\d+)\s+of\s+(\d+)(?:\s.*)?$/;
1000
+ /**
1001
+ * Parse a decimal group into a plan number, or `null` if it is not a value we
1002
+ * are willing to do arithmetic on.
1003
+ *
1004
+ * `parseInt` is deliberately not used on the raw field: it truncates (`"2 of 5"`
1005
+ * -> 2), accepts a sign (`"+2"`), and silently loses precision past
1006
+ * `Number.MAX_SAFE_INTEGER`, where the number we report and the string we write
1007
+ * back stop agreeing. The grammars above already exclude signs and trailing
1008
+ * text, so the only remaining hazard is magnitude.
1009
+ */
1010
+ function planNumberFrom(digits) {
1011
+ const n = Number(digits);
1012
+ return Number.isSafeInteger(n) ? n : null;
1013
+ }
1014
+ /**
1015
+ * Advance the leading integer of a written plan value, preserving everything
1016
+ * the author wrote around it: the zero-padding width ("04" -> "05") and any
1017
+ * trailing remainder ("2 of 99" -> "3 of 99", and the `\r` of a CRLF file).
1018
+ *
1019
+ * The three parse branches disagree about the field NAME and about whether a
1020
+ * total is carried inline, but they agree completely about this: only the
1021
+ * leading digits are the plan number, and nothing else on the line belongs to
1022
+ * this transition. Writing `String(newPlan)` instead — as the legacy branch
1023
+ * did — discards the author's text on a branch nobody was reading.
1024
+ *
1025
+ * padStart never truncates, so 09 -> 10 widens rather than clipping.
1026
+ */
1027
+ function bumpLeadingNumber(raw, next) {
1028
+ const digits = /^\d+/.exec(raw);
1029
+ // Total rather than pass-through. `raw.replace(/^\d+/, …)` returns the input
1030
+ // unchanged when there are no leading digits, so `+2` advanced in `data` and
1031
+ // wrote the file untouched — the command reported progress it had not made
1032
+ // and could be re-run forever. The grammars make that unreachable today;
1033
+ // returning null keeps it unreachable if a fourth branch is ever added.
1034
+ if (!digits)
1035
+ return null;
1036
+ return raw.replace(/^\d+/, () => String(next).padStart(digits[0].length, '0'));
1037
+ }
958
1038
  /**
959
1039
  * Update fields within the ## Current Position section for advancePlan.
960
1040
  * Mirrors `updateCurrentPositionFields` (state.cts:496) byte-for-behaviour:
@@ -1006,19 +1086,80 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
1006
1086
  mutated = true;
1007
1087
  }
1008
1088
  }
1009
- if (fields.plan) {
1089
+ if (fields.plan || fields.currentPlan) {
1010
1090
  // 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);
1091
+ //
1092
+ // Which NAME to write is decided by what the SECTION carries, not by which
1093
+ // header field the value was read from. Mirroring the header was wrong in
1094
+ // both directions: a legacy header with a `Current Plan:` section line left
1095
+ // the section a plan behind, and a hybrid header with a `Plan:` section line
1096
+ // mutated nothing at all. The invariant is per-name — every site spelled
1097
+ // `Current Plan` gets the `Current Plan` value, every `Plan` site gets the
1098
+ // `Plan` value — so both are passed in and each is written where its own
1099
+ // name appears.
1100
+ //
1101
+ // Title-Case LITERALS reach both the regex and stateReplaceField
1102
+ // (ADR-3408 §8.3(b)): a literal cannot collide with a lowercase/snake_case
1103
+ // frontmatter key, whatever the caller passed.
1104
+ //
1105
+ // The replacements go through a replacer FUNCTION, never a replacement
1106
+ // string. `fields.plan` is derived from file content, and `String.replace`
1107
+ // expands `$&`, `` $` `` and `$'` in a replacement string — a STATE.md
1108
+ // carrying `Current Plan: 04 of 06 $&` would splice part of itself into the
1109
+ // document. `stateReplaceField` already uses a function for this reason;
1110
+ // these arms now agree with it.
1111
+ // Each name is written INDEPENDENTLY, and each falls back on its own.
1112
+ //
1113
+ // Two defects lived in the previous shape, both of which produced the
1114
+ // split-brain document this arm exists to prevent:
1115
+ //
1116
+ // - The fallback was guarded by `!mutated`, and `mutated` is FUNCTION-wide
1117
+ // — already set by the `phase`/`status`/`lastActivity` arms above, which
1118
+ // `advancePlanCore` always populates. A section spelled `**Current
1119
+ // Plan:**` (bold) or as a pipe-table row therefore skipped its fallback
1120
+ // because an UNRELATED field had been refreshed, and the section stayed a
1121
+ // plan behind the header.
1122
+ // - The fallback then picked ONE name by ternary. In the legacy shape both
1123
+ // values are populated, so it always chose `Current Plan` and a
1124
+ // `**Plan:**` section line — which base did write — got nothing.
1125
+ //
1126
+ // `planWritten` is local, so nothing outside this arm can satisfy its guard.
1127
+ let planWritten = false;
1128
+ const writePlanField = (name, value) => {
1129
+ if (!value)
1130
+ return;
1131
+ // Plain `Name:` line first. Title-Case LITERALS reach both the regex and
1132
+ // stateReplaceField (ADR-3408 §8.3(b)), and the replacement goes through a
1133
+ // replacer FUNCTION so a `$&` / `` $` `` / `$'` in the author's text is not
1134
+ // expanded into the document.
1135
+ if (name === 'Current Plan') {
1136
+ if (/^Current Plan:/m.test(sectionBody)) {
1137
+ sectionBody = sectionBody.replace(/^Current Plan:.*$/m, () => `Current Plan: ${value}`);
1138
+ planWritten = true;
1139
+ return;
1140
+ }
1141
+ const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Current Plan', value);
1142
+ if (replaced !== null) {
1143
+ sectionBody = replaced;
1144
+ planWritten = true;
1145
+ }
1146
+ return;
1147
+ }
1148
+ if (/^Plan:/m.test(sectionBody)) {
1149
+ sectionBody = sectionBody.replace(/^Plan:.*$/m, () => `Plan: ${value}`);
1150
+ planWritten = true;
1151
+ return;
1152
+ }
1153
+ const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Plan', value);
1017
1154
  if (replaced !== null) {
1018
1155
  sectionBody = replaced;
1019
- mutated = true;
1156
+ planWritten = true;
1020
1157
  }
1021
- }
1158
+ };
1159
+ writePlanField('Current Plan', fields.currentPlan);
1160
+ writePlanField('Plan', fields.plan);
1161
+ if (planWritten)
1162
+ mutated = true;
1022
1163
  }
1023
1164
  if (!mutated)
1024
1165
  return content;
@@ -1030,8 +1171,10 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
1030
1171
  /**
1031
1172
  * Apply an `advancePlan` transition to STATE.md content.
1032
1173
  *
1033
- * Parses Current Plan / Total Plans (legacy separate fields or compound
1034
- * "Plan: X of Y" format), increments the plan number, updates body fields
1174
+ * Parses Current Plan / Total Plans in any of three shapes — the legacy
1175
+ * separate fields, the compound "Plan: X of Y", or the hybrid
1176
+ * "Current Plan: X of Y" (legacy name, compound value, no Total Plans
1177
+ * sibling) — increments the plan number, updates body fields
1035
1178
  * and the ## Current Position section. When currentPlan >= totalPlans,
1036
1179
  * takes the phase-complete branch (sets Status to "Phase complete — ready
1037
1180
  * for verification") instead of advancing.
@@ -1077,30 +1220,105 @@ function advancePlanCore(content, deps) {
1077
1220
  };
1078
1221
  }
1079
1222
  }
1080
- // Parse plan number — legacy first, then compound.
1223
+ // Parse plan number — legacy pair first, then the hybrid, then compound.
1224
+ //
1225
+ // These branches decide ONE thing: which numbers the advance is computed
1226
+ // from. They deliberately do not record which FIELD supplied them, because
1227
+ // the write path no longer asks — every spelling is written back from its own
1228
+ // raw text (#3791 review round 6, B1/M1). An earlier revision tracked a
1229
+ // `planSourceField`/`planRawValue` pair here and then wrote the OTHER
1230
+ // spelling from this one's numbers, which is precisely how a field ended up
1231
+ // holding a value nothing had derived for it.
1081
1232
  const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
1082
1233
  const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
1083
1234
  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)) {
1235
+ // Every branch below reads its numbers out of an ANCHORED match's capture
1236
+ // groups. Nothing here calls parseInt on a raw field value, so a value the
1237
+ // grammar does not fully describe cannot half-parse into a plausible number.
1238
+ const legacyNMatch = legacyPlan ? PLAN_SHAPE_N.exec(legacyPlan) : null;
1239
+ const legacyNofMMatch = legacyPlan ? PLAN_SHAPE_N_OF_M.exec(legacyPlan) : null;
1240
+ const totalNMatch = legacyTotal ? PLAN_SHAPE_N.exec(legacyTotal) : null;
1241
+ const planNofMMatch = planField ? PLAN_SHAPE_N_OF_M.exec(planField) : null;
1242
+ const planNMatch = planField ? PLAN_SHAPE_N.exec(planField) : null;
1243
+ let parsedCurrent = null;
1244
+ let parsedTotal = null;
1245
+ if (legacyPlan && legacyTotal && (legacyNMatch || legacyNofMMatch) && totalNMatch) {
1246
+ // Legacy pair wins whenever both fields are present and both are readable,
1247
+ // even if the Current Plan value also carries an "of M" — the explicit
1248
+ // sibling field is the stated intent, so it supplies the total.
1249
+ parsedCurrent = planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1]);
1250
+ parsedTotal = planNumberFrom(totalNMatch[1]);
1251
+ }
1252
+ else if (legacyNofMMatch) {
1253
+ // Hybrid: legacy field name, compound value, no readable Total Plans
1254
+ // sibling. Written by hand (and by agents) often enough to be worth
1255
+ // reading — #3784.
1256
+ parsedCurrent = planNumberFrom(legacyNofMMatch[1]);
1257
+ parsedTotal = planNumberFrom(legacyNofMMatch[2]);
1258
+ }
1259
+ else if (planNofMMatch) {
1260
+ parsedCurrent = planNumberFrom(planNofMMatch[1]);
1261
+ parsedTotal = planNumberFrom(planNofMMatch[2]);
1262
+ }
1263
+ // No branch for a bare `Plan: N` paired with a `Total Plans in Phase: M`
1264
+ // sibling and no `Current Plan` at all (#3791 review round 6, M2). A revision
1265
+ // of this PR accepted it; base did not (its `else if (planField)` arm had no
1266
+ // `of M` match and errored via NaN), and it is out of #3784's scope, which is
1267
+ // the hybrid `Current Plan: N of M`. It cannot be given the schema-row +
1268
+ // forcing-test coupling the other shapes have, either: `Plan` is body-only,
1269
+ // `buildStateFrontmatter` never reads it into frontmatter, so there is no
1270
+ // `current_*` key to hang a row on. An accepted shape with no schema row and
1271
+ // no forcing test is exactly the drift this diff is otherwise built to
1272
+ // prevent, so the shape is refused and named in the error instead.
1273
+ if (parsedCurrent === null || parsedTotal === null) {
1102
1274
  return { content: reassemble(body), updated: [], data: { error: true } };
1103
1275
  }
1276
+ const currentPlan = parsedCurrent;
1277
+ const totalPlans = parsedTotal;
1278
+ // Each SPELLING's own plan number, read from its own value (#3791 review
1279
+ // round 6, B1/M1). The parse above picks ONE field to advance FROM; these are
1280
+ // what each field independently claims, and they are the only honest basis
1281
+ // for writing that field back.
1282
+ const legacyOwnCurrent = legacyNMatch || legacyNofMMatch
1283
+ ? planNumberFrom((legacyNMatch ?? legacyNofMMatch)[1])
1284
+ : null;
1285
+ const planOwnCurrent = planNofMMatch || planNMatch
1286
+ ? planNumberFrom((planNofMMatch ?? planNMatch)[1])
1287
+ : null;
1288
+ // A document carrying BOTH spellings with DIFFERENT plan numbers disagrees
1289
+ // with itself, and no rule here can say which half is right. Refuse.
1290
+ //
1291
+ // This is the #3807 posture one field over: name the conflict, let the caller
1292
+ // resolve it, never pick. The alternative shipped in an earlier revision of
1293
+ // this PR and was the round-6 Blocker — with `Plan` as the parse source, the
1294
+ // write path re-stamped `Current Plan`'s value with the number it had just
1295
+ // derived from `Plan`, so `Current Plan: 7` beside `Plan: 2 of 5` silently
1296
+ // became `Current Plan: 3`. A number with no relationship to the field it was
1297
+ // written into, no error, no diagnostic.
1298
+ //
1299
+ // Placed BEFORE the phase-complete branch deliberately. Guarding only the
1300
+ // normal advance leaves `Current Plan: 7` beside `Plan: 5 of 5` writing a
1301
+ // terminal "Phase complete — ready for verification" into a document whose
1302
+ // two spellings never agreed on where execution was.
1303
+ //
1304
+ // Differing TOTALS are NOT a disagreement about position and are preserved,
1305
+ // not resolved: `Current Plan: 2` / `Total Plans in Phase: 5` beside
1306
+ // `Plan: 2 of 9` advances to `3` and `3 of 9`. Reconciling the two totals
1307
+ // would be this transition inventing an answer to a question nobody asked it.
1308
+ if (legacyOwnCurrent !== null && planOwnCurrent !== null && legacyOwnCurrent !== planOwnCurrent) {
1309
+ return {
1310
+ content: reassemble(body),
1311
+ updated: [],
1312
+ data: {
1313
+ error: true,
1314
+ reason: 'ambiguous_plan_position',
1315
+ plan_candidates: [
1316
+ `Current Plan: ${legacyPlan}`,
1317
+ `Plan: ${planField}`,
1318
+ ],
1319
+ },
1320
+ };
1321
+ }
1104
1322
  const updated = [];
1105
1323
  const statusDefaults = state_document_cjs_2.KNOWN_TEMPLATE_DEFAULTS['Status'];
1106
1324
  const lastActivityDefaults = state_document_cjs_2.KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
@@ -1122,24 +1340,98 @@ function advancePlanCore(content, deps) {
1122
1340
  }
1123
1341
  // Normal advance branch.
1124
1342
  const newPlan = currentPlan + 1;
1125
- let planDisplayValue;
1126
- if (useCompoundFormat) {
1127
- planDisplayValue = planField.replace(/^\d+/, String(newPlan));
1343
+ // The value each SPELLING should carry after the advance. A document may hold
1344
+ // both names (a `Current Plan:` header and a `Plan:` line in the section, or
1345
+ // the reverse), and each has always rendered differently — the legacy field
1346
+ // holds a bare/padded number while the section's `Plan:` line holds the
1347
+ // compound `N of M`.
1348
+ //
1349
+ // Each is advanced from ITS OWN raw text, never from the other's numbers
1350
+ // (#3791 review round 6, B1/M1). `bumpLeadingNumber` replaces only the leading
1351
+ // digits, so the field's zero-padding width, its own ` of M` and any trailing
1352
+ // annotation all survive — which is what the changeset claims, and what the
1353
+ // previous revision did only for whichever field happened to be the parse
1354
+ // source. The other field it re-stamped from numbers that were never its own.
1355
+ const advanceOwn = (raw, own) => {
1356
+ if (raw === null)
1357
+ return undefined;
1358
+ // Present but unreadable (`Plan: TBD`). Leave it exactly as authored: this
1359
+ // transition cannot advance what it cannot read, and writing a derived
1360
+ // number over it is the fabrication B1 was filed for. Stale-and-untouched is
1361
+ // honest; refusing the whole document because an unrelated line is
1362
+ // unreadable would be a narrowing #3784 does not license.
1363
+ if (own === null)
1364
+ return undefined;
1365
+ return bumpLeadingNumber(raw, newPlan) ?? undefined;
1366
+ };
1367
+ // Title-Case LITERALS to stateReplaceField (ADR-3408 §8.3(b)): a literal
1368
+ // cannot collide with a lowercase/snake_case frontmatter key, so it is safe
1369
+ // regardless of how the content argument was derived. `body` here is in fact
1370
+ // `stripFrontmatter(content)`, but the write-path drift guard does not do
1371
+ // dataflow tracking (by design), and satisfying its invariant by construction
1372
+ // is better than asking a reader to re-derive that it holds.
1373
+ // One value per SPELLING, then write both names everywhere they appear.
1374
+ //
1375
+ // `Current Plan` — whatever the author wrote, advanced in place: padding
1376
+ // and any ` of M` preserved.
1377
+ // `Plan` — likewise, so its OWN total survives. `Plan: 2 of 9`
1378
+ // beside a `Total Plans in Phase: 5` advances to
1379
+ // `3 of 9`, not `3 of 5`: the two totals disagreeing is
1380
+ // the document's business, not this transition's to
1381
+ // reconcile.
1382
+ //
1383
+ // The two are deliberately different strings for the legacy shape, which is
1384
+ // why this is a per-name value rather than one shared display value. Writing
1385
+ // only the name the value was PARSED from is what left the other name stale:
1386
+ // a `**Plan:** 2 of 6` header beside a `Current Plan:` line advanced one and
1387
+ // not the other, in whichever direction the precedence happened to fall.
1388
+ //
1389
+ // Each write is a no-op when that name is absent (`stateReplaceField` returns
1390
+ // null), so a document carrying only one spelling is unaffected — and
1391
+ // `undefined` means "present but not advanceable", which is left untouched
1392
+ // rather than overwritten.
1393
+ const currentPlanDisplayValue = advanceOwn(legacyPlan, legacyOwnCurrent);
1394
+ const planDisplayValue = planField === null
1395
+ // No top-level `Plan:` field to advance, but the `## Current Position`
1396
+ // section may still carry a `Plan:` line in a shape `stateExtractField`
1397
+ // does not read. There is no raw text here to preserve, so it gets the
1398
+ // compound rendering that line has always carried.
1399
+ ? `${newPlan} of ${totalPlans}`
1400
+ : advanceOwn(planField, planOwnCurrent);
1401
+ if (currentPlanDisplayValue !== undefined) {
1402
+ body = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Plan', currentPlanDisplayValue) || body;
1403
+ }
1404
+ // Only touch `Plan` when the document actually declares one. Writing it
1405
+ // unconditionally meant a `stateReplaceField` whose first match could be any
1406
+ // `Plan:` line anywhere in the body — including prose outside
1407
+ // `## Current Position` that was never a field. `planField` is the read of
1408
+ // that same field from the top of this function, so the write is scoped to a
1409
+ // document that has one.
1410
+ if (planField !== null && planDisplayValue !== undefined) {
1128
1411
  body = (0, state_document_cjs_1.stateReplaceField)(body, 'Plan', planDisplayValue) || body;
1129
1412
  }
1130
- else {
1131
- planDisplayValue = `${newPlan} of ${totalPlans}`;
1132
- body = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Plan', String(newPlan)) || body;
1133
- }
1134
1413
  body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Status', statusDefaults, 'Ready to execute') || body;
1135
1414
  body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last Activity', lastActivityDefaults, today) || body;
1136
1415
  body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last activity', lastActivityDefaults, today) || body;
1137
1416
  body = mutateCurrentPositionForAdvance(body, {
1138
1417
  status: 'Ready to execute',
1139
1418
  lastActivity: today,
1419
+ // Both spellings, each with its own value. The section writes whichever
1420
+ // name it actually carries; passing only the header's name is what left
1421
+ // two sites disagreeing about where execution is.
1140
1422
  plan: planDisplayValue,
1423
+ currentPlan: currentPlanDisplayValue,
1141
1424
  }, statusDefaults, lastActivityDefaults);
1142
- updated.push('Current Plan', 'Status', 'Last Activity', 'Current Position');
1425
+ // Report `Current Plan` only when it actually moved. The write above is
1426
+ // conditional now — a `Current Plan:` that is present but unreadable is left
1427
+ // as authored — so an unconditional push here would report progress this
1428
+ // transition had not made, which is the same sin `bumpLeadingNumber` returns
1429
+ // null to avoid. `reconcileReportedFields` at the `state.cts` caller would
1430
+ // catch it against the persisted bytes, but `transitionCore`'s own `updated`
1431
+ // is consumed directly too and has to be true on its own.
1432
+ if (currentPlanDisplayValue !== undefined)
1433
+ updated.push('Current Plan');
1434
+ updated.push('Status', 'Last Activity', 'Current Position');
1143
1435
  return {
1144
1436
  content: reassemble(body),
1145
1437
  updated,
@@ -2004,13 +2296,10 @@ function syncCore(content, intent, deps) {
2004
2296
  if (currentProgress) {
2005
2297
  const currentPercent = parseInt(currentProgress.replace(/[^\d]/g, ''), 10);
2006
2298
  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);
2299
+ const result = stateReplaceProgressPercent(modified, intent.percent);
2013
2300
  if (result) {
2301
+ const progressStr = formatProgressMachineSegment(intent.percent);
2302
+ changes.push(`Progress: ${currentProgress} -> ${progressStr}`);
2014
2303
  modified = result;
2015
2304
  updated.push('Progress');
2016
2305
  }