dorfl 0.1.2 → 0.2.1

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 (364) hide show
  1. package/dist/advance-drivers.d.ts +1 -1
  2. package/dist/advance-drivers.d.ts.map +1 -1
  3. package/dist/advance-drivers.js +15 -3
  4. package/dist/advance-drivers.js.map +1 -1
  5. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  6. package/dist/advance-lifecycle-template.js +76 -4
  7. package/dist/advance-lifecycle-template.js.map +1 -1
  8. package/dist/advance-treeless-publish.d.ts +23 -0
  9. package/dist/advance-treeless-publish.d.ts.map +1 -1
  10. package/dist/advance-treeless-publish.js +41 -0
  11. package/dist/advance-treeless-publish.js.map +1 -1
  12. package/dist/advance.d.ts +59 -7
  13. package/dist/advance.d.ts.map +1 -1
  14. package/dist/advance.js +389 -82
  15. package/dist/advance.js.map +1 -1
  16. package/dist/advancing-lock.d.ts +30 -2
  17. package/dist/advancing-lock.d.ts.map +1 -1
  18. package/dist/advancing-lock.js +50 -0
  19. package/dist/advancing-lock.js.map +1 -1
  20. package/dist/agent-launch.d.ts +12 -0
  21. package/dist/agent-launch.d.ts.map +1 -1
  22. package/dist/agent-launch.js +22 -12
  23. package/dist/agent-launch.js.map +1 -1
  24. package/dist/agent-stop.d.ts +40 -2
  25. package/dist/agent-stop.d.ts.map +1 -1
  26. package/dist/agent-stop.js +30 -2
  27. package/dist/agent-stop.js.map +1 -1
  28. package/dist/apply-decide.d.ts +17 -3
  29. package/dist/apply-decide.d.ts.map +1 -1
  30. package/dist/apply-decide.js +36 -7
  31. package/dist/apply-decide.js.map +1 -1
  32. package/dist/apply-merge-action.d.ts +17 -5
  33. package/dist/apply-merge-action.d.ts.map +1 -1
  34. package/dist/apply-merge-action.js +43 -8
  35. package/dist/apply-merge-action.js.map +1 -1
  36. package/dist/apply-persist.d.ts +57 -26
  37. package/dist/apply-persist.d.ts.map +1 -1
  38. package/dist/apply-persist.js +166 -37
  39. package/dist/apply-persist.js.map +1 -1
  40. package/dist/apply-stuck-action.d.ts +151 -0
  41. package/dist/apply-stuck-action.d.ts.map +1 -0
  42. package/dist/apply-stuck-action.js +125 -0
  43. package/dist/apply-stuck-action.js.map +1 -0
  44. package/dist/brand.d.ts +12 -1
  45. package/dist/brand.d.ts.map +1 -1
  46. package/dist/brand.js +2 -1
  47. package/dist/brand.js.map +1 -1
  48. package/dist/cli-spinner.d.ts +1 -1
  49. package/dist/cli-spinner.js +1 -1
  50. package/dist/cli.d.ts +10 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +320 -63
  53. package/dist/cli.js.map +1 -1
  54. package/dist/close-job-template.js +1 -1
  55. package/dist/complete.d.ts +6 -3
  56. package/dist/complete.d.ts.map +1 -1
  57. package/dist/complete.js +91 -37
  58. package/dist/complete.js.map +1 -1
  59. package/dist/config.d.ts +55 -9
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +56 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/continue-branch.d.ts.map +1 -1
  64. package/dist/continue-branch.js +22 -0
  65. package/dist/continue-branch.js.map +1 -1
  66. package/dist/decision-engine.d.ts +38 -10
  67. package/dist/decision-engine.d.ts.map +1 -1
  68. package/dist/decision-engine.js +10 -6
  69. package/dist/decision-engine.js.map +1 -1
  70. package/dist/do-autopick.d.ts.map +1 -1
  71. package/dist/do-autopick.js +11 -1
  72. package/dist/do-autopick.js.map +1 -1
  73. package/dist/do-config.d.ts +1 -1
  74. package/dist/do.d.ts +49 -56
  75. package/dist/do.d.ts.map +1 -1
  76. package/dist/do.js +419 -30
  77. package/dist/do.js.map +1 -1
  78. package/dist/env-config.d.ts.map +1 -1
  79. package/dist/env-config.js +12 -4
  80. package/dist/env-config.js.map +1 -1
  81. package/dist/failure-cause.d.ts +3 -2
  82. package/dist/failure-cause.d.ts.map +1 -1
  83. package/dist/failure-cause.js +28 -1
  84. package/dist/failure-cause.js.map +1 -1
  85. package/dist/format.d.ts +6 -6
  86. package/dist/format.d.ts.map +1 -1
  87. package/dist/format.js +10 -27
  88. package/dist/format.js.map +1 -1
  89. package/dist/frontmatter.d.ts +16 -4
  90. package/dist/frontmatter.d.ts.map +1 -1
  91. package/dist/frontmatter.js +27 -1
  92. package/dist/frontmatter.js.map +1 -1
  93. package/dist/gc.d.ts +65 -6
  94. package/dist/gc.d.ts.map +1 -1
  95. package/dist/gc.js +126 -9
  96. package/dist/gc.js.map +1 -1
  97. package/dist/github.d.ts +14 -0
  98. package/dist/github.d.ts.map +1 -1
  99. package/dist/github.js +73 -0
  100. package/dist/github.js.map +1 -1
  101. package/dist/harness.d.ts +25 -0
  102. package/dist/harness.d.ts.map +1 -1
  103. package/dist/harness.js.map +1 -1
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +2 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/install-ci-branch-protection.d.ts +132 -38
  109. package/dist/install-ci-branch-protection.d.ts.map +1 -1
  110. package/dist/install-ci-branch-protection.js +190 -48
  111. package/dist/install-ci-branch-protection.js.map +1 -1
  112. package/dist/install-ci-core.d.ts +21 -1
  113. package/dist/install-ci-core.d.ts.map +1 -1
  114. package/dist/install-ci-core.js +10 -3
  115. package/dist/install-ci-core.js.map +1 -1
  116. package/dist/install-ci-github.d.ts +32 -0
  117. package/dist/install-ci-github.d.ts.map +1 -1
  118. package/dist/install-ci-github.js +73 -0
  119. package/dist/install-ci-github.js.map +1 -1
  120. package/dist/install-skills.d.ts +123 -0
  121. package/dist/install-skills.d.ts.map +1 -0
  122. package/dist/install-skills.js +100 -0
  123. package/dist/install-skills.js.map +1 -0
  124. package/dist/intake-trigger-template.js +1 -1
  125. package/dist/intake.d.ts +17 -4
  126. package/dist/intake.d.ts.map +1 -1
  127. package/dist/intake.js +23 -7
  128. package/dist/intake.js.map +1 -1
  129. package/dist/integration-core.d.ts +10 -7
  130. package/dist/integration-core.d.ts.map +1 -1
  131. package/dist/integration-core.js +174 -60
  132. package/dist/integration-core.js.map +1 -1
  133. package/dist/integrator.d.ts +1 -1
  134. package/dist/integrator.d.ts.map +1 -1
  135. package/dist/integrator.js +19 -3
  136. package/dist/integrator.js.map +1 -1
  137. package/dist/isolation.d.ts +2 -2
  138. package/dist/isolation.d.ts.map +1 -1
  139. package/dist/isolation.js +9 -0
  140. package/dist/isolation.js.map +1 -1
  141. package/dist/item-lock.d.ts +196 -129
  142. package/dist/item-lock.d.ts.map +1 -1
  143. package/dist/item-lock.js +329 -254
  144. package/dist/item-lock.js.map +1 -1
  145. package/dist/ledger-write.d.ts +31 -26
  146. package/dist/ledger-write.d.ts.map +1 -1
  147. package/dist/ledger-write.js +100 -122
  148. package/dist/ledger-write.js.map +1 -1
  149. package/dist/lifecycle-gather.d.ts +17 -0
  150. package/dist/lifecycle-gather.d.ts.map +1 -1
  151. package/dist/lifecycle-gather.js +7 -1
  152. package/dist/lifecycle-gather.js.map +1 -1
  153. package/dist/lifecycle-pools.d.ts +40 -3
  154. package/dist/lifecycle-pools.d.ts.map +1 -1
  155. package/dist/lifecycle-pools.js +26 -6
  156. package/dist/lifecycle-pools.js.map +1 -1
  157. package/dist/merge-question-surfacer.d.ts +15 -1
  158. package/dist/merge-question-surfacer.d.ts.map +1 -1
  159. package/dist/merge-question-surfacer.js +18 -3
  160. package/dist/merge-question-surfacer.js.map +1 -1
  161. package/dist/migrate-stuck-locks.d.ts +129 -0
  162. package/dist/migrate-stuck-locks.d.ts.map +1 -0
  163. package/dist/migrate-stuck-locks.js +355 -0
  164. package/dist/migrate-stuck-locks.js.map +1 -0
  165. package/dist/needs-attention.d.ts +293 -16
  166. package/dist/needs-attention.d.ts.map +1 -1
  167. package/dist/needs-attention.js +532 -57
  168. package/dist/needs-attention.js.map +1 -1
  169. package/dist/orphan-sidecar.d.ts +8 -4
  170. package/dist/orphan-sidecar.d.ts.map +1 -1
  171. package/dist/orphan-sidecar.js +35 -2
  172. package/dist/orphan-sidecar.js.map +1 -1
  173. package/dist/pi-harness.d.ts +16 -0
  174. package/dist/pi-harness.d.ts.map +1 -1
  175. package/dist/pi-harness.js +82 -2
  176. package/dist/pi-harness.js.map +1 -1
  177. package/dist/placement.d.ts +3 -3
  178. package/dist/placement.js +1 -1
  179. package/dist/prd-to-spec.d.ts.map +1 -1
  180. package/dist/prd-to-spec.js +9 -5
  181. package/dist/prd-to-spec.js.map +1 -1
  182. package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
  183. package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
  184. package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
  185. package/dist/protocol/TASKING-PROTOCOL.md +3 -1
  186. package/dist/protocol/WORK-CONTRACT.md +22 -18
  187. package/dist/protocol/task-template.md +1 -1
  188. package/dist/reap-branches.d.ts +12 -9
  189. package/dist/reap-branches.d.ts.map +1 -1
  190. package/dist/reap-branches.js +25 -7
  191. package/dist/reap-branches.js.map +1 -1
  192. package/dist/recover-isolated.d.ts +12 -0
  193. package/dist/recover-isolated.d.ts.map +1 -1
  194. package/dist/recover-isolated.js +5 -0
  195. package/dist/recover-isolated.js.map +1 -1
  196. package/dist/repo-config.d.ts +23 -2
  197. package/dist/repo-config.d.ts.map +1 -1
  198. package/dist/repo-config.js +53 -7
  199. package/dist/repo-config.js.map +1 -1
  200. package/dist/repo-mirror.d.ts.map +1 -1
  201. package/dist/repo-mirror.js +18 -2
  202. package/dist/repo-mirror.js.map +1 -1
  203. package/dist/review-verdict.js +1 -1
  204. package/dist/review-verdict.js.map +1 -1
  205. package/dist/run.d.ts +1 -1
  206. package/dist/run.d.ts.map +1 -1
  207. package/dist/run.js +54 -19
  208. package/dist/run.js.map +1 -1
  209. package/dist/scan.d.ts +17 -8
  210. package/dist/scan.d.ts.map +1 -1
  211. package/dist/scan.js +22 -8
  212. package/dist/scan.js.map +1 -1
  213. package/dist/select-priority.d.ts +2 -2
  214. package/dist/select-priority.js +1 -1
  215. package/dist/select-priority.js.map +1 -1
  216. package/dist/sidecar-apply.js +1 -1
  217. package/dist/sidecar-apply.js.map +1 -1
  218. package/dist/sidecar.d.ts +45 -10
  219. package/dist/sidecar.d.ts.map +1 -1
  220. package/dist/sidecar.js +82 -2
  221. package/dist/sidecar.js.map +1 -1
  222. package/dist/skills/answer-questions/SKILL.md +89 -0
  223. package/dist/skills/capture-signal/SKILL.md +52 -0
  224. package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
  225. package/dist/skills/drive-tasks/SKILL.md +218 -0
  226. package/dist/skills/from-idea/SKILL.md +83 -0
  227. package/dist/skills/merge-prs/SKILL.md +70 -0
  228. package/dist/skills/orchestrate/SKILL.md +101 -0
  229. package/dist/skills/promote/SKILL.md +35 -0
  230. package/dist/skills/review/SKILL.md +16 -0
  231. package/dist/skills/setup/SKILL.md +258 -0
  232. package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
  233. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
  234. package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
  235. package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
  236. package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
  237. package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
  238. package/dist/skills/setup/protocol/spec-template.md +71 -0
  239. package/dist/skills/setup/protocol/task-template.md +65 -0
  240. package/dist/skills/surface-questions/SKILL.md +16 -0
  241. package/dist/skills/to-spec/SKILL.md +34 -0
  242. package/dist/skills/to-task/SKILL.md +19 -0
  243. package/dist/skills/triage-observations/SKILL.md +78 -0
  244. package/dist/skills/work/SKILL.md +51 -0
  245. package/dist/slug-namespace.d.ts +5 -5
  246. package/dist/slug-namespace.js +5 -5
  247. package/dist/slug-namespace.js.map +1 -1
  248. package/dist/spec-complete.d.ts.map +1 -1
  249. package/dist/spec-complete.js +2 -3
  250. package/dist/spec-complete.js.map +1 -1
  251. package/dist/start.d.ts.map +1 -1
  252. package/dist/start.js +51 -57
  253. package/dist/start.js.map +1 -1
  254. package/dist/surface-gate.d.ts.map +1 -1
  255. package/dist/surface-gate.js +10 -3
  256. package/dist/surface-gate.js.map +1 -1
  257. package/dist/surface-persist.js +1 -1
  258. package/dist/surface-persist.js.map +1 -1
  259. package/dist/tasker-review-loop.d.ts +4 -3
  260. package/dist/tasker-review-loop.d.ts.map +1 -1
  261. package/dist/tasker-review-loop.js.map +1 -1
  262. package/dist/tasking-lock.d.ts +2 -2
  263. package/dist/tasking-lock.d.ts.map +1 -1
  264. package/dist/tasking-lock.js +53 -32
  265. package/dist/tasking-lock.js.map +1 -1
  266. package/dist/tasking.d.ts +40 -6
  267. package/dist/tasking.d.ts.map +1 -1
  268. package/dist/tasking.js +224 -12
  269. package/dist/tasking.js.map +1 -1
  270. package/dist/triage-persist.d.ts +10 -6
  271. package/dist/triage-persist.d.ts.map +1 -1
  272. package/dist/triage-persist.js +34 -4
  273. package/dist/triage-persist.js.map +1 -1
  274. package/dist/vendor/incur/agents.d.ts +58 -0
  275. package/dist/vendor/incur/agents.d.ts.map +1 -0
  276. package/dist/vendor/incur/agents.js +343 -0
  277. package/dist/vendor/incur/agents.js.map +1 -0
  278. package/dist/verify-workflow-template.js +1 -1
  279. package/dist/watch-session.d.ts +11 -3
  280. package/dist/watch-session.d.ts.map +1 -1
  281. package/dist/watch-session.js +94 -7
  282. package/dist/watch-session.js.map +1 -1
  283. package/dist/work-layout.d.ts +7 -3
  284. package/dist/work-layout.d.ts.map +1 -1
  285. package/dist/work-layout.js +6 -3
  286. package/dist/work-layout.js.map +1 -1
  287. package/dist/workspace.d.ts +1 -1
  288. package/package.json +2 -2
  289. package/src/advance-drivers.ts +16 -3
  290. package/src/advance-lifecycle-template.ts +89 -4
  291. package/src/advance-treeless-publish.ts +45 -0
  292. package/src/advance.ts +467 -87
  293. package/src/advancing-lock.ts +96 -1
  294. package/src/agent-launch.ts +37 -12
  295. package/src/agent-stop.ts +60 -2
  296. package/src/apply-decide.ts +36 -7
  297. package/src/apply-merge-action.ts +45 -7
  298. package/src/apply-persist.ts +228 -54
  299. package/src/apply-stuck-action.ts +260 -0
  300. package/src/brand.ts +14 -2
  301. package/src/cli-spinner.ts +1 -1
  302. package/src/cli.ts +401 -70
  303. package/src/close-job-template.ts +1 -1
  304. package/src/complete.ts +110 -52
  305. package/src/config.ts +107 -13
  306. package/src/continue-branch.ts +22 -0
  307. package/src/decision-engine.ts +55 -17
  308. package/src/do-autopick.ts +12 -1
  309. package/src/do-config.ts +1 -1
  310. package/src/do.ts +511 -38
  311. package/src/env-config.ts +12 -4
  312. package/src/failure-cause.ts +30 -1
  313. package/src/format.ts +10 -30
  314. package/src/frontmatter.ts +35 -5
  315. package/src/gc.ts +172 -9
  316. package/src/github.ts +78 -0
  317. package/src/harness.ts +25 -0
  318. package/src/index.ts +10 -0
  319. package/src/install-ci-branch-protection.ts +282 -57
  320. package/src/install-ci-core.ts +30 -3
  321. package/src/install-ci-github.ts +86 -0
  322. package/src/install-skills.ts +166 -0
  323. package/src/intake-trigger-template.ts +1 -1
  324. package/src/intake.ts +46 -11
  325. package/src/integration-core.ts +199 -65
  326. package/src/integrator.ts +22 -9
  327. package/src/isolation.ts +11 -2
  328. package/src/item-lock.ts +485 -341
  329. package/src/ledger-write.ts +135 -150
  330. package/src/lifecycle-gather.ts +29 -6
  331. package/src/lifecycle-pools.ts +67 -9
  332. package/src/merge-question-surfacer.ts +28 -4
  333. package/src/migrate-stuck-locks.ts +451 -0
  334. package/src/needs-attention.ts +857 -84
  335. package/src/orphan-sidecar.ts +47 -6
  336. package/src/pi-harness.ts +82 -2
  337. package/src/placement.ts +3 -3
  338. package/src/prd-to-spec.ts +13 -5
  339. package/src/reap-branches.ts +38 -14
  340. package/src/recover-isolated.ts +17 -0
  341. package/src/repo-config.ts +55 -6
  342. package/src/repo-mirror.ts +23 -1
  343. package/src/review-verdict.ts +1 -1
  344. package/src/run.ts +64 -18
  345. package/src/scan.ts +22 -8
  346. package/src/select-priority.ts +3 -3
  347. package/src/sidecar-apply.ts +1 -1
  348. package/src/sidecar.ts +138 -11
  349. package/src/slug-namespace.ts +7 -7
  350. package/src/spec-complete.ts +2 -3
  351. package/src/start.ts +53 -59
  352. package/src/surface-gate.ts +10 -3
  353. package/src/surface-persist.ts +1 -1
  354. package/src/tasker-review-loop.ts +4 -3
  355. package/src/tasking-lock.ts +52 -36
  356. package/src/tasking.ts +261 -17
  357. package/src/triage-persist.ts +56 -12
  358. package/src/vendor/incur/LICENSE +21 -0
  359. package/src/vendor/incur/README.md +19 -0
  360. package/src/vendor/incur/agents.ts +392 -0
  361. package/src/verify-workflow-template.ts +1 -1
  362. package/src/watch-session.ts +102 -7
  363. package/src/work-layout.ts +6 -3
  364. package/src/workspace.ts +1 -1
@@ -313,8 +313,8 @@ export interface IntegrationCoreInput {
313
313
  // like a normal build.
314
314
  source: 'tasks-ready' | 'tasks-backlog' | 'in-progress' | 'done';
315
315
  /**
316
- * Vestigial: was `true` when completing FROM `work/needs-attention/` (a
317
- * recovery finish under the legacy folder model). The per-item-lock cutover
316
+ * Vestigial: was `true` when completing FROM the retired `needs-attention/`
317
+ * folder (a recovery finish under the legacy folder model). The per-item-lock cutover
318
318
  * (`cutover-needs-attention-becomes-lock-stuck-recovery-surface`) retired
319
319
  * that folder — a stuck item is now the lock `state: stuck` and the body
320
320
  * stays in `tasks/ready/`, so a recovery `complete` is structurally a normal
@@ -579,9 +579,11 @@ export interface IntegrationCoreResult {
579
579
  /** What the core resolved (the tail maps this 1:1 onto its own outcome). */
580
580
  outcome: IntegrationCoreOutcome;
581
581
  /**
582
- * True iff a FAILURE outcome was routed to `work/needs-attention/` via the
583
- * shared mechanism. False on the success path and when a failure was NOT moved
584
- * (e.g. a red re-gate that stays put in recovery).
582
+ * True iff a FAILURE outcome was routed through the shared needs-attention
583
+ * mechanism post lock-cutover, the per-item lock was amended to
584
+ * `state: stuck` with the reason on the lock entry (no folder move). False
585
+ * on the success path and when a failure was NOT surfaced (e.g. a red
586
+ * re-gate that stays put in recovery).
585
587
  */
586
588
  routedToNeedsAttention: boolean;
587
589
  /** The work branch that was processed (`work/<slug>`). */
@@ -687,6 +689,19 @@ export async function performIntegration(
687
689
  recoveryRebaseJitterMs: input.recoveryRebaseJitterMs,
688
690
  recoveryRebaseSleep: input.recoveryRebaseSleep,
689
691
  recoveryRebaseRandom: input.recoveryRebaseRandom,
692
+ // GATE-2 REVIEW on the recovery path (task
693
+ // `committed-recovery-always-reviews`): a stranded already-complete
694
+ // branch is NOT "already reviewed" — its earlier attempt's PR never
695
+ // merged and Gate 2 may never have run on it. So when `review` is on we
696
+ // MUST run Gate 2 on the rebased tip here too (the SAME reasoning that
697
+ // forces the re-verify: `<arbiter>/main` moved, the merged tree is new).
698
+ // Pass the whole `input` so the recovery tail can call `runGate2Review`
699
+ // with the identical gate/model/watch/round wiring the build path uses.
700
+ input,
701
+ // PR BODY on the recovery path: the recovery has NO fresh agent output
702
+ // (no rebuild), so carry whatever `body` the caller supplied so the PR
703
+ // gets a description instead of degrading to a blank `gh --fill`.
704
+ body: input.body,
690
705
  // Answered-merge land (task `committed-recovery-honours-fresh-worktree-gate`,
691
706
  // spec `land-time-reverify-and-parallel-merge-ceiling`): the apply-rung
692
707
  // dispatches an answered `merge` through this committed-recovery tail (the
@@ -740,7 +755,7 @@ export async function performIntegration(
740
755
  // passes no flag, so there an untrusted-origin task RELIABLY forces propose.
741
756
  //
742
757
  // SCOPE: the task BUILD transition ONLY. It NEVER fires for a `lifecycle`
743
- // transition (tasking / intake-emit — a prd/task FILE landing on main is inert;
758
+ // transition (tasking / intake-emit — a spec/task FILE landing on main is inert;
744
759
  // intake's OWN per-emit resolver already decided that mode), and the source file
745
760
  // here is the task being built. A `trusted`/unset task ⇒ untouched (zero
746
761
  // behaviour change for the normal human path).
@@ -793,10 +808,11 @@ export async function performIntegration(
793
808
  }
794
809
  if (!prep.passed) {
795
810
  const reason = `prepare (env-prep) failed (exit ${prep.exitCode})`;
796
- // Bounce from in-progress/ straight to needs-attention/ (the SAME seam a red
797
- // gate uses) — recording the prepare-failed reason + committing the move
798
- // (with the agent's uncommitted work) as ONE atomic transition. We NEVER
799
- // run `verify` on an env that could not be made ready.
811
+ // Mark the item stuck on its per-item lock (the SAME seam a red gate uses)
812
+ // — recording the prepare-failed reason on the lock entry + saving the
813
+ // agent's uncommitted work as a wip commit (post lock-cutover: a lock amend,
814
+ // no `in-progress/ needs-attention/` folder move). We NEVER run `verify`
815
+ // on an env that could not be made ready.
800
816
  const routed = await ledgerWrite.applyNeedsAttentionTransition({
801
817
  cwd,
802
818
  slug,
@@ -810,10 +826,10 @@ export async function performIntegration(
810
826
  routedToNeedsAttention: routed.moved,
811
827
  branch,
812
828
  reason: routed.moved
813
- ? `Env-prep (prepare) failed (exit ${prep.exitCode}); routed '${slug}' ` +
814
- 'to work/needs-attention/ (the environment could not be made ready, ' +
829
+ ? `Env-prep (prepare) failed (exit ${prep.exitCode}); marked '${slug}' ` +
830
+ 'stuck on its per-item lock (the environment could not be made ready, ' +
815
831
  'so the acceptance gate was NOT run). Fix the prepare command, then ' +
816
- 'return it to backlog/.'
832
+ '`requeue` to release the stuck lock.'
817
833
  : `Env-prep (prepare) failed (exit ${prep.exitCode}); not completing ` +
818
834
  `'${slug}' (the environment could not be made ready, so the ` +
819
835
  'acceptance gate was NOT run). Fix the prepare command, then retry.',
@@ -832,12 +848,12 @@ export async function performIntegration(
832
848
  note('Running the acceptance gate (verify)…');
833
849
  const gate = await runVerify({cwd, verify: input.verify, env});
834
850
  if (!gate.passed) {
835
- // Don't leave the item dangling in in-progress/: route it to
836
- // needs-attention/ with the reason (ADR §12) THROUGH the ledger write
837
- // seam's needs-attention transition. The item has NOT been committed/moved
838
- // yet, so the move bounces it straight from in-progress/ recording the
839
- // reason + committing the move (with the agent's uncommitted work) as ONE
840
- // atomic transition. No partial state.
851
+ // Don't leave the item dangling: mark it stuck on its per-item lock with
852
+ // the reason (ADR §12) THROUGH the ledger write seam's needs-attention
853
+ // transition. Post lock-cutover this is a lock amend (`state: stuck` + the
854
+ // reason on the lock entry, no `in-progress/ needs-attention/` folder
855
+ // move) plus saving the agent's uncommitted work as a wip commit. No
856
+ // partial state.
841
857
  const reason = `acceptance gate failed (exit ${gate.exitCode})`;
842
858
  const routed = await ledgerWrite.applyNeedsAttentionTransition({
843
859
  cwd,
@@ -860,9 +876,9 @@ export async function performIntegration(
860
876
  routedToNeedsAttention: routed.moved,
861
877
  branch,
862
878
  reason: routed.moved
863
- ? `Acceptance gate failed (exit ${gate.exitCode}); routed '${slug}' ` +
864
- 'to work/needs-attention/ (surfaced by status; return to backlog/ ' +
865
- 'once resolved). Fix the work, or use --skip-verify to override.'
879
+ ? `Acceptance gate failed (exit ${gate.exitCode}); marked '${slug}' ` +
880
+ 'stuck on its per-item lock (surfaced by status; `requeue` once ' +
881
+ 'resolved). Fix the work, or use --skip-verify to override.'
866
882
  : `Acceptance gate failed (exit ${gate.exitCode}); not completing ` +
867
883
  `'${slug}'. Fix the work, or use --skip-verify to override.`,
868
884
  };
@@ -1038,16 +1054,14 @@ export async function performIntegration(
1038
1054
  // 4. Rebase-before-integrate (ADR §10): rebase the work branch onto the
1039
1055
  // latest <arbiter>/main. Clean → continue. Conflict → abort + stop.
1040
1056
  //
1041
- // RECOVERY reconciliation: when completing FROM needs-attention/, the work
1042
- // branch's history still carries the original `in-progress needs-attention`
1043
- // MOVE-ONLY commit, and `<arbiter>/main` was SURFACED with that same move
1044
- // (the item is in needs-attention/ on main). Replaying that historical move
1045
- // onto main conflicts (main has no in-progress/<slug>.md) exactly the
1046
- // rebase conflict the human hit doing this by hand. So we DROP that move-only
1047
- // commit during the rebase: the replay becomes `wip + (needs-attention →
1048
- // done)`, which applies cleanly onto the surfaced main (it HAS the item in
1049
- // needs-attention/). The done-move thus SUPERSEDES the surfaced state — no
1050
- // leftover/conflicting on-`main` surface for the human to resolve.
1057
+ // RECOVERY reconciliation: post the per-item-lock cut-over, a stuck item's
1058
+ // body never moved into a `needs-attention/` folder (stuck is the lock
1059
+ // `state: stuck`; the body rests in `backlog/`), so a recovery `complete`'s
1060
+ // kept branch carries NO historical `in-progress needs-attention`
1061
+ // move-only commit to drop and `<arbiter>/main` holds no surface move to
1062
+ // conflict with. The old recovery drop (`rebaseDroppingNeedsAttentionSurface`,
1063
+ // drop-bookkeeping-rebase) is deleted; recovery is now the SAME plain replay
1064
+ // onto `<arbiter>/main` as any other build (see the plain-rebase note below).
1051
1065
  // Fetch the arbiter's `main` into the `<arbiter>/main` remote-tracking ref
1052
1066
  // EXPLICITLY. A `run` JOB WORKTREE is cut from a bare hub mirror whose remote
1053
1067
  // has no fetch refspec (so `<arbiter>/main` would not otherwise resolve / would
@@ -1209,12 +1223,12 @@ export async function performIntegration(
1209
1223
  // recovery above can fall through to integrate while a genuine code conflict
1210
1224
  // still routes here.
1211
1225
  async function rebaseConflictRoute(): Promise<IntegrationCoreResult> {
1212
- // Then route the item to needs-attention/ with the conflict reason (ADR
1213
- // §12) THROUGH the ledger write seam's needs-attention transition, rather
1214
- // than leaving it dangling in done/. The done-move was already committed
1215
- // above, so the item sits in work/done/; the move bounces it from there and
1216
- // commits the in-progress→needs-attention move (here done→needs-attention)
1217
- // as ONE transition. No partial state.
1226
+ // Then mark the item stuck on its per-item lock with the conflict reason
1227
+ // (ADR §12) THROUGH the ledger write seam's needs-attention transition,
1228
+ // rather than leaving it dangling. Post lock-cutover this is a lock amend
1229
+ // (`state: stuck` + the reason on the lock entry, no `done/
1230
+ // needs-attention/` folder move); the done-move already committed above
1231
+ // stands. No partial state.
1218
1232
  const reason = `rebase onto ${arbiter}/main conflicted (aborted, never auto-resolved)`;
1219
1233
  const routed = await ledgerWrite.applyNeedsAttentionTransition({
1220
1234
  cwd,
@@ -1236,9 +1250,9 @@ export async function performIntegration(
1236
1250
  commitMessage,
1237
1251
  reason: routed.moved
1238
1252
  ? `Rebasing ${branch} onto ${arbiter}/main conflicted; the rebase was ` +
1239
- `aborted (never auto-resolved) and '${slug}' was routed to ` +
1240
- 'work/needs-attention/ (surfaced by status). Resolve against the ' +
1241
- 'latest main, then return it to backlog/ and re-run.'
1253
+ `aborted (never auto-resolved) and '${slug}' was marked stuck on ` +
1254
+ 'its per-item lock (surfaced by status). Resolve against the latest ' +
1255
+ 'main, then `requeue` to release the stuck lock and re-run.'
1242
1256
  : `Rebasing ${branch} onto ${arbiter}/main conflicted; the rebase was ` +
1243
1257
  'aborted (never auto-resolved). Resolve against the latest main, ' +
1244
1258
  'then re-run complete.',
@@ -1318,9 +1332,9 @@ export async function performIntegration(
1318
1332
  branch,
1319
1333
  commitMessage,
1320
1334
  reason: routed.moved
1321
- ? `${what} on the rebased tip; routed '${slug}' to ` +
1322
- 'work/needs-attention/ (surfaced by status; return to backlog/ ' +
1323
- 'once resolved). Fix the work, or use --skip-verify to override.'
1335
+ ? `${what} on the rebased tip; marked '${slug}' stuck on its ` +
1336
+ 'per-item lock (surfaced by status; `requeue` once resolved). ' +
1337
+ 'Fix the work, or use --skip-verify to override.'
1324
1338
  : `${what} on the rebased tip; not completing '${slug}'. Fix the ` +
1325
1339
  'work, or use --skip-verify to override.',
1326
1340
  };
@@ -1335,7 +1349,8 @@ export async function performIntegration(
1335
1349
  if (gated.review) {
1336
1350
  if (gated.review.kind === 'blocked') {
1337
1351
  // The done-move was already committed (steps 2–3), so the slug sits in
1338
- // work/done/; the routing bounced it from there to needs-attention/.
1352
+ // work/done/; the routing marked it stuck on its per-item lock (post
1353
+ // lock-cutover — a lock amend, no folder move).
1339
1354
  return gated.review.result;
1340
1355
  }
1341
1356
  approvedVerdict = gated.review.verdict;
@@ -1503,9 +1518,10 @@ export async function performIntegration(
1503
1518
  commitMessage,
1504
1519
  reason: routed.moved
1505
1520
  ? `Integrating ${branch} onto ${arbiter}/main kept hitting a ` +
1506
- `non-fast-forward push (a sibling advanced main); '${slug}' was routed ` +
1507
- `to work/needs-attention/ (surfaced by status). Resolve against the ` +
1508
- `latest main, then return it to backlog/ and re-run.`
1521
+ `non-fast-forward push (a sibling advanced main); '${slug}' was ` +
1522
+ `marked stuck on its per-item lock (surfaced by status). Resolve ` +
1523
+ `against the latest main, then \`requeue\` to release the stuck ` +
1524
+ `lock and re-run.`
1509
1525
  : `Integrating ${branch} onto ${arbiter}/main kept hitting a ` +
1510
1526
  `non-fast-forward push (a sibling advanced main). Resolve against the ` +
1511
1527
  `latest main, then re-run complete.`,
@@ -1590,7 +1606,7 @@ export async function performIntegration(
1590
1606
  * primitive the build path uses.
1591
1607
  *
1592
1608
  * RE-GATE on the REBASED TIP (task `committed-recovery-honours-fresh-worktree-
1593
- * gate`, prd `land-time-reverify-and-parallel-merge-ceiling`): when the caller
1609
+ * gate`, spec `land-time-reverify-and-parallel-merge-ceiling`): when the caller
1594
1610
  * sets `freshWorktreeGate` (and not `skipVerify`), the EXISTING
1595
1611
  * `runFreshWorktreeGate` runs on the rebased tip AFTER the rebase loop and
1596
1612
  * BEFORE `applyCompleteTransition`, mirroring the build path's
@@ -1653,6 +1669,23 @@ async function recoverAlreadyCommitted(params: {
1653
1669
  prepare?: VerifyConfig;
1654
1670
  /** Acceptance gate config for the fresh gate (mirrors the build path). */
1655
1671
  verify?: VerifyConfig;
1672
+ /**
1673
+ * The full integration input, threaded so the recovery tail can run Gate 2
1674
+ * (`runGate2Review`) with the identical gate/model/watch/round wiring the
1675
+ * build path uses (task `committed-recovery-always-reviews`). Gate 2 runs on
1676
+ * the rebased tip when `input.review` is on and the fresh gate ran green — a
1677
+ * stranded already-complete branch is NOT trusted as already-reviewed (its
1678
+ * earlier attempt never merged). Optional so the pre-existing non-review
1679
+ * callers (and tests) that do not thread it stay byte-identical.
1680
+ */
1681
+ input?: IntegrationCoreInput;
1682
+ /**
1683
+ * The propose-mode PR BODY (task `committed-recovery-always-reviews`): the
1684
+ * recovery has NO fresh agent output (no rebuild), so the caller passes
1685
+ * whatever advisory body it has so the PR carries a description instead of a
1686
+ * blank `gh --fill`. Undefined ⇒ today's `--fill` fallback.
1687
+ */
1688
+ body?: string;
1656
1689
  /**
1657
1690
  * Autonomous needs-attention surface (mirrors the build path): when set, a
1658
1691
  * red rebased-tip gate cherry-picks the bounce onto `main` + pushes the
@@ -1704,6 +1737,14 @@ async function recoverAlreadyCommitted(params: {
1704
1737
  // (`isAncestor` is the SAME reachability predicate `gc.ts` uses; do not fork it.)
1705
1738
  // KEPT before the retry loop: a no-op MUST short-circuit before we burn any
1706
1739
  // re-fetch/re-rebase budget.
1740
+ //
1741
+ // DELIBERATELY the RAW ancestry predicate, NOT `isProvablyMergedForReap`
1742
+ // (task `reap-squash-merged-remote-work-branches`, ratified): this is an
1743
+ // integration-IDEMPOTENCY check, not reap-safety. A squash-landed LOOKALIKE
1744
+ // whose tip is NOT genuinely reachable would falsely read "already
1745
+ // integrated" here and skip a needed rebase/re-push. The squash-aware helper
1746
+ // is scoped to reap-safety ONLY; see gc.ts `isProvablyMergedForReap` and
1747
+ // work/notes/observations/reap-squash-helper-scope-2026-07-10.md.
1707
1748
  if (isAncestor(cwd, tip, `refs/remotes/${arbiter}/main`, env)) {
1708
1749
  const message =
1709
1750
  `Nothing to recover for '${slug}': its work branch tip is already on ` +
@@ -1801,7 +1842,7 @@ async function recoverAlreadyCommitted(params: {
1801
1842
  }
1802
1843
 
1803
1844
  // FRESH-WORKTREE GATE on the REBASED TIP (task `committed-recovery-honours-
1804
- // fresh-worktree-gate`, prd `land-time-reverify-and-parallel-merge-ceiling`):
1845
+ // fresh-worktree-gate`, spec `land-time-reverify-and-parallel-merge-ceiling`):
1805
1846
  // when `freshWorktreeGate` is set (the answered-merge land caller) and not
1806
1847
  // `--skip-verify`, re-run the acceptance gate on the rebased tip BEFORE we
1807
1848
  // integrate, mirroring the build path's `freshWorktreeGate && !skipVerify &&
@@ -1812,13 +1853,23 @@ async function recoverAlreadyCommitted(params: {
1812
1853
  // clean-rebase-but-broken merge. With `freshWorktreeGate` UNSET (the original
1813
1854
  // stranded-recovery caller, whose pre-strand build already gated) this whole
1814
1855
  // block is skipped and behaviour is byte-identical to before.
1856
+ // Carry an approved Gate-2 verdict (when review ran) so its authored `review`
1857
+ // prose is posted as a PR comment AFTER the integrate below — the SAME audit
1858
+ // trail the build path keeps (task `committed-recovery-always-reviews`).
1859
+ let approvedVerdict: ReviewVerdict | undefined;
1815
1860
  if (params.freshWorktreeGate && !params.skipVerify) {
1816
1861
  const tip = (
1817
1862
  await gitSoft(['rev-parse', '--verify', '--quiet', 'HEAD'], cwd, env)
1818
1863
  ).stdout.trim();
1819
- // No `review:` callback here: the recovery tail re-verifies an already-
1820
- // reviewed, already-committed result, so Gate-2 review semantics do not
1821
- // apply on this path.
1864
+ // GATE-2 REVIEW callback (task `committed-recovery-always-reviews`): when
1865
+ // `input.review` is on, run Gate 2 on the rebased tip AFTER the green verify
1866
+ // EXACTLY the build path's fresh-gate `review:` callback. A stranded
1867
+ // already-complete branch is NOT trusted as already-reviewed (its earlier
1868
+ // attempt's PR never merged, and Gate 2 may never have run on it); the same
1869
+ // reasoning that forces the re-verify here (`<arbiter>/main` moved, so the
1870
+ // merged tree is new) forces the re-review. The needs-attention ROUTING
1871
+ // targets `cwd` (the work branch + ledger), identical to the build path.
1872
+ const reviewInput = params.input;
1822
1873
  const gated = await runFreshWorktreeGate({
1823
1874
  cwd,
1824
1875
  commit: tip,
@@ -1826,6 +1877,19 @@ async function recoverAlreadyCommitted(params: {
1826
1877
  verify: params.verify,
1827
1878
  env,
1828
1879
  note,
1880
+ review:
1881
+ reviewInput?.review === true
1882
+ ? (reviewCwd) =>
1883
+ runGate2Review({
1884
+ reviewCwd,
1885
+ input: reviewInput,
1886
+ slug,
1887
+ branch,
1888
+ cwd,
1889
+ env,
1890
+ note,
1891
+ })
1892
+ : undefined,
1829
1893
  });
1830
1894
  if (!gated.passed) {
1831
1895
  const outcome: IntegrationCoreOutcome =
@@ -1851,15 +1915,43 @@ async function recoverAlreadyCommitted(params: {
1851
1915
  routedToNeedsAttention: routed.moved,
1852
1916
  branch,
1853
1917
  reason: routed.moved
1854
- ? `${what} on the rebased tip during committed-recovery; routed ` +
1855
- `'${slug}' to work/needs-attention/ (surfaced by status; return ` +
1856
- 'to backlog/ once resolved). Fix the work, or use --skip-verify ' +
1918
+ ? `${what} on the rebased tip during committed-recovery; marked ` +
1919
+ `'${slug}' stuck on its per-item lock (surfaced by status; ` +
1920
+ '`requeue` once resolved). Fix the work, or use --skip-verify ' +
1857
1921
  'to override.'
1858
1922
  : `${what} on the rebased tip during committed-recovery; not ` +
1859
1923
  `completing '${slug}'. Fix the work, or use --skip-verify to ` +
1860
1924
  'override.',
1861
1925
  };
1862
1926
  }
1927
+ // GATE-2 REVIEW outcome on the rebased tip: a BLOCK routes to needs-attention
1928
+ // exactly as the build path (only the reviewed tree moved, not the routing).
1929
+ // On APPROVE: carry the verdict for the post-integrate PR comment and write
1930
+ // the per-run non-blocking-nits observation. UNLIKE the build path this branch
1931
+ // integrates the ALREADY-committed kept tip (no upcoming done-move/commit to
1932
+ // sweep the nits file into), so we FOLD the observation into the kept commit
1933
+ // via `commit --amend` — mirroring the build path's fresh-gate amend so the
1934
+ // nits still land in the SAME commit that integrates (no separate commit).
1935
+ if (gated.review) {
1936
+ if (gated.review.kind === 'blocked') {
1937
+ return gated.review.result;
1938
+ }
1939
+ approvedVerdict = gated.review.verdict;
1940
+ const nitsBefore = await stagedCaptureNotes(cwd, env);
1941
+ writeReviewNitsObservation({
1942
+ cwd,
1943
+ slug,
1944
+ findings: gated.review.verdict?.findings ?? [],
1945
+ note,
1946
+ });
1947
+ await gitHard(['add', '-A'], cwd, env);
1948
+ if (!(await nothingStaged(cwd, env))) {
1949
+ const nitsAfter = await stagedCaptureNotes(cwd, env);
1950
+ if (nitsAfter.length > nitsBefore.length) {
1951
+ await gitHard(['commit', '-q', '--amend', '--no-edit'], cwd, env);
1952
+ }
1953
+ }
1954
+ }
1863
1955
  }
1864
1956
 
1865
1957
  // Integrate the rebased kept commit through the SAME complete-transition
@@ -1872,12 +1964,27 @@ async function recoverAlreadyCommitted(params: {
1872
1964
  (params.openPr
1873
1965
  ? bridgeProvider(params.openPr)
1874
1966
  : selectProvider({arbiterUrl: await arbiterUrl(cwd, arbiter, env)}));
1967
+ // PR TITLE + BODY on the recovery path (task `committed-recovery-always-
1968
+ // reviews`): the stranded branch already holds the task in `work/done/`, so
1969
+ // synthesise the SAME single-line `<type>(<slug>): <title>` PR title the build
1970
+ // path does (from the done-resident task file, never agent text) and scaffold
1971
+ // the task-pointer body around the caller-supplied advisory `body`. Undefined
1972
+ // body ⇒ `composeProposeBody` returns undefined ⇒ the provider degrades to
1973
+ // `gh --fill` (no regression); a supplied body gives the PR a real description
1974
+ // instead of the blank one the recovery path used to open.
1975
+ const prTitle = synthesiseProposeTitle({
1976
+ type: (params.input?.type ?? DEFAULT_TYPE).trim() || DEFAULT_TYPE,
1977
+ slug,
1978
+ title: readTaskTitle(workItemPath(cwd, 'done', slug)),
1979
+ });
1875
1980
  const integration = await ledgerWrite.applyCompleteTransition({
1876
1981
  arbiter,
1877
1982
  branch,
1878
1983
  mode,
1879
1984
  provider,
1880
1985
  noPR: params.noPR,
1986
+ title: prTitle,
1987
+ body: composeProposeBody({slug, body: params.body}),
1881
1988
  deleteMergedHead: true,
1882
1989
  cwd,
1883
1990
  env,
@@ -1889,6 +1996,32 @@ async function recoverAlreadyCommitted(params: {
1889
1996
  `(absorbed a moving ${arbiter}/main across ${attempt} re-fetch+re-` +
1890
1997
  `rebase attempt${attempt === 1 ? '' : 's'}).`,
1891
1998
  );
1999
+ // Post the Gate-2 review as a PR comment (task `committed-recovery-always-
2000
+ // reviews`): the SAME post-integrate audit trail the build path keeps — the
2001
+ // approved verdict's authored `review` prose, on the opened PR. Advisory only
2002
+ // (a block never reaches here — it returned above); the same URL/branch
2003
+ // precedence the build path uses (parsed url wins; else a branch-resolved
2004
+ // fallback when a PR opened but its url was unparseable). `postPRComment*`
2005
+ // never throws. A clean no-op in merge mode / push-only propose (no PR).
2006
+ if (approvedVerdict?.review !== undefined) {
2007
+ if (integration.url !== undefined) {
2008
+ const posted = provider.postPRComment({
2009
+ cwd,
2010
+ url: integration.url,
2011
+ body: approvedVerdict.review,
2012
+ env,
2013
+ });
2014
+ note(posted.instruction);
2015
+ } else if (integration.requestOpened) {
2016
+ const posted = provider.postPRCommentOnBranch({
2017
+ cwd,
2018
+ branch,
2019
+ body: approvedVerdict.review,
2020
+ env,
2021
+ });
2022
+ note(posted.instruction);
2023
+ }
2024
+ }
1892
2025
  return {
1893
2026
  outcome: 'completed',
1894
2027
  routedToNeedsAttention: false,
@@ -1931,8 +2064,9 @@ function resolveWorkBranch(
1931
2064
  *
1932
2065
  * Carries the {@link slug} so the autonomous-strand surface in `complete.ts`
1933
2066
  * (which catches this error in `performComplete`'s outer try/catch, OUTSIDE the
1934
- * `runComplete` slug scope) can publish the `in-progress/ needs-attention/`
1935
- * tree-less move without re-deriving the slug from the error message.
2067
+ * `runComplete` slug scope) can mark the item stuck on its per-item lock (post
2068
+ * lock-cutover the tree-less lock amend, no `in-progress/ needs-attention/`
2069
+ * folder move) without re-deriving the slug from the error message.
1936
2070
  */
1937
2071
  export class IntegrationNothingStaged extends Error {
1938
2072
  constructor(
@@ -2113,8 +2247,8 @@ export function composeProposeBody(input: {
2113
2247
  * On a review APPROVE that carries ≥1 NON-BLOCKING finding, write ONE per-run
2114
2248
  * observation `work/notes/observations/review-nits-<slug>-<YYYY-MM-DD>.md` capturing all
2115
2249
  * of this run's non-blocking nits, so they get a durable, contract-native home
2116
- * instead of evaporating (the block path already routes BLOCKING findings to
2117
- * needs-attention/; the approve path dropped non-blocking ones — see
2250
+ * instead of evaporating (the block path already records BLOCKING findings as the
2251
+ * stuck reason on the item's per-item lock; the approve path dropped non-blocking ones — see
2118
2252
  * `work/findings/review-nonblocking-findings-disposition.md`).
2119
2253
  *
2120
2254
  * The RUNNER writes it (the review agent stays write-free). It is a PLAIN
@@ -2841,8 +2975,8 @@ async function runGate2Review(params: {
2841
2975
  });
2842
2976
  const message = routed.moved
2843
2977
  ? `PR/code review (Gate 2) produced an UNPARSEABLE verdict for '${slug}'; ` +
2844
- 'routed it to work/needs-attention/ (work branch pushed + surfaced; ' +
2845
- 'transient-infra — re-run). NOT integrated.'
2978
+ 'marked it stuck on its per-item lock (work branch pushed + lock ' +
2979
+ 'surfaced; transient-infra — re-run). NOT integrated.'
2846
2980
  : `PR/code review (Gate 2) produced an UNPARSEABLE verdict for '${slug}'; ` +
2847
2981
  'NOT integrating.';
2848
2982
  note(message);
@@ -2888,9 +3022,9 @@ async function runGate2Review(params: {
2888
3022
  note,
2889
3023
  });
2890
3024
  const message = routed.moved
2891
- ? `PR/code review (Gate 2) blocked '${slug}'; routed it to ` +
2892
- 'work/needs-attention/ (surfaced by status; the blocking findings are ' +
2893
- 'recorded in the item body). NOT integrated.'
3025
+ ? `PR/code review (Gate 2) blocked '${slug}'; marked it stuck on its ` +
3026
+ 'per-item lock (surfaced by status; the blocking findings are ' +
3027
+ 'recorded on the lock entry). NOT integrated.'
2894
3028
  : `PR/code review (Gate 2) blocked '${slug}'; NOT integrating.`;
2895
3029
  note(message);
2896
3030
  return {
package/src/integrator.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type {IntegrationMode} from './config.js';
2
2
  import {git, run} from './git.js';
3
- import {isAncestor} from './gc.js';
3
+ import {isAncestor, isProvablyMergedForReap} from './gc.js';
4
+ import {parseWorkBranchRef} from './slug-namespace.js';
5
+ import type {SidecarType} from './sidecar.js';
4
6
  import type {BackoffOptions, Sleep} from './retry-backoff.js';
5
7
  import {pushProposeBranchWithStaleLeaseRetry} from './continue-branch.js';
6
8
 
@@ -135,7 +137,7 @@ export interface OpenRequestInput {
135
137
  * When present a provider passes it verbatim (e.g. `gh pr create --body`)
136
138
  * instead of `--fill`'s empty/commit-derived description. Typically the build
137
139
  * agent's final summary, optionally under a runner-scaffolded header (a pointer
138
- * to `work/done/<slug>.md` + the prd/ADR it serves). Absent ⇒ today's `--fill`
140
+ * to `work/done/<slug>.md` + the spec/ADR it serves). Absent ⇒ today's `--fill`
139
141
  * (no regression). This is the BODY-AT-OPEN surface; a follow-up PR COMMENT is
140
142
  * a separate provider method.
141
143
  */
@@ -681,13 +683,24 @@ function deleteMergedHeadBranch(input: IntegrateInput): void {
681
683
  );
682
684
  const headSha = parseLsRemoteSha(ls.stdout);
683
685
  if (headSha !== undefined) {
684
- const isMerged = run(
685
- 'git',
686
- ['merge-base', '--is-ancestor', headSha, `refs/remotes/${arbiter}/main`],
687
- input.cwd,
688
- {env},
689
- );
690
- if (isMerged.status !== 0) {
686
+ // Provably-merged-for-reap = fast-path ancestor OR squash-aware fallback
687
+ // (durable done/dropped record on <arbiter>/main + branch carries nothing
688
+ // main lacks). The SAME predicate the `gc.ts` worktree reaper and the
689
+ // `reap-branches.ts` remote sweep use — do not fork it.
690
+ const parsed = parseWorkBranchRef(branch);
691
+ const arbiterMainRef = `refs/remotes/${arbiter}/main`;
692
+ const provablyMerged =
693
+ parsed === undefined
694
+ ? isAncestor(input.cwd, headSha, arbiterMainRef, env)
695
+ : isProvablyMergedForReap({
696
+ cwd: input.cwd,
697
+ tip: headSha,
698
+ arbiterMain: arbiterMainRef,
699
+ namespace: parsed.namespace as SidecarType,
700
+ slug: parsed.slug,
701
+ env,
702
+ });
703
+ if (!provablyMerged) {
691
704
  return; // NOT provably merged (a racing un-merged amend) — never reap.
692
705
  }
693
706
  }
package/src/isolation.ts CHANGED
@@ -111,11 +111,11 @@ export interface PrepareInput {
111
111
  /** The work slug being processed (→ branch `work/<type>-<slug>`). */
112
112
  slug: string;
113
113
  /**
114
- * The item TYPE — `'task'` for a build (intake/`do task:`), `'prd'` for a
114
+ * The item TYPE — `'task'` for a build (intake/`do task:`), `'spec'` for a
115
115
  * spec-tasking run (`do spec:`). It NAMESPACES the work branch via
116
116
  * {@link workBranchRef} so a same-slug task and spec never collide on the
117
117
  * arbiter branch. Defaults to `'task'` (the overwhelmingly-common build
118
- * path); the spec-tasking path passes `'prd'` explicitly.
118
+ * path); the spec-tasking path passes `'spec'` explicitly.
119
119
  */
120
120
  type?: SlugNamespace;
121
121
  /**
@@ -394,6 +394,15 @@ export function inPlaceStrategy(options: {
394
394
  * path uses before force-resetting the work branch onto the claim commit. A
395
395
  * not-yet-fetched or rolled-back arbiter main that does NOT reach the claim must
396
396
  * fail fast here, never silently build on a stale base.
397
+ *
398
+ * DELIBERATELY the RAW `--is-ancestor` predicate, NOT the squash-aware
399
+ * `isProvablyMergedForReap` (task `reap-squash-merged-remote-work-branches`,
400
+ * ratified): this is a claim-REACHABILITY guard, not reap-safety. It demands
401
+ * GENUINE ancestry of the claim commit; a done-record-on-main match would
402
+ * defeat its whole purpose (refusing to build on a stale base). The
403
+ * squash-aware helper is scoped to reap-safety ONLY; this module's reap-lane
404
+ * (`reapJob` → `evaluateDeletionSafety`) is where it belongs. See
405
+ * work/notes/observations/reap-squash-helper-scope-2026-07-10.md.
397
406
  */
398
407
  function assertClaimCommitReachable(
399
408
  checkout: string,