@opengsd/gsd-core 1.14.0 → 1.16.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 (551) 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 +85 -5
  4. package/README.ja-JP.md +3 -3
  5. package/README.ko-KR.md +3 -3
  6. package/README.pt-BR.md +3 -3
  7. package/README.zh-CN.md +3 -3
  8. package/agents/gsd-code-fixer.compact.md +7 -6
  9. package/agents/gsd-code-fixer.md +9 -8
  10. package/agents/gsd-code-reviewer.compact.md +5 -3
  11. package/agents/gsd-code-reviewer.md +8 -6
  12. package/agents/gsd-debug-session-manager.compact.md +17 -2
  13. package/agents/gsd-debug-session-manager.md +17 -2
  14. package/agents/gsd-debugger.md +3 -3
  15. package/agents/gsd-eval-auditor.compact.md +1 -1
  16. package/agents/gsd-eval-auditor.md +1 -1
  17. package/agents/gsd-executor.md +17 -12
  18. package/agents/gsd-intel-updater.compact.md +1 -1
  19. package/agents/gsd-intel-updater.md +1 -1
  20. package/agents/gsd-mempalace-curator.md +2 -2
  21. package/agents/gsd-phase-researcher.md +19 -11
  22. package/agents/gsd-plan-checker.md +15 -9
  23. package/agents/gsd-planner.md +15 -11
  24. package/agents/gsd-project-researcher.compact.md +1 -1
  25. package/agents/gsd-project-researcher.md +1 -1
  26. package/agents/gsd-research-synthesizer.compact.md +1 -1
  27. package/agents/gsd-research-synthesizer.md +1 -1
  28. package/agents/gsd-ui-auditor.compact.md +21 -30
  29. package/agents/gsd-ui-auditor.md +166 -37
  30. package/agents/gsd-ui-researcher.compact.md +1 -1
  31. package/agents/gsd-ui-researcher.md +1 -1
  32. package/agents/gsd-verifier.md +37 -14
  33. package/bin/install.js +764 -287
  34. package/commands/gsd/add-tests.md +6 -1
  35. package/commands/gsd/ai-integration-phase.md +6 -1
  36. package/commands/gsd/audit-fix.md +5 -0
  37. package/commands/gsd/audit-milestone.md +6 -1
  38. package/commands/gsd/autonomous.md +7 -2
  39. package/commands/gsd/capture.md +9 -5
  40. package/commands/gsd/code-review.md +7 -2
  41. package/commands/gsd/complete-milestone.md +4 -0
  42. package/commands/gsd/config.md +7 -3
  43. package/commands/gsd/debug.md +11 -7
  44. package/commands/gsd/discuss-phase.md +7 -3
  45. package/commands/gsd/docs-update.md +12 -7
  46. package/commands/gsd/eval-review.md +6 -1
  47. package/commands/gsd/execute-phase.md +12 -7
  48. package/commands/gsd/extract-learnings.md +5 -0
  49. package/commands/gsd/fast.md +4 -0
  50. package/commands/gsd/forensics.md +5 -1
  51. package/commands/gsd/graphify.md +10 -6
  52. package/commands/gsd/health.md +5 -0
  53. package/commands/gsd/help.md +7 -2
  54. package/commands/gsd/import.md +7 -3
  55. package/commands/gsd/inbox.md +5 -0
  56. package/commands/gsd/ingest-docs.md +5 -1
  57. package/commands/gsd/manager.md +6 -1
  58. package/commands/gsd/map-codebase.md +7 -3
  59. package/commands/gsd/mempalace-capture.md +12 -4
  60. package/commands/gsd/mempalace-recall.md +5 -1
  61. package/commands/gsd/milestone-summary.md +5 -1
  62. package/commands/gsd/mvp-phase.md +8 -3
  63. package/commands/gsd/new-milestone.md +6 -1
  64. package/commands/gsd/new-project.md +5 -0
  65. package/commands/gsd/next.md +6 -1
  66. package/commands/gsd/ns-context.md +4 -0
  67. package/commands/gsd/ns-ideate.md +4 -0
  68. package/commands/gsd/ns-manage.md +4 -0
  69. package/commands/gsd/ns-project.md +4 -0
  70. package/commands/gsd/ns-review.md +4 -0
  71. package/commands/gsd/ns-workflow.md +4 -0
  72. package/commands/gsd/onboard.md +6 -1
  73. package/commands/gsd/pause-work.md +5 -1
  74. package/commands/gsd/phase.md +8 -4
  75. package/commands/gsd/plan-phase.md +6 -1
  76. package/commands/gsd/plan-review-convergence.md +11 -7
  77. package/commands/gsd/pr-branch.md +4 -0
  78. package/commands/gsd/profile-user.md +5 -1
  79. package/commands/gsd/progress.md +7 -2
  80. package/commands/gsd/quick-batch.md +21 -9
  81. package/commands/gsd/quick.md +12 -7
  82. package/commands/gsd/review.md +7 -4
  83. package/commands/gsd/secure-phase.md +6 -1
  84. package/commands/gsd/ship.md +5 -0
  85. package/commands/gsd/sketch.md +7 -2
  86. package/commands/gsd/spec-phase.md +5 -1
  87. package/commands/gsd/spike.md +8 -3
  88. package/commands/gsd/surface.md +5 -1
  89. package/commands/gsd/thread.md +4 -0
  90. package/commands/gsd/ui-phase.md +6 -1
  91. package/commands/gsd/ui-review.md +6 -1
  92. package/commands/gsd/ultraplan-phase.md +5 -1
  93. package/commands/gsd/undo.md +5 -1
  94. package/commands/gsd/update.md +6 -2
  95. package/commands/gsd/validate-phase.md +6 -1
  96. package/commands/gsd/verify-work.md +6 -1
  97. package/commands/gsd/workspace.md +7 -3
  98. package/gsd-core/bin/gsd-tools.cjs +477 -78
  99. package/gsd-core/bin/lib/active-workstream-store.cjs +15 -0
  100. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  101. package/gsd-core/bin/lib/agent-install-check.cjs +4 -1
  102. package/gsd-core/bin/lib/audit.cjs +144 -42
  103. package/gsd-core/bin/lib/broken-windows.cjs +13 -13
  104. package/gsd-core/bin/lib/capability-activation.cjs +9 -4
  105. package/gsd-core/bin/lib/capability-registry.cjs +197 -222
  106. package/gsd-core/bin/lib/capability-validator.cjs +16 -1
  107. package/gsd-core/bin/lib/check-auto-mode.cjs +35 -0
  108. package/gsd-core/bin/lib/check-command-router.cjs +164 -1625
  109. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +13 -2
  110. package/gsd-core/bin/lib/cli-exit.cjs +12 -0
  111. package/gsd-core/bin/lib/codex-agent-toml.cjs +32 -33
  112. package/gsd-core/bin/lib/command-aliases.cjs +7 -0
  113. package/gsd-core/bin/lib/command-routing-hub.cjs +48 -1
  114. package/gsd-core/bin/lib/commands.cjs +343 -207
  115. package/gsd-core/bin/lib/complexity-trigger.cjs +8 -7
  116. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  117. package/gsd-core/bin/lib/config.cjs +76 -19
  118. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  119. package/gsd-core/bin/lib/coverage.cjs +4 -8
  120. package/gsd-core/bin/lib/decision-coverage-support.cjs +259 -0
  121. package/gsd-core/bin/lib/decisions.cjs +30 -14
  122. package/gsd-core/bin/lib/drift.cjs +177 -42
  123. package/gsd-core/bin/lib/frontmatter-fence.cjs +90 -0
  124. package/gsd-core/bin/lib/frontmatter-splice.cjs +494 -0
  125. package/gsd-core/bin/lib/frontmatter.cjs +426 -234
  126. package/gsd-core/bin/lib/gap-checker.cjs +72 -29
  127. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +381 -0
  128. package/gsd-core/bin/lib/gate-args.cjs +53 -0
  129. package/gsd-core/bin/lib/gate-codebase-drift.cjs +285 -0
  130. package/gsd-core/bin/lib/gate-config.cjs +46 -0
  131. package/gsd-core/bin/lib/gate-context-drift.cjs +141 -0
  132. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +169 -0
  133. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +126 -0
  134. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +555 -0
  135. package/gsd-core/bin/lib/gate-evidence.cjs +138 -0
  136. package/gsd-core/bin/lib/gate-exit.cjs +27 -0
  137. package/gsd-core/bin/lib/gate-gap-analysis-plan-post.cjs +61 -0
  138. package/gsd-core/bin/lib/gate-phase-context.cjs +170 -0
  139. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +1 -1
  140. package/gsd-core/bin/lib/gate-predicate.cjs +165 -0
  141. package/gsd-core/bin/lib/gate-prohibition-enforcement.cjs +94 -0
  142. package/gsd-core/bin/lib/gate-schema-drift.cjs +165 -0
  143. package/gsd-core/bin/lib/gate-tdd-red-evidence.cjs +100 -0
  144. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +182 -0
  145. package/gsd-core/bin/lib/gate-ui-plan.cjs +86 -0
  146. package/gsd-core/bin/lib/gate-ui-safety.cjs +80 -0
  147. package/gsd-core/bin/lib/gate-verdict.cjs +64 -0
  148. package/gsd-core/bin/lib/gate-verify-command-paths.cjs +78 -0
  149. package/gsd-core/bin/lib/gate-verify-failure-directions.cjs +41 -0
  150. package/gsd-core/bin/lib/graphify.cjs +10 -2
  151. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +43 -47
  152. package/gsd-core/bin/lib/health-diagnostic.cjs +45 -8
  153. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  154. package/gsd-core/bin/lib/init.cjs +364 -132
  155. package/gsd-core/bin/lib/install-engine.cjs +30 -33
  156. package/gsd-core/bin/lib/install-profiles.cjs +7 -4
  157. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  158. package/gsd-core/bin/lib/io.cjs +122 -3
  159. package/gsd-core/bin/lib/loop-resolver.cjs +95 -0
  160. package/gsd-core/bin/lib/markdown-sectionizer.cjs +75 -1
  161. package/gsd-core/bin/lib/milestone.cjs +37 -6
  162. package/gsd-core/bin/lib/model-resolver.cjs +171 -62
  163. package/gsd-core/bin/lib/observability/event.cjs +1 -1
  164. package/gsd-core/bin/lib/observability/logger.cjs +46 -1
  165. package/gsd-core/bin/lib/pattern.cjs +10 -0
  166. package/gsd-core/bin/lib/phase-command-router.cjs +20 -5
  167. package/gsd-core/bin/lib/phase-estimation.cjs +5 -4
  168. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  169. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  170. package/gsd-core/bin/lib/phase-id.cjs +110 -8
  171. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -2
  172. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  173. package/gsd-core/bin/lib/phase-status.cjs +360 -0
  174. package/gsd-core/bin/lib/phase.cjs +489 -88
  175. package/gsd-core/bin/lib/plan-document.cjs +142 -20
  176. package/gsd-core/bin/lib/plan-drift-guard.cjs +5 -0
  177. package/gsd-core/bin/lib/planning-document.cjs +692 -0
  178. package/gsd-core/bin/lib/planning-inspect.cjs +60 -9
  179. package/gsd-core/bin/lib/planning-snapshot.cjs +18 -0
  180. package/gsd-core/bin/lib/planning-workspace.cjs +83 -55
  181. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  182. package/gsd-core/bin/lib/pristine-baseline.cjs +10 -0
  183. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  184. package/gsd-core/bin/lib/profile-output.cjs +6 -3
  185. package/gsd-core/bin/lib/prohibition-enforcement.cjs +0 -55
  186. package/gsd-core/bin/lib/project-root.cjs +41 -2
  187. package/gsd-core/bin/lib/quick-batch-command-router.cjs +35 -9
  188. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +11 -8
  189. package/gsd-core/bin/lib/real-home-guard.cjs +9 -1
  190. package/gsd-core/bin/lib/report-parser.cjs +269 -0
  191. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  192. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  193. package/gsd-core/bin/lib/roadmap-command-router.cjs +25 -19
  194. package/gsd-core/bin/lib/roadmap-parser.cjs +242 -15
  195. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1653 -65
  196. package/gsd-core/bin/lib/roadmap.cjs +405 -88
  197. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +373 -187
  198. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +5 -2
  199. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +57 -1
  200. package/gsd-core/bin/lib/runtime-homes.cjs +14 -7
  201. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +522 -624
  202. package/gsd-core/bin/lib/runtime-name-policy.cjs +246 -21
  203. package/gsd-core/bin/lib/runtime-slash.cjs +47 -30
  204. package/gsd-core/bin/lib/shell-command-projection.cjs +47 -8
  205. package/gsd-core/bin/lib/smart-entry.cjs +19 -3
  206. package/gsd-core/bin/lib/stale-bake-guard.cjs +32 -48
  207. package/gsd-core/bin/lib/state-contract.cjs +15 -18
  208. package/gsd-core/bin/lib/state-document.cjs +100 -22
  209. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  210. package/gsd-core/bin/lib/state.cjs +256 -105
  211. package/gsd-core/bin/lib/surface.cjs +19 -2
  212. package/gsd-core/bin/lib/tdd-red-evidence.cjs +48 -79
  213. package/gsd-core/bin/lib/uat-predicate.cjs +359 -38
  214. package/gsd-core/bin/lib/uat.cjs +432 -7
  215. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  216. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +167 -40
  217. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  218. package/gsd-core/bin/lib/vendor/README.md +31 -9
  219. package/gsd-core/bin/lib/vendor/saxes.cjs +1934 -0
  220. package/gsd-core/bin/lib/vendor/saxes.cjs.LICENSE.txt +92 -0
  221. package/gsd-core/bin/lib/vendor/tap-parser.cjs +8927 -0
  222. package/gsd-core/bin/lib/vendor/tap-parser.cjs.LICENSE.txt +152 -0
  223. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  224. package/gsd-core/bin/lib/verification.cjs +1378 -274
  225. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  226. package/gsd-core/bin/lib/verify-command-router.cjs +18 -7
  227. package/gsd-core/bin/lib/verify.cjs +357 -531
  228. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +14 -6
  229. package/gsd-core/bin/lib/workstream-inventory.cjs +31 -21
  230. package/gsd-core/bin/lib/workstream-name-policy.cjs +31 -1
  231. package/gsd-core/bin/lib/workstream.cjs +11 -2
  232. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  233. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  234. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -0
  235. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  236. package/gsd-core/references/autonomous-smart-discuss.md +2 -1
  237. package/gsd-core/references/autonomous-ui-design-contract.md +3 -3
  238. package/gsd-core/references/checkpoints.md +5 -3
  239. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  240. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  241. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  242. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  243. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  244. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  245. package/gsd-core/references/edge-probe.md +195 -21
  246. package/gsd-core/references/execute-mvp-tdd.md +5 -10
  247. package/gsd-core/references/execute-phase-between-wave-reset.md +10 -6
  248. package/gsd-core/references/execute-phase-response-language.md +1 -1
  249. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  250. package/gsd-core/references/gsd-run-resolver.md +1 -1
  251. package/gsd-core/references/loop-hook-dispatch.md +7 -1
  252. package/gsd-core/references/model-profiles.md +1 -1
  253. package/gsd-core/references/offer-next.md +1 -1
  254. package/gsd-core/references/phase-argument-parsing.md +9 -7
  255. package/gsd-core/references/phase-id-convention.md +28 -0
  256. package/gsd-core/references/planner-gap-closure.md +2 -0
  257. package/gsd-core/references/planner-load-graph-context.md +24 -13
  258. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  259. package/gsd-core/references/planning-config.md +12 -3
  260. package/gsd-core/references/spidr-splitting.md +1 -1
  261. package/gsd-core/references/tdd.md +37 -8
  262. package/gsd-core/references/ui-consideration-probe.md +10 -5
  263. package/gsd-core/references/verifier-phase-gates.md +5 -2
  264. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  265. package/gsd-core/references/verify-mvp-mode.md +2 -2
  266. package/gsd-core/references/workstream-flag.md +33 -3
  267. package/gsd-core/references/worktree-path-safety.md +321 -0
  268. package/gsd-core/templates/README.md +1 -1
  269. package/gsd-core/templates/UAT.md +17 -1
  270. package/gsd-core/templates/config.json +2 -11
  271. package/gsd-core/templates/verification-report.md +1 -1
  272. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  273. package/gsd-core/workflows/add-backlog.md +1 -1
  274. package/gsd-core/workflows/add-phase.md +8 -7
  275. package/gsd-core/workflows/add-tests.md +4 -3
  276. package/gsd-core/workflows/add-todo.md +6 -5
  277. package/gsd-core/workflows/ai-integration-phase.md +13 -4
  278. package/gsd-core/workflows/audit-fix.md +1 -1
  279. package/gsd-core/workflows/audit-milestone.md +4 -3
  280. package/gsd-core/workflows/audit-uat.md +1 -1
  281. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  282. package/gsd-core/workflows/autonomous.md +43 -23
  283. package/gsd-core/workflows/check-todos.md +7 -6
  284. package/gsd-core/workflows/cleanup.md +2 -2
  285. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  286. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +20 -13
  287. package/gsd-core/workflows/code-review-fix.md +112 -25
  288. package/gsd-core/workflows/code-review.md +146 -135
  289. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +4 -3
  290. package/gsd-core/workflows/complete-milestone.md +13 -8
  291. package/gsd-core/workflows/debug.md +32 -7
  292. package/gsd-core/workflows/diagnose-issues.md +3 -2
  293. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  294. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  295. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  296. package/gsd-core/workflows/discuss-phase.md +4 -3
  297. package/gsd-core/workflows/do.md +2 -2
  298. package/gsd-core/workflows/docs-update.md +6 -5
  299. package/gsd-core/workflows/edit-phase.md +4 -3
  300. package/gsd-core/workflows/eval-review.md +14 -5
  301. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  302. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1019 -0
  303. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +15 -4
  304. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +10 -7
  305. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  306. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +3 -1
  307. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +2 -2
  308. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  309. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  310. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  311. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  312. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  313. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  314. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -0
  315. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  316. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  317. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +187 -0
  318. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +2 -3
  319. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  320. package/gsd-core/workflows/execute-phase.md +96 -178
  321. package/gsd-core/workflows/execute-plan.md +18 -23
  322. package/gsd-core/workflows/explore.md +4 -4
  323. package/gsd-core/workflows/extract-learnings.md +4 -2
  324. package/gsd-core/workflows/fast.md +1 -1
  325. package/gsd-core/workflows/forensics.md +1 -1
  326. package/gsd-core/workflows/graduation.md +1 -1
  327. package/gsd-core/workflows/health.md +3 -2
  328. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  329. package/gsd-core/workflows/help/modes/full.md +5 -5
  330. package/gsd-core/workflows/help/modes/topic.md +15 -5
  331. package/gsd-core/workflows/import.md +4 -3
  332. package/gsd-core/workflows/inbox.md +2 -2
  333. package/gsd-core/workflows/ingest-docs.md +3 -3
  334. package/gsd-core/workflows/insert-phase.md +4 -3
  335. package/gsd-core/workflows/list-seeds.md +1 -1
  336. package/gsd-core/workflows/list-workspaces.md +1 -1
  337. package/gsd-core/workflows/manager.md +6 -4
  338. package/gsd-core/workflows/map-codebase.md +5 -4
  339. package/gsd-core/workflows/milestone-summary.md +3 -2
  340. package/gsd-core/workflows/mvp-phase.md +14 -14
  341. package/gsd-core/workflows/new-milestone.md +11 -11
  342. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  343. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  344. package/gsd-core/workflows/new-project.md +7 -7
  345. package/gsd-core/workflows/new-workspace.md +2 -2
  346. package/gsd-core/workflows/next.md +1 -1
  347. package/gsd-core/workflows/note.md +1 -1
  348. package/gsd-core/workflows/onboard.md +1 -1
  349. package/gsd-core/workflows/pause-work.md +2 -2
  350. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  351. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  352. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +1 -1
  353. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  354. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  355. package/gsd-core/workflows/plan-phase.md +50 -24
  356. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  357. package/gsd-core/workflows/plant-seed.md +62 -20
  358. package/gsd-core/workflows/pr-branch.md +113 -13
  359. package/gsd-core/workflows/profile-user.md +2 -2
  360. package/gsd-core/workflows/progress.md +19 -49
  361. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +27 -0
  362. package/gsd-core/workflows/quick/steps/quick-verification.md +4 -4
  363. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +30 -11
  364. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  365. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  366. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  367. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  368. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  369. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  370. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +9 -3
  371. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  372. package/gsd-core/workflows/quick-batch.md +15 -10
  373. package/gsd-core/workflows/quick.md +62 -39
  374. package/gsd-core/workflows/reapply-patches.md +9 -3
  375. package/gsd-core/workflows/remove-phase.md +3 -2
  376. package/gsd-core/workflows/remove-workspace.md +2 -2
  377. package/gsd-core/workflows/resume-project.md +3 -2
  378. package/gsd-core/workflows/review.md +33 -17
  379. package/gsd-core/workflows/scan.md +3 -2
  380. package/gsd-core/workflows/secure-phase.md +13 -13
  381. package/gsd-core/workflows/settings-advanced.md +30 -10
  382. package/gsd-core/workflows/settings-integrations.md +2 -3
  383. package/gsd-core/workflows/settings.md +4 -4
  384. package/gsd-core/workflows/ship.md +14 -13
  385. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  386. package/gsd-core/workflows/sketch.md +1 -1
  387. package/gsd-core/workflows/smart-entry.md +2 -2
  388. package/gsd-core/workflows/spec-phase.md +15 -5
  389. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  390. package/gsd-core/workflows/spike.md +1 -1
  391. package/gsd-core/workflows/stats.md +1 -1
  392. package/gsd-core/workflows/sync-skills.md +5 -5
  393. package/gsd-core/workflows/thread.md +2 -2
  394. package/gsd-core/workflows/transition.md +13 -23
  395. package/gsd-core/workflows/ui-phase.md +48 -11
  396. package/gsd-core/workflows/ui-review.md +21 -6
  397. package/gsd-core/workflows/ultraplan-phase.md +3 -2
  398. package/gsd-core/workflows/undo.md +339 -20
  399. package/gsd-core/workflows/update.md +7 -7
  400. package/gsd-core/workflows/validate-phase.md +12 -13
  401. package/gsd-core/workflows/verify-work/detail/elaboration.md +43 -3
  402. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  403. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -3
  404. package/gsd-core/workflows/verify-work.md +129 -55
  405. package/hooks/dist/gsd-agent-isolation-guard.js +32 -0
  406. package/hooks/dist/gsd-check-update-worker.js +8 -0
  407. package/hooks/dist/gsd-check-update.js +8 -0
  408. package/hooks/dist/gsd-context-monitor.js +31 -8
  409. package/hooks/dist/gsd-cursor-subagent-start.js +8 -0
  410. package/hooks/dist/gsd-secret-read-guard.js +161 -4
  411. package/hooks/dist/gsd-statusline.js +85 -20
  412. package/hooks/dist/gsd-update-banner.js +8 -0
  413. package/hooks/dist/gsd-validate-commit.sh +63 -4
  414. package/hooks/dist/gsd-windsurf-pre-write.js +11 -2
  415. package/hooks/dist/gsd-workflow-guard.js +5 -4
  416. package/hooks/dist/gsd-worktree-path-guard.js +6 -2
  417. package/hooks/dist/lib/cli-exit.js +12 -0
  418. package/hooks/dist/lib/git-probe.js +17 -1
  419. package/hooks/dist/lib/isolation-sentinel.js +2 -2
  420. package/hooks/gsd-agent-isolation-guard.js +32 -0
  421. package/hooks/gsd-check-update-worker.js +8 -0
  422. package/hooks/gsd-check-update.js +8 -0
  423. package/hooks/gsd-context-monitor.js +31 -8
  424. package/hooks/gsd-cursor-subagent-start.js +8 -0
  425. package/hooks/gsd-secret-read-guard.js +161 -4
  426. package/hooks/gsd-statusline.js +85 -20
  427. package/hooks/gsd-update-banner.js +8 -0
  428. package/hooks/gsd-validate-commit.sh +63 -4
  429. package/hooks/gsd-windsurf-pre-write.js +11 -2
  430. package/hooks/gsd-workflow-guard.js +5 -4
  431. package/hooks/gsd-worktree-path-guard.js +6 -2
  432. package/hooks/hooks.json +5 -5
  433. package/hooks/lib/cli-exit.js +12 -0
  434. package/hooks/lib/git-probe.js +17 -1
  435. package/hooks/lib/isolation-sentinel.js +2 -2
  436. package/package.json +22 -4
  437. package/scripts/build-hooks.js +15 -6
  438. package/scripts/changeset/parse.cjs +52 -4
  439. package/scripts/check-contract-drift.cjs +127 -11
  440. package/scripts/ci-timeout-report.cjs +770 -4
  441. package/scripts/command-contract-helpers.cjs +15 -8
  442. package/scripts/docs-guard-registry.cjs +34 -0
  443. package/scripts/gen-features.cjs +13 -8
  444. package/scripts/gen-hooks-cli-exit.cjs +12 -28
  445. package/scripts/gen-loop-host-contract.cjs +79 -1
  446. package/scripts/gen-platform-conformance-tier.cjs +187 -1
  447. package/scripts/gen-plugin-skills.cjs +87 -1
  448. package/scripts/gen-research-agents.cjs +24 -31
  449. package/scripts/gen-scripts-cli-exit.cjs +30 -3
  450. package/scripts/gen-test-timings.cjs +32 -7
  451. package/scripts/lib/cli-exit.cjs +12 -0
  452. package/scripts/lib/macos-conformance-tier.generated.cjs +34 -2
  453. package/scripts/lib/ndjson-reporter.cjs +31 -5
  454. package/scripts/lib/platform-conformance-tier.generated.cjs +45 -5
  455. package/scripts/lib/registration-ledger-preload.cjs +155 -0
  456. package/scripts/lib/vendor-bundle.cjs +59 -0
  457. package/scripts/lib/vendor-licenses/saxes-6.0.0.txt +64 -0
  458. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  459. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  460. package/scripts/lint-completion-predicate-drift.cjs +18 -19
  461. package/scripts/lint-descriptions.cjs +7 -3
  462. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +38 -1
  463. package/scripts/lint-eslint-glob-coverage.allowlist.json +20 -0
  464. package/scripts/lint-frontmatter-fence-drift.cjs +313 -0
  465. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +122 -16
  466. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  467. package/scripts/lint-phase-enumeration-drift.cjs +12 -8
  468. package/scripts/lint-phase-id-drift.cjs +319 -5
  469. package/scripts/lint-planning-document-positive-control.cjs +329 -0
  470. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  471. package/scripts/lint-response-language-coverage.cjs +3 -0
  472. package/scripts/lint-retired-runtime-name.cjs +619 -0
  473. package/scripts/lint-skill-deps.cjs +7 -3
  474. package/scripts/lint-state-write-path-drift.cjs +93 -0
  475. package/scripts/lint-test-file-count.allowlist.json +38 -9
  476. package/scripts/lint-test-file-count.cjs +34 -1
  477. package/scripts/lint-vendored-deps.cjs +41 -5
  478. package/scripts/lint-workflow-shellcheck-baseline.json +25 -10
  479. package/scripts/mutation-matrix.cjs +50 -5
  480. package/scripts/prompt-injection-scan.sh +4 -0
  481. package/scripts/release-tarball-smoke.cjs +194 -1
  482. package/scripts/require-issue-link-policy.cjs +6 -2
  483. package/scripts/sync-runtime-launcher.cjs +184 -2
  484. package/scripts/verify-npm-publish.cjs +76 -20
  485. package/skills/gsd-add-tests/SKILL.md +6 -1
  486. package/skills/gsd-ai-integration-phase/SKILL.md +6 -1
  487. package/skills/gsd-audit-fix/SKILL.md +5 -0
  488. package/skills/gsd-audit-milestone/SKILL.md +6 -1
  489. package/skills/gsd-autonomous/SKILL.md +7 -2
  490. package/skills/gsd-capture/SKILL.md +9 -5
  491. package/skills/gsd-code-review/SKILL.md +7 -2
  492. package/skills/gsd-complete-milestone/SKILL.md +4 -0
  493. package/skills/gsd-config/SKILL.md +8 -4
  494. package/skills/gsd-debug/SKILL.md +11 -7
  495. package/skills/gsd-discuss-phase/SKILL.md +7 -3
  496. package/skills/gsd-docs-update/SKILL.md +12 -7
  497. package/skills/gsd-eval-review/SKILL.md +6 -1
  498. package/skills/gsd-execute-phase/SKILL.md +12 -7
  499. package/skills/gsd-extract-learnings/SKILL.md +5 -0
  500. package/skills/gsd-fast/SKILL.md +4 -0
  501. package/skills/gsd-forensics/SKILL.md +5 -1
  502. package/skills/gsd-graphify/SKILL.md +10 -6
  503. package/skills/gsd-health/SKILL.md +5 -0
  504. package/skills/gsd-help/SKILL.md +7 -2
  505. package/skills/gsd-import/SKILL.md +7 -3
  506. package/skills/gsd-inbox/SKILL.md +5 -0
  507. package/skills/gsd-ingest-docs/SKILL.md +5 -1
  508. package/skills/gsd-manager/SKILL.md +6 -1
  509. package/skills/gsd-map-codebase/SKILL.md +7 -3
  510. package/skills/gsd-mempalace-capture/SKILL.md +12 -4
  511. package/skills/gsd-mempalace-recall/SKILL.md +5 -1
  512. package/skills/gsd-milestone-summary/SKILL.md +5 -1
  513. package/skills/gsd-mvp-phase/SKILL.md +8 -3
  514. package/skills/gsd-new-milestone/SKILL.md +6 -1
  515. package/skills/gsd-new-project/SKILL.md +5 -0
  516. package/skills/gsd-next/SKILL.md +6 -1
  517. package/skills/gsd-ns-context/SKILL.md +4 -0
  518. package/skills/gsd-ns-ideate/SKILL.md +4 -0
  519. package/skills/gsd-ns-manage/SKILL.md +4 -0
  520. package/skills/gsd-ns-project/SKILL.md +4 -0
  521. package/skills/gsd-ns-review/SKILL.md +4 -0
  522. package/skills/gsd-ns-workflow/SKILL.md +4 -0
  523. package/skills/gsd-onboard/SKILL.md +6 -1
  524. package/skills/gsd-pause-work/SKILL.md +5 -1
  525. package/skills/gsd-phase/SKILL.md +8 -4
  526. package/skills/gsd-plan-phase/SKILL.md +6 -1
  527. package/skills/gsd-plan-review-convergence/SKILL.md +10 -6
  528. package/skills/gsd-pr-branch/SKILL.md +4 -0
  529. package/skills/gsd-profile-user/SKILL.md +5 -1
  530. package/skills/gsd-progress/SKILL.md +7 -2
  531. package/skills/gsd-quick/SKILL.md +16 -10
  532. package/skills/gsd-quick-batch/SKILL.md +21 -9
  533. package/skills/gsd-review/SKILL.md +7 -4
  534. package/skills/gsd-review-backlog/SKILL.md +3 -2
  535. package/skills/gsd-secure-phase/SKILL.md +6 -1
  536. package/skills/gsd-ship/SKILL.md +5 -0
  537. package/skills/gsd-sketch/SKILL.md +7 -2
  538. package/skills/gsd-spec-phase/SKILL.md +5 -1
  539. package/skills/gsd-spike/SKILL.md +8 -3
  540. package/skills/gsd-surface/SKILL.md +5 -1
  541. package/skills/gsd-thread/SKILL.md +4 -0
  542. package/skills/gsd-ui-phase/SKILL.md +6 -1
  543. package/skills/gsd-ui-review/SKILL.md +6 -1
  544. package/skills/gsd-ultraplan-phase/SKILL.md +5 -1
  545. package/skills/gsd-undo/SKILL.md +5 -1
  546. package/skills/gsd-update/SKILL.md +6 -2
  547. package/skills/gsd-validate-phase/SKILL.md +6 -1
  548. package/skills/gsd-verify-work/SKILL.md +6 -1
  549. package/skills/gsd-workspace/SKILL.md +7 -3
  550. package/skills/gsd-workstreams/SKILL.md +6 -6
  551. package/vscode/package.json +1 -1
@@ -20,6 +20,17 @@ const security_cjs_1 = require("./security.cjs");
20
20
  // providing a deterministic failure path when git stalls (locked index, hung
21
21
  // remote, stalled NFS mount, etc.). Callers can override via deps.timeout.
22
22
  const DEFAULT_GIT_TIMEOUT_MS = 10000;
23
+ // #4721: the wave's `git merge --no-ff` is the one call in this module that runs
24
+ // the commit-family hooks (`pre-merge-commit`, `prepare-commit-msg`,
25
+ // `commit-msg`, `post-merge`), and a repo whose pre-merge hook is a test-suite
26
+ // gate routinely runs for minutes. That is hook runtime, not "git stalling", so
27
+ // the merge gets its own budget instead of inheriting DEFAULT_GIT_TIMEOUT_MS —
28
+ // raising the shared default would be the wrong lever, because every other
29
+ // caller in the module is exactly what the 10 s comment above describes.
30
+ // (`worktree add` runs `post-checkout` and every ref update runs
31
+ // `reference-transaction`; those are plumbing-cheap and stay on the default.)
32
+ // Callers override via deps.mergeTimeoutMs.
33
+ const DEFAULT_MERGE_TIMEOUT_MS = 10 * 60 * 1000;
23
34
  // #3021: accept the Workflow tool's worktree-wf_<runid>-<n> naming convention
24
35
  // (claude-orchestration's isolation:"worktree" emission) alongside the
25
36
  // existing agent-<id> / worktree-agent-<id> shapes.
@@ -54,7 +65,14 @@ function parseWorktreeEntries(porcelain) {
54
65
  continue;
55
66
  const branchLine = lines.find((l) => l.startsWith('branch refs/heads/'));
56
67
  const branch = branchLine ? branchLine.slice('branch refs/heads/'.length).trim() : null;
57
- entries.push({ path: worktreePath, branch });
68
+ // #4415: `prunable` appears either bare or with a reason. Keep the reason
69
+ // when git gives one, and fall back to a non-empty marker when it does not,
70
+ // so the field stays a truthful "git says stale" boolean either way.
71
+ const prunableLine = lines.find((l) => l === 'prunable' || l.startsWith('prunable '));
72
+ const prunable = prunableLine
73
+ ? (prunableLine.slice('prunable'.length).trim() || 'prunable')
74
+ : null;
75
+ entries.push({ path: worktreePath, branch, prunable });
58
76
  }
59
77
  return entries;
60
78
  }
@@ -481,6 +499,98 @@ function repoRootStillMidMerge(execGit, repoRoot) {
481
499
  return false; // ref not found — repoRoot is not mid-merge
482
500
  return true; // any other exit code (e.g. a fatal git error) — fail closed
483
501
  }
502
+ /**
503
+ * #4721: after a merge that was KILLED — at its budget or by a signal — and
504
+ * did not leave MERGE_HEAD behind, undo whatever it staged in repoRoot's index
505
+ * and re-apply any work it had autostashed. (A kill that lands once MERGE_HEAD
506
+ * exists — inside `commit-msg`, say — is the ordinary #2852 path: `git merge
507
+ * --abort` restores the tree and re-applies an autostash itself, unstaged, as
508
+ * it does for any aborted autostashed merge.)
509
+ *
510
+ * Why the staged set is attributable to the merge — on this path only: `git
511
+ * merge` refuses to start when the index already differs from HEAD ("your
512
+ * local changes … would be overwritten", even for paths the branch never
513
+ * touches), and a refusal is an immediate exit with a code, never a kill. The
514
+ * one way for a KILLED merge to leave a dirty index with no MERGE_HEAD is a
515
+ * kill between populating the index and writing MERGE_HEAD — i.e. during a
516
+ * merge hook. The exception is `merge.autoStash`: git then parks the pre-existing
517
+ * work in MERGE_AUTOSTASH and starts anyway, and a killed merge never
518
+ * re-applies it. Handled below; it is why the reset runs even on a clean
519
+ * index.
520
+ *
521
+ * `git reset --merge` (no commit → HEAD) is the restore: it resets the index
522
+ * to HEAD and updates the worktree only for the paths the index changed,
523
+ * keeping unrelated unstaged edits intact, and it refuses rather than clobbers
524
+ * when an unstaged edit overlaps a staged path. It also moves a pending
525
+ * MERGE_AUTOSTASH into the stash list ("Autostash exists; creating a new stash
526
+ * entry"), which `git stash pop --index` then re-applies — the same outcome
527
+ * `git merge --abort` gives an autostashed merge that could be aborted.
528
+ *
529
+ * Returns `halt: true` only when repoRoot is still (or unverifiably) dirty —
530
+ * the same repo-level carve-out `repoRootStillMidMerge` uses, and for the same
531
+ * reason: every remaining entry's merge would run against a dirty index.
532
+ */
533
+ function restoreMergeResidue(execGit, repoRoot, branch) {
534
+ const stagedPaths = (raw) => raw
535
+ .split('\n')
536
+ .map((line) => decodeGitQuotedPath(line.trim()))
537
+ .filter((p) => p.length > 0);
538
+ const leftStaged = (paths) => ({
539
+ halt: true,
540
+ warnings: paths.map((p) => ({ code: WAVE_CLEANUP_WARNING.MERGE_RESIDUE_LEFT_STAGED, branch, path: p })),
541
+ });
542
+ const staged = execGit(['diff', '--cached', '--name-only'], { cwd: repoRoot });
543
+ if (!gitResultOk(staged)) {
544
+ // Cannot tell whether the index is dirty — fail closed, same as an
545
+ // unverifiable MERGE_HEAD check. A null path marks "the check itself could
546
+ // not run", the convention SCOPE_CHECK_UNAVAILABLE already uses.
547
+ return leftStaged([null]);
548
+ }
549
+ const before = stagedPaths(staged.stdout || '');
550
+ // Exit 0 = git parked pre-existing work here before starting the merge;
551
+ // exit 1 = no autostash. Anything else is unknown: do not pop blind, but do
552
+ // say so — the reset below will have moved any stash into the list unread.
553
+ const autostash = execGit(['rev-parse', '--verify', '-q', 'MERGE_AUTOSTASH'], { cwd: repoRoot });
554
+ const hadAutostash = !autostash.timedOut && autostash.exitCode === 0;
555
+ const autostashUnknown = !!autostash.timedOut || (autostash.exitCode !== 0 && autostash.exitCode !== 1);
556
+ if (before.length === 0 && !hadAutostash && !autostashUnknown)
557
+ return { halt: false, warnings: [] };
558
+ const reset = execGit(['reset', '--merge'], { cwd: repoRoot });
559
+ const recheck = gitResultOk(reset) ? execGit(['diff', '--cached', '--name-only'], { cwd: repoRoot }) : null;
560
+ if (!recheck || !gitResultOk(recheck)) {
561
+ // The reset failed, or its result could not be re-read: report the set we
562
+ // know was staged, and halt.
563
+ return leftStaged(before.length > 0 ? before : [null]);
564
+ }
565
+ const after = stagedPaths(recheck.stdout || '');
566
+ if (after.length > 0)
567
+ return leftStaged(after);
568
+ const warnings = before.map((p) => ({ code: WAVE_CLEANUP_WARNING.MERGE_RESIDUE_RESTORED, branch, path: p }));
569
+ if (hadAutostash) {
570
+ const pop = execGit(['stash', 'pop', '--index'], { cwd: repoRoot });
571
+ if (!gitResultOk(pop)) {
572
+ warnings.push({ code: WAVE_CLEANUP_WARNING.MERGE_AUTOSTASH_UNRESTORED, branch, path: null });
573
+ // A failed pop keeps the stash entry, but it can leave conflict entries
574
+ // (`UU`) and partially applied paths behind it — and the next merge then
575
+ // fails with "you have unmerged files" (caught in review). Re-read rather
576
+ // than assume: a dirty index here halts exactly as an unrestorable
577
+ // residue does.
578
+ const afterPop = execGit(['diff', '--cached', '--name-only'], { cwd: repoRoot });
579
+ if (!gitResultOk(afterPop))
580
+ return { halt: true, warnings: [...warnings, ...leftStaged([null]).warnings] };
581
+ const dirty = stagedPaths(afterPop.stdout || '');
582
+ if (dirty.length > 0)
583
+ return { halt: true, warnings: [...warnings, ...leftStaged(dirty).warnings] };
584
+ }
585
+ }
586
+ else if (autostashUnknown) {
587
+ warnings.push({ code: WAVE_CLEANUP_WARNING.MERGE_AUTOSTASH_UNRESTORED, branch, path: null });
588
+ }
589
+ // Not a halt: the index is verified clean, or holds only the operator's own
590
+ // re-applied work (a successful `--index` pop), which the next merge
591
+ // autostashes again under the same config.
592
+ return { halt: false, warnings };
593
+ }
484
594
  // #2596: the single definition of "this file is an executor-written SUMMARY
485
595
  // artifact". Shared by `defaultFindSummaryFiles` (which walks for them to
486
596
  // rescue) and the scope advisory below (which must never flag them) — a plan's
@@ -646,14 +756,25 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
646
756
  const readFileSync = deps.readFileSync || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
647
757
  const mkdirSync = deps.mkdirSync || ((d, o) => node_fs_1.default.mkdirSync(d, o));
648
758
  const copyFileSync = deps.copyFileSync || node_fs_1.default.copyFileSync;
649
- const summaryPaths = findSummaryFiles(worktreePath);
759
+ // #4758: resolve the manifest's worktree_path against repoRoot once, at this
760
+ // boundary, so every reader of the field here — the fs walker below and the
761
+ // `git -C` calls — resolves it the way the caller's git consumers already do
762
+ // (`-C <path>` with `{ cwd: repoRoot }`). A relative value passed to the
763
+ // walker verbatim made `path.join` emit a relative directory whose reads
764
+ // resolved against process.cwd(), silently walking the wrong tree.
765
+ // With this resolution, the remaining deps-injectable readers (existsSync,
766
+ // readFileSync, mkdirSync, copyFileSync) only ever see absolute paths:
767
+ // `dest` is built from repoRoot, `absPath` from the walker's join off this
768
+ // resolved base.
769
+ const resolvedWorktreePath = node_path_1.default.resolve(repoRoot, worktreePath);
770
+ const summaryPaths = findSummaryFiles(resolvedWorktreePath);
650
771
  const rescuedRelPaths = new Set();
651
772
  const failures = [];
652
773
  for (const absPath of summaryPaths) {
653
774
  // relPath is the path relative to the worktree root (e.g. ".planning/q1-SUMMARY.md")
654
775
  // Normalize to forward slashes so the Set comparison against `git status --porcelain`
655
776
  // output works on Windows too (git always emits forward slashes in porcelain output).
656
- const relPath = (0, shell_command_projection_cjs_1.posixNormalize)(absPath.slice(worktreePath.length).replace(/^[/\\]/, ''));
777
+ const relPath = (0, shell_command_projection_cjs_1.posixNormalize)(absPath.slice(resolvedWorktreePath.length).replace(/^[/\\]/, ''));
657
778
  // #706: skip rescue when the SUMMARY is already committed on the branch.
658
779
  // Use `git cat-file -e HEAD:<relPath>` (not `ls-files --error-unmatch`) so
659
780
  // the check is against the committed tree, not the index. ls-files also
@@ -671,7 +792,7 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
671
792
  // copy when the main tree already holds identical content (which is also what
672
793
  // guards the #706 merge collision). A divergent dest is overwritten, but only
673
794
  // uncommitted main-tree content could be lost (committed content is git-recoverable).
674
- const catFileResult = execGit(['-C', worktreePath, 'cat-file', '-e', `HEAD:${relPath}`], { cwd: repoRoot });
795
+ const catFileResult = execGit(['-C', resolvedWorktreePath, 'cat-file', '-e', `HEAD:${relPath}`], { cwd: repoRoot });
675
796
  if (catFileResult.exitCode === 0) {
676
797
  // exit 0 → the SUMMARY is committed on HEAD; the merge will carry it, so skip rescue.
677
798
  continue;
@@ -719,6 +840,42 @@ const WAVE_CLEANUP_WARNING = Object.freeze({
719
840
  SCOPE_OUT_OF_DECLARED: 'scope_out_of_declared',
720
841
  /** The scope diff could not be computed, so conformance is unknown. */
721
842
  SCOPE_CHECK_UNAVAILABLE: 'scope_check_unavailable',
843
+ /**
844
+ * #4721: a killed merge (at its budget, or by a signal) left this path
845
+ * staged in repoRoot's index with no MERGE_HEAD, and `git reset --merge`
846
+ * restored it to HEAD. Informational — repoRoot is clean again.
847
+ */
848
+ MERGE_RESIDUE_RESTORED: 'merge_residue_restored',
849
+ /**
850
+ * #4721: a killed merge left this path staged in repoRoot's index with no
851
+ * MERGE_HEAD and it could NOT be restored (null path: the index could not
852
+ * be read at all) — or a failed autostash pop left it unmerged. repoRoot is
853
+ * dirty; committing from it would squash the executor's history into one
854
+ * parent. The wave halts.
855
+ */
856
+ MERGE_RESIDUE_LEFT_STAGED: 'merge_residue_left_staged',
857
+ /**
858
+ * #4721: the killed merge had parked pre-existing work in MERGE_AUTOSTASH
859
+ * (`merge.autoStash`), and re-applying it failed or could not be verified.
860
+ * The work is in the stash list, not lost. Path is always null. On its own
861
+ * the index is clean and the wave continues; when a failed pop left
862
+ * unmerged entries it is accompanied by MERGE_RESIDUE_LEFT_STAGED rows and
863
+ * the wave halts.
864
+ */
865
+ MERGE_AUTOSTASH_UNRESTORED: 'merge_autostash_unrestored',
866
+ /**
867
+ * #4415: an entry was merged on the evidence that its checkout was already
868
+ * gone, rather than on a clean read of a present worktree.
869
+ *
870
+ * Emitted because "the harness cleanly removed a finished executor" and
871
+ * "something else removed this path" are the same signature to this code —
872
+ * git still registers the path -> branch binding, and `statSync` reports
873
+ * ENOENT, in both cases. Before this path existed, EVERY anomalous absence
874
+ * blocked loudly, which gave an operator something to investigate; accepting
875
+ * the routine case silently would take that signal away from the case that is
876
+ * not routine. Advisory, never a gate: the entry still merged.
877
+ */
878
+ ACCEPTED_ABSENT_WORKTREE: 'accepted_absent_worktree',
722
879
  });
723
880
  /**
724
881
  * The literal directory prefix a declared path covers, or `null` when the
@@ -834,6 +991,127 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
834
991
  const pending = [];
835
992
  const allWarnings = [];
836
993
  let ok = true;
994
+ // #4415: two questions, two sources, each asked only what it can actually prove.
995
+ //
996
+ // IDENTITY — "is the checkout registered at this path the branch the manifest
997
+ // names?" — comes from `git worktree list --porcelain`.
998
+ // REMOVAL — "is the directory actually gone, as opposed to unreadable?" —
999
+ // comes from `statSync`'s errno.
1000
+ //
1001
+ // Neither source can answer the other's question, and both mistakes have been
1002
+ // measured rather than reasoned about:
1003
+ //
1004
+ // 1. An earlier cut inferred removal from `fs.existsSync` returning false and,
1005
+ // with no checkout left to read, fell back to `refs/heads/<branch>` for
1006
+ // identity. Git never loses the binding: after `rm -rf` it still prints
1007
+ // `worktree <path>` + `branch refs/heads/<branch>`. The ref fallback weakened
1008
+ // identity from "the checkout registered here is on this branch" to "a branch
1009
+ // by this name exists", which let a foreign sibling branch through the gate.
1010
+ //
1011
+ // 2. `prunable` is NOT a removal test. Measured: with a parent directory at mode
1012
+ // 000, git emits `prunable gitdir file points to non-existent location` for a
1013
+ // checkout that is still there — it cannot traverse the parent, so it reports
1014
+ // the gitdir file as missing. Accepting `prunable` as "removed" would merge
1015
+ // over uncommitted work in an unreadable worktree, which is the very thing the
1016
+ // dirty check exists to refuse. `statSync` separates them: ENOENT is gone,
1017
+ // EACCES/EIO is unreadable.
1018
+ //
1019
+ // The identity read is a SNAPSHOT taken before the loop, and that is load-bearing:
1020
+ // `git worktree prune` is repository-wide, so the first absent entry's teardown
1021
+ // clears EVERY stale registration, including those of entries not yet evaluated.
1022
+ // Measured: two removed worktrees, one `prune`, and both registrations are gone.
1023
+ // Reading the list per entry would therefore merge the first harness-removed
1024
+ // worktree of a wave and block the rest as `branch_mismatch` — worse than the bug
1025
+ // this PR fixes, since a wave of parallel executors is the normal case.
1026
+ //
1027
+ // Captured LAZILY, on the first entry that actually needs identity, and reused for
1028
+ // the rest of the wave. Laziness is what keeps the read off the happy path — a wave
1029
+ // whose worktrees are all present never spends the subprocess — and it is still
1030
+ // early enough to be a true pre-prune snapshot, because every teardown that prunes
1031
+ // consults this predicate first.
1032
+ let worktreeListSnapshot = null;
1033
+ const snapshotWorktreeList = () => {
1034
+ if (!worktreeListSnapshot)
1035
+ worktreeListSnapshot = readWorktreeList(plan.repoRoot, { execGit });
1036
+ return worktreeListSnapshot;
1037
+ };
1038
+ const resolveAgainstRepoRoot = (worktreePath) => node_path_1.default.resolve(plan.repoRoot, worktreePath);
1039
+ const findRegistered = (listed, target) => (listed.ok
1040
+ ? listed.entries.find((listedEntry) => resolveAgainstRepoRoot(listedEntry.path) === target)
1041
+ : undefined);
1042
+ // `worktree_path` comes from the manifest verbatim and may be relative, while the
1043
+ // porcelain always reports absolute paths; git resolves the manifest form against
1044
+ // repoRoot (every call passes `-C <path>` with `cwd: plan.repoRoot`), so match it
1045
+ // the same way.
1046
+ const registeredFor = (worktreePath) => {
1047
+ const target = resolveAgainstRepoRoot(worktreePath);
1048
+ const fromSnapshot = findRegistered(snapshotWorktreeList(), target);
1049
+ if (fromSnapshot)
1050
+ return fromSnapshot;
1051
+ // Absent from the snapshot: it may have been registered after the wave began.
1052
+ // A list that cannot be read yields no entry, which blocks — the fail-safe way.
1053
+ return findRegistered(readWorktreeList(plan.repoRoot, { execGit }), target);
1054
+ };
1055
+ const statSyncRaw = deps.statSync || node_fs_1.default.statSync;
1056
+ // Only ENOENT is removal. A path that stats successfully is present; any other
1057
+ // errno means it could not be read, and an unreadable checkout blocked before
1058
+ // this PR and must keep blocking.
1059
+ const confirmedGone = (worktreePath) => {
1060
+ try {
1061
+ statSyncRaw(resolveAgainstRepoRoot(worktreePath));
1062
+ return false;
1063
+ }
1064
+ catch (err) {
1065
+ return err?.code === 'ENOENT';
1066
+ }
1067
+ };
1068
+ // Carries git's `prunable` reason for the most recent acceptance, so the
1069
+ // warning below can quote git rather than paraphrase it. Set only on the
1070
+ // accepting call; callers that reject never read it.
1071
+ let lastAcceptedPrunableReason = null;
1072
+ const absentAndIdentified = (worktreePath, branch) => {
1073
+ const registered = registeredFor(worktreePath);
1074
+ if (!registered || registered.branch !== branch)
1075
+ return false;
1076
+ if (!confirmedGone(worktreePath))
1077
+ return false;
1078
+ lastAcceptedPrunableReason = registered.prunable;
1079
+ return true;
1080
+ };
1081
+ /**
1082
+ * #4415 (maintainer review round 3): record that an entry took the absent path.
1083
+ *
1084
+ * Two Medium findings close here together. This code cannot distinguish "the
1085
+ * harness cleanly removed a finished executor" from "an operator or an external
1086
+ * process removed this path" — both leave git's registration intact and both
1087
+ * stat ENOENT. Before the absent path existed, every anomalous absence blocked
1088
+ * loudly; accepting the routine case silently would have removed that signal
1089
+ * from the case that is not routine, reporting `merged_removed`/`ok`
1090
+ * indistinguishably from an ordinary merge. The module already carries an
1091
+ * advisory channel for a materially less risky condition (scope conformance) a
1092
+ * few lines below, so withholding one here was inconsistent with its own
1093
+ * pattern.
1094
+ *
1095
+ * It also gives `WorktreeEntry.prunable` its consumer. The field was parsed and
1096
+ * documented as "worth surfacing to an operator" and then never read — dead
1097
+ * weight, and its bare-marker test asserted an outcome driven by other code.
1098
+ * Quoting git's own reason here is what that parsing was for.
1099
+ */
1100
+ const noteAcceptedAbsent = (result, entry) => {
1101
+ // The parser normalises a bare `prunable` line to the literal 'prunable' so the
1102
+ // field stays truthy either way. That sentinel is the marker echoed back, not a
1103
+ // reason, so it is reported as "no reason given" rather than quoted at an
1104
+ // operator as though git had said something.
1105
+ const reason = lastAcceptedPrunableReason === 'prunable' ? null : lastAcceptedPrunableReason;
1106
+ const warning = {
1107
+ code: WAVE_CLEANUP_WARNING.ACCEPTED_ABSENT_WORKTREE,
1108
+ branch: entry.branch,
1109
+ path: entry.worktree_path,
1110
+ detail: reason,
1111
+ };
1112
+ result.warnings.push(warning);
1113
+ allWarnings.push(warning);
1114
+ };
837
1115
  // #2852: every per-entry failure site marks the SAME shape — status='blocked',
838
1116
  // a reason code, the captured stderr, push to results, flip the overall `ok`
839
1117
  // flag — and then either `continue` (isolate, the default) or, for the one
@@ -855,12 +1133,46 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
855
1133
  stderr: '',
856
1134
  warnings: [],
857
1135
  };
1136
+ // #4415: the harness may have already removed this worktree. Claude Code
1137
+ // removes a subagent's worktree the moment the subagent finishes with a
1138
+ // clean tree, and an executor that committed everything — SUMMARY.md
1139
+ // included, under `commit_docs: true` — is exactly that case, so by wave
1140
+ // cleanup the directory is routinely gone while the branch it left behind
1141
+ // is intact and mergeable.
1142
+ //
1143
+ // `git -C <gone> rev-parse` fails, and that failure was indistinguishable
1144
+ // from a genuine mismatch, so the entry blocked as `branch_mismatch` and
1145
+ // nothing merged. Disambiguate at the point of failure rather than ahead of
1146
+ // it: a SUCCESSFUL read still decides identity exactly as before (a present
1147
+ // worktree on the wrong branch blocks, unchanged), and only a FAILED read
1148
+ // asks git and the filesystem why.
1149
+ let worktreeAbsent = false;
858
1150
  const branchCheck = execGit(['-C', entry.worktree_path, 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: plan.repoRoot });
859
- if (!gitResultOk(branchCheck) || branchCheck.stdout.trim() !== entry.branch) {
1151
+ if (!gitResultOk(branchCheck)) {
1152
+ // The in-worktree read failed. Ask git WHY, instead of asking the
1153
+ // filesystem WHETHER: identity is still on record in the porcelain output,
1154
+ // so the #3677 swap control keeps its teeth here rather than degrading to
1155
+ // "some branch by this name exists".
1156
+ //
1157
+ // Blocked unless git still binds this path to the branch the manifest names
1158
+ // AND the directory is confirmed gone (ENOENT). Each way of failing that is a
1159
+ // genuine mismatch: a different branch registered at the path is the swap the
1160
+ // control exists to catch; a path git does not list at all is an entry naming
1161
+ // something git has no record of; and a path that stats, or that fails to stat
1162
+ // for any reason other than ENOENT, is a checkout that is present or merely
1163
+ // unreadable — which blocked before this PR and must keep blocking.
1164
+ if (!absentAndIdentified(entry.worktree_path, entry.branch)) {
1165
+ blockEntry(result, 'branch_mismatch', branchCheck?.stderr || '');
1166
+ // #2852: isolate — this entry's problem does not touch repoRoot's git state,
1167
+ // so every remaining entry is still independently evaluated.
1168
+ continue;
1169
+ }
1170
+ worktreeAbsent = true;
1171
+ noteAcceptedAbsent(result, entry);
1172
+ }
1173
+ else if (branchCheck.stdout.trim() !== entry.branch) {
860
1174
  blockEntry(result, 'branch_mismatch', branchCheck?.stderr || '');
861
- // #2852: isolate — this entry's problem does not touch repoRoot's git state,
862
- // so every remaining entry is still independently evaluated.
863
- continue;
1175
+ continue; // #2852: isolate
864
1176
  }
865
1177
  const mergeBase = execGit(['merge-base', 'HEAD', entry.branch], { cwd: plan.repoRoot });
866
1178
  const allowedBases = Array.isArray(entry.allowed_bases) && entry.allowed_bases.length > 0
@@ -928,38 +1240,101 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
928
1240
  result.warnings.push(...scopeWarnings);
929
1241
  allWarnings.push(...scopeWarnings);
930
1242
  }
931
- // Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
932
- // The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
933
- // orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
934
- const { rescuedRelPaths, failures: rescueFailures } = rescueSummaryArtifacts(entry.worktree_path, plan.repoRoot, deps);
935
- if (rescueFailures.length > 0) {
936
- blockEntry(result, 'summary_rescue_failed', rescueFailures.map((f) => `${f.relPath}: ${f.error}`).join('; '));
937
- continue; // #2852: isolate
938
- }
939
- const worktreeStatus = execGit(['-C', entry.worktree_path, 'status', '--porcelain', '--untracked-files=all'], { cwd: plan.repoRoot });
940
- if (!gitResultOk(worktreeStatus)) {
941
- blockEntry(result, 'worktree_dirty', worktreeStatus?.stderr || '');
942
- continue; // #2852: isolate
943
- }
944
- // Filter rescued SUMMARY paths out of the porcelain output before deciding dirty.
945
- // A line like "?? .planning/q1-SUMMARY.md" should not block when the SUMMARY
946
- // has already been rescued into the main tree.
947
- const dirtyLines = (worktreeStatus.stdout || '')
948
- .split('\n')
949
- .filter((line) => {
950
- if (!line.trim())
951
- return false;
952
- // porcelain v1 format: "XY path" (3-char prefix + space + path)
953
- const filePath = line.slice(3).trim();
954
- return !rescuedRelPaths.has(filePath);
955
- });
956
- if (dirtyLines.length > 0) {
957
- blockEntry(result, 'worktree_dirty', dirtyLines.join('\n'));
958
- continue; // #2852: isolate
1243
+ // #4415: both steps below read the worktree directory. The rescue exists to
1244
+ // save work the executor left UNCOMMITTED; the dirty check exists to refuse
1245
+ // to merge over it. Against an absent path they fail differently, and only
1246
+ // one of them is loud: the default SUMMARY finder catches the unreadable
1247
+ // directory and simply returns no files, while `git -C <gone> status` errors
1248
+ // — and THAT is what surfaced as `worktree_dirty`, a block with nothing
1249
+ // merged. (Corrected in Codex review round 2: an earlier version of this
1250
+ // comment claimed both reads error.)
1251
+ //
1252
+ // The harness removes a worktree only when its tree is clean, so in the case
1253
+ // this fix targets there is genuinely nothing to rescue. That is a property
1254
+ // of the harness, NOT something checked here: this code cannot tell who
1255
+ // removed the directory, and a forced or manual `rm -rf` of a DIRTY worktree
1256
+ // would already have destroyed an uncommitted SUMMARY before cleanup ran.
1257
+ // What is claimed is only the narrow thing true either way — a missing
1258
+ // source cannot be read, so skipping the read loses nothing that still
1259
+ // exists. (Codex review round 1.)
1260
+ if (!worktreeAbsent) {
1261
+ // Safety net: rescue uncommitted SUMMARY.md artifacts before the dirty check.
1262
+ // The executor leaves <quick_id>-SUMMARY.md uncommitted by contract — the
1263
+ // orchestrator commits it. Mirrors quick.md shell fallback (#2296, #2070, #2838, #3804).
1264
+ //
1265
+ // Destructured in place, not hoisted: every path that reaches the consumer
1266
+ // below has already run this line. (An earlier cut hoisted it on the
1267
+ // reasoning that the nested block created another route in; Codex review
1268
+ // round 2 showed that is not so — flipping `worktreeAbsent` SKIPS the
1269
+ // consumer rather than reaching it unassigned.)
1270
+ const { rescuedRelPaths, failures: rescueFailures } = rescueSummaryArtifacts(entry.worktree_path, plan.repoRoot, deps);
1271
+ if (rescueFailures.length > 0) {
1272
+ blockEntry(result, 'summary_rescue_failed', rescueFailures.map((f) => `${f.relPath}: ${f.error}`).join('; '));
1273
+ continue; // #2852: isolate
1274
+ }
1275
+ const worktreeStatus = execGit(['-C', entry.worktree_path, 'status', '--porcelain', '--untracked-files=all'], { cwd: plan.repoRoot });
1276
+ if (!gitResultOk(worktreeStatus)) {
1277
+ // #4415 (Codex review round 1): the harness can remove the worktree
1278
+ // between the branch read and here — while the repoRoot-side base,
1279
+ // deletion and scope checks run. `worktreeAbsent` records what was true
1280
+ // at IDENTIFICATION time, not now, so without this a mid-entry removal
1281
+ // failed `status` and blocked `worktree_dirty` with nothing merged: the
1282
+ // same bug as the branch read, one window later. Same disambiguation,
1283
+ // applied at the same point — the read failed, so ask why.
1284
+ //
1285
+ // Deliberately NOT extended to a rescue FAILURE above: a copy that
1286
+ // errored part-way can mean an uncommitted SUMMARY was genuinely lost,
1287
+ // and that must keep blocking. A rescue that simply finds nothing to
1288
+ // copy reports no failure and falls through to here.
1289
+ //
1290
+ // Identity was already established by the successful branch read above, so
1291
+ // the question here is only staleness — but it is asked of git, on the same
1292
+ // terms as the identification site, because a `status` failure is no more
1293
+ // self-explaining than a `rev-parse` failure was.
1294
+ if (!absentAndIdentified(entry.worktree_path, entry.branch)) {
1295
+ blockEntry(result, 'worktree_dirty', worktreeStatus?.stderr || '');
1296
+ continue; // #2852: isolate
1297
+ }
1298
+ worktreeAbsent = true;
1299
+ noteAcceptedAbsent(result, entry);
1300
+ }
1301
+ if (!worktreeAbsent) {
1302
+ // Filter rescued SUMMARY paths out of the porcelain output before deciding dirty.
1303
+ // A line like "?? .planning/q1-SUMMARY.md" should not block when the SUMMARY
1304
+ // has already been rescued into the main tree.
1305
+ const dirtyLines = (worktreeStatus.stdout || '')
1306
+ .split('\n')
1307
+ .filter((line) => {
1308
+ if (!line.trim())
1309
+ return false;
1310
+ // porcelain v1 format: "XY path" (3-char prefix + space + path)
1311
+ const filePath = line.slice(3).trim();
1312
+ return !rescuedRelPaths.has(filePath);
1313
+ });
1314
+ if (dirtyLines.length > 0) {
1315
+ blockEntry(result, 'worktree_dirty', dirtyLines.join('\n'));
1316
+ continue; // #2852: isolate
1317
+ }
1318
+ }
959
1319
  }
960
- const merge = execGit(['merge', entry.branch, '--no-ff', '--no-edit', '-m', `chore: merge executor worktree (${entry.branch})`], { cwd: plan.repoRoot });
1320
+ // #4721: the merge runs user hooks, so it carries its own budget — see
1321
+ // DEFAULT_MERGE_TIMEOUT_MS. Every other call in this gauntlet keeps the
1322
+ // module default.
1323
+ const mergeTimeoutMs = deps.mergeTimeoutMs ?? DEFAULT_MERGE_TIMEOUT_MS;
1324
+ const merge = execGit(['merge', entry.branch, '--no-ff', '--no-edit', '-m', `chore: merge executor worktree (${entry.branch})`], { cwd: plan.repoRoot, timeout: mergeTimeoutMs });
961
1325
  if (!gitResultOk(merge)) {
962
- blockEntry(result, 'merge_failed', merge?.stderr || merge?.stdout || '');
1326
+ if (merge?.timedOut) {
1327
+ // #4721: say "timeout" when it was one. The captured output is whatever
1328
+ // the hook printed before git was killed, which read as a git error under
1329
+ // the old `merge_failed` label and made a healthy executor branch look
1330
+ // broken. The hook itself is a child of the killed git process and may
1331
+ // still be running.
1332
+ const partial = (merge.stderr || merge.stdout || '').trim();
1333
+ blockEntry(result, 'merge_timed_out', `git merge did not finish within ${mergeTimeoutMs} ms and was killed (a merge hook such as pre-merge-commit may still be running; raise deps.mergeTimeoutMs or shorten the hook)${partial ? `; output before the kill: ${partial}` : ''}`);
1334
+ }
1335
+ else {
1336
+ blockEntry(result, 'merge_failed', merge?.stderr || merge?.stdout || '');
1337
+ }
963
1338
  // #2852: a failed --no-ff merge MIGHT leave repoRoot itself mid-merge
964
1339
  // (MERGE_HEAD set, conflict markers in the tree) — unlike every other block
965
1340
  // reason above, that specific state is NOT scoped to this one entry: a second
@@ -978,21 +1353,111 @@ function executeWorktreeWaveCleanupPlan(plan, deps = {}) {
978
1353
  pending.push(...entries.slice(i + 1));
979
1354
  break;
980
1355
  }
1356
+ // #4721: "no MERGE_HEAD" is not "tree never touched". A merge killed while
1357
+ // its pre-merge-commit hook ran has already written the merged tree into
1358
+ // repoRoot's index (and set ORIG_HEAD) but never got to write MERGE_HEAD,
1359
+ // so the #2852 check above reads it as clean while the executor's whole
1360
+ // diff sits staged against the old HEAD. `git merge --abort` cannot see
1361
+ // that state either. Left alone, the next `git merge` in this wave would
1362
+ // refuse ("your local changes would be overwritten") or, worse, an
1363
+ // orchestrator that trusts the block reason and commits from repoRoot
1364
+ // squashes the executor's history into one parent. Restore it; if that
1365
+ // cannot be verified, halt the wave exactly as the mid-merge case does.
1366
+ //
1367
+ // ONLY when git was KILLED — at its budget, or by a signal from outside.
1368
+ // A merge git REFUSED (no MERGE_HEAD either) leaves the index exactly as
1369
+ // it found it — and "your local changes would be overwritten" is precisely
1370
+ // the refusal a pre-existing dirty index earns, so on that path anything
1371
+ // staged is the operator's own work and must not be touched (caught in
1372
+ // review). A kill is the one shape that stages a tree git never finished
1373
+ // with, and an external SIGTERM produces the same state as the timeout
1374
+ // without `timedOut` (caught in review too). The seam normalizes a
1375
+ // signal death to exitCode 1 and carries the signal alongside, so the
1376
+ // signal — never the exit code — is the tell; a refused merge has none.
1377
+ const mergeKilled = !!merge?.timedOut || !!merge?.signal;
1378
+ if (mergeKilled) {
1379
+ const residue = restoreMergeResidue(execGit, plan.repoRoot, entry.branch);
1380
+ result.warnings.push(...residue.warnings);
1381
+ allWarnings.push(...residue.warnings);
1382
+ if (residue.halt) {
1383
+ pending.push(...entries.slice(i + 1));
1384
+ break;
1385
+ }
1386
+ }
981
1387
  continue; // #2852: isolate — repoRoot is not (or no longer) mid-merge
982
1388
  }
983
- let remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
984
- if (!gitResultOk(remove)) {
985
- // Locked worktrees require unlock before remove (or --force --force).
986
- // Attempt: git worktree unlock <path> (ignore failure — already unlocked is ok)
987
- // then retry git worktree remove --force. (#3707)
988
- execGit(['worktree', 'unlock', entry.worktree_path], { cwd: plan.repoRoot });
989
- remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
990
- }
991
- if (!gitResultOk(remove)) {
992
- blockEntry(result, 'worktree_remove_failed', remove?.stderr || '');
993
- // #2852: isolate — the merge already landed on repoRoot; only this entry's
994
- // worktree/branch teardown is affected.
995
- continue;
1389
+ if (worktreeAbsent) {
1390
+ // #4415 (Codex review round 2): this entry was accepted WITHOUT the rescue
1391
+ // and dirty checks, on the evidence that it had no checkout. Never issue
1392
+ // `worktree remove --force` for it. If a registered checkout has since
1393
+ // reappeared at that path — recreated between the checks and here — a
1394
+ // forced removal would delete contents that never passed either check,
1395
+ // which is strictly worse than the bug this PR fixes.
1396
+ //
1397
+ // Re-confirm absence immediately before tearing down (maintainer review,
1398
+ // Major). Presence was classified once, at identification, and everything
1399
+ // between then and here — the base, deletion and scope gates, and the merge
1400
+ // itself — is a window in which a worktree can reappear. "Prune only" was
1401
+ // offered as sufficient on its own, on the argument that prune leaves a live
1402
+ // checkout alone and the `branch -D` below would then fail visibly. That
1403
+ // argument holds only while prune's own staleness check is not fooled by the
1404
+ // same filesystem-visibility gap that produced the false absence one call
1405
+ // earlier. If it is, prune clears the admin entry, `branch -D` then SUCCEEDS,
1406
+ // and a live, unreviewed, un-rescued worktree loses its branch — destroying
1407
+ // state, where the pre-fix bug only ever blocked. That asymmetry is why this
1408
+ // check is worth a `statSync`: the failure it prevents is unrecoverable, and
1409
+ // the check costs no subprocess.
1410
+ if (!confirmedGone(entry.worktree_path)) {
1411
+ blockEntry(result, 'worktree_remove_failed', `worktree ${entry.worktree_path} reappeared after being accepted as absent; refusing to prune or delete its branch`);
1412
+ continue; // #2852: isolate — the merge already landed on repoRoot
1413
+ }
1414
+ // Prune only: it clears the admin entry when the directory really is gone.
1415
+ const prune = execGit(['worktree', 'prune'], { cwd: plan.repoRoot });
1416
+ if (!gitResultOk(prune)) {
1417
+ blockEntry(result, 'worktree_remove_failed', prune?.stderr || '');
1418
+ continue; // #2852: isolate — the merge already landed on repoRoot
1419
+ }
1420
+ }
1421
+ else {
1422
+ let remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
1423
+ if (!gitResultOk(remove)) {
1424
+ // Locked worktrees require unlock before remove (or --force --force).
1425
+ // Attempt: git worktree unlock <path> (ignore failure — already unlocked is ok)
1426
+ // then retry git worktree remove --force. (#3707)
1427
+ execGit(['worktree', 'unlock', entry.worktree_path], { cwd: plan.repoRoot });
1428
+ remove = execGit(['worktree', 'remove', entry.worktree_path, '--force'], { cwd: plan.repoRoot });
1429
+ }
1430
+ if (!gitResultOk(remove)) {
1431
+ // #4415: a remove that fails only because the path is already gone ("is
1432
+ // not a working tree") used to surface as `worktree_remove_failed` AFTER
1433
+ // the merge had already landed, leaving the branch undeleted and the
1434
+ // operator to run `git worktree prune` + `git branch -D` + `rm -rf` by
1435
+ // hand every wave. What is actually left behind is the admin entry under
1436
+ // .git/worktrees, which is exactly what `prune` clears. Staleness is
1437
+ // re-read here rather than reusing the branch-step answer: the harness
1438
+ // removes worktrees on subagent completion, which can land in between.
1439
+ // Asked of git, so a `remove` that failed for any reason OTHER than the
1440
+ // path being gone — a lock this did not clear, a permissions error — still
1441
+ // blocks instead of being tidied away by a prune.
1442
+ if (!absentAndIdentified(entry.worktree_path, entry.branch)) {
1443
+ blockEntry(result, 'worktree_remove_failed', remove?.stderr || '');
1444
+ // #2852: isolate — the merge already landed on repoRoot; only this entry's
1445
+ // worktree/branch teardown is affected.
1446
+ continue;
1447
+ }
1448
+ // NB: `git worktree prune` is repository-wide maintenance, not an
1449
+ // entry-scoped operation — it clears every stale admin entry, not only this
1450
+ // one. NOT harmless, and an earlier version of this comment was wrong to
1451
+ // say so (Codex review round 4): because identity now comes from the
1452
+ // registration, a prune here destroys the evidence later entries in the same
1453
+ // wave need. That is why the identity read is a snapshot taken before the
1454
+ // loop; see `worktreeListSnapshot`.
1455
+ const prune = execGit(['worktree', 'prune'], { cwd: plan.repoRoot });
1456
+ if (!gitResultOk(prune)) {
1457
+ blockEntry(result, 'worktree_remove_failed', prune?.stderr || '');
1458
+ continue; // #2852: isolate
1459
+ }
1460
+ }
996
1461
  }
997
1462
  const branchDelete = execGit(['branch', '-D', entry.branch], { cwd: plan.repoRoot });
998
1463
  if (!gitResultOk(branchDelete)) {
@@ -1946,6 +2411,269 @@ function cmdWorktreeReapOrphans(cwd, deps = {}) {
1946
2411
  }
1947
2412
  write(`${JSON.stringify({ ok: true, reaped: result.filter((r) => r.status === 'reaped').length, entries: result }, null, 2)}\n`);
1948
2413
  }
2414
+ // ─── Worker lifecycle records (#4624) ────────────────────────────────────────
2415
+ // Durable per-worker launch/terminal state for the orchestrator-worktree
2416
+ // backend. The dispatch fragment spawns external executor processes with a
2417
+ // bare background `wait`; when the orchestrator's turn ends before a worker
2418
+ // finishes, nothing records the launch or guarantees reconciliation, and a
2419
+ // resumed session has no state to recover — it re-derives everything from
2420
+ // manual PID/log discovery. These records persist the launch identity, the
2421
+ // result location, and the terminal outcome as a small JSON file beside the
2422
+ // worktree (NOT inside it — cleanup removes the worktree; the record must
2423
+ // survive it), so `worker-status` can answer "who was dispatched, is it
2424
+ // still running, did it finish its artifacts, does it need reconciliation"
2425
+ // deterministically on resume.
2426
+ /** Path of the lifecycle record for a worktree: a SIBLING of the worktree dir. */
2427
+ function workerRecordPath(worktreePath) {
2428
+ return `${worktreePath}.worker.json`;
2429
+ }
2430
+ const RENAME_RETRY_ERRNOS = new Set(['EPERM', 'EBUSY', 'EACCES']);
2431
+ // local/require-fs-op-fallback: a concurrent reader or antivirus scanner can
2432
+ // transiently hold the rename target open on Windows (DEFECT.WINDOWS-FS-OPS) —
2433
+ // bounded retry on the transient errnos, per the house pattern.
2434
+ function renameWithRetry(tmp, target) {
2435
+ let lastErr;
2436
+ for (let attempt = 0; attempt < 4; attempt++) {
2437
+ try {
2438
+ node_fs_1.default.renameSync(tmp, target);
2439
+ return;
2440
+ }
2441
+ catch (err) {
2442
+ lastErr = err;
2443
+ const code = err.code;
2444
+ if (code && RENAME_RETRY_ERRNOS.has(code) && attempt < 3) {
2445
+ const until = Date.now() + 25 * (attempt + 1);
2446
+ while (Date.now() < until) { /* bounded spin: transient locks clear in <100ms */ }
2447
+ continue;
2448
+ }
2449
+ throw err;
2450
+ }
2451
+ }
2452
+ throw lastErr;
2453
+ }
2454
+ function writeWorkerRecord(recordPath, record, deps = {}) {
2455
+ const writeFile = deps.writeFile || ((p, d) => node_fs_1.default.writeFileSync(p, d));
2456
+ const tmp = `${recordPath}.tmp-${process.pid}`;
2457
+ writeFile(tmp, `${JSON.stringify(record, null, 2)}\n`);
2458
+ renameWithRetry(tmp, recordPath);
2459
+ }
2460
+ function cmdWorktreeWorkerRecord(cwd, args = [], deps = {}) {
2461
+ const flag = (name) => {
2462
+ const i = args.indexOf(name);
2463
+ if (i < 0 || i + 1 >= args.length)
2464
+ return '';
2465
+ return args[i + 1];
2466
+ };
2467
+ const write = deps.write || ((s) => process.stdout.write(s));
2468
+ const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
2469
+ const worktreePath = flag('--path');
2470
+ const pidText = flag('--pid');
2471
+ const plan = flag('--plan');
2472
+ const summaryPath = flag('--summary-path');
2473
+ const logFile = flag('--log-file');
2474
+ if (!worktreePath || !pidText || !plan || !summaryPath) {
2475
+ writeErr('Usage: worktree worker-record --path <worktree> --pid <pid> --plan <plan_number> --summary-path <path> [--log-file <path>]\n');
2476
+ process.exitCode = 2;
2477
+ return;
2478
+ }
2479
+ const pid = Number(pidText);
2480
+ if (!Number.isInteger(pid) || pid <= 0) {
2481
+ writeErr(`[gsd] worktree.worker-record: invalid --pid: ${pidText}\n`);
2482
+ process.exitCode = 2;
2483
+ return;
2484
+ }
2485
+ const resolvedWorktree = node_path_1.default.resolve(cwd, worktreePath);
2486
+ const recordPath = workerRecordPath(resolvedWorktree);
2487
+ const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
2488
+ let existing = null;
2489
+ try {
2490
+ existing = JSON.parse(readFile(recordPath));
2491
+ }
2492
+ catch { /* no record yet — first dispatch for this worktree */ }
2493
+ if (existing && existing.state === 'running') {
2494
+ // Duplicate-dispatch guard (#4624): a running record means a resumed
2495
+ // session re-entered the dispatch step. The worker must be reconciled
2496
+ // (worker-status → completion-reconciliation), never re-spawned.
2497
+ const hint = 'A worker is already recorded RUNNING for this worktree. Reconcile it (worktree worker-status, then execute-phase/steps/completion-reconciliation.md) before any new dispatch — never re-dispatch a recorded plan.';
2498
+ writeErr(`[gsd] worktree.worker-record: already_running — ${hint}\n`);
2499
+ write(`${JSON.stringify({ ok: false, reason: 'already_running', hint, record: existing }, null, 2)}\n`);
2500
+ process.exitCode = 1;
2501
+ return;
2502
+ }
2503
+ const record = {
2504
+ agentId: node_path_1.default.basename(resolvedWorktree),
2505
+ pid,
2506
+ plan,
2507
+ worktreePath: resolvedWorktree,
2508
+ summaryPath: node_path_1.default.resolve(cwd, summaryPath),
2509
+ logFile: logFile ? node_path_1.default.resolve(cwd, logFile) : '',
2510
+ startedAt: new Date().toISOString(),
2511
+ state: 'running',
2512
+ exitCode: null,
2513
+ note: '',
2514
+ completedAt: null,
2515
+ };
2516
+ try {
2517
+ writeWorkerRecord(recordPath, record, deps);
2518
+ }
2519
+ catch (err) {
2520
+ writeErr(`[gsd] worktree.worker-record: write_failed — ${err.message}\n`);
2521
+ write(`${JSON.stringify({ ok: false, reason: 'write_failed', error: err.message }, null, 2)}\n`);
2522
+ process.exitCode = 1;
2523
+ return;
2524
+ }
2525
+ write(`${JSON.stringify({ ok: true, record }, null, 2)}\n`);
2526
+ }
2527
+ function workerStatusView(record, deps = {}) {
2528
+ // Only ESRCH is dead (defaultIsPidAlive contract): a verdict feeding a
2529
+ // merge/reconcile decision must fail toward ALIVE on unrecognized errnos.
2530
+ const pidAlive = deps.isPidAlive || defaultIsPidAlive;
2531
+ const exists = deps.existsSync || node_fs_1.default.existsSync;
2532
+ const summaryExists = exists(record.summaryPath);
2533
+ const alive = record.state === 'running' && pidAlive(record.pid);
2534
+ return {
2535
+ agentId: record.agentId,
2536
+ pid: record.pid,
2537
+ plan: record.plan,
2538
+ worktreePath: record.worktreePath,
2539
+ summaryPath: record.summaryPath,
2540
+ logFile: record.logFile,
2541
+ state: record.state,
2542
+ exitCode: record.exitCode,
2543
+ note: record.note,
2544
+ startedAt: record.startedAt,
2545
+ completedAt: record.completedAt,
2546
+ pidAlive: record.state === 'running' ? alive : null,
2547
+ summaryExists,
2548
+ needsReconciliation: record.state === 'running' && !alive,
2549
+ };
2550
+ }
2551
+ function readWorkerRecordsFromRoot(root, readFile, readdir) {
2552
+ let entries = [];
2553
+ try {
2554
+ entries = readdir(root).filter((f) => f.endsWith('.worker.json'));
2555
+ }
2556
+ catch { /* root missing — no workers ever recorded */
2557
+ return [];
2558
+ }
2559
+ const out = [];
2560
+ for (const entry of entries) {
2561
+ const recordPath = node_path_1.default.join(root, entry);
2562
+ try {
2563
+ out.push({ path: recordPath, record: JSON.parse(readFile(recordPath)) });
2564
+ }
2565
+ catch { // a torn/partial record must not hide the others
2566
+ out.push({ path: recordPath, record: null });
2567
+ }
2568
+ }
2569
+ return out;
2570
+ }
2571
+ function cmdWorktreeWorkerStatus(cwd, args = [], deps = {}) {
2572
+ const flag = (name) => {
2573
+ const i = args.indexOf(name);
2574
+ if (i < 0 || i + 1 >= args.length)
2575
+ return '';
2576
+ return args[i + 1];
2577
+ };
2578
+ const write = deps.write || ((s) => process.stdout.write(s));
2579
+ const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
2580
+ const worktreePath = flag('--path');
2581
+ const root = flag('--root');
2582
+ if (!worktreePath === !root) { // exactly one of the two
2583
+ writeErr('Usage: worktree worker-status (--path <worktree> | --root <worktrees-dir>)\n');
2584
+ process.exitCode = 2;
2585
+ return;
2586
+ }
2587
+ const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
2588
+ const readdir = deps.readdir || ((p) => node_fs_1.default.readdirSync(p));
2589
+ const views = [];
2590
+ if (worktreePath) {
2591
+ const recordPath = workerRecordPath(node_path_1.default.resolve(cwd, worktreePath));
2592
+ let record = null;
2593
+ let unreadable = false;
2594
+ try {
2595
+ record = JSON.parse(readFile(recordPath));
2596
+ }
2597
+ catch {
2598
+ // A record that exists but cannot be parsed is NOT "never dispatched" —
2599
+ // conflating the two invites a re-dispatch. Surface it as a candidate.
2600
+ unreadable = node_fs_1.default.existsSync(recordPath);
2601
+ }
2602
+ if (unreadable) {
2603
+ // exists but unparseable — a reconciliation candidate, never "never dispatched"
2604
+ views.push({ state: 'unreadable', needsReconciliation: true, recordPath });
2605
+ }
2606
+ else if (record) {
2607
+ views.push(workerStatusView(record, deps));
2608
+ }
2609
+ else {
2610
+ // no record file: genuinely never dispatched (found:false)
2611
+ write(`${JSON.stringify({ ok: true, found: false, workers: [] }, null, 2)}\n`);
2612
+ return;
2613
+ }
2614
+ }
2615
+ else {
2616
+ for (const { record } of readWorkerRecordsFromRoot(node_path_1.default.resolve(cwd, root), readFile, readdir)) {
2617
+ if (!record) { // torn record is itself a reconciliation candidate
2618
+ views.push({ state: 'unreadable', needsReconciliation: true });
2619
+ continue;
2620
+ }
2621
+ views.push(workerStatusView(record, deps));
2622
+ }
2623
+ }
2624
+ write(`${JSON.stringify({ ok: true, found: true, workers: views }, null, 2)}\n`);
2625
+ }
2626
+ function cmdWorktreeWorkerComplete(cwd, args = [], deps = {}) {
2627
+ const flag = (name) => {
2628
+ const i = args.indexOf(name);
2629
+ if (i < 0 || i + 1 >= args.length)
2630
+ return '';
2631
+ return args[i + 1];
2632
+ };
2633
+ const write = deps.write || ((s) => process.stdout.write(s));
2634
+ const writeErr = deps.writeErr || ((s) => process.stderr.write(s));
2635
+ const worktreePath = flag('--path');
2636
+ const exitText = flag('--exit-code');
2637
+ const note = flag('--note');
2638
+ if (!worktreePath || !exitText) {
2639
+ writeErr('Usage: worktree worker-complete --path <worktree> --exit-code <n> [--note <recovery info>]\n');
2640
+ process.exitCode = 2;
2641
+ return;
2642
+ }
2643
+ const exitCode = Number(exitText);
2644
+ if (!Number.isInteger(exitCode)) {
2645
+ writeErr(`[gsd] worktree.worker-complete: invalid --exit-code: ${exitText}\n`);
2646
+ process.exitCode = 2;
2647
+ return;
2648
+ }
2649
+ const recordPath = workerRecordPath(node_path_1.default.resolve(cwd, worktreePath));
2650
+ const readFile = deps.readFile || ((p) => node_fs_1.default.readFileSync(p, 'utf8'));
2651
+ let record;
2652
+ try {
2653
+ record = JSON.parse(readFile(recordPath));
2654
+ }
2655
+ catch (err) {
2656
+ writeErr(`[gsd] worktree.worker-complete: no_record — ${err.message}\n`);
2657
+ write(`${JSON.stringify({ ok: false, reason: 'no_record', error: err.message }, null, 2)}\n`);
2658
+ process.exitCode = 1;
2659
+ return;
2660
+ }
2661
+ const alreadyComplete = record.state === 'complete';
2662
+ record.state = 'complete';
2663
+ record.exitCode = exitCode;
2664
+ record.note = note || record.note || '';
2665
+ record.completedAt = record.completedAt || new Date().toISOString();
2666
+ try {
2667
+ writeWorkerRecord(recordPath, record, deps);
2668
+ }
2669
+ catch (err) {
2670
+ writeErr(`[gsd] worktree.worker-complete: write_failed — ${err.message}\n`);
2671
+ write(`${JSON.stringify({ ok: false, reason: 'write_failed', error: err.message }, null, 2)}\n`);
2672
+ process.exitCode = 1;
2673
+ return;
2674
+ }
2675
+ write(`${JSON.stringify({ ok: true, alreadyComplete, record }, null, 2)}\n`);
2676
+ }
1949
2677
  // Unused exports kept for API compatibility
1950
2678
  void parseWorktreeListPaths;
1951
2679
  // ─── Moved from core.cjs (ADR-857 T0 #1268 rehome-core-squatters) ─────────────
@@ -2012,6 +2740,7 @@ module.exports = {
2012
2740
  planWorktreeWaveCleanup,
2013
2741
  executeWorktreeWaveCleanupPlan,
2014
2742
  WAVE_CLEANUP_WARNING,
2743
+ DEFAULT_MERGE_TIMEOUT_MS,
2015
2744
  planWaveScopeConformance,
2016
2745
  isSummaryArtifactRelPath,
2017
2746
  cmdWorktreeCleanupWave,
@@ -2022,6 +2751,10 @@ module.exports = {
2022
2751
  cmdWorktreeCreate,
2023
2752
  reapOrphanWorktrees,
2024
2753
  cmdWorktreeReapOrphans,
2754
+ workerRecordPath,
2755
+ cmdWorktreeWorkerRecord,
2756
+ cmdWorktreeWorkerStatus,
2757
+ cmdWorktreeWorkerComplete,
2025
2758
  resolveWorktreeRoot,
2026
2759
  pruneOrphanedWorktrees,
2027
2760
  };