dorfl 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +309 -489
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +46 -14
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +123 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -0
  162. package/dist/install-ci-action-pins.js +190 -0
  163. package/dist/install-ci-action-pins.js.map +1 -0
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +155 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +304 -200
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -292
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +8 -4
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +375 -538
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +52 -15
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +209 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +388 -233
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -327
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +8 -4
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
package/src/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {Command, Option} from 'commander';
2
+ import {Command, InvalidArgumentError, Option} from 'commander';
3
3
  import type {Command as Commander} from 'commander';
4
4
  import {createInterface} from 'node:readline';
5
5
  import {fileURLToPath} from 'node:url';
@@ -66,7 +66,11 @@ import {
66
66
  type IsolatedAdvanceContext,
67
67
  } from './advance-isolated.js';
68
68
  import {advanceRegistrySetRunTick} from './advance-loop-driver.js';
69
- import {performIntake, resolveIntakeIntegrationModes} from './intake.js';
69
+ import {
70
+ performIntake,
71
+ resolveIntakeIntegrationModes,
72
+ type PerformIntakeOptions,
73
+ } from './intake.js';
70
74
  import {workFolderPrefix} from './work-layout.js';
71
75
  import {
72
76
  performDoAuto,
@@ -90,6 +94,7 @@ import {
90
94
  encodeRepoKey,
91
95
  } from './repo-mirror.js';
92
96
  import {identityEnv, type Identity} from './identity.js';
97
+ import {runMergeQuestionTick} from './merge-question-tick.js';
93
98
  import {
94
99
  harnessFlagOverrides,
95
100
  doFlagOverrides,
@@ -121,7 +126,10 @@ import {
121
126
  type DataLeak,
122
127
  } from './prd-to-spec.js';
123
128
  import {resyncProtocol, type ResyncResult} from './resync-protocol.js';
124
- import {sweepRemoteMergedBranches} from './reap-branches.js';
129
+ import {
130
+ sweepRemoteMergedBranches,
131
+ sweepTerminalItemLocks,
132
+ } from './reap-branches.js';
125
133
  import {sweepOrphanSidecars} from './orphan-sidecar.js';
126
134
  import {sweepLedgerDuplicates, formatLedgerSweep} from './ledger-lint.js';
127
135
  import {status, formatStatus} from './status.js';
@@ -161,6 +169,19 @@ import {GitHubCIContext} from './install-ci-github.js';
161
169
  import {loadCapabilityRegistry} from './install-ci-core.js';
162
170
  import {performCloseMergedIssues} from './close-job.js';
163
171
  import {installSkills, type InstallSkillsResult} from './install-skills.js';
172
+ import {parsePhase, PhaseUsageError, type Phase} from './phase.js';
173
+ import {activateProcessPhase} from './phase-recorder.js';
174
+ import {performBuildPhase, type BuildPhaseVerb} from './ci-phase-build.js';
175
+ import {performIntakePhase} from './ci-phase-intake.js';
176
+ import {performTaskingPhase} from './ci-phase-tasking.js';
177
+ import {advancePhasePath, performTreelessPhase} from './ci-phase-treeless.js';
178
+ import {
179
+ AgentResultUsageError,
180
+ parseAgentJobResult,
181
+ parseAgentTimeoutMinutes,
182
+ type AgentJobResult,
183
+ } from './ci-agent-result.js';
184
+ import {singleJobWarning} from './single-job-warning.js';
164
185
 
165
186
  interface ScanFlags {
166
187
  config?: string;
@@ -168,10 +189,61 @@ interface ScanFlags {
168
189
  json?: boolean;
169
190
  here?: boolean;
170
191
  reconcileLocks?: boolean;
192
+ allArbiters?: boolean;
171
193
  arbiterRemote?: string;
172
194
  arbiter?: string;
173
195
  }
174
196
 
197
+ /**
198
+ * Resolve the ARBITER SCOPE of a `--reconcile-locks` write for `status` or `scan`
199
+ * (tasks `reconcile-locks-stays-within-the-current-arbiter` and
200
+ * `status-no-arbiter-is-honoured`). Like `gc`, the drain is arbiter-scoped by
201
+ * DEFAULT: the hub key of the arbiter resolved from the cwd (its `--arbiter`
202
+ * remote, else the configured `defaultArbiter`), so a drain run in repo A never
203
+ * releases repo B's locks. `--all-arbiters` is the explicit, loud global opt-in
204
+ * and returns `undefined` (= every mirror). With neither a resolvable cwd arbiter
205
+ * nor the flag this REFUSES (exit 1) rather than silently widening the write.
206
+ * Returns `undefined` too when `--reconcile-locks` is absent (the key is then
207
+ * ignored by the report builders).
208
+ */
209
+ function resolveReconcileArbiterKey(
210
+ verb: 'status' | 'scan',
211
+ flags: {
212
+ reconcileLocks?: boolean;
213
+ allArbiters?: boolean;
214
+ arbiter?: string | false;
215
+ },
216
+ defaultArbiter: string,
217
+ ): string | undefined {
218
+ if (flags.reconcileLocks !== true) {
219
+ return undefined;
220
+ }
221
+ if (flags.allArbiters === true) {
222
+ console.error(
223
+ '>> --all-arbiters: releasing stale per-item locks GLOBALLY across ' +
224
+ 'ALL registered arbiters (NOT scoped to the current repo).',
225
+ );
226
+ return undefined;
227
+ }
228
+ // `typeof` guard: on `status`, `--no-arbiter` is commander's negation of
229
+ // `--arbiter`, so it arrives as `arbiter: false`, not a remote name.
230
+ const remote =
231
+ typeof flags.arbiter === 'string' ? flags.arbiter : defaultArbiter;
232
+ const key = resolveArbiterKeyFromCwd(process.cwd(), remote, process.env);
233
+ if (key === undefined) {
234
+ console.error(
235
+ `refusing: \`${verb} --reconcile-locks\` is ARBITER-SCOPED and could ` +
236
+ `not resolve an arbiter from the current directory (no '${remote}' ` +
237
+ 'git remote, or not inside a repo). It will NOT fall back to ' +
238
+ 'releasing locks on every arbiter. Run it from a repo with an ' +
239
+ 'arbiter remote, pass --arbiter <remote>, or --all-arbiters to ' +
240
+ 'reconcile every registered arbiter.',
241
+ );
242
+ process.exit(1);
243
+ }
244
+ return key;
245
+ }
246
+
175
247
  /**
176
248
  * Whether `--auto-build` / `--no-auto-build` was explicitly passed on the command
177
249
  * line. Commander gives a negatable boolean option a default of `true`, so we
@@ -750,6 +822,16 @@ function explicitSpecsLandInFromFlag(
750
822
 
751
823
  interface DoFlags {
752
824
  config?: string;
825
+ /** Hidden, CI-only `--phase lock|agent|apply` (see {@link phaseOption}). */
826
+ phase?: Phase;
827
+ /** Hidden, CI-only: the directory the agent phase writes the handoff to. */
828
+ handoffOut?: string;
829
+ /** Hidden, CI-only: the directory the apply phase reads the handoff from. */
830
+ handoffIn?: string;
831
+ /** Hidden, CI-only: `needs.agent.result`, for the apply phase. */
832
+ agentResult?: AgentJobResult;
833
+ /** Hidden, CI-only: the lock job's `agentTimeoutMinutes`, for the apply phase. */
834
+ agentTimeoutMinutes?: number;
753
835
  arbiter?: string;
754
836
  remote?: string;
755
837
  /** `--isolated`: build in a job worktree off THIS repo's arbiter (no checkout takeover). Since `make-isolated-default-build-mode` this is the DEFAULT; the flag remains accepted as a redundant explicit opt-IN alias (D3). */
@@ -764,8 +846,6 @@ interface DoFlags {
764
846
  observationTriage?: string;
765
847
  /** `--surface-blockers` / `--no-surface-blockers`: the declared-blocked-work gate (`advance`). */
766
848
  surfaceBlockers?: boolean;
767
- /** `--merge-questions <off|ask|auto>`: the merge-question SURFACER gate (`advance`). SEPARATE from `--observation-triage` with a HIGHER default. */
768
- mergeQuestions?: string;
769
849
  /** `--strict-merge-approval` / `--no-strict-merge-approval`: the OPT-IN strictness layered on the OQ6 stale-approval default (`advance`). Default OFF — ON re-surfaces the merge-question on a merge-base change instead of auto-landing on a green re-verify. */
770
850
  strictMergeApproval?: boolean;
771
851
  merge?: boolean;
@@ -804,6 +884,16 @@ interface DoFlags {
804
884
 
805
885
  interface IntakeFlags {
806
886
  config?: string;
887
+ /** Hidden, CI-only `--phase lock|agent|apply` (see {@link phaseOption}). */
888
+ phase?: Phase;
889
+ /** Hidden, CI-only: the directory the agent phase writes the handoff to. */
890
+ handoffOut?: string;
891
+ /** Hidden, CI-only: the directory the apply phase reads the handoff from. */
892
+ handoffIn?: string;
893
+ /** Hidden, CI-only: `needs.agent.result`, for the apply phase. */
894
+ agentResult?: AgentJobResult;
895
+ /** Hidden, CI-only: accepted for parity with `do`/`advance`; intake does not use it. */
896
+ agentTimeoutMinutes?: number;
807
897
  arbiter?: string;
808
898
  merge?: boolean;
809
899
  propose?: boolean;
@@ -1027,10 +1117,15 @@ interface StatusFlags {
1027
1117
  config?: string;
1028
1118
  workspace?: string;
1029
1119
  arbiterRemote?: string;
1030
- arbiter?: string;
1031
- noArbiter?: boolean;
1120
+ /**
1121
+ * `--arbiter <remote>` gives the remote name; `--no-arbiter` is commander's
1122
+ * NEGATION of the same option and arrives as `false` (there is no separate
1123
+ * `noArbiter` key). `undefined` when neither is passed.
1124
+ */
1125
+ arbiter?: string | false;
1032
1126
  here?: boolean;
1033
1127
  reconcileLocks?: boolean;
1128
+ allArbiters?: boolean;
1034
1129
  json?: boolean;
1035
1130
  }
1036
1131
 
@@ -1307,6 +1402,254 @@ function resolvePackageVersion(): string {
1307
1402
  }
1308
1403
  }
1309
1404
 
1405
+ /**
1406
+ * The hidden, CI-only `--phase lock|agent|apply` option shared by `intake`,
1407
+ * `advance` and `do` (spec `ci-agent-job-without-write-token`, see `phase.ts`).
1408
+ * Validated at PARSE time ({@link parsePhase}): an unknown value, or any value
1409
+ * outside GitHub Actions, is a commander usage error before the action runs.
1410
+ * Absent ⇒ today's single process.
1411
+ */
1412
+ function phaseOption(): Option {
1413
+ return new Option(
1414
+ '--phase <phase>',
1415
+ '(CI-only) run as one job of the split CI item workflow: lock, agent or apply. Requires GITHUB_ACTIONS=true.',
1416
+ )
1417
+ .hideHelp()
1418
+ .argParser((raw: string): Phase => {
1419
+ try {
1420
+ return parsePhase(raw);
1421
+ } catch (err) {
1422
+ if (err instanceof PhaseUsageError) {
1423
+ throw new InvalidArgumentError(err.message);
1424
+ }
1425
+ throw err;
1426
+ }
1427
+ });
1428
+ }
1429
+
1430
+ /**
1431
+ * Enter the `--phase` for this one-shot process (a no-op without one): the phase
1432
+ * guards fire from here on, and the agent phase records its write seams.
1433
+ */
1434
+ function enterCliPhase(phase: Phase | undefined): void {
1435
+ if (phase !== undefined) {
1436
+ activateProcessPhase(phase);
1437
+ }
1438
+ }
1439
+
1440
+ /**
1441
+ * The hidden, CI-only handoff-directory options of the phase mode: the agent
1442
+ * phase writes the handoff to `--handoff-out`, the apply phase reads it from
1443
+ * `--handoff-in` (task `ci-split-build-path`).
1444
+ */
1445
+ function handoffDirOptions(): Option[] {
1446
+ return [
1447
+ new Option(
1448
+ '--handoff-out <dir>',
1449
+ '(CI-only, --phase agent) write the handoff artifact to this directory',
1450
+ ).hideHelp(),
1451
+ new Option(
1452
+ '--handoff-in <dir>',
1453
+ '(CI-only, --phase apply) read the handoff artifact from this directory (under $RUNNER_TEMP)',
1454
+ ).hideHelp(),
1455
+ ];
1456
+ }
1457
+
1458
+ /**
1459
+ * The hidden, CI-only agent-result options of the apply phase (task
1460
+ * `ci-split-agent-result-and-reruns`, decision 5): `--agent-result` is
1461
+ * `needs.agent.result` (only `success` reads the handoff), and
1462
+ * `--agent-timeout-minutes` is the lock job's trusted `agentTimeoutMinutes`,
1463
+ * which tells a timed-out agent job from a cancelled one.
1464
+ */
1465
+ function agentResultOptions(): Option[] {
1466
+ const parse =
1467
+ <T>(fn: (raw: string) => T) =>
1468
+ (raw: string): T => {
1469
+ try {
1470
+ return fn(raw);
1471
+ } catch (err) {
1472
+ if (err instanceof AgentResultUsageError) {
1473
+ throw new InvalidArgumentError(err.message);
1474
+ }
1475
+ throw err;
1476
+ }
1477
+ };
1478
+ return [
1479
+ new Option(
1480
+ '--agent-result <result>',
1481
+ '(CI-only, --phase apply) the agent job result (needs.agent.result): success, failure, cancelled or skipped',
1482
+ )
1483
+ .hideHelp()
1484
+ .argParser(parse(parseAgentJobResult)),
1485
+ new Option(
1486
+ '--agent-timeout-minutes <n>',
1487
+ "(CI-only, --phase apply) the agent job's timeout, the lock job's agentTimeoutMinutes output",
1488
+ )
1489
+ .hideHelp()
1490
+ .argParser(parse(parseAgentTimeoutMinutes)),
1491
+ ];
1492
+ }
1493
+
1494
+ /**
1495
+ * Run ONE phase of the split build path (`do <slug>` / `advance task:<slug>`)
1496
+ * or tasking path (`do spec:<slug>` / `advance spec:<slug>`, task
1497
+ * `ci-split-tasking`) for a single named item with `--phase`, and exit with its
1498
+ * code. The lock phase
1499
+ * reads its trusted facts back from nowhere (it produces them); the agent and
1500
+ * apply phases read them from `DORFL_LOCK_OUTPUTS` (see `ci-lock-outputs.ts`).
1501
+ */
1502
+ async function runBuildPhaseAndExit(
1503
+ verb: BuildPhaseVerb,
1504
+ args: string[],
1505
+ flags: DoFlags,
1506
+ options: Omit<DoOptions, 'arg'>,
1507
+ ): Promise<never> {
1508
+ const phase = flags.phase!;
1509
+ if (args.length !== 1) {
1510
+ console.error(
1511
+ `error: --phase runs ONE CI item; name exactly one item (got ${args.length}).`,
1512
+ );
1513
+ process.exit(1);
1514
+ }
1515
+ const handoffDir = phase === 'agent' ? flags.handoffOut : flags.handoffIn;
1516
+ if (phase !== 'lock' && handoffDir === undefined) {
1517
+ console.error(
1518
+ `error: --phase ${phase} needs ${phase === 'agent' ? '--handoff-out' : '--handoff-in'} <dir>.`,
1519
+ );
1520
+ process.exit(1);
1521
+ }
1522
+ if (phase === 'apply' && flags.agentResult === undefined) {
1523
+ console.error(
1524
+ 'error: --phase apply needs --agent-result <result> (needs.agent.result).',
1525
+ );
1526
+ process.exit(1);
1527
+ }
1528
+ // A `spec:<slug>` item is the TASKING path (task `ci-split-tasking`); every
1529
+ // other item is the build path, which refuses a non-task itself.
1530
+ const result =
1531
+ parseSlugArg(args[0]).explicit === 'spec'
1532
+ ? await performTaskingPhase({
1533
+ ...options,
1534
+ arg: args[0],
1535
+ phase,
1536
+ verb,
1537
+ handoffDir,
1538
+ agentResult: flags.agentResult,
1539
+ agentTimeoutMinutes: flags.agentTimeoutMinutes,
1540
+ })
1541
+ : await performBuildPhase({
1542
+ ...options,
1543
+ arg: args[0],
1544
+ phase,
1545
+ verb,
1546
+ handoffDir,
1547
+ allowBacklog: flags.allowBacklog === true,
1548
+ agentResult: flags.agentResult,
1549
+ agentTimeoutMinutes: flags.agentTimeoutMinutes,
1550
+ });
1551
+ if (result.exitCode !== 0) {
1552
+ console.error(`error: ${result.message}`);
1553
+ } else {
1554
+ console.error(`>> ${result.message}`);
1555
+ }
1556
+ process.exit(result.exitCode);
1557
+ }
1558
+
1559
+ /**
1560
+ * Run ONE phase of `advance <item> --phase` and exit with its code. The item's
1561
+ * rung picks the split path ({@link advancePhasePath}): the build and tasking
1562
+ * paths as before ({@link runBuildPhaseAndExit}), and the tree-less rungs
1563
+ * (surface, triage, apply; task `ci-split-treeless-rungs`) through
1564
+ * {@link performTreelessPhase} with the same advance context the laptop tick
1565
+ * gets.
1566
+ */
1567
+ async function runAdvancePhaseAndExit(
1568
+ args: string[],
1569
+ flags: DoFlags,
1570
+ doOptions: Omit<DoOptions, 'arg'>,
1571
+ advanceContext: AdvanceContext,
1572
+ ): Promise<never> {
1573
+ const phase = flags.phase!;
1574
+ if (args.length === 1) {
1575
+ const path = await advancePhasePath({
1576
+ phase,
1577
+ arg: args[0],
1578
+ cwd: advanceContext.cwd,
1579
+ arbiter: advanceContext.arbiter,
1580
+ env: identityEnv(doOptions.identity, process.env),
1581
+ });
1582
+ if (path === 'treeless') {
1583
+ const handoffDir = phase === 'agent' ? flags.handoffOut : flags.handoffIn;
1584
+ if (phase !== 'lock' && handoffDir === undefined) {
1585
+ console.error(
1586
+ `error: --phase ${phase} needs ${phase === 'agent' ? '--handoff-out' : '--handoff-in'} <dir>.`,
1587
+ );
1588
+ process.exit(1);
1589
+ }
1590
+ if (phase === 'apply' && flags.agentResult === undefined) {
1591
+ console.error(
1592
+ 'error: --phase apply needs --agent-result <result> (needs.agent.result).',
1593
+ );
1594
+ process.exit(1);
1595
+ }
1596
+ const result = await performTreelessPhase({
1597
+ ...advanceContext,
1598
+ arg: args[0],
1599
+ phase,
1600
+ identity: doOptions.identity,
1601
+ handoffDir,
1602
+ agentResult: flags.agentResult,
1603
+ agentTimeoutMinutes: flags.agentTimeoutMinutes,
1604
+ });
1605
+ if (result.exitCode !== 0) {
1606
+ console.error(`error: ${result.message}`);
1607
+ } else {
1608
+ console.error(`>> ${result.message}`);
1609
+ }
1610
+ process.exit(result.exitCode);
1611
+ }
1612
+ }
1613
+ return runBuildPhaseAndExit('advance', args, flags, doOptions);
1614
+ }
1615
+
1616
+ /**
1617
+ * Run ONE phase of the split intake path (`intake <N> --phase`) and exit with
1618
+ * its code (task `ci-split-intake`). The agent and apply phases read the lock
1619
+ * job's facts from `DORFL_LOCK_OUTPUTS`.
1620
+ */
1621
+ async function runIntakePhaseAndExit(
1622
+ flags: IntakeFlags,
1623
+ options: PerformIntakeOptions,
1624
+ ): Promise<never> {
1625
+ const phase = flags.phase!;
1626
+ const handoffDir = phase === 'agent' ? flags.handoffOut : flags.handoffIn;
1627
+ if (phase !== 'lock' && handoffDir === undefined) {
1628
+ console.error(
1629
+ `error: --phase ${phase} needs ${phase === 'agent' ? '--handoff-out' : '--handoff-in'} <dir>.`,
1630
+ );
1631
+ process.exit(1);
1632
+ }
1633
+ if (phase === 'apply' && flags.agentResult === undefined) {
1634
+ console.error(
1635
+ 'error: --phase apply needs --agent-result <result> (needs.agent.result).',
1636
+ );
1637
+ process.exit(1);
1638
+ }
1639
+ const result = await performIntakePhase({
1640
+ ...options,
1641
+ phase,
1642
+ handoffDir,
1643
+ agentResult: flags.agentResult,
1644
+ });
1645
+ if (result.exitCode !== 0) {
1646
+ console.error(`error: ${result.message}`);
1647
+ } else {
1648
+ console.error(`>> ${result.message}`);
1649
+ }
1650
+ process.exit(result.exitCode);
1651
+ }
1652
+
1310
1653
  export function buildProgram(): Command {
1311
1654
  const program = new Command();
1312
1655
 
@@ -1389,7 +1732,11 @@ export function buildProgram(): Command {
1389
1732
  )
1390
1733
  .option(
1391
1734
  '--reconcile-locks',
1392
- 'WRITE: release the per-item locks whose item is already at rest in a terminal folder on the arbiter’s main (e.g. a merged propose PR left its lock behind). Without this flag `scan` is strictly read-only. Not normally needed, the claim path releases them automatically on every unit of work. An item that is NOT terminal on main (a live build, an open PR, a stuck item) is NEVER released, with or without the flag.',
1735
+ 'WRITE: release the per-item locks whose item is already at rest in a terminal folder on the arbiter’s main (e.g. a merged propose PR left its lock behind). Without this flag `scan` is strictly read-only. SCOPE: by DEFAULT this is ARBITER-SCOPED, like gc and `status --reconcile-locks`: it releases locks ONLY on the arbiter resolved from the current directory (its --arbiter remote, default: the configured defaultArbiter), so a drain run in repo A never touches repo B’s locks (other repos’ stale locks are still classified). Pass --all-arbiters to release across EVERY registered arbiter. Refuses when no arbiter resolves from the cwd and --all-arbiters is not given. Not normally needed, the claim path releases them automatically on every unit of work. An item that is NOT terminal on main (a live build, an open PR, a stuck item) is NEVER released, with or without the flag.',
1736
+ )
1737
+ .option(
1738
+ '--all-arbiters',
1739
+ 'with --reconcile-locks: release stale locks across EVERY registered arbiter (loud + global), not just the current repo’s. No effect on the read-only report, which always covers every registered repo.',
1393
1740
  )
1394
1741
  .option('--json', 'output the raw report as JSON')
1395
1742
  .action(async (flags: ScanFlags, command: Commander) => {
@@ -1432,10 +1779,19 @@ export function buildProgram(): Command {
1432
1779
  }
1433
1780
  return;
1434
1781
  }
1782
+ // SCOPE of the registry-loop `--reconcile-locks` WRITE: arbiter-scoped by
1783
+ // default, `--all-arbiters` for the global drain, refusal when neither
1784
+ // resolves (the SAME rule `status --reconcile-locks` applies).
1785
+ const reconcileArbiterKey = resolveReconcileArbiterKey(
1786
+ 'scan',
1787
+ flags,
1788
+ config.defaultArbiter,
1789
+ );
1435
1790
  const report = await scan(config, {
1436
1791
  warn,
1437
1792
  override,
1438
1793
  reconcileLocks: flags.reconcileLocks === true,
1794
+ reconcileArbiterKey,
1439
1795
  });
1440
1796
  // The cwd-local section: resolve it ONLY when a participating cwd is NOT
1441
1797
  // already covered by a registered mirror. A FETCH-FREE pre-check
@@ -2425,7 +2781,13 @@ export function buildProgram(): Command {
2425
2781
  '--merge-retries <n>',
2426
2782
  'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.',
2427
2783
  )
2784
+ .addOption(phaseOption())
2785
+ .addOption(handoffDirOptions()[0])
2786
+ .addOption(handoffDirOptions()[1])
2787
+ .addOption(agentResultOptions()[0])
2788
+ .addOption(agentResultOptions()[1])
2428
2789
  .action(async (rawSlugs: string[], flags: DoFlags) => {
2790
+ enterCliPhase(flags.phase);
2429
2791
  // Variadic grammar (`do-autopick`): zero args = AUTO-PICK; one = the single
2430
2792
  // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count.
2431
2793
  const args = rawSlugs ?? [];
@@ -2798,6 +3160,7 @@ export function buildProgram(): Command {
2798
3160
  // sequential `performDo` (do-autopick runs the EXISTING pipeline per item).
2799
3161
  const baseDoOptions: Omit<DoOptions, 'arg'> = {
2800
3162
  cwd,
3163
+ phase: flags.phase,
2801
3164
  arbiter: flags.arbiter ?? config.defaultArbiter,
2802
3165
  // The host-only runner IDENTITY (a bot): scopes the runner's git/provider
2803
3166
  // ops (claim, push, integrate, `gh`) — NEVER the agent launch. Absent ⇒
@@ -2910,6 +3273,12 @@ export function buildProgram(): Command {
2910
3273
  process.exit(1);
2911
3274
  }
2912
3275
 
3276
+ // CI PHASE MODE (task `ci-split-build-path`): `--phase` runs ONE job of the
3277
+ // split item workflow for a single named task or spec, on the SAME pipeline.
3278
+ if (flags.phase !== undefined) {
3279
+ await runBuildPhaseAndExit('do', args, flags, baseDoOptions);
3280
+ }
3281
+
2913
3282
  // DISPATCH the variadic grammar (in-place forms):
2914
3283
  // zero args -> AUTO-PICK `count` (default 1) across the two pools
2915
3284
  // (ordered by selectionOrder; default drain = tasks-first)
@@ -2964,7 +3333,7 @@ export function buildProgram(): Command {
2964
3333
  .command('advance')
2965
3334
  .helpGroup(HEADLINE_GROUP)
2966
3335
  .description(
2967
- 'Advance work/ item(s) one lifecycle rung toward ready/built (SPEC advance-loop), the SEQUENTIAL one-shot driver over the advance tick. advance <slug> (bare = the task) | advance spec:<slug> (the spec tasking rung) | advance obs:<slug> (triage an observation) | advance (auto-pick one eligible) | advance <a> <b> (those, in sequence) | advance -n <x> (x eligible, in sequence). Each item: classify (read-only, no model, no lock) → take the `advancing` CAS lock → dispatch winner-only — build/task rungs ORCHESTRATE `do`/`do spec:`, surface/apply always run, triage respects observationTriage (off|ask|auto). The bare/`-n` selection respects the per-action gates (build→autoBuild, task→autoTask, triage→observationTriage); `-n` is ALWAYS sequential (parallelism is `run` / the CI matrix).',
3336
+ 'Advance work/ item(s) one lifecycle rung toward ready/built (SPEC advance-loop), the SEQUENTIAL one-shot driver over the advance tick. advance <slug> (bare = the task) | advance spec:<slug> (the spec tasking rung) | advance obs:<slug> (triage an observation) | advance (auto-pick one eligible) | advance <a> <b> (those, in sequence) | advance -n <x> (x eligible, in sequence). Each item: classify (read-only, no model, no lock) → take the `advancing` CAS lock → dispatch winner-only — build/task rungs ORCHESTRATE `do`/`do spec:`, surface/apply always run, triage respects observationTriage (off|ask|auto). The bare/`-n` selection respects the per-action gates (build→autoBuild, task→autoTask, triage→observationTriage); `-n` is ALWAYS sequential (parallelism is `run` / the CI matrix). Before it selects, the bare/`-n` form also asks a merge question for each unmerged work/task-<slug> branch with no open PR and no live build (the `surface-merge-questions` pass; gated by mergeQuestions off|ask).',
2968
3337
  )
2969
3338
  .argument(
2970
3339
  '[slugs...]',
@@ -3001,15 +3370,11 @@ export function buildProgram(): Command {
3001
3370
  )
3002
3371
  .option(
3003
3372
  '--strict-merge-approval',
3004
- 'opt in to the host-agnostic "dismiss stale approvals on base change" discipline (spec `land-time-reverify-and-parallel-merge-ceiling` sidecar OQ6): when the merge-base CHANGED between the human’s merge-answer and the apply step, RE-SURFACE the merge-question (clear the answer back to no-answer; re-author the question on main/runner under the advancing lock) instead of auto-landing on a green re-verify. Default OFF (a green re-verify is trusted as sufficient; honour the prior answer). Story #16’s RED-re-verify refusal is UNCHANGED and independent of this flag. Resolved flag > env > per-repo > global > default off.',
3373
+ 'opt in to the host-agnostic "dismiss stale approvals on base change" discipline (spec `land-time-reverify-and-parallel-merge-ceiling` sidecar OQ6): when `main`’s code (anything outside `work/`) moved since the merge-question was asked (the `askedAtMain` it records), RE-SURFACE the merge-question (append a follow-up asked against the current `main`, under the advancing lock) instead of auto-landing on a green re-verify. A question written before `askedAtMain` existed never re-stales. Default OFF (a green re-verify is trusted as sufficient; honour the prior answer). Story #16’s RED-re-verify refusal is UNCHANGED and independent of this flag. Resolved flag > env > per-repo > global > default off.',
3005
3374
  )
3006
3375
  .option(
3007
3376
  '--no-strict-merge-approval',
3008
- 'honour the prior merge-answer and land when the rebased tip re-verifies GREEN even if the merge-base changed (default; the cheap green-re-verify-is-enough path)',
3009
- )
3010
- .option(
3011
- '--merge-questions <mode>',
3012
- 'the merge-question SURFACER gate (off|ask|auto): off drops the surfacer (only for a repo that lands by some other means); ask (default) enumerates unmerged `work/*` branches and surfaces a merge-question sidecar a human answers; auto self-supplies the `merge` answer and lands via the SAME deterministic apply-time re-verify (the merge-mode-like fast path). SEPARATE axis from --observation-triage with a HIGHER default (a dropped merge-question means pushed work never lands). Resolved flag > env > per-repo > global > default ask.',
3377
+ 'honour the prior merge-answer and land when the rebased tip re-verifies GREEN even if `main` moved since the question was asked (default; the cheap green-re-verify-is-enough path)',
3013
3378
  )
3014
3379
  .option(
3015
3380
  '--merge',
@@ -3027,7 +3392,13 @@ export function buildProgram(): Command {
3027
3392
  '--watch',
3028
3393
  "stream the build agent's high-signal events live by tailing the pi session log (requires harness: pi; READ-ONLY observer — does not change outcome/gate/git). The same view `do --watch` gives, threaded through the build rung; CI uses it so the job log shows the agent working instead of freezing.",
3029
3394
  )
3395
+ .addOption(phaseOption())
3396
+ .addOption(handoffDirOptions()[0])
3397
+ .addOption(handoffDirOptions()[1])
3398
+ .addOption(agentResultOptions()[0])
3399
+ .addOption(agentResultOptions()[1])
3030
3400
  .action(async (rawSlugs: string[], flags: DoFlags) => {
3401
+ enterCliPhase(flags.phase);
3031
3402
  // Variadic grammar (mirrors `do`): zero args = AUTO-PICK; one = the single
3032
3403
  // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count
3033
3404
  // (ALWAYS sequential, US #25).
@@ -3289,6 +3660,7 @@ export function buildProgram(): Command {
3289
3660
  // hands the resolved arg to `performDo`, never re-implementing it.
3290
3661
  const doOptions: Omit<DoOptions, 'arg'> = {
3291
3662
  cwd,
3663
+ phase: flags.phase,
3292
3664
  // `--watch`: stream the build agent's session live (pi harness only;
3293
3665
  // validated in `performDo`). Threaded through the orchestrated build rung
3294
3666
  // so `advance --watch` (and CI) shows the agent working, not a frozen log.
@@ -3351,6 +3723,7 @@ export function buildProgram(): Command {
3351
3723
  // Surface + apply stay ALWAYS allowed regardless of the gate family.
3352
3724
  const advanceContext: AdvanceContext = {
3353
3725
  cwd,
3726
+ phase: flags.phase,
3354
3727
  arbiter: flags.arbiter ?? config.defaultArbiter,
3355
3728
  doOptions,
3356
3729
  surfaceGate: harnessSurfaceGate({harness, agentCmd: config.agentCmd}),
@@ -3360,6 +3733,14 @@ export function buildProgram(): Command {
3360
3733
  observationTriage: config.observationTriage,
3361
3734
  triageGate: harnessTriageGate({harness, agentCmd: config.agentCmd}),
3362
3735
  triageModel: config.triageModel,
3736
+ // The answered-merge land (the apply rung's `kind: merge` action) cuts
3737
+ // its job worktree under `workspacesDir` and gates the rebased tip with
3738
+ // `prepare` / `verify`; without them an answered `merge` is refused
3739
+ // as a usage error (task `wire-merge-questions-into-the-advance-tick`).
3740
+ workspacesDir: config.workspacesDir,
3741
+ prepare: config.prepare,
3742
+ verify: config.verify,
3743
+ strictMergeApproval: config.strictMergeApproval,
3363
3744
  note: (message) => console.error(`>> ${message}`),
3364
3745
  };
3365
3746
 
@@ -3368,6 +3749,24 @@ export function buildProgram(): Command {
3368
3749
  // ticks in sequence and would tail several logs; reject rather than silently
3369
3750
  // stream only one. The CI propose matrix names a single item per leg, so it
3370
3751
  // satisfies this; the `-n` merge job must NOT pass `--watch`.
3752
+ // CI PHASE MODE (task `ci-split-build-path`): `--phase` runs ONE job of the
3753
+ // split item workflow: the build, tasking and tree-less (surface, triage,
3754
+ // apply) paths are split (task `ci-split-treeless-rungs`), and the apply
3755
+ // rung's answered merge action with them (task
3756
+ // `ci-split-answered-merge-action`), whose agent job checks the branch
3757
+ // out through `createJob` under `workspacesDir` and gates the rebased tip
3758
+ // with the repo's `prepare` / `verify`. Threaded for the phase mode ONLY:
3759
+ // the in-place laptop tick below keeps today's context.
3760
+ if (flags.phase !== undefined) {
3761
+ await runAdvancePhaseAndExit(args, flags, doOptions, {
3762
+ ...advanceContext,
3763
+ workspacesDir: config.workspacesDir,
3764
+ prepare: config.prepare,
3765
+ verify: config.verify,
3766
+ strictMergeApproval: config.strictMergeApproval,
3767
+ });
3768
+ }
3769
+
3371
3770
  const advanceMulti =
3372
3771
  args.length === 0 || count !== undefined || args.length > 1;
3373
3772
  if (advanceMulti && flags.watch === true) {
@@ -3461,7 +3860,7 @@ export function buildProgram(): Command {
3461
3860
  )
3462
3861
  .option(
3463
3862
  '--remote-branches',
3464
- 'SWEEP the arbiter’s remote work/* BRANCHES instead of job worktrees: delete (via git push --delete, NEVER --force) exactly those PROVABLY MERGED into <arbiter>/main (git merge-base --is-ancestor, the SAME predicate the worktree reaper uses), and RETAIN the rest with a reason. An in-flight/un-merged branch (the recovery point) is NEVER touched. Provider-agnostic plain git — works on a --bare arbiter. The merged-only complement of `requeue --reset`.',
3863
+ 'SWEEP the arbiter’s remote work/* BRANCHES instead of job worktrees: delete (via git push --delete, NEVER --force) exactly those PROVABLY MERGED into <arbiter>/main (git merge-base --is-ancestor, the SAME predicate the worktree reaper uses), and RETAIN the rest with a reason. An in-flight/un-merged branch (the recovery point) is NEVER touched. Provider-agnostic plain git — works on a --bare arbiter. The merged-only complement of `requeue --reset`. The same run ALSO releases every per-item lock whose item is TERMINAL on <arbiter>/main (the `status --reconcile-locks` reconcile: leased on the sha it read, never forced; a lock whose item is not terminal is never touched), so the locks merged propose PRs leave behind do not accumulate.',
3465
3864
  )
3466
3865
  .option(
3467
3866
  '--arbiter <remote-or-url>',
@@ -3477,7 +3876,7 @@ export function buildProgram(): Command {
3477
3876
  )
3478
3877
  .option(
3479
3878
  '--dry-run',
3480
- '(with --remote-branches) REPORT which merged branches WOULD be reaped without deleting anything (a read-only preview)',
3879
+ '(with --remote-branches) REPORT which merged branches WOULD be reaped (and which terminal-item locks WOULD be released) without deleting anything (a read-only preview)',
3481
3880
  )
3482
3881
  .option('--json', 'output the raw result as JSON')
3483
3882
  .action(async (flags: GcFlags) => {
@@ -3631,9 +4030,27 @@ export function buildProgram(): Command {
3631
4030
  dryRun: flags.dryRun === true,
3632
4031
  note: (message) => console.error(`>> ${message}`),
3633
4032
  });
4033
+ // The TERMINAL-LOCK release (task `ci-releases-locks-of-terminal-items`)
4034
+ // rides this SAME invocation for the same reason the orphan sweep does: the
4035
+ // scheduled `reap-merged-branches` CI job runs exactly this command, and a
4036
+ // merged propose item is never claimed again, so without it the lock a
4037
+ // merged PR leaves behind accumulates on the arbiter for ever. It is the
4038
+ // `status --reconcile-locks` reconcile (terminal-on-main only, leased on
4039
+ // the sha it read, never forced), scoped to the one `--arbiter` swept
4040
+ // here. Honours `--dry-run` (report-only preview).
4041
+ const locks = await sweepTerminalItemLocks({
4042
+ cwd: sweepCwd,
4043
+ arbiter: flags.arbiter ?? 'origin',
4044
+ dryRun: flags.dryRun === true,
4045
+ note: (message) => console.error(`>> ${message}`),
4046
+ });
3634
4047
  if (flags.json) {
3635
4048
  console.log(
3636
- JSON.stringify({...sweep, orphanSidecars: orphans}, null, 2),
4049
+ JSON.stringify(
4050
+ {...sweep, orphanSidecars: orphans, terminalLocks: locks},
4051
+ null,
4052
+ 2,
4053
+ ),
3637
4054
  );
3638
4055
  return;
3639
4056
  }
@@ -3644,6 +4061,11 @@ export function buildProgram(): Command {
3644
4061
  for (const w of orphans.wouldReap) {
3645
4062
  console.log(` [would-reap] ${w.path} \u2014 orphan sidecar`);
3646
4063
  }
4064
+ for (const entry of locks.wouldRelease) {
4065
+ console.log(
4066
+ ` [would-release] lock ${entry} \u2014 item terminal on main`,
4067
+ );
4068
+ }
3647
4069
  } else {
3648
4070
  for (const r of sweep.reaped) {
3649
4071
  console.log(` [reaped] ${r.branch} \u2014 merged`);
@@ -3651,6 +4073,14 @@ export function buildProgram(): Command {
3651
4073
  for (const r of orphans.reaped) {
3652
4074
  console.log(` [reaped] ${r.path} \u2014 orphan sidecar`);
3653
4075
  }
4076
+ for (const entry of locks.released) {
4077
+ console.log(
4078
+ ` [released] lock ${entry} \u2014 item terminal on main`,
4079
+ );
4080
+ }
4081
+ }
4082
+ for (const e of locks.errors) {
4083
+ console.log(` [kept] lock ${e.entry} \u2014 ${e.message}`);
3654
4084
  }
3655
4085
  for (const ret of sweep.retained) {
3656
4086
  console.log(` [retained] ${ret.branch} \u2014 ${ret.reasonText}`);
@@ -3660,8 +4090,19 @@ export function buildProgram(): Command {
3660
4090
  ? sweep.wouldReap.length + orphans.wouldReap.length
3661
4091
  : sweep.reaped.length + orphans.reaped.length;
3662
4092
  const verb = flags.dryRun === true ? 'would reap' : 'reaped';
4093
+ const releasedLocks =
4094
+ flags.dryRun === true
4095
+ ? locks.wouldRelease.length
4096
+ : locks.released.length;
4097
+ const releaseVerb =
4098
+ flags.dryRun === true ? 'would release' : 'released';
3663
4099
  console.log(
3664
- `Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained.`,
4100
+ `Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained; ` +
4101
+ `${releasedLocks} terminal-item lock(s) ${releaseVerb}` +
4102
+ (locks.errors.length > 0
4103
+ ? `, ${locks.errors.length} kept on error`
4104
+ : '') +
4105
+ '.',
3665
4106
  );
3666
4107
  return;
3667
4108
  }
@@ -3773,7 +4214,8 @@ export function buildProgram(): Command {
3773
4214
  }
3774
4215
  for (const retained of result.retained) {
3775
4216
  console.log(
3776
- ` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}`,
4217
+ ` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}` +
4218
+ (retained.hint ? ` (${retained.hint})` : ''),
3777
4219
  );
3778
4220
  }
3779
4221
  for (const orphan of result.sweptOrphans) {
@@ -3921,7 +4363,11 @@ export function buildProgram(): Command {
3921
4363
  )
3922
4364
  .option(
3923
4365
  '--reconcile-locks',
3924
- 'WRITE: release the per-item locks reported as "Completed, lock not yet released" (item already at rest in a terminal folder on the arbiter’s main, e.g. a merged propose PR). Without this flag `status` is strictly read-only and only REPORTS them. Not normally needed, the claim path releases them automatically on every unit of work; this is the manual “drain them now” lever. An item that is NOT terminal on main (a live build, an open PR, a stuck item) is NEVER released, with or without the flag.',
4366
+ 'WRITE: release the per-item locks reported as "Completed, lock not yet released" (item already at rest in a terminal folder on the arbiter’s main, e.g. a merged propose PR). Without this flag `status` is strictly read-only and only REPORTS them. SCOPE: by DEFAULT this is ARBITER-SCOPED, like gc: it releases locks ONLY on the arbiter resolved from the current directory (its --arbiter remote, default: the configured defaultArbiter), so a drain run in repo A never touches repo B’s locks (other repos’ stale locks are still REPORTED). Pass --all-arbiters to release across EVERY registered arbiter. Refuses when no arbiter resolves from the cwd and --all-arbiters is not given. Not normally needed, the claim path releases them automatically on every unit of work; this is the manual “drain them now” lever. An item that is NOT terminal on main (a live build, an open PR, a stuck item) is NEVER released, with or without the flag.',
4367
+ )
4368
+ .option(
4369
+ '--all-arbiters',
4370
+ 'with --reconcile-locks: release stale locks across EVERY registered arbiter (loud + global), not just the current repo’s. No effect on the read-only report, which always covers every registered repo.',
3925
4371
  )
3926
4372
  .option('--json', 'output the raw report as JSON')
3927
4373
  .action(async (flags: StatusFlags) => {
@@ -3937,7 +4383,10 @@ export function buildProgram(): Command {
3937
4383
  config,
3938
4384
  override,
3939
4385
  arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
3940
- lockArbiterRemote: flags.arbiter ?? 'origin',
4386
+ // `--no-arbiter` arrives as `arbiter: false` (commander negation):
4387
+ // never pass `false` on as a remote name.
4388
+ lockArbiterRemote:
4389
+ typeof flags.arbiter === 'string' ? flags.arbiter : 'origin',
3941
4390
  reconcileLocks: flags.reconcileLocks === true,
3942
4391
  warn,
3943
4392
  });
@@ -3964,10 +4413,23 @@ export function buildProgram(): Command {
3964
4413
  // REGISTERED HUB MIRROR (the registry), read from its bare `main` ref
3965
4414
  // through the read seam (mirrors have no working tree).
3966
4415
  const mirrorPaths = listMirrors({workspacesDir}).map((m) => m.path);
4416
+ // SCOPE of the `--reconcile-locks` WRITE (task
4417
+ // `reconcile-locks-stays-within-the-current-arbiter`): ARBITER-SCOPED by
4418
+ // default, resolved from the cwd exactly like `gc`, so a drain in repo A
4419
+ // never releases repo B's locks. `--all-arbiters` is the explicit, loud
4420
+ // global opt-in. With neither a resolvable cwd arbiter nor the flag we
4421
+ // REFUSE rather than silently widening the write to every arbiter.
4422
+ const reconcileArbiterKey = resolveReconcileArbiterKey(
4423
+ 'status',
4424
+ flags,
4425
+ config.defaultArbiter,
4426
+ );
3967
4427
  // Fold in the current repo's arbiter state (the old `arbiter status`, ADR
3968
4428
  // §1/§7) unless --no-arbiter. Read-only; tolerates not being in a repo.
4429
+ // Commander treats `--no-arbiter` as the NEGATION of `--arbiter <remote>`
4430
+ // (one `arbiter` key): it sets `arbiter: false`, never a `noArbiter` key.
3969
4431
  const arbiter =
3970
- flags.noArbiter === true
4432
+ flags.arbiter === false
3971
4433
  ? undefined
3972
4434
  : arbiterStatus({
3973
4435
  cwd: process.cwd(),
@@ -3993,6 +4455,7 @@ export function buildProgram(): Command {
3993
4455
  arbiter,
3994
4456
  cwd: cwdSection,
3995
4457
  reconcileLocks: flags.reconcileLocks === true,
4458
+ reconcileArbiterKey,
3996
4459
  warn,
3997
4460
  });
3998
4461
  if (flags.json) {
@@ -4006,7 +4469,7 @@ export function buildProgram(): Command {
4006
4469
  .command('requeue <slug>')
4007
4470
  .helpGroup(HEADLINE_GROUP)
4008
4471
  .description(
4009
- 'Requeue a STUCK task to the backlog for re-claiming (ADR §12/§14). Recovers a task whose per-item lock is held — stuck (the resolved-recovery path: a previously-routed needs-attention item, now lock `state: stuck`) OR active (a claim that never surfaced — an un-surfaced abort, a killed run, or an in-place requeue note). The body rests in work/tasks/backlog/<slug>.md (claim never moves it under the per-item-lock model); requeue releases the lock so the item is claimable again. The release is published as a TREE-LESS compare-and-swap to the arbiter ref, EXACTLY like claim — it NEVER stages or commits in the cwd working tree, so a requeue in a shared checkout can never sweep up a concurrent writer’s uncommitted files. Escalation ladder (least to most destructive): (1) DEFAULT = keep + continue: leave the work/<slug> branch UNTOUCHED so the next claim CONTINUES from its tip (rebased onto fresh main at onboard-time); if the work/<slug> branch is not on the arbiter (never pushed, or a prior --reset already deleted it), default requeue succeeds anyway with a fresh-claim note. (2) --reconcile (alias --rebase) = NON-DESTRUCTIVE recovery: re-sync the mirror to the arbiter (prune-fetch) and RETRY the rebase of the kept branch onto latest arbiter/main in a scratch worktree; on a clean rebase, push the reconciled tip back (--force-with-lease, work branch only) and release the lock; on a genuine content conflict AFTER the clean re-sync, leave the item stuck and the branch UNTOUCHED (nothing deleted) so the human can retry when the churn settles. NEVER deletes the remote branch. (3) --reset = DESTRUCTIVE last resort: delete the remote work/<slug> branch FIRST (then release the lock) so the next claim starts fresh (guarded; never the default). -m/--message appends a dated handoff note to the item body (all modes; append-only).',
4472
+ 'Requeue a STUCK task to the backlog for re-claiming (ADR §12/§14). Recovers a task whose per-item lock is held — stuck (the resolved-recovery path: a previously-routed needs-attention item, now lock `state: stuck`) OR active (a claim that never surfaced — an un-surfaced abort, a killed run, or an in-place requeue note). The body rests in work/tasks/backlog/<slug>.md (claim never moves it under the per-item-lock model); requeue releases the lock so the item is claimable again. The release is published as a TREE-LESS compare-and-swap to the arbiter ref, EXACTLY like claim — it NEVER stages or commits in the cwd working tree, so a requeue in a shared checkout can never sweep up a concurrent writer’s uncommitted files. Escalation ladder (least to most destructive): (1) DEFAULT = keep + continue: leave the work/<slug> branch UNTOUCHED so the next claim CONTINUES from its tip (rebased onto fresh main at onboard-time); if the work/<slug> branch is not on the arbiter (never pushed, or a prior --reset already deleted it), default requeue succeeds anyway with a fresh-claim note. (2) --reconcile (alias --rebase) = NON-DESTRUCTIVE recovery: re-sync the mirror to the arbiter (prune-fetch) and RETRY the rebase of the kept branch onto latest arbiter/main in a scratch worktree; on a clean rebase, push the reconciled tip back (--force-with-lease, work branch only) and release the lock; on a genuine content conflict AFTER the clean re-sync, leave the item stuck and the branch UNTOUCHED (nothing deleted) so the human can retry when the churn settles. NEVER deletes the remote branch. (3) --reset = DESTRUCTIVE last resort: delete the remote work/<slug> branch FIRST (then release the lock) so the next claim starts fresh (guarded; never the default). RELEASED LOCK: a surfaced item (question sidecar on main) has its lock already released by the surface; if its body rests in the pool and its work/<slug> branch is on the arbiter, --reconcile and --reset still act on it, taking a short per-item lock for the operation and releasing it on every outcome; plain requeue on such an item is a no-op (the next claim already continues from the kept branch). -m/--message adds a dated handoff note to the item body, just before its `## Acceptance criteria` so it cannot collide with a kept branch’s done-move tail on the continue rebase (all modes; additive). CRASHED RUN: when the lock is held and a DEAD run left its retained job worktree (workspacesDir/work/*) with commits or uncommitted files the arbiter’s work/<slug> lacks, requeue first commits the residue as a wip commit and pushes the branch (plain push, never --force), so the next claim continues from it; if that save fails, the lock and the worktree are KEPT and requeue says why (not on --reset, which discards the work by design).',
4010
4473
  )
4011
4474
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
4012
4475
  .option(
@@ -4054,6 +4517,11 @@ export function buildProgram(): Command {
4054
4517
  );
4055
4518
  process.exit(1);
4056
4519
  }
4520
+ // Where `do --isolated` retains job worktrees: a CRASHED run's retained
4521
+ // worktree may hold local-only commits, which requeue saves (pushes to the
4522
+ // work branch) before it releases the lock (task
4523
+ // `a-crashed-runs-local-work-is-saved-before-its-lock-is-released`).
4524
+ const {workspacesDir} = resolveGlobalConfig(loadConfig(flags.config), {});
4057
4525
  const result = await ledgerWrite.applyReturnToBacklogTransition({
4058
4526
  cwd,
4059
4527
  slug,
@@ -4061,6 +4529,7 @@ export function buildProgram(): Command {
4061
4529
  // arbiter to `origin` so the common case Just Works; `--arbiter` overrides.
4062
4530
  // `--cwd` is purely the ORIGIN SOURCE the remote is resolved from.
4063
4531
  arbiter: flags.arbiter ?? 'origin',
4532
+ workspacesDir,
4064
4533
  reset: flags.reset,
4065
4534
  reconcile,
4066
4535
  message: flags.message,
@@ -4076,7 +4545,13 @@ export function buildProgram(): Command {
4076
4545
  : result.reconciled
4077
4546
  ? ` (--reconcile: re-synced mirror + rebased ${workBranchRef('task', slug)} onto latest main and pushed the reconciled tip back; next claim continues from it)`
4078
4547
  : ' (kept the work branch; next claim continues from its tip)';
4079
- console.log(`Requeued '${slug}' to backlog for re-claiming.${how}`);
4548
+ const saved =
4549
+ result.crashedRunSave?.kind === 'saved'
4550
+ ? ` Saved the crashed run's local work from ${result.crashedRunSave.dir} to ${result.crashedRunSave.branch} first.`
4551
+ : '';
4552
+ console.log(
4553
+ `Requeued '${slug}' to backlog for re-claiming.${how}${saved}`,
4554
+ );
4080
4555
  });
4081
4556
 
4082
4557
  // `promote [item]` (spec `staging-pool-position-gate-and-trust-model`, tasks
@@ -4450,7 +4925,13 @@ export function buildProgram(): Command {
4450
4925
  '--sessions-dir <dir>',
4451
4926
  'HOST-ONLY root folder under which the pi session file is generated',
4452
4927
  )
4928
+ .addOption(phaseOption())
4929
+ .addOption(handoffDirOptions()[0])
4930
+ .addOption(handoffDirOptions()[1])
4931
+ .addOption(agentResultOptions()[0])
4932
+ .addOption(agentResultOptions()[1])
4453
4933
  .action(async (rawNumber: string, flags: IntakeFlags) => {
4934
+ enterCliPhase(flags.phase);
4454
4935
  const issueNumber = Number(rawNumber);
4455
4936
  if (
4456
4937
  rawNumber.trim() === '' ||
@@ -4555,9 +5036,10 @@ export function buildProgram(): Command {
4555
5036
  );
4556
5037
  process.exit(1);
4557
5038
  }
4558
- const result = await performIntake({
5039
+ const intakeOptions: PerformIntakeOptions = {
4559
5040
  issueNumber,
4560
5041
  cwd,
5042
+ phase: flags.phase,
4561
5043
  arbiter: flags.arbiter ?? config.defaultArbiter,
4562
5044
  integration: modes,
4563
5045
  // The resolved cross-job CAS-retry cap (config `mergeRetries`). `intake`
@@ -4597,7 +5079,13 @@ export function buildProgram(): Command {
4597
5079
  // decision/review AGENT launches); absent ⇒ ambient.
4598
5080
  identity: config.identity,
4599
5081
  note: (message) => console.error(`>> ${message}`),
4600
- });
5082
+ };
5083
+ // CI PHASE MODE (task `ci-split-intake`): `--phase` runs ONE job of the
5084
+ // split item workflow for `issue:<N>`, on the SAME intake pipeline.
5085
+ if (flags.phase !== undefined) {
5086
+ await runIntakePhaseAndExit(flags, intakeOptions);
5087
+ }
5088
+ const result = await performIntake(intakeOptions);
4601
5089
  if (result.exitCode !== 0) {
4602
5090
  console.error(`error: ${result.message}`);
4603
5091
  } else {
@@ -4617,7 +5105,7 @@ export function buildProgram(): Command {
4617
5105
  .command('close-merged-issues')
4618
5106
  .helpGroup(ADVANCED_GROUP)
4619
5107
  .description(
4620
- 'Close source issues whose work has landed on main (CI capability E, spec runner-in-ci). Resolves each closing issue from the work/ tree (resolveClosingIssue: a lone task closes its own `issue:`; a fanned task reaches the number via `task.spec: → spec issue:`), runs the existing "spec complete?" query for the spec case (closes ONLY when ALL its spec:<slug> tasks are in work/done/), and closes via the IssueProvider seam (atomic comment+close; NO direct gh). Re-implements NONE of the resolution/query/close — it WIRES them. Invoked by the emitted close-job workflow on a merge to main; DEGRADES (never crashes) on a missing/unauthenticated gh.',
5108
+ 'Close source issues whose work has landed on main (CI capability E, spec runner-in-ci). Resolves each closing issue from the work/ tree (resolveClosingIssue: a lone task closes its own `issue:`; a fanned task reaches the number via `task.spec: → spec issue:`), runs the existing "spec complete?" query for the spec case (closes ONLY when ALL its spec:<slug> tasks are in work/done/), and closes via the IssueProvider seam (atomic comment+close; NO direct gh). Re-implements NONE of the resolution/query/close — it WIRES them. Invoked by the emitted close-job workflow on a merge to main and hourly (a land the CI pushes itself triggers no workflow); an issue that is already closed is skipped (`already-closed`), not closed again. DEGRADES (never crashes) on a missing/unauthenticated gh.',
4621
5109
  )
4622
5110
  .option(
4623
5111
  '--cwd <dir>',
@@ -4640,6 +5128,10 @@ export function buildProgram(): Command {
4640
5128
  console.error(
4641
5129
  `>> closed issue #${c.issueNumber} (${c.via} ${c.slug}).`,
4642
5130
  );
5131
+ } else if (c.decision === 'already-closed') {
5132
+ console.error(
5133
+ `>> issue #${c.issueNumber} (${c.via} ${c.slug}) already closed.`,
5134
+ );
4643
5135
  } else if (c.decision === 'close-failed') {
4644
5136
  console.error(
4645
5137
  `>> issue #${c.issueNumber} (${c.via} ${c.slug}) NOT closed: ${c.reason}`,
@@ -4659,6 +5151,56 @@ export function buildProgram(): Command {
4659
5151
  process.exit(0);
4660
5152
  });
4661
5153
 
5154
+ // `surface-merge-questions` (task `wire-merge-questions-into-the-advance-tick`,
5155
+ // decision 5): the no-agent entry point the generated `advance-lifecycle`
5156
+ // workflow's `surface-merge-questions` job runs. It asks, in the current
5157
+ // checkout of `main`, a merge question for every unmerged `work/task-<slug>`
5158
+ // branch with no open PR and no live build, and publishes the questions to
5159
+ // `<arbiter>/main`. Gated by `mergeQuestions` (`off` does nothing). The laptop
5160
+ // bare `advance` runs the SAME pass before it selects.
5161
+ program
5162
+ .command('surface-merge-questions')
5163
+ .helpGroup(ADVANCED_GROUP)
5164
+ .description(
5165
+ 'Ask a merge question (a `kind: merge` sidecar entry, answered `merge | hold | drop`) for every unmerged `work/task-<slug>` branch on the arbiter that has no open PR (GitHub), whose task rests in tasks/ready/ or tasks/backlog/, whose lock is free or kept by its finished propose build, and whose tip carries the done-move; then publish the questions to <arbiter>/main (never --force). Fetches the arbiter first. Runs no agent. Gated by `mergeQuestions` (off|ask, default ask; DORFL_MERGE_QUESTIONS > dorfl.json > global): `off` surfaces nothing. The CI `advance-lifecycle` workflow runs it as its `surface-merge-questions` job; the bare laptop `advance` runs the same pass before it selects.',
5166
+ )
5167
+ .option(
5168
+ '--arbiter <name>',
5169
+ 'the arbiter remote whose work branches to ask about (default: the configured defaultArbiter)',
5170
+ )
5171
+ .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
5172
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
5173
+ .action(
5174
+ async (flags: {arbiter?: string; ghBin?: string; config: string}) => {
5175
+ const cwd = process.cwd();
5176
+ const {global, override} = loadGlobalAndOverride(flags.config);
5177
+ let config;
5178
+ try {
5179
+ config = resolveRepoConfig({repoPath: cwd, global, override}).config;
5180
+ } catch (err) {
5181
+ console.error(
5182
+ `error: ${err instanceof Error ? err.message : String(err)}`,
5183
+ );
5184
+ process.exit(1);
5185
+ }
5186
+ const pass = await runMergeQuestionTick({
5187
+ cwd,
5188
+ arbiter: flags.arbiter ?? config.defaultArbiter,
5189
+ mergeQuestions: config.mergeQuestions,
5190
+ ghBin: flags.ghBin,
5191
+ env: identityEnv(config.identity, process.env),
5192
+ note: (message) => console.error(`>> ${message}`),
5193
+ });
5194
+ // A failed pass (fetch, lock read, or the GitHub open-PR listing) is
5195
+ // an `error:` line and a non-zero exit, so a CI job cannot go green
5196
+ // while asking about no branch.
5197
+ console.error(
5198
+ pass.failed ? `error: ${pass.message}` : `>> ${pass.message}`,
5199
+ );
5200
+ process.exit(pass.failed ? 1 : 0);
5201
+ },
5202
+ );
5203
+
4662
5204
  // The REGISTRY command group (ADR §1): the registered set of targets IS the
4663
5205
  // hub-mirror set on disk. `remote add --local` absorbs the old `arbiter init`;
4664
5206
  // `arbiter status` is folded into `status`. There is no standalone `arbiter`
@@ -4819,7 +5361,7 @@ export function buildProgram(): Command {
4819
5361
  .command('install-ci')
4820
5362
  .helpGroup(ADVANCED_GROUP)
4821
5363
  .description(
4822
- 'Scaffold the CI auth/setup foundation (a one-time, human-run SCAFFOLDER): write the shared composite setup action (`dorfl-setup`) + provider auth (models.json default, or auth.json + GH_PAT + OAuth refresh) and set the provider secrets via the GitHub seam. Interactive wizard, or `--config <file>` for a non-interactive reproduction; `--export-config` round-trips the config; `--fake` writes to `.fake/` (never `.github/`) and sets NO real secret (a snapshot dry-run).',
5364
+ 'Scaffold the CI auth/setup foundation (a one-time, human-run SCAFFOLDER): write the shared composite setup action (`dorfl-setup`) + provider auth (a generated models.json; put rotating credentials such as OAuth behind a proxy provider `baseUrl`) and set the provider secrets via the GitHub seam. Interactive wizard, or `--config <file>` for a non-interactive reproduction; `--export-config` round-trips the config; `--fake` writes to `.fake/` (never `.github/`) and sets NO real secret (a snapshot dry-run).',
4823
5365
  )
4824
5366
  .option(
4825
5367
  '--config <file>',
@@ -4938,6 +5480,26 @@ export function buildProgram(): Command {
4938
5480
  console.log(formatSkillsAddReport(result, flags.local === true));
4939
5481
  });
4940
5482
 
5483
+ // Decision 9 (task `ci-split-warn-single-job-workflows`): warn before an
5484
+ // agent-spawning verb runs in an old single-job CI workflow (GitHub Actions,
5485
+ // no `--phase`, a credential persisted in the checkout's git config).
5486
+ // Warn-only: the action still runs.
5487
+ program.hook('preAction', (_program, actionCommand) => {
5488
+ if (actionCommand.parent !== program) {
5489
+ return;
5490
+ }
5491
+ const phase = (actionCommand.opts() as {phase?: Phase}).phase;
5492
+ const warning = singleJobWarning({
5493
+ verb: actionCommand.name(),
5494
+ phase,
5495
+ env: process.env,
5496
+ cwd: process.cwd(),
5497
+ });
5498
+ if (warning !== undefined) {
5499
+ console.error(warning);
5500
+ }
5501
+ });
5502
+
4941
5503
  return program;
4942
5504
  }
4943
5505