@opengsd/gsd-core 1.13.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 (257) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-advisor-researcher.compact.md +85 -0
  4. package/agents/gsd-ai-researcher.compact.md +96 -0
  5. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  6. package/agents/gsd-code-fixer.compact.md +458 -0
  7. package/agents/gsd-code-fixer.md +5 -5
  8. package/agents/gsd-code-reviewer.compact.md +269 -0
  9. package/agents/gsd-code-reviewer.md +15 -3
  10. package/agents/gsd-codebase-mapper.compact.md +760 -0
  11. package/agents/gsd-debug-session-manager.compact.md +345 -0
  12. package/agents/gsd-doc-classifier.compact.md +192 -0
  13. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  14. package/agents/gsd-doc-verifier.compact.md +143 -0
  15. package/agents/gsd-doc-writer.compact.md +440 -0
  16. package/agents/gsd-dom-verifier.compact.md +138 -0
  17. package/agents/gsd-domain-researcher.compact.md +141 -0
  18. package/agents/gsd-eval-auditor.compact.md +160 -0
  19. package/agents/gsd-eval-planner.compact.md +137 -0
  20. package/agents/gsd-framework-selector.compact.md +82 -0
  21. package/agents/gsd-integration-checker.compact.md +245 -0
  22. package/agents/gsd-intel-updater.compact.md +226 -0
  23. package/agents/gsd-mempalace-curator.compact.md +45 -0
  24. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  25. package/agents/gsd-pattern-mapper.compact.md +275 -0
  26. package/agents/gsd-project-researcher.compact.md +587 -0
  27. package/agents/gsd-research-synthesizer.compact.md +212 -0
  28. package/agents/gsd-roadmapper.compact.md +454 -0
  29. package/agents/gsd-roadmapper.md +13 -0
  30. package/agents/gsd-security-auditor.compact.md +162 -0
  31. package/agents/gsd-ui-auditor.compact.md +404 -0
  32. package/agents/gsd-ui-checker.compact.md +277 -0
  33. package/agents/gsd-ui-researcher.compact.md +282 -0
  34. package/agents/gsd-user-profiler.compact.md +108 -0
  35. package/bin/install.js +206 -68
  36. package/commands/gsd/cleanup.md +1 -0
  37. package/commands/gsd/code-review.md +2 -1
  38. package/commands/gsd/complete-milestone.md +1 -0
  39. package/commands/gsd/config.md +1 -0
  40. package/commands/gsd/debug.md +1 -0
  41. package/commands/gsd/graphify.md +1 -0
  42. package/commands/gsd/health.md +1 -0
  43. package/commands/gsd/mempalace-capture.md +1 -0
  44. package/commands/gsd/mempalace-recall.md +1 -0
  45. package/commands/gsd/new-milestone.md +1 -0
  46. package/commands/gsd/new-project.md +1 -0
  47. package/commands/gsd/next.md +1 -0
  48. package/commands/gsd/pause-work.md +1 -0
  49. package/commands/gsd/phase.md +1 -0
  50. package/commands/gsd/pr-branch.md +1 -0
  51. package/commands/gsd/resume-work.md +1 -0
  52. package/commands/gsd/review-backlog.md +1 -0
  53. package/commands/gsd/settings.md +2 -1
  54. package/commands/gsd/stats.md +1 -0
  55. package/commands/gsd/thread.md +1 -0
  56. package/commands/gsd/workspace.md +1 -0
  57. package/commands/gsd/workstreams.md +1 -0
  58. package/gsd-core/bin/check-latest-version.cjs +8 -3
  59. package/gsd-core/bin/gsd-tools.cjs +338 -125
  60. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  61. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  62. package/gsd-core/bin/lib/audit.cjs +39 -22
  63. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  64. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  65. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  66. package/gsd-core/bin/lib/capability-registry.cjs +79 -67
  67. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  68. package/gsd-core/bin/lib/capability-validator.cjs +14 -1
  69. package/gsd-core/bin/lib/check-command-router.cjs +113 -36
  70. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  71. package/gsd-core/bin/lib/commands.cjs +650 -72
  72. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  73. package/gsd-core/bin/lib/config.cjs +153 -38
  74. package/gsd-core/bin/lib/coverage.cjs +1 -1
  75. package/gsd-core/bin/lib/decisions.cjs +137 -34
  76. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  77. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  78. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  79. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  80. package/gsd-core/bin/lib/init.cjs +409 -47
  81. package/gsd-core/bin/lib/install-engine.cjs +16 -3
  82. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  83. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  84. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  85. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  86. package/gsd-core/bin/lib/milestone.cjs +19 -8
  87. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  88. package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +161 -22
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  91. package/gsd-core/bin/lib/phase.cjs +167 -63
  92. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  93. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  94. package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
  95. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  96. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  97. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  98. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  99. package/gsd-core/bin/lib/research-store.cjs +11 -12
  100. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  101. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  102. package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
  103. package/gsd-core/bin/lib/roadmap.cjs +108 -14
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
  105. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
  106. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
  108. package/gsd-core/bin/lib/security.cjs +126 -7
  109. package/gsd-core/bin/lib/state-document.cjs +130 -28
  110. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  111. package/gsd-core/bin/lib/state-transition.cjs +142 -28
  112. package/gsd-core/bin/lib/state.cjs +223 -27
  113. package/gsd-core/bin/lib/surface.cjs +60 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  115. package/gsd-core/bin/lib/uat.cjs +1 -1
  116. package/gsd-core/bin/lib/update-context.cjs +30 -24
  117. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  118. package/gsd-core/bin/lib/verification.cjs +47 -15
  119. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  120. package/gsd-core/bin/lib/verify.cjs +188 -23
  121. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  122. package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
  123. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  124. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  125. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  126. package/gsd-core/references/compact-content-gate.md +66 -0
  127. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  128. package/gsd-core/references/model-profiles.md +12 -3
  129. package/gsd-core/references/planning-config.md +3 -0
  130. package/gsd-core/references/tdd.md +5 -2
  131. package/gsd-core/references/thinking-models-planning.md +18 -2
  132. package/gsd-core/references/verification-patterns.md +17 -4
  133. package/gsd-core/references/worktree-path-safety.md +112 -2
  134. package/gsd-core/templates/README.md +7 -1
  135. package/gsd-core/templates/state.md +6 -3
  136. package/gsd-core/templates/summary.compact.md +212 -0
  137. package/gsd-core/templates/user-setup.compact.md +199 -0
  138. package/gsd-core/templates/user-setup.md +0 -9
  139. package/gsd-core/workflows/add-todo.md +3 -2
  140. package/gsd-core/workflows/autonomous.md +13 -10
  141. package/gsd-core/workflows/check-todos.md +4 -2
  142. package/gsd-core/workflows/cleanup.md +3 -1
  143. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
  144. package/gsd-core/workflows/code-review-fix.md +3 -3
  145. package/gsd-core/workflows/code-review.md +156 -30
  146. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  147. package/gsd-core/workflows/complete-milestone.md +39 -262
  148. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  149. package/gsd-core/workflows/docs-update.md +14 -155
  150. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  151. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
  152. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  153. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
  154. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  155. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  156. package/gsd-core/workflows/execute-phase.md +53 -152
  157. package/gsd-core/workflows/execute-plan.md +20 -7
  158. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  159. package/gsd-core/workflows/help.md +1 -1
  160. package/gsd-core/workflows/map-codebase.md +50 -3
  161. package/gsd-core/workflows/new-milestone.md +54 -12
  162. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  163. package/gsd-core/workflows/new-project.md +32 -202
  164. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  165. package/gsd-core/workflows/plan-phase.md +22 -181
  166. package/gsd-core/workflows/pr-branch.md +19 -7
  167. package/gsd-core/workflows/quick.md +8 -1
  168. package/gsd-core/workflows/reapply-patches.md +77 -3
  169. package/gsd-core/workflows/settings.md +18 -5
  170. package/gsd-core/workflows/update.md +7 -5
  171. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  172. package/gsd-core/workflows/verify-work.md +20 -180
  173. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  174. package/hooks/dist/gsd-context-monitor.js +88 -15
  175. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  176. package/hooks/dist/gsd-secret-read-guard.js +44 -18
  177. package/hooks/dist/gsd-statusline.js +11 -7
  178. package/hooks/dist/gsd-validate-commit.sh +34 -4
  179. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  180. package/hooks/dist/gsd-write-guard.js +46 -1
  181. package/hooks/dist/lib/dispatch-identity.js +187 -0
  182. package/hooks/dist/lib/filename-classification.js +64 -0
  183. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  184. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  185. package/hooks/gsd-agent-isolation-guard.js +42 -16
  186. package/hooks/gsd-context-monitor.js +88 -15
  187. package/hooks/gsd-cursor-subagent-start.js +34 -14
  188. package/hooks/gsd-secret-read-guard.js +44 -18
  189. package/hooks/gsd-statusline.js +11 -7
  190. package/hooks/gsd-validate-commit.sh +34 -4
  191. package/hooks/gsd-worktree-path-guard.js +25 -14
  192. package/hooks/gsd-write-guard.js +46 -1
  193. package/hooks/lib/dispatch-identity.js +187 -0
  194. package/hooks/lib/filename-classification.js +64 -0
  195. package/hooks/lib/isolation-deny-reason.js +53 -1
  196. package/hooks/lib/isolation-sentinel.js +58 -19
  197. package/package.json +10 -6
  198. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  199. package/scripts/benchmark-compact-content.cjs +368 -0
  200. package/scripts/check-contract-drift.cjs +4 -1
  201. package/scripts/check-env.cjs +36 -8
  202. package/scripts/check-glossary-refs.cjs +25 -21
  203. package/scripts/ci-next-health.cjs +271 -0
  204. package/scripts/ci-prepare-test-scope.cjs +7 -7
  205. package/scripts/ci-test-scope.cjs +126 -20
  206. package/scripts/ci-timeout-report.cjs +1 -1
  207. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  208. package/scripts/docs-guard-registry.cjs +7 -2
  209. package/scripts/gen-adr-index.cjs +8 -2
  210. package/scripts/gen-inventory-manifest.cjs +12 -0
  211. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  212. package/scripts/lib/drift-scan.cjs +1 -1
  213. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  214. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  215. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  216. package/scripts/lib/suite-detection.cjs +32 -0
  217. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  218. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
  219. package/scripts/lint-phase-id-drift.cjs +338 -13
  220. package/scripts/lint-response-language-coverage.cjs +9 -3
  221. package/scripts/lint-source-test-name-collision.cjs +1 -1
  222. package/scripts/lint-test-file-count.allowlist.json +1 -0
  223. package/scripts/lint-vendored-deps.cjs +128 -17
  224. package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
  225. package/scripts/prompt-injection-scan.sh +14 -0
  226. package/scripts/workflow-size.cjs +139 -0
  227. package/skills/gsd-cleanup/SKILL.md +1 -0
  228. package/skills/gsd-code-review/SKILL.md +2 -1
  229. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  230. package/skills/gsd-config/SKILL.md +1 -0
  231. package/skills/gsd-debug/SKILL.md +1 -0
  232. package/skills/gsd-graphify/SKILL.md +1 -0
  233. package/skills/gsd-health/SKILL.md +1 -0
  234. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  235. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  236. package/skills/gsd-new-milestone/SKILL.md +1 -0
  237. package/skills/gsd-new-project/SKILL.md +1 -0
  238. package/skills/gsd-next/SKILL.md +1 -0
  239. package/skills/gsd-pause-work/SKILL.md +1 -0
  240. package/skills/gsd-phase/SKILL.md +1 -0
  241. package/skills/gsd-pr-branch/SKILL.md +1 -0
  242. package/skills/gsd-resume-work/SKILL.md +1 -0
  243. package/skills/gsd-review-backlog/SKILL.md +1 -0
  244. package/skills/gsd-settings/SKILL.md +2 -1
  245. package/skills/gsd-stats/SKILL.md +1 -0
  246. package/skills/gsd-thread/SKILL.md +1 -0
  247. package/skills/gsd-workspace/SKILL.md +1 -0
  248. package/skills/gsd-workstreams/SKILL.md +1 -0
  249. package/vscode/package.json +1 -1
  250. package/gsd-core/templates/claude-md.md +0 -145
  251. package/gsd-core/templates/codebase/concerns.md +0 -310
  252. package/gsd-core/templates/codebase/conventions.md +0 -307
  253. package/gsd-core/templates/codebase/integrations.md +0 -280
  254. package/gsd-core/templates/codebase/structure.md +0 -285
  255. package/gsd-core/templates/codebase/testing.md +0 -480
  256. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  257. package/gsd-core/templates/discovery.md +0 -146
@@ -43,24 +43,31 @@ exports.STATE_FIELD_SCHEMA = exports.STATUS_LIFECYCLE_ENUM = void 0;
43
43
  /**
44
44
  * The seven CANONICAL values `normalizeStateStatus` (`src/state-document.cts`)
45
45
  * maps recognized raw status prose TO — the function's default fallback plus
46
- * each branch's literal output, in the order the function tests them. This is
47
- * NOT the raw body prose vocabulary `CONTEXT.md`'s "STATE.md Status Lifecycle
48
- * (ADR-2207)" entry documents (`Ready to plan` → `All phases complete` →
49
- * `<version> milestone complete` → `Awaiting next milestone`, plus the
50
- * handler-authored strings in `KNOWN_TEMPLATE_DEFAULTS['Status']`) — that is
51
- * free-form prose `normalizeStateStatus` READS.
46
+ * each vocabulary entry's literal output. This is NOT the raw body prose
47
+ * vocabulary `CONTEXT.md`'s "STATE.md Status Lifecycle (ADR-2207)" entry
48
+ * documents (`Ready to plan` → `All phases complete` → `<version> milestone
49
+ * complete` → `Awaiting next milestone`, plus the handler-authored strings
50
+ * in `KNOWN_TEMPLATE_DEFAULTS['Status']`) — that is free-form prose
51
+ * `normalizeStateStatus` READS.
52
52
  *
53
53
  * CORRECTED (#3873 phase-3 test-matrix row 26 — verified by executing
54
54
  * `normalizeStateStatus`, not by reading this docstring's prior claim):
55
55
  * this is NOT a closed set the `status` frontmatter key is restricted to at
56
- * runtime. `normalizeStateStatus` is deliberately LENIENT: its fallback is
57
- * `normalizedStatus = status || 'unknown'`, and when none of its
58
- * substring-match branches recognize the raw input, that fallback — the
59
- * caller's raw, UNRECOGNIZED prose — is returned unchanged. A status value
60
- * outside this seven-member set is not rejected, coerced, or normalized; it
61
- * passes straight through into the frontmatter. `STATUS_LIFECYCLE_ENUM` is
62
- * therefore the set of values the normalizer maps recognized input ONTO, not
63
- * a runtime-enforced closed vocabulary for the field.
56
+ * runtime. `normalizeStateStatus` is deliberately LENIENT: its fallback
57
+ * returns the caller's raw, UNRECOGNIZED prose unchanged — when none of its
58
+ * vocabulary entries recognize the whole-field input, that raw value is what
59
+ * the function returns. A status value outside this seven-member set is not
60
+ * rejected, coerced, or normalized; it passes straight through into the
61
+ * frontmatter. `STATUS_LIFECYCLE_ENUM` is therefore the set of values the
62
+ * normalizer maps recognized input ONTO, not a runtime-enforced closed
63
+ * vocabulary for the field.
64
+ *
65
+ * #4186: recognition is ANCHORED (whole-field match against the declared
66
+ * `STATUS_EXACT_TOKENS` / `STATUS_ANCHORED_PATTERNS` tables in
67
+ * `src/state-document.cts`), never a substring scan of the prose — prose
68
+ * merely CONTAINING a status word (a `.planning/` path, `verifica*`,
69
+ * `completezza`) passes through verbatim instead of being rewritten to a
70
+ * credible wrong token.
64
71
  */
65
72
  exports.STATUS_LIFECYCLE_ENUM = Object.freeze([
66
73
  'unknown',
@@ -36,6 +36,11 @@ const state_document_cjs_2 = require("./state-document.cjs");
36
36
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
37
37
  const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
38
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");
39
44
  // eslint-disable-next-line @typescript-eslint/no-require-imports
40
45
  const stateMdSchemaMod = require("./state-md-schema.cjs");
41
46
  const { STATE_FIELD_SCHEMA } = stateMdSchemaMod;
@@ -44,12 +49,13 @@ function formatProgressMachineSegment(percent) {
44
49
  // ADR-3180 Decision 7: rounding and the 100 ceiling belong to the
45
50
  // completion-ratio kernel. The floor is added here because this helper is
46
51
  // 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.
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.
50
57
  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}%`;
58
+ return `[${(0, phase_lifecycle_cjs_1.renderProgressBar)(clamped, 10)}] ${clamped}%`;
53
59
  }
54
60
  // Consumers (a future STATE.md writer that bypasses all three reintroduces the
55
61
  // #4213 divergence class): `cmdStateUpdateProgress` and `syncCore`'s progress
@@ -60,11 +66,29 @@ function formatProgressMachineSegment(percent) {
60
66
  // `syncCore`'s call here.
61
67
  function stateReplaceProgressPercent(content, percent) {
62
68
  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;
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;
68
92
  const plainProgressPattern = /^(Progress:[ \t]*)([^\r\n]*)/im;
69
93
  const pattern = boldProgressPattern.test(body)
70
94
  ? boldProgressPattern
@@ -508,6 +532,72 @@ function preservedValuesEqual(a, b) {
508
532
  }
509
533
  return a === b;
510
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
+ }
511
601
  /**
512
602
  * Executor for `preservation: 'preserve-always'` (ADR-3408 §8.1). Only
513
603
  * `progress` carries this policy today. Preserves #3242/#1446/#2440/#2969
@@ -523,25 +613,39 @@ function applyPreserveAlways(field, cls, ctx) {
523
613
  const derived = ctx.postFm[field];
524
614
  const derivedMeasured = scanMeasuredSomething(cls, derived);
525
615
  const curatedMeasured = scanMeasuredSomething(cls, curated);
526
- // On a resyncing write the fresh derivation is authoritative — UNLESS it
527
- // measured nothing while the curated block did (#3756), AND the caller did
528
- // not explicitly name a progress-affecting field this write. The
529
- // unmeasured-scan guard exists to stop an INCIDENTAL resync (e.g. `state
530
- // add-decision`, whose `resync` defaults true for reasons that have
531
- // nothing to do with `progress`) from dropping a real curated block when a
532
- // milestone-scoped disk scan measures nothing (#3756's archived-milestone
533
- // case). It must not also block a write the user pointed AT `progress` on
534
- // purpose: `preserve-always`'s own contract is "never overwrite unless the
535
- // caller explicitly names this field" (FIELD_CLASSIFICATION doc comment),
536
- // and `state update Progress` / `state patch Progress=...` are exactly
537
- // that naming — the resync they trigger must win even when the disk scan
538
- // it also drives (e.g. because there are no phase dirs at all) reads as
539
- // "unmeasured" (tests/frontmatter.test.cjs: "state.update \"Progress\"
540
- // resyncs progress frontmatter from the updated body", pre-existing, #3242).
541
- 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)))
542
641
  return;
543
642
  let next;
544
- 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) {
545
649
  // #2440: total_plans and total_phases always take the derived (post-sync)
546
650
  // value even under !resync. This is used by cmdStatePlannedPhase where
547
651
  // total_plans must correct upward after plans are added. For body-only
@@ -824,7 +928,17 @@ function beginPhaseCore(content, intent, deps) {
824
928
  const focusLabel = intent.phaseName
825
929
  ? `Phase ${intent.phaseNumber} — ${intent.phaseName}`
826
930
  : `Phase ${intent.phaseNumber}`;
827
- 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;
828
942
  if (focusPattern.test(body)) {
829
943
  body = body.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
830
944
  updated.push('Current focus');