dorfl 0.0.0 → 0.1.1

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