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/dist/cli.js 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 { createInterface } from 'node:readline';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { realpathSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
@@ -26,7 +26,7 @@ import { performAdvance } from './advance.js';
26
26
  import { performAdvanceAuto, performAdvanceArgs, runAdvanceTickWithTreelessPublish, } from './advance-drivers.js';
27
27
  import { performAdvanceIsolated, performAdvanceIsolatedAuto, performAdvanceIsolatedArgs, } from './advance-isolated.js';
28
28
  import { advanceRegistrySetRunTick } from './advance-loop-driver.js';
29
- import { performIntake, resolveIntakeIntegrationModes } from './intake.js';
29
+ import { performIntake, resolveIntakeIntegrationModes, } from './intake.js';
30
30
  import { workFolderPrefix } from './work-layout.js';
31
31
  import { performDoAuto, performDoArgs, } from './do-autopick.js';
32
32
  import { createHarness } from './pi-harness.js';
@@ -35,6 +35,7 @@ import { shouldUseColor } from './output.js';
35
35
  import { resolveRepoConfig, resolveRepoConfigFromLoaded, loadRepoConfigFromContent, REPO_CONFIG_FILENAME, } from './repo-config.js';
36
36
  import { ensureMirrorMain, readRepoConfigFromMirrorMain, encodeRepoKey, } from './repo-mirror.js';
37
37
  import { identityEnv } from './identity.js';
38
+ import { runMergeQuestionTick } from './merge-question-tick.js';
38
39
  import { harnessFlagOverrides, doFlagOverrides, doNeedsAgentCmd, NO_AGENT_CMD_MESSAGE, reviewFlagOverrides, freshWorktreeGateFlagOverrides, mergeRetriesFlagOverrides, noPRFlagOverrides, triageModelFlagOverrides, } from './do-config.js';
39
40
  import { harnessReviewGate, harnessTaskAcceptanceGate } from './review-gate.js';
40
41
  import { harnessSurfaceGate } from './surface-gate.js';
@@ -47,7 +48,7 @@ import { resolvePromptGuidance, applyModelFallbacks } from './config.js';
47
48
  import { gc, resolveArbiterKeyFromCwd, enumerateWorkspaceArbiters, RETAIN_REASON_TEXT, } from './gc.js';
48
49
  import { runPrdToSpec, } from './prd-to-spec.js';
49
50
  import { resyncProtocol } from './resync-protocol.js';
50
- import { sweepRemoteMergedBranches } from './reap-branches.js';
51
+ import { sweepRemoteMergedBranches, sweepTerminalItemLocks, } from './reap-branches.js';
51
52
  import { sweepOrphanSidecars } from './orphan-sidecar.js';
52
53
  import { sweepLedgerDuplicates, formatLedgerSweep } from './ledger-lint.js';
53
54
  import { status, formatStatus } from './status.js';
@@ -66,6 +67,50 @@ import { GitHubCIContext } from './install-ci-github.js';
66
67
  import { loadCapabilityRegistry } from './install-ci-core.js';
67
68
  import { performCloseMergedIssues } from './close-job.js';
68
69
  import { installSkills } from './install-skills.js';
70
+ import { parsePhase, PhaseUsageError } from './phase.js';
71
+ import { activateProcessPhase } from './phase-recorder.js';
72
+ import { performBuildPhase } from './ci-phase-build.js';
73
+ import { performIntakePhase } from './ci-phase-intake.js';
74
+ import { performTaskingPhase } from './ci-phase-tasking.js';
75
+ import { advancePhasePath, performTreelessPhase } from './ci-phase-treeless.js';
76
+ import { AgentResultUsageError, parseAgentJobResult, parseAgentTimeoutMinutes, } from './ci-agent-result.js';
77
+ import { singleJobWarning } from './single-job-warning.js';
78
+ /**
79
+ * Resolve the ARBITER SCOPE of a `--reconcile-locks` write for `status` or `scan`
80
+ * (tasks `reconcile-locks-stays-within-the-current-arbiter` and
81
+ * `status-no-arbiter-is-honoured`). Like `gc`, the drain is arbiter-scoped by
82
+ * DEFAULT: the hub key of the arbiter resolved from the cwd (its `--arbiter`
83
+ * remote, else the configured `defaultArbiter`), so a drain run in repo A never
84
+ * releases repo B's locks. `--all-arbiters` is the explicit, loud global opt-in
85
+ * and returns `undefined` (= every mirror). With neither a resolvable cwd arbiter
86
+ * nor the flag this REFUSES (exit 1) rather than silently widening the write.
87
+ * Returns `undefined` too when `--reconcile-locks` is absent (the key is then
88
+ * ignored by the report builders).
89
+ */
90
+ function resolveReconcileArbiterKey(verb, flags, defaultArbiter) {
91
+ if (flags.reconcileLocks !== true) {
92
+ return undefined;
93
+ }
94
+ if (flags.allArbiters === true) {
95
+ console.error('>> --all-arbiters: releasing stale per-item locks GLOBALLY across ' +
96
+ 'ALL registered arbiters (NOT scoped to the current repo).');
97
+ return undefined;
98
+ }
99
+ // `typeof` guard: on `status`, `--no-arbiter` is commander's negation of
100
+ // `--arbiter`, so it arrives as `arbiter: false`, not a remote name.
101
+ const remote = typeof flags.arbiter === 'string' ? flags.arbiter : defaultArbiter;
102
+ const key = resolveArbiterKeyFromCwd(process.cwd(), remote, process.env);
103
+ if (key === undefined) {
104
+ console.error(`refusing: \`${verb} --reconcile-locks\` is ARBITER-SCOPED and could ` +
105
+ `not resolve an arbiter from the current directory (no '${remote}' ` +
106
+ 'git remote, or not inside a repo). It will NOT fall back to ' +
107
+ 'releasing locks on every arbiter. Run it from a repo with an ' +
108
+ 'arbiter remote, pass --arbiter <remote>, or --all-arbiters to ' +
109
+ 'reconcile every registered arbiter.');
110
+ process.exit(1);
111
+ }
112
+ return key;
113
+ }
69
114
  /**
70
115
  * Whether `--auto-build` / `--no-auto-build` was explicitly passed on the command
71
116
  * line. Commander gives a negatable boolean option a default of `true`, so we
@@ -758,6 +803,207 @@ function resolvePackageVersion() {
758
803
  return 'unknown';
759
804
  }
760
805
  }
806
+ /**
807
+ * The hidden, CI-only `--phase lock|agent|apply` option shared by `intake`,
808
+ * `advance` and `do` (spec `ci-agent-job-without-write-token`, see `phase.ts`).
809
+ * Validated at PARSE time ({@link parsePhase}): an unknown value, or any value
810
+ * outside GitHub Actions, is a commander usage error before the action runs.
811
+ * Absent ⇒ today's single process.
812
+ */
813
+ function phaseOption() {
814
+ return new Option('--phase <phase>', '(CI-only) run as one job of the split CI item workflow: lock, agent or apply. Requires GITHUB_ACTIONS=true.')
815
+ .hideHelp()
816
+ .argParser((raw) => {
817
+ try {
818
+ return parsePhase(raw);
819
+ }
820
+ catch (err) {
821
+ if (err instanceof PhaseUsageError) {
822
+ throw new InvalidArgumentError(err.message);
823
+ }
824
+ throw err;
825
+ }
826
+ });
827
+ }
828
+ /**
829
+ * Enter the `--phase` for this one-shot process (a no-op without one): the phase
830
+ * guards fire from here on, and the agent phase records its write seams.
831
+ */
832
+ function enterCliPhase(phase) {
833
+ if (phase !== undefined) {
834
+ activateProcessPhase(phase);
835
+ }
836
+ }
837
+ /**
838
+ * The hidden, CI-only handoff-directory options of the phase mode: the agent
839
+ * phase writes the handoff to `--handoff-out`, the apply phase reads it from
840
+ * `--handoff-in` (task `ci-split-build-path`).
841
+ */
842
+ function handoffDirOptions() {
843
+ return [
844
+ new Option('--handoff-out <dir>', '(CI-only, --phase agent) write the handoff artifact to this directory').hideHelp(),
845
+ new Option('--handoff-in <dir>', '(CI-only, --phase apply) read the handoff artifact from this directory (under $RUNNER_TEMP)').hideHelp(),
846
+ ];
847
+ }
848
+ /**
849
+ * The hidden, CI-only agent-result options of the apply phase (task
850
+ * `ci-split-agent-result-and-reruns`, decision 5): `--agent-result` is
851
+ * `needs.agent.result` (only `success` reads the handoff), and
852
+ * `--agent-timeout-minutes` is the lock job's trusted `agentTimeoutMinutes`,
853
+ * which tells a timed-out agent job from a cancelled one.
854
+ */
855
+ function agentResultOptions() {
856
+ const parse = (fn) => (raw) => {
857
+ try {
858
+ return fn(raw);
859
+ }
860
+ catch (err) {
861
+ if (err instanceof AgentResultUsageError) {
862
+ throw new InvalidArgumentError(err.message);
863
+ }
864
+ throw err;
865
+ }
866
+ };
867
+ return [
868
+ new Option('--agent-result <result>', '(CI-only, --phase apply) the agent job result (needs.agent.result): success, failure, cancelled or skipped')
869
+ .hideHelp()
870
+ .argParser(parse(parseAgentJobResult)),
871
+ new Option('--agent-timeout-minutes <n>', "(CI-only, --phase apply) the agent job's timeout, the lock job's agentTimeoutMinutes output")
872
+ .hideHelp()
873
+ .argParser(parse(parseAgentTimeoutMinutes)),
874
+ ];
875
+ }
876
+ /**
877
+ * Run ONE phase of the split build path (`do <slug>` / `advance task:<slug>`)
878
+ * or tasking path (`do spec:<slug>` / `advance spec:<slug>`, task
879
+ * `ci-split-tasking`) for a single named item with `--phase`, and exit with its
880
+ * code. The lock phase
881
+ * reads its trusted facts back from nowhere (it produces them); the agent and
882
+ * apply phases read them from `DORFL_LOCK_OUTPUTS` (see `ci-lock-outputs.ts`).
883
+ */
884
+ async function runBuildPhaseAndExit(verb, args, flags, options) {
885
+ const phase = flags.phase;
886
+ if (args.length !== 1) {
887
+ console.error(`error: --phase runs ONE CI item; name exactly one item (got ${args.length}).`);
888
+ process.exit(1);
889
+ }
890
+ const handoffDir = phase === 'agent' ? flags.handoffOut : flags.handoffIn;
891
+ if (phase !== 'lock' && handoffDir === undefined) {
892
+ console.error(`error: --phase ${phase} needs ${phase === 'agent' ? '--handoff-out' : '--handoff-in'} <dir>.`);
893
+ process.exit(1);
894
+ }
895
+ if (phase === 'apply' && flags.agentResult === undefined) {
896
+ console.error('error: --phase apply needs --agent-result <result> (needs.agent.result).');
897
+ process.exit(1);
898
+ }
899
+ // A `spec:<slug>` item is the TASKING path (task `ci-split-tasking`); every
900
+ // other item is the build path, which refuses a non-task itself.
901
+ const result = parseSlugArg(args[0]).explicit === 'spec'
902
+ ? await performTaskingPhase({
903
+ ...options,
904
+ arg: args[0],
905
+ phase,
906
+ verb,
907
+ handoffDir,
908
+ agentResult: flags.agentResult,
909
+ agentTimeoutMinutes: flags.agentTimeoutMinutes,
910
+ })
911
+ : await performBuildPhase({
912
+ ...options,
913
+ arg: args[0],
914
+ phase,
915
+ verb,
916
+ handoffDir,
917
+ allowBacklog: flags.allowBacklog === true,
918
+ agentResult: flags.agentResult,
919
+ agentTimeoutMinutes: flags.agentTimeoutMinutes,
920
+ });
921
+ if (result.exitCode !== 0) {
922
+ console.error(`error: ${result.message}`);
923
+ }
924
+ else {
925
+ console.error(`>> ${result.message}`);
926
+ }
927
+ process.exit(result.exitCode);
928
+ }
929
+ /**
930
+ * Run ONE phase of `advance <item> --phase` and exit with its code. The item's
931
+ * rung picks the split path ({@link advancePhasePath}): the build and tasking
932
+ * paths as before ({@link runBuildPhaseAndExit}), and the tree-less rungs
933
+ * (surface, triage, apply; task `ci-split-treeless-rungs`) through
934
+ * {@link performTreelessPhase} with the same advance context the laptop tick
935
+ * gets.
936
+ */
937
+ async function runAdvancePhaseAndExit(args, flags, doOptions, advanceContext) {
938
+ const phase = flags.phase;
939
+ if (args.length === 1) {
940
+ const path = await advancePhasePath({
941
+ phase,
942
+ arg: args[0],
943
+ cwd: advanceContext.cwd,
944
+ arbiter: advanceContext.arbiter,
945
+ env: identityEnv(doOptions.identity, process.env),
946
+ });
947
+ if (path === 'treeless') {
948
+ const handoffDir = phase === 'agent' ? flags.handoffOut : flags.handoffIn;
949
+ if (phase !== 'lock' && handoffDir === undefined) {
950
+ console.error(`error: --phase ${phase} needs ${phase === 'agent' ? '--handoff-out' : '--handoff-in'} <dir>.`);
951
+ process.exit(1);
952
+ }
953
+ if (phase === 'apply' && flags.agentResult === undefined) {
954
+ console.error('error: --phase apply needs --agent-result <result> (needs.agent.result).');
955
+ process.exit(1);
956
+ }
957
+ const result = await performTreelessPhase({
958
+ ...advanceContext,
959
+ arg: args[0],
960
+ phase,
961
+ identity: doOptions.identity,
962
+ handoffDir,
963
+ agentResult: flags.agentResult,
964
+ agentTimeoutMinutes: flags.agentTimeoutMinutes,
965
+ });
966
+ if (result.exitCode !== 0) {
967
+ console.error(`error: ${result.message}`);
968
+ }
969
+ else {
970
+ console.error(`>> ${result.message}`);
971
+ }
972
+ process.exit(result.exitCode);
973
+ }
974
+ }
975
+ return runBuildPhaseAndExit('advance', args, flags, doOptions);
976
+ }
977
+ /**
978
+ * Run ONE phase of the split intake path (`intake <N> --phase`) and exit with
979
+ * its code (task `ci-split-intake`). The agent and apply phases read the lock
980
+ * job's facts from `DORFL_LOCK_OUTPUTS`.
981
+ */
982
+ async function runIntakePhaseAndExit(flags, options) {
983
+ const phase = flags.phase;
984
+ const handoffDir = phase === 'agent' ? flags.handoffOut : flags.handoffIn;
985
+ if (phase !== 'lock' && handoffDir === undefined) {
986
+ console.error(`error: --phase ${phase} needs ${phase === 'agent' ? '--handoff-out' : '--handoff-in'} <dir>.`);
987
+ process.exit(1);
988
+ }
989
+ if (phase === 'apply' && flags.agentResult === undefined) {
990
+ console.error('error: --phase apply needs --agent-result <result> (needs.agent.result).');
991
+ process.exit(1);
992
+ }
993
+ const result = await performIntakePhase({
994
+ ...options,
995
+ phase,
996
+ handoffDir,
997
+ agentResult: flags.agentResult,
998
+ });
999
+ if (result.exitCode !== 0) {
1000
+ console.error(`error: ${result.message}`);
1001
+ }
1002
+ else {
1003
+ console.error(`>> ${result.message}`);
1004
+ }
1005
+ process.exit(result.exitCode);
1006
+ }
761
1007
  export function buildProgram() {
762
1008
  const program = new Command();
763
1009
  program
@@ -805,7 +1051,8 @@ export function buildProgram() {
805
1051
  .option('--arbiter-remote <name>', `the current repo's arbiter remote to fetch + diff its local section against (default: ${DEFAULT_ARBITER_REMOTE})`)
806
1052
  .option('--arbiter <remote>', 'the COORDINATION arbiter remote whose per-item lock refs (refs/dorfl/lock/*) gate the cwd selection pool (held in-flight items are subtracted); default: origin (the same remote claim/do use, NOT the --arbiter-remote divergence name)')
807
1053
  .option('--here', 'report ONLY the current repo (the cwd working tree, fetch-first): skip the cross-repo registry loop entirely. The fast, focused path — no N-mirror fetches.')
808
- .option('--reconcile-locks', '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.')
1054
+ .option('--reconcile-locks', '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.')
1055
+ .option('--all-arbiters', '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.')
809
1056
  .option('--json', 'output the raw report as JSON')
810
1057
  .action(async (flags, command) => {
811
1058
  const fileConfig = loadConfig(flags.config);
@@ -836,10 +1083,15 @@ export function buildProgram() {
836
1083
  }
837
1084
  return;
838
1085
  }
1086
+ // SCOPE of the registry-loop `--reconcile-locks` WRITE: arbiter-scoped by
1087
+ // default, `--all-arbiters` for the global drain, refusal when neither
1088
+ // resolves (the SAME rule `status --reconcile-locks` applies).
1089
+ const reconcileArbiterKey = resolveReconcileArbiterKey('scan', flags, config.defaultArbiter);
839
1090
  const report = await scan(config, {
840
1091
  warn,
841
1092
  override,
842
1093
  reconcileLocks: flags.reconcileLocks === true,
1094
+ reconcileArbiterKey,
843
1095
  });
844
1096
  // The cwd-local section: resolve it ONLY when a participating cwd is NOT
845
1097
  // already covered by a registered mirror. A FETCH-FREE pre-check
@@ -1449,7 +1701,13 @@ export function buildProgram() {
1449
1701
  .option('--fresh-worktree-gate', 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that actually integrates), so a green gate provably describes the merged artifact. ON by default. Resolved flag > env > per-repo > global > default on.')
1450
1702
  .option('--no-fresh-worktree-gate', "run the acceptance gate in the agent's build worktree (the pre-rebase tree) as before — the opt-out for when the per-gate install cost is too high")
1451
1703
  .option('--merge-retries <n>', 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.')
1704
+ .addOption(phaseOption())
1705
+ .addOption(handoffDirOptions()[0])
1706
+ .addOption(handoffDirOptions()[1])
1707
+ .addOption(agentResultOptions()[0])
1708
+ .addOption(agentResultOptions()[1])
1452
1709
  .action(async (rawSlugs, flags) => {
1710
+ enterCliPhase(flags.phase);
1453
1711
  // Variadic grammar (`do-autopick`): zero args = AUTO-PICK; one = the single
1454
1712
  // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count.
1455
1713
  const args = rawSlugs ?? [];
@@ -1791,6 +2049,7 @@ export function buildProgram() {
1791
2049
  // sequential `performDo` (do-autopick runs the EXISTING pipeline per item).
1792
2050
  const baseDoOptions = {
1793
2051
  cwd,
2052
+ phase: flags.phase,
1794
2053
  arbiter: flags.arbiter ?? config.defaultArbiter,
1795
2054
  // The host-only runner IDENTITY (a bot): scopes the runner's git/provider
1796
2055
  // ops (claim, push, integrate, `gh`) — NEVER the agent launch. Absent ⇒
@@ -1897,6 +2156,11 @@ export function buildProgram() {
1897
2156
  `the pool). Name a single task: dorfl do task:<slug> --allow-backlog.`);
1898
2157
  process.exit(1);
1899
2158
  }
2159
+ // CI PHASE MODE (task `ci-split-build-path`): `--phase` runs ONE job of the
2160
+ // split item workflow for a single named task or spec, on the SAME pipeline.
2161
+ if (flags.phase !== undefined) {
2162
+ await runBuildPhaseAndExit('do', args, flags, baseDoOptions);
2163
+ }
1900
2164
  // DISPATCH the variadic grammar (in-place forms):
1901
2165
  // zero args -> AUTO-PICK `count` (default 1) across the two pools
1902
2166
  // (ordered by selectionOrder; default drain = tasks-first)
@@ -1948,7 +2212,7 @@ export function buildProgram() {
1948
2212
  program
1949
2213
  .command('advance')
1950
2214
  .helpGroup(HEADLINE_GROUP)
1951
- .description('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).')
2215
+ .description('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).')
1952
2216
  .argument('[slugs...]', 'the item(s) to advance: bare (= the task), task:<slug>, spec:<slug>, or obs:<slug> (an observation). Zero args = auto-pick one eligible; multiple = advance them in sequence.')
1953
2217
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
1954
2218
  .option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
@@ -1958,14 +2222,19 @@ export function buildProgram() {
1958
2222
  .option('--observation-triage <mode>', 'the observation-inbox gate (off|ask|auto): off (default) leaves observations untouched (the triage pool is dropped from auto-pick); ask surfaces a promote/keep/delete question for each untriaged observation; auto auto-disposes the no-question cases (duplicate/map) and asks about the rest. Resolved flag > env > per-repo > global > default. An explicit `advance obs:<slug>` bypasses the selection gate and runs in ask-mode (auto-disposes only under `auto`).')
1959
2223
  .option('--surface-blockers', 'the declared-blocked-work gate (the orthogonal peer of --observation-triage): render a task/spec carrying needsAnswers:true into an answerable question sidecar (the needsAnswers-blocked pool is enumerated into auto-pick). Resolved flag > env > per-repo > global > default off. An explicit `advance <slug>`/`advance spec:<slug>` bypasses this selection gate and surfaces regardless. Does NOT gate apply (an answered sidecar still applies) or needs-attention (always on).')
1960
2224
  .option('--no-surface-blockers', 'leave a needsAnswers:true task/spec silently blocked (default; the blocked pool is dropped from auto-pick)')
1961
- .option('--strict-merge-approval', '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.')
1962
- .option('--no-strict-merge-approval', '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)')
1963
- .option('--merge-questions <mode>', '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.')
2225
+ .option('--strict-merge-approval', '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.')
2226
+ .option('--no-strict-merge-approval', '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)')
1964
2227
  .option('--merge', 'integrate the advanced item(s) in merge mode this invocation (mutually exclusive with --propose; overrides config). The CI merge shape is a SINGLE SEQUENTIAL job, so this rides the `-n`/named-sequence path, never the matrix.')
1965
2228
  .option('--propose', 'integrate the advanced item(s) in propose mode this invocation (default; mutually exclusive with --merge; overrides config). The CI propose shape is the parallel matrix (one PR per item).')
1966
2229
  .option('--tasks-land-in <where>', 'where `advance spec:<slug>` tasking output lands: `backlog` (staged) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence. Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
1967
2230
  .option('--watch', "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.")
2231
+ .addOption(phaseOption())
2232
+ .addOption(handoffDirOptions()[0])
2233
+ .addOption(handoffDirOptions()[1])
2234
+ .addOption(agentResultOptions()[0])
2235
+ .addOption(agentResultOptions()[1])
1968
2236
  .action(async (rawSlugs, flags) => {
2237
+ enterCliPhase(flags.phase);
1969
2238
  // Variadic grammar (mirrors `do`): zero args = AUTO-PICK; one = the single
1970
2239
  // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count
1971
2240
  // (ALWAYS sequential, US #25).
@@ -2207,6 +2476,7 @@ export function buildProgram() {
2207
2476
  // hands the resolved arg to `performDo`, never re-implementing it.
2208
2477
  const doOptions = {
2209
2478
  cwd,
2479
+ phase: flags.phase,
2210
2480
  // `--watch`: stream the build agent's session live (pi harness only;
2211
2481
  // validated in `performDo`). Threaded through the orchestrated build rung
2212
2482
  // so `advance --watch` (and CI) shows the agent working, not a frozen log.
@@ -2269,6 +2539,7 @@ export function buildProgram() {
2269
2539
  // Surface + apply stay ALWAYS allowed regardless of the gate family.
2270
2540
  const advanceContext = {
2271
2541
  cwd,
2542
+ phase: flags.phase,
2272
2543
  arbiter: flags.arbiter ?? config.defaultArbiter,
2273
2544
  doOptions,
2274
2545
  surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
@@ -2278,6 +2549,14 @@ export function buildProgram() {
2278
2549
  observationTriage: config.observationTriage,
2279
2550
  triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
2280
2551
  triageModel: config.triageModel,
2552
+ // The answered-merge land (the apply rung's `kind: merge` action) cuts
2553
+ // its job worktree under `workspacesDir` and gates the rebased tip with
2554
+ // `prepare` / `verify`; without them an answered `merge` is refused
2555
+ // as a usage error (task `wire-merge-questions-into-the-advance-tick`).
2556
+ workspacesDir: config.workspacesDir,
2557
+ prepare: config.prepare,
2558
+ verify: config.verify,
2559
+ strictMergeApproval: config.strictMergeApproval,
2281
2560
  note: (message) => console.error(`>> ${message}`),
2282
2561
  };
2283
2562
  // `--watch` tails ONE pi session, so it only fits the single-named-item form
@@ -2285,6 +2564,23 @@ export function buildProgram() {
2285
2564
  // ticks in sequence and would tail several logs; reject rather than silently
2286
2565
  // stream only one. The CI propose matrix names a single item per leg, so it
2287
2566
  // satisfies this; the `-n` merge job must NOT pass `--watch`.
2567
+ // CI PHASE MODE (task `ci-split-build-path`): `--phase` runs ONE job of the
2568
+ // split item workflow: the build, tasking and tree-less (surface, triage,
2569
+ // apply) paths are split (task `ci-split-treeless-rungs`), and the apply
2570
+ // rung's answered merge action with them (task
2571
+ // `ci-split-answered-merge-action`), whose agent job checks the branch
2572
+ // out through `createJob` under `workspacesDir` and gates the rebased tip
2573
+ // with the repo's `prepare` / `verify`. Threaded for the phase mode ONLY:
2574
+ // the in-place laptop tick below keeps today's context.
2575
+ if (flags.phase !== undefined) {
2576
+ await runAdvancePhaseAndExit(args, flags, doOptions, {
2577
+ ...advanceContext,
2578
+ workspacesDir: config.workspacesDir,
2579
+ prepare: config.prepare,
2580
+ verify: config.verify,
2581
+ strictMergeApproval: config.strictMergeApproval,
2582
+ });
2583
+ }
2288
2584
  const advanceMulti = args.length === 0 || count !== undefined || args.length > 1;
2289
2585
  if (advanceMulti && flags.watch === true) {
2290
2586
  console.error('error: --watch streams ONE session; it does not combine with the ' +
@@ -2353,11 +2649,11 @@ export function buildProgram() {
2353
2649
  .option('--yes', 'confirm a destructive --force sweep non-interactively')
2354
2650
  .option('--ledger [repoPath]', 'SWEEP the work/ lifecycle LEDGER instead of job worktrees: REPORT (never delete) every slug present in more than one work/ status folder (the one-slug-one-folder belt-and-suspenders), with its folders + candidate canonical folder, for a HUMAN to resolve. Defaults to the cwd repo.')
2355
2651
  .option('--reap-stale-locks', '(with --ledger) OPT-IN: also CLEAR every ORPHAN lock the report finds via the SAME leased delete `release-lock` uses, so one command sweeps all orphaned locks instead of N hand-run release-locks. Reaps the `cleared-stale` class: a held `active` lock that is EITHER terminal on `<arbiter>/main` (stranded between the durable move and the release) OR non-terminal + SURFACED (`needsAnswers:true` + sidecar on `<arbiter>/main`) = the crash-window orphan the ordered bounce transition leaves when step 1 lands but step 2 never runs. A `kept-in-flight` (active + non-terminal + NOT surfaced — a healthy build) is NEVER reaped, even with this flag. A concurrent change to a lock ref makes its leased delete REJECT (reported), never --force. WITHOUT this flag `gc --ledger` stays report-only (fail-loud, deletes nothing).')
2356
- .option('--remote-branches', '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`.')
2652
+ .option('--remote-branches', '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.')
2357
2653
  .option('--arbiter <remote-or-url>', 'SCOPE the worktree sweep to a SPECIFIC arbiter (a remote NAME resolved from --cwd, or a direct arbiter URL) instead of the cwd\u2019s. (With --remote-branches / --ledger it names the arbiter git remote whose work/* branches / locks to sweep; default there: origin.)')
2358
2654
  .option('--all-arbiters', 'GLOBAL, LOUD override: sweep worktrees across EVERY arbiter under workspacesDir/work/* (today\u2019s pre-scoping behaviour), not just the cwd\u2019s. Prints a banner naming every arbiter it will act on before doing anything; combining it with --force still requires --yes. This is the destructive cross-repo path \u2014 opt into it explicitly.')
2359
2655
  .option('--cwd <dir>', '(with --remote-branches) the local repo/clone whose --arbiter remote points at the arbiter to sweep (default: cwd); only remote refs are read + deleted, never the working tree')
2360
- .option('--dry-run', '(with --remote-branches) REPORT which merged branches WOULD be reaped without deleting anything (a read-only preview)')
2656
+ .option('--dry-run', '(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)')
2361
2657
  .option('--json', 'output the raw result as JSON')
2362
2658
  .action(async (flags) => {
2363
2659
  const config = resolveGlobalConfig(loadConfig(flags.config), {});
@@ -2496,8 +2792,22 @@ export function buildProgram() {
2496
2792
  dryRun: flags.dryRun === true,
2497
2793
  note: (message) => console.error(`>> ${message}`),
2498
2794
  });
2795
+ // The TERMINAL-LOCK release (task `ci-releases-locks-of-terminal-items`)
2796
+ // rides this SAME invocation for the same reason the orphan sweep does: the
2797
+ // scheduled `reap-merged-branches` CI job runs exactly this command, and a
2798
+ // merged propose item is never claimed again, so without it the lock a
2799
+ // merged PR leaves behind accumulates on the arbiter for ever. It is the
2800
+ // `status --reconcile-locks` reconcile (terminal-on-main only, leased on
2801
+ // the sha it read, never forced), scoped to the one `--arbiter` swept
2802
+ // here. Honours `--dry-run` (report-only preview).
2803
+ const locks = await sweepTerminalItemLocks({
2804
+ cwd: sweepCwd,
2805
+ arbiter: flags.arbiter ?? 'origin',
2806
+ dryRun: flags.dryRun === true,
2807
+ note: (message) => console.error(`>> ${message}`),
2808
+ });
2499
2809
  if (flags.json) {
2500
- console.log(JSON.stringify({ ...sweep, orphanSidecars: orphans }, null, 2));
2810
+ console.log(JSON.stringify({ ...sweep, orphanSidecars: orphans, terminalLocks: locks }, null, 2));
2501
2811
  return;
2502
2812
  }
2503
2813
  if (flags.dryRun === true) {
@@ -2507,6 +2817,9 @@ export function buildProgram() {
2507
2817
  for (const w of orphans.wouldReap) {
2508
2818
  console.log(` [would-reap] ${w.path} \u2014 orphan sidecar`);
2509
2819
  }
2820
+ for (const entry of locks.wouldRelease) {
2821
+ console.log(` [would-release] lock ${entry} \u2014 item terminal on main`);
2822
+ }
2510
2823
  }
2511
2824
  else {
2512
2825
  for (const r of sweep.reaped) {
@@ -2515,6 +2828,12 @@ export function buildProgram() {
2515
2828
  for (const r of orphans.reaped) {
2516
2829
  console.log(` [reaped] ${r.path} \u2014 orphan sidecar`);
2517
2830
  }
2831
+ for (const entry of locks.released) {
2832
+ console.log(` [released] lock ${entry} \u2014 item terminal on main`);
2833
+ }
2834
+ }
2835
+ for (const e of locks.errors) {
2836
+ console.log(` [kept] lock ${e.entry} \u2014 ${e.message}`);
2518
2837
  }
2519
2838
  for (const ret of sweep.retained) {
2520
2839
  console.log(` [retained] ${ret.branch} \u2014 ${ret.reasonText}`);
@@ -2523,7 +2842,16 @@ export function buildProgram() {
2523
2842
  ? sweep.wouldReap.length + orphans.wouldReap.length
2524
2843
  : sweep.reaped.length + orphans.reaped.length;
2525
2844
  const verb = flags.dryRun === true ? 'would reap' : 'reaped';
2526
- console.log(`Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained.`);
2845
+ const releasedLocks = flags.dryRun === true
2846
+ ? locks.wouldRelease.length
2847
+ : locks.released.length;
2848
+ const releaseVerb = flags.dryRun === true ? 'would release' : 'released';
2849
+ console.log(`Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained; ` +
2850
+ `${releasedLocks} terminal-item lock(s) ${releaseVerb}` +
2851
+ (locks.errors.length > 0
2852
+ ? `, ${locks.errors.length} kept on error`
2853
+ : '') +
2854
+ '.');
2527
2855
  return;
2528
2856
  }
2529
2857
  // SCOPE RESOLUTION (this task): the worktree reaper is ARBITER-SCOPED by
@@ -2617,7 +2945,8 @@ export function buildProgram() {
2617
2945
  console.log(` [reaped] ${reaped.slug} \u2014 ${how}`);
2618
2946
  }
2619
2947
  for (const retained of result.retained) {
2620
- console.log(` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}`);
2948
+ console.log(` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}` +
2949
+ (retained.hint ? ` (${retained.hint})` : ''));
2621
2950
  }
2622
2951
  for (const orphan of result.sweptOrphans) {
2623
2952
  console.log(` [swept] ${orphan.dir} \u2014 ${orphan.kind}`);
@@ -2716,7 +3045,8 @@ export function buildProgram() {
2716
3045
  .option('--arbiter <remote>', 'the COORDINATION arbiter remote whose per-item lock refs gate the cwd selection pool (held in-flight items are subtracted); default: origin')
2717
3046
  .option('--no-arbiter', "skip the current repo's arbiter section")
2718
3047
  .option('--here', 'report ONLY the current repo (the cwd working tree, fetch-first): skip the jobs, registry-mirror, and arbiter sections entirely. "This repo, nothing else" — the fast, focused path.')
2719
- .option('--reconcile-locks', '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.')
3048
+ .option('--reconcile-locks', '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.')
3049
+ .option('--all-arbiters', '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.')
2720
3050
  .option('--json', 'output the raw report as JSON')
2721
3051
  .action(async (flags) => {
2722
3052
  const config = resolveGlobalConfig(loadConfig(flags.config), {});
@@ -2728,7 +3058,9 @@ export function buildProgram() {
2728
3058
  config,
2729
3059
  override,
2730
3060
  arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
2731
- lockArbiterRemote: flags.arbiter ?? 'origin',
3061
+ // `--no-arbiter` arrives as `arbiter: false` (commander negation):
3062
+ // never pass `false` on as a remote name.
3063
+ lockArbiterRemote: typeof flags.arbiter === 'string' ? flags.arbiter : 'origin',
2732
3064
  reconcileLocks: flags.reconcileLocks === true,
2733
3065
  warn,
2734
3066
  });
@@ -2756,9 +3088,18 @@ export function buildProgram() {
2756
3088
  // REGISTERED HUB MIRROR (the registry), read from its bare `main` ref
2757
3089
  // through the read seam (mirrors have no working tree).
2758
3090
  const mirrorPaths = listMirrors({ workspacesDir }).map((m) => m.path);
3091
+ // SCOPE of the `--reconcile-locks` WRITE (task
3092
+ // `reconcile-locks-stays-within-the-current-arbiter`): ARBITER-SCOPED by
3093
+ // default, resolved from the cwd exactly like `gc`, so a drain in repo A
3094
+ // never releases repo B's locks. `--all-arbiters` is the explicit, loud
3095
+ // global opt-in. With neither a resolvable cwd arbiter nor the flag we
3096
+ // REFUSE rather than silently widening the write to every arbiter.
3097
+ const reconcileArbiterKey = resolveReconcileArbiterKey('status', flags, config.defaultArbiter);
2759
3098
  // Fold in the current repo's arbiter state (the old `arbiter status`, ADR
2760
3099
  // §1/§7) unless --no-arbiter. Read-only; tolerates not being in a repo.
2761
- const arbiter = flags.noArbiter === true
3100
+ // Commander treats `--no-arbiter` as the NEGATION of `--arbiter <remote>`
3101
+ // (one `arbiter` key): it sets `arbiter: false`, never a `noArbiter` key.
3102
+ const arbiter = flags.arbiter === false
2762
3103
  ? undefined
2763
3104
  : arbiterStatus({
2764
3105
  cwd: process.cwd(),
@@ -2783,6 +3124,7 @@ export function buildProgram() {
2783
3124
  arbiter,
2784
3125
  cwd: cwdSection,
2785
3126
  reconcileLocks: flags.reconcileLocks === true,
3127
+ reconcileArbiterKey,
2786
3128
  warn,
2787
3129
  });
2788
3130
  if (flags.json) {
@@ -2795,7 +3137,7 @@ export function buildProgram() {
2795
3137
  program
2796
3138
  .command('requeue <slug>')
2797
3139
  .helpGroup(HEADLINE_GROUP)
2798
- .description('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).')
3140
+ .description('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).')
2799
3141
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
2800
3142
  .option('--cwd <dir>', 'the repo/working clone whose work/ tree the arbiter remote is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree')
2801
3143
  .option('--arbiter <remote>', 'the arbiter git remote the tree-less move is CAS-published to (default: origin). --cwd resolves this remote; the move is never written to the cwd tree.')
@@ -2825,6 +3167,11 @@ export function buildProgram() {
2825
3167
  '(non-destructive recovery vs destructive last resort). Pick one.');
2826
3168
  process.exit(1);
2827
3169
  }
3170
+ // Where `do --isolated` retains job worktrees: a CRASHED run's retained
3171
+ // worktree may hold local-only commits, which requeue saves (pushes to the
3172
+ // work branch) before it releases the lock (task
3173
+ // `a-crashed-runs-local-work-is-saved-before-its-lock-is-released`).
3174
+ const { workspacesDir } = resolveGlobalConfig(loadConfig(flags.config), {});
2828
3175
  const result = await ledgerWrite.applyReturnToBacklogTransition({
2829
3176
  cwd,
2830
3177
  slug,
@@ -2832,6 +3179,7 @@ export function buildProgram() {
2832
3179
  // arbiter to `origin` so the common case Just Works; `--arbiter` overrides.
2833
3180
  // `--cwd` is purely the ORIGIN SOURCE the remote is resolved from.
2834
3181
  arbiter: flags.arbiter ?? 'origin',
3182
+ workspacesDir,
2835
3183
  reset: flags.reset,
2836
3184
  reconcile,
2837
3185
  message: flags.message,
@@ -2847,7 +3195,10 @@ export function buildProgram() {
2847
3195
  : result.reconciled
2848
3196
  ? ` (--reconcile: re-synced mirror + rebased ${workBranchRef('task', slug)} onto latest main and pushed the reconciled tip back; next claim continues from it)`
2849
3197
  : ' (kept the work branch; next claim continues from its tip)';
2850
- console.log(`Requeued '${slug}' to backlog for re-claiming.${how}`);
3198
+ const saved = result.crashedRunSave?.kind === 'saved'
3199
+ ? ` Saved the crashed run's local work from ${result.crashedRunSave.dir} to ${result.crashedRunSave.branch} first.`
3200
+ : '';
3201
+ console.log(`Requeued '${slug}' to backlog for re-claiming.${how}${saved}`);
2851
3202
  });
2852
3203
  // `promote [item]` (spec `staging-pool-position-gate-and-trust-model`, tasks
2853
3204
  // `pre-backlog-staging-folder-and-promote-step-a` /
@@ -3097,7 +3448,13 @@ export function buildProgram() {
3097
3448
  .option('--harness <adapter>', 'harness adapter that launches the decision agent: null (default) or pi')
3098
3449
  .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
3099
3450
  .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the pi session file is generated')
3451
+ .addOption(phaseOption())
3452
+ .addOption(handoffDirOptions()[0])
3453
+ .addOption(handoffDirOptions()[1])
3454
+ .addOption(agentResultOptions()[0])
3455
+ .addOption(agentResultOptions()[1])
3100
3456
  .action(async (rawNumber, flags) => {
3457
+ enterCliPhase(flags.phase);
3101
3458
  const issueNumber = Number(rawNumber);
3102
3459
  if (rawNumber.trim() === '' ||
3103
3460
  !Number.isInteger(issueNumber) ||
@@ -3188,9 +3545,10 @@ export function buildProgram() {
3188
3545
  console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
3189
3546
  process.exit(1);
3190
3547
  }
3191
- const result = await performIntake({
3548
+ const intakeOptions = {
3192
3549
  issueNumber,
3193
3550
  cwd,
3551
+ phase: flags.phase,
3194
3552
  arbiter: flags.arbiter ?? config.defaultArbiter,
3195
3553
  integration: modes,
3196
3554
  // The resolved cross-job CAS-retry cap (config `mergeRetries`). `intake`
@@ -3230,7 +3588,13 @@ export function buildProgram() {
3230
3588
  // decision/review AGENT launches); absent ⇒ ambient.
3231
3589
  identity: config.identity,
3232
3590
  note: (message) => console.error(`>> ${message}`),
3233
- });
3591
+ };
3592
+ // CI PHASE MODE (task `ci-split-intake`): `--phase` runs ONE job of the
3593
+ // split item workflow for `issue:<N>`, on the SAME intake pipeline.
3594
+ if (flags.phase !== undefined) {
3595
+ await runIntakePhaseAndExit(flags, intakeOptions);
3596
+ }
3597
+ const result = await performIntake(intakeOptions);
3234
3598
  if (result.exitCode !== 0) {
3235
3599
  console.error(`error: ${result.message}`);
3236
3600
  }
@@ -3249,7 +3613,7 @@ export function buildProgram() {
3249
3613
  program
3250
3614
  .command('close-merged-issues')
3251
3615
  .helpGroup(ADVANCED_GROUP)
3252
- .description('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.')
3616
+ .description('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.')
3253
3617
  .option('--cwd <dir>', 'the repo working dir whose work/ tree to scan (default: cwd)')
3254
3618
  .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
3255
3619
  .option('--json', 'output the raw result as JSON')
@@ -3268,6 +3632,9 @@ export function buildProgram() {
3268
3632
  if (c.decision === 'closed') {
3269
3633
  console.error(`>> closed issue #${c.issueNumber} (${c.via} ${c.slug}).`);
3270
3634
  }
3635
+ else if (c.decision === 'already-closed') {
3636
+ console.error(`>> issue #${c.issueNumber} (${c.via} ${c.slug}) already closed.`);
3637
+ }
3271
3638
  else if (c.decision === 'close-failed') {
3272
3639
  console.error(`>> issue #${c.issueNumber} (${c.via} ${c.slug}) NOT closed: ${c.reason}`);
3273
3640
  }
@@ -3281,6 +3648,45 @@ export function buildProgram() {
3281
3648
  // crash (exit 0), exactly like intake's bounce close.
3282
3649
  process.exit(0);
3283
3650
  });
3651
+ // `surface-merge-questions` (task `wire-merge-questions-into-the-advance-tick`,
3652
+ // decision 5): the no-agent entry point the generated `advance-lifecycle`
3653
+ // workflow's `surface-merge-questions` job runs. It asks, in the current
3654
+ // checkout of `main`, a merge question for every unmerged `work/task-<slug>`
3655
+ // branch with no open PR and no live build, and publishes the questions to
3656
+ // `<arbiter>/main`. Gated by `mergeQuestions` (`off` does nothing). The laptop
3657
+ // bare `advance` runs the SAME pass before it selects.
3658
+ program
3659
+ .command('surface-merge-questions')
3660
+ .helpGroup(ADVANCED_GROUP)
3661
+ .description('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.')
3662
+ .option('--arbiter <name>', 'the arbiter remote whose work branches to ask about (default: the configured defaultArbiter)')
3663
+ .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
3664
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3665
+ .action(async (flags) => {
3666
+ const cwd = process.cwd();
3667
+ const { global, override } = loadGlobalAndOverride(flags.config);
3668
+ let config;
3669
+ try {
3670
+ config = resolveRepoConfig({ repoPath: cwd, global, override }).config;
3671
+ }
3672
+ catch (err) {
3673
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
3674
+ process.exit(1);
3675
+ }
3676
+ const pass = await runMergeQuestionTick({
3677
+ cwd,
3678
+ arbiter: flags.arbiter ?? config.defaultArbiter,
3679
+ mergeQuestions: config.mergeQuestions,
3680
+ ghBin: flags.ghBin,
3681
+ env: identityEnv(config.identity, process.env),
3682
+ note: (message) => console.error(`>> ${message}`),
3683
+ });
3684
+ // A failed pass (fetch, lock read, or the GitHub open-PR listing) is
3685
+ // an `error:` line and a non-zero exit, so a CI job cannot go green
3686
+ // while asking about no branch.
3687
+ console.error(pass.failed ? `error: ${pass.message}` : `>> ${pass.message}`);
3688
+ process.exit(pass.failed ? 1 : 0);
3689
+ });
3284
3690
  // The REGISTRY command group (ADR §1): the registered set of targets IS the
3285
3691
  // hub-mirror set on disk. `remote add --local` absorbs the old `arbiter init`;
3286
3692
  // `arbiter status` is folded into `status`. There is no standalone `arbiter`
@@ -3408,7 +3814,7 @@ export function buildProgram() {
3408
3814
  program
3409
3815
  .command('install-ci')
3410
3816
  .helpGroup(ADVANCED_GROUP)
3411
- .description('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).')
3817
+ .description('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).')
3412
3818
  .option('--config <file>', 'non-interactive: load the CI config from this JSON file (skips the wizard)')
3413
3819
  .option('--export-config <file>', 'write the gathered config as JSON to this path instead of generating artifacts')
3414
3820
  .option('--include-secrets', '(with --export-config) also gather + include the secret values in the export')
@@ -3485,6 +3891,25 @@ export function buildProgram() {
3485
3891
  const result = installSkills({ global: flags.local !== true });
3486
3892
  console.log(formatSkillsAddReport(result, flags.local === true));
3487
3893
  });
3894
+ // Decision 9 (task `ci-split-warn-single-job-workflows`): warn before an
3895
+ // agent-spawning verb runs in an old single-job CI workflow (GitHub Actions,
3896
+ // no `--phase`, a credential persisted in the checkout's git config).
3897
+ // Warn-only: the action still runs.
3898
+ program.hook('preAction', (_program, actionCommand) => {
3899
+ if (actionCommand.parent !== program) {
3900
+ return;
3901
+ }
3902
+ const phase = actionCommand.opts().phase;
3903
+ const warning = singleJobWarning({
3904
+ verb: actionCommand.name(),
3905
+ phase,
3906
+ env: process.env,
3907
+ cwd: process.cwd(),
3908
+ });
3909
+ if (warning !== undefined) {
3910
+ console.error(warning);
3911
+ }
3912
+ });
3488
3913
  return program;
3489
3914
  }
3490
3915
  /**