gsd-antigravity-kit 2.0.0 → 2.1.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 (258) hide show
  1. package/.agent/skills/gsd/SKILL.md +26 -4
  2. package/.agent/skills/gsd/VERSION +1 -1
  3. package/.agent/skills/gsd/assets/templates/AI-SPEC.md +246 -0
  4. package/.agent/skills/gsd/assets/templates/DEBUG.md +7 -2
  5. package/.agent/skills/gsd/assets/templates/config.json +56 -48
  6. package/.agent/skills/gsd/assets/templates/research.md +40 -0
  7. package/.agent/skills/gsd/assets/templates/spec.md +307 -0
  8. package/.agent/skills/gsd/assets/templates/state.md +8 -0
  9. package/.agent/skills/gsd/bin/gsd-tools.cjs +212 -11
  10. package/.agent/skills/gsd/bin/help-manifest.json +8 -2
  11. package/.agent/skills/gsd/bin/hooks/gsd-check-update-worker.js +108 -0
  12. package/.agent/skills/gsd/bin/hooks/gsd-check-update.js +14 -89
  13. package/.agent/skills/gsd/bin/hooks/gsd-context-monitor.js +34 -5
  14. package/.agent/skills/gsd/bin/hooks/gsd-phase-boundary.sh +1 -0
  15. package/.agent/skills/gsd/bin/hooks/gsd-prompt-guard.js +1 -1
  16. package/.agent/skills/gsd/bin/hooks/gsd-read-guard.js +6 -1
  17. package/.agent/skills/gsd/bin/hooks/gsd-session-state.sh +1 -0
  18. package/.agent/skills/gsd/bin/hooks/gsd-statusline.js +150 -16
  19. package/.agent/skills/gsd/bin/hooks/gsd-validate-commit.sh +1 -0
  20. package/.agent/skills/gsd/bin/hooks/gsd-workflow-guard.js +1 -1
  21. package/.agent/skills/gsd/bin/lib/audit.cjs +757 -0
  22. package/.agent/skills/gsd/bin/lib/commands.cjs +17 -7
  23. package/.agent/skills/gsd/bin/lib/config.cjs +66 -20
  24. package/.agent/skills/gsd/bin/lib/core.cjs +212 -12
  25. package/.agent/skills/gsd/bin/lib/frontmatter.cjs +6 -8
  26. package/.agent/skills/gsd/bin/lib/graphify.cjs +494 -0
  27. package/.agent/skills/gsd/bin/lib/gsd2-import.cjs +511 -0
  28. package/.agent/skills/gsd/bin/lib/init.cjs +371 -18
  29. package/.agent/skills/gsd/bin/lib/intel.cjs +9 -30
  30. package/.agent/skills/gsd/bin/lib/milestone.cjs +18 -17
  31. package/.agent/skills/gsd/bin/lib/model-profiles.cjs +1 -0
  32. package/.agent/skills/gsd/bin/lib/phase.cjs +225 -98
  33. package/.agent/skills/gsd/bin/lib/profile-output.cjs +17 -5
  34. package/.agent/skills/gsd/bin/lib/roadmap.cjs +12 -5
  35. package/.agent/skills/gsd/bin/lib/state.cjs +394 -129
  36. package/.agent/skills/gsd/bin/lib/template.cjs +8 -4
  37. package/.agent/skills/gsd/bin/lib/uat.cjs +2 -1
  38. package/.agent/skills/gsd/bin/lib/verify.cjs +111 -42
  39. package/.agent/skills/gsd/migration_report.md +2 -2
  40. package/.agent/skills/gsd/references/agents/gsd-advisor-researcher.md +23 -0
  41. package/.agent/skills/gsd/references/agents/gsd-ai-researcher.md +133 -0
  42. package/.agent/skills/gsd/references/agents/gsd-code-fixer.md +11 -10
  43. package/.agent/skills/gsd/references/agents/gsd-code-reviewer.md +2 -2
  44. package/.agent/skills/gsd/references/agents/gsd-codebase-mapper.md +13 -2
  45. package/.agent/skills/gsd/references/agents/gsd-debug-session-manager.md +314 -0
  46. package/.agent/skills/gsd/references/agents/gsd-debugger.md +147 -76
  47. package/.agent/skills/gsd/references/agents/gsd-doc-verifier.md +1 -1
  48. package/.agent/skills/gsd/references/agents/gsd-doc-writer.md +615 -602
  49. package/.agent/skills/gsd/references/agents/gsd-domain-researcher.md +153 -0
  50. package/.agent/skills/gsd/references/agents/gsd-eval-auditor.md +175 -0
  51. package/.agent/skills/gsd/references/agents/gsd-eval-planner.md +154 -0
  52. package/.agent/skills/gsd/references/agents/gsd-executor.md +108 -38
  53. package/.agent/skills/gsd/references/agents/gsd-framework-selector.md +160 -0
  54. package/.agent/skills/gsd/references/agents/gsd-integration-checker.md +454 -443
  55. package/.agent/skills/gsd/references/agents/gsd-intel-updater.md +40 -20
  56. package/.agent/skills/gsd/references/agents/gsd-nyquist-auditor.md +187 -176
  57. package/.agent/skills/gsd/references/agents/gsd-pattern-mapper.md +335 -0
  58. package/.agent/skills/gsd/references/agents/gsd-phase-researcher.md +112 -13
  59. package/.agent/skills/gsd/references/agents/gsd-plan-checker.md +104 -10
  60. package/.agent/skills/gsd/references/agents/gsd-planner.md +125 -167
  61. package/.agent/skills/gsd/references/agents/gsd-project-researcher.md +25 -2
  62. package/.agent/skills/gsd/references/agents/gsd-research-synthesizer.md +3 -3
  63. package/.agent/skills/gsd/references/agents/gsd-roadmapper.md +12 -1
  64. package/.agent/skills/gsd/references/agents/gsd-security-auditor.md +139 -128
  65. package/.agent/skills/gsd/references/agents/gsd-ui-auditor.md +3 -3
  66. package/.agent/skills/gsd/references/agents/gsd-ui-checker.md +11 -2
  67. package/.agent/skills/gsd/references/agents/gsd-ui-researcher.md +27 -4
  68. package/.agent/skills/gsd/references/agents/gsd-verifier.md +13 -19
  69. package/.agent/skills/gsd/references/commands/atomic/add-todo.md +2 -2
  70. package/.agent/skills/gsd/references/commands/atomic/check-todos.md +2 -2
  71. package/.agent/skills/gsd/references/commands/atomic/cleanup.md +2 -2
  72. package/.agent/skills/gsd/references/commands/atomic/do.md +2 -2
  73. package/.agent/skills/gsd/references/commands/atomic/help.md +2 -2
  74. package/.agent/skills/gsd/references/commands/atomic/join-discord.md +2 -2
  75. package/.agent/skills/gsd/references/commands/atomic/note.md +2 -2
  76. package/.agent/skills/gsd/references/commands/atomic/session-report.md +2 -2
  77. package/.agent/skills/gsd/references/commands/atomic/ship.md +2 -2
  78. package/.agent/skills/gsd/references/commands/atomic/stats.md +2 -2
  79. package/.agent/skills/gsd/references/commands/atomic/thread.md +141 -41
  80. package/.agent/skills/gsd/references/commands/atomic/undo.md +2 -2
  81. package/.agent/skills/gsd/references/commands/milestone/add-backlog.md +15 -12
  82. package/.agent/skills/gsd/references/commands/milestone/audit-milestone.md +2 -2
  83. package/.agent/skills/gsd/references/commands/milestone/complete-milestone.md +2 -2
  84. package/.agent/skills/gsd/references/commands/milestone/milestone-summary.md +2 -2
  85. package/.agent/skills/gsd/references/commands/milestone/new-milestone.md +2 -2
  86. package/.agent/skills/gsd/references/commands/milestone/plan-milestone-gaps.md +2 -2
  87. package/.agent/skills/gsd/references/commands/milestone/plant-seed.md +2 -2
  88. package/.agent/skills/gsd/references/commands/milestone/review-backlog.md +4 -4
  89. package/.agent/skills/gsd/references/commands/misc/ai-integration-phase.md +38 -0
  90. package/.agent/skills/gsd/references/commands/misc/audit-fix.md +2 -2
  91. package/.agent/skills/gsd/references/commands/misc/audit-uat.md +2 -2
  92. package/.agent/skills/gsd/references/commands/misc/eval-review.md +34 -0
  93. package/.agent/skills/gsd/references/commands/misc/extract_learnings.md +24 -0
  94. package/.agent/skills/gsd/references/commands/misc/from-gsd2.md +49 -0
  95. package/.agent/skills/gsd/references/commands/misc/graphify.md +203 -0
  96. package/.agent/skills/gsd/references/commands/misc/inbox.md +40 -0
  97. package/.agent/skills/gsd/references/commands/misc/next.md +5 -3
  98. package/.agent/skills/gsd/references/commands/misc/progress.md +4 -3
  99. package/.agent/skills/gsd/references/commands/misc/sketch-wrap-up.md +33 -0
  100. package/.agent/skills/gsd/references/commands/misc/sketch.md +47 -0
  101. package/.agent/skills/gsd/references/commands/misc/spec-phase.md +64 -0
  102. package/.agent/skills/gsd/references/commands/misc/spike-wrap-up.md +33 -0
  103. package/.agent/skills/gsd/references/commands/misc/spike.md +43 -0
  104. package/.agent/skills/gsd/references/commands/misc/verify-work.md +2 -2
  105. package/.agent/skills/gsd/references/commands/phase/add-phase.md +2 -2
  106. package/.agent/skills/gsd/references/commands/phase/add-tests.md +2 -2
  107. package/.agent/skills/gsd/references/commands/phase/discuss-phase.md +5 -5
  108. package/.agent/skills/gsd/references/commands/phase/execute-phase.md +4 -4
  109. package/.agent/skills/gsd/references/commands/phase/insert-phase.md +2 -2
  110. package/.agent/skills/gsd/references/commands/phase/list-phase-assumptions.md +2 -2
  111. package/.agent/skills/gsd/references/commands/phase/plan-phase.md +3 -3
  112. package/.agent/skills/gsd/references/commands/phase/remove-phase.md +2 -2
  113. package/.agent/skills/gsd/references/commands/phase/research-phase.md +5 -5
  114. package/.agent/skills/gsd/references/commands/phase/secure-phase.md +2 -2
  115. package/.agent/skills/gsd/references/commands/phase/ui-phase.md +2 -2
  116. package/.agent/skills/gsd/references/commands/phase/ui-review.md +2 -2
  117. package/.agent/skills/gsd/references/commands/phase/validate-phase.md +2 -2
  118. package/.agent/skills/gsd/references/commands/phase/workstreams.md +9 -9
  119. package/.agent/skills/gsd/references/commands/project/analyze-dependencies.md +2 -2
  120. package/.agent/skills/gsd/references/commands/project/explore.md +2 -2
  121. package/.agent/skills/gsd/references/commands/project/import.md +2 -2
  122. package/.agent/skills/gsd/references/commands/project/intel.md +10 -10
  123. package/.agent/skills/gsd/references/commands/project/list-workspaces.md +2 -2
  124. package/.agent/skills/gsd/references/commands/project/map-codebase.md +2 -2
  125. package/.agent/skills/gsd/references/commands/project/new-project.md +2 -2
  126. package/.agent/skills/gsd/references/commands/project/new-workspace.md +2 -2
  127. package/.agent/skills/gsd/references/commands/project/remove-workspace.md +2 -2
  128. package/.agent/skills/gsd/references/commands/project/scan.md +2 -2
  129. package/.agent/skills/gsd/references/commands/system/autonomous.md +4 -3
  130. package/.agent/skills/gsd/references/commands/system/code-review-fix.md +3 -3
  131. package/.agent/skills/gsd/references/commands/system/code-review.md +3 -3
  132. package/.agent/skills/gsd/references/commands/system/debug.md +177 -100
  133. package/.agent/skills/gsd/references/commands/system/docs-update.md +2 -2
  134. package/.agent/skills/gsd/references/commands/system/fast.md +2 -2
  135. package/.agent/skills/gsd/references/commands/system/forensics.md +2 -2
  136. package/.agent/skills/gsd/references/commands/system/gsd-tools.md +153 -6
  137. package/.agent/skills/gsd/references/commands/system/health.md +2 -2
  138. package/.agent/skills/gsd/references/commands/system/manager.md +3 -3
  139. package/.agent/skills/gsd/references/commands/system/pause-work.md +2 -2
  140. package/.agent/skills/gsd/references/commands/system/pr-branch.md +2 -2
  141. package/.agent/skills/gsd/references/commands/system/profile-user.md +2 -2
  142. package/.agent/skills/gsd/references/commands/system/quick.md +127 -3
  143. package/.agent/skills/gsd/references/commands/system/reapply-patches.md +45 -6
  144. package/.agent/skills/gsd/references/commands/system/resume-work.md +2 -2
  145. package/.agent/skills/gsd/references/commands/system/review.md +6 -4
  146. package/.agent/skills/gsd/references/commands/system/set-profile.md +3 -3
  147. package/.agent/skills/gsd/references/commands/system/settings.md +2 -2
  148. package/.agent/skills/gsd/references/commands/system/update.md +2 -2
  149. package/.agent/skills/gsd/references/docs/ai-evals.md +156 -0
  150. package/.agent/skills/gsd/references/docs/ai-frameworks.md +186 -0
  151. package/.agent/skills/gsd/references/docs/artifact-types.md +18 -0
  152. package/.agent/skills/gsd/references/docs/autonomous-smart-discuss.md +277 -0
  153. package/.agent/skills/gsd/references/docs/checkpoints.md +30 -0
  154. package/.agent/skills/gsd/references/docs/common-bug-patterns.md +49 -49
  155. package/.agent/skills/gsd/references/docs/continuation-format.md +11 -7
  156. package/.agent/skills/gsd/references/docs/debugger-philosophy.md +76 -0
  157. package/.agent/skills/gsd/references/docs/decimal-phase-calculation.md +64 -64
  158. package/.agent/skills/gsd/references/docs/executor-examples.md +110 -0
  159. package/.agent/skills/gsd/references/docs/git-integration.md +4 -4
  160. package/.agent/skills/gsd/references/docs/git-planning-commit.md +40 -38
  161. package/.agent/skills/gsd/references/docs/ios-scaffold.md +123 -0
  162. package/.agent/skills/gsd/references/docs/mandatory-initial-read.md +2 -0
  163. package/.agent/skills/gsd/references/docs/phase-argument-parsing.md +61 -61
  164. package/.agent/skills/gsd/references/docs/planner-antipatterns.md +89 -0
  165. package/.agent/skills/gsd/references/docs/planner-revision.md +87 -87
  166. package/.agent/skills/gsd/references/docs/planner-source-audit.md +73 -0
  167. package/.agent/skills/gsd/references/docs/planning-config.md +33 -8
  168. package/.agent/skills/gsd/references/docs/project-skills-discovery.md +19 -0
  169. package/.agent/skills/gsd/references/docs/sketch-interactivity.md +41 -0
  170. package/.agent/skills/gsd/references/docs/sketch-theme-system.md +94 -0
  171. package/.agent/skills/gsd/references/docs/sketch-tooling.md +45 -0
  172. package/.agent/skills/gsd/references/docs/sketch-variant-patterns.md +81 -0
  173. package/.agent/skills/gsd/references/docs/tdd.md +67 -0
  174. package/.agent/skills/gsd/references/docs/universal-anti-patterns.md +5 -0
  175. package/.agent/skills/gsd/references/docs/workstream-flag.md +11 -11
  176. package/.agent/skills/gsd/references/mapping.md +1 -1
  177. package/.agent/skills/gsd/references/workflows/add-phase.md +112 -112
  178. package/.agent/skills/gsd/references/workflows/add-tests.md +6 -3
  179. package/.agent/skills/gsd/references/workflows/add-todo.md +5 -3
  180. package/.agent/skills/gsd/references/workflows/ai-integration-phase.md +284 -0
  181. package/.agent/skills/gsd/references/workflows/audit-fix.md +157 -157
  182. package/.agent/skills/gsd/references/workflows/audit-milestone.md +340 -340
  183. package/.agent/skills/gsd/references/workflows/audit-uat.md +109 -109
  184. package/.agent/skills/gsd/references/workflows/autonomous.md +20 -288
  185. package/.agent/skills/gsd/references/workflows/check-todos.md +4 -2
  186. package/.agent/skills/gsd/references/workflows/cleanup.md +3 -1
  187. package/.agent/skills/gsd/references/workflows/code-review-fix.md +497 -497
  188. package/.agent/skills/gsd/references/workflows/code-review.md +515 -515
  189. package/.agent/skills/gsd/references/workflows/complete-milestone.md +97 -24
  190. package/.agent/skills/gsd/references/workflows/diagnose-issues.md +238 -238
  191. package/.agent/skills/gsd/references/workflows/discovery-phase.md +2 -0
  192. package/.agent/skills/gsd/references/workflows/discuss-phase-assumptions.md +11 -11
  193. package/.agent/skills/gsd/references/workflows/discuss-phase.md +143 -19
  194. package/.agent/skills/gsd/references/workflows/do.md +8 -2
  195. package/.agent/skills/gsd/references/workflows/docs-update.md +5 -3
  196. package/.agent/skills/gsd/references/workflows/eval-review.md +155 -0
  197. package/.agent/skills/gsd/references/workflows/execute-phase.md +338 -54
  198. package/.agent/skills/gsd/references/workflows/execute-plan.md +80 -104
  199. package/.agent/skills/gsd/references/workflows/explore.md +3 -1
  200. package/.agent/skills/gsd/references/workflows/extract_learnings.md +232 -0
  201. package/.agent/skills/gsd/references/workflows/forensics.md +3 -3
  202. package/.agent/skills/gsd/references/workflows/health.md +2 -2
  203. package/.agent/skills/gsd/references/workflows/help.md +59 -1
  204. package/.agent/skills/gsd/references/workflows/import.md +3 -1
  205. package/.agent/skills/gsd/references/workflows/inbox.md +387 -384
  206. package/.agent/skills/gsd/references/workflows/insert-phase.md +130 -130
  207. package/.agent/skills/gsd/references/workflows/list-workspaces.md +56 -56
  208. package/.agent/skills/gsd/references/workflows/manager.md +5 -3
  209. package/.agent/skills/gsd/references/workflows/map-codebase.md +19 -5
  210. package/.agent/skills/gsd/references/workflows/milestone-summary.md +6 -6
  211. package/.agent/skills/gsd/references/workflows/new-milestone.md +63 -9
  212. package/.agent/skills/gsd/references/workflows/new-project.md +126 -22
  213. package/.agent/skills/gsd/references/workflows/new-workspace.md +6 -4
  214. package/.agent/skills/gsd/references/workflows/next.md +220 -153
  215. package/.agent/skills/gsd/references/workflows/note.md +2 -0
  216. package/.agent/skills/gsd/references/workflows/pause-work.md +11 -7
  217. package/.agent/skills/gsd/references/workflows/plan-milestone-gaps.md +273 -273
  218. package/.agent/skills/gsd/references/workflows/plan-phase.md +281 -62
  219. package/.agent/skills/gsd/references/workflows/plant-seed.md +4 -1
  220. package/.agent/skills/gsd/references/workflows/pr-branch.md +41 -13
  221. package/.agent/skills/gsd/references/workflows/profile-user.md +15 -13
  222. package/.agent/skills/gsd/references/workflows/progress.md +133 -21
  223. package/.agent/skills/gsd/references/workflows/quick.md +67 -27
  224. package/.agent/skills/gsd/references/workflows/remove-phase.md +155 -155
  225. package/.agent/skills/gsd/references/workflows/remove-workspace.md +4 -2
  226. package/.agent/skills/gsd/references/workflows/research-phase.md +3 -3
  227. package/.agent/skills/gsd/references/workflows/resume-project.md +3 -3
  228. package/.agent/skills/gsd/references/workflows/review.md +71 -8
  229. package/.agent/skills/gsd/references/workflows/scan.md +102 -102
  230. package/.agent/skills/gsd/references/workflows/secure-phase.md +7 -5
  231. package/.agent/skills/gsd/references/workflows/settings.md +24 -7
  232. package/.agent/skills/gsd/references/workflows/ship.md +71 -6
  233. package/.agent/skills/gsd/references/workflows/sketch-wrap-up.md +283 -0
  234. package/.agent/skills/gsd/references/workflows/sketch.md +263 -0
  235. package/.agent/skills/gsd/references/workflows/spec-phase.md +262 -0
  236. package/.agent/skills/gsd/references/workflows/spike-wrap-up.md +273 -0
  237. package/.agent/skills/gsd/references/workflows/spike.md +270 -0
  238. package/.agent/skills/gsd/references/workflows/stats.md +60 -60
  239. package/.agent/skills/gsd/references/workflows/transition.md +671 -671
  240. package/.agent/skills/gsd/references/workflows/ui-phase.md +33 -12
  241. package/.agent/skills/gsd/references/workflows/ui-review.md +6 -4
  242. package/.agent/skills/gsd/references/workflows/undo.md +3 -1
  243. package/.agent/skills/gsd/references/workflows/update.md +113 -2
  244. package/.agent/skills/gsd/references/workflows/validate-phase.md +7 -5
  245. package/.agent/skills/gsd/references/workflows/verify-phase.md +93 -10
  246. package/.agent/skills/gsd/references/workflows/verify-work.md +50 -10
  247. package/.agent/skills/gsd-converter/references/mapping.md +1 -1
  248. package/.agent/skills/gsd-converter/scripts/convert.py +36 -17
  249. package/.agent/skills/gsd-converter/scripts/regression_test.py +68 -33
  250. package/README.md +3 -2
  251. package/package.json +4 -2
  252. package/.agent/skills/release-manager/SKILL.md +0 -162
  253. package/.agent/skills/release-manager/bin/LICENSE +0 -21
  254. package/.agent/skills/release-manager/bin/gh.exe +0 -0
  255. package/.agent/skills/release-manager/references/update_kb_from_fixes.md +0 -29
  256. package/.agent/skills/release-manager/scripts/release.ps1 +0 -222
  257. package/.agent/skills/selectpaste-update/SKILL.md +0 -46
  258. package/.agent/skills/selectpaste-update/scripts/sync-commands.py +0 -317
@@ -4,14 +4,29 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error } = require('./core.cjs');
7
+ const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error, atomicWriteFileSync } = require('./core.cjs');
8
8
  const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
9
9
 
10
+ // Cache disk scan results from buildStateFrontmatter per cwd per process (#1967).
11
+ // Avoids re-reading N+1 directories on every state write when the phase structure
12
+ // hasn't changed within the same gsd-tools invocation.
13
+ const _diskScanCache = new Map();
14
+
10
15
  /** Shorthand — every state command needs this path */
11
16
  function getStatePath(cwd) {
12
17
  return planningPaths(cwd).state;
13
18
  }
14
19
 
20
+ // Track all lock files held by this process so they can be removed on exit.
21
+ // process.on('exit') fires even on process.exit(1), unlike try/finally which is
22
+ // skipped when error() calls process.exit(1) inside a locked region (#1916).
23
+ const _heldStateLocks = new Set();
24
+ process.on('exit', () => {
25
+ for (const lockPath of _heldStateLocks) {
26
+ try { require('fs').unlinkSync(lockPath); } catch { /* already gone */ }
27
+ }
28
+ });
29
+
15
30
  // Shared helper: extract a field value from STATE.md content.
16
31
  // Supports both **Field:** bold and plain Field: format.
17
32
  function stateExtractField(content, fieldName) {
@@ -184,18 +199,22 @@ function cmdStateUpdate(cwd, field, value) {
184
199
 
185
200
  const statePath = planningPaths(cwd).state;
186
201
  try {
187
- let content = fs.readFileSync(statePath, 'utf-8');
188
- const fieldEscaped = escapeRegex(field);
189
- // Try **Field:** bold format first, then plain Field: format
190
- const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
191
- const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
192
- if (boldPattern.test(content)) {
193
- content = content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
194
- writeStateMd(statePath, content, cwd);
195
- output({ updated: true });
196
- } else if (plainPattern.test(content)) {
197
- content = content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
198
- writeStateMd(statePath, content, cwd);
202
+ let updated = false;
203
+ readModifyWriteStateMd(statePath, (content) => {
204
+ const fieldEscaped = escapeRegex(field);
205
+ // Try **Field:** bold format first, then plain Field: format
206
+ const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
207
+ const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
208
+ if (boldPattern.test(content)) {
209
+ updated = true;
210
+ return content.replace(boldPattern, (_match, prefix) => `${prefix}${value}`);
211
+ } else if (plainPattern.test(content)) {
212
+ updated = true;
213
+ return content.replace(plainPattern, (_match, prefix) => `${prefix}${value}`);
214
+ }
215
+ return content;
216
+ }, cwd);
217
+ if (updated) {
199
218
  output({ updated: true });
200
219
  } else {
201
220
  output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
@@ -274,9 +293,10 @@ function cmdStateAdvancePlan(cwd, raw) {
274
293
  const statePath = planningPaths(cwd).state;
275
294
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
276
295
 
277
- let content = fs.readFileSync(statePath, 'utf-8');
278
296
  const today = new Date().toISOString().split('T')[0];
297
+ let result = null;
279
298
 
299
+ readModifyWriteStateMd(statePath, (content) => {
280
300
  // Try legacy separate fields first, then compound "Plan: X of Y" format
281
301
  const legacyPlan = stateExtractField(content, 'Current Plan');
282
302
  const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
@@ -286,43 +306,54 @@ function cmdStateAdvancePlan(cwd, raw) {
286
306
  let useCompoundFormat = false;
287
307
 
288
308
  if (legacyPlan && legacyTotal) {
289
- currentPlan = parseInt(legacyPlan, 10);
290
- totalPlans = parseInt(legacyTotal, 10);
309
+ currentPlan = parseInt(legacyPlan, 10);
310
+ totalPlans = parseInt(legacyTotal, 10);
291
311
  } else if (planField) {
292
- // Compound format: "2 of 6 in current phase" or "2 of 6"
293
- currentPlan = parseInt(planField, 10);
294
- const ofMatch = planField.match(/of\s+(\d+)/);
295
- totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
296
- useCompoundFormat = true;
312
+ // Compound format: "2 of 6 in current phase" or "2 of 6"
313
+ currentPlan = parseInt(planField, 10);
314
+ const ofMatch = planField.match(/of\s+(\d+)/);
315
+ totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
316
+ useCompoundFormat = true;
297
317
  }
298
318
 
299
319
  if (isNaN(currentPlan) || isNaN(totalPlans)) {
300
- output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
301
- return;
320
+ result = { error: true };
321
+ return content;
302
322
  }
303
323
 
304
324
  if (currentPlan >= totalPlans) {
305
- content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
306
- content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
307
- content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
308
- writeStateMd(statePath, content, cwd);
309
- output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
325
+ content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
326
+ content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
327
+ content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
328
+ result = { advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' };
310
329
  } else {
311
- const newPlan = currentPlan + 1;
312
- let planDisplayValue;
313
- if (useCompoundFormat) {
330
+ const newPlan = currentPlan + 1;
331
+ let planDisplayValue;
332
+ if (useCompoundFormat) {
314
333
  // Preserve compound format: "X of Y in current phase" → replace X only
315
334
  planDisplayValue = planField.replace(/^\d+/, String(newPlan));
316
335
  content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
317
- } else {
336
+ } else {
318
337
  planDisplayValue = `${newPlan} of ${totalPlans}`;
319
338
  content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
339
+ }
340
+ content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
341
+ content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
342
+ content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
343
+ result = { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans };
320
344
  }
321
- content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
322
- content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
323
- content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
324
- writeStateMd(statePath, content, cwd);
325
- output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
345
+ return content;
346
+ }, cwd);
347
+
348
+ if (!result || result.error) {
349
+ output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
350
+ return;
351
+ }
352
+
353
+ if (result.advanced === false) {
354
+ output(result, raw, 'false');
355
+ } else {
356
+ output(result, raw, 'true');
326
357
  }
327
358
  }
328
359
 
@@ -330,7 +361,6 @@ function cmdStateRecordMetric(cwd, options, raw) {
330
361
  const statePath = planningPaths(cwd).state;
331
362
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
332
363
 
333
- let content = fs.readFileSync(statePath, 'utf-8');
334
364
  const { phase, plan, duration, tasks, files } = options;
335
365
 
336
366
  if (!phase || !plan || !duration) {
@@ -338,22 +368,29 @@ function cmdStateRecordMetric(cwd, options, raw) {
338
368
  return;
339
369
  }
340
370
 
371
+ let recorded = false;
372
+ readModifyWriteStateMd(statePath, (content) => {
341
373
  // Find Performance Metrics section and its table
342
374
  const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
343
375
  const metricsMatch = content.match(metricsPattern);
344
376
 
345
377
  if (metricsMatch) {
346
- let tableBody = metricsMatch[2].trimEnd();
347
- const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
378
+ let tableBody = metricsMatch[2].trimEnd();
379
+ const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
348
380
 
349
- if (tableBody.trim() === '' || tableBody.includes('None yet')) {
381
+ if (tableBody.trim() === '' || tableBody.includes('None yet')) {
350
382
  tableBody = newRow;
351
- } else {
383
+ } else {
352
384
  tableBody = tableBody + '\n' + newRow;
385
+ }
386
+
387
+ recorded = true;
388
+ return content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
353
389
  }
390
+ return content;
391
+ }, cwd);
354
392
 
355
- content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
356
- writeStateMd(statePath, content, cwd);
393
+ if (recorded) {
357
394
  output({ recorded: true, phase, plan, duration }, raw, 'true');
358
395
  } else {
359
396
  output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
@@ -364,9 +401,7 @@ function cmdStateUpdateProgress(cwd, raw) {
364
401
  const statePath = planningPaths(cwd).state;
365
402
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
366
403
 
367
- let content = fs.readFileSync(statePath, 'utf-8');
368
-
369
- // Count summaries across current milestone phases only
404
+ // Count summaries across current milestone phases only (outside lock — read-only)
370
405
  const phasesDir = planningPaths(cwd).phases;
371
406
  let totalPlans = 0;
372
407
  let totalSummaries = 0;
@@ -389,17 +424,26 @@ function cmdStateUpdateProgress(cwd, raw) {
389
424
  const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
390
425
  const progressStr = `[${bar}] ${percent}%`;
391
426
 
427
+ let updated = false;
428
+ const _totalPlans = totalPlans;
429
+ const _totalSummaries = totalSummaries;
430
+
431
+ readModifyWriteStateMd(statePath, (content) => {
392
432
  // Try **Progress:** bold format first, then plain Progress: format
393
433
  const boldProgressPattern = /(\*\*Progress:\*\*\s*).*/i;
394
434
  const plainProgressPattern = /^(Progress:\s*).*/im;
395
435
  if (boldProgressPattern.test(content)) {
396
- content = content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
397
- writeStateMd(statePath, content, cwd);
398
- output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
436
+ updated = true;
437
+ return content.replace(boldProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
399
438
  } else if (plainProgressPattern.test(content)) {
400
- content = content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
401
- writeStateMd(statePath, content, cwd);
402
- output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
439
+ updated = true;
440
+ return content.replace(plainProgressPattern, (_match, prefix) => `${prefix}${progressStr}`);
441
+ }
442
+ return content;
443
+ }, cwd);
444
+
445
+ if (updated) {
446
+ output({ updated: true, percent, completed: _totalSummaries, total: _totalPlans, bar: progressStr }, raw, progressStr);
403
447
  } else {
404
448
  output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
405
449
  }
@@ -423,20 +467,26 @@ function cmdStateAddDecision(cwd, options, raw) {
423
467
 
424
468
  if (!summaryText) { output({ error: 'summary required' }, raw); return; }
425
469
 
426
- let content = fs.readFileSync(statePath, 'utf-8');
427
470
  const entry = `- [Phase ${phase || '?'}]: ${summaryText}${rationaleText ? ` — ${rationaleText}` : ''}`;
471
+ let added = false;
428
472
 
473
+ readModifyWriteStateMd(statePath, (content) => {
429
474
  // Find Decisions section (various heading patterns)
430
475
  const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
431
476
  const match = content.match(sectionPattern);
432
477
 
433
478
  if (match) {
434
- let sectionBody = match[2];
435
- // Remove placeholders
436
- sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
437
- sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
438
- content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
439
- writeStateMd(statePath, content, cwd);
479
+ let sectionBody = match[2];
480
+ // Remove placeholders
481
+ sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
482
+ sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
483
+ added = true;
484
+ return content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
485
+ }
486
+ return content;
487
+ }, cwd);
488
+
489
+ if (added) {
440
490
  output({ added: true, decision: entry }, raw, 'true');
441
491
  } else {
442
492
  output({ added: false, reason: 'Decisions section not found in STATE.md' }, raw, 'false');
@@ -458,18 +508,24 @@ function cmdStateAddBlocker(cwd, text, raw) {
458
508
 
459
509
  if (!blockerText) { output({ error: 'text required' }, raw); return; }
460
510
 
461
- let content = fs.readFileSync(statePath, 'utf-8');
462
511
  const entry = `- ${blockerText}`;
512
+ let added = false;
463
513
 
514
+ readModifyWriteStateMd(statePath, (content) => {
464
515
  const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
465
516
  const match = content.match(sectionPattern);
466
517
 
467
518
  if (match) {
468
- let sectionBody = match[2];
469
- sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
470
- sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
471
- content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
472
- writeStateMd(statePath, content, cwd);
519
+ let sectionBody = match[2];
520
+ sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
521
+ sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
522
+ added = true;
523
+ return content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
524
+ }
525
+ return content;
526
+ }, cwd);
527
+
528
+ if (added) {
473
529
  output({ added: true, blocker: blockerText }, raw, 'true');
474
530
  } else {
475
531
  output({ added: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
@@ -481,27 +537,33 @@ function cmdStateResolveBlocker(cwd, text, raw) {
481
537
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
482
538
  if (!text) { output({ error: 'text required' }, raw); return; }
483
539
 
484
- let content = fs.readFileSync(statePath, 'utf-8');
540
+ let resolved = false;
485
541
 
542
+ readModifyWriteStateMd(statePath, (content) => {
486
543
  const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
487
544
  const match = content.match(sectionPattern);
488
545
 
489
546
  if (match) {
490
- const sectionBody = match[2];
491
- const lines = sectionBody.split('\n');
492
- const filtered = lines.filter(line => {
547
+ const sectionBody = match[2];
548
+ const lines = sectionBody.split('\n');
549
+ const filtered = lines.filter(line => {
493
550
  if (!line.startsWith('- ')) return true;
494
551
  return !line.toLowerCase().includes(text.toLowerCase());
495
- });
552
+ });
496
553
 
497
- let newBody = filtered.join('\n');
498
- // If section is now empty, add placeholder
499
- if (!newBody.trim() || !newBody.includes('- ')) {
554
+ let newBody = filtered.join('\n');
555
+ // If section is now empty, add placeholder
556
+ if (!newBody.trim() || !newBody.includes('- ')) {
500
557
  newBody = 'None\n';
558
+ }
559
+
560
+ resolved = true;
561
+ return content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
501
562
  }
563
+ return content;
564
+ }, cwd);
502
565
 
503
- content = content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
504
- writeStateMd(statePath, content, cwd);
566
+ if (resolved) {
505
567
  output({ resolved: true, blocker: text }, raw, 'true');
506
568
  } else {
507
569
  output({ resolved: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
@@ -512,10 +574,10 @@ function cmdStateRecordSession(cwd, options, raw) {
512
574
  const statePath = planningPaths(cwd).state;
513
575
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
514
576
 
515
- let content = fs.readFileSync(statePath, 'utf-8');
516
577
  const now = new Date().toISOString();
517
578
  const updated = [];
518
579
 
580
+ readModifyWriteStateMd(statePath, (content) => {
519
581
  // Update Last session / Last Date
520
582
  let result = stateReplaceField(content, 'Last session', now);
521
583
  if (result) { content = result; updated.push('Last session'); }
@@ -524,9 +586,9 @@ function cmdStateRecordSession(cwd, options, raw) {
524
586
 
525
587
  // Update Stopped at
526
588
  if (options.stopped_at) {
527
- result = stateReplaceField(content, 'Stopped At', options.stopped_at);
528
- if (!result) result = stateReplaceField(content, 'Stopped at', options.stopped_at);
529
- if (result) { content = result; updated.push('Stopped At'); }
589
+ result = stateReplaceField(content, 'Stopped At', options.stopped_at);
590
+ if (!result) result = stateReplaceField(content, 'Stopped at', options.stopped_at);
591
+ if (result) { content = result; updated.push('Stopped At'); }
530
592
  }
531
593
 
532
594
  // Update Resume file
@@ -535,8 +597,10 @@ function cmdStateRecordSession(cwd, options, raw) {
535
597
  if (!result) result = stateReplaceField(content, 'Resume file', resumeFile);
536
598
  if (result) { content = result; updated.push('Resume File'); }
537
599
 
600
+ return content;
601
+ }, cwd);
602
+
538
603
  if (updated.length > 0) {
539
- writeStateMd(statePath, content, cwd);
540
604
  output({ recorded: true, updated }, raw, 'true');
541
605
  } else {
542
606
  output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
@@ -678,28 +742,40 @@ function buildStateFrontmatter(bodyContent, cwd) {
678
742
  try {
679
743
  const phasesDir = planningPaths(cwd).phases;
680
744
  if (fs.existsSync(phasesDir)) {
681
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
682
- const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
745
+ // Use cached disk scan when available — avoids N+1 readdirSync calls
746
+ // on repeated buildStateFrontmatter invocations within the same process (#1967)
747
+ let cached = _diskScanCache.get(cwd);
748
+ if (!cached) {
749
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
750
+ const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
683
751
  .filter(e => e.isDirectory()).map(e => e.name)
684
752
  .filter(isDirInMilestone);
685
- let diskTotalPlans = 0;
686
- let diskTotalSummaries = 0;
687
- let diskCompletedPhases = 0;
753
+ let diskTotalPlans = 0;
754
+ let diskTotalSummaries = 0;
755
+ let diskCompletedPhases = 0;
688
756
 
689
- for (const dir of phaseDirs) {
757
+ for (const dir of phaseDirs) {
690
758
  const files = fs.readdirSync(path.join(phasesDir, dir));
691
759
  const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
692
760
  const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
693
761
  diskTotalPlans += plans;
694
762
  diskTotalSummaries += summaries;
695
763
  if (plans > 0 && summaries >= plans) diskCompletedPhases++;
764
+ }
765
+ cached = {
766
+ totalPhases: isDirInMilestone.phaseCount > 0
767
+ ? Math.max(phaseDirs.length, isDirInMilestone.phaseCount)
768
+ : phaseDirs.length,
769
+ completedPhases: diskCompletedPhases,
770
+ totalPlans: diskTotalPlans,
771
+ completedPlans: diskTotalSummaries,
772
+ };
773
+ _diskScanCache.set(cwd, cached);
696
774
  }
697
- totalPhases = isDirInMilestone.phaseCount > 0
698
- ? Math.max(phaseDirs.length, isDirInMilestone.phaseCount)
699
- : phaseDirs.length;
700
- completedPhases = diskCompletedPhases;
701
- totalPlans = diskTotalPlans;
702
- completedPlans = diskTotalSummaries;
775
+ totalPhases = cached.totalPhases;
776
+ completedPhases = cached.completedPhases;
777
+ totalPlans = cached.totalPlans;
778
+ completedPlans = cached.completedPlans;
703
779
  }
704
780
  } catch { /* intentionally empty */ }
705
781
  }
@@ -805,6 +881,9 @@ function acquireStateLock(statePath) {
805
881
  const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
806
882
  fs.writeSync(fd, String(process.pid));
807
883
  fs.closeSync(fd);
884
+ // Register for exit-time cleanup so process.exit(1) inside a locked region
885
+ // cannot leave a stale lock file (#1916).
886
+ _heldStateLocks.add(lockPath);
808
887
  return lockPath;
809
888
  } catch (err) {
810
889
  if (err.code === 'EEXIST') {
@@ -821,8 +900,7 @@ function acquireStateLock(statePath) {
821
900
  return lockPath;
822
901
  }
823
902
  const jitter = Math.floor(Math.random() * 50);
824
- const start = Date.now();
825
- while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
903
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, retryDelay + jitter);
826
904
  continue;
827
905
  }
828
906
  return lockPath; // non-EEXIST error — proceed without lock
@@ -832,6 +910,7 @@ function acquireStateLock(statePath) {
832
910
  }
833
911
 
834
912
  function releaseStateLock(lockPath) {
913
+ _heldStateLocks.delete(lockPath);
835
914
  try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
836
915
  }
837
916
 
@@ -842,10 +921,14 @@ function releaseStateLock(lockPath) {
842
921
  * each other's changes (race condition with read-modify-write cycle).
843
922
  */
844
923
  function writeStateMd(statePath, content, cwd) {
924
+ // Invalidate disk scan cache before computing new frontmatter — the write
925
+ // may create new PLAN/SUMMARY files that buildStateFrontmatter must see.
926
+ // Safe for any calling pattern, not just short-lived CLI processes (#1967).
927
+ if (cwd) _diskScanCache.delete(cwd);
845
928
  const synced = syncStateFrontmatter(content, cwd);
846
929
  const lockPath = acquireStateLock(statePath);
847
930
  try {
848
- fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
931
+ atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
849
932
  } finally {
850
933
  releaseStateLock(lockPath);
851
934
  }
@@ -863,7 +946,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd) {
863
946
  const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
864
947
  const modified = transformFn(content);
865
948
  const synced = syncStateFrontmatter(modified, cwd);
866
- fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
949
+ atomicWriteFileSync(statePath, normalizeMd(synced), 'utf-8');
867
950
  } finally {
868
951
  releaseStateLock(lockPath);
869
952
  }
@@ -913,10 +996,10 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
913
996
  return;
914
997
  }
915
998
 
916
- let content = fs.readFileSync(statePath, 'utf-8');
917
999
  const today = new Date().toISOString().split('T')[0];
918
1000
  const updated = [];
919
1001
 
1002
+ readModifyWriteStateMd(statePath, (content) => {
920
1003
  // Update Status field
921
1004
  const statusValue = `Executing Phase ${phaseNumber}`;
922
1005
  let result = stateReplaceField(content, 'Status', statusValue);
@@ -937,8 +1020,8 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
937
1020
 
938
1021
  // Update Current Phase Name
939
1022
  if (phaseName) {
940
- result = stateReplaceField(content, 'Current Phase Name', phaseName);
941
- if (result) { content = result; updated.push('Current Phase Name'); }
1023
+ result = stateReplaceField(content, 'Current Phase Name', phaseName);
1024
+ if (result) { content = result; updated.push('Current Phase Name'); }
942
1025
  }
943
1026
 
944
1027
  // Update Current Plan to 1 (starting from the first plan)
@@ -947,16 +1030,16 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
947
1030
 
948
1031
  // Update Total Plans in Phase
949
1032
  if (planCount) {
950
- result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
951
- if (result) { content = result; updated.push('Total Plans in Phase'); }
1033
+ result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
1034
+ if (result) { content = result; updated.push('Total Plans in Phase'); }
952
1035
  }
953
1036
 
954
1037
  // Update **Current focus:** body text line (#1104)
955
1038
  const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
956
1039
  const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
957
1040
  if (focusPattern.test(content)) {
958
- content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
959
- updated.push('Current focus');
1041
+ content = content.replace(focusPattern, (_match, prefix) => `${prefix}${focusLabel}`);
1042
+ updated.push('Current focus');
960
1043
  }
961
1044
 
962
1045
  // Update ## Current Position section (#1104, #1365)
@@ -965,44 +1048,43 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
965
1048
  const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
966
1049
  const positionMatch = content.match(positionPattern);
967
1050
  if (positionMatch) {
968
- const header = positionMatch[1];
969
- let posBody = positionMatch[2];
1051
+ const header = positionMatch[1];
1052
+ let posBody = positionMatch[2];
970
1053
 
971
- // Update or insert Phase line
972
- const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
973
- if (/^Phase:/m.test(posBody)) {
1054
+ // Update or insert Phase line
1055
+ const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
1056
+ if (/^Phase:/m.test(posBody)) {
974
1057
  posBody = posBody.replace(/^Phase:.*$/m, newPhase);
975
- } else {
1058
+ } else {
976
1059
  posBody = newPhase + '\n' + posBody;
977
- }
1060
+ }
978
1061
 
979
- // Update or insert Plan line
980
- const newPlan = `Plan: 1 of ${planCount || '?'}`;
981
- if (/^Plan:/m.test(posBody)) {
1062
+ // Update or insert Plan line
1063
+ const newPlan = `Plan: 1 of ${planCount || '?'}`;
1064
+ if (/^Plan:/m.test(posBody)) {
982
1065
  posBody = posBody.replace(/^Plan:.*$/m, newPlan);
983
- } else {
1066
+ } else {
984
1067
  posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
985
- }
1068
+ }
986
1069
 
987
- // Update Status line if present
988
- const newStatus = `Status: Executing Phase ${phaseNumber}`;
989
- if (/^Status:/m.test(posBody)) {
1070
+ // Update Status line if present
1071
+ const newStatus = `Status: Executing Phase ${phaseNumber}`;
1072
+ if (/^Status:/m.test(posBody)) {
990
1073
  posBody = posBody.replace(/^Status:.*$/m, newStatus);
991
- }
1074
+ }
992
1075
 
993
- // Update Last activity line if present
994
- const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
995
- if (/^Last activity:/im.test(posBody)) {
1076
+ // Update Last activity line if present
1077
+ const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
1078
+ if (/^Last activity:/im.test(posBody)) {
996
1079
  posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
997
- }
1080
+ }
998
1081
 
999
- content = content.replace(positionPattern, `${header}${posBody}`);
1000
- updated.push('Current Position');
1082
+ content = content.replace(positionPattern, `${header}${posBody}`);
1083
+ updated.push('Current Position');
1001
1084
  }
1002
1085
 
1003
- if (updated.length > 0) {
1004
- writeStateMd(statePath, content, cwd);
1005
- }
1086
+ return content;
1087
+ }, cwd);
1006
1088
 
1007
1089
  output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
1008
1090
  }
@@ -1325,11 +1407,193 @@ function cmdStateSync(cwd, options, raw) {
1325
1407
  output({ synced: true, changes, dry_run: false }, raw);
1326
1408
  }
1327
1409
 
1410
+ /**
1411
+ * Prune old entries from STATE.md sections that grow unboundedly (#1970).
1412
+ * Moves decisions, recently-completed summaries, and resolved blockers
1413
+ * older than keepRecent phases to STATE-ARCHIVE.md.
1414
+ *
1415
+ * Options:
1416
+ * keepRecent: number of recent phases to retain (default: 3)
1417
+ * dryRun: if true, return what would be pruned without modifying STATE.md
1418
+ */
1419
+ function cmdStatePrune(cwd, options, raw) {
1420
+ const silent = !!options.silent;
1421
+ const emit = silent ? () => {} : (result, r, v) => output(result, r, v);
1422
+ const statePath = planningPaths(cwd).state;
1423
+ if (!fs.existsSync(statePath)) { emit({ error: 'STATE.md not found' }, raw); return; }
1424
+
1425
+ const keepRecent = parseInt(options.keepRecent, 10) || 3;
1426
+ const dryRun = !!options.dryRun;
1427
+ const currentPhaseRaw = stateExtractField(fs.readFileSync(statePath, 'utf-8'), 'Current Phase');
1428
+ const currentPhase = parseInt(currentPhaseRaw, 10) || 0;
1429
+ const cutoff = currentPhase - keepRecent;
1430
+
1431
+ if (cutoff <= 0) {
1432
+ emit({ pruned: false, reason: `Only ${currentPhase} phases — nothing to prune with --keep-recent ${keepRecent}` }, raw, 'false');
1433
+ return;
1434
+ }
1435
+
1436
+ const archivePath = path.join(path.dirname(statePath), 'STATE-ARCHIVE.md');
1437
+ const archived = [];
1438
+
1439
+ // Shared pruning logic applied to both dry-run and real passes.
1440
+ // Returns { newContent, archivedSections }.
1441
+ function prunePass(content) {
1442
+ const sections = [];
1443
+
1444
+ // Prune Decisions section: entries like "- [Phase N]: ..."
1445
+ const decisionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
1446
+ const decMatch = content.match(decisionPattern);
1447
+ if (decMatch) {
1448
+ const lines = decMatch[2].split('\n');
1449
+ const keep = [];
1450
+ const archive = [];
1451
+ for (const line of lines) {
1452
+ const phaseMatch = line.match(/^\s*-\s*\[Phase\s+(\d+)/i);
1453
+ if (phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
1454
+ archive.push(line);
1455
+ } else {
1456
+ keep.push(line);
1457
+ }
1458
+ }
1459
+ if (archive.length > 0) {
1460
+ sections.push({ section: 'Decisions', count: archive.length, lines: archive });
1461
+ content = content.replace(decisionPattern, (_m, header) => `${header}${keep.join('\n')}`);
1462
+ }
1463
+ }
1464
+
1465
+ // Prune Recently Completed section: entries mentioning phase numbers
1466
+ const recentPattern = /(###?\s*Recently Completed\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
1467
+ const recMatch = content.match(recentPattern);
1468
+ if (recMatch) {
1469
+ const lines = recMatch[2].split('\n');
1470
+ const keep = [];
1471
+ const archive = [];
1472
+ for (const line of lines) {
1473
+ const phaseMatch = line.match(/Phase\s+(\d+)/i);
1474
+ if (phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
1475
+ archive.push(line);
1476
+ } else {
1477
+ keep.push(line);
1478
+ }
1479
+ }
1480
+ if (archive.length > 0) {
1481
+ sections.push({ section: 'Recently Completed', count: archive.length, lines: archive });
1482
+ content = content.replace(recentPattern, (_m, header) => `${header}${keep.join('\n')}`);
1483
+ }
1484
+ }
1485
+
1486
+ // Prune resolved blockers: lines marked as resolved (strikethrough ~~text~~
1487
+ // or "[RESOLVED]" prefix) with a phase reference older than cutoff
1488
+ const blockersPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Blockers\s*&\s*Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
1489
+ const blockersMatch = content.match(blockersPattern);
1490
+ if (blockersMatch) {
1491
+ const lines = blockersMatch[2].split('\n');
1492
+ const keep = [];
1493
+ const archive = [];
1494
+ for (const line of lines) {
1495
+ const isResolved = /~~.*~~|\[RESOLVED\]/i.test(line);
1496
+ const phaseMatch = line.match(/Phase\s+(\d+)/i);
1497
+ if (isResolved && phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
1498
+ archive.push(line);
1499
+ } else {
1500
+ keep.push(line);
1501
+ }
1502
+ }
1503
+ if (archive.length > 0) {
1504
+ sections.push({ section: 'Blockers (resolved)', count: archive.length, lines: archive });
1505
+ content = content.replace(blockersPattern, (_m, header) => `${header}${keep.join('\n')}`);
1506
+ }
1507
+ }
1508
+
1509
+ // Prune Performance Metrics table rows: keep only rows for phases > cutoff.
1510
+ // Preserves header rows (| Phase | ... and |---|...) and any prose around the table.
1511
+ const metricsPattern = /(###?\s*Performance Metrics\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
1512
+ const metricsMatch = content.match(metricsPattern);
1513
+ if (metricsMatch) {
1514
+ const sectionLines = metricsMatch[2].split('\n');
1515
+ const keep = [];
1516
+ const archive = [];
1517
+ for (const line of sectionLines) {
1518
+ // Table data row: starts with | followed by a number (phase)
1519
+ const tableRowMatch = line.match(/^\|\s*(\d+)\s*\|/);
1520
+ if (tableRowMatch) {
1521
+ const rowPhase = parseInt(tableRowMatch[1], 10);
1522
+ if (rowPhase <= cutoff) {
1523
+ archive.push(line);
1524
+ } else {
1525
+ keep.push(line);
1526
+ }
1527
+ } else {
1528
+ // Header row, separator row, or prose — always keep
1529
+ keep.push(line);
1530
+ }
1531
+ }
1532
+ if (archive.length > 0) {
1533
+ sections.push({ section: 'Performance Metrics', count: archive.length, lines: archive });
1534
+ content = content.replace(metricsPattern, (_m, header) => `${header}${keep.join('\n')}`);
1535
+ }
1536
+ }
1537
+
1538
+ return { newContent: content, archivedSections: sections };
1539
+ }
1540
+
1541
+ if (dryRun) {
1542
+ // Dry-run: compute what would be pruned without writing anything
1543
+ const content = fs.readFileSync(statePath, 'utf-8');
1544
+ const result = prunePass(content);
1545
+ const totalPruned = result.archivedSections.reduce((sum, s) => sum + s.count, 0);
1546
+ emit({
1547
+ pruned: false,
1548
+ dry_run: true,
1549
+ cutoff_phase: cutoff,
1550
+ keep_recent: keepRecent,
1551
+ sections: result.archivedSections.map(s => ({ section: s.section, entries_would_archive: s.count })),
1552
+ total_would_archive: totalPruned,
1553
+ note: totalPruned > 0 ? 'Run without --dry-run to actually prune' : 'Nothing to prune',
1554
+ }, raw, totalPruned > 0 ? 'true' : 'false');
1555
+ return;
1556
+ }
1557
+
1558
+ readModifyWriteStateMd(statePath, (content) => {
1559
+ const result = prunePass(content);
1560
+ archived.push(...result.archivedSections);
1561
+ return result.newContent;
1562
+ }, cwd);
1563
+
1564
+ // Write archived entries to STATE-ARCHIVE.md
1565
+ if (archived.length > 0) {
1566
+ const timestamp = new Date().toISOString().split('T')[0];
1567
+ let archiveContent = '';
1568
+ if (fs.existsSync(archivePath)) {
1569
+ archiveContent = fs.readFileSync(archivePath, 'utf-8');
1570
+ } else {
1571
+ archiveContent = '# STATE Archive\n\nPruned entries from STATE.md. Recoverable but no longer loaded into agent context.\n\n';
1572
+ }
1573
+ archiveContent += `## Pruned ${timestamp} (phases 1-${cutoff}, kept recent ${keepRecent})\n\n`;
1574
+ for (const section of archived) {
1575
+ archiveContent += `### ${section.section}\n\n${section.lines.join('\n')}\n\n`;
1576
+ }
1577
+ atomicWriteFileSync(archivePath, archiveContent);
1578
+ }
1579
+
1580
+ const totalPruned = archived.reduce((sum, s) => sum + s.count, 0);
1581
+ emit({
1582
+ pruned: totalPruned > 0,
1583
+ cutoff_phase: cutoff,
1584
+ keep_recent: keepRecent,
1585
+ sections: archived.map(s => ({ section: s.section, entries_archived: s.count })),
1586
+ total_archived: totalPruned,
1587
+ archive_file: totalPruned > 0 ? 'STATE-ARCHIVE.md' : null,
1588
+ }, raw, totalPruned > 0 ? 'true' : 'false');
1589
+ }
1590
+
1328
1591
  module.exports = {
1329
1592
  stateExtractField,
1330
1593
  stateReplaceField,
1331
1594
  stateReplaceFieldWithFallback,
1332
1595
  writeStateMd,
1596
+ readModifyWriteStateMd,
1333
1597
  updatePerformanceMetricsSection,
1334
1598
  cmdStateLoad,
1335
1599
  cmdStateGet,
@@ -1348,6 +1612,7 @@ module.exports = {
1348
1612
  cmdStatePlannedPhase,
1349
1613
  cmdStateValidate,
1350
1614
  cmdStateSync,
1615
+ cmdStatePrune,
1351
1616
  cmdSignalWaiting,
1352
1617
  cmdSignalResume,
1353
1618
  };