dorfl 0.14.3 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +173 -36
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +379 -186
  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 +458 -218
  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,989 @@
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
+ import { existsSync } from 'node:fs';
53
+ import { join } from 'node:path';
54
+ import { classifyTick } from './advance-classify.js';
55
+ import { readItemSignals } from './advance.js';
56
+ import { HANDOFF_LIMITS, HandoffRejected, handoffName as deriveHandoffName, } from './ci-handoff-format.js';
57
+ import { writeHandoff } from './ci-handoff.js';
58
+ import { validateApplyHandoff } from './ci-handoff-apply.js';
59
+ import { PhaseDriverError, agentTimeoutMinutesAt, arbiterLockSha, arbiterRefSha, boundHandoffText, checkLockOwnership, emitLockOutputs, fetchArbiterMain, noSmudgeEnv, pushHandoffLfs, releaseLockLeased, repoConfigAt, requireHeldLock, resolveAgentResult, runnerTempFrom, taskSourceAtBase, withBaseWorktree, } from './ci-phase-driver.js';
60
+ import { performClaim } from './claim-cas.js';
61
+ import { deadlineSurfaceReason, performDo, routeDeadlineCheckpoint, saveAgentFailure, saveAgentStop, } from './do.js';
62
+ import { checkGatePreconditions, detectLockfileOnDisk, } from './gate-readiness.js';
63
+ import { run, runAsync } from './git.js';
64
+ import { selectProvider } from './github.js';
65
+ import { identityEnv } from './identity.js';
66
+ import { leaseLockReleases, markLockKeptForProposePr } from './item-lock.js';
67
+ import { arbiterUrl, integrationLand, } from './integration-core.js';
68
+ import { proposeRequestNotOpened } from './integrator.js';
69
+ import { ledgerRead } from './ledger-read.js';
70
+ import { ledgerWrite } from './ledger-write.js';
71
+ import { commitAbortedWork, } from './needs-attention.js';
72
+ import { refWrite } from './ref-write.js';
73
+ import { createPhaseRecorder, recordingReviewProvider, runAgentPhase, } from './phase-recorder.js';
74
+ import { SlugResolutionError, resolveAdvanceArg, resolveSlug, workBranchRef, } from './slug-namespace.js';
75
+ import { workItemRel } from './work-layout.js';
76
+ const DEFAULT_ARBITER = 'origin';
77
+ /**
78
+ * Run one phase of the build path. The CLI calls this for `do <slug> --phase`
79
+ * and `advance task:<slug> --phase`.
80
+ */
81
+ export async function performBuildPhase(options) {
82
+ try {
83
+ switch (options.phase) {
84
+ case 'lock':
85
+ return await performBuildLockPhase(options);
86
+ case 'agent':
87
+ return await performBuildAgentPhase(options);
88
+ case 'apply':
89
+ return await performBuildApplyPhase(options);
90
+ }
91
+ }
92
+ catch (err) {
93
+ if (err instanceof PhaseDriverError || err instanceof SlugResolutionError) {
94
+ return { exitCode: 1, outcome: 'usage-error', message: err.message };
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // lock
101
+ // ---------------------------------------------------------------------------
102
+ /** The task slug a build phase acts on, from the (trusted) workflow argument. */
103
+ function buildSlug(options, repoPath) {
104
+ if (options.verb === 'advance') {
105
+ const resolved = resolveAdvanceArg({
106
+ arg: options.arg,
107
+ repoPath,
108
+ read: options.read ?? ledgerRead,
109
+ });
110
+ if (resolved.namespace !== 'task') {
111
+ throw new PhaseDriverError(`--phase splits only the build path so far; ${options.arg} is not a task`);
112
+ }
113
+ return resolved.slug;
114
+ }
115
+ const resolved = resolveSlug({
116
+ arg: options.arg,
117
+ repoPath,
118
+ read: options.read ?? ledgerRead,
119
+ });
120
+ if (resolved.namespace !== 'task') {
121
+ throw new PhaseDriverError(`--phase splits only the build path so far; ${options.arg} is not a task`);
122
+ }
123
+ return resolved.slug;
124
+ }
125
+ /**
126
+ * The lock phase: classify the item at the arbiter's `main`, claim it and
127
+ * publish the trusted facts. Runs no agent and no repository code. An item that
128
+ * already advanced (not claimable at `baseSha`), or whose lock another run
129
+ * holds, is refused BEFORE any write.
130
+ */
131
+ export async function performBuildLockPhase(options) {
132
+ const note = options.note ?? (() => { });
133
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
134
+ const cwd = options.cwd;
135
+ const env = identityEnv(options.identity, options.env ?? process.env);
136
+ const githubOutput = options.githubOutput ?? env.GITHUB_OUTPUT;
137
+ const publish = (facts) => {
138
+ emitLockOutputs(facts, { githubOutput, note });
139
+ return facts;
140
+ };
141
+ const baseSha = await fetchArbiterMain({ cwd, arbiter, env });
142
+ const verdict = await withBaseWorktree({ cwd, baseSha, env }, async (base) => {
143
+ const slug = buildSlug(options, base);
144
+ if (options.verb === 'advance') {
145
+ const item = `task:${slug}`;
146
+ const signals = readItemSignals({
147
+ repoPath: base,
148
+ type: 'task',
149
+ slug,
150
+ item,
151
+ });
152
+ const kind = classifyTick({ type: 'task', ...signals }).kind;
153
+ if (kind !== 'build-task') {
154
+ if (kind === 'no-op') {
155
+ return { slug, skip: `no-op for ${item} at ${baseSha}` };
156
+ }
157
+ throw new PhaseDriverError(`--phase splits only the build path so far; ${item} classifies ` +
158
+ `as ${kind} at ${baseSha}`);
159
+ }
160
+ }
161
+ const folders = options.allowBacklog === true
162
+ ? ['tasks-ready', 'tasks-backlog']
163
+ : ['tasks-ready'];
164
+ const claimable = folders.some((f) => existsSync(join(base, workItemRel(f, `${slug}.md`))));
165
+ if (!claimable) {
166
+ return {
167
+ slug,
168
+ skip: `'${slug}' is not claimable at ${arbiter}/main (${baseSha}): it ` +
169
+ 'already advanced, or was removed',
170
+ };
171
+ }
172
+ const guard = checkGatePreconditions({
173
+ freshWorktreeGate: options.freshWorktreeGate,
174
+ prepare: options.prepare,
175
+ verify: options.verify,
176
+ lockfile: detectLockfileOnDisk(base),
177
+ });
178
+ if (guard !== undefined)
179
+ throw new PhaseDriverError(guard.message);
180
+ return { slug };
181
+ });
182
+ const slug = verdict.slug;
183
+ const item = `task:${slug}`;
184
+ if (verdict.skip !== undefined) {
185
+ return {
186
+ exitCode: 0,
187
+ outcome: 'no-op',
188
+ slug,
189
+ message: verdict.skip,
190
+ lockOutputs: publish({ acquired: false, baseSha }),
191
+ };
192
+ }
193
+ if ((await arbiterLockSha({ cwd, arbiter, item, env })) !== undefined) {
194
+ const message = `'${slug}' is already locked on ${arbiter}; backing off.`;
195
+ return {
196
+ exitCode: 0,
197
+ outcome: 'lost',
198
+ slug,
199
+ message,
200
+ lockOutputs: publish({ acquired: false, rung: 'build-task', baseSha }),
201
+ };
202
+ }
203
+ const claim = await performClaim({
204
+ slug,
205
+ cwd,
206
+ arbiter,
207
+ allowBacklog: options.allowBacklog === true,
208
+ env,
209
+ note,
210
+ });
211
+ if (claim.exitCode !== 0) {
212
+ const outcome = claim.outcome === 'lost' ? 'lost' : 'usage-error';
213
+ return {
214
+ // A claim lost to another run is handled, so green ("A handled
215
+ // outcome is green", `ci-phase-driver.ts`).
216
+ exitCode: outcome === 'lost' ? 0 : claim.exitCode,
217
+ outcome,
218
+ slug,
219
+ message: claim.message,
220
+ lockOutputs: publish({ acquired: false, rung: 'build-task', baseSha }),
221
+ };
222
+ }
223
+ const lockSha = await arbiterLockSha({ cwd, arbiter, item, env });
224
+ if (lockSha === undefined) {
225
+ throw new Error(`the lock of ${item} vanished right after the claim`);
226
+ }
227
+ const continueTip = await arbiterRefSha({
228
+ cwd,
229
+ arbiter,
230
+ ref: `refs/heads/${workBranchRef('task', slug)}`,
231
+ env,
232
+ });
233
+ const facts = publish({
234
+ acquired: true,
235
+ needsAgent: true,
236
+ rung: 'build-task',
237
+ baseSha,
238
+ lockSha,
239
+ continueTip,
240
+ handoffName: deriveHandoffName(item, options.runAttempt ?? env.GITHUB_RUN_ATTEMPT ?? '1'),
241
+ agentTimeoutMinutes: agentTimeoutMinutesAt(cwd, baseSha, env),
242
+ });
243
+ return {
244
+ exitCode: 0,
245
+ outcome: 'locked',
246
+ slug,
247
+ message: `locked ${item} at ${baseSha} (lock ${lockSha})`,
248
+ lockOutputs: facts,
249
+ };
250
+ }
251
+ // ---------------------------------------------------------------------------
252
+ // agent
253
+ // ---------------------------------------------------------------------------
254
+ /** Every write seam call the build pipeline can reach (all are recorded). */
255
+ function buildRecordableCalls() {
256
+ return [
257
+ 'integrationLand.land',
258
+ ...Object.keys(ledgerWrite).map((m) => `ledgerWrite.${m}`),
259
+ ...Object.keys(refWrite).map((m) => `refWrite.${m}`),
260
+ 'reviewProvider.openRequest',
261
+ 'reviewProvider.postPRComment',
262
+ 'reviewProvider.postPRCommentOnBranch',
263
+ 'reviewProvider.closeRequestOnBranch',
264
+ ];
265
+ }
266
+ /** The `agent-failed` detail for a halt at a write the build path does not carry. */
267
+ function haltDetail(intent) {
268
+ const input = intent.input;
269
+ const reason = input !== null &&
270
+ typeof input === 'object' &&
271
+ typeof input.reason === 'string'
272
+ ? `: ${input.reason}`
273
+ : '';
274
+ return (`the build stopped at ${intent.seam}.${intent.method}${reason} ` +
275
+ '(this outcome is not carried through the CI phases)');
276
+ }
277
+ /**
278
+ * The work branch to bundle as the WIP of a non-integrate intent, or
279
+ * `undefined` when it carries nothing to recover: the checkout is not on the
280
+ * item's work branch, or the branch has no commit that `<arbiter>/main` lacks
281
+ * (the apply phase refuses a bundle without a new commit).
282
+ */
283
+ function wipBundle(cwd, ctx) {
284
+ const workBranch = workBranchRef('task', ctx.slug);
285
+ const head = run('git', ['symbolic-ref', '--quiet', 'HEAD'], cwd, {
286
+ env: ctx.env,
287
+ });
288
+ if (head.status !== 0 || head.stdout.trim() !== `refs/heads/${workBranch}`) {
289
+ return undefined;
290
+ }
291
+ const ahead = run('git', [
292
+ 'rev-list',
293
+ '--count',
294
+ `refs/heads/${workBranch}`,
295
+ `^refs/remotes/${ctx.arbiter}/main`,
296
+ `^${ctx.baseSha}`,
297
+ ], cwd, { env: ctx.env });
298
+ if (ahead.status !== 0 || Number(ahead.stdout.trim()) === 0) {
299
+ return undefined;
300
+ }
301
+ return { repo: cwd, workBranch, baseSha: ctx.baseSha };
302
+ }
303
+ /** A bounded reason (or question) for the handoff record. */
304
+ function reasonText(text) {
305
+ return boundHandoffText(text, HANDOFF_LIMITS.reasonChars);
306
+ }
307
+ /** The `needs-attention` handover of a bounce's recorded input. */
308
+ function needsAttention(ctx, input, bundle) {
309
+ return {
310
+ record: {
311
+ schema: 1,
312
+ item: ctx.item,
313
+ intent: { kind: 'needs-attention' },
314
+ products: {
315
+ reason: reasonText(input.reason),
316
+ ...(input.questions === undefined || input.questions.length === 0
317
+ ? {}
318
+ : { questions: input.questions.map(reasonText) }),
319
+ },
320
+ },
321
+ bundle,
322
+ };
323
+ }
324
+ /**
325
+ * Map the build's FIRST WRITE (the recorded halt) to the handoff (task
326
+ * `ci-split-build-path-non-integrate-intents`). The seam call names the write;
327
+ * the pipeline's {@link BuildBoundary} annotation names the outcome when several
328
+ * share one seam call. A bounce's LOCAL half (the wip commit, which the seam
329
+ * would have made before pushing) runs here, so the wip travels in the bundle.
330
+ */
331
+ function handOverHalt(intent, ctx) {
332
+ const call = `${intent.seam}.${intent.method}`;
333
+ const boundary = intent.annotation;
334
+ if (call === 'integrationLand.land') {
335
+ const land = intent.input;
336
+ if (land.branch !== workBranchRef('task', ctx.slug)) {
337
+ throw new Error(`the land half names the branch ${land.branch}, not ` +
338
+ workBranchRef('task', ctx.slug));
339
+ }
340
+ const limit = HANDOFF_LIMITS.commentChars;
341
+ return {
342
+ record: {
343
+ schema: 1,
344
+ item: ctx.item,
345
+ intent: { kind: 'integrate' },
346
+ products: {
347
+ prTitle: land.title,
348
+ prBody: boundHandoffText(land.body ?? '', limit),
349
+ ...(land.reviewProse === undefined
350
+ ? {}
351
+ : { reviewProse: boundHandoffText(land.reviewProse, limit) }),
352
+ },
353
+ },
354
+ bundle: { repo: land.cwd, workBranch: land.branch, baseSha: ctx.baseSha },
355
+ };
356
+ }
357
+ // The deadline checkpoint: its marker commit is already made (locally) when
358
+ // the branch save is recorded.
359
+ if (call === 'refWrite.saveWorkBranch' &&
360
+ boundary?.kind === 'deadline-checkpoint') {
361
+ const input = intent.input;
362
+ commitAbortedWork({ cwd: input.cwd, slug: ctx.slug, env: ctx.env });
363
+ const bundle = wipBundle(input.cwd, ctx);
364
+ if (!boundary.predecessorGone) {
365
+ const reason = deadlineSurfaceReason({
366
+ slug: ctx.slug,
367
+ kind: 'unreaped',
368
+ detail: boundary.reapDetail,
369
+ });
370
+ return needsAttention(ctx, { reason }, bundle);
371
+ }
372
+ if (bundle === undefined) {
373
+ // Nothing on the branch: no progress, so today's route surfaces.
374
+ const reason = deadlineSurfaceReason({
375
+ slug: ctx.slug,
376
+ kind: 'no-progress',
377
+ });
378
+ return needsAttention(ctx, { reason }, undefined);
379
+ }
380
+ return {
381
+ record: {
382
+ schema: 1,
383
+ item: ctx.item,
384
+ intent: { kind: 'deadline-checkpoint' },
385
+ products: {},
386
+ },
387
+ bundle,
388
+ };
389
+ }
390
+ // The empty-diff STOP surfaces a question on main without a branch save.
391
+ if (boundary?.kind === 'stop' && boundary.stopKind === 'empty-diff') {
392
+ return {
393
+ record: {
394
+ schema: 1,
395
+ item: ctx.item,
396
+ intent: { kind: 'stop' },
397
+ products: {
398
+ reason: reasonText(boundary.reason),
399
+ stopKind: 'empty-diff',
400
+ },
401
+ },
402
+ };
403
+ }
404
+ if (call === 'ledgerWrite.applyNeedsAttentionTransition') {
405
+ const input = intent.input;
406
+ commitAbortedWork({ cwd: input.cwd, slug: ctx.slug, env: ctx.env });
407
+ const bundle = input.pushBranch === false ? undefined : wipBundle(input.cwd, ctx);
408
+ if (boundary?.kind === 'agent-failed') {
409
+ return { record: agentFailed(ctx.item, boundary.failureDetail), bundle };
410
+ }
411
+ if (boundary?.kind === 'stop') {
412
+ return {
413
+ record: {
414
+ schema: 1,
415
+ item: ctx.item,
416
+ intent: { kind: 'stop' },
417
+ products: {
418
+ reason: reasonText(boundary.reason),
419
+ stopKind: boundary.stopKind,
420
+ },
421
+ },
422
+ bundle,
423
+ };
424
+ }
425
+ return needsAttention(ctx, input, bundle);
426
+ }
427
+ // A tree-less surface (the continue rebase conflicted at onboarding): the
428
+ // kept branch is untouched on the arbiter, so nothing is bundled.
429
+ if (call === 'ledgerWrite.applyTreelessNeedsAttentionTransition') {
430
+ const input = intent.input;
431
+ return needsAttention(ctx, input, undefined);
432
+ }
433
+ return { record: agentFailed(ctx.item, haltDetail(intent)) };
434
+ }
435
+ /**
436
+ * The agent phase: after a read-only lock-ownership check, run the build
437
+ * pipeline (`performDo`, without its claim) under the phase recorder, halt at
438
+ * the first write, and write the handoff. Holds a read-only token: it never
439
+ * writes to the arbiter.
440
+ */
441
+ export async function performBuildAgentPhase(options) {
442
+ const note = options.note ?? (() => { });
443
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
444
+ const cwd = options.cwd;
445
+ const env = options.env ?? process.env;
446
+ const held = requireHeldLock({
447
+ lockOutputs: options.lockOutputs,
448
+ env,
449
+ rung: 'build-task',
450
+ });
451
+ if (options.handoffDir === undefined) {
452
+ throw new PhaseDriverError('the agent phase needs a handoff directory');
453
+ }
454
+ const slug = buildSlug(options, options.repoPath ?? cwd);
455
+ const item = `task:${slug}`;
456
+ const ownership = await checkLockOwnership({
457
+ cwd,
458
+ arbiter,
459
+ item,
460
+ lockSha: held.lockSha,
461
+ env,
462
+ });
463
+ if (!ownership.owned) {
464
+ return {
465
+ exitCode: 0,
466
+ outcome: 'stale-lock',
467
+ slug,
468
+ message: ownership.message,
469
+ };
470
+ }
471
+ const recorder = createPhaseRecorder({ record: buildRecordableCalls() });
472
+ const ctx = { item, slug, arbiter, baseSha: held.baseSha, env };
473
+ let handover;
474
+ try {
475
+ // EVERY review provider the pipeline can reach records (never only an
476
+ // injected one): without an injected instance, resolve the one the
477
+ // integration core would select from the arbiter URL, and wrap it.
478
+ const provider = options.providerInstance ??
479
+ selectProvider({ arbiterUrl: await arbiterUrl(cwd, arbiter, env) });
480
+ const outcome = await runAgentPhase(recorder, () => performDo({
481
+ ...options,
482
+ phase: 'agent',
483
+ providerInstance: recordingReviewProvider(provider, recorder),
484
+ }));
485
+ handover = outcome.halted
486
+ ? handOverHalt(outcome.intent, ctx)
487
+ : {
488
+ record: agentFailed(item, `the build ended without reaching a write: ${outcome.result.message}`),
489
+ };
490
+ }
491
+ catch (err) {
492
+ const message = err instanceof Error ? err.message : String(err);
493
+ handover = { record: agentFailed(item, `the build failed: ${message}`) };
494
+ }
495
+ const written = writeHandoff({
496
+ dir: options.handoffDir,
497
+ rung: 'build-task',
498
+ record: handover.record,
499
+ bundle: handover.bundle,
500
+ });
501
+ if (written.lfsMissing.length > 0) {
502
+ note(`the local LFS store lacks ${written.lfsMissing.length} object(s) a new ` +
503
+ `commit points at (${written.lfsMissing.slice(0, 5).join(', ')}); the ` +
504
+ 'apply job will reject the handoff');
505
+ }
506
+ const kind = handover.record.intent.kind;
507
+ const message = `handed over ${kind} for ${item}`;
508
+ return {
509
+ exitCode: 0,
510
+ outcome: 'handed-over',
511
+ slug,
512
+ message,
513
+ intent: kind,
514
+ };
515
+ }
516
+ function agentFailed(item, detail) {
517
+ return {
518
+ schema: 1,
519
+ item,
520
+ intent: { kind: 'agent-failed' },
521
+ products: {
522
+ failureDetail: boundHandoffText(detail, HANDOFF_LIMITS.reasonChars),
523
+ },
524
+ };
525
+ }
526
+ // ---------------------------------------------------------------------------
527
+ // apply
528
+ // ---------------------------------------------------------------------------
529
+ /** Surface the item to needs-attention (tree-less) with `reason`. */
530
+ async function surface(params) {
531
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
532
+ cwd: params.cwd,
533
+ slug: params.slug,
534
+ reason: params.reason,
535
+ arbiter: params.arbiter,
536
+ env: params.env,
537
+ note: params.note,
538
+ });
539
+ const where = surfaced.moved
540
+ ? 'surfaced it to needs-attention'
541
+ : `could not surface it (${surfaced.reasonNotMoved ?? 'unknown'})`;
542
+ // A clean surface is green ("A handled outcome is green",
543
+ // `ci-phase-driver.ts`); only a surface that did not land stays red.
544
+ return {
545
+ exitCode: surfaced.moved ? 0 : 1,
546
+ outcome: surfaced.moved ? params.outcome : 'surface-unmoved',
547
+ slug: params.slug,
548
+ message: `${params.reason}; ${where}.`,
549
+ };
550
+ }
551
+ /**
552
+ * The apply phase: check the lock is still this run's, validate the handoff as
553
+ * hostile, then resume the build at the intent's write half. Runs no agent and
554
+ * no repository code; every write goes through dorfl's seams.
555
+ */
556
+ export async function performBuildApplyPhase(options) {
557
+ const note = options.note ?? (() => { });
558
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
559
+ const cwd = options.cwd;
560
+ const env = noSmudgeEnv(identityEnv(options.identity, options.env ?? process.env));
561
+ const held = requireHeldLock({
562
+ lockOutputs: options.lockOutputs,
563
+ env,
564
+ rung: 'build-task',
565
+ });
566
+ if (options.agentResult === undefined) {
567
+ throw new PhaseDriverError('the apply phase needs the agent job result (--agent-result, ' +
568
+ 'needs.agent.result): only success reads the handoff');
569
+ }
570
+ const agentResult = options.agentResult;
571
+ const slug = buildSlug(options, options.repoPath ?? cwd);
572
+ const item = `task:${slug}`;
573
+ // Lock ownership BEFORE the first write (spec §8): a lock that was released,
574
+ // reaped or re-taken since the lock job ran means this run writes nothing.
575
+ // This is also what makes a "Re-run failed jobs" of a finished run safe: it
576
+ // replays the old lock outputs and artifact, but the first apply already
577
+ // released or surfaced the item, so the lock is gone and nothing is written:
578
+ // a handled outcome, so the re-run is green.
579
+ const ownership = await checkLockOwnership({
580
+ cwd,
581
+ arbiter,
582
+ item,
583
+ lockSha: held.lockSha,
584
+ env,
585
+ });
586
+ if (!ownership.owned) {
587
+ return {
588
+ exitCode: 0,
589
+ outcome: 'stale-lock',
590
+ slug,
591
+ message: ownership.message,
592
+ };
593
+ }
594
+ // Every lock release from here on (the land's, the surface's, the
595
+ // return-to-backlog's) is leased on the sha this run owns: none of them may
596
+ // delete a lock another run took since the ownership check.
597
+ const restoreLease = leaseLockReleases(item, held.lockSha);
598
+ try {
599
+ const ctx = { options, held, slug, item, arbiter, cwd, env };
600
+ // The agent job's result (decision 5) BEFORE any artifact is read: only
601
+ // `success` reads it; the build path has no agent-less rung, so a
602
+ // `skipped` agent job is never the deterministic case here.
603
+ const decision = await resolveAgentResult({
604
+ result: agentResult,
605
+ held,
606
+ agentTimeoutMinutes: options.agentTimeoutMinutes,
607
+ api: options.actionsApi,
608
+ env,
609
+ });
610
+ switch (decision.action) {
611
+ case 'read-handoff':
612
+ return await applyOwned(ctx);
613
+ case 'deterministic':
614
+ return await surfaceAgentResult(ctx, 'the agent job was skipped (needsAgent: false), but the build ' +
615
+ 'path has no rung that runs without an agent; the handoff was not read');
616
+ case 'surface':
617
+ return await surfaceAgentResult(ctx, decision.reason);
618
+ case 'release':
619
+ return await releaseAfterCancel(ctx, decision.reason);
620
+ }
621
+ }
622
+ finally {
623
+ restoreLease();
624
+ }
625
+ }
626
+ /**
627
+ * The apply phase once the lock is known to be this run's: validate the
628
+ * handoff, publish a continued branch with its lease, then resume at the
629
+ * intent's write half.
630
+ */
631
+ async function applyOwned(ctx) {
632
+ const { options, held, slug, item, arbiter, cwd, env } = ctx;
633
+ const note = options.note ?? (() => { });
634
+ let validated;
635
+ try {
636
+ if (options.handoffDir === undefined) {
637
+ throw new HandoffRejected('layout', 'no handoff directory was given');
638
+ }
639
+ validated = validateApplyHandoff({
640
+ dir: options.handoffDir,
641
+ runnerTemp: runnerTempFrom(options.runnerTemp, env),
642
+ repo: cwd,
643
+ trust: {
644
+ item,
645
+ rung: 'build-task',
646
+ baseSha: held.baseSha,
647
+ arbiter,
648
+ integrationMode: options.integration ?? 'propose',
649
+ },
650
+ env,
651
+ });
652
+ }
653
+ catch (err) {
654
+ if (!(err instanceof HandoffRejected))
655
+ throw err;
656
+ note(err.message);
657
+ return surface({
658
+ cwd,
659
+ arbiter,
660
+ slug,
661
+ reason: err.message,
662
+ env,
663
+ note,
664
+ outcome: 'rejected',
665
+ });
666
+ }
667
+ const record = validated.handoff.record;
668
+ const bundle = validated.bundle;
669
+ // The LFS objects go FIRST (decision 6), before any ref of any intent (the
670
+ // continue push, the land's pushes, a WIP branch save), so a ref never lands
671
+ // pointing at a missing object.
672
+ const lfs = await pushHandoffLfs({
673
+ cwd,
674
+ arbiter,
675
+ objects: validated.lfsObjects,
676
+ env,
677
+ note,
678
+ });
679
+ if (!lfs.ok) {
680
+ return surface({
681
+ cwd,
682
+ arbiter,
683
+ slug,
684
+ reason: lfs.reason,
685
+ env,
686
+ note,
687
+ outcome: lfs.rejected ? 'rejected' : 'surfaced',
688
+ });
689
+ }
690
+ if (bundle !== undefined) {
691
+ await gitHard(['checkout', '--quiet', '-B', bundle.workBranch, bundle.tip], cwd, env);
692
+ // The CONTINUE push (decision 7): the agent job rebased the kept branch
693
+ // locally; publish it with a lease on the tip the LOCK job observed, before
694
+ // any other write. A branch that moved since then refuses the push, and
695
+ // this run writes nothing at all.
696
+ if (held.continueTip !== undefined) {
697
+ const pushed = await refWrite.pushLeasedWorkBranch({
698
+ arbiter,
699
+ branch: bundle.workBranch,
700
+ commit: bundle.tip,
701
+ expectedTip: held.continueTip,
702
+ cwd,
703
+ env,
704
+ });
705
+ if (pushed.status !== 0) {
706
+ const message = `${bundle.workBranch} on ${arbiter} moved since the lock job saw it ` +
707
+ `at ${held.continueTip}, so the leased push of the continued branch ` +
708
+ `was refused (${pushed.stderr.trim()}). Nothing was written and the ` +
709
+ `lock of ${item} is still held: inspect the branch, then \`dorfl ` +
710
+ `requeue ${slug}\` to retry the item.`;
711
+ return { exitCode: 1, outcome: 'stale-lease', slug, message };
712
+ }
713
+ }
714
+ }
715
+ const pushBranch = bundle !== undefined;
716
+ const branch = workBranchRef('task', slug);
717
+ switch (record.intent.kind) {
718
+ case 'integrate':
719
+ return applyIntegrate(ctx, validated);
720
+ case 'needs-attention': {
721
+ const { reason, questions } = record.products;
722
+ const routed = pushBranch
723
+ ? await ledgerWrite.applyNeedsAttentionTransition({
724
+ cwd,
725
+ slug,
726
+ reason,
727
+ questions,
728
+ arbiter,
729
+ env,
730
+ note,
731
+ })
732
+ : await ledgerWrite.applyTreelessNeedsAttentionTransition({
733
+ cwd,
734
+ slug,
735
+ reason,
736
+ questions,
737
+ arbiter,
738
+ env,
739
+ note,
740
+ });
741
+ const message = routed.moved
742
+ ? `Surfaced '${slug}' to needs-attention: ${reason}`
743
+ : `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
744
+ return {
745
+ exitCode: routed.moved ? 0 : 1,
746
+ outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
747
+ slug,
748
+ message,
749
+ };
750
+ }
751
+ case 'stop': {
752
+ const stop = record.products;
753
+ return fromDoResult(await saveAgentStop({
754
+ slug,
755
+ branch,
756
+ cwd,
757
+ arbiter,
758
+ reason: stop.reason,
759
+ kind: stop.stopKind,
760
+ pushBranch,
761
+ env,
762
+ note,
763
+ }));
764
+ }
765
+ case 'agent-failed':
766
+ return fromDoResult(await saveAgentFailure({
767
+ slug,
768
+ branch,
769
+ cwd,
770
+ arbiter,
771
+ detail: record.products.failureDetail,
772
+ pushBranch,
773
+ env,
774
+ note,
775
+ }));
776
+ case 'deadline-checkpoint':
777
+ return fromDoResult(await routeDeadlineCheckpoint({
778
+ slug,
779
+ branch,
780
+ cwd,
781
+ arbiter,
782
+ maxAutoCheckpoints: maxAutoCheckpointsAt(cwd, held.baseSha, options.maxAutoCheckpoints, env),
783
+ env,
784
+ note,
785
+ }));
786
+ default:
787
+ return surface({
788
+ cwd,
789
+ arbiter,
790
+ slug,
791
+ reason: `the agent job handed over ${record.intent.kind}, which the build ` +
792
+ 'path does not carry',
793
+ env,
794
+ note,
795
+ outcome: 'surfaced',
796
+ });
797
+ }
798
+ }
799
+ /**
800
+ * Surface the item to needs-attention because the agent job did not succeed
801
+ * (decision 5): tree-less (nothing from the agent job is pushed: its artifact is
802
+ * not read), the lock released leased on `lockSha`. A kept work branch stays as
803
+ * it is on the arbiter.
804
+ */
805
+ async function surfaceAgentResult(ctx, reason) {
806
+ const { options, slug, arbiter, cwd, env } = ctx;
807
+ const note = options.note ?? (() => { });
808
+ const routed = await ledgerWrite.applyTreelessNeedsAttentionTransition({
809
+ cwd,
810
+ slug,
811
+ reason,
812
+ arbiter,
813
+ env,
814
+ note,
815
+ });
816
+ const message = routed.moved
817
+ ? `Surfaced '${slug}' to needs-attention: ${reason}`
818
+ : `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
819
+ return {
820
+ exitCode: routed.moved ? 0 : 1,
821
+ outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
822
+ slug,
823
+ message,
824
+ };
825
+ }
826
+ /**
827
+ * The agent job was cancelled before its timeout (decision 5): only release the
828
+ * lock, leased on `lockSha`, so the next run picks the item up again.
829
+ */
830
+ async function releaseAfterCancel(ctx, reason) {
831
+ const { options, held, slug, item, arbiter, cwd, env } = ctx;
832
+ const note = options.note ?? (() => { });
833
+ const released = await releaseLockLeased({
834
+ cwd,
835
+ arbiter,
836
+ item,
837
+ expectedSha: held.lockSha,
838
+ env,
839
+ });
840
+ note(released.message);
841
+ const message = released.released
842
+ ? `Released '${slug}': ${reason}.`
843
+ : `Could not release '${slug}' (${released.message}): ${reason}.`;
844
+ return {
845
+ exitCode: released.released ? 0 : 1,
846
+ outcome: released.released ? 'released' : 'release-refused',
847
+ slug,
848
+ message,
849
+ };
850
+ }
851
+ /** Map a reused `do` write half's result to the apply phase's. */
852
+ function fromDoResult(r) {
853
+ const outcome = r.outcome === 'deadline-auto-continued'
854
+ ? 'auto-continued'
855
+ : r.routedToNeedsAttention === true
856
+ ? 'surfaced'
857
+ : 'surface-unmoved';
858
+ return { exitCode: r.exitCode, outcome, slug: r.slug, message: r.message };
859
+ }
860
+ /**
861
+ * The `maxAutoCheckpoints` ceiling from the repository config AT `baseSha`
862
+ * (trusted, the arbiter's `main` the lock job classified at), never from the
863
+ * bundle: a work branch could raise its own ceiling otherwise. Falls back to
864
+ * the resolved option, then to the default, when the file does not set a
865
+ * valid value.
866
+ */
867
+ function maxAutoCheckpointsAt(cwd, baseSha, fallback, env) {
868
+ const cap = repoConfigAt(cwd, baseSha, env)?.maxAutoCheckpoints;
869
+ if (typeof cap === 'number' && Number.isInteger(cap) && cap >= 1) {
870
+ return cap;
871
+ }
872
+ return fallback ?? 5;
873
+ }
874
+ /** The `integrate` write half: the land loop, the review comment, the release. */
875
+ async function applyIntegrate(ctx, validated) {
876
+ const { options, held, slug, item, arbiter, cwd, env } = ctx;
877
+ const note = options.note ?? (() => { });
878
+ const record = validated.handoff.record;
879
+ if (record.intent.kind !== 'integrate' || validated.bundle === undefined) {
880
+ throw new Error('applyIntegrate needs an integrate handoff with a bundle');
881
+ }
882
+ const products = record.products;
883
+ const branch = validated.bundle.workBranch;
884
+ const tip = validated.bundle.tip;
885
+ if (validated.forcedPropose) {
886
+ note(`Untrusted-origin task '${slug}': forcing the BUILD transition to ` +
887
+ 'propose (recomputed from the task at the base).');
888
+ }
889
+ const commitMessage = (await gitHard(['log', '-1', '--format=%s', tip], cwd, env)).trim();
890
+ const land = await integrationLand.land({
891
+ cwd,
892
+ arbiter,
893
+ slug,
894
+ branch,
895
+ lifecycle: false,
896
+ source: taskSourceAtBase(cwd, held.baseSha, slug, env),
897
+ surfaceArbiter: arbiter,
898
+ commitMessage,
899
+ env,
900
+ note,
901
+ mode: validated.mode,
902
+ noPR: options.noPR,
903
+ providerInstance: options.providerInstance,
904
+ title: products.prTitle,
905
+ body: proposeBody(products.prBody, validated.ledgerReport),
906
+ reviewProse: products.reviewProse,
907
+ mergeRetries: options.mergeRetries,
908
+ mergeJitterMs: options.mergeJitterMs,
909
+ // The landed-vs-gated report (decision 2): the agent job gated the
910
+ // bundle tip, so a merge land that lands another tree says so.
911
+ gatedTip: tip,
912
+ });
913
+ if (land.outcome !== 'completed') {
914
+ return {
915
+ exitCode: land.routedToNeedsAttention ? 0 : 1,
916
+ outcome: land.routedToNeedsAttention ? 'surfaced' : 'usage-error',
917
+ slug,
918
+ message: land.reason ?? `the land ended as ${land.outcome}`,
919
+ land,
920
+ };
921
+ }
922
+ const integration = land.integration;
923
+ if (integration.mergedToMain === true || integration.alreadyLanded === true) {
924
+ const released = await releaseLockLeased({
925
+ cwd,
926
+ arbiter,
927
+ item,
928
+ expectedSha: held.lockSha,
929
+ env,
930
+ });
931
+ note(released.message);
932
+ return {
933
+ exitCode: 0,
934
+ outcome: 'landed',
935
+ slug,
936
+ message: `Completed '${slug}': merged to ${arbiter}/main.`,
937
+ land,
938
+ };
939
+ }
940
+ // The lock stays held until the PR merges; stamp it as kept for a propose
941
+ // PR (task `wire-merge-questions-into-the-advance-tick`, decision 4), leased
942
+ // on this run's `lockSha`, so the merge-question surfacer and an answered
943
+ // `merge` can tell it from a live build. Best-effort: an unmarked kept lock
944
+ // only keeps the branch out of the merge-question loop.
945
+ const marked = await markLockKeptForProposePr({
946
+ item,
947
+ cwd,
948
+ arbiter,
949
+ expectedSha: held.lockSha,
950
+ env,
951
+ });
952
+ if (marked.outcome !== 'transitioned')
953
+ note(marked.message);
954
+ // Say "for review" ONLY when a PR was opened (task
955
+ // `intake-reports-the-pr-it-actually-opened`): a degraded `gh pr create`
956
+ // pushed the branch but opened nothing, and the land already printed why.
957
+ const notOpened = proposeRequestNotOpened(integration);
958
+ return {
959
+ exitCode: 0,
960
+ outcome: 'proposed',
961
+ slug,
962
+ message: notOpened === undefined
963
+ ? `Completed '${slug}': pushed ${branch} for review; the lock stays ` +
964
+ 'held until the PR merges.'
965
+ : `Completed '${slug}': pushed ${branch} but opened NO PR ` +
966
+ `(${notOpened}); the lock stays held until the branch lands.`,
967
+ land,
968
+ };
969
+ }
970
+ /**
971
+ * The propose PR body: the agent's summary (from the validated record) with the
972
+ * bundle validation's ledger report appended (user story 23). The report is
973
+ * never cut; the summary is shortened to keep the whole under the comment limit.
974
+ */
975
+ function proposeBody(prBody, ledgerReport) {
976
+ if (ledgerReport === undefined)
977
+ return prBody === '' ? undefined : prBody;
978
+ const room = HANDOFF_LIMITS.commentChars - ledgerReport.length - 2;
979
+ const summary = boundHandoffText(prBody, Math.max(0, room));
980
+ return summary === '' ? ledgerReport : `${summary}\n\n${ledgerReport}`;
981
+ }
982
+ async function gitHard(args, cwd, env) {
983
+ const r = await runAsync('git', args, cwd, { env });
984
+ if (r.status !== 0) {
985
+ throw new Error(`git ${args.join(' ')} failed (exit ${r.status}): ${r.stderr.trim()}`);
986
+ }
987
+ return r.stdout;
988
+ }
989
+ //# sourceMappingURL=ci-phase-build.js.map