dorfl 0.14.3 → 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 +305 -486
  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 +44 -13
  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 +25 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -1
  162. package/dist/install-ci-action-pins.js +25 -0
  163. package/dist/install-ci-action-pins.js.map +1 -1
  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 +148 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +278 -190
  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 -293
  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 +6 -3
  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 +371 -535
  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 +50 -14
  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 +25 -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 +352 -223
  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 -328
  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 +6 -3
  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
@@ -0,0 +1,1328 @@
1
+ /**
2
+ * **The build path split into the three CI phases** (spec
3
+ * `ci-agent-job-without-write-token` §3 build row and §8, ADR
4
+ * `ci-agent-job-holds-no-write-token`, tasks `ci-split-build-path` and
5
+ * `ci-split-build-path-non-integrate-intents`).
6
+ *
7
+ * `advance task:<slug>` / `do <slug>` with `--phase` run ONE of three halves of
8
+ * the one build pipeline (`performDo`), never a second implementation:
9
+ *
10
+ * - **lock** ({@link performBuildLockPhase}): fetch `<arbiter>/main` (its tip
11
+ * is `baseSha`, and the tree the item is classified in, never the run's
12
+ * commit); claim (`acquireItemLock`, `action: implement`); publish
13
+ * `acquired`, `needsAgent`, `rung`, `baseSha`, `lockSha`, `continueTip`,
14
+ * `handoffName` (it carries `github.run_attempt`, so a "Re-run all jobs"
15
+ * attempt never collides with an earlier attempt's artifact) and
16
+ * `agentTimeoutMinutes` (from the config at `baseSha`). An item that is not
17
+ * claimable at `baseSha` (it already advanced, or its lock is held) is a
18
+ * no-op that writes nothing.
19
+ * - **agent** ({@link performBuildAgentPhase}): check, read-only, that the lock
20
+ * ref still equals `lockSha`, then run `performDo` without its claim under
21
+ * the phase recorder: the continue rebase (LOCAL only, decision 7), the
22
+ * build agent, the STOP and deadline detection, the gate, the Gate-2 review,
23
+ * the done-move commit, the local rebase and the fresh-worktree gate run
24
+ * exactly as today, and the pipeline halts at its first write, which names
25
+ * the intent handed over ({@link handOverHalt}):
26
+ * - the land half (`integrationLand.land`): `integrate`, with the
27
+ * work-branch bundle, the PR title and body and the Gate-2 review prose;
28
+ * - a needs-attention bounce (red gate, blocked review, rebase conflict):
29
+ * `needs-attention`, the wip committed locally and bundled;
30
+ * - the deadline checkpoint's branch save: `deadline-checkpoint`, the
31
+ * checkpoint commit bundled;
32
+ * - the agent's deliberate STOP: `stop`; an agent failure: `agent-failed`
33
+ * (both with the wip bundled, when there is any).
34
+ * - **apply** ({@link performBuildApplyPhase}): refuse to write unless the
35
+ * lock ref still equals `lockSha` (so a "Re-run failed jobs" of a finished
36
+ * run writes nothing); act on the agent job's result first (task
37
+ * `ci-split-agent-result-and-reruns`): anything but `success` never reads
38
+ * the handoff, and surfaces the item (failure, timeout, an unexpected skip)
39
+ * or only releases the lock (a real cancel); validate the handoff as hostile
40
+ * (`validateApplyHandoff`); push the handoff's Git LFS objects before any
41
+ * ref, for every intent (decision 6, `pushLfsObjects`); when the lock job saw a kept branch, push the
42
+ * bundle's tip with a lease on that `continueTip` first (decision 7; a
43
+ * stale lease writes nothing); then resume at the write half of the intent:
44
+ * the land half (`landIntegration`, the unchanged compare-and-swap loop, then
45
+ * the review comment), the needs-attention route, `routeDeadlineCheckpoint`
46
+ * (with `maxAutoCheckpoints` from the config at `baseSha`),
47
+ * `saveAgentStop` or `saveAgentFailure`. Every lock release in the apply
48
+ * phase is leased on `lockSha` (`leaseLockReleases`), and the lock is
49
+ * released once the work is durably on `main` (a propose PR keeps it held,
50
+ * exactly as `complete` does today).
51
+ */
52
+
53
+ import {existsSync} from 'node:fs';
54
+ import {join} from 'node:path';
55
+ import {classifyTick} from './advance-classify.js';
56
+ import {readItemSignals} from './advance.js';
57
+ import {
58
+ HANDOFF_LIMITS,
59
+ HandoffRejected,
60
+ handoffName as deriveHandoffName,
61
+ type AgentFailedProducts,
62
+ type HandoffRecord,
63
+ type NeedsAttentionProducts,
64
+ type StopProducts,
65
+ } from './ci-handoff-format.js';
66
+ import {writeHandoff} from './ci-handoff.js';
67
+ import {validateApplyHandoff} from './ci-handoff-apply.js';
68
+ import type {LockOutputs} from './ci-lock-outputs.js';
69
+ import type {AgentJobResult, GithubApiGet} from './ci-agent-result.js';
70
+ import {
71
+ PhaseDriverError,
72
+ agentTimeoutMinutesAt,
73
+ arbiterLockSha,
74
+ arbiterRefSha,
75
+ boundHandoffText,
76
+ checkLockOwnership,
77
+ emitLockOutputs,
78
+ fetchArbiterMain,
79
+ noSmudgeEnv,
80
+ pushHandoffLfs,
81
+ releaseLockLeased,
82
+ repoConfigAt,
83
+ requireHeldLock,
84
+ resolveAgentResult,
85
+ runnerTempFrom,
86
+ taskSourceAtBase,
87
+ withBaseWorktree,
88
+ type HeldLock,
89
+ } from './ci-phase-driver.js';
90
+ import {performClaim} from './claim-cas.js';
91
+ import {
92
+ deadlineSurfaceReason,
93
+ performDo,
94
+ routeDeadlineCheckpoint,
95
+ saveAgentFailure,
96
+ saveAgentStop,
97
+ type BuildBoundary,
98
+ type DoOptions,
99
+ type DoResult,
100
+ } from './do.js';
101
+ import {
102
+ checkGatePreconditions,
103
+ detectLockfileOnDisk,
104
+ } from './gate-readiness.js';
105
+ import {run, runAsync} from './git.js';
106
+ import {selectProvider} from './github.js';
107
+ import {identityEnv} from './identity.js';
108
+ import {leaseLockReleases, markLockKeptForProposePr} from './item-lock.js';
109
+ import {
110
+ arbiterUrl,
111
+ integrationLand,
112
+ type IntegrationCoreResult,
113
+ type IntegrationLandInput,
114
+ } from './integration-core.js';
115
+ import {proposeRequestNotOpened} from './integrator.js';
116
+ import {ledgerRead} from './ledger-read.js';
117
+ import {ledgerWrite} from './ledger-write.js';
118
+ import {
119
+ commitAbortedWork,
120
+ type RouteToNeedsAttentionOptions,
121
+ } from './needs-attention.js';
122
+ import {refWrite} from './ref-write.js';
123
+ import type {Phase} from './phase.js';
124
+ import {
125
+ createPhaseRecorder,
126
+ recordingReviewProvider,
127
+ runAgentPhase,
128
+ type RecordedWriteIntent,
129
+ type WriteSeamCall,
130
+ } from './phase-recorder.js';
131
+ import {
132
+ SlugResolutionError,
133
+ resolveAdvanceArg,
134
+ resolveSlug,
135
+ workBranchRef,
136
+ } from './slug-namespace.js';
137
+ import {workItemRel} from './work-layout.js';
138
+
139
+ const DEFAULT_ARBITER = 'origin';
140
+
141
+ /** The verb a build phase runs under (they classify the argument differently). */
142
+ export type BuildPhaseVerb = 'do' | 'advance';
143
+
144
+ /** Options of a build phase run: the `do` pipeline's options plus the phase inputs. */
145
+ export interface BuildPhaseOptions extends DoOptions {
146
+ /** Which of the three jobs this process is. */
147
+ phase: Phase;
148
+ /** `do <slug>` or `advance task:<slug>`. */
149
+ verb: BuildPhaseVerb;
150
+ /**
151
+ * The handoff directory: the agent phase writes the artifact here (it must be
152
+ * empty or absent); the apply phase reads it (it must be under
153
+ * {@link runnerTemp}).
154
+ */
155
+ handoffDir?: string;
156
+ /** `$RUNNER_TEMP` (the apply phase); defaults to the env's. */
157
+ runnerTemp?: string;
158
+ /** The lock job's outputs (agent / apply); defaults to `DORFL_LOCK_OUTPUTS`. */
159
+ lockOutputs?: LockOutputs;
160
+ /** `github.run_attempt` (the lock phase names the artifact with it); default the env's, else 1. */
161
+ runAttempt?: string;
162
+ /** The `$GITHUB_OUTPUT` file the lock phase appends to; default the env's. */
163
+ githubOutput?: string;
164
+ /** The merge-mode CAS-loop jitter (tests pass 0). */
165
+ mergeJitterMs?: number;
166
+ /**
167
+ * apply: `needs.agent.result` (`--agent-result`), REQUIRED. Only `success`
168
+ * reads the handoff (decision 5, task `ci-split-agent-result-and-reruns`).
169
+ */
170
+ agentResult?: AgentJobResult;
171
+ /**
172
+ * apply: the agent job's timeout (`--agent-timeout-minutes`, the trusted
173
+ * lock output); defaults to the lock outputs' `agentTimeoutMinutes`.
174
+ */
175
+ agentTimeoutMinutes?: number;
176
+ /** apply: the Actions API reader (tests stub it); default `fetch` with `GITHUB_TOKEN`. */
177
+ actionsApi?: GithubApiGet;
178
+ }
179
+
180
+ /** How a build phase run ended. */
181
+ export type BuildPhaseOutcome =
182
+ /** lock: the item is claimed; the agent phase runs next. */
183
+ | 'locked'
184
+ /** lock: nothing to do at the arbiter's `main` (not claimable, or not a build). */
185
+ | 'no-op'
186
+ /** lock: another run holds the item's lock. */
187
+ | 'lost'
188
+ /** agent / apply: the lock ref no longer equals `lockSha`; nothing was done. */
189
+ | 'stale-lock'
190
+ /** agent: the handoff was written (see `intent`). */
191
+ | 'handed-over'
192
+ /** apply: the work landed on `main` and the lock was released. */
193
+ | 'landed'
194
+ /** apply: the branch was pushed (and a PR requested); the lock stays held. */
195
+ | 'proposed'
196
+ /** apply: the item was surfaced to needs-attention. */
197
+ | 'surfaced'
198
+ /** apply: surfacing the item did not land on the arbiter (it stays locked). */
199
+ | 'surface-unmoved'
200
+ /** apply: a deadline checkpoint was saved and the lock released to continue. */
201
+ | 'auto-continued'
202
+ /** apply: the agent job was cancelled (not timed out); the lock was only released. */
203
+ | 'released'
204
+ /** apply: the cancelled agent job's leased lock release was refused. */
205
+ | 'release-refused'
206
+ /**
207
+ * apply: the kept work branch moved on the arbiter since the lock job saw it
208
+ * (`continueTip`), so the leased continue push was refused; nothing was written.
209
+ */
210
+ | 'stale-lease'
211
+ /** apply: the handoff broke a rule; nothing from it was written. */
212
+ | 'rejected'
213
+ /** A usage or environment problem, or a path this task does not split yet. */
214
+ | 'usage-error';
215
+
216
+ /** The result of one build phase run. */
217
+ export interface BuildPhaseResult {
218
+ /**
219
+ * The job's exit code: a HANDLED outcome is 0 (a surface that landed, a
220
+ * rejected handoff that was surfaced, a lock lost to another run, a stale
221
+ * lock on a re-run); only an unhandled one is non-zero. See "A handled
222
+ * outcome is green" in `ci-phase-driver.ts`.
223
+ */
224
+ exitCode: 0 | 1 | 2 | 3;
225
+ outcome: BuildPhaseOutcome;
226
+ /**
227
+ * The result line: the CLI prints it (`>> ` or `error: `), so the phase does
228
+ * NOT also `note` it (each line appears once in the job log).
229
+ */
230
+ message: string;
231
+ slug?: string;
232
+ /** lock: the facts published. */
233
+ lockOutputs?: LockOutputs;
234
+ /** agent: the handed-over intent kind. */
235
+ intent?: HandoffRecord['intent']['kind'];
236
+ /** apply: the land half's result, when it ran. */
237
+ land?: IntegrationCoreResult;
238
+ }
239
+
240
+ /**
241
+ * Run one phase of the build path. The CLI calls this for `do <slug> --phase`
242
+ * and `advance task:<slug> --phase`.
243
+ */
244
+ export async function performBuildPhase(
245
+ options: BuildPhaseOptions,
246
+ ): Promise<BuildPhaseResult> {
247
+ try {
248
+ switch (options.phase) {
249
+ case 'lock':
250
+ return await performBuildLockPhase(options);
251
+ case 'agent':
252
+ return await performBuildAgentPhase(options);
253
+ case 'apply':
254
+ return await performBuildApplyPhase(options);
255
+ }
256
+ } catch (err) {
257
+ if (err instanceof PhaseDriverError || err instanceof SlugResolutionError) {
258
+ return {exitCode: 1, outcome: 'usage-error', message: err.message};
259
+ }
260
+ throw err;
261
+ }
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // lock
266
+ // ---------------------------------------------------------------------------
267
+
268
+ /** The task slug a build phase acts on, from the (trusted) workflow argument. */
269
+ function buildSlug(options: BuildPhaseOptions, repoPath: string): string {
270
+ if (options.verb === 'advance') {
271
+ const resolved = resolveAdvanceArg({
272
+ arg: options.arg,
273
+ repoPath,
274
+ read: options.read ?? ledgerRead,
275
+ });
276
+ if (resolved.namespace !== 'task') {
277
+ throw new PhaseDriverError(
278
+ `--phase splits only the build path so far; ${options.arg} is not a task`,
279
+ );
280
+ }
281
+ return resolved.slug;
282
+ }
283
+ const resolved = resolveSlug({
284
+ arg: options.arg,
285
+ repoPath,
286
+ read: options.read ?? ledgerRead,
287
+ });
288
+ if (resolved.namespace !== 'task') {
289
+ throw new PhaseDriverError(
290
+ `--phase splits only the build path so far; ${options.arg} is not a task`,
291
+ );
292
+ }
293
+ return resolved.slug;
294
+ }
295
+
296
+ /**
297
+ * The lock phase: classify the item at the arbiter's `main`, claim it and
298
+ * publish the trusted facts. Runs no agent and no repository code. An item that
299
+ * already advanced (not claimable at `baseSha`), or whose lock another run
300
+ * holds, is refused BEFORE any write.
301
+ */
302
+ export async function performBuildLockPhase(
303
+ options: BuildPhaseOptions,
304
+ ): Promise<BuildPhaseResult> {
305
+ const note = options.note ?? (() => {});
306
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
307
+ const cwd = options.cwd;
308
+ const env = identityEnv(options.identity, options.env ?? process.env);
309
+ const githubOutput = options.githubOutput ?? env.GITHUB_OUTPUT;
310
+ const publish = (facts: LockOutputs): LockOutputs => {
311
+ emitLockOutputs(facts, {githubOutput, note});
312
+ return facts;
313
+ };
314
+
315
+ const baseSha = await fetchArbiterMain({cwd, arbiter, env});
316
+ const verdict = await withBaseWorktree(
317
+ {cwd, baseSha, env},
318
+ async (base): Promise<{slug: string; skip?: string}> => {
319
+ const slug = buildSlug(options, base);
320
+ if (options.verb === 'advance') {
321
+ const item = `task:${slug}`;
322
+ const signals = readItemSignals({
323
+ repoPath: base,
324
+ type: 'task',
325
+ slug,
326
+ item,
327
+ });
328
+ const kind = classifyTick({type: 'task', ...signals}).kind;
329
+ if (kind !== 'build-task') {
330
+ if (kind === 'no-op') {
331
+ return {slug, skip: `no-op for ${item} at ${baseSha}`};
332
+ }
333
+ throw new PhaseDriverError(
334
+ `--phase splits only the build path so far; ${item} classifies ` +
335
+ `as ${kind} at ${baseSha}`,
336
+ );
337
+ }
338
+ }
339
+ const folders =
340
+ options.allowBacklog === true
341
+ ? (['tasks-ready', 'tasks-backlog'] as const)
342
+ : (['tasks-ready'] as const);
343
+ const claimable = folders.some((f) =>
344
+ existsSync(join(base, workItemRel(f, `${slug}.md`))),
345
+ );
346
+ if (!claimable) {
347
+ return {
348
+ slug,
349
+ skip:
350
+ `'${slug}' is not claimable at ${arbiter}/main (${baseSha}): it ` +
351
+ 'already advanced, or was removed',
352
+ };
353
+ }
354
+ const guard = checkGatePreconditions({
355
+ freshWorktreeGate: options.freshWorktreeGate,
356
+ prepare: options.prepare,
357
+ verify: options.verify,
358
+ lockfile: detectLockfileOnDisk(base),
359
+ });
360
+ if (guard !== undefined) throw new PhaseDriverError(guard.message);
361
+ return {slug};
362
+ },
363
+ );
364
+ const slug = verdict.slug;
365
+ const item = `task:${slug}`;
366
+ if (verdict.skip !== undefined) {
367
+ return {
368
+ exitCode: 0,
369
+ outcome: 'no-op',
370
+ slug,
371
+ message: verdict.skip,
372
+ lockOutputs: publish({acquired: false, baseSha}),
373
+ };
374
+ }
375
+ if ((await arbiterLockSha({cwd, arbiter, item, env})) !== undefined) {
376
+ const message = `'${slug}' is already locked on ${arbiter}; backing off.`;
377
+ return {
378
+ exitCode: 0,
379
+ outcome: 'lost',
380
+ slug,
381
+ message,
382
+ lockOutputs: publish({acquired: false, rung: 'build-task', baseSha}),
383
+ };
384
+ }
385
+
386
+ const claim = await performClaim({
387
+ slug,
388
+ cwd,
389
+ arbiter,
390
+ allowBacklog: options.allowBacklog === true,
391
+ env,
392
+ note,
393
+ });
394
+ if (claim.exitCode !== 0) {
395
+ const outcome = claim.outcome === 'lost' ? 'lost' : 'usage-error';
396
+ return {
397
+ // A claim lost to another run is handled, so green ("A handled
398
+ // outcome is green", `ci-phase-driver.ts`).
399
+ exitCode: outcome === 'lost' ? 0 : claim.exitCode,
400
+ outcome,
401
+ slug,
402
+ message: claim.message,
403
+ lockOutputs: publish({acquired: false, rung: 'build-task', baseSha}),
404
+ };
405
+ }
406
+ const lockSha = await arbiterLockSha({cwd, arbiter, item, env});
407
+ if (lockSha === undefined) {
408
+ throw new Error(`the lock of ${item} vanished right after the claim`);
409
+ }
410
+ const continueTip = await arbiterRefSha({
411
+ cwd,
412
+ arbiter,
413
+ ref: `refs/heads/${workBranchRef('task', slug)}`,
414
+ env,
415
+ });
416
+ const facts = publish({
417
+ acquired: true,
418
+ needsAgent: true,
419
+ rung: 'build-task',
420
+ baseSha,
421
+ lockSha,
422
+ continueTip,
423
+ handoffName: deriveHandoffName(
424
+ item,
425
+ options.runAttempt ?? env.GITHUB_RUN_ATTEMPT ?? '1',
426
+ ),
427
+ agentTimeoutMinutes: agentTimeoutMinutesAt(cwd, baseSha, env),
428
+ });
429
+ return {
430
+ exitCode: 0,
431
+ outcome: 'locked',
432
+ slug,
433
+ message: `locked ${item} at ${baseSha} (lock ${lockSha})`,
434
+ lockOutputs: facts,
435
+ };
436
+ }
437
+
438
+ // ---------------------------------------------------------------------------
439
+ // agent
440
+ // ---------------------------------------------------------------------------
441
+
442
+ /** Every write seam call the build pipeline can reach (all are recorded). */
443
+ function buildRecordableCalls(): WriteSeamCall[] {
444
+ return [
445
+ 'integrationLand.land',
446
+ ...Object.keys(ledgerWrite).map((m) => `ledgerWrite.${m}` as const),
447
+ ...Object.keys(refWrite).map((m) => `refWrite.${m}` as const),
448
+ 'reviewProvider.openRequest',
449
+ 'reviewProvider.postPRComment',
450
+ 'reviewProvider.postPRCommentOnBranch',
451
+ 'reviewProvider.closeRequestOnBranch',
452
+ ];
453
+ }
454
+
455
+ /** The `agent-failed` detail for a halt at a write the build path does not carry. */
456
+ function haltDetail(intent: RecordedWriteIntent): string {
457
+ const input = intent.input as {reason?: unknown} | undefined;
458
+ const reason =
459
+ input !== null &&
460
+ typeof input === 'object' &&
461
+ typeof input.reason === 'string'
462
+ ? `: ${input.reason}`
463
+ : '';
464
+ return (
465
+ `the build stopped at ${intent.seam}.${intent.method}${reason} ` +
466
+ '(this outcome is not carried through the CI phases)'
467
+ );
468
+ }
469
+
470
+ /** What the agent phase writes: the record, and the work branch to bundle. */
471
+ interface Handover {
472
+ record: HandoffRecord;
473
+ bundle?: {repo: string; workBranch: string; baseSha: string};
474
+ }
475
+
476
+ /** The context {@link handOverHalt} maps a halt in. */
477
+ interface HaltContext {
478
+ item: string;
479
+ slug: string;
480
+ arbiter: string;
481
+ baseSha: string;
482
+ env: NodeJS.ProcessEnv;
483
+ }
484
+
485
+ /**
486
+ * The work branch to bundle as the WIP of a non-integrate intent, or
487
+ * `undefined` when it carries nothing to recover: the checkout is not on the
488
+ * item's work branch, or the branch has no commit that `<arbiter>/main` lacks
489
+ * (the apply phase refuses a bundle without a new commit).
490
+ */
491
+ function wipBundle(
492
+ cwd: string,
493
+ ctx: HaltContext,
494
+ ): Handover['bundle'] | undefined {
495
+ const workBranch = workBranchRef('task', ctx.slug);
496
+ const head = run('git', ['symbolic-ref', '--quiet', 'HEAD'], cwd, {
497
+ env: ctx.env,
498
+ });
499
+ if (head.status !== 0 || head.stdout.trim() !== `refs/heads/${workBranch}`) {
500
+ return undefined;
501
+ }
502
+ const ahead = run(
503
+ 'git',
504
+ [
505
+ 'rev-list',
506
+ '--count',
507
+ `refs/heads/${workBranch}`,
508
+ `^refs/remotes/${ctx.arbiter}/main`,
509
+ `^${ctx.baseSha}`,
510
+ ],
511
+ cwd,
512
+ {env: ctx.env},
513
+ );
514
+ if (ahead.status !== 0 || Number(ahead.stdout.trim()) === 0) {
515
+ return undefined;
516
+ }
517
+ return {repo: cwd, workBranch, baseSha: ctx.baseSha};
518
+ }
519
+
520
+ /** A bounded reason (or question) for the handoff record. */
521
+ function reasonText(text: string): string {
522
+ return boundHandoffText(text, HANDOFF_LIMITS.reasonChars);
523
+ }
524
+
525
+ /** The `needs-attention` handover of a bounce's recorded input. */
526
+ function needsAttention(
527
+ ctx: HaltContext,
528
+ input: {reason: string; questions?: string[]},
529
+ bundle: Handover['bundle'] | undefined,
530
+ ): Handover {
531
+ return {
532
+ record: {
533
+ schema: 1,
534
+ item: ctx.item,
535
+ intent: {kind: 'needs-attention'},
536
+ products: {
537
+ reason: reasonText(input.reason),
538
+ ...(input.questions === undefined || input.questions.length === 0
539
+ ? {}
540
+ : {questions: input.questions.map(reasonText)}),
541
+ },
542
+ },
543
+ bundle,
544
+ };
545
+ }
546
+
547
+ /**
548
+ * Map the build's FIRST WRITE (the recorded halt) to the handoff (task
549
+ * `ci-split-build-path-non-integrate-intents`). The seam call names the write;
550
+ * the pipeline's {@link BuildBoundary} annotation names the outcome when several
551
+ * share one seam call. A bounce's LOCAL half (the wip commit, which the seam
552
+ * would have made before pushing) runs here, so the wip travels in the bundle.
553
+ */
554
+ function handOverHalt(intent: RecordedWriteIntent, ctx: HaltContext): Handover {
555
+ const call = `${intent.seam}.${intent.method}`;
556
+ const boundary = intent.annotation as BuildBoundary | undefined;
557
+
558
+ if (call === 'integrationLand.land') {
559
+ const land = intent.input as IntegrationLandInput;
560
+ if (land.branch !== workBranchRef('task', ctx.slug)) {
561
+ throw new Error(
562
+ `the land half names the branch ${land.branch}, not ` +
563
+ workBranchRef('task', ctx.slug),
564
+ );
565
+ }
566
+ const limit = HANDOFF_LIMITS.commentChars;
567
+ return {
568
+ record: {
569
+ schema: 1,
570
+ item: ctx.item,
571
+ intent: {kind: 'integrate'},
572
+ products: {
573
+ prTitle: land.title,
574
+ prBody: boundHandoffText(land.body ?? '', limit),
575
+ ...(land.reviewProse === undefined
576
+ ? {}
577
+ : {reviewProse: boundHandoffText(land.reviewProse, limit)}),
578
+ },
579
+ },
580
+ bundle: {repo: land.cwd, workBranch: land.branch, baseSha: ctx.baseSha},
581
+ };
582
+ }
583
+
584
+ // The deadline checkpoint: its marker commit is already made (locally) when
585
+ // the branch save is recorded.
586
+ if (
587
+ call === 'refWrite.saveWorkBranch' &&
588
+ boundary?.kind === 'deadline-checkpoint'
589
+ ) {
590
+ const input = intent.input as RouteToNeedsAttentionOptions;
591
+ commitAbortedWork({cwd: input.cwd, slug: ctx.slug, env: ctx.env});
592
+ const bundle = wipBundle(input.cwd, ctx);
593
+ if (!boundary.predecessorGone) {
594
+ const reason = deadlineSurfaceReason({
595
+ slug: ctx.slug,
596
+ kind: 'unreaped',
597
+ detail: boundary.reapDetail,
598
+ });
599
+ return needsAttention(ctx, {reason}, bundle);
600
+ }
601
+ if (bundle === undefined) {
602
+ // Nothing on the branch: no progress, so today's route surfaces.
603
+ const reason = deadlineSurfaceReason({
604
+ slug: ctx.slug,
605
+ kind: 'no-progress',
606
+ });
607
+ return needsAttention(ctx, {reason}, undefined);
608
+ }
609
+ return {
610
+ record: {
611
+ schema: 1,
612
+ item: ctx.item,
613
+ intent: {kind: 'deadline-checkpoint'},
614
+ products: {},
615
+ },
616
+ bundle,
617
+ };
618
+ }
619
+
620
+ // The empty-diff STOP surfaces a question on main without a branch save.
621
+ if (boundary?.kind === 'stop' && boundary.stopKind === 'empty-diff') {
622
+ return {
623
+ record: {
624
+ schema: 1,
625
+ item: ctx.item,
626
+ intent: {kind: 'stop'},
627
+ products: {
628
+ reason: reasonText(boundary.reason),
629
+ stopKind: 'empty-diff',
630
+ },
631
+ },
632
+ };
633
+ }
634
+
635
+ if (call === 'ledgerWrite.applyNeedsAttentionTransition') {
636
+ const input = intent.input as RouteToNeedsAttentionOptions;
637
+ commitAbortedWork({cwd: input.cwd, slug: ctx.slug, env: ctx.env});
638
+ const bundle =
639
+ input.pushBranch === false ? undefined : wipBundle(input.cwd, ctx);
640
+ if (boundary?.kind === 'agent-failed') {
641
+ return {record: agentFailed(ctx.item, boundary.failureDetail), bundle};
642
+ }
643
+ if (boundary?.kind === 'stop') {
644
+ return {
645
+ record: {
646
+ schema: 1,
647
+ item: ctx.item,
648
+ intent: {kind: 'stop'},
649
+ products: {
650
+ reason: reasonText(boundary.reason),
651
+ stopKind: boundary.stopKind,
652
+ },
653
+ },
654
+ bundle,
655
+ };
656
+ }
657
+ return needsAttention(ctx, input, bundle);
658
+ }
659
+
660
+ // A tree-less surface (the continue rebase conflicted at onboarding): the
661
+ // kept branch is untouched on the arbiter, so nothing is bundled.
662
+ if (call === 'ledgerWrite.applyTreelessNeedsAttentionTransition') {
663
+ const input = intent.input as {reason: string; questions?: string[]};
664
+ return needsAttention(ctx, input, undefined);
665
+ }
666
+
667
+ return {record: agentFailed(ctx.item, haltDetail(intent))};
668
+ }
669
+
670
+ /**
671
+ * The agent phase: after a read-only lock-ownership check, run the build
672
+ * pipeline (`performDo`, without its claim) under the phase recorder, halt at
673
+ * the first write, and write the handoff. Holds a read-only token: it never
674
+ * writes to the arbiter.
675
+ */
676
+ export async function performBuildAgentPhase(
677
+ options: BuildPhaseOptions,
678
+ ): Promise<BuildPhaseResult> {
679
+ const note = options.note ?? (() => {});
680
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
681
+ const cwd = options.cwd;
682
+ const env = options.env ?? process.env;
683
+ const held = requireHeldLock({
684
+ lockOutputs: options.lockOutputs,
685
+ env,
686
+ rung: 'build-task',
687
+ });
688
+ if (options.handoffDir === undefined) {
689
+ throw new PhaseDriverError('the agent phase needs a handoff directory');
690
+ }
691
+ const slug = buildSlug(options, options.repoPath ?? cwd);
692
+ const item = `task:${slug}`;
693
+
694
+ const ownership = await checkLockOwnership({
695
+ cwd,
696
+ arbiter,
697
+ item,
698
+ lockSha: held.lockSha,
699
+ env,
700
+ });
701
+ if (!ownership.owned) {
702
+ return {
703
+ exitCode: 0,
704
+ outcome: 'stale-lock',
705
+ slug,
706
+ message: ownership.message,
707
+ };
708
+ }
709
+
710
+ const recorder = createPhaseRecorder({record: buildRecordableCalls()});
711
+ const ctx: HaltContext = {item, slug, arbiter, baseSha: held.baseSha, env};
712
+ let handover: Handover;
713
+ try {
714
+ // EVERY review provider the pipeline can reach records (never only an
715
+ // injected one): without an injected instance, resolve the one the
716
+ // integration core would select from the arbiter URL, and wrap it.
717
+ const provider =
718
+ options.providerInstance ??
719
+ selectProvider({arbiterUrl: await arbiterUrl(cwd, arbiter, env)});
720
+ const outcome = await runAgentPhase(recorder, () =>
721
+ performDo({
722
+ ...options,
723
+ phase: 'agent',
724
+ providerInstance: recordingReviewProvider(provider, recorder),
725
+ }),
726
+ );
727
+ handover = outcome.halted
728
+ ? handOverHalt(outcome.intent, ctx)
729
+ : {
730
+ record: agentFailed(
731
+ item,
732
+ `the build ended without reaching a write: ${outcome.result.message}`,
733
+ ),
734
+ };
735
+ } catch (err) {
736
+ const message = err instanceof Error ? err.message : String(err);
737
+ handover = {record: agentFailed(item, `the build failed: ${message}`)};
738
+ }
739
+
740
+ const written = writeHandoff({
741
+ dir: options.handoffDir,
742
+ rung: 'build-task',
743
+ record: handover.record,
744
+ bundle: handover.bundle,
745
+ });
746
+ if (written.lfsMissing.length > 0) {
747
+ note(
748
+ `the local LFS store lacks ${written.lfsMissing.length} object(s) a new ` +
749
+ `commit points at (${written.lfsMissing.slice(0, 5).join(', ')}); the ` +
750
+ 'apply job will reject the handoff',
751
+ );
752
+ }
753
+ const kind = handover.record.intent.kind;
754
+ const message = `handed over ${kind} for ${item}`;
755
+ return {
756
+ exitCode: 0,
757
+ outcome: 'handed-over',
758
+ slug,
759
+ message,
760
+ intent: kind,
761
+ };
762
+ }
763
+
764
+ function agentFailed(item: string, detail: string): HandoffRecord {
765
+ return {
766
+ schema: 1,
767
+ item,
768
+ intent: {kind: 'agent-failed'},
769
+ products: {
770
+ failureDetail: boundHandoffText(detail, HANDOFF_LIMITS.reasonChars),
771
+ },
772
+ };
773
+ }
774
+
775
+ // ---------------------------------------------------------------------------
776
+ // apply
777
+ // ---------------------------------------------------------------------------
778
+
779
+ /** Surface the item to needs-attention (tree-less) with `reason`. */
780
+ async function surface(params: {
781
+ cwd: string;
782
+ arbiter: string;
783
+ slug: string;
784
+ reason: string;
785
+ env: NodeJS.ProcessEnv;
786
+ note: (message: string) => void;
787
+ outcome: BuildPhaseOutcome;
788
+ }): Promise<BuildPhaseResult> {
789
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
790
+ cwd: params.cwd,
791
+ slug: params.slug,
792
+ reason: params.reason,
793
+ arbiter: params.arbiter,
794
+ env: params.env,
795
+ note: params.note,
796
+ });
797
+ const where = surfaced.moved
798
+ ? 'surfaced it to needs-attention'
799
+ : `could not surface it (${surfaced.reasonNotMoved ?? 'unknown'})`;
800
+ // A clean surface is green ("A handled outcome is green",
801
+ // `ci-phase-driver.ts`); only a surface that did not land stays red.
802
+ return {
803
+ exitCode: surfaced.moved ? 0 : 1,
804
+ outcome: surfaced.moved ? params.outcome : 'surface-unmoved',
805
+ slug: params.slug,
806
+ message: `${params.reason}; ${where}.`,
807
+ };
808
+ }
809
+
810
+ /**
811
+ * The apply phase: check the lock is still this run's, validate the handoff as
812
+ * hostile, then resume the build at the intent's write half. Runs no agent and
813
+ * no repository code; every write goes through dorfl's seams.
814
+ */
815
+ export async function performBuildApplyPhase(
816
+ options: BuildPhaseOptions,
817
+ ): Promise<BuildPhaseResult> {
818
+ const note = options.note ?? (() => {});
819
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
820
+ const cwd = options.cwd;
821
+ const env = noSmudgeEnv(
822
+ identityEnv(options.identity, options.env ?? process.env),
823
+ );
824
+ const held: HeldLock = requireHeldLock({
825
+ lockOutputs: options.lockOutputs,
826
+ env,
827
+ rung: 'build-task',
828
+ });
829
+ if (options.agentResult === undefined) {
830
+ throw new PhaseDriverError(
831
+ 'the apply phase needs the agent job result (--agent-result, ' +
832
+ 'needs.agent.result): only success reads the handoff',
833
+ );
834
+ }
835
+ const agentResult = options.agentResult;
836
+ const slug = buildSlug(options, options.repoPath ?? cwd);
837
+ const item = `task:${slug}`;
838
+
839
+ // Lock ownership BEFORE the first write (spec §8): a lock that was released,
840
+ // reaped or re-taken since the lock job ran means this run writes nothing.
841
+ // This is also what makes a "Re-run failed jobs" of a finished run safe: it
842
+ // replays the old lock outputs and artifact, but the first apply already
843
+ // released or surfaced the item, so the lock is gone and nothing is written:
844
+ // a handled outcome, so the re-run is green.
845
+ const ownership = await checkLockOwnership({
846
+ cwd,
847
+ arbiter,
848
+ item,
849
+ lockSha: held.lockSha,
850
+ env,
851
+ });
852
+ if (!ownership.owned) {
853
+ return {
854
+ exitCode: 0,
855
+ outcome: 'stale-lock',
856
+ slug,
857
+ message: ownership.message,
858
+ };
859
+ }
860
+
861
+ // Every lock release from here on (the land's, the surface's, the
862
+ // return-to-backlog's) is leased on the sha this run owns: none of them may
863
+ // delete a lock another run took since the ownership check.
864
+ const restoreLease = leaseLockReleases(item, held.lockSha);
865
+ try {
866
+ const ctx: ApplyContext = {options, held, slug, item, arbiter, cwd, env};
867
+ // The agent job's result (decision 5) BEFORE any artifact is read: only
868
+ // `success` reads it; the build path has no agent-less rung, so a
869
+ // `skipped` agent job is never the deterministic case here.
870
+ const decision = await resolveAgentResult({
871
+ result: agentResult,
872
+ held,
873
+ agentTimeoutMinutes: options.agentTimeoutMinutes,
874
+ api: options.actionsApi,
875
+ env,
876
+ });
877
+ switch (decision.action) {
878
+ case 'read-handoff':
879
+ return await applyOwned(ctx);
880
+ case 'deterministic':
881
+ return await surfaceAgentResult(
882
+ ctx,
883
+ 'the agent job was skipped (needsAgent: false), but the build ' +
884
+ 'path has no rung that runs without an agent; the handoff was not read',
885
+ );
886
+ case 'surface':
887
+ return await surfaceAgentResult(ctx, decision.reason);
888
+ case 'release':
889
+ return await releaseAfterCancel(ctx, decision.reason);
890
+ }
891
+ } finally {
892
+ restoreLease();
893
+ }
894
+ }
895
+
896
+ /** What {@link applyOwned} works with (the apply phase, past the ownership check). */
897
+ interface ApplyContext {
898
+ options: BuildPhaseOptions;
899
+ held: HeldLock;
900
+ slug: string;
901
+ item: string;
902
+ arbiter: string;
903
+ cwd: string;
904
+ env: NodeJS.ProcessEnv;
905
+ }
906
+
907
+ /**
908
+ * The apply phase once the lock is known to be this run's: validate the
909
+ * handoff, publish a continued branch with its lease, then resume at the
910
+ * intent's write half.
911
+ */
912
+ async function applyOwned(ctx: ApplyContext): Promise<BuildPhaseResult> {
913
+ const {options, held, slug, item, arbiter, cwd, env} = ctx;
914
+ const note = options.note ?? (() => {});
915
+
916
+ let validated;
917
+ try {
918
+ if (options.handoffDir === undefined) {
919
+ throw new HandoffRejected('layout', 'no handoff directory was given');
920
+ }
921
+ validated = validateApplyHandoff({
922
+ dir: options.handoffDir,
923
+ runnerTemp: runnerTempFrom(options.runnerTemp, env),
924
+ repo: cwd,
925
+ trust: {
926
+ item,
927
+ rung: 'build-task',
928
+ baseSha: held.baseSha,
929
+ arbiter,
930
+ integrationMode: options.integration ?? 'propose',
931
+ },
932
+ env,
933
+ });
934
+ } catch (err) {
935
+ if (!(err instanceof HandoffRejected)) throw err;
936
+ note(err.message);
937
+ return surface({
938
+ cwd,
939
+ arbiter,
940
+ slug,
941
+ reason: err.message,
942
+ env,
943
+ note,
944
+ outcome: 'rejected',
945
+ });
946
+ }
947
+
948
+ const record = validated.handoff.record;
949
+ const bundle = validated.bundle;
950
+
951
+ // The LFS objects go FIRST (decision 6), before any ref of any intent (the
952
+ // continue push, the land's pushes, a WIP branch save), so a ref never lands
953
+ // pointing at a missing object.
954
+ const lfs = await pushHandoffLfs({
955
+ cwd,
956
+ arbiter,
957
+ objects: validated.lfsObjects,
958
+ env,
959
+ note,
960
+ });
961
+ if (!lfs.ok) {
962
+ return surface({
963
+ cwd,
964
+ arbiter,
965
+ slug,
966
+ reason: lfs.reason,
967
+ env,
968
+ note,
969
+ outcome: lfs.rejected ? 'rejected' : 'surfaced',
970
+ });
971
+ }
972
+
973
+ if (bundle !== undefined) {
974
+ await gitHard(
975
+ ['checkout', '--quiet', '-B', bundle.workBranch, bundle.tip],
976
+ cwd,
977
+ env,
978
+ );
979
+ // The CONTINUE push (decision 7): the agent job rebased the kept branch
980
+ // locally; publish it with a lease on the tip the LOCK job observed, before
981
+ // any other write. A branch that moved since then refuses the push, and
982
+ // this run writes nothing at all.
983
+ if (held.continueTip !== undefined) {
984
+ const pushed = await refWrite.pushLeasedWorkBranch({
985
+ arbiter,
986
+ branch: bundle.workBranch,
987
+ commit: bundle.tip,
988
+ expectedTip: held.continueTip,
989
+ cwd,
990
+ env,
991
+ });
992
+ if (pushed.status !== 0) {
993
+ const message =
994
+ `${bundle.workBranch} on ${arbiter} moved since the lock job saw it ` +
995
+ `at ${held.continueTip}, so the leased push of the continued branch ` +
996
+ `was refused (${pushed.stderr.trim()}). Nothing was written and the ` +
997
+ `lock of ${item} is still held: inspect the branch, then \`dorfl ` +
998
+ `requeue ${slug}\` to retry the item.`;
999
+ return {exitCode: 1, outcome: 'stale-lease', slug, message};
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ const pushBranch = bundle !== undefined;
1005
+ const branch = workBranchRef('task', slug);
1006
+ switch (record.intent.kind) {
1007
+ case 'integrate':
1008
+ return applyIntegrate(ctx, validated);
1009
+ case 'needs-attention': {
1010
+ const {reason, questions} = record.products as NeedsAttentionProducts;
1011
+ const routed = pushBranch
1012
+ ? await ledgerWrite.applyNeedsAttentionTransition({
1013
+ cwd,
1014
+ slug,
1015
+ reason,
1016
+ questions,
1017
+ arbiter,
1018
+ env,
1019
+ note,
1020
+ })
1021
+ : await ledgerWrite.applyTreelessNeedsAttentionTransition({
1022
+ cwd,
1023
+ slug,
1024
+ reason,
1025
+ questions,
1026
+ arbiter,
1027
+ env,
1028
+ note,
1029
+ });
1030
+ const message = routed.moved
1031
+ ? `Surfaced '${slug}' to needs-attention: ${reason}`
1032
+ : `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
1033
+ return {
1034
+ exitCode: routed.moved ? 0 : 1,
1035
+ outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
1036
+ slug,
1037
+ message,
1038
+ };
1039
+ }
1040
+ case 'stop': {
1041
+ const stop = record.products as StopProducts;
1042
+ return fromDoResult(
1043
+ await saveAgentStop({
1044
+ slug,
1045
+ branch,
1046
+ cwd,
1047
+ arbiter,
1048
+ reason: stop.reason,
1049
+ kind: stop.stopKind,
1050
+ pushBranch,
1051
+ env,
1052
+ note,
1053
+ }),
1054
+ );
1055
+ }
1056
+ case 'agent-failed':
1057
+ return fromDoResult(
1058
+ await saveAgentFailure({
1059
+ slug,
1060
+ branch,
1061
+ cwd,
1062
+ arbiter,
1063
+ detail: (record.products as AgentFailedProducts).failureDetail,
1064
+ pushBranch,
1065
+ env,
1066
+ note,
1067
+ }),
1068
+ );
1069
+ case 'deadline-checkpoint':
1070
+ return fromDoResult(
1071
+ await routeDeadlineCheckpoint({
1072
+ slug,
1073
+ branch,
1074
+ cwd,
1075
+ arbiter,
1076
+ maxAutoCheckpoints: maxAutoCheckpointsAt(
1077
+ cwd,
1078
+ held.baseSha,
1079
+ options.maxAutoCheckpoints,
1080
+ env,
1081
+ ),
1082
+ env,
1083
+ note,
1084
+ }),
1085
+ );
1086
+ default:
1087
+ return surface({
1088
+ cwd,
1089
+ arbiter,
1090
+ slug,
1091
+ reason:
1092
+ `the agent job handed over ${record.intent.kind}, which the build ` +
1093
+ 'path does not carry',
1094
+ env,
1095
+ note,
1096
+ outcome: 'surfaced',
1097
+ });
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ * Surface the item to needs-attention because the agent job did not succeed
1103
+ * (decision 5): tree-less (nothing from the agent job is pushed: its artifact is
1104
+ * not read), the lock released leased on `lockSha`. A kept work branch stays as
1105
+ * it is on the arbiter.
1106
+ */
1107
+ async function surfaceAgentResult(
1108
+ ctx: ApplyContext,
1109
+ reason: string,
1110
+ ): Promise<BuildPhaseResult> {
1111
+ const {options, slug, arbiter, cwd, env} = ctx;
1112
+ const note = options.note ?? (() => {});
1113
+ const routed = await ledgerWrite.applyTreelessNeedsAttentionTransition({
1114
+ cwd,
1115
+ slug,
1116
+ reason,
1117
+ arbiter,
1118
+ env,
1119
+ note,
1120
+ });
1121
+ const message = routed.moved
1122
+ ? `Surfaced '${slug}' to needs-attention: ${reason}`
1123
+ : `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
1124
+ return {
1125
+ exitCode: routed.moved ? 0 : 1,
1126
+ outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
1127
+ slug,
1128
+ message,
1129
+ };
1130
+ }
1131
+
1132
+ /**
1133
+ * The agent job was cancelled before its timeout (decision 5): only release the
1134
+ * lock, leased on `lockSha`, so the next run picks the item up again.
1135
+ */
1136
+ async function releaseAfterCancel(
1137
+ ctx: ApplyContext,
1138
+ reason: string,
1139
+ ): Promise<BuildPhaseResult> {
1140
+ const {options, held, slug, item, arbiter, cwd, env} = ctx;
1141
+ const note = options.note ?? (() => {});
1142
+ const released = await releaseLockLeased({
1143
+ cwd,
1144
+ arbiter,
1145
+ item,
1146
+ expectedSha: held.lockSha,
1147
+ env,
1148
+ });
1149
+ note(released.message);
1150
+ const message = released.released
1151
+ ? `Released '${slug}': ${reason}.`
1152
+ : `Could not release '${slug}' (${released.message}): ${reason}.`;
1153
+ return {
1154
+ exitCode: released.released ? 0 : 1,
1155
+ outcome: released.released ? 'released' : 'release-refused',
1156
+ slug,
1157
+ message,
1158
+ };
1159
+ }
1160
+
1161
+ /** Map a reused `do` write half's result to the apply phase's. */
1162
+ function fromDoResult(r: DoResult): BuildPhaseResult {
1163
+ const outcome: BuildPhaseOutcome =
1164
+ r.outcome === 'deadline-auto-continued'
1165
+ ? 'auto-continued'
1166
+ : r.routedToNeedsAttention === true
1167
+ ? 'surfaced'
1168
+ : 'surface-unmoved';
1169
+ return {exitCode: r.exitCode, outcome, slug: r.slug, message: r.message};
1170
+ }
1171
+
1172
+ /**
1173
+ * The `maxAutoCheckpoints` ceiling from the repository config AT `baseSha`
1174
+ * (trusted, the arbiter's `main` the lock job classified at), never from the
1175
+ * bundle: a work branch could raise its own ceiling otherwise. Falls back to
1176
+ * the resolved option, then to the default, when the file does not set a
1177
+ * valid value.
1178
+ */
1179
+ function maxAutoCheckpointsAt(
1180
+ cwd: string,
1181
+ baseSha: string,
1182
+ fallback: number | undefined,
1183
+ env: NodeJS.ProcessEnv,
1184
+ ): number {
1185
+ const cap = repoConfigAt(cwd, baseSha, env)?.maxAutoCheckpoints;
1186
+ if (typeof cap === 'number' && Number.isInteger(cap) && cap >= 1) {
1187
+ return cap;
1188
+ }
1189
+ return fallback ?? 5;
1190
+ }
1191
+
1192
+ /** The `integrate` write half: the land loop, the review comment, the release. */
1193
+ async function applyIntegrate(
1194
+ ctx: ApplyContext,
1195
+ validated: ReturnType<typeof validateApplyHandoff>,
1196
+ ): Promise<BuildPhaseResult> {
1197
+ const {options, held, slug, item, arbiter, cwd, env} = ctx;
1198
+ const note = options.note ?? (() => {});
1199
+ const record = validated.handoff.record;
1200
+ if (record.intent.kind !== 'integrate' || validated.bundle === undefined) {
1201
+ throw new Error('applyIntegrate needs an integrate handoff with a bundle');
1202
+ }
1203
+ const products = record.products as {
1204
+ prTitle: string;
1205
+ prBody: string;
1206
+ reviewProse?: string;
1207
+ };
1208
+ const branch = validated.bundle.workBranch;
1209
+ const tip = validated.bundle.tip;
1210
+ if (validated.forcedPropose) {
1211
+ note(
1212
+ `Untrusted-origin task '${slug}': forcing the BUILD transition to ` +
1213
+ 'propose (recomputed from the task at the base).',
1214
+ );
1215
+ }
1216
+ const commitMessage = (
1217
+ await gitHard(['log', '-1', '--format=%s', tip], cwd, env)
1218
+ ).trim();
1219
+ const land = await integrationLand.land({
1220
+ cwd,
1221
+ arbiter,
1222
+ slug,
1223
+ branch,
1224
+ lifecycle: false,
1225
+ source: taskSourceAtBase(cwd, held.baseSha, slug, env),
1226
+ surfaceArbiter: arbiter,
1227
+ commitMessage,
1228
+ env,
1229
+ note,
1230
+ mode: validated.mode,
1231
+ noPR: options.noPR,
1232
+ providerInstance: options.providerInstance,
1233
+ title: products.prTitle,
1234
+ body: proposeBody(products.prBody, validated.ledgerReport),
1235
+ reviewProse: products.reviewProse,
1236
+ mergeRetries: options.mergeRetries,
1237
+ mergeJitterMs: options.mergeJitterMs,
1238
+ // The landed-vs-gated report (decision 2): the agent job gated the
1239
+ // bundle tip, so a merge land that lands another tree says so.
1240
+ gatedTip: tip,
1241
+ });
1242
+
1243
+ if (land.outcome !== 'completed') {
1244
+ return {
1245
+ exitCode: land.routedToNeedsAttention ? 0 : 1,
1246
+ outcome: land.routedToNeedsAttention ? 'surfaced' : 'usage-error',
1247
+ slug,
1248
+ message: land.reason ?? `the land ended as ${land.outcome}`,
1249
+ land,
1250
+ };
1251
+ }
1252
+ const integration = land.integration!;
1253
+ if (integration.mergedToMain === true || integration.alreadyLanded === true) {
1254
+ const released = await releaseLockLeased({
1255
+ cwd,
1256
+ arbiter,
1257
+ item,
1258
+ expectedSha: held.lockSha,
1259
+ env,
1260
+ });
1261
+ note(released.message);
1262
+ return {
1263
+ exitCode: 0,
1264
+ outcome: 'landed',
1265
+ slug,
1266
+ message: `Completed '${slug}': merged to ${arbiter}/main.`,
1267
+ land,
1268
+ };
1269
+ }
1270
+ // The lock stays held until the PR merges; stamp it as kept for a propose
1271
+ // PR (task `wire-merge-questions-into-the-advance-tick`, decision 4), leased
1272
+ // on this run's `lockSha`, so the merge-question surfacer and an answered
1273
+ // `merge` can tell it from a live build. Best-effort: an unmarked kept lock
1274
+ // only keeps the branch out of the merge-question loop.
1275
+ const marked = await markLockKeptForProposePr({
1276
+ item,
1277
+ cwd,
1278
+ arbiter,
1279
+ expectedSha: held.lockSha,
1280
+ env,
1281
+ });
1282
+ if (marked.outcome !== 'transitioned') note(marked.message);
1283
+ // Say "for review" ONLY when a PR was opened (task
1284
+ // `intake-reports-the-pr-it-actually-opened`): a degraded `gh pr create`
1285
+ // pushed the branch but opened nothing, and the land already printed why.
1286
+ const notOpened = proposeRequestNotOpened(integration);
1287
+ return {
1288
+ exitCode: 0,
1289
+ outcome: 'proposed',
1290
+ slug,
1291
+ message:
1292
+ notOpened === undefined
1293
+ ? `Completed '${slug}': pushed ${branch} for review; the lock stays ` +
1294
+ 'held until the PR merges.'
1295
+ : `Completed '${slug}': pushed ${branch} but opened NO PR ` +
1296
+ `(${notOpened}); the lock stays held until the branch lands.`,
1297
+ land,
1298
+ };
1299
+ }
1300
+
1301
+ /**
1302
+ * The propose PR body: the agent's summary (from the validated record) with the
1303
+ * bundle validation's ledger report appended (user story 23). The report is
1304
+ * never cut; the summary is shortened to keep the whole under the comment limit.
1305
+ */
1306
+ function proposeBody(
1307
+ prBody: string,
1308
+ ledgerReport: string | undefined,
1309
+ ): string | undefined {
1310
+ if (ledgerReport === undefined) return prBody === '' ? undefined : prBody;
1311
+ const room = HANDOFF_LIMITS.commentChars - ledgerReport.length - 2;
1312
+ const summary = boundHandoffText(prBody, Math.max(0, room));
1313
+ return summary === '' ? ledgerReport : `${summary}\n\n${ledgerReport}`;
1314
+ }
1315
+
1316
+ async function gitHard(
1317
+ args: string[],
1318
+ cwd: string,
1319
+ env: NodeJS.ProcessEnv,
1320
+ ): Promise<string> {
1321
+ const r = await runAsync('git', args, cwd, {env});
1322
+ if (r.status !== 0) {
1323
+ throw new Error(
1324
+ `git ${args.join(' ')} failed (exit ${r.status}): ${r.stderr.trim()}`,
1325
+ );
1326
+ }
1327
+ return r.stdout;
1328
+ }