@opengsd/gsd-core 1.11.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 (498) 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-code-fixer.md +1 -1
  5. package/agents/gsd-debug-session-manager.md +1 -1
  6. package/agents/gsd-debugger.md +1 -1
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +78 -42
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +0 -1
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +3 -1
  15. package/agents/gsd-plan-checker.md +91 -112
  16. package/agents/gsd-planner.md +20 -4
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +82 -7
  21. package/agents/gsd-ui-researcher.md +70 -3
  22. package/agents/gsd-verifier.md +24 -2
  23. package/bin/install.js +847 -200
  24. package/commands/gsd/discuss-phase.md +1 -1
  25. package/commands/gsd/execute-phase.md +1 -1
  26. package/commands/gsd/import.md +1 -1
  27. package/commands/gsd/ns-workflow.md +2 -1
  28. package/commands/gsd/phase.md +1 -1
  29. package/commands/gsd/quick-batch.md +105 -0
  30. package/commands/gsd/quick.md +8 -4
  31. package/commands/gsd/surface.md +18 -8
  32. package/gsd-core/bin/gsd-tools.cjs +761 -100
  33. package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
  34. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  35. package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
  36. package/gsd-core/bin/lib/api-coverage.cjs +30 -9
  37. package/gsd-core/bin/lib/artifacts.cjs +2 -0
  38. package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
  39. package/gsd-core/bin/lib/audit.cjs +163 -41
  40. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  41. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  42. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  43. package/gsd-core/bin/lib/capability-registry.cjs +785 -144
  44. package/gsd-core/bin/lib/capability-state.cjs +25 -4
  45. package/gsd-core/bin/lib/capability-validator.cjs +321 -18
  46. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  47. package/gsd-core/bin/lib/check-command-router.cjs +229 -6
  48. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  49. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  50. package/gsd-core/bin/lib/clusters.cjs +1 -0
  51. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  52. package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
  53. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  54. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  55. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  56. package/gsd-core/bin/lib/commands.cjs +877 -54
  57. package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
  58. package/gsd-core/bin/lib/config-loader.cjs +121 -29
  59. package/gsd-core/bin/lib/config.cjs +92 -2
  60. package/gsd-core/bin/lib/configuration.cjs +129 -37
  61. package/gsd-core/bin/lib/core-utils.cjs +118 -14
  62. package/gsd-core/bin/lib/decisions.cjs +213 -1
  63. package/gsd-core/bin/lib/edge-probe.cjs +23 -2
  64. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  65. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  66. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  67. package/gsd-core/bin/lib/frontmatter.cjs +975 -326
  68. package/gsd-core/bin/lib/gap-checker.cjs +41 -8
  69. package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  72. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
  73. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  74. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
  75. package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
  76. package/gsd-core/bin/lib/host-integration.cjs +96 -11
  77. package/gsd-core/bin/lib/init-command-router.cjs +132 -21
  78. package/gsd-core/bin/lib/init.cjs +252 -56
  79. package/gsd-core/bin/lib/install-engine.cjs +252 -15
  80. package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
  81. package/gsd-core/bin/lib/install-profiles.cjs +100 -18
  82. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  83. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  84. package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
  85. package/gsd-core/bin/lib/intel.cjs +101 -26
  86. package/gsd-core/bin/lib/io.cjs +195 -15
  87. package/gsd-core/bin/lib/learnings.cjs +85 -14
  88. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  89. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  90. package/gsd-core/bin/lib/markdown-table.cjs +175 -4
  91. package/gsd-core/bin/lib/milestone.cjs +112 -7
  92. package/gsd-core/bin/lib/model-catalog.cjs +177 -19
  93. package/gsd-core/bin/lib/model-resolver.cjs +10 -28
  94. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  95. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  96. package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
  97. package/gsd-core/bin/lib/phase-id.cjs +321 -13
  98. package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
  99. package/gsd-core/bin/lib/phase-locator.cjs +138 -17
  100. package/gsd-core/bin/lib/phase.cjs +1175 -115
  101. package/gsd-core/bin/lib/plan-document.cjs +273 -0
  102. package/gsd-core/bin/lib/plan-scan.cjs +13 -2
  103. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  104. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  105. package/gsd-core/bin/lib/planning-snapshot.cjs +165 -34
  106. package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
  107. package/gsd-core/bin/lib/probe-core.cjs +4 -1
  108. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  109. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  110. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  111. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  112. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  113. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  114. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
  115. package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
  116. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  117. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  118. package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
  119. package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
  120. package/gsd-core/bin/lib/roadmap.cjs +248 -64
  121. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
  122. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  123. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
  124. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
  125. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  126. package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
  127. package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
  128. package/gsd-core/bin/lib/smart-entry.cjs +19 -31
  129. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  130. package/gsd-core/bin/lib/state-command-router.cjs +47 -18
  131. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  132. package/gsd-core/bin/lib/state-document.cjs +216 -5
  133. package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
  134. package/gsd-core/bin/lib/state-transition.cjs +850 -145
  135. package/gsd-core/bin/lib/state.cjs +1629 -287
  136. package/gsd-core/bin/lib/surface.cjs +33 -10
  137. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  138. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  139. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  140. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  141. package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
  142. package/gsd-core/bin/lib/uat.cjs +2542 -387
  143. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  144. package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
  145. package/gsd-core/bin/lib/unusable-input.cjs +13 -0
  146. package/gsd-core/bin/lib/update-context.cjs +6 -2
  147. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  148. package/gsd-core/bin/lib/validate.cjs +230 -12
  149. package/gsd-core/bin/lib/vendor/README.md +43 -5
  150. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  151. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  152. package/gsd-core/bin/lib/verification.cjs +287 -13
  153. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  154. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  155. package/gsd-core/bin/lib/verify.cjs +441 -56
  156. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  157. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  158. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  159. package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
  160. package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
  161. package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
  162. package/gsd-core/bin/shared/exit-codes.json +8 -0
  163. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  164. package/gsd-core/bin/shared/model-catalog.json +8 -1
  165. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  166. package/gsd-core/references/agent-contracts.md +6 -5
  167. package/gsd-core/references/api-coverage.md +24 -2
  168. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  169. package/gsd-core/references/checkpoints.md +37 -19
  170. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  171. package/gsd-core/references/edge-probe.md +17 -5
  172. package/gsd-core/references/execute-mvp-tdd.md +18 -18
  173. package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
  174. package/gsd-core/references/execute-phase-response-language.md +6 -0
  175. package/gsd-core/references/execute-phase-wave-guard.md +11 -9
  176. package/gsd-core/references/executor-examples.md +42 -0
  177. package/gsd-core/references/failing-direction.md +78 -0
  178. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  179. package/gsd-core/references/gate-prompts.md +1 -1
  180. package/gsd-core/references/git-integration.md +5 -5
  181. package/gsd-core/references/git-planning-commit.md +3 -3
  182. package/gsd-core/references/gsd-run-resolver.md +1 -1
  183. package/gsd-core/references/loop-hook-dispatch.md +22 -0
  184. package/gsd-core/references/model-profiles.md +1 -1
  185. package/gsd-core/references/mvp-concepts.md +2 -2
  186. package/gsd-core/references/nyquist-compliance.md +74 -0
  187. package/gsd-core/references/offer-next.md +3 -5
  188. package/gsd-core/references/phase-argument-parsing.md +3 -3
  189. package/gsd-core/references/plan-checker-examples.md +41 -0
  190. package/gsd-core/references/planner-antipatterns.md +25 -0
  191. package/gsd-core/references/planner-chunked.md +5 -1
  192. package/gsd-core/references/planner-coupling.md +42 -0
  193. package/gsd-core/references/planner-failing-direction.md +53 -0
  194. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  195. package/gsd-core/references/planner-quick-batch.md +71 -0
  196. package/gsd-core/references/planner-reviews.md +47 -0
  197. package/gsd-core/references/planner-revision.md +76 -3
  198. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  199. package/gsd-core/references/planning-config.md +39 -9
  200. package/gsd-core/references/response-language-directive.md +9 -0
  201. package/gsd-core/references/reviewer-instances.md +31 -0
  202. package/gsd-core/references/revision-loop.md +118 -11
  203. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  204. package/gsd-core/references/tdd.md +15 -12
  205. package/gsd-core/references/ui-brand.md +65 -21
  206. package/gsd-core/references/ui-consideration-probe.md +1 -1
  207. package/gsd-core/references/universal-anti-patterns.md +2 -2
  208. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  209. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  210. package/gsd-core/references/verify-mvp-mode.md +1 -1
  211. package/gsd-core/references/workstream-flag.md +11 -11
  212. package/gsd-core/templates/README.md +1 -1
  213. package/gsd-core/templates/SECURITY.md +3 -3
  214. package/gsd-core/templates/UI-SPEC.md +25 -3
  215. package/gsd-core/templates/VALIDATION.md +3 -3
  216. package/gsd-core/templates/phase-prompt.md +7 -0
  217. package/gsd-core/templates/state.md +7 -0
  218. package/gsd-core/templates/verification-report.md +5 -0
  219. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  220. package/gsd-core/workflows/add-backlog.md +3 -1
  221. package/gsd-core/workflows/add-phase.md +5 -3
  222. package/gsd-core/workflows/add-tests.md +4 -9
  223. package/gsd-core/workflows/add-todo.md +2 -2
  224. package/gsd-core/workflows/ai-integration-phase.md +5 -10
  225. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  226. package/gsd-core/workflows/audit-fix.md +14 -3
  227. package/gsd-core/workflows/audit-milestone.md +11 -9
  228. package/gsd-core/workflows/audit-uat.md +19 -2
  229. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  230. package/gsd-core/workflows/autonomous.md +12 -26
  231. package/gsd-core/workflows/check-todos.md +2 -2
  232. package/gsd-core/workflows/cleanup.md +3 -3
  233. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
  234. package/gsd-core/workflows/code-review-fix.md +3 -1
  235. package/gsd-core/workflows/code-review.md +192 -69
  236. package/gsd-core/workflows/complete-milestone.md +28 -14
  237. package/gsd-core/workflows/debug.md +6 -4
  238. package/gsd-core/workflows/diagnose-issues.md +17 -7
  239. package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
  240. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  241. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  242. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  243. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  244. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
  245. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  246. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  247. package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
  248. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  249. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  250. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  251. package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
  252. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  253. package/gsd-core/workflows/discuss-phase.md +2 -2
  254. package/gsd-core/workflows/do.md +46 -19
  255. package/gsd-core/workflows/docs-update.md +6 -5
  256. package/gsd-core/workflows/edit-phase.md +3 -1
  257. package/gsd-core/workflows/eval-review.md +5 -10
  258. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
  259. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
  260. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  261. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  262. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  263. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
  264. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  265. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  266. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +4 -2
  267. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  268. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  269. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  270. package/gsd-core/workflows/execute-phase.md +68 -66
  271. package/gsd-core/workflows/execute-plan.md +25 -20
  272. package/gsd-core/workflows/explore.md +3 -1
  273. package/gsd-core/workflows/extract-learnings.md +3 -1
  274. package/gsd-core/workflows/fast.md +8 -2
  275. package/gsd-core/workflows/forensics.md +3 -1
  276. package/gsd-core/workflows/graduation.md +6 -6
  277. package/gsd-core/workflows/health.md +4 -7
  278. package/gsd-core/workflows/help/modes/brief.md +2 -0
  279. package/gsd-core/workflows/help/modes/default.md +2 -0
  280. package/gsd-core/workflows/help/modes/full.md +12 -0
  281. package/gsd-core/workflows/help/modes/topic.md +2 -0
  282. package/gsd-core/workflows/help.md +2 -0
  283. package/gsd-core/workflows/import.md +17 -14
  284. package/gsd-core/workflows/inbox.md +5 -6
  285. package/gsd-core/workflows/ingest-docs.md +45 -12
  286. package/gsd-core/workflows/insert-phase.md +7 -5
  287. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  288. package/gsd-core/workflows/list-seeds.md +7 -3
  289. package/gsd-core/workflows/list-workspaces.md +3 -1
  290. package/gsd-core/workflows/manager.md +15 -26
  291. package/gsd-core/workflows/map-codebase.md +3 -1
  292. package/gsd-core/workflows/milestone-summary.md +3 -1
  293. package/gsd-core/workflows/mvp-phase.md +3 -3
  294. package/gsd-core/workflows/new-milestone.md +10 -22
  295. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  296. package/gsd-core/workflows/new-project.md +17 -29
  297. package/gsd-core/workflows/new-workspace.md +2 -2
  298. package/gsd-core/workflows/next.md +4 -2
  299. package/gsd-core/workflows/node-repair.md +2 -0
  300. package/gsd-core/workflows/note.md +2 -0
  301. package/gsd-core/workflows/onboard.md +1 -1
  302. package/gsd-core/workflows/pause-work.md +20 -5
  303. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  304. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  305. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
  306. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
  307. package/gsd-core/workflows/plan-phase.md +251 -54
  308. package/gsd-core/workflows/plan-review-convergence.md +148 -19
  309. package/gsd-core/workflows/plant-seed.md +3 -3
  310. package/gsd-core/workflows/pr-branch.md +195 -51
  311. package/gsd-core/workflows/profile-user.md +17 -15
  312. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  313. package/gsd-core/workflows/progress.md +52 -15
  314. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  315. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
  316. package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
  317. package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
  318. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  319. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  320. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  321. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  322. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  323. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  324. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  325. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  326. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  327. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  328. package/gsd-core/workflows/quick-batch.md +203 -0
  329. package/gsd-core/workflows/quick.md +33 -32
  330. package/gsd-core/workflows/reapply-patches.md +2 -0
  331. package/gsd-core/workflows/remove-phase.md +6 -4
  332. package/gsd-core/workflows/remove-workspace.md +3 -3
  333. package/gsd-core/workflows/resume-project.md +14 -14
  334. package/gsd-core/workflows/review.md +404 -21
  335. package/gsd-core/workflows/scan.md +3 -1
  336. package/gsd-core/workflows/section-manifest.json +12 -0
  337. package/gsd-core/workflows/secure-phase.md +3 -3
  338. package/gsd-core/workflows/session-report.md +2 -0
  339. package/gsd-core/workflows/settings-advanced.md +9 -9
  340. package/gsd-core/workflows/settings-integrations.md +66 -32
  341. package/gsd-core/workflows/settings.md +4 -6
  342. package/gsd-core/workflows/ship.md +22 -16
  343. package/gsd-core/workflows/sketch-wrap-up.md +13 -17
  344. package/gsd-core/workflows/sketch.md +13 -19
  345. package/gsd-core/workflows/smart-entry.md +4 -6
  346. package/gsd-core/workflows/spec-phase.md +31 -4
  347. package/gsd-core/workflows/spike-wrap-up.md +9 -11
  348. package/gsd-core/workflows/spike.md +21 -32
  349. package/gsd-core/workflows/stats.md +4 -2
  350. package/gsd-core/workflows/sync-skills.md +13 -5
  351. package/gsd-core/workflows/thread.md +13 -7
  352. package/gsd-core/workflows/transition.md +7 -5
  353. package/gsd-core/workflows/ui-phase.md +36 -21
  354. package/gsd-core/workflows/ui-review.md +7 -11
  355. package/gsd-core/workflows/ultraplan-phase.md +7 -13
  356. package/gsd-core/workflows/undo.md +9 -17
  357. package/gsd-core/workflows/update.md +47 -48
  358. package/gsd-core/workflows/validate-phase.md +3 -3
  359. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  360. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  361. package/gsd-core/workflows/verify-work.md +106 -21
  362. package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
  363. package/hooks/dist/gsd-check-update-worker.js +19 -2
  364. package/hooks/dist/gsd-config-reload.js +18 -12
  365. package/hooks/dist/gsd-context-monitor.js +302 -22
  366. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  367. package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
  368. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  369. package/hooks/dist/gsd-cursor-stop.js +2 -1
  370. package/hooks/dist/gsd-cursor-subagent-start.js +28 -23
  371. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
  372. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  373. package/hooks/dist/gsd-graphify-update.sh +22 -18
  374. package/hooks/dist/gsd-node-runner.sh +77 -0
  375. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  376. package/hooks/dist/gsd-prompt-guard.js +46 -12
  377. package/hooks/dist/gsd-read-guard.js +18 -7
  378. package/hooks/dist/gsd-read-injection-scanner.js +22 -13
  379. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  380. package/hooks/dist/gsd-session-state.sh +1 -0
  381. package/hooks/dist/gsd-statusline.js +222 -29
  382. package/hooks/dist/gsd-validate-commit.sh +523 -12
  383. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  384. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  385. package/hooks/dist/gsd-workflow-guard.js +36 -17
  386. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  387. package/hooks/dist/gsd-write-guard.js +35 -25
  388. package/hooks/dist/lib/cli-exit.js +560 -0
  389. package/hooks/dist/lib/exit-code-registry.js +98 -0
  390. package/hooks/dist/lib/git-cmd.js +210 -1
  391. package/hooks/dist/lib/git-probe.js +84 -0
  392. package/hooks/dist/lib/hook-exit.js +81 -0
  393. package/hooks/dist/lib/injection-patterns.js +36 -6
  394. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  395. package/hooks/gsd-agent-isolation-guard.js +77 -38
  396. package/hooks/gsd-check-update-worker.js +19 -2
  397. package/hooks/gsd-config-reload.js +18 -12
  398. package/hooks/gsd-context-monitor.js +302 -22
  399. package/hooks/gsd-cursor-post-tool.js +3 -1
  400. package/hooks/gsd-cursor-pre-tool.js +3 -1
  401. package/hooks/gsd-cursor-session-start.js +2 -1
  402. package/hooks/gsd-cursor-stop.js +2 -1
  403. package/hooks/gsd-cursor-subagent-start.js +28 -23
  404. package/hooks/gsd-cursor-subagent-stop.js +3 -1
  405. package/hooks/gsd-ensure-canonical-path.js +2 -1
  406. package/hooks/gsd-graphify-update.sh +22 -18
  407. package/hooks/gsd-node-runner.sh +77 -0
  408. package/hooks/gsd-phase-boundary.sh +1 -0
  409. package/hooks/gsd-prompt-guard.js +46 -12
  410. package/hooks/gsd-read-guard.js +18 -7
  411. package/hooks/gsd-read-injection-scanner.js +22 -13
  412. package/hooks/gsd-secret-read-guard.js +1079 -0
  413. package/hooks/gsd-session-state.sh +1 -0
  414. package/hooks/gsd-statusline.js +222 -29
  415. package/hooks/gsd-validate-commit.sh +523 -12
  416. package/hooks/gsd-windsurf-pre-command.js +16 -11
  417. package/hooks/gsd-windsurf-pre-write.js +22 -13
  418. package/hooks/gsd-workflow-guard.js +36 -17
  419. package/hooks/gsd-worktree-path-guard.js +36 -21
  420. package/hooks/gsd-write-guard.js +35 -25
  421. package/hooks/hooks.json +6 -0
  422. package/hooks/lib/cli-exit.js +560 -0
  423. package/hooks/lib/exit-code-registry.js +98 -0
  424. package/hooks/lib/git-cmd.js +210 -1
  425. package/hooks/lib/git-probe.js +84 -0
  426. package/hooks/lib/hook-exit.js +81 -0
  427. package/hooks/lib/injection-patterns.js +36 -6
  428. package/hooks/managed-hooks-registry.cjs +4 -0
  429. package/package.json +14 -9
  430. package/scripts/base64-scan.sh +74 -12
  431. package/scripts/build-hooks.js +12 -0
  432. package/scripts/check-glossary-refs.cjs +77 -15
  433. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  434. package/scripts/ci-check-job-near-cap.cjs +49 -0
  435. package/scripts/ci-pr-mergeability.cjs +262 -0
  436. package/scripts/ci-test-scope.cjs +52 -12
  437. package/scripts/ci-timeout-report.cjs +230 -0
  438. package/scripts/docs-guard-registry.cjs +406 -0
  439. package/scripts/gen-capability-registry.cjs +8 -6
  440. package/scripts/gen-exit-code-docs.cjs +318 -0
  441. package/scripts/gen-exit-code-registry.cjs +891 -0
  442. package/scripts/gen-features.cjs +836 -0
  443. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  444. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  445. package/scripts/gen-loop-host-contract.cjs +189 -4
  446. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  447. package/scripts/gen-state-md-docs.cjs +727 -0
  448. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  449. package/scripts/lib/ci-job-timing.cjs +72 -0
  450. package/scripts/lib/cli-exit.cjs +546 -44
  451. package/scripts/lib/drift-scan.cjs +32 -2
  452. package/scripts/lib/exit-code-registry.cjs +98 -0
  453. package/scripts/lib/ndjson-reporter.cjs +119 -0
  454. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  455. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  456. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  457. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  458. package/scripts/lint-docs-guard-registration.cjs +495 -0
  459. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
  460. package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
  461. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  462. package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
  463. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  464. package/scripts/lint-phase-enumeration-drift.cjs +45 -14
  465. package/scripts/lint-phase-id-drift.cjs +133 -8
  466. package/scripts/lint-planning-prompt-drift.cjs +38 -1
  467. package/scripts/lint-portable-grep.cjs +176 -0
  468. package/scripts/lint-removed-but-needed.cjs +184 -16
  469. package/scripts/lint-response-language-coverage.cjs +524 -0
  470. package/scripts/lint-seam-enforcement.cjs +182 -0
  471. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  472. package/scripts/lint-source-test-name-collision.cjs +241 -0
  473. package/scripts/lint-state-write-path-drift.cjs +337 -432
  474. package/scripts/lint-test-file-count.allowlist.json +124 -4
  475. package/scripts/lint-test-file-count.cjs +25 -3
  476. package/scripts/lint-unreachable-guard-drift.cjs +51 -64
  477. package/scripts/lint-vendored-deps.cjs +208 -35
  478. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  479. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  480. package/scripts/mutation-matrix.cjs +599 -50
  481. package/scripts/npm-audit-baseline.cjs +376 -0
  482. package/scripts/prompt-injection-scan.sh +83 -14
  483. package/scripts/require-issue-link-policy.cjs +16 -1
  484. package/scripts/secret-scan.sh +75 -13
  485. package/scripts/select-docs-guards.cjs +56 -0
  486. package/scripts/sync-runtime-launcher.cjs +22 -3
  487. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  488. package/skills/gsd-execute-phase/SKILL.md +1 -1
  489. package/skills/gsd-import/SKILL.md +1 -1
  490. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  491. package/skills/gsd-phase/SKILL.md +1 -1
  492. package/skills/gsd-quick/SKILL.md +8 -4
  493. package/skills/gsd-quick-batch/SKILL.md +105 -0
  494. package/skills/gsd-surface/SKILL.md +18 -8
  495. package/vscode/package.json +1 -1
  496. package/bin/lib/ui-safety-gate.cjs +0 -109
  497. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  498. package/scripts/state-write-path-drift-baseline.json +0 -19
@@ -0,0 +1,840 @@
1
+ "use strict";
2
+ /**
3
+ * Quick-Batch Core Primitives (#3675, part of epic #3344 / ADR-1239
4
+ * "Quick-batch binding").
5
+ *
6
+ * Pure/state primitives and CLI-testable core operations for batching
7
+ * several `/gsd:quick`-shaped tasks together: task-list parsing, collision-safe
8
+ * quick-ID preallocation, `BATCH.json` schema/validation/resume, deterministic
9
+ * dependency-DAG + file-overlap wave construction, and exactly-once STATE.md
10
+ * completion. NO agent dispatch, NO worktree creation, NO user-facing command —
11
+ * those are Phase 4 (#3676)'s job. This module only ADDS new call sites onto
12
+ * four existing, unmodified primitives:
13
+ *
14
+ * - `cmdInitQuick`'s quick-ID grammar (src/init.cts) — replicated here (NOT
15
+ * delegated to per-item, since that function's own 2-second granularity is
16
+ * not collision-safe under batch allocation; see `allocateQuickIds`).
17
+ * - `appendQuickTaskRow` (src/markdown-table.cts) — reused as-is, called at
18
+ * most once per quick id, gated by our own idempotency check (see
19
+ * `hasQuickTaskRow`) since the function itself carries no idempotency.
20
+ * - `withPlanningLock` (src/planning-workspace.cts) — reused as-is; every
21
+ * durable, cross-call collision-sensitive operation here
22
+ * (`createBatch`/`completeQuickItem`/`resumeBatch`) runs inside exactly
23
+ * ONE (never nested) `withPlanningLock` transaction.
24
+ * - `partitionByFileOverlap` (src/file-overlap-partitioner.cts, #3674) —
25
+ * reused as-is, called per dependency-DAG layer in `computeWaves`, over
26
+ * PRE-NORMALIZED `planned_files` (normalization happens at THIS module's
27
+ * boundary, never inside the Phase 2 helper — its own design lock).
28
+ *
29
+ * `BATCH.json` lives at `.planning/quick-batches/<batch-id>/BATCH.json` — a
30
+ * SIBLING of `.planning/quick/`, never inside it, so `scanQuickTasks`
31
+ * (src/audit.cts) never misreads a batch manifest as a broken quick task.
32
+ *
33
+ * ADR-457 build-at-publish: compiled by tsc to gsd-core/bin/lib/quick-batch.cjs.
34
+ */
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ const node_fs_1 = __importDefault(require("node:fs"));
39
+ const node_path_1 = __importDefault(require("node:path"));
40
+ const security_cjs_1 = require("./security.cjs");
41
+ const markdown_table_cjs_1 = require("./markdown-table.cjs");
42
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
43
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
44
+ const clock_cjs_1 = require("./clock.cjs");
45
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
46
+ const planningWorkspace = require("./planning-workspace.cjs");
47
+ const { withPlanningLock, planningDir, planningRoot, planningPaths, quickDirFrom } = planningWorkspace;
48
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
49
+ const fileOverlapPartitioner = require("./file-overlap-partitioner.cjs");
50
+ const { partitionByFileOverlap } = fileOverlapPartitioner;
51
+ // ─── Quick-id grammar (replicated from cmdInitQuick, unchanged) ───────────────
52
+ const QUICK_ID_RE = /^\d{6}-[0-9a-z]{3}$/;
53
+ const MAX_TIME_BLOCK = 36 * 36 * 36 - 1; // 3-char base36 ceiling (46655)
54
+ function idFromDateAndBlock(dateStr, block) {
55
+ return dateStr + '-' + block.toString(36).padStart(3, '0');
56
+ }
57
+ /** yyMMdd + starting time-block (floor(secondsSinceMidnight/2)) for `instant`. */
58
+ function dateAndStartBlock(instant) {
59
+ const yy = String(instant.getFullYear()).slice(-2);
60
+ const mm = String(instant.getMonth() + 1).padStart(2, '0');
61
+ const dd = String(instant.getDate()).padStart(2, '0');
62
+ const dateStr = yy + mm + dd;
63
+ const secondsSinceMidnight = instant.getHours() * 3600 + instant.getMinutes() * 60 + instant.getSeconds();
64
+ const startBlock = Math.floor(secondsSinceMidnight / 2);
65
+ return { dateStr, startBlock };
66
+ }
67
+ /**
68
+ * Generate `count` distinct `YYMMDD-xxx` ids starting at `startBlock`,
69
+ * advancing past any id already in `used` (mutated in place with the newly
70
+ * allocated ids too, so a second call sharing the same `used` set never
71
+ * re-issues one of them). Throws if the day's block space is exhausted
72
+ * (46656 ids/day — never reachable in practice, but a real fail-closed ceiling
73
+ * rather than an infinite loop).
74
+ */
75
+ function allocateIdsGivenUsed(dateStr, startBlock, count, used) {
76
+ const result = [];
77
+ let block = startBlock;
78
+ while (result.length < count) {
79
+ if (block > MAX_TIME_BLOCK) {
80
+ throw new Error(`quick-batch: exhausted collision-free quick ids for ${dateStr} (advanced past block ${MAX_TIME_BLOCK})`);
81
+ }
82
+ const candidate = idFromDateAndBlock(dateStr, block);
83
+ if (!used.has(candidate)) {
84
+ used.add(candidate);
85
+ result.push(candidate);
86
+ }
87
+ block++;
88
+ }
89
+ return result;
90
+ }
91
+ /** Existing quick ids already claimed on disk under `.planning/quick/`. */
92
+ function collectExistingQuickIds(quickDir) {
93
+ const usedIds = new Set();
94
+ let entries;
95
+ try {
96
+ entries = node_fs_1.default.readdirSync(quickDir);
97
+ }
98
+ catch {
99
+ return usedIds;
100
+ }
101
+ for (const name of entries) {
102
+ const m = /^(\d{6}-[0-9a-z]{3})(?:-|$)/.exec(name);
103
+ if (m)
104
+ usedIds.add(m[1]);
105
+ }
106
+ return usedIds;
107
+ }
108
+ /**
109
+ * Existing quick ids (and batch ids) already claimed by SIBLING batches under
110
+ * `.planning/quick-batches/*` — mutates `used` in place. This is what makes
111
+ * two concurrent (lock-serialized) `createBatch` calls collision-free even
112
+ * before either batch has dispatched any real `.planning/quick/<id>-<slug>`
113
+ * directory (#3675 design lock row 8/15): the second call's allocation scan
114
+ * sees the first call's already-PERSISTED `BATCH.json`, not just real
115
+ * dispatched directories. A corrupt/unreadable sibling manifest is skipped,
116
+ * never allowed to block a DIFFERENT batch's allocation.
117
+ */
118
+ function collectExistingBatchQuickIds(cwd, used) {
119
+ const batchesDir = node_path_1.default.join(planningDir(cwd), 'quick-batches');
120
+ let entries;
121
+ try {
122
+ entries = node_fs_1.default.readdirSync(batchesDir);
123
+ }
124
+ catch {
125
+ return;
126
+ }
127
+ for (const name of entries) {
128
+ const manifestPath = node_path_1.default.join(batchesDir, name, 'BATCH.json');
129
+ let raw;
130
+ try {
131
+ raw = node_fs_1.default.readFileSync(manifestPath, 'utf-8');
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ const parsed = (0, security_cjs_1.safeJsonParse)(raw, { maxLength: 1048576, label: 'sibling BATCH.json' });
137
+ if (!parsed.ok || !parsed.value || typeof parsed.value !== 'object')
138
+ continue;
139
+ const obj = parsed.value;
140
+ if (typeof obj.batch_id === 'string')
141
+ used.add(obj.batch_id);
142
+ if (Array.isArray(obj.items)) {
143
+ for (const it of obj.items) {
144
+ if (it && typeof it === 'object' && typeof it.quick_id === 'string') {
145
+ used.add(it.quick_id);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Allocate `count` distinct, collision-free `YYMMDD-xxx` quick ids under
153
+ * `withPlanningLock`, checked against on-disk `.planning/quick/` entries.
154
+ *
155
+ * NOTE: this primitive does NOT persist anything — it only reserves ids in
156
+ * memory for the duration of one call. Cross-call collision-freedom (#3675
157
+ * design lock row 8/15) is a property of `createBatch`, which allocates AND
158
+ * durably writes `BATCH.json` inside the SAME lock transaction; two bare
159
+ * `allocateQuickIds` calls with no intervening persistence can still overlap.
160
+ * Exposed standalone for direct grammar-level testability (rows 7/13/14).
161
+ */
162
+ function allocateQuickIds(cwd, count, options = {}) {
163
+ if (!Number.isInteger(count) || count < 1) {
164
+ return { ok: false, reason: `count must be a positive integer, got ${String(count)}` };
165
+ }
166
+ const clock = options.clock ?? clock_cjs_1.realClock;
167
+ try {
168
+ const ids = withPlanningLock(cwd, () => {
169
+ const quickDir = quickDirFrom(planningDir(cwd));
170
+ const used = collectExistingQuickIds(quickDir);
171
+ collectExistingBatchQuickIds(cwd, used);
172
+ const { dateStr, startBlock } = dateAndStartBlock(new Date(clock.now()));
173
+ return allocateIdsGivenUsed(dateStr, startBlock, count, used);
174
+ }, clock);
175
+ return { ok: true, value: ids };
176
+ }
177
+ catch (err) {
178
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
179
+ }
180
+ }
181
+ // ─── Task-list parsing ──────────────────────────────────────────────────────────
182
+ const BULLET_OR_NUMBER_RE = /^\s*(?:[-*]|\d+[.)])\s+(.+?)\s*$/;
183
+ /**
184
+ * Parse an inline bulleted (`-`/`*`) or numbered (`1.`/`1)`) task list.
185
+ * Non-matching lines (blank lines, prose) are ignored. Order is preserved;
186
+ * duplicate descriptions are preserved as distinct entries (never deduped).
187
+ * Requires at least 2 items (#3675/#3344 AC minimum for a "batch").
188
+ */
189
+ function parseTaskList(text) {
190
+ if (typeof text !== 'string') {
191
+ return { ok: false, reason: 'task list input must be a string' };
192
+ }
193
+ const items = [];
194
+ for (const line of text.split(/\r?\n/)) {
195
+ const m = BULLET_OR_NUMBER_RE.exec(line);
196
+ if (m)
197
+ items.push({ description: m[1] });
198
+ }
199
+ if (items.length < 2) {
200
+ return { ok: false, reason: `quick-batch requires at least 2 explicit tasks, found ${items.length}` };
201
+ }
202
+ return { ok: true, value: items };
203
+ }
204
+ /**
205
+ * Parse a task list from a file, strictly confined to the planning workspace
206
+ * root (`.planning/`) — traversal, symlink escape, and non-regular-file
207
+ * targets (directory, socket, device, FIFO) are all rejected.
208
+ */
209
+ function parseTaskListFromFile(cwd, filePath) {
210
+ const root = planningRoot(cwd);
211
+ let safePath;
212
+ try {
213
+ safePath = (0, security_cjs_1.requireSafePath)(filePath, root, 'quick-batch --file', { allowAbsolute: true });
214
+ }
215
+ catch (err) {
216
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
217
+ }
218
+ let stat;
219
+ try {
220
+ stat = node_fs_1.default.statSync(safePath);
221
+ }
222
+ catch (err) {
223
+ return { ok: false, reason: `unable to stat --file path: ${err instanceof Error ? err.message : String(err)}` };
224
+ }
225
+ if (!stat.isFile()) {
226
+ return { ok: false, reason: `--file path is not a regular file: ${filePath}` };
227
+ }
228
+ let content;
229
+ try {
230
+ content = node_fs_1.default.readFileSync(safePath, 'utf-8');
231
+ }
232
+ catch (err) {
233
+ return { ok: false, reason: `unable to read --file path: ${err instanceof Error ? err.message : String(err)}` };
234
+ }
235
+ return parseTaskList(content);
236
+ }
237
+ // ─── Dependency-DAG validation ──────────────────────────────────────────────────
238
+ function resolveDependencyIndex(ref, items) {
239
+ if (typeof ref === 'number') {
240
+ return Number.isInteger(ref) && ref >= 0 && ref < items.length ? ref : null;
241
+ }
242
+ if (typeof ref === 'string') {
243
+ const idx = items.findIndex((it) => it.clientId === ref);
244
+ return idx === -1 ? null : idx;
245
+ }
246
+ return null;
247
+ }
248
+ /**
249
+ * Validate that every `dependsOn` reference resolves to another item WITHIN
250
+ * this same input array (never an unknown/cross-batch reference), and that
251
+ * the resulting dependency graph is acyclic. Fails closed before any wave is
252
+ * built, per the #3675 design lock.
253
+ */
254
+ function validateDag(items) {
255
+ const adj = items.map(() => []);
256
+ for (let i = 0; i < items.length; i++) {
257
+ const deps = items[i].dependsOn ?? [];
258
+ for (const ref of deps) {
259
+ const idx = resolveDependencyIndex(ref, items);
260
+ if (idx === null) {
261
+ return { ok: false, reason: `item ${i} (${items[i].description}) declares an unknown dependency reference: ${JSON.stringify(ref)}` };
262
+ }
263
+ if (idx === i) {
264
+ return { ok: false, reason: `item ${i} (${items[i].description}) declares a dependency on itself` };
265
+ }
266
+ adj[i].push(idx);
267
+ }
268
+ }
269
+ const WHITE = 0, GRAY = 1, BLACK = 2;
270
+ const color = new Array(items.length).fill(WHITE);
271
+ const stack = [];
272
+ let cycleDiagnostic = null;
273
+ const dfs = (u) => {
274
+ color[u] = GRAY;
275
+ stack.push(u);
276
+ for (const v of adj[u]) {
277
+ if (color[v] === GRAY) {
278
+ const idx = stack.indexOf(v);
279
+ cycleDiagnostic = `dependency cycle detected: ${stack.slice(idx).concat(v).join(' -> ')}`;
280
+ return true;
281
+ }
282
+ if (color[v] === WHITE && dfs(v))
283
+ return true;
284
+ }
285
+ stack.pop();
286
+ color[u] = BLACK;
287
+ return false;
288
+ };
289
+ for (let i = 0; i < items.length; i++) {
290
+ if (color[i] === WHITE && dfs(i)) {
291
+ return { ok: false, reason: cycleDiagnostic ?? 'dependency cycle detected' };
292
+ }
293
+ }
294
+ return { ok: true, value: adj };
295
+ }
296
+ /** Reshape persisted `QuickBatchItem`s into `computeWaves`' input shape. */
297
+ function toWaveInput(items) {
298
+ return items.map((it) => ({ quickId: it.quick_id, dependsOn: it.depends_on, plannedFiles: it.planned_files }));
299
+ }
300
+ /**
301
+ * Deterministic wave construction: an item's wave is strictly after every one
302
+ * of its dependencies' waves (DAG readiness), and no two items in the same
303
+ * wave share a (normalized) file — reusing `partitionByFileOverlap` per
304
+ * dependency-DAG layer, over path-separator-normalized `plannedFiles`
305
+ * (normalization happens HERE, never inside `partitionByFileOverlap` itself,
306
+ * per Phase 2's own design lock). Same input order always yields the same
307
+ * waves. Fails closed on an unknown dependency or a cycle.
308
+ */
309
+ function computeWaves(items) {
310
+ const byId = new Map(items.map((it) => [it.quickId, it]));
311
+ for (const it of items) {
312
+ for (const dep of it.dependsOn) {
313
+ if (!byId.has(dep)) {
314
+ return { ok: false, reason: `item ${it.quickId} depends on unknown item ${dep}` };
315
+ }
316
+ }
317
+ }
318
+ const layer = new Map();
319
+ const state = new Map();
320
+ let cycleDiagnostic = null;
321
+ const visit = (id, pathStack) => {
322
+ const st = state.get(id) ?? 0;
323
+ if (st === 1) {
324
+ const idx = pathStack.indexOf(id);
325
+ cycleDiagnostic = `dependency cycle detected: ${pathStack.slice(idx).concat(id).join(' -> ')}`;
326
+ return -1;
327
+ }
328
+ if (st === 2)
329
+ return layer.get(id);
330
+ state.set(id, 1);
331
+ const it = byId.get(id);
332
+ let maxDepLayer = -1;
333
+ for (const dep of it.dependsOn) {
334
+ const depLayer = visit(dep, [...pathStack, id]);
335
+ if (depLayer === -1)
336
+ return -1;
337
+ if (depLayer > maxDepLayer)
338
+ maxDepLayer = depLayer;
339
+ }
340
+ const l = maxDepLayer + 1;
341
+ layer.set(id, l);
342
+ state.set(id, 2);
343
+ return l;
344
+ };
345
+ for (const it of items) {
346
+ if (visit(it.quickId, []) === -1) {
347
+ return { ok: false, reason: cycleDiagnostic ?? 'dependency cycle detected' };
348
+ }
349
+ }
350
+ const maxLayer = items.length ? Math.max(...items.map((it) => layer.get(it.quickId))) : -1;
351
+ const waves = [];
352
+ for (let l = 0; l <= maxLayer; l++) {
353
+ const layerItems = items.filter((it) => layer.get(it.quickId) === l);
354
+ const overlapInput = layerItems.map((it) => ({
355
+ id: it.quickId,
356
+ files: it.plannedFiles.map(shell_command_projection_cjs_1.posixNormalize),
357
+ }));
358
+ const stages = partitionByFileOverlap(overlapInput);
359
+ for (const stage of stages)
360
+ waves.push(stage);
361
+ }
362
+ return { ok: true, value: waves };
363
+ }
364
+ // ─── BATCH.json creation ────────────────────────────────────────────────────────
365
+ function batchManifestPath(cwd, batchId) {
366
+ return node_path_1.default.join(planningDir(cwd), 'quick-batches', batchId, 'BATCH.json');
367
+ }
368
+ /**
369
+ * Create a new quick-batch: validates the dependency DAG, allocates N+1
370
+ * collision-free quick ids (one for the batch itself, one per item) and
371
+ * durably writes `BATCH.json` — all inside ONE `withPlanningLock` transaction,
372
+ * so concurrent `createBatch` calls can never collide (#3675 design lock).
373
+ * `BATCH.json` lives at `.planning/quick-batches/<batch-id>/BATCH.json`, a
374
+ * SIBLING of `.planning/quick/` — never inside it (scanQuickTasks safety).
375
+ */
376
+ function createBatch(cwd, itemsInput, options = {}) {
377
+ if (!Array.isArray(itemsInput) || itemsInput.length === 0) {
378
+ return { ok: false, reason: 'createBatch requires at least one item' };
379
+ }
380
+ const dagResult = validateDag(itemsInput);
381
+ if (!dagResult.ok)
382
+ return dagResult;
383
+ const clock = options.clock ?? clock_cjs_1.realClock;
384
+ try {
385
+ return withPlanningLock(cwd, () => {
386
+ const quickDir = quickDirFrom(planningDir(cwd));
387
+ const used = collectExistingQuickIds(quickDir);
388
+ collectExistingBatchQuickIds(cwd, used);
389
+ const { dateStr, startBlock } = dateAndStartBlock(new Date(clock.now()));
390
+ const allocated = allocateIdsGivenUsed(dateStr, startBlock, itemsInput.length + 1, used);
391
+ const batchId = allocated[0];
392
+ const quickIds = allocated.slice(1);
393
+ const items = itemsInput.map((input, i) => ({
394
+ quick_id: quickIds[i],
395
+ client_id: input.clientId ?? null,
396
+ description: input.description,
397
+ status: 'pending',
398
+ depends_on: (input.dependsOn ?? []).map((ref) => {
399
+ const idx = resolveDependencyIndex(ref, itemsInput);
400
+ return quickIds[idx];
401
+ }),
402
+ planned_files: (input.plannedFiles ?? []).map(shell_command_projection_cjs_1.posixNormalize),
403
+ directory: input.directory ?? null,
404
+ worktree: input.worktree ?? null,
405
+ dispatched_worktree: input.dispatchedWorktree ?? null,
406
+ dispatched_branch: input.dispatchedBranch ?? null,
407
+ dispatched_base: input.dispatchedBase ?? null,
408
+ wave: -1,
409
+ commit: null,
410
+ failure_reason: null,
411
+ }));
412
+ const wavesResult = computeWaves(toWaveInput(items));
413
+ if (!wavesResult.ok)
414
+ return wavesResult;
415
+ const waveOf = new Map();
416
+ wavesResult.value.forEach((wave, idx) => {
417
+ for (const quickId of wave)
418
+ waveOf.set(quickId, idx);
419
+ });
420
+ for (const it of items)
421
+ it.wave = waveOf.get(it.quick_id);
422
+ const manifest = {
423
+ schema_version: 1,
424
+ batch_id: batchId,
425
+ created_at: clock.nowIso(),
426
+ options: options.batchOptions ?? {},
427
+ base_revision: options.baseRevision ?? null,
428
+ items,
429
+ };
430
+ const manifestPath = batchManifestPath(cwd, batchId);
431
+ (0, shell_command_projection_cjs_1.platformWriteSync)(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
432
+ return { ok: true, value: { batchId, manifestPath, manifest } };
433
+ }, clock);
434
+ }
435
+ catch (err) {
436
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
437
+ }
438
+ }
439
+ // ─── BATCH.json loading + schema validation ─────────────────────────────────────
440
+ const VALID_STATUSES = new Set(['pending', 'complete', 'failed', 'blocked']);
441
+ const SAFE_BATCH_ID_RE = /^[0-9a-zA-Z-]+$/;
442
+ function validateBatchSchema(parsed, batchId) {
443
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
444
+ return { ok: false, reason: `BATCH.json for batch ${batchId} is not a JSON object` };
445
+ }
446
+ const obj = parsed;
447
+ if (obj.schema_version !== 1) {
448
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has an unsupported or missing schema_version` };
449
+ }
450
+ if (typeof obj.batch_id !== 'string' || obj.batch_id !== batchId) {
451
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has a mismatched or missing batch_id` };
452
+ }
453
+ if (typeof obj.created_at !== 'string') {
454
+ return { ok: false, reason: `BATCH.json for batch ${batchId} is missing created_at` };
455
+ }
456
+ if (obj.options !== undefined && (typeof obj.options !== 'object' || obj.options === null || Array.isArray(obj.options))) {
457
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has an invalid options field` };
458
+ }
459
+ const optionsValue = obj.options ?? {};
460
+ if (obj.base_revision !== null && obj.base_revision !== undefined && typeof obj.base_revision !== 'string') {
461
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has an invalid base_revision field` };
462
+ }
463
+ const baseRevisionValue = typeof obj.base_revision === 'string' ? obj.base_revision : null;
464
+ if (!Array.isArray(obj.items) || obj.items.length === 0) {
465
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has no items` };
466
+ }
467
+ const items = [];
468
+ const seenIds = new Set();
469
+ for (const raw of obj.items) {
470
+ if (!raw || typeof raw !== 'object') {
471
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has a malformed item` };
472
+ }
473
+ const it = raw;
474
+ if (typeof it.quick_id !== 'string' || !QUICK_ID_RE.test(it.quick_id)) {
475
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has an item with an invalid quick_id` };
476
+ }
477
+ if (seenIds.has(it.quick_id)) {
478
+ return { ok: false, reason: `BATCH.json for batch ${batchId} has a duplicate quick_id: ${it.quick_id}` };
479
+ }
480
+ seenIds.add(it.quick_id);
481
+ if (typeof it.description !== 'string') {
482
+ return { ok: false, reason: `item ${it.quick_id} is missing description` };
483
+ }
484
+ if (typeof it.status !== 'string' || !VALID_STATUSES.has(it.status)) {
485
+ return { ok: false, reason: `item ${it.quick_id} has an invalid or missing status` };
486
+ }
487
+ if (!Array.isArray(it.depends_on) || !it.depends_on.every((d) => typeof d === 'string')) {
488
+ return { ok: false, reason: `item ${it.quick_id} has a malformed depends_on` };
489
+ }
490
+ if (!Array.isArray(it.planned_files) || !it.planned_files.every((f) => typeof f === 'string')) {
491
+ return { ok: false, reason: `item ${it.quick_id} has a malformed planned_files` };
492
+ }
493
+ if (it.directory !== null && it.directory !== undefined && typeof it.directory !== 'string') {
494
+ return { ok: false, reason: `item ${it.quick_id} has an invalid directory field` };
495
+ }
496
+ if (it.worktree !== null && it.worktree !== undefined && typeof it.worktree !== 'string') {
497
+ return { ok: false, reason: `item ${it.quick_id} has an invalid worktree field` };
498
+ }
499
+ if (typeof it.worktree === 'string' && it.worktree !== '' && !node_fs_1.default.existsSync(it.worktree)) {
500
+ return { ok: false, reason: `item ${it.quick_id} references a worktree that does not exist on disk: ${it.worktree}` };
501
+ }
502
+ // #3677: dispatched_worktree/branch/base deliberately carry NO
503
+ // existence check (unlike `worktree` above) — they must stay readable
504
+ // after a legitimate post-merge worktree removal.
505
+ if (it.dispatched_worktree !== null && it.dispatched_worktree !== undefined && typeof it.dispatched_worktree !== 'string') {
506
+ return { ok: false, reason: `item ${it.quick_id} has an invalid dispatched_worktree field` };
507
+ }
508
+ if (it.dispatched_branch !== null && it.dispatched_branch !== undefined && typeof it.dispatched_branch !== 'string') {
509
+ return { ok: false, reason: `item ${it.quick_id} has an invalid dispatched_branch field` };
510
+ }
511
+ if (it.dispatched_base !== null && it.dispatched_base !== undefined && typeof it.dispatched_base !== 'string') {
512
+ return { ok: false, reason: `item ${it.quick_id} has an invalid dispatched_base field` };
513
+ }
514
+ if (it.wave !== undefined && (typeof it.wave !== 'number' || !Number.isInteger(it.wave) || it.wave < 0)) {
515
+ return { ok: false, reason: `item ${it.quick_id} has an invalid wave field` };
516
+ }
517
+ if (it.commit !== null && it.commit !== undefined && typeof it.commit !== 'string') {
518
+ return { ok: false, reason: `item ${it.quick_id} has an invalid commit field` };
519
+ }
520
+ if (it.failure_reason !== null && it.failure_reason !== undefined && typeof it.failure_reason !== 'string') {
521
+ return { ok: false, reason: `item ${it.quick_id} has an invalid failure_reason field` };
522
+ }
523
+ items.push({
524
+ quick_id: it.quick_id,
525
+ client_id: typeof it.client_id === 'string' ? it.client_id : null,
526
+ description: it.description,
527
+ status: it.status,
528
+ depends_on: it.depends_on,
529
+ planned_files: it.planned_files,
530
+ directory: typeof it.directory === 'string' ? it.directory : null,
531
+ worktree: typeof it.worktree === 'string' ? it.worktree : null,
532
+ dispatched_worktree: typeof it.dispatched_worktree === 'string' ? it.dispatched_worktree : null,
533
+ dispatched_branch: typeof it.dispatched_branch === 'string' ? it.dispatched_branch : null,
534
+ dispatched_base: typeof it.dispatched_base === 'string' ? it.dispatched_base : null,
535
+ wave: typeof it.wave === 'number' ? it.wave : -1,
536
+ commit: typeof it.commit === 'string' ? it.commit : null,
537
+ failure_reason: typeof it.failure_reason === 'string' ? it.failure_reason : null,
538
+ });
539
+ }
540
+ for (const it of items) {
541
+ for (const dep of it.depends_on) {
542
+ if (!seenIds.has(dep)) {
543
+ return { ok: false, reason: `item ${it.quick_id} depends on ${dep}, which is not present in this batch` };
544
+ }
545
+ }
546
+ }
547
+ const cycleCheck = computeWaves(toWaveInput(items));
548
+ if (!cycleCheck.ok)
549
+ return cycleCheck;
550
+ return {
551
+ ok: true,
552
+ value: {
553
+ schema_version: 1,
554
+ batch_id: batchId,
555
+ created_at: obj.created_at,
556
+ options: optionsValue,
557
+ base_revision: baseRevisionValue,
558
+ items,
559
+ },
560
+ };
561
+ }
562
+ /**
563
+ * Load and schema-validate `BATCH.json` for `batchId`. Fails closed —
564
+ * never guesses partial state — on: missing file, corrupt/truncated JSON,
565
+ * a schema violation, an out-of-batch dependency reference, a dependency
566
+ * cycle, or an item referencing a worktree path absent from disk.
567
+ */
568
+ function loadBatch(cwd, batchId) {
569
+ if (typeof batchId !== 'string' || batchId === '' || !SAFE_BATCH_ID_RE.test(batchId)) {
570
+ return { ok: false, reason: `invalid batch id: ${String(batchId)}` };
571
+ }
572
+ const manifestPath = batchManifestPath(cwd, batchId);
573
+ let raw;
574
+ try {
575
+ raw = node_fs_1.default.readFileSync(manifestPath, 'utf-8');
576
+ }
577
+ catch (err) {
578
+ const e = err;
579
+ return {
580
+ ok: false,
581
+ reason: e.code === 'ENOENT'
582
+ ? `no BATCH.json found for batch ${batchId}`
583
+ : `unable to read BATCH.json for batch ${batchId}: ${e.message}`,
584
+ };
585
+ }
586
+ const parsed = (0, security_cjs_1.safeJsonParse)(raw, { maxLength: 1048576 });
587
+ if (!parsed.ok) {
588
+ return { ok: false, reason: `BATCH.json for batch ${batchId} is not valid JSON: ${parsed.error ?? 'unknown parse error'}` };
589
+ }
590
+ return validateBatchSchema(parsed.value, batchId);
591
+ }
592
+ // ─── Exactly-once STATE.md completion ───────────────────────────────────────────
593
+ const QUICK_TASKS_HEADING_RE = /^quick tasks completed\b/i;
594
+ /**
595
+ * Idempotency check backing exactly-once STATE completion: does a "Quick
596
+ * Tasks Completed" table row already carry this `quickId` in its `#` column?
597
+ * Mirrors `appendQuickTaskRow`'s own section/table resolution (heading match
598
+ * -> parseMarkdownTable -> matchTableSchema) without modifying it, so the two
599
+ * can never silently diverge on what counts as "the" Quick Tasks table.
600
+ */
601
+ function hasQuickTaskRow(stateContent, quickId) {
602
+ if (!stateContent)
603
+ return false;
604
+ const sections = (0, markdown_sectionizer_cjs_1.collectSections)(stateContent, (h) => QUICK_TASKS_HEADING_RE.test(h.text.trim()));
605
+ for (const section of sections) {
606
+ const parsed = (0, markdown_table_cjs_1.parseMarkdownTable)(section.body);
607
+ if (!parsed.ok)
608
+ continue;
609
+ const match = (0, markdown_table_cjs_1.matchTableSchema)(parsed.value.columns);
610
+ if (!match || match.id !== 'QuickTasks')
611
+ continue;
612
+ for (const row of parsed.value.rows) {
613
+ if (row['#'] === quickId)
614
+ return true;
615
+ }
616
+ }
617
+ return false;
618
+ }
619
+ /**
620
+ * Mark one batch item complete: appends exactly one STATE.md "Quick Tasks
621
+ * Completed" row (idempotency key = `quickId`, checked via `hasQuickTaskRow`
622
+ * BEFORE calling `appendQuickTaskRow` — that function itself has no
623
+ * idempotency of its own), then marks the item `complete` in `BATCH.json`.
624
+ * Both steps run inside ONE `withPlanningLock` transaction. A repeat call for
625
+ * an already-complete item is a no-op (no re-append, no re-write).
626
+ */
627
+ function completeQuickItem(cwd, batchId, quickId, fields, options = {}) {
628
+ const clock = options.clock ?? clock_cjs_1.realClock;
629
+ try {
630
+ return withPlanningLock(cwd, () => {
631
+ const loaded = loadBatch(cwd, batchId);
632
+ if (!loaded.ok)
633
+ return loaded;
634
+ const manifest = loaded.value;
635
+ const item = manifest.items.find((it) => it.quick_id === quickId);
636
+ if (!item)
637
+ return { ok: false, reason: `batch ${batchId} has no item ${quickId}` };
638
+ const statePath = planningPaths(cwd).state;
639
+ const stateContent = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : '';
640
+ let appended = false;
641
+ if (!hasQuickTaskRow(stateContent, quickId)) {
642
+ const appendResult = (0, markdown_table_cjs_1.appendQuickTaskRow)(stateContent, {
643
+ description: fields.description,
644
+ date: fields.date,
645
+ commit: fields.commit,
646
+ directory: fields.directory,
647
+ quickId,
648
+ });
649
+ if (!appendResult.ok)
650
+ return appendResult;
651
+ (0, shell_command_projection_cjs_1.platformWriteSync)(statePath, appendResult.value.content);
652
+ appended = true;
653
+ }
654
+ if (item.status !== 'complete') {
655
+ item.status = 'complete';
656
+ item.commit = fields.commit;
657
+ (0, shell_command_projection_cjs_1.platformWriteSync)(batchManifestPath(cwd, batchId), JSON.stringify(manifest, null, 2) + '\n');
658
+ }
659
+ return { ok: true, value: { appended, manifest } };
660
+ }, clock);
661
+ }
662
+ catch (err) {
663
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
664
+ }
665
+ }
666
+ /**
667
+ * Persist post-planning `depends_on`/`planned_files` updates and recompute
668
+ * `wave` for every item (Phase 4 / #3676, resolving design doc Open
669
+ * Question 1's "no mutator exists" gap as ONE additive export on this
670
+ * module — never a second, independent writer against the same
671
+ * `BATCH.json`). Runs inside ONE `withPlanningLock` transaction, reusing
672
+ * the exact `loadBatch` -> mutate -> `computeWaves` -> `platformWriteSync`
673
+ * shape `resumeBatch`/`completeQuickItem` already use.
674
+ *
675
+ * Fails closed WITHOUT persisting anything when an update references an
676
+ * unknown `quickId`, an unknown/self dependency, or the resulting graph has
677
+ * a cycle — `computeWaves` (reused, not duplicated) is the single source of
678
+ * truth for that validation, exactly as it is at `createBatch` time.
679
+ */
680
+ function updateBatchItems(cwd, batchId, updates, options = {}) {
681
+ const clock = options.clock ?? clock_cjs_1.realClock;
682
+ try {
683
+ return withPlanningLock(cwd, () => {
684
+ const loaded = loadBatch(cwd, batchId);
685
+ if (!loaded.ok)
686
+ return loaded;
687
+ const manifest = loaded.value;
688
+ const byId = new Map(manifest.items.map((it) => [it.quick_id, it]));
689
+ for (const update of updates) {
690
+ const item = byId.get(update.quickId);
691
+ if (!item) {
692
+ return { ok: false, reason: `batch ${batchId} has no item ${update.quickId}` };
693
+ }
694
+ if (update.dependsOn !== undefined) {
695
+ for (const dep of update.dependsOn) {
696
+ if (dep === update.quickId) {
697
+ return { ok: false, reason: `item ${update.quickId} declares a dependency on itself` };
698
+ }
699
+ if (!byId.has(dep)) {
700
+ return { ok: false, reason: `item ${update.quickId} declares an unknown dependency reference: ${JSON.stringify(dep)}` };
701
+ }
702
+ }
703
+ item.depends_on = [...update.dependsOn];
704
+ }
705
+ if (update.plannedFiles !== undefined) {
706
+ item.planned_files = update.plannedFiles.map(shell_command_projection_cjs_1.posixNormalize);
707
+ }
708
+ // #3677: durable worktree-recovery fields — persisted verbatim, no
709
+ // existence/shape validation (opaque identifiers this module never
710
+ // interprets, same as `commit`). Explicit `null` clears a field
711
+ // (post-merge); `undefined` leaves it untouched, same convention
712
+ // `dependsOn`/`plannedFiles` above already use.
713
+ if (update.dispatchedWorktree !== undefined) {
714
+ item.dispatched_worktree = update.dispatchedWorktree;
715
+ }
716
+ if (update.dispatchedBranch !== undefined) {
717
+ item.dispatched_branch = update.dispatchedBranch;
718
+ }
719
+ if (update.dispatchedBase !== undefined) {
720
+ item.dispatched_base = update.dispatchedBase;
721
+ }
722
+ }
723
+ const wavesResult = computeWaves(toWaveInput(manifest.items));
724
+ if (!wavesResult.ok)
725
+ return wavesResult;
726
+ const waveOf = new Map();
727
+ wavesResult.value.forEach((wave, idx) => {
728
+ for (const quickId of wave)
729
+ waveOf.set(quickId, idx);
730
+ });
731
+ for (const it of manifest.items)
732
+ it.wave = waveOf.get(it.quick_id);
733
+ (0, shell_command_projection_cjs_1.platformWriteSync)(batchManifestPath(cwd, batchId), JSON.stringify(manifest, null, 2) + '\n');
734
+ return { ok: true, value: { manifest } };
735
+ }, clock);
736
+ }
737
+ catch (err) {
738
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
739
+ }
740
+ }
741
+ /**
742
+ * Resume a batch: skips `complete` items, leaves `failed` items failed (no
743
+ * auto-retry), leaves `blocked` items blocked unless their dependency's
744
+ * outcome changed, and — crash-window safety — detects a STATE.md row that
745
+ * already exists (by `quickId`, via `hasQuickTaskRow`) for a non-complete
746
+ * item and marks it complete WITHOUT re-appending (the "STATE row written,
747
+ * BATCH.json not yet updated" crash window). Idempotent: two calls in a row
748
+ * on an unchanged manifest produce zero transitions the second time.
749
+ *
750
+ * Runs inside `withPlanningLock` — the same durable read-modify-write
751
+ * `BATCH.json` uses — so a resume racing a concurrent `completeQuickItem` (or
752
+ * another resume) can never lose an update.
753
+ *
754
+ * When `options.currentBaseRevision` is supplied and the manifest carries a
755
+ * non-null `base_revision` that differs from it, resume refuses with a
756
+ * recoverable diagnostic rather than guessing past the divergence (ADR-1239
757
+ * "Quick-batch binding" § Base divergence). Reconciling the divergence — a
758
+ * rebase, a fresh batch — is a caller (Phase 4) decision; this primitive only
759
+ * detects and reports it. Omit the option to skip the check entirely.
760
+ */
761
+ function resumeBatch(cwd, batchId, options = {}) {
762
+ const clock = options.clock ?? clock_cjs_1.realClock;
763
+ try {
764
+ return withPlanningLock(cwd, () => {
765
+ const loaded = loadBatch(cwd, batchId);
766
+ if (!loaded.ok)
767
+ return loaded;
768
+ const manifest = loaded.value;
769
+ if (options.currentBaseRevision !== undefined
770
+ && manifest.base_revision !== null
771
+ && manifest.base_revision !== options.currentBaseRevision) {
772
+ return {
773
+ ok: false,
774
+ reason: `batch ${batchId} base revision diverged: created against ${manifest.base_revision}, current is ${options.currentBaseRevision}`,
775
+ };
776
+ }
777
+ const statePath = planningPaths(cwd).state;
778
+ const stateContent = node_fs_1.default.existsSync(statePath) ? node_fs_1.default.readFileSync(statePath, 'utf-8') : '';
779
+ const transitions = [];
780
+ const byId = new Map(manifest.items.map((it) => [it.quick_id, it]));
781
+ // Crash-window detection: a non-complete item whose STATE row already
782
+ // exists is completed without re-appending.
783
+ for (const it of manifest.items) {
784
+ if (it.status !== 'complete' && hasQuickTaskRow(stateContent, it.quick_id)) {
785
+ transitions.push({ quickId: it.quick_id, from: it.status, to: 'complete' });
786
+ it.status = 'complete';
787
+ }
788
+ }
789
+ // Propagate blocked/failed along the DAG to a fixed point (bounded by
790
+ // item count) so transitive blocking resolves in one resume call. A
791
+ // `blocked` item whose dependency outcome improved reverts to `pending`
792
+ // (eligible for re-evaluation); a `failed` item is never touched (no
793
+ // auto-retry).
794
+ let changed = true;
795
+ let iterations = 0;
796
+ while (changed && iterations <= manifest.items.length) {
797
+ changed = false;
798
+ iterations++;
799
+ for (const it of manifest.items) {
800
+ if (it.status === 'complete' || it.status === 'failed')
801
+ continue;
802
+ const anyDepBad = it.depends_on.some((d) => {
803
+ const dep = byId.get(d);
804
+ return dep !== undefined && (dep.status === 'failed' || dep.status === 'blocked');
805
+ });
806
+ const nextStatus = anyDepBad ? 'blocked' : (it.status === 'blocked' ? 'pending' : it.status);
807
+ if (nextStatus !== it.status) {
808
+ transitions.push({ quickId: it.quick_id, from: it.status, to: nextStatus });
809
+ it.status = nextStatus;
810
+ changed = true;
811
+ }
812
+ }
813
+ }
814
+ const eligible = manifest.items
815
+ .filter((it) => it.status === 'pending' && it.depends_on.every((d) => byId.get(d)?.status === 'complete'))
816
+ .map((it) => it.quick_id);
817
+ if (transitions.length > 0) {
818
+ (0, shell_command_projection_cjs_1.platformWriteSync)(batchManifestPath(cwd, batchId), JSON.stringify(manifest, null, 2) + '\n');
819
+ }
820
+ return { ok: true, value: { eligible, transitions, manifest } };
821
+ }, clock);
822
+ }
823
+ catch (err) {
824
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
825
+ }
826
+ }
827
+ module.exports = {
828
+ parseTaskList,
829
+ parseTaskListFromFile,
830
+ allocateQuickIds,
831
+ allocateIdsGivenUsed,
832
+ MAX_TIME_BLOCK,
833
+ createBatch,
834
+ loadBatch,
835
+ computeWaves,
836
+ resumeBatch,
837
+ completeQuickItem,
838
+ hasQuickTaskRow,
839
+ updateBatchItems,
840
+ };