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
@@ -0,0 +1,1379 @@
1
+ import {existsSync, readdirSync, type Dirent} from 'node:fs';
2
+ import {basename, join} from 'node:path';
3
+ import {workItemPath, WORK_ROOT} from './work-layout.js';
4
+ import type {VerifyConfig} from './verify.js';
5
+ import type {ReviewGate} from './review-gate.js';
6
+ import {
7
+ performIntegration,
8
+ IntegrationNothingStaged,
9
+ synthesiseProposeTitle,
10
+ composeProposeBody,
11
+ PR_TITLE_MAX,
12
+ } from './integration-core.js';
13
+ import {ledgerWrite} from './ledger-write.js';
14
+ import {releaseItemLock} from './item-lock.js';
15
+ import {workBranchRef, parseWorkBranchRef} from './slug-namespace.js';
16
+ import type {ReviewProvider} from './integrator.js';
17
+ import type {IntegrationMode} from './config.js';
18
+ import {runAsync, localMainAheadCount, type RunResult} from './git.js';
19
+ import {hasUncommittedSourceChanges} from './agent-stop.js';
20
+ import {formatProposeNextStep, shouldUseColor} from './output.js';
21
+ import {
22
+ checkGatePreconditions,
23
+ detectLockfileOnDisk,
24
+ } from './gate-readiness.js';
25
+
26
+ // Re-export the propose-mode title/body helpers from their new home (the shared
27
+ // integration core) so existing importers (`test/propose-pr-body.test.ts`) keep
28
+ // working unchanged after the gate→integrate band moved out of this file.
29
+ export {synthesiseProposeTitle, composeProposeBody, PR_TITLE_MAX};
30
+
31
+ /**
32
+ * `dorfl complete [<slug>] [--skip-verify] [--type <t>] [--message <s>]
33
+ * [--arbiter <remote>]` — the human "finish this" command that runs the same
34
+ * back-half the autonomous runner runs: gate → mark done → commit → integrate.
35
+ *
36
+ * It runs on a `work/<slug>` branch (slug inferred from the branch if omitted).
37
+ * The build agent leaves its work UNCOMMITTED (it does no git), so `complete`
38
+ * `git add -A`s the agent's work AND the done-move into ONE atomic commit. This
39
+ * is the same finish/integration logic `run-once`/`watch` reuse; here it is
40
+ * human-driven and so allows a `--skip-verify` escape hatch the autonomous
41
+ * runner never uses (ADR §8).
42
+ *
43
+ * The item's SOURCE folder is normally `work/tasks/ready/` (the pool — claim
44
+ * leaves the body there, task
45
+ * `cutover-claim-body-stays-and-complete-sources-from-backlog`). A RUNNER-OWNED
46
+ * RECOVERY of a SPURIOUSLY-stuck item (env-polluted gate, transient flake,
47
+ * since-fixed cause) sources from the SAME pool location: the per-item-lock
48
+ * cutover (`cutover-needs-attention-becomes-lock-stuck-recovery-surface`)
49
+ * retired the `work/needs-attention/` folder — stuck is the lock `state: stuck`
50
+ * and the body stays in `tasks/ready/` while claimed, so a recovery `complete`
51
+ * is structurally a normal build. The gate is RE-RUN and authoritative (only a
52
+ * GREEN re-gate completes; a still-red item stays stuck on the lock).
53
+ * `--skip-verify` stays the only, human-only, loud override.
54
+ *
55
+ * The integration step is split by mode (config `integration`, ADR §6):
56
+ * merge — push the rebased branch to `<arbiter>/main`, then sync the LOCAL
57
+ * clone to that new main (the push is authoritative; the local sync
58
+ * is the ergonomic finish so the user ends on an up-to-date main).
59
+ * propose — push the `work/<slug>` branch (the safety-bearing step) and report
60
+ * the next step. Full provider-driven PR/MR creation lands with the
61
+ * integration seam; until then `complete` pushes + tells the human.
62
+ *
63
+ * In BOTH modes `complete` lands the human back on local `main` by default
64
+ * ("finish, ready for the next thing") — but the move differs because the work's
65
+ * location differs: `merge` switches to `main` AND fast-forwards it to the just-
66
+ * pushed `<arbiter>/main` (the work landed there); `propose` ONLY `git switch
67
+ * main` (arbiter main has not moved, so there is nothing to ff). `--no-switch`
68
+ * opts out in either mode (stay on `work/<slug>` to keep iterating).
69
+ *
70
+ * After landing on `main`, the LOCAL `work/<slug>` branch is deleted iff its work
71
+ * is provably on the arbiter (the SAME predicate as worktree deletion, ADR §4,
72
+ * mode-agnostic): its tip is an ancestor of `<arbiter>/main` (merged) OR
73
+ * `<arbiter>/work/<slug>` exists with its tip == the local tip (pushed & up-to-
74
+ * date). Otherwise the branch is KEPT (unmerged / unpushed / a diverged un-pushed
75
+ * amend = not safe). The REMOTE branch is NEVER deleted (a propose PR is built
76
+ * from it). `--no-switch` keeps the branch too.
77
+ *
78
+ * Before integrating, the work branch is rebased onto the latest `<arbiter>/main`
79
+ * (ADR §10): a clean rebase continues; a conflicting rebase is `--abort`ed and
80
+ * surfaced as needs-attention — `complete` NEVER auto-resolves. It NEVER
81
+ * `--force`es to main.
82
+ *
83
+ * Exit codes:
84
+ * 0 completed (done-move + commit + integrate succeeded)
85
+ * 1 usage/environment error, or a refusal (gate failed, nothing to commit,
86
+ * not on a work branch, rebase conflict needs the human)
87
+ *
88
+ * On the two FAILURE paths the human can't paper over — a red gate (without
89
+ * `--skip-verify`) and a rebase conflict (ADR §10) — `complete` no longer leaves
90
+ * the item dangling. Instead it routes the item through the shared
91
+ * `needs-attention` mechanism (ADR §12): record the reason on the per-item lock
92
+ * entry (`state: stuck`, post-cutover task
93
+ * `cutover-needs-attention-becomes-lock-stuck-recovery-surface`) so the stuck
94
+ * item is surfaced by `status` and returnable to `backlog/`. The success and
95
+ * `--skip-verify` paths are unchanged. The exit code stays 1 (the work did NOT
96
+ * complete); the outcome still names WHY (gate-failed / rebase-conflict) and
97
+ * `routedToNeedsAttention` records that the lock amend happened.
98
+ */
99
+
100
+ export type CompleteOutcome =
101
+ | 'completed' // gated, moved, committed, integrated
102
+ | 'already-integrated' // stranded-done auto-recover: tip already on <arbiter>/main → clean no-op (no re-push, no double-integrate)
103
+ | 'prepare-failed' // the env-prep step (prepare) was red — env not ready, verify not run
104
+ | 'gate-failed' // the acceptance gate was red (and not skipped)
105
+ | 'review-blocked' // Gate 2 (PR/code review) returned `block` (or exhausted rounds)
106
+ | 'review-unparseable' // Gate 2 ran but its verdict JSON was malformed/unparseable — work-preserving route, classified `transient-infra` (NOT a reviewer block)
107
+ | 'rebase-conflict' // rebase onto arbiter/main conflicted (aborted; human resolves)
108
+ | 'invariant-violation' // one-slug-one-folder would break (slug in two folders on the arbiter)
109
+ | 'strand-surfaced' // autonomous source-strand refusal (or empty-staged) surfaced to needs-attention on the arbiter (parity with run's never-strand-in-in-progress posture)
110
+ | 'surface-unmoved' // autonomous source-strand surface tried but did NOT land on the arbiter — the item is HONESTLY still in-progress (CAS contention / no arbiter); never a fake success
111
+ | 'refused' // nothing to complete (nothing to commit, wrong folder, …)
112
+ | 'usage-error'; // usage / environment problem
113
+
114
+ export interface CompleteOptions {
115
+ /** The slug to complete. If omitted, inferred from a `work/<slug>` branch. */
116
+ slug?: string;
117
+ /** The working clone/checkout the work branch lives in. */
118
+ cwd: string;
119
+ /** Name of the arbiter git remote. Defaults to `origin`. */
120
+ arbiter?: string;
121
+ /** Integration mode: `propose` (default) or `merge`. */
122
+ integration?: IntegrationMode;
123
+ /**
124
+ * **The explicit `--merge` override** for the untrusted-origin build-propose rule
125
+ * (task `untrusted-origin-forces-build-propose`). `true` iff the operator
126
+ * EXPLICITLY typed `--merge` on this invocation (vs `merge` resolved from
127
+ * config). Forwarded to {@link performIntegration}'s `explicitMerge`: an explicit
128
+ * `--merge` OVERRIDES the untrusted-origin build-propose rule (the operator is
129
+ * present; CLI always wins). Unset on the autonomous path ⇒ an untrusted-origin
130
+ * task reliably forces `propose`.
131
+ */
132
+ explicitMerge?: boolean;
133
+ /**
134
+ * Override the pre-flight DIVERGENCE guard (`--ignore-diverged-main`, mirroring
135
+ * `--ignore-not-ready`): proceed even when local `main` is ahead of
136
+ * `<arbiter>/main` (has unpushed commits). MERGE MODE ONLY (only merge mode ff's
137
+ * local `main`). When overridden and the divergence persists, the now-NON-FATAL
138
+ * {@link syncLocalMain} reports it honestly (work on the arbiter; local `main`
139
+ * left to rebase). Loud, never the default.
140
+ */
141
+ ignoreDivergedMain?: boolean;
142
+ /**
143
+ * Leave the human ON the `work/<slug>` branch (and KEEP it) in either mode,
144
+ * instead of switching back to `main` + deleting the provably-landed branch.
145
+ * For "I'll keep iterating on this branch" (e.g. addressing review feedback).
146
+ */
147
+ noSwitch?: boolean;
148
+ /** The declared per-repo ENV-PREP step (string | list), run ONCE before the
149
+ * first `verify` to make the env ready. Unset ⇒ a no-op (NO default install). */
150
+ prepare?: VerifyConfig;
151
+ /** The declared per-repo gate (string | list). Unset ⇒ the default command. */
152
+ verify?: VerifyConfig;
153
+ /**
154
+ * Run the acceptance gate (`prepare` then `verify`) against the REBASED tip
155
+ * in a CLEAN throwaway worktree (the tree that integrates) when `true` (the
156
+ * default), rather than the current checkout (the pre-rebase tree) when
157
+ * `false`. `complete` is a SINGLE-JOB path, so this is the resolved flag
158
+ * passed UNCONDITIONALLY (no `run`-fleet downgrade). Forwarded verbatim to
159
+ * {@link performIntegration}.
160
+ */
161
+ freshWorktreeGate?: boolean;
162
+ /**
163
+ * **The cross-job merge-serialiser CAS-retry cap** (config `mergeRetries`, prd
164
+ * `land-time-reverify-and-parallel-merge-ceiling` Story 5 / Applied Answer q1
165
+ * (a)). Threaded verbatim into {@link performIntegration} as `mergeRetries` so
166
+ * the resolved per-repo value controls how many re-rebase-and-retry attempts
167
+ * the merge-mode `${branch}:main` push makes before a contender bounces to
168
+ * needs-attention. Resolved ONCE per `performComplete` call — the same
169
+ * per-item resolution as `freshWorktreeGate` / `reviewMaxRounds`.
170
+ */
171
+ mergeRetries?: number;
172
+ /** Skip the acceptance gate (human-only escape hatch; never used unattended). */
173
+ skipVerify?: boolean;
174
+ /**
175
+ * **`--allow-backlog`** (prd
176
+ * `do-allow-backlog-drive-staged-tasks-without-promotion`): TREAT a
177
+ * `tasks/backlog/`-resident body (staging) as a VALID build source, so a
178
+ * staged task driven in place done-moves `tasks/backlog/ → tasks/done/`
179
+ * directly. GATED on the explicit flag: WITHOUT it, a body resting in
180
+ * `tasks/backlog/` is NOT a sanctioned completion position (a corrupt/wip
181
+ * strand) and `complete` still REFUSES honestly — only the deliberate drive
182
+ * promotes from staging. Default off ⇒ today's refusal-on-staging behaviour.
183
+ * Threaded only by the `do … --allow-backlog` task-build path; no autonomous
184
+ * caller sets it.
185
+ */
186
+ allowBacklog?: boolean;
187
+ /**
188
+ * **Gate 2 — the PR/code review gate** (GATES prd `work/prds/tasked/review.md`). When
189
+ * `true`, after the green `verify` and BEFORE the done-move, run the `review`
190
+ * SKILL as a FRESH-CONTEXT agent (its own harness launch) and route its verdict:
191
+ * `approve` → proceed to done-move/commit/integrate; `block` → route to
192
+ * needs-attention (NEVER merge). `verify` is the non-skippable floor; review is
193
+ * a JUDGEMENT gate ON TOP (ADR §8), never a replacement. Default OFF.
194
+ */
195
+ review?: boolean;
196
+ /**
197
+ * The model the REVIEW agent runs on (de-correlation from the builder). Flows
198
+ * to the review-agent launch through the EXISTING harness seam
199
+ * (`LaunchInput.model` / `substituteModel`). Unset ⇒ no forced review model.
200
+ */
201
+ reviewModel?: string;
202
+ /**
203
+ * Bound the revise↔review loop (Gate 2). On exhaustion the gate ERRORS OUT and
204
+ * forces needs-attention (never silently merges or loops). Defaults to 2.
205
+ */
206
+ reviewMaxRounds?: number;
207
+ /**
208
+ * The review-gate SEAM (injectable, like `do`'s `dorfl`): a fresh-context
209
+ * review that returns a parsed `{verdict, findings}`. Tests inject a canned
210
+ * verdict (no real model); production wires the harness-backed gate
211
+ * (`harnessReviewGate`). Required when `review` is on (a missing gate with
212
+ * `review` on is a usage error — the floor must not be silently skipped).
213
+ */
214
+ reviewGate?: ReviewGate;
215
+ /**
216
+ * `--watch`: tail the Gate-2 REVIEW agent's pi session `.jsonl` live, the SAME
217
+ * way `do --watch` tails the build agent's (task `watch-review-session`).
218
+ * Threaded into the `reviewGate` invocation below so the production gate
219
+ * (`harnessReviewGate`) routes its launch through the shared
220
+ * `launchWithOptionalWatch` helper. OFF (the default) ⇒ the review path is
221
+ * byte-for-byte unchanged (sync launch, no tailer). Observability only — it
222
+ * never changes the verdict/routing.
223
+ */
224
+ watch?: boolean;
225
+ /** Where the tailed review lines are written (defaults to stderr). */
226
+ watchSink?: (line: string) => void;
227
+ /**
228
+ * The HOST-ONLY sessions root the review session FILE is generated under
229
+ * (resolved `config.sessionsDir`). Threaded to the review-agent launch so its
230
+ * (distinct `<slug>-review`) session lands under the same root as the build's.
231
+ */
232
+ sessionsDir?: string;
233
+ /** Conventional-commit type for the completion commit. Defaults to `feat`. */
234
+ type?: string;
235
+ /** Commit summary. Defaults to the task `title` minus a leading `slug — `. */
236
+ message?: string;
237
+ /**
238
+ * Optional review-request BODY (propose mode) — the PR/MR DESCRIPTION, DISTINCT
239
+ * from {@link message} (which is the COMMIT summary). Advisory prose that gates
240
+ * nothing. The autonomous `do` path passes the build agent's final summary
241
+ * (`LaunchResult.output`); a human `complete --propose` may pass one via a NEW
242
+ * `--body` flag (NOT `--message`). The runner scaffolds a deterministic header
243
+ * (a pointer to `work/done/<slug>.md`) above it. Absent ⇒ today's `gh pr create
244
+ * --fill` empty/commit-derived body (no regression). Ignored in `merge` mode.
245
+ */
246
+ body?: string;
247
+ /** Optional injectable PR opener (e.g. `gh pr create`); used in `propose` mode. */
248
+ openPr?: (opts: {
249
+ cwd: string;
250
+ branch: string;
251
+ env?: NodeJS.ProcessEnv;
252
+ }) => void;
253
+ /**
254
+ * Optional FULLY-FORMED review provider INSTANCE used VERBATIM (the SAME seam
255
+ * `run` exposes via `RunOptions.provider`; forwarded to `performIntegration` as
256
+ * `providerInstance`). Tests/embeddings inject a stubbed `GitHubProvider` (a
257
+ * custom `gh` path) to drive the full propose pipeline OFFLINE without a real
258
+ * GitHub arbiter. This is the resolved provider OBJECT — NOT a config override
259
+ * (there is none; the provider is purely arbiter-derived). Unset ⇒ the core
260
+ * selects the provider from the arbiter URL as normal.
261
+ */
262
+ providerInstance?: ReviewProvider;
263
+ /**
264
+ * **The PR-INTENT axis** (config `noPR`, ADR §6). When `true` on the propose
265
+ * path, push the branch but SKIP the review request (the explicit suppress-PR
266
+ * intent, re-homing the old `provider: none` use). NOT a provider choice — the
267
+ * provider is purely arbiter-derived. Threaded verbatim into
268
+ * {@link performIntegration}. Ignored in `merge` mode and when `openPr` is
269
+ * injected. Unset/false ⇒ propose opens the PR via the arbiter-derived provider.
270
+ */
271
+ noPR?: boolean;
272
+ /**
273
+ * Surface a needs-attention bounce ON THE ARBITER (the AUTONOMOUS variant of
274
+ * the failure path). When set, the two FAILURE routings (`gate-failed` /
275
+ * `rebase-conflict`) pass this arbiter remote into the ledger write seam's
276
+ * `applyNeedsAttentionTransition`, which (mode M) cherry-picks the move-only
277
+ * commit onto the arbiter's `main` — making the stuck state observable to
278
+ * `scan`/`status`/another machine. This is exactly what `run`'s `runOneItem`
279
+ * does (it passes `arbiter: job.arbiterRemote`).
280
+ *
281
+ * Unset (the default) keeps `complete`'s HUMAN behaviour: route LOCALLY only
282
+ * (no on-`main` surfacing) — a human is right there, so no cross-machine
283
+ * surfacing is needed. The unattended `do` worker (the CI command) sets this
284
+ * so a stuck CI run is NOT invisible.
285
+ */
286
+ surfaceArbiter?: string;
287
+ /** Environment for child git/provider processes (the identity-scoped env). */
288
+ env?: NodeJS.ProcessEnv;
289
+ /**
290
+ * Environment for the REVIEW-AGENT launch (Gate 2) — the AMBIENT env, never the
291
+ * identity-scoped {@link env} (an agent must not act as the bot). Threaded to
292
+ * {@link performIntegration}'s `agentEnv`. Unset ⇒ falls back to {@link env}
293
+ * (byte-for-byte unchanged for non-identity callers, e.g. the human `complete`).
294
+ */
295
+ agentEnv?: NodeJS.ProcessEnv;
296
+ /** Sink for human-readable progress notes. */
297
+ note?: (message: string) => void;
298
+ /**
299
+ * Sink for a pre-formatted block printed VERBATIM (no `>> ` prefix), used for
300
+ * the visually-distinct propose-mode next-step block so its blank lines and
301
+ * heading stand out cleanly. Defaults to `note` when not supplied.
302
+ */
303
+ noteBlock?: (message: string) => void;
304
+ /**
305
+ * Emit ANSI color in the (cosmetic) propose-mode next-step block. Defaults
306
+ * to the TTY/`NO_COLOR` rule against `process.stdout`; injectable so tests
307
+ * can simulate a TTY (color) vs. a pipe / `NO_COLOR` (plain) without a real
308
+ * terminal. Color-only — it changes no gate/done-move/commit/integrate logic.
309
+ */
310
+ color?: boolean;
311
+ }
312
+
313
+ export interface CompleteResult {
314
+ exitCode: 0 | 1;
315
+ outcome: CompleteOutcome;
316
+ /**
317
+ * True iff a FAILURE outcome (gate-failed / rebase-conflict) was routed to
318
+ * `work/needs-attention/` via the shared mechanism (ADR §12), rather than left
319
+ * dangling. Undefined/false on the success, `--skip-verify`, refused, and
320
+ * usage-error paths (none of which move the item to needs-attention).
321
+ */
322
+ routedToNeedsAttention?: boolean;
323
+ /** The work branch that was completed, when one was resolved. */
324
+ branch?: string;
325
+ /** The completion commit message that was authored, on success. */
326
+ commitMessage?: string;
327
+ /** True when the work landed on the arbiter's `main` (merge mode). */
328
+ mergedToMain?: boolean;
329
+ /**
330
+ * Merge mode only: whether the LOCAL `main` was fast-forwarded to the
331
+ * just-pushed `<arbiter>/main` (the ergonomic courtesy AFTER the authoritative
332
+ * push). `true` on the normal ff path; `false` when local `main` had DIVERGED
333
+ * and the ff could not apply — a NON-FATAL skip (the arbiter push already
334
+ * defined success, so `outcome` stays `completed` / exit 0; the operator is told
335
+ * to `git rebase` to sync). Undefined outside merge mode / `--no-switch`.
336
+ */
337
+ localMainSynced?: boolean;
338
+ /** The branch HEAD ended on after completing (`main`, or the work branch). */
339
+ switchedTo?: string;
340
+ /** True iff the local `work/<slug>` branch was deleted (provably on arbiter). */
341
+ deletedLocalBranch?: boolean;
342
+ /**
343
+ * The review-request URL (e.g. a GitHub PR) opened in `propose` mode, when a
344
+ * provider opened one and reported it (ADR §6). Absent in merge mode and on
345
+ * the push-only / degraded path.
346
+ */
347
+ prUrl?: string;
348
+ /** Human-readable summary of the terminal condition. */
349
+ message: string;
350
+ }
351
+
352
+ const DEFAULT_ARBITER = 'origin';
353
+ const DEFAULT_INTEGRATION: IntegrationMode = 'propose';
354
+
355
+ /**
356
+ * Resolve the integration mode the human asked for ON THIS invocation from the
357
+ * mutually-exclusive `--merge` / `--propose` flags. Returns the explicit mode,
358
+ * or `undefined` when neither flag was given (per-repo > global > default then
359
+ * decides). Throws when BOTH are given (they are mutually exclusive). This is
360
+ * the TOP of the complete-time precedence chain (flag > per-repo > global >
361
+ * default); the autonomous runner uses no flag and so resolves the same
362
+ * underlying order.
363
+ */
364
+ export function integrationFromFlags(flags: {
365
+ merge?: boolean;
366
+ propose?: boolean;
367
+ }): IntegrationMode | undefined {
368
+ if (flags.merge && flags.propose) {
369
+ throw new Error(
370
+ '--merge and --propose are mutually exclusive; pass at most one.',
371
+ );
372
+ }
373
+ if (flags.merge) {
374
+ return 'merge';
375
+ }
376
+ if (flags.propose) {
377
+ return 'propose';
378
+ }
379
+ return undefined;
380
+ }
381
+
382
+ /** Raised for usage/environment errors (exit 1, outcome 'usage-error'). */
383
+ class CompleteUsageError extends Error {}
384
+
385
+ /**
386
+ * The CAUSE-CLASS of a {@link CompleteRefusal}. `source-strand` is the SLUG-IS-
387
+ * STUCK class (no in-progress/needs-attention/done file on the branch tree) — on
388
+ * the AUTONOMOUS path (`surfaceArbiter` set) it surfaces to needs-attention via
389
+ * the tree-less seam so the next tick does not re-claim-and-recrash forever.
390
+ * `diverged-main` is an ENV/OPERATOR condition (local main ahead of
391
+ * `<arbiter>/main`) — NOT a stuck task, so it is NEVER bounced even on the
392
+ * autonomous path; it stays the local `refused`.
393
+ */
394
+ type RefusalKind = 'source-strand' | 'diverged-main' | 'gate-unrunnable';
395
+
396
+ /** Raised for a deliberate REFUSAL (exit 1, outcome 'refused'). */
397
+ class CompleteRefusal extends Error {
398
+ constructor(
399
+ message: string,
400
+ readonly kind: RefusalKind,
401
+ readonly slug?: string,
402
+ ) {
403
+ super(message);
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Run the complete ritual. Never throws for the expected gate-failed /
409
+ * rebase-conflict / refused cases — those are returned with exit 1 and a
410
+ * specific outcome. Usage/environment problems also surface as exit 1.
411
+ */
412
+ export async function performComplete(
413
+ options: CompleteOptions,
414
+ ): Promise<CompleteResult> {
415
+ const note = options.note ?? (() => {});
416
+ try {
417
+ return await runComplete(options, note);
418
+ } catch (err) {
419
+ // AUTONOMOUS-STRAND SURFACE (task
420
+ // `autonomous-integration-refusal-surfaces-not-strands-in-progress`, prd
421
+ // `ledger-integrity` story 7). On the AUTONOMOUS path (`surfaceArbiter` set
422
+ // — the human-vs-autonomous gate the core's other failures use), a SOURCE-
423
+ // STRAND `CompleteRefusal` (the slug-is-stuck `nothing to complete`) and the
424
+ // core's empty-commit `IntegrationNothingStaged` would otherwise return a
425
+ // bare `refused` and silently strand the item in `work/in-progress/` on the
426
+ // arbiter — so the next autonomous tick re-claims it and re-crashes the
427
+ // SAME way forever. Mirror `run`'s posture (an autonomous failure NEVER
428
+ // silently strands the item in in-progress/) by surfacing it to
429
+ // `needs-attention/` via the TREE-LESS arbiter-truth seam
430
+ // (`applyTreelessNeedsAttentionTransition`, the same mechanism `requeue`
431
+ // uses in reverse): there is no working-tree `.md` to `git mv` here (the
432
+ // strand IS that the source file is missing on the branch), so the
433
+ // cwd-bound `applyNeedsAttentionTransition` cannot be used — the tree-less
434
+ // seam resolves the source folder on the arbiter and CAS-publishes the
435
+ // move-only `.md` relocation. The DIVERGED-MAIN refusal is an env/operator
436
+ // condition (NOT a stuck task), so it is excluded — leave it `refused`.
437
+ // When the tree-less surface cannot land (CAS contention exhausted, or no
438
+ // arbiter), report the HONEST still-in-progress signal
439
+ // (`outcome: 'surface-unmoved'`) the gate-fail path's `moved:false` mirrors
440
+ // in `do.ts`/`run.ts` — never a fake success.
441
+ const strandSlug = strandRefusalSlug(err);
442
+ if (strandSlug && options.surfaceArbiter) {
443
+ return await surfaceAutonomousStrand({
444
+ cwd: options.cwd,
445
+ slug: strandSlug,
446
+ reason: (err as Error).message,
447
+ arbiter: options.surfaceArbiter,
448
+ env: options.env,
449
+ note,
450
+ });
451
+ }
452
+ if (
453
+ err instanceof CompleteRefusal ||
454
+ err instanceof IntegrationNothingStaged
455
+ ) {
456
+ // `IntegrationNothingStaged` is the core's empty-commit refusal — the same
457
+ // `refused` outcome the inline band raised before the extraction. The
458
+ // diverged-main `CompleteRefusal` (env/operator condition) also lands here
459
+ // on the autonomous path, deliberately UNCHANGED: it is NOT a stuck task.
460
+ return {exitCode: 1, outcome: 'refused', message: err.message};
461
+ }
462
+ if (err instanceof CompleteUsageError) {
463
+ return {exitCode: 1, outcome: 'usage-error', message: err.message};
464
+ }
465
+ const message = err instanceof Error ? err.message : String(err);
466
+ return {exitCode: 1, outcome: 'usage-error', message};
467
+ }
468
+ }
469
+
470
+ /**
471
+ * The SLUG of an autonomous-bounceable strand refusal, or `undefined` when the
472
+ * caught error is NOT a strand class (a `diverged-main` refusal, a usage error,
473
+ * a usual throw). The two strand classes — a `source-strand` {@link
474
+ * CompleteRefusal} and the core's {@link IntegrationNothingStaged} — BOTH leave
475
+ * the slug stuck in `work/in-progress/` on the arbiter (the claim landed and
476
+ * nothing has moved it since), so the autonomous bounce-set is exactly those
477
+ * two. The `diverged-main` refusal is deliberately EXCLUDED: it is an
478
+ * env/operator condition (local main ahead of `<arbiter>/main`), not a stuck
479
+ * task; bouncing it would mis-attribute an env problem to the work.
480
+ */
481
+ function strandRefusalSlug(err: unknown): string | undefined {
482
+ if (err instanceof CompleteRefusal && err.kind === 'source-strand') {
483
+ return err.slug;
484
+ }
485
+ if (err instanceof IntegrationNothingStaged) {
486
+ return err.slug;
487
+ }
488
+ return undefined;
489
+ }
490
+
491
+ /**
492
+ * Surface an autonomous-path strand refusal to `needs-attention/` on the
493
+ * arbiter via the TREE-LESS seam ({@link
494
+ * ledgerWrite.applyTreelessNeedsAttentionTransition}). The mechanism is the
495
+ * one `requeue`/`continue` already use in reverse: fetch `<arbiter>/main`,
496
+ * resolve the slug's actual current folder ON THE ARBITER (arbiter-is-truth),
497
+ * build the one-file `work/<src>/<slug>.md → work/needs-attention/<slug>.md`
498
+ * move on a scratch index, and CAS-publish it. It NEVER touches the caller's
499
+ * working tree / HEAD / index — important here because the refusal was raised
500
+ * precisely because the working tree does NOT hold an `.md` to `git mv`. It is
501
+ * idempotent (a re-surface of an already-surfaced slug is a no-op) and reports
502
+ * `moved: false` honestly when it cannot land (CAS contention exhausted, or no
503
+ * arbiter). The HONEST still-in-progress signal is then `outcome:
504
+ * 'surface-unmoved'` — the SAME vocabulary `do`/`run`/`start` expose for the
505
+ * after-commit tree-less surface that did not land; never a fake success.
506
+ */
507
+ async function surfaceAutonomousStrand(params: {
508
+ cwd: string;
509
+ slug: string;
510
+ reason: string;
511
+ arbiter: string;
512
+ env: NodeJS.ProcessEnv | undefined;
513
+ note: (m: string) => void;
514
+ }): Promise<CompleteResult> {
515
+ const {cwd, slug, reason, arbiter, env, note} = params;
516
+ const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
517
+ cwd,
518
+ slug,
519
+ reason,
520
+ arbiter,
521
+ env,
522
+ note,
523
+ });
524
+ if (surfaced.moved) {
525
+ const message =
526
+ `'${slug}' refused (${reason}); surfaced to work/needs-attention/ on ` +
527
+ `${arbiter}/main so the next autonomous tick does NOT re-claim it.`;
528
+ note(message);
529
+ return {
530
+ exitCode: 1,
531
+ outcome: 'strand-surfaced',
532
+ routedToNeedsAttention: true,
533
+ message,
534
+ };
535
+ }
536
+ const message =
537
+ `'${slug}' refused (${reason}); the tree-less surface to needs-attention ` +
538
+ `did NOT reach ${arbiter}/main — the item is HONESTLY still IN-PROGRESS on ` +
539
+ `the arbiter (retry/resolve). ${surfaced.reasonNotMoved ?? ''}`.trim();
540
+ note(message);
541
+ return {
542
+ exitCode: 1,
543
+ outcome: 'surface-unmoved',
544
+ routedToNeedsAttention: false,
545
+ message,
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Locate the slug's ledger record at a RENAMED done-position on the branch tree, to
551
+ * survive a SELF-RENAMING FOLDER task (the `folder-taxonomy-reorg-and-rename`
552
+ * migration).
553
+ *
554
+ * The trap: a task whose job is to `git mv` the ledger folders themselves (e.g.
555
+ * `done/ -> tasks/done/`) runs through the runner's INSTALLED (pre-rename) binary,
556
+ * whose compiled-in {@link workItemPath} still resolves `done` to `work/done/`.
557
+ * When the agent has placed its OWN record at the NEW done-position
558
+ * (`work/tasks/done/<slug>.md`) as part of the migration, the binary-known
559
+ * `work/done/<slug>.md` does not exist, and the resolver below would crash with
560
+ * `nothing to complete`, reaping the build.
561
+ *
562
+ * This scan is the layout-agnostic backstop: when the record is at NONE of the
563
+ * binary-known ledger folders, walk `work/` and return true iff `<slug>.md` exists
564
+ * directly inside a folder whose LEAF name is `done` (covering BOTH `work/done/`
565
+ * and a renamed `work/tasks/done/`). It deliberately matches ONLY a `done` leaf:
566
+ * a record left in a renamed POOL (`tasks/ready/`) is NOT a finished task and must
567
+ * still refuse, not be mis-integrated as done. When this fires, the slug is treated
568
+ * as already-done-moved by the agent into its terminal position, so the runner
569
+ * SKIPS its own `git mv` (the existing `source: 'done'` / stranded-done path) and
570
+ * just integrates the agent's tree as-is — no binary-vs-branch folder-name
571
+ * reconciliation is needed.
572
+ */
573
+ function recordAtRenamedDonePosition(cwd: string, slug: string): boolean {
574
+ const file = `${slug}.md`;
575
+ const workRoot = join(cwd, WORK_ROOT);
576
+ const found: string[] = [];
577
+ const walk = (dir: string): void => {
578
+ let entries: Dirent[];
579
+ try {
580
+ entries = readdirSync(dir, {withFileTypes: true});
581
+ } catch {
582
+ return;
583
+ }
584
+ for (const entry of entries) {
585
+ const full = join(dir, entry.name);
586
+ if (entry.isDirectory()) {
587
+ walk(full);
588
+ } else if (entry.name === file && basename(dir) === 'done') {
589
+ found.push(full);
590
+ }
591
+ }
592
+ };
593
+ walk(workRoot);
594
+ return found.length > 0;
595
+ }
596
+
597
+ async function runComplete(
598
+ options: CompleteOptions,
599
+ note: (m: string) => void,
600
+ ): Promise<CompleteResult> {
601
+ const cwd = options.cwd;
602
+ const env = options.env;
603
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
604
+ // The integration mode. The shared core integrates a `merge` automatically on a
605
+ // green gate (and an `approve` when review is on) and leaves a `propose` to a
606
+ // human; there is no downgrade, so the mode it resolves always equals this one.
607
+ // The tail below still reads the resolved mode from `core.integration.mode`.
608
+ const requestedMode = options.integration ?? DEFAULT_INTEGRATION;
609
+
610
+ if ((await gitSoft(['rev-parse', '--git-dir'], cwd, env)).status !== 0) {
611
+ throw new CompleteUsageError('not inside a git repository');
612
+ }
613
+ if ((await gitSoft(['remote', 'get-url', arbiter], cwd, env)).status !== 0) {
614
+ throw new CompleteUsageError(
615
+ `no git remote named '${arbiter}' (set one, or pass --arbiter)`,
616
+ );
617
+ }
618
+
619
+ // Resolve the slug + work branch. `complete` runs ON the work branch (it
620
+ // rebases + pushes it), and the branch carries the namespaced `work/<type>-
621
+ // <slug>` identity. So PREFER the branch HEAD is on (recovering BOTH the type
622
+ // and the slug from it); only when an explicit slug is given AND HEAD is not on
623
+ // a work branch do we synthesise the task-namespaced branch (`complete` is a
624
+ // task command).
625
+ const headBranch = await currentBranch(cwd, env);
626
+ const headParsed = parseWorkBranchRef(headBranch);
627
+ const slug = options.slug || headParsed?.slug || '';
628
+ if (!slug) {
629
+ throw new CompleteUsageError(
630
+ 'missing <slug> and the current branch is not a work/<type>-<slug> ' +
631
+ 'branch. usage: dorfl complete [<slug>] [--skip-verify] ' +
632
+ '[--type t] [--message s] [--arbiter remote]',
633
+ );
634
+ }
635
+ // The branch HEAD is on when it IS a work branch for this slug (so an explicit
636
+ // `prd:`-style recovery still completes the branch it is standing on); else
637
+ // synthesise the task branch.
638
+ const branch =
639
+ headParsed && headParsed.slug === slug
640
+ ? headBranch
641
+ : workBranchRef('task', slug);
642
+
643
+ // We must be ON the work branch — that is where the agent's work lives and
644
+ // what we rebase + push. (Unlike `start`, `complete` mutates the checkout.)
645
+ const head = headBranch;
646
+ if (head !== branch) {
647
+ throw new CompleteUsageError(
648
+ `not on ${branch} (HEAD is '${head}'). ` +
649
+ `Check out the work branch before completing '${slug}'.`,
650
+ );
651
+ }
652
+
653
+ // The task is normally in `tasks/ready/` (claim leaves the body there — task
654
+ // `cutover-claim-body-stays-and-complete-sources-from-backlog`). The legacy
655
+ // runner-owned recovery path used to also probe `work/needs-attention/<slug>.md`,
656
+ // but the per-item-lock cutover (task
657
+ // `cutover-needs-attention-becomes-lock-stuck-recovery-surface`) retired that
658
+ // folder: a stuck item is now the lock `state: stuck`, the body stays in
659
+ // `tasks/ready/` while claimed, and a recovery `complete` sources from there
660
+ // like any other build. The `needs-attention/` folder probe + the `recovering`
661
+ // re-gate branch are GONE (this task,
662
+ // `finish-needs-attention-folder-cutover-remove-legacy-recovery-readers`):
663
+ // nothing writes that folder anymore so the arm could not fire.
664
+ //
665
+ // This LOCAL source resolution is now only a PRE-FLIGHT (which folder the
666
+ // checkout holds) and the content FALLBACK: the integration core's done-move
667
+ // RESOLVES THE ACTUAL SOURCE FOLDER FROM THE ARBITER (arbiter-is-truth) and
668
+ // removes it from THERE, so the move can never disagree with what the arbiter
669
+ // holds even when the local tree diverges (ledger-integrity defect 1). The
670
+ // `source` we pass is the arbiter-content fallback for the degenerate "arbiter
671
+ // holds nothing" case, not the authority. `in-progress/` is RETAINED below for
672
+ // the legacy/bounce surfaces that may still source from it until its folder
673
+ // removal (9c, OUT of this task's scope).
674
+ const backlog = workItemPath(cwd, 'tasks-ready', slug);
675
+ const staged = workItemPath(cwd, 'tasks-backlog', slug);
676
+ const inProgress = workItemPath(cwd, 'in-progress', slug);
677
+ const done = workItemPath(cwd, 'done', slug);
678
+ const onBacklog = existsSync(backlog);
679
+ // `--allow-backlog` drive (prd
680
+ // `do-allow-backlog-drive-staged-tasks-without-promotion`): a staged task driven
681
+ // in place RESTS in `tasks/backlog/` (claim never moved it). Detect it as a build
682
+ // source so the done-move goes `tasks/backlog/ → tasks/done/` DIRECTLY (the
683
+ // human's explicit drive IS the promotion; it never bounces through the pool).
684
+ // LOWER priority than `tasks/ready/` so a same-slug pool copy wins the resolution.
685
+ // GATED on the explicit flag: WITHOUT it, a body in staging is a corrupt/wip
686
+ // strand `complete` must REFUSE (the honest "not finished, not done" position),
687
+ // NOT silently integrate — only the deliberate drive promotes from staging.
688
+ const onPreBacklog = options.allowBacklog === true && existsSync(staged);
689
+ const onInProgress = existsSync(inProgress);
690
+ // SELF-RENAMING FOLDER task backstop: the binary-known `work/done/<slug>.md`,
691
+ // OR — when none of the binary-known ledger folders hold the record — the slug
692
+ // at a RENAMED done-position the migration agent placed it in (e.g.
693
+ // `work/tasks/done/<slug>.md`). See {@link recordAtRenamedDonePosition}. The
694
+ // scan runs ONLY in the all-binary-folders-empty case, so a normal task's
695
+ // resolution is byte-for-byte unchanged.
696
+ const onDone =
697
+ existsSync(done) ||
698
+ (!onBacklog &&
699
+ !onPreBacklog &&
700
+ !onInProgress &&
701
+ recordAtRenamedDonePosition(cwd, slug));
702
+ // STRANDED-DONE AUTO-RECOVER (prd `ledger-integrity` story 6, the autonomous
703
+ // half of `finish-already-committed-branch`). When neither in-progress/ nor
704
+ // needs-attention/ holds the slug on the BRANCH tree BUT done/ does, the work
705
+ // branch was already built + done-moved + committed by a prior run that never
706
+ // landed on the arbiter (a terminal push failed, or the PR never merged). The
707
+ // autonomous `do`/`advance`/plain-`complete` path used to refuse this with
708
+ // `nothing to complete`; instead route into the SHARED recover tail (the SAME
709
+ // `committedRecovery: true` path `complete --isolated` drives), reusing the
710
+ // existing capability. Detection here is the FRONT-GATE only — folder shape;
711
+ // `recoverAlreadyCommitted` owns the unspoofable tip-vs-arbiter `isAncestor`
712
+ // decision (one reachability check, not two), so a tip ALREADY on
713
+ // `<arbiter>/main` returns `already-integrated` (clean no-op, NEVER a
714
+ // re-push/double-integrate). Mutually exclusive with the build-path source +
715
+ // `recovering` flags — the core ignores them when `committedRecovery` is set.
716
+ const folderShapeStranded =
717
+ !onBacklog && !onPreBacklog && !onInProgress && onDone;
718
+ // DIRTY-CONTINUE GATE (task `recover-autodetect-gated-on-nothing-to-commit`).
719
+ // The folder-shape stranded-done auto-detect is necessary but NOT sufficient on
720
+ // a CONTINUE: a requeued task whose prior attempt already done-moved the slug
721
+ // into `done/` on the kept branch looks IDENTICAL by folder shape to a genuine
722
+ // finished strand, even when THIS run's agent produced NEW uncommitted edits.
723
+ // The recover path skips the build/commit/done-move steps and only rebases the
724
+ // ALREADY-committed kept tip, so firing the recover on a dirty continue would
725
+ // SILENTLY DISCARD the new work. Disambiguate on the WORKING-TREE-DIRTY check
726
+ // (the porcelain half of `isWorkBranchDiffEmpty`, extracted as
727
+ // `hasUncommittedSourceChanges`). NOT the core's `nothingStaged`: that is
728
+ // INDEX-only and reads empty BEFORE the core's later `git add -A`, so it would
729
+ // miss the agent's UNSTAGED edits. NOT the FULL `isWorkBranchDiffEmpty`: its
730
+ // commits-ahead half is true for a GENUINE STRAND too (the kept tip carries
731
+ // source commits ahead of main), so it would WRONGLY BLOCK the legitimate
732
+ // recover and break the task-1 / finished-strand behaviour.
733
+ const dirtyContinue =
734
+ folderShapeStranded && (await hasUncommittedSourceChanges({cwd, env}));
735
+ const committedRecovery = folderShapeStranded && !dirtyContinue;
736
+ // CONTINUE-BUILD (task `complete-builds-on-already-done-moved-continue`,
737
+ // scope option A — the explicit `source: 'done'` contract). On a DIRTY
738
+ // continue whose kept branch already holds the slug in `work/done/`, route
739
+ // through the build path's NEW continue-build state: the integration core
740
+ // SKIPS the step-2 `git mv` (the slug is already in done/) and is exempted
741
+ // from the originTrust read + the divergent-done-move reconcile (there is no
742
+ // first-time move on this commit), but still runs prepare → gate → `git add
743
+ // -A` → commit → rebase → integrate on the NEW work. Replaces the blocker
744
+ // task's needs-attention BOUNCE (the dirty continue now AUTO-LANDS instead
745
+ // of surfacing). See `docs/adr/continue-build-already-done-moved.md`.
746
+ // Default branch (post `needs-attention/`-folder cutover, this task): when
747
+ // NONE of the recognised source folders holds the slug, the build cannot
748
+ // proceed — refuse via the `existsSync(sourcePath)` check below (the explicit
749
+ // "nothing to complete" CompleteRefusal). We default `source` to `'tasks-ready'`
750
+ // so `sourcePath` resolves to the canonical pool path the refusal message
751
+ // names; the refusal fires because that file does not exist on disk.
752
+ const source: 'tasks-ready' | 'tasks-backlog' | 'in-progress' | 'done' =
753
+ dirtyContinue
754
+ ? 'done'
755
+ : onBacklog
756
+ ? 'tasks-ready'
757
+ : onInProgress
758
+ ? 'in-progress'
759
+ : onPreBacklog
760
+ ? 'tasks-backlog'
761
+ : 'tasks-ready';
762
+ const sourcePath =
763
+ source === 'tasks-ready'
764
+ ? backlog
765
+ : source === 'tasks-backlog'
766
+ ? staged
767
+ : source === 'in-progress'
768
+ ? inProgress
769
+ : done;
770
+ if (dirtyContinue) {
771
+ // Announce LOUDLY (parallel to the `committedRecovery` recovery note above):
772
+ // the autonomous integrate path took the CONTINUE-BUILD branch, not the
773
+ // clean-strand recover and not a refusal — the CI/job log must record that
774
+ // the new work was built + integrated on top of the kept already-done-moved
775
+ // tip (no second `git mv`).
776
+ note(
777
+ `>> continue-build on '${slug}': the task is already in work/done/ on the ` +
778
+ 'kept branch (a prior attempt done-moved it); building + integrating the ' +
779
+ 'new uncommitted work on top of the kept tip (no second git mv).',
780
+ );
781
+ }
782
+ // The presence check guards the genuine-strand case (no record anywhere). It is
783
+ // DELIBERATELY skipped for `source === 'done'` (the continue-build / stranded-done
784
+ // state): there the record is ALREADY in its terminal done-position — presence
785
+ // was established by `folderShapeStranded`/`onDone`, which is LAYOUT-AWARE (it
786
+ // accepts a renamed `work/tasks/done/<slug>.md` via `recordAtRenamedDonePosition`).
787
+ // The binary's `sourcePath` (= `done` = `work/done/<slug>.md`) would NOT exist for
788
+ // a self-renaming-folder task, so re-checking it here would wrongly refuse a
789
+ // build the done-detection already accepted. `source: 'done'` skips the `git mv`
790
+ // anyway, so there is nothing this check needs to protect for that branch.
791
+ if (!committedRecovery && source !== 'done' && !existsSync(sourcePath)) {
792
+ throw new CompleteRefusal(
793
+ `work/tasks/ready/${slug}.md not found — ` +
794
+ 'nothing to complete (already done, or wrong slug?).',
795
+ 'source-strand',
796
+ slug,
797
+ );
798
+ }
799
+ // `source: 'done'` is the CONTINUE-BUILD state — NOT a recover (the agent
800
+ // produced new work this run) and NOT the clean-strand recover
801
+ // (`committedRecovery` is mutually exclusive with `dirtyContinue` by
802
+ // construction above). It falls through to `performIntegration` like a
803
+ // normal build, where the core skips the step-2 `git mv` for this source.
804
+ // Post `needs-attention/`-folder cutover (task
805
+ // `finish-needs-attention-folder-cutover-remove-legacy-recovery-readers`):
806
+ // `source === 'needs-attention'` is no longer reachable here (the folder is
807
+ // retired; a stuck item is the lock `state: stuck` and the body rests in
808
+ // `tasks/ready/`). A recovery `complete` of a stuck item now sources from
809
+ // `tasks/ready/` like a normal build, so `recovering` is always `false`. The
810
+ // integration core still accepts the flag (vestigial, ignored) — every caller
811
+ // passes `false` and the re-gate branches that read it are dead.
812
+ const recovering = false;
813
+ if (committedRecovery) {
814
+ // Announce LOUDLY: a stranded already-complete branch signals an EARLIER
815
+ // un-merged PR — the CI/job log must record that the autonomous path took
816
+ // the recovery branch, not a normal completion. The `already-integrated`
817
+ // no-op gets its own clear note from the core itself.
818
+ note(
819
+ `>> recovered a stranded already-complete branch for '${slug}' — ` +
820
+ 'integrating the kept commit (no rebuild). This signals an earlier ' +
821
+ 'un-merged PR.',
822
+ );
823
+ }
824
+
825
+ // Pre-flight DIVERGENCE GUARD (merge mode only) — the SAME class of refusal `do`
826
+ // raises up front: a local `main` AHEAD of `<arbiter>/main` (unpushed commits)
827
+ // cannot be fast-forwarded by the merge-back, so refuse BEFORE the gate so no
828
+ // work is wasted. Only merge mode ff's local `main`; propose only switches to it
829
+ // (no ff), so the guard is irrelevant there. `--ignore-diverged-main` overrides
830
+ // (mirrors `--ignore-not-ready`); when overridden, the now-NON-FATAL
831
+ // `syncLocalMain` handles the persisting divergence honestly at integrate-time.
832
+ if (requestedMode === 'merge' && options.ignoreDivergedMain !== true) {
833
+ await gitHard(['fetch', '--quiet', arbiter], cwd, env);
834
+ const ahead = await localMainAheadCount(cwd, arbiter, env);
835
+ if (ahead > 0) {
836
+ throw new CompleteRefusal(
837
+ `local main is ahead of ${arbiter}/main by ${ahead} commit` +
838
+ `${ahead === 1 ? '' : 's'} (unpushed); a merge completion ff's local ` +
839
+ "main, which can't fast-forward against a diverged main — push or " +
840
+ 'reconcile main first (or re-run with --ignore-diverged-main to ' +
841
+ 'proceed anyway).',
842
+ 'diverged-main',
843
+ slug,
844
+ );
845
+ }
846
+ }
847
+
848
+ // STATIC fresh-worktree-gate readiness guard — the human/recovery mirror of
849
+ // `performDo`'s step 3d (task
850
+ // `do-fails-fast-when-acceptance-gate-statically-unrunnable`). When the fresh-
851
+ // worktree gate is ON for THIS invocation AND `prepare` resolves to no commands
852
+ // AND a lockfile is present, the throwaway worktree the gate runs in will have
853
+ // no installed deps, so the gate cannot run. Surface the precise error BEFORE
854
+ // `performIntegration` does any work (skip-verify bypasses the gate entirely
855
+ // and so bypasses this guard too — if there is no gate to run, there is no
856
+ // precondition to check). Gated on `freshWorktreeGate === true` for THIS
857
+ // invocation: when OFF, the gate runs in the CURRENT checkout (which carries
858
+ // its deps), so the throwaway-worktree reasoning does not apply.
859
+ if (options.skipVerify !== true) {
860
+ const guard = checkGatePreconditions({
861
+ freshWorktreeGate: options.freshWorktreeGate,
862
+ prepare: options.prepare,
863
+ lockfile: detectLockfileOnDisk(cwd),
864
+ });
865
+ if (guard !== undefined) {
866
+ throw new CompleteRefusal(guard.message, 'gate-unrunnable');
867
+ }
868
+ }
869
+
870
+ // CORE: run the SHARED gate→integrate band (verify → review → effective-mode
871
+ // decision → done-move → commit → rebase → integrate → needs-attention routing).
872
+ // It returns DATA (the routing + the effective-mode decision already happened
873
+ // inside it); the TAIL below does only `complete`'s caller-specific post-step
874
+ // (switch-to-main / ff / delete-branch / `--no-switch` / the propose next-step
875
+ // block). The human-vs-autonomous difference rides on `surfaceArbiter` (DATA).
876
+ // `IntegrationNothingStaged` is the one refusal the core raises (empty commit);
877
+ // `performComplete`'s try/catch maps it to `refused`, unchanged.
878
+ const core = await performIntegration({
879
+ cwd,
880
+ arbiter,
881
+ slug,
882
+ // The namespaced work branch HEAD is on (resolved above) — pass it through
883
+ // so the integrate core pushes the EXACT branch, not a re-synthesised default.
884
+ branch,
885
+ source,
886
+ recovering,
887
+ // Route stranded-done into the shared recover tail (front-gate detection;
888
+ // the core does the unspoofable tip-vs-arbiter ancestry check).
889
+ committedRecovery,
890
+ prepare: options.prepare,
891
+ verify: options.verify,
892
+ freshWorktreeGate: options.freshWorktreeGate,
893
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — the
894
+ // git-alone FLOOR of the cross-job land-queue, resolved through the gate-family
895
+ // precedence chain (prd `land-time-reverify-and-parallel-merge-ceiling` Story
896
+ // 5). Threaded so a wide-matrix CI's raised cap actually reaches the merge loop.
897
+ mergeRetries: options.mergeRetries,
898
+ skipVerify: options.skipVerify,
899
+ // The untrusted-origin build-propose rule's override (task
900
+ // `untrusted-origin-forces-build-propose`): an explicit `--merge` lets the
901
+ // operator land an untrusted-origin task on main; the autonomous path leaves
902
+ // it unset so untrusted-origin reliably forces propose.
903
+ explicitMerge: options.explicitMerge,
904
+ review: options.review,
905
+ reviewGate: options.reviewGate,
906
+ reviewModel: options.reviewModel,
907
+ reviewMaxRounds: options.reviewMaxRounds,
908
+ mode: requestedMode,
909
+ noPR: options.noPR,
910
+ providerInstance: options.providerInstance,
911
+ openPr: options.openPr,
912
+ body: options.body,
913
+ type: options.type,
914
+ message: options.message,
915
+ surfaceArbiter: options.surfaceArbiter,
916
+ watch: options.watch,
917
+ watchSink: options.watchSink,
918
+ color: options.color,
919
+ sessionsDir: options.sessionsDir,
920
+ env,
921
+ agentEnv: options.agentEnv,
922
+ note,
923
+ });
924
+
925
+ // The FAILURE outcomes map 1:1 onto `complete`'s — the core already note()'d
926
+ // the reason and did any routing; the tail never runs for them.
927
+ if (core.outcome === 'prepare-failed') {
928
+ return {
929
+ exitCode: 1,
930
+ outcome: 'prepare-failed',
931
+ routedToNeedsAttention: core.routedToNeedsAttention,
932
+ branch: core.branch,
933
+ message: core.reason ?? '',
934
+ };
935
+ }
936
+ if (core.outcome === 'gate-failed') {
937
+ return {
938
+ exitCode: 1,
939
+ outcome: 'gate-failed',
940
+ routedToNeedsAttention: core.routedToNeedsAttention,
941
+ branch: core.branch,
942
+ message: core.reason ?? '',
943
+ };
944
+ }
945
+ if (core.outcome === 'review-blocked') {
946
+ return {
947
+ exitCode: 1,
948
+ outcome: 'review-blocked',
949
+ routedToNeedsAttention: core.routedToNeedsAttention,
950
+ branch: core.branch,
951
+ message: core.reason ?? '',
952
+ };
953
+ }
954
+ if (core.outcome === 'review-unparseable') {
955
+ // Gate 2 RAN but its verdict was UNPARSEABLE (malformed JSON). The core already
956
+ // routed it work-preservingly via `applyNeedsAttentionTransition` (branch pushed +
957
+ // surfaced on the autonomous path). Map it 1:1 like the other routed failures; the
958
+ // `do`/`run` tail re-labels its cause `transient-infra` (NOT a reviewer block) off
959
+ // the parse-failure phrase in `core.reason`.
960
+ return {
961
+ exitCode: 1,
962
+ outcome: 'review-unparseable',
963
+ routedToNeedsAttention: core.routedToNeedsAttention,
964
+ branch: core.branch,
965
+ message: core.reason ?? '',
966
+ };
967
+ }
968
+ if (core.outcome === 'rebase-conflict') {
969
+ return {
970
+ exitCode: 1,
971
+ outcome: 'rebase-conflict',
972
+ routedToNeedsAttention: core.routedToNeedsAttention,
973
+ branch: core.branch,
974
+ commitMessage: core.commitMessage,
975
+ message: core.reason ?? '',
976
+ };
977
+ }
978
+ if (core.outcome === 'already-integrated') {
979
+ // Stranded-done auto-recover: the kept tip is ALREADY on `<arbiter>/main`
980
+ // (the PR merged out-of-band before the re-run). A clean, successful no-op
981
+ // — NEVER a re-push / second PR. The core already emitted its honest note;
982
+ // the caller's tail (switch-to-main / ff / delete-branch) is irrelevant
983
+ // because the work is already integrated and the branch may or may not
984
+ // still exist locally.
985
+ //
986
+ // CROSS-SUBSTRATE RELEASE (prd `ledger-status-per-item-lock-refs` US #9/#10;
987
+ // task `complete-lock-then-durable-main-move-crash-safe`): the durable
988
+ // `main` record is already terminal (the kept tip is provably on
989
+ // `<arbiter>/main`), so the per-item lock claim took is now stale: release it
990
+ // SECOND (the move already landed FIRST). `durablyOnMain: true` unconditionally
991
+ // here (unlike the propose build path, this branch is reached ONLY when the
992
+ // work is already integrated on `main`). Best-effort + idempotent (`not-held`
993
+ // is fine when a prior reconcile/release cleared it).
994
+ await releaseClaimLockAfterDurableMove(slug, cwd, arbiter, env, true, note);
995
+ return {
996
+ exitCode: 0,
997
+ outcome: 'already-integrated',
998
+ branch: core.branch,
999
+ message: core.reason ?? '',
1000
+ };
1001
+ }
1002
+ if (core.outcome === 'invariant-violation') {
1003
+ // The one-slug-one-folder guard FAILED LOUD: the arbiter already holds the
1004
+ // slug in >1 status folder (a corrupt ledger). Nothing was committed/moved;
1005
+ // refuse rather than publish corruption (the human resolves the duplicate —
1006
+ // `scan`/`gc` surfaces it — then re-runs). Exit 1, never routed.
1007
+ return {
1008
+ exitCode: 1,
1009
+ outcome: 'invariant-violation',
1010
+ routedToNeedsAttention: false,
1011
+ branch: core.branch,
1012
+ message: core.reason ?? '',
1013
+ };
1014
+ }
1015
+
1016
+ // SUCCESS: the core integrated. `result` is its integration result; `mode` is
1017
+ // the mode the core resolved, read from the result (it always equals the
1018
+ // requested mode now that there is no downgrade). The tail switches/ffs per the
1019
+ // mode that integrated.
1020
+ const result = core.integration!;
1021
+ const commitMessage = core.commitMessage;
1022
+ const mode = result.mode;
1023
+
1024
+ // CROSS-SUBSTRATE RELEASE, gated on the work being DURABLY ON `main` (prd
1025
+ // `ledger-status-per-item-lock-refs` US #9/#10; ADR
1026
+ // `ledger-status-on-per-item-lock-refs`; the trail's Amendment 6; task
1027
+ // `complete-lock-then-durable-main-move-crash-safe`). ORDER MATTERS: the
1028
+ // per-item lock that `claim` acquired (`action: implement`, keyed
1029
+ // `task:<slug>`) is released SECOND, AFTER the durable `main` move landed FIRST
1030
+ // inside `performIntegration`.
1031
+ //
1032
+ // CRITICAL (task `propose-keep-lock-until-pr-merge`): in PROPOSE mode the
1033
+ // done-move did NOT land on `main` here. The `git mv tasks/ready -> done` is
1034
+ // committed on the WORK BRANCH and pushed as the PR; `<arbiter>/main` still
1035
+ // shows the body in `tasks/ready/` until a human MERGES the PR. So releasing
1036
+ // the lock now would leave the task BOTH unlocked AND in `tasks/ready/` on
1037
+ // `main`, i.e. fully eligible again, for the whole review window: the next
1038
+ // advance tick re-claims it (lock absent), rebuilds, the PR meanwhile merges,
1039
+ // the diff is empty, and the item is mis-marked `stuck`. So we release ONLY
1040
+ // when the work is provably on `main`: merge mode (`mergedToMain`) or an
1041
+ // already-landed propose (`alreadyLanded`, the benign already-on-main race
1042
+ // tail). A plain open propose PR KEEPS the lock held (the open PR IS the
1043
+ // in-flight state); it is released when the work lands on `main`, reconciled by
1044
+ // `reconcileItemLockAgainstMain` when the PR merges out-of-band.
1045
+ //
1046
+ // A crash between the move and the release leaves a `done`-on-`main` item with
1047
+ // a still-held lock; `reconcileItemLockAgainstMain` recovers it (the `main`
1048
+ // record is authoritative over the stale lock). The release is best-effort +
1049
+ // idempotent (`not-held` when the body predates the lock or a reconcile already
1050
+ // cleared it).
1051
+ const durablyOnMain =
1052
+ result.mergedToMain === true || result.alreadyLanded === true;
1053
+ await releaseClaimLockAfterDurableMove(
1054
+ slug,
1055
+ cwd,
1056
+ arbiter,
1057
+ env,
1058
+ durablyOnMain,
1059
+ note,
1060
+ );
1061
+
1062
+ // Land back on `main` by default in BOTH modes (the move differs per mode),
1063
+ // then delete the local work branch iff its work is provably on the arbiter.
1064
+ // `--no-switch` keeps the human on the work branch AND keeps the branch.
1065
+ let switchedTo = branch;
1066
+ let deletedLocalBranch = false;
1067
+ // Merge mode only: whether the local `main` ff'd (the courtesy after the
1068
+ // authoritative push). `true` by default; flipped to `false` by `syncLocalMain`
1069
+ // on a NON-FATAL diverged-`main` skip. Undefined in propose / `--no-switch`.
1070
+ let localMainSynced: boolean | undefined;
1071
+ if (!options.noSwitch) {
1072
+ if (mode === 'merge') {
1073
+ // merge: the work landed on <arbiter>/main, so switch to main AND ff it.
1074
+ // The ff is a COURTESY — if local `main` diverged it cannot apply, which is
1075
+ // NON-FATAL (the push already defined success); `syncLocalMain` reports it.
1076
+ localMainSynced = await syncLocalMain(cwd, arbiter, env, note);
1077
+ } else {
1078
+ // propose: the work is on a pushed branch awaiting review, NOT on main,
1079
+ // so JUST switch to main — do NOT ff (arbiter main has not moved).
1080
+ await gitHard(['switch', '--quiet', 'main'], cwd, env);
1081
+ }
1082
+ switchedTo = 'main';
1083
+ // Delete the LOCAL work branch when provably on the arbiter (same predicate
1084
+ // as worktree deletion, ADR §4, mode-agnostic). NEVER delete the remote.
1085
+ deletedLocalBranch = await deleteLocalBranchIfProvablyOnArbiter(
1086
+ cwd,
1087
+ arbiter,
1088
+ branch,
1089
+ env,
1090
+ );
1091
+ }
1092
+
1093
+ if (mode === 'merge') {
1094
+ // When the local ff was SKIPPED (diverged main, non-fatal), say so honestly —
1095
+ // the work IS on the arbiter; only the local courtesy ff was left undone.
1096
+ const localState =
1097
+ localMainSynced === false
1098
+ ? 'local main left diverged (run `git rebase origin/main` to sync)'
1099
+ : 'local main updated';
1100
+ const landed = options.noSwitch
1101
+ ? `merged to ${arbiter}/main; left on ${branch} (--no-switch).`
1102
+ : `merged to ${arbiter}/main; ${localState}` +
1103
+ `${deletedLocalBranch ? ` and ${branch} deleted` : ''}.`;
1104
+ const message = `Completed '${slug}': ${landed}`;
1105
+ note(message);
1106
+ return {
1107
+ exitCode: 0,
1108
+ outcome: 'completed',
1109
+ branch,
1110
+ commitMessage,
1111
+ mergedToMain: result.mergedToMain,
1112
+ localMainSynced,
1113
+ switchedTo,
1114
+ deletedLocalBranch,
1115
+ message,
1116
+ };
1117
+ }
1118
+
1119
+ // propose: the branch is pushed; report the next step. The next step is the
1120
+ // ONE thing the human must act on, so it is emitted as a visually-distinct
1121
+ // block (blank lines + heading + TTY-aware color) ON TOP of the plain summary
1122
+ // note below — cosmetic only; the structured result is unchanged.
1123
+ const color = options.color ?? shouldUseColor(process.stdout);
1124
+ const noteBlock = options.noteBlock ?? note;
1125
+ noteBlock(
1126
+ formatProposeNextStep({
1127
+ branch,
1128
+ arbiter,
1129
+ requestOpened: result.requestOpened,
1130
+ color,
1131
+ }),
1132
+ );
1133
+ const next = result.requestOpened
1134
+ ? result.url
1135
+ ? `pushed ${branch} and opened a review (${result.url})`
1136
+ : `pushed ${branch} and opened a review`
1137
+ : `pushed ${branch} to ${arbiter}. ` + 'Open a PR/MR to land it on main';
1138
+ const tail = options.noSwitch
1139
+ ? `; left on ${branch} (--no-switch).`
1140
+ : deletedLocalBranch
1141
+ ? `; switched to main and deleted ${branch}.`
1142
+ : '; switched to main.';
1143
+ const message = `Completed '${slug}': ${next}${tail}`;
1144
+ note(message);
1145
+ return {
1146
+ exitCode: 0,
1147
+ outcome: 'completed',
1148
+ branch,
1149
+ commitMessage,
1150
+ mergedToMain: false,
1151
+ switchedTo,
1152
+ deletedLocalBranch,
1153
+ prUrl: result.url,
1154
+ message,
1155
+ };
1156
+ }
1157
+
1158
+ /**
1159
+ * Release the per-item lock `claim` ALSO acquired (`action: implement`, keyed
1160
+ * `task:<slug>`) AFTER the durable `main` move has landed — the SECOND, lock-
1161
+ * release half of complete's cross-substrate ordering (prd
1162
+ * `ledger-status-per-item-lock-refs` US #9/#10; task
1163
+ * `complete-lock-then-durable-main-move-crash-safe`).
1164
+ *
1165
+ * Called ONLY on the SUCCESS paths (`completed` / `already-integrated`). The lock
1166
+ * is released ONLY when the work is DURABLY ON `<arbiter>/main` (`durablyOnMain`):
1167
+ * merge mode (`mergedToMain`), an already-landed propose race tail
1168
+ * (`alreadyLanded`), or the `already-integrated` recovery (provably on main). In
1169
+ * those cases the lock is no longer holding anything in flight and is cleanly
1170
+ * released SECOND.
1171
+ *
1172
+ * When `durablyOnMain` is FALSE (a plain open propose PR: the done-move is on the
1173
+ * PR branch, NOT on `main`), the lock is KEPT HELD: the open PR IS the in-flight
1174
+ * state, and the held lock is what keeps the still-`tasks/ready/`-on-`main` task
1175
+ * out of the eligible pool until the PR merges (task
1176
+ * `propose-keep-lock-until-pr-merge`). Releasing here would let the next advance
1177
+ * tick re-claim the in-flight task and mis-mark it `stuck` on an empty diff. The
1178
+ * lock is released when the work lands on `main`, reconciled by
1179
+ * `reconcileItemLockAgainstMain` when the PR merges out-of-band.
1180
+ *
1181
+ * The FAILURE paths (gate-failed / review-blocked / rebase-conflict /
1182
+ * prepare-failed) deliberately do NOT call this: those route to needs-attention
1183
+ * (the lock is marked `stuck`, not released) and the item is still in flight.
1184
+ *
1185
+ * Best-effort + idempotent (a `not-held` is fine: the body may predate the lock,
1186
+ * or a crash-recovery `reconcileItemLockAgainstMain` may already have cleared it).
1187
+ * A release fault never fails an already-landed completion: the durable `main`
1188
+ * record is authoritative, and a stranded lock is exactly what recovery clears.
1189
+ */
1190
+ async function releaseClaimLockAfterDurableMove(
1191
+ slug: string,
1192
+ cwd: string,
1193
+ arbiter: string,
1194
+ env: NodeJS.ProcessEnv | undefined,
1195
+ durablyOnMain: boolean,
1196
+ note: (message: string) => void,
1197
+ ): Promise<void> {
1198
+ if (!durablyOnMain) {
1199
+ // PROPOSE, PR open: the done-move is on the PR branch, not on `main`. KEEP the
1200
+ // lock held so the in-flight task is not re-claimed during the review window.
1201
+ note(
1202
+ `'${slug}': keeping the per-item lock HELD (propose PR open; the work is ` +
1203
+ 'not yet on main). It is released when the PR merges (reconciled against ' +
1204
+ 'main).',
1205
+ );
1206
+ return;
1207
+ }
1208
+ try {
1209
+ await releaseItemLock({item: `task:${slug}`, cwd, arbiter, env});
1210
+ } catch {
1211
+ // Best-effort: a release fault leaves a stale lock that recovery
1212
+ // (`reconcileItemLockAgainstMain`) clears: the durable move already defined
1213
+ // success. Never fail an already-landed completion on the lock release.
1214
+ }
1215
+ }
1216
+
1217
+ /**
1218
+ * Sync the local `main` to the just-pushed `<arbiter>/main`: switch to main and
1219
+ * fast-forward it. We fetch the arbiter's main (we just pushed our rebased work
1220
+ * there) then `merge --ff-only` so the user lands on an up-to-date local main
1221
+ * without merge noise.
1222
+ *
1223
+ * The ff is a COURTESY, NOT the safety-bearing step — the authoritative push to
1224
+ * `<arbiter>/main` already defined `complete`'s success. So the ff is NON-FATAL:
1225
+ * if local `main` has DIVERGED (unpushed commits the arbiter lacks) the ff cannot
1226
+ * apply; rather than throw (which would make `complete` exit non-zero even though
1227
+ * the merge ALREADY LANDED on the arbiter), we print a clear "rebase to sync"
1228
+ * message and return `false` (the caller records `localMainSynced: false` and
1229
+ * keeps `outcome: completed` / exit 0). Returns `true` on a normal ff.
1230
+ *
1231
+ * Only the diverged / ff-cannot-apply case is softened — the `fetch` and `switch`
1232
+ * stay `gitHard` (a genuinely different failure is NOT masked, per the task).
1233
+ * Exported for direct testing of the softened-vs-fatal boundary in isolation.
1234
+ */
1235
+ export async function syncLocalMain(
1236
+ cwd: string,
1237
+ arbiter: string,
1238
+ env: NodeJS.ProcessEnv | undefined,
1239
+ note: (m: string) => void,
1240
+ ): Promise<boolean> {
1241
+ await gitHard(['fetch', '--quiet', arbiter], cwd, env);
1242
+ await gitHard(['switch', '--quiet', 'main'], cwd, env);
1243
+ // SOFT: a non-zero here is (almost always) "not possible to fast-forward" —
1244
+ // local `main` diverged. That is the one case we make non-fatal.
1245
+ const ff = await gitSoft(
1246
+ ['merge', '--ff-only', '--quiet', `${arbiter}/main`],
1247
+ cwd,
1248
+ env,
1249
+ );
1250
+ if (ff.status === 0) {
1251
+ return true;
1252
+ }
1253
+ note(
1254
+ `work landed on ${arbiter}/main; your local main couldn't fast-forward (it ` +
1255
+ 'has diverged) — run `git rebase origin/main` to sync.',
1256
+ );
1257
+ return false;
1258
+ }
1259
+
1260
+ /**
1261
+ * Delete the LOCAL `work/<slug>` branch iff its work is PROVABLY on the arbiter
1262
+ * — the SAME predicate as worktree deletion (ADR §4), mode-agnostic. Must be
1263
+ * called AFTER we have switched off the branch (you cannot delete the branch you
1264
+ * are on). Returns whether the local branch was deleted.
1265
+ *
1266
+ * Provably-on-arbiter ⇔ EITHER:
1267
+ * - merged: the branch tip is an ancestor of `<arbiter>/main`, OR
1268
+ * - pushed & up-to-date: `<arbiter>/<branch>` exists AND its tip == the local
1269
+ * branch tip (so a later un-pushed amend is NEVER lost — we verify the
1270
+ * remote tip equals the local tip, not merely that "a branch was pushed").
1271
+ *
1272
+ * Otherwise the branch is KEPT (unmerged / unpushed / diverged = not safe). The
1273
+ * REMOTE branch is NEVER touched (a propose PR is built from it). A `git fetch`
1274
+ * refreshes the remote-tracking refs so reachability is read against the LIVE
1275
+ * arbiter; an unreachable arbiter simply reads as not-provable → keep (safe).
1276
+ */
1277
+ async function deleteLocalBranchIfProvablyOnArbiter(
1278
+ cwd: string,
1279
+ arbiter: string,
1280
+ branch: string,
1281
+ env: NodeJS.ProcessEnv | undefined,
1282
+ ): Promise<boolean> {
1283
+ if (!(await isLocalBranchProvablyOnArbiter(cwd, arbiter, branch, env))) {
1284
+ return false; // not provably on the arbiter → KEEP the local branch
1285
+ }
1286
+ // Provably on the arbiter → delete ONLY the local branch (force-delete: the
1287
+ // `-d` safety check uses the upstream/HEAD, which we have already proven via
1288
+ // the predicate above; `-D` avoids a false "not fully merged" in propose mode).
1289
+ await gitHard(['branch', '-D', branch], cwd, env);
1290
+ return true;
1291
+ }
1292
+
1293
+ /**
1294
+ * The provably-safe predicate (ADR §4), mode-agnostic, applied to a LOCAL
1295
+ * `branch` in `cwd`: true iff its tip is an ancestor of `<arbiter>/main` (merged)
1296
+ * OR `<arbiter>/<branch>` exists with its tip == the local tip (pushed &
1297
+ * up-to-date). False when the local branch is absent, or its work is unmerged /
1298
+ * unpushed / a diverged un-pushed amend (remote tip != local tip). A `git fetch`
1299
+ * refreshes the remote-tracking refs; an unreachable arbiter reads as not-
1300
+ * provable → false (the safe direction). Exported for direct testing of the
1301
+ * kept-vs-deleted decision in isolation.
1302
+ */
1303
+ export async function isLocalBranchProvablyOnArbiter(
1304
+ cwd: string,
1305
+ arbiter: string,
1306
+ branch: string,
1307
+ env: NodeJS.ProcessEnv | undefined,
1308
+ ): Promise<boolean> {
1309
+ const localTip = (
1310
+ await gitSoft(['rev-parse', '--verify', '--quiet', branch], cwd, env)
1311
+ ).stdout.trim();
1312
+ if (localTip === '') {
1313
+ return false; // no such local branch
1314
+ }
1315
+
1316
+ // Refresh remote-tracking refs against the live arbiter (best-effort).
1317
+ await gitSoft(['fetch', '--quiet', arbiter], cwd, env);
1318
+
1319
+ // Merged: tip is an ancestor of <arbiter>/main.
1320
+ const merged =
1321
+ (
1322
+ await gitSoft(
1323
+ ['merge-base', '--is-ancestor', localTip, `${arbiter}/main`],
1324
+ cwd,
1325
+ env,
1326
+ )
1327
+ ).status === 0;
1328
+ if (merged) {
1329
+ return true;
1330
+ }
1331
+
1332
+ // Pushed & up-to-date: <arbiter>/<branch> exists AND its tip == the local tip
1333
+ // (we verify remote-tip == local-tip so a later un-pushed amend is never lost).
1334
+ const remoteTip = (
1335
+ await gitSoft(
1336
+ ['rev-parse', '--verify', '--quiet', `refs/remotes/${arbiter}/${branch}`],
1337
+ cwd,
1338
+ env,
1339
+ )
1340
+ ).stdout.trim();
1341
+ return remoteTip !== '' && remoteTip === localTip;
1342
+ }
1343
+
1344
+ /** The short symbolic name of HEAD, or '' for a detached HEAD. */
1345
+ async function currentBranch(
1346
+ cwd: string,
1347
+ env: NodeJS.ProcessEnv | undefined,
1348
+ ): Promise<string> {
1349
+ const sym = await gitSoft(
1350
+ ['symbolic-ref', '--quiet', '--short', 'HEAD'],
1351
+ cwd,
1352
+ env,
1353
+ );
1354
+ return sym.status === 0 ? sym.stdout.trim() : '';
1355
+ }
1356
+
1357
+ /** Run git, returning the raw result (no throw) — for soft checks. */
1358
+ function gitSoft(
1359
+ args: string[],
1360
+ cwd: string,
1361
+ env: NodeJS.ProcessEnv | undefined,
1362
+ ): Promise<RunResult> {
1363
+ return runAsync('git', args, cwd, {env});
1364
+ }
1365
+
1366
+ /** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
1367
+ async function gitHard(
1368
+ args: string[],
1369
+ cwd: string,
1370
+ env: NodeJS.ProcessEnv | undefined,
1371
+ ): Promise<RunResult> {
1372
+ const result = await runAsync('git', args, cwd, {env});
1373
+ if (result.status !== 0) {
1374
+ throw new Error(
1375
+ `git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
1376
+ );
1377
+ }
1378
+ return result;
1379
+ }