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/dist/cli.js ADDED
@@ -0,0 +1,3094 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from 'commander';
3
+ import { createInterface } from 'node:readline';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { realpathSync, mkdirSync, rmSync } from 'node:fs';
6
+ import { join as joinPath } from 'node:path';
7
+ import { git } from './git.js';
8
+ import { loadConfig, mergeConfig, defaultConfigPath, } from './config.js';
9
+ import { defaultConfigOverridePath, loadConfigOverride, } from './config-override.js';
10
+ import { envOverrides } from './env-config.js';
11
+ import { scan } from './scan.js';
12
+ import { remoteAdd, remoteRm, listMirrors, RegistryError } from './registry.js';
13
+ import { findParticipatingRepos } from './detect.js';
14
+ import { formatReport } from './format.js';
15
+ import { resolveCwdSection, cwdSectionDisposition } from './cwd-section.js';
16
+ import { runLoop, } from './run.js';
17
+ import { performClaim } from './claim-cas.js';
18
+ import { createClaimSpinner } from './cli-spinner.js';
19
+ import { performStart } from './start.js';
20
+ import { performWorkOn, loadHumanWorktreesDir, persistHumanWorktreesDir, } from './work-on.js';
21
+ import { performComplete, integrationFromFlags } from './complete.js';
22
+ import { performRecoverIsolated, locateIsolatedRecovery, } from './recover-isolated.js';
23
+ import { performDo, performDoRemote, resolveArbiterUrlFromCheckout, } from './do.js';
24
+ import { performDoRemoteAuto, performDoRemoteArgs } from './do-remote-auto.js';
25
+ import { performAdvance } from './advance.js';
26
+ import { performAdvanceAuto, performAdvanceArgs, runAdvanceTickWithTreelessPublish, } from './advance-drivers.js';
27
+ import { performAdvanceIsolated, performAdvanceIsolatedAuto, performAdvanceIsolatedArgs, } from './advance-isolated.js';
28
+ import { advanceRegistrySetRunTick } from './advance-loop-driver.js';
29
+ import { performIntake, resolveIntakeIntegrationModes } from './intake.js';
30
+ import { workFolderPrefix } from './work-layout.js';
31
+ import { performDoAuto, performDoArgs, } from './do-autopick.js';
32
+ import { createHarness } from './pi-harness.js';
33
+ import { generateSessionPath } from './session-path.js';
34
+ import { shouldUseColor } from './output.js';
35
+ import { resolveRepoConfig, resolveRepoConfigFromLoaded, loadRepoConfigFromContent, REPO_CONFIG_FILENAME, } from './repo-config.js';
36
+ import { ensureMirrorMain, readRepoConfigFromMirrorMain, encodeRepoKey, } from './repo-mirror.js';
37
+ import { identityEnv } from './identity.js';
38
+ import { harnessFlagOverrides, doFlagOverrides, doNeedsAgentCmd, NO_AGENT_CMD_MESSAGE, reviewFlagOverrides, freshWorktreeGateFlagOverrides, mergeRetriesFlagOverrides, noPRFlagOverrides, } from './do-config.js';
39
+ import { harnessReviewGate, harnessTaskAcceptanceGate } from './review-gate.js';
40
+ import { harnessSurfaceGate } from './surface-gate.js';
41
+ import { harnessTriageGate } from './triage-gate.js';
42
+ import { harnessApplyDecider } from './apply-decide.js';
43
+ import { harnessTaskReviewGate } from './tasker-review-loop.js';
44
+ import { runVerify } from './verify.js';
45
+ import { renderPrompt } from './prompt.js';
46
+ import { resolvePromptGuidance } from './config.js';
47
+ import { gc, RETAIN_REASON_TEXT } from './gc.js';
48
+ import { runPrdToSpec, } from './prd-to-spec.js';
49
+ import { sweepRemoteMergedBranches } from './reap-branches.js';
50
+ import { sweepOrphanSidecars } from './orphan-sidecar.js';
51
+ import { sweepLedgerDuplicates, formatLedgerSweep } from './ledger-lint.js';
52
+ import { status, formatStatus } from './status.js';
53
+ import { ledgerWrite } from './ledger-write.js';
54
+ import { releaseItemLock, reportItemLocks, formatItemLockReport, itemLockReportNeedsAttention, reapStaleItemLocks, formatReapReport, reapReportNeedsAttention, } from './item-lock.js';
55
+ import { promoteFromPreBacklog, promoteFromPreSpec, listPromotable, } from './needs-attention.js';
56
+ import { parseSlugArg } from './slug-namespace.js';
57
+ import { dropSource } from './drop-source.js';
58
+ import { arbiterStatus, DEFAULT_ARBITER_REMOTE } from './arbiter.js';
59
+ import { resolveTaskOnlyArg, workBranchRef, SlugResolutionError, } from './slug-namespace.js';
60
+ import { brand } from './brand.js';
61
+ import { installCI } from './install-ci.js';
62
+ import { GitHubCIContext } from './install-ci-github.js';
63
+ import { loadCapabilityRegistry } from './install-ci-core.js';
64
+ import { performCloseMergedIssues } from './close-job.js';
65
+ /**
66
+ * Whether `--auto-build` / `--no-auto-build` was explicitly passed on the command
67
+ * line. Commander gives a negatable boolean option a default of `true`, so we
68
+ * must check the value SOURCE to distinguish "user set it" from "default"; only an
69
+ * explicit flag becomes a config override (so config/defaults still win
70
+ * otherwise).
71
+ */
72
+ function autoBuildFromCli(command) {
73
+ if (!command) {
74
+ return undefined;
75
+ }
76
+ if (command.getOptionValueSource('autoBuild') === 'cli') {
77
+ return command.getOptionValue('autoBuild');
78
+ }
79
+ return undefined;
80
+ }
81
+ /**
82
+ * Build the overrides a user supplied via CLI flags. Discovery is the registry
83
+ * (the hub-mirror set, ADR §1), so there are no `--root`/`--include`/`--exclude`
84
+ * flags any more — only the autonomy-gate `--auto-build` toggle.
85
+ */
86
+ function flagOverrides(flags, command) {
87
+ const overrides = {};
88
+ const autoBuild = autoBuildFromCli(command);
89
+ if (autoBuild !== undefined) {
90
+ overrides.autoBuild = autoBuild;
91
+ }
92
+ return overrides;
93
+ }
94
+ /**
95
+ * Resolve the global (non-per-repo) config along the chain
96
+ *
97
+ * flag > ENV (DORFL_*) > global file > built-in default
98
+ *
99
+ * by layering the file config, then the `DORFL_*` env layer, then the
100
+ * flag overrides on top. Env is a per-machine source (like a flag or the global
101
+ * file) and may set ANY key, host-only included. Used by the commands that build
102
+ * a single global config (`scan`, `run`); per-repo commands fold env in via
103
+ * `resolveRepoConfig` instead.
104
+ */
105
+ function resolveGlobalConfig(fileConfig, flags) {
106
+ return mergeConfig({ ...fileConfig, ...envOverrides(), ...flags });
107
+ }
108
+ /**
109
+ * Load BOTH the global config file AND the per-machine `config.override.json`
110
+ * sibling for the given `--config` flag (or the default paths). This is the
111
+ * SINGLE entry point CLI commands use so the override layer is wired uniformly
112
+ * everywhere a per-repo resolution happens (ADR
113
+ * `per-machine-config-override-layer`); a missing override file is a no-op
114
+ * (empty map) — byte-identical to the pre-override behaviour.
115
+ */
116
+ function loadGlobalAndOverride(configPath) {
117
+ return {
118
+ global: loadConfig(configPath),
119
+ override: loadConfigOverride(defaultConfigOverridePath(configPath)),
120
+ };
121
+ }
122
+ /**
123
+ * Resolve the effective config for a `do --remote <r>` run, layering the target
124
+ * repo's COMMITTED `.dorfl.json` (read from `<arbiter>/main` via the hub
125
+ * mirror) into the SAME `flag > env > per-repo > global > default` chain in-place
126
+ * `do` uses. This is the no-checkout analogue of {@link resolveRepoConfig}: there
127
+ * is no working tree, so the bytes come from the arbiter's `main` instead of the
128
+ * cwd — but the parse + allow/reject FILTER (`loadRepoConfigFromContent`) and the
129
+ * layering (`resolveRepoConfigFromLoaded`) are the EXISTING per-repo machinery,
130
+ * reused verbatim. Host-only keys in the committed file are rejected + reported
131
+ * exactly as the in-place read rejects them.
132
+ *
133
+ * Resilient by design: a config-less repo (no file on main) OR an unreachable
134
+ * mirror falls back to global+default (the pre-task behaviour), with a warning
135
+ * on a genuine fetch/read fault — a `--remote` build must not be blocked because
136
+ * the arbiter was momentarily offline.
137
+ *
138
+ * The config read uses {@link ensureMirrorMain} (main-only, NO-prune), NOT the
139
+ * all-heads pruning {@link ensureMirror}: `git show main:.dorfl.json` only
140
+ * needs `main`, and the all-heads `+refs/heads/*:refs/heads/*` fetch would let a
141
+ * `work/<slug>` branch CHECKED OUT in some stale job worktree block it (git
142
+ * refuses to fetch into a checked-out branch), throwing the read into its
143
+ * fallback and silently dropping the per-repo `harness`/`verify`/etc. The build's
144
+ * own worktree MATERIALISATION still calls the all-heads `ensureMirror` later
145
+ * (continue-detection needs the kept `work/<slug>` head) — that is a separate,
146
+ * untouched concern; only the CONFIG-READ fetch is narrowed here.
147
+ */
148
+ function resolveRemoteRepoConfig(options) {
149
+ const { remote, workspacesDir, global, flags, identity, note, override } = options;
150
+ let loaded;
151
+ try {
152
+ const env = identityEnv(identity, process.env);
153
+ const mirror = ensureMirrorMain({ url: remote, workspacesDir, env });
154
+ const content = readRepoConfigFromMirrorMain(mirror.path, env);
155
+ loaded =
156
+ content === undefined
157
+ ? {
158
+ path: `${remote}#main:${REPO_CONFIG_FILENAME}`,
159
+ config: {},
160
+ rejected: [],
161
+ }
162
+ : loadRepoConfigFromContent(content, `${remote}#main:${REPO_CONFIG_FILENAME}`);
163
+ }
164
+ catch (err) {
165
+ // A fetch/read fault (offline arbiter, corrupt mirror) must NOT block the
166
+ // build: warn + fall back to global+default (today's no-per-repo behaviour).
167
+ note(`could not read the target repo's ${REPO_CONFIG_FILENAME} from ` +
168
+ `${remote}/main; resolving config from global + flags only. ` +
169
+ `${err instanceof Error ? err.message : String(err)}`);
170
+ loaded = { path: `${remote}#main`, config: {}, rejected: [] };
171
+ }
172
+ if (loaded.message) {
173
+ note(loaded.message);
174
+ }
175
+ return resolveRepoConfigFromLoaded(loaded, {
176
+ global,
177
+ flags,
178
+ override,
179
+ arbiterUrl: remote,
180
+ }).config;
181
+ }
182
+ /**
183
+ * Build the {@link RunTick} that **plain `run`** (no flag) loops: the REGISTRY-SET
184
+ * ADVANCE tick (task `run-uses-advance-tick`). This points the deliberate
185
+ * {@link RunTick} swap seam at the precursor's registry-set advance driver
186
+ * ({@link advanceRegistrySetRunTick}) instead of the build-only `runOnce` tick, so
187
+ * plain `run` ≡ advance with calm-default gates: behaviour-preserving today
188
+ * (both lifecycle gates default off ⇒ build ready tasks / task ready prds /
189
+ * route failures to needs-attention, over the SAME registry-set discovery +
190
+ * per-mirror job-worktree isolation the build tick used), lifecycle-capable the
191
+ * moment a gate is flipped (triage / surface / apply).
192
+ *
193
+ * Where the deprecated single-mirror advance wiring drained ONE named mirror
194
+ * IN-PLACE in the cwd checkout (the library {@link advanceRunTick}, now reached
195
+ * only by the precursor's single-mirror tests, no longer the CLI), this discovers
196
+ * the WHOLE registry via
197
+ * `scan(config)` (the SAME discovery the build tick uses) and the registry-set
198
+ * driver threads a PER-MIRROR job-worktree `doDriver` so each mirror's build/task
199
+ * rungs run isolated off THAT mirror's arbiter (NOT `process.cwd()`). The
200
+ * tree-less surface/triage/apply rungs commit in a per-mirror working CLONE of the
201
+ * mirror's arbiter (materialised lazily under the agents' workspace), since a bare
202
+ * hub mirror has no work tree to `git mv`/`git commit` in.
203
+ */
204
+ function buildRegistrySetAdvanceTick(options) {
205
+ const { config, workspace, arbiter, env, override } = options;
206
+ const gitEnv = identityEnv(config.identity, env);
207
+ const harness = createHarness({ harness: config.harness, piBin: config.piBin });
208
+ return advanceRegistrySetRunTick({
209
+ config,
210
+ override,
211
+ workspace,
212
+ // The SELECTION-layer gates for the loop/CI path, IDENTICAL to the
213
+ // single-mirror wiring above: `observationTriage != off` enumerates the
214
+ // observation (triage) pool; `surfaceBlockers` enumerates the `needsAnswers`-
215
+ // blocked (surface) pool. `off`/`false` drops the respective pool. Apply
216
+ // (consume) is always-on (never gated here). Both default to their calm state,
217
+ // so plain `run` out of the box is behaviour-identical to the old build tick.
218
+ lifecycleGates: {
219
+ triage: config.observationTriage !== 'off',
220
+ surface: config.surfaceBlockers,
221
+ // `surfaceStaging` widens the SURFACE candidate set into STAGING (prd
222
+ // `staging-surface-and-apply-promote-safety` F2). Default `true` — a
223
+ // tasked `needsAnswers` task in `tasks/backlog/` (or prd in
224
+ // `prds/proposed/`) surfaces its questions BEFORE promotion. BUILD/claim
225
+ // stays pool-only either way.
226
+ surfaceStaging: config.surfaceStaging,
227
+ },
228
+ // Build the per-mirror advance CONTEXT the registry-set driver injects its
229
+ // per-mirror job-worktree `doDriver` on top of: the build/task `doOptions`
230
+ // base + the surface/triage gate seams + a tree-less working clone of THIS
231
+ // mirror's arbiter (the ledger-write cwd the surface/triage/apply rungs commit
232
+ // in — a bare mirror cannot `git mv`/`git commit`).
233
+ contextFor: ({ mirrorPath, originUrl }) => {
234
+ // A per-mirror working clone of the mirror's arbiter for the tree-less
235
+ // lifecycle rungs (surface/triage/apply). Keyed by the mirror's repo key so
236
+ // distinct mirrors get distinct clones; re-created fresh each tick so the
237
+ // rungs always commit onto the latest mirror `main` (idempotent, cheap
238
+ // local clone). The build/task rungs DO NOT use this cwd (the worktree
239
+ // `doDriver` replaces it); it serves ONLY the tree-less moves.
240
+ const treelessCwd = joinPath(workspace, 'advance-cwd', encodeRepoKey(originUrl).split('/').join('__'));
241
+ rmSync(treelessCwd, { recursive: true, force: true });
242
+ mkdirSync(joinPath(treelessCwd, '..'), { recursive: true });
243
+ git(['clone', '--quiet', mirrorPath, treelessCwd], workspace, {
244
+ env: gitEnv,
245
+ });
246
+ const doOptions = {
247
+ cwd: treelessCwd,
248
+ arbiter: arbiter ?? config.defaultArbiter,
249
+ identity: config.identity,
250
+ autoTask: config.autoTask,
251
+ integration: config.integration,
252
+ // The per-TRANSITION TASKING override: the `do prd:` tasking path threads
253
+ // `taskingIntegration ?? integration`; the build path stays on `integration`.
254
+ taskingIntegration: config.taskingIntegration,
255
+ // The TASK-PLACEMENT configured default (`do prd:` tasking output:
256
+ // `pre-backlog` staged vs `ready` pool). No operator flag on this
257
+ // registry-driven advance context, so only the configured default rung is
258
+ // threaded (the resolver still layers untrusted-origin force + built-in floor).
259
+ tasksLandIn: config.tasksLandIn,
260
+ prepare: config.prepare,
261
+ verify: config.verify,
262
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
263
+ freshWorktreeGate: config.freshWorktreeGate,
264
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
265
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — resolved per-repo
266
+ // and threaded so the registry-driven advance path's `do` inherits it.
267
+ mergeRetries: config.mergeRetries,
268
+ noPR: config.noPR,
269
+ harness,
270
+ agentCmd: config.agentCmd,
271
+ model: config.model,
272
+ sessionsDir: config.sessionsDir,
273
+ review: config.review,
274
+ reviewModel: config.reviewModel,
275
+ reviewMaxRounds: config.reviewMaxRounds,
276
+ reviewGate: config.review
277
+ ? harnessReviewGate({ harness, agentCmd: config.agentCmd })
278
+ : undefined,
279
+ reviewLoop: config.taskerLoop
280
+ ? harnessTaskReviewGate({ harness, agentCmd: config.agentCmd })
281
+ : undefined,
282
+ taskerLoopMax: config.taskerLoopMax,
283
+ taskerLoopModel: config.taskerLoopModel,
284
+ taskReviewGate: config.review
285
+ ? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
286
+ : undefined,
287
+ color: shouldUseColor(process.stdout),
288
+ note: (message) => console.error(`>> ${message}`),
289
+ noteBlock: (message) => console.error(message),
290
+ };
291
+ const context = {
292
+ cwd: treelessCwd,
293
+ arbiter: arbiter ?? config.defaultArbiter,
294
+ doOptions,
295
+ surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
296
+ surfaceModel: config.model,
297
+ applyDecide: harnessApplyDecider({ harness, agentCmd: config.agentCmd }),
298
+ applyModel: config.model,
299
+ observationTriage: config.observationTriage,
300
+ triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
301
+ triageModel: config.model,
302
+ // The ANSWERED-MERGE LAND DISPATCH context (task
303
+ // `apply-rung-merge-disposition`, prd `land-time-reverify-and-parallel-
304
+ // merge-ceiling`): the dispatcher cuts a per-job worktree via
305
+ // `workspace.ts` `createJob` off the hub mirror (so we thread the resolved
306
+ // `workspacesDir` + the real arbiter URL — the per-mirror tree-less
307
+ // `treelessCwd` has `origin` pointing at the LOCAL mirror path, NOT the
308
+ // arbiter URL, so we MUST pass `originUrl` directly here), then drives
309
+ // `performIntegration` with `committedRecovery: true` +
310
+ // `freshWorktreeGate: true` (the rebased tip is re-verified, the RED
311
+ // route refuses, the GREEN route lands). `prepare`/`verify` are the SAME
312
+ // per-repo gate the build path uses; `strictMergeApproval` is the OQ6
313
+ // opt-in resolved by the sibling task `strict-merge-approval-gate`
314
+ // (default OFF ⇒ honour + land on a green re-verify).
315
+ workspacesDir: workspace,
316
+ arbiterUrl: originUrl,
317
+ prepare: config.prepare,
318
+ verify: config.verify,
319
+ strictMergeApproval: config.strictMergeApproval,
320
+ note: (message) => console.error(`>> ${message}`),
321
+ };
322
+ return context;
323
+ },
324
+ });
325
+ }
326
+ /**
327
+ * Resolve the arbiter URL for `do --isolated <slug>` from the CURRENT repo (cwd).
328
+ *
329
+ * `--isolated` builds in a job worktree off MY OWN arbiter (the same isolation +
330
+ * integrate pipeline `do --remote <url>` uses), so it needs the URL of the cwd's
331
+ * arbiter remote. It uses the SAME arbiter-remote resolution in-place `do` does:
332
+ * `--arbiter` > the resolved cwd `defaultArbiter` (the per-repo/global config), as
333
+ * the remote NAME, then `git remote get-url <name>` in the checkout to get its URL.
334
+ * That URL is then fed into the EXISTING `performDoRemote` pipeline as `remote`.
335
+ *
336
+ * Returns the URL, or `undefined` when there is no resolvable arbiter (cwd is not
337
+ * a git repo, or the named arbiter remote does not exist) — the "isolated against
338
+ * what?" case the caller turns into a clear error naming `--remote <url>`.
339
+ */
340
+ function resolveDefaultArbiterForCwd(cwd, global, flags, override) {
341
+ // The SAME per-repo config read in-place `do` uses (`resolveRepoConfig` on the
342
+ // cwd), so `--isolated` resolves the arbiter remote NAME (`defaultArbiter`)
343
+ // through the identical `flag > env > per-repo > global > default` chain. An
344
+ // absent `.dorfl.json` falls back to the global/default (`origin`).
345
+ return resolveRepoConfig({ repoPath: cwd, global, flags, override }).config
346
+ .defaultArbiter;
347
+ }
348
+ /**
349
+ * First-use prompt for the human worktree root (`work-on`). Offers `suggestion`
350
+ * as the default (Enter accepts it); a blank non-interactive answer aborts. The
351
+ * prompt goes to stderr so `--print-dir`'s stdout stays clean.
352
+ */
353
+ function promptForWorktreesRoot(suggestion) {
354
+ return new Promise((resolvePrompt) => {
355
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
356
+ rl.question('work-on needs a human worktree root (NOT under ~/.dorfl). ' +
357
+ `Where should parallel worktrees live? [${suggestion}] `, (answer) => {
358
+ rl.close();
359
+ const trimmed = answer.trim();
360
+ resolvePrompt(trimmed === '' ? suggestion : trimmed);
361
+ });
362
+ });
363
+ }
364
+ function runFlagOverrides(flags, command) {
365
+ const overrides = flagOverrides(flags, command);
366
+ if (flags.maxParallel !== undefined) {
367
+ overrides.maxParallel = Number(flags.maxParallel);
368
+ }
369
+ if (flags.perRepoMax !== undefined) {
370
+ overrides.perRepoMax = Number(flags.perRepoMax);
371
+ }
372
+ if (flags.arbiter !== undefined) {
373
+ overrides.defaultArbiter = flags.arbiter;
374
+ }
375
+ if (flags.integration === 'propose' || flags.integration === 'merge') {
376
+ overrides.integration = flags.integration;
377
+ }
378
+ // `--no-pr` (the PR-INTENT axis): suppress the PR even on an authed GitHub
379
+ // arbiter. Commander stores the negatable flag as `pr` (false when `--no-pr` is
380
+ // passed). Rides the SAME flag-override chain as `integration`.
381
+ if (flags.pr === false) {
382
+ overrides.noPR = true;
383
+ }
384
+ // The harness/adapter flags (--agent-cmd/--model/--harness/--pi-bin) map via
385
+ // the SHARED per-key mapping `do` also reuses (do-config.harnessFlagOverrides),
386
+ // so there is exactly ONE override path for them.
387
+ Object.assign(overrides, harnessFlagOverrides(flags));
388
+ // Gate 2 (PR/code review) flags ride the SAME flag-override path so
389
+ // `--review`/`--review-model`/`--review-max-rounds` resolve
390
+ // flag > env > per-repo > global > default — mirroring the `do` command (the
391
+ // fleet inherits the review gate via the converged `performIntegration` core).
392
+ Object.assign(overrides, reviewFlagOverrides(flags));
393
+ // `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain: gate
394
+ // the REBASED tip in a clean throwaway worktree (ON by default). The `run` fleet
395
+ // caller additionally downgrades it to today's gate at `perRepoMax > 1` (the
396
+ // fleet conditional lives in `runOnce`, not in this flag mapping).
397
+ Object.assign(overrides, freshWorktreeGateFlagOverrides(flags));
398
+ // `--merge-retries <n>` rides the SAME chain: the cross-job merge-serialiser
399
+ // CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5 /
400
+ // Applied Answer q1 (a)). The `run` fleet inherits the resolved cap through
401
+ // the converged `performIntegration` core (config.mergeRetries threads into
402
+ // the merge loop, replacing the bare `DEFAULT_MERGE_RETRIES` fallback).
403
+ Object.assign(overrides, mergeRetriesFlagOverrides(flags));
404
+ return overrides;
405
+ }
406
+ function formatItemLine(item) {
407
+ const extra = item.detail ? ` — ${item.detail}` : '';
408
+ return ` [${item.status}] ${item.repoPath} :: ${item.slug}${extra}`;
409
+ }
410
+ /**
411
+ * Resolve the EXPLICIT operator placement override from `--tasks-land-in <where>`
412
+ * (the top of the `do prd:` tasking-placement precedence — task
413
+ * `runner-deterministic-slice-placement-policy-and-precedence`). Mirrors the
414
+ * `flagMode === 'merge'` ⇒ `explicitMerge: true` shape: it contributes
415
+ * `explicitTasksLandIn` ONLY when the operator actually typed the flag, so an
416
+ * untrusted-origin's staging force still wins when the value came from config, not
417
+ * the flag. An invalid value FAILS LOUDLY (a usage error, never silently dropped
418
+ * — the SAME discipline the `--observation-triage` enum + the
419
+ * `DORFL_TASKS_LAND_IN` env coercion use).
420
+ */
421
+ function explicitTasksLandInFromFlag(raw) {
422
+ if (raw === undefined) {
423
+ return undefined;
424
+ }
425
+ if (raw !== 'pre-backlog' && raw !== 'ready') {
426
+ throw new Error(`--tasks-land-in must be 'pre-backlog' or 'ready' (got '${raw}').`);
427
+ }
428
+ return raw;
429
+ }
430
+ /**
431
+ * The SPEC twin of {@link explicitTasksLandInFromFlag} (spec
432
+ * `prd-to-spec-vocabulary-cutover-and-migration-command`). Resolve the EXPLICIT
433
+ * operator spec-placement override from `--specs-land-in <where>` for `intake`'s
434
+ * `spec` dispatch — the TOP of the same precedence chain that the tasking
435
+ * placement uses. Contributes `explicitSpecsLandIn` ONLY when the operator
436
+ * actually typed the flag, so an untrusted-origin's staging force still wins when
437
+ * the value came from config. An invalid value FAILS LOUDLY (a usage error,
438
+ * never silently dropped), mirroring the task helper above and the
439
+ * `DORFL_SPECS_LAND_IN` env coercion.
440
+ *
441
+ * HARD CUTOVER: the legacy `--prds-land-in` flag is GONE (clean break, spec US
442
+ * #8); only `--specs-land-in` is accepted.
443
+ */
444
+ function explicitSpecsLandInFromFlag(raw, flagName = '--specs-land-in') {
445
+ if (raw === undefined) {
446
+ return undefined;
447
+ }
448
+ if (raw !== 'pre-proposed' && raw !== 'ready') {
449
+ throw new Error(`${flagName} must be 'pre-proposed' or 'ready' (got '${raw}').`);
450
+ }
451
+ return raw;
452
+ }
453
+ /** Human-readable report for the `prd-to-spec` migration result. */
454
+ function printPrdToSpecReport(result) {
455
+ if (result.refused) {
456
+ const v = result.refused;
457
+ const label = v.kind === 'dirty-tree'
458
+ ? 'dirty working tree'
459
+ : v.kind === 'held-lock'
460
+ ? 'a held per-item lock'
461
+ : 'an in-progress work-branch carrying unlanded work';
462
+ console.error(`REFUSED: the repo is not quiescent (${label}): ${v.offender}. ` +
463
+ 'Land or discard the in-flight work, then re-run. (prd-to-spec ' +
464
+ 'never migrates over uncommitted/in-flight state.)');
465
+ return;
466
+ }
467
+ const verb = result.dryRun ? 'WOULD' : 'DID';
468
+ console.log(result.dryRun
469
+ ? '=== prd-to-spec (DRY RUN — nothing written) ==='
470
+ : '=== prd-to-spec ===');
471
+ if (result.resync) {
472
+ const changed = result.resync.docs.filter((d) => !d.unchanged).length;
473
+ console.log(`Contract re-sync: ${verb} sync ${result.resync.docs.length} protocol ` +
474
+ `doc(s) (${changed} changed) + bump ${result.resync.versionPath}.`);
475
+ }
476
+ console.log(`Folders: ${verb} move ${result.folderMoves.length} folder(s).`);
477
+ for (const m of result.folderMoves) {
478
+ console.log(` ${m.from} -> ${m.to}`);
479
+ }
480
+ console.log(`Item content: ${verb} rewrite ${result.contentRewrites.length} item(s).`);
481
+ console.log(`Config: ${verb} rename ${result.configRewrites.length} key(s)` +
482
+ (result.configRewrites.length > 0
483
+ ? ` (${result.configRewrites.map((c) => `${c.from}->${c.to}`).join(', ')})`
484
+ : '') +
485
+ '.');
486
+ console.log(`Refs: ${verb} rename ${result.refRenames.length} inert ref(s).`);
487
+ for (const r of result.refRenames) {
488
+ console.log(` ${r.from} -> ${r.to}`);
489
+ }
490
+ if (result.dryRun) {
491
+ console.log('(Re-run without --dry-run to apply; the leak scan gates the output.)');
492
+ return;
493
+ }
494
+ if (result.leaks.length === 0) {
495
+ console.log('Leak scan: GREEN (no surviving prd data ref).');
496
+ }
497
+ else {
498
+ console.error(`Leak scan: FAILED (${result.leaks.length} leak(s)):`);
499
+ for (const leak of result.leaks) {
500
+ printLeak(leak);
501
+ }
502
+ }
503
+ }
504
+ function printLeak(leak) {
505
+ console.error(` [${leak.lens}] ${leak.where}: '${leak.token}' — ${leak.why}`);
506
+ }
507
+ /**
508
+ * Resolve a task-only command's slug argument through the §3a namespace guard
509
+ * (`resolveTaskOnlyArg`): accept bare (= task) + `task:` (explicit alias),
510
+ * REJECT `spec:` with a clear "operates on tasks, not specs" error (and the
511
+ * legacy `prd:` with "operates on tasks, not prds", still accepted through the
512
+ * cutover). On rejection it prints the error to stderr and exits 1 (the task-only
513
+ * commands never act on a spec). An OMITTED slug (`start`/`complete`/`prompt`
514
+ * infer it from the branch) passes through untouched.
515
+ *
516
+ * `do` is the ONE command that spans both namespaces; it consumes the full
517
+ * `resolveSlug` (with the cross-namespace collision check) in the `do-in-place`
518
+ * task. This guard is the task-only half of ADR §3a.
519
+ */
520
+ function resolveTaskOnlySlug(slug) {
521
+ if (slug === undefined) {
522
+ return undefined;
523
+ }
524
+ try {
525
+ return resolveTaskOnlyArg(slug);
526
+ }
527
+ catch (err) {
528
+ if (err instanceof SlugResolutionError) {
529
+ console.error(`error: ${err.message}`);
530
+ process.exit(1);
531
+ }
532
+ throw err;
533
+ }
534
+ }
535
+ /**
536
+ * Build the INTERACTIVE launcher closure for `--agent` (task
537
+ * `agent-interactive-launch`), or `undefined` when `--agent` was not passed.
538
+ *
539
+ * It resolves the harness + model the SAME way the autonomous `do`/`run` path
540
+ * does — per-repo config layered flag > env > per-repo > global > default (ADR
541
+ * §13) — so the human starts pinned to the intended model (decision #4). The
542
+ * returned closure is what `start.ts`/`work-on.ts` call AFTER onboarding: it
543
+ * generates the pi `--session` path for the onboarded working tree and calls
544
+ * `harness.launchInteractive` (which inherits stdio, drops `--print`, feeds no
545
+ * prompt, foreground). A NON-pi harness throws a clear pi-only error from the
546
+ * adapter (decision #2). This keeps the git-logic modules decoupled from
547
+ * `createHarness`/config (they only receive the thin {@link InteractiveLauncher}).
548
+ *
549
+ * `repoPath` is the per-repo config root (the current checkout for `start` /
550
+ * in-repo `work-on`); `undefined` (remote `work-on`, no checkout) resolves from
551
+ * the global config only — mirroring `do --remote`.
552
+ */
553
+ function buildInteractiveLauncher(flags, configPath, repoPath) {
554
+ if (flags.agent !== true) {
555
+ return undefined;
556
+ }
557
+ const { global, override } = loadGlobalAndOverride(configPath);
558
+ const overrides = harnessFlagOverrides(flags);
559
+ const config = repoPath !== undefined
560
+ ? resolveRepoConfig({ repoPath, global, flags: overrides, override }).config
561
+ : resolveGlobalConfig(global, overrides);
562
+ const harness = createHarness({
563
+ harness: config.harness,
564
+ piBin: config.piBin,
565
+ });
566
+ return (site) => {
567
+ // Generate the pi `--session` path for the onboarded working tree so the
568
+ // human session is recorded + dashboard-visible (decision #2); the resolved
569
+ // model flows in (decision #4). The harness's `launchInteractive` runs pi
570
+ // WITHOUT `--print`, inherited stdio, no piped prompt, in `site.dir`.
571
+ const session = generateSessionPath({
572
+ sessionsDir: config.sessionsDir,
573
+ cwd: site.dir,
574
+ id: site.slug,
575
+ });
576
+ harness.launchInteractive({
577
+ slug: site.slug,
578
+ dir: site.dir,
579
+ model: config.model,
580
+ session,
581
+ env: site.env,
582
+ });
583
+ };
584
+ }
585
+ /**
586
+ * The shared `start`/`resume` action body. `start` and `resume` are the two
587
+ * human in-place verbs of ADR §4: `start` BEGINS work here (claim if needed +
588
+ * switch); `resume` CONTINUES here (re-engage an already-in-progress item by
589
+ * switching to its `work/<slug>` branch WITHOUT claiming). The runtime
590
+ * difference is exactly the `resume` flag — `resume` forces it on (its only mode
591
+ * is to re-engage), while `start` honours the (now hidden) `--resume` alias.
592
+ * Both are task-only (§3a: accept bare + `task:`, reject `prd:`).
593
+ */
594
+ async function runStartAction(rawSlug, flags, resume) {
595
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
596
+ const slug = resolveTaskOnlySlug(rawSlug);
597
+ const cwd = process.cwd();
598
+ // `resume --isolated <slug>`: re-engage the slug's RETAINED job worktree (the
599
+ // inverse of `do --isolated`) WITHOUT claiming \u2014 locate it off THIS repo's
600
+ // arbiter and report its path so the operator can cd in. The symmetric
601
+ // companion of `complete --isolated` (finish the stranded worktree). `start`
602
+ // (begin-here) has no isolated form \u2014 there is nothing retained to re-engage yet.
603
+ if (resume && flags.isolated === true) {
604
+ if (slug === undefined || slug === '') {
605
+ console.error('error: resume --isolated requires <slug> (the retained worktree to re-engage).');
606
+ process.exit(1);
607
+ }
608
+ const { config } = loadHumanWorktreesDir(flags.config ?? defaultConfigPath());
609
+ const located = locateIsolatedRecovery({
610
+ slug,
611
+ cwd,
612
+ arbiter: flags.arbiter ?? config.defaultArbiter,
613
+ workspacesDir: flags.workspace ?? config.workspacesDir,
614
+ env: process.env,
615
+ });
616
+ if ('error' in located) {
617
+ console.error(`error: ${located.error}`);
618
+ process.exit(1);
619
+ }
620
+ if (!located.present) {
621
+ console.error(`>> No retained isolated worktree for '${slug}' (already integrated and ` +
622
+ 'reaped, or never stranded) \u2014 nothing to resume.');
623
+ process.exit(0);
624
+ }
625
+ console.error(`>> Re-engaging the retained worktree for '${slug}'. cd into it to ` +
626
+ `continue, then 'dorfl complete --isolated ${slug}' to finish:`);
627
+ process.stdout.write(`${located.dir}\n`);
628
+ process.exit(0);
629
+ }
630
+ const result = await performStart({
631
+ slug,
632
+ cwd,
633
+ arbiter: flags.arbiter ?? 'origin',
634
+ // `resume` (the verb) always asserts ownership; `start` honours --resume.
635
+ resume: resume || flags.resume === true,
636
+ override: flags.ignoreNotReady === true,
637
+ // `--agent`: launch the configured harness INTERACTIVELY in the checkout
638
+ // after onboarding (task `agent-interactive-launch`). The per-repo config
639
+ // root is the current checkout.
640
+ launchInteractive: buildInteractiveLauncher(flags, flags.config, cwd),
641
+ // HUMAN commands (`start` = "begin here", `resume` = "continue here"): the
642
+ // onboard/branch/switch is the human's, so it is NOT given the runner
643
+ // `config.identity` (the autonomous onboard is `do`/`run`, identity-aware).
644
+ // Ambient `process.env` threaded EXPLICITLY so the choice is declared here,
645
+ // not left to the seam's silent `?? process.env` fallback.
646
+ env: process.env,
647
+ note: (message) => console.error(`>> ${message}`),
648
+ });
649
+ if (result.exitCode !== 0) {
650
+ console.error(`error: ${result.message}`);
651
+ }
652
+ process.exit(result.exitCode);
653
+ }
654
+ /**
655
+ * Help GROUP labels for the two-tier surface (ADR §7). commander v14's
656
+ * `command.helpGroup(...)` renders each command under its label heading, so the
657
+ * HEADLINE tier (the surface a user reaches for) lists first and the
658
+ * ADVANCED/PLUMBING tier (kept, but de-emphasised) lists under its own heading
659
+ * — without removing or hiding anything. Headline: run/do/work-on/start/resume/
660
+ * complete/requeue/scan/status + remote add/ls/find. Advanced: claim/prompt/
661
+ * verify/gc + remote rm.
662
+ */
663
+ const HEADLINE_GROUP = 'Commands:';
664
+ const ADVANCED_GROUP = 'Advanced / plumbing:';
665
+ /** Help group for the de-emphasised plumbing FLAGS named in ADR §7. */
666
+ const ADVANCED_OPT_GROUP = 'Advanced / plumbing options:';
667
+ export function buildProgram() {
668
+ const program = new Command();
669
+ program
670
+ .name(brand.bin)
671
+ .description('Autonomous parallel agents over file-based work/ queues.');
672
+ program
673
+ .command('scan')
674
+ .helpGroup(HEADLINE_GROUP)
675
+ .description('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).')
676
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
677
+ .option('--auto-build', 'allow agents to auto-build undeclared (not humanOnly) tasks')
678
+ .option('--no-auto-build', 'forbid agents from auto-building undeclared tasks (default)')
679
+ .option('--arbiter-remote <name>', `the current repo's arbiter remote to fetch + diff its local section against (default: ${DEFAULT_ARBITER_REMOTE})`)
680
+ .option('--arbiter <remote>', 'the COORDINATION arbiter remote whose per-item lock refs (refs/dorfl/lock/*) gate the cwd selection pool (held in-flight items are subtracted); default: origin (the same remote claim/do use, NOT the --arbiter-remote divergence name)')
681
+ .option('--here', 'report ONLY the current repo (the cwd working tree, fetch-first): skip the cross-repo registry loop entirely. The fast, focused path — no N-mirror fetches.')
682
+ .option('--json', 'output the raw report as JSON')
683
+ .action(async (flags, command) => {
684
+ const fileConfig = loadConfig(flags.config);
685
+ const override = loadConfigOverride(defaultConfigOverridePath(flags.config));
686
+ const config = resolveGlobalConfig(fileConfig, flagOverrides(flags, command));
687
+ const warn = (message) => console.error(`>> ${message}`);
688
+ const resolveCwd = () => resolveCwdSection({
689
+ cwd: process.cwd(),
690
+ config,
691
+ override,
692
+ arbiterRemote: flags.arbiterRemote,
693
+ lockArbiterRemote: flags.arbiter ?? 'origin',
694
+ warn,
695
+ });
696
+ // `--here`: report ONLY the cwd — skip the registry loop ENTIRELY (the fast,
697
+ // focused path, and the CI shape). The report carries an empty `repos[]` so
698
+ // the `--json` consumers (the CI matrix `jq`) read `.cwd.repo.*` exactly as
699
+ // before, with `.repos[]` simply yielding nothing.
700
+ if (flags.here === true) {
701
+ const cwdSection = await resolveCwd();
702
+ const emptyReport = { repos: [], totalItems: 0, totalEligible: 0 };
703
+ if (flags.json) {
704
+ console.log(JSON.stringify({ ...emptyReport, cwd: cwdSection }, (_key, value) => (value instanceof Set ? [...value] : value), 2));
705
+ }
706
+ else {
707
+ console.log(formatReport(emptyReport, cwdSection));
708
+ }
709
+ return;
710
+ }
711
+ const report = await scan(config, { warn, override });
712
+ // The cwd-local section: resolve it ONLY when a participating cwd is NOT
713
+ // already covered by a registered mirror. A FETCH-FREE pre-check
714
+ // (`cwdSectionDisposition`) decides this with zero network I/O; an
715
+ // already-registered cwd is skipped so we never re-fetch the SAME arbiter the
716
+ // registry loop just fetched (the `scan-here-and-skip-redundant-cwd`
717
+ // decision), and an UNregistered cwd is still shown standalone so a
718
+ // mirror-less repo you are standing in is never invisible.
719
+ const disposition = cwdSectionDisposition({
720
+ cwd: process.cwd(),
721
+ config,
722
+ arbiterRemote: flags.arbiterRemote,
723
+ });
724
+ const cwdSection = disposition.participating && !disposition.alsoRegistered
725
+ ? await resolveCwd()
726
+ : undefined;
727
+ if (flags.json) {
728
+ console.log(JSON.stringify({ ...report, cwd: cwdSection }, (_key, value) => (value instanceof Set ? [...value] : value), 2));
729
+ }
730
+ else {
731
+ console.log(formatReport(report, cwdSection));
732
+ }
733
+ });
734
+ program
735
+ .command('run')
736
+ .helpGroup(HEADLINE_GROUP)
737
+ .description('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`).')
738
+ .option('--once', 'run a SINGLE supervised tick then stop — the debug/test affordance on the daemon (NOT the CI path; CI uses `do`)')
739
+ .option('--advance', '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.')
740
+ .option('--max-iterations <n>', 'stop after N ticks (a bounded session; default: loop forever)')
741
+ .option('--max-duration <seconds>', 'stop after this many seconds of wall-clock (a bounded session; default: no bound)')
742
+ .option('--interval <seconds>', 'pause this many seconds between ticks (default: 0, back-to-back)')
743
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
744
+ .option('--auto-build', 'allow agents to auto-build undeclared (not humanOnly) tasks')
745
+ .option('--no-auto-build', 'forbid agents from auto-building undeclared tasks (default)')
746
+ .option('--max-parallel <n>', 'global cap on items claimed+run this tick')
747
+ .option('--per-repo-max <n>', 'per-repo cap on concurrent claims')
748
+ .option('--arbiter <remote>', 'name of the arbiter git remote')
749
+ .option('--integration <mode>', 'integration mode: propose (default) or merge')
750
+ .option('--no-pr', '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.')
751
+ .option('--agent-cmd <cmd>', 'command to run one agent on a task prompt')
752
+ .option('--model <id>', '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).')
753
+ .option('--harness <adapter>', 'harness adapter that launches the agent + reports liveness: null (default, shells out to agentCmd) or pi (the pi CLI)')
754
+ .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
755
+ .option('--sessions-dir <dir>', '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).')
756
+ .option('--workspace <dir>', 'execution working area for hub mirrors + job worktrees (default: workspacesDir / ~/.dorfl)')
757
+ .option('--review', '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.')
758
+ .option('--no-review', 'do NOT run Gate 2 this tick (overrides config)')
759
+ .option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
760
+ .option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
761
+ .option('--fresh-worktree-gate', '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.')
762
+ .option('--no-fresh-worktree-gate', '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')
763
+ .option('--merge-retries <n>', '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.')
764
+ .option('--json', 'output the raw result as JSON')
765
+ .action(async (flags, command) => {
766
+ const fileConfig = loadConfig(flags.config);
767
+ const override = loadConfigOverride(defaultConfigOverridePath(flags.config));
768
+ const config = resolveGlobalConfig(fileConfig, runFlagOverrides(flags, command));
769
+ // The null adapter shells out to agentCmd, so it is required there; the
770
+ // pi adapter invokes the pi CLI directly and does not consume agentCmd.
771
+ // Share the ONE predicate (doNeedsAgentCmd) with `do`/`--remote`.
772
+ if (doNeedsAgentCmd(config)) {
773
+ throw new Error(NO_AGENT_CMD_MESSAGE);
774
+ }
775
+ const workspace = flags.workspace ?? config.workspacesDir;
776
+ // Gate 2 (PR/code review): wire the PRODUCTION harness-backed gate ONLY when
777
+ // `config.review` resolves on (mirror the `do`/`complete` commands). The
778
+ // per-repo review flags are resolved per-item inside `runOneItem`; only the
779
+ // gate SEAM is threaded here. Off ⇒ undefined ⇒ no review (the default).
780
+ const reviewGate = config.review ? harnessReviewGate() : undefined;
781
+ const onWarn = (message) => console.error(`>> ${message}`);
782
+ // Plain `run` (no flag) NOW drives the REGISTRY-SET ADVANCE tick as its
783
+ // per-item unit (task `run-uses-advance-tick`), via the deliberate
784
+ // {@link RunTick} swap seam: the loop machinery (`runLoop`) is UNCHANGED, the
785
+ // tick it loops is the precursor's registry-set advance driver instead of the
786
+ // build-only `runOnce`. With BOTH lifecycle gates at their calm defaults
787
+ // (observationTriage off, surfaceBlockers off) the advance tick degrades to
788
+ // EXACTLY the old build tick's behaviour over the SAME substrate (registry-set
789
+ // discovery + per-mirror job-worktree isolation) — behaviour-preserving today;
790
+ // flip a gate and the SAME tick performs the lifecycle (triage/surface/apply).
791
+ // `run` ≡ CI: the same advance tick, a different cadence.
792
+ const advanceTick = buildRegistrySetAdvanceTick({
793
+ config,
794
+ workspace,
795
+ arbiter: flags.arbiter,
796
+ env: process.env,
797
+ override,
798
+ });
799
+ // `--advance` is now a DEPRECATED NO-OP ALIAS: plain `run` already IS advance,
800
+ // so there is no separate mode to opt into. Warn (but do not fail) so an
801
+ // existing `run --advance` invocation keeps working without surprise.
802
+ if (flags.advance) {
803
+ onWarn('`run --advance` is deprecated and ignored: plain `run` already runs the ' +
804
+ 'advance tick (build/task with calm-default gates; set observationTriage ' +
805
+ '/ surfaceBlockers for the lifecycle).');
806
+ }
807
+ const printTick = (result) => {
808
+ if (flags.json) {
809
+ console.log(JSON.stringify(result, null, 2));
810
+ return;
811
+ }
812
+ for (const item of result.items) {
813
+ console.log(formatItemLine(item));
814
+ }
815
+ console.log(`Summary: ${result.claimedAndDone} done, ${result.skipped} skipped, ${result.failed} failed.`);
816
+ };
817
+ // `run --once` = ONE debug tick (NOT the CI path; CI is `do`). The existing
818
+ // `runOnce` IS this tick.
819
+ if (flags.once) {
820
+ // The advance tick IS a RunTick, so `run --once` debug-ticks it (one
821
+ // registry-set advance batch) identically to how it looped.
822
+ const result = await advanceTick({
823
+ config,
824
+ workspace,
825
+ reviewGate,
826
+ onWarn,
827
+ });
828
+ printTick(result);
829
+ return;
830
+ }
831
+ // `run` (no flag) = the cross-repo, parallel, forever-looping DAEMON: loop
832
+ // the concurrent tick over the registry until a stop bound (--max-iterations
833
+ // / --max-duration) or a SIGINT/SIGTERM (graceful shutdown after the current
834
+ // tick). Stuck items surface via the existing needs-attention seam inside the
835
+ // tick — the loop never infinite-retries and adds no bespoke reporting.
836
+ let stopRequested = false;
837
+ const requestStop = () => {
838
+ if (!stopRequested) {
839
+ stopRequested = true;
840
+ console.error('>> stop requested — finishing the current tick, then exiting.');
841
+ }
842
+ };
843
+ process.on('SIGINT', requestStop);
844
+ process.on('SIGTERM', requestStop);
845
+ try {
846
+ const summary = await runLoop({
847
+ config,
848
+ workspace,
849
+ reviewGate,
850
+ onWarn,
851
+ // The swap seam: plain `run` ALWAYS drives the registry-set ADVANCE tick
852
+ // (build/task with calm-default gates; the lifecycle when a gate is on).
853
+ tick: advanceTick,
854
+ maxIterations: flags.maxIterations !== undefined
855
+ ? Number(flags.maxIterations)
856
+ : undefined,
857
+ maxDurationMs: flags.maxDuration !== undefined
858
+ ? Number(flags.maxDuration) * 1000
859
+ : undefined,
860
+ intervalMs: flags.interval !== undefined ? Number(flags.interval) * 1000 : 0,
861
+ stop: () => stopRequested,
862
+ onTick: (result, iteration) => {
863
+ if (!flags.json) {
864
+ console.error(`>> tick ${iteration}:`);
865
+ }
866
+ printTick(result);
867
+ },
868
+ });
869
+ if (!flags.json) {
870
+ console.log(`Loop ended (${summary.stoppedBy}) after ${summary.iterations} tick(s): ` +
871
+ `${summary.claimedAndDone} done, ${summary.skipped} skipped, ${summary.failed} failed.`);
872
+ }
873
+ else {
874
+ console.log(JSON.stringify(summary, null, 2));
875
+ }
876
+ }
877
+ finally {
878
+ process.off('SIGINT', requestStop);
879
+ process.off('SIGTERM', requestStop);
880
+ }
881
+ });
882
+ program
883
+ .command('verify')
884
+ .helpGroup(ADVANCED_GROUP)
885
+ .description("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/.")
886
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
887
+ .action(async (flags) => {
888
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
889
+ // DELIBERATELY verify-ONLY: the standalone `verify` command does NOT run the
890
+ // `prepare` env-prep step first. `verify` is the PURE acceptance gate (env-
891
+ // ready is a separate concern); a human invoking it prepares their own
892
+ // checkout. `prepare` runs only in the runner's fresh-worktree lifecycle
893
+ // (`do`/`run`/`complete` → `performIntegration`), where a fresh job worktree
894
+ // off the hub mirror genuinely needs deps before the gate can be trusted.
895
+ const result = await runVerify({
896
+ cwd: process.cwd(),
897
+ verify: config.verify,
898
+ });
899
+ process.exit(result.exitCode);
900
+ });
901
+ program
902
+ .command('claim')
903
+ .helpGroup(ADVANCED_GROUP)
904
+ .description('Atomically claim a work/backlog/<slug>.md item via a compare-and-swap push to the arbiter (in-process; mirrors scripts/claim.sh).')
905
+ .argument('<slug>', 'the slug of the backlog item to claim')
906
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
907
+ .option('--retries <n>', 'cap on push retries when main advances', '3')
908
+ .option('--dry-run', 'show the intended push without mutating the arbiter')
909
+ .option('--ignore-not-ready', 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)')
910
+ .action(async (rawSlug, flags) => {
911
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
912
+ const slug = resolveTaskOnlySlug(rawSlug);
913
+ // Wrap ONLY this CLI surface's `performClaim` call with the spinner
914
+ // helper (task `claim-cas-spinner`): the push can take seconds, so the
915
+ // terminal looked frozen. In non-TTY mode the helper is a no-op and
916
+ // stderr stays byte-identical to today (silent on success,
917
+ // `error: <message>` on failure, `>> <note>` lines unchanged). The
918
+ // autonomous `performClaim` call sites (`do`/`run`/`start`/`work-on`/
919
+ // `continue-branch`) are explicitly OUT OF SCOPE.
920
+ const spinner = createClaimSpinner({
921
+ stream: process.stderr,
922
+ isTTY: process.stdout.isTTY === true,
923
+ clock: {
924
+ setInterval: (fn, ms) => setInterval(fn, ms),
925
+ clearInterval: (handle) => clearInterval(handle),
926
+ },
927
+ label: `Claiming ${slug}\u2026`,
928
+ });
929
+ const onSigint = () => {
930
+ spinner.stop();
931
+ process.exit(130);
932
+ };
933
+ process.on('SIGINT', onSigint);
934
+ spinner.start();
935
+ let result;
936
+ try {
937
+ result = await performClaim({
938
+ slug,
939
+ cwd: process.cwd(),
940
+ arbiter: flags.arbiter ?? 'origin',
941
+ retries: flags.retries !== undefined ? Number(flags.retries) : undefined,
942
+ dryRun: flags.dryRun,
943
+ humanPath: true,
944
+ override: flags.ignoreNotReady === true,
945
+ // HUMAN command (the `humanPath: true` above already says so): the
946
+ // standalone `claim` CAS micro-commit + push is the human's, so it is
947
+ // NOT given the runner `config.identity`. The AUTONOMOUS claim is the one
948
+ // inside `do`/`run`/`intake` (identity-aware). Thread the ambient
949
+ // `process.env` EXPLICITLY so the human-identity choice is declared at the
950
+ // call site, not left to the seam's silent `?? process.env` fallback.
951
+ env: process.env,
952
+ note: (message) => spinner.note(message),
953
+ });
954
+ }
955
+ catch (err) {
956
+ // Unhandled error: tear the spinner down cleanly BEFORE the throw
957
+ // propagates so the cursor is restored + no orphaned ANSI state.
958
+ spinner.stop();
959
+ process.off('SIGINT', onSigint);
960
+ throw err;
961
+ }
962
+ spinner.finish(result);
963
+ process.off('SIGINT', onSigint);
964
+ process.exit(result.exitCode);
965
+ });
966
+ program
967
+ .command('start')
968
+ .helpGroup(HEADLINE_GROUP)
969
+ .description('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.')
970
+ .argument('[slug]', 'the slug to start (inferred from a work/<slug> branch if omitted)')
971
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
972
+ // `--resume` is now the HIDDEN alias of the `resume` verb (ADR §4/§7): the
973
+ // documented surface is `start` = begin here, `resume` = continue here. Kept
974
+ // (hidden) for muscle memory; addHelpText below points at the verb.
975
+ .addOption(new Option('--resume', '(hidden alias of the `resume` verb) assert ownership of an already in-progress item: switch to its work branch without claiming').hideHelp())
976
+ .option('--ignore-not-ready', 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)')
977
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
978
+ .option('--agent', '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`.')
979
+ .option('--harness <name>', 'harness adapter for --agent: pi (interactive launch requires pi). Resolved flag > env > per-repo > global > default.')
980
+ .option('--model <model>', 'model the interactive --agent session starts pinned to (routing intent; you may switch inside pi). Resolved flag > env > per-repo > global > default.')
981
+ .option('--pi-bin <path>', 'path to the pi CLI binary (for --agent)')
982
+ .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the --agent pi session file is generated')
983
+ .action((rawSlug, flags) => runStartAction(rawSlug, flags, false));
984
+ program
985
+ .command('resume')
986
+ .helpGroup(HEADLINE_GROUP)
987
+ .description('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.')
988
+ .argument('[slug]', 'the slug to resume (inferred from a work/<slug> branch if omitted)')
989
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
990
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
991
+ .option('--isolated', "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).")
992
+ .option('--workspace <dir>', 'execution working area for job worktrees (--isolated; default: workspacesDir / ~/.dorfl)')
993
+ .action((rawSlug, flags) => runStartAction(rawSlug, flags, true));
994
+ program
995
+ .command('work-on')
996
+ .helpGroup(HEADLINE_GROUP)
997
+ .description('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).')
998
+ .argument('<slug>', 'the slug to work on (bare = the task; the target repo is the current one, or --remote <r>)')
999
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1000
+ .option('--remote <r>', '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).')
1001
+ .option('--arbiter <remote>', 'name of the arbiter git remote in the current repo (in-repo form; default: origin)', 'origin')
1002
+ .addOption(new Option('--copy <patterns>', '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.').helpGroup(ADVANCED_OPT_GROUP))
1003
+ .addOption(new Option('--copy-from <path>', 'source dir for --copy in the remote form (required there; there is no implicit current repo)').helpGroup(ADVANCED_OPT_GROUP))
1004
+ .addOption(new Option('--print-dir', 'print ONLY the worktree path to stdout (for a shell wrapper: work-on(){ cd "$(dorfl work-on "$@" --print-dir)"; })').helpGroup(ADVANCED_OPT_GROUP))
1005
+ .option('--workspace <dir>', 'execution working area for hub mirrors (default: workspacesDir / ~/.dorfl)')
1006
+ .option('--ignore-not-ready', 'override the readiness guard: claim despite an unmet blockedBy, and silence the needsAnswers warning (loud, never default)')
1007
+ .option('--agent', '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`.')
1008
+ .option('--harness <name>', 'harness adapter for --agent: pi (interactive launch requires pi). Resolved flag > env > per-repo > global > default.')
1009
+ .option('--model <model>', 'model the interactive --agent session starts pinned to (routing intent; you may switch inside pi). Resolved flag > env > per-repo > global > default.')
1010
+ .option('--pi-bin <path>', 'path to the pi CLI binary (for --agent)')
1011
+ .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the --agent pi session file is generated')
1012
+ .action(async (rawSlug, flags) => {
1013
+ // The two forms are now distinguished by the `--remote` FLAG (ADR §4,
1014
+ // consistent with `do --remote`), not a positional <remote>: bare =
1015
+ // the current repo, `--remote <r>` = any registered repo.
1016
+ const remote = flags.remote !== undefined && flags.remote.trim() !== ''
1017
+ ? flags.remote
1018
+ : undefined;
1019
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1020
+ const theSlug = resolveTaskOnlySlug(rawSlug);
1021
+ const configPath = flags.config ?? defaultConfigPath();
1022
+ const { dir: configuredRoot, config } = loadHumanWorktreesDir(configPath);
1023
+ const workspace = flags.workspace ?? config.workspacesDir;
1024
+ // --print-dir wants a clean stdout, so all human-facing notes go to
1025
+ // stderr; the path is the ONLY thing on stdout (printed below).
1026
+ const printDir = flags.printDir === true;
1027
+ const result = await performWorkOn({
1028
+ slug: theSlug,
1029
+ remote,
1030
+ cwd: process.cwd(),
1031
+ arbiter: flags.arbiter ?? 'origin',
1032
+ copy: flags.copy,
1033
+ copyFrom: flags.copyFrom,
1034
+ override: flags.ignoreNotReady === true,
1035
+ workspacesDir: workspace,
1036
+ humanWorktreesDir: configuredRoot,
1037
+ promptForRoot: (suggestion) => promptForWorktreesRoot(suggestion),
1038
+ saveRoot: (chosen) => persistHumanWorktreesDir(chosen, configPath),
1039
+ // `--agent`: launch the configured harness INTERACTIVELY in the new
1040
+ // worktree after creation (task `agent-interactive-launch`). In-repo
1041
+ // mode resolves per-repo config from the current checkout; remote mode
1042
+ // (no checkout) resolves from the global config only (like `do --remote`).
1043
+ launchInteractive: buildInteractiveLauncher(flags, configPath, remote === undefined ? process.cwd() : undefined),
1044
+ // HUMAN command (the description says so): claim + worktree + branch is
1045
+ // the human's, NOT given the runner `config.identity`. Ambient
1046
+ // `process.env` threaded EXPLICITLY (not the seam's silent fallback).
1047
+ env: process.env,
1048
+ note: (message) => console.error(`>> ${message}`),
1049
+ });
1050
+ if (result.exitCode !== 0) {
1051
+ console.error(`error: ${result.message}`);
1052
+ process.exit(result.exitCode);
1053
+ }
1054
+ if (printDir) {
1055
+ // Path only on stdout, so `cd "$(... --print-dir)"` works.
1056
+ process.stdout.write(`${result.dir}\n`);
1057
+ }
1058
+ process.exit(0);
1059
+ });
1060
+ program
1061
+ .command('prompt')
1062
+ .helpGroup(ADVANCED_GROUP)
1063
+ .description("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.")
1064
+ .argument('[slug]', 'the slug to render (inferred from a work/<slug> branch if omitted)')
1065
+ .action((rawSlug) => {
1066
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1067
+ const slug = resolveTaskOnlySlug(rawSlug);
1068
+ // Resolve the `promptGuidance` NUDGE namespace through the SAME chain the
1069
+ // gate family uses (env > per-repo > global > default), so e.g. a
1070
+ // `promptGuidance.testFirst:true` in `.dorfl.json` strengthens the
1071
+ // wrapper line for `dorfl prompt` exactly as it would in `do`/`run`.
1072
+ const cwd = process.cwd();
1073
+ const global = loadConfig();
1074
+ const resolved = resolveRepoConfig({ repoPath: cwd, global }).config;
1075
+ const output = renderPrompt({
1076
+ slug,
1077
+ cwd,
1078
+ promptGuidance: resolvePromptGuidance(resolved),
1079
+ });
1080
+ process.stdout.write(output);
1081
+ });
1082
+ program
1083
+ .command('complete')
1084
+ .helpGroup(HEADLINE_GROUP)
1085
+ .description('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.')
1086
+ .argument('[slug]', 'the slug to complete (inferred from a work/<slug> branch if omitted)')
1087
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1088
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
1089
+ .option('--merge', 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)')
1090
+ .option('--propose', 'integrate in propose mode this invocation (mutually exclusive with --merge; overrides config)')
1091
+ .option('--no-pr', '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.')
1092
+ .option('--no-switch', 'stay on the work/<slug> branch (and keep it) instead of switching back to main')
1093
+ .option('--ignore-diverged-main', '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.')
1094
+ .option('--isolated', "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.")
1095
+ .option('--workspace <dir>', 'execution working area for job worktrees (--isolated; default: workspacesDir / ~/.dorfl)')
1096
+ .addOption(new Option('--skip-verify', 'skip the acceptance gate (human-only escape hatch; the runner never skips)').helpGroup(ADVANCED_OPT_GROUP))
1097
+ .addOption(new Option('--type <type>', 'conventional-commit type for the commit')
1098
+ .default('feat')
1099
+ .helpGroup(ADVANCED_OPT_GROUP))
1100
+ .addOption(new Option('--message <summary>', 'commit summary (default: the task title, minus a leading "slug \u2014 " prefix)').helpGroup(ADVANCED_OPT_GROUP))
1101
+ .option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > per-repo > global > default off.')
1102
+ .option('--no-review', 'do NOT run Gate 2 this invocation (overrides config)')
1103
+ .option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
1104
+ .option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
1105
+ .option('--fresh-worktree-gate', '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.')
1106
+ .option('--no-fresh-worktree-gate', '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')
1107
+ .option('--merge-retries <n>', 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.')
1108
+ .action(async (rawSlug, flags) => {
1109
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
1110
+ const slug = resolveTaskOnlySlug(rawSlug);
1111
+ const cwd = process.cwd();
1112
+ const { global, override } = loadGlobalAndOverride(flags.config);
1113
+ // `--isolated`: FINISH a stranded isolated worktree (the recover-already-
1114
+ // committed path) instead of completing the current checkout. It LOCATES the
1115
+ // slug's retained job worktree off THIS repo's arbiter and runs ONLY the
1116
+ // rebase\u2192integrate tail from the kept commit. The slug is REQUIRED (there is no
1117
+ // branch to infer it from in the operator's checkout).
1118
+ if (flags.isolated === true) {
1119
+ if (slug === undefined || slug === '') {
1120
+ console.error('error: complete --isolated requires <slug> (the stranded item to finish).');
1121
+ process.exit(1);
1122
+ }
1123
+ const flagMode = integrationFromFlags(flags);
1124
+ const resolved = resolveRepoConfig({
1125
+ repoPath: cwd,
1126
+ global,
1127
+ override,
1128
+ flags: {
1129
+ ...(flagMode ? { integration: flagMode } : {}),
1130
+ ...noPRFlagOverrides(flags),
1131
+ },
1132
+ });
1133
+ if (resolved.message) {
1134
+ console.error(`>> ${resolved.message}`);
1135
+ }
1136
+ const isoConfig = resolved.config;
1137
+ const recovered = await performRecoverIsolated({
1138
+ slug,
1139
+ cwd,
1140
+ arbiter: flags.arbiter ?? isoConfig.defaultArbiter,
1141
+ workspacesDir: flags.workspace ?? isoConfig.workspacesDir,
1142
+ integration: isoConfig.integration,
1143
+ noPR: isoConfig.noPR,
1144
+ note: (message) => console.error(`>> ${message}`),
1145
+ env: process.env,
1146
+ });
1147
+ if (recovered.exitCode !== 0) {
1148
+ console.error(`error: ${recovered.message}`);
1149
+ }
1150
+ process.exit(recovered.exitCode);
1151
+ }
1152
+ // Resolve the integration mode at completion time, highest first:
1153
+ // --merge/--propose flag > per-repo .dorfl.json > global > default.
1154
+ // The flag sits at the TOP of the same chain the autonomous runner uses
1155
+ // (per-repo > global > default), so human and autonomous paths agree.
1156
+ const flagMode = integrationFromFlags(flags);
1157
+ const resolved = resolveRepoConfig({
1158
+ repoPath: cwd,
1159
+ global,
1160
+ override,
1161
+ // The integrate-time mode AND the Gate-2 review flags ride the SAME
1162
+ // flag > env > per-repo > global > default chain.
1163
+ flags: {
1164
+ ...(flagMode ? { integration: flagMode } : {}),
1165
+ ...reviewFlagOverrides(flags),
1166
+ // `--fresh-worktree-gate`/`--no-fresh-worktree-gate` rides the SAME chain.
1167
+ ...freshWorktreeGateFlagOverrides(flags),
1168
+ // `--merge-retries <n>` rides the SAME chain: the cross-job merge-serialiser
1169
+ // CAS-retry cap (prd `land-time-reverify-and-parallel-merge-ceiling` Story 5
1170
+ // / Applied Answer q1 (a)).
1171
+ ...mergeRetriesFlagOverrides(flags),
1172
+ // `--no-pr` (the PR-INTENT axis) rides the SAME chain.
1173
+ ...noPRFlagOverrides(flags),
1174
+ },
1175
+ });
1176
+ if (resolved.message) {
1177
+ console.error(`>> ${resolved.message}`);
1178
+ }
1179
+ const config = resolved.config;
1180
+ const result = await performComplete({
1181
+ slug,
1182
+ cwd,
1183
+ arbiter: flags.arbiter ?? config.defaultArbiter,
1184
+ integration: config.integration,
1185
+ // An EXPLICIT `--merge` overrides the untrusted-origin build-propose rule (task
1186
+ // `untrusted-origin-forces-build-propose`): `flagMode` is the typed flag
1187
+ // (undefined when none), so this is true ONLY when the operator typed
1188
+ // `--merge`, never when `merge` was resolved from config.
1189
+ explicitMerge: flagMode === 'merge',
1190
+ noPR: config.noPR,
1191
+ noSwitch: flags.switch === false,
1192
+ ignoreDivergedMain: flags.ignoreDivergedMain === true,
1193
+ prepare: config.prepare,
1194
+ verify: config.verify,
1195
+ skipVerify: flags.skipVerify,
1196
+ // Gate 2 (PR/code review): when `review` resolves on, run the `review`
1197
+ // SKILL as a fresh-context agent (the production harness-backed gate)
1198
+ // AFTER the green verify and BEFORE the done-move. The `reviewModel`
1199
+ // override flows to the launch through the existing harness seam.
1200
+ review: config.review,
1201
+ reviewModel: config.reviewModel,
1202
+ reviewMaxRounds: config.reviewMaxRounds,
1203
+ reviewGate: config.review ? harnessReviewGate() : undefined,
1204
+ // Run the acceptance gate against the REBASED tip in a clean throwaway
1205
+ // worktree (the tree that integrates) when ON (the default). `complete` is
1206
+ // a single-job path, so the resolved flag is passed UNCONDITIONALLY (no
1207
+ // fleet downgrade).
1208
+ freshWorktreeGate: config.freshWorktreeGate,
1209
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
1210
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — the resolved
1211
+ // per-repo value reaches the merge loop via `performComplete`→
1212
+ // `performIntegration`.
1213
+ mergeRetries: config.mergeRetries,
1214
+ type: flags.type,
1215
+ message: flags.message,
1216
+ // Color the propose-mode next-step block only on an interactive
1217
+ // stdout TTY (and not under NO_COLOR); plain when piped/redirected.
1218
+ color: shouldUseColor(process.stdout),
1219
+ note: (message) => console.error(`>> ${message}`),
1220
+ // The propose next-step block is printed verbatim (no `>> ` prefix)
1221
+ // so its blank lines + heading stand out as the human call-to-action.
1222
+ noteBlock: (message) => console.error(message),
1223
+ // `complete` is a HUMAN command: a human finishing/merging the work, so
1224
+ // the commit/push/PR is THEIRS — it is deliberately NOT given the runner
1225
+ // `config.identity` (the autonomous completion is `do`'s own integrated
1226
+ // complete, which IS identity-aware). Thread the ambient `process.env`
1227
+ // EXPLICITLY so the human-identity choice is declared at the call site,
1228
+ // not left to the seam's silent `?? process.env` fallback (parity with
1229
+ // `requeue`).
1230
+ env: process.env,
1231
+ });
1232
+ if (result.exitCode !== 0) {
1233
+ console.error(`error: ${result.message}`);
1234
+ }
1235
+ process.exit(result.exitCode);
1236
+ });
1237
+ program
1238
+ .command('do')
1239
+ .helpGroup(HEADLINE_GROUP)
1240
+ .description('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.')
1241
+ // EXTENSIBLE argument grammar (the three do-* tasks grow this one block):
1242
+ // `do-autopick` widens the single optional positional into a VARIADIC one so
1243
+ // `do` (zero args = auto-pick), `do <a> <b> …` (named, in sequence), and
1244
+ // `do <slug>` (exactly one) all share the one command. `-n <x>` is the count
1245
+ // for the auto-pick form. `do` stays SEQUENTIAL (parallelism is `run`).
1246
+ .argument('[slugs...]', '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.')
1247
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1248
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
1249
+ .option('-n, --number <x>', '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.')
1250
+ .option('--selection-order <order>', '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.')
1251
+ .option('--remote <r>', '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)')
1252
+ .option('--isolated', "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).")
1253
+ .option('--merge', 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)')
1254
+ .option('--propose', 'integrate in propose mode this invocation (default; mutually exclusive with --merge; overrides config)')
1255
+ .option('--tasks-land-in <where>', '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.')
1256
+ .option('--no-pr', '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.')
1257
+ .option('--ignore-diverged-main', '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.')
1258
+ .option('--allow-backlog', '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.')
1259
+ .option('--agent-cmd <cmd>', 'command to run the agent on the task prompt')
1260
+ .option('--model <id>', 'model the agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
1261
+ .option('--harness <adapter>', 'harness adapter that launches the agent: null (default, shells out to agentCmd) or pi (the pi CLI)')
1262
+ .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
1263
+ .option('--sessions-dir <dir>', '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).')
1264
+ .option('--watch', "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)")
1265
+ .option('--review', 'run Gate 2 (PR/code review) after verify, before the done-move (overrides config). Resolved flag > env > per-repo > global > default off.')
1266
+ .option('--no-review', 'do NOT run Gate 2 this invocation (overrides config)')
1267
+ .option('--review-model <id>', 'model the Gate-2 review agent runs on (de-correlated from the builder; routing intent). Resolved flag > env > per-repo > global > default.')
1268
+ .option('--review-max-rounds <n>', 'bound the revise/review loop; on exhaustion force needs-attention (default 2)')
1269
+ .option('--tasker-loop', '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).')
1270
+ .option('--no-tasker-loop', 'skip the tasker improver loop on `do prd:<slug>`')
1271
+ .option('--tasker-loop-max <n>', '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)')
1272
+ .option('--tasker-loop-model <id>', '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.')
1273
+ .option('--fresh-worktree-gate', 'run the acceptance gate (prepare then verify) against the REBASED tip in a CLEAN throwaway worktree (the tree that actually integrates), so a green gate provably describes the merged artifact. ON by default. Resolved flag > env > per-repo > global > default on.')
1274
+ .option('--no-fresh-worktree-gate', "run the acceptance gate in the agent's build worktree (the pre-rebase tree) as before — the opt-out for when the per-gate install cost is too high")
1275
+ .option('--merge-retries <n>', 'cross-job merge-serialiser CAS-retry cap (see `run --help`); resolved flag > env > per-repo > global > default 1000.')
1276
+ .action(async (rawSlugs, flags) => {
1277
+ // Variadic grammar (`do-autopick`): zero args = AUTO-PICK; one = the single
1278
+ // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count.
1279
+ const args = rawSlugs ?? [];
1280
+ // `-n <x>` parse + validation. It is the AUTO-PICK count (sequential), so it
1281
+ // is mutually exclusive with NAMING items (you either auto-pick a count or
1282
+ // name the items, not both).
1283
+ let count;
1284
+ if (flags.number !== undefined) {
1285
+ const n = Number(flags.number);
1286
+ if (flags.number.trim() === '' || !Number.isInteger(n) || n < 1) {
1287
+ console.error(`error: -n/--number must be a positive integer (got '${flags.number}').`);
1288
+ process.exit(1);
1289
+ }
1290
+ if (args.length > 0) {
1291
+ console.error('error: -n/--number auto-picks a COUNT of eligible items; do not also ' +
1292
+ 'name items. Use `do -n <x>` OR `do <a> <b> ...`, not both.');
1293
+ process.exit(1);
1294
+ }
1295
+ count = n;
1296
+ }
1297
+ const cwd = process.cwd();
1298
+ const { global, override } = loadGlobalAndOverride(flags.config);
1299
+ // Resolve the integration mode at integrate-time, highest first:
1300
+ // --merge/--propose flag > per-repo .dorfl.json > global > default.
1301
+ // (Same chain `complete` uses — `do` is the autonomous twin.)
1302
+ let flagMode;
1303
+ try {
1304
+ flagMode = integrationFromFlags(flags);
1305
+ }
1306
+ catch (err) {
1307
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
1308
+ process.exit(1);
1309
+ }
1310
+ // `do --remote <r>` / `do --isolated <slug>`: run the NO-CHECKOUT job-worktree
1311
+ // pipeline. Both materialise a hub mirror + job worktree in the agents' area
1312
+ // (`workspacesDir`) and reap per ADR §4 — the human area is NEVER touched.
1313
+ //
1314
+ // `--remote <r>` names the TARGETING axis (a FOREIGN repo, no checkout); the
1315
+ // arbiter spec is the `<r>` URL. `--isolated` names the ISOLATION intent (a
1316
+ // worktree off MY OWN arbiter, even though I am inside the repo); its arbiter
1317
+ // URL is RESOLVED FROM THE CWD's arbiter remote (the same `--arbiter` >
1318
+ // per-repo/global `defaultArbiter` name in-place `do` uses). The two are
1319
+ // ORTHOGONAL: `--isolated` + `--remote` is REDUNDANT (a foreign `--remote` is
1320
+ // already isolated), so we accept it and `--remote` WINS (see `## Decisions`).
1321
+ //
1322
+ // In BOTH cases the repo's COMMITTED `.dorfl.json` is reachable on
1323
+ // `<arbiter>/main` (the mirror), so we layer it — `flag > env > per-repo >
1324
+ // global > default` parity with in-place `do` (task
1325
+ // `remote-do-reads-per-repo-config-from-arbiter-main`). Only the whitelisted
1326
+ // `REPO_ALLOWED_KEYS` are layered (host-only keys stay global/flag/env-only,
1327
+ // rejected by the SAME `repo-config.ts` split).
1328
+ const isolatedNoRemote = flags.isolated === true && flags.remote === undefined;
1329
+ if (flags.remote !== undefined || isolatedNoRemote) {
1330
+ // The form's user-facing name + canonical usage, for the shared error
1331
+ // messages below (so `--isolated` errors read in its own terms).
1332
+ const form = isolatedNoRemote ? '--isolated' : '--remote';
1333
+ const usage = isolatedNoRemote
1334
+ ? '`do --isolated <slug>`'
1335
+ : '`do --remote <r> <slug>`';
1336
+ // The no-checkout forms now support the SAME variadic grammar the in-place
1337
+ // form does: a single NAMED item, MULTIPLE named items (sequential), and
1338
+ // AUTO-PICK / `-n <x>` (sequential) over the MIRROR-SIDE eligible-pool scan
1339
+ // (`mirror-side-eligible-pool-scan`). The old inline `-n`×`--remote` REFUSAL
1340
+ // is GONE — the mirror scan backs it now (US #25); `-n` stays ALWAYS
1341
+ // SEQUENTIAL (parallelism is `run` / the CI matrix). `-n` is still mutually
1342
+ // exclusive with naming items (validated above, shared with the in-place form).
1343
+ const remoteFlags = doFlagOverrides(flags, flagMode);
1344
+ // Resolve the arbiter spec the rest of the pipeline consumes as `remote`.
1345
+ // `--remote` supplies it directly (a foreign URL). `--isolated` resolves it
1346
+ // from the CWD's arbiter remote (`git remote get-url`); no resolvable
1347
+ // arbiter ⇒ a CLEAR error naming `--remote <url>` as the foreign-repo
1348
+ // alternative — NOT a confusing URL-parse failure downstream.
1349
+ let effectiveRemote;
1350
+ if (isolatedNoRemote) {
1351
+ const bootstrapIdentity = resolveGlobalConfig(global, remoteFlags).identity;
1352
+ const arbiterName = flags.arbiter ??
1353
+ resolveDefaultArbiterForCwd(cwd, global, remoteFlags, override);
1354
+ const resolvedUrl = resolveArbiterUrlFromCheckout(cwd, arbiterName, identityEnv(bootstrapIdentity, process.env));
1355
+ if (resolvedUrl === undefined) {
1356
+ console.error(`error: --isolated builds in a worktree off this repo's arbiter ` +
1357
+ `('${arbiterName}'), but no such arbiter remote is configured/found ` +
1358
+ `here. Run inside a participating repo (a clone with an arbiter ` +
1359
+ `remote), or use --remote <url> to target another repo.`);
1360
+ process.exit(1);
1361
+ }
1362
+ effectiveRemote = resolvedUrl;
1363
+ }
1364
+ else {
1365
+ effectiveRemote = flags.remote;
1366
+ }
1367
+ // BOOTSTRAP resolution (global + flags, no per-repo layer) — it supplies
1368
+ // the HOST-ONLY keys needed to even reach the arbiter's committed file:
1369
+ // `workspacesDir` (where the mirror lives) and `identity` (the git env the
1370
+ // mirror fetch runs under). These are host-only by definition (rejected
1371
+ // per-repo), so reading them from global+flags first is correct and stable.
1372
+ const bootstrap = resolveGlobalConfig(global, remoteFlags);
1373
+ // Source the committed `.dorfl.json` from `<arbiter>/main` via the
1374
+ // hub mirror, then layer ONLY its whitelisted keys through the EXISTING
1375
+ // per-repo machinery. The read refreshes ONLY `main` (no-prune), so a
1376
+ // `work/<slug>` branch checked out in a stale worktree can never block it,
1377
+ // and the build's later all-heads materialisation fetch is unaffected. A
1378
+ // config-less repo (no file on
1379
+ // main, or an unreachable mirror) → exactly the bootstrap config, i.e.
1380
+ // byte-identical to the pre-task global+default behaviour.
1381
+ const remoteConfig = resolveRemoteRepoConfig({
1382
+ remote: effectiveRemote,
1383
+ workspacesDir: bootstrap.workspacesDir,
1384
+ global,
1385
+ flags: remoteFlags,
1386
+ identity: bootstrap.identity,
1387
+ note: (message) => console.error(`>> ${message}`),
1388
+ override,
1389
+ });
1390
+ if (doNeedsAgentCmd(remoteConfig)) {
1391
+ console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
1392
+ process.exit(1);
1393
+ }
1394
+ const remoteHarness = createHarness({
1395
+ harness: remoteConfig.harness,
1396
+ piBin: remoteConfig.piBin,
1397
+ });
1398
+ // The per-item `DoRemoteOptions` (everything BUT `arg`) — built ONCE and
1399
+ // reused for the single-item path AND threaded by the mirror-side auto-pick
1400
+ // driver (`performDoRemoteAuto`) to each sequential `performDoRemote`.
1401
+ const baseRemoteOptions = {
1402
+ remote: effectiveRemote,
1403
+ workspacesDir: remoteConfig.workspacesDir,
1404
+ arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
1405
+ // Host-only runner IDENTITY — scopes git/provider ops only (not the
1406
+ // agent launch); absent ⇒ ambient.
1407
+ identity: remoteConfig.identity,
1408
+ // `do --remote prd:<slug>` tasking-gate policy (task-build path ignores it).
1409
+ autoTask: remoteConfig.autoTask,
1410
+ // The resolved `promptGuidance` nudge — threaded into the remote worker
1411
+ // prompt (runRemotePipeline → buildAgentPrompt), mirroring in-place `do`.
1412
+ promptGuidance: resolvePromptGuidance(remoteConfig),
1413
+ integration: remoteConfig.integration,
1414
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule.
1415
+ explicitMerge: flagMode === 'merge',
1416
+ // Per-TRANSITION TASKING override (the `do --remote prd:` tasking path).
1417
+ taskingIntegration: remoteConfig.taskingIntegration,
1418
+ // TASK-PLACEMENT: the configured default + the EXPLICIT operator override
1419
+ // (`--tasks-land-in`), the top of the placement precedence — mirrors
1420
+ // `explicitMerge` (set only when the flag was typed).
1421
+ tasksLandIn: remoteConfig.tasksLandIn,
1422
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
1423
+ prepare: remoteConfig.prepare,
1424
+ verify: remoteConfig.verify,
1425
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
1426
+ freshWorktreeGate: remoteConfig.freshWorktreeGate,
1427
+ // Cross-job merge-serialiser CAS-retry cap (resolved through the per-repo
1428
+ // chain on the arbiter-side `.dorfl.json` too) — prd
1429
+ // `land-time-reverify-and-parallel-merge-ceiling` Story 5.
1430
+ mergeRetries: remoteConfig.mergeRetries,
1431
+ noPR: remoteConfig.noPR,
1432
+ harness: remoteHarness,
1433
+ agentCmd: remoteConfig.agentCmd,
1434
+ model: remoteConfig.model,
1435
+ sessionsDir: remoteConfig.sessionsDir,
1436
+ review: remoteConfig.review,
1437
+ reviewModel: remoteConfig.reviewModel,
1438
+ reviewMaxRounds: remoteConfig.reviewMaxRounds,
1439
+ reviewGate: remoteConfig.review
1440
+ ? harnessReviewGate({
1441
+ harness: remoteHarness,
1442
+ agentCmd: remoteConfig.agentCmd,
1443
+ })
1444
+ : undefined,
1445
+ // The tasker IMPROVER loop on the `do --remote prd:` path is ON by default
1446
+ // (auto-tasking has no `verify` floor, so the loop is the task path's
1447
+ // quality engine). `--tasker-loop`/`--no-tasker-loop` gates wiring the seam;
1448
+ // `taskerLoopMax`/`taskerLoopModel` resolve per-repo (flag > env > per-repo
1449
+ // > global > default). DISTINCT from the gate's `--review*` family.
1450
+ reviewLoop: remoteConfig.taskerLoop
1451
+ ? harnessTaskReviewGate({
1452
+ harness: remoteHarness,
1453
+ agentCmd: remoteConfig.agentCmd,
1454
+ })
1455
+ : undefined,
1456
+ taskerLoopMax: remoteConfig.taskerLoopMax,
1457
+ taskerLoopModel: remoteConfig.taskerLoopModel,
1458
+ // The task-SET ACCEPTANCE GATE on the `do --remote prd:` path too.
1459
+ taskReviewGate: remoteConfig.review
1460
+ ? harnessTaskAcceptanceGate({
1461
+ harness: remoteHarness,
1462
+ agentCmd: remoteConfig.agentCmd,
1463
+ })
1464
+ : undefined,
1465
+ watch: flags.watch === true,
1466
+ color: shouldUseColor(process.stdout),
1467
+ note: (message) => console.error(`>> ${message}`),
1468
+ noteBlock: (message) => console.error(message),
1469
+ };
1470
+ // DISPATCH the variadic grammar (the NO-CHECKOUT forms):
1471
+ // zero args -> AUTO-PICK `count` (default 1) over the MIRROR-SIDE
1472
+ // eligible-pool scan, run SEQUENTIALLY.
1473
+ // one named arg -> the single-item remote pipeline (unchanged).
1474
+ // many named args -> those, IN SEQUENCE (operator's order; no pool).
1475
+ // `--watch` tails ONE session, so it only fits the single-named-item form;
1476
+ // the auto/`-n`/multi forms run many ticks and do not stream a single log.
1477
+ const remoteMulti = args.length === 0 || count !== undefined || args.length > 1;
1478
+ if (remoteMulti && flags.watch === true) {
1479
+ console.error(`error: --watch streams ONE session; it does not combine with the ` +
1480
+ `${form} auto-pick / -n / multi-item forms. Name a single item: ${usage}.`);
1481
+ process.exit(1);
1482
+ }
1483
+ // `--allow-backlog` is EXPLICIT-SINGLE-TASK-ONLY (the leak-fence): it must
1484
+ // not combine with the no-checkout auto-pick / -n / multi-item forms
1485
+ // (those select FROM the pool). Reject the misuse loudly, mirroring the
1486
+ // in-place guard + the `--watch` multi guard above.
1487
+ if (remoteMulti && flags.allowBacklog === true) {
1488
+ console.error(`error: --allow-backlog drives ONE named staged task in place; it does ` +
1489
+ `not combine with the ${form} auto-pick / -n / multi-item forms ` +
1490
+ `(those select from the pool). Name a single task: ${usage} --allow-backlog.`);
1491
+ process.exit(1);
1492
+ }
1493
+ if (args.length === 0 || count !== undefined) {
1494
+ // AUTO-PICK / `-n <x>` over the MIRROR-SIDE eligible-pool scan, SEQUENTIAL.
1495
+ const multi = await performDoRemoteAuto({
1496
+ ...baseRemoteOptions,
1497
+ config: remoteConfig,
1498
+ count,
1499
+ warn: (message) => console.error(`>> ${message}`),
1500
+ });
1501
+ console.error(`>> ${multi.message}`);
1502
+ process.exit(multi.exitCode);
1503
+ }
1504
+ if (args.length > 1) {
1505
+ // EXPLICIT named items, IN SEQUENCE (the operator's order; no pool).
1506
+ const multi = await performDoRemoteArgs(args, {
1507
+ ...baseRemoteOptions,
1508
+ config: remoteConfig,
1509
+ });
1510
+ console.error(`>> ${multi.message}`);
1511
+ process.exit(multi.exitCode);
1512
+ }
1513
+ // Exactly one named item: the single-item remote pipeline.
1514
+ // `--allow-backlog` rides ONLY this single-named-task call (never the
1515
+ // shared base used by auto-pick / multi) — the leak-fence.
1516
+ const remoteResult = await performDoRemote({
1517
+ ...baseRemoteOptions,
1518
+ arg: args[0],
1519
+ allowBacklog: flags.allowBacklog === true,
1520
+ });
1521
+ if (remoteResult.exitCode !== 0) {
1522
+ console.error(`error: ${remoteResult.message}`);
1523
+ }
1524
+ process.exit(remoteResult.exitCode);
1525
+ }
1526
+ // Thread the `do` CLI flags (--harness/--agent-cmd/--pi-bin/--model)
1527
+ // AND the integrate-time mode into the resolved config — the SAME flag
1528
+ // override path `run` uses (do-config.doFlagOverrides reuses
1529
+ // harnessFlagOverrides). Passing only `{integration}` here silently
1530
+ // DROPPED --harness pi etc.; now flag > env > per-repo > global > default
1531
+ // holds for `do` as for `run`.
1532
+ const resolved = resolveRepoConfig({
1533
+ repoPath: cwd,
1534
+ global,
1535
+ override,
1536
+ flags: doFlagOverrides(flags, flagMode),
1537
+ });
1538
+ if (resolved.message) {
1539
+ console.error(`>> ${resolved.message}`);
1540
+ }
1541
+ const config = resolved.config;
1542
+ // The null adapter shells out to agentCmd, so it is required there; the
1543
+ // pi adapter invokes the pi CLI directly and does not consume agentCmd.
1544
+ if (doNeedsAgentCmd(config)) {
1545
+ console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
1546
+ process.exit(1);
1547
+ }
1548
+ const harness = createHarness({
1549
+ harness: config.harness,
1550
+ piBin: config.piBin,
1551
+ });
1552
+ // The per-item `DoOptions` (everything BUT `arg`) — built ONCE and reused for
1553
+ // the single-item path AND threaded by the multi-item layer to each
1554
+ // sequential `performDo` (do-autopick runs the EXISTING pipeline per item).
1555
+ const baseDoOptions = {
1556
+ cwd,
1557
+ arbiter: flags.arbiter ?? config.defaultArbiter,
1558
+ // The host-only runner IDENTITY (a bot): scopes the runner's git/provider
1559
+ // ops (claim, push, integrate, `gh`) — NEVER the agent launch. Absent ⇒
1560
+ // ambient (today's behaviour). Mapped Config → DoOptions like model/agentCmd.
1561
+ identity: config.identity,
1562
+ // `do prd:<slug>` tasking-gate policy (the task-build path ignores it).
1563
+ autoTask: config.autoTask,
1564
+ // The resolved `promptGuidance` NUDGE namespace (e.g. `testFirst`),
1565
+ // threaded into the worker prompt by performDo → buildAgentPrompt so a
1566
+ // per-repo `promptGuidance.testFirst:true` actually strengthens the
1567
+ // autonomous `do` worker's wrapper line (not just `dorfl prompt`).
1568
+ promptGuidance: resolvePromptGuidance(config),
1569
+ integration: config.integration,
1570
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule (task
1571
+ // `untrusted-origin-forces-build-propose`): true ONLY when the operator
1572
+ // typed `--merge` (`flagMode`), never when `merge` came from config — so an
1573
+ // untrusted-origin task still forces propose under a config `merge`.
1574
+ explicitMerge: flagMode === 'merge',
1575
+ // Per-TRANSITION TASKING override: the `do prd:` tasking path threads
1576
+ // `taskingIntegration ?? integration`; the task-build path stays on
1577
+ // `integration`. Unset ⇒ tasking falls back to `integration` (today's behaviour).
1578
+ taskingIntegration: config.taskingIntegration,
1579
+ // TASK-PLACEMENT (`do prd:` tasking output): the configured default rung +
1580
+ // the EXPLICIT operator override `--tasks-land-in` (top of the precedence).
1581
+ // `explicitTasksLandIn` is set ONLY when the flag was typed (mirrors
1582
+ // `explicitMerge`), so an untrusted-origin staging force still wins under a
1583
+ // config default.
1584
+ tasksLandIn: config.tasksLandIn,
1585
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
1586
+ // In-place divergence guard override (mirrors --ignore-not-ready).
1587
+ ignoreDivergedMain: flags.ignoreDivergedMain === true,
1588
+ prepare: config.prepare,
1589
+ verify: config.verify,
1590
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
1591
+ freshWorktreeGate: config.freshWorktreeGate,
1592
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
1593
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)) — resolved per-repo
1594
+ // and threaded to `performComplete`→`performIntegration`.
1595
+ mergeRetries: config.mergeRetries,
1596
+ noPR: config.noPR,
1597
+ harness,
1598
+ agentCmd: config.agentCmd,
1599
+ model: config.model,
1600
+ // The HOST-ONLY sessions root (resolved Config → DoOptions bridge, like
1601
+ // model/agentCmd): the path generator turns it into
1602
+ // `<sessionsDir>/<id>.jsonl` for `--session`. Without this map the key
1603
+ // resolves but never reaches the launch (a silent no-op).
1604
+ sessionsDir: config.sessionsDir,
1605
+ // Gate 2 (PR/code review) rides inside `complete` (so CI inherits it for
1606
+ // free): when `review` resolves on, run the `review` SKILL as a
1607
+ // fresh-context agent (its OWN harness launch — same adapter + agentCmd,
1608
+ // `reviewModel` via the existing model-routing seam) after the green
1609
+ // verify, before the done-move. A block routes to needs-attention.
1610
+ review: config.review,
1611
+ reviewModel: config.reviewModel,
1612
+ reviewMaxRounds: config.reviewMaxRounds,
1613
+ reviewGate: config.review
1614
+ ? harnessReviewGate({ harness, agentCmd: config.agentCmd })
1615
+ : undefined,
1616
+ // The tasker IMPROVER loop on the `do prd:` tasking path is ON by default
1617
+ // (auto-tasking has no `verify` floor — the loop is the task path's quality
1618
+ // engine). `--tasker-loop`/`--no-tasker-loop` gates wiring the seam;
1619
+ // `taskerLoopMax`/`taskerLoopModel` resolve per-repo (flag > env > per-repo
1620
+ // > global > default); the task-build path ignores all of these. DISTINCT
1621
+ // from the acceptance gate's `--review*` family.
1622
+ reviewLoop: config.taskerLoop
1623
+ ? harnessTaskReviewGate({
1624
+ harness,
1625
+ agentCmd: config.agentCmd,
1626
+ })
1627
+ : undefined,
1628
+ taskerLoopMax: config.taskerLoopMax,
1629
+ taskerLoopModel: config.taskerLoopModel,
1630
+ // The task-SET ACCEPTANCE GATE (slice-acceptance-gate): the task-path
1631
+ // mirror of Gate-2, on the SAME `--review` family (so `--no-review` skips
1632
+ // it). ONE-SHOT (no rounds); production wires the task-SET-prompt gate.
1633
+ taskReviewGate: config.review
1634
+ ? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
1635
+ : undefined,
1636
+ // `--watch`: tail the pi session log live (pi harness only; the
1637
+ // performDo guard errors clearly on any other adapter). READ-ONLY.
1638
+ watch: flags.watch === true,
1639
+ color: shouldUseColor(process.stdout),
1640
+ note: (message) => console.error(`>> ${message}`),
1641
+ noteBlock: (message) => console.error(message),
1642
+ };
1643
+ // `--allow-backlog` is EXPLICIT-SINGLE-TASK-ONLY (prd
1644
+ // `do-allow-backlog-drive-staged-tasks-without-promotion`, decision 4): it
1645
+ // drives ONE named staged task in place. It must NOT combine with the
1646
+ // AUTO-PICK (zero-args / -n) or MULTI-ITEM forms — those select FROM the
1647
+ // pool, and letting the flag widen a pool selection is exactly the
1648
+ // competition-bug-one-layer-down the fence forbids. Reject the misuse loudly
1649
+ // (mirroring the `--watch` multi guard) rather than silently widen a pool.
1650
+ if (flags.allowBacklog === true &&
1651
+ (args.length !== 1 || count !== undefined)) {
1652
+ console.error('error: --allow-backlog drives ONE named staged task in place; it does ' +
1653
+ 'not combine with auto-pick / -n / multi-item forms (those select from ' +
1654
+ `the pool). Name a single task: dorfl do task:<slug> --allow-backlog.`);
1655
+ process.exit(1);
1656
+ }
1657
+ // DISPATCH the variadic grammar (in-place forms):
1658
+ // zero args -> AUTO-PICK `count` (default 1) across the two pools
1659
+ // (ordered by selectionOrder; default drain = tasks-first)
1660
+ // one named arg -> the single-item pipeline (unchanged from do-in-place)
1661
+ // many named args -> those, IN SEQUENCE (operator's order; no pool)
1662
+ // Auto-pick / multi-arg run the EXISTING `performDo` pipeline per item,
1663
+ // sequentially (`do` is sequential; parallelism is `run`).
1664
+ if (args.length === 0) {
1665
+ const multi = await performDoAuto({
1666
+ ...baseDoOptions,
1667
+ config,
1668
+ override,
1669
+ count,
1670
+ });
1671
+ console.error(`>> ${multi.message}`);
1672
+ process.exit(multi.exitCode);
1673
+ }
1674
+ if (args.length > 1) {
1675
+ const multi = await performDoArgs(args, {
1676
+ ...baseDoOptions,
1677
+ config,
1678
+ });
1679
+ console.error(`>> ${multi.message}`);
1680
+ process.exit(multi.exitCode);
1681
+ }
1682
+ // Exactly one named item: the single-item in-place pipeline (do-in-place).
1683
+ // `--allow-backlog` rides ONLY this single-named-task call (never the
1684
+ // auto-pick / multi base above) — the leak-fence: the flag is read from the
1685
+ // typed CLI flag here, not from config/env, and never reaches a pool path.
1686
+ const result = await performDo({
1687
+ ...baseDoOptions,
1688
+ arg: args[0],
1689
+ allowBacklog: flags.allowBacklog === true,
1690
+ });
1691
+ if (result.exitCode !== 0) {
1692
+ console.error(`error: ${result.message}`);
1693
+ }
1694
+ process.exit(result.exitCode);
1695
+ });
1696
+ // `advance` — the SIBLING top-level verb (NOT a `do` subcommand; `do`
1697
+ // subcommands + a standalone `task` verb are REJECTED in prd `advance-loop`).
1698
+ // It reuses the SAME shared `prefix:arg` resolver `do` uses, EXTENDED with the
1699
+ // `obs:` namespace, and wires the classify → lock → execute SKELETON: classify
1700
+ // the rung (read-only, no model, no lock), take the `advancing` CAS borrow, then
1701
+ // dispatch winner-only — build/task rungs ORCHESTRATE `do`/`do prd:` (never a
1702
+ // duplicate), surface/apply/triage dispatch to a named executor seam later
1703
+ // tasks fill. The DRIVERS (one-shot/loop) + `-n` + per-action gates and the
1704
+ // rung BODIES are LATER tasks; the bare eligible-SET form errors clearly here.
1705
+ program
1706
+ .command('advance')
1707
+ .helpGroup(HEADLINE_GROUP)
1708
+ .description('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).')
1709
+ .argument('[slugs...]', '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.')
1710
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
1711
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
1712
+ .option('-n, --number <x>', '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.')
1713
+ .option('--isolated', "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.")
1714
+ .option('--selection-order <order>', '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.')
1715
+ .option('--observation-triage <mode>', 'the observation-inbox gate (off|ask|auto): off (default) leaves observations untouched (the triage pool is dropped from auto-pick); ask surfaces a promote/keep/delete question for each untriaged observation; auto auto-disposes the no-question cases (duplicate/map) and asks about the rest. Resolved flag > env > per-repo > global > default. An explicit `advance obs:<slug>` bypasses the selection gate and runs in ask-mode (auto-disposes only under `auto`).')
1716
+ .option('--surface-blockers', '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).')
1717
+ .option('--no-surface-blockers', 'leave a needsAnswers:true task/prd silently blocked (default; the blocked pool is dropped from auto-pick)')
1718
+ .option('--strict-merge-approval', '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.')
1719
+ .option('--no-strict-merge-approval', 'honour the prior merge-answer and land when the rebased tip re-verifies GREEN even if the merge-base changed (default; the cheap green-re-verify-is-enough path)')
1720
+ .option('--merge-questions <mode>', 'the merge-question SURFACER gate (off|ask|auto): off drops the surfacer (only for a repo that lands by some other means); ask (default) enumerates unmerged `work/*` branches and surfaces a merge-question sidecar a human answers; auto self-supplies the `merge` answer and lands via the SAME deterministic apply-time re-verify (the merge-mode-like fast path). SEPARATE axis from --observation-triage with a HIGHER default (a dropped merge-question means pushed work never lands). Resolved flag > env > per-repo > global > default ask.')
1721
+ .option('--merge', 'integrate the advanced item(s) in merge mode this invocation (mutually exclusive with --propose; overrides config). The CI merge shape is a SINGLE SEQUENTIAL job, so this rides the `-n`/named-sequence path, never the matrix.')
1722
+ .option('--propose', 'integrate the advanced item(s) in propose mode this invocation (default; mutually exclusive with --merge; overrides config). The CI propose shape is the parallel matrix (one PR per item).')
1723
+ .option('--tasks-land-in <where>', '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.')
1724
+ .option('--watch', "stream the build agent's high-signal events live by tailing the pi session log (requires harness: pi; READ-ONLY observer — does not change outcome/gate/git). The same view `do --watch` gives, threaded through the build rung; CI uses it so the job log shows the agent working instead of freezing.")
1725
+ .action(async (rawSlugs, flags) => {
1726
+ // Variadic grammar (mirrors `do`): zero args = AUTO-PICK; one = the single
1727
+ // named item; many = those, IN SEQUENCE. `-n <x>` is the auto-pick count
1728
+ // (ALWAYS sequential, US #25).
1729
+ const args = rawSlugs ?? [];
1730
+ // `-n <x>` parse + validation — the AUTO-PICK count (sequential), mutually
1731
+ // exclusive with NAMING items (the SAME contract `do -n` enforces).
1732
+ let count;
1733
+ if (flags.number !== undefined) {
1734
+ const n = Number(flags.number);
1735
+ if (flags.number.trim() === '' || !Number.isInteger(n) || n < 1) {
1736
+ console.error(`error: -n/--number must be a positive integer (got '${flags.number}').`);
1737
+ process.exit(1);
1738
+ }
1739
+ if (args.length > 0) {
1740
+ console.error('error: -n/--number auto-picks a COUNT of eligible items; do not also ' +
1741
+ 'name items. Use `advance -n <x>` OR `advance <a> <b> ...`, not both.');
1742
+ process.exit(1);
1743
+ }
1744
+ count = n;
1745
+ }
1746
+ const cwd = process.cwd();
1747
+ const { global, override } = loadGlobalAndOverride(flags.config);
1748
+ // Resolve the integration mode this invocation asks for, highest first:
1749
+ // --merge/--propose flag > per-repo .dorfl.json > global > default.
1750
+ // The SAME chain `do`/`complete` use (via `integrationFromFlags`), so the
1751
+ // human, the autonomous runner, and the CI workflow all resolve the SAME
1752
+ // order. This is what ties the CI dispatch `integrationMode` to the actual
1753
+ // open-PR-vs-merge-to-main behaviour: the propose-matrix legs pass
1754
+ // `--propose` and the single sequential merge job passes `--merge`, so the
1755
+ // integration mode can never DESYNC from the job shape the input selected.
1756
+ let flagMode;
1757
+ try {
1758
+ flagMode = integrationFromFlags(flags);
1759
+ }
1760
+ catch (err) {
1761
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
1762
+ process.exit(1);
1763
+ }
1764
+ // Build the flag overrides (the `--observation-triage` enum FAILS LOUDLY on a
1765
+ // typo, like the env coercion) before resolving — a bad gate value is a clean
1766
+ // usage error, never silently dropped.
1767
+ let doOverrides;
1768
+ try {
1769
+ doOverrides = doFlagOverrides(flags, flagMode);
1770
+ }
1771
+ catch (err) {
1772
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
1773
+ process.exit(1);
1774
+ }
1775
+ // `advance --isolated`: run the advance TICK in an ISOLATED worktree off
1776
+ // THIS repo's arbiter (resolved from cwd), then integrate + reap — the
1777
+ // in-place-but-isolated form, the SAME ergonomic `do --isolated` has. We
1778
+ // REUSE `do --isolated`'s arbiter-from-cwd resolver + the isolation substrate
1779
+ // (`ensureMirror` + the job-worktree `doDriver` + reap), threading the
1780
+ // arbiter URL into the NEW isolated advance-tick runner. `--isolated` is the
1781
+ // only ISOLATION axis here: `advance --remote <url>` is a SEPARATE concern
1782
+ // (the action already TYPES `flags.remote` via `DoFlags`, but no `--remote`
1783
+ // plumbing exists on `advance` — see `## Decisions`), so `--isolated` always
1784
+ // resolves the arbiter from the CWD.
1785
+ if (flags.isolated === true) {
1786
+ // BOOTSTRAP resolution (global + flags, no per-repo layer) supplies the
1787
+ // host-only keys needed to even reach the arbiter (`workspacesDir`,
1788
+ // `identity`), exactly as `do --isolated` bootstraps them.
1789
+ const bootstrap = resolveGlobalConfig(global, doOverrides);
1790
+ const arbiterName = flags.arbiter ??
1791
+ resolveDefaultArbiterForCwd(cwd, global, doOverrides, override);
1792
+ const arbiterUrl = resolveArbiterUrlFromCheckout(cwd, arbiterName, identityEnv(bootstrap.identity, process.env));
1793
+ if (arbiterUrl === undefined) {
1794
+ // The SAME clear "isolated against what?" error `do --isolated` gives —
1795
+ // naming `--remote <url>` as the foreign-repo alternative, NOT a
1796
+ // downstream URL-parse failure.
1797
+ console.error(`error: --isolated advances in a worktree off this repo's arbiter ` +
1798
+ `('${arbiterName}'), but no such arbiter remote is configured/found ` +
1799
+ `here. Run inside a participating repo (a clone with an arbiter ` +
1800
+ `remote), or use --remote <url> to target another repo.`);
1801
+ process.exit(1);
1802
+ }
1803
+ // Source the arbiter's COMMITTED `.dorfl.json` from `<arbiter>/main`
1804
+ // via the hub mirror + layer ONLY its whitelisted keys — the SAME
1805
+ // resolution `do --isolated` uses, so the gate family (autoBuild/autoTask/
1806
+ // observationTriage/surfaceBlockers) + selectionOrder + integration resolve
1807
+ // off the arbiter exactly as the in-place advance resolves them off cwd.
1808
+ const remoteConfig = resolveRemoteRepoConfig({
1809
+ remote: arbiterUrl,
1810
+ workspacesDir: bootstrap.workspacesDir,
1811
+ global,
1812
+ flags: doOverrides,
1813
+ identity: bootstrap.identity,
1814
+ note: (message) => console.error(`>> ${message}`),
1815
+ override,
1816
+ });
1817
+ if (doNeedsAgentCmd(remoteConfig)) {
1818
+ console.error(`error: ${NO_AGENT_CMD_MESSAGE}`);
1819
+ process.exit(1);
1820
+ }
1821
+ const isoHarness = createHarness({
1822
+ harness: remoteConfig.harness,
1823
+ piBin: remoteConfig.piBin,
1824
+ });
1825
+ // The base `do` options the build/task rungs ORCHESTRATE through the
1826
+ // INJECTED job-worktree driver (the isolated advance-tick runner wires it).
1827
+ const isoDoOptions = {
1828
+ cwd,
1829
+ // `--watch`: stream the build agent's session live (pi harness only;
1830
+ // validated in `performDo`). Threaded through the orchestrated build rung
1831
+ // so `advance --isolated --watch` (and CI) shows the agent working.
1832
+ watch: flags.watch === true,
1833
+ arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
1834
+ identity: remoteConfig.identity,
1835
+ autoTask: remoteConfig.autoTask,
1836
+ integration: remoteConfig.integration,
1837
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule.
1838
+ explicitMerge: flagMode === 'merge',
1839
+ // Per-TRANSITION TASKING override (the isolated `do --remote prd:` path).
1840
+ taskingIntegration: remoteConfig.taskingIntegration,
1841
+ // TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
1842
+ // (set only when typed, mirroring `explicitMerge`).
1843
+ tasksLandIn: remoteConfig.tasksLandIn,
1844
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
1845
+ prepare: remoteConfig.prepare,
1846
+ verify: remoteConfig.verify,
1847
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
1848
+ freshWorktreeGate: remoteConfig.freshWorktreeGate,
1849
+ // Cross-job merge-serialiser CAS-retry cap — prd
1850
+ // `land-time-reverify-and-parallel-merge-ceiling` Story 5.
1851
+ mergeRetries: remoteConfig.mergeRetries,
1852
+ noPR: remoteConfig.noPR,
1853
+ harness: isoHarness,
1854
+ agentCmd: remoteConfig.agentCmd,
1855
+ model: remoteConfig.model,
1856
+ sessionsDir: remoteConfig.sessionsDir,
1857
+ review: remoteConfig.review,
1858
+ reviewModel: remoteConfig.reviewModel,
1859
+ reviewMaxRounds: remoteConfig.reviewMaxRounds,
1860
+ reviewGate: remoteConfig.review
1861
+ ? harnessReviewGate({
1862
+ harness: isoHarness,
1863
+ agentCmd: remoteConfig.agentCmd,
1864
+ })
1865
+ : undefined,
1866
+ reviewLoop: remoteConfig.taskerLoop
1867
+ ? harnessTaskReviewGate({
1868
+ harness: isoHarness,
1869
+ agentCmd: remoteConfig.agentCmd,
1870
+ })
1871
+ : undefined,
1872
+ taskerLoopMax: remoteConfig.taskerLoopMax,
1873
+ taskerLoopModel: remoteConfig.taskerLoopModel,
1874
+ taskReviewGate: remoteConfig.review
1875
+ ? harnessTaskAcceptanceGate({
1876
+ harness: isoHarness,
1877
+ agentCmd: remoteConfig.agentCmd,
1878
+ })
1879
+ : undefined,
1880
+ color: shouldUseColor(process.stdout),
1881
+ note: (message) => console.error(`>> ${message}`),
1882
+ noteBlock: (message) => console.error(message),
1883
+ };
1884
+ // The shared per-item ISOLATED advance CONTEXT (everything BUT `arg` and
1885
+ // `cwd`/`doDriver`, which the runner supplies from the isolated clone).
1886
+ const isoContext = {
1887
+ remote: arbiterUrl,
1888
+ workspacesDir: remoteConfig.workspacesDir,
1889
+ arbiter: flags.arbiter ?? remoteConfig.defaultArbiter,
1890
+ doOptions: isoDoOptions,
1891
+ surfaceGate: harnessSurfaceGate({
1892
+ harness: isoHarness,
1893
+ agentCmd: remoteConfig.agentCmd,
1894
+ }),
1895
+ surfaceModel: remoteConfig.model,
1896
+ applyDecide: harnessApplyDecider({
1897
+ harness: isoHarness,
1898
+ agentCmd: remoteConfig.agentCmd,
1899
+ }),
1900
+ applyModel: remoteConfig.model,
1901
+ observationTriage: remoteConfig.observationTriage,
1902
+ triageGate: harnessTriageGate({
1903
+ harness: isoHarness,
1904
+ agentCmd: remoteConfig.agentCmd,
1905
+ }),
1906
+ triageModel: remoteConfig.model,
1907
+ note: (message) => console.error(`>> ${message}`),
1908
+ env: process.env,
1909
+ };
1910
+ // DISPATCH the variadic grammar, ISOLATED + SEQUENTIAL (mirrors
1911
+ // `do --isolated`): zero args / `-n` -> AUTO-PICK over the mirror-side
1912
+ // eligible-pool scan; many named -> those in sequence; one named -> the
1913
+ // single isolated tick. `-n` stays ALWAYS SEQUENTIAL (US #25).
1914
+ if (args.length === 0 || count !== undefined) {
1915
+ const multi = await performAdvanceIsolatedAuto({
1916
+ ...isoContext,
1917
+ config: remoteConfig,
1918
+ count,
1919
+ warn: (message) => console.error(`>> ${message}`),
1920
+ lifecycleGates: {
1921
+ triage: remoteConfig.observationTriage !== 'off',
1922
+ surface: remoteConfig.surfaceBlockers,
1923
+ surfaceStaging: remoteConfig.surfaceStaging,
1924
+ },
1925
+ });
1926
+ console.error(`>> ${multi.message}`);
1927
+ process.exit(multi.exitCode);
1928
+ }
1929
+ if (args.length > 1) {
1930
+ const multi = await performAdvanceIsolatedArgs(args, {
1931
+ ...isoContext,
1932
+ config: remoteConfig,
1933
+ });
1934
+ console.error(`>> ${multi.message}`);
1935
+ process.exit(multi.exitCode);
1936
+ }
1937
+ // Exactly one named item: the single ISOLATED advance tick.
1938
+ const result = await performAdvanceIsolated({
1939
+ ...isoContext,
1940
+ arg: args[0],
1941
+ });
1942
+ if (result.exitCode !== 0) {
1943
+ console.error(`error: ${result.message}`);
1944
+ }
1945
+ process.exit(result.exitCode);
1946
+ }
1947
+ const resolved = resolveRepoConfig({
1948
+ repoPath: cwd,
1949
+ global,
1950
+ override,
1951
+ flags: doOverrides,
1952
+ });
1953
+ if (resolved.message) {
1954
+ console.error(`>> ${resolved.message}`);
1955
+ }
1956
+ const config = resolved.config;
1957
+ const harness = createHarness({
1958
+ harness: config.harness,
1959
+ piBin: config.piBin,
1960
+ });
1961
+ // The base `do` options the build/task rungs ORCHESTRATE `performDo` with
1962
+ // (the ONE build path / ONE task path). `advance` is a driver ON TOP — it
1963
+ // hands the resolved arg to `performDo`, never re-implementing it.
1964
+ const doOptions = {
1965
+ cwd,
1966
+ // `--watch`: stream the build agent's session live (pi harness only;
1967
+ // validated in `performDo`). Threaded through the orchestrated build rung
1968
+ // so `advance --watch` (and CI) shows the agent working, not a frozen log.
1969
+ watch: flags.watch === true,
1970
+ arbiter: flags.arbiter ?? config.defaultArbiter,
1971
+ identity: config.identity,
1972
+ autoTask: config.autoTask,
1973
+ integration: config.integration,
1974
+ // EXPLICIT `--merge` override for the untrusted-origin build-propose rule (a
1975
+ // bare `advance` auto-pick passes no flag ⇒ unset ⇒ untrusted forces propose).
1976
+ explicitMerge: flagMode === 'merge',
1977
+ // Per-TRANSITION TASKING override (the `do prd:` tasking path threads
1978
+ // `taskingIntegration ?? integration`; the build path stays on `integration`).
1979
+ taskingIntegration: config.taskingIntegration,
1980
+ // TASK-PLACEMENT: configured default + EXPLICIT `--tasks-land-in` override
1981
+ // (set only when typed, mirroring `explicitMerge`).
1982
+ tasksLandIn: config.tasksLandIn,
1983
+ explicitTasksLandIn: explicitTasksLandInFromFlag(flags.tasksLandIn),
1984
+ prepare: config.prepare,
1985
+ verify: config.verify,
1986
+ // Single-job build path: gate the REBASED tip (the default) unconditionally.
1987
+ freshWorktreeGate: config.freshWorktreeGate,
1988
+ // Cross-job merge-serialiser CAS-retry cap (prd `land-time-reverify-and-
1989
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)).
1990
+ mergeRetries: config.mergeRetries,
1991
+ noPR: config.noPR,
1992
+ harness,
1993
+ agentCmd: config.agentCmd,
1994
+ model: config.model,
1995
+ sessionsDir: config.sessionsDir,
1996
+ review: config.review,
1997
+ reviewModel: config.reviewModel,
1998
+ reviewMaxRounds: config.reviewMaxRounds,
1999
+ reviewGate: config.review
2000
+ ? harnessReviewGate({ harness, agentCmd: config.agentCmd })
2001
+ : undefined,
2002
+ reviewLoop: config.taskerLoop
2003
+ ? harnessTaskReviewGate({ harness, agentCmd: config.agentCmd })
2004
+ : undefined,
2005
+ taskerLoopMax: config.taskerLoopMax,
2006
+ taskerLoopModel: config.taskerLoopModel,
2007
+ taskReviewGate: config.review
2008
+ ? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
2009
+ : undefined,
2010
+ color: shouldUseColor(process.stdout),
2011
+ note: (message) => console.error(`>> ${message}`),
2012
+ noteBlock: (message) => console.error(message),
2013
+ };
2014
+ // The shared per-item advance CONTEXT (everything BUT `arg`) — built ONCE
2015
+ // and threaded by the one-shot DRIVER to each sequential tick. The SURFACE
2016
+ // rung spawns `surface-questions` fresh-context through the SAME harness seam
2017
+ // the review gate uses (the engine then PERSISTS); the TRIAGE rung is
2018
+ // question-gated by default; `observationTriage: 'auto'` enables the
2019
+ // conservative auto-disposition exception (`ask`/`off` surface the question).
2020
+ // Surface + apply stay ALWAYS allowed regardless of the gate family.
2021
+ const advanceContext = {
2022
+ cwd,
2023
+ arbiter: flags.arbiter ?? config.defaultArbiter,
2024
+ doOptions,
2025
+ surfaceGate: harnessSurfaceGate({ harness, agentCmd: config.agentCmd }),
2026
+ surfaceModel: config.model,
2027
+ applyDecide: harnessApplyDecider({ harness, agentCmd: config.agentCmd }),
2028
+ applyModel: config.model,
2029
+ observationTriage: config.observationTriage,
2030
+ triageGate: harnessTriageGate({ harness, agentCmd: config.agentCmd }),
2031
+ triageModel: config.model,
2032
+ note: (message) => console.error(`>> ${message}`),
2033
+ };
2034
+ // `--watch` tails ONE pi session, so it only fits the single-named-item form
2035
+ // (mirrors `do --watch`). The auto-pick / `-n` / multi-item forms run many
2036
+ // ticks in sequence and would tail several logs; reject rather than silently
2037
+ // stream only one. The CI propose matrix names a single item per leg, so it
2038
+ // satisfies this; the `-n` merge job must NOT pass `--watch`.
2039
+ const advanceMulti = args.length === 0 || count !== undefined || args.length > 1;
2040
+ if (advanceMulti && flags.watch === true) {
2041
+ console.error('error: --watch streams ONE session; it does not combine with the ' +
2042
+ 'auto-pick / -n / multi-item forms. Name a single item.');
2043
+ process.exit(1);
2044
+ }
2045
+ // DISPATCH the variadic grammar (the one-shot SEQUENTIAL driver):
2046
+ // zero args -> AUTO-PICK `count` (default 1) over the eligible pool
2047
+ // (tasks-first then prds-to-task; per-action gates
2048
+ // respected by the SELECTION layer; ordered by selectionOrder).
2049
+ // one named arg -> the single-item tick (the always-allowed surface/apply
2050
+ // path runs regardless of the gate family).
2051
+ // many named args -> those, IN SEQUENCE (operator's order; no pool).
2052
+ // `-n` / auto-pick / multi-arg all run the EXISTING tick per item,
2053
+ // SEQUENTIALLY (parallelism is `run` / the CI matrix, never `-n`).
2054
+ if (args.length === 0) {
2055
+ const multi = await performAdvanceAuto({
2056
+ ...advanceContext,
2057
+ config,
2058
+ override,
2059
+ count,
2060
+ // The SELECTION-layer gates: `observationTriage != off` enumerates the
2061
+ // observation (triage) pool into auto-pick; `surfaceBlockers` enumerates
2062
+ // the `needsAnswers`-blocked (surface) pool. `off`/`false` drops the
2063
+ // respective pool (its item is left untouched / silently blocked). The
2064
+ // two gates are orthogonal peers. The triage rung's ask-vs-auto
2065
+ // distinction is read inside the tick from `observationTriage` (threaded
2066
+ // on `advanceContext`). Apply (consume) is always-on (never gated here).
2067
+ lifecycleGates: {
2068
+ triage: config.observationTriage !== 'off',
2069
+ surface: config.surfaceBlockers,
2070
+ surfaceStaging: config.surfaceStaging,
2071
+ },
2072
+ });
2073
+ console.error(`>> ${multi.message}`);
2074
+ process.exit(multi.exitCode);
2075
+ }
2076
+ if (args.length > 1) {
2077
+ const multi = await performAdvanceArgs(args, {
2078
+ ...advanceContext,
2079
+ config,
2080
+ });
2081
+ console.error(`>> ${multi.message}`);
2082
+ process.exit(multi.exitCode);
2083
+ }
2084
+ // Exactly one named item: the single-item tick. Wrapped in the shared
2085
+ // in-place tree-less publish so a surfaced sidecar / `triaged:` marker /
2086
+ // applied-answer commit reaches the arbiter's `main` (the CI ephemeral-runner
2087
+ // case — the local commit would otherwise be lost). The wrapper is the SAME
2088
+ // gate the `--isolated` / loop drivers use (`TREELESS_RUNGS` + exit 0 +
2089
+ // arbiter configured); a build/task rung integrates through the `doDriver`
2090
+ // band already, and a no-arbiter laptop checkout sits on the real `main`.
2091
+ const result = await runAdvanceTickWithTreelessPublish({ ...advanceContext, arg: args[0] }, performAdvance);
2092
+ if (result.exitCode !== 0) {
2093
+ console.error(`error: ${result.message}`);
2094
+ }
2095
+ process.exit(result.exitCode);
2096
+ });
2097
+ program
2098
+ .command('gc')
2099
+ .helpGroup(ADVANCED_GROUP)
2100
+ .description('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.')
2101
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2102
+ .option('--workspace <dir>', 'execution working area to sweep (default: workspacesDir / ~/.dorfl)')
2103
+ .option('--force', 'OVERRIDE the predicate: remove worktrees even with un-saved work (requires --yes; never the default)')
2104
+ .option('--yes', 'confirm a destructive --force sweep non-interactively')
2105
+ .option('--ledger [repoPath]', 'SWEEP the work/ lifecycle LEDGER instead of job worktrees: REPORT (never delete) every slug present in more than one work/ status folder (the one-slug-one-folder belt-and-suspenders), with its folders + candidate canonical folder, for a HUMAN to resolve. Defaults to the cwd repo.')
2106
+ .option('--reap-stale-locks', '(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).')
2107
+ .option('--remote-branches', 'SWEEP the arbiter’s remote work/* BRANCHES instead of job worktrees: delete (via git push --delete, NEVER --force) exactly those PROVABLY MERGED into <arbiter>/main (git merge-base --is-ancestor, the SAME predicate the worktree reaper uses), and RETAIN the rest with a reason. An in-flight/un-merged branch (the recovery point) is NEVER touched. Provider-agnostic plain git — works on a --bare arbiter. The merged-only complement of `requeue --reset`.')
2108
+ .option('--arbiter <remote>', '(with --remote-branches) the arbiter git remote whose work/* branches to sweep (default: origin); resolved from --cwd')
2109
+ .option('--cwd <dir>', '(with --remote-branches) the local repo/clone whose --arbiter remote points at the arbiter to sweep (default: cwd); only remote refs are read + deleted, never the working tree')
2110
+ .option('--dry-run', '(with --remote-branches) REPORT which merged branches WOULD be reaped without deleting anything (a read-only preview)')
2111
+ .option('--json', 'output the raw result as JSON')
2112
+ .action(async (flags) => {
2113
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
2114
+ const workspacesDir = flags.workspace ?? config.workspacesDir;
2115
+ // The `gc`-STYLE ledger SWEEP (prd `ledger-integrity` story 3): a SEPARATE
2116
+ // surface from the worktree reaper below — it REPORTS one-slug-one-folder
2117
+ // violations in a repo's `work/` lifecycle ledger and NEVER deletes (a human
2118
+ // resolves each). Distinct `work/`: the ledger, not the execution substrate.
2119
+ if (flags.ledger !== undefined) {
2120
+ const repoPath = typeof flags.ledger === 'string' ? flags.ledger : process.cwd();
2121
+ const result = sweepLedgerDuplicates(repoPath);
2122
+ // The UNIFIED-LOCK stuck/orphaned-lock REPORT (task
2123
+ // `release-lock-verb-and-gc-stuck-report`, prd
2124
+ // `ledger-status-per-item-lock-refs` US #12/#13/#14): generalises the
2125
+ // advancing-marker report from advancing-only to the unified per-item
2126
+ // lock. The locks live on the ARBITER ref (`refs/dorfl/lock/*`),
2127
+ // not in the local tree, so this reads the arbiter (cwd's `--arbiter`
2128
+ // remote). Best-effort: an absent lock-ref namespace / unreachable arbiter
2129
+ // degrades to an EMPTY report ("all locks released" — recoverable, US #12),
2130
+ // exactly as an absent lock-ref namespace reads. It REPORTS only,
2131
+ // wiring `reconcileItemLockAgainstMain`'s read-only twin to DISTINGUISH a
2132
+ // held/stuck lock from a stale-active lock over a terminal item WITHOUT
2133
+ // clearing (no auto-sweep; a human asserts a lock is dead via
2134
+ // `release-lock`).
2135
+ // OPT-IN SWEEP (`--reap-stale-locks`): the WRITE twin of the report. A
2136
+ // human asserting "clear the dead TERMINAL locks now": for EXACTLY the
2137
+ // `cleared-stale` class (terminal-on-main + active = stranded) perform the
2138
+ // SAME leased delete `release-lock` / the recovery use, so one command
2139
+ // sweeps every orphaned terminal lock. A `kept-stuck` / `kept-in-flight`
2140
+ // lock is NEVER reaped (scope fence); a concurrent change makes a clear
2141
+ // REJECT (reported `lost`), never --force. WITHOUT the flag the surface
2142
+ // below stays report-only (fail-loud, deletes nothing).
2143
+ if (flags.reapStaleLocks) {
2144
+ const reap = await reapStaleItemLocks(flags.cwd ?? repoPath, flags.arbiter ?? 'origin', process.env);
2145
+ if (flags.json) {
2146
+ console.log(JSON.stringify({ ...result, reap }, null, 2));
2147
+ }
2148
+ else {
2149
+ const reapLines = formatReapReport(reap);
2150
+ const blocks = [];
2151
+ if (result.duplicates.length > 0) {
2152
+ blocks.push(formatLedgerSweep(result));
2153
+ }
2154
+ if (reapLines.length > 0) {
2155
+ blocks.push(reapLines.join('\n'));
2156
+ }
2157
+ console.log(blocks.length > 0
2158
+ ? blocks.join('\n\n')
2159
+ : formatLedgerSweep(result));
2160
+ }
2161
+ // Fail-loud AFTER the sweep: a `kept-stuck` (rightly left for a human) or
2162
+ // a `lost`/`error` (a stale lock whose leased delete lost the race) still
2163
+ // needs attention; a clean sweep that reaped every stale lock and left
2164
+ // only healthy in-flight holds exits 0.
2165
+ process.exit(result.duplicates.length > 0 || reapReportNeedsAttention(reap)
2166
+ ? 1
2167
+ : 0);
2168
+ }
2169
+ const lockReport = await reportItemLocks(flags.cwd ?? repoPath, flags.arbiter ?? 'origin', process.env);
2170
+ if (flags.json) {
2171
+ console.log(JSON.stringify({ ...result, lockReport }, null, 2));
2172
+ }
2173
+ else {
2174
+ const lockLines = formatItemLockReport(lockReport);
2175
+ if (result.duplicates.length === 0 && lockLines.length === 0) {
2176
+ console.log(formatLedgerSweep(result));
2177
+ }
2178
+ else {
2179
+ const blocks = [];
2180
+ const sweepText = formatLedgerSweep(result);
2181
+ // Only print the duplicate block when it found something (otherwise
2182
+ // it returns the "clean" line, which is misleading when there ARE
2183
+ // lingering locks below it).
2184
+ if (result.duplicates.length > 0) {
2185
+ blocks.push(sweepText);
2186
+ }
2187
+ if (lockLines.length > 0) {
2188
+ blocks.push(lockLines.join('\n'));
2189
+ }
2190
+ console.log(blocks.join('\n\n'));
2191
+ }
2192
+ }
2193
+ // A corrupt ledger, a stuck advancing-lock marker, OR a per-item lock that
2194
+ // NEEDS HUMAN ATTENTION is a fail-loud condition: exit non-zero so a human
2195
+ // (or a script) cannot miss it, mirroring the integration core's refusal.
2196
+ // ALL are REPORTED here (never auto-deleted — no automatic sweep exists; a
2197
+ // human clears a NAMED unified lock via `release-lock`).
2198
+ //
2199
+ // SCOPED to the ATTENTION verdicts only (prd US#14/#21, ADR
2200
+ // `ledger-status-on-per-item-lock-refs`: this surface is the STUCK /
2201
+ // crash-orphaned lock, NOT every held one): a `kept-stuck` (terminal +
2202
+ // stuck) or a `cleared-stale`-eligible (terminal + stale active = orphaned)
2203
+ // lock fails loud, but a `kept-in-flight` (active, non-terminal) lock is the
2204
+ // NORMAL in-flight state of a healthy concurrent build (read by `status` as
2205
+ // healthy) — it is reported informationally and does NOT make a routine
2206
+ // `gc --ledger` health check exit non-zero.
2207
+ process.exit(result.duplicates.length > 0 ||
2208
+ itemLockReportNeedsAttention(lockReport)
2209
+ ? 1
2210
+ : 0);
2211
+ }
2212
+ // The REMOTE merged-BRANCH sweep (this task): a SEPARATE surface from the
2213
+ // worktree reaper below — it deletes PROVABLY-MERGED remote `work/*` branches
2214
+ // on the arbiter (the cross-machine counterpart of reaping local worktrees),
2215
+ // guarded by the SAME ancestor-of-main predicate. Provider-agnostic plain git
2216
+ // (works on a `--bare` arbiter); NEVER `--force` (a merged ref needs none),
2217
+ // NEVER touches an in-flight branch.
2218
+ if (flags.remoteBranches) {
2219
+ const sweepCwd = flags.cwd ?? process.cwd();
2220
+ const sweep = sweepRemoteMergedBranches({
2221
+ cwd: sweepCwd,
2222
+ arbiter: flags.arbiter ?? 'origin',
2223
+ dryRun: flags.dryRun === true,
2224
+ note: (message) => console.error(`>> ${message}`),
2225
+ });
2226
+ // The ORPHAN-SIDECAR sweep (prd
2227
+ // `agentic-question-resolution-retire-disposition-vocabulary`, US #10) rides
2228
+ // the SAME `--remote-branches` invocation the SCHEDULED CI lifecycle workflow
2229
+ // runs (`dorfl gc --remote-branches --arbiter origin`) — so the reap of a
2230
+ // `work/questions/<type>-<slug>.md` whose source item was deleted out-of-band
2231
+ // actually FIRES on the cron tick (not behind an un-passed flag). It operates
2232
+ // on the WORKING TREE of the checkout `gc` runs in (the same `cwd` the branch
2233
+ // sweep targets) — CI checks out the repo — so no arbiter ref query is needed
2234
+ // beyond the by-identity source-existence check
2235
+ // (`resolveItemPathByIdentity`). A `git rm` deletion (notes/sidecars leave by
2236
+ // deletion; git history is the archive), so a wrong source-delete is
2237
+ // recoverable from history. Honours `--dry-run` (report-only preview).
2238
+ const orphans = sweepOrphanSidecars({
2239
+ cwd: sweepCwd,
2240
+ dryRun: flags.dryRun === true,
2241
+ note: (message) => console.error(`>> ${message}`),
2242
+ });
2243
+ if (flags.json) {
2244
+ console.log(JSON.stringify({ ...sweep, orphanSidecars: orphans }, null, 2));
2245
+ return;
2246
+ }
2247
+ if (flags.dryRun === true) {
2248
+ for (const w of sweep.wouldReap) {
2249
+ console.log(` [would-reap] ${w.branch} \u2014 merged`);
2250
+ }
2251
+ for (const w of orphans.wouldReap) {
2252
+ console.log(` [would-reap] ${w.path} \u2014 orphan sidecar`);
2253
+ }
2254
+ }
2255
+ else {
2256
+ for (const r of sweep.reaped) {
2257
+ console.log(` [reaped] ${r.branch} \u2014 merged`);
2258
+ }
2259
+ for (const r of orphans.reaped) {
2260
+ console.log(` [reaped] ${r.path} \u2014 orphan sidecar`);
2261
+ }
2262
+ }
2263
+ for (const ret of sweep.retained) {
2264
+ console.log(` [retained] ${ret.branch} \u2014 ${ret.reasonText}`);
2265
+ }
2266
+ const reapedCount = flags.dryRun === true
2267
+ ? sweep.wouldReap.length + orphans.wouldReap.length
2268
+ : sweep.reaped.length + orphans.reaped.length;
2269
+ const verb = flags.dryRun === true ? 'would reap' : 'reaped';
2270
+ console.log(`Summary: ${reapedCount} ${verb}, ${sweep.retained.length} retained.`);
2271
+ return;
2272
+ }
2273
+ // `--force` discards un-saved work, so it is gated behind an explicit
2274
+ // confirmation (`--yes`) — loud + intentional, NEVER the default (ADR §4).
2275
+ if (flags.force && !flags.yes) {
2276
+ console.error('refusing to --force without --yes: this DISCARDS un-saved work in ' +
2277
+ 'retained worktrees (commits not on the arbiter, dirty trees). ' +
2278
+ 'Re-run with `gc --force --yes` to confirm.');
2279
+ process.exit(1);
2280
+ }
2281
+ if (flags.force) {
2282
+ console.error('>> --force: OVERRIDING the deletion-safety predicate; un-saved work ' +
2283
+ 'in retained worktrees will be DISCARDED.');
2284
+ }
2285
+ const result = gc({
2286
+ workspacesDir,
2287
+ force: flags.force === true,
2288
+ note: (message) => console.error(`>> ${message}`),
2289
+ });
2290
+ if (flags.json) {
2291
+ console.log(JSON.stringify(result, null, 2));
2292
+ return;
2293
+ }
2294
+ for (const reaped of result.reaped) {
2295
+ const how = reaped.forced
2296
+ ? 'FORCED (discarded un-saved work)'
2297
+ : (reaped.verdict.reachableVia ?? 'safe');
2298
+ console.log(` [reaped] ${reaped.slug} \u2014 ${how}`);
2299
+ }
2300
+ for (const retained of result.retained) {
2301
+ console.log(` [retained] ${retained.slug} \u2014 ${RETAIN_REASON_TEXT[retained.reason]}`);
2302
+ }
2303
+ console.log(`Summary: ${result.reaped.length} reaped, ${result.retained.length} retained.`);
2304
+ });
2305
+ program
2306
+ .command('prd-to-spec')
2307
+ .helpGroup(ADVANCED_GROUP)
2308
+ .description("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).")
2309
+ .option('--repo <dir>', 'the repo working-tree root to migrate (default: cwd)')
2310
+ .option('--dry-run', 'REPORT exactly what each layer WOULD change, touching nothing (no writes, no ref renames, no leak-scan)')
2311
+ .option('--json', 'output the raw result as JSON')
2312
+ .action((flags) => {
2313
+ const repoPath = flags.repo ?? process.cwd();
2314
+ const result = runPrdToSpec({
2315
+ repoPath,
2316
+ dryRun: flags.dryRun === true,
2317
+ });
2318
+ if (flags.json) {
2319
+ console.log(JSON.stringify(result, null, 2));
2320
+ }
2321
+ else {
2322
+ printPrdToSpecReport(result);
2323
+ }
2324
+ // Exit non-zero on a REFUSAL (quiescence gate) or a non-green leak scan
2325
+ // (a converted tree that still carries a dangling `prd` ref). A clean
2326
+ // dry-run or a green migration exits 0.
2327
+ if (result.refused || result.leaks.length > 0) {
2328
+ process.exit(1);
2329
+ }
2330
+ });
2331
+ program
2332
+ .command('status')
2333
+ .helpGroup(HEADLINE_GROUP)
2334
+ .description('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).')
2335
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2336
+ .option('--workspace <dir>', 'execution working area to inspect (default: workspacesDir / ~/.dorfl)')
2337
+ .option('--arbiter-remote <name>', `the current repo's arbiter remote to report on (folds in the old \`arbiter status\`; default: ${DEFAULT_ARBITER_REMOTE})`)
2338
+ .option('--arbiter <remote>', 'the COORDINATION arbiter remote whose per-item lock refs gate the cwd selection pool (held in-flight items are subtracted); default: origin')
2339
+ .option('--no-arbiter', "skip the current repo's arbiter section")
2340
+ .option('--here', 'report ONLY the current repo (the cwd working tree, fetch-first): skip the jobs, registry-mirror, and arbiter sections entirely. "This repo, nothing else" — the fast, focused path.')
2341
+ .option('--json', 'output the raw report as JSON')
2342
+ .action(async (flags) => {
2343
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
2344
+ const override = loadConfigOverride(defaultConfigOverridePath(flags.config));
2345
+ const workspacesDir = flags.workspace ?? config.workspacesDir;
2346
+ const warn = (message) => console.error(`>> ${message}`);
2347
+ const resolveCwd = () => resolveCwdSection({
2348
+ cwd: process.cwd(),
2349
+ config,
2350
+ override,
2351
+ arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
2352
+ lockArbiterRemote: flags.arbiter ?? 'origin',
2353
+ warn,
2354
+ });
2355
+ // `--here`: report ONLY the cwd — skip the jobs, registry-mirror, and arbiter
2356
+ // sections entirely ("this repo, nothing else"). `status` is built with NO
2357
+ // jobs (empty workspace view), NO mirrors, and NO arbiter, so only the cwd
2358
+ // block renders.
2359
+ if (flags.here === true) {
2360
+ const cwdSection = await resolveCwd();
2361
+ const report = await status({
2362
+ workspacesDir,
2363
+ mirrorPaths: [],
2364
+ cwd: cwdSection,
2365
+ warn,
2366
+ });
2367
+ if (flags.json) {
2368
+ console.log(JSON.stringify(report, null, 2));
2369
+ }
2370
+ else {
2371
+ console.log(formatStatus(report));
2372
+ }
2373
+ return;
2374
+ }
2375
+ // Surface the folder-native needs-attention set (ADR §12) from each
2376
+ // REGISTERED HUB MIRROR (the registry), read from its bare `main` ref
2377
+ // through the read seam (mirrors have no working tree).
2378
+ const mirrorPaths = listMirrors({ workspacesDir }).map((m) => m.path);
2379
+ // Fold in the current repo's arbiter state (the old `arbiter status`, ADR
2380
+ // §1/§7) unless --no-arbiter. Read-only; tolerates not being in a repo.
2381
+ const arbiter = flags.noArbiter === true
2382
+ ? undefined
2383
+ : arbiterStatus({
2384
+ cwd: process.cwd(),
2385
+ remote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
2386
+ });
2387
+ // The cwd-local section: resolve it ONLY when a participating cwd is NOT
2388
+ // already covered by a registered mirror, via the SAME fetch-free pre-check
2389
+ // `scan` uses (`cwdSectionDisposition`) — so an already-registered cwd is not
2390
+ // re-fetched (the registry/jobs view already covers it) while a mirror-less
2391
+ // cwd is still shown standalone.
2392
+ const disposition = cwdSectionDisposition({
2393
+ cwd: process.cwd(),
2394
+ config,
2395
+ arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
2396
+ });
2397
+ const cwdSection = disposition.participating && !disposition.alsoRegistered
2398
+ ? await resolveCwd()
2399
+ : undefined;
2400
+ const report = await status({
2401
+ workspacesDir,
2402
+ mirrorPaths,
2403
+ arbiter,
2404
+ cwd: cwdSection,
2405
+ warn,
2406
+ });
2407
+ if (flags.json) {
2408
+ console.log(JSON.stringify(report, null, 2));
2409
+ }
2410
+ else {
2411
+ console.log(formatStatus(report));
2412
+ }
2413
+ });
2414
+ program
2415
+ .command('requeue <slug>')
2416
+ .helpGroup(HEADLINE_GROUP)
2417
+ .description('Requeue a STUCK task to the backlog for re-claiming (ADR §12/§14). Recovers a task whose per-item lock is held — stuck (the resolved-recovery path: a previously-routed needs-attention item, now lock `state: stuck`) OR active (a claim that never surfaced — an un-surfaced abort, a killed run, or an in-place requeue note). The body rests in work/tasks/backlog/<slug>.md (claim never moves it under the per-item-lock model); requeue releases the lock so the item is claimable again. The release is published as a TREE-LESS compare-and-swap to the arbiter ref, EXACTLY like claim — it NEVER stages or commits in the cwd working tree, so a requeue in a shared checkout can never sweep up a concurrent writer’s uncommitted files. 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).')
2418
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2419
+ .option('--cwd <dir>', 'the repo/working clone whose work/ tree the arbiter remote is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree')
2420
+ .option('--arbiter <remote>', 'the arbiter git remote the tree-less move is CAS-published to (default: origin). --cwd resolves this remote; the move is never written to the cwd tree.')
2421
+ .option('--reset', '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.')
2422
+ .option('-m, --message <note>', 'append a dated handoff note to the item body for the next agent (append-only; applies to both default and --reset)')
2423
+ .action(async (rawSlug, flags) => {
2424
+ // Task-only command (§3a): accept bare + `task:`, reject `prd:`.
2425
+ const slug = resolveTaskOnlySlug(rawSlug);
2426
+ const cwd = flags.cwd ?? process.cwd();
2427
+ // Route the requeue (default keep+continue / --reset discard / -m handoff)
2428
+ // THROUGH the ledger write seam's transition (same seam the needs-attention
2429
+ // move uses), not the helper.
2430
+ //
2431
+ // `requeue` is a HUMAN command (like `complete`): the human is putting a
2432
+ // resolved item back, so the move/commit/push is THEIRS — it is NOT given
2433
+ // the runner identity (`config.identity`). The autonomous re-attempt is
2434
+ // `do` (which IS identity-aware), not this. We thread the ambient
2435
+ // `process.env` EXPLICITLY so the human-identity choice is visible at the
2436
+ // call site, rather than relying on the seam's silent `?? process.env`
2437
+ // default by omission (the implicit fallback that made `requeue`'s human
2438
+ // attribution accidental rather than declared).
2439
+ const result = await ledgerWrite.applyReturnToBacklogTransition({
2440
+ cwd,
2441
+ slug,
2442
+ // Tree-less CAS needs a ref to push to (parity with `claim`): default the
2443
+ // arbiter to `origin` so the common case Just Works; `--arbiter` overrides.
2444
+ // `--cwd` is purely the ORIGIN SOURCE the remote is resolved from.
2445
+ arbiter: flags.arbiter ?? 'origin',
2446
+ reset: flags.reset,
2447
+ message: flags.message,
2448
+ env: process.env,
2449
+ note: (message) => console.error(`>> ${message}`),
2450
+ });
2451
+ if (!result.moved) {
2452
+ console.error(`error: ${result.reasonNotMoved}`);
2453
+ process.exit(1);
2454
+ }
2455
+ const how = result.deletedRemoteBranch
2456
+ ? ` (--reset: deleted the remote ${workBranchRef('task', slug)} branch; next claim starts fresh)`
2457
+ : ' (kept the work branch; next claim continues from its tip)';
2458
+ console.log(`Requeued '${slug}' to backlog for re-claiming.${how}`);
2459
+ });
2460
+ // `promote [item]` (prd `staging-pool-position-gate-and-trust-model`, tasks
2461
+ // `pre-backlog-staging-folder-and-promote-step-a` /
2462
+ // `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the HUMAN/runner-
2463
+ // owned verb that moves a STAGED item into its agent-eligible POOL — a task
2464
+ // `work/pre-backlog/<slug>.md → work/backlog/<slug>.md`, a prd
2465
+ // `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md` — as a tree-less CAS on the
2466
+ // arbiter, the SAME trust model + mechanism as `requeue`. The agent emits STAGED;
2467
+ // only this verb (a human, or the runner) admits it to the pool. With NO argument
2468
+ // it LISTS what is promotable (the "what is staged waiting for me?" discovery), so
2469
+ // a human need not remember the staged slugs.
2470
+ program
2471
+ .command('promote [item]')
2472
+ .helpGroup(HEADLINE_GROUP)
2473
+ .description('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.')
2474
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2475
+ .option('--cwd <dir>', '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')
2476
+ .option('--arbiter <remote>', 'the arbiter git remote the promotion is CAS-published to / the staging folders are listed from (default: origin)')
2477
+ .action(async (rawItem, flags) => {
2478
+ const cwd = flags.cwd ?? process.cwd();
2479
+ const arbiter = flags.arbiter ?? 'origin';
2480
+ // `promote` is a HUMAN command (like `requeue`): the move/commit/push is
2481
+ // THEIRS — NOT the runner identity. Thread the ambient env explicitly.
2482
+ const env = process.env;
2483
+ const note = (message) => console.error(`>> ${message}`);
2484
+ // NO ARGUMENT → LIST what is promotable (read-only discovery).
2485
+ if (rawItem === undefined) {
2486
+ const listed = await listPromotable({ cwd, arbiter, env });
2487
+ if (listed.error) {
2488
+ console.error(`error: ${listed.error}`);
2489
+ process.exit(1);
2490
+ }
2491
+ if (listed.items.length === 0) {
2492
+ console.log(`Nothing staged to promote on ${arbiter}/main (work/pre-backlog/ and work/prds/proposed/ are empty).`);
2493
+ return;
2494
+ }
2495
+ console.log('Staged, awaiting promotion (run `promote <item>`):');
2496
+ for (const item of listed.items) {
2497
+ console.log(` ${item.namespace}:${item.slug}`);
2498
+ }
2499
+ return;
2500
+ }
2501
+ // AN ITEM → promote it. `task:`/`spec:` are explicit (the legacy `prd:`
2502
+ // prefix is still ACCEPTED as an input alias through the cutover — the
2503
+ // contract task drops it); a bare slug defaults to a task (mirrors
2504
+ // `requeue`). An `obs:`/`observation:` prefix is rejected (observations have
2505
+ // no pool).
2506
+ const parsed = parseSlugArg(rawItem);
2507
+ if (parsed.explicit === 'observation') {
2508
+ console.error(`error: promote takes a task or spec, not an observation ('${rawItem}'). Observations have no agent pool.`);
2509
+ process.exit(1);
2510
+ }
2511
+ // A `spec:` prefix produces the `spec` namespace (the dispatch + messages
2512
+ // speak `spec`); only bare/`task:` stays `task`.
2513
+ const namespace = parsed.explicit === 'spec' ? 'spec' : 'task';
2514
+ const slug = parsed.slug;
2515
+ const result = namespace === 'spec'
2516
+ ? await promoteFromPreSpec({ cwd, slug, arbiter, env, note })
2517
+ : await promoteFromPreBacklog({ cwd, slug, arbiter, env, note });
2518
+ if (!result.moved) {
2519
+ console.error(`error: ${result.reasonNotMoved}`);
2520
+ process.exit(1);
2521
+ }
2522
+ const dest = namespace === 'spec'
2523
+ ? workFolderPrefix('specs-ready')
2524
+ : workFolderPrefix('tasks-ready');
2525
+ console.log(`Promoted ${namespace} '${slug}' into the pool (${dest}); it is now ${namespace === 'spec' ? 'auto-taskable' : 'claimable'}.`);
2526
+ });
2527
+ // NOTE: the legacy `release-advancing <item>` verb is RETIRED by the capstone
2528
+ // cut-over (task `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`):
2529
+ // the `work/advancing/<entry>.md` marker is gone and an advance hold is now just
2530
+ // `action: advance` on the UNIFIED per-item lock, so `release-lock <item>` (below)
2531
+ // is the SOLE named human release for ALL holds (implement/task/advance).
2532
+ // `release-lock <item>` (task `release-lock-verb-and-gc-stuck-report`, prd
2533
+ // `ledger-status-per-item-lock-refs` US #14): the HUMAN-invoked named release of
2534
+ // a stuck/orphaned UNIFIED per-item lock (`refs/dorfl/lock/<entry>`) —
2535
+ // the GENERALISATION of `release-advancing` from the advancing-only marker to
2536
+ // the one lock per item (implement/task/advance × active/stuck). Same trust
2537
+ // model as `release-advancing` / `requeue`: a HUMAN asserts the lock is dead by
2538
+ // NAMING it; the tool never guesses liveness (there is NO heartbeat, NO
2539
+ // auto-sweep — the `gc --ledger` report only REPORTS lingering locks). Routes
2540
+ // through the existing leased-delete `releaseItemLock` (deleting the ref IS the
2541
+ // release; the parentless lock commit becomes gc-reclaimable). NEVER `--force`.
2542
+ // Idempotent: deleting an absent ref is a clean exit-0 "nothing to clear"
2543
+ // (`not-held`), NOT a failure — deleting the lock ref(s) is "all locks released"
2544
+ // and recoverable (the work is safe on the `work/<slug>` branches + `main`).
2545
+ program
2546
+ .command('release-lock <item>')
2547
+ .helpGroup(HEADLINE_GROUP)
2548
+ .description('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).')
2549
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2550
+ .option('--cwd <dir>', 'the repo/working clone whose arbiter remote the lock ref is DELETED on (default: cwd)')
2551
+ .option('--arbiter <remote>', 'the arbiter git remote the lock ref is deleted on (default: origin)')
2552
+ .action(async (item, flags) => {
2553
+ const cwd = flags.cwd ?? process.cwd();
2554
+ const arbiter = flags.arbiter ?? 'origin';
2555
+ const result = await releaseItemLock({
2556
+ item,
2557
+ cwd,
2558
+ arbiter,
2559
+ env: process.env,
2560
+ });
2561
+ if (result.outcome === 'released') {
2562
+ console.log(`Released lock '${result.entry}' (${result.ref} deleted on ${arbiter}; the item itself was untouched — it rests on main / its work/<slug> branch).`);
2563
+ return;
2564
+ }
2565
+ // IDEMPOTENT exit semantics: `releaseItemLock` returns `not-held` when the
2566
+ // ref is ALREADY absent. For a HUMAN re-running the verb on an
2567
+ // already-cleared lock that is the CORRECT "nothing to clear" outcome —
2568
+ // deleting the lock ref(s) is "all locks released" and recoverable — so map
2569
+ // it to a clean exit-0 with an honest message (NOT a failure).
2570
+ if (result.outcome === 'not-held') {
2571
+ console.log(`No lock to release for '${result.entry}' (${result.ref} is already absent on ${arbiter} — “all locks released”, recoverable).`);
2572
+ return;
2573
+ }
2574
+ console.error(`error: ${result.message}`);
2575
+ process.exit(1);
2576
+ });
2577
+ // `drop <slug>` (prd `agentic-question-resolution-retire-disposition-vocabulary`,
2578
+ // US #5/#11; task `direct-delete-question-cli-helper`): the DIRECT "throw it
2579
+ // away" verb — `git rm` a source item AND its question sidecar (when present) in
2580
+ // ONE revertible commit, the reason in the commit MESSAGE (git history is the
2581
+ // archive). It does NOT round-trip through the decision engine or spawn an agent
2582
+ // (that is the SEPARATE agentic `delete-source` verdict in apply-persist.ts);
2583
+ // this is the human/skill/CLI no-ceremony delete of decision 7. DISTINCT from
2584
+ // the existing `remote rm` (the hub-MIRROR deleter) — different concern, no
2585
+ // collision. A LOCAL one-commit primitive over the working tree (like apply): it
2586
+ // does NOT touch the arbiter; the human pushes/integrates the revertible commit
2587
+ // however they normally do.
2588
+ program
2589
+ .command('drop <slug>')
2590
+ .helpGroup(HEADLINE_GROUP)
2591
+ .description('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).')
2592
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2593
+ .option('--cwd <dir>', 'the working clone the revertible delete commit is made in (default: cwd)')
2594
+ .option('--reason <text>', 'why you are throwing this away — recorded in the commit MESSAGE (git history is the archive). Optional; recorded as "(no reason given)" when omitted.')
2595
+ .action((slug, flags) => {
2596
+ const cwd = flags.cwd ?? process.cwd();
2597
+ // `drop` is a DIRECT HUMAN action (like the apply rung's local commit): the
2598
+ // delete/commit is THEIRS, so thread the ambient env explicitly.
2599
+ const env = process.env;
2600
+ const note = (message) => console.error(`>> ${message}`);
2601
+ const result = dropSource({
2602
+ cwd,
2603
+ item: slug,
2604
+ reason: flags.reason,
2605
+ env,
2606
+ note,
2607
+ });
2608
+ if (result.outcome === 'not-found') {
2609
+ // Nothing to throw away (the source is already gone). A clean exit-0
2610
+ // no-op, NOT a failure — deleting something already absent is success.
2611
+ console.log(`Nothing to drop for '${result.item}' — no source item resolves by identity (already gone).`);
2612
+ return;
2613
+ }
2614
+ console.log(`Dropped '${result.item}'${result.sidecarPath ? ' + its sidecar' : ''} in one revertible commit (${result.commit?.slice(0, 8)}; reason in the message). Recover with \`git revert\` if this was wrong.`);
2615
+ });
2616
+ program
2617
+ .command('intake')
2618
+ .helpGroup(HEADLINE_GROUP)
2619
+ .description('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.')
2620
+ .argument('<number>', 'the GitHub issue number to intake (e.g. `intake 42`)')
2621
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2622
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
2623
+ .option('--merge', 'integrate BOTH outcomes (task AND spec) in merge mode (aggregate; overridden per type by --merge-*/--propose-*; mutually exclusive with --propose)')
2624
+ .option('--propose', 'integrate BOTH outcomes (task AND spec) in propose mode (aggregate; default; overridden per type; mutually exclusive with --merge)')
2625
+ .option('--no-pr', '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.')
2626
+ .option('--merge-spec', 'integrate a spec outcome in merge mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --propose-spec)')
2627
+ .option('--propose-spec', 'integrate a spec outcome in propose mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --merge-spec)')
2628
+ .option('--merge-task', 'integrate a task outcome in merge mode (granular; overrides --merge/--propose for a task; mutually exclusive with --propose-task)')
2629
+ .option('--propose-task', 'integrate a task outcome in propose mode (granular; overrides --merge/--propose for a task; mutually exclusive with --merge-task)')
2630
+ .option('--origin-trust <trusted|untrusted>', "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.")
2631
+ .option('--specs-land-in <where>', '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.')
2632
+ .option('--agent-cmd <cmd>', 'command to run the decision agent')
2633
+ .option('--model <id>', 'model the decision agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
2634
+ .option('--harness <adapter>', 'harness adapter that launches the decision agent: null (default) or pi')
2635
+ .option('--pi-bin <path>', 'pi CLI binary the pi harness invokes (default: pi on PATH)')
2636
+ .option('--sessions-dir <dir>', 'HOST-ONLY root folder under which the pi session file is generated')
2637
+ .action(async (rawNumber, flags) => {
2638
+ const issueNumber = Number(rawNumber);
2639
+ if (rawNumber.trim() === '' ||
2640
+ !Number.isInteger(issueNumber) ||
2641
+ issueNumber < 1) {
2642
+ console.error(`error: intake takes a positive issue NUMBER (got '${rawNumber}').`);
2643
+ process.exit(1);
2644
+ }
2645
+ const cwd = process.cwd();
2646
+ const { global, override } = loadGlobalAndOverride(flags.config);
2647
+ const resolved = resolveRepoConfig({
2648
+ repoPath: cwd,
2649
+ global,
2650
+ override,
2651
+ flags: {
2652
+ ...harnessFlagOverrides(flags),
2653
+ // `--no-pr` (the PR-INTENT axis) rides the SAME chain.
2654
+ ...noPRFlagOverrides(flags),
2655
+ },
2656
+ });
2657
+ if (resolved.message) {
2658
+ console.error(`>> ${resolved.message}`);
2659
+ }
2660
+ const config = resolved.config;
2661
+ // Resolve the PER-OUTCOME integration modes (prd US #9): `intake` decides
2662
+ // the artifact TYPE at runtime, so a single --merge/--propose can't express
2663
+ // a type-conditional policy. The granular flags override the aggregate; an
2664
+ // UNSET type falls back to the per-repo/global `integration` (the SAME chain
2665
+ // `do`/`complete` use — flag > per-repo > global > default propose). `intake`
2666
+ // is GATE-FREE, so autoTask/autoBuild are NOT consulted (the explicit
2667
+ // invocation is its own authorization). `intake` owns only these KNOBS; WHICH
2668
+ // knobs CI sets is CI's POLICY (`runner-in-ci`), NOT here.
2669
+ let modes;
2670
+ try {
2671
+ modes = resolveIntakeIntegrationModes(flags, config.integration);
2672
+ }
2673
+ catch (err) {
2674
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
2675
+ process.exit(1);
2676
+ }
2677
+ // The ORIGIN-TRUST stamp (task `untrusted-origin-forces-build-propose`):
2678
+ // the CI shell passes `--origin-trust <trusted|untrusted>`; `intake` writes
2679
+ // it onto the emitted artifact (it does NOT resolve trust — the ~L296
2680
+ // boundary). UNSET ⇒ undefined ⇒ emit unstamped (a local intake is
2681
+ // human/trusted). An INVALID value FAILS LOUDLY (an autonomy/trust signal
2682
+ // must never be quietly ignored), mirroring the observation-triage enum.
2683
+ let originTrust;
2684
+ if (flags.originTrust !== undefined) {
2685
+ if (flags.originTrust !== 'trusted' &&
2686
+ flags.originTrust !== 'untrusted') {
2687
+ console.error(`error: --origin-trust must be 'trusted' or 'untrusted' (got '${flags.originTrust}').`);
2688
+ process.exit(1);
2689
+ }
2690
+ originTrust = flags.originTrust;
2691
+ }
2692
+ const harness = createHarness({
2693
+ harness: config.harness,
2694
+ piBin: config.piBin,
2695
+ });
2696
+ // The OPERATOR's EXPLICIT spec-placement override (`--specs-land-in`), the
2697
+ // TOP of the placement precedence — mirrors `explicitTasksLandInFromFlag` on
2698
+ // the `do spec:` path. Fails loudly on a bad value.
2699
+ let explicitSpecsLandIn;
2700
+ try {
2701
+ explicitSpecsLandIn = explicitSpecsLandInFromFlag(flags.specsLandIn);
2702
+ }
2703
+ catch (err) {
2704
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
2705
+ process.exit(1);
2706
+ }
2707
+ const result = await performIntake({
2708
+ issueNumber,
2709
+ cwd,
2710
+ arbiter: flags.arbiter ?? config.defaultArbiter,
2711
+ integration: modes,
2712
+ // The origin-trust stamp the CI shell passes IN (unset ⇒ unstamped).
2713
+ originTrust,
2714
+ noPR: config.noPR,
2715
+ // SPEC-PLACEMENT: the configured-default `specsLandIn` rung + the EXPLICIT
2716
+ // `--specs-land-in` override (top of the precedence). The shared placement
2717
+ // resolver in `intake.ts` overlays the untrusted-origin staging force.
2718
+ specsLandIn: config.specsLandIn,
2719
+ explicitSpecsLandIn,
2720
+ harness,
2721
+ agentCmd: config.agentCmd,
2722
+ model: config.model,
2723
+ sessionsDir: config.sessionsDir,
2724
+ // Host-only runner IDENTITY — scopes intake's `gh`/git ops (not the
2725
+ // decision/review AGENT launches); absent ⇒ ambient.
2726
+ identity: config.identity,
2727
+ note: (message) => console.error(`>> ${message}`),
2728
+ });
2729
+ if (result.exitCode !== 0) {
2730
+ console.error(`error: ${result.message}`);
2731
+ }
2732
+ else {
2733
+ console.error(`>> ${result.message}`);
2734
+ }
2735
+ process.exit(result.exitCode);
2736
+ });
2737
+ // The CI CLOSE-JOB driver (prd `runner-in-ci`, capability E; task
2738
+ // `install-ci-close-job-workflow`). The thin JOB the emitted close-job workflow
2739
+ // invokes on a merge to main: resolve which source issue(s) the landed work
2740
+ // closes (resolveClosingIssue), run the "prd complete?" query for the prd case
2741
+ // (prd-complete-query, done), and close via the IssueProvider seam — all
2742
+ // UNCHANGED engine pieces, CONSUMED not re-built (the Out-of-Scope fence). CI
2743
+ // owns ONLY the job + trigger. Local-runnable too (a manual catch-up close).
2744
+ program
2745
+ .command('close-merged-issues')
2746
+ .helpGroup(ADVANCED_GROUP)
2747
+ .description('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.')
2748
+ .option('--cwd <dir>', 'the repo working dir whose work/ tree to scan (default: cwd)')
2749
+ .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
2750
+ .option('--json', 'output the raw result as JSON')
2751
+ .action(async (flags) => {
2752
+ const repoPath = flags.cwd ?? process.cwd();
2753
+ const result = await performCloseMergedIssues({
2754
+ repoPath,
2755
+ ghBin: flags.ghBin,
2756
+ env: process.env,
2757
+ });
2758
+ if (flags.json) {
2759
+ console.log(JSON.stringify(result, null, 2));
2760
+ }
2761
+ else {
2762
+ for (const c of result.candidates) {
2763
+ if (c.decision === 'closed') {
2764
+ console.error(`>> closed issue #${c.issueNumber} (${c.via} ${c.slug}).`);
2765
+ }
2766
+ else if (c.decision === 'close-failed') {
2767
+ console.error(`>> issue #${c.issueNumber} (${c.via} ${c.slug}) NOT closed: ${c.reason}`);
2768
+ }
2769
+ else {
2770
+ console.error(`>> issue #${c.issueNumber} (${c.via} ${c.slug}) left open (${c.decision}).`);
2771
+ }
2772
+ }
2773
+ console.error(`>> close-merged-issues: closed ${result.closed.length} issue(s).`);
2774
+ }
2775
+ // The close-job is a terminal CI tick: a degraded close is reported, not a
2776
+ // crash (exit 0), exactly like intake's bounce close.
2777
+ process.exit(0);
2778
+ });
2779
+ // The REGISTRY command group (ADR §1): the registered set of targets IS the
2780
+ // hub-mirror set on disk. `remote add --local` absorbs the old `arbiter init`;
2781
+ // `arbiter status` is folded into `status`. There is no standalone `arbiter`
2782
+ // command group, and no `roots`/`remotes` config field.
2783
+ const remote = program
2784
+ .command('remote')
2785
+ .helpGroup(HEADLINE_GROUP)
2786
+ .description('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`.');
2787
+ remote
2788
+ .command('add <target>')
2789
+ .helpGroup(HEADLINE_GROUP)
2790
+ .description('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).')
2791
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2792
+ .option('--local', 'provision a local --bare arbiter from <target> (a working repo) and register it (absorbs `arbiter init`)')
2793
+ .option('--arbiter-remote <name>', `name of the arbiter remote to wire in the working repo on --local (default: ${DEFAULT_ARBITER_REMOTE})`)
2794
+ .option('--force', '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)')
2795
+ .action((target, flags) => {
2796
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
2797
+ try {
2798
+ const result = remoteAdd({
2799
+ target,
2800
+ local: flags.local,
2801
+ workspacesDir: config.workspacesDir,
2802
+ arbitersDir: config.arbitersDir,
2803
+ arbiterRemote: flags.arbiterRemote ?? DEFAULT_ARBITER_REMOTE,
2804
+ force: flags.force,
2805
+ note: (message) => console.error(`>> ${message}`),
2806
+ });
2807
+ if (result.arbiter) {
2808
+ const a = result.arbiter;
2809
+ console.log(a.created
2810
+ ? `Provisioned bare arbiter at ${a.path}`
2811
+ : `Arbiter already exists at ${a.path} (not clobbered)`);
2812
+ console.log(`Wired remote '${a.remote}' -> ${a.url}`);
2813
+ }
2814
+ console.log(result.created
2815
+ ? `Registered '${result.key}' (${result.transport}) — hub mirror at ${result.mirrorPath}`
2816
+ : `'${result.key}' already registered (mirror at ${result.mirrorPath})`);
2817
+ }
2818
+ catch (err) {
2819
+ if (err instanceof RegistryError) {
2820
+ console.error(`error: ${err.message}`);
2821
+ process.exit(1);
2822
+ }
2823
+ throw err;
2824
+ }
2825
+ });
2826
+ remote
2827
+ .command('rm <target>')
2828
+ .helpGroup(ADVANCED_GROUP)
2829
+ .description('Delete a hub mirror by key (host/org/name) or origin URL. The ONLY mirror deleter — `gc` NEVER reaps mirrors. Plumbing tier.')
2830
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2831
+ .action((target, flags) => {
2832
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
2833
+ const result = remoteRm({ target, workspacesDir: config.workspacesDir });
2834
+ if (!result.removed) {
2835
+ console.error(`error: no registered mirror matches '${target}'.`);
2836
+ process.exit(1);
2837
+ }
2838
+ console.log(`Removed mirror '${result.key}' (${result.path}).`);
2839
+ });
2840
+ remote
2841
+ .command('ls')
2842
+ .helpGroup(HEADLINE_GROUP)
2843
+ .description('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.')
2844
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2845
+ .option('--json', 'output the raw list as JSON')
2846
+ .action((flags) => {
2847
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
2848
+ const mirrors = listMirrors({ workspacesDir: config.workspacesDir });
2849
+ if (flags.json) {
2850
+ console.log(JSON.stringify(mirrors, null, 2));
2851
+ return;
2852
+ }
2853
+ if (mirrors.length === 0) {
2854
+ console.log('No registered mirrors. Use `remote add <url>` or `remote find <folder>`.');
2855
+ return;
2856
+ }
2857
+ for (const m of mirrors) {
2858
+ console.log(`${m.key} ${m.transport} ${m.originUrl ?? '(no origin)'}`);
2859
+ }
2860
+ });
2861
+ remote
2862
+ .command('find <folder>')
2863
+ .helpGroup(HEADLINE_GROUP)
2864
+ .description('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.')
2865
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
2866
+ .option('--yes', 'add all discovered participating repos (no prompt)')
2867
+ .action(async (folder, flags) => {
2868
+ const config = resolveGlobalConfig(loadConfig(flags.config), {});
2869
+ const repos = findParticipatingRepos(folder);
2870
+ if (repos.length === 0) {
2871
+ console.log(`No work/-participating repos found under ${folder}.`);
2872
+ return;
2873
+ }
2874
+ const chosen = flags.yes ? repos : await promptMultiSelect(repos);
2875
+ if (chosen.length === 0) {
2876
+ console.log('Nothing selected; no mirrors added.');
2877
+ return;
2878
+ }
2879
+ for (const repoPath of chosen) {
2880
+ // Each discovered repo is registered as a LOCAL bare arbiter (it is a
2881
+ // working checkout on disk, not a remote URL) — the same path
2882
+ // `remote add --local` takes. The transport guard still applies.
2883
+ try {
2884
+ const result = remoteAdd({
2885
+ target: repoPath,
2886
+ local: true,
2887
+ workspacesDir: config.workspacesDir,
2888
+ arbitersDir: config.arbitersDir,
2889
+ arbiterRemote: DEFAULT_ARBITER_REMOTE,
2890
+ note: (message) => console.error(`>> ${message}`),
2891
+ });
2892
+ console.log(`${result.created ? 'Registered' : 'Already registered'} '${result.key}' (${repoPath}).`);
2893
+ }
2894
+ catch (err) {
2895
+ if (err instanceof RegistryError) {
2896
+ console.error(`skipped ${repoPath}: ${err.message}`);
2897
+ continue;
2898
+ }
2899
+ throw err;
2900
+ }
2901
+ }
2902
+ });
2903
+ program
2904
+ .command('install-ci')
2905
+ .helpGroup(ADVANCED_GROUP)
2906
+ .description('Scaffold the CI auth/setup foundation (a one-time, human-run SCAFFOLDER): write the shared composite setup action (`dorfl-setup`) + provider auth (models.json default, or auth.json + GH_PAT + OAuth refresh) and set the provider secrets via the GitHub seam. Interactive wizard, or `--config <file>` for a non-interactive reproduction; `--export-config` round-trips the config; `--fake` writes to `.fake/` (never `.github/`) and sets NO real secret (a snapshot dry-run).')
2907
+ .option('--config <file>', 'non-interactive: load the CI config from this JSON file (skips the wizard)')
2908
+ .option('--export-config <file>', 'write the gathered config as JSON to this path instead of generating artifacts')
2909
+ .option('--include-secrets', '(with --export-config) also gather + include the secret values in the export')
2910
+ .option('--fake', 'snapshot mode: write artifacts to `.fake/` (NEVER `.github/`) and set NO real secret')
2911
+ .option('--repo <owner/repo>', 'the GitHub repo to set secrets on (else auto-detected via gh)')
2912
+ .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
2913
+ .option('--cwd <dir>', 'the target repo working dir (default: cwd)')
2914
+ .option('--install-source <registry|workspace>', '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.')
2915
+ .option('--max-parallel <n>', '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.')
2916
+ .action(async (flags) => {
2917
+ const workDir = flags.cwd ?? process.cwd();
2918
+ if (flags.installSource !== undefined &&
2919
+ flags.installSource !== 'registry' &&
2920
+ flags.installSource !== 'workspace') {
2921
+ console.error(`install-ci: --install-source must be "registry" or "workspace" (got "${flags.installSource}")`);
2922
+ process.exitCode = 1;
2923
+ return;
2924
+ }
2925
+ let maxParallel;
2926
+ if (flags.maxParallel !== undefined) {
2927
+ const n = Number(flags.maxParallel);
2928
+ if (!Number.isInteger(n) || n < 1) {
2929
+ console.error(`install-ci: --max-parallel must be a positive integer (got "${flags.maxParallel}")`);
2930
+ process.exitCode = 1;
2931
+ return;
2932
+ }
2933
+ maxParallel = n;
2934
+ }
2935
+ const ctx = new GitHubCIContext({
2936
+ workDir,
2937
+ repo: flags.repo,
2938
+ ghBin: flags.ghBin,
2939
+ });
2940
+ // Discover the registered capability emitters (the directory-of-modules
2941
+ // seam: each capability self-registers from its own file under
2942
+ // `install-ci-capabilities/`, picked up here WITHOUT a shared-list edit).
2943
+ // This core task ships only a no-op reference (emits []); the sibling
2944
+ // capability tasks add self-registering modules that flow through here
2945
+ // automatically once landed. A no-op emitter contributes nothing.
2946
+ const capabilities = await loadCapabilityRegistry();
2947
+ const prompts = flags.config ? undefined : readlinePrompts();
2948
+ await installCI({
2949
+ ctx,
2950
+ fake: flags.fake === true,
2951
+ configFile: flags.config,
2952
+ exportConfig: flags.exportConfig,
2953
+ includeSecrets: flags.includeSecrets === true,
2954
+ installSource: flags.installSource,
2955
+ maxParallel,
2956
+ prompts,
2957
+ capabilities,
2958
+ log: (line) => console.error(line),
2959
+ });
2960
+ });
2961
+ return program;
2962
+ }
2963
+ /**
2964
+ * A readline-backed {@link WizardPrompts} for the interactive `install-ci`
2965
+ * wizard. Prompts go to stderr (stdout is reserved for any machine output); a
2966
+ * non-TTY invocation falls back to defaults (or empty), so a piped run never
2967
+ * hangs — use `--config` for a fully non-interactive reproduction.
2968
+ */
2969
+ function readlinePrompts() {
2970
+ const ask = (message, mask = false) => new Promise((resolvePrompt) => {
2971
+ if (!process.stdin.isTTY) {
2972
+ resolvePrompt('');
2973
+ return;
2974
+ }
2975
+ const rl = createInterface({
2976
+ input: process.stdin,
2977
+ output: process.stderr,
2978
+ });
2979
+ void mask; // readline has no native masking; secrets are typed visibly
2980
+ rl.question(`${message} `, (answer) => {
2981
+ rl.close();
2982
+ resolvePrompt(answer);
2983
+ });
2984
+ });
2985
+ return {
2986
+ async input(message, opts) {
2987
+ const hint = opts?.default ? ` [${opts.default}]` : '';
2988
+ const answer = (await ask(`${message}${hint}`)).trim();
2989
+ return answer === '' && opts?.default ? opts.default : answer;
2990
+ },
2991
+ async password(message) {
2992
+ return (await ask(message, true)).trim();
2993
+ },
2994
+ async confirm(message, opts) {
2995
+ const hint = opts.default ? ' [Y/n]' : ' [y/N]';
2996
+ const answer = (await ask(`${message}${hint}`)).trim().toLowerCase();
2997
+ if (answer === '')
2998
+ return opts.default;
2999
+ return answer === 'y' || answer === 'yes';
3000
+ },
3001
+ async select(message, choices) {
3002
+ process.stderr.write(`${message}\n`);
3003
+ choices.forEach((c, i) => {
3004
+ process.stderr.write(` [${i + 1}] ${c.name}\n`);
3005
+ });
3006
+ const answer = (await ask('Choose (number):')).trim();
3007
+ const n = Number(answer);
3008
+ if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
3009
+ return choices[n - 1].value;
3010
+ }
3011
+ return choices[0].value; // default to the first choice
3012
+ },
3013
+ };
3014
+ }
3015
+ /**
3016
+ * A minimal interactive multi-select toggle for `remote find`: list the
3017
+ * discovered repos numbered, let the user type the numbers to add (space/comma
3018
+ * separated; `all` for everything; blank for none). A non-interactive (no TTY)
3019
+ * invocation selects nothing — use `--yes` to add all without a prompt.
3020
+ */
3021
+ function promptMultiSelect(repos) {
3022
+ return new Promise((resolvePrompt) => {
3023
+ if (!process.stdin.isTTY) {
3024
+ resolvePrompt([]);
3025
+ return;
3026
+ }
3027
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
3028
+ process.stderr.write('Discovered work/-participating repos:\n');
3029
+ repos.forEach((repo, i) => {
3030
+ process.stderr.write(` [${i + 1}] ${repo}\n`);
3031
+ });
3032
+ rl.question('Add which? (numbers, `all`, or blank for none) ', (answer) => {
3033
+ rl.close();
3034
+ const trimmed = answer.trim().toLowerCase();
3035
+ if (trimmed === '') {
3036
+ resolvePrompt([]);
3037
+ return;
3038
+ }
3039
+ if (trimmed === 'all') {
3040
+ resolvePrompt([...repos]);
3041
+ return;
3042
+ }
3043
+ const picks = new Set();
3044
+ for (const token of trimmed.split(/[\s,]+/)) {
3045
+ const n = Number(token);
3046
+ if (Number.isInteger(n) && n >= 1 && n <= repos.length) {
3047
+ picks.add(repos[n - 1]);
3048
+ }
3049
+ }
3050
+ resolvePrompt([...picks]);
3051
+ });
3052
+ });
3053
+ }
3054
+ /**
3055
+ * Run the CLI: build the program and parse argv. Split from {@link buildProgram}
3056
+ * so tests can build + introspect/parse the program WITHOUT triggering a real
3057
+ * argv parse + `process.exit` on import (the module-level bootstrap below only
3058
+ * fires when this file is the process entry point).
3059
+ */
3060
+ export async function runCli(argv = process.argv) {
3061
+ const program = buildProgram();
3062
+ try {
3063
+ await program.parseAsync(argv);
3064
+ }
3065
+ catch (err) {
3066
+ console.error(err instanceof Error ? err.message : String(err));
3067
+ process.exit(1);
3068
+ }
3069
+ }
3070
+ // Only bootstrap when invoked as the entry point (the installed `bin`), never on
3071
+ // import (so `buildProgram`/`runCli` are import-safe for tests).
3072
+ if (isCliEntryPoint()) {
3073
+ void runCli();
3074
+ }
3075
+ /**
3076
+ * True iff this module is the process entry point (the `dorfl` bin).
3077
+ * Resolves both sides through `realpathSync` so a bin SYMLINK (npm/pnpm install
3078
+ * a `node_modules/.bin/dorfl` link to `dist/cli.js`) still matches.
3079
+ */
3080
+ function isCliEntryPoint() {
3081
+ const entry = process.argv[1];
3082
+ if (!entry) {
3083
+ return false;
3084
+ }
3085
+ try {
3086
+ const entryReal = realpathSync(entry);
3087
+ const selfReal = realpathSync(fileURLToPath(import.meta.url));
3088
+ return entryReal === selfReal;
3089
+ }
3090
+ catch {
3091
+ return false;
3092
+ }
3093
+ }
3094
+ //# sourceMappingURL=cli.js.map