dorfl 0.0.0 → 0.1.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 (619) hide show
  1. package/dist/advance-ci-template.d.ts +73 -0
  2. package/dist/advance-ci-template.d.ts.map +1 -0
  3. package/dist/advance-ci-template.js +104 -0
  4. package/dist/advance-ci-template.js.map +1 -0
  5. package/dist/advance-classify.d.ts +132 -0
  6. package/dist/advance-classify.d.ts.map +1 -0
  7. package/dist/advance-classify.js +120 -0
  8. package/dist/advance-classify.js.map +1 -0
  9. package/dist/advance-drivers.d.ts +182 -0
  10. package/dist/advance-drivers.d.ts.map +1 -0
  11. package/dist/advance-drivers.js +231 -0
  12. package/dist/advance-drivers.js.map +1 -0
  13. package/dist/advance-isolated.d.ts +156 -0
  14. package/dist/advance-isolated.d.ts.map +1 -0
  15. package/dist/advance-isolated.js +256 -0
  16. package/dist/advance-isolated.js.map +1 -0
  17. package/dist/advance-lifecycle-template.d.ts +107 -0
  18. package/dist/advance-lifecycle-template.d.ts.map +1 -0
  19. package/dist/advance-lifecycle-template.js +668 -0
  20. package/dist/advance-lifecycle-template.js.map +1 -0
  21. package/dist/advance-loop-driver.d.ts +325 -0
  22. package/dist/advance-loop-driver.d.ts.map +1 -0
  23. package/dist/advance-loop-driver.js +437 -0
  24. package/dist/advance-loop-driver.js.map +1 -0
  25. package/dist/advance-treeless-publish.d.ts +108 -0
  26. package/dist/advance-treeless-publish.d.ts.map +1 -0
  27. package/dist/advance-treeless-publish.js +71 -0
  28. package/dist/advance-treeless-publish.js.map +1 -0
  29. package/dist/advance.d.ts +340 -0
  30. package/dist/advance.d.ts.map +1 -0
  31. package/dist/advance.js +1122 -0
  32. package/dist/advance.js.map +1 -0
  33. package/dist/advancing-lock.d.ts +294 -0
  34. package/dist/advancing-lock.d.ts.map +1 -0
  35. package/dist/advancing-lock.js +594 -0
  36. package/dist/advancing-lock.js.map +1 -0
  37. package/dist/agent-launch.d.ts +79 -0
  38. package/dist/agent-launch.d.ts.map +1 -0
  39. package/dist/agent-launch.js +61 -0
  40. package/dist/agent-launch.js.map +1 -0
  41. package/dist/agent-stop.d.ts +149 -0
  42. package/dist/agent-stop.d.ts.map +1 -0
  43. package/dist/agent-stop.js +307 -0
  44. package/dist/agent-stop.js.map +1 -0
  45. package/dist/apply-decide.d.ts +127 -0
  46. package/dist/apply-decide.d.ts.map +1 -0
  47. package/dist/apply-decide.js +176 -0
  48. package/dist/apply-decide.js.map +1 -0
  49. package/dist/apply-merge-action.d.ts +206 -0
  50. package/dist/apply-merge-action.d.ts.map +1 -0
  51. package/dist/apply-merge-action.js +307 -0
  52. package/dist/apply-merge-action.js.map +1 -0
  53. package/dist/apply-persist.d.ts +174 -0
  54. package/dist/apply-persist.d.ts.map +1 -0
  55. package/dist/apply-persist.js +359 -0
  56. package/dist/apply-persist.js.map +1 -0
  57. package/dist/arbiter.d.ts +120 -0
  58. package/dist/arbiter.d.ts.map +1 -0
  59. package/dist/arbiter.js +255 -0
  60. package/dist/arbiter.js.map +1 -0
  61. package/dist/brand.d.ts +70 -0
  62. package/dist/brand.d.ts.map +1 -0
  63. package/dist/brand.js +84 -0
  64. package/dist/brand.js.map +1 -0
  65. package/dist/buildable-body.d.ts +132 -0
  66. package/dist/buildable-body.d.ts.map +1 -0
  67. package/dist/buildable-body.js +131 -0
  68. package/dist/buildable-body.js.map +1 -0
  69. package/dist/categorise.d.ts +66 -0
  70. package/dist/categorise.d.ts.map +1 -0
  71. package/dist/categorise.js +106 -0
  72. package/dist/categorise.js.map +1 -0
  73. package/dist/claim-cas.d.ts +117 -0
  74. package/dist/claim-cas.d.ts.map +1 -0
  75. package/dist/claim-cas.js +312 -0
  76. package/dist/claim-cas.js.map +1 -0
  77. package/dist/cli-spinner.d.ts +112 -0
  78. package/dist/cli-spinner.d.ts.map +1 -0
  79. package/dist/cli-spinner.js +157 -0
  80. package/dist/cli-spinner.js.map +1 -0
  81. package/dist/cli.d.ts +11 -0
  82. package/dist/cli.d.ts.map +1 -0
  83. package/dist/cli.js +3094 -0
  84. package/dist/cli.js.map +1 -0
  85. package/dist/close-job-template.d.ts +70 -0
  86. package/dist/close-job-template.d.ts.map +1 -0
  87. package/dist/close-job-template.js +180 -0
  88. package/dist/close-job-template.js.map +1 -0
  89. package/dist/close-job.d.ts +95 -0
  90. package/dist/close-job.d.ts.map +1 -0
  91. package/dist/close-job.js +226 -0
  92. package/dist/close-job.js.map +1 -0
  93. package/dist/complete.d.ts +361 -0
  94. package/dist/complete.d.ts.map +1 -0
  95. package/dist/complete.js +885 -0
  96. package/dist/complete.js.map +1 -0
  97. package/dist/concurrency.d.ts +68 -0
  98. package/dist/concurrency.d.ts.map +1 -0
  99. package/dist/concurrency.js +112 -0
  100. package/dist/concurrency.js.map +1 -0
  101. package/dist/config-override.d.ts +76 -0
  102. package/dist/config-override.d.ts.map +1 -0
  103. package/dist/config-override.js +50 -0
  104. package/dist/config-override.js.map +1 -0
  105. package/dist/config.d.ts +668 -0
  106. package/dist/config.d.ts.map +1 -0
  107. package/dist/config.js +241 -0
  108. package/dist/config.js.map +1 -0
  109. package/dist/continue-branch.d.ts +249 -0
  110. package/dist/continue-branch.d.ts.map +1 -0
  111. package/dist/continue-branch.js +389 -0
  112. package/dist/continue-branch.js.map +1 -0
  113. package/dist/cwd-section.d.ts +186 -0
  114. package/dist/cwd-section.d.ts.map +1 -0
  115. package/dist/cwd-section.js +209 -0
  116. package/dist/cwd-section.js.map +1 -0
  117. package/dist/decision-engine.d.ts +170 -0
  118. package/dist/decision-engine.d.ts.map +1 -0
  119. package/dist/decision-engine.js +136 -0
  120. package/dist/decision-engine.js.map +1 -0
  121. package/dist/detect.d.ts +17 -0
  122. package/dist/detect.d.ts.map +1 -0
  123. package/dist/detect.js +118 -0
  124. package/dist/detect.js.map +1 -0
  125. package/dist/do-autopick.d.ts +85 -0
  126. package/dist/do-autopick.d.ts.map +1 -0
  127. package/dist/do-autopick.js +112 -0
  128. package/dist/do-autopick.js.map +1 -0
  129. package/dist/do-config.d.ts +312 -0
  130. package/dist/do-config.d.ts.map +1 -0
  131. package/dist/do-config.js +358 -0
  132. package/dist/do-config.js.map +1 -0
  133. package/dist/do-remote-auto.d.ts +75 -0
  134. package/dist/do-remote-auto.d.ts.map +1 -0
  135. package/dist/do-remote-auto.js +111 -0
  136. package/dist/do-remote-auto.js.map +1 -0
  137. package/dist/do.d.ts +621 -0
  138. package/dist/do.d.ts.map +1 -0
  139. package/dist/do.js +1882 -0
  140. package/dist/do.js.map +1 -0
  141. package/dist/drop-source.d.ts +96 -0
  142. package/dist/drop-source.d.ts.map +1 -0
  143. package/dist/drop-source.js +91 -0
  144. package/dist/drop-source.js.map +1 -0
  145. package/dist/eligibility.d.ts +46 -0
  146. package/dist/eligibility.d.ts.map +1 -0
  147. package/dist/eligibility.js +34 -0
  148. package/dist/eligibility.js.map +1 -0
  149. package/dist/env-config.d.ts +51 -0
  150. package/dist/env-config.d.ts.map +1 -0
  151. package/dist/env-config.js +272 -0
  152. package/dist/env-config.js.map +1 -0
  153. package/dist/failure-cause.d.ts +70 -0
  154. package/dist/failure-cause.d.ts.map +1 -0
  155. package/dist/failure-cause.js +126 -0
  156. package/dist/failure-cause.js.map +1 -0
  157. package/dist/format.d.ts +43 -0
  158. package/dist/format.d.ts.map +1 -0
  159. package/dist/format.js +256 -0
  160. package/dist/format.js.map +1 -0
  161. package/dist/frontmatter.d.ts +215 -0
  162. package/dist/frontmatter.d.ts.map +1 -0
  163. package/dist/frontmatter.js +345 -0
  164. package/dist/frontmatter.js.map +1 -0
  165. package/dist/gate-readiness.d.ts +84 -0
  166. package/dist/gate-readiness.d.ts.map +1 -0
  167. package/dist/gate-readiness.js +103 -0
  168. package/dist/gate-readiness.js.map +1 -0
  169. package/dist/gc.d.ts +165 -0
  170. package/dist/gc.d.ts.map +1 -0
  171. package/dist/gc.js +313 -0
  172. package/dist/gc.js.map +1 -0
  173. package/dist/gh-failure.d.ts +42 -0
  174. package/dist/gh-failure.d.ts.map +1 -0
  175. package/dist/gh-failure.js +49 -0
  176. package/dist/gh-failure.js.map +1 -0
  177. package/dist/git.d.ts +75 -0
  178. package/dist/git.d.ts.map +1 -0
  179. package/dist/git.js +130 -0
  180. package/dist/git.js.map +1 -0
  181. package/dist/github.d.ts +187 -0
  182. package/dist/github.d.ts.map +1 -0
  183. package/dist/github.js +343 -0
  184. package/dist/github.js.map +1 -0
  185. package/dist/harness.d.ts +242 -0
  186. package/dist/harness.d.ts.map +1 -0
  187. package/dist/harness.js +157 -0
  188. package/dist/harness.js.map +1 -0
  189. package/dist/identity.d.ts +167 -0
  190. package/dist/identity.d.ts.map +1 -0
  191. package/dist/identity.js +231 -0
  192. package/dist/identity.js.map +1 -0
  193. package/dist/index.d.ts +147 -0
  194. package/dist/index.d.ts.map +1 -0
  195. package/dist/index.js +76 -0
  196. package/dist/index.js.map +1 -0
  197. package/dist/install-ci-branch-protection.d.ts +147 -0
  198. package/dist/install-ci-branch-protection.d.ts.map +1 -0
  199. package/dist/install-ci-branch-protection.js +166 -0
  200. package/dist/install-ci-branch-protection.js.map +1 -0
  201. package/dist/install-ci-capabilities/advance-lifecycle.d.ts +15 -0
  202. package/dist/install-ci-capabilities/advance-lifecycle.d.ts.map +1 -0
  203. package/dist/install-ci-capabilities/advance-lifecycle.js +28 -0
  204. package/dist/install-ci-capabilities/advance-lifecycle.js.map +1 -0
  205. package/dist/install-ci-capabilities/close-job.d.ts +13 -0
  206. package/dist/install-ci-capabilities/close-job.d.ts.map +1 -0
  207. package/dist/install-ci-capabilities/close-job.js +26 -0
  208. package/dist/install-ci-capabilities/close-job.js.map +1 -0
  209. package/dist/install-ci-capabilities/example-noop.d.ts +16 -0
  210. package/dist/install-ci-capabilities/example-noop.d.ts.map +1 -0
  211. package/dist/install-ci-capabilities/example-noop.js +23 -0
  212. package/dist/install-ci-capabilities/example-noop.js.map +1 -0
  213. package/dist/install-ci-capabilities/intake.d.ts +15 -0
  214. package/dist/install-ci-capabilities/intake.d.ts.map +1 -0
  215. package/dist/install-ci-capabilities/intake.js +28 -0
  216. package/dist/install-ci-capabilities/intake.js.map +1 -0
  217. package/dist/install-ci-capabilities/verify.d.ts +14 -0
  218. package/dist/install-ci-capabilities/verify.d.ts.map +1 -0
  219. package/dist/install-ci-capabilities/verify.js +27 -0
  220. package/dist/install-ci-capabilities/verify.js.map +1 -0
  221. package/dist/install-ci-core.d.ts +446 -0
  222. package/dist/install-ci-core.d.ts.map +1 -0
  223. package/dist/install-ci-core.js +760 -0
  224. package/dist/install-ci-core.js.map +1 -0
  225. package/dist/install-ci-github.d.ts +167 -0
  226. package/dist/install-ci-github.d.ts.map +1 -0
  227. package/dist/install-ci-github.js +315 -0
  228. package/dist/install-ci-github.js.map +1 -0
  229. package/dist/install-ci.d.ts +105 -0
  230. package/dist/install-ci.d.ts.map +1 -0
  231. package/dist/install-ci.js +363 -0
  232. package/dist/install-ci.js.map +1 -0
  233. package/dist/intake-event.d.ts +88 -0
  234. package/dist/intake-event.d.ts.map +1 -0
  235. package/dist/intake-event.js +66 -0
  236. package/dist/intake-event.js.map +1 -0
  237. package/dist/intake-marker.d.ts +95 -0
  238. package/dist/intake-marker.d.ts.map +1 -0
  239. package/dist/intake-marker.js +127 -0
  240. package/dist/intake-marker.js.map +1 -0
  241. package/dist/intake-triage.d.ts +48 -0
  242. package/dist/intake-triage.d.ts.map +1 -0
  243. package/dist/intake-triage.js +95 -0
  244. package/dist/intake-triage.js.map +1 -0
  245. package/dist/intake-trigger-template.d.ts +185 -0
  246. package/dist/intake-trigger-template.d.ts.map +1 -0
  247. package/dist/intake-trigger-template.js +449 -0
  248. package/dist/intake-trigger-template.js.map +1 -0
  249. package/dist/intake.d.ts +569 -0
  250. package/dist/intake.d.ts.map +1 -0
  251. package/dist/intake.js +1628 -0
  252. package/dist/intake.js.map +1 -0
  253. package/dist/integration-core.d.ts +539 -0
  254. package/dist/integration-core.d.ts.map +1 -0
  255. package/dist/integration-core.js +2195 -0
  256. package/dist/integration-core.js.map +1 -0
  257. package/dist/integrator.d.ts +343 -0
  258. package/dist/integrator.d.ts.map +1 -0
  259. package/dist/integrator.js +400 -0
  260. package/dist/integrator.js.map +1 -0
  261. package/dist/isolation.d.ts +219 -0
  262. package/dist/isolation.d.ts.map +1 -0
  263. package/dist/isolation.js +261 -0
  264. package/dist/isolation.js.map +1 -0
  265. package/dist/issue-provider.d.ts +349 -0
  266. package/dist/issue-provider.d.ts.map +1 -0
  267. package/dist/issue-provider.js +360 -0
  268. package/dist/issue-provider.js.map +1 -0
  269. package/dist/item-lock.d.ts +626 -0
  270. package/dist/item-lock.d.ts.map +1 -0
  271. package/dist/item-lock.js +1381 -0
  272. package/dist/item-lock.js.map +1 -0
  273. package/dist/item-path.d.ts +49 -0
  274. package/dist/item-path.d.ts.map +1 -0
  275. package/dist/item-path.js +66 -0
  276. package/dist/item-path.js.map +1 -0
  277. package/dist/ledger-lint.d.ts +129 -0
  278. package/dist/ledger-lint.d.ts.map +1 -0
  279. package/dist/ledger-lint.js +249 -0
  280. package/dist/ledger-lint.js.map +1 -0
  281. package/dist/ledger-read.d.ts +357 -0
  282. package/dist/ledger-read.d.ts.map +1 -0
  283. package/dist/ledger-read.js +442 -0
  284. package/dist/ledger-read.js.map +1 -0
  285. package/dist/ledger-write.d.ts +330 -0
  286. package/dist/ledger-write.d.ts.map +1 -0
  287. package/dist/ledger-write.js +411 -0
  288. package/dist/ledger-write.js.map +1 -0
  289. package/dist/lifecycle-gather.d.ts +30 -0
  290. package/dist/lifecycle-gather.d.ts.map +1 -0
  291. package/dist/lifecycle-gather.js +205 -0
  292. package/dist/lifecycle-gather.js.map +1 -0
  293. package/dist/lifecycle-pools.d.ts +180 -0
  294. package/dist/lifecycle-pools.d.ts.map +1 -0
  295. package/dist/lifecycle-pools.js +78 -0
  296. package/dist/lifecycle-pools.js.map +1 -0
  297. package/dist/merge-question-surfacer.d.ts +166 -0
  298. package/dist/merge-question-surfacer.d.ts.map +1 -0
  299. package/dist/merge-question-surfacer.js +297 -0
  300. package/dist/merge-question-surfacer.js.map +1 -0
  301. package/dist/mint-adr.d.ts +126 -0
  302. package/dist/mint-adr.d.ts.map +1 -0
  303. package/dist/mint-adr.js +257 -0
  304. package/dist/mint-adr.js.map +1 -0
  305. package/dist/mirror-pool-scan.d.ts +125 -0
  306. package/dist/mirror-pool-scan.d.ts.map +1 -0
  307. package/dist/mirror-pool-scan.js +104 -0
  308. package/dist/mirror-pool-scan.js.map +1 -0
  309. package/dist/needs-attention.d.ts +341 -0
  310. package/dist/needs-attention.d.ts.map +1 -0
  311. package/dist/needs-attention.js +900 -0
  312. package/dist/needs-attention.js.map +1 -0
  313. package/dist/orphan-sidecar.d.ts +79 -0
  314. package/dist/orphan-sidecar.d.ts.map +1 -0
  315. package/dist/orphan-sidecar.js +71 -0
  316. package/dist/orphan-sidecar.js.map +1 -0
  317. package/dist/output.d.ts +48 -0
  318. package/dist/output.d.ts.map +1 -0
  319. package/dist/output.js +66 -0
  320. package/dist/output.js.map +1 -0
  321. package/dist/pi-harness.d.ts +179 -0
  322. package/dist/pi-harness.d.ts.map +1 -0
  323. package/dist/pi-harness.js +342 -0
  324. package/dist/pi-harness.js.map +1 -0
  325. package/dist/placement.d.ts +99 -0
  326. package/dist/placement.d.ts.map +1 -0
  327. package/dist/placement.js +67 -0
  328. package/dist/placement.js.map +1 -0
  329. package/dist/prd-to-spec.d.ts +315 -0
  330. package/dist/prd-to-spec.d.ts.map +1 -0
  331. package/dist/prd-to-spec.js +684 -0
  332. package/dist/prd-to-spec.js.map +1 -0
  333. package/dist/prepare.d.ts +121 -0
  334. package/dist/prepare.d.ts.map +1 -0
  335. package/dist/prepare.js +140 -0
  336. package/dist/prepare.js.map +1 -0
  337. package/dist/prompt.d.ts +360 -0
  338. package/dist/prompt.d.ts.map +1 -0
  339. package/dist/prompt.js +499 -0
  340. package/dist/prompt.js.map +1 -0
  341. package/dist/protocol/ADR-FORMAT.md +47 -0
  342. package/dist/protocol/CLAIM-PROTOCOL.md +217 -0
  343. package/dist/protocol/REVIEW-PROTOCOL.md +119 -0
  344. package/dist/protocol/SURFACE-PROTOCOL.md +121 -0
  345. package/dist/protocol/TASKING-PROTOCOL.md +122 -0
  346. package/dist/protocol/WORK-CONTRACT.md +276 -0
  347. package/dist/protocol/spec-template.md +71 -0
  348. package/dist/protocol/task-template.md +65 -0
  349. package/dist/readiness.d.ts +66 -0
  350. package/dist/readiness.d.ts.map +1 -0
  351. package/dist/readiness.js +36 -0
  352. package/dist/readiness.js.map +1 -0
  353. package/dist/reap-branches.d.ts +102 -0
  354. package/dist/reap-branches.d.ts.map +1 -0
  355. package/dist/reap-branches.js +149 -0
  356. package/dist/reap-branches.js.map +1 -0
  357. package/dist/recover-isolated.d.ts +72 -0
  358. package/dist/recover-isolated.d.ts.map +1 -0
  359. package/dist/recover-isolated.js +188 -0
  360. package/dist/recover-isolated.js.map +1 -0
  361. package/dist/registry.d.ts +172 -0
  362. package/dist/registry.d.ts.map +1 -0
  363. package/dist/registry.js +296 -0
  364. package/dist/registry.js.map +1 -0
  365. package/dist/repo-config.d.ts +201 -0
  366. package/dist/repo-config.d.ts.map +1 -0
  367. package/dist/repo-config.js +414 -0
  368. package/dist/repo-config.js.map +1 -0
  369. package/dist/repo-key.d.ts +20 -0
  370. package/dist/repo-key.d.ts.map +1 -0
  371. package/dist/repo-key.js +68 -0
  372. package/dist/repo-key.js.map +1 -0
  373. package/dist/repo-mirror.d.ts +177 -0
  374. package/dist/repo-mirror.d.ts.map +1 -0
  375. package/dist/repo-mirror.js +271 -0
  376. package/dist/repo-mirror.js.map +1 -0
  377. package/dist/retry-backoff.d.ts +90 -0
  378. package/dist/retry-backoff.d.ts.map +1 -0
  379. package/dist/retry-backoff.js +98 -0
  380. package/dist/retry-backoff.js.map +1 -0
  381. package/dist/review-gate.d.ts +173 -0
  382. package/dist/review-gate.d.ts.map +1 -0
  383. package/dist/review-gate.js +261 -0
  384. package/dist/review-gate.js.map +1 -0
  385. package/dist/review-verdict.d.ts +149 -0
  386. package/dist/review-verdict.d.ts.map +1 -0
  387. package/dist/review-verdict.js +332 -0
  388. package/dist/review-verdict.js.map +1 -0
  389. package/dist/run.d.ts +221 -0
  390. package/dist/run.d.ts.map +1 -0
  391. package/dist/run.js +963 -0
  392. package/dist/run.js.map +1 -0
  393. package/dist/scan.d.ts +308 -0
  394. package/dist/scan.d.ts.map +1 -0
  395. package/dist/scan.js +374 -0
  396. package/dist/scan.js.map +1 -0
  397. package/dist/select-order.d.ts +75 -0
  398. package/dist/select-order.d.ts.map +1 -0
  399. package/dist/select-order.js +108 -0
  400. package/dist/select-order.js.map +1 -0
  401. package/dist/select-priority.d.ts +188 -0
  402. package/dist/select-priority.d.ts.map +1 -0
  403. package/dist/select-priority.js +80 -0
  404. package/dist/select-priority.js.map +1 -0
  405. package/dist/select.d.ts +25 -0
  406. package/dist/select.d.ts.map +1 -0
  407. package/dist/select.js +43 -0
  408. package/dist/select.js.map +1 -0
  409. package/dist/session-path.d.ts +36 -0
  410. package/dist/session-path.d.ts.map +1 -0
  411. package/dist/session-path.js +129 -0
  412. package/dist/session-path.js.map +1 -0
  413. package/dist/sidecar-apply.d.ts +83 -0
  414. package/dist/sidecar-apply.d.ts.map +1 -0
  415. package/dist/sidecar-apply.js +111 -0
  416. package/dist/sidecar-apply.js.map +1 -0
  417. package/dist/sidecar.d.ts +245 -0
  418. package/dist/sidecar.d.ts.map +1 -0
  419. package/dist/sidecar.js +481 -0
  420. package/dist/sidecar.js.map +1 -0
  421. package/dist/slug-namespace.d.ts +204 -0
  422. package/dist/slug-namespace.d.ts.map +1 -0
  423. package/dist/slug-namespace.js +229 -0
  424. package/dist/slug-namespace.js.map +1 -0
  425. package/dist/spec-complete.d.ts +44 -0
  426. package/dist/spec-complete.d.ts.map +1 -0
  427. package/dist/spec-complete.js +69 -0
  428. package/dist/spec-complete.js.map +1 -0
  429. package/dist/start.d.ts +97 -0
  430. package/dist/start.d.ts.map +1 -0
  431. package/dist/start.js +633 -0
  432. package/dist/start.js.map +1 -0
  433. package/dist/status.d.ts +199 -0
  434. package/dist/status.d.ts.map +1 -0
  435. package/dist/status.js +228 -0
  436. package/dist/status.js.map +1 -0
  437. package/dist/surface-gate.d.ts +162 -0
  438. package/dist/surface-gate.d.ts.map +1 -0
  439. package/dist/surface-gate.js +206 -0
  440. package/dist/surface-gate.js.map +1 -0
  441. package/dist/surface-persist.d.ts +86 -0
  442. package/dist/surface-persist.d.ts.map +1 -0
  443. package/dist/surface-persist.js +129 -0
  444. package/dist/surface-persist.js.map +1 -0
  445. package/dist/tasker-review-loop.d.ts +249 -0
  446. package/dist/tasker-review-loop.d.ts.map +1 -0
  447. package/dist/tasker-review-loop.js +369 -0
  448. package/dist/tasker-review-loop.js.map +1 -0
  449. package/dist/tasking-eligibility.d.ts +74 -0
  450. package/dist/tasking-eligibility.d.ts.map +1 -0
  451. package/dist/tasking-eligibility.js +52 -0
  452. package/dist/tasking-eligibility.js.map +1 -0
  453. package/dist/tasking-lock.d.ts +111 -0
  454. package/dist/tasking-lock.d.ts.map +1 -0
  455. package/dist/tasking-lock.js +256 -0
  456. package/dist/tasking-lock.js.map +1 -0
  457. package/dist/tasking.d.ts +275 -0
  458. package/dist/tasking.d.ts.map +1 -0
  459. package/dist/tasking.js +952 -0
  460. package/dist/tasking.js.map +1 -0
  461. package/dist/triage-gate.d.ts +127 -0
  462. package/dist/triage-gate.d.ts.map +1 -0
  463. package/dist/triage-gate.js +139 -0
  464. package/dist/triage-gate.js.map +1 -0
  465. package/dist/triage-persist.d.ts +163 -0
  466. package/dist/triage-persist.d.ts.map +1 -0
  467. package/dist/triage-persist.js +387 -0
  468. package/dist/triage-persist.js.map +1 -0
  469. package/dist/verdict-json.d.ts +32 -0
  470. package/dist/verdict-json.d.ts.map +1 -0
  471. package/dist/verdict-json.js +74 -0
  472. package/dist/verdict-json.js.map +1 -0
  473. package/dist/verify-workflow-template.d.ts +60 -0
  474. package/dist/verify-workflow-template.d.ts.map +1 -0
  475. package/dist/verify-workflow-template.js +126 -0
  476. package/dist/verify-workflow-template.js.map +1 -0
  477. package/dist/verify.d.ts +60 -0
  478. package/dist/verify.d.ts.map +1 -0
  479. package/dist/verify.js +62 -0
  480. package/dist/verify.js.map +1 -0
  481. package/dist/watch-session.d.ts +112 -0
  482. package/dist/watch-session.d.ts.map +1 -0
  483. package/dist/watch-session.js +347 -0
  484. package/dist/watch-session.js.map +1 -0
  485. package/dist/work-layout.d.ts +198 -0
  486. package/dist/work-layout.d.ts.map +1 -0
  487. package/dist/work-layout.js +217 -0
  488. package/dist/work-layout.js.map +1 -0
  489. package/dist/work-on.d.ts +154 -0
  490. package/dist/work-on.d.ts.map +1 -0
  491. package/dist/work-on.js +387 -0
  492. package/dist/work-on.js.map +1 -0
  493. package/dist/workspace.d.ts +224 -0
  494. package/dist/workspace.d.ts.map +1 -0
  495. package/dist/workspace.js +325 -0
  496. package/dist/workspace.js.map +1 -0
  497. package/package.json +46 -2
  498. package/src/advance-ci-template.ts +203 -0
  499. package/src/advance-classify.ts +197 -0
  500. package/src/advance-drivers.ts +414 -0
  501. package/src/advance-isolated.ts +432 -0
  502. package/src/advance-lifecycle-template.ts +791 -0
  503. package/src/advance-loop-driver.ts +745 -0
  504. package/src/advance-treeless-publish.ts +177 -0
  505. package/src/advance.ts +1564 -0
  506. package/src/advancing-lock.ts +988 -0
  507. package/src/agent-launch.ts +137 -0
  508. package/src/agent-stop.ts +361 -0
  509. package/src/apply-decide.ts +242 -0
  510. package/src/apply-merge-action.ts +502 -0
  511. package/src/apply-persist.ts +518 -0
  512. package/src/arbiter.ts +372 -0
  513. package/src/brand.ts +111 -0
  514. package/src/buildable-body.ts +196 -0
  515. package/src/categorise.ts +158 -0
  516. package/src/claim-cas.ts +513 -0
  517. package/src/cli-spinner.ts +225 -0
  518. package/src/cli.ts +4369 -0
  519. package/src/close-job-template.ts +236 -0
  520. package/src/close-job.ts +319 -0
  521. package/src/complete.ts +1379 -0
  522. package/src/concurrency.ts +151 -0
  523. package/src/config-override.ts +116 -0
  524. package/src/config.ts +883 -0
  525. package/src/continue-branch.ts +542 -0
  526. package/src/cwd-section.ts +392 -0
  527. package/src/decision-engine.ts +272 -0
  528. package/src/detect.ts +124 -0
  529. package/src/do-autopick.ts +223 -0
  530. package/src/do-config.ts +589 -0
  531. package/src/do-remote-auto.ts +197 -0
  532. package/src/do.ts +2623 -0
  533. package/src/drop-source.ts +194 -0
  534. package/src/eligibility.ts +79 -0
  535. package/src/env-config.ts +305 -0
  536. package/src/failure-cause.ts +142 -0
  537. package/src/format.ts +313 -0
  538. package/src/frontmatter.ts +485 -0
  539. package/src/gate-readiness.ts +147 -0
  540. package/src/gc.ts +510 -0
  541. package/src/gh-failure.ts +53 -0
  542. package/src/git.ts +186 -0
  543. package/src/github.ts +468 -0
  544. package/src/harness.ts +355 -0
  545. package/src/identity.ts +322 -0
  546. package/src/index.ts +785 -0
  547. package/src/install-ci-branch-protection.ts +255 -0
  548. package/src/install-ci-capabilities/advance-lifecycle.ts +34 -0
  549. package/src/install-ci-capabilities/close-job.ts +32 -0
  550. package/src/install-ci-capabilities/example-noop.ts +24 -0
  551. package/src/install-ci-capabilities/intake.ts +34 -0
  552. package/src/install-ci-capabilities/verify.ts +33 -0
  553. package/src/install-ci-core.ts +1088 -0
  554. package/src/install-ci-github.ts +376 -0
  555. package/src/install-ci.ts +552 -0
  556. package/src/intake-event.ts +102 -0
  557. package/src/intake-marker.ts +195 -0
  558. package/src/intake-triage.ts +138 -0
  559. package/src/intake-trigger-template.ts +591 -0
  560. package/src/intake.ts +2445 -0
  561. package/src/integration-core.ts +3065 -0
  562. package/src/integrator.ts +771 -0
  563. package/src/isolation.ts +484 -0
  564. package/src/issue-provider.ts +733 -0
  565. package/src/item-lock.ts +1858 -0
  566. package/src/item-path.ts +75 -0
  567. package/src/ledger-lint.ts +332 -0
  568. package/src/ledger-read.ts +924 -0
  569. package/src/ledger-write.ts +865 -0
  570. package/src/lifecycle-gather.ts +298 -0
  571. package/src/lifecycle-pools.ts +250 -0
  572. package/src/merge-question-surfacer.ts +496 -0
  573. package/src/mint-adr.ts +362 -0
  574. package/src/mirror-pool-scan.ts +240 -0
  575. package/src/needs-attention.ts +1506 -0
  576. package/src/orphan-sidecar.ts +150 -0
  577. package/src/output.ts +89 -0
  578. package/src/pi-harness.ts +403 -0
  579. package/src/placement.ts +131 -0
  580. package/src/prd-to-spec.ts +1023 -0
  581. package/src/prepare.ts +230 -0
  582. package/src/prompt.ts +760 -0
  583. package/src/readiness.ts +98 -0
  584. package/src/reap-branches.ts +278 -0
  585. package/src/recover-isolated.ts +276 -0
  586. package/src/registry.ts +475 -0
  587. package/src/repo-config.ts +550 -0
  588. package/src/repo-key.ts +74 -0
  589. package/src/repo-mirror.ts +367 -0
  590. package/src/retry-backoff.ts +130 -0
  591. package/src/review-gate.ts +389 -0
  592. package/src/review-verdict.ts +422 -0
  593. package/src/run.ts +1430 -0
  594. package/src/scan.ts +611 -0
  595. package/src/select-order.ts +143 -0
  596. package/src/select-priority.ts +266 -0
  597. package/src/select.ts +62 -0
  598. package/src/session-path.ts +153 -0
  599. package/src/sidecar-apply.ts +216 -0
  600. package/src/sidecar.ts +700 -0
  601. package/src/slug-namespace.ts +367 -0
  602. package/src/spec-complete.ts +118 -0
  603. package/src/start.ts +974 -0
  604. package/src/status.ts +441 -0
  605. package/src/surface-gate.ts +337 -0
  606. package/src/surface-persist.ts +241 -0
  607. package/src/tasker-review-loop.ts +671 -0
  608. package/src/tasking-eligibility.ts +114 -0
  609. package/src/tasking-lock.ts +416 -0
  610. package/src/tasking.ts +1438 -0
  611. package/src/triage-gate.ts +248 -0
  612. package/src/triage-persist.ts +570 -0
  613. package/src/verdict-json.ts +73 -0
  614. package/src/verify-workflow-template.ts +159 -0
  615. package/src/verify.ts +123 -0
  616. package/src/watch-session.ts +397 -0
  617. package/src/work-layout.ts +262 -0
  618. package/src/work-on.ts +660 -0
  619. package/src/workspace.ts +502 -0
package/src/cli.ts ADDED
@@ -0,0 +1,4369 @@
1
+ #!/usr/bin/env node
2
+ import {Command, Option} from 'commander';
3
+ import type {Command as Commander} from 'commander';
4
+ import {createInterface} from 'node:readline';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {realpathSync, mkdirSync, rmSync} from 'node:fs';
7
+ import {join as joinPath} from 'node:path';
8
+ import {git} from './git.js';
9
+ import {
10
+ loadConfig,
11
+ mergeConfig,
12
+ defaultConfigPath,
13
+ type Config,
14
+ type PartialConfig,
15
+ } from './config.js';
16
+ import {
17
+ defaultConfigOverridePath,
18
+ loadConfigOverride,
19
+ type ConfigOverrideMap,
20
+ } from './config-override.js';
21
+ import {envOverrides} from './env-config.js';
22
+ import {scan} from './scan.js';
23
+ import {remoteAdd, remoteRm, listMirrors, RegistryError} from './registry.js';
24
+ import {findParticipatingRepos} from './detect.js';
25
+ import {formatReport} from './format.js';
26
+ import {resolveCwdSection, cwdSectionDisposition} from './cwd-section.js';
27
+ import {
28
+ runOnce,
29
+ runLoop,
30
+ type ItemResult,
31
+ type RunOnceResult,
32
+ type RunTick,
33
+ } from './run.js';
34
+ import {performClaim} from './claim-cas.js';
35
+ import {createClaimSpinner} from './cli-spinner.js';
36
+ import {performStart} from './start.js';
37
+ import {
38
+ performWorkOn,
39
+ loadHumanWorktreesDir,
40
+ persistHumanWorktreesDir,
41
+ } from './work-on.js';
42
+ import {performComplete, integrationFromFlags} from './complete.js';
43
+ import {
44
+ performRecoverIsolated,
45
+ locateIsolatedRecovery,
46
+ } from './recover-isolated.js';
47
+ import {
48
+ performDo,
49
+ performDoRemote,
50
+ resolveArbiterUrlFromCheckout,
51
+ type DoOptions,
52
+ type DoRemoteOptions,
53
+ } from './do.js';
54
+ import {performDoRemoteAuto, performDoRemoteArgs} from './do-remote-auto.js';
55
+ import {performAdvance, type AdvanceContext} from './advance.js';
56
+ import {
57
+ performAdvanceAuto,
58
+ performAdvanceArgs,
59
+ runAdvanceTickWithTreelessPublish,
60
+ type AdvanceMultiResult,
61
+ } from './advance-drivers.js';
62
+ import {
63
+ performAdvanceIsolated,
64
+ performAdvanceIsolatedAuto,
65
+ performAdvanceIsolatedArgs,
66
+ type IsolatedAdvanceContext,
67
+ } from './advance-isolated.js';
68
+ import {advanceRegistrySetRunTick} from './advance-loop-driver.js';
69
+ import {performIntake, resolveIntakeIntegrationModes} from './intake.js';
70
+ import {workFolderPrefix} from './work-layout.js';
71
+ import {
72
+ performDoAuto,
73
+ performDoArgs,
74
+ type DoMultiResult,
75
+ } from './do-autopick.js';
76
+ import {createHarness} from './pi-harness.js';
77
+ import {generateSessionPath} from './session-path.js';
78
+ import type {InteractiveLauncher} from './harness.js';
79
+ import {shouldUseColor} from './output.js';
80
+ import {
81
+ resolveRepoConfig,
82
+ resolveRepoConfigFromLoaded,
83
+ loadRepoConfigFromContent,
84
+ REPO_CONFIG_FILENAME,
85
+ type LoadedRepoConfig,
86
+ } from './repo-config.js';
87
+ import {
88
+ ensureMirrorMain,
89
+ readRepoConfigFromMirrorMain,
90
+ encodeRepoKey,
91
+ } from './repo-mirror.js';
92
+ import {identityEnv, type Identity} from './identity.js';
93
+ import {
94
+ harnessFlagOverrides,
95
+ doFlagOverrides,
96
+ doNeedsAgentCmd,
97
+ NO_AGENT_CMD_MESSAGE,
98
+ reviewFlagOverrides,
99
+ freshWorktreeGateFlagOverrides,
100
+ mergeRetriesFlagOverrides,
101
+ noPRFlagOverrides,
102
+ } from './do-config.js';
103
+ import {harnessReviewGate, harnessTaskAcceptanceGate} from './review-gate.js';
104
+ import {harnessSurfaceGate} from './surface-gate.js';
105
+ import {harnessTriageGate} from './triage-gate.js';
106
+ import {harnessApplyDecider} from './apply-decide.js';
107
+ import {harnessTaskReviewGate} from './tasker-review-loop.js';
108
+ import {runVerify} from './verify.js';
109
+ import {renderPrompt} from './prompt.js';
110
+ import {resolvePromptGuidance} from './config.js';
111
+ import {gc, RETAIN_REASON_TEXT} from './gc.js';
112
+ import {
113
+ runPrdToSpec,
114
+ type PrdToSpecResult,
115
+ type DataLeak,
116
+ } from './prd-to-spec.js';
117
+ import {sweepRemoteMergedBranches} from './reap-branches.js';
118
+ import {sweepOrphanSidecars} from './orphan-sidecar.js';
119
+ import {sweepLedgerDuplicates, formatLedgerSweep} from './ledger-lint.js';
120
+ import {status, formatStatus} from './status.js';
121
+ import {ledgerWrite} from './ledger-write.js';
122
+ import {
123
+ releaseItemLock,
124
+ reportItemLocks,
125
+ formatItemLockReport,
126
+ itemLockReportNeedsAttention,
127
+ reapStaleItemLocks,
128
+ formatReapReport,
129
+ reapReportNeedsAttention,
130
+ } from './item-lock.js';
131
+ import {
132
+ promoteFromPreBacklog,
133
+ promoteFromPreSpec,
134
+ listPromotable,
135
+ } from './needs-attention.js';
136
+ import {parseSlugArg} from './slug-namespace.js';
137
+ import {dropSource} from './drop-source.js';
138
+ import {arbiterStatus, DEFAULT_ARBITER_REMOTE} from './arbiter.js';
139
+ import {
140
+ resolveTaskOnlyArg,
141
+ workBranchRef,
142
+ SlugResolutionError,
143
+ } from './slug-namespace.js';
144
+ import {brand} from './brand.js';
145
+ import {installCI, type WizardPrompts} from './install-ci.js';
146
+ import {GitHubCIContext} from './install-ci-github.js';
147
+ import {loadCapabilityRegistry} from './install-ci-core.js';
148
+ import {performCloseMergedIssues} from './close-job.js';
149
+
150
+ interface ScanFlags {
151
+ config?: string;
152
+ autoBuild?: boolean;
153
+ json?: boolean;
154
+ here?: boolean;
155
+ arbiterRemote?: string;
156
+ arbiter?: string;
157
+ }
158
+
159
+ /**
160
+ * Whether `--auto-build` / `--no-auto-build` was explicitly passed on the command
161
+ * line. Commander gives a negatable boolean option a default of `true`, so we
162
+ * must check the value SOURCE to distinguish "user set it" from "default"; only an
163
+ * explicit flag becomes a config override (so config/defaults still win
164
+ * otherwise).
165
+ */
166
+ function autoBuildFromCli(command: Commander | undefined): boolean | undefined {
167
+ if (!command) {
168
+ return undefined;
169
+ }
170
+ if (command.getOptionValueSource('autoBuild') === 'cli') {
171
+ return command.getOptionValue('autoBuild') as boolean;
172
+ }
173
+ return undefined;
174
+ }
175
+
176
+ /**
177
+ * Build the overrides a user supplied via CLI flags. Discovery is the registry
178
+ * (the hub-mirror set, ADR §1), so there are no `--root`/`--include`/`--exclude`
179
+ * flags any more — only the autonomy-gate `--auto-build` toggle.
180
+ */
181
+ function flagOverrides(flags: ScanFlags, command?: Commander): PartialConfig {
182
+ const overrides: PartialConfig = {};
183
+ const autoBuild = autoBuildFromCli(command);
184
+ if (autoBuild !== undefined) {
185
+ overrides.autoBuild = autoBuild;
186
+ }
187
+ return overrides;
188
+ }
189
+
190
+ /**
191
+ * Resolve the global (non-per-repo) config along the chain
192
+ *
193
+ * flag > ENV (DORFL_*) > global file > built-in default
194
+ *
195
+ * by layering the file config, then the `DORFL_*` env layer, then the
196
+ * flag overrides on top. Env is a per-machine source (like a flag or the global
197
+ * file) and may set ANY key, host-only included. Used by the commands that build
198
+ * a single global config (`scan`, `run`); per-repo commands fold env in via
199
+ * `resolveRepoConfig` instead.
200
+ */
201
+ function resolveGlobalConfig(
202
+ fileConfig: PartialConfig,
203
+ flags: PartialConfig,
204
+ ): Config {
205
+ return mergeConfig({...fileConfig, ...envOverrides(), ...flags});
206
+ }
207
+
208
+ /**
209
+ * Load BOTH the global config file AND the per-machine `config.override.json`
210
+ * sibling for the given `--config` flag (or the default paths). This is the
211
+ * SINGLE entry point CLI commands use so the override layer is wired uniformly
212
+ * everywhere a per-repo resolution happens (ADR
213
+ * `per-machine-config-override-layer`); a missing override file is a no-op
214
+ * (empty map) — byte-identical to the pre-override behaviour.
215
+ */
216
+ function loadGlobalAndOverride(configPath: string | undefined): {
217
+ global: Config;
218
+ override: ConfigOverrideMap;
219
+ } {
220
+ return {
221
+ global: loadConfig(configPath),
222
+ override: loadConfigOverride(defaultConfigOverridePath(configPath)),
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Resolve the effective config for a `do --remote <r>` run, layering the target
228
+ * repo's COMMITTED `.dorfl.json` (read from `<arbiter>/main` via the hub
229
+ * mirror) into the SAME `flag > env > per-repo > global > default` chain in-place
230
+ * `do` uses. This is the no-checkout analogue of {@link resolveRepoConfig}: there
231
+ * is no working tree, so the bytes come from the arbiter's `main` instead of the
232
+ * cwd — but the parse + allow/reject FILTER (`loadRepoConfigFromContent`) and the
233
+ * layering (`resolveRepoConfigFromLoaded`) are the EXISTING per-repo machinery,
234
+ * reused verbatim. Host-only keys in the committed file are rejected + reported
235
+ * exactly as the in-place read rejects them.
236
+ *
237
+ * Resilient by design: a config-less repo (no file on main) OR an unreachable
238
+ * mirror falls back to global+default (the pre-task behaviour), with a warning
239
+ * on a genuine fetch/read fault — a `--remote` build must not be blocked because
240
+ * the arbiter was momentarily offline.
241
+ *
242
+ * The config read uses {@link ensureMirrorMain} (main-only, NO-prune), NOT the
243
+ * all-heads pruning {@link ensureMirror}: `git show main:.dorfl.json` only
244
+ * needs `main`, and the all-heads `+refs/heads/*:refs/heads/*` fetch would let a
245
+ * `work/<slug>` branch CHECKED OUT in some stale job worktree block it (git
246
+ * refuses to fetch into a checked-out branch), throwing the read into its
247
+ * fallback and silently dropping the per-repo `harness`/`verify`/etc. The build's
248
+ * own worktree MATERIALISATION still calls the all-heads `ensureMirror` later
249
+ * (continue-detection needs the kept `work/<slug>` head) — that is a separate,
250
+ * untouched concern; only the CONFIG-READ fetch is narrowed here.
251
+ */
252
+ function resolveRemoteRepoConfig(options: {
253
+ remote: string;
254
+ workspacesDir: string;
255
+ global: Config;
256
+ flags: PartialConfig;
257
+ identity: Identity | undefined;
258
+ note: (message: string) => void;
259
+ /**
260
+ * The per-machine override map (from `loadConfigOverride`). The hub key is
261
+ * derived from `remote` (the URL is in hand), so the per-repo entry applies
262
+ * without a git read. Default: empty (no override) — byte-identical to
263
+ * pre-override behaviour.
264
+ */
265
+ override?: ConfigOverrideMap;
266
+ }): Config {
267
+ const {remote, workspacesDir, global, flags, identity, note, override} =
268
+ options;
269
+ let loaded: LoadedRepoConfig;
270
+ try {
271
+ const env = identityEnv(identity, process.env);
272
+ const mirror = ensureMirrorMain({url: remote, workspacesDir, env});
273
+ const content = readRepoConfigFromMirrorMain(mirror.path, env);
274
+ loaded =
275
+ content === undefined
276
+ ? {
277
+ path: `${remote}#main:${REPO_CONFIG_FILENAME}`,
278
+ config: {},
279
+ rejected: [],
280
+ }
281
+ : loadRepoConfigFromContent(
282
+ content,
283
+ `${remote}#main:${REPO_CONFIG_FILENAME}`,
284
+ );
285
+ } catch (err) {
286
+ // A fetch/read fault (offline arbiter, corrupt mirror) must NOT block the
287
+ // build: warn + fall back to global+default (today's no-per-repo behaviour).
288
+ note(
289
+ `could not read the target repo's ${REPO_CONFIG_FILENAME} from ` +
290
+ `${remote}/main; resolving config from global + flags only. ` +
291
+ `${err instanceof Error ? err.message : String(err)}`,
292
+ );
293
+ loaded = {path: `${remote}#main`, config: {}, rejected: []};
294
+ }
295
+ if (loaded.message) {
296
+ note(loaded.message);
297
+ }
298
+ return resolveRepoConfigFromLoaded(loaded, {
299
+ global,
300
+ flags,
301
+ override,
302
+ arbiterUrl: remote,
303
+ }).config;
304
+ }
305
+
306
+ /**
307
+ * Build the {@link RunTick} that **plain `run`** (no flag) loops: the REGISTRY-SET
308
+ * ADVANCE tick (task `run-uses-advance-tick`). This points the deliberate
309
+ * {@link RunTick} swap seam at the precursor's registry-set advance driver
310
+ * ({@link advanceRegistrySetRunTick}) instead of the build-only `runOnce` tick, so
311
+ * plain `run` ≡ advance with calm-default gates: behaviour-preserving today
312
+ * (both lifecycle gates default off ⇒ build ready tasks / task ready prds /
313
+ * route failures to needs-attention, over the SAME registry-set discovery +
314
+ * per-mirror job-worktree isolation the build tick used), lifecycle-capable the
315
+ * moment a gate is flipped (triage / surface / apply).
316
+ *
317
+ * Where the deprecated single-mirror advance wiring drained ONE named mirror
318
+ * IN-PLACE in the cwd checkout (the library {@link advanceRunTick}, now reached
319
+ * only by the precursor's single-mirror tests, no longer the CLI), this discovers
320
+ * the WHOLE registry via
321
+ * `scan(config)` (the SAME discovery the build tick uses) and the registry-set
322
+ * driver threads a PER-MIRROR job-worktree `doDriver` so each mirror's build/task
323
+ * rungs run isolated off THAT mirror's arbiter (NOT `process.cwd()`). The
324
+ * tree-less surface/triage/apply rungs commit in a per-mirror working CLONE of the
325
+ * mirror's arbiter (materialised lazily under the agents' workspace), since a bare
326
+ * hub mirror has no work tree to `git mv`/`git commit` in.
327
+ */
328
+ function buildRegistrySetAdvanceTick(options: {
329
+ config: Config;
330
+ workspace: string;
331
+ arbiter?: string;
332
+ env?: NodeJS.ProcessEnv;
333
+ /**
334
+ * The per-machine {@link ConfigOverrideMap} — threaded into the registry-set
335
+ * advance driver so per-mirror config resolution honours the override (ADR
336
+ * `per-machine-config-override-layer`).
337
+ */
338
+ override?: ConfigOverrideMap;
339
+ }): RunTick {
340
+ const {config, workspace, arbiter, env, override} = options;
341
+ const gitEnv = identityEnv(config.identity, env);
342
+ const harness = createHarness({harness: config.harness, piBin: config.piBin});
343
+ return advanceRegistrySetRunTick({
344
+ config,
345
+ override,
346
+ workspace,
347
+ // The SELECTION-layer gates for the loop/CI path, IDENTICAL to the
348
+ // single-mirror wiring above: `observationTriage != off` enumerates the
349
+ // observation (triage) pool; `surfaceBlockers` enumerates the `needsAnswers`-
350
+ // blocked (surface) pool. `off`/`false` drops the respective pool. Apply
351
+ // (consume) is always-on (never gated here). Both default to their calm state,
352
+ // so plain `run` out of the box is behaviour-identical to the old build tick.
353
+ lifecycleGates: {
354
+ triage: config.observationTriage !== 'off',
355
+ surface: config.surfaceBlockers,
356
+ // `surfaceStaging` widens the SURFACE candidate set into STAGING (prd
357
+ // `staging-surface-and-apply-promote-safety` F2). Default `true` — a
358
+ // tasked `needsAnswers` task in `tasks/backlog/` (or prd in
359
+ // `prds/proposed/`) surfaces its questions BEFORE promotion. BUILD/claim
360
+ // stays pool-only either way.
361
+ surfaceStaging: config.surfaceStaging,
362
+ },
363
+ // Build the per-mirror advance CONTEXT the registry-set driver injects its
364
+ // per-mirror job-worktree `doDriver` on top of: the build/task `doOptions`
365
+ // base + the surface/triage gate seams + a tree-less working clone of THIS
366
+ // mirror's arbiter (the ledger-write cwd the surface/triage/apply rungs commit
367
+ // in — a bare mirror cannot `git mv`/`git commit`).
368
+ contextFor: ({mirrorPath, originUrl}) => {
369
+ // A per-mirror working clone of the mirror's arbiter for the tree-less
370
+ // lifecycle rungs (surface/triage/apply). Keyed by the mirror's repo key so
371
+ // distinct mirrors get distinct clones; re-created fresh each tick so the
372
+ // rungs always commit onto the latest mirror `main` (idempotent, cheap
373
+ // local clone). The build/task rungs DO NOT use this cwd (the worktree
374
+ // `doDriver` replaces it); it serves ONLY the tree-less moves.
375
+ const treelessCwd = joinPath(
376
+ workspace,
377
+ 'advance-cwd',
378
+ encodeRepoKey(originUrl).split('/').join('__'),
379
+ );
380
+ rmSync(treelessCwd, {recursive: true, force: true});
381
+ mkdirSync(joinPath(treelessCwd, '..'), {recursive: true});
382
+ git(['clone', '--quiet', mirrorPath, treelessCwd], workspace, {
383
+ env: gitEnv,
384
+ });
385
+ const doOptions: Omit<DoOptions, 'arg'> = {
386
+ cwd: treelessCwd,
387
+ arbiter: arbiter ?? config.defaultArbiter,
388
+ identity: config.identity,
389
+ autoTask: config.autoTask,
390
+ integration: config.integration,
391
+ // The per-TRANSITION TASKING override: the `do prd:` tasking path threads
392
+ // `taskingIntegration ?? integration`; the build path stays on `integration`.
393
+ taskingIntegration: config.taskingIntegration,
394
+ // The TASK-PLACEMENT configured default (`do prd:` tasking output:
395
+ // `pre-backlog` staged vs `ready` pool). No operator flag on this
396
+ // registry-driven advance context, so only the configured default rung is
397
+ // threaded (the resolver still layers untrusted-origin force + built-in floor).
398
+ tasksLandIn: config.tasksLandIn,
399
+ prepare: config.prepare,
400
+ verify: config.verify,
401
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
402
+ freshWorktreeGate: config.freshWorktreeGate,
403
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
404
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — resolved per-repo
405
+ // and threaded so the registry-driven advance path's `do` inherits it.
406
+ mergeRetries: config.mergeRetries,
407
+ noPR: config.noPR,
408
+ harness,
409
+ agentCmd: config.agentCmd,
410
+ model: config.model,
411
+ sessionsDir: config.sessionsDir,
412
+ review: config.review,
413
+ reviewModel: config.reviewModel,
414
+ reviewMaxRounds: config.reviewMaxRounds,
415
+ reviewGate: config.review
416
+ ? harnessReviewGate({harness, agentCmd: config.agentCmd})
417
+ : undefined,
418
+ reviewLoop: config.taskerLoop
419
+ ? harnessTaskReviewGate({harness, agentCmd: config.agentCmd})
420
+ : undefined,
421
+ taskerLoopMax: config.taskerLoopMax,
422
+ taskerLoopModel: config.taskerLoopModel,
423
+ taskReviewGate: config.review
424
+ ? harnessTaskAcceptanceGate({harness, agentCmd: config.agentCmd})
425
+ : undefined,
426
+ color: shouldUseColor(process.stdout),
427
+ note: (message) => console.error(`>> ${message}`),
428
+ noteBlock: (message) => console.error(message),
429
+ };
430
+ const context: AdvanceContext = {
431
+ cwd: treelessCwd,
432
+ arbiter: arbiter ?? config.defaultArbiter,
433
+ doOptions,
434
+ surfaceGate: harnessSurfaceGate({harness, agentCmd: config.agentCmd}),
435
+ surfaceModel: config.model,
436
+ applyDecide: harnessApplyDecider({harness, agentCmd: config.agentCmd}),
437
+ applyModel: config.model,
438
+ observationTriage: config.observationTriage,
439
+ triageGate: harnessTriageGate({harness, agentCmd: config.agentCmd}),
440
+ triageModel: config.model,
441
+ // The ANSWERED-MERGE LAND DISPATCH context (task
442
+ // `apply-rung-merge-disposition`, prd `land-time-reverify-and-parallel-
443
+ // merge-ceiling`): the dispatcher cuts a per-job worktree via
444
+ // `workspace.ts` `createJob` off the hub mirror (so we thread the resolved
445
+ // `workspacesDir` + the real arbiter URL — the per-mirror tree-less
446
+ // `treelessCwd` has `origin` pointing at the LOCAL mirror path, NOT the
447
+ // arbiter URL, so we MUST pass `originUrl` directly here), then drives
448
+ // `performIntegration` with `committedRecovery: true` +
449
+ // `freshWorktreeGate: true` (the rebased tip is re-verified, the RED
450
+ // route refuses, the GREEN route lands). `prepare`/`verify` are the SAME
451
+ // per-repo gate the build path uses; `strictMergeApproval` is the OQ6
452
+ // opt-in resolved by the sibling task `strict-merge-approval-gate`
453
+ // (default OFF ⇒ honour + land on a green re-verify).
454
+ workspacesDir: workspace,
455
+ arbiterUrl: originUrl,
456
+ prepare: config.prepare,
457
+ verify: config.verify,
458
+ strictMergeApproval: config.strictMergeApproval,
459
+ note: (message) => console.error(`>> ${message}`),
460
+ };
461
+ return context;
462
+ },
463
+ });
464
+ }
465
+
466
+ /**
467
+ * Resolve the arbiter URL for `do --isolated <slug>` from the CURRENT repo (cwd).
468
+ *
469
+ * `--isolated` builds in a job worktree off MY OWN arbiter (the same isolation +
470
+ * integrate pipeline `do --remote <url>` uses), so it needs the URL of the cwd's
471
+ * arbiter remote. It uses the SAME arbiter-remote resolution in-place `do` does:
472
+ * `--arbiter` > the resolved cwd `defaultArbiter` (the per-repo/global config), as
473
+ * the remote NAME, then `git remote get-url <name>` in the checkout to get its URL.
474
+ * That URL is then fed into the EXISTING `performDoRemote` pipeline as `remote`.
475
+ *
476
+ * Returns the URL, or `undefined` when there is no resolvable arbiter (cwd is not
477
+ * a git repo, or the named arbiter remote does not exist) — the "isolated against
478
+ * what?" case the caller turns into a clear error naming `--remote <url>`.
479
+ */
480
+ function resolveDefaultArbiterForCwd(
481
+ cwd: string,
482
+ global: Config,
483
+ flags: PartialConfig,
484
+ override?: ConfigOverrideMap,
485
+ ): string {
486
+ // The SAME per-repo config read in-place `do` uses (`resolveRepoConfig` on the
487
+ // cwd), so `--isolated` resolves the arbiter remote NAME (`defaultArbiter`)
488
+ // through the identical `flag > env > per-repo > global > default` chain. An
489
+ // absent `.dorfl.json` falls back to the global/default (`origin`).
490
+ return resolveRepoConfig({repoPath: cwd, global, flags, override}).config
491
+ .defaultArbiter;
492
+ }
493
+
494
+ /**
495
+ * First-use prompt for the human worktree root (`work-on`). Offers `suggestion`
496
+ * as the default (Enter accepts it); a blank non-interactive answer aborts. The
497
+ * prompt goes to stderr so `--print-dir`'s stdout stays clean.
498
+ */
499
+ function promptForWorktreesRoot(suggestion: string): Promise<string> {
500
+ return new Promise((resolvePrompt) => {
501
+ const rl = createInterface({input: process.stdin, output: process.stderr});
502
+ rl.question(
503
+ 'work-on needs a human worktree root (NOT under ~/.dorfl). ' +
504
+ `Where should parallel worktrees live? [${suggestion}] `,
505
+ (answer) => {
506
+ rl.close();
507
+ const trimmed = answer.trim();
508
+ resolvePrompt(trimmed === '' ? suggestion : trimmed);
509
+ },
510
+ );
511
+ });
512
+ }
513
+
514
+ interface RunFlags extends ScanFlags {
515
+ once?: boolean;
516
+ /**
517
+ * `run --advance` is a DEPRECATED NO-OP ALIAS (task `run-uses-advance-tick`).
518
+ * Plain `run` (no flag) now ALREADY drives the registry-set ADVANCE tick with
519
+ * calm-default gates, so there is no longer a separate advance MODE to opt into:
520
+ * passing `--advance` warns + is otherwise ignored (it does NOT change the tick,
521
+ * which is already advance). Kept so an existing `run --advance` invocation does
522
+ * not break; it carries NO value (the old `--advance <mirror>` single-mirror
523
+ * form is gone — the daemon discovers the WHOLE registry via `scan(config)`, the
524
+ * SAME discovery the build tick used).
525
+ */
526
+ advance?: boolean;
527
+ maxIterations?: string;
528
+ maxDuration?: string;
529
+ interval?: string;
530
+ maxParallel?: string;
531
+ perRepoMax?: string;
532
+ arbiter?: string;
533
+ integration?: string;
534
+ /** `--no-pr` ⇒ commander stores `pr === false` (the suppress-PR intent). */
535
+ pr?: boolean;
536
+ agentCmd?: string;
537
+ model?: string;
538
+ harness?: string;
539
+ piBin?: string;
540
+ sessionsDir?: string;
541
+ workspace?: string;
542
+ review?: boolean;
543
+ reviewModel?: string;
544
+ reviewMaxRounds?: string;
545
+ /** `--fresh-worktree-gate` / `--no-fresh-worktree-gate` — gate the REBASED tip in a clean throwaway worktree (ON by default). */
546
+ freshWorktreeGate?: boolean;
547
+ /** `--merge-retries <n>` — the cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). */
548
+ mergeRetries?: string;
549
+ }
550
+
551
+ function runFlagOverrides(flags: RunFlags, command?: Commander): PartialConfig {
552
+ const overrides = flagOverrides(flags, command);
553
+ if (flags.maxParallel !== undefined) {
554
+ overrides.maxParallel = Number(flags.maxParallel);
555
+ }
556
+ if (flags.perRepoMax !== undefined) {
557
+ overrides.perRepoMax = Number(flags.perRepoMax);
558
+ }
559
+ if (flags.arbiter !== undefined) {
560
+ overrides.defaultArbiter = flags.arbiter;
561
+ }
562
+ if (flags.integration === 'propose' || flags.integration === 'merge') {
563
+ overrides.integration = flags.integration;
564
+ }
565
+ // `--no-pr` (the PR-INTENT axis): suppress the PR even on an authed GitHub
566
+ // arbiter. Commander stores the negatable flag as `pr` (false when `--no-pr` is
567
+ // passed). Rides the SAME flag-override chain as `integration`.
568
+ if (flags.pr === false) {
569
+ overrides.noPR = true;
570
+ }
571
+ // The harness/adapter flags (--agent-cmd/--model/--harness/--pi-bin) map via
572
+ // the SHARED per-key mapping `do` also reuses (do-config.harnessFlagOverrides),
573
+ // so there is exactly ONE override path for them.
574
+ Object.assign(overrides, harnessFlagOverrides(flags));
575
+ // Gate 2 (PR/code review) flags ride the SAME flag-override path so
576
+ // `--review`/`--review-model`/`--review-max-rounds` resolve
577
+ // flag > env > per-repo > global > default — mirroring the `do` command (the
578
+ // fleet inherits the review gate via the converged `performIntegration` core).
579
+ Object.assign(overrides, reviewFlagOverrides(flags));
580
+ // `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain: gate
581
+ // the REBASED tip in a clean throwaway worktree (ON by default). The `run` fleet
582
+ // caller additionally downgrades it to today's gate at `perRepoMax > 1` (the
583
+ // fleet conditional lives in `runOnce`, not in this flag mapping).
584
+ Object.assign(overrides, freshWorktreeGateFlagOverrides(flags));
585
+ // `--merge-retries <n>` rides the SAME chain: the cross-job merge-serialiser
586
+ // CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5 /
587
+ // Applied Answer q1 (a)). The `run` fleet inherits the resolved cap through
588
+ // the converged `performIntegration` core (config.mergeRetries threads into
589
+ // the merge loop, replacing the bare `DEFAULT_MERGE_RETRIES` fallback).
590
+ Object.assign(overrides, mergeRetriesFlagOverrides(flags));
591
+ return overrides;
592
+ }
593
+
594
+ function formatItemLine(item: ItemResult): string {
595
+ const extra = item.detail ? ` — ${item.detail}` : '';
596
+ return ` [${item.status}] ${item.repoPath} :: ${item.slug}${extra}`;
597
+ }
598
+
599
+ interface ClaimFlags {
600
+ arbiter?: string;
601
+ retries?: string;
602
+ dryRun?: boolean;
603
+ ignoreNotReady?: boolean;
604
+ }
605
+
606
+ interface VerifyFlags {
607
+ config?: string;
608
+ }
609
+
610
+ /**
611
+ * The flags that drive an INTERACTIVE `--agent` launch (task
612
+ * `agent-interactive-launch`), shared by `start` and `work-on`. `--agent` opts
613
+ * into launching the configured harness interactively after onboarding; the
614
+ * harness/model/pi-bin/sessions-dir flags resolve the SAME way the autonomous
615
+ * `do`/`run` path resolves them (flag > env > per-repo > global > default), so
616
+ * the human starts pinned to the intended model (decision #4).
617
+ */
618
+ interface AgentLaunchFlags {
619
+ agent?: boolean;
620
+ harness?: string;
621
+ model?: string;
622
+ piBin?: string;
623
+ sessionsDir?: string;
624
+ }
625
+
626
+ interface StartFlags extends AgentLaunchFlags {
627
+ config?: string;
628
+ arbiter?: string;
629
+ resume?: boolean;
630
+ ignoreNotReady?: boolean;
631
+ /** `--isolated` (resume only): re-engage the slug's retained job worktree. */
632
+ isolated?: boolean;
633
+ workspace?: string;
634
+ }
635
+
636
+ interface WorkOnFlags extends AgentLaunchFlags {
637
+ config?: string;
638
+ arbiter?: string;
639
+ remote?: string;
640
+ copy?: string;
641
+ copyFrom?: string;
642
+ ignoreNotReady?: boolean;
643
+ printDir?: boolean;
644
+ workspace?: string;
645
+ }
646
+
647
+ interface CompleteFlags {
648
+ config?: string;
649
+ arbiter?: string;
650
+ merge?: boolean;
651
+ propose?: boolean;
652
+ /** `--no-pr` ⇒ commander stores `pr === false` (the suppress-PR intent). */
653
+ pr?: boolean;
654
+ switch?: boolean;
655
+ ignoreDivergedMain?: boolean;
656
+ skipVerify?: boolean;
657
+ type?: string;
658
+ message?: string;
659
+ review?: boolean;
660
+ reviewModel?: string;
661
+ reviewMaxRounds?: string;
662
+ /** `--fresh-worktree-gate` / `--no-fresh-worktree-gate` — gate the REBASED tip in a clean throwaway worktree (ON by default). */
663
+ freshWorktreeGate?: boolean;
664
+ /** `--merge-retries <n>` — the cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). */
665
+ mergeRetries?: string;
666
+ /** `--isolated`: finish the slug's retained job worktree (the stranded-branch recover). */
667
+ isolated?: boolean;
668
+ workspace?: string;
669
+ }
670
+
671
+ /**
672
+ * Resolve the EXPLICIT operator placement override from `--tasks-land-in <where>`
673
+ * (the top of the `do prd:` tasking-placement precedence — task
674
+ * `runner-deterministic-slice-placement-policy-and-precedence`). Mirrors the
675
+ * `flagMode === 'merge'` ⇒ `explicitMerge: true` shape: it contributes
676
+ * `explicitTasksLandIn` ONLY when the operator actually typed the flag, so an
677
+ * untrusted-origin's staging force still wins when the value came from config, not
678
+ * the flag. An invalid value FAILS LOUDLY (a usage error, never silently dropped
679
+ * — the SAME discipline the `--observation-triage` enum + the
680
+ * `DORFL_TASKS_LAND_IN` env coercion use).
681
+ */
682
+ function explicitTasksLandInFromFlag(
683
+ raw: string | undefined,
684
+ ): 'pre-backlog' | 'ready' | undefined {
685
+ if (raw === undefined) {
686
+ return undefined;
687
+ }
688
+ if (raw !== 'pre-backlog' && raw !== 'ready') {
689
+ throw new Error(
690
+ `--tasks-land-in must be 'pre-backlog' or 'ready' (got '${raw}').`,
691
+ );
692
+ }
693
+ return raw;
694
+ }
695
+
696
+ /**
697
+ * The SPEC twin of {@link explicitTasksLandInFromFlag} (spec
698
+ * `prd-to-spec-vocabulary-cutover-and-migration-command`). Resolve the EXPLICIT
699
+ * operator spec-placement override from `--specs-land-in <where>` for `intake`'s
700
+ * `spec` dispatch — the TOP of the same precedence chain that the tasking
701
+ * placement uses. Contributes `explicitSpecsLandIn` ONLY when the operator
702
+ * actually typed the flag, so an untrusted-origin's staging force still wins when
703
+ * the value came from config. An invalid value FAILS LOUDLY (a usage error,
704
+ * never silently dropped), mirroring the task helper above and the
705
+ * `DORFL_SPECS_LAND_IN` env coercion.
706
+ *
707
+ * HARD CUTOVER: the legacy `--prds-land-in` flag is GONE (clean break, spec US
708
+ * #8); only `--specs-land-in` is accepted.
709
+ */
710
+ function explicitSpecsLandInFromFlag(
711
+ raw: string | undefined,
712
+ flagName = '--specs-land-in',
713
+ ): 'pre-proposed' | 'ready' | undefined {
714
+ if (raw === undefined) {
715
+ return undefined;
716
+ }
717
+ if (raw !== 'pre-proposed' && raw !== 'ready') {
718
+ throw new Error(
719
+ `${flagName} must be 'pre-proposed' or 'ready' (got '${raw}').`,
720
+ );
721
+ }
722
+ return raw;
723
+ }
724
+
725
+ interface DoFlags {
726
+ config?: string;
727
+ arbiter?: string;
728
+ remote?: string;
729
+ /** `--isolated`: build in a job worktree off THIS repo's arbiter (no checkout takeover). */
730
+ isolated?: boolean;
731
+ /** `-n <x>`: do x eligible items in sequence (auto-pick form). */
732
+ number?: string;
733
+ /** `--selection-order <order>`: a preset keyword (drain/groom) or comma-separated pool order. */
734
+ selectionOrder?: string;
735
+ /** `--observation-triage <off|ask|auto>`: the observation-inbox gate (`advance`). */
736
+ observationTriage?: string;
737
+ /** `--surface-blockers` / `--no-surface-blockers`: the declared-blocked-work gate (`advance`). */
738
+ surfaceBlockers?: boolean;
739
+ /** `--merge-questions <off|ask|auto>`: the merge-question SURFACER gate (`advance`). SEPARATE from `--observation-triage` with a HIGHER default. */
740
+ mergeQuestions?: string;
741
+ /** `--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. */
742
+ strictMergeApproval?: boolean;
743
+ merge?: boolean;
744
+ propose?: boolean;
745
+ /** `--tasks-land-in <pre-backlog|ready>`: the explicit operator placement override for `do prd:` tasking output (top of the placement precedence). Resolves into the `tasksLandIn` config key. */
746
+ tasksLandIn?: string;
747
+ /** `--no-pr` ⇒ commander stores `pr === false` (the suppress-PR intent). */
748
+ pr?: boolean;
749
+ ignoreDivergedMain?: boolean;
750
+ /** `--allow-backlog`: drive a staged (tasks/backlog/) task in place without promoting it (`do task:` only). EXPLICIT-INVOCATION-ONLY — never config/env. */
751
+ allowBacklog?: boolean;
752
+ agentCmd?: string;
753
+ model?: string;
754
+ harness?: string;
755
+ piBin?: string;
756
+ sessionsDir?: string;
757
+ watch?: boolean;
758
+ review?: boolean;
759
+ reviewModel?: string;
760
+ reviewMaxRounds?: string;
761
+ /** `--tasker-loop` / `--no-tasker-loop` — the tasker improver loop on/off toggle (`do prd:` path). Resolves into the `taskerLoop` config key. */
762
+ taskerLoop?: boolean;
763
+ /** `--tasker-loop-max <n>` — the tasker improver loop's in-context convergence cap (`do prd:` path). Resolves into the `taskerLoopMax` config key. */
764
+ taskerLoopMax?: string;
765
+ /** `--tasker-loop-model <id>` — the tasker improver loop reviewer's de-correlated model (`do prd:` path). Resolves into the `taskerLoopModel` config key. */
766
+ taskerLoopModel?: string;
767
+ /** `--fresh-worktree-gate` / `--no-fresh-worktree-gate` — gate the REBASED tip in a clean throwaway worktree (ON by default). */
768
+ freshWorktreeGate?: boolean;
769
+ /** `--merge-retries <n>` — the cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). */
770
+ mergeRetries?: string;
771
+ }
772
+
773
+ interface IntakeFlags {
774
+ config?: string;
775
+ arbiter?: string;
776
+ merge?: boolean;
777
+ propose?: boolean;
778
+ /** `--no-pr` ⇒ commander stores `pr === false` (the suppress-PR intent). */
779
+ pr?: boolean;
780
+ mergeSpec?: boolean;
781
+ proposeSpec?: boolean;
782
+ mergeTask?: boolean;
783
+ proposeTask?: boolean;
784
+ /**
785
+ * `--origin-trust <trusted|untrusted>` — the author-trust verdict the CI shell
786
+ * passes IN so `intake` STAMPS the emitted prd/task (task
787
+ * `untrusted-origin-forces-build-propose`). `intake` does NOT resolve trust; the
788
+ * shell derives it from the SAME `author_association` case as the integration
789
+ * flags. UNSET (a local intake) ⇒ emit unstamped ⇒ human/trusted.
790
+ */
791
+ originTrust?: string;
792
+ /** `--specs-land-in <pre-proposed|ready>`: the explicit operator spec-placement override (top of the precedence). Resolves into the `specsLandIn` config key. */
793
+ specsLandIn?: string;
794
+ agentCmd?: string;
795
+ model?: string;
796
+ harness?: string;
797
+ piBin?: string;
798
+ sessionsDir?: string;
799
+ }
800
+
801
+ interface GcFlags {
802
+ config?: string;
803
+ workspace?: string;
804
+ force?: boolean;
805
+ yes?: boolean;
806
+ json?: boolean;
807
+ ledger?: string;
808
+ remoteBranches?: boolean;
809
+ arbiter?: string;
810
+ cwd?: string;
811
+ dryRun?: boolean;
812
+ reapStaleLocks?: boolean;
813
+ }
814
+
815
+ interface PrdToSpecFlags {
816
+ repo?: string;
817
+ dryRun?: boolean;
818
+ json?: boolean;
819
+ }
820
+
821
+ /** Human-readable report for the `prd-to-spec` migration result. */
822
+ function printPrdToSpecReport(result: PrdToSpecResult): void {
823
+ if (result.refused) {
824
+ const v = result.refused;
825
+ const label =
826
+ v.kind === 'dirty-tree'
827
+ ? 'dirty working tree'
828
+ : v.kind === 'held-lock'
829
+ ? 'a held per-item lock'
830
+ : 'an in-progress work-branch carrying unlanded work';
831
+ console.error(
832
+ `REFUSED: the repo is not quiescent (${label}): ${v.offender}. ` +
833
+ 'Land or discard the in-flight work, then re-run. (prd-to-spec ' +
834
+ 'never migrates over uncommitted/in-flight state.)',
835
+ );
836
+ return;
837
+ }
838
+
839
+ const verb = result.dryRun ? 'WOULD' : 'DID';
840
+ console.log(
841
+ result.dryRun
842
+ ? '=== prd-to-spec (DRY RUN — nothing written) ==='
843
+ : '=== prd-to-spec ===',
844
+ );
845
+
846
+ if (result.resync) {
847
+ const changed = result.resync.docs.filter((d) => !d.unchanged).length;
848
+ console.log(
849
+ `Contract re-sync: ${verb} sync ${result.resync.docs.length} protocol ` +
850
+ `doc(s) (${changed} changed) + bump ${result.resync.versionPath}.`,
851
+ );
852
+ }
853
+ console.log(`Folders: ${verb} move ${result.folderMoves.length} folder(s).`);
854
+ for (const m of result.folderMoves) {
855
+ console.log(` ${m.from} -> ${m.to}`);
856
+ }
857
+ console.log(
858
+ `Item content: ${verb} rewrite ${result.contentRewrites.length} item(s).`,
859
+ );
860
+ console.log(
861
+ `Config: ${verb} rename ${result.configRewrites.length} key(s)` +
862
+ (result.configRewrites.length > 0
863
+ ? ` (${result.configRewrites.map((c) => `${c.from}->${c.to}`).join(', ')})`
864
+ : '') +
865
+ '.',
866
+ );
867
+ console.log(`Refs: ${verb} rename ${result.refRenames.length} inert ref(s).`);
868
+ for (const r of result.refRenames) {
869
+ console.log(` ${r.from} -> ${r.to}`);
870
+ }
871
+
872
+ if (result.dryRun) {
873
+ console.log(
874
+ '(Re-run without --dry-run to apply; the leak scan gates the output.)',
875
+ );
876
+ return;
877
+ }
878
+ if (result.leaks.length === 0) {
879
+ console.log('Leak scan: GREEN (no surviving prd data ref).');
880
+ } else {
881
+ console.error(`Leak scan: FAILED (${result.leaks.length} leak(s)):`);
882
+ for (const leak of result.leaks) {
883
+ printLeak(leak);
884
+ }
885
+ }
886
+ }
887
+
888
+ function printLeak(leak: DataLeak): void {
889
+ console.error(
890
+ ` [${leak.lens}] ${leak.where}: '${leak.token}' — ${leak.why}`,
891
+ );
892
+ }
893
+
894
+ interface StatusFlags {
895
+ config?: string;
896
+ workspace?: string;
897
+ arbiterRemote?: string;
898
+ arbiter?: string;
899
+ noArbiter?: boolean;
900
+ here?: boolean;
901
+ json?: boolean;
902
+ }
903
+
904
+ interface RequeueFlags {
905
+ config?: string;
906
+ cwd?: string;
907
+ arbiter?: string;
908
+ reset?: boolean;
909
+ message?: string;
910
+ }
911
+
912
+ interface PromoteFlags {
913
+ config?: string;
914
+ cwd?: string;
915
+ arbiter?: string;
916
+ }
917
+
918
+ interface ReleaseLockFlags {
919
+ config?: string;
920
+ cwd?: string;
921
+ arbiter?: string;
922
+ }
923
+
924
+ interface DropFlags {
925
+ config?: string;
926
+ cwd?: string;
927
+ reason?: string;
928
+ }
929
+
930
+ interface RemoteAddFlags {
931
+ config?: string;
932
+ local?: boolean;
933
+ arbiterRemote?: string;
934
+ force?: boolean;
935
+ }
936
+
937
+ interface RemoteRmFlags {
938
+ config?: string;
939
+ }
940
+
941
+ interface RemoteLsFlags {
942
+ config?: string;
943
+ json?: boolean;
944
+ }
945
+
946
+ interface RemoteFindFlags {
947
+ config?: string;
948
+ yes?: boolean;
949
+ }
950
+
951
+ interface InstallCiFlags {
952
+ config?: string;
953
+ fake?: boolean;
954
+ exportConfig?: string;
955
+ includeSecrets?: boolean;
956
+ installSource?: string;
957
+ maxParallel?: string;
958
+ cwd?: string;
959
+ repo?: string;
960
+ ghBin?: string;
961
+ }
962
+
963
+ interface CloseMergedIssuesFlags {
964
+ cwd?: string;
965
+ ghBin?: string;
966
+ json?: boolean;
967
+ }
968
+
969
+ /**
970
+ * Resolve a task-only command's slug argument through the §3a namespace guard
971
+ * (`resolveTaskOnlyArg`): accept bare (= task) + `task:` (explicit alias),
972
+ * REJECT `spec:` with a clear "operates on tasks, not specs" error (and the
973
+ * legacy `prd:` with "operates on tasks, not prds", still accepted through the
974
+ * cutover). On rejection it prints the error to stderr and exits 1 (the task-only
975
+ * commands never act on a spec). An OMITTED slug (`start`/`complete`/`prompt`
976
+ * infer it from the branch) passes through untouched.
977
+ *
978
+ * `do` is the ONE command that spans both namespaces; it consumes the full
979
+ * `resolveSlug` (with the cross-namespace collision check) in the `do-in-place`
980
+ * task. This guard is the task-only half of ADR §3a.
981
+ */
982
+ function resolveTaskOnlySlug(slug: string | undefined): string | undefined {
983
+ if (slug === undefined) {
984
+ return undefined;
985
+ }
986
+ try {
987
+ return resolveTaskOnlyArg(slug);
988
+ } catch (err) {
989
+ if (err instanceof SlugResolutionError) {
990
+ console.error(`error: ${err.message}`);
991
+ process.exit(1);
992
+ }
993
+ throw err;
994
+ }
995
+ }
996
+
997
+ /**
998
+ * Build the INTERACTIVE launcher closure for `--agent` (task
999
+ * `agent-interactive-launch`), or `undefined` when `--agent` was not passed.
1000
+ *
1001
+ * It resolves the harness + model the SAME way the autonomous `do`/`run` path
1002
+ * does — per-repo config layered flag > env > per-repo > global > default (ADR
1003
+ * §13) — so the human starts pinned to the intended model (decision #4). The
1004
+ * returned closure is what `start.ts`/`work-on.ts` call AFTER onboarding: it
1005
+ * generates the pi `--session` path for the onboarded working tree and calls
1006
+ * `harness.launchInteractive` (which inherits stdio, drops `--print`, feeds no
1007
+ * prompt, foreground). A NON-pi harness throws a clear pi-only error from the
1008
+ * adapter (decision #2). This keeps the git-logic modules decoupled from
1009
+ * `createHarness`/config (they only receive the thin {@link InteractiveLauncher}).
1010
+ *
1011
+ * `repoPath` is the per-repo config root (the current checkout for `start` /
1012
+ * in-repo `work-on`); `undefined` (remote `work-on`, no checkout) resolves from
1013
+ * the global config only — mirroring `do --remote`.
1014
+ */
1015
+ function buildInteractiveLauncher(
1016
+ flags: AgentLaunchFlags,
1017
+ configPath: string | undefined,
1018
+ repoPath: string | undefined,
1019
+ ): InteractiveLauncher | undefined {
1020
+ if (flags.agent !== true) {
1021
+ return undefined;
1022
+ }
1023
+ const {global, override} = loadGlobalAndOverride(configPath);
1024
+ const overrides = harnessFlagOverrides(flags);
1025
+ const config =
1026
+ repoPath !== undefined
1027
+ ? resolveRepoConfig({repoPath, global, flags: overrides, override}).config
1028
+ : resolveGlobalConfig(global, overrides);
1029
+ const harness = createHarness({
1030
+ harness: config.harness,
1031
+ piBin: config.piBin,
1032
+ });
1033
+ return (site) => {
1034
+ // Generate the pi `--session` path for the onboarded working tree so the
1035
+ // human session is recorded + dashboard-visible (decision #2); the resolved
1036
+ // model flows in (decision #4). The harness's `launchInteractive` runs pi
1037
+ // WITHOUT `--print`, inherited stdio, no piped prompt, in `site.dir`.
1038
+ const session = generateSessionPath({
1039
+ sessionsDir: config.sessionsDir,
1040
+ cwd: site.dir,
1041
+ id: site.slug,
1042
+ });
1043
+ harness.launchInteractive({
1044
+ slug: site.slug,
1045
+ dir: site.dir,
1046
+ model: config.model,
1047
+ session,
1048
+ env: site.env,
1049
+ });
1050
+ };
1051
+ }
1052
+
1053
+ /**
1054
+ * The shared `start`/`resume` action body. `start` and `resume` are the two
1055
+ * human in-place verbs of ADR §4: `start` BEGINS work here (claim if needed +
1056
+ * switch); `resume` CONTINUES here (re-engage an already-in-progress item by
1057
+ * switching to its `work/<slug>` branch WITHOUT claiming). The runtime
1058
+ * difference is exactly the `resume` flag — `resume` forces it on (its only mode
1059
+ * is to re-engage), while `start` honours the (now hidden) `--resume` alias.
1060
+ * Both are task-only (§3a: accept bare + `task:`, reject `prd:`).
1061
+ */
1062
+ async function runStartAction(
1063
+ rawSlug: string | undefined,
1064
+ flags: StartFlags,
1065
+ resume: boolean,
1066
+ ): Promise<void> {
1067
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1068
+ const slug = resolveTaskOnlySlug(rawSlug);
1069
+ const cwd = process.cwd();
1070
+
1071
+ // `resume --isolated <slug>`: re-engage the slug's RETAINED job worktree (the
1072
+ // inverse of `do --isolated`) WITHOUT claiming \u2014 locate it off THIS repo's
1073
+ // arbiter and report its path so the operator can cd in. The symmetric
1074
+ // companion of `complete --isolated` (finish the stranded worktree). `start`
1075
+ // (begin-here) has no isolated form \u2014 there is nothing retained to re-engage yet.
1076
+ if (resume && flags.isolated === true) {
1077
+ if (slug === undefined || slug === '') {
1078
+ console.error(
1079
+ 'error: resume --isolated requires <slug> (the retained worktree to re-engage).',
1080
+ );
1081
+ process.exit(1);
1082
+ }
1083
+ const {config} = loadHumanWorktreesDir(flags.config ?? defaultConfigPath());
1084
+ const located = locateIsolatedRecovery({
1085
+ slug,
1086
+ cwd,
1087
+ arbiter: flags.arbiter ?? config.defaultArbiter,
1088
+ workspacesDir: flags.workspace ?? config.workspacesDir,
1089
+ env: process.env,
1090
+ });
1091
+ if ('error' in located) {
1092
+ console.error(`error: ${located.error}`);
1093
+ process.exit(1);
1094
+ }
1095
+ if (!located.present) {
1096
+ console.error(
1097
+ `>> No retained isolated worktree for '${slug}' (already integrated and ` +
1098
+ 'reaped, or never stranded) \u2014 nothing to resume.',
1099
+ );
1100
+ process.exit(0);
1101
+ }
1102
+ console.error(
1103
+ `>> Re-engaging the retained worktree for '${slug}'. cd into it to ` +
1104
+ `continue, then 'dorfl complete --isolated ${slug}' to finish:`,
1105
+ );
1106
+ process.stdout.write(`${located.dir}\n`);
1107
+ process.exit(0);
1108
+ }
1109
+
1110
+ const result = await performStart({
1111
+ slug,
1112
+ cwd,
1113
+ arbiter: flags.arbiter ?? 'origin',
1114
+ // `resume` (the verb) always asserts ownership; `start` honours --resume.
1115
+ resume: resume || flags.resume === true,
1116
+ override: flags.ignoreNotReady === true,
1117
+ // `--agent`: launch the configured harness INTERACTIVELY in the checkout
1118
+ // after onboarding (task `agent-interactive-launch`). The per-repo config
1119
+ // root is the current checkout.
1120
+ launchInteractive: buildInteractiveLauncher(flags, flags.config, cwd),
1121
+ // HUMAN commands (`start` = "begin here", `resume` = "continue here"): the
1122
+ // onboard/branch/switch is the human's, so it is NOT given the runner
1123
+ // `config.identity` (the autonomous onboard is `do`/`run`, identity-aware).
1124
+ // Ambient `process.env` threaded EXPLICITLY so the choice is declared here,
1125
+ // not left to the seam's silent `?? process.env` fallback.
1126
+ env: process.env,
1127
+ note: (message) => console.error(`>> ${message}`),
1128
+ });
1129
+ if (result.exitCode !== 0) {
1130
+ console.error(`error: ${result.message}`);
1131
+ }
1132
+ process.exit(result.exitCode);
1133
+ }
1134
+
1135
+ /**
1136
+ * Help GROUP labels for the two-tier surface (ADR §7). commander v14's
1137
+ * `command.helpGroup(...)` renders each command under its label heading, so the
1138
+ * HEADLINE tier (the surface a user reaches for) lists first and the
1139
+ * ADVANCED/PLUMBING tier (kept, but de-emphasised) lists under its own heading
1140
+ * — without removing or hiding anything. Headline: run/do/work-on/start/resume/
1141
+ * complete/requeue/scan/status + remote add/ls/find. Advanced: claim/prompt/
1142
+ * verify/gc + remote rm.
1143
+ */
1144
+ const HEADLINE_GROUP = 'Commands:';
1145
+ const ADVANCED_GROUP = 'Advanced / plumbing:';
1146
+ /** Help group for the de-emphasised plumbing FLAGS named in ADR §7. */
1147
+ const ADVANCED_OPT_GROUP = 'Advanced / plumbing options:';
1148
+
1149
+ export function buildProgram(): Command {
1150
+ const program = new Command();
1151
+
1152
+ program
1153
+ .name(brand.bin)
1154
+ .description('Autonomous parallel agents over file-based work/ queues.');
1155
+
1156
+ program
1157
+ .command('scan')
1158
+ .helpGroup(HEADLINE_GROUP)
1159
+ .description(
1160
+ 'Read-only: list the cross-repo queue of work items (across the registered hub mirrors) and whether each is runnable now. Discovery is the registry — the hub-mirror set under workspacesDir/repos/ (no --root/roots).',
1161
+ )
1162
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1163
+ .option(
1164
+ '--auto-build',
1165
+ 'allow agents to auto-build undeclared (not humanOnly) tasks',
1166
+ )
1167
+ .option(
1168
+ '--no-auto-build',
1169
+ 'forbid agents from auto-building undeclared tasks (default)',
1170
+ )
1171
+ .option(
1172
+ '--arbiter-remote <name>',
1173
+ `the current repo's arbiter remote to fetch + diff its local section against (default: ${DEFAULT_ARBITER_REMOTE})`,
1174
+ )
1175
+ .option(
1176
+ '--arbiter <remote>',
1177
+ '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)',
1178
+ )
1179
+ .option(
1180
+ '--here',
1181
+ '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.',
1182
+ )
1183
+ .option('--json', 'output the raw report as JSON')
1184
+ .action(async (flags: ScanFlags, command: Commander) => {
1185
+ const fileConfig = loadConfig(flags.config);
1186
+ const override = loadConfigOverride(
1187
+ defaultConfigOverridePath(flags.config),
1188
+ );
1189
+ const config = resolveGlobalConfig(
1190
+ fileConfig,
1191
+ flagOverrides(flags, command),
1192
+ );
1193
+ const warn = (message: string) => console.error(`>> ${message}`);
1194
+ const resolveCwd = () =>
1195
+ resolveCwdSection({
1196
+ cwd: process.cwd(),
1197
+ config,
1198
+ override,
1199
+ arbiterRemote: flags.arbiterRemote,
1200
+ lockArbiterRemote: flags.arbiter ?? 'origin',
1201
+ warn,
1202
+ });
1203
+ // `--here`: report ONLY the cwd — skip the registry loop ENTIRELY (the fast,
1204
+ // focused path, and the CI shape). The report carries an empty `repos[]` so
1205
+ // the `--json` consumers (the CI matrix `jq`) read `.cwd.repo.*` exactly as
1206
+ // before, with `.repos[]` simply yielding nothing.
1207
+ if (flags.here === true) {
1208
+ const cwdSection = await resolveCwd();
1209
+ const emptyReport = {repos: [], totalItems: 0, totalEligible: 0};
1210
+ if (flags.json) {
1211
+ console.log(
1212
+ JSON.stringify(
1213
+ {...emptyReport, cwd: cwdSection},
1214
+ (_key, value) => (value instanceof Set ? [...value] : value),
1215
+ 2,
1216
+ ),
1217
+ );
1218
+ } else {
1219
+ console.log(formatReport(emptyReport, cwdSection));
1220
+ }
1221
+ return;
1222
+ }
1223
+ const report = await scan(config, {warn, override});
1224
+ // The cwd-local section: resolve it ONLY when a participating cwd is NOT
1225
+ // already covered by a registered mirror. A FETCH-FREE pre-check
1226
+ // (`cwdSectionDisposition`) decides this with zero network I/O; an
1227
+ // already-registered cwd is skipped so we never re-fetch the SAME arbiter the
1228
+ // registry loop just fetched (the `scan-here-and-skip-redundant-cwd`
1229
+ // decision), and an UNregistered cwd is still shown standalone so a
1230
+ // mirror-less repo you are standing in is never invisible.
1231
+ const disposition = cwdSectionDisposition({
1232
+ cwd: process.cwd(),
1233
+ config,
1234
+ arbiterRemote: flags.arbiterRemote,
1235
+ });
1236
+ const cwdSection =
1237
+ disposition.participating && !disposition.alsoRegistered
1238
+ ? await resolveCwd()
1239
+ : undefined;
1240
+ if (flags.json) {
1241
+ console.log(
1242
+ JSON.stringify(
1243
+ {...report, cwd: cwdSection},
1244
+ (_key, value) => (value instanceof Set ? [...value] : value),
1245
+ 2,
1246
+ ),
1247
+ );
1248
+ } else {
1249
+ console.log(formatReport(report, cwdSection));
1250
+ }
1251
+ });
1252
+
1253
+ program
1254
+ .command('run')
1255
+ .helpGroup(HEADLINE_GROUP)
1256
+ .description(
1257
+ 'The cross-repo, parallel daemon: loop the supervised tick over the registry — each tick claims up to maxParallel eligible items (perRepoMax per repo), runs the agents CONCURRENTLY in isolation, integrates, then loops (forever, or until a stop bound). Stuck items surface via the needs-attention seam (on main). `run --once` = one debug tick (NOT the CI path — CI is `do`).',
1258
+ )
1259
+ .option(
1260
+ '--once',
1261
+ 'run a SINGLE supervised tick then stop — the debug/test affordance on the daemon (NOT the CI path; CI uses `do`)',
1262
+ )
1263
+ .option(
1264
+ '--advance',
1265
+ 'DEPRECATED no-op alias: plain `run` ALREADY drives the registry-set advance tick (build/task with calm-default gates; flip observationTriage / surfaceBlockers for the lifecycle). Passing this warns and is otherwise ignored. The old `--advance <mirror>` single-mirror form is gone — the daemon discovers the whole registry via scan.',
1266
+ )
1267
+ .option(
1268
+ '--max-iterations <n>',
1269
+ 'stop after N ticks (a bounded session; default: loop forever)',
1270
+ )
1271
+ .option(
1272
+ '--max-duration <seconds>',
1273
+ 'stop after this many seconds of wall-clock (a bounded session; default: no bound)',
1274
+ )
1275
+ .option(
1276
+ '--interval <seconds>',
1277
+ 'pause this many seconds between ticks (default: 0, back-to-back)',
1278
+ )
1279
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1280
+ .option(
1281
+ '--auto-build',
1282
+ 'allow agents to auto-build undeclared (not humanOnly) tasks',
1283
+ )
1284
+ .option(
1285
+ '--no-auto-build',
1286
+ 'forbid agents from auto-building undeclared tasks (default)',
1287
+ )
1288
+ .option('--max-parallel <n>', 'global cap on items claimed+run this tick')
1289
+ .option('--per-repo-max <n>', 'per-repo cap on concurrent claims')
1290
+ .option('--arbiter <remote>', 'name of the arbiter git remote')
1291
+ .option(
1292
+ '--integration <mode>',
1293
+ 'integration mode: propose (default) or merge',
1294
+ )
1295
+ .option(
1296
+ '--no-pr',
1297
+ 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.',
1298
+ )
1299
+ .option('--agent-cmd <cmd>', 'command to run one agent on a task prompt')
1300
+ .option(
1301
+ '--model <id>',
1302
+ 'model the agent runs on (routing intent; auth/keys stay the harness\u2019s job). pi: passed as --model; null/shell: substitutes a {model} placeholder in agentCmd. Resolved flag > env > per-repo > global > default (unset).',
1303
+ )
1304
+ .option(
1305
+ '--harness <adapter>',
1306
+ 'harness adapter that launches the agent + reports liveness: null (default, shells out to agentCmd) or pi (the pi CLI)',
1307
+ )
1308
+ .option(
1309
+ '--pi-bin <path>',
1310
+ 'pi CLI binary the pi harness invokes (default: pi on PATH)',
1311
+ )
1312
+ .option(
1313
+ '--sessions-dir <dir>',
1314
+ 'HOST-ONLY root folder under which pi session files are generated (--session <dir>/<id>.jsonl). Default: pi per-cwd folder under ~/.pi/agent/sessions. Resolved flag > env > global > default (no per-repo).',
1315
+ )
1316
+ .option(
1317
+ '--workspace <dir>',
1318
+ 'execution working area for hub mirrors + job worktrees (default: workspacesDir / ~/.dorfl)',
1319
+ )
1320
+ .option(
1321
+ '--review',
1322
+ 'run Gate 2 (PR/code review) after verify, before the done-move, on every item (overrides config). Resolved flag > env > per-repo > global > default off.',
1323
+ )
1324
+ .option('--no-review', 'do NOT run Gate 2 this tick (overrides config)')
1325
+ .option(
1326
+ '--review-model <id>',
1327
+ 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.',
1328
+ )
1329
+ .option(
1330
+ '--review-max-rounds <n>',
1331
+ 'bound the revise/review loop; on exhaustion force needs-attention (default 2)',
1332
+ )
1333
+ .option(
1334
+ '--fresh-worktree-gate',
1335
+ 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that integrates). ON by default; the `run` fleet uses it only when same-repo concurrency is off (perRepoMax=1), else today\u2019s in-build-worktree gate.',
1336
+ )
1337
+ .option(
1338
+ '--no-fresh-worktree-gate',
1339
+ 'run the acceptance gate in the build worktree (the pre-rebase tree) — the opt-out for when the per-gate install cost is too high',
1340
+ )
1341
+ .option(
1342
+ '--merge-retries <n>',
1343
+ 'cross-job merge-serialiser CAS-retry cap: a non-fast-forward `${branch}:main` push re-rebases onto the moved <arbiter>/main and retries up to <n> times before a contender bounces to needs-attention. The CAS loop IS the cross-job queue (the in-process integrateLock only serialises sibling integrates in one process), so a wide-matrix CI raises this. Default 1000 (a large liveness ceiling, NOT a small contention budget). Resolved flag > env > per-repo > global > default.',
1344
+ )
1345
+ .option('--json', 'output the raw result as JSON')
1346
+ .action(async (flags: RunFlags, command: Commander) => {
1347
+ const fileConfig = loadConfig(flags.config);
1348
+ const override = loadConfigOverride(
1349
+ defaultConfigOverridePath(flags.config),
1350
+ );
1351
+ const config = resolveGlobalConfig(
1352
+ fileConfig,
1353
+ runFlagOverrides(flags, command),
1354
+ );
1355
+ // The null adapter shells out to agentCmd, so it is required there; the
1356
+ // pi adapter invokes the pi CLI directly and does not consume agentCmd.
1357
+ // Share the ONE predicate (doNeedsAgentCmd) with `do`/`--remote`.
1358
+ if (doNeedsAgentCmd(config)) {
1359
+ throw new Error(NO_AGENT_CMD_MESSAGE);
1360
+ }
1361
+ const workspace = flags.workspace ?? config.workspacesDir;
1362
+ // Gate 2 (PR/code review): wire the PRODUCTION harness-backed gate ONLY when
1363
+ // `config.review` resolves on (mirror the `do`/`complete` commands). The
1364
+ // per-repo review flags are resolved per-item inside `runOneItem`; only the
1365
+ // gate SEAM is threaded here. Off ⇒ undefined ⇒ no review (the default).
1366
+ const reviewGate = config.review ? harnessReviewGate() : undefined;
1367
+ const onWarn = (message: string) => console.error(`>> ${message}`);
1368
+
1369
+ // Plain `run` (no flag) NOW drives the REGISTRY-SET ADVANCE tick as its
1370
+ // per-item unit (task `run-uses-advance-tick`), via the deliberate
1371
+ // {@link RunTick} swap seam: the loop machinery (`runLoop`) is UNCHANGED, the
1372
+ // tick it loops is the precursor's registry-set advance driver instead of the
1373
+ // build-only `runOnce`. With BOTH lifecycle gates at their calm defaults
1374
+ // (observationTriage off, surfaceBlockers off) the advance tick degrades to
1375
+ // EXACTLY the old build tick's behaviour over the SAME substrate (registry-set
1376
+ // discovery + per-mirror job-worktree isolation) — behaviour-preserving today;
1377
+ // flip a gate and the SAME tick performs the lifecycle (triage/surface/apply).
1378
+ // `run` ≡ CI: the same advance tick, a different cadence.
1379
+ const advanceTick = buildRegistrySetAdvanceTick({
1380
+ config,
1381
+ workspace,
1382
+ arbiter: flags.arbiter,
1383
+ env: process.env,
1384
+ override,
1385
+ });
1386
+ // `--advance` is now a DEPRECATED NO-OP ALIAS: plain `run` already IS advance,
1387
+ // so there is no separate mode to opt into. Warn (but do not fail) so an
1388
+ // existing `run --advance` invocation keeps working without surprise.
1389
+ if (flags.advance) {
1390
+ onWarn(
1391
+ '`run --advance` is deprecated and ignored: plain `run` already runs the ' +
1392
+ 'advance tick (build/task with calm-default gates; set observationTriage ' +
1393
+ '/ surfaceBlockers for the lifecycle).',
1394
+ );
1395
+ }
1396
+
1397
+ const printTick = (result: RunOnceResult): void => {
1398
+ if (flags.json) {
1399
+ console.log(JSON.stringify(result, null, 2));
1400
+ return;
1401
+ }
1402
+ for (const item of result.items) {
1403
+ console.log(formatItemLine(item));
1404
+ }
1405
+ console.log(
1406
+ `Summary: ${result.claimedAndDone} done, ${result.skipped} skipped, ${result.failed} failed.`,
1407
+ );
1408
+ };
1409
+
1410
+ // `run --once` = ONE debug tick (NOT the CI path; CI is `do`). The existing
1411
+ // `runOnce` IS this tick.
1412
+ if (flags.once) {
1413
+ // The advance tick IS a RunTick, so `run --once` debug-ticks it (one
1414
+ // registry-set advance batch) identically to how it looped.
1415
+ const result = await advanceTick({
1416
+ config,
1417
+ workspace,
1418
+ reviewGate,
1419
+ onWarn,
1420
+ });
1421
+ printTick(result);
1422
+ return;
1423
+ }
1424
+
1425
+ // `run` (no flag) = the cross-repo, parallel, forever-looping DAEMON: loop
1426
+ // the concurrent tick over the registry until a stop bound (--max-iterations
1427
+ // / --max-duration) or a SIGINT/SIGTERM (graceful shutdown after the current
1428
+ // tick). Stuck items surface via the existing needs-attention seam inside the
1429
+ // tick — the loop never infinite-retries and adds no bespoke reporting.
1430
+ let stopRequested = false;
1431
+ const requestStop = (): void => {
1432
+ if (!stopRequested) {
1433
+ stopRequested = true;
1434
+ console.error(
1435
+ '>> stop requested — finishing the current tick, then exiting.',
1436
+ );
1437
+ }
1438
+ };
1439
+ process.on('SIGINT', requestStop);
1440
+ process.on('SIGTERM', requestStop);
1441
+ try {
1442
+ const summary = await runLoop({
1443
+ config,
1444
+ workspace,
1445
+ reviewGate,
1446
+ onWarn,
1447
+ // The swap seam: plain `run` ALWAYS drives the registry-set ADVANCE tick
1448
+ // (build/task with calm-default gates; the lifecycle when a gate is on).
1449
+ tick: advanceTick,
1450
+ maxIterations:
1451
+ flags.maxIterations !== undefined
1452
+ ? Number(flags.maxIterations)
1453
+ : undefined,
1454
+ maxDurationMs:
1455
+ flags.maxDuration !== undefined
1456
+ ? Number(flags.maxDuration) * 1000
1457
+ : undefined,
1458
+ intervalMs:
1459
+ flags.interval !== undefined ? Number(flags.interval) * 1000 : 0,
1460
+ stop: () => stopRequested,
1461
+ onTick: (result, iteration) => {
1462
+ if (!flags.json) {
1463
+ console.error(`>> tick ${iteration}:`);
1464
+ }
1465
+ printTick(result);
1466
+ },
1467
+ });
1468
+ if (!flags.json) {
1469
+ console.log(
1470
+ `Loop ended (${summary.stoppedBy}) after ${summary.iterations} tick(s): ` +
1471
+ `${summary.claimedAndDone} done, ${summary.skipped} skipped, ${summary.failed} failed.`,
1472
+ );
1473
+ } else {
1474
+ console.log(JSON.stringify(summary, null, 2));
1475
+ }
1476
+ } finally {
1477
+ process.off('SIGINT', requestStop);
1478
+ process.off('SIGTERM', requestStop);
1479
+ }
1480
+ });
1481
+
1482
+ program
1483
+ .command('verify')
1484
+ .helpGroup(ADVANCED_GROUP)
1485
+ .description(
1486
+ "Run the repo's declared acceptance gate (per-repo `verify` config) and exit with its status (0 = pass). Deterministic shell gate; no model. Read-only with respect to work/.",
1487
+ )
1488
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1489
+ .action(async (flags: VerifyFlags) => {
1490
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
1491
+ // DELIBERATELY verify-ONLY: the standalone `verify` command does NOT run the
1492
+ // `prepare` env-prep step first. `verify` is the PURE acceptance gate (env-
1493
+ // ready is a separate concern); a human invoking it prepares their own
1494
+ // checkout. `prepare` runs only in the runner's fresh-worktree lifecycle
1495
+ // (`do`/`run`/`complete` → `performIntegration`), where a fresh job worktree
1496
+ // off the hub mirror genuinely needs deps before the gate can be trusted.
1497
+ const result = await runVerify({
1498
+ cwd: process.cwd(),
1499
+ verify: config.verify,
1500
+ });
1501
+ process.exit(result.exitCode);
1502
+ });
1503
+
1504
+ program
1505
+ .command('claim')
1506
+ .helpGroup(ADVANCED_GROUP)
1507
+ .description(
1508
+ 'Atomically claim a work/backlog/<slug>.md item via a compare-and-swap push to the arbiter (in-process; mirrors scripts/claim.sh).',
1509
+ )
1510
+ .argument('<slug>', 'the slug of the backlog item to claim')
1511
+ .option(
1512
+ '--arbiter <remote>',
1513
+ 'name of the arbiter git remote (default: origin)',
1514
+ 'origin',
1515
+ )
1516
+ .option('--retries <n>', 'cap on push retries when main advances', '3')
1517
+ .option('--dry-run', 'show the intended push without mutating the arbiter')
1518
+ .option(
1519
+ '--ignore-not-ready',
1520
+ 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)',
1521
+ )
1522
+ .action(async (rawSlug: string, flags: ClaimFlags) => {
1523
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1524
+ const slug = resolveTaskOnlySlug(rawSlug) as string;
1525
+ // Wrap ONLY this CLI surface's `performClaim` call with the spinner
1526
+ // helper (task `claim-cas-spinner`): the push can take seconds, so the
1527
+ // terminal looked frozen. In non-TTY mode the helper is a no-op and
1528
+ // stderr stays byte-identical to today (silent on success,
1529
+ // `error: <message>` on failure, `>> <note>` lines unchanged). The
1530
+ // autonomous `performClaim` call sites (`do`/`run`/`start`/`work-on`/
1531
+ // `continue-branch`) are explicitly OUT OF SCOPE.
1532
+ const spinner = createClaimSpinner({
1533
+ stream: process.stderr,
1534
+ isTTY: process.stdout.isTTY === true,
1535
+ clock: {
1536
+ setInterval: (fn, ms) => setInterval(fn, ms),
1537
+ clearInterval: (handle) =>
1538
+ clearInterval(handle as ReturnType<typeof setInterval>),
1539
+ },
1540
+ label: `Claiming ${slug}\u2026`,
1541
+ });
1542
+ const onSigint = (): void => {
1543
+ spinner.stop();
1544
+ process.exit(130);
1545
+ };
1546
+ process.on('SIGINT', onSigint);
1547
+ spinner.start();
1548
+ let result;
1549
+ try {
1550
+ result = await performClaim({
1551
+ slug,
1552
+ cwd: process.cwd(),
1553
+ arbiter: flags.arbiter ?? 'origin',
1554
+ retries:
1555
+ flags.retries !== undefined ? Number(flags.retries) : undefined,
1556
+ dryRun: flags.dryRun,
1557
+ humanPath: true,
1558
+ override: flags.ignoreNotReady === true,
1559
+ // HUMAN command (the `humanPath: true` above already says so): the
1560
+ // standalone `claim` CAS micro-commit + push is the human's, so it is
1561
+ // NOT given the runner `config.identity`. The AUTONOMOUS claim is the one
1562
+ // inside `do`/`run`/`intake` (identity-aware). Thread the ambient
1563
+ // `process.env` EXPLICITLY so the human-identity choice is declared at the
1564
+ // call site, not left to the seam's silent `?? process.env` fallback.
1565
+ env: process.env,
1566
+ note: (message) => spinner.note(message),
1567
+ });
1568
+ } catch (err) {
1569
+ // Unhandled error: tear the spinner down cleanly BEFORE the throw
1570
+ // propagates so the cursor is restored + no orphaned ANSI state.
1571
+ spinner.stop();
1572
+ process.off('SIGINT', onSigint);
1573
+ throw err;
1574
+ }
1575
+ spinner.finish(result);
1576
+ process.off('SIGINT', onSigint);
1577
+ process.exit(result.exitCode);
1578
+ });
1579
+
1580
+ program
1581
+ .command('start')
1582
+ .helpGroup(HEADLINE_GROUP)
1583
+ .description(
1584
+ 'Claim a backlog item (only if needed) and onboard onto its work/<slug> branch in the CURRENT checkout. Decides on the folder on <arbiter>/main, never on a frontmatter field. Launches no agent/editor.',
1585
+ )
1586
+ .argument(
1587
+ '[slug]',
1588
+ 'the slug to start (inferred from a work/<slug> branch if omitted)',
1589
+ )
1590
+ .option(
1591
+ '--arbiter <remote>',
1592
+ 'name of the arbiter git remote (default: origin)',
1593
+ 'origin',
1594
+ )
1595
+ // `--resume` is now the HIDDEN alias of the `resume` verb (ADR §4/§7): the
1596
+ // documented surface is `start` = begin here, `resume` = continue here. Kept
1597
+ // (hidden) for muscle memory; addHelpText below points at the verb.
1598
+ .addOption(
1599
+ new Option(
1600
+ '--resume',
1601
+ '(hidden alias of the `resume` verb) assert ownership of an already in-progress item: switch to its work branch without claiming',
1602
+ ).hideHelp(),
1603
+ )
1604
+ .option(
1605
+ '--ignore-not-ready',
1606
+ 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)',
1607
+ )
1608
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1609
+ .option(
1610
+ '--agent',
1611
+ 'after onboarding, launch the configured harness INTERACTIVELY in the checkout (foreground, you drive it — no prepared prompt). Requires harness: pi. Not a tracked job (no record/gate); you still run `complete`/`requeue`.',
1612
+ )
1613
+ .option(
1614
+ '--harness <name>',
1615
+ 'harness adapter for --agent: pi (interactive launch requires pi). Resolved flag > env > per-repo > global > default.',
1616
+ )
1617
+ .option(
1618
+ '--model <model>',
1619
+ 'model the interactive --agent session starts pinned to (routing intent; you may switch inside pi). Resolved flag > env > per-repo > global > default.',
1620
+ )
1621
+ .option('--pi-bin <path>', 'path to the pi CLI binary (for --agent)')
1622
+ .option(
1623
+ '--sessions-dir <dir>',
1624
+ 'HOST-ONLY root folder under which the --agent pi session file is generated',
1625
+ )
1626
+ .action((rawSlug: string | undefined, flags: StartFlags) =>
1627
+ runStartAction(rawSlug, flags, false),
1628
+ );
1629
+
1630
+ program
1631
+ .command('resume')
1632
+ .helpGroup(HEADLINE_GROUP)
1633
+ .description(
1634
+ 'Re-engage an already in-progress item in the CURRENT checkout: switch to its work/<slug> branch WITHOUT claiming (the item is already in-progress; you assert ownership). The human “continue here” verb — the counterpart to `start` (“begin here”). Decides on the folder on <arbiter>/main, never on a frontmatter field. Launches no agent/editor.',
1635
+ )
1636
+ .argument(
1637
+ '[slug]',
1638
+ 'the slug to resume (inferred from a work/<slug> branch if omitted)',
1639
+ )
1640
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1641
+ .option(
1642
+ '--arbiter <remote>',
1643
+ 'name of the arbiter git remote (default: origin)',
1644
+ 'origin',
1645
+ )
1646
+ .option(
1647
+ '--isolated',
1648
+ "re-engage the slug's RETAINED isolated job worktree (the inverse of `do --isolated`) WITHOUT claiming: locate it off THIS repo's arbiter and print its path to cd into. The symmetric companion of `complete --isolated` (finish the stranded worktree).",
1649
+ )
1650
+ .option(
1651
+ '--workspace <dir>',
1652
+ 'execution working area for job worktrees (--isolated; default: workspacesDir / ~/.dorfl)',
1653
+ )
1654
+ .action((rawSlug: string | undefined, flags: StartFlags) =>
1655
+ runStartAction(rawSlug, flags, true),
1656
+ );
1657
+
1658
+ program
1659
+ .command('work-on')
1660
+ .helpGroup(HEADLINE_GROUP)
1661
+ .description(
1662
+ 'HUMAN command: claim a task and create an isolated worktree in a human-friendly location (under config humanWorktreesDir, NEVER ~/.dorfl) for parallel work, and cd you in by default (via the shell wrapper). Two forms: `work-on <slug>` (in-repo: infer the arbiter from the current repo) and `work-on --remote <r> <slug>` (ensure a hub mirror via repo-mirror, creating if absent) — consistent with `do --remote` (bare = current repo; --remote = anywhere). BOTH claim, then always fetch + branch work/<slug> off the freshly-fetched <arbiter>/main — same claim, same starting commit; only the worktree LOCATION differs. --copy <patterns> copies named gitignored files (copy, not symlink; --copy-from required in remote mode) with a security notice. A binary cannot cd your shell, so install the wrapper `work-on(){ cd "$(dorfl work-on "$@" --print-dir)"; }`; --print-dir is that wrapper’s plumbing (emits ONLY the path).',
1663
+ )
1664
+ .argument(
1665
+ '<slug>',
1666
+ 'the slug to work on (bare = the task; the target repo is the current one, or --remote <r>)',
1667
+ )
1668
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1669
+ .option(
1670
+ '--remote <r>',
1671
+ 'work on a REGISTERED repo with NO checkout: ensure a hub mirror via repo-mirror (creating if absent) and claim against it (consistent with `do --remote`). Omit for the in-repo form (the arbiter is inferred from the current repo).',
1672
+ )
1673
+ .option(
1674
+ '--arbiter <remote>',
1675
+ 'name of the arbiter git remote in the current repo (in-repo form; default: origin)',
1676
+ 'origin',
1677
+ )
1678
+ .addOption(
1679
+ new Option(
1680
+ '--copy <patterns>',
1681
+ 'comma-separated gitignored filenames to COPY into the worktree (e.g. .env.local,.env). In-repo: from the current repo; remote: requires --copy-from. Copy, not symlink.',
1682
+ ).helpGroup(ADVANCED_OPT_GROUP),
1683
+ )
1684
+ .addOption(
1685
+ new Option(
1686
+ '--copy-from <path>',
1687
+ 'source dir for --copy in the remote form (required there; there is no implicit current repo)',
1688
+ ).helpGroup(ADVANCED_OPT_GROUP),
1689
+ )
1690
+ .addOption(
1691
+ new Option(
1692
+ '--print-dir',
1693
+ 'print ONLY the worktree path to stdout (for a shell wrapper: work-on(){ cd "$(dorfl work-on "$@" --print-dir)"; })',
1694
+ ).helpGroup(ADVANCED_OPT_GROUP),
1695
+ )
1696
+ .option(
1697
+ '--workspace <dir>',
1698
+ 'execution working area for hub mirrors (default: workspacesDir / ~/.dorfl)',
1699
+ )
1700
+ .option(
1701
+ '--ignore-not-ready',
1702
+ 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)',
1703
+ )
1704
+ .option(
1705
+ '--agent',
1706
+ 'after creating the worktree, launch the configured harness INTERACTIVELY in it (foreground, you drive it — no prepared prompt). Requires harness: pi. Not a tracked job (no record/gate); you still run `complete`/`requeue`.',
1707
+ )
1708
+ .option(
1709
+ '--harness <name>',
1710
+ 'harness adapter for --agent: pi (interactive launch requires pi). Resolved flag > env > per-repo > global > default.',
1711
+ )
1712
+ .option(
1713
+ '--model <model>',
1714
+ 'model the interactive --agent session starts pinned to (routing intent; you may switch inside pi). Resolved flag > env > per-repo > global > default.',
1715
+ )
1716
+ .option('--pi-bin <path>', 'path to the pi CLI binary (for --agent)')
1717
+ .option(
1718
+ '--sessions-dir <dir>',
1719
+ 'HOST-ONLY root folder under which the --agent pi session file is generated',
1720
+ )
1721
+ .action(async (rawSlug: string, flags: WorkOnFlags) => {
1722
+ // The two forms are now distinguished by the `--remote` FLAG (ADR §4,
1723
+ // consistent with `do --remote`), not a positional <remote>: bare =
1724
+ // the current repo, `--remote <r>` = any registered repo.
1725
+ const remote =
1726
+ flags.remote !== undefined && flags.remote.trim() !== ''
1727
+ ? flags.remote
1728
+ : undefined;
1729
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1730
+ const theSlug = resolveTaskOnlySlug(rawSlug) as string;
1731
+
1732
+ const configPath = flags.config ?? defaultConfigPath();
1733
+ const {dir: configuredRoot, config} = loadHumanWorktreesDir(configPath);
1734
+ const workspace = flags.workspace ?? config.workspacesDir;
1735
+
1736
+ // --print-dir wants a clean stdout, so all human-facing notes go to
1737
+ // stderr; the path is the ONLY thing on stdout (printed below).
1738
+ const printDir = flags.printDir === true;
1739
+ const result = await performWorkOn({
1740
+ slug: theSlug,
1741
+ remote,
1742
+ cwd: process.cwd(),
1743
+ arbiter: flags.arbiter ?? 'origin',
1744
+ copy: flags.copy,
1745
+ copyFrom: flags.copyFrom,
1746
+ override: flags.ignoreNotReady === true,
1747
+ workspacesDir: workspace,
1748
+ humanWorktreesDir: configuredRoot,
1749
+ promptForRoot: (suggestion) => promptForWorktreesRoot(suggestion),
1750
+ saveRoot: (chosen) => persistHumanWorktreesDir(chosen, configPath),
1751
+ // `--agent`: launch the configured harness INTERACTIVELY in the new
1752
+ // worktree after creation (task `agent-interactive-launch`). In-repo
1753
+ // mode resolves per-repo config from the current checkout; remote mode
1754
+ // (no checkout) resolves from the global config only (like `do --remote`).
1755
+ launchInteractive: buildInteractiveLauncher(
1756
+ flags,
1757
+ configPath,
1758
+ remote === undefined ? process.cwd() : undefined,
1759
+ ),
1760
+ // HUMAN command (the description says so): claim + worktree + branch is
1761
+ // the human's, NOT given the runner `config.identity`. Ambient
1762
+ // `process.env` threaded EXPLICITLY (not the seam's silent fallback).
1763
+ env: process.env,
1764
+ note: (message) => console.error(`>> ${message}`),
1765
+ });
1766
+ if (result.exitCode !== 0) {
1767
+ console.error(`error: ${result.message}`);
1768
+ process.exit(result.exitCode);
1769
+ }
1770
+ if (printDir) {
1771
+ // Path only on stdout, so `cd "$(... --print-dir)"` works.
1772
+ process.stdout.write(`${result.dir}\n`);
1773
+ }
1774
+ process.exit(0);
1775
+ });
1776
+
1777
+ program
1778
+ .command('prompt')
1779
+ .helpGroup(ADVANCED_GROUP)
1780
+ .description(
1781
+ "Print to stdout the work-agent prompt for a task: the canonical CLAIM-PROTOCOL wrapper + the task's own ## Prompt (with <slug> and source prd substituted). Resolves work/in-progress/<slug>.md then work/backlog/<slug>.md; infers <slug> from a work/<slug> branch when omitted. Read-only, stdout only — the same assembly the autonomous runner feeds agentCmd.",
1782
+ )
1783
+ .argument(
1784
+ '[slug]',
1785
+ 'the slug to render (inferred from a work/<slug> branch if omitted)',
1786
+ )
1787
+ .action((rawSlug: string | undefined) => {
1788
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1789
+ const slug = resolveTaskOnlySlug(rawSlug);
1790
+ // Resolve the `promptGuidance` NUDGE namespace through the SAME chain the
1791
+ // gate family uses (env > per-repo > global > default), so e.g. a
1792
+ // `promptGuidance.testFirst:true` in `.dorfl.json` strengthens the
1793
+ // wrapper line for `dorfl prompt` exactly as it would in `do`/`run`.
1794
+ const cwd = process.cwd();
1795
+ const global = loadConfig();
1796
+ const resolved = resolveRepoConfig({repoPath: cwd, global}).config;
1797
+ const output = renderPrompt({
1798
+ slug,
1799
+ cwd,
1800
+ promptGuidance: resolvePromptGuidance(resolved),
1801
+ });
1802
+ process.stdout.write(output);
1803
+ });
1804
+
1805
+ program
1806
+ .command('complete')
1807
+ .helpGroup(HEADLINE_GROUP)
1808
+ .description(
1809
+ 'On a work/<slug> branch (slug inferred if omitted): run the gate, mark done (git mv in-progress\u2192done), commit (<type>(<slug>): <summary>; done) the agent\u2019s uncommitted work + the move, rebase onto <arbiter>/main, and integrate. Mode resolved at completion time (--merge/--propose > per-repo > global > default propose): merge\u2192push to main + switch+ff local main; propose\u2192push branch + switch to main (no ff). Then delete the LOCAL work branch iff provably on the arbiter (never the remote); --no-switch stays on the branch and keeps it. Never --force.',
1810
+ )
1811
+ .argument(
1812
+ '[slug]',
1813
+ 'the slug to complete (inferred from a work/<slug> branch if omitted)',
1814
+ )
1815
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1816
+ .option(
1817
+ '--arbiter <remote>',
1818
+ 'name of the arbiter git remote (default: origin)',
1819
+ 'origin',
1820
+ )
1821
+ .option(
1822
+ '--merge',
1823
+ 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)',
1824
+ )
1825
+ .option(
1826
+ '--propose',
1827
+ 'integrate in propose mode this invocation (mutually exclusive with --merge; overrides config)',
1828
+ )
1829
+ .option(
1830
+ '--no-pr',
1831
+ 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.',
1832
+ )
1833
+ .option(
1834
+ '--no-switch',
1835
+ 'stay on the work/<slug> branch (and keep it) instead of switching back to main',
1836
+ )
1837
+ .option(
1838
+ '--ignore-diverged-main',
1839
+ 'override the merge-mode divergence guard: complete --merge even when local main is ahead of <arbiter>/main (unpushed). The work still lands on the arbiter; local main is left for you to `git rebase`. Loud, never default.',
1840
+ )
1841
+ .option(
1842
+ '--isolated',
1843
+ "FINISH a STRANDED isolated worktree: integrate the slug's already-committed, already-done-moved retained job worktree (a terminal push failed AFTER the done-move+commit) by running ONLY the rebase\u2192integrate tail from the kept commit \u2014 the locate-EXISTING inverse of `do --isolated`. Detection is unspoofable: an already-integrated task is a clean no-op; no retained worktree is a clean \u201cnothing to recover\u201d. --merge/--propose/--arbiter resolve identically to a normal integrate; the already-passed gate is skipped.",
1844
+ )
1845
+ .option(
1846
+ '--workspace <dir>',
1847
+ 'execution working area for job worktrees (--isolated; default: workspacesDir / ~/.dorfl)',
1848
+ )
1849
+ .addOption(
1850
+ new Option(
1851
+ '--skip-verify',
1852
+ 'skip the acceptance gate (human-only escape hatch; the runner never skips)',
1853
+ ).helpGroup(ADVANCED_OPT_GROUP),
1854
+ )
1855
+ .addOption(
1856
+ new Option('--type <type>', 'conventional-commit type for the commit')
1857
+ .default('feat')
1858
+ .helpGroup(ADVANCED_OPT_GROUP),
1859
+ )
1860
+ .addOption(
1861
+ new Option(
1862
+ '--message <summary>',
1863
+ 'commit summary (default: the task title, minus a leading "slug \u2014 " prefix)',
1864
+ ).helpGroup(ADVANCED_OPT_GROUP),
1865
+ )
1866
+ .option(
1867
+ '--review',
1868
+ 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > per-repo > global > default off.',
1869
+ )
1870
+ .option(
1871
+ '--no-review',
1872
+ 'do NOT run Gate 2 this invocation (overrides config)',
1873
+ )
1874
+ .option(
1875
+ '--review-model <id>',
1876
+ 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.',
1877
+ )
1878
+ .option(
1879
+ '--review-max-rounds <n>',
1880
+ 'bound the revise/review loop; on exhaustion force needs-attention (default 2)',
1881
+ )
1882
+ .option(
1883
+ '--fresh-worktree-gate',
1884
+ 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that integrates). ON by default. Resolved flag > env > per-repo > global > default on.',
1885
+ )
1886
+ .option(
1887
+ '--no-fresh-worktree-gate',
1888
+ 'run the acceptance gate in the current checkout (the pre-rebase tree) — the opt-out for when the per-gate install cost is too high',
1889
+ )
1890
+ .option(
1891
+ '--merge-retries <n>',
1892
+ 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.',
1893
+ )
1894
+ .action(async (rawSlug: string | undefined, flags: CompleteFlags) => {
1895
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1896
+ const slug = resolveTaskOnlySlug(rawSlug);
1897
+ const cwd = process.cwd();
1898
+ const {global, override} = loadGlobalAndOverride(flags.config);
1899
+
1900
+ // `--isolated`: FINISH a stranded isolated worktree (the recover-already-
1901
+ // committed path) instead of completing the current checkout. It LOCATES the
1902
+ // slug's retained job worktree off THIS repo's arbiter and runs ONLY the
1903
+ // rebase\u2192integrate tail from the kept commit. The slug is REQUIRED (there is no
1904
+ // branch to infer it from in the operator's checkout).
1905
+ if (flags.isolated === true) {
1906
+ if (slug === undefined || slug === '') {
1907
+ console.error(
1908
+ 'error: complete --isolated requires <slug> (the stranded item to finish).',
1909
+ );
1910
+ process.exit(1);
1911
+ }
1912
+ const flagMode = integrationFromFlags(flags);
1913
+ const resolved = resolveRepoConfig({
1914
+ repoPath: cwd,
1915
+ global,
1916
+ override,
1917
+ flags: {
1918
+ ...(flagMode ? {integration: flagMode} : {}),
1919
+ ...noPRFlagOverrides(flags),
1920
+ },
1921
+ });
1922
+ if (resolved.message) {
1923
+ console.error(`>> ${resolved.message}`);
1924
+ }
1925
+ const isoConfig = resolved.config;
1926
+ const recovered = await performRecoverIsolated({
1927
+ slug,
1928
+ cwd,
1929
+ arbiter: flags.arbiter ?? isoConfig.defaultArbiter,
1930
+ workspacesDir: flags.workspace ?? isoConfig.workspacesDir,
1931
+ integration: isoConfig.integration,
1932
+ noPR: isoConfig.noPR,
1933
+ note: (message) => console.error(`>> ${message}`),
1934
+ env: process.env,
1935
+ });
1936
+ if (recovered.exitCode !== 0) {
1937
+ console.error(`error: ${recovered.message}`);
1938
+ }
1939
+ process.exit(recovered.exitCode);
1940
+ }
1941
+
1942
+ // Resolve the integration mode at completion time, highest first:
1943
+ // --merge/--propose flag > per-repo .dorfl.json > global > default.
1944
+ // The flag sits at the TOP of the same chain the autonomous runner uses
1945
+ // (per-repo > global > default), so human and autonomous paths agree.
1946
+ const flagMode = integrationFromFlags(flags);
1947
+ const resolved = resolveRepoConfig({
1948
+ repoPath: cwd,
1949
+ global,
1950
+ override,
1951
+ // The integrate-time mode AND the Gate-2 review flags ride the SAME
1952
+ // flag > env > per-repo > global > default chain.
1953
+ flags: {
1954
+ ...(flagMode ? {integration: flagMode} : {}),
1955
+ ...reviewFlagOverrides(flags),
1956
+ // `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain.
1957
+ ...freshWorktreeGateFlagOverrides(flags),
1958
+ // `--merge-retries <n>` rides the SAME chain: the cross-job merge-serialiser
1959
+ // CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5
1960
+ // / Applied Answer q1 (a)).
1961
+ ...mergeRetriesFlagOverrides(flags),
1962
+ // `--no-pr` (the PR-INTENT axis) rides the SAME chain.
1963
+ ...noPRFlagOverrides(flags),
1964
+ },
1965
+ });
1966
+ if (resolved.message) {
1967
+ console.error(`>> ${resolved.message}`);
1968
+ }
1969
+ const config = resolved.config;
1970
+ const result = await performComplete({
1971
+ slug,
1972
+ cwd,
1973
+ arbiter: flags.arbiter ?? config.defaultArbiter,
1974
+ integration: config.integration,
1975
+ // An EXPLICIT `--merge` overrides the untrusted-origin build-propose rule (task
1976
+ // `untrusted-origin-forces-build-propose`): `flagMode` is the typed flag
1977
+ // (undefined when none), so this is true ONLY when the operator typed
1978
+ // `--merge`, never when `merge` was resolved from config.
1979
+ explicitMerge: flagMode === 'merge',
1980
+ noPR: config.noPR,
1981
+ noSwitch: flags.switch === false,
1982
+ ignoreDivergedMain: flags.ignoreDivergedMain === true,
1983
+ prepare: config.prepare,
1984
+ verify: config.verify,
1985
+ skipVerify: flags.skipVerify,
1986
+ // Gate 2 (PR/code review): when `review` resolves on, run the `review`
1987
+ // SKILL as a fresh-context agent (the production harness-backed gate)
1988
+ // AFTER the green verify and BEFORE the done-move. The `reviewModel`
1989
+ // override flows to the launch through the existing harness seam.
1990
+ review: config.review,
1991
+ reviewModel: config.reviewModel,
1992
+ reviewMaxRounds: config.reviewMaxRounds,
1993
+ reviewGate: config.review ? harnessReviewGate() : undefined,
1994
+ // Run the acceptance gate against the REBASED tip in a clean throwaway
1995
+ // worktree (the tree that integrates) when ON (the default). `complete` is
1996
+ // a single-job path, so the resolved flag is passed UNCONDITIONALLY (no
1997
+ // fleet downgrade).
1998
+ freshWorktreeGate: config.freshWorktreeGate,
1999
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
2000
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — the resolved
2001
+ // per-repo value reaches the merge loop via `performComplete`→
2002
+ // `performIntegration`.
2003
+ mergeRetries: config.mergeRetries,
2004
+ type: flags.type,
2005
+ message: flags.message,
2006
+ // Color the propose-mode next-step block only on an interactive
2007
+ // stdout TTY (and not under NO_COLOR); plain when piped/redirected.
2008
+ color: shouldUseColor(process.stdout),
2009
+ note: (message) => console.error(`>> ${message}`),
2010
+ // The propose next-step block is printed verbatim (no `>> ` prefix)
2011
+ // so its blank lines + heading stand out as the human call-to-action.
2012
+ noteBlock: (message) => console.error(message),
2013
+ // `complete` is a HUMAN command: a human finishing/merging the work, so
2014
+ // the commit/push/PR is THEIRS — it is deliberately NOT given the runner
2015
+ // `config.identity` (the autonomous completion is `do`'s own integrated
2016
+ // complete, which IS identity-aware). Thread the ambient `process.env`
2017
+ // EXPLICITLY so the human-identity choice is declared at the call site,
2018
+ // not left to the seam's silent `?? process.env` fallback (parity with
2019
+ // `requeue`).
2020
+ env: process.env,
2021
+ });
2022
+ if (result.exitCode !== 0) {
2023
+ console.error(`error: ${result.message}`);
2024
+ }
2025
+ process.exit(result.exitCode);
2026
+ });
2027
+
2028
+ program
2029
+ .command('do')
2030
+ .helpGroup(HEADLINE_GROUP)
2031
+ .description(
2032
+ 'The per-repo WORKER (the CI command): claim + onboard onto work/<slug>, run the agent, gate, integrate, and exit. In the CURRENT checkout by default (refuses on a dirty tree, integrates in-place). With --remote <r>: against a REGISTERED repo with NO checkout — materialise a hub mirror + job worktree in the agents\u2019 area, run the same pipeline there, then reap. do <slug> | do task:<slug> | do spec:<slug> (the tasking path; the legacy prd:<slug> is still accepted) | do (auto-pick one) | do <a> <b> (those, in sequence) | do -n <x> (x eligible, in sequence). Auto-pick draws TASKS-FIRST then SPECS-to-task by default (per-repo selectionOrder reorders the pools). --propose (default) / --merge resolved at integrate-time. Supersedes ar-run.sh.',
2033
+ )
2034
+ // EXTENSIBLE argument grammar (the three do-* tasks grow this one block):
2035
+ // `do-autopick` widens the single optional positional into a VARIADIC one so
2036
+ // `do` (zero args = auto-pick), `do <a> <b> …` (named, in sequence), and
2037
+ // `do <slug>` (exactly one) all share the one command. `-n <x>` is the count
2038
+ // for the auto-pick form. `do` stays SEQUENTIAL (parallelism is `run`).
2039
+ .argument(
2040
+ '[slugs...]',
2041
+ 'the item(s) to do: bare (= the task), task:<slug>, or spec:<slug> (task the spec; the legacy prd:<slug> is still accepted). Zero args = auto-pick; multiple = do them in sequence.',
2042
+ )
2043
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2044
+ .option(
2045
+ '--arbiter <remote>',
2046
+ 'name of the arbiter git remote (default: per-repo/global defaultArbiter)',
2047
+ )
2048
+ .option(
2049
+ '-n, --number <x>',
2050
+ 'AUTO-PICK x eligible items and do them IN SEQUENCE (ordered by selectionOrder, default drain = tasks-first then prds-to-task). Sequential — never a parallelism knob (that is `run`). Mutually exclusive with naming items.',
2051
+ )
2052
+ .option(
2053
+ '--selection-order <order>',
2054
+ 'order the auto-pick pools (build/task/surface/triage; apply is always first): a preset keyword (drain (default) | groom) or an explicit comma-separated pool list (e.g. build,task,surface,triage). Resolved flag > env > per-repo > global > default.',
2055
+ )
2056
+ .option(
2057
+ '--remote <r>',
2058
+ 'run against a REGISTERED repo with NO checkout: materialise a hub mirror + job worktree in the agents\u2019 area (auto-registers an unknown remote), run the pipeline there, then reap (never touches the human area)',
2059
+ )
2060
+ .option(
2061
+ '--isolated',
2062
+ "build in an ISOLATED job worktree off THIS repo's arbiter (inferred from cwd) instead of taking over the current checkout, then integrate + reap \u2014 the in-place-but-isolated form. Shares the same grammar as the no-checkout forms: a single named item, multiple named items (in sequence), AND -n/auto-pick over the mirror-side eligible-pool scan. Always SEQUENTIAL (parallelism is `run` / the CI matrix). Orthogonal to --remote (a foreign repo); with --remote, remote wins (isolation is already implied).",
2063
+ )
2064
+ .option(
2065
+ '--merge',
2066
+ 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)',
2067
+ )
2068
+ .option(
2069
+ '--propose',
2070
+ 'integrate in propose mode this invocation (default; mutually exclusive with --merge; overrides config)',
2071
+ )
2072
+ .option(
2073
+ '--tasks-land-in <where>',
2074
+ 'where `do prd:<slug>` tasking output lands: `pre-backlog` (staged, not agent-eligible) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > tasksLandIn default > built-in). Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.',
2075
+ )
2076
+ .option(
2077
+ '--no-pr',
2078
+ 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.',
2079
+ )
2080
+ .option(
2081
+ '--ignore-diverged-main',
2082
+ 'override the in-place divergence guard: run even when local main is ahead of <arbiter>/main (unpushed). The work still lands on the arbiter; local main is left for you to `git rebase`. In-place only; loud, never default.',
2083
+ )
2084
+ .option(
2085
+ '--allow-backlog',
2086
+ 'do task:<slug> ONLY: also FIND, CLAIM, and COMPLETE a task that lives in tasks/backlog/ (staging), driving it in place WITHOUT promoting it to the pool (so no advance leg / run daemon can claim it out from under you). The done-move goes tasks/backlog/ -> tasks/done/ directly (your explicit drive IS the promotion). EXPLICIT-INVOCATION-ONLY: default off, never set by run/auto-pick/advance or config/env.',
2087
+ )
2088
+ .option('--agent-cmd <cmd>', 'command to run the agent on the task prompt')
2089
+ .option(
2090
+ '--model <id>',
2091
+ 'model the agent runs on (routing intent; resolved flag > env > per-repo > global > default)',
2092
+ )
2093
+ .option(
2094
+ '--harness <adapter>',
2095
+ 'harness adapter that launches the agent: null (default, shells out to agentCmd) or pi (the pi CLI)',
2096
+ )
2097
+ .option(
2098
+ '--pi-bin <path>',
2099
+ 'pi CLI binary the pi harness invokes (default: pi on PATH)',
2100
+ )
2101
+ .option(
2102
+ '--sessions-dir <dir>',
2103
+ 'HOST-ONLY root folder under which the pi session file is generated (--session <dir>/<id>.jsonl). Default: pi per-cwd folder under ~/.pi/agent/sessions. Resolved flag > env > global > default (no per-repo).',
2104
+ )
2105
+ .option(
2106
+ '--watch',
2107
+ "stream the agent's high-signal events live by tailing the pi session log (requires harness: pi; READ-ONLY observer — does not change outcome/gate/git)",
2108
+ )
2109
+ .option(
2110
+ '--review',
2111
+ 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > env > per-repo > global > default off.',
2112
+ )
2113
+ .option(
2114
+ '--no-review',
2115
+ 'do NOT run Gate 2 this invocation (overrides config)',
2116
+ )
2117
+ .option(
2118
+ '--review-model <id>',
2119
+ 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.',
2120
+ )
2121
+ .option(
2122
+ '--review-max-rounds <n>',
2123
+ 'bound the revise/review loop; on exhaustion force needs-attention (default 2)',
2124
+ )
2125
+ .option(
2126
+ '--tasker-loop',
2127
+ 'run the tasker IMPROVER loop on `do prd:<slug>` (review→edit→converge over the produced task set). ON by default; --no-tasker-loop skips it. DISTINCT from the acceptance gate (--review).',
2128
+ )
2129
+ .option(
2130
+ '--no-tasker-loop',
2131
+ 'skip the tasker improver loop on `do prd:<slug>`',
2132
+ )
2133
+ .option(
2134
+ '--tasker-loop-max <n>',
2135
+ 'cap the tasker improver loop on `do prd:<slug>` (in-context review passes); on exhaustion with blockers, reject via needsAnswers / route the prd to needs-attention (default 3)',
2136
+ )
2137
+ .option(
2138
+ '--tasker-loop-model <id>',
2139
+ 'model the tasker improver loop review agent runs on (de-correlated from the tasker; routing intent). Resolved flag > env > per-repo > global > default. DISTINCT from --review-model.',
2140
+ )
2141
+ .option(
2142
+ '--fresh-worktree-gate',
2143
+ '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.',
2144
+ )
2145
+ .option(
2146
+ '--no-fresh-worktree-gate',
2147
+ "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",
2148
+ )
2149
+ .option(
2150
+ '--merge-retries <n>',
2151
+ 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.',
2152
+ )
2153
+ .action(async (rawSlugs: string[], flags: DoFlags) => {
2154
+ // Variadic grammar (`do-autopick`): zero args = AUTO-PICK; one = the single
2155
+ // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count.
2156
+ const args = rawSlugs ?? [];
2157
+
2158
+ // `-n <x>` parse + validation. It is the AUTO-PICK count (sequential), so it
2159
+ // is mutually exclusive with NAMING items (you either auto-pick a count or
2160
+ // name the items, not both).
2161
+ let count: number | undefined;
2162
+ if (flags.number !== undefined) {
2163
+ const n = Number(flags.number);
2164
+ if (flags.number.trim() === '' || !Number.isInteger(n) || n < 1) {
2165
+ console.error(
2166
+ `error: -n/--number must be a positive integer (got '${flags.number}').`,
2167
+ );
2168
+ process.exit(1);
2169
+ }
2170
+ if (args.length > 0) {
2171
+ console.error(
2172
+ 'error: -n/--number auto-picks a COUNT of eligible items; do not also ' +
2173
+ 'name items. Use `do -n <x>` OR `do <a> <b> ...`, not both.',
2174
+ );
2175
+ process.exit(1);
2176
+ }
2177
+ count = n;
2178
+ }
2179
+
2180
+ const cwd = process.cwd();
2181
+ const {global, override} = loadGlobalAndOverride(flags.config);
2182
+ // Resolve the integration mode at integrate-time, highest first:
2183
+ // --merge/--propose flag > per-repo .dorfl.json > global > default.
2184
+ // (Same chain `complete` uses — `do` is the autonomous twin.)
2185
+ let flagMode;
2186
+ try {
2187
+ flagMode = integrationFromFlags(flags);
2188
+ } catch (err) {
2189
+ console.error(
2190
+ `error: ${err instanceof Error ? err.message : String(err)}`,
2191
+ );
2192
+ process.exit(1);
2193
+ }
2194
+
2195
+ // `do --remote <r>` / `do --isolated <slug>`: run the NO-CHECKOUT job-worktree
2196
+ // pipeline. Both materialise a hub mirror + job worktree in the agents' area
2197
+ // (`workspacesDir`) and reap per ADR §4 — the human area is NEVER touched.
2198
+ //
2199
+ // `--remote <r>` names the TARGETING axis (a FOREIGN repo, no checkout); the
2200
+ // arbiter spec is the `<r>` URL. `--isolated` names the ISOLATION intent (a
2201
+ // worktree off MY OWN arbiter, even though I am inside the repo); its arbiter
2202
+ // URL is RESOLVED FROM THE CWD's arbiter remote (the same `--arbiter` >
2203
+ // per-repo/global `defaultArbiter` name in-place `do` uses). The two are
2204
+ // ORTHOGONAL: `--isolated` + `--remote` is REDUNDANT (a foreign `--remote` is
2205
+ // already isolated), so we accept it and `--remote` WINS (see `## Decisions`).
2206
+ //
2207
+ // In BOTH cases the repo's COMMITTED `.dorfl.json` is reachable on
2208
+ // `<arbiter>/main` (the mirror), so we layer it — `flag > env > per-repo >
2209
+ // global > default` parity with in-place `do` (task
2210
+ // `remote-do-reads-per-repo-config-from-arbiter-main`). Only the whitelisted
2211
+ // `REPO_ALLOWED_KEYS` are layered (host-only keys stay global/flag/env-only,
2212
+ // rejected by the SAME `repo-config.ts` split).
2213
+ const isolatedNoRemote =
2214
+ flags.isolated === true && flags.remote === undefined;
2215
+ if (flags.remote !== undefined || isolatedNoRemote) {
2216
+ // The form's user-facing name + canonical usage, for the shared error
2217
+ // messages below (so `--isolated` errors read in its own terms).
2218
+ const form = isolatedNoRemote ? '--isolated' : '--remote';
2219
+ const usage = isolatedNoRemote
2220
+ ? '`do --isolated <slug>`'
2221
+ : '`do --remote <r> <slug>`';
2222
+ // The no-checkout forms now support the SAME variadic grammar the in-place
2223
+ // form does: a single NAMED item, MULTIPLE named items (sequential), and
2224
+ // AUTO-PICK / `-n <x>` (sequential) over the MIRROR-SIDE eligible-pool scan
2225
+ // (`mirror-side-eligible-pool-scan`). The old inline `-n`×`--remote` REFUSAL
2226
+ // is GONE — the mirror scan backs it now (US #25); `-n` stays ALWAYS
2227
+ // SEQUENTIAL (parallelism is `run` / the CI matrix). `-n` is still mutually
2228
+ // exclusive with naming items (validated above, shared with the in-place form).
2229
+ const remoteFlags = doFlagOverrides(flags, flagMode);
2230
+ // Resolve the arbiter spec the rest of the pipeline consumes as `remote`.
2231
+ // `--remote` supplies it directly (a foreign URL). `--isolated` resolves it
2232
+ // from the CWD's arbiter remote (`git remote get-url`); no resolvable
2233
+ // arbiter ⇒ a CLEAR error naming `--remote <url>` as the foreign-repo
2234
+ // alternative — NOT a confusing URL-parse failure downstream.
2235
+ let effectiveRemote: string;
2236
+ if (isolatedNoRemote) {
2237
+ const bootstrapIdentity = resolveGlobalConfig(
2238
+ global,
2239
+ remoteFlags,
2240
+ ).identity;
2241
+ const arbiterName =
2242
+ flags.arbiter ??
2243
+ resolveDefaultArbiterForCwd(cwd, global, remoteFlags, override);
2244
+ const resolvedUrl = resolveArbiterUrlFromCheckout(
2245
+ cwd,
2246
+ arbiterName,
2247
+ identityEnv(bootstrapIdentity, process.env),
2248
+ );
2249
+ if (resolvedUrl === undefined) {
2250
+ console.error(
2251
+ `error: --isolated builds in a worktree off this repo's arbiter ` +
2252
+ `('${arbiterName}'), but no such arbiter remote is configured/found ` +
2253
+ `here. Run inside a participating repo (a clone with an arbiter ` +
2254
+ `remote), or use --remote <url> to target another repo.`,
2255
+ );
2256
+ process.exit(1);
2257
+ }
2258
+ effectiveRemote = resolvedUrl;
2259
+ } else {
2260
+ effectiveRemote = flags.remote as string;
2261
+ }
2262
+ // BOOTSTRAP resolution (global + flags, no per-repo layer) — it supplies
2263
+ // the HOST-ONLY keys needed to even reach the arbiter's committed file:
2264
+ // `workspacesDir` (where the mirror lives) and `identity` (the git env the
2265
+ // mirror fetch runs under). These are host-only by definition (rejected
2266
+ // per-repo), so reading them from global+flags first is correct and stable.
2267
+ const bootstrap = resolveGlobalConfig(global, remoteFlags);
2268
+ // Source the committed `.dorfl.json` from `<arbiter>/main` via the
2269
+ // hub mirror, then layer ONLY its whitelisted keys through the EXISTING
2270
+ // per-repo machinery. The read refreshes ONLY `main` (no-prune), so a
2271
+ // `work/<slug>` branch checked out in a stale worktree can never block it,
2272
+ // and the build's later all-heads materialisation fetch is unaffected. A
2273
+ // config-less repo (no file on
2274
+ // main, or an unreachable mirror) → exactly the bootstrap config, i.e.
2275
+ // byte-identical to the pre-task global+default behaviour.
2276
+ const remoteConfig = resolveRemoteRepoConfig({
2277
+ remote: effectiveRemote,
2278
+ workspacesDir: bootstrap.workspacesDir,
2279
+ global,
2280
+ flags: remoteFlags,
2281
+ identity: bootstrap.identity,
2282
+ note: (message) => console.error(`>> ${message}`),
2283
+ override,
2284
+ });
2285
+ if (doNeedsAgentCmd(remoteConfig)) {
2286
+ console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
2287
+ process.exit(1);
2288
+ }
2289
+ const remoteHarness = createHarness({
2290
+ harness: remoteConfig.harness,
2291
+ piBin: remoteConfig.piBin,
2292
+ });
2293
+ // The per-item `DoRemoteOptions` (everything BUT `arg`) — built ONCE and
2294
+ // reused for the single-item path AND threaded by the mirror-side auto-pick
2295
+ // driver (`performDoRemoteAuto`) to each sequential `performDoRemote`.
2296
+ const baseRemoteOptions: Omit<DoRemoteOptions, 'arg'> = {
2297
+ remote: effectiveRemote,
2298
+ workspacesDir: remoteConfig.workspacesDir,
2299
+ arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
2300
+ // Host-only runner IDENTITY — scopes git/provider ops only (not the
2301
+ // agent launch); absent ⇒ ambient.
2302
+ identity: remoteConfig.identity,
2303
+ // `do --remote prd:<slug>` tasking-gate policy (task-build path ignores it).
2304
+ autoTask: remoteConfig.autoTask,
2305
+ // The resolved `promptGuidance` nudge — threaded into the remote worker
2306
+ // prompt (runRemotePipeline → buildAgentPrompt), mirroring in-place `do`.
2307
+ promptGuidance: resolvePromptGuidance(remoteConfig),
2308
+ integration: remoteConfig.integration,
2309
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule.
2310
+ explicitMerge: flagMode === 'merge',
2311
+ // Per-TRANSITION TASKING override (the `do --remote prd:` tasking path).
2312
+ taskingIntegration: remoteConfig.taskingIntegration,
2313
+ // TASK-PLACEMENT: the configured default + the EXPLICIT operator override
2314
+ // (`--tasks-land-in`), the top of the placement precedence — mirrors
2315
+ // `explicitMerge` (set only when the flag was typed).
2316
+ tasksLandIn: remoteConfig.tasksLandIn,
2317
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
2318
+ prepare: remoteConfig.prepare,
2319
+ verify: remoteConfig.verify,
2320
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
2321
+ freshWorktreeGate: remoteConfig.freshWorktreeGate,
2322
+ // Cross-job merge-serialiser CAS-retry cap (resolved through the per-repo
2323
+ // chain on the arbiter-side `.dorfl.json` too) — prd
2324
+ // `land-time-reverify-and-parallel-merge-ceiling` Story 5.
2325
+ mergeRetries: remoteConfig.mergeRetries,
2326
+ noPR: remoteConfig.noPR,
2327
+ harness: remoteHarness,
2328
+ agentCmd: remoteConfig.agentCmd,
2329
+ model: remoteConfig.model,
2330
+ sessionsDir: remoteConfig.sessionsDir,
2331
+ review: remoteConfig.review,
2332
+ reviewModel: remoteConfig.reviewModel,
2333
+ reviewMaxRounds: remoteConfig.reviewMaxRounds,
2334
+ reviewGate: remoteConfig.review
2335
+ ? harnessReviewGate({
2336
+ harness: remoteHarness,
2337
+ agentCmd: remoteConfig.agentCmd,
2338
+ })
2339
+ : undefined,
2340
+ // The tasker IMPROVER loop on the `do --remote prd:` path is ON by default
2341
+ // (auto-tasking has no `verify` floor, so the loop is the task path's
2342
+ // quality engine). `--tasker-loop`/`--no-tasker-loop` gates wiring the seam;
2343
+ // `taskerLoopMax`/`taskerLoopModel` resolve per-repo (flag > env > per-repo
2344
+ // > global > default). DISTINCT from the gate's `--review*` family.
2345
+ reviewLoop: remoteConfig.taskerLoop
2346
+ ? harnessTaskReviewGate({
2347
+ harness: remoteHarness,
2348
+ agentCmd: remoteConfig.agentCmd,
2349
+ })
2350
+ : undefined,
2351
+ taskerLoopMax: remoteConfig.taskerLoopMax,
2352
+ taskerLoopModel: remoteConfig.taskerLoopModel,
2353
+ // The task-SET ACCEPTANCE GATE on the `do --remote prd:` path too.
2354
+ taskReviewGate: remoteConfig.review
2355
+ ? harnessTaskAcceptanceGate({
2356
+ harness: remoteHarness,
2357
+ agentCmd: remoteConfig.agentCmd,
2358
+ })
2359
+ : undefined,
2360
+ watch: flags.watch === true,
2361
+ color: shouldUseColor(process.stdout),
2362
+ note: (message) => console.error(`>> ${message}`),
2363
+ noteBlock: (message) => console.error(message),
2364
+ };
2365
+
2366
+ // DISPATCH the variadic grammar (the NO-CHECKOUT forms):
2367
+ // zero args -> AUTO-PICK `count` (default 1) over the MIRROR-SIDE
2368
+ // eligible-pool scan, run SEQUENTIALLY.
2369
+ // one named arg -> the single-item remote pipeline (unchanged).
2370
+ // many named args -> those, IN SEQUENCE (operator's order; no pool).
2371
+ // `--watch` tails ONE session, so it only fits the single-named-item form;
2372
+ // the auto/`-n`/multi forms run many ticks and do not stream a single log.
2373
+ const remoteMulti =
2374
+ args.length === 0 || count !== undefined || args.length > 1;
2375
+ if (remoteMulti && flags.watch === true) {
2376
+ console.error(
2377
+ `error: --watch streams ONE session; it does not combine with the ` +
2378
+ `${form} auto-pick / -n / multi-item forms. Name a single item: ${usage}.`,
2379
+ );
2380
+ process.exit(1);
2381
+ }
2382
+ // `--allow-backlog` is EXPLICIT-SINGLE-TASK-ONLY (the leak-fence): it must
2383
+ // not combine with the no-checkout auto-pick / -n / multi-item forms
2384
+ // (those select FROM the pool). Reject the misuse loudly, mirroring the
2385
+ // in-place guard + the `--watch` multi guard above.
2386
+ if (remoteMulti && flags.allowBacklog === true) {
2387
+ console.error(
2388
+ `error: --allow-backlog drives ONE named staged task in place; it does ` +
2389
+ `not combine with the ${form} auto-pick / -n / multi-item forms ` +
2390
+ `(those select from the pool). Name a single task: ${usage} --allow-backlog.`,
2391
+ );
2392
+ process.exit(1);
2393
+ }
2394
+ if (args.length === 0 || count !== undefined) {
2395
+ // AUTO-PICK / `-n <x>` over the MIRROR-SIDE eligible-pool scan, SEQUENTIAL.
2396
+ const multi = await performDoRemoteAuto({
2397
+ ...baseRemoteOptions,
2398
+ config: remoteConfig,
2399
+ count,
2400
+ warn: (message) => console.error(`>> ${message}`),
2401
+ });
2402
+ console.error(`>> ${multi.message}`);
2403
+ process.exit(multi.exitCode);
2404
+ }
2405
+ if (args.length > 1) {
2406
+ // EXPLICIT named items, IN SEQUENCE (the operator's order; no pool).
2407
+ const multi = await performDoRemoteArgs(args, {
2408
+ ...baseRemoteOptions,
2409
+ config: remoteConfig,
2410
+ });
2411
+ console.error(`>> ${multi.message}`);
2412
+ process.exit(multi.exitCode);
2413
+ }
2414
+
2415
+ // Exactly one named item: the single-item remote pipeline.
2416
+ // `--allow-backlog` rides ONLY this single-named-task call (never the
2417
+ // shared base used by auto-pick / multi) — the leak-fence.
2418
+ const remoteResult = await performDoRemote({
2419
+ ...baseRemoteOptions,
2420
+ arg: args[0],
2421
+ allowBacklog: flags.allowBacklog === true,
2422
+ });
2423
+ if (remoteResult.exitCode !== 0) {
2424
+ console.error(`error: ${remoteResult.message}`);
2425
+ }
2426
+ process.exit(remoteResult.exitCode);
2427
+ }
2428
+
2429
+ // Thread the `do` CLI flags (--harness/--agent-cmd/--pi-bin/--model)
2430
+ // AND the integrate-time mode into the resolved config — the SAME flag
2431
+ // override path `run` uses (do-config.doFlagOverrides reuses
2432
+ // harnessFlagOverrides). Passing only `{integration}` here silently
2433
+ // DROPPED --harness pi etc.; now flag > env > per-repo > global > default
2434
+ // holds for `do` as for `run`.
2435
+ const resolved = resolveRepoConfig({
2436
+ repoPath: cwd,
2437
+ global,
2438
+ override,
2439
+ flags: doFlagOverrides(flags, flagMode),
2440
+ });
2441
+ if (resolved.message) {
2442
+ console.error(`>> ${resolved.message}`);
2443
+ }
2444
+ const config = resolved.config;
2445
+ // The null adapter shells out to agentCmd, so it is required there; the
2446
+ // pi adapter invokes the pi CLI directly and does not consume agentCmd.
2447
+ if (doNeedsAgentCmd(config)) {
2448
+ console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
2449
+ process.exit(1);
2450
+ }
2451
+ const harness = createHarness({
2452
+ harness: config.harness,
2453
+ piBin: config.piBin,
2454
+ });
2455
+ // The per-item `DoOptions` (everything BUT `arg`) — built ONCE and reused for
2456
+ // the single-item path AND threaded by the multi-item layer to each
2457
+ // sequential `performDo` (do-autopick runs the EXISTING pipeline per item).
2458
+ const baseDoOptions: Omit<DoOptions, 'arg'> = {
2459
+ cwd,
2460
+ arbiter: flags.arbiter ?? config.defaultArbiter,
2461
+ // The host-only runner IDENTITY (a bot): scopes the runner's git/provider
2462
+ // ops (claim, push, integrate, `gh`) — NEVER the agent launch. Absent ⇒
2463
+ // ambient (today's behaviour). Mapped Config → DoOptions like model/agentCmd.
2464
+ identity: config.identity,
2465
+ // `do prd:<slug>` tasking-gate policy (the task-build path ignores it).
2466
+ autoTask: config.autoTask,
2467
+ // The resolved `promptGuidance` NUDGE namespace (e.g. `testFirst`),
2468
+ // threaded into the worker prompt by performDo → buildAgentPrompt so a
2469
+ // per-repo `promptGuidance.testFirst:true` actually strengthens the
2470
+ // autonomous `do` worker's wrapper line (not just `dorfl prompt`).
2471
+ promptGuidance: resolvePromptGuidance(config),
2472
+ integration: config.integration,
2473
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule (task
2474
+ // `untrusted-origin-forces-build-propose`): true ONLY when the operator
2475
+ // typed `--merge` (`flagMode`), never when `merge` came from config — so an
2476
+ // untrusted-origin task still forces propose under a config `merge`.
2477
+ explicitMerge: flagMode === 'merge',
2478
+ // Per-TRANSITION TASKING override: the `do prd:` tasking path threads
2479
+ // `taskingIntegration ?? integration`; the task-build path stays on
2480
+ // `integration`. Unset ⇒ tasking falls back to `integration` (today's behaviour).
2481
+ taskingIntegration: config.taskingIntegration,
2482
+ // TASK-PLACEMENT (`do prd:` tasking output): the configured default rung +
2483
+ // the EXPLICIT operator override `--tasks-land-in` (top of the precedence).
2484
+ // `explicitTasksLandIn` is set ONLY when the flag was typed (mirrors
2485
+ // `explicitMerge`), so an untrusted-origin staging force still wins under a
2486
+ // config default.
2487
+ tasksLandIn: config.tasksLandIn,
2488
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
2489
+ // In-place divergence guard override (mirrors --ignore-not-ready).
2490
+ ignoreDivergedMain: flags.ignoreDivergedMain === true,
2491
+ prepare: config.prepare,
2492
+ verify: config.verify,
2493
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
2494
+ freshWorktreeGate: config.freshWorktreeGate,
2495
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
2496
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — resolved per-repo
2497
+ // and threaded to `performComplete`→`performIntegration`.
2498
+ mergeRetries: config.mergeRetries,
2499
+ noPR: config.noPR,
2500
+ harness,
2501
+ agentCmd: config.agentCmd,
2502
+ model: config.model,
2503
+ // The HOST-ONLY sessions root (resolved Config → DoOptions bridge, like
2504
+ // model/agentCmd): the path generator turns it into
2505
+ // `<sessionsDir>/<id>.jsonl` for `--session`. Without this map the key
2506
+ // resolves but never reaches the launch (a silent no-op).
2507
+ sessionsDir: config.sessionsDir,
2508
+ // Gate 2 (PR/code review) rides inside `complete` (so CI inherits it for
2509
+ // free): when `review` resolves on, run the `review` SKILL as a
2510
+ // fresh-context agent (its OWN harness launch — same adapter + agentCmd,
2511
+ // `reviewModel` via the existing model-routing seam) after the green
2512
+ // verify, before the done-move. A block routes to needs-attention.
2513
+ review: config.review,
2514
+ reviewModel: config.reviewModel,
2515
+ reviewMaxRounds: config.reviewMaxRounds,
2516
+ reviewGate: config.review
2517
+ ? harnessReviewGate({harness, agentCmd: config.agentCmd})
2518
+ : undefined,
2519
+ // The tasker IMPROVER loop on the `do prd:` tasking path is ON by default
2520
+ // (auto-tasking has no `verify` floor — the loop is the task path's quality
2521
+ // engine). `--tasker-loop`/`--no-tasker-loop` gates wiring the seam;
2522
+ // `taskerLoopMax`/`taskerLoopModel` resolve per-repo (flag > env > per-repo
2523
+ // > global > default); the task-build path ignores all of these. DISTINCT
2524
+ // from the acceptance gate's `--review*` family.
2525
+ reviewLoop: config.taskerLoop
2526
+ ? harnessTaskReviewGate({
2527
+ harness,
2528
+ agentCmd: config.agentCmd,
2529
+ })
2530
+ : undefined,
2531
+ taskerLoopMax: config.taskerLoopMax,
2532
+ taskerLoopModel: config.taskerLoopModel,
2533
+ // The task-SET ACCEPTANCE GATE (slice-acceptance-gate): the task-path
2534
+ // mirror of Gate-2, on the SAME `--review` family (so `--no-review` skips
2535
+ // it). ONE-SHOT (no rounds); production wires the task-SET-prompt gate.
2536
+ taskReviewGate: config.review
2537
+ ? harnessTaskAcceptanceGate({harness, agentCmd: config.agentCmd})
2538
+ : undefined,
2539
+ // `--watch`: tail the pi session log live (pi harness only; the
2540
+ // performDo guard errors clearly on any other adapter). READ-ONLY.
2541
+ watch: flags.watch === true,
2542
+ color: shouldUseColor(process.stdout),
2543
+ note: (message) => console.error(`>> ${message}`),
2544
+ noteBlock: (message) => console.error(message),
2545
+ };
2546
+
2547
+ // `--allow-backlog` is EXPLICIT-SINGLE-TASK-ONLY (prd
2548
+ // `do-allow-backlog-drive-staged-tasks-without-promotion`, decision 4): it
2549
+ // drives ONE named staged task in place. It must NOT combine with the
2550
+ // AUTO-PICK (zero-args / -n) or MULTI-ITEM forms — those select FROM the
2551
+ // pool, and letting the flag widen a pool selection is exactly the
2552
+ // competition-bug-one-layer-down the fence forbids. Reject the misuse loudly
2553
+ // (mirroring the `--watch` multi guard) rather than silently widen a pool.
2554
+ if (
2555
+ flags.allowBacklog === true &&
2556
+ (args.length !== 1 || count !== undefined)
2557
+ ) {
2558
+ console.error(
2559
+ 'error: --allow-backlog drives ONE named staged task in place; it does ' +
2560
+ 'not combine with auto-pick / -n / multi-item forms (those select from ' +
2561
+ `the pool). Name a single task: dorfl do task:<slug> --allow-backlog.`,
2562
+ );
2563
+ process.exit(1);
2564
+ }
2565
+
2566
+ // DISPATCH the variadic grammar (in-place forms):
2567
+ // zero args -> AUTO-PICK `count` (default 1) across the two pools
2568
+ // (ordered by selectionOrder; default drain = tasks-first)
2569
+ // one named arg -> the single-item pipeline (unchanged from do-in-place)
2570
+ // many named args -> those, IN SEQUENCE (operator's order; no pool)
2571
+ // Auto-pick / multi-arg run the EXISTING `performDo` pipeline per item,
2572
+ // sequentially (`do` is sequential; parallelism is `run`).
2573
+ if (args.length === 0) {
2574
+ const multi: DoMultiResult = await performDoAuto({
2575
+ ...baseDoOptions,
2576
+ config,
2577
+ override,
2578
+ count,
2579
+ });
2580
+ console.error(`>> ${multi.message}`);
2581
+ process.exit(multi.exitCode);
2582
+ }
2583
+ if (args.length > 1) {
2584
+ const multi: DoMultiResult = await performDoArgs(args, {
2585
+ ...baseDoOptions,
2586
+ config,
2587
+ });
2588
+ console.error(`>> ${multi.message}`);
2589
+ process.exit(multi.exitCode);
2590
+ }
2591
+
2592
+ // Exactly one named item: the single-item in-place pipeline (do-in-place).
2593
+ // `--allow-backlog` rides ONLY this single-named-task call (never the
2594
+ // auto-pick / multi base above) — the leak-fence: the flag is read from the
2595
+ // typed CLI flag here, not from config/env, and never reaches a pool path.
2596
+ const result = await performDo({
2597
+ ...baseDoOptions,
2598
+ arg: args[0],
2599
+ allowBacklog: flags.allowBacklog === true,
2600
+ });
2601
+ if (result.exitCode !== 0) {
2602
+ console.error(`error: ${result.message}`);
2603
+ }
2604
+ process.exit(result.exitCode);
2605
+ });
2606
+
2607
+ // `advance` — the SIBLING top-level verb (NOT a `do` subcommand; `do`
2608
+ // subcommands + a standalone `task` verb are REJECTED in prd `advance-loop`).
2609
+ // It reuses the SAME shared `prefix:arg` resolver `do` uses, EXTENDED with the
2610
+ // `obs:` namespace, and wires the classify → lock → execute SKELETON: classify
2611
+ // the rung (read-only, no model, no lock), take the `advancing` CAS borrow, then
2612
+ // dispatch winner-only — build/task rungs ORCHESTRATE `do`/`do prd:` (never a
2613
+ // duplicate), surface/apply/triage dispatch to a named executor seam later
2614
+ // tasks fill. The DRIVERS (one-shot/loop) + `-n` + per-action gates and the
2615
+ // rung BODIES are LATER tasks; the bare eligible-SET form errors clearly here.
2616
+ program
2617
+ .command('advance')
2618
+ .helpGroup(HEADLINE_GROUP)
2619
+ .description(
2620
+ 'Advance work/ item(s) one lifecycle rung toward ready/built (PRD advance-loop), the SEQUENTIAL one-shot driver over the advance tick. advance <slug> (bare = the task) | advance spec:<slug> (the spec tasking rung; the legacy prd:<slug> is still accepted) | 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).',
2621
+ )
2622
+ .argument(
2623
+ '[slugs...]',
2624
+ 'the item(s) to advance: bare (= the task), task:<slug>, spec:<slug> (the legacy prd:<slug> is still accepted), or obs:<slug> (an observation). Zero args = auto-pick one eligible; multiple = advance them in sequence.',
2625
+ )
2626
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2627
+ .option(
2628
+ '--arbiter <remote>',
2629
+ 'name of the arbiter git remote (default: per-repo/global defaultArbiter)',
2630
+ )
2631
+ .option(
2632
+ '-n, --number <x>',
2633
+ 'AUTO-PICK x eligible items and advance them IN SEQUENCE (ordered by selectionOrder, default drain = tasks-first then prds-to-task). Sequential — never a parallelism knob (that is `run` / the CI matrix). Mutually exclusive with naming items.',
2634
+ )
2635
+ .option(
2636
+ '--isolated',
2637
+ "advance in an ISOLATED worktree off THIS repo's arbiter (inferred from cwd) instead of taking over the current checkout, then integrate + reap — the in-place-but-isolated form. Shares the same grammar: a single named item, multiple named items (in sequence), AND -n/auto-pick over the mirror-side eligible-pool scan. Always SEQUENTIAL (parallelism is `run` / the CI matrix). Lets you advance from a busy/dirty checkout or anywhere with a participating arbiter.",
2638
+ )
2639
+ .option(
2640
+ '--selection-order <order>',
2641
+ 'order the auto-pick pools (build/task/surface/triage; apply is always first): a preset keyword (drain (default) | groom) or an explicit comma-separated pool list. Resolved flag > env > per-repo > global > default.',
2642
+ )
2643
+ .option(
2644
+ '--observation-triage <mode>',
2645
+ '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`).',
2646
+ )
2647
+ .option(
2648
+ '--surface-blockers',
2649
+ 'the declared-blocked-work gate (the orthogonal peer of --observation-triage): render a task/prd 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 prd:<slug>` bypasses this selection gate and surfaces regardless. Does NOT gate apply (an answered sidecar still applies) or needs-attention (always on).',
2650
+ )
2651
+ .option(
2652
+ '--no-surface-blockers',
2653
+ 'leave a needsAnswers:true task/prd silently blocked (default; the blocked pool is dropped from auto-pick)',
2654
+ )
2655
+ .option(
2656
+ '--strict-merge-approval',
2657
+ 'opt in to the host-agnostic "dismiss stale approvals on base change" discipline (prd `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.',
2658
+ )
2659
+ .option(
2660
+ '--no-strict-merge-approval',
2661
+ '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)',
2662
+ )
2663
+ .option(
2664
+ '--merge-questions <mode>',
2665
+ '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.',
2666
+ )
2667
+ .option(
2668
+ '--merge',
2669
+ '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.',
2670
+ )
2671
+ .option(
2672
+ '--propose',
2673
+ '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).',
2674
+ )
2675
+ .option(
2676
+ '--tasks-land-in <where>',
2677
+ 'where `advance prd:<slug>` tasking output lands: `pre-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.',
2678
+ )
2679
+ .option(
2680
+ '--watch',
2681
+ "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.",
2682
+ )
2683
+ .action(async (rawSlugs: string[], flags: DoFlags) => {
2684
+ // Variadic grammar (mirrors `do`): zero args = AUTO-PICK; one = the single
2685
+ // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count
2686
+ // (ALWAYS sequential, US #25).
2687
+ const args = rawSlugs ?? [];
2688
+
2689
+ // `-n <x>` parse + validation — the AUTO-PICK count (sequential), mutually
2690
+ // exclusive with NAMING items (the SAME contract `do -n` enforces).
2691
+ let count: number | undefined;
2692
+ if (flags.number !== undefined) {
2693
+ const n = Number(flags.number);
2694
+ if (flags.number.trim() === '' || !Number.isInteger(n) || n < 1) {
2695
+ console.error(
2696
+ `error: -n/--number must be a positive integer (got '${flags.number}').`,
2697
+ );
2698
+ process.exit(1);
2699
+ }
2700
+ if (args.length > 0) {
2701
+ console.error(
2702
+ 'error: -n/--number auto-picks a COUNT of eligible items; do not also ' +
2703
+ 'name items. Use `advance -n <x>` OR `advance <a> <b> ...`, not both.',
2704
+ );
2705
+ process.exit(1);
2706
+ }
2707
+ count = n;
2708
+ }
2709
+
2710
+ const cwd = process.cwd();
2711
+ const {global, override} = loadGlobalAndOverride(flags.config);
2712
+ // Resolve the integration mode this invocation asks for, highest first:
2713
+ // --merge/--propose flag > per-repo .dorfl.json > global > default.
2714
+ // The SAME chain `do`/`complete` use (via `integrationFromFlags`), so the
2715
+ // human, the autonomous runner, and the CI workflow all resolve the SAME
2716
+ // order. This is what ties the CI dispatch `integrationMode` to the actual
2717
+ // open-PR-vs-merge-to-main behaviour: the propose-matrix legs pass
2718
+ // `--propose` and the single sequential merge job passes `--merge`, so the
2719
+ // integration mode can never DESYNC from the job shape the input selected.
2720
+ let flagMode;
2721
+ try {
2722
+ flagMode = integrationFromFlags(flags);
2723
+ } catch (err) {
2724
+ console.error(
2725
+ `error: ${err instanceof Error ? err.message : String(err)}`,
2726
+ );
2727
+ process.exit(1);
2728
+ }
2729
+ // Build the flag overrides (the `--observation-triage` enum FAILS LOUDLY on a
2730
+ // typo, like the env coercion) before resolving — a bad gate value is a clean
2731
+ // usage error, never silently dropped.
2732
+ let doOverrides;
2733
+ try {
2734
+ doOverrides = doFlagOverrides(flags, flagMode);
2735
+ } catch (err) {
2736
+ console.error(
2737
+ `error: ${err instanceof Error ? err.message : String(err)}`,
2738
+ );
2739
+ process.exit(1);
2740
+ }
2741
+
2742
+ // `advance --isolated`: run the advance TICK in an ISOLATED worktree off
2743
+ // THIS repo's arbiter (resolved from cwd), then integrate + reap — the
2744
+ // in-place-but-isolated form, the SAME ergonomic `do --isolated` has. We
2745
+ // REUSE `do --isolated`'s arbiter-from-cwd resolver + the isolation substrate
2746
+ // (`ensureMirror` + the job-worktree `doDriver` + reap), threading the
2747
+ // arbiter URL into the NEW isolated advance-tick runner. `--isolated` is the
2748
+ // only ISOLATION axis here: `advance --remote <url>` is a SEPARATE concern
2749
+ // (the action already TYPES `flags.remote` via `DoFlags`, but no `--remote`
2750
+ // plumbing exists on `advance` — see `## Decisions`), so `--isolated` always
2751
+ // resolves the arbiter from the CWD.
2752
+ if (flags.isolated === true) {
2753
+ // BOOTSTRAP resolution (global + flags, no per-repo layer) supplies the
2754
+ // host-only keys needed to even reach the arbiter (`workspacesDir`,
2755
+ // `identity`), exactly as `do --isolated` bootstraps them.
2756
+ const bootstrap = resolveGlobalConfig(global, doOverrides);
2757
+ const arbiterName =
2758
+ flags.arbiter ??
2759
+ resolveDefaultArbiterForCwd(cwd, global, doOverrides, override);
2760
+ const arbiterUrl = resolveArbiterUrlFromCheckout(
2761
+ cwd,
2762
+ arbiterName,
2763
+ identityEnv(bootstrap.identity, process.env),
2764
+ );
2765
+ if (arbiterUrl === undefined) {
2766
+ // The SAME clear "isolated against what?" error `do --isolated` gives —
2767
+ // naming `--remote <url>` as the foreign-repo alternative, NOT a
2768
+ // downstream URL-parse failure.
2769
+ console.error(
2770
+ `error: --isolated advances in a worktree off this repo's arbiter ` +
2771
+ `('${arbiterName}'), but no such arbiter remote is configured/found ` +
2772
+ `here. Run inside a participating repo (a clone with an arbiter ` +
2773
+ `remote), or use --remote <url> to target another repo.`,
2774
+ );
2775
+ process.exit(1);
2776
+ }
2777
+ // Source the arbiter's COMMITTED `.dorfl.json` from `<arbiter>/main`
2778
+ // via the hub mirror + layer ONLY its whitelisted keys — the SAME
2779
+ // resolution `do --isolated` uses, so the gate family (autoBuild/autoTask/
2780
+ // observationTriage/surfaceBlockers) + selectionOrder + integration resolve
2781
+ // off the arbiter exactly as the in-place advance resolves them off cwd.
2782
+ const remoteConfig = resolveRemoteRepoConfig({
2783
+ remote: arbiterUrl,
2784
+ workspacesDir: bootstrap.workspacesDir,
2785
+ global,
2786
+ flags: doOverrides,
2787
+ identity: bootstrap.identity,
2788
+ note: (message) => console.error(`>> ${message}`),
2789
+ override,
2790
+ });
2791
+ if (doNeedsAgentCmd(remoteConfig)) {
2792
+ console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
2793
+ process.exit(1);
2794
+ }
2795
+ const isoHarness = createHarness({
2796
+ harness: remoteConfig.harness,
2797
+ piBin: remoteConfig.piBin,
2798
+ });
2799
+ // The base `do` options the build/task rungs ORCHESTRATE through the
2800
+ // INJECTED job-worktree driver (the isolated advance-tick runner wires it).
2801
+ const isoDoOptions: Omit<DoOptions, 'arg'> = {
2802
+ cwd,
2803
+ // `--watch`: stream the build agent's session live (pi harness only;
2804
+ // validated in `performDo`). Threaded through the orchestrated build rung
2805
+ // so `advance --isolated --watch` (and CI) shows the agent working.
2806
+ watch: flags.watch === true,
2807
+ arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
2808
+ identity: remoteConfig.identity,
2809
+ autoTask: remoteConfig.autoTask,
2810
+ integration: remoteConfig.integration,
2811
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule.
2812
+ explicitMerge: flagMode === 'merge',
2813
+ // Per-TRANSITION TASKING override (the isolated `do --remote prd:` path).
2814
+ taskingIntegration: remoteConfig.taskingIntegration,
2815
+ // TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
2816
+ // (set only when typed, mirroring `explicitMerge`).
2817
+ tasksLandIn: remoteConfig.tasksLandIn,
2818
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
2819
+ prepare: remoteConfig.prepare,
2820
+ verify: remoteConfig.verify,
2821
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
2822
+ freshWorktreeGate: remoteConfig.freshWorktreeGate,
2823
+ // Cross-job merge-serialiser CAS-retry cap — prd
2824
+ // `land-time-reverify-and-parallel-merge-ceiling` Story 5.
2825
+ mergeRetries: remoteConfig.mergeRetries,
2826
+ noPR: remoteConfig.noPR,
2827
+ harness: isoHarness,
2828
+ agentCmd: remoteConfig.agentCmd,
2829
+ model: remoteConfig.model,
2830
+ sessionsDir: remoteConfig.sessionsDir,
2831
+ review: remoteConfig.review,
2832
+ reviewModel: remoteConfig.reviewModel,
2833
+ reviewMaxRounds: remoteConfig.reviewMaxRounds,
2834
+ reviewGate: remoteConfig.review
2835
+ ? harnessReviewGate({
2836
+ harness: isoHarness,
2837
+ agentCmd: remoteConfig.agentCmd,
2838
+ })
2839
+ : undefined,
2840
+ reviewLoop: remoteConfig.taskerLoop
2841
+ ? harnessTaskReviewGate({
2842
+ harness: isoHarness,
2843
+ agentCmd: remoteConfig.agentCmd,
2844
+ })
2845
+ : undefined,
2846
+ taskerLoopMax: remoteConfig.taskerLoopMax,
2847
+ taskerLoopModel: remoteConfig.taskerLoopModel,
2848
+ taskReviewGate: remoteConfig.review
2849
+ ? harnessTaskAcceptanceGate({
2850
+ harness: isoHarness,
2851
+ agentCmd: remoteConfig.agentCmd,
2852
+ })
2853
+ : undefined,
2854
+ color: shouldUseColor(process.stdout),
2855
+ note: (message) => console.error(`>> ${message}`),
2856
+ noteBlock: (message) => console.error(message),
2857
+ };
2858
+ // The shared per-item ISOLATED advance CONTEXT (everything BUT `arg` and
2859
+ // `cwd`/`doDriver`, which the runner supplies from the isolated clone).
2860
+ const isoContext: IsolatedAdvanceContext & {
2861
+ env: NodeJS.ProcessEnv;
2862
+ } = {
2863
+ remote: arbiterUrl,
2864
+ workspacesDir: remoteConfig.workspacesDir,
2865
+ arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
2866
+ doOptions: isoDoOptions,
2867
+ surfaceGate: harnessSurfaceGate({
2868
+ harness: isoHarness,
2869
+ agentCmd: remoteConfig.agentCmd,
2870
+ }),
2871
+ surfaceModel: remoteConfig.model,
2872
+ applyDecide: harnessApplyDecider({
2873
+ harness: isoHarness,
2874
+ agentCmd: remoteConfig.agentCmd,
2875
+ }),
2876
+ applyModel: remoteConfig.model,
2877
+ observationTriage: remoteConfig.observationTriage,
2878
+ triageGate: harnessTriageGate({
2879
+ harness: isoHarness,
2880
+ agentCmd: remoteConfig.agentCmd,
2881
+ }),
2882
+ triageModel: remoteConfig.model,
2883
+ note: (message) => console.error(`>> ${message}`),
2884
+ env: process.env,
2885
+ };
2886
+
2887
+ // DISPATCH the variadic grammar, ISOLATED + SEQUENTIAL (mirrors
2888
+ // `do --isolated`): zero args / `-n` -> AUTO-PICK over the mirror-side
2889
+ // eligible-pool scan; many named -> those in sequence; one named -> the
2890
+ // single isolated tick. `-n` stays ALWAYS SEQUENTIAL (US #25).
2891
+ if (args.length === 0 || count !== undefined) {
2892
+ const multi = await performAdvanceIsolatedAuto({
2893
+ ...isoContext,
2894
+ config: remoteConfig,
2895
+ count,
2896
+ warn: (message) => console.error(`>> ${message}`),
2897
+ lifecycleGates: {
2898
+ triage: remoteConfig.observationTriage !== 'off',
2899
+ surface: remoteConfig.surfaceBlockers,
2900
+ surfaceStaging: remoteConfig.surfaceStaging,
2901
+ },
2902
+ });
2903
+ console.error(`>> ${multi.message}`);
2904
+ process.exit(multi.exitCode);
2905
+ }
2906
+ if (args.length > 1) {
2907
+ const multi = await performAdvanceIsolatedArgs(args, {
2908
+ ...isoContext,
2909
+ config: remoteConfig,
2910
+ });
2911
+ console.error(`>> ${multi.message}`);
2912
+ process.exit(multi.exitCode);
2913
+ }
2914
+ // Exactly one named item: the single ISOLATED advance tick.
2915
+ const result = await performAdvanceIsolated({
2916
+ ...isoContext,
2917
+ arg: args[0],
2918
+ });
2919
+ if (result.exitCode !== 0) {
2920
+ console.error(`error: ${result.message}`);
2921
+ }
2922
+ process.exit(result.exitCode);
2923
+ }
2924
+
2925
+ const resolved = resolveRepoConfig({
2926
+ repoPath: cwd,
2927
+ global,
2928
+ override,
2929
+ flags: doOverrides,
2930
+ });
2931
+ if (resolved.message) {
2932
+ console.error(`>> ${resolved.message}`);
2933
+ }
2934
+ const config = resolved.config;
2935
+ const harness = createHarness({
2936
+ harness: config.harness,
2937
+ piBin: config.piBin,
2938
+ });
2939
+ // The base `do` options the build/task rungs ORCHESTRATE `performDo` with
2940
+ // (the ONE build path / ONE task path). `advance` is a driver ON TOP — it
2941
+ // hands the resolved arg to `performDo`, never re-implementing it.
2942
+ const doOptions: Omit<DoOptions, 'arg'> = {
2943
+ cwd,
2944
+ // `--watch`: stream the build agent's session live (pi harness only;
2945
+ // validated in `performDo`). Threaded through the orchestrated build rung
2946
+ // so `advance --watch` (and CI) shows the agent working, not a frozen log.
2947
+ watch: flags.watch === true,
2948
+ arbiter: flags.arbiter ?? config.defaultArbiter,
2949
+ identity: config.identity,
2950
+ autoTask: config.autoTask,
2951
+ integration: config.integration,
2952
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule (a
2953
+ // bare `advance` auto-pick passes no flag ⇒ unset ⇒ untrusted forces propose).
2954
+ explicitMerge: flagMode === 'merge',
2955
+ // Per-TRANSITION TASKING override (the `do prd:` tasking path threads
2956
+ // `taskingIntegration ?? integration`; the build path stays on `integration`).
2957
+ taskingIntegration: config.taskingIntegration,
2958
+ // TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
2959
+ // (set only when typed, mirroring `explicitMerge`).
2960
+ tasksLandIn: config.tasksLandIn,
2961
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
2962
+ prepare: config.prepare,
2963
+ verify: config.verify,
2964
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
2965
+ freshWorktreeGate: config.freshWorktreeGate,
2966
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
2967
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)).
2968
+ mergeRetries: config.mergeRetries,
2969
+ noPR: config.noPR,
2970
+ harness,
2971
+ agentCmd: config.agentCmd,
2972
+ model: config.model,
2973
+ sessionsDir: config.sessionsDir,
2974
+ review: config.review,
2975
+ reviewModel: config.reviewModel,
2976
+ reviewMaxRounds: config.reviewMaxRounds,
2977
+ reviewGate: config.review
2978
+ ? harnessReviewGate({harness, agentCmd: config.agentCmd})
2979
+ : undefined,
2980
+ reviewLoop: config.taskerLoop
2981
+ ? harnessTaskReviewGate({harness, agentCmd: config.agentCmd})
2982
+ : undefined,
2983
+ taskerLoopMax: config.taskerLoopMax,
2984
+ taskerLoopModel: config.taskerLoopModel,
2985
+ taskReviewGate: config.review
2986
+ ? harnessTaskAcceptanceGate({harness, agentCmd: config.agentCmd})
2987
+ : undefined,
2988
+ color: shouldUseColor(process.stdout),
2989
+ note: (message) => console.error(`>> ${message}`),
2990
+ noteBlock: (message) => console.error(message),
2991
+ };
2992
+ // The shared per-item advance CONTEXT (everything BUT `arg`) — built ONCE
2993
+ // and threaded by the one-shot DRIVER to each sequential tick. The SURFACE
2994
+ // rung spawns `surface-questions` fresh-context through the SAME harness seam
2995
+ // the review gate uses (the engine then PERSISTS); the TRIAGE rung is
2996
+ // question-gated by default; `observationTriage: 'auto'` enables the
2997
+ // conservative auto-disposition exception (`ask`/`off` surface the question).
2998
+ // Surface + apply stay ALWAYS allowed regardless of the gate family.
2999
+ const advanceContext: AdvanceContext = {
3000
+ cwd,
3001
+ arbiter: flags.arbiter ?? config.defaultArbiter,
3002
+ doOptions,
3003
+ surfaceGate: harnessSurfaceGate({harness, agentCmd: config.agentCmd}),
3004
+ surfaceModel: config.model,
3005
+ applyDecide: harnessApplyDecider({harness, agentCmd: config.agentCmd}),
3006
+ applyModel: config.model,
3007
+ observationTriage: config.observationTriage,
3008
+ triageGate: harnessTriageGate({harness, agentCmd: config.agentCmd}),
3009
+ triageModel: config.model,
3010
+ note: (message) => console.error(`>> ${message}`),
3011
+ };
3012
+
3013
+ // `--watch` tails ONE pi session, so it only fits the single-named-item form
3014
+ // (mirrors `do --watch`). The auto-pick / `-n` / multi-item forms run many
3015
+ // ticks in sequence and would tail several logs; reject rather than silently
3016
+ // stream only one. The CI propose matrix names a single item per leg, so it
3017
+ // satisfies this; the `-n` merge job must NOT pass `--watch`.
3018
+ const advanceMulti =
3019
+ args.length === 0 || count !== undefined || args.length > 1;
3020
+ if (advanceMulti && flags.watch === true) {
3021
+ console.error(
3022
+ 'error: --watch streams ONE session; it does not combine with the ' +
3023
+ 'auto-pick / -n / multi-item forms. Name a single item.',
3024
+ );
3025
+ process.exit(1);
3026
+ }
3027
+
3028
+ // DISPATCH the variadic grammar (the one-shot SEQUENTIAL driver):
3029
+ // zero args -> AUTO-PICK `count` (default 1) over the eligible pool
3030
+ // (tasks-first then prds-to-task; per-action gates
3031
+ // respected by the SELECTION layer; ordered by selectionOrder).
3032
+ // one named arg -> the single-item tick (the always-allowed surface/apply
3033
+ // path runs regardless of the gate family).
3034
+ // many named args -> those, IN SEQUENCE (operator's order; no pool).
3035
+ // `-n` / auto-pick / multi-arg all run the EXISTING tick per item,
3036
+ // SEQUENTIALLY (parallelism is `run` / the CI matrix, never `-n`).
3037
+ if (args.length === 0) {
3038
+ const multi: AdvanceMultiResult = await performAdvanceAuto({
3039
+ ...advanceContext,
3040
+ config,
3041
+ override,
3042
+ count,
3043
+ // The SELECTION-layer gates: `observationTriage != off` enumerates the
3044
+ // observation (triage) pool into auto-pick; `surfaceBlockers` enumerates
3045
+ // the `needsAnswers`-blocked (surface) pool. `off`/`false` drops the
3046
+ // respective pool (its item is left untouched / silently blocked). The
3047
+ // two gates are orthogonal peers. The triage rung's ask-vs-auto
3048
+ // distinction is read inside the tick from `observationTriage` (threaded
3049
+ // on `advanceContext`). Apply (consume) is always-on (never gated here).
3050
+ lifecycleGates: {
3051
+ triage: config.observationTriage !== 'off',
3052
+ surface: config.surfaceBlockers,
3053
+ surfaceStaging: config.surfaceStaging,
3054
+ },
3055
+ });
3056
+ console.error(`>> ${multi.message}`);
3057
+ process.exit(multi.exitCode);
3058
+ }
3059
+ if (args.length > 1) {
3060
+ const multi: AdvanceMultiResult = await performAdvanceArgs(args, {
3061
+ ...advanceContext,
3062
+ config,
3063
+ });
3064
+ console.error(`>> ${multi.message}`);
3065
+ process.exit(multi.exitCode);
3066
+ }
3067
+
3068
+ // Exactly one named item: the single-item tick. Wrapped in the shared
3069
+ // in-place tree-less publish so a surfaced sidecar / `triaged:` marker /
3070
+ // applied-answer commit reaches the arbiter's `main` (the CI ephemeral-runner
3071
+ // case — the local commit would otherwise be lost). The wrapper is the SAME
3072
+ // gate the `--isolated` / loop drivers use (`TREELESS_RUNGS` + exit 0 +
3073
+ // arbiter configured); a build/task rung integrates through the `doDriver`
3074
+ // band already, and a no-arbiter laptop checkout sits on the real `main`.
3075
+ const result = await runAdvanceTickWithTreelessPublish(
3076
+ {...advanceContext, arg: args[0]},
3077
+ performAdvance,
3078
+ );
3079
+ if (result.exitCode !== 0) {
3080
+ console.error(`error: ${result.message}`);
3081
+ }
3082
+ process.exit(result.exitCode);
3083
+ });
3084
+
3085
+ program
3086
+ .command('gc')
3087
+ .helpGroup(ADVANCED_GROUP)
3088
+ .description(
3089
+ 'Re-apply the provably-safe deletion predicate (ADR \u00a74) across every job worktree under workspacesDir/work/*: reap the provably-safe ones (clean tree AND branch tip reachable on the arbiter \u2014 merged or pushed) via git worktree remove (+ prune, never rm -rf), and report each RETAINED one with a reason. The catch-up for when end-of-job auto-reap did not run (runner crash/kill). --force overrides the predicate (discards un-saved work) \u2014 loud, never default.',
3090
+ )
3091
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3092
+ .option(
3093
+ '--workspace <dir>',
3094
+ 'execution working area to sweep (default: workspacesDir / ~/.dorfl)',
3095
+ )
3096
+ .option(
3097
+ '--force',
3098
+ 'OVERRIDE the predicate: remove worktrees even with un-saved work (requires --yes; never the default)',
3099
+ )
3100
+ .option('--yes', 'confirm a destructive --force sweep non-interactively')
3101
+ .option(
3102
+ '--ledger [repoPath]',
3103
+ '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.',
3104
+ )
3105
+ .option(
3106
+ '--reap-stale-locks',
3107
+ '(with --ledger) OPT-IN: also CLEAR every STALE terminal lock the report finds (a held `active` per-item lock whose item is already TERMINAL on <arbiter>/main — the `cleared-stale` class) via the SAME leased delete `release-lock` uses, so one command sweeps all orphaned terminal locks instead of N hand-run release-locks. SCOPED to `cleared-stale` ONLY: a `kept-stuck` (terminal + stuck) or a `kept-in-flight` (active, non-terminal) lock 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).',
3108
+ )
3109
+ .option(
3110
+ '--remote-branches',
3111
+ '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`.',
3112
+ )
3113
+ .option(
3114
+ '--arbiter <remote>',
3115
+ '(with --remote-branches) the arbiter git remote whose work/* branches to sweep (default: origin); resolved from --cwd',
3116
+ )
3117
+ .option(
3118
+ '--cwd <dir>',
3119
+ '(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',
3120
+ )
3121
+ .option(
3122
+ '--dry-run',
3123
+ '(with --remote-branches) REPORT which merged branches WOULD be reaped without deleting anything (a read-only preview)',
3124
+ )
3125
+ .option('--json', 'output the raw result as JSON')
3126
+ .action(async (flags: GcFlags) => {
3127
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
3128
+ const workspacesDir = flags.workspace ?? config.workspacesDir;
3129
+
3130
+ // The `gc`-STYLE ledger SWEEP (prd `ledger-integrity` story 3): a SEPARATE
3131
+ // surface from the worktree reaper below — it REPORTS one-slug-one-folder
3132
+ // violations in a repo's `work/` lifecycle ledger and NEVER deletes (a human
3133
+ // resolves each). Distinct `work/`: the ledger, not the execution substrate.
3134
+ if (flags.ledger !== undefined) {
3135
+ const repoPath =
3136
+ typeof flags.ledger === 'string' ? flags.ledger : process.cwd();
3137
+ const result = sweepLedgerDuplicates(repoPath);
3138
+ // The UNIFIED-LOCK stuck/orphaned-lock REPORT (task
3139
+ // `release-lock-verb-and-gc-stuck-report`, prd
3140
+ // `ledger-status-per-item-lock-refs` US #12/#13/#14): generalises the
3141
+ // advancing-marker report from advancing-only to the unified per-item
3142
+ // lock. The locks live on the ARBITER ref (`refs/dorfl/lock/*`),
3143
+ // not in the local tree, so this reads the arbiter (cwd's `--arbiter`
3144
+ // remote). Best-effort: an absent lock-ref namespace / unreachable arbiter
3145
+ // degrades to an EMPTY report ("all locks released" — recoverable, US #12),
3146
+ // exactly as an absent lock-ref namespace reads. It REPORTS only,
3147
+ // wiring `reconcileItemLockAgainstMain`'s read-only twin to DISTINGUISH a
3148
+ // held/stuck lock from a stale-active lock over a terminal item WITHOUT
3149
+ // clearing (no auto-sweep; a human asserts a lock is dead via
3150
+ // `release-lock`).
3151
+ // OPT-IN SWEEP (`--reap-stale-locks`): the WRITE twin of the report. A
3152
+ // human asserting "clear the dead TERMINAL locks now": for EXACTLY the
3153
+ // `cleared-stale` class (terminal-on-main + active = stranded) perform the
3154
+ // SAME leased delete `release-lock` / the recovery use, so one command
3155
+ // sweeps every orphaned terminal lock. A `kept-stuck` / `kept-in-flight`
3156
+ // lock is NEVER reaped (scope fence); a concurrent change makes a clear
3157
+ // REJECT (reported `lost`), never --force. WITHOUT the flag the surface
3158
+ // below stays report-only (fail-loud, deletes nothing).
3159
+ if (flags.reapStaleLocks) {
3160
+ const reap = await reapStaleItemLocks(
3161
+ flags.cwd ?? repoPath,
3162
+ flags.arbiter ?? 'origin',
3163
+ process.env,
3164
+ );
3165
+ if (flags.json) {
3166
+ console.log(JSON.stringify({...result, reap}, null, 2));
3167
+ } else {
3168
+ const reapLines = formatReapReport(reap);
3169
+ const blocks: string[] = [];
3170
+ if (result.duplicates.length > 0) {
3171
+ blocks.push(formatLedgerSweep(result));
3172
+ }
3173
+ if (reapLines.length > 0) {
3174
+ blocks.push(reapLines.join('\n'));
3175
+ }
3176
+ console.log(
3177
+ blocks.length > 0
3178
+ ? blocks.join('\n\n')
3179
+ : formatLedgerSweep(result),
3180
+ );
3181
+ }
3182
+ // Fail-loud AFTER the sweep: a `kept-stuck` (rightly left for a human) or
3183
+ // a `lost`/`error` (a stale lock whose leased delete lost the race) still
3184
+ // needs attention; a clean sweep that reaped every stale lock and left
3185
+ // only healthy in-flight holds exits 0.
3186
+ process.exit(
3187
+ result.duplicates.length > 0 || reapReportNeedsAttention(reap)
3188
+ ? 1
3189
+ : 0,
3190
+ );
3191
+ }
3192
+ const lockReport = await reportItemLocks(
3193
+ flags.cwd ?? repoPath,
3194
+ flags.arbiter ?? 'origin',
3195
+ process.env,
3196
+ );
3197
+ if (flags.json) {
3198
+ console.log(JSON.stringify({...result, lockReport}, null, 2));
3199
+ } else {
3200
+ const lockLines = formatItemLockReport(lockReport);
3201
+ if (result.duplicates.length === 0 && lockLines.length === 0) {
3202
+ console.log(formatLedgerSweep(result));
3203
+ } else {
3204
+ const blocks: string[] = [];
3205
+ const sweepText = formatLedgerSweep(result);
3206
+ // Only print the duplicate block when it found something (otherwise
3207
+ // it returns the "clean" line, which is misleading when there ARE
3208
+ // lingering locks below it).
3209
+ if (result.duplicates.length > 0) {
3210
+ blocks.push(sweepText);
3211
+ }
3212
+ if (lockLines.length > 0) {
3213
+ blocks.push(lockLines.join('\n'));
3214
+ }
3215
+ console.log(blocks.join('\n\n'));
3216
+ }
3217
+ }
3218
+ // A corrupt ledger, a stuck advancing-lock marker, OR a per-item lock that
3219
+ // NEEDS HUMAN ATTENTION is a fail-loud condition: exit non-zero so a human
3220
+ // (or a script) cannot miss it, mirroring the integration core's refusal.
3221
+ // ALL are REPORTED here (never auto-deleted — no automatic sweep exists; a
3222
+ // human clears a NAMED unified lock via `release-lock`).
3223
+ //
3224
+ // SCOPED to the ATTENTION verdicts only (prd US#14/#21, ADR
3225
+ // `ledger-status-on-per-item-lock-refs`: this surface is the STUCK /
3226
+ // crash-orphaned lock, NOT every held one): a `kept-stuck` (terminal +
3227
+ // stuck) or a `cleared-stale`-eligible (terminal + stale active = orphaned)
3228
+ // lock fails loud, but a `kept-in-flight` (active, non-terminal) lock is the
3229
+ // NORMAL in-flight state of a healthy concurrent build (read by `status` as
3230
+ // healthy) — it is reported informationally and does NOT make a routine
3231
+ // `gc --ledger` health check exit non-zero.
3232
+ process.exit(
3233
+ result.duplicates.length > 0 ||
3234
+ itemLockReportNeedsAttention(lockReport)
3235
+ ? 1
3236
+ : 0,
3237
+ );
3238
+ }
3239
+
3240
+ // The REMOTE merged-BRANCH sweep (this task): a SEPARATE surface from the
3241
+ // worktree reaper below — it deletes PROVABLY-MERGED remote `work/*` branches
3242
+ // on the arbiter (the cross-machine counterpart of reaping local worktrees),
3243
+ // guarded by the SAME ancestor-of-main predicate. Provider-agnostic plain git
3244
+ // (works on a `--bare` arbiter); NEVER `--force` (a merged ref needs none),
3245
+ // NEVER touches an in-flight branch.
3246
+ if (flags.remoteBranches) {
3247
+ const sweepCwd = flags.cwd ?? process.cwd();
3248
+ const sweep = sweepRemoteMergedBranches({
3249
+ cwd: sweepCwd,
3250
+ arbiter: flags.arbiter ?? 'origin',
3251
+ dryRun: flags.dryRun === true,
3252
+ note: (message) => console.error(`>> ${message}`),
3253
+ });
3254
+ // The ORPHAN-SIDECAR sweep (prd
3255
+ // `agentic-question-resolution-retire-disposition-vocabulary`, US #10) rides
3256
+ // the SAME `--remote-branches` invocation the SCHEDULED CI lifecycle workflow
3257
+ // runs (`dorfl gc --remote-branches --arbiter origin`) — so the reap of a
3258
+ // `work/questions/<type>-<slug>.md` whose source item was deleted out-of-band
3259
+ // actually FIRES on the cron tick (not behind an un-passed flag). It operates
3260
+ // on the WORKING TREE of the checkout `gc` runs in (the same `cwd` the branch
3261
+ // sweep targets) — CI checks out the repo — so no arbiter ref query is needed
3262
+ // beyond the by-identity source-existence check
3263
+ // (`resolveItemPathByIdentity`). A `git rm` deletion (notes/sidecars leave by
3264
+ // deletion; git history is the archive), so a wrong source-delete is
3265
+ // recoverable from history. Honours `--dry-run` (report-only preview).
3266
+ const orphans = sweepOrphanSidecars({
3267
+ cwd: sweepCwd,
3268
+ dryRun: flags.dryRun === true,
3269
+ note: (message) => console.error(`>> ${message}`),
3270
+ });
3271
+ if (flags.json) {
3272
+ console.log(
3273
+ JSON.stringify({...sweep, orphanSidecars: orphans}, null, 2),
3274
+ );
3275
+ return;
3276
+ }
3277
+ if (flags.dryRun === true) {
3278
+ for (const w of sweep.wouldReap) {
3279
+ console.log(` [would-reap] ${w.branch} \u2014 merged`);
3280
+ }
3281
+ for (const w of orphans.wouldReap) {
3282
+ console.log(` [would-reap] ${w.path} \u2014 orphan sidecar`);
3283
+ }
3284
+ } else {
3285
+ for (const r of sweep.reaped) {
3286
+ console.log(` [reaped] ${r.branch} \u2014 merged`);
3287
+ }
3288
+ for (const r of orphans.reaped) {
3289
+ console.log(` [reaped] ${r.path} \u2014 orphan sidecar`);
3290
+ }
3291
+ }
3292
+ for (const ret of sweep.retained) {
3293
+ console.log(` [retained] ${ret.branch} \u2014 ${ret.reasonText}`);
3294
+ }
3295
+ const reapedCount =
3296
+ flags.dryRun === true
3297
+ ? sweep.wouldReap.length + orphans.wouldReap.length
3298
+ : sweep.reaped.length + orphans.reaped.length;
3299
+ const verb = flags.dryRun === true ? 'would reap' : 'reaped';
3300
+ console.log(
3301
+ `Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained.`,
3302
+ );
3303
+ return;
3304
+ }
3305
+
3306
+ // `--force` discards un-saved work, so it is gated behind an explicit
3307
+ // confirmation (`--yes`) — loud + intentional, NEVER the default (ADR §4).
3308
+ if (flags.force && !flags.yes) {
3309
+ console.error(
3310
+ 'refusing to --force without --yes: this DISCARDS un-saved work in ' +
3311
+ 'retained worktrees (commits not on the arbiter, dirty trees). ' +
3312
+ 'Re-run with `gc --force --yes` to confirm.',
3313
+ );
3314
+ process.exit(1);
3315
+ }
3316
+ if (flags.force) {
3317
+ console.error(
3318
+ '>> --force: OVERRIDING the deletion-safety predicate; un-saved work ' +
3319
+ 'in retained worktrees will be DISCARDED.',
3320
+ );
3321
+ }
3322
+
3323
+ const result = gc({
3324
+ workspacesDir,
3325
+ force: flags.force === true,
3326
+ note: (message) => console.error(`>> ${message}`),
3327
+ });
3328
+
3329
+ if (flags.json) {
3330
+ console.log(JSON.stringify(result, null, 2));
3331
+ return;
3332
+ }
3333
+ for (const reaped of result.reaped) {
3334
+ const how = reaped.forced
3335
+ ? 'FORCED (discarded un-saved work)'
3336
+ : (reaped.verdict.reachableVia ?? 'safe');
3337
+ console.log(` [reaped] ${reaped.slug} \u2014 ${how}`);
3338
+ }
3339
+ for (const retained of result.retained) {
3340
+ console.log(
3341
+ ` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}`,
3342
+ );
3343
+ }
3344
+ console.log(
3345
+ `Summary: ${result.reaped.length} reaped, ${result.retained.length} retained.`,
3346
+ );
3347
+ });
3348
+
3349
+ program
3350
+ .command('prd-to-spec')
3351
+ .helpGroup(ADVANCED_GROUP)
3352
+ .description(
3353
+ "Migrate THIS repo's work/ DATA + config + inert git refs from the legacy `prd` vocabulary to `spec`, after upgrading the dorfl package (whose code/contract already speak `spec`). Self-contained: (1) REFUSES unless the repo is quiescent (clean tree AND no held per-item lock AND no in-progress work/prd-* branch), naming the offender; (2) re-syncs work/protocol/* to the new `spec` contract; (3) mechanically converts all four data layers (folders work/prds/* -> work/specs/* via git mv, `prd:` frontmatter + inert refs across ALL items incl. tasks/done + specs/tasked, `prdsLandIn` config key, inert lock-refs/work-branches). Idempotent; --dry-run previews every layer without writing; the forward+reverse leak scan over the converted tree is the acceptance gate. Runs IN-PLACE on the current repo (does no git commit/push -- the human commits the result).",
3354
+ )
3355
+ .option(
3356
+ '--repo <dir>',
3357
+ 'the repo working-tree root to migrate (default: cwd)',
3358
+ )
3359
+ .option(
3360
+ '--dry-run',
3361
+ 'REPORT exactly what each layer WOULD change, touching nothing (no writes, no ref renames, no leak-scan)',
3362
+ )
3363
+ .option('--json', 'output the raw result as JSON')
3364
+ .action((flags: PrdToSpecFlags) => {
3365
+ const repoPath = flags.repo ?? process.cwd();
3366
+ const result = runPrdToSpec({
3367
+ repoPath,
3368
+ dryRun: flags.dryRun === true,
3369
+ });
3370
+
3371
+ if (flags.json) {
3372
+ console.log(JSON.stringify(result, null, 2));
3373
+ } else {
3374
+ printPrdToSpecReport(result);
3375
+ }
3376
+
3377
+ // Exit non-zero on a REFUSAL (quiescence gate) or a non-green leak scan
3378
+ // (a converted tree that still carries a dangling `prd` ref). A clean
3379
+ // dry-run or a green migration exits 0.
3380
+ if (result.refused || result.leaks.length > 0) {
3381
+ process.exit(1);
3382
+ }
3383
+ });
3384
+
3385
+ program
3386
+ .command('status')
3387
+ .helpGroup(HEADLINE_GROUP)
3388
+ .description(
3389
+ 'Read-only operational dashboard of JOBS (distinct from scan’s backlog queue): list every job under workspacesDir/work/* from its .dorfl-job.json record + worktree state, grouped active (running + alive) vs failed/retained (needs-attention with its reason, a crashed running-but-dead job, or a done-but-un-reaped one). Liveness comes from the harness seam (PID/session), NOT mtime. Never claims/runs/moves/deletes (deletion is gc).',
3390
+ )
3391
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3392
+ .option(
3393
+ '--workspace <dir>',
3394
+ 'execution working area to inspect (default: workspacesDir / ~/.dorfl)',
3395
+ )
3396
+ .option(
3397
+ '--arbiter-remote <name>',
3398
+ `the current repo's arbiter remote to report on (folds in the old \`arbiter status\`; default: ${DEFAULT_ARBITER_REMOTE})`,
3399
+ )
3400
+ .option(
3401
+ '--arbiter <remote>',
3402
+ 'the COORDINATION arbiter remote whose per-item lock refs gate the cwd selection pool (held in-flight items are subtracted); default: origin',
3403
+ )
3404
+ .option('--no-arbiter', "skip the current repo's arbiter section")
3405
+ .option(
3406
+ '--here',
3407
+ '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.',
3408
+ )
3409
+ .option('--json', 'output the raw report as JSON')
3410
+ .action(async (flags: StatusFlags) => {
3411
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
3412
+ const override = loadConfigOverride(
3413
+ defaultConfigOverridePath(flags.config),
3414
+ );
3415
+ const workspacesDir = flags.workspace ?? config.workspacesDir;
3416
+ const warn = (message: string) => console.error(`>> ${message}`);
3417
+ const resolveCwd = () =>
3418
+ resolveCwdSection({
3419
+ cwd: process.cwd(),
3420
+ config,
3421
+ override,
3422
+ arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
3423
+ lockArbiterRemote: flags.arbiter ?? 'origin',
3424
+ warn,
3425
+ });
3426
+ // `--here`: report ONLY the cwd — skip the jobs, registry-mirror, and arbiter
3427
+ // sections entirely ("this repo, nothing else"). `status` is built with NO
3428
+ // jobs (empty workspace view), NO mirrors, and NO arbiter, so only the cwd
3429
+ // block renders.
3430
+ if (flags.here === true) {
3431
+ const cwdSection = await resolveCwd();
3432
+ const report = await status({
3433
+ workspacesDir,
3434
+ mirrorPaths: [],
3435
+ cwd: cwdSection,
3436
+ warn,
3437
+ });
3438
+ if (flags.json) {
3439
+ console.log(JSON.stringify(report, null, 2));
3440
+ } else {
3441
+ console.log(formatStatus(report));
3442
+ }
3443
+ return;
3444
+ }
3445
+ // Surface the folder-native needs-attention set (ADR §12) from each
3446
+ // REGISTERED HUB MIRROR (the registry), read from its bare `main` ref
3447
+ // through the read seam (mirrors have no working tree).
3448
+ const mirrorPaths = listMirrors({workspacesDir}).map((m) => m.path);
3449
+ // Fold in the current repo's arbiter state (the old `arbiter status`, ADR
3450
+ // §1/§7) unless --no-arbiter. Read-only; tolerates not being in a repo.
3451
+ const arbiter =
3452
+ flags.noArbiter === true
3453
+ ? undefined
3454
+ : arbiterStatus({
3455
+ cwd: process.cwd(),
3456
+ remote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
3457
+ });
3458
+ // The cwd-local section: resolve it ONLY when a participating cwd is NOT
3459
+ // already covered by a registered mirror, via the SAME fetch-free pre-check
3460
+ // `scan` uses (`cwdSectionDisposition`) — so an already-registered cwd is not
3461
+ // re-fetched (the registry/jobs view already covers it) while a mirror-less
3462
+ // cwd is still shown standalone.
3463
+ const disposition = cwdSectionDisposition({
3464
+ cwd: process.cwd(),
3465
+ config,
3466
+ arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
3467
+ });
3468
+ const cwdSection =
3469
+ disposition.participating && !disposition.alsoRegistered
3470
+ ? await resolveCwd()
3471
+ : undefined;
3472
+ const report = await status({
3473
+ workspacesDir,
3474
+ mirrorPaths,
3475
+ arbiter,
3476
+ cwd: cwdSection,
3477
+ warn,
3478
+ });
3479
+ if (flags.json) {
3480
+ console.log(JSON.stringify(report, null, 2));
3481
+ } else {
3482
+ console.log(formatStatus(report));
3483
+ }
3484
+ });
3485
+
3486
+ program
3487
+ .command('requeue <slug>')
3488
+ .helpGroup(HEADLINE_GROUP)
3489
+ .description(
3490
+ '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. DEFAULT = keep + continue: leave the work/<slug> branch UNTOUCHED so the next claim CONTINUES from its tip (rebased onto fresh main at onboard-time). --reset = discard + fresh: 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 (both modes; append-only).',
3491
+ )
3492
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3493
+ .option(
3494
+ '--cwd <dir>',
3495
+ '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',
3496
+ )
3497
+ .option(
3498
+ '--arbiter <remote>',
3499
+ '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.',
3500
+ )
3501
+ .option(
3502
+ '--reset',
3503
+ 'DISCARD the kept work: delete the remote work/<slug> branch FIRST, then move to backlog so the next claim starts FRESH (guarded; a deliberate departure from the never-delete-the-remote-branch invariant). Never the default.',
3504
+ )
3505
+ .option(
3506
+ '-m, --message <note>',
3507
+ 'append a dated handoff note to the item body for the next agent (append-only; applies to both default and --reset)',
3508
+ )
3509
+ .action(async (rawSlug: string, flags: RequeueFlags) => {
3510
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
3511
+ const slug = resolveTaskOnlySlug(rawSlug) as string;
3512
+ const cwd = flags.cwd ?? process.cwd();
3513
+ // Route the requeue (default keep+continue / --reset discard / -m handoff)
3514
+ // THROUGH the ledger write seam's transition (same seam the needs-attention
3515
+ // move uses), not the helper.
3516
+ //
3517
+ // `requeue` is a HUMAN command (like `complete`): the human is putting a
3518
+ // resolved item back, so the move/commit/push is THEIRS — it is NOT given
3519
+ // the runner identity (`config.identity`). The autonomous re-attempt is
3520
+ // `do` (which IS identity-aware), not this. We thread the ambient
3521
+ // `process.env` EXPLICITLY so the human-identity choice is visible at the
3522
+ // call site, rather than relying on the seam's silent `?? process.env`
3523
+ // default by omission (the implicit fallback that made `requeue`'s human
3524
+ // attribution accidental rather than declared).
3525
+ const result = await ledgerWrite.applyReturnToBacklogTransition({
3526
+ cwd,
3527
+ slug,
3528
+ // Tree-less CAS needs a ref to push to (parity with `claim`): default the
3529
+ // arbiter to `origin` so the common case Just Works; `--arbiter` overrides.
3530
+ // `--cwd` is purely the ORIGIN SOURCE the remote is resolved from.
3531
+ arbiter: flags.arbiter ?? 'origin',
3532
+ reset: flags.reset,
3533
+ message: flags.message,
3534
+ env: process.env,
3535
+ note: (message) => console.error(`>> ${message}`),
3536
+ });
3537
+ if (!result.moved) {
3538
+ console.error(`error: ${result.reasonNotMoved}`);
3539
+ process.exit(1);
3540
+ }
3541
+ const how = result.deletedRemoteBranch
3542
+ ? ` (--reset: deleted the remote ${workBranchRef('task', slug)} branch; next claim starts fresh)`
3543
+ : ' (kept the work branch; next claim continues from its tip)';
3544
+ console.log(`Requeued '${slug}' to backlog for re-claiming.${how}`);
3545
+ });
3546
+
3547
+ // `promote [item]` (prd `staging-pool-position-gate-and-trust-model`, tasks
3548
+ // `pre-backlog-staging-folder-and-promote-step-a` /
3549
+ // `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the HUMAN/runner-
3550
+ // owned verb that moves a STAGED item into its agent-eligible POOL — a task
3551
+ // `work/pre-backlog/<slug>.md → work/backlog/<slug>.md`, a prd
3552
+ // `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md` — as a tree-less CAS on the
3553
+ // arbiter, the SAME trust model + mechanism as `requeue`. The agent emits STAGED;
3554
+ // only this verb (a human, or the runner) admits it to the pool. With NO argument
3555
+ // it LISTS what is promotable (the "what is staged waiting for me?" discovery), so
3556
+ // a human need not remember the staged slugs.
3557
+ program
3558
+ .command('promote [item]')
3559
+ .helpGroup(HEADLINE_GROUP)
3560
+ .description(
3561
+ 'Admit a STAGED item into its agent-eligible POOL (the runner/human side of the staging gate): a task `work/pre-backlog/<slug>.md → work/backlog/<slug>.md`, a prd `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md`, published as a TREE-LESS compare-and-swap to the arbiter ref (EXACTLY like requeue/claim — it never stages/commits in the cwd tree). The agent only ever CREATES staged; this verb is the gate a human (or the runner) opens. Accepts `task:<slug>` / `prd:<slug>` / a bare `<slug>` (= task). With NO argument, LISTS every promotable item (the tasks in pre-backlog/ + the prds in prds/proposed/ on the arbiter) so you can see what is staged waiting for promotion. Idempotent: promoting an already-pooled slug is a clean no-op success.',
3562
+ )
3563
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3564
+ .option(
3565
+ '--cwd <dir>',
3566
+ 'the repo/working clone whose arbiter remote the tree-less move is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree',
3567
+ )
3568
+ .option(
3569
+ '--arbiter <remote>',
3570
+ 'the arbiter git remote the promotion is CAS-published to / the staging folders are listed from (default: origin)',
3571
+ )
3572
+ .action(async (rawItem: string | undefined, flags: PromoteFlags) => {
3573
+ const cwd = flags.cwd ?? process.cwd();
3574
+ const arbiter = flags.arbiter ?? 'origin';
3575
+ // `promote` is a HUMAN command (like `requeue`): the move/commit/push is
3576
+ // THEIRS — NOT the runner identity. Thread the ambient env explicitly.
3577
+ const env = process.env;
3578
+ const note = (message: string) => console.error(`>> ${message}`);
3579
+
3580
+ // NO ARGUMENT → LIST what is promotable (read-only discovery).
3581
+ if (rawItem === undefined) {
3582
+ const listed = await listPromotable({cwd, arbiter, env});
3583
+ if (listed.error) {
3584
+ console.error(`error: ${listed.error}`);
3585
+ process.exit(1);
3586
+ }
3587
+ if (listed.items.length === 0) {
3588
+ console.log(
3589
+ `Nothing staged to promote on ${arbiter}/main (work/pre-backlog/ and work/prds/proposed/ are empty).`,
3590
+ );
3591
+ return;
3592
+ }
3593
+ console.log('Staged, awaiting promotion (run `promote <item>`):');
3594
+ for (const item of listed.items) {
3595
+ console.log(` ${item.namespace}:${item.slug}`);
3596
+ }
3597
+ return;
3598
+ }
3599
+
3600
+ // AN ITEM → promote it. `task:`/`spec:` are explicit (the legacy `prd:`
3601
+ // prefix is still ACCEPTED as an input alias through the cutover — the
3602
+ // contract task drops it); a bare slug defaults to a task (mirrors
3603
+ // `requeue`). An `obs:`/`observation:` prefix is rejected (observations have
3604
+ // no pool).
3605
+ const parsed = parseSlugArg(rawItem);
3606
+ if (parsed.explicit === 'observation') {
3607
+ console.error(
3608
+ `error: promote takes a task or spec, not an observation ('${rawItem}'). Observations have no agent pool.`,
3609
+ );
3610
+ process.exit(1);
3611
+ }
3612
+ // A `spec:` prefix produces the `spec` namespace (the dispatch + messages
3613
+ // speak `spec`); only bare/`task:` stays `task`.
3614
+ const namespace = parsed.explicit === 'spec' ? 'spec' : 'task';
3615
+ const slug = parsed.slug;
3616
+ const result =
3617
+ namespace === 'spec'
3618
+ ? await promoteFromPreSpec({cwd, slug, arbiter, env, note})
3619
+ : await promoteFromPreBacklog({cwd, slug, arbiter, env, note});
3620
+ if (!result.moved) {
3621
+ console.error(`error: ${result.reasonNotMoved}`);
3622
+ process.exit(1);
3623
+ }
3624
+ const dest =
3625
+ namespace === 'spec'
3626
+ ? workFolderPrefix('specs-ready')
3627
+ : workFolderPrefix('tasks-ready');
3628
+ console.log(
3629
+ `Promoted ${namespace} '${slug}' into the pool (${dest}); it is now ${
3630
+ namespace === 'spec' ? 'auto-taskable' : 'claimable'
3631
+ }.`,
3632
+ );
3633
+ });
3634
+
3635
+ // NOTE: the legacy `release-advancing <item>` verb is RETIRED by the capstone
3636
+ // cut-over (task `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`):
3637
+ // the `work/advancing/<entry>.md` marker is gone and an advance hold is now just
3638
+ // `action: advance` on the UNIFIED per-item lock, so `release-lock <item>` (below)
3639
+ // is the SOLE named human release for ALL holds (implement/task/advance).
3640
+
3641
+ // `release-lock <item>` (task `release-lock-verb-and-gc-stuck-report`, prd
3642
+ // `ledger-status-per-item-lock-refs` US #14): the HUMAN-invoked named release of
3643
+ // a stuck/orphaned UNIFIED per-item lock (`refs/dorfl/lock/<entry>`) —
3644
+ // the GENERALISATION of `release-advancing` from the advancing-only marker to
3645
+ // the one lock per item (implement/task/advance × active/stuck). Same trust
3646
+ // model as `release-advancing` / `requeue`: a HUMAN asserts the lock is dead by
3647
+ // NAMING it; the tool never guesses liveness (there is NO heartbeat, NO
3648
+ // auto-sweep — the `gc --ledger` report only REPORTS lingering locks). Routes
3649
+ // through the existing leased-delete `releaseItemLock` (deleting the ref IS the
3650
+ // release; the parentless lock commit becomes gc-reclaimable). NEVER `--force`.
3651
+ // Idempotent: deleting an absent ref is a clean exit-0 "nothing to clear"
3652
+ // (`not-held`), NOT a failure — deleting the lock ref(s) is "all locks released"
3653
+ // and recoverable (the work is safe on the `work/<slug>` branches + `main`).
3654
+ program
3655
+ .command('release-lock <item>')
3656
+ .helpGroup(HEADLINE_GROUP)
3657
+ .description(
3658
+ 'Clear a NAMED stuck/orphaned UNIFIED per-item lock (refs/dorfl/lock/<entry>) by DELETING the ref on the arbiter — the recovery verb for a lock the system orphaned (a crashed build/task/advance that left the hold behind). The generalisation of `release-advancing` from the advancing marker to the ONE lock per item. Same trust model as `requeue`: a HUMAN asserts the lock is dead by NAMING it; the tool never guesses liveness (the lock has NO heartbeat, so there is NO automatic sweep / age-based reaper anywhere). Accepts the same item forms as the lock API: `task:<slug>` / `prd:<slug>` / `obs:<slug>` / a bare `<slug>` (= task). Idempotent — re-running on an already-cleared lock is a clean exit-0 no-op (deleting the lock ref is “all locks released”, recoverable). NEVER `--force`. Discoverable via `gc --ledger` (it REPORTS every lingering lock, never deletes).',
3659
+ )
3660
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3661
+ .option(
3662
+ '--cwd <dir>',
3663
+ 'the repo/working clone whose arbiter remote the lock ref is DELETED on (default: cwd)',
3664
+ )
3665
+ .option(
3666
+ '--arbiter <remote>',
3667
+ 'the arbiter git remote the lock ref is deleted on (default: origin)',
3668
+ )
3669
+ .action(async (item: string, flags: ReleaseLockFlags) => {
3670
+ const cwd = flags.cwd ?? process.cwd();
3671
+ const arbiter = flags.arbiter ?? 'origin';
3672
+ const result = await releaseItemLock({
3673
+ item,
3674
+ cwd,
3675
+ arbiter,
3676
+ env: process.env,
3677
+ });
3678
+ if (result.outcome === 'released') {
3679
+ console.log(
3680
+ `Released lock '${result.entry}' (${result.ref} deleted on ${arbiter}; the item itself was untouched — it rests on main / its work/<slug> branch).`,
3681
+ );
3682
+ return;
3683
+ }
3684
+ // IDEMPOTENT exit semantics: `releaseItemLock` returns `not-held` when the
3685
+ // ref is ALREADY absent. For a HUMAN re-running the verb on an
3686
+ // already-cleared lock that is the CORRECT "nothing to clear" outcome —
3687
+ // deleting the lock ref(s) is "all locks released" and recoverable — so map
3688
+ // it to a clean exit-0 with an honest message (NOT a failure).
3689
+ if (result.outcome === 'not-held') {
3690
+ console.log(
3691
+ `No lock to release for '${result.entry}' (${result.ref} is already absent on ${arbiter} — “all locks released”, recoverable).`,
3692
+ );
3693
+ return;
3694
+ }
3695
+ console.error(`error: ${result.message}`);
3696
+ process.exit(1);
3697
+ });
3698
+
3699
+ // `drop <slug>` (prd `agentic-question-resolution-retire-disposition-vocabulary`,
3700
+ // US #5/#11; task `direct-delete-question-cli-helper`): the DIRECT "throw it
3701
+ // away" verb — `git rm` a source item AND its question sidecar (when present) in
3702
+ // ONE revertible commit, the reason in the commit MESSAGE (git history is the
3703
+ // archive). It does NOT round-trip through the decision engine or spawn an agent
3704
+ // (that is the SEPARATE agentic `delete-source` verdict in apply-persist.ts);
3705
+ // this is the human/skill/CLI no-ceremony delete of decision 7. DISTINCT from
3706
+ // the existing `remote rm` (the hub-MIRROR deleter) — different concern, no
3707
+ // collision. A LOCAL one-commit primitive over the working tree (like apply): it
3708
+ // does NOT touch the arbiter; the human pushes/integrates the revertible commit
3709
+ // however they normally do.
3710
+ program
3711
+ .command('drop <slug>')
3712
+ .helpGroup(HEADLINE_GROUP)
3713
+ .description(
3714
+ 'DIRECTLY delete a source item + its question sidecar (when present) in ONE revertible commit — the "I just want to throw this away" path that does NOT round-trip through the decision engine or any agent. Resolves the source by its namespaced identity (`task:<slug>` / `prd:<slug>` / `obs:<slug>` / a bare `<slug>` = task), `git rm`s the source AND its sidecar together, and records your --reason in the commit MESSAGE (git history is the archive). A single revertible commit, so a wrong delete is recoverable via `git revert`. DISTINCT from `remote rm` (the hub-mirror deleter). A LOCAL working-tree commit (like the apply rung); it does not touch the arbiter — push/integrate it as you normally would. If the named source is already gone it is a clean no-op (nothing to throw away).',
3715
+ )
3716
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3717
+ .option(
3718
+ '--cwd <dir>',
3719
+ 'the working clone the revertible delete commit is made in (default: cwd)',
3720
+ )
3721
+ .option(
3722
+ '--reason <text>',
3723
+ 'why you are throwing this away — recorded in the commit MESSAGE (git history is the archive). Optional; recorded as "(no reason given)" when omitted.',
3724
+ )
3725
+ .action((slug: string, flags: DropFlags) => {
3726
+ const cwd = flags.cwd ?? process.cwd();
3727
+ // `drop` is a DIRECT HUMAN action (like the apply rung's local commit): the
3728
+ // delete/commit is THEIRS, so thread the ambient env explicitly.
3729
+ const env = process.env;
3730
+ const note = (message: string) => console.error(`>> ${message}`);
3731
+ const result = dropSource({
3732
+ cwd,
3733
+ item: slug,
3734
+ reason: flags.reason,
3735
+ env,
3736
+ note,
3737
+ });
3738
+ if (result.outcome === 'not-found') {
3739
+ // Nothing to throw away (the source is already gone). A clean exit-0
3740
+ // no-op, NOT a failure — deleting something already absent is success.
3741
+ console.log(
3742
+ `Nothing to drop for '${result.item}' — no source item resolves by identity (already gone).`,
3743
+ );
3744
+ return;
3745
+ }
3746
+ console.log(
3747
+ `Dropped '${result.item}'${
3748
+ result.sidecarPath ? ' + its sidecar' : ''
3749
+ } in one revertible commit (${result.commit?.slice(
3750
+ 0,
3751
+ 8,
3752
+ )}; reason in the message). Recover with \`git revert\` if this was wrong.`,
3753
+ );
3754
+ });
3755
+
3756
+ program
3757
+ .command('intake')
3758
+ .helpGroup(HEADLINE_GROUP)
3759
+ .description(
3760
+ 'Front-of-funnel: turn a GitHub issue into the right work/ artifact. Reads issue #N + its comment thread via the issue seam (gh), runs a prompt→verdict decision, and dispatches it: a clear, small issue → a proposed work/backlog/<slug>.md PR carrying an `issue: N` closure link (read by a future CI close-job; not `Fixes #N`). GATE-FREE — your explicit invocation IS the authorization (autoTask/autoBuild do NOT apply), exactly as `do`. A LOCAL one-shot AND the SAME command CI schedules. PER-OUTCOME integration modes (the artifact TYPE is decided at runtime): --merge/--propose set BOTH; --merge-spec/--propose-spec and --merge-task/--propose-task override per type; granular overrides the aggregate; unset ⇒ propose for both.',
3761
+ )
3762
+ .argument(
3763
+ '<number>',
3764
+ 'the GitHub issue number to intake (e.g. `intake 42`)',
3765
+ )
3766
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
3767
+ .option(
3768
+ '--arbiter <remote>',
3769
+ 'name of the arbiter git remote (default: per-repo/global defaultArbiter)',
3770
+ )
3771
+ .option(
3772
+ '--merge',
3773
+ 'integrate BOTH outcomes (task AND spec) in merge mode (aggregate; overridden per type by --merge-*/--propose-*; mutually exclusive with --propose)',
3774
+ )
3775
+ .option(
3776
+ '--propose',
3777
+ 'integrate BOTH outcomes (task AND spec) in propose mode (aggregate; default; overridden per type; mutually exclusive with --merge)',
3778
+ )
3779
+ .option(
3780
+ '--no-pr',
3781
+ 'propose without opening a PR for intake emissions: push the branch but deliberately skip the review request (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.',
3782
+ )
3783
+ .option(
3784
+ '--merge-spec',
3785
+ 'integrate a spec outcome in merge mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --propose-spec)',
3786
+ )
3787
+ .option(
3788
+ '--propose-spec',
3789
+ 'integrate a spec outcome in propose mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --merge-spec)',
3790
+ )
3791
+ .option(
3792
+ '--merge-task',
3793
+ 'integrate a task outcome in merge mode (granular; overrides --merge/--propose for a task; mutually exclusive with --propose-task)',
3794
+ )
3795
+ .option(
3796
+ '--propose-task',
3797
+ 'integrate a task outcome in propose mode (granular; overrides --merge/--propose for a task; mutually exclusive with --merge-task)',
3798
+ )
3799
+ .option(
3800
+ '--origin-trust <trusted|untrusted>',
3801
+ "the author-trust verdict to STAMP onto the emitted prd/task (origin: issue + originTrust: <value>), so an untrusted origin survives the merge boundary and later forces the task's BUILD transition to propose. CI's intake.yml derives it from the SAME author_association case as the integration flags. UNSET (a local intake) ⇒ emitted unstamped (human/trusted) — the human running intake IS the checkpoint.",
3802
+ )
3803
+ .option(
3804
+ '--specs-land-in <where>',
3805
+ 'where an intake-authored spec lands: `pre-proposed` (staged, not auto-taskable) or `ready` (the auto-tasking pool). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > specsLandIn default > built-in). Resolved flag > env (DORFL_SPECS_LAND_IN) > per-repo > global > built-in.',
3806
+ )
3807
+ .option('--agent-cmd <cmd>', 'command to run the decision agent')
3808
+ .option(
3809
+ '--model <id>',
3810
+ 'model the decision agent runs on (routing intent; resolved flag > env > per-repo > global > default)',
3811
+ )
3812
+ .option(
3813
+ '--harness <adapter>',
3814
+ 'harness adapter that launches the decision agent: null (default) or pi',
3815
+ )
3816
+ .option(
3817
+ '--pi-bin <path>',
3818
+ 'pi CLI binary the pi harness invokes (default: pi on PATH)',
3819
+ )
3820
+ .option(
3821
+ '--sessions-dir <dir>',
3822
+ 'HOST-ONLY root folder under which the pi session file is generated',
3823
+ )
3824
+ .action(async (rawNumber: string, flags: IntakeFlags) => {
3825
+ const issueNumber = Number(rawNumber);
3826
+ if (
3827
+ rawNumber.trim() === '' ||
3828
+ !Number.isInteger(issueNumber) ||
3829
+ issueNumber < 1
3830
+ ) {
3831
+ console.error(
3832
+ `error: intake takes a positive issue NUMBER (got '${rawNumber}').`,
3833
+ );
3834
+ process.exit(1);
3835
+ }
3836
+ const cwd = process.cwd();
3837
+ const {global, override} = loadGlobalAndOverride(flags.config);
3838
+ const resolved = resolveRepoConfig({
3839
+ repoPath: cwd,
3840
+ global,
3841
+ override,
3842
+ flags: {
3843
+ ...harnessFlagOverrides(flags),
3844
+ // `--no-pr` (the PR-INTENT axis) rides the SAME chain.
3845
+ ...noPRFlagOverrides(flags),
3846
+ },
3847
+ });
3848
+ if (resolved.message) {
3849
+ console.error(`>> ${resolved.message}`);
3850
+ }
3851
+ const config = resolved.config;
3852
+ // Resolve the PER-OUTCOME integration modes (prd US #9): `intake` decides
3853
+ // the artifact TYPE at runtime, so a single --merge/--propose can't express
3854
+ // a type-conditional policy. The granular flags override the aggregate; an
3855
+ // UNSET type falls back to the per-repo/global `integration` (the SAME chain
3856
+ // `do`/`complete` use — flag > per-repo > global > default propose). `intake`
3857
+ // is GATE-FREE, so autoTask/autoBuild are NOT consulted (the explicit
3858
+ // invocation is its own authorization). `intake` owns only these KNOBS; WHICH
3859
+ // knobs CI sets is CI's POLICY (`runner-in-ci`), NOT here.
3860
+ let modes;
3861
+ try {
3862
+ modes = resolveIntakeIntegrationModes(flags, config.integration);
3863
+ } catch (err) {
3864
+ console.error(
3865
+ `error: ${err instanceof Error ? err.message : String(err)}`,
3866
+ );
3867
+ process.exit(1);
3868
+ }
3869
+ // The ORIGIN-TRUST stamp (task `untrusted-origin-forces-build-propose`):
3870
+ // the CI shell passes `--origin-trust <trusted|untrusted>`; `intake` writes
3871
+ // it onto the emitted artifact (it does NOT resolve trust — the ~L296
3872
+ // boundary). UNSET ⇒ undefined ⇒ emit unstamped (a local intake is
3873
+ // human/trusted). An INVALID value FAILS LOUDLY (an autonomy/trust signal
3874
+ // must never be quietly ignored), mirroring the observation-triage enum.
3875
+ let originTrust: 'trusted' | 'untrusted' | undefined;
3876
+ if (flags.originTrust !== undefined) {
3877
+ if (
3878
+ flags.originTrust !== 'trusted' &&
3879
+ flags.originTrust !== 'untrusted'
3880
+ ) {
3881
+ console.error(
3882
+ `error: --origin-trust must be 'trusted' or 'untrusted' (got '${flags.originTrust}').`,
3883
+ );
3884
+ process.exit(1);
3885
+ }
3886
+ originTrust = flags.originTrust;
3887
+ }
3888
+ const harness = createHarness({
3889
+ harness: config.harness,
3890
+ piBin: config.piBin,
3891
+ });
3892
+ // The OPERATOR's EXPLICIT spec-placement override (`--specs-land-in`), the
3893
+ // TOP of the placement precedence — mirrors `explicitTasksLandInFromFlag` on
3894
+ // the `do spec:` path. Fails loudly on a bad value.
3895
+ let explicitSpecsLandIn: 'pre-proposed' | 'ready' | undefined;
3896
+ try {
3897
+ explicitSpecsLandIn = explicitSpecsLandInFromFlag(flags.specsLandIn);
3898
+ } catch (err) {
3899
+ console.error(
3900
+ `error: ${err instanceof Error ? err.message : String(err)}`,
3901
+ );
3902
+ process.exit(1);
3903
+ }
3904
+ const result = await performIntake({
3905
+ issueNumber,
3906
+ cwd,
3907
+ arbiter: flags.arbiter ?? config.defaultArbiter,
3908
+ integration: modes,
3909
+ // The origin-trust stamp the CI shell passes IN (unset ⇒ unstamped).
3910
+ originTrust,
3911
+ noPR: config.noPR,
3912
+ // SPEC-PLACEMENT: the configured-default `specsLandIn` rung + the EXPLICIT
3913
+ // `--specs-land-in` override (top of the precedence). The shared placement
3914
+ // resolver in `intake.ts` overlays the untrusted-origin staging force.
3915
+ specsLandIn: config.specsLandIn,
3916
+ explicitSpecsLandIn,
3917
+ harness,
3918
+ agentCmd: config.agentCmd,
3919
+ model: config.model,
3920
+ sessionsDir: config.sessionsDir,
3921
+ // Host-only runner IDENTITY — scopes intake's `gh`/git ops (not the
3922
+ // decision/review AGENT launches); absent ⇒ ambient.
3923
+ identity: config.identity,
3924
+ note: (message) => console.error(`>> ${message}`),
3925
+ });
3926
+ if (result.exitCode !== 0) {
3927
+ console.error(`error: ${result.message}`);
3928
+ } else {
3929
+ console.error(`>> ${result.message}`);
3930
+ }
3931
+ process.exit(result.exitCode);
3932
+ });
3933
+
3934
+ // The CI CLOSE-JOB driver (prd `runner-in-ci`, capability E; task
3935
+ // `install-ci-close-job-workflow`). The thin JOB the emitted close-job workflow
3936
+ // invokes on a merge to main: resolve which source issue(s) the landed work
3937
+ // closes (resolveClosingIssue), run the "prd complete?" query for the prd case
3938
+ // (prd-complete-query, done), and close via the IssueProvider seam — all
3939
+ // UNCHANGED engine pieces, CONSUMED not re-built (the Out-of-Scope fence). CI
3940
+ // owns ONLY the job + trigger. Local-runnable too (a manual catch-up close).
3941
+ program
3942
+ .command('close-merged-issues')
3943
+ .helpGroup(ADVANCED_GROUP)
3944
+ .description(
3945
+ 'Close source issues whose work has landed on main (CI capability E, prd 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.prd: → prd issue:`), runs the existing "prd complete?" query for the prd case (closes ONLY when ALL its prd:<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.',
3946
+ )
3947
+ .option(
3948
+ '--cwd <dir>',
3949
+ 'the repo working dir whose work/ tree to scan (default: cwd)',
3950
+ )
3951
+ .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
3952
+ .option('--json', 'output the raw result as JSON')
3953
+ .action(async (flags: CloseMergedIssuesFlags) => {
3954
+ const repoPath = flags.cwd ?? process.cwd();
3955
+ const result = await performCloseMergedIssues({
3956
+ repoPath,
3957
+ ghBin: flags.ghBin,
3958
+ env: process.env,
3959
+ });
3960
+ if (flags.json) {
3961
+ console.log(JSON.stringify(result, null, 2));
3962
+ } else {
3963
+ for (const c of result.candidates) {
3964
+ if (c.decision === 'closed') {
3965
+ console.error(
3966
+ `>> closed issue #${c.issueNumber} (${c.via} ${c.slug}).`,
3967
+ );
3968
+ } else if (c.decision === 'close-failed') {
3969
+ console.error(
3970
+ `>> issue #${c.issueNumber} (${c.via} ${c.slug}) NOT closed: ${c.reason}`,
3971
+ );
3972
+ } else {
3973
+ console.error(
3974
+ `>> issue #${c.issueNumber} (${c.via} ${c.slug}) left open (${c.decision}).`,
3975
+ );
3976
+ }
3977
+ }
3978
+ console.error(
3979
+ `>> close-merged-issues: closed ${result.closed.length} issue(s).`,
3980
+ );
3981
+ }
3982
+ // The close-job is a terminal CI tick: a degraded close is reported, not a
3983
+ // crash (exit 0), exactly like intake's bounce close.
3984
+ process.exit(0);
3985
+ });
3986
+
3987
+ // The REGISTRY command group (ADR §1): the registered set of targets IS the
3988
+ // hub-mirror set on disk. `remote add --local` absorbs the old `arbiter init`;
3989
+ // `arbiter status` is folded into `status`. There is no standalone `arbiter`
3990
+ // command group, and no `roots`/`remotes` config field.
3991
+ const remote = program
3992
+ .command('remote')
3993
+ .helpGroup(HEADLINE_GROUP)
3994
+ .description(
3995
+ 'The registry: the registered set of targets IS the hub mirrors on disk under workspacesDir/repos/ (no roots/remotes config). add/rm/ls/find manage that set. `remote add --local` provisions a bare arbiter (absorbing `arbiter init`); `arbiter status` is folded into `status`.',
3996
+ );
3997
+
3998
+ remote
3999
+ .command('add <target>')
4000
+ .helpGroup(HEADLINE_GROUP)
4001
+ .description(
4002
+ 'Register a target by creating its hub mirror (idempotent). <target> is the arbiter URL; with --local it is a WORKING REPO whose bare arbiter is provisioned under arbitersDir (~/git, precious DATA, NEVER ~/.dorfl) and THAT arbiter is registered (absorbing `arbiter init`). The project-identity guard refuses registering one project (same projectId tail) under a second key unless --force; --force REPLACES the existing mirror but STILL refuses if a worktree of the replaced mirror holds un-pushed work (data-loss guard).',
4003
+ )
4004
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
4005
+ .option(
4006
+ '--local',
4007
+ 'provision a local --bare arbiter from <target> (a working repo) and register it (absorbs `arbiter init`)',
4008
+ )
4009
+ .option(
4010
+ '--arbiter-remote <name>',
4011
+ `name of the arbiter remote to wire in the working repo on --local (default: ${DEFAULT_ARBITER_REMOTE})`,
4012
+ )
4013
+ .option(
4014
+ '--force',
4015
+ 'REPLACE this project’s existing mirror (re-link remote ↔ bare arbiter deliberately); overrides the registration POLICY block ONLY — still refuses if a worktree of the replaced mirror holds un-pushed work (the data-loss block is never overridden)',
4016
+ )
4017
+ .action((target: string, flags: RemoteAddFlags) => {
4018
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
4019
+ try {
4020
+ const result = remoteAdd({
4021
+ target,
4022
+ local: flags.local,
4023
+ workspacesDir: config.workspacesDir,
4024
+ arbitersDir: config.arbitersDir,
4025
+ arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
4026
+ force: flags.force,
4027
+ note: (message) => console.error(`>> ${message}`),
4028
+ });
4029
+ if (result.arbiter) {
4030
+ const a = result.arbiter;
4031
+ console.log(
4032
+ a.created
4033
+ ? `Provisioned bare arbiter at ${a.path}`
4034
+ : `Arbiter already exists at ${a.path} (not clobbered)`,
4035
+ );
4036
+ console.log(`Wired remote '${a.remote}' -> ${a.url}`);
4037
+ }
4038
+ console.log(
4039
+ result.created
4040
+ ? `Registered '${result.key}' (${result.transport}) — hub mirror at ${result.mirrorPath}`
4041
+ : `'${result.key}' already registered (mirror at ${result.mirrorPath})`,
4042
+ );
4043
+ } catch (err) {
4044
+ if (err instanceof RegistryError) {
4045
+ console.error(`error: ${err.message}`);
4046
+ process.exit(1);
4047
+ }
4048
+ throw err;
4049
+ }
4050
+ });
4051
+
4052
+ remote
4053
+ .command('rm <target>')
4054
+ .helpGroup(ADVANCED_GROUP)
4055
+ .description(
4056
+ 'Delete a hub mirror by key (host/org/name) or origin URL. The ONLY mirror deleter — `gc` NEVER reaps mirrors. Plumbing tier.',
4057
+ )
4058
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
4059
+ .action((target: string, flags: RemoteRmFlags) => {
4060
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
4061
+ const result = remoteRm({target, workspacesDir: config.workspacesDir});
4062
+ if (!result.removed) {
4063
+ console.error(`error: no registered mirror matches '${target}'.`);
4064
+ process.exit(1);
4065
+ }
4066
+ console.log(`Removed mirror '${result.key}' (${result.path}).`);
4067
+ });
4068
+
4069
+ remote
4070
+ .command('ls')
4071
+ .helpGroup(HEADLINE_GROUP)
4072
+ .description(
4073
+ 'List every registered hub mirror with its origin URL + transport. The origin URL is read from each mirror (the key encoding is lossy — it drops scheme/transport), so it is authoritative, not reconstructed from the key.',
4074
+ )
4075
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
4076
+ .option('--json', 'output the raw list as JSON')
4077
+ .action((flags: RemoteLsFlags) => {
4078
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
4079
+ const mirrors = listMirrors({workspacesDir: config.workspacesDir});
4080
+ if (flags.json) {
4081
+ console.log(JSON.stringify(mirrors, null, 2));
4082
+ return;
4083
+ }
4084
+ if (mirrors.length === 0) {
4085
+ console.log(
4086
+ 'No registered mirrors. Use `remote add <url>` or `remote find <folder>`.',
4087
+ );
4088
+ return;
4089
+ }
4090
+ for (const m of mirrors) {
4091
+ console.log(
4092
+ `${m.key} ${m.transport} ${m.originUrl ?? '(no origin)'}`,
4093
+ );
4094
+ }
4095
+ });
4096
+
4097
+ remote
4098
+ .command('find <folder>')
4099
+ .helpGroup(HEADLINE_GROUP)
4100
+ .description(
4101
+ 'Discover work/-participating repos under <folder> (a populated work/backlog/), then toggle-add the chosen ones via `remote add`. Interactive multi-select by default; --yes adds ALL discovered repos non-interactively.',
4102
+ )
4103
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
4104
+ .option('--yes', 'add all discovered participating repos (no prompt)')
4105
+ .action(async (folder: string, flags: RemoteFindFlags) => {
4106
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
4107
+ const repos = findParticipatingRepos(folder);
4108
+ if (repos.length === 0) {
4109
+ console.log(`No work/-participating repos found under ${folder}.`);
4110
+ return;
4111
+ }
4112
+ const chosen = flags.yes ? repos : await promptMultiSelect(repos);
4113
+ if (chosen.length === 0) {
4114
+ console.log('Nothing selected; no mirrors added.');
4115
+ return;
4116
+ }
4117
+ for (const repoPath of chosen) {
4118
+ // Each discovered repo is registered as a LOCAL bare arbiter (it is a
4119
+ // working checkout on disk, not a remote URL) — the same path
4120
+ // `remote add --local` takes. The transport guard still applies.
4121
+ try {
4122
+ const result = remoteAdd({
4123
+ target: repoPath,
4124
+ local: true,
4125
+ workspacesDir: config.workspacesDir,
4126
+ arbitersDir: config.arbitersDir,
4127
+ arbiterRemote: DEFAULT_ARBITER_REMOTE,
4128
+ note: (message) => console.error(`>> ${message}`),
4129
+ });
4130
+ console.log(
4131
+ `${result.created ? 'Registered' : 'Already registered'} '${result.key}' (${repoPath}).`,
4132
+ );
4133
+ } catch (err) {
4134
+ if (err instanceof RegistryError) {
4135
+ console.error(`skipped ${repoPath}: ${err.message}`);
4136
+ continue;
4137
+ }
4138
+ throw err;
4139
+ }
4140
+ }
4141
+ });
4142
+
4143
+ program
4144
+ .command('install-ci')
4145
+ .helpGroup(ADVANCED_GROUP)
4146
+ .description(
4147
+ '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).',
4148
+ )
4149
+ .option(
4150
+ '--config <file>',
4151
+ 'non-interactive: load the CI config from this JSON file (skips the wizard)',
4152
+ )
4153
+ .option(
4154
+ '--export-config <file>',
4155
+ 'write the gathered config as JSON to this path instead of generating artifacts',
4156
+ )
4157
+ .option(
4158
+ '--include-secrets',
4159
+ '(with --export-config) also gather + include the secret values in the export',
4160
+ )
4161
+ .option(
4162
+ '--fake',
4163
+ 'snapshot mode: write artifacts to `.fake/` (NEVER `.github/`) and set NO real secret',
4164
+ )
4165
+ .option(
4166
+ '--repo <owner/repo>',
4167
+ 'the GitHub repo to set secrets on (else auto-detected via gh)',
4168
+ )
4169
+ .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
4170
+ .option('--cwd <dir>', 'the target repo working dir (default: cwd)')
4171
+ .option(
4172
+ '--install-source <registry|workspace>',
4173
+ 'where the CI installs the CLI from: `registry` (npm install -g, the default) or `workspace` (build from the checked-out source + link onto PATH, for the self-hosting monorepo). Overrides auto-detection in both directions.',
4174
+ )
4175
+ .option(
4176
+ '--max-parallel <n>',
4177
+ 'cap on CONCURRENT advance-lifecycle matrix legs (the propose/merge `max-parallel`). Each leg is a full agent session, so a large fan-out can exhaust the model provider rate limit + thrash the CAS. Default 4.',
4178
+ )
4179
+ .action(async (flags: InstallCiFlags) => {
4180
+ const workDir = flags.cwd ?? process.cwd();
4181
+ if (
4182
+ flags.installSource !== undefined &&
4183
+ flags.installSource !== 'registry' &&
4184
+ flags.installSource !== 'workspace'
4185
+ ) {
4186
+ console.error(
4187
+ `install-ci: --install-source must be "registry" or "workspace" (got "${flags.installSource}")`,
4188
+ );
4189
+ process.exitCode = 1;
4190
+ return;
4191
+ }
4192
+ let maxParallel: number | undefined;
4193
+ if (flags.maxParallel !== undefined) {
4194
+ const n = Number(flags.maxParallel);
4195
+ if (!Number.isInteger(n) || n < 1) {
4196
+ console.error(
4197
+ `install-ci: --max-parallel must be a positive integer (got "${flags.maxParallel}")`,
4198
+ );
4199
+ process.exitCode = 1;
4200
+ return;
4201
+ }
4202
+ maxParallel = n;
4203
+ }
4204
+ const ctx = new GitHubCIContext({
4205
+ workDir,
4206
+ repo: flags.repo,
4207
+ ghBin: flags.ghBin,
4208
+ });
4209
+ // Discover the registered capability emitters (the directory-of-modules
4210
+ // seam: each capability self-registers from its own file under
4211
+ // `install-ci-capabilities/`, picked up here WITHOUT a shared-list edit).
4212
+ // This core task ships only a no-op reference (emits []); the sibling
4213
+ // capability tasks add self-registering modules that flow through here
4214
+ // automatically once landed. A no-op emitter contributes nothing.
4215
+ const capabilities = await loadCapabilityRegistry();
4216
+ const prompts = flags.config ? undefined : readlinePrompts();
4217
+ await installCI({
4218
+ ctx,
4219
+ fake: flags.fake === true,
4220
+ configFile: flags.config,
4221
+ exportConfig: flags.exportConfig,
4222
+ includeSecrets: flags.includeSecrets === true,
4223
+ installSource: flags.installSource as
4224
+ | 'registry'
4225
+ | 'workspace'
4226
+ | undefined,
4227
+ maxParallel,
4228
+ prompts,
4229
+ capabilities,
4230
+ log: (line) => console.error(line),
4231
+ });
4232
+ });
4233
+
4234
+ return program;
4235
+ }
4236
+
4237
+ /**
4238
+ * A readline-backed {@link WizardPrompts} for the interactive `install-ci`
4239
+ * wizard. Prompts go to stderr (stdout is reserved for any machine output); a
4240
+ * non-TTY invocation falls back to defaults (or empty), so a piped run never
4241
+ * hangs — use `--config` for a fully non-interactive reproduction.
4242
+ */
4243
+ function readlinePrompts(): WizardPrompts {
4244
+ const ask = (message: string, mask = false): Promise<string> =>
4245
+ new Promise((resolvePrompt) => {
4246
+ if (!process.stdin.isTTY) {
4247
+ resolvePrompt('');
4248
+ return;
4249
+ }
4250
+ const rl = createInterface({
4251
+ input: process.stdin,
4252
+ output: process.stderr,
4253
+ });
4254
+ void mask; // readline has no native masking; secrets are typed visibly
4255
+ rl.question(`${message} `, (answer) => {
4256
+ rl.close();
4257
+ resolvePrompt(answer);
4258
+ });
4259
+ });
4260
+ return {
4261
+ async input(message, opts) {
4262
+ const hint = opts?.default ? ` [${opts.default}]` : '';
4263
+ const answer = (await ask(`${message}${hint}`)).trim();
4264
+ return answer === '' && opts?.default ? opts.default : answer;
4265
+ },
4266
+ async password(message) {
4267
+ return (await ask(message, true)).trim();
4268
+ },
4269
+ async confirm(message, opts) {
4270
+ const hint = opts.default ? ' [Y/n]' : ' [y/N]';
4271
+ const answer = (await ask(`${message}${hint}`)).trim().toLowerCase();
4272
+ if (answer === '') return opts.default;
4273
+ return answer === 'y' || answer === 'yes';
4274
+ },
4275
+ async select(message, choices) {
4276
+ process.stderr.write(`${message}\n`);
4277
+ choices.forEach((c, i) => {
4278
+ process.stderr.write(` [${i + 1}] ${c.name}\n`);
4279
+ });
4280
+ const answer = (await ask('Choose (number):')).trim();
4281
+ const n = Number(answer);
4282
+ if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
4283
+ return choices[n - 1].value;
4284
+ }
4285
+ return choices[0].value; // default to the first choice
4286
+ },
4287
+ };
4288
+ }
4289
+
4290
+ /**
4291
+ * A minimal interactive multi-select toggle for `remote find`: list the
4292
+ * discovered repos numbered, let the user type the numbers to add (space/comma
4293
+ * separated; `all` for everything; blank for none). A non-interactive (no TTY)
4294
+ * invocation selects nothing — use `--yes` to add all without a prompt.
4295
+ */
4296
+ function promptMultiSelect(repos: string[]): Promise<string[]> {
4297
+ return new Promise((resolvePrompt) => {
4298
+ if (!process.stdin.isTTY) {
4299
+ resolvePrompt([]);
4300
+ return;
4301
+ }
4302
+ const rl = createInterface({input: process.stdin, output: process.stderr});
4303
+ process.stderr.write('Discovered work/-participating repos:\n');
4304
+ repos.forEach((repo, i) => {
4305
+ process.stderr.write(` [${i + 1}] ${repo}\n`);
4306
+ });
4307
+ rl.question('Add which? (numbers, `all`, or blank for none) ', (answer) => {
4308
+ rl.close();
4309
+ const trimmed = answer.trim().toLowerCase();
4310
+ if (trimmed === '') {
4311
+ resolvePrompt([]);
4312
+ return;
4313
+ }
4314
+ if (trimmed === 'all') {
4315
+ resolvePrompt([...repos]);
4316
+ return;
4317
+ }
4318
+ const picks = new Set<string>();
4319
+ for (const token of trimmed.split(/[\s,]+/)) {
4320
+ const n = Number(token);
4321
+ if (Number.isInteger(n) && n >= 1 && n <= repos.length) {
4322
+ picks.add(repos[n - 1]);
4323
+ }
4324
+ }
4325
+ resolvePrompt([...picks]);
4326
+ });
4327
+ });
4328
+ }
4329
+
4330
+ /**
4331
+ * Run the CLI: build the program and parse argv. Split from {@link buildProgram}
4332
+ * so tests can build + introspect/parse the program WITHOUT triggering a real
4333
+ * argv parse + `process.exit` on import (the module-level bootstrap below only
4334
+ * fires when this file is the process entry point).
4335
+ */
4336
+ export async function runCli(argv: string[] = process.argv): Promise<void> {
4337
+ const program = buildProgram();
4338
+ try {
4339
+ await program.parseAsync(argv);
4340
+ } catch (err: unknown) {
4341
+ console.error(err instanceof Error ? err.message : String(err));
4342
+ process.exit(1);
4343
+ }
4344
+ }
4345
+
4346
+ // Only bootstrap when invoked as the entry point (the installed `bin`), never on
4347
+ // import (so `buildProgram`/`runCli` are import-safe for tests).
4348
+ if (isCliEntryPoint()) {
4349
+ void runCli();
4350
+ }
4351
+
4352
+ /**
4353
+ * True iff this module is the process entry point (the `dorfl` bin).
4354
+ * Resolves both sides through `realpathSync` so a bin SYMLINK (npm/pnpm install
4355
+ * a `node_modules/.bin/dorfl` link to `dist/cli.js`) still matches.
4356
+ */
4357
+ function isCliEntryPoint(): boolean {
4358
+ const entry = process.argv[1];
4359
+ if (!entry) {
4360
+ return false;
4361
+ }
4362
+ try {
4363
+ const entryReal = realpathSync(entry);
4364
+ const selfReal = realpathSync(fileURLToPath(import.meta.url));
4365
+ return entryReal === selfReal;
4366
+ } catch {
4367
+ return false;
4368
+ }
4369
+ }