dorfl 0.14.2 → 0.15.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 (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +309 -489
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +46 -14
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +123 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -0
  162. package/dist/install-ci-action-pins.js +190 -0
  163. package/dist/install-ci-action-pins.js.map +1 -0
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +155 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +304 -200
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -292
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +8 -4
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +375 -538
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +52 -15
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +209 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +388 -233
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -327
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +8 -4
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
@@ -1,19 +1,28 @@
1
- import {existsSync, readFileSync} from 'node:fs';
1
+ import {existsSync, readFileSync, statSync} from 'node:fs';
2
2
  import {join} from 'node:path';
3
3
  import {
4
- isEntryAnswered,
5
4
  parseSidecar,
6
5
  resolveSidecarIdentity,
7
6
  sidecarPathFor,
8
- type SidecarEntry,
9
7
  type SidecarModel,
10
8
  } from './sidecar.js';
11
9
  import {run as runProc} from './git.js';
12
- import {createJob, type Job} from './workspace.js';
10
+ import {createJob, withCheckoutCommitIdentity, type Job} from './workspace.js';
11
+ import {continueRebaseSentence} from './continue-branch.js';
12
+ import {ensureMirrorMain, type EnsureMirrorResult} from './repo-mirror.js';
13
13
  import {
14
14
  performIntegration,
15
+ type IntegrationCoreInput,
15
16
  type IntegrationCoreResult,
16
17
  } from './integration-core.js';
18
+ import {
19
+ changedFileBlobs,
20
+ lfsObjectPath,
21
+ localLfsObjectsDir,
22
+ scanLfsPointers,
23
+ } from './ci-handoff-lfs.js';
24
+ import {commitAbortedWork} from './needs-attention.js';
25
+ import {createPhaseRecorder, runAgentPhase} from './phase-recorder.js';
17
26
  import type {VerifyConfig} from './verify.js';
18
27
 
19
28
  /**
@@ -58,9 +67,9 @@ import type {VerifyConfig} from './verify.js';
58
67
  * "HONOUR the prior approval + land on a green re-verify" path; opt-in
59
68
  * `strictMergeApproval` (resolved per-repo via the gate-family precedence chain
60
69
  * by the sibling task `strict-merge-approval-gate`, default OFF) RE-SURFACES
61
- * the merge-question instead of landing when the merge-base moved between the
62
- * surfacer's question and this apply. The RED-re-verify refusal is unchanged
63
- * in both modes.
70
+ * the merge-question instead of landing when `main`'s code moved since the
71
+ * question was asked (its recorded `askedAtMain`; see `approvedBaseMoved`).
72
+ * The RED-re-verify refusal is unchanged in both modes.
64
73
  *
65
74
  * House-style boundary: this module is the DETERMINISTIC DISPATCH LAYER. It
66
75
  * does NOT re-implement rebase / verify / integrate (it drives the EXISTING
@@ -72,48 +81,16 @@ import type {VerifyConfig} from './verify.js';
72
81
  * needs-attention, that `verify` ran on the rebased tip).
73
82
  */
74
83
 
75
- /** The three deterministic actions a merge-question answer encodes. */
76
- export type MergeActionVerb = 'merge' | 'hold' | 'drop';
77
-
78
- /** A detected, answer-driven merge-action keyed off ONE answered `kind: merge` entry. */
79
- export interface DetectedMergeAction {
80
- /** The deterministic verb parsed from the entry's answer text. */
81
- verb: MergeActionVerb;
82
- /** The answered `kind: merge` entry the verb came from. */
83
- entry: SidecarEntry;
84
- }
85
-
86
- /**
87
- * Parse the human's plain free-text answer into the deterministic
88
- * {@link MergeActionVerb}. The merge-question surfacer renders
89
- * `merge | hold | drop` in the entry's `default` as a human-readable hint, so
90
- * the human typically types one of those words verbatim. We accept any text
91
- * whose first whole word (case-insensitive) is one of `merge` / `hold` / `drop`
92
- * — this is the same machine-parseable choice shape the surfacer's applied
93
- * answer q1 documents ("a DETERMINISTIC CHOICE shape … the human picks and the
94
- * system parses unambiguously").
95
- *
96
- * Returns `undefined` on ANYTHING else (an empty answer, a typo, a long
97
- * narrative without one of the three words at the start) so the caller can
98
- * route the ambiguity HONESTLY — never default-to-merge on a malformed answer,
99
- * which would invent a land the human did not authorise.
100
- */
101
- export function parseMergeAnswer(text: string): MergeActionVerb | undefined {
102
- const trimmed = text.trim().toLowerCase();
103
- if (trimmed === '') return undefined;
104
- // The first whole alphabetic word; trailing punctuation (a comma, em-dash,
105
- // period) and any following commentary are tolerated. We deliberately do
106
- // NOT consume hyphens / apostrophes — the three verbs are plain ASCII
107
- // words, and over-tolerating typo-shapes would silently invent a verb the
108
- // human did not pick.
109
- const match = /^([a-z]+)/.exec(trimmed);
110
- if (match === null) return undefined;
111
- const word = match[1];
112
- if (word === 'merge' || word === 'hold' || word === 'drop') {
113
- return word;
114
- }
115
- return undefined;
116
- }
84
+ export {
85
+ answeredMergeActionIn,
86
+ parseMergeAnswer,
87
+ type DetectedMergeAction,
88
+ type MergeActionVerb,
89
+ } from './merge-answer.js';
90
+ import {
91
+ answeredMergeActionIn,
92
+ type DetectedMergeAction,
93
+ } from './merge-answer.js';
117
94
 
118
95
  /**
119
96
  * Detect the `kind: merge` action verb that should drive the next apply run.
@@ -151,21 +128,7 @@ export function detectAnsweredMergeAction(
151
128
  } catch {
152
129
  return undefined;
153
130
  }
154
- // (1) Unanswered `kind: merge` follow-up ⇒ re-paused; the apply must NOT fire.
155
- for (const entry of model.entries) {
156
- if (entry.kind === 'merge' && !isEntryAnswered(entry)) return undefined;
157
- }
158
- // (2) LATEST answered `kind: merge` entry — a fresh follow-up beats a stale one.
159
- for (let i = model.entries.length - 1; i >= 0; i--) {
160
- const entry = model.entries[i];
161
- if (entry.kind !== 'merge') continue;
162
- if (!isEntryAnswered(entry)) continue;
163
- const verb = parseMergeAnswer(entry.answer);
164
- if (verb !== undefined) {
165
- return {verb, entry};
166
- }
167
- }
168
- return undefined;
131
+ return answeredMergeActionIn(model);
169
132
  }
170
133
 
171
134
  /** Input the production merge-action handler consumes. */
@@ -197,8 +160,8 @@ export interface MergeActionInput {
197
160
  * `strictMergeApproval` (resolved per-repo by the sibling task
198
161
  * `strict-merge-approval-gate`; default OFF). OFF ⇒ honour the prior answer
199
162
  * + land on a green re-verify (the cheap default). ON ⇒ re-surface the
200
- * merge-question when the merge-base moved between the surfacer's question
201
- * and this apply (don't land; the apply rung folds this into a re-pause).
163
+ * merge-question when `main`'s code moved since the question was asked
164
+ * (don't land; the apply rung folds this into a re-pause).
202
165
  */
203
166
  strictMergeApproval?: boolean;
204
167
  /** Bounded recovery-rebase retry knob (mirrors the build path). */
@@ -228,9 +191,9 @@ export type MergeActionOutcome =
228
191
  */
229
192
  | 'refused'
230
193
  /**
231
- * `answer=merge` + `strictMergeApproval` ON + the merge-base moved between
232
- * the surfacer's question and this apply: the dispatcher RE-SURFACES the
233
- * merge-question (the apply rung appends a fresh follow-up + re-pauses).
194
+ * `answer=merge` + `strictMergeApproval` ON + `main`'s code moved since the
195
+ * question was asked: the dispatcher RE-SURFACES the merge-question (the
196
+ * apply rung appends a fresh follow-up asked at the new `main` + re-pauses).
234
197
  */
235
198
  | 'restale'
236
199
  /** `answer=hold` ⇒ no land; the apply rung records the answer in body as usual. */
@@ -250,6 +213,12 @@ export interface MergeActionResult {
250
213
  * `already-integrated`, …), so callers can branch on it.
251
214
  */
252
215
  integration?: IntegrationCoreResult;
216
+ /**
217
+ * On `restale`: the arbiter `main` the approval was found stale against. The
218
+ * apply rung records it as the re-surfaced question's `askedAtMain`, so a
219
+ * re-answer while `main` stays put lands (no re-stale livelock).
220
+ */
221
+ main?: string;
253
222
  }
254
223
 
255
224
  /**
@@ -282,50 +251,77 @@ function resolveArbiterUrl(input: MergeActionInput): string | undefined {
282
251
  return url === '' ? undefined : url;
283
252
  }
284
253
 
254
+ /** The verdict of {@link approvedBaseMoved}. */
255
+ interface ApprovedBaseCheck {
256
+ /** `main`'s code moved since the answered question was asked ⇒ re-surface. */
257
+ stale: boolean;
258
+ /** The arbiter `main` the check compared against (when it was read). */
259
+ main?: string;
260
+ }
261
+
285
262
  /**
286
- * The `strictMergeApproval` re-stale check (SPEC OQ6 opt-in): did `<arbiter>/
287
- * main` move past the merge-base of the work branch since the surfacer
288
- * authored the question? The git-alone analogue of GitHub's "dismiss stale
289
- * approvals when the base changes" — host-agnostic by reachability.
263
+ * The `strictMergeApproval` re-stale check (SPEC OQ6 opt-in; task
264
+ * `strict-merge-approval-restale-check-runs-before-the-continue-rebase`): did
265
+ * `main` move since the answered merge question was ASKED? The git-alone
266
+ * analogue of GitHub's "dismiss stale approvals when the base changes".
267
+ *
268
+ * ONE implementation for the laptop dispatcher ({@link performMergeAction}) and
269
+ * the CI agent half ({@link prepareMergeLand}), and both call it BEFORE
270
+ * {@link createJob}, whose continue rebase would otherwise make the kept branch
271
+ * a descendant of the current `main` and hide the move (the bug this replaced).
272
+ *
273
+ * The approved base is the `askedAtMain` the answered entry carries (stamped by
274
+ * the merge-question surfacer, and by the re-surface path with the `main` the
275
+ * follow-up is asked against, so a re-answer while `main` stays put lands and
276
+ * the CI split cannot livelock). It is compared with the arbiter's current
277
+ * `main`, freshly fetched into the hub mirror {@link createJob} then reuses.
290
278
  *
291
- * We fetch the latest `<arbiter>/main` from the JOB worktree's hub mirror (the
292
- * worktree's `origin`), compute `merge-base(<branch>, <origin/main>)`, and
293
- * compare against `<origin/main>` itself: if the merge-base IS `<origin/main>`,
294
- * the branch is a strict descendant — the merge-base did NOT move — and the
295
- * cheap default applies (HONOUR + land on green re-verify). If the merge-base
296
- * is NOT `<origin/main>`, `main` advanced past the branch's divergence point
297
- * since the branch was last rebased — the merge-base MOVED — and the strict
298
- * mode re-surfaces.
279
+ * "Moved" means the tree OUTSIDE `work/` differs: the question, its answer and
280
+ * every other ledger write are commits on `main` under `work/`, so a plain sha
281
+ * compare would re-stale every answer (the answer commit itself moves `main`).
299
282
  *
300
- * Returns `true` when the merge-base moved (⇒ re-surface), `false` otherwise.
301
- * On any plumbing failure returns `false` (do NOT spuriously re-surface — the
302
- * red re-verify is still the load-bearing safety; a transient git failure must
303
- * not block a clean answer-then-land).
283
+ * Never stale (the cheap default applies) when the entry carries no
284
+ * `askedAtMain` (a sidecar written before it existed) or on any plumbing
285
+ * failure: the red re-verify on the rebased tip stays the load-bearing safety,
286
+ * and a transient git failure must not block a clean answer-then-land.
304
287
  */
305
- function mergeBaseMoved(job: Job, env: NodeJS.ProcessEnv | undefined): boolean {
306
- const fetch = runProc(
288
+ function approvedBaseMoved(
289
+ url: string,
290
+ input: MergeActionInput,
291
+ ): ApprovedBaseCheck {
292
+ const asked = input.action.entry.askedAtMain;
293
+ if (asked === undefined) return {stale: false};
294
+ let mirror: EnsureMirrorResult;
295
+ try {
296
+ mirror = ensureMirrorMain({
297
+ url,
298
+ workspacesDir: input.workspacesDir,
299
+ env: input.env,
300
+ });
301
+ } catch {
302
+ return {stale: false};
303
+ }
304
+ const main = mirror.mainSha;
305
+ if (main === asked) return {stale: false, main};
306
+ const diff = runProc(
307
307
  'git',
308
- [
309
- 'fetch',
310
- '--quiet',
311
- job.arbiterRemote,
312
- `+refs/heads/main:refs/remotes/${job.arbiterRemote}/main`,
313
- ],
314
- job.dir,
315
- {env},
308
+ ['diff-tree', '--quiet', '-r', asked, main, '--', ':(exclude)work'],
309
+ mirror.path,
310
+ {env: input.env},
316
311
  );
317
- if (fetch.status !== 0) return false;
318
- const mainRef = `${job.arbiterRemote}/main`;
319
- const mainSha = runProc(
320
- 'git',
321
- ['rev-parse', '--verify', '--quiet', mainRef],
322
- job.dir,
323
- {env},
312
+ // 0: no change outside `work/`; 1: changed; anything else: plumbing failure.
313
+ return {stale: diff.status === 1, main};
314
+ }
315
+
316
+ /** The re-stale message both halves report. */
317
+ function restaleMessage(input: MergeActionInput, main: string): string {
318
+ const asked = input.action.entry.askedAtMain ?? '(unknown)';
319
+ return (
320
+ `merge-question for ${input.item} answered MERGE, but ` +
321
+ `strictMergeApproval is ON and \`main\` moved since the question was ` +
322
+ `asked (asked at ${asked}, now ${main}): RE-SURFACING the ` +
323
+ `merge-question (no land; the human re-confirms against the new base).`
324
324
  );
325
- if (mainSha.status !== 0) return false;
326
- const base = runProc('git', ['merge-base', 'HEAD', mainRef], job.dir, {env});
327
- if (base.status !== 0) return false;
328
- return base.stdout.trim() !== mainSha.stdout.trim();
329
325
  }
330
326
 
331
327
  /**
@@ -405,6 +401,22 @@ export async function performMergeAction(
405
401
  };
406
402
  }
407
403
 
404
+ // STRICT re-stale check (OQ6 opt-in), BEFORE the checkout's continue rebase
405
+ // (which would push the rebased branch, and hide the move): when ON and
406
+ // `main` moved since the question was asked, RE-SURFACE the merge-question
407
+ // (the apply rung folds this into a re-pause) instead of landing. Default
408
+ // OFF ⇒ skipped; the cheap "green re-verify is enough" path runs.
409
+ if (input.strictMergeApproval === true) {
410
+ const check = approvedBaseMoved(url, input);
411
+ if (check.stale && check.main !== undefined) {
412
+ return {
413
+ outcome: 'restale',
414
+ message: restaleMessage(input, check.main),
415
+ main: check.main,
416
+ };
417
+ }
418
+ }
419
+
408
420
  // Check out the unmerged `work/<type>-<slug>` via the EXISTING per-job
409
421
  // worktree seam — the SAME seam build/recovery callers use, NOT a bespoke
410
422
  // worktree or clone. The job dir is the worktree `performIntegration` works
@@ -431,17 +443,17 @@ export async function performMergeAction(
431
443
  }
432
444
 
433
445
  try {
434
- // CONTINUE-rebase-conflict: createJob's CONTINUE-rebase aborted on a
435
- // genuine code conflict. The kept work stays on the branch (recoverable);
436
- // we refuse the land and route via the standard refusal shape.
446
+ // CONTINUE-rebase-conflict: createJob's CONTINUE-rebase aborted (a
447
+ // genuine code conflict, or any other rebase failure, named as such). The
448
+ // kept work stays on the branch (recoverable); we refuse the land and
449
+ // route via the standard refusal shape.
437
450
  if (job.continueRebaseConflict) {
438
- return {
439
- outcome: 'refused',
440
- message:
441
- `merge-question for ${input.item} answered MERGE — but rebasing ` +
442
- `\`work/${input.slug}\` onto current main conflicted (the kept work is ` +
443
- `intact on the branch; resolve and re-answer). NOT landing.`,
444
- };
451
+ const message =
452
+ `merge-question for ${input.item} answered MERGE — but ` +
453
+ `${continueRebaseSentence(job.branch, job.continueRebaseFailure)} (the kept work is intact on the ` +
454
+ `branch; resolve and re-answer). NOT landing.`;
455
+ note(message);
456
+ return {outcome: 'refused', message};
445
457
  }
446
458
  if (job.continuePushFailure !== undefined) {
447
459
  return {
@@ -453,50 +465,15 @@ export async function performMergeAction(
453
465
  };
454
466
  }
455
467
 
456
- // STRICT re-stale check (OQ6 opt-in): when ON and the merge-base moved
457
- // between the surfacer's question and this apply, RE-SURFACE the
458
- // merge-question (the apply rung folds this into a re-pause) instead of
459
- // landing. Default OFF ⇒ this block is skipped; the cheap "green
460
- // re-verify is enough" path runs.
461
- if (input.strictMergeApproval === true && mergeBaseMoved(job, input.env)) {
462
- return {
463
- outcome: 'restale',
464
- message:
465
- `merge-question for ${input.item} answered MERGE — but ` +
466
- `strictMergeApproval is ON and the merge-base of \`work/${input.slug}\` ` +
467
- `moved between answer and apply: RE-SURFACING the merge-question ` +
468
- `(no land; the human re-confirms against the new base).`,
469
- };
470
- }
471
-
472
468
  // LAND through the EXISTING `performIntegration`. The branch's tip
473
469
  // already carries its done-move commit (surfacer enumerates `work/*`
474
470
  // unreachable from main, so it is a previously-built strand), so we
475
471
  // drive the committed-recovery tail — but with `freshWorktreeGate: true`
476
472
  // so the REBASED TIP is re-verified BEFORE the integrate (the
477
473
  // `committed-recovery-honours-fresh-worktree-gate` task's contract).
478
- const integration = await performIntegration({
479
- cwd: job.dir,
480
- arbiter: job.arbiterRemote,
481
- slug: input.slug,
482
- source: 'tasks-ready',
483
- recovering: false,
484
- committedRecovery: true,
485
- freshWorktreeGate: true,
486
- prepare: input.prepare,
487
- verify: input.verify,
488
- mode: 'merge',
489
- // surfaceArbiter is set so a RED rebased-tip gate (or a rebase
490
- // conflict surfaced during the retry loop) routes to needs-attention
491
- // observably on the arbiter (not local-only). The build path uses
492
- // this shape; we mirror it here for the answered-merge land.
493
- surfaceArbiter: job.arbiterRemote,
494
- recoveryRebaseRetries: input.recoveryRebaseRetries,
495
- recoveryRebaseJitterMs: input.recoveryRebaseJitterMs,
496
- mergeRetries: input.mergeRetries,
497
- env: input.env,
498
- note,
499
- });
474
+ const integration = await performIntegration(
475
+ mergeLandIntegrationInput(job, input, note),
476
+ );
500
477
 
501
478
  if (integration.outcome === 'completed') {
502
479
  return {
@@ -538,3 +515,273 @@ export async function performMergeAction(
538
515
  }
539
516
  }
540
517
  }
518
+
519
+ /**
520
+ * The `performIntegration` input of the answered-merge land: the committed-
521
+ * recovery tail (the branch already carries its done-move) with the fresh-
522
+ * worktree gate on the rebased tip, in merge mode. Shared by the laptop
523
+ * dispatcher ({@link performMergeAction}) and the CI agent half
524
+ * ({@link prepareMergeLand}), so both run the SAME rebase and gate.
525
+ */
526
+ function mergeLandIntegrationInput(
527
+ job: Job,
528
+ input: MergeActionInput,
529
+ note: (message: string) => void,
530
+ ): IntegrationCoreInput {
531
+ return {
532
+ cwd: job.dir,
533
+ arbiter: job.arbiterRemote,
534
+ slug: input.slug,
535
+ source: 'tasks-ready',
536
+ recovering: false,
537
+ committedRecovery: true,
538
+ freshWorktreeGate: true,
539
+ prepare: input.prepare,
540
+ verify: input.verify,
541
+ mode: 'merge',
542
+ // surfaceArbiter is set so a RED rebased-tip gate (or a rebase
543
+ // conflict surfaced during the retry loop) routes to needs-attention
544
+ // observably on the arbiter (not local-only). The build path uses
545
+ // this shape; we mirror it here for the answered-merge land.
546
+ surfaceArbiter: job.arbiterRemote,
547
+ recoveryRebaseRetries: input.recoveryRebaseRetries,
548
+ recoveryRebaseJitterMs: input.recoveryRebaseJitterMs,
549
+ mergeRetries: input.mergeRetries,
550
+ env: input.env,
551
+ note,
552
+ };
553
+ }
554
+
555
+ // ---------------------------------------------------------------------------
556
+ // The CI agent half (task `ci-split-answered-merge-action`)
557
+ // ---------------------------------------------------------------------------
558
+
559
+ /**
560
+ * What the CI agent job found for an answered `merge` (spec
561
+ * `ci-agent-job-without-write-token` §3, the `apply, kind: merge` row). Each
562
+ * maps to one handoff intent of the `apply` rung:
563
+ *
564
+ * - `integrate`: the rebased tip passed the fresh-worktree gate; the apply job
565
+ * pushes it leased and lands it (`integrate`, the rebased tip bundled);
566
+ * - `restale`: `strictMergeApproval` is on and `main`'s code moved since the
567
+ * question was asked (`merge-restale`; checked before any checkout);
568
+ * - `needs-attention`: a red gate on the rebased tip (the rebased tip bundled)
569
+ * or a rebase conflict (nothing bundled: the kept branch is untouched);
570
+ * - `already-integrated`: the kept tip is already on `main`; nothing to land
571
+ * (the lock job's `needsAgent: false` covers this case, so here it means the
572
+ * branch landed between the lock job and this one).
573
+ */
574
+ export type MergeLandPreparation =
575
+ | {kind: 'integrate'; repo: string; workBranch: string; tip: string}
576
+ | {kind: 'restale'; message: string; main: string}
577
+ | {
578
+ kind: 'needs-attention';
579
+ reason: string;
580
+ questions?: string[];
581
+ /** The rebased (red) tip to publish, when there is one. */
582
+ bundle?: {repo: string; workBranch: string};
583
+ }
584
+ | {kind: 'already-integrated'; message: string};
585
+
586
+ /** A {@link MergeLandPreparation} and the job worktree it lives in. */
587
+ export interface PreparedMergeLand {
588
+ preparation: MergeLandPreparation;
589
+ /** Remove the job worktree; call it once the handoff (and its bundle) is written. */
590
+ dispose(): void;
591
+ }
592
+
593
+ /**
594
+ * The AGENT half of the answered-merge land, for the CI agent job, which holds a
595
+ * read-only token: the SAME optional `strictMergeApproval` re-stale check
596
+ * (before the checkout), checkout ({@link createJob}), rebase and
597
+ * fresh-worktree gate (`prepare` +
598
+ * `verify` run the branch's code) as {@link performMergeAction}, but nothing is
599
+ * written to the arbiter. The continue rebase stays local (decision 7,
600
+ * `localContinue`), and `performIntegration` runs under the phase recorder, so
601
+ * its first write (the land, or the needs-attention route of a red gate) halts
602
+ * it and names what the apply job performs. Only an answered `merge` gets here
603
+ * (`hold` / `drop` need no agent job).
604
+ *
605
+ * The caller writes the handoff from the returned preparation (a bundle reads
606
+ * the job worktree) and then calls `dispose`. Throws when the checkout itself
607
+ * fails (the agent job then fails, and the apply job surfaces the item).
608
+ */
609
+ export async function prepareMergeLand(
610
+ answered: MergeActionInput,
611
+ ): Promise<PreparedMergeLand> {
612
+ // The job worktree lives in the hub mirror, which does not see the
613
+ // checkout's local git config, where the CI workflow sets the identity: the
614
+ // continue rebase (and the wip commit of a red gate) need it
615
+ // (`withCheckoutCommitIdentity`).
616
+ const input: MergeActionInput = {
617
+ ...answered,
618
+ env: withCheckoutCommitIdentity(answered.cwd, answered.env),
619
+ };
620
+ const note = input.note ?? (() => {});
621
+ const identity = resolveSidecarIdentity(input.item);
622
+ if (identity.type !== 'task' || input.action.verb !== 'merge') {
623
+ throw new Error(
624
+ `prepareMergeLand: only an answered merge of a task is prepared ` +
625
+ `(got ${input.item}, answer ${input.action.verb})`,
626
+ );
627
+ }
628
+ const url = resolveArbiterUrl(input);
629
+ if (url === undefined) {
630
+ throw new Error(
631
+ `the arbiter URL of ${input.item} could not be resolved (\`git remote ` +
632
+ `get-url ${input.arbiter}\` failed in ${input.cwd})`,
633
+ );
634
+ }
635
+ // The re-stale check runs BEFORE the (local) continue rebase, exactly as on
636
+ // the laptop: the same `approvedBaseMoved`.
637
+ if (input.strictMergeApproval === true) {
638
+ const check = approvedBaseMoved(url, input);
639
+ if (check.stale && check.main !== undefined) {
640
+ const main = check.main;
641
+ return {
642
+ preparation: {
643
+ kind: 'restale',
644
+ message: restaleMessage(input, main),
645
+ main,
646
+ },
647
+ dispose: () => {},
648
+ };
649
+ }
650
+ }
651
+ const job = createJob({
652
+ url,
653
+ slug: input.slug,
654
+ type: 'task',
655
+ workspacesDir: input.workspacesDir,
656
+ localContinue: true,
657
+ env: input.env,
658
+ });
659
+ const dispose = (): void => {
660
+ try {
661
+ job.dispose();
662
+ } catch {
663
+ // Best-effort, as in performMergeAction: `gc` reaps a leftover.
664
+ }
665
+ };
666
+ const prepared = (preparation: MergeLandPreparation): PreparedMergeLand => ({
667
+ preparation,
668
+ dispose,
669
+ });
670
+ try {
671
+ if (job.continueRebaseConflict) {
672
+ const reason =
673
+ `${continueRebaseSentence(job.branch, job.continueRebaseFailure)}; the kept work is intact on the ` +
674
+ 'branch. Resolve it, then answer the merge question again.';
675
+ note(reason);
676
+ return prepared({kind: 'needs-attention', reason});
677
+ }
678
+ const outcome = await runAgentPhase(
679
+ createPhaseRecorder({
680
+ record: [
681
+ 'ledgerWrite.applyCompleteTransition',
682
+ 'ledgerWrite.applyNeedsAttentionTransition',
683
+ ],
684
+ }),
685
+ () => performIntegration(mergeLandIntegrationInput(job, input, note)),
686
+ );
687
+ if (outcome.halted) {
688
+ const call = `${outcome.intent.seam}.${outcome.intent.method}`;
689
+ if (call === 'ledgerWrite.applyCompleteTransition') {
690
+ fetchKeptLfsObjects(job, input.env, note);
691
+ const tip = runProc('git', ['rev-parse', 'HEAD'], job.dir, {
692
+ env: input.env,
693
+ }).stdout.trim();
694
+ return prepared({
695
+ kind: 'integrate',
696
+ repo: job.dir,
697
+ workBranch: job.branch,
698
+ tip,
699
+ });
700
+ }
701
+ // A red gate (or prepare) on the rebased tip: the route's local half
702
+ // (the wip commit) runs here, so the rebased tip travels in the bundle.
703
+ const routed = outcome.intent.input as {
704
+ reason: string;
705
+ questions?: string[];
706
+ };
707
+ commitAbortedWork({cwd: job.dir, slug: input.slug, env: input.env});
708
+ fetchKeptLfsObjects(job, input.env, note);
709
+ return prepared({
710
+ kind: 'needs-attention',
711
+ reason: routed.reason,
712
+ ...(routed.questions === undefined
713
+ ? {}
714
+ : {questions: routed.questions}),
715
+ bundle: {repo: job.dir, workBranch: job.branch},
716
+ });
717
+ }
718
+ const result = outcome.result;
719
+ if (result.outcome === 'already-integrated') {
720
+ return prepared({
721
+ kind: 'already-integrated',
722
+ message: result.reason ?? `work/task-${input.slug} is already on main`,
723
+ });
724
+ }
725
+ // A rebase conflict on every attempt (or an invariant violation): nothing
726
+ // was written and the kept branch is untouched on the arbiter.
727
+ return prepared({
728
+ kind: 'needs-attention',
729
+ reason:
730
+ result.reason ??
731
+ `the answered merge of ${input.item} ended as ${result.outcome}`,
732
+ });
733
+ } catch (err) {
734
+ dispose();
735
+ throw err;
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Bring into the job's local LFS store the objects of every LFS pointer the
741
+ * kept commits (`HEAD ^<origin>/main`) add or change, so the handoff carries
742
+ * them (`writeLfsObjects` copies from that store) and the apply job pushes them
743
+ * before the branch (decision 6). The kept branch was built elsewhere, so its
744
+ * objects live on the arbiter's LFS store, not in this fresh job: a read-only
745
+ * `git lfs fetch` of those commits. Best-effort: an object still missing is
746
+ * reported by the handoff writer, and the apply job then rejects the handoff
747
+ * and surfaces the item.
748
+ */
749
+ function fetchKeptLfsObjects(
750
+ job: Job,
751
+ env: NodeJS.ProcessEnv | undefined,
752
+ note: (message: string) => void,
753
+ ): void {
754
+ const commits = runProc(
755
+ 'git',
756
+ ['rev-list', 'HEAD', `^refs/remotes/${job.arbiterRemote}/main`],
757
+ job.dir,
758
+ {env},
759
+ )
760
+ .stdout.split('\n')
761
+ .filter((l) => l !== '');
762
+ if (commits.length === 0) return;
763
+ const {pointers} = scanLfsPointers({
764
+ cwd: job.dir,
765
+ blobs: changedFileBlobs(job.dir, commits, env),
766
+ env,
767
+ });
768
+ if (pointers.length === 0) return;
769
+ const store = localLfsObjectsDir(job.dir, env);
770
+ const missing = pointers.some((p) => {
771
+ const path = lfsObjectPath(store, p.oid);
772
+ return !existsSync(path) || !statSync(path).isFile();
773
+ });
774
+ if (!missing) return;
775
+ const fetched = runProc(
776
+ 'git',
777
+ ['lfs', 'fetch', job.arbiterRemote, ...commits],
778
+ job.dir,
779
+ {env},
780
+ );
781
+ if (fetched.status !== 0) {
782
+ note(
783
+ `git lfs fetch of the kept commits failed (${fetched.stderr.trim().slice(0, 300)}); ` +
784
+ 'the handoff will lack their LFS objects',
785
+ );
786
+ }
787
+ }
@@ -7,7 +7,7 @@ import {
7
7
  type SidecarEntry,
8
8
  type SidecarModel,
9
9
  } from './sidecar.js';
10
- import {deleteRemoteWorkBranchIfPresent} from './needs-attention.js';
10
+ import {refWrite} from './ref-write.js';
11
11
 
12
12
  /**
13
13
  * The **answered STUCK-QUESTION ACTION DISPATCH** (spec
@@ -229,7 +229,7 @@ export async function performStuckAction(
229
229
  }
230
230
 
231
231
  // verb === 'reset'
232
- const dropped = await deleteRemoteWorkBranchIfPresent({
232
+ const dropped = await refWrite.deleteRemoteWorkBranch({
233
233
  cwd: input.cwd,
234
234
  arbiter: input.arbiter,
235
235
  slug: input.slug,