dorfl 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (619) hide show
  1. package/dist/advance-ci-template.d.ts +73 -0
  2. package/dist/advance-ci-template.d.ts.map +1 -0
  3. package/dist/advance-ci-template.js +104 -0
  4. package/dist/advance-ci-template.js.map +1 -0
  5. package/dist/advance-classify.d.ts +132 -0
  6. package/dist/advance-classify.d.ts.map +1 -0
  7. package/dist/advance-classify.js +120 -0
  8. package/dist/advance-classify.js.map +1 -0
  9. package/dist/advance-drivers.d.ts +182 -0
  10. package/dist/advance-drivers.d.ts.map +1 -0
  11. package/dist/advance-drivers.js +231 -0
  12. package/dist/advance-drivers.js.map +1 -0
  13. package/dist/advance-isolated.d.ts +156 -0
  14. package/dist/advance-isolated.d.ts.map +1 -0
  15. package/dist/advance-isolated.js +256 -0
  16. package/dist/advance-isolated.js.map +1 -0
  17. package/dist/advance-lifecycle-template.d.ts +107 -0
  18. package/dist/advance-lifecycle-template.d.ts.map +1 -0
  19. package/dist/advance-lifecycle-template.js +668 -0
  20. package/dist/advance-lifecycle-template.js.map +1 -0
  21. package/dist/advance-loop-driver.d.ts +325 -0
  22. package/dist/advance-loop-driver.d.ts.map +1 -0
  23. package/dist/advance-loop-driver.js +437 -0
  24. package/dist/advance-loop-driver.js.map +1 -0
  25. package/dist/advance-treeless-publish.d.ts +108 -0
  26. package/dist/advance-treeless-publish.d.ts.map +1 -0
  27. package/dist/advance-treeless-publish.js +71 -0
  28. package/dist/advance-treeless-publish.js.map +1 -0
  29. package/dist/advance.d.ts +340 -0
  30. package/dist/advance.d.ts.map +1 -0
  31. package/dist/advance.js +1122 -0
  32. package/dist/advance.js.map +1 -0
  33. package/dist/advancing-lock.d.ts +294 -0
  34. package/dist/advancing-lock.d.ts.map +1 -0
  35. package/dist/advancing-lock.js +594 -0
  36. package/dist/advancing-lock.js.map +1 -0
  37. package/dist/agent-launch.d.ts +79 -0
  38. package/dist/agent-launch.d.ts.map +1 -0
  39. package/dist/agent-launch.js +61 -0
  40. package/dist/agent-launch.js.map +1 -0
  41. package/dist/agent-stop.d.ts +149 -0
  42. package/dist/agent-stop.d.ts.map +1 -0
  43. package/dist/agent-stop.js +307 -0
  44. package/dist/agent-stop.js.map +1 -0
  45. package/dist/apply-decide.d.ts +127 -0
  46. package/dist/apply-decide.d.ts.map +1 -0
  47. package/dist/apply-decide.js +176 -0
  48. package/dist/apply-decide.js.map +1 -0
  49. package/dist/apply-merge-action.d.ts +206 -0
  50. package/dist/apply-merge-action.d.ts.map +1 -0
  51. package/dist/apply-merge-action.js +307 -0
  52. package/dist/apply-merge-action.js.map +1 -0
  53. package/dist/apply-persist.d.ts +174 -0
  54. package/dist/apply-persist.d.ts.map +1 -0
  55. package/dist/apply-persist.js +359 -0
  56. package/dist/apply-persist.js.map +1 -0
  57. package/dist/arbiter.d.ts +120 -0
  58. package/dist/arbiter.d.ts.map +1 -0
  59. package/dist/arbiter.js +255 -0
  60. package/dist/arbiter.js.map +1 -0
  61. package/dist/brand.d.ts +70 -0
  62. package/dist/brand.d.ts.map +1 -0
  63. package/dist/brand.js +84 -0
  64. package/dist/brand.js.map +1 -0
  65. package/dist/buildable-body.d.ts +132 -0
  66. package/dist/buildable-body.d.ts.map +1 -0
  67. package/dist/buildable-body.js +131 -0
  68. package/dist/buildable-body.js.map +1 -0
  69. package/dist/categorise.d.ts +66 -0
  70. package/dist/categorise.d.ts.map +1 -0
  71. package/dist/categorise.js +106 -0
  72. package/dist/categorise.js.map +1 -0
  73. package/dist/claim-cas.d.ts +117 -0
  74. package/dist/claim-cas.d.ts.map +1 -0
  75. package/dist/claim-cas.js +312 -0
  76. package/dist/claim-cas.js.map +1 -0
  77. package/dist/cli-spinner.d.ts +112 -0
  78. package/dist/cli-spinner.d.ts.map +1 -0
  79. package/dist/cli-spinner.js +157 -0
  80. package/dist/cli-spinner.js.map +1 -0
  81. package/dist/cli.d.ts +11 -0
  82. package/dist/cli.d.ts.map +1 -0
  83. package/dist/cli.js +3094 -0
  84. package/dist/cli.js.map +1 -0
  85. package/dist/close-job-template.d.ts +70 -0
  86. package/dist/close-job-template.d.ts.map +1 -0
  87. package/dist/close-job-template.js +180 -0
  88. package/dist/close-job-template.js.map +1 -0
  89. package/dist/close-job.d.ts +95 -0
  90. package/dist/close-job.d.ts.map +1 -0
  91. package/dist/close-job.js +226 -0
  92. package/dist/close-job.js.map +1 -0
  93. package/dist/complete.d.ts +361 -0
  94. package/dist/complete.d.ts.map +1 -0
  95. package/dist/complete.js +885 -0
  96. package/dist/complete.js.map +1 -0
  97. package/dist/concurrency.d.ts +68 -0
  98. package/dist/concurrency.d.ts.map +1 -0
  99. package/dist/concurrency.js +112 -0
  100. package/dist/concurrency.js.map +1 -0
  101. package/dist/config-override.d.ts +76 -0
  102. package/dist/config-override.d.ts.map +1 -0
  103. package/dist/config-override.js +50 -0
  104. package/dist/config-override.js.map +1 -0
  105. package/dist/config.d.ts +668 -0
  106. package/dist/config.d.ts.map +1 -0
  107. package/dist/config.js +241 -0
  108. package/dist/config.js.map +1 -0
  109. package/dist/continue-branch.d.ts +249 -0
  110. package/dist/continue-branch.d.ts.map +1 -0
  111. package/dist/continue-branch.js +389 -0
  112. package/dist/continue-branch.js.map +1 -0
  113. package/dist/cwd-section.d.ts +186 -0
  114. package/dist/cwd-section.d.ts.map +1 -0
  115. package/dist/cwd-section.js +209 -0
  116. package/dist/cwd-section.js.map +1 -0
  117. package/dist/decision-engine.d.ts +170 -0
  118. package/dist/decision-engine.d.ts.map +1 -0
  119. package/dist/decision-engine.js +136 -0
  120. package/dist/decision-engine.js.map +1 -0
  121. package/dist/detect.d.ts +17 -0
  122. package/dist/detect.d.ts.map +1 -0
  123. package/dist/detect.js +118 -0
  124. package/dist/detect.js.map +1 -0
  125. package/dist/do-autopick.d.ts +85 -0
  126. package/dist/do-autopick.d.ts.map +1 -0
  127. package/dist/do-autopick.js +112 -0
  128. package/dist/do-autopick.js.map +1 -0
  129. package/dist/do-config.d.ts +312 -0
  130. package/dist/do-config.d.ts.map +1 -0
  131. package/dist/do-config.js +358 -0
  132. package/dist/do-config.js.map +1 -0
  133. package/dist/do-remote-auto.d.ts +75 -0
  134. package/dist/do-remote-auto.d.ts.map +1 -0
  135. package/dist/do-remote-auto.js +111 -0
  136. package/dist/do-remote-auto.js.map +1 -0
  137. package/dist/do.d.ts +621 -0
  138. package/dist/do.d.ts.map +1 -0
  139. package/dist/do.js +1882 -0
  140. package/dist/do.js.map +1 -0
  141. package/dist/drop-source.d.ts +96 -0
  142. package/dist/drop-source.d.ts.map +1 -0
  143. package/dist/drop-source.js +91 -0
  144. package/dist/drop-source.js.map +1 -0
  145. package/dist/eligibility.d.ts +46 -0
  146. package/dist/eligibility.d.ts.map +1 -0
  147. package/dist/eligibility.js +34 -0
  148. package/dist/eligibility.js.map +1 -0
  149. package/dist/env-config.d.ts +51 -0
  150. package/dist/env-config.d.ts.map +1 -0
  151. package/dist/env-config.js +272 -0
  152. package/dist/env-config.js.map +1 -0
  153. package/dist/failure-cause.d.ts +70 -0
  154. package/dist/failure-cause.d.ts.map +1 -0
  155. package/dist/failure-cause.js +126 -0
  156. package/dist/failure-cause.js.map +1 -0
  157. package/dist/format.d.ts +43 -0
  158. package/dist/format.d.ts.map +1 -0
  159. package/dist/format.js +256 -0
  160. package/dist/format.js.map +1 -0
  161. package/dist/frontmatter.d.ts +215 -0
  162. package/dist/frontmatter.d.ts.map +1 -0
  163. package/dist/frontmatter.js +345 -0
  164. package/dist/frontmatter.js.map +1 -0
  165. package/dist/gate-readiness.d.ts +84 -0
  166. package/dist/gate-readiness.d.ts.map +1 -0
  167. package/dist/gate-readiness.js +103 -0
  168. package/dist/gate-readiness.js.map +1 -0
  169. package/dist/gc.d.ts +165 -0
  170. package/dist/gc.d.ts.map +1 -0
  171. package/dist/gc.js +313 -0
  172. package/dist/gc.js.map +1 -0
  173. package/dist/gh-failure.d.ts +42 -0
  174. package/dist/gh-failure.d.ts.map +1 -0
  175. package/dist/gh-failure.js +49 -0
  176. package/dist/gh-failure.js.map +1 -0
  177. package/dist/git.d.ts +75 -0
  178. package/dist/git.d.ts.map +1 -0
  179. package/dist/git.js +130 -0
  180. package/dist/git.js.map +1 -0
  181. package/dist/github.d.ts +187 -0
  182. package/dist/github.d.ts.map +1 -0
  183. package/dist/github.js +343 -0
  184. package/dist/github.js.map +1 -0
  185. package/dist/harness.d.ts +242 -0
  186. package/dist/harness.d.ts.map +1 -0
  187. package/dist/harness.js +157 -0
  188. package/dist/harness.js.map +1 -0
  189. package/dist/identity.d.ts +167 -0
  190. package/dist/identity.d.ts.map +1 -0
  191. package/dist/identity.js +231 -0
  192. package/dist/identity.js.map +1 -0
  193. package/dist/index.d.ts +147 -0
  194. package/dist/index.d.ts.map +1 -0
  195. package/dist/index.js +76 -0
  196. package/dist/index.js.map +1 -0
  197. package/dist/install-ci-branch-protection.d.ts +147 -0
  198. package/dist/install-ci-branch-protection.d.ts.map +1 -0
  199. package/dist/install-ci-branch-protection.js +166 -0
  200. package/dist/install-ci-branch-protection.js.map +1 -0
  201. package/dist/install-ci-capabilities/advance-lifecycle.d.ts +15 -0
  202. package/dist/install-ci-capabilities/advance-lifecycle.d.ts.map +1 -0
  203. package/dist/install-ci-capabilities/advance-lifecycle.js +28 -0
  204. package/dist/install-ci-capabilities/advance-lifecycle.js.map +1 -0
  205. package/dist/install-ci-capabilities/close-job.d.ts +13 -0
  206. package/dist/install-ci-capabilities/close-job.d.ts.map +1 -0
  207. package/dist/install-ci-capabilities/close-job.js +26 -0
  208. package/dist/install-ci-capabilities/close-job.js.map +1 -0
  209. package/dist/install-ci-capabilities/example-noop.d.ts +16 -0
  210. package/dist/install-ci-capabilities/example-noop.d.ts.map +1 -0
  211. package/dist/install-ci-capabilities/example-noop.js +23 -0
  212. package/dist/install-ci-capabilities/example-noop.js.map +1 -0
  213. package/dist/install-ci-capabilities/intake.d.ts +15 -0
  214. package/dist/install-ci-capabilities/intake.d.ts.map +1 -0
  215. package/dist/install-ci-capabilities/intake.js +28 -0
  216. package/dist/install-ci-capabilities/intake.js.map +1 -0
  217. package/dist/install-ci-capabilities/verify.d.ts +14 -0
  218. package/dist/install-ci-capabilities/verify.d.ts.map +1 -0
  219. package/dist/install-ci-capabilities/verify.js +27 -0
  220. package/dist/install-ci-capabilities/verify.js.map +1 -0
  221. package/dist/install-ci-core.d.ts +446 -0
  222. package/dist/install-ci-core.d.ts.map +1 -0
  223. package/dist/install-ci-core.js +760 -0
  224. package/dist/install-ci-core.js.map +1 -0
  225. package/dist/install-ci-github.d.ts +167 -0
  226. package/dist/install-ci-github.d.ts.map +1 -0
  227. package/dist/install-ci-github.js +315 -0
  228. package/dist/install-ci-github.js.map +1 -0
  229. package/dist/install-ci.d.ts +105 -0
  230. package/dist/install-ci.d.ts.map +1 -0
  231. package/dist/install-ci.js +363 -0
  232. package/dist/install-ci.js.map +1 -0
  233. package/dist/intake-event.d.ts +88 -0
  234. package/dist/intake-event.d.ts.map +1 -0
  235. package/dist/intake-event.js +66 -0
  236. package/dist/intake-event.js.map +1 -0
  237. package/dist/intake-marker.d.ts +95 -0
  238. package/dist/intake-marker.d.ts.map +1 -0
  239. package/dist/intake-marker.js +127 -0
  240. package/dist/intake-marker.js.map +1 -0
  241. package/dist/intake-triage.d.ts +48 -0
  242. package/dist/intake-triage.d.ts.map +1 -0
  243. package/dist/intake-triage.js +95 -0
  244. package/dist/intake-triage.js.map +1 -0
  245. package/dist/intake-trigger-template.d.ts +185 -0
  246. package/dist/intake-trigger-template.d.ts.map +1 -0
  247. package/dist/intake-trigger-template.js +449 -0
  248. package/dist/intake-trigger-template.js.map +1 -0
  249. package/dist/intake.d.ts +569 -0
  250. package/dist/intake.d.ts.map +1 -0
  251. package/dist/intake.js +1628 -0
  252. package/dist/intake.js.map +1 -0
  253. package/dist/integration-core.d.ts +539 -0
  254. package/dist/integration-core.d.ts.map +1 -0
  255. package/dist/integration-core.js +2195 -0
  256. package/dist/integration-core.js.map +1 -0
  257. package/dist/integrator.d.ts +343 -0
  258. package/dist/integrator.d.ts.map +1 -0
  259. package/dist/integrator.js +400 -0
  260. package/dist/integrator.js.map +1 -0
  261. package/dist/isolation.d.ts +219 -0
  262. package/dist/isolation.d.ts.map +1 -0
  263. package/dist/isolation.js +261 -0
  264. package/dist/isolation.js.map +1 -0
  265. package/dist/issue-provider.d.ts +349 -0
  266. package/dist/issue-provider.d.ts.map +1 -0
  267. package/dist/issue-provider.js +360 -0
  268. package/dist/issue-provider.js.map +1 -0
  269. package/dist/item-lock.d.ts +626 -0
  270. package/dist/item-lock.d.ts.map +1 -0
  271. package/dist/item-lock.js +1381 -0
  272. package/dist/item-lock.js.map +1 -0
  273. package/dist/item-path.d.ts +49 -0
  274. package/dist/item-path.d.ts.map +1 -0
  275. package/dist/item-path.js +66 -0
  276. package/dist/item-path.js.map +1 -0
  277. package/dist/ledger-lint.d.ts +129 -0
  278. package/dist/ledger-lint.d.ts.map +1 -0
  279. package/dist/ledger-lint.js +249 -0
  280. package/dist/ledger-lint.js.map +1 -0
  281. package/dist/ledger-read.d.ts +357 -0
  282. package/dist/ledger-read.d.ts.map +1 -0
  283. package/dist/ledger-read.js +442 -0
  284. package/dist/ledger-read.js.map +1 -0
  285. package/dist/ledger-write.d.ts +330 -0
  286. package/dist/ledger-write.d.ts.map +1 -0
  287. package/dist/ledger-write.js +411 -0
  288. package/dist/ledger-write.js.map +1 -0
  289. package/dist/lifecycle-gather.d.ts +30 -0
  290. package/dist/lifecycle-gather.d.ts.map +1 -0
  291. package/dist/lifecycle-gather.js +205 -0
  292. package/dist/lifecycle-gather.js.map +1 -0
  293. package/dist/lifecycle-pools.d.ts +180 -0
  294. package/dist/lifecycle-pools.d.ts.map +1 -0
  295. package/dist/lifecycle-pools.js +78 -0
  296. package/dist/lifecycle-pools.js.map +1 -0
  297. package/dist/merge-question-surfacer.d.ts +166 -0
  298. package/dist/merge-question-surfacer.d.ts.map +1 -0
  299. package/dist/merge-question-surfacer.js +297 -0
  300. package/dist/merge-question-surfacer.js.map +1 -0
  301. package/dist/mint-adr.d.ts +126 -0
  302. package/dist/mint-adr.d.ts.map +1 -0
  303. package/dist/mint-adr.js +257 -0
  304. package/dist/mint-adr.js.map +1 -0
  305. package/dist/mirror-pool-scan.d.ts +125 -0
  306. package/dist/mirror-pool-scan.d.ts.map +1 -0
  307. package/dist/mirror-pool-scan.js +104 -0
  308. package/dist/mirror-pool-scan.js.map +1 -0
  309. package/dist/needs-attention.d.ts +341 -0
  310. package/dist/needs-attention.d.ts.map +1 -0
  311. package/dist/needs-attention.js +900 -0
  312. package/dist/needs-attention.js.map +1 -0
  313. package/dist/orphan-sidecar.d.ts +79 -0
  314. package/dist/orphan-sidecar.d.ts.map +1 -0
  315. package/dist/orphan-sidecar.js +71 -0
  316. package/dist/orphan-sidecar.js.map +1 -0
  317. package/dist/output.d.ts +48 -0
  318. package/dist/output.d.ts.map +1 -0
  319. package/dist/output.js +66 -0
  320. package/dist/output.js.map +1 -0
  321. package/dist/pi-harness.d.ts +179 -0
  322. package/dist/pi-harness.d.ts.map +1 -0
  323. package/dist/pi-harness.js +342 -0
  324. package/dist/pi-harness.js.map +1 -0
  325. package/dist/placement.d.ts +99 -0
  326. package/dist/placement.d.ts.map +1 -0
  327. package/dist/placement.js +67 -0
  328. package/dist/placement.js.map +1 -0
  329. package/dist/prd-to-spec.d.ts +315 -0
  330. package/dist/prd-to-spec.d.ts.map +1 -0
  331. package/dist/prd-to-spec.js +684 -0
  332. package/dist/prd-to-spec.js.map +1 -0
  333. package/dist/prepare.d.ts +121 -0
  334. package/dist/prepare.d.ts.map +1 -0
  335. package/dist/prepare.js +140 -0
  336. package/dist/prepare.js.map +1 -0
  337. package/dist/prompt.d.ts +360 -0
  338. package/dist/prompt.d.ts.map +1 -0
  339. package/dist/prompt.js +499 -0
  340. package/dist/prompt.js.map +1 -0
  341. package/dist/protocol/ADR-FORMAT.md +47 -0
  342. package/dist/protocol/CLAIM-PROTOCOL.md +217 -0
  343. package/dist/protocol/REVIEW-PROTOCOL.md +119 -0
  344. package/dist/protocol/SURFACE-PROTOCOL.md +121 -0
  345. package/dist/protocol/TASKING-PROTOCOL.md +122 -0
  346. package/dist/protocol/WORK-CONTRACT.md +276 -0
  347. package/dist/protocol/spec-template.md +71 -0
  348. package/dist/protocol/task-template.md +65 -0
  349. package/dist/readiness.d.ts +66 -0
  350. package/dist/readiness.d.ts.map +1 -0
  351. package/dist/readiness.js +36 -0
  352. package/dist/readiness.js.map +1 -0
  353. package/dist/reap-branches.d.ts +102 -0
  354. package/dist/reap-branches.d.ts.map +1 -0
  355. package/dist/reap-branches.js +149 -0
  356. package/dist/reap-branches.js.map +1 -0
  357. package/dist/recover-isolated.d.ts +72 -0
  358. package/dist/recover-isolated.d.ts.map +1 -0
  359. package/dist/recover-isolated.js +188 -0
  360. package/dist/recover-isolated.js.map +1 -0
  361. package/dist/registry.d.ts +172 -0
  362. package/dist/registry.d.ts.map +1 -0
  363. package/dist/registry.js +296 -0
  364. package/dist/registry.js.map +1 -0
  365. package/dist/repo-config.d.ts +201 -0
  366. package/dist/repo-config.d.ts.map +1 -0
  367. package/dist/repo-config.js +414 -0
  368. package/dist/repo-config.js.map +1 -0
  369. package/dist/repo-key.d.ts +20 -0
  370. package/dist/repo-key.d.ts.map +1 -0
  371. package/dist/repo-key.js +68 -0
  372. package/dist/repo-key.js.map +1 -0
  373. package/dist/repo-mirror.d.ts +177 -0
  374. package/dist/repo-mirror.d.ts.map +1 -0
  375. package/dist/repo-mirror.js +271 -0
  376. package/dist/repo-mirror.js.map +1 -0
  377. package/dist/retry-backoff.d.ts +90 -0
  378. package/dist/retry-backoff.d.ts.map +1 -0
  379. package/dist/retry-backoff.js +98 -0
  380. package/dist/retry-backoff.js.map +1 -0
  381. package/dist/review-gate.d.ts +173 -0
  382. package/dist/review-gate.d.ts.map +1 -0
  383. package/dist/review-gate.js +261 -0
  384. package/dist/review-gate.js.map +1 -0
  385. package/dist/review-verdict.d.ts +149 -0
  386. package/dist/review-verdict.d.ts.map +1 -0
  387. package/dist/review-verdict.js +332 -0
  388. package/dist/review-verdict.js.map +1 -0
  389. package/dist/run.d.ts +221 -0
  390. package/dist/run.d.ts.map +1 -0
  391. package/dist/run.js +963 -0
  392. package/dist/run.js.map +1 -0
  393. package/dist/scan.d.ts +308 -0
  394. package/dist/scan.d.ts.map +1 -0
  395. package/dist/scan.js +374 -0
  396. package/dist/scan.js.map +1 -0
  397. package/dist/select-order.d.ts +75 -0
  398. package/dist/select-order.d.ts.map +1 -0
  399. package/dist/select-order.js +108 -0
  400. package/dist/select-order.js.map +1 -0
  401. package/dist/select-priority.d.ts +188 -0
  402. package/dist/select-priority.d.ts.map +1 -0
  403. package/dist/select-priority.js +80 -0
  404. package/dist/select-priority.js.map +1 -0
  405. package/dist/select.d.ts +25 -0
  406. package/dist/select.d.ts.map +1 -0
  407. package/dist/select.js +43 -0
  408. package/dist/select.js.map +1 -0
  409. package/dist/session-path.d.ts +36 -0
  410. package/dist/session-path.d.ts.map +1 -0
  411. package/dist/session-path.js +129 -0
  412. package/dist/session-path.js.map +1 -0
  413. package/dist/sidecar-apply.d.ts +83 -0
  414. package/dist/sidecar-apply.d.ts.map +1 -0
  415. package/dist/sidecar-apply.js +111 -0
  416. package/dist/sidecar-apply.js.map +1 -0
  417. package/dist/sidecar.d.ts +245 -0
  418. package/dist/sidecar.d.ts.map +1 -0
  419. package/dist/sidecar.js +481 -0
  420. package/dist/sidecar.js.map +1 -0
  421. package/dist/slug-namespace.d.ts +204 -0
  422. package/dist/slug-namespace.d.ts.map +1 -0
  423. package/dist/slug-namespace.js +229 -0
  424. package/dist/slug-namespace.js.map +1 -0
  425. package/dist/spec-complete.d.ts +44 -0
  426. package/dist/spec-complete.d.ts.map +1 -0
  427. package/dist/spec-complete.js +69 -0
  428. package/dist/spec-complete.js.map +1 -0
  429. package/dist/start.d.ts +97 -0
  430. package/dist/start.d.ts.map +1 -0
  431. package/dist/start.js +633 -0
  432. package/dist/start.js.map +1 -0
  433. package/dist/status.d.ts +199 -0
  434. package/dist/status.d.ts.map +1 -0
  435. package/dist/status.js +228 -0
  436. package/dist/status.js.map +1 -0
  437. package/dist/surface-gate.d.ts +162 -0
  438. package/dist/surface-gate.d.ts.map +1 -0
  439. package/dist/surface-gate.js +206 -0
  440. package/dist/surface-gate.js.map +1 -0
  441. package/dist/surface-persist.d.ts +86 -0
  442. package/dist/surface-persist.d.ts.map +1 -0
  443. package/dist/surface-persist.js +129 -0
  444. package/dist/surface-persist.js.map +1 -0
  445. package/dist/tasker-review-loop.d.ts +249 -0
  446. package/dist/tasker-review-loop.d.ts.map +1 -0
  447. package/dist/tasker-review-loop.js +369 -0
  448. package/dist/tasker-review-loop.js.map +1 -0
  449. package/dist/tasking-eligibility.d.ts +74 -0
  450. package/dist/tasking-eligibility.d.ts.map +1 -0
  451. package/dist/tasking-eligibility.js +52 -0
  452. package/dist/tasking-eligibility.js.map +1 -0
  453. package/dist/tasking-lock.d.ts +111 -0
  454. package/dist/tasking-lock.d.ts.map +1 -0
  455. package/dist/tasking-lock.js +256 -0
  456. package/dist/tasking-lock.js.map +1 -0
  457. package/dist/tasking.d.ts +275 -0
  458. package/dist/tasking.d.ts.map +1 -0
  459. package/dist/tasking.js +952 -0
  460. package/dist/tasking.js.map +1 -0
  461. package/dist/triage-gate.d.ts +127 -0
  462. package/dist/triage-gate.d.ts.map +1 -0
  463. package/dist/triage-gate.js +139 -0
  464. package/dist/triage-gate.js.map +1 -0
  465. package/dist/triage-persist.d.ts +163 -0
  466. package/dist/triage-persist.d.ts.map +1 -0
  467. package/dist/triage-persist.js +387 -0
  468. package/dist/triage-persist.js.map +1 -0
  469. package/dist/verdict-json.d.ts +32 -0
  470. package/dist/verdict-json.d.ts.map +1 -0
  471. package/dist/verdict-json.js +74 -0
  472. package/dist/verdict-json.js.map +1 -0
  473. package/dist/verify-workflow-template.d.ts +60 -0
  474. package/dist/verify-workflow-template.d.ts.map +1 -0
  475. package/dist/verify-workflow-template.js +126 -0
  476. package/dist/verify-workflow-template.js.map +1 -0
  477. package/dist/verify.d.ts +60 -0
  478. package/dist/verify.d.ts.map +1 -0
  479. package/dist/verify.js +62 -0
  480. package/dist/verify.js.map +1 -0
  481. package/dist/watch-session.d.ts +112 -0
  482. package/dist/watch-session.d.ts.map +1 -0
  483. package/dist/watch-session.js +347 -0
  484. package/dist/watch-session.js.map +1 -0
  485. package/dist/work-layout.d.ts +198 -0
  486. package/dist/work-layout.d.ts.map +1 -0
  487. package/dist/work-layout.js +217 -0
  488. package/dist/work-layout.js.map +1 -0
  489. package/dist/work-on.d.ts +154 -0
  490. package/dist/work-on.d.ts.map +1 -0
  491. package/dist/work-on.js +387 -0
  492. package/dist/work-on.js.map +1 -0
  493. package/dist/workspace.d.ts +224 -0
  494. package/dist/workspace.d.ts.map +1 -0
  495. package/dist/workspace.js +325 -0
  496. package/dist/workspace.js.map +1 -0
  497. package/package.json +46 -2
  498. package/src/advance-ci-template.ts +203 -0
  499. package/src/advance-classify.ts +197 -0
  500. package/src/advance-drivers.ts +414 -0
  501. package/src/advance-isolated.ts +432 -0
  502. package/src/advance-lifecycle-template.ts +791 -0
  503. package/src/advance-loop-driver.ts +745 -0
  504. package/src/advance-treeless-publish.ts +177 -0
  505. package/src/advance.ts +1564 -0
  506. package/src/advancing-lock.ts +988 -0
  507. package/src/agent-launch.ts +137 -0
  508. package/src/agent-stop.ts +361 -0
  509. package/src/apply-decide.ts +242 -0
  510. package/src/apply-merge-action.ts +502 -0
  511. package/src/apply-persist.ts +518 -0
  512. package/src/arbiter.ts +372 -0
  513. package/src/brand.ts +111 -0
  514. package/src/buildable-body.ts +196 -0
  515. package/src/categorise.ts +158 -0
  516. package/src/claim-cas.ts +513 -0
  517. package/src/cli-spinner.ts +225 -0
  518. package/src/cli.ts +4369 -0
  519. package/src/close-job-template.ts +236 -0
  520. package/src/close-job.ts +319 -0
  521. package/src/complete.ts +1379 -0
  522. package/src/concurrency.ts +151 -0
  523. package/src/config-override.ts +116 -0
  524. package/src/config.ts +883 -0
  525. package/src/continue-branch.ts +542 -0
  526. package/src/cwd-section.ts +392 -0
  527. package/src/decision-engine.ts +272 -0
  528. package/src/detect.ts +124 -0
  529. package/src/do-autopick.ts +223 -0
  530. package/src/do-config.ts +589 -0
  531. package/src/do-remote-auto.ts +197 -0
  532. package/src/do.ts +2623 -0
  533. package/src/drop-source.ts +194 -0
  534. package/src/eligibility.ts +79 -0
  535. package/src/env-config.ts +305 -0
  536. package/src/failure-cause.ts +142 -0
  537. package/src/format.ts +313 -0
  538. package/src/frontmatter.ts +485 -0
  539. package/src/gate-readiness.ts +147 -0
  540. package/src/gc.ts +510 -0
  541. package/src/gh-failure.ts +53 -0
  542. package/src/git.ts +186 -0
  543. package/src/github.ts +468 -0
  544. package/src/harness.ts +355 -0
  545. package/src/identity.ts +322 -0
  546. package/src/index.ts +785 -0
  547. package/src/install-ci-branch-protection.ts +255 -0
  548. package/src/install-ci-capabilities/advance-lifecycle.ts +34 -0
  549. package/src/install-ci-capabilities/close-job.ts +32 -0
  550. package/src/install-ci-capabilities/example-noop.ts +24 -0
  551. package/src/install-ci-capabilities/intake.ts +34 -0
  552. package/src/install-ci-capabilities/verify.ts +33 -0
  553. package/src/install-ci-core.ts +1088 -0
  554. package/src/install-ci-github.ts +376 -0
  555. package/src/install-ci.ts +552 -0
  556. package/src/intake-event.ts +102 -0
  557. package/src/intake-marker.ts +195 -0
  558. package/src/intake-triage.ts +138 -0
  559. package/src/intake-trigger-template.ts +591 -0
  560. package/src/intake.ts +2445 -0
  561. package/src/integration-core.ts +3065 -0
  562. package/src/integrator.ts +771 -0
  563. package/src/isolation.ts +484 -0
  564. package/src/issue-provider.ts +733 -0
  565. package/src/item-lock.ts +1858 -0
  566. package/src/item-path.ts +75 -0
  567. package/src/ledger-lint.ts +332 -0
  568. package/src/ledger-read.ts +924 -0
  569. package/src/ledger-write.ts +865 -0
  570. package/src/lifecycle-gather.ts +298 -0
  571. package/src/lifecycle-pools.ts +250 -0
  572. package/src/merge-question-surfacer.ts +496 -0
  573. package/src/mint-adr.ts +362 -0
  574. package/src/mirror-pool-scan.ts +240 -0
  575. package/src/needs-attention.ts +1506 -0
  576. package/src/orphan-sidecar.ts +150 -0
  577. package/src/output.ts +89 -0
  578. package/src/pi-harness.ts +403 -0
  579. package/src/placement.ts +131 -0
  580. package/src/prd-to-spec.ts +1023 -0
  581. package/src/prepare.ts +230 -0
  582. package/src/prompt.ts +760 -0
  583. package/src/readiness.ts +98 -0
  584. package/src/reap-branches.ts +278 -0
  585. package/src/recover-isolated.ts +276 -0
  586. package/src/registry.ts +475 -0
  587. package/src/repo-config.ts +550 -0
  588. package/src/repo-key.ts +74 -0
  589. package/src/repo-mirror.ts +367 -0
  590. package/src/retry-backoff.ts +130 -0
  591. package/src/review-gate.ts +389 -0
  592. package/src/review-verdict.ts +422 -0
  593. package/src/run.ts +1430 -0
  594. package/src/scan.ts +611 -0
  595. package/src/select-order.ts +143 -0
  596. package/src/select-priority.ts +266 -0
  597. package/src/select.ts +62 -0
  598. package/src/session-path.ts +153 -0
  599. package/src/sidecar-apply.ts +216 -0
  600. package/src/sidecar.ts +700 -0
  601. package/src/slug-namespace.ts +367 -0
  602. package/src/spec-complete.ts +118 -0
  603. package/src/start.ts +974 -0
  604. package/src/status.ts +441 -0
  605. package/src/surface-gate.ts +337 -0
  606. package/src/surface-persist.ts +241 -0
  607. package/src/tasker-review-loop.ts +671 -0
  608. package/src/tasking-eligibility.ts +114 -0
  609. package/src/tasking-lock.ts +416 -0
  610. package/src/tasking.ts +1438 -0
  611. package/src/triage-gate.ts +248 -0
  612. package/src/triage-persist.ts +570 -0
  613. package/src/verdict-json.ts +73 -0
  614. package/src/verify-workflow-template.ts +159 -0
  615. package/src/verify.ts +123 -0
  616. package/src/watch-session.ts +397 -0
  617. package/src/work-layout.ts +262 -0
  618. package/src/work-on.ts +660 -0
  619. package/src/workspace.ts +502 -0
package/src/do.ts ADDED
@@ -0,0 +1,2623 @@
1
+ import {existsSync, mkdirSync, readFileSync, rmSync} from 'node:fs';
2
+ import {dirname, join} from 'node:path';
3
+ import {performStart} from './start.js';
4
+ import {performComplete} from './complete.js';
5
+ import {performClaim} from './claim-cas.js';
6
+ import {
7
+ resolveSlug,
8
+ SlugResolutionError,
9
+ workBranchRef,
10
+ } from './slug-namespace.js';
11
+ import {performTask, type TaskResult} from './tasking.js';
12
+ import type {TaskReviewGate} from './tasker-review-loop.js';
13
+ import {
14
+ resolveTask,
15
+ buildAgentPrompt,
16
+ resolveContinueContext,
17
+ resolvePromptGuidanceForItem,
18
+ PromptError,
19
+ } from './prompt.js';
20
+ import {NullHarness, type Harness} from './harness.js';
21
+ import {PiHarness} from './pi-harness.js';
22
+ import {launchWithOptionalWatch} from './agent-launch.js';
23
+ import {ledgerRead, type LedgerReadStrategy} from './ledger-read.js';
24
+ import {
25
+ ledgerWrite,
26
+ type ApplyNeedsAttentionTransitionResult,
27
+ } from './ledger-write.js';
28
+ import type {SurfaceToNeedsAttentionResult} from './needs-attention.js';
29
+ import {
30
+ jobWorktreeStrategy,
31
+ selectIsolationStrategy,
32
+ type IsolatedTree,
33
+ } from './isolation.js';
34
+ import {ensureMirror, encodeRepoKey, mirrorPath} from './repo-mirror.js';
35
+ import {jobWorktreePath} from './workspace.js';
36
+ import {reapJob} from './gc.js';
37
+ import {isGitHubArbiterUrl, GitHubProvider} from './github.js';
38
+ import type {ReviewProvider} from './integrator.js';
39
+ import {arbiterUrl} from './integration-core.js';
40
+ import {
41
+ shouldFailProposePrIntent,
42
+ PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
43
+ } from './do-config.js';
44
+ import {
45
+ checkGatePreconditions,
46
+ detectLockfileOnDisk,
47
+ detectLockfileOnMirrorMain,
48
+ } from './gate-readiness.js';
49
+ import type {IntegrationMode, PromptGuidance} from './config.js';
50
+ import type {VerifyConfig} from './verify.js';
51
+ import type {ReviewGate} from './review-gate.js';
52
+ import {git, run, runAsync, localMainAheadCount} from './git.js';
53
+ import {
54
+ identityEnv,
55
+ assertTransportAllowed,
56
+ type Identity,
57
+ } from './identity.js';
58
+ import {
59
+ parseStopSentinel,
60
+ isWorkBranchDiffEmpty,
61
+ emptyDiffStopReason,
62
+ } from './agent-stop.js';
63
+ import {
64
+ classifyFailureCause,
65
+ failureCauseLabel,
66
+ type FailureCause,
67
+ } from './failure-cause.js';
68
+
69
+ /**
70
+ * `dorfl do <slug>` (in-place form) — the per-repo, in-place WORKER that
71
+ * claims + builds + gates + integrates in ONE checkout, then EXITS (ADR §3).
72
+ * **This is the CI command** (CI has a checkout, is one repo, is one triggered
73
+ * invocation, exits) and it ABSORBS the manual `ar-run.sh` test-driver.
74
+ *
75
+ * In-place `do` is on the ISOLATION SEAM (`do-run-share-isolation-seam`): it is
76
+ * the FIRST production consumer of `selectIsolationStrategy`/`inPlaceStrategy`,
77
+ * so all THREE `do`/`run` forms (in-place `do`, `do --remote`, `run`) share the
78
+ * ONE `IsolatedTree`-handle post-claim shape. The composition mirrors
79
+ * `do --remote`/`run` exactly: keep the two in-place GUARDS (dirty-tree refusal
80
+ * + pre-flight diverged-main guard) in this driver BEFORE onboarding, CLAIM
81
+ * explicitly via the CAS (`performClaim`), then let
82
+ * `selectIsolationStrategy({checkout}).prepare()` do the ONBOARDING half (fetch +
83
+ * continue-detection + fresh-main `work/<slug>` switch, incl. the §14
84
+ * continue/rebase + §10 conflict path) WITHOUT re-claiming. The agent run is the
85
+ * ONLY new middle step (the one `ar-run.sh` shelled out for, `prompt | pi`). The
86
+ * runner owns EVERY git-state transition (claim, done-move, completion commit,
87
+ * integration); the agent only edits code.
88
+ *
89
+ * The in-place ISOLATION (ADR §3): the current checkout / CI container IS the
90
+ * isolation — no hub mirror, no external worktree. `inPlaceStrategy.prepare()`
91
+ * puts the checkout on `work/<slug>` cut from the freshly-fetched
92
+ * `<arbiter>/main`; its handle's `teardown` is a NO-OP (the checkout is left in a
93
+ * defined state on `work/<slug>`, NEVER reaped). `do --remote` (the job-worktree
94
+ * strategy) is the SEPARATE `do-remote` task; auto-pick / multi-arg / `-n` is
95
+ * `do-autopick`.
96
+ *
97
+ * CLAIM SEMANTICS (autonomous, same as `do --remote`/`run`): the claim is
98
+ * explicit and claim-or-lose. An item that is NOT in the pool (`tasks-ready`) on
99
+ * the arbiter (already in-progress / done / absent) is not claimable, so the CAS
100
+ * returns
101
+ * `lost` (exit 2) and the run skips cleanly — `do`, the unattended CI worker,
102
+ * never re-claims an item someone else holds and never silently picks up a
103
+ * needs-attention item (a human does that through the human face).
104
+ *
105
+ * **CRITICAL — `do` is AUTONOMOUS, so its failure path is `run`'s, NOT
106
+ * `complete`'s.** On a red gate / rebase conflict, `performComplete` routes the
107
+ * item to needs-attention via the SAME seam call as `run`'s `runOneItem`, but
108
+ * `complete` calls it WITHOUT an arbiter (the human path: a human is right
109
+ * there). `do` runs UNATTENDED, so it MUST get the autonomous, arbiter-passed
110
+ * surfacing like `run` (the on-`main` cherry-pick that makes a stuck CI run
111
+ * visible to `scan`/`status`/another machine). We achieve this by passing
112
+ * `surfaceArbiter` into `performComplete` (resolution (a) from the task): the
113
+ * success path reuses `complete`'s machinery; only the NEEDS-ATTENTION routing
114
+ * becomes the autonomous variant.
115
+ *
116
+ * `--propose` (default) / `--merge` is resolved at integrate-time exactly like
117
+ * `complete` (the caller threads the resolved mode in as `integration`).
118
+ */
119
+
120
+ /** The terminal status of one in-place `do` run. */
121
+ export type DoOutcome =
122
+ | 'completed' // claimed/onboarded → agent → gate green → integrated → exited
123
+ | 'lost' // claim lost the race — skipped cleanly
124
+ | 'contended' // claim push kept being rejected
125
+ | 'needs-attention' // red gate / rebase conflict / review-block → surfaced (autonomous)
126
+ | 'surface-unmoved' // the tree-less surface to needs-attention did NOT land on the arbiter (lost the CAS race / no arbiter) — the item is STILL in-progress on the arbiter; retry/resolve
127
+ | 'agent-failed' // the agent ran but produced bad/empty output (the conservative generic), OR the cause is unknown — work SAVED + surfaced
128
+ | 'transient-infra' // a harness-surfaced model/connection outage (post-retry) or a git/provider outage — RETRY the same work (FAILURE-CAUSE axis)
129
+ | 'config-error' // a thrown CORE wiring/config error (e.g. review on, no reviewGate) — fix the WIRING, not the task (FAILURE-CAUSE axis)
130
+ | 'agent-stopped' // the agent DELIBERATELY stopped (task drifted/ambiguous) OR produced no change → surfaced; gate + Gate-2 SKIPPED
131
+ | 'refused' // refused (dirty tree, wrong folder, nothing to complete, …)
132
+ | 'usage-error' // usage / environment problem, or a slug-resolution error
133
+ | 'tasked' // `do prd:<slug>` — the prd was tasked into work/backlog/ (runner-owned)
134
+ | 'gate-refused' // `do prd:<slug>` — the tasking gate refused (honest skip)
135
+ | 'stale'; // `do prd:<slug>` — the held prd was edited under the lock (stale tasking)
136
+
137
+ export interface DoResult {
138
+ exitCode: 0 | 1 | 2 | 3;
139
+ outcome: DoOutcome;
140
+ /** The resolved bare slug acted on (task or prd), when one was resolved. */
141
+ slug?: string;
142
+ /** The work branch the run operated on, when one was created/switched-to. */
143
+ branch?: string;
144
+ /**
145
+ * True iff a FAILURE (agent-failed) SAVED + surfaced the partial work via the
146
+ * needs-attention mechanism (committed the agent's work, pushed the
147
+ * `work/<slug>` branch, surfaced on the arbiter's main) rather than dropping it.
148
+ * Undefined/false on the success, lost/contended/refused, and usage-error paths.
149
+ */
150
+ routedToNeedsAttention?: boolean;
151
+ /** Human-readable summary of the terminal condition. */
152
+ message: string;
153
+ }
154
+
155
+ /** The agent invocation: edits code in `cwd` to satisfy the prompt. */
156
+ export type DoDorfl = (input: {
157
+ cwd: string;
158
+ prompt: string;
159
+ slug: string;
160
+ env?: NodeJS.ProcessEnv;
161
+ }) => {
162
+ ok: boolean;
163
+ detail?: string;
164
+ /**
165
+ * The agent's FINAL SUMMARY (the harness seam's `LaunchResult.output`): the
166
+ * channel the propose-mode PR BODY is built from. Optional so a test agent may
167
+ * supply a body; production surfaces the build agent's last assistant message.
168
+ * Absent ⇒ no body ⇒ the provider degrades to `--fill` (no regression).
169
+ */
170
+ output?: string;
171
+ };
172
+
173
+ export interface DoOptions {
174
+ /** The raw CLI slug argument: bare (= task), `task:<slug>`, or `prd:<slug>`. */
175
+ arg: string;
176
+ /** The working clone/checkout to run in-place in. */
177
+ cwd: string;
178
+ /** Name of the arbiter git remote. Defaults to `origin`. */
179
+ arbiter?: string;
180
+ /**
181
+ * The optional runner IDENTITY (a bot), threaded from host-only
182
+ * `config.identity`. Scopes the runner's GIT/provider ops (claim, push,
183
+ * integrate, `gh`) — NEVER the agent launch (the agent stays ambient; it must
184
+ * not commit as the identity). Absent ⇒ ambient. See {@link identityEnv}.
185
+ */
186
+ identity?: Identity;
187
+ /**
188
+ * Per-repo `autoTask` policy (resolved by `autoslice-gate`: flag > env >
189
+ * per-repo > global > default false). It gates the AUTO-PICK / pool path only
190
+ * (`do-autopick.ts`'s taskable-prd pool): "may an agent auto-task an
191
+ * UNDECLARED prd in this repo?". An EXPLICITLY-named `do prd:<slug>` tasks
192
+ * REGARDLESS of this policy (the dispatch passes `explicit: true` to
193
+ * `performTask` — naming the prd IS the authorization, exactly as `do <task>`
194
+ * builds regardless of `autoBuild`). Ignored by the task-build path.
195
+ */
196
+ autoTask?: boolean;
197
+ /**
198
+ * The resolved {@link PromptGuidance} NUDGE namespace (e.g. `testFirst`),
199
+ * threaded from the per-repo config by the CLI and forwarded INTO
200
+ * {@link buildAgentPrompt} so the autonomous in-place `do` worker prompt
201
+ * actually carries the nudge. Absent ⇒ every member false ⇒ byte-identical to
202
+ * today. (Without this the per-repo `promptGuidance.testFirst` was a silent
203
+ * no-op on the build path — only `dorfl prompt` honoured it.)
204
+ */
205
+ promptGuidance?: PromptGuidance;
206
+ /**
207
+ * **The tasker review→edit→converge LOOP seam** (`slicer-review-edit-loop`):
208
+ * consumed ONLY by the `do prd:<slug>` tasking path — after the agent produces
209
+ * candidate tasks, run the `review` SKILL as a review→edit→re-review loop that
210
+ * improves them, routing the verdict through the needsAnswers / needs-attention
211
+ * sink. Ignored by the task-build path. Omitted ⇒ no loop (candidate tasks land
212
+ * as-is). Production wires {@link harnessTaskReviewGate}; tests inject a canned
213
+ * verdict+edits.
214
+ */
215
+ reviewLoop?: TaskReviewGate;
216
+ /** The tasker improver loop's `taskerLoopMax` cap (flag > env > per-repo > global > default). Loop only. */
217
+ taskerLoopMax?: number;
218
+ /** The tasker improver loop's de-correlated review model (`--tasker-loop-model`). Loop only. */
219
+ taskerLoopModel?: string;
220
+ /** How many fresh-context (M) executions of the tasker loop to run. Default 1. Loop only. */
221
+ reviewExecutions?: number;
222
+ /** Integration mode resolved at integrate-time (flag > per-repo > global > default). */
223
+ integration?: IntegrationMode;
224
+ /**
225
+ * **The explicit `--merge` override** for the untrusted-origin build-propose rule
226
+ * (task `untrusted-origin-forces-build-propose`). `true` iff the operator
227
+ * EXPLICITLY typed `--merge` (vs `merge` resolved from config). Forwarded to the
228
+ * task-BUILD `performComplete` → `performIntegration` so an explicit `--merge`
229
+ * OVERRIDES the untrusted-origin build-propose rule. The autonomous/CI path (a bare
230
+ * `advance`/`do` auto-pick) passes no flag ⇒ unset ⇒ an untrusted-origin task
231
+ * reliably forces `propose`. (Build transition only; the tasking transition is
232
+ * unaffected — a task FILE landing on main is inert.)
233
+ */
234
+ explicitMerge?: boolean;
235
+ /**
236
+ * **Per-TRANSITION override for the TASKING transition only** (config
237
+ * `taskingIntegration`). Consumed ONLY by the `do prd:<slug>` tasking path: the
238
+ * value threaded into {@link performTask} is `taskingIntegration ?? integration`,
239
+ * so an unset override is byte-for-byte today's behaviour (tasking uses
240
+ * `integration`). The task-BUILD path ALWAYS threads `integration` (never this
241
+ * key). An explicit `--merge`/`--propose` flag wins over BOTH (the flag-override
242
+ * layer sets `integration` AND `taskingIntegration` to the typed mode — see
243
+ * `do-config.ts`). DISTINCT from intake's per-EMITTED-TYPE `{task, prd}` resolver.
244
+ */
245
+ taskingIntegration?: IntegrationMode;
246
+ /**
247
+ * **The per-repo TASK-PLACEMENT default** (prd
248
+ * `staging-pool-position-gate-and-trust-model` US #5, task
249
+ * `runner-deterministic-slice-placement-policy-and-precedence`). Consumed by
250
+ * the `do prd:<slug>` tasking path: the value is fed as the
251
+ * CONFIGURED-DEFAULT rung into the runner-deterministic placement resolver
252
+ * (`src/placement.ts`). Resolved per-repo through the SAME chain as
253
+ * `taskingIntegration` (flag > env > per-repo > global > built-in
254
+ * `pre-backlog`). The task-BUILD path ignores it (placement is a TASKING
255
+ * lifecycle concern).
256
+ */
257
+ tasksLandIn?: 'pre-backlog' | 'ready';
258
+ /**
259
+ * **The OPERATOR's EXPLICIT task-placement override** (the TOP precedence
260
+ * rung in the placement resolver). Set ONLY when the operator typed
261
+ * `--tasks-land-in <where>` on this invocation; never when the value came
262
+ * from config. Wins over `originTrust: untrusted` (the operator is present;
263
+ * CLI always wins, no special force-key) — the positional analogue of
264
+ * `explicitMerge` overriding the untrusted-origin build-propose rule.
265
+ */
266
+ explicitTasksLandIn?: 'pre-backlog' | 'ready';
267
+ /**
268
+ * Override the pre-flight DIVERGENCE guard (`--ignore-diverged-main`, mirroring
269
+ * `--ignore-not-ready`): proceed even when local `main` is ahead of
270
+ * `<arbiter>/main` (has unpushed commits). When overridden and the divergence
271
+ * persists, `complete`'s now-NON-FATAL local-main sync handles the outcome
272
+ * honestly (the work lands on the arbiter; local `main` is left for the operator
273
+ * to rebase). Loud, never the default.
274
+ */
275
+ ignoreDivergedMain?: boolean;
276
+ /**
277
+ * **`--allow-backlog`** (prd
278
+ * `do-allow-backlog-drive-staged-tasks-without-promotion`): let this `do task:`
279
+ * invocation FIND, CLAIM, and COMPLETE a task that lives in `tasks/backlog/`
280
+ * (staging) IN PLACE, WITHOUT first promoting it to the pool — so no `advance`
281
+ * leg / `run` daemon can claim it out from under the operator. It widens ONLY
282
+ * resolution (`resolveTask` also searches `tasks-backlog`, lowest priority) +
283
+ * the claimable predicate (`performClaim` accepts a staged body); claim/lock/
284
+ * build/gate are identical, and the done-move goes `tasks/backlog/ → tasks/done/`
285
+ * directly (the explicit drive IS the promotion). EXPLICIT-INVOCATION-ONLY:
286
+ * default off, never set by `run`/auto-pick/`advance` (the leak-fence). The
287
+ * task-build path only; the `do prd:` path is unaffected.
288
+ */
289
+ allowBacklog?: boolean;
290
+ /** The declared per-repo ENV-PREP step (string | list), run ONCE before the
291
+ * first `verify` on a fresh worktree. Unset ⇒ a no-op (NO default install). */
292
+ prepare?: VerifyConfig;
293
+ /** The declared per-repo acceptance gate (string | list). */
294
+ verify?: VerifyConfig;
295
+ /**
296
+ * Run the acceptance gate against the REBASED tip in a clean throwaway worktree
297
+ * (the tree that integrates) when `true` (the default), else in the build
298
+ * worktree (the pre-rebase tree). `do` is a SINGLE-JOB path, so the resolved
299
+ * flag is passed UNCONDITIONALLY (no `run`-fleet downgrade).
300
+ */
301
+ freshWorktreeGate?: boolean;
302
+ /**
303
+ * **The cross-job merge-serialiser CAS-retry cap** (config `mergeRetries`, prd
304
+ * `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1
305
+ * (a)). Threaded VERBATIM into `performComplete` → `performIntegration` so a
306
+ * wide-matrix CI's raised cap actually reaches the cross-job land queue (the
307
+ * CAS loop IS the queue across separate jobs). Resolved per-repo through the
308
+ * gate-family precedence chain (flag > env > per-repo > global > default).
309
+ */
310
+ mergeRetries?: number;
311
+ /**
312
+ * **The PR-INTENT axis** (config `noPR`, ADR §6): when `true`, propose pushes
313
+ * the branch but deliberately skips the PR (the explicit suppress-PR intent).
314
+ * NOT a provider choice — the provider is purely arbiter-derived. Threaded
315
+ * verbatim into `performComplete`. Unset/false ⇒ propose opens the PR normally.
316
+ */
317
+ noPR?: boolean;
318
+ /**
319
+ * The `gh` AUTH/AVAILABILITY PROBE the PR-INTENT pre-flight guard runs UP FRONT
320
+ * (propose + GitHub arbiter + `noPR` unset): `true` ⇒ `gh` CAN open a PR. The
321
+ * probe is the signal (mirroring `GitHubProvider.available`), NOT a config check
322
+ * — an absent identity falls back to ambient `gh` auth and the probe reports it
323
+ * available. Injectable so tests stub `gh` without a real binary; production
324
+ * defaults to `new GitHubProvider().available(cwd, env)`. Side-effecting (it
325
+ * shells `gh`), a deliberate pre-flight cost justified by saving a wasted build.
326
+ */
327
+ ghCanOpenPr?: (cwd: string, env: NodeJS.ProcessEnv | undefined) => boolean;
328
+ /**
329
+ * Optional FULLY-FORMED review provider INSTANCE used VERBATIM (the SAME seam
330
+ * `run` exposes via `RunOptions.provider`; threaded to `performComplete` →
331
+ * `performIntegration` as `providerInstance`). Tests/embeddings inject a stubbed
332
+ * `GitHubProvider` (a custom `gh` path) to drive the full propose pipeline
333
+ * OFFLINE without a real GitHub arbiter. The resolved provider OBJECT, NOT a
334
+ * config override (there is none). Unset ⇒ the core selects from the arbiter URL.
335
+ */
336
+ providerInstance?: ReviewProvider;
337
+ /**
338
+ * **Gate 2 — the PR/code review gate** (GATES prd `work/prds/tasked/review.md`):
339
+ * threaded VERBATIM into `performComplete` (the gate rides inside the shared
340
+ * `do`/`complete` pipeline, so CI inherits it for free). When `review` is on,
341
+ * the `review` SKILL runs as a fresh-context agent AFTER the green `verify` and
342
+ * BEFORE the done-move; a `block` maps to the `needs-attention` outcome the same
343
+ * way `gate-failed` does (exit 1). `reviewModel`/`reviewMaxRounds`
344
+ * tune it; `reviewGate` is the injectable seam (production: harness-backed).
345
+ */
346
+ review?: boolean;
347
+ reviewModel?: string;
348
+ reviewMaxRounds?: number;
349
+ reviewGate?: ReviewGate;
350
+ /**
351
+ * **The task-SET ACCEPTANCE GATE seam** (task `slice-acceptance-gate`):
352
+ * consumed ONLY by the `do prd:<slug>` tasking path. When `review` resolves on,
353
+ * a fresh-context review of the produced task SET runs BEFORE the tasks
354
+ * integrate (riding `performIntegration`'s review block); `block` routes the set
355
+ * to needs-attention, `approve` lets it integrate. It rides the SAME BUILD
356
+ * `--review`/`--no-review`/`--review-model` family as Gate-2 (one gate-config
357
+ * story) and is ONE-SHOT (no rounds; it does NOT inherit `reviewMaxRounds`). It
358
+ * is DISTINCT from the build {@link reviewGate} (a task-SET prompt, not a code
359
+ * diff) and from the tasker improver loop ({@link reviewLoop}). Production wires
360
+ * `harnessTaskAcceptanceGate`; tests inject a canned verdict. Omitted ⇒ no gate.
361
+ */
362
+ taskReviewGate?: ReviewGate;
363
+ /**
364
+ * The autonomous agent invocation. Tests inject this to edit files directly;
365
+ * production wires the harness seam (the prompt-fed, run-to-completion launch
366
+ * `run` uses). When omitted, {@link harness} is used.
367
+ */
368
+ dorfl?: DoDorfl;
369
+ /** The harness seam used when `dorfl` is omitted; defaults to the null adapter. */
370
+ harness?: Harness;
371
+ /**
372
+ * `do --watch`: stream the agent's high-signal events live by tailing the pi
373
+ * session `.jsonl` (task `do-watch`, option (a)). A READ-ONLY observer — it
374
+ * NEVER changes the run's outcome, gate, integration, git, or exit code; only a
375
+ * concurrent log-tail is added. REQUIRES the pi harness (the null adapter has
376
+ * no session log to tail) — passing it with a non-pi harness is a usage error.
377
+ */
378
+ watch?: boolean;
379
+ /** The configured agent command the harness shells out to (null adapter). */
380
+ agentCmd?: string;
381
+ /** The model routing intent forwarded to the harness (ADR §13). */
382
+ model?: string;
383
+ /**
384
+ * The HOST-ONLY root folder under which this run's pi session FILE is generated
385
+ * (resolved `config.sessionsDir`; flag > env > global > default). `undefined`
386
+ * ⇒ pi's default per-cwd folder. Mapped from `Config` in `cli.ts`'s `do`
387
+ * action (the bridge from resolved config to the launch — without it the key
388
+ * resolves but never reaches pi).
389
+ */
390
+ sessionsDir?: string;
391
+ /** Override the read seam (slug resolution); defaults to {@link ledgerRead}. */
392
+ read?: LedgerReadStrategy;
393
+ /** Override the resolver's repo path (slug-namespace existence reads). Defaults to `cwd`. */
394
+ repoPath?: string;
395
+ /** Environment for child git/agent processes. */
396
+ env?: NodeJS.ProcessEnv;
397
+ /** Sink for human-readable progress notes. */
398
+ note?: (message: string) => void;
399
+ /**
400
+ * Sink for a pre-formatted block printed VERBATIM (forwarded to `complete`'s
401
+ * propose-mode next-step block). Defaults to `note`.
402
+ */
403
+ noteBlock?: (message: string) => void;
404
+ /** Emit ANSI color in `complete`'s (cosmetic) propose next-step block. */
405
+ color?: boolean;
406
+ /**
407
+ * Where `--watch`'s tailed events are written (defaults to stderr). Tests inject
408
+ * a sink to assert the surfaced lines without a real terminal.
409
+ */
410
+ watchSink?: (line: string) => void;
411
+ }
412
+
413
+ /**
414
+ * The agent-launch fields {@link runDoAgent} reads, shared by the in-place
415
+ * {@link DoOptions} and the remote {@link DoRemoteOptions} (so one launch helper
416
+ * serves both forms). A structural subset — both option shapes satisfy it.
417
+ */
418
+ interface DoAgentLaunchOptions {
419
+ dorfl?: DoDorfl;
420
+ harness?: Harness;
421
+ agentCmd?: string;
422
+ model?: string;
423
+ sessionsDir?: string;
424
+ /**
425
+ * The resolved {@link PromptGuidance} NUDGE namespace (e.g. `testFirst`),
426
+ * threaded from the per-repo config by the CLI and forwarded INTO
427
+ * {@link buildAgentPrompt} so the autonomous worker prompt actually carries the
428
+ * nudge. Absent ⇒ every member false ⇒ the wrapper is byte-identical to today.
429
+ * (Without this the per-repo `promptGuidance.testFirst` was a silent no-op on
430
+ * the build path — only `dorfl prompt` honoured it.)
431
+ */
432
+ promptGuidance?: PromptGuidance;
433
+ watch?: boolean;
434
+ watchSink?: (line: string) => void;
435
+ color?: boolean;
436
+ env?: NodeJS.ProcessEnv;
437
+ /**
438
+ * The optional runner IDENTITY (a bot). Threaded from the host-only
439
+ * `config.identity`. It scopes the runner's GIT + provider operations (claim,
440
+ * push, integration, `gh`) via process-scoped env overrides — NEVER the AGENT
441
+ * launch (the agent keeps the plain ambient `env`; the agent must not commit as
442
+ * the identity, only the runner's own git transitions do). Absent ⇒ ambient
443
+ * (today's behaviour, byte-for-byte).
444
+ */
445
+ identity?: Identity;
446
+ }
447
+
448
+ /**
449
+ * Options for {@link performDoRemote} — `do --remote <r> <arg>`. It carries the
450
+ * SAME pipeline knobs as {@link DoOptions} (integration / verify / review /
451
+ * agent-launch), but selects a REGISTERED repo by `remote` (auto-mirrored) +
452
+ * `workspacesDir` (the agents' execution area) INSTEAD of an in-place `cwd`:
453
+ * there is no checkout, so the worktree is materialised under `workspacesDir`.
454
+ */
455
+ export interface DoRemoteOptions extends DoAgentLaunchOptions {
456
+ /** The raw CLI slug argument: bare (= task), `task:<slug>`, or `prd:<slug>`. */
457
+ arg: string;
458
+ /**
459
+ * The registered remote spec/URL to run against (`do --remote <r>`). Resolved
460
+ * to a hub mirror via `ensureMirror` (auto-created when unregistered).
461
+ */
462
+ remote: string;
463
+ /**
464
+ * The execution working area (config `workspacesDir`) — the AGENTS' area where
465
+ * the hub mirror + job worktree live. NEVER the human area.
466
+ */
467
+ workspacesDir: string;
468
+ /**
469
+ * Name of the arbiter remote, used ONLY for human-readable surfacing messages
470
+ * (the actual rebase/integrate/push target inside the worktree is `origin`, the
471
+ * bare mirror's clone remote). Defaults to `origin`.
472
+ */
473
+ arbiter?: string;
474
+ /**
475
+ * Per-repo `autoTask` policy — gates the AUTO-PICK / pool path only. An
476
+ * EXPLICITLY-named `do --remote prd:<slug>` tasks regardless of it (the
477
+ * dispatch passes `explicit: true`), mirroring `do <task>` vs `autoBuild`.
478
+ * Ignored by the task-build path.
479
+ */
480
+ autoTask?: boolean;
481
+ /** The tasker review→edit→converge loop seam — `do --remote prd:<slug>` path only (see {@link DoOptions.reviewLoop}). */
482
+ reviewLoop?: TaskReviewGate;
483
+ /** The tasker improver loop's `taskerLoopMax` cap. Loop only. */
484
+ taskerLoopMax?: number;
485
+ /** The tasker improver loop's de-correlated review model (`--tasker-loop-model`). Loop only. */
486
+ taskerLoopModel?: string;
487
+ /** How many fresh-context (M) executions of the tasker loop to run. Default 1. Loop only. */
488
+ reviewExecutions?: number;
489
+ /** Integration mode resolved at integrate-time (flag > per-repo > global > default). */
490
+ integration?: IntegrationMode;
491
+ /**
492
+ * The explicit `--merge` override for the untrusted-origin build-propose rule on the
493
+ * `do --remote` task-BUILD path. See {@link DoOptions.explicitMerge}.
494
+ */
495
+ explicitMerge?: boolean;
496
+ /**
497
+ * **Per-TRANSITION override for the TASKING transition only** (config
498
+ * `taskingIntegration`) on the `do --remote prd:<slug>` path: threaded into
499
+ * {@link performTask} as `taskingIntegration ?? integration`. Unset ⇒ tasking
500
+ * uses `integration` (today's behaviour); the task-BUILD path always threads
501
+ * `integration`. See {@link DoOptions.taskingIntegration}.
502
+ */
503
+ taskingIntegration?: IntegrationMode;
504
+ /**
505
+ * **The per-repo TASK-PLACEMENT default** (prd
506
+ * `staging-pool-position-gate-and-trust-model` US #5) on the `do --remote
507
+ * prd:<slug>` path: threaded into {@link performTask} as the
508
+ * configured-default rung. See {@link DoOptions.tasksLandIn}.
509
+ */
510
+ tasksLandIn?: 'pre-backlog' | 'ready';
511
+ /**
512
+ * **The OPERATOR's EXPLICIT task-placement override** on the `do --remote
513
+ * prd:` path. See {@link DoOptions.explicitTasksLandIn}.
514
+ */
515
+ explicitTasksLandIn?: 'pre-backlog' | 'ready';
516
+ /** The declared per-repo ENV-PREP step (string | list), run ONCE before the
517
+ * first `verify` on a fresh worktree. Unset ⇒ a no-op (NO default install). */
518
+ prepare?: VerifyConfig;
519
+ /** The declared per-repo acceptance gate (string | list). */
520
+ verify?: VerifyConfig;
521
+ /**
522
+ * Run the acceptance gate against the REBASED tip in a clean throwaway worktree
523
+ * (the tree that integrates) when `true` (the default), else in the build
524
+ * worktree. `do --remote`/`--isolated` is a SINGLE-JOB path, so the resolved
525
+ * flag is passed UNCONDITIONALLY (no `run`-fleet downgrade).
526
+ */
527
+ freshWorktreeGate?: boolean;
528
+ /**
529
+ * **The cross-job merge-serialiser CAS-retry cap** (config `mergeRetries`, prd
530
+ * `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1
531
+ * (a)). Threaded VERBATIM into `performComplete` → `performIntegration` so the
532
+ * resolved cap reaches the cross-job land queue (the CAS loop IS the queue
533
+ * across separate jobs). Mirrors {@link DoOptions.mergeRetries} so the in-place
534
+ * and no-checkout `do` paths share ONE knob.
535
+ */
536
+ mergeRetries?: number;
537
+ /**
538
+ * **The PR-INTENT axis** (config `noPR`, ADR §6): when `true`, propose pushes
539
+ * the branch but skips the PR (the explicit suppress-PR intent). NOT a provider
540
+ * choice — the provider is purely arbiter-derived. Unset/false ⇒ PR opens.
541
+ */
542
+ noPR?: boolean;
543
+ /**
544
+ * The `gh` AUTH/AVAILABILITY PROBE the PR-INTENT pre-flight guard runs UP FRONT
545
+ * on the AUTONOMOUS path too (propose + GitHub arbiter + `noPR` unset): `true` ⇒
546
+ * `gh` CAN open a PR. Mirrors {@link DoOptions.ghCanOpenPr} so the in-place and
547
+ * no-checkout `do` paths share ONE signal. The probe (not a config check) is the
548
+ * signal — an absent `providers.github` identity falls back to ambient `gh` auth,
549
+ * which the probe reports available, so a working ambient setup still PROCEEDS.
550
+ * Injectable so tests stub `gh` without a real binary; production defaults to
551
+ * `new GitHubProvider().available(probeDir, env)`, run in the claim clone (which
552
+ * carries the arbiter remote), NOT a bare cwd.
553
+ */
554
+ ghCanOpenPr?: (cwd: string, env: NodeJS.ProcessEnv | undefined) => boolean;
555
+ /**
556
+ * Optional FULLY-FORMED review provider INSTANCE (tests/embeddings inject a
557
+ * stubbed `GitHubProvider` to drive the propose pipeline offline). The resolved
558
+ * provider OBJECT, NOT a config override. Unset ⇒ the core selects from the
559
+ * arbiter URL.
560
+ */
561
+ providerInstance?: ReviewProvider;
562
+ /** Gate 2 (PR/code review) toggle — threaded verbatim into `performComplete`. */
563
+ review?: boolean;
564
+ reviewModel?: string;
565
+ reviewMaxRounds?: number;
566
+ reviewGate?: ReviewGate;
567
+ /** The task-SET ACCEPTANCE GATE seam — `do --remote prd:<slug>` path only (see {@link DoOptions.taskReviewGate}). */
568
+ taskReviewGate?: ReviewGate;
569
+ /**
570
+ * **`--allow-backlog`** on the no-checkout `do --remote`/`--isolated` path (see
571
+ * {@link DoOptions.allowBacklog}): widen resolution + the claimable predicate to
572
+ * ALSO accept a `tasks/backlog/`-resident task, so the human-driven `drive-tasks`
573
+ * opt-in backlog mode can build a STAGED task in place. The done-move goes
574
+ * `tasks/backlog/ → tasks/done/` directly. Default off; never set by an
575
+ * autonomous path. The task-build path only.
576
+ */
577
+ allowBacklog?: boolean;
578
+ /** Override the read seam (slug resolution); defaults to {@link ledgerRead}. */
579
+ read?: LedgerReadStrategy;
580
+ /** Sink for human-readable progress notes. */
581
+ note?: (message: string) => void;
582
+ /** Sink for a pre-formatted block (forwarded to `complete`'s next-step block). */
583
+ noteBlock?: (message: string) => void;
584
+ }
585
+
586
+ const DEFAULT_ARBITER = 'origin';
587
+
588
+ /**
589
+ * Map a `do prd:<slug>` {@link TaskResult} onto the `do` {@link DoResult}
590
+ * contract: outcomes pass through (tasked / gate-refused / stale / agent-failed /
591
+ * usage-error), the lock-lost outcome splits into `lost` (exit 2) vs `contended`
592
+ * (exit 3) by its exit code, and the tasking-only exit 4 (stale) is reported on
593
+ * the `do` exit contract (`0|1|2|3`) as exit 1 — the needs-attention-class
594
+ * failure code, same as a stuck build.
595
+ */
596
+ function taskResultToDoResult(tasked: TaskResult): DoResult {
597
+ let outcome: DoOutcome;
598
+ let exitCode: 0 | 1 | 2 | 3;
599
+ switch (tasked.outcome) {
600
+ case 'tasked':
601
+ outcome = 'tasked';
602
+ exitCode = 0;
603
+ break;
604
+ case 'gate-refused':
605
+ outcome = 'gate-refused';
606
+ exitCode = 1;
607
+ break;
608
+ case 'lock-lost':
609
+ if (tasked.exitCode === 3) {
610
+ outcome = 'contended';
611
+ exitCode = 3;
612
+ } else {
613
+ outcome = 'lost';
614
+ exitCode = 2;
615
+ }
616
+ break;
617
+ case 'stale':
618
+ outcome = 'stale';
619
+ exitCode = 1;
620
+ break;
621
+ case 'needs-attention':
622
+ // The tasker review→edit loop found the decomposition unclear and routed the
623
+ // prd to needs-attention (no guessed tasks). Same exit class as a stuck
624
+ // build (1).
625
+ outcome = 'needs-attention';
626
+ exitCode = 1;
627
+ break;
628
+ case 'agent-failed':
629
+ outcome = 'agent-failed';
630
+ exitCode = 1;
631
+ break;
632
+ default:
633
+ outcome = 'usage-error';
634
+ exitCode = 1;
635
+ }
636
+ return {exitCode, outcome, slug: tasked.slug, message: tasked.message};
637
+ }
638
+
639
+ /**
640
+ * Run the in-place `do` ritual end-to-end. Never throws for the expected
641
+ * lost/contended/refused/needs-attention cases — those are returned with the
642
+ * appropriate exit code and outcome. The runner owns all git; the agent only
643
+ * edits code.
644
+ */
645
+ export async function performDo(options: DoOptions): Promise<DoResult> {
646
+ const note = options.note ?? (() => {});
647
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
648
+ const cwd = options.cwd;
649
+ // `env` here is the runner's GIT/provider env, scoped to the configured
650
+ // identity (claim, push, integrate, `gh`). The AGENT launch is the ONE thing
651
+ // that must NOT be the identity — it stays on the ambient `options.env`
652
+ // (`runDoAgent` reads `options.env` directly, never this local `env`), so the
653
+ // agent never commits as the bot; only the runner's own transitions do. Absent
654
+ // identity ⇒ `options.env` unchanged (byte-for-byte ambient). A configured
655
+ // identity that cannot be resolved (e.g. `tokenEnv` names an unset env var) is
656
+ // a clean usage error here, never a crash or a silent ambient fallback.
657
+ let env: NodeJS.ProcessEnv;
658
+ try {
659
+ env = identityEnv(options.identity, options.env ?? process.env);
660
+ } catch (err) {
661
+ const message = err instanceof Error ? err.message : String(err);
662
+ note(message);
663
+ return {exitCode: 1, outcome: 'usage-error', message};
664
+ }
665
+
666
+ // 0. `--watch` REQUIRES the pi harness (task `do-watch`): only the pi adapter
667
+ // writes a session `.jsonl` event log to tail. The null/shell adapter has no
668
+ // session log / event taxonomy, so there is nothing to observe — ERROR
669
+ // CLEARLY here, BEFORE any git transition (no claim, no branch), rather than
670
+ // silently running without the view. The injected `dorfl` (tests /
671
+ // custom embeddings) is its own launch path and is exempt.
672
+ if (
673
+ options.watch === true &&
674
+ options.dorfl === undefined &&
675
+ !(options.harness instanceof PiHarness)
676
+ ) {
677
+ return {
678
+ exitCode: 1,
679
+ outcome: 'usage-error',
680
+ message:
681
+ '`do --watch` requires the pi harness; configure `harness: pi` or drop ' +
682
+ '`--watch`.',
683
+ };
684
+ }
685
+
686
+ // 1. Resolve the slug across BOTH namespaces — `do` is the ONE command that
687
+ // spans them (ADR §3a): bare → task (after a no-prd-collision check;
688
+ // ERROR on collision), `task:`/`prd:` explicit. A collision / resolution
689
+ // failure is a loud usage error (exit 1).
690
+ let resolved;
691
+ try {
692
+ resolved = resolveSlug({
693
+ arg: options.arg,
694
+ repoPath: options.repoPath ?? cwd,
695
+ read: options.read ?? ledgerRead,
696
+ });
697
+ } catch (err) {
698
+ if (err instanceof SlugResolutionError) {
699
+ return {exitCode: 1, outcome: 'usage-error', message: err.message};
700
+ }
701
+ const message = err instanceof Error ? err.message : String(err);
702
+ return {exitCode: 1, outcome: 'usage-error', message};
703
+ }
704
+
705
+ // 2. `do spec:<slug>` → the spec-TASKING path (`autoslice-command`): the in-place
706
+ // `do` worker is AUTONOMOUS, so it tasks as the AGENT (gate-bound + lock).
707
+ // The orchestration (gate → lock → to-task harness → runner-owned commit)
708
+ // lives in `tasking.ts`; `do` dispatches `spec:` here. The agent only writes
709
+ // task files — the runner owns every git transition (same boundary as the
710
+ // build path). It does NOT run the task-build pipeline below.
711
+ // `resolveSlug` returns `{namespace:'spec'}` for a `spec:<slug>` arg, so
712
+ // `do spec:<slug>` routes here to tasking.
713
+ if (resolved.namespace === 'spec') {
714
+ const tasked = await performTask({
715
+ slug: resolved.slug,
716
+ cwd,
717
+ arbiter,
718
+ doer: 'agent',
719
+ autoTask: options.autoTask,
720
+ // EXPLICIT dispatch: a `do prd:<slug>` target was NAMED (the operator typed
721
+ // it, or the auto-pick POOL already filtered it on `autoTask` before
722
+ // dispatching here — the single policy-enforcement point). So the tasking gate
723
+ // drops the `autoTask` policy term and binds only the prd's own readiness
724
+ // (`humanOnly`/`needsAnswers`) + `taskedAfter`, EXACTLY as `do <task>` builds a
725
+ // named task regardless of `autoBuild` (the pool gates the policy, not the
726
+ // explicit claim).
727
+ explicit: true,
728
+ // The injected dorfl (tests) writes task files directly. The
729
+ // DoDorfl shape is a structural superset of TaskDorfl (its
730
+ // extra `output` is ignored by the tasking path), so it threads straight in.
731
+ dorfl: options.dorfl,
732
+ harness: options.harness,
733
+ agentCmd: options.agentCmd,
734
+ model: options.model,
735
+ sessionsDir: options.sessionsDir,
736
+ // The integrate-time args (task `slice-output-through-integration`): the
737
+ // `provider` is the SAME the task-build path threads (arg parity), but the
738
+ // MODE is the per-TRANSITION TASKING resolution (`per-transition-integration-
739
+ // mode-tasking-vs-build`): `taskingIntegration ?? integration`. Unset override ⇒
740
+ // falls back to `integration` (today's behaviour); a repo with
741
+ // `integration:'propose'` + `taskingIntegration:'merge'` lands the task FILES
742
+ // on main here while the BUILD path below still threads plain `integration`.
743
+ integration: options.taskingIntegration ?? options.integration,
744
+ // The per-repo TASK-PLACEMENT default + the operator's explicit
745
+ // override (task `runner-deterministic-slice-placement-policy-and-
746
+ // precedence`). The tasker reads them as the configured-default + the
747
+ // top rung of the runner-deterministic placement resolver; the
748
+ // `originTrust: untrusted` force is read inside the tasker from the
749
+ // prd's stamped frontmatter.
750
+ tasksLandIn: options.tasksLandIn,
751
+ explicitTasksLandIn: options.explicitTasksLandIn,
752
+ noPR: options.noPR,
753
+ providerInstance: options.providerInstance,
754
+ // The tasker review→edit→converge loop (slicer-review-edit-loop): improves the
755
+ // candidate tasks in place + routes the verdict through the needsAnswers /
756
+ // needs-attention sink. Threaded only on the `do prd:` path; omitted ⇒ no loop.
757
+ reviewLoop: options.reviewLoop,
758
+ taskerLoopMax: options.taskerLoopMax,
759
+ reviewExecutions: options.reviewExecutions,
760
+ taskerLoopModel: options.taskerLoopModel,
761
+ // The task-SET ACCEPTANCE GATE (slice-acceptance-gate): rides the BUILD
762
+ // `--review`/`--review-model` family — a fresh-context review of the produced
763
+ // SET before it integrates, ONE-SHOT, independent of the improver loop above.
764
+ review: options.review,
765
+ reviewGate: options.taskReviewGate,
766
+ acceptanceReviewModel: options.reviewModel,
767
+ env,
768
+ // The tasker + review AGENTS launch AMBIENT, never the identity env.
769
+ agentEnv: options.env,
770
+ note,
771
+ });
772
+ return taskResultToDoResult(tasked);
773
+ }
774
+
775
+ const slug = resolved.slug;
776
+
777
+ // 3. Refuse on a DIRTY working tree (ar-run.sh's first guard). `do` runs in a
778
+ // REAL checkout (the human's clone / the CI container); it must NOT
779
+ // entangle unrelated work or run over uncommitted changes. (Mirrors the
780
+ // bash driver: "error: working tree is dirty — commit/stash before
781
+ // running a task.")
782
+ if (await isDirtyTree(cwd, env)) {
783
+ const message =
784
+ `working tree is dirty — commit or stash before running '${slug}' ` +
785
+ '(do runs in-place in this checkout and will not entangle unrelated ' +
786
+ 'changes).';
787
+ return {exitCode: 1, outcome: 'refused', slug, message};
788
+ }
789
+
790
+ // 3b. Refuse on a DIVERGED local `main` (MERGE MODE ONLY — mirrors `complete`'s
791
+ // guard). A local `main` AHEAD of `<arbiter>/main` (unpushed commits) breaks
792
+ // ONLY the paths that fast-forward local `main`, and only merge mode ff's it:
793
+ // the task builds off `<arbiter>/main`, so a merge-back ff cannot apply over
794
+ // a diverged main. Propose mode never ff's local `main` (it pushes the work
795
+ // branch + opens a PR; completion only `switch`es to main, no ff), so the
796
+ // guard is irrelevant there and must NOT fire. Catch it UP FRONT — before the
797
+ // claim + agent run — so a whole build is not wasted. Resolve the mode the
798
+ // SAME way the rest of the flow does (the `options.integration` we thread into
799
+ // `complete`), then fetch (as the onboarding flow does) and compare.
800
+ // `--ignore-diverged-main` overrides (mirrors `--ignore-not-ready`); when
801
+ // overridden, Part 1's non-fatal sync handles the persisting divergence
802
+ // honestly at complete-time.
803
+ if (
804
+ (options.integration ?? 'propose') === 'merge' &&
805
+ options.ignoreDivergedMain !== true
806
+ ) {
807
+ await runAsync('git', ['fetch', '--quiet', arbiter], cwd, {env});
808
+ const ahead = await localMainAheadCount(cwd, arbiter, env);
809
+ if (ahead > 0) {
810
+ const message =
811
+ `local main is ahead of ${arbiter}/main by ${ahead} commit` +
812
+ `${ahead === 1 ? '' : 's'} (unpushed); the task builds off ${arbiter}/main ` +
813
+ "and the merge-back can't fast-forward — push or reconcile main first " +
814
+ '(or re-run with --ignore-diverged-main to proceed anyway).';
815
+ return {exitCode: 1, outcome: 'refused', slug, message};
816
+ }
817
+ }
818
+
819
+ // 3c. PR-INTENT pre-flight guard (the honest-failure value of the `noPR` axis).
820
+ // When this run is `propose` on a GITHUB arbiter and the operator INTENDS a PR
821
+ // (`noPR` unset), run a `gh` AUTH/AVAILABILITY PROBE UP FRONT — BEFORE the
822
+ // claim + agent run — and FAIL FAST if `gh` genuinely cannot open one, instead
823
+ // of letting integration silently degrade to manual-PR instructions. This sits
824
+ // alongside the dirty-tree / diverged-main guards (and mirrors the shared
825
+ // `doNeedsAgentCmd`/`NO_AGENT_CMD_MESSAGE` up-front refusal) so no build work
826
+ // is wasted. CRITICAL: the PROBE is the signal, NOT "is a `providers.github`
827
+ // identity present" — an absent identity falls back to AMBIENT `gh` auth (the
828
+ // common local-dev case), which the probe correctly reports as available, so a
829
+ // working ambient setup still PROCEEDS. A genuinely transient mid-run `gh`
830
+ // outage (probe passes here, the API fails later) is left to the runtime
831
+ // degrade. `noPR: true` skips the guard entirely (no PR is intended).
832
+ {
833
+ const url = await arbiterUrl(cwd, arbiter, env);
834
+ const probe =
835
+ options.ghCanOpenPr ??
836
+ ((probeCwd, probeEnv) =>
837
+ new GitHubProvider().available(probeCwd, probeEnv));
838
+ if (
839
+ shouldFailProposePrIntent({
840
+ mode: options.integration ?? 'propose',
841
+ arbiterIsGitHub: url !== undefined && isGitHubArbiterUrl(url),
842
+ noPR: options.noPR,
843
+ ghCanOpenPr: () => probe(cwd, env),
844
+ })
845
+ ) {
846
+ return {
847
+ exitCode: 1,
848
+ outcome: 'refused',
849
+ slug,
850
+ message: PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
851
+ };
852
+ }
853
+ }
854
+
855
+ // 3d. STATIC fresh-worktree-gate readiness guard (task
856
+ // `do-fails-fast-when-acceptance-gate-statically-unrunnable`). When the
857
+ // fresh-worktree gate is ON AND `prepare` resolves to no commands AND a
858
+ // lockfile is present, the throwaway worktree the gate runs in will have no
859
+ // `node_modules` and `verify`'s tools (`prettier`/`tsc`/`vitest`) will be
860
+ // "command not found" — fail fast HERE, BEFORE the claim and BEFORE spawning
861
+ // the build agent, instead of wasting a whole `do` run and (worse) routing
862
+ // correct work to needs-attention as if the task were at fault. A repo with
863
+ // NO lockfile is the intentional dep-free case (the design point preserved)
864
+ // and proceeds. There is NO verify-unset case — `resolveVerifyCommands`
865
+ // substitutes the default gate when verify is unset/all-blank, so verify is
866
+ // never statically unrunnable-because-unset (the guard is deps-only).
867
+ {
868
+ const guard = checkGatePreconditions({
869
+ freshWorktreeGate: options.freshWorktreeGate,
870
+ prepare: options.prepare,
871
+ lockfile: detectLockfileOnDisk(cwd),
872
+ });
873
+ if (guard !== undefined) {
874
+ return {exitCode: 1, outcome: 'refused', slug, message: guard.message};
875
+ }
876
+ }
877
+
878
+ // 4. Onboard via the ISOLATION SEAM (`selectIsolationStrategy`/`inPlaceStrategy`)
879
+ // with the SAME claim-first composition `do --remote` (and `run`) use — this
880
+ // is the consumer that finally puts in-place `do` on the seam, so all THREE
881
+ // `do`/`run` forms share the one `IsolatedTree`-handle post-claim shape:
882
+ //
883
+ // a. CLAIM explicitly via the CAS (the claim is the `do` driver's job,
884
+ // BEFORE prepare). A lost/contended/usage claim is propagated verbatim
885
+ // and NOTHING is onboarded — the same clean skip `run`/`do --remote` do.
886
+ // An already-in-progress / done / absent item is NOT claimable, so the
887
+ // CAS returns `lost` (exit 2): `do` (the autonomous CI worker) never
888
+ // re-claims an item someone else holds, exactly like its siblings.
889
+ // b. `selectIsolationStrategy({checkout})` → `inPlaceStrategy`, whose
890
+ // `prepare` does the ONBOARDING half (fetch + continue-detection +
891
+ // fresh-main `work/<slug>` switch, incl. the §14 continue/rebase path)
892
+ // WITHOUT re-claiming — the split the seam mandates (claim → driver,
893
+ // onboarding → strategy).
894
+ const claim = await performClaim({
895
+ slug,
896
+ cwd,
897
+ arbiter,
898
+ // `--allow-backlog`: widen the claimable predicate to ALSO accept a
899
+ // `tasks/backlog/`-resident body. Claim stays a pure lock (writes nothing to
900
+ // `main`, moves nothing); default off ⇒ pool-only, exactly as today.
901
+ allowBacklog: options.allowBacklog === true,
902
+ env,
903
+ note,
904
+ });
905
+ if (claim.outcome === 'lost') {
906
+ return {exitCode: 2, outcome: 'lost', slug, message: claim.message};
907
+ }
908
+ if (claim.outcome === 'contended') {
909
+ return {exitCode: 3, outcome: 'contended', slug, message: claim.message};
910
+ }
911
+ if (claim.exitCode !== 0) {
912
+ // usage/environment error (not inside a repo, no arbiter remote, dirty
913
+ // index, …): surface verbatim. NOTHING was onboarded.
914
+ return {exitCode: 1, outcome: 'usage-error', slug, message: claim.message};
915
+ }
916
+
917
+ // The claim landed (the item is now in-progress on the arbiter). Onboard the
918
+ // checkout onto its work branch THROUGH the seam — the in-place strategy puts
919
+ // `cwd` on `work/<slug>` off the freshly-fetched `<arbiter>/main` (or continues
920
+ // a kept requeue branch + rebases it, §14/§10). `prepare` can throw on a genuine
921
+ // plumbing failure (unreachable arbiter, …) — surface that as a usage error,
922
+ // never a false success.
923
+ let tree: IsolatedTree;
924
+ try {
925
+ tree = selectIsolationStrategy({checkout: cwd, arbiter}).prepare({
926
+ slug,
927
+ // The task-build path: namespace the branch `work/task-<slug>`, and
928
+ // branch it from the EXACT claim commit (the defensive guard) so a stale
929
+ // same-named branch (e.g. one `intake` left) is re-pointed, never reused.
930
+ type: 'task',
931
+ claimCommit: claim.claimCommit,
932
+ env,
933
+ });
934
+ } catch (err) {
935
+ const message = err instanceof Error ? err.message : String(err);
936
+ return {exitCode: 1, outcome: 'usage-error', slug, message};
937
+ }
938
+ const branch = tree.branch;
939
+
940
+ // 4a. CONTINUE rebase conflict (ADR §14 + §10): a requeue kept a `work/<slug>`
941
+ // that did not replay onto the current main at onboard-time (aborted, never
942
+ // auto-resolved). Surface to needs-attention TREE-LESSLY via the SAME `#89`
943
+ // mechanism `requeue` uses for the reverse direction — the rebase was
944
+ // ABORTED, so the kept `work/<slug>` tip == the arbiter tip (already on the
945
+ // arbiter, after-commit, recoverable). The surface is purely the one-file
946
+ // `in-progress/ → needs-attention/` ledger move + reason (no branch push, no
947
+ // worktree mutation) instead of running the agent — the §10 path. The work
948
+ // did NOT onboard; the runner owns the bounce.
949
+ if (tree.continueRebaseConflict) {
950
+ const reason =
951
+ `continuing the kept ${tree.branch}: rebase onto the latest main ` +
952
+ 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
953
+ 'main, or `requeue --reset` to discard and start fresh';
954
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
955
+ cwd: tree.dir,
956
+ slug,
957
+ reason,
958
+ arbiter: tree.arbiterRemote,
959
+ env,
960
+ note,
961
+ });
962
+ if (!surfaced.moved) {
963
+ return surfaceUnmovedDoResult({slug, branch, reason, surfaced});
964
+ }
965
+ return {
966
+ exitCode: 1,
967
+ outcome: 'needs-attention',
968
+ slug,
969
+ branch,
970
+ message: reason,
971
+ };
972
+ }
973
+
974
+ // 4b. CONTINUE reconcile-push TERMINAL failure (the stale-lease-strand bug):
975
+ // the onboard reconcile push of the kept (already-committed) work branch
976
+ // FAILED terminally (stale-lease cap exhausted, or a non-stale-lease
977
+ // rejection / unreachable arbiter). The push helper THROWS; the strategy
978
+ // CATCHES it and flags `continuePushFailure` so the run does NOT crash
979
+ // leaving the task silently in-progress. Surface to needs-attention
980
+ // TREE-LESSLY via the SAME `#89` mechanism `requeue` uses — the kept branch
981
+ // is already on the arbiter (after-commit, recoverable), so the surface is
982
+ // purely the one-file ledger move + reason (no branch push, no worktree).
983
+ if (tree.continuePushFailure !== undefined) {
984
+ const reason =
985
+ `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
986
+ `to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
987
+ 'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
988
+ 'once the churn settles, or `requeue --reset` to discard and start fresh';
989
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
990
+ cwd: tree.dir,
991
+ slug,
992
+ reason,
993
+ arbiter: tree.arbiterRemote,
994
+ env,
995
+ note,
996
+ });
997
+ if (!surfaced.moved) {
998
+ return surfaceUnmovedDoResult({slug, branch, reason, surfaced});
999
+ }
1000
+ return {
1001
+ exitCode: 1,
1002
+ outcome: 'needs-attention',
1003
+ slug,
1004
+ branch,
1005
+ message: reason,
1006
+ };
1007
+ }
1008
+
1009
+ // 5. Run the agent autonomously in the checkout, ON the work branch — the
1010
+ // SAME prompt assembly `dorfl prompt` emits (canonical wrapper +
1011
+ // source prd + the task's ## Prompt). The agent only edits code (it does
1012
+ // no git). This is the one NEW middle step `ar-run.sh` shelled out for
1013
+ // (`prompt | pi`).
1014
+ // The post-claim pipeline reads the uniform `IsolatedTree` handle (`tree.dir`)
1015
+ // — in-place that IS `cwd`, but reading the handle keeps the shared shape the
1016
+ // future advance-loop tick wraps (no in-place-only special case).
1017
+ let prompt: string;
1018
+ try {
1019
+ // CONTINUE-aware resolution: on a continue (the arbiter holds a kept
1020
+ // `work/<slug>` whose tip is STRANDED off main) the task may already be in
1021
+ // `work/done/`; admit `done/` ONLY behind the tip-vs-arbiter stranded gate
1022
+ // (story 5), reusing the SAME refs the continue-detection uses.
1023
+ const task = resolveTask(
1024
+ tree.dir,
1025
+ slug,
1026
+ {
1027
+ cwd: tree.dir,
1028
+ branchRef: `${tree.arbiterRemote}/${tree.branch}`,
1029
+ mainRef: `${tree.arbiterRemote}/main`,
1030
+ env,
1031
+ },
1032
+ // `--allow-backlog`: also resolve a `tasks/backlog/`-resident body
1033
+ // (lowest priority). Default off ⇒ unchanged.
1034
+ {allowBacklog: options.allowBacklog === true},
1035
+ );
1036
+ // CONTINUE-mode (the `agent-prompt-continue-context` task): if the arbiter
1037
+ // holds a kept `work/<slug>` ahead of main (a requeue) the checkout was
1038
+ // CONTINUED onto it — inject the continue block (prior diff + reason + note).
1039
+ // REUSE the SAME continue-detection the onboarding path used (in-place clone
1040
+ // refs: `<arbiter>/work/<slug>` vs `<arbiter>/main`).
1041
+ const continueContext = resolveContinueContext({
1042
+ cwd: tree.dir,
1043
+ slug,
1044
+ arbiter: tree.arbiterRemote,
1045
+ branchRef: `${tree.arbiterRemote}/${tree.branch}`,
1046
+ mainRef: `${tree.arbiterRemote}/main`,
1047
+ content: readFileSync(task.path, 'utf8'),
1048
+ env,
1049
+ });
1050
+ // Per-item override layer: a task or prd may pin `promptGuidance.testFirst`
1051
+ // in its frontmatter, superseding the resolved repo policy for THIS item.
1052
+ const itemGuidance = resolvePromptGuidanceForItem({
1053
+ cwd: tree.dir,
1054
+ repoResolved: {testFirst: options.promptGuidance?.testFirst === true},
1055
+ taskContent: readFileSync(task.path, 'utf8'),
1056
+ });
1057
+ prompt = buildAgentPrompt(task.slug, task.prd, task.taskPrompt, {
1058
+ cwd: tree.dir,
1059
+ continueContext,
1060
+ promptGuidance: itemGuidance,
1061
+ });
1062
+ } catch (err) {
1063
+ if (err instanceof PromptError) {
1064
+ return await saveAgentFailure({
1065
+ slug,
1066
+ branch,
1067
+ cwd: tree.dir,
1068
+ arbiter: tree.arbiterRemote,
1069
+ detail: err.message,
1070
+ env,
1071
+ note,
1072
+ });
1073
+ }
1074
+ throw err;
1075
+ }
1076
+
1077
+ let agent: {ok: boolean; detail?: string; output?: string};
1078
+ try {
1079
+ agent = await runDoAgent(options, tree.dir, prompt, slug);
1080
+ } catch (err) {
1081
+ const message = err instanceof Error ? err.message : String(err);
1082
+ return await saveAgentFailure({
1083
+ slug,
1084
+ branch,
1085
+ cwd: tree.dir,
1086
+ arbiter: tree.arbiterRemote,
1087
+ detail: message,
1088
+ env,
1089
+ note,
1090
+ });
1091
+ }
1092
+ if (!agent.ok) {
1093
+ const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
1094
+ return await saveAgentFailure({
1095
+ slug,
1096
+ branch,
1097
+ cwd: tree.dir,
1098
+ arbiter: tree.arbiterRemote,
1099
+ detail,
1100
+ env,
1101
+ note,
1102
+ });
1103
+ }
1104
+
1105
+ // 5b. HONOR a deliberate STOP (task `agent-stop-signal`). The agent exited
1106
+ // cleanly (`agent.ok`), but the CLAIM-PROTOCOL wrapper tells it to STOP and
1107
+ // report on a DRIFTED/ambiguous/stale-premise task WITHOUT building. Detect
1108
+ // that BEFORE the gate via the in-band sentinel (the agent's reason is the
1109
+ // needs-attention reason VERBATIM); a clean STOP with no source change is the
1110
+ // deterministic empty-diff backstop. Either routes to needs-attention and
1111
+ // SKIPS the acceptance gate AND Gate-2 — a clean STOP is NOT "a build that
1112
+ // changed nothing".
1113
+ const stopReason = await resolveStopReason({
1114
+ output: agent.output,
1115
+ slug,
1116
+ cwd: tree.dir,
1117
+ arbiter: tree.arbiterRemote,
1118
+ env,
1119
+ });
1120
+ if (stopReason !== undefined) {
1121
+ return await saveAgentStop({
1122
+ slug,
1123
+ branch,
1124
+ cwd: tree.dir,
1125
+ arbiter: tree.arbiterRemote,
1126
+ reason: stopReason,
1127
+ env,
1128
+ note,
1129
+ });
1130
+ }
1131
+
1132
+ // 6. Gate + done-move + commit + rebase + integrate + branch-tidy LIKE
1133
+ // `complete` — but with the AUTONOMOUS needs-attention surfacing (pass
1134
+ // `surfaceArbiter` so a red gate / rebase conflict surfaces on the
1135
+ // arbiter's main, cross-machine visible — a stuck CI `do` that only routed
1136
+ // locally would be invisible). The success path reuses `complete`'s
1137
+ // machinery unchanged.
1138
+ //
1139
+ // Push-time transport-coherence guard (identity): refuse a forbidden transport
1140
+ // for THIS arbiter's actual URL rather than silently pushing under an ambient
1141
+ // credential. A no-op when no identity is configured.
1142
+ try {
1143
+ assertTransportAllowed(options.identity, tree.arbiterUrl);
1144
+ } catch (err) {
1145
+ const message = err instanceof Error ? err.message : String(err);
1146
+ return await saveAgentFailure({
1147
+ slug,
1148
+ branch,
1149
+ cwd: tree.dir,
1150
+ arbiter: tree.arbiterRemote,
1151
+ detail: message,
1152
+ env,
1153
+ note,
1154
+ });
1155
+ }
1156
+ const completed = await performComplete({
1157
+ slug,
1158
+ cwd: tree.dir,
1159
+ arbiter: tree.arbiterRemote,
1160
+ integration: options.integration,
1161
+ // An explicit `--merge` overrides the untrusted-origin build-propose rule (task
1162
+ // `untrusted-origin-forces-build-propose`); the autonomous path leaves it
1163
+ // unset so untrusted-origin reliably forces propose.
1164
+ explicitMerge: options.explicitMerge,
1165
+ // `do` already ran the pre-flight divergence guard UP FRONT (step 3b), before
1166
+ // the claim + agent; skip `complete`'s redundant re-check. When `do` was run
1167
+ // with --ignore-diverged-main the guard was bypassed there too, so either way
1168
+ // the (now non-fatal) local-main sync handles any persisting divergence.
1169
+ ignoreDivergedMain: true,
1170
+ // `--allow-backlog`: treat a `tasks/backlog/`-resident body as a valid build
1171
+ // source so the done-move goes `tasks/backlog/ → tasks/done/` directly. Gated
1172
+ // on the flag (without it `complete` refuses a staged strand). Default off.
1173
+ allowBacklog: options.allowBacklog === true,
1174
+ prepare: options.prepare,
1175
+ verify: options.verify,
1176
+ freshWorktreeGate: options.freshWorktreeGate,
1177
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — the
1178
+ // git-alone FLOOR of the cross-job land queue (prd `land-time-reverify-and-
1179
+ // parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). Threaded so a
1180
+ // wide-matrix CI's raised cap actually reaches the merge loop.
1181
+ mergeRetries: options.mergeRetries,
1182
+ noPR: options.noPR,
1183
+ // The resolved provider INSTANCE seam (tests/embeddings inject a stubbed
1184
+ // GitHubProvider to drive the propose pipeline offline). Unset ⇒ the core
1185
+ // selects from the arbiter URL.
1186
+ providerInstance: options.providerInstance,
1187
+ // Half B (propose-mode PR body): the build agent's FINAL SUMMARY, captured
1188
+ // from the harness seam's `LaunchResult.output` (surfaced by `runDoAgent`
1189
+ // below) and threaded as the PR description. `complete` scaffolds a
1190
+ // deterministic header (task pointer) above it. Undefined ⇒ no body ⇒ the
1191
+ // provider degrades to `gh ... --fill` (no regression).
1192
+ body: agent.output,
1193
+ // Gate 2 (PR/code review) rides INSIDE `complete`: run the `review` SKILL as a
1194
+ // fresh-context agent after the green `verify` (the non-skippable floor) and
1195
+ // before the done-move. A `block` re-uses the same needs-attention surfacing
1196
+ // (`surfaceArbiter`) the red gate does; mapped to `needs-attention` below.
1197
+ review: options.review,
1198
+ reviewModel: options.reviewModel,
1199
+ reviewMaxRounds: options.reviewMaxRounds,
1200
+ reviewGate: options.reviewGate,
1201
+ // `--watch` (task `watch-review-session`): tail the Gate-2 review agent's
1202
+ // session live too, AFTER the build stream the `runDoAgent` watch surfaced
1203
+ // (the gate prints a build→review boundary). Threaded into the gate launch via
1204
+ // `complete`; OFF ⇒ the review path is byte-identical (sync launch, no tailer).
1205
+ watch: options.watch,
1206
+ watchSink: options.watchSink,
1207
+ sessionsDir: options.sessionsDir,
1208
+ // The autonomous failure-surfacing: route stuck items to the arbiter's
1209
+ // main (the `run` semantics), NOT local-only (the human `complete`).
1210
+ surfaceArbiter: tree.arbiterRemote,
1211
+ color: options.color,
1212
+ note,
1213
+ noteBlock: options.noteBlock,
1214
+ env,
1215
+ // The review AGENT (Gate 2) launches AMBIENT — never the identity env (an
1216
+ // agent must not act as the bot; only the runner's git ops carry identity).
1217
+ agentEnv: options.env,
1218
+ });
1219
+
1220
+ if (
1221
+ completed.outcome === 'completed' ||
1222
+ completed.outcome === 'already-integrated'
1223
+ ) {
1224
+ // `already-integrated` is the stranded-done auto-recover's clean no-op (the
1225
+ // kept tip was already on `<arbiter>/main` — e.g. the prior PR merged
1226
+ // out-of-band before the re-claim). It is a SUCCESSFUL terminal state — the
1227
+ // work is integrated — so it folds into `DoOutcome 'completed'` (exit 0),
1228
+ // not a new outcome value: the autonomous caller just needs to know the
1229
+ // integrate path ended cleanly. The distinct `CompleteOutcome` value
1230
+ // preserves the no-op signal for tests + the `complete` surface.
1231
+ return {
1232
+ exitCode: 0,
1233
+ outcome: 'completed',
1234
+ slug,
1235
+ branch,
1236
+ message: completed.message,
1237
+ };
1238
+ }
1239
+ if (completed.outcome === 'review-unparseable') {
1240
+ // Gate 2 RAN but its verdict was UNPARSEABLE (malformed JSON). The core already
1241
+ // routed it work-preservingly (branch pushed + surfaced). This is the FAILURE-
1242
+ // CAUSE axis, NOT a reviewer block: classify the parse-failure phrase the core
1243
+ // recorded → `transient-infra` (re-run the SAME work: the gate output is
1244
+ // stochastic + the parser now repairs the control-char class, so a re-run is far
1245
+ // more likely to parse). Same `classifyFailureCause` `run` uses, so `do`/`run`
1246
+ // agree on the label. (`classifyFailureCause` only EVER returns config/transient/
1247
+ // agent-failed; the malformed-JSON signature lands transient-infra, never a
1248
+ // silent success.)
1249
+ const cause = classifyFailureCause(completed.message);
1250
+ return {
1251
+ exitCode: 1,
1252
+ outcome: failureCauseToDoOutcome(cause),
1253
+ slug,
1254
+ branch,
1255
+ message: completed.message,
1256
+ };
1257
+ }
1258
+ if (
1259
+ completed.outcome === 'prepare-failed' ||
1260
+ completed.outcome === 'gate-failed' ||
1261
+ completed.outcome === 'review-blocked' ||
1262
+ completed.outcome === 'rebase-conflict' ||
1263
+ completed.outcome === 'strand-surfaced'
1264
+ ) {
1265
+ // Failed env-prep / red gate / Gate-2 review block / rebase conflict — routed
1266
+ // to needs-attention (surfaced on the arbiter). A `prepare-failed` (the env
1267
+ // could not be made ready, so verify was NOT run) and a `review-blocked` are
1268
+ // mapped HERE the SAME way `gate-failed` is. The work did NOT complete; the
1269
+ // runner owns the bounce.
1270
+ //
1271
+ // `strand-surfaced` is the autonomous-strand parity (the SHARED `complete.ts`
1272
+ // seam already surfaced the source-strand / empty-staged refusal to
1273
+ // needs-attention on the arbiter) — in-place `performDo` inherits the fix
1274
+ // here, mapped to the SAME `needs-attention` outcome shape `do --remote`
1275
+ // (`runRemotePipeline`) uses, so `advance task:<slug>` (via the default
1276
+ // `doDriver = performDo`) agrees with the remote path on the caller-visible
1277
+ // label.
1278
+ return {
1279
+ exitCode: 1,
1280
+ outcome: 'needs-attention',
1281
+ slug,
1282
+ branch,
1283
+ message: completed.message,
1284
+ };
1285
+ }
1286
+ if (completed.outcome === 'surface-unmoved') {
1287
+ // Strand-surface could not land on the arbiter (CAS contention exhausted /
1288
+ // no arbiter) — HONESTLY still in-progress on the arbiter. Mirror
1289
+ // `runRemotePipeline`'s `surface-unmoved` mapping so in-place `performDo`
1290
+ // (and `advance task:<slug>` via the default `doDriver`) agrees with
1291
+ // `do --remote` on the same signal, never a fake success.
1292
+ return {
1293
+ exitCode: 1,
1294
+ outcome: 'surface-unmoved',
1295
+ slug,
1296
+ branch,
1297
+ message: completed.message,
1298
+ };
1299
+ }
1300
+ // refused (nothing to commit, wrong folder) / usage-error: surface verbatim —
1301
+ // BUT first reclassify a thrown CORE wiring/config error (which `performComplete`
1302
+ // swallows into `usage-error`) onto the SAME `config-error` cause `run` records,
1303
+ // closing the cross-path divergence (`do`: usage-error vs `run`: agent-failed for
1304
+ // the identical thrown core error). Best-effort: a non-config usage-error stays
1305
+ // `usage-error` (the conservative default — the classifier only re-labels what it
1306
+ // recognises).
1307
+ if (completed.outcome === 'usage-error') {
1308
+ const cause = classifyFailureCause(completed.message);
1309
+ if (cause === 'config-error') {
1310
+ return {
1311
+ exitCode: 1,
1312
+ outcome: 'config-error',
1313
+ slug,
1314
+ branch,
1315
+ message: completed.message,
1316
+ };
1317
+ }
1318
+ }
1319
+ const outcome: DoOutcome =
1320
+ completed.outcome === 'refused' ? 'refused' : 'usage-error';
1321
+ return {exitCode: 1, outcome, slug, branch, message: completed.message};
1322
+ }
1323
+
1324
+ /**
1325
+ * The EXACT recovery one-liner handed to the operator when an isolated/remote `do`
1326
+ * integration fails terminally AFTER the work was committed + done-moved (the
1327
+ * stale-lease-strand class surfaced by Part B #97), leaving the job worktree
1328
+ * RETAINED. It points them straight at the recover-already-committed path
1329
+ * (`complete --isolated <slug>`) so they need not reverse-engineer the encoded
1330
+ * worktree path \u2014 the FINISH half of try-to-finish / else-surface. Detection is
1331
+ * unspoofable (an already-integrated task is a clean no-op), so re-running it is
1332
+ * always safe.
1333
+ *
1334
+ * `complete --isolated` recovers the retained WORKTREE, so it ONLY works ON THE
1335
+ * MACHINE that ran the job (the worktree is local + reaped when that runner ends).
1336
+ * From a DIFFERENT checkout (e.g. a CI-stranded job finished on your laptop),
1337
+ * `--isolated` finds no local worktree and silently no-ops; there, check out the
1338
+ * already-pushed work branch off the arbiter and run plain `complete`. Both named.
1339
+ */
1340
+ export function recoverIsolatedOneLiner(slug: string): string {
1341
+ return (
1342
+ `To FINISH the stranded branch once the cause clears: ON THE SAME MACHINE that ` +
1343
+ `ran the job, run \`dorfl complete --isolated ${slug}\` (integrates the ` +
1344
+ `kept commit from the retained worktree; a no-op if already integrated). From ` +
1345
+ `ANOTHER checkout (e.g. a CI-stranded job finished on your laptop), check out ` +
1346
+ `the pushed work branch off the arbiter and run plain \`dorfl complete ` +
1347
+ `${slug}\` instead.`
1348
+ );
1349
+ }
1350
+
1351
+ /**
1352
+ * Build the HONEST per-op fragment describing WHAT actually reached the arbiter
1353
+ * after a needs-attention route — reading the seam's captured per-op outcome
1354
+ * (`branchPush`) rather than ASSUMING "pushed" off the local move. Shared by
1355
+ * every save-failure / save-stop message site so they can never drift from
1356
+ * reality (the observed bug: the report claimed "pushed" when the branch push
1357
+ * was skipped-empty or failed). A PUSH failure (HIGH severity: work-at-risk /
1358
+ * breaks cross-machine recovery) flips the fragment to a loud "saved LOCALLY
1359
+ * only" with recovery guidance. The OBSERVABLE half (the stuck state) now rides
1360
+ * on the per-item lock `state: stuck` amend — there is no separate on-`main`
1361
+ * surface to report.
1362
+ */
1363
+ function routeReport(
1364
+ routed: ApplyNeedsAttentionTransitionResult,
1365
+ branch: string,
1366
+ ): {fragment: string; pushFailed: boolean} {
1367
+ const branchPush = routed.branchPush ?? 'not-attempted';
1368
+ const branchFailed = branchPush === 'failed';
1369
+
1370
+ if (branchFailed) {
1371
+ // HIGH severity: the recoverable branch push did not reach the arbiter — say
1372
+ // so loudly, the work is saved LOCALLY only, and how to recover.
1373
+ return {
1374
+ fragment:
1375
+ `push of ${branch} FAILED — the work is saved LOCALLY only; push it ` +
1376
+ 'when online, then `requeue` (continue), or `requeue --reset` to discard',
1377
+ pushFailed: true,
1378
+ };
1379
+ }
1380
+
1381
+ // The push that was attempted succeeded (or was honestly skipped). Report
1382
+ // the branch state truthfully.
1383
+ let landed: string;
1384
+ if (branchPush === 'pushed') {
1385
+ landed = `pushed ${branch}`;
1386
+ } else if (branchPush === 'skipped-empty') {
1387
+ landed = `branch ${branch} skipped (nothing to recover yet)`;
1388
+ } else {
1389
+ landed = 'saved locally';
1390
+ }
1391
+ return {
1392
+ fragment: `${landed}. Recover via \`requeue\` (continue) or \`requeue --reset\` to discard`,
1393
+ pushFailed: false,
1394
+ };
1395
+ }
1396
+
1397
+ /**
1398
+ * SAVE the partial work of a FAILED agent instead of dropping it (the keystone of
1399
+ * the `agent-fail-saves-work` task). An agent failure (`runDoAgent` returned
1400
+ * `ok:false`, threw, or the prompt could not be assembled) used to BARE-RETURN
1401
+ * `agent-failed`, leaving whatever the agent edited only on the local work branch
1402
+ * in the (disposable, possibly remote) job worktree — silently lost.
1403
+ *
1404
+ * This routes it through the SAME work-preserving machinery a RED GATE uses: the
1405
+ * ledger write seam's needs-attention transition (`git add -A` + a wip commit
1406
+ * capturing the agent's work + the `git mv → needs-attention/` move-only commit
1407
+ * with the failure detail recorded as the reason in the body), surfaced on the
1408
+ * arbiter's `main` (the autonomous, cross-machine-visible mode-M surfacing `do`
1409
+ * already uses for the gate-fail path) so `scan`/`status`/another machine see it.
1410
+ *
1411
+ * It ALSO pushes the `work/<slug>` branch to the arbiter so the saved partial
1412
+ * commits travel cross-machine and the item is RECOVERABLE via `requeue`
1413
+ * (continue): the continue-detection in `continue-branch.ts` looks for an arbiter
1414
+ * `work/<slug>` ahead of main. That push now lives IN the ledger write seam (the
1415
+ * RECOVERABLE half of the needs-attention transition — fired when an `arbiter` is
1416
+ * given, best-effort, emptiness-guarded), consolidated there by
1417
+ * `centralise-bounce-branch-push` so it cannot drift from the OBSERVABLE surface;
1418
+ * this function no longer pushes separately.
1419
+ *
1420
+ * The EMPTY-failure case (the agent made NO commits / no changes) is handled
1421
+ * without crashing on an empty commit: `routeToNeedsAttention` (under the seam)
1422
+ * skips the wip commit when the tree is clean, and the move-only commit (reason +
1423
+ * the `git mv`) is always non-empty, so the failure reason is still surfaced.
1424
+ *
1425
+ * The OUTCOME is the classified failure CAUSE (the FAILURE-CAUSE axis): the
1426
+ * genuinely-new `transient-infra` / `config-error` where the surfaced detail makes
1427
+ * the cause knowable, else the conservative generic `agent-failed` (still distinct
1428
+ * from a clean success and from a red `gate-failed`/`needs-attention` — `do`'s exit
1429
+ * contract stays coherent). Only the WORK-PRESERVING side-effect (unchanged here)
1430
+ * matches the gate-failure path. We do NOT validate or "fix" the partial work — a broken
1431
+ * tree committed + surfaced (with the reason) is recoverable; the human chooses
1432
+ * `requeue` (continue) vs `requeue --reset` (discard).
1433
+ */
1434
+ async function saveAgentFailure(params: {
1435
+ slug: string;
1436
+ branch: string | undefined;
1437
+ cwd: string;
1438
+ arbiter: string;
1439
+ detail: string;
1440
+ env: NodeJS.ProcessEnv | undefined;
1441
+ note: (message: string) => void;
1442
+ }): Promise<DoResult> {
1443
+ const {slug, cwd, arbiter, detail, env, note} = params;
1444
+ // The work branch is the namespaced build branch (`work/task-<slug>`; the
1445
+ // onboarding switched the checkout to it before the agent ran); derive it from
1446
+ // the slug so the push target is always defined even when the caller's `branch`
1447
+ // was not narrowed.
1448
+ const branch = params.branch ?? workBranchRef('task', slug);
1449
+ // Classify the failure CAUSE (best-effort + conservative) from the surfaced
1450
+ // detail — the SAME `classifyFailureCause` `run` uses, so `do`/`run` agree on the
1451
+ // same error. The cause LABEL prefixes the recorded reason so the cause is legible
1452
+ // on the needs-attention route without a second naming scheme; `agent-failed`
1453
+ // keeps the historical "agent failed:" prefix (no reason-prose regression).
1454
+ const cause = classifyFailureCause(detail);
1455
+ const reason = `${failureCauseLabel(cause)}: ${detail}`;
1456
+
1457
+ // Route through the SAME seam the gate-fail path uses: save the agent's work as
1458
+ // a wip commit (skipped when the tree is clean — the empty-failure case), amend
1459
+ // the item's per-item lock ref to `state: stuck` with the reason in the body,
1460
+ // surface that lock amend on the arbiter (OBSERVABLE, mode-M, cross-machine
1461
+ // visible) AND push the `work/<slug>` branch (RECOVERABLE — so a requeue-continue
1462
+ // reading <arbiter>/work/<slug> lands on the saved wip). Both halves fire from
1463
+ // the single `arbiter` here; no separate push to forget.
1464
+ const routed = await ledgerWrite.applyNeedsAttentionTransition({
1465
+ cwd,
1466
+ slug,
1467
+ reason,
1468
+ arbiter,
1469
+ env,
1470
+ note,
1471
+ });
1472
+
1473
+ const report = routed.moved ? routeReport(routed, branch) : undefined;
1474
+ const message = routed.moved
1475
+ ? `Agent run failed building '${slug}' [${cause}] (${detail}); SAVED the ` +
1476
+ `partial work and marked '${slug}' stuck on its lock (${report!.fragment}).`
1477
+ : `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
1478
+ `mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
1479
+ note(message);
1480
+ return {
1481
+ exitCode: 1,
1482
+ outcome: failureCauseToDoOutcome(cause),
1483
+ slug,
1484
+ branch,
1485
+ routedToNeedsAttention: routed.moved,
1486
+ message,
1487
+ };
1488
+ }
1489
+
1490
+ /**
1491
+ * Map a {@link FailureCause} onto the `do` {@link DoOutcome}. The cause names ARE
1492
+ * the outcome names (the FAILURE-CAUSE axis reuses the terminal vocabulary), so
1493
+ * this is identity — a single helper documents the mapping + keeps the `do`/`run`
1494
+ * sites symmetric (`run` has the twin {@link failureCauseToItemStatus}).
1495
+ */
1496
+ function failureCauseToDoOutcome(cause: FailureCause): DoOutcome {
1497
+ return cause;
1498
+ }
1499
+
1500
+ /**
1501
+ * Build the HONEST result for a CONTINUE-site surface that did NOT land on the
1502
+ * arbiter (`{moved: false}`). The tree-less `in-progress/ → needs-attention/` move
1503
+ * lost the CAS race against a busy arbiter (its contention-retry cap exhausted) or
1504
+ * had no arbiter to publish to, so the item is STILL in-progress on the arbiter —
1505
+ * a clean `needs-attention` would mislead (it claims the surface landed). Distinct
1506
+ * `surface-unmoved` outcome, carrying `reasonNotMoved`, so the caller/human can
1507
+ * tell it from a successful surface and retry/resolve. The `moved: true` path is
1508
+ * left byte-for-byte unchanged (this branch is only reached on `!moved`).
1509
+ */
1510
+ function surfaceUnmovedDoResult(params: {
1511
+ slug: string;
1512
+ branch?: string;
1513
+ reason: string;
1514
+ surfaced: SurfaceToNeedsAttentionResult;
1515
+ }): DoResult {
1516
+ const {slug, branch, reason, surfaced} = params;
1517
+ const message =
1518
+ `'${slug}' could NOT be surfaced to needs-attention — the surface did not ` +
1519
+ `reach the arbiter's main; the item is still IN-PROGRESS on the arbiter ` +
1520
+ `(retry/resolve). ${surfaced.reasonNotMoved ?? reason}`;
1521
+ return {exitCode: 1, outcome: 'surface-unmoved', slug, branch, message};
1522
+ }
1523
+
1524
+ /**
1525
+ * Resolve the STOP reason for a clean (`agent.ok`) run, or `undefined` when the
1526
+ * run is a genuine build that should proceed to the gate (task
1527
+ * `agent-stop-signal`). TWO independent triggers, the sentinel winning:
1528
+ *
1529
+ * 1. The IN-BAND STOP sentinel in the agent's output ({@link parseStopSentinel})
1530
+ * — the principled case: the agent declared the task drifted/ambiguous and
1531
+ * reported WHY. Its reason is used VERBATIM (a non-empty diff with a sentinel
1532
+ * is still a STOP — the agent may have left scratch; the sentinel wins).
1533
+ * 2. The DETERMINISTIC empty-diff backstop ({@link isWorkBranchDiffEmpty}) — the
1534
+ * observable safety net for when the agent stopped WITHOUT (or with a
1535
+ * malformed) sentinel: `agent.ok` but no source change vs `<arbiter>/main` is
1536
+ * never a successful build.
1537
+ *
1538
+ * Shared by `performDo` and `runRemotePipeline` so the in-place and remote forms
1539
+ * detect a STOP identically.
1540
+ */
1541
+ async function resolveStopReason(params: {
1542
+ output: string | undefined;
1543
+ slug: string;
1544
+ cwd: string;
1545
+ arbiter: string;
1546
+ env: NodeJS.ProcessEnv | undefined;
1547
+ }): Promise<string | undefined> {
1548
+ const {output, slug, cwd, arbiter, env} = params;
1549
+ const sentinel = parseStopSentinel(output);
1550
+ if (sentinel !== undefined) {
1551
+ return sentinel.reason;
1552
+ }
1553
+ if (await isWorkBranchDiffEmpty({cwd, arbiter, env})) {
1554
+ return emptyDiffStopReason(slug);
1555
+ }
1556
+ return undefined;
1557
+ }
1558
+
1559
+ /**
1560
+ * Route a DELIBERATE agent STOP (task `agent-stop-signal`) to needs-attention
1561
+ * through the SAME work-preserving seam `saveAgentFailure` uses (save the branch,
1562
+ * surface on the arbiter) — but as the DISTINCT `agent-stopped` outcome, NOT
1563
+ * `agent-failed` (the agent did not error) nor `needs-attention` (no red gate /
1564
+ * rebase conflict). The agent's STOP reason is recorded VERBATIM as the
1565
+ * needs-attention reason. The acceptance gate AND Gate-2 are NEVER reached.
1566
+ */
1567
+ async function saveAgentStop(params: {
1568
+ slug: string;
1569
+ branch: string | undefined;
1570
+ cwd: string;
1571
+ arbiter: string;
1572
+ reason: string;
1573
+ env: NodeJS.ProcessEnv | undefined;
1574
+ note: (message: string) => void;
1575
+ }): Promise<DoResult> {
1576
+ const {slug, cwd, arbiter, reason, env, note} = params;
1577
+ const branch = params.branch ?? workBranchRef('task', slug);
1578
+
1579
+ const routed = await ledgerWrite.applyNeedsAttentionTransition({
1580
+ cwd,
1581
+ slug,
1582
+ reason,
1583
+ arbiter,
1584
+ env,
1585
+ note,
1586
+ });
1587
+
1588
+ const report = routed.moved ? routeReport(routed, branch) : undefined;
1589
+ const message = routed.moved
1590
+ ? `The agent STOPPED building '${slug}' (the task drifted / is ambiguous / ` +
1591
+ `produced no change); marked '${slug}' stuck on its lock (${report!.fragment}) ` +
1592
+ `WITHOUT running the gate or Gate-2 review. Reason: ${reason}`
1593
+ : `The agent STOPPED building '${slug}' but it could not be marked stuck ` +
1594
+ `(${routed.reasonNotMoved ?? 'unknown'}). Reason: ${reason}`;
1595
+ note(message);
1596
+ return {
1597
+ exitCode: 1,
1598
+ outcome: 'agent-stopped',
1599
+ slug,
1600
+ branch,
1601
+ routedToNeedsAttention: routed.moved,
1602
+ message,
1603
+ };
1604
+ }
1605
+
1606
+ /**
1607
+ * Run the agent against the checkout. Prefers the injected `dorfl` (tests
1608
+ * / custom embeddings); otherwise launches `agentCmd` through the SHARED
1609
+ * {@link launchWithOptionalWatch} helper (the null adapter by default),
1610
+ * forwarding the model routing intent.
1611
+ *
1612
+ * With `--watch` (pi harness only, validated earlier), the helper launches the
1613
+ * agent NON-BLOCKING (`PiHarness.launchAsync` — `spawn`, not `spawnSync`) so a
1614
+ * `SessionTailer` can READ the growing session `.jsonl` concurrently and surface
1615
+ * the high-signal events live. The tailer is a pure observer: the launch result
1616
+ * is IDENTICAL to the non-watch path, so outcome / gate / git / exit code are
1617
+ * unchanged — only a concurrent log-tail is added.
1618
+ *
1619
+ * The build session-id is the SLUG (in-place `do` has no work-id), which the
1620
+ * helper makes unique per launch; the Gate-2 REVIEW launch uses the SAME helper
1621
+ * with a DISTINCT id (`<slug>-review`) so the two sessions never collide — one
1622
+ * watch implementation, two callers (task `watch-review-session`).
1623
+ */
1624
+ async function runDoAgent(
1625
+ options: DoAgentLaunchOptions,
1626
+ cwd: string,
1627
+ prompt: string,
1628
+ slug: string,
1629
+ ): Promise<{ok: boolean; detail?: string; output?: string}> {
1630
+ if (options.dorfl) {
1631
+ return options.dorfl({cwd, prompt, slug, env: options.env});
1632
+ }
1633
+ const harness = options.harness ?? new NullHarness();
1634
+ const launched = await launchWithOptionalWatch({
1635
+ harness,
1636
+ dir: cwd,
1637
+ slug,
1638
+ command: options.agentCmd ?? '',
1639
+ prompt,
1640
+ model: options.model,
1641
+ // In-place `do` has NO work-id, so the build session id is the slug.
1642
+ sessionId: slug,
1643
+ sessionsDir: options.sessionsDir,
1644
+ watch: options.watch,
1645
+ watchSink: options.watchSink,
1646
+ color: options.color,
1647
+ env: options.env,
1648
+ });
1649
+ // Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
1650
+ // for the propose-mode PR body — instead of dropping it. Absent (no parseable
1651
+ // assistant text) ⇒ undefined ⇒ the body degrades to `--fill` (no regression).
1652
+ return {ok: launched.ok, detail: launched.detail, output: launched.output};
1653
+ }
1654
+
1655
+ /**
1656
+ * True when the working tree has uncommitted (unstaged OR staged) changes — the
1657
+ * dirty-tree refusal predicate (ar-run.sh: `git diff --quiet` AND
1658
+ * `git diff --cached --quiet`). Read-only.
1659
+ */
1660
+ async function isDirtyTree(
1661
+ cwd: string,
1662
+ env: NodeJS.ProcessEnv | undefined,
1663
+ ): Promise<boolean> {
1664
+ const unstaged = await runAsync('git', ['diff', '--quiet'], cwd, {env});
1665
+ if (unstaged.status !== 0) {
1666
+ return true;
1667
+ }
1668
+ const staged = await runAsync('git', ['diff', '--cached', '--quiet'], cwd, {
1669
+ env,
1670
+ });
1671
+ return staged.status !== 0;
1672
+ }
1673
+
1674
+ /**
1675
+ * `dorfl do --remote <r> <arg>` — the per-repo `do` WORKER run against a
1676
+ * REGISTERED repo with NO checkout (`docs/adr/command-surface-and-journeys.md`
1677
+ * §3). Where the in-place {@link performDo} uses the CURRENT checkout AS its
1678
+ * isolation, this form materialises a **hub mirror + job worktree in the AGENTS'
1679
+ * area** (`workspacesDir`, the SAME isolation `run` uses — NEVER the human area),
1680
+ * runs the existing `do` pipeline against that worktree, then tears it down per
1681
+ * the §4 provably-safe deletion predicate.
1682
+ *
1683
+ * **Option A — materialise-then-reuse** (the drift correction; the full
1684
+ * IsolatedTree-seam unification is the SEPARATE `do-run-share-isolation-seam`
1685
+ * task). `performDo` composes the human verbs against a literal `cwd`; this
1686
+ * function does the same — it just points that `cwd` at a freshly-cut job
1687
+ * worktree instead of a checkout. It reuses (does NOT reimplement) the pipeline:
1688
+ * `performStart` (resume) → agent → `performComplete`.
1689
+ *
1690
+ * **The claim ↔ worktree ↔ start composition.** Both `createJob` (cuts the
1691
+ * `work/<slug>` branch off the mirror's fresh main) and `performStart` (claims +
1692
+ * switches to `work/<slug>`) overlap. To compose without double-claiming or
1693
+ * fighting over the branch we order them as the task mandates:
1694
+ *
1695
+ * 1. **CLAIM FIRST** — the CAS push to the arbiter, run in a throwaway clone of
1696
+ * the mirror (the CAS needs a non-bare checkout with an `origin/main`
1697
+ * tracking ref + a worktree to commit in, which a bare mirror does not
1698
+ * provide — exactly `work-on`'s remote-form claim context).
1699
+ * 2. **MATERIALISE the worktree** off the POST-CLAIM fresh main via the EXISTING
1700
+ * job-worktree machinery (`jobWorktreeStrategy`/`createJob`): `createJob`
1701
+ * re-`ensureMirror`s (fetching the claim move) then cuts `work/<slug>` off
1702
+ * the freshly-fetched mirror main.
1703
+ * 3. **RUN start/agent/complete** against the worktree dir as `cwd`. `start` is
1704
+ * driven with `resume: true` so it PLAIN-SWITCHES the branch `createJob`
1705
+ * already created (the item is in-progress on the arbiter after the claim;
1706
+ * `performStart`'s resume path switches without re-claiming) — no
1707
+ * double-claim, no branch fight. The worktree's arbiter remote is `origin`
1708
+ * (the bare mirror's clone remote), so both `start` and `complete` use
1709
+ * `origin`.
1710
+ *
1711
+ * **Teardown** re-applies the §4 predicate via the strategy handle's `teardown`
1712
+ * (`reapJob`): reap the worktree iff clean AND on the arbiter, retain otherwise
1713
+ * (the never-lose-work signal). NEVER `--force`.
1714
+ *
1715
+ * **Recovery contract.** The worktree is disposable; the durable artifact is the
1716
+ * `work/<slug>` BRANCH (pushed by the autonomous needs-attention surfacing on a
1717
+ * stuck/failed run). A human recovers via the human face (`requeue` + re-claim,
1718
+ * or `work-on`), NEVER by editing the agents'-area worktree.
1719
+ */
1720
+ /**
1721
+ * Resolve the URL of a CHECKOUT's arbiter remote — the primitive `do --isolated`
1722
+ * needs to point the job-worktree pipeline ({@link performDoRemote}) at MY OWN
1723
+ * arbiter (`git -C <cwd> remote get-url <arbiter>`). Returns the URL, or
1724
+ * `undefined` when the cwd is not a git repo or has no such remote (the
1725
+ * "isolated against what?" case the CLI turns into a clear error naming
1726
+ * `--remote <url>`). Does NOT fork the isolation/integrate path — it only feeds
1727
+ * the EXISTING `performDoRemote` its `remote` URL.
1728
+ */
1729
+ export function resolveArbiterUrlFromCheckout(
1730
+ cwd: string,
1731
+ arbiter: string,
1732
+ env?: NodeJS.ProcessEnv,
1733
+ ): string | undefined {
1734
+ const res = run('git', ['remote', 'get-url', arbiter], cwd, {env});
1735
+ if (res.status !== 0) {
1736
+ return undefined;
1737
+ }
1738
+ const url = res.stdout.trim();
1739
+ return url === '' ? undefined : url;
1740
+ }
1741
+
1742
+ export async function performDoRemote(
1743
+ options: DoRemoteOptions,
1744
+ ): Promise<DoResult> {
1745
+ const note = options.note ?? (() => {});
1746
+ // The runner's GIT/provider env, scoped to the configured identity (claim,
1747
+ // push, integrate, `gh`). The AGENT launch stays ambient via `options.env`
1748
+ // (`runDoAgent` reads it directly) — the agent must not commit as the bot.
1749
+ // Absent identity ⇒ `options.env` unchanged (byte-for-byte ambient).
1750
+ const workspacesDir = options.workspacesDir;
1751
+
1752
+ // Resolve the identity env AND run the push-time transport-coherence guard:
1753
+ // refuse a forbidden transport for THIS remote's URL (the registered remote),
1754
+ // and fail cleanly on an unresolvable identity (e.g. `tokenEnv` unset) — never
1755
+ // a crash or a silent ambient fallback. Both are no-ops without an identity.
1756
+ let env: NodeJS.ProcessEnv;
1757
+ try {
1758
+ env = identityEnv(options.identity, options.env ?? process.env);
1759
+ assertTransportAllowed(options.identity, options.remote);
1760
+ } catch (err) {
1761
+ return {
1762
+ exitCode: 1,
1763
+ outcome: 'usage-error',
1764
+ message: err instanceof Error ? err.message : String(err),
1765
+ };
1766
+ }
1767
+
1768
+ // 0. `--watch` REQUIRES the pi harness (same guard as in-place `do`): only the
1769
+ // pi adapter writes a session `.jsonl` to tail. Error CLEARLY here, BEFORE
1770
+ // any mirror/claim/worktree side-effect. The injected `dorfl` (tests)
1771
+ // is its own launch path and is exempt.
1772
+ if (
1773
+ options.watch === true &&
1774
+ options.dorfl === undefined &&
1775
+ !(options.harness instanceof PiHarness)
1776
+ ) {
1777
+ return {
1778
+ exitCode: 1,
1779
+ outcome: 'usage-error',
1780
+ message:
1781
+ '`do --watch` requires the pi harness; configure `harness: pi` or drop ' +
1782
+ '`--watch`.',
1783
+ };
1784
+ }
1785
+
1786
+ // 1. Resolve / auto-create the hub mirror for `<r>` (the `registry-remote` /
1787
+ // `work-on`-remote precedent: an unregistered remote is auto-mirrored
1788
+ // before use). `ensureMirror` creates it (`git clone --bare`) when absent
1789
+ // or fetches it when present, under `workspacesDir/repos/` — the agents'
1790
+ // area, NEVER the human area.
1791
+ let mirror;
1792
+ try {
1793
+ mirror = ensureMirror({url: options.remote, workspacesDir, env});
1794
+ } catch (err) {
1795
+ const message = err instanceof Error ? err.message : String(err);
1796
+ return {exitCode: 1, outcome: 'usage-error', message};
1797
+ }
1798
+ note(
1799
+ mirror.created
1800
+ ? `Auto-registered hub mirror for ${mirror.url} at ${mirror.path}.`
1801
+ : `Using hub mirror for ${mirror.url} at ${mirror.path}.`,
1802
+ );
1803
+
1804
+ // 1b. PR-INTENT pre-flight guard — the AUTONOMOUS mirror of in-place `performDo`
1805
+ // step 3c. The SAME predicate, run UP FRONT here (mirror resolved, BEFORE the
1806
+ // claim clone / worktree) so a `propose` run on a GitHub arbiter that INTENDS a
1807
+ // PR (`noPR` unset) fails fast when `gh` genuinely cannot open one, instead of
1808
+ // letting integration silently degrade to manual-PR instructions. NO claim/
1809
+ // build side-effect: it precedes even the throwaway claim CLONE below. The probe
1810
+ // runs in the bare mirror dir (`mirror.path`), which carries the arbiter remote
1811
+ // as `origin`, NOT a bare cwd. CRITICAL: the PROBE is the signal, NOT a config
1812
+ // check — an absent `providers.github` identity falls back to AMBIENT `gh` auth
1813
+ // (the probe reports it available), so a working ambient setup still PROCEEDS.
1814
+ // `arbiterIsGitHub` is derived from `mirror.url` (the resolved arbiter URL).
1815
+ // REUSES the predicate + message so the in-place + autonomous paths cannot
1816
+ // drift.
1817
+ {
1818
+ const probe =
1819
+ options.ghCanOpenPr ??
1820
+ ((probeCwd, probeEnv) =>
1821
+ new GitHubProvider().available(probeCwd, probeEnv));
1822
+ if (
1823
+ shouldFailProposePrIntent({
1824
+ mode: options.integration ?? 'propose',
1825
+ arbiterIsGitHub: isGitHubArbiterUrl(mirror.url),
1826
+ noPR: options.noPR,
1827
+ ghCanOpenPr: () => probe(mirror.path, env),
1828
+ })
1829
+ ) {
1830
+ return {
1831
+ exitCode: 1,
1832
+ outcome: 'refused',
1833
+ message: PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
1834
+ };
1835
+ }
1836
+ }
1837
+
1838
+ // 1c. STATIC fresh-worktree-gate readiness guard — the AUTONOMOUS mirror of
1839
+ // `performDo` step 3d. When the fresh-worktree gate is ON AND `prepare`
1840
+ // resolves to no commands AND a lockfile is present IN THE MIRROR, the
1841
+ // throwaway worktree the gate runs in will have no installed deps, so the
1842
+ // gate cannot run. Probe the bare mirror's main tree (`git ls-tree main`)
1843
+ // so the guard fires BEFORE the throwaway claim clone is even cut. A repo
1844
+ // with NO lockfile is the intentional dep-free case and proceeds. Deps-only
1845
+ // (verify-unset is impossible — `resolveVerifyCommands` defaults the gate).
1846
+ {
1847
+ const guard = checkGatePreconditions({
1848
+ freshWorktreeGate: options.freshWorktreeGate,
1849
+ prepare: options.prepare,
1850
+ lockfile: detectLockfileOnMirrorMain(mirror.path, env),
1851
+ });
1852
+ if (guard !== undefined) {
1853
+ return {exitCode: 1, outcome: 'refused', message: guard.message};
1854
+ }
1855
+ }
1856
+
1857
+ // 2. A throwaway claim clone of the mirror (the CAS context). Slug resolution
1858
+ // + the claim both run here against `origin` (the arbiter URL). Keyed PER
1859
+ // ARG (not a single fixed `__remote` path per mirror) so two CONCURRENT
1860
+ // `do --remote` calls on the SAME mirror — the registry-set advance batch's
1861
+ // per-mirror concurrency (`perRepoMax > 1`) — get DISTINCT claim clones rather
1862
+ // than racing one shared dir (the SAME per-job-clone keying `run`'s
1863
+ // `claimAgainstRepo` uses; the prior single-shot caller never raced, so this
1864
+ // is a pure concurrency hardening, not a behaviour change for it).
1865
+ const claimKey = options.arg.replace(/[^a-zA-Z0-9._-]/g, '_');
1866
+ const claimDir = join(
1867
+ workspacesDir,
1868
+ 'claim',
1869
+ `${encodeRepoKey(mirror.url).split('/').join('__')}__${claimKey}`,
1870
+ );
1871
+ rmSync(claimDir, {recursive: true, force: true});
1872
+ mkdirSync(dirname(claimDir), {recursive: true});
1873
+ git(['clone', '--quiet', mirror.url, claimDir], dirname(claimDir), {env});
1874
+
1875
+ try {
1876
+ // 2a. Resolve the slug across BOTH namespaces against the claim clone (it
1877
+ // carries `work/` from the mirror's main). A collision / resolution
1878
+ // failure is a loud usage error; a `prd:` arg reaches the not-yet-wired
1879
+ // stub — identical behaviour to in-place `do`.
1880
+ let resolved;
1881
+ try {
1882
+ resolved = resolveSlug({
1883
+ arg: options.arg,
1884
+ repoPath: claimDir,
1885
+ read: options.read ?? ledgerRead,
1886
+ });
1887
+ } catch (err) {
1888
+ if (err instanceof SlugResolutionError) {
1889
+ return {exitCode: 1, outcome: 'usage-error', message: err.message};
1890
+ }
1891
+ const message = err instanceof Error ? err.message : String(err);
1892
+ return {exitCode: 1, outcome: 'usage-error', message};
1893
+ }
1894
+
1895
+ // `do --remote spec:<slug>` routes to tasking (the `spec` namespace).
1896
+ if (resolved.namespace === 'spec') {
1897
+ // `do --remote spec:<slug>`: task the spec as the AGENT, against the claim
1898
+ // clone (its `origin` IS the arbiter URL + it carries a working tree from the
1899
+ // mirror's main). No job worktree is needed — the tasking transition is a
1900
+ // runner-owned `spec → tasking → spec` move + emit-backlog on the arbiter, not
1901
+ // a build pipeline. The agent only writes task files; the runner does all git.
1902
+ const tasked = await performTask({
1903
+ slug: resolved.slug,
1904
+ cwd: claimDir,
1905
+ arbiter: 'origin',
1906
+ doer: 'agent',
1907
+ autoTask: options.autoTask,
1908
+ // EXPLICIT dispatch (same as the in-place path above): the `prd:<slug>` was
1909
+ // NAMED (typed, or pool-filtered on `autoTask` before reaching here), so the
1910
+ // tasking gate drops the policy term — only the prd's own readiness +
1911
+ // `taskedAfter` bind, mirroring the build path vs `autoBuild`.
1912
+ explicit: true,
1913
+ dorfl: options.dorfl,
1914
+ harness: options.harness,
1915
+ agentCmd: options.agentCmd,
1916
+ model: options.model,
1917
+ sessionsDir: options.sessionsDir,
1918
+ // The integrate-time args (task `slice-output-through-integration`): the
1919
+ // `provider` is the SAME the task-build path threads (arg parity), but the
1920
+ // MODE is the per-TRANSITION TASKING resolution
1921
+ // (`per-transition-integration-mode-slicing-vs-build`):
1922
+ // `taskingIntegration ?? integration`, so the `--remote prd:` output ALSO
1923
+ // routes through the shared core with the tasking-resolved mode.
1924
+ integration: options.taskingIntegration ?? options.integration,
1925
+ // The per-repo TASK-PLACEMENT default + the operator's explicit
1926
+ // override (task `runner-deterministic-slice-placement-policy-and-
1927
+ // precedence`). Same threading as the in-place `do prd:` path.
1928
+ tasksLandIn: options.tasksLandIn,
1929
+ explicitTasksLandIn: options.explicitTasksLandIn,
1930
+ noPR: options.noPR,
1931
+ providerInstance: options.providerInstance,
1932
+ // The tasker review→edit→converge loop on the `do --remote prd:` path too.
1933
+ reviewLoop: options.reviewLoop,
1934
+ taskerLoopMax: options.taskerLoopMax,
1935
+ reviewExecutions: options.reviewExecutions,
1936
+ taskerLoopModel: options.taskerLoopModel,
1937
+ // The task-SET ACCEPTANCE GATE on the `do --remote prd:` path too.
1938
+ review: options.review,
1939
+ reviewGate: options.taskReviewGate,
1940
+ acceptanceReviewModel: options.reviewModel,
1941
+ env,
1942
+ // The tasker + review AGENTS launch AMBIENT, never the identity env.
1943
+ agentEnv: options.env,
1944
+ note,
1945
+ });
1946
+ return taskResultToDoResult(tasked);
1947
+ }
1948
+ const slug = resolved.slug;
1949
+
1950
+ // 3. CLAIM FIRST (the CAS push to the arbiter), in the throwaway clone.
1951
+ // `origin` there IS the arbiter URL. A lost/contended/usage claim is
1952
+ // propagated verbatim — NO worktree is materialised (clean failure, like
1953
+ // `run`'s `runOneItem`).
1954
+ const claim = await performClaim({
1955
+ slug,
1956
+ cwd: claimDir,
1957
+ arbiter: 'origin',
1958
+ // `--allow-backlog`: accept a `tasks/backlog/`-resident body (staging). The
1959
+ // claim stays a pure lock. Default off ⇒ pool-only.
1960
+ allowBacklog: options.allowBacklog === true,
1961
+ env,
1962
+ note,
1963
+ });
1964
+ if (claim.outcome === 'lost') {
1965
+ return {exitCode: 2, outcome: 'lost', slug, message: claim.message};
1966
+ }
1967
+ if (claim.outcome === 'contended') {
1968
+ return {exitCode: 3, outcome: 'contended', slug, message: claim.message};
1969
+ }
1970
+ if (claim.exitCode !== 0) {
1971
+ return {
1972
+ exitCode: 1,
1973
+ outcome: 'usage-error',
1974
+ slug,
1975
+ message: claim.message,
1976
+ };
1977
+ }
1978
+
1979
+ // 4. MATERIALISE the job worktree off the POST-CLAIM fresh main via the
1980
+ // EXISTING job-worktree machinery (the SAME path `run` uses). `createJob`
1981
+ // re-`ensureMirror`s (fetching the claim move) then cuts `work/<slug>`
1982
+ // off the freshly-fetched mirror main, in the agents' area.
1983
+ const strategy = jobWorktreeStrategy({
1984
+ fromRepo: claimDir,
1985
+ arbiter: 'origin',
1986
+ workspacesDir,
1987
+ });
1988
+ let tree: IsolatedTree | undefined;
1989
+ let result: DoResult | undefined;
1990
+ try {
1991
+ try {
1992
+ tree = strategy.prepare({slug, type: 'task', env});
1993
+ } catch (err) {
1994
+ // `prepare()`/`createJob` THREW before returning the handle (e.g. an
1995
+ // onboard reconcile/stale-lease push surfaced as a throw). `tree` is
1996
+ // undefined, so the normal teardown below would be skipped and a
1997
+ // partially-created worktree could LEAK with no teardown attempt at all —
1998
+ // and its checked-out `work/<slug>` branch would then poison the next
1999
+ // build's fetch. Best-effort reap the deterministic worktree path for this
2000
+ // slug (it is reaped ONLY if its branch is reachable on the arbiter —
2001
+ // never lose work), then re-throw so the failure is still reported.
2002
+ reapPreparedWorktreeLeak(mirror.url, slug, workspacesDir, env, note);
2003
+ throw err;
2004
+ }
2005
+ result = await runRemotePipeline(options, tree, slug, note, env);
2006
+ return result;
2007
+ } finally {
2008
+ // 7. Teardown via the strategy handle. On a CLEAN completion: reap iff clean
2009
+ // AND on the arbiter (the standard §4 predicate). On a FAILURE return
2010
+ // (needs-attention / config-error / refused etc. — the seam already
2011
+ // surfaced the item + pushed the branch): reap on REACHABILITY ALONE, so a
2012
+ // churn-dirty-but-arbiter-safe worktree does not linger to poison the next
2013
+ // build's config-read/materialisation fetch. A worktree whose work is NOT
2014
+ // yet on the arbiter is RETAINED either way (never lose work). NEVER
2015
+ // --force.
2016
+ if (tree) {
2017
+ const reachableOnly =
2018
+ result !== undefined && result.outcome !== 'completed';
2019
+ tree.teardown({reachableOnly});
2020
+ }
2021
+ }
2022
+ } finally {
2023
+ // Remove the throwaway claim clone either way.
2024
+ rmSync(claimDir, {recursive: true, force: true});
2025
+ }
2026
+ }
2027
+
2028
+ /**
2029
+ * Best-effort reap of a worktree that {@link IsolationStrategy.prepare}/`createJob`
2030
+ * may have created at the deterministic per-job path BEFORE it threw (so the
2031
+ * handle was never returned and the normal teardown is skipped). Reaps it ONLY
2032
+ * if its branch is provably reachable on the arbiter — a `prepare` throw can
2033
+ * happen AFTER a clean continue-push (work is safe ⇒ reap so it can't poison the
2034
+ * next build) or BEFORE any push (work not safe ⇒ retain; never lose work). All
2035
+ * git ops are swallowed: this runs on an already-failing path and must never mask
2036
+ * the original throw.
2037
+ */
2038
+ function reapPreparedWorktreeLeak(
2039
+ mirrorUrl: string,
2040
+ slug: string,
2041
+ workspacesDir: string,
2042
+ env: NodeJS.ProcessEnv | undefined,
2043
+ note: (m: string) => void,
2044
+ ): void {
2045
+ try {
2046
+ const dir = jobWorktreePath(workspacesDir, mirrorUrl, slug);
2047
+ if (!existsSync(dir)) {
2048
+ return;
2049
+ }
2050
+ const mirrorDir = mirrorPath(workspacesDir, mirrorUrl);
2051
+ const result = reapJob({
2052
+ dir,
2053
+ branch: workBranchRef('task', slug),
2054
+ mirrorPath: mirrorDir,
2055
+ // Same failure-path stance as the normal teardown: reachable-on-arbiter is
2056
+ // enough (don't let incidental churn retain a worktree whose branch is
2057
+ // already safe). Reachability still gates — unsaved work is retained.
2058
+ reachableOnly: true,
2059
+ env,
2060
+ });
2061
+ if (result.removed) {
2062
+ note(
2063
+ `Reaped leaked worktree for ${slug} after prepare() threw ` +
2064
+ `(branch safe on the arbiter).`,
2065
+ );
2066
+ }
2067
+ } catch {
2068
+ // best-effort — never mask the original prepare() throw
2069
+ }
2070
+ }
2071
+
2072
+ /**
2073
+ * Run the existing `do` pipeline (start[resume] → agent → complete) against an
2074
+ * already-materialised job worktree. Mirrors {@link performDo}'s middle/back —
2075
+ * but `cwd` is the worktree and the arbiter remote inside it is `origin`. The
2076
+ * needs-attention surfacing is the AUTONOMOUS, arbiter-passed variant (like
2077
+ * `run`/in-place `do`): a stuck remote `do` must be cross-machine visible.
2078
+ */
2079
+ async function runRemotePipeline(
2080
+ options: DoRemoteOptions,
2081
+ tree: IsolatedTree,
2082
+ slug: string,
2083
+ note: (m: string) => void,
2084
+ env: NodeJS.ProcessEnv | undefined,
2085
+ ): Promise<DoResult> {
2086
+ const cwd = tree.dir;
2087
+ const arbiterRemote = tree.arbiterRemote; // `origin` (the bare mirror's clone).
2088
+
2089
+ // 4a. CONTINUE rebase conflict (ADR §14 + §10): a requeue kept a `work/<slug>`
2090
+ // that did not replay onto the current main at onboard-time (aborted, never
2091
+ // auto-resolved). Route to needs-attention via the seam (surfaced on the
2092
+ // arbiter + the kept branch already on the arbiter) instead of running the
2093
+ // agent — the §10 path `run` uses.
2094
+ if (tree.continueRebaseConflict) {
2095
+ const reason =
2096
+ `continuing the kept ${tree.branch}: rebase onto the latest main ` +
2097
+ 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
2098
+ 'main, or `requeue --reset` to discard and start fresh';
2099
+ await ledgerWrite.applyNeedsAttentionTransition({
2100
+ cwd,
2101
+ slug,
2102
+ reason,
2103
+ arbiter: arbiterRemote,
2104
+ env,
2105
+ note,
2106
+ });
2107
+ return {
2108
+ exitCode: 1,
2109
+ outcome: 'needs-attention',
2110
+ slug,
2111
+ branch: tree.branch,
2112
+ message: reason,
2113
+ };
2114
+ }
2115
+
2116
+ // 4b. CONTINUE reconcile-push TERMINAL failure (the stale-lease-strand bug this
2117
+ // fix kills): the onboard reconcile push of the kept (already-committed)
2118
+ // work branch to the arbiter FAILED terminally (the stale-lease retry cap
2119
+ // exhausted, or a non-stale-lease rejection / unreachable arbiter). The push
2120
+ // helper THROWS; `createJob` CATCHES it and flags `continuePushFailure`
2121
+ // rather than letting the throw escape (which crashed the run and left the
2122
+ // task silently in `work/in-progress/` on the arbiter, the work stranded in
2123
+ // the worktree). Route to needs-attention via the SAME seam the conflict path
2124
+ // uses — surfaced on the arbiter, the kept branch already on the arbiter from
2125
+ // the prior requeue (recoverable) — instead of running the agent.
2126
+ if (tree.continuePushFailure !== undefined) {
2127
+ const reason =
2128
+ `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
2129
+ `to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
2130
+ 'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
2131
+ 'once the churn settles, or `requeue --reset` to discard and start fresh';
2132
+ await ledgerWrite.applyNeedsAttentionTransition({
2133
+ cwd,
2134
+ slug,
2135
+ reason,
2136
+ arbiter: arbiterRemote,
2137
+ env,
2138
+ note,
2139
+ });
2140
+ return {
2141
+ exitCode: 1,
2142
+ outcome: 'needs-attention',
2143
+ slug,
2144
+ branch: tree.branch,
2145
+ message: reason,
2146
+ };
2147
+ }
2148
+
2149
+ // 5. HARDEN start to work against a job-worktree `cwd` (the load-bearing work).
2150
+ // `performStart` reads/switches via the `<arbiter>/main` REMOTE-TRACKING ref
2151
+ // (`origin/main` here). A job worktree is cut from a BARE hub mirror whose
2152
+ // `origin` remote has NO fetch refspec, so `origin/main` /
2153
+ // `origin/work/<slug>` would not otherwise resolve — and `start` would
2154
+ // wrongly read the slug as "absent". Prime those two remote-tracking refs
2155
+ // EXPLICITLY (the SAME technique `integrator.rebaseOntoArbiterMain` /
2156
+ // `gc.fetchTracking` use against a bare-mirror worktree). After this the
2157
+ // existing `performStart` runs UNCHANGED against the worktree.
2158
+ await primeWorktreeTrackingRef(cwd, arbiterRemote, 'main', env);
2159
+ await primeWorktreeTrackingRef(
2160
+ cwd,
2161
+ arbiterRemote,
2162
+ workBranchRef('task', slug),
2163
+ env,
2164
+ );
2165
+
2166
+ // Onboard onto the work branch like in-place `do` — but the item is ALREADY
2167
+ // CLAIMED (step 3), so `performStart` runs with `resume: true`: it
2168
+ // PLAIN-SWITCHES the `work/<slug>` branch `createJob` already created WITHOUT
2169
+ // re-claiming (no double-claim, no branch fight). This proves `start` works
2170
+ // against a job-worktree `cwd`.
2171
+ const started = await performStart({
2172
+ slug,
2173
+ cwd,
2174
+ arbiter: arbiterRemote,
2175
+ resume: true,
2176
+ // `--allow-backlog`: a staged body (claimed, still in `tasks/backlog/`) is a
2177
+ // valid onboard residence; without this `start` would read it as `absent`.
2178
+ allowBacklog: options.allowBacklog === true,
2179
+ env,
2180
+ note,
2181
+ });
2182
+ if (started.outcome === 'needs-attention') {
2183
+ return {
2184
+ exitCode: 1,
2185
+ outcome: 'needs-attention',
2186
+ slug,
2187
+ branch: started.branch,
2188
+ message: started.message,
2189
+ };
2190
+ }
2191
+ // FORWARD the honest un-moved signal end-to-end: a continue-site surface that
2192
+ // did NOT land on the arbiter (start's `surface-unmoved`) must NOT degrade to
2193
+ // `usage-error` here — the item is still in-progress on the arbiter (retry/resolve).
2194
+ if (started.outcome === 'surface-unmoved') {
2195
+ return {
2196
+ exitCode: 1,
2197
+ outcome: 'surface-unmoved',
2198
+ slug,
2199
+ branch: started.branch,
2200
+ message: started.message,
2201
+ };
2202
+ }
2203
+ if (started.exitCode !== 0) {
2204
+ const outcome: DoOutcome =
2205
+ started.outcome === 'refused' ? 'refused' : 'usage-error';
2206
+ return {exitCode: 1, outcome, slug, message: started.message};
2207
+ }
2208
+ const branch = started.branch;
2209
+
2210
+ // 6. Build the prompt + run the agent autonomously in the worktree (the SAME
2211
+ // assembly in-place `do` uses). The agent only edits code.
2212
+ let prompt: string;
2213
+ try {
2214
+ // CONTINUE-aware resolution (job-worktree path): admit `work/done/` ONLY
2215
+ // behind the tip-vs-arbiter stranded gate (story 5), using the worktree's
2216
+ // primed `<origin>/work/<slug>` vs `<origin>/main` tracking refs.
2217
+ const task = resolveTask(
2218
+ cwd,
2219
+ slug,
2220
+ {
2221
+ cwd,
2222
+ branchRef: `${arbiterRemote}/${branch}`,
2223
+ mainRef: `${arbiterRemote}/main`,
2224
+ env,
2225
+ },
2226
+ // `--allow-backlog`: also resolve a `tasks/backlog/`-resident body.
2227
+ {allowBacklog: options.allowBacklog === true},
2228
+ );
2229
+ // CONTINUE-mode (job-worktree path): the worktree's tracking refs were primed
2230
+ // above (`primeWorktreeTrackingRef`), so reuse the SAME continue-detection with
2231
+ // the worktree's `<origin>/work/<slug>` vs `<origin>/main` refs.
2232
+ const continueContext = resolveContinueContext({
2233
+ cwd,
2234
+ slug,
2235
+ arbiter: arbiterRemote,
2236
+ branchRef: `${arbiterRemote}/${branch}`,
2237
+ mainRef: `${arbiterRemote}/main`,
2238
+ content: readFileSync(task.path, 'utf8'),
2239
+ env,
2240
+ });
2241
+ // Per-item override layer (mirrors in-place `do`): the task/prd frontmatter
2242
+ // may override the resolved repo `promptGuidance.testFirst` for THIS item.
2243
+ const itemGuidance = resolvePromptGuidanceForItem({
2244
+ cwd,
2245
+ repoResolved: {testFirst: options.promptGuidance?.testFirst === true},
2246
+ taskContent: readFileSync(task.path, 'utf8'),
2247
+ });
2248
+ prompt = buildAgentPrompt(task.slug, task.prd, task.taskPrompt, {
2249
+ cwd,
2250
+ continueContext,
2251
+ promptGuidance: itemGuidance,
2252
+ });
2253
+ } catch (err) {
2254
+ if (err instanceof PromptError) {
2255
+ return await saveRemoteAgentFailure({
2256
+ slug,
2257
+ branch,
2258
+ cwd,
2259
+ arbiterRemote,
2260
+ detail: err.message,
2261
+ env,
2262
+ note,
2263
+ });
2264
+ }
2265
+ throw err;
2266
+ }
2267
+
2268
+ let agent: {ok: boolean; detail?: string; output?: string};
2269
+ try {
2270
+ agent = await runDoAgent(options, cwd, prompt, slug);
2271
+ } catch (err) {
2272
+ const message = err instanceof Error ? err.message : String(err);
2273
+ return await saveRemoteAgentFailure({
2274
+ slug,
2275
+ branch,
2276
+ cwd,
2277
+ arbiterRemote,
2278
+ detail: message,
2279
+ env,
2280
+ note,
2281
+ });
2282
+ }
2283
+ if (!agent.ok) {
2284
+ const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
2285
+ return await saveRemoteAgentFailure({
2286
+ slug,
2287
+ branch,
2288
+ cwd,
2289
+ arbiterRemote,
2290
+ detail,
2291
+ env,
2292
+ note,
2293
+ });
2294
+ }
2295
+
2296
+ // 6b. HONOR a deliberate STOP (task `agent-stop-signal`) — the SAME detection
2297
+ // in-place `do` runs, shared via `resolveStopReason`/`saveAgentStop`. A
2298
+ // sentinel STOP (verbatim reason) or an empty work-branch diff routes to
2299
+ // needs-attention (surfaced on the arbiter) and SKIPS the gate + Gate-2. The
2300
+ // diff base is the worktree's `origin` (arbiterRemote).
2301
+ const stopReason = await resolveStopReason({
2302
+ output: agent.output,
2303
+ slug,
2304
+ cwd,
2305
+ arbiter: arbiterRemote,
2306
+ env,
2307
+ });
2308
+ if (stopReason !== undefined) {
2309
+ return await saveAgentStop({
2310
+ slug,
2311
+ branch,
2312
+ cwd,
2313
+ arbiter: arbiterRemote,
2314
+ reason: stopReason,
2315
+ env,
2316
+ note,
2317
+ });
2318
+ }
2319
+
2320
+ // 7. Gate + done-move + commit + rebase + integrate LIKE in-place `do`: the
2321
+ // AUTONOMOUS surfacing (`surfaceArbiter: origin`) so a red gate / rebase
2322
+ // conflict / review block surfaces on the arbiter's main (cross-machine
2323
+ // visible). The success path reuses `complete`'s machinery unchanged.
2324
+ const completed = await performComplete({
2325
+ slug,
2326
+ cwd,
2327
+ arbiter: arbiterRemote,
2328
+ integration: options.integration,
2329
+ // An explicit `--merge` overrides the untrusted-origin build-propose rule (task
2330
+ // `untrusted-origin-forces-build-propose`); unset on the autonomous path so
2331
+ // untrusted-origin reliably forces propose.
2332
+ explicitMerge: options.explicitMerge,
2333
+ // SCOPE: the divergence guard is in-place only. A job worktree is cut fresh off
2334
+ // the bare mirror and never ff's the operator's local main, so the guard does
2335
+ // not apply here — opt out explicitly (the task: do NOT touch do --remote/run).
2336
+ ignoreDivergedMain: true,
2337
+ // `--allow-backlog`: a staged build sources from `tasks/backlog/` (gated on
2338
+ // the flag; otherwise a staged strand still refuses). Default off.
2339
+ allowBacklog: options.allowBacklog === true,
2340
+ prepare: options.prepare,
2341
+ verify: options.verify,
2342
+ freshWorktreeGate: options.freshWorktreeGate,
2343
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — see
2344
+ // the in-place site above for the rationale; mirrored here so the no-checkout
2345
+ // `do --remote`/`--isolated` path threads the same resolved cap.
2346
+ mergeRetries: options.mergeRetries,
2347
+ noPR: options.noPR,
2348
+ providerInstance: options.providerInstance,
2349
+ body: agent.output,
2350
+ review: options.review,
2351
+ reviewModel: options.reviewModel,
2352
+ reviewMaxRounds: options.reviewMaxRounds,
2353
+ reviewGate: options.reviewGate,
2354
+ watch: options.watch,
2355
+ watchSink: options.watchSink,
2356
+ sessionsDir: options.sessionsDir,
2357
+ surfaceArbiter: arbiterRemote,
2358
+ color: options.color,
2359
+ note,
2360
+ noteBlock: options.noteBlock,
2361
+ env,
2362
+ // The review AGENT (Gate 2) launches AMBIENT — never the identity env (an
2363
+ // agent must not act as the bot; only the runner's git ops carry identity).
2364
+ agentEnv: options.env,
2365
+ });
2366
+
2367
+ if (
2368
+ completed.outcome === 'completed' ||
2369
+ completed.outcome === 'already-integrated'
2370
+ ) {
2371
+ // Stranded-done auto-recover's clean no-op folds into `completed` here too
2372
+ // (see the in-place performDo handler for the rationale): same SHARED
2373
+ // `complete.ts` seam — so `do --remote` (this `performDoRemote`) inherits
2374
+ // the auto-recover without per-caller duplication.
2375
+ return {
2376
+ exitCode: 0,
2377
+ outcome: 'completed',
2378
+ slug,
2379
+ branch,
2380
+ message: completed.message,
2381
+ };
2382
+ }
2383
+ if (completed.outcome === 'review-unparseable') {
2384
+ // Gate 2 RAN but its verdict was UNPARSEABLE (malformed JSON). The core already
2385
+ // routed it work-preservingly (branch pushed + surfaced). FAILURE-CAUSE axis, NOT
2386
+ // a reviewer block: classify the parse-failure phrase → `transient-infra` (re-run
2387
+ // the SAME work), the SAME convergence in-place `do`/`run` apply, so `do --remote`
2388
+ // agrees on the label. The job worktree is RETAINED by the §4 reap (a
2389
+ // not-provably-safe tree); hand the operator the recover-one-liner like the other
2390
+ // retained-tree failures.
2391
+ note(recoverIsolatedOneLiner(slug));
2392
+ const cause = classifyFailureCause(completed.message);
2393
+ return {
2394
+ exitCode: 1,
2395
+ outcome: failureCauseToDoOutcome(cause),
2396
+ slug,
2397
+ branch,
2398
+ message: completed.message,
2399
+ };
2400
+ }
2401
+ if (
2402
+ completed.outcome === 'prepare-failed' ||
2403
+ completed.outcome === 'gate-failed' ||
2404
+ completed.outcome === 'review-blocked' ||
2405
+ completed.outcome === 'rebase-conflict' ||
2406
+ completed.outcome === 'strand-surfaced'
2407
+ ) {
2408
+ // The job worktree is RETAINED (the §4 reap keeps a not-provably-safe tree).
2409
+ // When the work was committed + done-moved but the integrate failed terminally
2410
+ // (the stale-lease-strand class Part B #97 surfaces), the operator FINISHES the
2411
+ // stranded branch with the recover-already-committed path \u2014 hand them the EXACT
2412
+ // one-liner so they need not reverse-engineer the encoded worktree path.
2413
+ //
2414
+ // `strand-surfaced` is the autonomous-strand parity (the SHARED `complete.ts`
2415
+ // seam already surfaced the source-strand / empty-staged refusal to
2416
+ // needs-attention on the arbiter) — `do --remote` inherits the fix here
2417
+ // without per-caller duplication; mapped to the SAME `needs-attention`
2418
+ // outcome shape the in-place `performDo` uses.
2419
+ note(recoverIsolatedOneLiner(slug));
2420
+ return {
2421
+ exitCode: 1,
2422
+ outcome: 'needs-attention',
2423
+ slug,
2424
+ branch,
2425
+ message: completed.message,
2426
+ };
2427
+ }
2428
+ if (completed.outcome === 'surface-unmoved') {
2429
+ // Strand-surface could not land on the arbiter (CAS contention exhausted) —
2430
+ // HONESTLY still in-progress on the arbiter. Mirror in-place `performDo`'s
2431
+ // `surface-unmoved` mapping so `do --remote` agrees on the same signal.
2432
+ return {
2433
+ exitCode: 1,
2434
+ outcome: 'surface-unmoved',
2435
+ slug,
2436
+ branch,
2437
+ message: completed.message,
2438
+ };
2439
+ }
2440
+ // Reclassify a thrown CORE wiring/config error (swallowed into `usage-error` by
2441
+ // `performComplete`) onto `config-error` — the SAME convergence in-place `do`
2442
+ // applies, so `do --remote` agrees with `do`/`run` on the same error too.
2443
+ if (completed.outcome === 'usage-error') {
2444
+ const cause = classifyFailureCause(completed.message);
2445
+ if (cause === 'config-error') {
2446
+ return {
2447
+ exitCode: 1,
2448
+ outcome: 'config-error',
2449
+ slug,
2450
+ branch,
2451
+ message: completed.message,
2452
+ };
2453
+ }
2454
+ }
2455
+ const outcome: DoOutcome =
2456
+ completed.outcome === 'refused' ? 'refused' : 'usage-error';
2457
+ return {exitCode: 1, outcome, slug, branch, message: completed.message};
2458
+ }
2459
+
2460
+ /**
2461
+ * SAVE the partial work of a FAILED agent in a remote `do` worktree — the same
2462
+ * work-preserving routing in-place `do`'s {@link saveAgentFailure} uses, but
2463
+ * against the worktree's `origin` arbiter remote. The agent's edits + the failure
2464
+ * reason are committed + surfaced on the arbiter's main AND the `work/<slug>`
2465
+ * branch is pushed (the RECOVERABLE durable artifact — the disposable worktree is
2466
+ * NOT the recovery surface). The outcome is the classified failure CAUSE
2467
+ * (`transient-infra` / `config-error` / the generic `agent-failed`), same as the
2468
+ * in-place form.
2469
+ */
2470
+ async function saveRemoteAgentFailure(params: {
2471
+ slug: string;
2472
+ branch: string | undefined;
2473
+ cwd: string;
2474
+ arbiterRemote: string;
2475
+ detail: string;
2476
+ env: NodeJS.ProcessEnv | undefined;
2477
+ note: (message: string) => void;
2478
+ }): Promise<DoResult> {
2479
+ const {slug, cwd, arbiterRemote, detail, env, note} = params;
2480
+ const branch = params.branch ?? workBranchRef('task', slug);
2481
+ // Same best-effort cause classification as in-place `do`'s `saveAgentFailure`
2482
+ // (shared `classifyFailureCause`), so the remote form labels the SAME error the
2483
+ // SAME way too.
2484
+ const cause = classifyFailureCause(detail);
2485
+ const reason = `${failureCauseLabel(cause)}: ${detail}`;
2486
+
2487
+ const routed = await ledgerWrite.applyNeedsAttentionTransition({
2488
+ cwd,
2489
+ slug,
2490
+ reason,
2491
+ arbiter: arbiterRemote,
2492
+ env,
2493
+ note,
2494
+ });
2495
+
2496
+ const report = routed.moved ? routeReport(routed, branch) : undefined;
2497
+ const message = routed.moved
2498
+ ? `Agent run failed building '${slug}' [${cause}] (${detail}); SAVED the ` +
2499
+ `partial work and marked '${slug}' stuck on its lock (${report!.fragment}).`
2500
+ : `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
2501
+ `mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
2502
+ note(message);
2503
+ return {
2504
+ exitCode: 1,
2505
+ outcome: failureCauseToDoOutcome(cause),
2506
+ slug,
2507
+ branch,
2508
+ routedToNeedsAttention: routed.moved,
2509
+ message,
2510
+ };
2511
+ }
2512
+
2513
+ /**
2514
+ * Prime ONE arbiter head into its remote-tracking ref inside a job worktree
2515
+ * (`+refs/heads/<head>:refs/remotes/<arbiter>/<head>`). A job worktree is cut
2516
+ * from a BARE hub mirror whose `origin` remote has no fetch refspec, so
2517
+ * `<arbiter>/main` / `<arbiter>/work/<slug>` would not otherwise resolve — which
2518
+ * makes the (otherwise unchanged) `performStart` read the slug as absent. This is
2519
+ * the EXACT technique `integrator.rebaseOntoArbiterMain` + `gc.fetchTracking`
2520
+ * already use for the same bare-mirror-worktree reason. Best-effort: an
2521
+ * unreachable arbiter / a missing head leaves the local ref absent, which `start`
2522
+ * then handles (the FRESH branch already exists locally from `createJob`).
2523
+ */
2524
+ async function primeWorktreeTrackingRef(
2525
+ cwd: string,
2526
+ arbiter: string,
2527
+ head: string,
2528
+ env: NodeJS.ProcessEnv | undefined,
2529
+ ): Promise<void> {
2530
+ // SOFT (no throw): the head may not exist on the arbiter (e.g. a FRESH cut has
2531
+ // no pushed `work/<slug>` yet) — that is the common, expected case, and `start`
2532
+ // handles a missing remote-tracking ref (the local branch `createJob` created
2533
+ // is plain-switched). A genuine fetch error likewise leaves the ref absent →
2534
+ // the safe direction.
2535
+ await runAsync(
2536
+ 'git',
2537
+ [
2538
+ 'fetch',
2539
+ '--quiet',
2540
+ arbiter,
2541
+ `+refs/heads/${head}:refs/remotes/${arbiter}/${head}`,
2542
+ ],
2543
+ cwd,
2544
+ {env},
2545
+ );
2546
+ }
2547
+
2548
+ /**
2549
+ * Adapt the IN-PLACE {@link DoOptions} build/task driver onto the WORKTREE-
2550
+ * ISOLATED {@link performDoRemote} pipeline (task
2551
+ * `advance-loop-driver-registry-set-job-worktrees`) — the per-mirror job-worktree
2552
+ * `doDriver` the registry-set advance driver threads into the advance tick.
2553
+ *
2554
+ * The advance tick's build/task rung ORCHESTRATES `do` by calling the
2555
+ * {@link AdvanceContext.doDriver} seam with a resolved {@link DoOptions} (whose
2556
+ * `cwd` is the per-mirror in-place checkout). The DEFAULT driver is
2557
+ * {@link performDo} (in-place, the cwd checkout IS the isolation — the human-local
2558
+ * one-shot `advance` + today's `run --advance`). The DAEMON/CI registry-set path
2559
+ * wants the SAME per-job-worktree isolation `run`'s build tick gives `runOneItem`,
2560
+ * so it injects THIS driver instead: it re-routes the orchestration onto
2561
+ * {@link performDoRemote}, which materialises a hub mirror + job worktree off the
2562
+ * mirror's arbiter via the EXISTING `jobWorktreeStrategy` (no second isolation
2563
+ * mechanism), runs the SAME `do` pipeline there, and reaps per the §4 predicate.
2564
+ * The cwd checkout is NEVER touched (the equivalence the task asserts vs plain
2565
+ * `run`).
2566
+ *
2567
+ * The pipeline knobs (verify / integration / review family / agent launch /
2568
+ * identity) ride VERBATIM from the threaded `DoOptions` onto the structurally-
2569
+ * matching {@link DoRemoteOptions} fields; `cwd` is DROPPED (the worktree replaces
2570
+ * it) and `remote` + `workspacesDir` are supplied from this driver's closure (the
2571
+ * mirror's arbiter URL + the agents' execution area). A `prd:` arg flows through
2572
+ * unchanged — `performDoRemote` tasks it against the claim clone with NO build
2573
+ * worktree (the tasking/surface/triage/apply rungs are tree-less ledger moves, the
2574
+ * substrate the task's criterion 4 preserves).
2575
+ */
2576
+ export function jobWorktreeDoDriver(closure: {
2577
+ /** The mirror's arbiter URL (`git -C <mirror> remote get-url origin`) — `performDoRemote`'s `remote`. */
2578
+ remote: string;
2579
+ /** The agents' execution area (config `workspacesDir`) where the hub mirror + worktree live. */
2580
+ workspacesDir: string;
2581
+ }): (options: DoOptions) => Promise<DoResult> {
2582
+ return (options: DoOptions): Promise<DoResult> => {
2583
+ // Map the in-place DoOptions onto the worktree DoRemoteOptions. `cwd` is
2584
+ // intentionally DROPPED (the worktree is the isolation, not the checkout);
2585
+ // every other pipeline knob rides verbatim onto its structural twin.
2586
+ const remoteOptions: DoRemoteOptions = {
2587
+ arg: options.arg,
2588
+ remote: closure.remote,
2589
+ workspacesDir: closure.workspacesDir,
2590
+ arbiter: options.arbiter,
2591
+ identity: options.identity,
2592
+ autoTask: options.autoTask,
2593
+ reviewLoop: options.reviewLoop,
2594
+ taskerLoopMax: options.taskerLoopMax,
2595
+ taskerLoopModel: options.taskerLoopModel,
2596
+ reviewExecutions: options.reviewExecutions,
2597
+ integration: options.integration,
2598
+ // The explicit `--merge` override for the untrusted-origin build-propose rule.
2599
+ explicitMerge: options.explicitMerge,
2600
+ prepare: options.prepare,
2601
+ verify: options.verify,
2602
+ noPR: options.noPR,
2603
+ review: options.review,
2604
+ reviewModel: options.reviewModel,
2605
+ reviewMaxRounds: options.reviewMaxRounds,
2606
+ reviewGate: options.reviewGate,
2607
+ taskReviewGate: options.taskReviewGate,
2608
+ dorfl: options.dorfl,
2609
+ harness: options.harness,
2610
+ agentCmd: options.agentCmd,
2611
+ model: options.model,
2612
+ sessionsDir: options.sessionsDir,
2613
+ watch: options.watch,
2614
+ watchSink: options.watchSink,
2615
+ color: options.color,
2616
+ read: options.read,
2617
+ env: options.env,
2618
+ note: options.note,
2619
+ noteBlock: options.noteBlock,
2620
+ };
2621
+ return performDoRemote(remoteOptions);
2622
+ };
2623
+ }