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/run.ts ADDED
@@ -0,0 +1,1430 @@
1
+ import {type Config, resolvePromptGuidance} from './config.js';
2
+ import {resolveRepoConfig} from './repo-config.js';
3
+ import type {ConfigOverrideMap} from './config-override.js';
4
+ import {scan, type ScanReport} from './scan.js';
5
+ import {selectCandidates, type Candidate} from './select.js';
6
+ import {performClaim, type ClaimCasResult} from './claim-cas.js';
7
+ import {updateJobRecord, encodeWorkId} from './workspace.js';
8
+ import {encodeRepoKey} from './repo-mirror.js';
9
+ import {run as runProcess} from './git.js';
10
+ import {mkdirSync, readFileSync, rmSync} from 'node:fs';
11
+ import {ledgerWrite} from './ledger-write.js';
12
+ import type {SurfaceToNeedsAttentionResult} from './needs-attention.js';
13
+ import {
14
+ jobWorktreeStrategy,
15
+ type IsolatedTree,
16
+ type IsolationStrategy,
17
+ } from './isolation.js';
18
+ import {runConcurrent, createKeyedLock} from './concurrency.js';
19
+ import type {Harness} from './harness.js';
20
+ import {createHarness} from './pi-harness.js';
21
+ import {generateSessionPath} from './session-path.js';
22
+ import {
23
+ resolveTask,
24
+ buildAgentPrompt,
25
+ resolveContinueContext,
26
+ resolvePromptGuidanceForItem,
27
+ PromptError,
28
+ } from './prompt.js';
29
+ import {type IntegrateResult, type ReviewProvider} from './integrator.js';
30
+ import {
31
+ parseStopSentinel,
32
+ isWorkBranchDiffEmpty,
33
+ emptyDiffStopReason,
34
+ } from './agent-stop.js';
35
+ import {
36
+ classifyFailureCause,
37
+ failureCauseLabel,
38
+ type FailureCause,
39
+ } from './failure-cause.js';
40
+ import {performIntegration} from './integration-core.js';
41
+ import {
42
+ shouldFailProposePrIntent,
43
+ PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
44
+ } from './do-config.js';
45
+ import {
46
+ checkGatePreconditions,
47
+ detectLockfileOnDisk,
48
+ detectLockfileOnMirrorMain,
49
+ } from './gate-readiness.js';
50
+ import {isGitHubArbiterUrl, GitHubProvider} from './github.js';
51
+ import {identityEnv, assertTransportAllowed} from './identity.js';
52
+ import type {ReviewGate} from './review-gate.js';
53
+ import type {BackoffOptions, Sleep} from './retry-backoff.js';
54
+ import {tmpdir} from 'node:os';
55
+ import {join, dirname} from 'node:path';
56
+
57
+ /** Is `repoPath` a BARE git repository (a registry hub mirror has no work tree)? */
58
+ function isBareRepo(
59
+ repoPath: string,
60
+ env: NodeJS.ProcessEnv | undefined,
61
+ ): boolean {
62
+ const result = runProcess(
63
+ 'git',
64
+ ['rev-parse', '--is-bare-repository'],
65
+ repoPath,
66
+ {env},
67
+ );
68
+ return result.status === 0 && result.stdout.trim() === 'true';
69
+ }
70
+
71
+ /**
72
+ * The outcome of {@link claimAgainstRepo}: the claim result PLUS the path the
73
+ * job-worktree strategy should resolve its mirror/arbiter URL from (`fromRepo`).
74
+ * For an in-place checkout that is the checkout itself; for a bare registry
75
+ * mirror it is the throwaway claim CLONE (whose `origin` IS the arbiter URL).
76
+ */
77
+ interface RepoClaim {
78
+ claim: ClaimCasResult;
79
+ /** Pass as `jobWorktreeStrategy({fromRepo})` (kept alive until AFTER prepare). */
80
+ fromRepo: string;
81
+ /** Arbiter remote NAME valid inside `fromRepo` (in-place: configured; bare: `origin`). */
82
+ strategyArbiter: string;
83
+ /**
84
+ * Remove the throwaway claim clone (no-op for the in-place path). MUST be called
85
+ * only AFTER `strategy.prepare()` — the strategy resolves the mirror URL off
86
+ * `fromRepo`, so the clone must still exist at prepare time.
87
+ */
88
+ cleanup(): void;
89
+ }
90
+
91
+ /**
92
+ * Claim a slug against a repo that may be EITHER a working checkout (the in-place
93
+ * test/`do` path) OR a BARE registry hub mirror (`run`'s production discovery).
94
+ *
95
+ * The claim CAS needs a NON-bare checkout with an `origin/main` tracking ref and
96
+ * a work tree to commit in (a bare mirror cannot `git mv`/`git commit`). So for a
97
+ * bare mirror we clone it into a throwaway claim dir under `workspace` and claim
98
+ * THERE against `origin` (the mirror URL) — the EXACT pattern `do --remote` uses
99
+ * (`do.ts` §2–3). For a non-bare checkout we claim in place against the configured
100
+ * arbiter remote, unchanged. The returned `fromRepo` is what the job-worktree
101
+ * strategy then resolves the mirror off (the claim clone, or the checkout).
102
+ */
103
+ async function claimAgainstRepo(opts: {
104
+ slug: string;
105
+ repoPath: string;
106
+ workspace: string;
107
+ arbiter: string;
108
+ env: NodeJS.ProcessEnv | undefined;
109
+ }): Promise<RepoClaim> {
110
+ const {slug, repoPath, workspace, arbiter, env} = opts;
111
+ if (!isBareRepo(repoPath, env)) {
112
+ // In-place checkout: claim directly against the configured arbiter remote.
113
+ const claim = await performClaim({slug, cwd: repoPath, arbiter, env});
114
+ return {
115
+ claim,
116
+ fromRepo: repoPath,
117
+ strategyArbiter: arbiter,
118
+ cleanup: () => {},
119
+ };
120
+ }
121
+
122
+ // Bare hub mirror (the registry): clone it into a throwaway claim dir and claim
123
+ // there against `origin` (the mirror URL = the arbiter). Keyed per slug so two
124
+ // in-flight jobs in the same mirror get DISTINCT claim clones (worktree
125
+ // isolation — no shared mutable state across in-flight jobs).
126
+ const mirrorUrl = runProcess(
127
+ 'git',
128
+ ['remote', 'get-url', 'origin'],
129
+ repoPath,
130
+ {
131
+ env,
132
+ },
133
+ ).stdout.trim();
134
+ const url = mirrorUrl !== '' ? mirrorUrl : `file://${repoPath}`;
135
+ const claimDir = join(
136
+ workspace,
137
+ 'claim',
138
+ `${encodeRepoKey(url).split('/').join('__')}__${slug}`,
139
+ );
140
+ rmSync(claimDir, {recursive: true, force: true});
141
+ mkdirSync(dirname(claimDir), {recursive: true});
142
+ runProcess('git', ['clone', '--quiet', url, claimDir], dirname(claimDir), {
143
+ env,
144
+ });
145
+ const claim = await performClaim({
146
+ slug,
147
+ cwd: claimDir,
148
+ arbiter: 'origin',
149
+ env,
150
+ });
151
+ return {
152
+ claim,
153
+ fromRepo: claimDir,
154
+ // Inside the clone the arbiter is `origin` (the mirror URL).
155
+ strategyArbiter: 'origin',
156
+ cleanup: () => rmSync(claimDir, {recursive: true, force: true}),
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Resolve the arbiter URL for `repoPath` PRE-CLAIM, the SAME way
162
+ * {@link claimAgainstRepo} derives the claim target. A BARE registry hub mirror's
163
+ * arbiter is its `origin` remote URL (falling back to `file://${repoPath}` when it
164
+ * has none); a working CHECKOUT's arbiter is the configured `arbiter` remote URL
165
+ * (falling back to `file://${repoPath}`). Used by the PR-INTENT pre-flight guard
166
+ * to know whether the arbiter is GitHub BEFORE the claim — it must not claim then
167
+ * refuse (that would strand the item in-progress on main). Read-only; mutates
168
+ * nothing.
169
+ */
170
+ function arbiterUrlForRepo(opts: {
171
+ repoPath: string;
172
+ arbiter: string;
173
+ env: NodeJS.ProcessEnv | undefined;
174
+ }): string {
175
+ const {repoPath, arbiter, env} = opts;
176
+ const remote = isBareRepo(repoPath, env) ? 'origin' : arbiter;
177
+ const res = runProcess('git', ['remote', 'get-url', remote], repoPath, {env});
178
+ const url = res.status === 0 ? res.stdout.trim() : '';
179
+ return url !== '' ? url : `file://${repoPath}`;
180
+ }
181
+
182
+ /** What happened to one selected item across the whole pipeline. */
183
+ export type ItemStatus =
184
+ | 'claimed-done' // tests green + rebased clean → integrated (pushed/merged)
185
+ | 'lost-race' // claim exit 2 — skipped cleanly
186
+ | 'claim-contended' // claim exit 3
187
+ | 'claim-error' // claim exit 1 / unexpected
188
+ | 'tests-failed' // claimed + ran, but gate red → routed to needs-attention
189
+ | 'needs-attention' // rebase conflict at integrate time (ADR §10) — human must look
190
+ | '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
191
+ | 'agent-failed' // the agent ran but produced bad/empty output (the conservative generic), OR the cause is unknown
192
+ | 'transient-infra' // a harness-surfaced model/connection outage (post-retry) or a git/provider outage — RETRY the same work (FAILURE-CAUSE axis)
193
+ | 'config-error' // a thrown CORE wiring/config error (e.g. review on, no reviewGate) — fix the WIRING, not the task (FAILURE-CAUSE axis)
194
+ | 'agent-stopped'; // the agent DELIBERATELY stopped (task drifted) OR produced no change — gate + Gate-2 skipped
195
+
196
+ export interface ItemResult {
197
+ repoPath: string;
198
+ slug: string;
199
+ status: ItemStatus;
200
+ /** Integration outcome, when the item reached done. */
201
+ integration?: IntegrateResult;
202
+ detail?: string;
203
+ }
204
+
205
+ export interface RunOnceResult {
206
+ claimedAndDone: number;
207
+ skipped: number;
208
+ failed: number;
209
+ /** Items routed to needs-attention (rebase conflict, red gate). */
210
+ needsAttention: number;
211
+ items: ItemResult[];
212
+ }
213
+
214
+ /** The agent invocation: edits code in `cwd` to satisfy the prompt. */
215
+ export type Dorfl = (input: {
216
+ cwd: string;
217
+ prompt: string;
218
+ slug: string;
219
+ env?: NodeJS.ProcessEnv;
220
+ }) => {
221
+ ok: boolean;
222
+ detail?: string;
223
+ /**
224
+ * The agent's FINAL SUMMARY (the harness seam's `LaunchResult.output`): the
225
+ * channel the propose-mode PR BODY is built from (mirrors `do`'s
226
+ * `DoDorfl.output`). Optional so a test agent may supply one; production
227
+ * surfaces the build agent's last assistant message. Absent ⇒ no body ⇒ the
228
+ * provider degrades to `--fill` (no regression).
229
+ */
230
+ output?: string;
231
+ };
232
+
233
+ export interface RunOnceOptions {
234
+ config: Config;
235
+ /**
236
+ * Pre-computed scan report; if omitted, discovery falls back to the registry
237
+ * (the hub-mirror set, ADR §1). Callers that still operate on working checkouts
238
+ * (the CLI / its tests) inject an explicit `report` built from working-tree
239
+ * paths; the registry reframe landed in `run-daemon-reframe` (work/done/).
240
+ */
241
+ report?: ScanReport;
242
+ /**
243
+ * The execution working area (bare hub mirrors + per-job worktrees). Defaults
244
+ * to `config.workspacesDir`. STATE, not cache (ADR §3).
245
+ */
246
+ workspace?: string;
247
+ /**
248
+ * How to invoke the configured agent. When omitted, the work runs through the
249
+ * harness seam (null adapter by default) shelling out to `config.agentCmd`.
250
+ * Tests inject this to edit files directly without a real agent.
251
+ */
252
+ dorfl?: Dorfl;
253
+ /** The harness seam (ADR §5); defaults to the null adapter. */
254
+ harness?: Harness;
255
+ /**
256
+ * The PR/code review gate (Gate 2) SEAM, threaded into the shared
257
+ * `performIntegration` core. Tests inject a canned `approve`/`block` verdict
258
+ * (no real model); the CLI passes the production `harnessReviewGate()` ONLY
259
+ * when `config.review` resolves on. Unset ⇒ no review (the default). The core
260
+ * throws if `review` is on but this is absent — `runOneItem` guards that.
261
+ */
262
+ reviewGate?: ReviewGate;
263
+ /**
264
+ * An explicitly-injected, fully-formed review provider INSTANCE that overrides
265
+ * per-item auto-detection (tests / embedding). Forwarded to
266
+ * `performIntegration` as `providerInstance` (carrying title/body/url). Unset ⇒
267
+ * the core selects the provider PURELY from the arbiter URL (no override axis).
268
+ */
269
+ provider?: ReviewProvider;
270
+ /**
271
+ * The `gh` AUTH/AVAILABILITY PROBE the PR-INTENT pre-flight guard runs UP FRONT
272
+ * per item (propose + GitHub arbiter + `noPR` unset): `true` ⇒ `gh` CAN open a
273
+ * PR. The SAME signal in-place `do` (and `do --remote`) uses, run BEFORE the
274
+ * claim so no item is left claimed when `gh` is genuinely unauthed. The probe
275
+ * (not a config check) is the signal — an absent `providers.github` identity
276
+ * falls back to ambient `gh` auth, which the probe reports available, so the run
277
+ * PROCEEDS. Injectable so tests stub `gh` without a real binary; production
278
+ * defaults to `new GitHubProvider().available(repoPath, env)`.
279
+ */
280
+ ghCanOpenPr?: (cwd: string, env: NodeJS.ProcessEnv | undefined) => boolean;
281
+ /** Optional injectable PR opener for `integration: propose` (legacy bridge). */
282
+ openPr?: (opts: {
283
+ cwd: string;
284
+ branch: string;
285
+ env?: NodeJS.ProcessEnv;
286
+ }) => void;
287
+ /** Environment for git/agent child processes. */
288
+ env?: NodeJS.ProcessEnv;
289
+ /** Override agent-id generation (tests). Retained for API compat; unused for branch naming. */
290
+ agentId?: () => string;
291
+ /**
292
+ * Sink for non-fatal warnings (e.g. a repo's `.dorfl.json` naming
293
+ * runner/host-only keys that were ignored). Defaults to a no-op so the core
294
+ * stays pure; the CLI wires this to stderr.
295
+ */
296
+ onWarn?: (message: string) => void;
297
+ /**
298
+ * The per-machine {@link ConfigOverrideMap} (from `loadConfigOverride`),
299
+ * threaded into the per-repo resolution `runOneItem` performs so the override
300
+ * applies to every tick item (ADR `per-machine-config-override-layer`).
301
+ * Default: empty (no override).
302
+ */
303
+ override?: ConfigOverrideMap;
304
+ /**
305
+ * Bounded-backoff bounds for the needs-attention route's git network ops
306
+ * (surface fetch+push, branch push). Defaults to the shared `DEFAULT_BACKOFF`.
307
+ * Threaded into the failure-routing seam so a degraded run gives up cleanly
308
+ * rather than hanging the tick.
309
+ */
310
+ backoff?: BackoffOptions;
311
+ /**
312
+ * Injectable sleep for that backoff — the SAME seam {@link runLoop} uses for
313
+ * its inter-tick sleep. Defaults to a real `setTimeout`. Tests inject a no-op
314
+ * so a fully-offline-arbiter failure route resolves with NO real waits.
315
+ */
316
+ sleep?: Sleep;
317
+ }
318
+
319
+ /**
320
+ * Run one supervised tick: claim up to `maxParallel` eligible items
321
+ * (≤ `perRepoMax` per repo), run the agent on each in an isolated **job
322
+ * worktree** (the shared execution substrate — hub mirror + worktree + seams),
323
+ * gate on acceptance tests, rebase onto the latest arbiter main, and integrate
324
+ * the green ones. The runner owns EVERY git-state transition (claim, done-move,
325
+ * completion commit, integration); the agent only edits code. A lost race (claim
326
+ * exit 2) is skipped cleanly; failing work never reaches `work/done/`; a rebase
327
+ * conflict is aborted and routed to needs-attention (ADR §10).
328
+ */
329
+ export async function runOnce(options: RunOnceOptions): Promise<RunOnceResult> {
330
+ const config = options.config;
331
+ // Default discovery is the REGISTRY (the hub-mirror set, ADR §1) — the reframe
332
+ // that landed in `run-daemon-reframe` (work/done/). Callers that still operate
333
+ // on WORKING CHECKOUTS (the CLI / its tests) inject an explicit `report` built
334
+ // from working-tree paths ({@link scanRepoPaths}); without an injected report
335
+ // we fall back to the registry scan (async).
336
+ const report =
337
+ options.report ?? (await scan(config, {override: options.override}));
338
+ const candidates = selectCandidates(report, {
339
+ maxParallel: config.maxParallel,
340
+ perRepoMax: config.perRepoMax,
341
+ });
342
+
343
+ // The launch harness: an explicit injection wins (tests); otherwise build the
344
+ // adapter the config selects (`harness: 'pi'` ⇒ the pi CLI; default ⇒ null,
345
+ // shelling out to `agentCmd`). pi specifics stay behind the seam.
346
+ const harness =
347
+ options.harness ??
348
+ createHarness({harness: config.harness, piBin: config.piBin});
349
+ // An explicitly-injected provider INSTANCE (tests / embedding) wins over
350
+ // per-item auto-detection; otherwise each item selects its own provider PURELY
351
+ // from its arbiter URL (see runOneItem). This is the resolved provider OBJECT,
352
+ // not a config override (there is none — the provider is arbiter-derived).
353
+ const provider = options.provider;
354
+ const workspace = options.workspace ?? config.workspacesDir;
355
+ const env = options.env;
356
+
357
+ // The per-repo claim serialiser: the ONLY shared-working-tree step in the
358
+ // otherwise-concurrent tick is the claim (it prepares its micro-commit in the
359
+ // repo's checkout/mirror). Two concurrent claims in ONE repo would corrupt each
360
+ // other's HEAD/index, so we serialise the CLAIM per repo (cheap — mv + commit +
361
+ // CAS push) while the agent run + the rebase-or-abort integration stay fully
362
+ // concurrent in each job's OWN worktree. Distinct repos claim in parallel.
363
+ const claimLock = createKeyedLock();
364
+
365
+ // The per-repo INTEGRATE serialiser — the SIBLING of `claimLock`, the same
366
+ // `createKeyedLock` primitive keyed on the same repo key. The land-on-`main`
367
+ // TAIL of merge-mode integration (fetch+rebase → `${branch}:main` push) is a
368
+ // shared-`main` step: two concurrent SAME-repo merge jobs both rebase onto the
369
+ // same pre-merge base and the loser's plain push is non-fast-forward (today it
370
+ // only works by benign timing). Serialising ONLY that tail per repo (the seam
371
+ // is applied INSIDE `performIntegration`, around step 4–5, NOT the whole call —
372
+ // the slow gate + Gate-2 review stay at the front, OUTSIDE the lock, so they
373
+ // run concurrently) makes the loser re-fetch + rebase onto the winner's
374
+ // now-advanced main, so its push is a clean fast-forward. Distinct repos
375
+ // integrate in parallel (per-repo key); a genuine same-repo code conflict
376
+ // routes exactly ONE job to needs-attention (it cannot both-land).
377
+ //
378
+ // This lock is the SIBLING-INTEGRATE serialiser ONLY. It does NOT cover the
379
+ // claim-vs-integrate race: a sibling same-repo CLAIM advances `<arbiter>/main`
380
+ // under the SEPARATE `claimLock` (above), which can land INSIDE this job's
381
+ // integrate push window. That is closed independently by the integrator's
382
+ // bounded re-rebase-and-retry on a non-fast-forward merge push (task
383
+ // `run-fleet-claim-integrate-and-sibling-rebase-concurrency-safe`, Race 1) — the
384
+ // two locks are NOT merged (merging would serialise the cheap claim behind the
385
+ // slow gated integrate, killing run's parallelism), the retry handles the
386
+ // cross-lock main-advance instead.
387
+ const integrateLock = createKeyedLock();
388
+
389
+ // GENUINELY CONCURRENT (ADR §3 — the whole point of `run`): run up to
390
+ // `maxParallel` `runOneItem`s IN FLIGHT at once, capped at `perRepoMax`
391
+ // concurrently per repo. The caps now bound ACTUAL in-flight execution, not just
392
+ // selection (this replaced the old `for (const c) { await runOneItem }` loop
393
+ // that ran selected candidates one-at-a-time). The substrate is parallel-ready —
394
+ // each slug → a distinct `work/<slug>` branch → a distinct job worktree, and the
395
+ // arbiter CAS serialises claims (a loser gets dropped) — so the only shared state
396
+ // across in-flight jobs is read-only config; each job owns its own worktree.
397
+ // Integration ordering is independent per job: each rebases-or-aborts onto a
398
+ // moving `<arbiter>/main` on its own, and a conflict routes only THAT job to
399
+ // needs-attention (the executor never lets one job's failure abort the others).
400
+ const settled = await runConcurrent({
401
+ items: candidates,
402
+ maxInFlight: config.maxParallel,
403
+ keyFor: (candidate) => candidate.repoPath,
404
+ perKeyMax: config.perRepoMax,
405
+ worker: (candidate) =>
406
+ runOneItem(candidate, {
407
+ config,
408
+ override: options.override,
409
+ workspace,
410
+ dorfl: options.dorfl,
411
+ harness,
412
+ reviewGate: options.reviewGate,
413
+ provider,
414
+ ghCanOpenPr: options.ghCanOpenPr,
415
+ openPr: options.openPr,
416
+ env,
417
+ onWarn: options.onWarn,
418
+ backoff: options.backoff,
419
+ sleep: options.sleep,
420
+ claimLock,
421
+ integrateLock,
422
+ }),
423
+ });
424
+ // `runOneItem` is total (it maps every failure to an `ItemResult` and never
425
+ // throws), so a captured `{error}` slot is a defensive last resort — surface it
426
+ // as a claim-error item rather than crashing the whole tick.
427
+ const items: ItemResult[] = settled.map((slot, i) =>
428
+ 'ok' in slot
429
+ ? slot.ok
430
+ : {
431
+ repoPath: candidates[i].repoPath,
432
+ slug: candidates[i].slug,
433
+ status: 'claim-error' as const,
434
+ detail: (slot.error as Error)?.message ?? String(slot.error),
435
+ },
436
+ );
437
+
438
+ const claimedAndDone = items.filter(
439
+ (i) => i.status === 'claimed-done',
440
+ ).length;
441
+ const skipped = items.filter(
442
+ (i) => i.status === 'lost-race' || i.status === 'claim-contended',
443
+ ).length;
444
+ const needsAttention = items.filter(
445
+ (i) =>
446
+ i.status === 'tests-failed' ||
447
+ i.status === 'needs-attention' ||
448
+ i.status === 'agent-stopped',
449
+ ).length;
450
+ const failed = items.filter(
451
+ (i) =>
452
+ i.status === 'tests-failed' ||
453
+ i.status === 'needs-attention' ||
454
+ i.status === 'agent-stopped' ||
455
+ i.status === 'agent-failed' ||
456
+ // The FAILURE-CAUSE refinements of `agent-failed` count the SAME way (they
457
+ // are the same agent/run-failure routed to needs-attention, just labelled
458
+ // by cause).
459
+ i.status === 'transient-infra' ||
460
+ i.status === 'config-error' ||
461
+ // The surface to needs-attention did NOT land (lost the CAS race); the item
462
+ // is still in-progress on the arbiter — a genuine FAILURE (not a clean
463
+ // needs-attention), so it counts as failed, NOT in `needsAttention`.
464
+ i.status === 'surface-unmoved' ||
465
+ i.status === 'claim-error',
466
+ ).length;
467
+
468
+ return {claimedAndDone, skipped, failed, needsAttention, items};
469
+ }
470
+
471
+ interface OneItemContext {
472
+ config: Config;
473
+ override?: ConfigOverrideMap;
474
+ workspace: string;
475
+ dorfl?: Dorfl;
476
+ harness: Harness;
477
+ /** The PR/code review gate (Gate 2) seam threaded into `performIntegration`. */
478
+ reviewGate?: ReviewGate;
479
+ /** An explicitly-injected provider that overrides per-item auto-detection. */
480
+ provider?: ReviewProvider;
481
+ /**
482
+ * The `gh` AUTH/AVAILABILITY PROBE the PR-INTENT pre-flight guard runs per item
483
+ * BEFORE the claim (see {@link RunOnceOptions.ghCanOpenPr}). Injectable for
484
+ * tests; production defaults to `new GitHubProvider().available(repoPath, env)`.
485
+ */
486
+ ghCanOpenPr?: (cwd: string, env: NodeJS.ProcessEnv | undefined) => boolean;
487
+ openPr?: (opts: {
488
+ cwd: string;
489
+ branch: string;
490
+ env?: NodeJS.ProcessEnv;
491
+ }) => void;
492
+ env?: NodeJS.ProcessEnv;
493
+ /**
494
+ * The INTEGRATION-PATH env (identity feature): the env for git/provider
495
+ * operations (claim, push, needs-attention surface, `gh`), derived from
496
+ * `config.identity` per-repo. Distinct from `env` (the AGENT launch env, which
497
+ * stays plain ambient — the runner, not the agent, acts as the identity). Set
498
+ * per-item in {@link runOneItem}; absent ⇒ falls back to `env` (no identity).
499
+ */
500
+ gitEnv?: NodeJS.ProcessEnv;
501
+ onWarn?: (message: string) => void;
502
+ /** Bounded-backoff bounds for the needs-attention route's network ops. */
503
+ backoff?: BackoffOptions;
504
+ /** Injectable sleep for that backoff (tests; defaults to real `setTimeout`). */
505
+ sleep?: Sleep;
506
+ /**
507
+ * Per-repo claim serialiser (shared across the tick's in-flight jobs). The claim
508
+ * step mutates the repo's shared working tree, so it is serialised per repo;
509
+ * everything after the claim runs in this job's own worktree and stays
510
+ * concurrent. Optional so a direct `runOneItem` caller (none today) degrades to
511
+ * an un-contended lock.
512
+ */
513
+ claimLock?: <T>(key: string, fn: () => Promise<T>) => Promise<T>;
514
+ /**
515
+ * Per-repo INTEGRATE serialiser (shared across the tick's in-flight jobs), the
516
+ * SIBLING of {@link claimLock}. Threaded into `performIntegration` as the
517
+ * `integrateLock` seam, where it wraps ONLY the rebase-to-integrate TAIL (step
518
+ * 4 fetch+rebase → step 5 integrate), so same-repo merge jobs land on `main`
519
+ * one-at-a-time (the loser rebases onto the winner's advanced main ⇒ a clean
520
+ * fast-forward) while their gate + Gate-2 review still run concurrently.
521
+ * Cross-repo integration stays concurrent (keyed per repo). Optional so a
522
+ * direct `runOneItem` caller (none today) degrades to an un-contended lock.
523
+ */
524
+ integrateLock?: <T>(key: string, fn: () => Promise<T>) => Promise<T>;
525
+ }
526
+
527
+ async function runOneItem(
528
+ candidate: Candidate,
529
+ ctx: OneItemContext,
530
+ ): Promise<ItemResult> {
531
+ const {slug, repoPath} = candidate;
532
+ const base: ItemResult = {repoPath, slug, status: 'lost-race'};
533
+
534
+ // Resolve THIS repo's effective config against its own `.dorfl.json`
535
+ // layered over the global config (flag > per-repo > global > default). Each
536
+ // repo gets its own integration mode / arbiter, so repo A can be `merge`
537
+ // while repo B is `propose` in one tick.
538
+ const resolved = resolveRepoConfig({
539
+ repoPath,
540
+ global: ctx.config,
541
+ override: ctx.override,
542
+ });
543
+ const config = resolved.config;
544
+ if (resolved.message) {
545
+ ctx.onWarn?.(resolved.message);
546
+ }
547
+
548
+ // The IDENTITY env for this repo's GIT + provider operations (claim, push,
549
+ // integration, `gh`). Derived from the host-only `config.identity` (absent ⇒
550
+ // `ctx.env` unchanged, byte-for-byte ambient). This is the INTEGRATION-PATH
551
+ // env ONLY: the agent launch keeps the plain ambient `ctx.env` (the runner,
552
+ // not the agent, acts as the configured identity — design point 4). The push-
553
+ // time transport-coherence guard (`assertTransportAllowed`) fires before the
554
+ // integration push (where the arbiter URL is known), turning a forbidden
555
+ // transport into a clear error, never a silent wrong-account push.
556
+ let gitEnv: NodeJS.ProcessEnv;
557
+ try {
558
+ gitEnv = identityEnv(config.identity, ctx.env ?? process.env);
559
+ } catch (err) {
560
+ // A configured identity that cannot be resolved (e.g. `tokenEnv` names an
561
+ // unset env var) is a CONFIG error — fail THIS item cleanly (surfaced), never
562
+ // crash the tick or silently fall back to an ambient credential.
563
+ const message = (err as Error).message;
564
+ ctx.onWarn?.(message);
565
+ return {...base, status: 'config-error', detail: message};
566
+ }
567
+ // A per-ITEM context carrying `gitEnv` so the needs-attention save helpers
568
+ // (`saveAgentFailure`/`saveAgentStop`) push under the identity too. We rebind a
569
+ // COPY (never mutate the shared tick `ctx` — items run concurrently).
570
+ ctx = {...ctx, gitEnv};
571
+
572
+ // 0. PR-INTENT pre-flight guard — the AUTONOMOUS mirror of in-place `performDo`
573
+ // step 3c. The SAME predicate, run UP FRONT here (per-repo config resolved,
574
+ // BEFORE the claim) so a `propose` item on a GitHub arbiter that INTENDS a PR
575
+ // (`noPR` unset) fails fast when `gh` genuinely cannot open one, instead of
576
+ // letting integration silently degrade to manual-PR instructions. CRITICAL: the
577
+ // probe runs BEFORE the claim — claiming then refusing would strand the item
578
+ // in-progress on main (the claim precedes `prepare()`). The arbiter URL is
579
+ // derived from `repoPath` the SAME way `claimAgainstRepo` derives the claim
580
+ // target (`arbiterUrlForRepo`). The PROBE is the signal, NOT a config check —
581
+ // an absent `providers.github` identity falls back to AMBIENT `gh` auth (the
582
+ // probe reports it available), so a working ambient setup PROCEEDS. A true
583
+ // result returns a CLEAN PRE-CLAIM item result on the `config-error` failure-
584
+ // cause status (a wiring/config failure, NOT a task fault), with NO claim,
585
+ // build, or half-built needs-attention surface — and it never crashes the tick
586
+ // or aborts siblings (the result is returned; `runConcurrent` continues).
587
+ // REUSES the predicate + message so the in-place + autonomous paths cannot
588
+ // drift.
589
+ {
590
+ const arbiterUrl = arbiterUrlForRepo({
591
+ repoPath,
592
+ arbiter: config.defaultArbiter,
593
+ env: gitEnv,
594
+ });
595
+ const probe =
596
+ ctx.ghCanOpenPr ??
597
+ ((probeCwd, probeEnv) =>
598
+ new GitHubProvider().available(probeCwd, probeEnv));
599
+ if (
600
+ shouldFailProposePrIntent({
601
+ mode: config.integration,
602
+ arbiterIsGitHub: isGitHubArbiterUrl(arbiterUrl),
603
+ noPR: config.noPR,
604
+ ghCanOpenPr: () => probe(repoPath, gitEnv),
605
+ })
606
+ ) {
607
+ return {
608
+ ...base,
609
+ status: 'config-error',
610
+ detail: PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE,
611
+ };
612
+ }
613
+ }
614
+
615
+ // 0b. STATIC fresh-worktree-gate readiness guard — the fleet mirror of
616
+ // `performDo` step 3d / `performDoRemote` step 1c (task
617
+ // `do-fails-fast-when-acceptance-gate-statically-unrunnable`). When the
618
+ // fresh-worktree gate is ON AND `prepare` resolves to no commands AND a
619
+ // lockfile is present in the repo, the throwaway worktree the gate runs in
620
+ // will have no installed deps, so the gate cannot run. Fail this item
621
+ // CLEANLY before the claim (no claim, no isolate, no agent, no surface) so a
622
+ // fleet tick never burns a build to discover a STATIC config gap and never
623
+ // routes correct work to needs-attention for this reason. Surfaced on the
624
+ // same `config-error` axis the PR-intent guard above uses (a wiring/config
625
+ // fault, not a task fault). The repo discovery is either a working checkout
626
+ // (in-place tests) OR a BARE hub mirror (production fleet); detect the
627
+ // lockfile from whichever shape `repoPath` is. Deps-only — there is no
628
+ // verify-unset case (the gate substitutes `DEFAULT_VERIFY_COMMAND`).
629
+ {
630
+ const lockfile =
631
+ detectLockfileOnDisk(repoPath) ??
632
+ detectLockfileOnMirrorMain(repoPath, gitEnv);
633
+ const guard = checkGatePreconditions({
634
+ freshWorktreeGate: config.freshWorktreeGate,
635
+ prepare: config.prepare,
636
+ lockfile,
637
+ });
638
+ if (guard !== undefined) {
639
+ return {...base, status: 'config-error', detail: guard.message};
640
+ }
641
+ }
642
+
643
+ // 1. Claim (the runner's first git-state transition) via the in-process CAS.
644
+ // `claimAgainstRepo` handles BOTH discovery shapes: a working CHECKOUT (the
645
+ // in-place test/`do` path — claim directly) and a BARE registry hub mirror
646
+ // (`run`'s production discovery — claim in a throwaway clone, since a bare
647
+ // repo cannot `git mv`/`git commit`; the SAME pattern `do --remote` uses). It
648
+ // returns the `fromRepo`/`strategyArbiter` the worktree strategy resolves its
649
+ // mirror off (kept alive until AFTER prepare). `performClaim` is async, so two
650
+ // awaited claims over one slug genuinely race — the arbiter ref-CAS picks the
651
+ // winner.
652
+ const doClaim = () =>
653
+ claimAgainstRepo({
654
+ slug,
655
+ repoPath,
656
+ workspace: ctx.workspace,
657
+ arbiter: config.defaultArbiter,
658
+ env: gitEnv,
659
+ });
660
+ // Serialise the claim PER REPO (it prepares its micro-commit in a checkout/clone
661
+ // of the repo) so two in-flight jobs in the same repo do not stomp on each
662
+ // other; the arbiter CAS then picks the winner across machines. Different repos
663
+ // (and everything AFTER the claim) run concurrently.
664
+ const repoClaim = ctx.claimLock
665
+ ? await ctx.claimLock(repoPath, doClaim)
666
+ : await doClaim();
667
+ const claim = repoClaim.claim;
668
+ if (claim.outcome === 'lost') {
669
+ repoClaim.cleanup();
670
+ return {...base, status: 'lost-race'};
671
+ }
672
+ if (claim.outcome === 'contended') {
673
+ repoClaim.cleanup();
674
+ return {...base, status: 'claim-contended'};
675
+ }
676
+ if (claim.outcome === 'usage-error') {
677
+ repoClaim.cleanup();
678
+ return {...base, status: 'claim-error', detail: claim.message};
679
+ }
680
+
681
+ // 2. Isolate via the isolation-strategy seam (ADR §3). `run` always selects
682
+ // the JOB-WORKTREE strategy: materialise the hub mirror + cut a per-job
683
+ // worktree off the freshly-fetched `<hub>/main` in the agents' area
684
+ // (ADR §1/§2). The seam yields a UNIFORM HANDLE (dir/branch/arbiterRemote/
685
+ // arbiterUrl + a strategy-appropriate teardown) that the post-claim pipeline
686
+ // below reads from — never a concrete `Job` — so the SAME steps serve the
687
+ // in-place strategy too (wired by `do-in-place`). The mirror is keyed off
688
+ // the arbiter URL resolved from this repo, so jobs reuse it (fetch, not
689
+ // re-clone) across the run.
690
+ const strategy: IsolationStrategy = jobWorktreeStrategy({
691
+ // In-place: the checkout. Bare-mirror: the throwaway claim CLONE (still alive)
692
+ // — the strategy resolves the mirror URL off it at prepare time, then we drop it.
693
+ fromRepo: repoClaim.fromRepo,
694
+ arbiter: repoClaim.strategyArbiter,
695
+ workspacesDir: ctx.workspace,
696
+ });
697
+ let tree: IsolatedTree | undefined;
698
+ try {
699
+ tree = strategy.prepare({slug, env: gitEnv});
700
+ // The claim clone (bare-mirror path) has done its job: the mirror is
701
+ // materialised + the worktree cut. Drop it (no-op for the in-place path).
702
+ repoClaim.cleanup();
703
+
704
+ // 2a. CONTINUE rebase conflict (ADR §14 + §10): a requeue kept a
705
+ // `work/<slug>` whose commits did not replay cleanly onto the current
706
+ // main at onboard-time (aborted, never auto-resolved). Route the item to
707
+ // needs-attention TREE-LESSLY via the SAME `#89` mechanism `requeue` uses
708
+ // for the reverse direction — the rebase was ABORTED, so this worktree's
709
+ // `work/<slug>` tip == the arbiter tip (the kept branch, unchanged,
710
+ // after-commit). The DURABLE artifact is that branch on the arbiter + the
711
+ // main surface (ADR §14: the job worktree is a disposable cache; recovery
712
+ // flows through the branch + folder-native surfaces, NOT by editing the
713
+ // worktree). So the surface is purely the one-file `in-progress/ →
714
+ // needs-attention/` ledger move + reason — no branch push, no worktree
715
+ // mutation. Because the branch is provably on the arbiter, the §4 reap
716
+ // predicate still HOLDS and this worktree is reaped — more §14-aligned.
717
+ if (tree.continueRebaseConflict) {
718
+ const reason =
719
+ `continuing the kept ${tree.branch}: rebase onto the latest main ` +
720
+ 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
721
+ 'main, or `requeue --reset` to discard and start fresh';
722
+ updateJobRecord(tree.dir, {state: 'needs-attention', reason});
723
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
724
+ cwd: tree.dir,
725
+ slug,
726
+ reason,
727
+ arbiter: tree.arbiterRemote,
728
+ env: gitEnv,
729
+ });
730
+ if (!surfaced.moved) {
731
+ return surfaceUnmovedItemResult({base, tree, slug, reason, surfaced});
732
+ }
733
+ return {...base, status: 'needs-attention', detail: reason};
734
+ }
735
+
736
+ // 2b. CONTINUE reconcile-push TERMINAL failure (the stale-lease-strand bug):
737
+ // the onboard reconcile push of the kept (already-committed) work branch
738
+ // FAILED terminally (stale-lease cap exhausted, or a non-stale-lease
739
+ // rejection / unreachable arbiter). The push helper THROWS; `createJob`
740
+ // CATCHES it and flags `continuePushFailure` so the tick does NOT crash
741
+ // leaving the task silently in-progress on the arbiter. Surface to
742
+ // needs-attention TREE-LESSLY via the SAME `#89` mechanism `requeue` uses —
743
+ // the kept branch already on the arbiter from the prior requeue
744
+ // (after-commit, recoverable), so the surface is purely the one-file ledger
745
+ // move + reason (no branch push, no worktree) — instead of running the agent.
746
+ if (tree.continuePushFailure !== undefined) {
747
+ const reason =
748
+ `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
749
+ `to the arbiter failed terminally (${tree.continuePushFailure}) — the ` +
750
+ 'kept branch is left intact on the arbiter (recoverable); `requeue` to ' +
751
+ 'retry once the churn settles, or `requeue --reset` to discard and start ' +
752
+ 'fresh';
753
+ updateJobRecord(tree.dir, {state: 'needs-attention', reason});
754
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
755
+ cwd: tree.dir,
756
+ slug,
757
+ reason,
758
+ arbiter: tree.arbiterRemote,
759
+ env: gitEnv,
760
+ });
761
+ if (!surfaced.moved) {
762
+ return surfaceUnmovedItemResult({base, tree, slug, reason, surfaced});
763
+ }
764
+ return {...base, status: 'needs-attention', detail: reason};
765
+ }
766
+
767
+ // 3. Build the prompt — the SAME dual-use assembly `dorfl prompt`
768
+ // emits: the canonical wrapper (+ source prd) + the task's ## Prompt.
769
+ let prompt: string;
770
+ try {
771
+ // CONTINUE-aware resolution: only on a continue (the job continued a kept
772
+ // arbiter `work/<slug>`) may the task already be in `work/done/`; admit
773
+ // `done/` ONLY behind the tip-vs-arbiter stranded gate (story 5). In a bare
774
+ // hub mirror's worktree the refs are the LOCAL heads `work/<slug>` / `main`.
775
+ const task = resolveTask(
776
+ tree.dir,
777
+ slug,
778
+ tree.continued
779
+ ? {
780
+ cwd: tree.dir,
781
+ branchRef: tree.branch,
782
+ mainRef: 'main',
783
+ env: gitEnv,
784
+ }
785
+ : undefined,
786
+ );
787
+ // CONTINUE-mode (the `agent-prompt-continue-context` task): when the job
788
+ // CONTINUED a kept arbiter `work/<slug>` (a requeue), inject the continue
789
+ // block (prior diff + reason + handoff note). REUSE the SAME continue-
790
+ // detection — in a bare hub mirror's worktree the refs are the LOCAL heads
791
+ // `work/<slug>` and `main` (after `ensureMirror`'s mirror-style fetch).
792
+ const continueContext = tree.continued
793
+ ? resolveContinueContext({
794
+ cwd: tree.dir,
795
+ slug,
796
+ arbiter: tree.arbiterRemote,
797
+ branchRef: tree.branch,
798
+ mainRef: 'main',
799
+ content: readFileSync(task.path, 'utf8'),
800
+ env: gitEnv,
801
+ })
802
+ : undefined;
803
+ // Thread the resolved per-repo nudge through the per-item override layer
804
+ // (a task or prd may pin `promptGuidance.testFirst` in its frontmatter,
805
+ // superseding the repo policy for THIS item) before the wrapper is built.
806
+ const itemGuidance = resolvePromptGuidanceForItem({
807
+ cwd: tree.dir,
808
+ repoResolved: resolvePromptGuidance(config),
809
+ taskContent: readFileSync(task.path, 'utf8'),
810
+ });
811
+ prompt = buildAgentPrompt(task.slug, task.prd, task.taskPrompt, {
812
+ cwd: tree.dir,
813
+ continueContext,
814
+ promptGuidance: itemGuidance,
815
+ });
816
+ } catch (err) {
817
+ if (err instanceof PromptError) {
818
+ return await saveAgentFailure(base, tree, slug, err.message, ctx);
819
+ }
820
+ throw err;
821
+ }
822
+
823
+ // 4. Run the agent — via the injected runner (tests) or the harness seam
824
+ // (null adapter by default), shelling out to the configured agentCmd. The
825
+ // resolved per-repo `model` (ADR §13) flows through the seam to the adapter;
826
+ // a `{model}`-in-agentCmd misconfiguration surfaces as agent-failed.
827
+ let agent: {ok: boolean; detail?: string; output?: string};
828
+ try {
829
+ agent = runAgent(
830
+ ctx,
831
+ tree,
832
+ prompt,
833
+ slug,
834
+ config.agentCmd,
835
+ config.model,
836
+ config.sessionsDir,
837
+ );
838
+ } catch (err) {
839
+ return await saveAgentFailure(
840
+ base,
841
+ tree,
842
+ slug,
843
+ (err as Error).message,
844
+ ctx,
845
+ );
846
+ }
847
+ if (!agent.ok) {
848
+ return await saveAgentFailure(
849
+ base,
850
+ tree,
851
+ slug,
852
+ agent.detail ?? `the agent failed to build '${slug}'.`,
853
+ ctx,
854
+ );
855
+ }
856
+
857
+ // 4b. HONOR a deliberate STOP (task `agent-stop-signal`) — the SAME detection
858
+ // in-place/remote `do` run, mirrored here. The agent exited cleanly but the
859
+ // CLAIM-PROTOCOL wrapper tells it to STOP on a DRIFTED/ambiguous task; an
860
+ // in-band sentinel carries its reason VERBATIM, and the empty-diff backstop
861
+ // catches a stop without one. Either routes to needs-attention (surfaced on
862
+ // the arbiter) and SKIPS the gate + Gate-2 (the whole `performIntegration`
863
+ // band) — a clean STOP is NOT "a build that changed nothing".
864
+ const sentinel = parseStopSentinel(agent.output);
865
+ const stopReason =
866
+ sentinel !== undefined
867
+ ? sentinel.reason
868
+ : (await isWorkBranchDiffEmpty({
869
+ cwd: tree.dir,
870
+ arbiter: tree.arbiterRemote,
871
+ env: gitEnv,
872
+ }))
873
+ ? emptyDiffStopReason(slug)
874
+ : undefined;
875
+ if (stopReason !== undefined) {
876
+ return await saveAgentStop(base, tree, slug, stopReason, ctx);
877
+ }
878
+
879
+ // 5–7 (CONVERGED). The whole gate → review → done-move → commit → rebase →
880
+ // integrate band — plus the needs-attention routing on any failure — now runs
881
+ // through the SHARED `performIntegration` core (`integration-core.ts`, the
882
+ // run/do convergence prd). `run` no longer forks its own gate / done-move /
883
+ // completion commit / `Integrator`+`integrateWithRebase`: that closed all
884
+ // three drift instances at once (the fleet now gets the review gate, the PR
885
+ // title/body, AND the per-repo language-agnostic `verify` gate instead of the
886
+ // old test-only `pnpm -r test` floor). The HEAD above (claim, isolate, agent,
887
+ // failure-save) and the TAIL below (job record + worktree reap) stay here;
888
+ // the band is what they share. `run` is ALWAYS autonomous, so it ALWAYS
889
+ // passes `surfaceArbiter` (every failure surfaces on the arbiter's main +
890
+ // pushes the branch). The injected `openPr` legacy bridge is forwarded to the
891
+ // core unchanged; absent it the core selects the provider PURELY from the
892
+ // arbiter URL (a GitHub remote ⇒ `gh pr create`). The per-repo `noPR` INTENT
893
+ // (suppress the PR) rides separately — it does not pick a provider.
894
+ //
895
+ // `performIntegration` THROWS a plain `Error` for a misconfigured gate
896
+ // (`review` on with no `reviewGate` wired) — `run`'s CLI always wires one when
897
+ // `config.review` is on, so that is a defensive case, but it must NOT crash
898
+ // the whole tick. We catch it and route the item through the same
899
+ // work-preserving needs-attention seam an agent failure uses (`saveAgentFailure`)
900
+ // so the worktree is handled and the run continues to the next item.
901
+ // Push-time transport-coherence guard (identity feature): refuse a forbidden
902
+ // transport for THIS arbiter's actual URL rather than silently pushing under
903
+ // an ambient credential. A no-op when no identity is configured.
904
+ try {
905
+ assertTransportAllowed(config.identity, tree.arbiterUrl);
906
+ } catch (err) {
907
+ return await saveAgentFailure(
908
+ base,
909
+ tree,
910
+ slug,
911
+ (err as Error).message,
912
+ ctx,
913
+ );
914
+ }
915
+
916
+ let core;
917
+ try {
918
+ core = await performIntegration({
919
+ cwd: tree.dir,
920
+ // The arbiter remote name valid inside the isolated tree (job-worktree:
921
+ // the mirror's clone remote `origin`), NOT the source repo's
922
+ // `defaultArbiter` name.
923
+ arbiter: tree.arbiterRemote,
924
+ slug,
925
+ // Claim no longer moves the body (task
926
+ // `cutover-claim-body-stays-and-complete-sources-from-backlog`): a
927
+ // freshly-built task RESTS in `tasks/ready/` on `main`, so the done-move
928
+ // sources from there.
929
+ source: 'tasks-ready',
930
+ recovering: false,
931
+ // `run` is ALWAYS autonomous → surface every failure on the arbiter's
932
+ // main AND push the work branch (DATA, not a caller-identity flag).
933
+ surfaceArbiter: tree.arbiterRemote,
934
+ // The per-repo, language-agnostic gate (ADR §8) — the protocol-conformance
935
+ // fix: `run` now honours `config.verify` instead of the deleted
936
+ // `defaultTestGate`'s hardcoded `pnpm -r test`.
937
+ verify: config.verify,
938
+ // The per-repo ENV-PREP step (`prepare`), sequenced ONCE before the first
939
+ // `verify` on the fresh job worktree so it has deps (a fresh worktree off
940
+ // the mirror has no `node_modules`). Unset ⇒ a no-op; never baked into verify.
941
+ prepare: config.prepare,
942
+ // FRESH-WORKTREE GATE (task `gate-on-rebased-tip-fresh-worktree`): run the
943
+ // acceptance gate against the REBASED tip in a clean throwaway worktree (the
944
+ // tree that integrates) so a green gate provably describes the merged
945
+ // artifact. Passed UNCONDITIONALLY (the resolved flag) at ANY `perRepoMax`:
946
+ // the two PRE-EXISTING run-fleet same-repo races the gate's latency used to
947
+ // make deterministic are now CLOSED on their own merits (task
948
+ // `run-fleet-claim-integrate-and-sibling-rebase-concurrency-safe`) — RACE 1
949
+ // (claim-vs-integrate non-fast-forward push) by the integrator's bounded
950
+ // re-rebase-and-retry on the merge push, RACE 2 (sibling-slug divergent-base
951
+ // ledger rebase) by the step-4 sibling-ledger reconciliation arm — so the
952
+ // `perRepoMax === 1` downgrade is REMOVED and the fresh rebased-tip gate runs
953
+ // on the `run` fleet at any same-repo parallelism. Single-job callers
954
+ // (`do`/`--isolated`/`--remote`/`complete`) already pass it unconditionally.
955
+ freshWorktreeGate: config.freshWorktreeGate,
956
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
957
+ // the git-alone FLOOR of the cross-job land queue (prd `land-time-reverify-
958
+ // and-parallel-merge-ceiling` Story 5 / Applied Answer q1 (a)). Threaded
959
+ // from the per-repo resolved config so a wide-matrix CI's raised cap actually
960
+ // reaches the merge loop — across separate `run` fleet jobs the CAS loop IS
961
+ // the queue (`integrateLock` only serialises sibling integrates in ONE process).
962
+ mergeRetries: config.mergeRetries,
963
+ // Gate 2 (PR/code review): the per-repo resolved flags ride from `config`;
964
+ // only the gate SEAM is threaded through `ctx` (the CLI wires the prod
965
+ // `harnessReviewGate()` only when `config.review` is on).
966
+ review: config.review,
967
+ reviewModel: config.reviewModel,
968
+ reviewMaxRounds: config.reviewMaxRounds,
969
+ reviewGate: ctx.reviewGate,
970
+ mode: config.integration,
971
+ // PR-INTENT: the per-repo `noPR` (suppress the PR even on an authed GitHub
972
+ // arbiter). NOT a provider choice — the provider is purely arbiter-derived.
973
+ noPR: config.noPR,
974
+ // Provider: an injected `openPr` wins (legacy test bridge); otherwise the
975
+ // core selects PURELY from the arbiter URL (a GitHub remote ⇒ `gh pr
976
+ // create`, else push-only `none`). `providerInstance` (the resolved object
977
+ // the `run` seam injects) STAYS — it is a DIFFERENT `provider`.
978
+ providerInstance: ctx.provider,
979
+ openPr: ctx.openPr,
980
+ // The per-repo INTEGRATE serialiser (sibling of the claim lock): the
981
+ // core applies it around ONLY the rebase-to-integrate TAIL, keyed on
982
+ // the repo path (the same key the claim lock uses), so two same-repo
983
+ // merge jobs land on `main` one-at-a-time. Absent on single-job paths.
984
+ integrateLock: ctx.integrateLock,
985
+ integrateLockKey: repoPath,
986
+ // Half A/B: the synthesised single-line title + the agent's surfaced
987
+ // final summary as the PR body (the core scaffolds the task-pointer
988
+ // header). `title` is synthesised inside the core from the task's
989
+ // frontmatter; `body` is the agent output (undefined ⇒ `--fill`).
990
+ body: agent.output,
991
+ env: gitEnv,
992
+ // The review AGENT (Gate 2) launches AMBIENT — never the identity env
993
+ // (an agent must not act as the bot; only the runner's git ops do).
994
+ agentEnv: ctx.env,
995
+ });
996
+ } catch (err) {
997
+ // A thrown core error (misconfigured gate, or an unexpected plumbing
998
+ // failure) — SAVE the work + route to needs-attention, never crash the tick.
999
+ return await saveAgentFailure(
1000
+ base,
1001
+ tree,
1002
+ slug,
1003
+ (err as Error).message,
1004
+ ctx,
1005
+ );
1006
+ }
1007
+
1008
+ // TAIL: map the core's DATA outcome onto the job record + `ItemStatus`. The
1009
+ // core already ran the gate/review/move/commit/rebase/integrate AND did the
1010
+ // needs-attention routing; `run`'s tail does ONLY the job-record write + (in
1011
+ // `finally`) the worktree reap. `run` NEVER switches/ff/deletes branches
1012
+ // (that is `do`'s in-place tail; a job worktree is reaped, not switched).
1013
+ if (core.outcome === 'gate-failed') {
1014
+ updateJobRecord(tree.dir, {
1015
+ state: 'needs-attention',
1016
+ reason: core.reason,
1017
+ });
1018
+ return {...base, status: 'tests-failed', detail: core.reason};
1019
+ }
1020
+ if (core.outcome === 'review-unparseable') {
1021
+ // Gate 2 RAN but its verdict was UNPARSEABLE (malformed JSON). The core already
1022
+ // routed it work-preservingly (branch pushed + surfaced). FAILURE-CAUSE axis,
1023
+ // NOT a reviewer block: classify the parse-failure phrase the core recorded →
1024
+ // `transient-infra` (re-run the SAME work — the stochastic gate + the parser's
1025
+ // repair pass make a re-run far more likely to parse), the SAME
1026
+ // `classifyFailureCause` `do` uses, so the cross-path label agrees. The job
1027
+ // record carries the classified status; the worktree is retained by the §4 reap.
1028
+ const cause = classifyFailureCause(core.reason);
1029
+ updateJobRecord(tree.dir, {
1030
+ state: 'needs-attention',
1031
+ reason: core.reason,
1032
+ });
1033
+ return {
1034
+ ...base,
1035
+ status: failureCauseToItemStatus(cause),
1036
+ detail: core.reason,
1037
+ };
1038
+ }
1039
+ if (
1040
+ core.outcome === 'prepare-failed' ||
1041
+ core.outcome === 'review-blocked' ||
1042
+ core.outcome === 'rebase-conflict' ||
1043
+ core.outcome === 'invariant-violation'
1044
+ ) {
1045
+ // `prepare-failed`: the env-prep (install) step was red, so the env could
1046
+ // not be made ready and `verify` was NOT run — distinct from a `tests-failed`
1047
+ // red gate. Route it to needs-attention like the others (a human fixes the
1048
+ // prepare command, then re-runs); the core already surfaced the bounce.
1049
+ // `invariant-violation`: the one-slug-one-folder guard FAILED LOUD (the
1050
+ // arbiter already holds the slug in >1 status folder — a corrupt ledger).
1051
+ // The core integrated NOTHING. On the LEAST-supervised caller this MUST
1052
+ // NOT fall through to the SUCCESS branch (state:'done' / 'claimed-done'
1053
+ // with no prUrl) — that would misreport a refusal as a completed job, the
1054
+ // opposite of fail-loud. Route it to needs-attention like a rebase conflict
1055
+ // (a human resolves the duplicate, then re-runs); `complete.ts` mirrors this
1056
+ // refusal with exit 1.
1057
+ updateJobRecord(tree.dir, {
1058
+ state: 'needs-attention',
1059
+ reason: core.reason,
1060
+ });
1061
+ return {...base, status: 'needs-attention', detail: core.reason};
1062
+ }
1063
+
1064
+ // SUCCESS: the core integrated. Record the PR/MR URL (when a provider opened
1065
+ // one) on the job, surfaced by `status`.
1066
+ const prUrl = core.integration?.url;
1067
+ updateJobRecord(tree.dir, {state: 'done', prUrl});
1068
+ return {...base, status: 'claimed-done', integration: core.integration};
1069
+ } finally {
1070
+ // Strategy-appropriate teardown via the uniform handle (ADR §4). Job-worktree:
1071
+ // re-apply the provably-safe deletion predicate and remove the worktree ONLY
1072
+ // if it holds (clean tree AND the branch tip reachable on the arbiter — merged
1073
+ // or pushed). One rule, no done-vs-failed special-casing: a claimed-done job
1074
+ // is on the arbiter → reaped; a tests-failed / needs-attention / un-pushed job
1075
+ // is NOT → retained (the worktree is the needs-attention signal; `gc` catches
1076
+ // up). In-place: a NO-OP (the checkout is never reaped). NEVER `--force` here.
1077
+ if (tree) {
1078
+ tree.teardown();
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ /**
1084
+ * Run the agent against the isolated tree. Prefers the injected `dorfl`
1085
+ * (tests / custom embeddings); otherwise launches `agentCmd` through the harness
1086
+ * seam (recording the PID in the job's harness block, when there is one).
1087
+ */
1088
+ function runAgent(
1089
+ ctx: OneItemContext,
1090
+ tree: IsolatedTree,
1091
+ prompt: string,
1092
+ slug: string,
1093
+ agentCmd: string,
1094
+ model: string | undefined,
1095
+ sessionsDir: string | undefined,
1096
+ ): {ok: boolean; detail?: string; output?: string} {
1097
+ if (ctx.dorfl) {
1098
+ return ctx.dorfl({cwd: tree.dir, prompt, slug, env: ctx.env});
1099
+ }
1100
+ // Generate the full pi session-FILE path (task `session-path-pi-default`):
1101
+ // `<sessionsDir>/<work-id>-<unique>.jsonl`, or pi's per-cwd default folder when
1102
+ // `sessionsDir` is unset. The work-id (arbiter URL + slug) is `run`'s natural
1103
+ // unique-per-claim id. Threaded through the seam so the pi adapter passes
1104
+ // `--session <path>` (never `--session-dir` into the worktree).
1105
+ const session = generateSessionPath({
1106
+ sessionsDir,
1107
+ cwd: tree.dir,
1108
+ id: encodeWorkId(tree.arbiterUrl, slug),
1109
+ });
1110
+ const launched = ctx.harness.launch({
1111
+ dir: tree.dir,
1112
+ slug,
1113
+ command: agentCmd,
1114
+ prompt,
1115
+ // The model routing intent (ADR §13) — the adapter decides HOW it reaches
1116
+ // its tool (pi: `--model`; null/shell: `{model}` placeholder).
1117
+ model,
1118
+ session,
1119
+ env: ctx.env,
1120
+ });
1121
+ updateJobRecord(tree.dir, {harness: launched.record});
1122
+ // Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
1123
+ // for the propose-mode PR body — instead of dropping it (mirrors `do`'s
1124
+ // `runDoAgent`). Absent (no parseable assistant text) ⇒ undefined ⇒ `--fill`.
1125
+ return {ok: launched.ok, detail: launched.detail, output: launched.output};
1126
+ }
1127
+
1128
+ /**
1129
+ * SAVE the partial work of a FAILED agent instead of dropping it (closing the
1130
+ * fifth-and-last instance of the recurring asymmetry —
1131
+ * `work/notes/observations/run-agent-failure-does-not-save-work.md`). `run`'s
1132
+ * agent-failure return points (prompt-assembly fail / `runAgent` throw /
1133
+ * `agent.ok === false`) used to BARE-RETURN `agent-failed`, leaving whatever the
1134
+ * agent edited only on the LOCAL work branch in the (disposable, possibly remote)
1135
+ * job worktree — not on the arbiter, so a requeue-continue on a DIFFERENT machine
1136
+ * (or after a `gc` reap) re-cut fresh off main and orphaned it.
1137
+ *
1138
+ * This mirrors `do.ts`'s `saveAgentFailure`: route the failure through the SAME
1139
+ * ledger write seam's needs-attention transition the gate-fail / integrate-
1140
+ * conflict bounces use — which (with the arbiter) saves the agent's work as a wip
1141
+ * commit, `git mv`s the item to needs-attention/ with the failure detail recorded
1142
+ * as the reason, surfaces the move-only commit on the arbiter's `main`
1143
+ * (OBSERVABLE), AND pushes the `work/<slug>` branch (RECOVERABLE — the durable
1144
+ * artifact a requeue-continue lands the next agent on; continue-detection reads
1145
+ * <arbiter>/work/<slug> ahead of main). The push is best-effort — an unreachable
1146
+ * arbiter leaves the retained worktree + the local commits standing (the genuinely
1147
+ * un-pushed case). The status is the classified failure CAUSE (`transient-infra` /
1148
+ * `config-error` where knowable, else the conservative generic `agent-failed`);
1149
+ * only the work-preserving side-effect now matches the gate-fail path.
1150
+ */
1151
+ async function saveAgentFailure(
1152
+ base: ItemResult,
1153
+ tree: IsolatedTree,
1154
+ slug: string,
1155
+ detail: string,
1156
+ ctx: OneItemContext,
1157
+ ): Promise<ItemResult> {
1158
+ // Classify the failure CAUSE (best-effort + conservative) from the surfaced
1159
+ // detail via the SAME `classifyFailureCause` `do` uses — so a thrown CORE config/
1160
+ // wiring error reads as `config-error` (NOT `agent-failed`) on BOTH paths, closing
1161
+ // the cross-path divergence; a harness-surfaced model/connection outage reads as
1162
+ // `transient-infra`; an unrecognised cause stays the generic `agent-failed`. The
1163
+ // cause LABEL prefixes the recorded reason so the cause is legible on the
1164
+ // needs-attention route; `agent-failed` keeps the historical "agent failed:"
1165
+ // prefix (no reason-prose regression).
1166
+ const cause = classifyFailureCause(detail);
1167
+ const reason = `${failureCauseLabel(cause)}: ${detail}`;
1168
+ updateJobRecord(tree.dir, {state: 'needs-attention', reason});
1169
+ await ledgerWrite.applyNeedsAttentionTransition({
1170
+ cwd: tree.dir,
1171
+ slug,
1172
+ reason,
1173
+ // Autonomous (`run`): surface on the arbiter's main AND push the work branch
1174
+ // (the seam does both now), so the fleet's failed-agent work is cross-machine
1175
+ // recoverable via requeue-continue. The route is fault-tolerant: a git outage
1176
+ // is retried with bounded backoff then gives up cleanly (never hangs the tick).
1177
+ arbiter: tree.arbiterRemote,
1178
+ env: ctx.gitEnv ?? ctx.env,
1179
+ backoff: ctx.backoff,
1180
+ sleep: ctx.sleep,
1181
+ });
1182
+ return {...base, status: failureCauseToItemStatus(cause), detail};
1183
+ }
1184
+
1185
+ /**
1186
+ * Map a {@link FailureCause} onto the `run` {@link ItemStatus}. The cause names
1187
+ * ARE the status names (the FAILURE-CAUSE axis reuses the terminal vocabulary), so
1188
+ * this is identity — a single helper documents the mapping + keeps the `do`/`run`
1189
+ * sites symmetric (`do` has the twin `failureCauseToDoOutcome`).
1190
+ */
1191
+ function failureCauseToItemStatus(cause: FailureCause): ItemStatus {
1192
+ return cause;
1193
+ }
1194
+
1195
+ /**
1196
+ * Build the HONEST {@link ItemResult} for a CONTINUE-site surface that did NOT
1197
+ * land on the arbiter (`{moved: false}`). The tree-less `in-progress/ →
1198
+ * needs-attention/` move lost the CAS race against a busy arbiter (its
1199
+ * contention-retry cap exhausted) or had no arbiter to publish to, so the item is
1200
+ * STILL in-progress on the arbiter — reporting a clean `needs-attention` would
1201
+ * mislead (it claims the surface landed, when it did not). The DISTINCT
1202
+ * `surface-unmoved` status carries `reasonNotMoved` so the caller/human can tell
1203
+ * it apart from a successful surface and retry/resolve.
1204
+ *
1205
+ * The local job record was set to `state: 'needs-attention'` (its LOCAL intent,
1206
+ * recorded regardless of the arbiter move) right before the surface; on a
1207
+ * `moved: false` we OVERWRITE its reason so that local record does not
1208
+ * confusingly claim a landed surface either.
1209
+ */
1210
+ function surfaceUnmovedItemResult(params: {
1211
+ base: ItemResult;
1212
+ tree: IsolatedTree;
1213
+ slug: string;
1214
+ reason: string;
1215
+ surfaced: SurfaceToNeedsAttentionResult;
1216
+ }): ItemResult {
1217
+ const {base, tree, slug, reason, surfaced} = params;
1218
+ const detail =
1219
+ `'${slug}' could NOT be surfaced to needs-attention — the surface did not ` +
1220
+ `reach the arbiter's main; the item is still IN-PROGRESS on the arbiter ` +
1221
+ `(retry/resolve). ${surfaced.reasonNotMoved ?? reason}`;
1222
+ updateJobRecord(tree.dir, {state: 'needs-attention', reason: detail});
1223
+ return {...base, status: 'surface-unmoved', detail};
1224
+ }
1225
+
1226
+ /**
1227
+ * Route a DELIBERATE agent STOP (task `agent-stop-signal`) to needs-attention
1228
+ * through the SAME work-preserving seam `saveAgentFailure` uses, but as the
1229
+ * DISTINCT `agent-stopped` status (NOT `agent-failed` — the agent did not error;
1230
+ * NOT `tests-failed`/`needs-attention` — there was no red gate / rebase conflict).
1231
+ * The agent's STOP reason is recorded VERBATIM; the job record names it honestly.
1232
+ * The `performIntegration` band (gate + Gate-2) is NEVER reached.
1233
+ */
1234
+ async function saveAgentStop(
1235
+ base: ItemResult,
1236
+ tree: IsolatedTree,
1237
+ slug: string,
1238
+ reason: string,
1239
+ ctx: OneItemContext,
1240
+ ): Promise<ItemResult> {
1241
+ updateJobRecord(tree.dir, {state: 'needs-attention', reason});
1242
+ await ledgerWrite.applyNeedsAttentionTransition({
1243
+ cwd: tree.dir,
1244
+ slug,
1245
+ reason,
1246
+ // Autonomous (`run`): surface on the arbiter's main AND push the work branch
1247
+ // (the seam does both), so the stopped item is cross-machine visible/recoverable.
1248
+ arbiter: tree.arbiterRemote,
1249
+ env: ctx.gitEnv ?? ctx.env,
1250
+ backoff: ctx.backoff,
1251
+ sleep: ctx.sleep,
1252
+ });
1253
+ return {...base, status: 'agent-stopped', detail: reason};
1254
+ }
1255
+
1256
+ /** A throwaway default workspace under the OS temp dir (CLI convenience). */
1257
+ export function defaultRunWorkspace(): string {
1258
+ return join(tmpdir(), 'dorfl-workspace');
1259
+ }
1260
+
1261
+ /**
1262
+ * One supervised TICK over the registry, as a swappable unit. Today the tick IS
1263
+ * `runOnce` (claim+build+integrate a concurrent batch of eligible TASKS). The
1264
+ * loop ({@link runLoop}) is deliberately written against this signature — NOT
1265
+ * against `runOnce` directly — so the advance-loop prd can later swap the tick
1266
+ * (build / task / triage / surface / apply) WITHOUT re-architecting the loop
1267
+ * (the forward-pointer: the loop owns concurrency/scheduling, the tick owns one
1268
+ * item's work). The loop never reaches inside a tick.
1269
+ */
1270
+ export type RunTick = (options: RunOnceOptions) => Promise<RunOnceResult>;
1271
+
1272
+ export interface RunLoopOptions extends RunOnceOptions {
1273
+ /**
1274
+ * Stop after this many ticks. Unset ⇒ no iteration bound (the forever daemon,
1275
+ * the future system service). A bounded session (an operator's stop discipline,
1276
+ * absorbing the retired `watch` verb) sets this and/or `maxDurationMs`.
1277
+ */
1278
+ maxIterations?: number;
1279
+ /**
1280
+ * Stop once this many ms of wall-clock have elapsed (checked before each tick
1281
+ * AND before each inter-tick sleep). Unset ⇒ no duration bound.
1282
+ */
1283
+ maxDurationMs?: number;
1284
+ /**
1285
+ * Pause between ticks (ms). Default 0 (back-to-back). The loop sleeps via the
1286
+ * injectable {@link RunLoopOptions.sleep} so tests need not wait real time.
1287
+ */
1288
+ intervalMs?: number;
1289
+ /** The tick to loop. Defaults to {@link runOnce}; tests/advance-loop inject. */
1290
+ tick?: RunTick;
1291
+ /** Clock seam (tests). Defaults to `Date.now`. */
1292
+ now?: () => number;
1293
+ /** Sleep seam (tests). Defaults to a real `setTimeout`. */
1294
+ sleep?: (ms: number) => Promise<void>;
1295
+ /**
1296
+ * Cooperative stop signal, polled before each tick. When it returns true the
1297
+ * loop ends cleanly after the current tick. The CLI wires this to a SIGINT/
1298
+ * SIGTERM flag so the long-running daemon shuts down gracefully; tests use it to
1299
+ * end an otherwise-forever loop deterministically.
1300
+ */
1301
+ stop?: () => boolean;
1302
+ /** Called once per completed tick with its result + 1-based index (CLI logging). */
1303
+ onTick?: (result: RunOnceResult, iteration: number) => void;
1304
+ }
1305
+
1306
+ /** Aggregate outcome of a {@link runLoop} session. */
1307
+ export interface RunLoopResult {
1308
+ /** Number of ticks actually executed. */
1309
+ iterations: number;
1310
+ /** Why the loop ended. */
1311
+ stoppedBy: 'max-iterations' | 'max-duration' | 'signal';
1312
+ /** Sum of `claimedAndDone` across all ticks. */
1313
+ claimedAndDone: number;
1314
+ /** Sum of `skipped` across all ticks. */
1315
+ skipped: number;
1316
+ /** Sum of `failed` across all ticks. */
1317
+ failed: number;
1318
+ /** Sum of `needsAttention` across all ticks. */
1319
+ needsAttention: number;
1320
+ /** Each tick's result, in order. */
1321
+ ticks: RunOnceResult[];
1322
+ }
1323
+
1324
+ const realSleep = (ms: number): Promise<void> =>
1325
+ new Promise((resolve) => setTimeout(resolve, ms));
1326
+
1327
+ /**
1328
+ * `run` (no flag) — the cross-repo, parallel, **forever-looping daemon** (ADR §3):
1329
+ * loop the supervised concurrent tick over the registry until a stop bound
1330
+ * (max-iterations / max-duration / a stop signal) or forever (the future system
1331
+ * service). Each tick claims + runs a CONCURRENT batch ({@link runOnce}’s
1332
+ * `maxParallel`/`perRepoMax` in-flight execution), integrates, then the loop
1333
+ * continues. This absorbs the retired `watch` verb’s bounded-session + surface-
1334
+ * failures behaviour: a stuck item (timeout / red gate / rebase conflict) is
1335
+ * routed through the EXISTING ledger needs-attention seam INSIDE the tick
1336
+ * (`runOneItem` → `applyNeedsAttentionTransition`, surfaced on `main`) — the loop
1337
+ * does NOT infinite-retry it and adds NO bespoke failure reporting; it just keeps
1338
+ * ticking. `run --once` is exactly ONE tick (call {@link runOnce} directly, the
1339
+ * debug/test affordance — NOT the CI path; CI is `do`).
1340
+ *
1341
+ * The loop owns ONLY scheduling (when/whether to tick); the TICK owns one batch’s
1342
+ * work — kept separable so advance-loop can swap the tick later (see {@link
1343
+ * RunTick}).
1344
+ */
1345
+ export async function runLoop(options: RunLoopOptions): Promise<RunLoopResult> {
1346
+ const tick = options.tick ?? runOnce;
1347
+ const now = options.now ?? Date.now;
1348
+ const sleep = options.sleep ?? realSleep;
1349
+ const intervalMs = options.intervalMs ?? 0;
1350
+ const start = now();
1351
+ const deadline =
1352
+ options.maxDurationMs !== undefined
1353
+ ? start + options.maxDurationMs
1354
+ : undefined;
1355
+
1356
+ // The per-tick options are the RunOnceOptions subset of RunLoopOptions; the
1357
+ // loop-only knobs (bounds / seams / callbacks) are not forwarded to the tick.
1358
+ const tickOptions: RunOnceOptions = {
1359
+ config: options.config,
1360
+ report: options.report,
1361
+ workspace: options.workspace,
1362
+ dorfl: options.dorfl,
1363
+ harness: options.harness,
1364
+ reviewGate: options.reviewGate,
1365
+ provider: options.provider,
1366
+ openPr: options.openPr,
1367
+ env: options.env,
1368
+ agentId: options.agentId,
1369
+ onWarn: options.onWarn,
1370
+ };
1371
+
1372
+ const ticks: RunOnceResult[] = [];
1373
+ let stoppedBy: RunLoopResult['stoppedBy'] = 'max-iterations';
1374
+
1375
+ // eslint-disable-next-line no-constant-condition
1376
+ while (true) {
1377
+ // Stop-condition checks BEFORE running a tick (so a zero-iteration / already-
1378
+ // expired / pre-signalled session does no work).
1379
+ if (options.stop?.()) {
1380
+ stoppedBy = 'signal';
1381
+ break;
1382
+ }
1383
+ if (
1384
+ options.maxIterations !== undefined &&
1385
+ ticks.length >= options.maxIterations
1386
+ ) {
1387
+ stoppedBy = 'max-iterations';
1388
+ break;
1389
+ }
1390
+ if (deadline !== undefined && now() >= deadline) {
1391
+ stoppedBy = 'max-duration';
1392
+ break;
1393
+ }
1394
+
1395
+ const result = await tick(tickOptions);
1396
+ ticks.push(result);
1397
+ options.onTick?.(result, ticks.length);
1398
+
1399
+ // Re-check the bounds AFTER the tick so we never sleep past a stop condition.
1400
+ if (
1401
+ options.maxIterations !== undefined &&
1402
+ ticks.length >= options.maxIterations
1403
+ ) {
1404
+ stoppedBy = 'max-iterations';
1405
+ break;
1406
+ }
1407
+ if (deadline !== undefined && now() >= deadline) {
1408
+ stoppedBy = 'max-duration';
1409
+ break;
1410
+ }
1411
+ if (options.stop?.()) {
1412
+ stoppedBy = 'signal';
1413
+ break;
1414
+ }
1415
+
1416
+ if (intervalMs > 0) {
1417
+ await sleep(intervalMs);
1418
+ }
1419
+ }
1420
+
1421
+ return {
1422
+ iterations: ticks.length,
1423
+ stoppedBy,
1424
+ claimedAndDone: ticks.reduce((n, t) => n + t.claimedAndDone, 0),
1425
+ skipped: ticks.reduce((n, t) => n + t.skipped, 0),
1426
+ failed: ticks.reduce((n, t) => n + t.failed, 0),
1427
+ needsAttention: ticks.reduce((n, t) => n + t.needsAttention, 0),
1428
+ ticks,
1429
+ };
1430
+ }