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,1506 @@
1
+ import {rmSync} from 'node:fs';
2
+ import {tmpdir} from 'node:os';
3
+ import {join} from 'node:path';
4
+ import {
5
+ type WorkFolderKey,
6
+ workFolderPrefix,
7
+ workFolderRel,
8
+ workItemRel,
9
+ isWorkItemFile,
10
+ } from './work-layout.js';
11
+ import {run, runAsync, type RunResult} from './git.js';
12
+ import {branchAheadOf} from './continue-branch.js';
13
+ import {
14
+ acquireItemLock,
15
+ releaseItemLock,
16
+ readItemLock,
17
+ itemLockRef,
18
+ lockEntryFor,
19
+ parseLockEntry,
20
+ type LockEntry,
21
+ } from './item-lock.js';
22
+ import {ledgerWrite, type LedgerTransitionKind} from './ledger-write.js';
23
+ import {workBranchRef} from './slug-namespace.js';
24
+ import {
25
+ retryWithBackoff,
26
+ realSleep,
27
+ type BackoffOptions,
28
+ type Sleep,
29
+ } from './retry-backoff.js';
30
+
31
+ /**
32
+ * The **needs-attention mechanism** (ADR `ledger-status-on-per-item-lock-refs`;
33
+ * prd `ledger-status-per-item-lock-refs`; ADR §12 for the original folder model).
34
+ * Every "couldn't finish, a human must look" outcome (a failed acceptance gate
35
+ * (red `verify`), a rebase/merge conflict (ADR §10), a task the agent reported
36
+ * too ambiguous to build, a timeout, or a rejected review) resolves to ONE
37
+ * observable move: the RUNNER AMENDS the claimed item's HELD per-item lock
38
+ * `active → stuck` (`refs/dorfl/lock/<entry>`), writing the reason (+ any
39
+ * agent-surfaced questions) into the lock-entry BODY. There is NO `git mv` to a
40
+ * `work/needs-attention/` folder and NO on-`main` surface (the lock cut-over,
41
+ * task `cutover-needs-attention-becomes-lock-stuck-recovery-surface`): so a
42
+ * protected-`main` bounce succeeds, and a work branch cut from `main` inherits no
43
+ * stuck record. The RECOVERABLE half is the kept `work/<slug>` branch.
44
+ *
45
+ * This is the conflict-safe form of "surfacing": the surface is the lock
46
+ * `state: stuck`, read by `scan`/`status`/`gc --ledger` reading the lock refs (a
47
+ * COMMAND a human runs, not an `ls` of a folder). There is **no status/label
48
+ * field** on `main` (honours WORK-CONTRACT rule 3). The reason is prose in the
49
+ * lock-entry body, never a source-of-truth frontmatter field.
50
+ *
51
+ * Ownership: this module OWNS the mechanism (the move helper + the surface
52
+ * reader + the return path). Consumers (`complete.ts`'s gate-failed/rebase-
53
+ * conflict abort paths, the runner's stuck routing in `run.ts`, the human
54
+ * `return` command) drive these through the ledger write seam's NEEDS-ATTENTION
55
+ * transition (`ledgerWrite.applyNeedsAttentionTransition` /
56
+ * `applyReturnToBacklogTransition` in `ledger-write.ts`), whose sole strategy
57
+ * delegates to `routeToNeedsAttention` / `returnToBacklog` here UNCHANGED — so
58
+ * the later cherry-pick-to-`main` surfacing is built AGAINST the seam, not
59
+ * bolted onto this move code. The build agent NEVER does this — agents do no git
60
+ * (ADR §12).
61
+ */
62
+
63
+ /** Marker that opens the appended reason block in a needs-attention item body. */
64
+ const REASON_HEADING = '## Needs attention';
65
+
66
+ export interface RouteToNeedsAttentionOptions {
67
+ /** The working clone / job worktree the `work/<slug>` branch lives in. */
68
+ cwd: string;
69
+ /** The slug of the in-progress item to bounce. */
70
+ slug: string;
71
+ /** Why the item is stuck (red gate, rebase conflict, ambiguity, timeout, …). */
72
+ reason: string;
73
+ /** Any questions the agent surfaced for the human, recorded under the reason. */
74
+ questions?: string[];
75
+ /**
76
+ * The arbiter remote to push the transition to (like the done-move). When
77
+ * omitted, the move is committed locally only (the caller pushes the branch as
78
+ * part of its own flow, e.g. the runner's integration step).
79
+ */
80
+ arbiter?: string;
81
+ /**
82
+ * The work branch to push to the arbiter (the RECOVERABLE half — see the seam
83
+ * docstring). DEFAULT `work/<slug>`: the build-bounce branch the wip/move
84
+ * commits landed on. A tasking bounce passes its own branch (`work/prds/
85
+ * ready-<slug>`). The supplied branch MUST be the one HEAD is on (the branch the
86
+ * wip/move commits landed on) — NEVER a default that differs from HEAD; a
87
+ * caller NOT checked out on the work branch (e.g. a temp branch off main) must
88
+ * be SURFACE-ONLY ({@link pushBranch} `false`) so no wrong-branch ref is
89
+ * pushed. Only consulted when {@link arbiter} is given and {@link pushBranch}
90
+ * is not `false`.
91
+ */
92
+ branch?: string;
93
+ /**
94
+ * SURFACE-ONLY when `false`: publish the ledger surface (when an `arbiter` is
95
+ * given) but push NO work branch. For a caller that is NOT checked out on the
96
+ * work branch (a throwaway temp branch off main — e.g. `start.ts`'s
97
+ * `routeContinueConflict`, whose real `work/<slug>` is already on the arbiter
98
+ * from the prior requeue). Defaults to pushing (the build-bounce common case).
99
+ */
100
+ pushBranch?: boolean;
101
+ /** Environment for child git processes (identity etc.). */
102
+ env?: NodeJS.ProcessEnv;
103
+ /** Sink for human-readable progress notes. */
104
+ note?: (message: string) => void;
105
+ /**
106
+ * Bounded-backoff bounds for the RECOVERABLE branch push (the OUTAGE-retry,
107
+ * NOT the instant-contention loop). Defaults to {@link DEFAULT_BACKOFF}.
108
+ */
109
+ backoff?: BackoffOptions;
110
+ /**
111
+ * Injectable sleep for the backoff (tests drive the retry timeline with NO
112
+ * real waits — the `run.ts` `sleep`/`realSleep` seam). Defaults to a real
113
+ * `setTimeout`. Threaded into {@link backoff} when the latter omits its own.
114
+ */
115
+ sleep?: Sleep;
116
+ }
117
+
118
+ /** The outcome of the RECOVERABLE branch push (the per-op honest report). */
119
+ export type BranchPushOutcome =
120
+ /** Pushed to the arbiter (cross-machine recoverable). */
121
+ | 'pushed'
122
+ /** Skipped by the emptiness guard — nothing beyond main to recover YET. */
123
+ | 'skipped-empty'
124
+ /** Retried with backoff, then gave up — saved LOCALLY only. */
125
+ | 'failed'
126
+ /** No arbiter / surface-only — no push was attempted. */
127
+ | 'not-attempted';
128
+
129
+ export interface RouteToNeedsAttentionResult {
130
+ /** True iff the item was moved + committed. */
131
+ moved: boolean;
132
+ /** When `moved`, the committed transition message (of the MOVE-ONLY commit). */
133
+ commitMessage?: string;
134
+ /**
135
+ * The per-op outcome of the RECOVERABLE branch push (honest reporting — the
136
+ * message reads THIS, never assumes "pushed" off the local move). Absent when
137
+ * the item did not move.
138
+ */
139
+ branchPush?: BranchPushOutcome;
140
+ /** When the branch push FAILED after retries, the last git error (for the report). */
141
+ pushError?: string;
142
+ /**
143
+ * When `moved`, the sha of the **move-only** commit — the tip of `work/<slug>`
144
+ * that carries PURELY the `git mv → needs-attention/` + the reason (the wip
145
+ * commit holding the aborted agent work sits BELOW it). A surfacing strategy
146
+ * cherry-picks THIS commit to make the stuck state observable, so the wip never
147
+ * reaches the ledger.
148
+ */
149
+ moveCommit?: string;
150
+ /** When NOT moved, why (e.g. the slug was not in-progress). */
151
+ reasonNotMoved?: string;
152
+ }
153
+
154
+ export interface ReturnToBacklogOptions {
155
+ /** The working clone the `work/` tree lives in. */
156
+ cwd: string;
157
+ /**
158
+ * The slug of the stuck item to re-queue — recovered from `needs-attention/`
159
+ * OR `in-progress/` (the actual current folder is resolved on the arbiter).
160
+ */
161
+ slug: string;
162
+ /** The arbiter remote to push the transition to. Optional (see above). */
163
+ arbiter?: string;
164
+ /**
165
+ * `requeue --reset` (the destructive opt-out): DISCARD the kept work, so the
166
+ * NEXT claim starts FRESH. At requeue-time — BEFORE the backlog move — delete
167
+ * the remote `work/<slug>` branch on `arbiter`
168
+ * (`git push <arbiter> --delete work/<slug>`, plain provider-agnostic git that
169
+ * works against a local `--bare` arbiter) and drop any stale LOCAL `work/<slug>`.
170
+ * Delete-before-move closes the claim-race window (no backlog item exists while
171
+ * the to-be-discarded branch still does). A FAILED delete ABORTS the requeue
172
+ * (no backlog move) — the item stays in needs-attention rather than become
173
+ * claimable while continuing from a branch you meant to throw away. Requires
174
+ * `arbiter`. Explicit/guarded — a deliberate departure from the loud "never
175
+ * delete the remote branch" invariant; never on the default (keep+continue)
176
+ * path.
177
+ */
178
+ reset?: boolean;
179
+ /**
180
+ * `requeue -m "<note>"` (the handoff note): an optional human steer for the
181
+ * NEXT agent. APPENDED (never overwritten) as a dated `## Requeue YYYY-MM-DD`
182
+ * section to the item BODY before the move — the ledger file is the durable,
183
+ * conflict-safe, cross-machine home (same place the needs-attention reason
184
+ * lives). Repeated requeues ACCUMULATE a handoff log. Applies to BOTH modes
185
+ * (a steer is relevant even on `--reset`).
186
+ */
187
+ message?: string;
188
+ /** Environment for child git processes. */
189
+ env?: NodeJS.ProcessEnv;
190
+ /** Sink for human-readable progress notes. */
191
+ note?: (message: string) => void;
192
+ }
193
+
194
+ export interface ReturnToBacklogResult {
195
+ /** True iff the item was moved back + committed. */
196
+ moved: boolean;
197
+ /** When `moved`, the committed transition message. */
198
+ commitMessage?: string;
199
+ /** True iff `--reset` deleted the remote `work/<slug>` branch on the arbiter. */
200
+ deletedRemoteBranch?: boolean;
201
+ /** When NOT moved, why (e.g. the slug was in neither needs-attention/ nor in-progress/, or a failed --reset delete). */
202
+ reasonNotMoved?: string;
203
+ }
204
+
205
+ export interface SurfaceToNeedsAttentionOptions {
206
+ /**
207
+ * The working clone the move is ORIGINATED from — purely the ORIGIN SOURCE
208
+ * (it resolves the arbiter remote + holds the object store the plumbing writes
209
+ * into), NEVER a write TARGET. Tree-less: the cwd index/HEAD/working tree are
210
+ * never touched (parity with {@link returnToBacklog}).
211
+ */
212
+ cwd: string;
213
+ /** The slug of the in-progress item to surface to needs-attention. */
214
+ slug: string;
215
+ /** Why the item is stuck (terminal continue-push failure, rebase conflict, …). */
216
+ reason: string;
217
+ /** Any questions the agent surfaced for the human, recorded under the reason. */
218
+ questions?: string[];
219
+ /**
220
+ * The arbiter remote the surface move is CAS-published to. REQUIRED — like
221
+ * {@link returnToBacklog}, the move is a tree-less compare-and-swap to the
222
+ * arbiter ref, so there is no local-only mode.
223
+ */
224
+ arbiter: string;
225
+ /** Environment for child git processes (identity etc.). */
226
+ env?: NodeJS.ProcessEnv;
227
+ /** Sink for human-readable progress notes. */
228
+ note?: (message: string) => void;
229
+ }
230
+
231
+ export interface SurfaceToNeedsAttentionResult {
232
+ /** True iff the item was surfaced (moved + CAS-published) on the arbiter. */
233
+ moved: boolean;
234
+ /** When `moved`, the committed transition message. */
235
+ commitMessage?: string;
236
+ /** When NOT moved, why (no arbiter, item not on the arbiter, contention exhausted). */
237
+ reasonNotMoved?: string;
238
+ }
239
+
240
+ /**
241
+ * Save the RECOVERABLE half of a stuck-item bounce (ADR §12). The RUNNER calls
242
+ * this; the build agent never does.
243
+ *
244
+ * **Post `ledger-status-per-item-lock-refs` cut-over (tasks 9a–9d, decision i+):**
245
+ * a bounce is now a PURE LOCK AMEND — the seam (`bounceToStuckLock` →
246
+ * `markStuckItemLock`) marks the per-item lock `state: stuck` and records the
247
+ * reason/questions ON THE LOCK ENTRY. There is NO `git mv` to a
248
+ * `needs-attention/` folder and NO on-`main` surface commit. The item body never
249
+ * moves (it rests in `backlog/` since claim stopped moving it, task 9a). So what
250
+ * REMAINS here is purely the never-lose-work half:
251
+ *
252
+ * 1. A **wip** commit on the `work/<slug>` branch tip holding whatever the agent
253
+ * left uncommitted (`git add -A`). Skipped when the tree is already clean
254
+ * (no aborted work to save). No bookkeeping trailer — after the cut-over NO
255
+ * transient-status move-only commit lands on a branch, so a branch rebases
256
+ * PLAINLY with nothing to drop (`drop-bookkeeping-rebase` is deleted, 9d).
257
+ * 2. Optionally PUSH the work branch to the arbiter so the saved wip travels
258
+ * cross-machine and a `requeue` continues from the branch tip. BEST-EFFORT
259
+ * (an unreachable arbiter leaves the local branch standing — recovery
260
+ * degrades, never crashes the bounce; retried with bounded backoff on an
261
+ * outage), BRANCH-PARAMETERISED (default `work/<slug>`; an explicit `branch`
262
+ * overrides — the tasking bounce passes `work/prds/ready-<slug>`; `pushBranch: false`
263
+ * ⇒ push NOTHING), and EMPTINESS-GUARDED (a branch with no commits beyond
264
+ * main, or an absent branch, is skipped). The branch MUST be the one HEAD is
265
+ * on. The work-branch push is NOT a `main` write.
266
+ *
267
+ * The stuck STATE itself (the `state: stuck` + reason/questions) is owned by the
268
+ * lock amend in the seam, not by this function. NEVER throws for the expected
269
+ * case — returns `{moved, ...}` so consumers can branch cleanly; genuine git
270
+ * plumbing failures still throw (they are unexpected).
271
+ */
272
+ export async function routeToNeedsAttention(
273
+ options: RouteToNeedsAttentionOptions,
274
+ ): Promise<RouteToNeedsAttentionResult> {
275
+ const note = options.note ?? (() => {});
276
+ const {cwd, slug, env} = options;
277
+
278
+ // TASK `cutover-needs-attention-becomes-lock-stuck-recovery-surface`
279
+ // (decision i+): the bounce is now a PURE lock amend (done by the seam) — there
280
+ // is NO `git mv` to `needs-attention/` and NO on-`main` surface. What REMAINS
281
+ // here is the RECOVERABLE half: SAVE the agent's uncommitted work as a wip
282
+ // commit on the `work/<slug>` branch tip and PUSH the branch to the arbiter, so
283
+ // the partial work travels cross-machine and a `requeue` continues from the
284
+ // branch tip. The reason/questions ride on the lock entry (the seam amends it),
285
+ // NOT a moved `.md`. The work branch push is NOT a `main` write.
286
+
287
+ // 1. WIP commit: save whatever the agent left uncommitted to the work branch
288
+ // tip. Skip when the tree is clean (no aborted work to save). NOTE this no
289
+ // longer needs a folder source — the body rests in `backlog/` (task 9a) and
290
+ // never moves on a bounce.
291
+ gitHard(['add', '-A'], cwd, env);
292
+ const hadWip = !nothingStaged(cwd, env);
293
+ if (hadWip) {
294
+ // Save the agent's uncommitted work as a plain wip commit on the work-branch
295
+ // tip (the RECOVERABLE half — a `requeue` continues from it). No bookkeeping
296
+ // trailer: after the per-item-lock cut-over (tasks 9a–9d) NO transient status
297
+ // (no `needs-attention/` move-only commit) lands on a branch, so a branch cut
298
+ // from `main` rebases PLAINLY with nothing to drop — the `drop-bookkeeping-rebase`
299
+ // machinery and its `Dorfl-Bookkeeping` trailer are gone (9d).
300
+ const wipBody = `chore(${slug}): save aborted work (wip)`;
301
+ gitHard(['commit', '-q', '-m', wipBody], cwd, env);
302
+ }
303
+ const commitMessage = `chore(${slug}): bounce to stuck; ${options.reason}`;
304
+ const moveCommit = hadWip ? revParseHead(cwd, env) : undefined;
305
+ note(`Bounced '${slug}' to stuck (lock): ${options.reason}`);
306
+
307
+ // 2. Push the work branch to the arbiter — the RECOVERABLE half of the bounce
308
+ // (so the saved wip travels cross-machine and a requeue continues from the
309
+ // branch tip). Three behaviours: SURFACE-ONLY (no push) when
310
+ // `pushBranch === false`; an explicit `branch` target; else the default
311
+ // `work/<slug>`. BEST-EFFORT (no throw on a failed/unreachable push), RETRIED
312
+ // with bounded backoff on an OUTAGE, and EMPTINESS-GUARDED (a branch with no
313
+ // work beyond main / an absent branch is skipped). The OUTCOME is CAPTURED +
314
+ // RETURNED (`branchPush`) so the caller reports what ACTUALLY landed.
315
+ let branchPush: BranchPushOutcome = 'not-attempted';
316
+ let pushError: string | undefined;
317
+ if (options.arbiter && options.pushBranch !== false) {
318
+ // DEFAULT to the task-namespaced build-bounce branch; a non-task caller
319
+ // (the tasking bounce) passes its own `work/prds/ready-<slug>` via `branch`.
320
+ const branch = options.branch ?? workBranchRef('task', slug);
321
+ if (branchAheadOf(cwd, branch, 'main', env)) {
322
+ const arbiter = options.arbiter;
323
+ const result = await retryWithBackoff(
324
+ async () => {
325
+ const r = gitSoftRun(
326
+ ['push', arbiter, `${branch}:${branch}`],
327
+ cwd,
328
+ env,
329
+ );
330
+ return r.status === 0
331
+ ? {ok: true as const, value: undefined}
332
+ : {ok: false as const, error: r.stderr.trim()};
333
+ },
334
+ {sleep: options.sleep ?? realSleep, ...options.backoff},
335
+ );
336
+ if (result.ok) {
337
+ branchPush = 'pushed';
338
+ } else {
339
+ branchPush = 'failed';
340
+ pushError = result.lastError;
341
+ note(
342
+ `Could not push ${branch} to ${arbiter} after ${result.attempts} ` +
343
+ `attempt(s) (${pushError ?? 'unknown error'}) — the work is saved ` +
344
+ 'LOCALLY only; push the branch when online, then `requeue`.',
345
+ );
346
+ }
347
+ } else {
348
+ branchPush = 'skipped-empty';
349
+ note(
350
+ `Skipped pushing ${branch} (no work beyond main / branch absent) — ` +
351
+ 'nothing to recover.',
352
+ );
353
+ }
354
+ }
355
+
356
+ return {moved: true, commitMessage, moveCommit, branchPush, pushError};
357
+ }
358
+
359
+ /**
360
+ * The clean re-queue (ADR §12 / WORK-CONTRACT return path): once the human has
361
+ * resolved the cause, move the stuck item back to `work/backlog/<slug>.md` and
362
+ * commit it so the item can be re-claimed (it must not rot stuck). It recovers a
363
+ * task stuck in EITHER `work/needs-attention/<slug>.md` (the resolved-surface
364
+ * path) OR `work/in-progress/<slug>.md` (a claim that never surfaced — an
365
+ * un-surfaced abort, a killed run, or an in-place requeue note; defect 2, story
366
+ * 4): the slug's ACTUAL current folder is resolved on the arbiter and moved to
367
+ * `backlog/` via the SAME tree-less CAS. Any recorded reason/handoff block stays
368
+ * in the body as a durable note of what happened; the resolution itself is the
369
+ * human's.
370
+ *
371
+ * The `requeue` verb's THREE behaviours (ADR §14 / task
372
+ * `requeue-continue-and-reset`) are realised here:
373
+ * - **default = KEEP + CONTINUE.** The `work/<slug>` branch is left UNTOUCHED;
374
+ * it is the durable artifact the next claim CONTINUES from (the continue-
375
+ * detection in `continue-branch.ts` feeds both onboarding paths). This
376
+ * function only does the ledger move.
377
+ * - **`--reset` = DISCARD + FRESH.** When `reset` is set, DELETE the remote
378
+ * `work/<slug>` branch on `arbiter` FIRST (+ drop any stale local branch),
379
+ * THEN the backlog move. Delete-before-move closes the claim-race window; a
380
+ * FAILED delete ABORTS (no backlog move) so the item stays in
381
+ * needs-attention. The next claim then finds NO arbiter branch and cuts
382
+ * fresh — no special claim-time logic.
383
+ * - **`-m "<note>"` = HANDOFF NOTE.** When `message` is set, APPEND a dated
384
+ * `## Requeue YYYY-MM-DD` section to the item BODY (append-only; accumulates
385
+ * over repeated requeues) for the next agent. Applies to BOTH modes.
386
+ *
387
+ * Like the move, NEVER throws for the expected "not in needs-attention" case.
388
+ */
389
+ /**
390
+ * Read the item's lock entry from the LOCAL lock ref (no fetch) — the resilient
391
+ * fall-back for {@link returnToBacklog} when an arbiter fetch fails (e.g. a
392
+ * `--reset` against a moved-away arbiter). The up-front soft fetch already
393
+ * refreshed the local refs, so reading them locally is the best-effort truth
394
+ * without throwing.
395
+ */
396
+ async function readLocalItemLock(
397
+ slug: string,
398
+ cwd: string,
399
+ env: NodeJS.ProcessEnv | undefined,
400
+ ): Promise<LockEntry | undefined> {
401
+ const ref = itemLockRef(lockEntryFor(`task:${slug}`));
402
+ const show = await gitSoftAsync(['show', `${ref}:lock.md`], cwd, env);
403
+ if (show.status !== 0) {
404
+ return undefined;
405
+ }
406
+ return parseLockEntry(show.stdout);
407
+ }
408
+
409
+ export async function returnToBacklog(
410
+ options: ReturnToBacklogOptions,
411
+ ): Promise<ReturnToBacklogResult> {
412
+ const note = options.note ?? (() => {});
413
+ const {cwd, slug, env} = options;
414
+
415
+ // Tree-less CAS, EXACTLY like `claim` (`performClaim`): the move is published
416
+ // to the arbiter ref via the shared `ledger-write` write seam — it NEVER stages
417
+ // or commits in the cwd working tree (so a `requeue` in a shared checkout can no
418
+ // longer sweep up a concurrent writer's uncommitted files — the `8c92f63`
419
+ // incident, see
420
+ // `work/notes/observations/drive-backlog-skill-assumes-in-place-do-not-remote.md`).
421
+ // `--cwd` is purely the ORIGIN SOURCE (it resolves the arbiter remote + holds
422
+ // the object store the plumbing writes into), never a write TARGET. A tree-less
423
+ // CAS needs a ref to push to, so an `arbiter` is REQUIRED (parity with `claim`).
424
+ if (!options.arbiter) {
425
+ return {
426
+ moved: false,
427
+ reasonNotMoved:
428
+ `requeue for '${slug}' needs an --arbiter: the move is published as a ` +
429
+ 'tree-less compare-and-swap to the arbiter ref (like claim), so there ' +
430
+ 'is no local-only mode — pass --arbiter.',
431
+ };
432
+ }
433
+ const arbiter = options.arbiter;
434
+
435
+ if (
436
+ (await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
437
+ ) {
438
+ return {
439
+ moved: false,
440
+ reasonNotMoved: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
441
+ };
442
+ }
443
+
444
+ // Refresh the remote-tracking refs so every check below (the item's residence,
445
+ // the continue-branch guard, the CAS base) sees the arbiter's TRUTH, not a stale
446
+ // local copy. This is a fetch, not a checkout — the working tree is untouched.
447
+ await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
448
+
449
+ // Is the item LOCK-HELD on the arbiter? (task
450
+ // `cutover-needs-attention-becomes-lock-stuck-recovery-surface`, decision i+:
451
+ // stuck-state is the per-item lock `state: stuck`, NOT a `needs-attention/`
452
+ // folder file). `requeue` recovers a STUCK hold (the resolved-recovery path) and
453
+ // tolerates an ACTIVE hold (a killed run that never surfaced) — both legitimate
454
+ // in-flight states the human's recovery verb returns to the pool. We read the
455
+ // LOCK ref (arbiter-is-truth), NOT a folder. No held lock ⇒ nothing to requeue
456
+ // (the item is already at rest — unclaimed in `backlog/`, or terminal).
457
+ // Read the held lock TOLERANTLY: a broken/unreachable arbiter (e.g. a
458
+ // `--reset` against a moved-away arbiter) must NOT throw out of requeue — the
459
+ // up-front soft fetch above already refreshed the local lock refs, so on a
460
+ // fetch fault we fall back to the local lock ref (best-effort) rather than
461
+ // crashing. A genuinely absent lock still refuses below.
462
+ let held: Awaited<ReturnType<typeof readItemLock>>;
463
+ try {
464
+ held = await readItemLock({item: `task:${slug}`, cwd, arbiter, env});
465
+ } catch {
466
+ held = await readLocalItemLock(slug, cwd, env);
467
+ }
468
+ if (!held) {
469
+ return {
470
+ moved: false,
471
+ reasonNotMoved:
472
+ `'${slug}' has no held per-item lock on ${arbiter} — nothing to requeue ` +
473
+ '(wrong slug, or already at rest in backlog/done?). requeue recovers a ' +
474
+ 'task whose lock is held stuck (needs-attention) or active (a killed ' +
475
+ 'in-progress run).',
476
+ };
477
+ }
478
+
479
+ // `--reset`: DELETE the remote work branch (before the backlog move). The
480
+ // deletion is WRITE-THROUGH: the LOCAL refs that drive continue-detection
481
+ // (`refs/remotes/<arbiter>/work/<slug>` AND any local head `work/<slug>`) are
482
+ // deleted FIRST, THEN the arbiter `git push --delete`. The asymmetry is the
483
+ // point: the arbiter is the source of truth and the local ref is derived, so
484
+ // inverting today's arbiter-first ordering converts a dangerous failure mode
485
+ // (local AHEAD of arbiter — a permanent stale-continue) into a self-healing
486
+ // one (local BEHIND — the next fetch restores it from the arbiter). A FAILED
487
+ // arbiter delete still ABORTS the requeue (no backlog move); the local
488
+ // behind-state is recoverable by a subsequent fetch and CANNOT drive a wrong
489
+ // continue. Delete-before-move also closes the claim-race window.
490
+ let deletedRemoteBranch = false;
491
+ if (options.reset) {
492
+ const branch = workBranchRef('task', slug);
493
+ // LOCAL-FIRST: the tracking ref `branchAheadOf` reads (the one whose
494
+ // staleness today silently turns `--reset` into a no-op — verified live in
495
+ // `work/notes/observations/requeue-reset-does-not-prune-hub-mirror-stale-branch-ref.md`,
496
+ // where `--reset` deleted the arbiter branch but the local tracking ref
497
+ // survived and resurrected a "continue" on the next `do`). Both deletes
498
+ // are best-effort — their absence is fine, what matters is they are not
499
+ // LEFT BEHIND when the arbiter delete succeeds.
500
+ await gitSoftAsync(
501
+ ['update-ref', '-d', `refs/remotes/${arbiter}/${branch}`],
502
+ cwd,
503
+ env,
504
+ );
505
+ await gitSoftAsync(['branch', '-D', branch], cwd, env);
506
+ // THEN the arbiter delete (explicit/guarded departure from the "never delete
507
+ // the remote branch" invariant; only on the `--reset` path, never the
508
+ // default).
509
+ const del = await gitSoftAsync(
510
+ ['push', arbiter, '--delete', branch],
511
+ cwd,
512
+ env,
513
+ );
514
+ if (del.status !== 0) {
515
+ const stderr = del.stderr.trim();
516
+ // Tolerate "remote ref does not exist" (already gone): treat as deleted.
517
+ const alreadyGone = /remote ref does not exist|unable to delete/i.test(
518
+ stderr,
519
+ );
520
+ if (!alreadyGone) {
521
+ const message =
522
+ `requeue --reset for '${slug}': failed to delete the remote branch ` +
523
+ `${branch} on ${arbiter} (${stderr || 'unknown error'}); ` +
524
+ 'aborting the requeue — item left in needs-attention (no backlog move). ' +
525
+ 'The local tracking ref was already cleared (write-through ordering); ' +
526
+ 'a subsequent fetch will restore it from the arbiter — the local store ' +
527
+ 'is BEHIND the arbiter (self-healing), never AHEAD (which would drive a ' +
528
+ 'stale continue).';
529
+ note(message);
530
+ return {moved: false, reasonNotMoved: message};
531
+ }
532
+ }
533
+ deletedRemoteBranch = true;
534
+ note(`Deleted the remote branch ${branch} on ${arbiter} (--reset).`);
535
+ }
536
+
537
+ // DEFAULT (keep+continue) REQUEUE-SAFETY GUARD: a claimable item's continue-
538
+ // branch MUST be reachable by ANY worker, so before releasing the lock verify
539
+ // the ARBITER branch `<arbiter>/work/<slug>` exists + is ahead of main — the
540
+ // EXACT "is the continue-branch on the arbiter?" question the continue-path asks
541
+ // in `isolation.ts`. We check the ARBITER ref (already fetched above), NOT the
542
+ // local `work/<slug>` (which SURVIVES a failed push). NOT on `--reset` (which
543
+ // discards the branch by design).
544
+ if (!options.reset) {
545
+ const branch = workBranchRef('task', slug);
546
+ const onArbiter = branchAheadOf(
547
+ cwd,
548
+ `${arbiter}/${branch}`,
549
+ `${arbiter}/main`,
550
+ env,
551
+ );
552
+ if (!onArbiter) {
553
+ const message =
554
+ `the work branch ${branch} isn't on ${arbiter} (the continue ` +
555
+ `branch a cross-machine worker would resume from) — push it first, or ` +
556
+ '`requeue --reset` to discard and start fresh. Item left stuck (lock not ' +
557
+ 'released).';
558
+ note(message);
559
+ return {moved: false, reasonNotMoved: message};
560
+ }
561
+ }
562
+
563
+ const commitMessage = `chore(${slug}): return to backlog for re-claiming`;
564
+ const handoff =
565
+ options.message && options.message.trim() !== ''
566
+ ? options.message.trim()
567
+ : undefined;
568
+ // The body rests EITHER in the pool (`tasks-ready`) OR — for a staged item driven
569
+ // with `--allow-backlog` — in staging (`tasks-backlog`). Probe in the SAME
570
+ // precedence `resolveTask`/`--allow-backlog` uses (ready first, then backlog), so
571
+ // the handoff note finds a staged body too (obs
572
+ // `requeue-dash-m-fails-and-strands-lock-for-staged-backlog-item`).
573
+ const bodyResidenceCandidates: readonly WorkFolderKey[] = [
574
+ 'tasks-ready',
575
+ 'tasks-backlog',
576
+ ];
577
+
578
+ // `-m "<note>"` (the handoff steer): APPEND a dated `## Requeue YYYY-MM-DD`
579
+ // section to the item BODY where it already rests (pool or staging), via the SAME
580
+ // tree-less CAS move (same-folder rewrite with the body transform) — it NEVER
581
+ // stages/commits in the cwd tree. The handoff is OPTIONAL and NON-FATAL: a failed
582
+ // append degrades to a WARNING and the lock release below STILL runs, because the
583
+ // lock release is the load-bearing recovery and must never be stranded by an
584
+ // optional note (obs `requeue-dash-m-fails-and-strands-lock-for-staged-backlog-item`).
585
+ if (handoff !== undefined) {
586
+ const noted = await runTreelessLedgerMove({
587
+ cwd,
588
+ slug,
589
+ arbiter,
590
+ kind: 'requeue',
591
+ onContended: 'requeue',
592
+ explicitMainRefspec: false,
593
+ env,
594
+ note,
595
+ plan: (base) => {
596
+ const bodyRel = bodyResidenceCandidates
597
+ .map((folder) => workItemRel(folder, `${slug}.md`))
598
+ .find((rel) => pathInCommit(base, rel, cwd, env));
599
+ if (bodyRel === undefined) {
600
+ // The body is in neither tasks/ready/ nor tasks/backlog/ on this base —
601
+ // nothing to annotate (the durable move that placed it must land first).
602
+ return 'missing';
603
+ }
604
+ return prepareTreelessMoveCommit({
605
+ cwd,
606
+ slug,
607
+ base,
608
+ sourceRel: bodyRel,
609
+ destRel: bodyRel,
610
+ transformBody: (body) => appendRequeueNoteText(body, handoff),
611
+ commitMessage: `chore(${slug}): requeue handoff note`,
612
+ refNamespace: 'requeue',
613
+ env,
614
+ });
615
+ },
616
+ });
617
+ if (!noted) {
618
+ // NON-FATAL: warn and fall through to the lock release. We do NOT strand the
619
+ // lock for a failed OPTIONAL note (the previous behaviour, which left a
620
+ // half-applied state: branch deleted on --reset, lock still held).
621
+ note(
622
+ `requeue for '${slug}': could not append the -m handoff note (the body ` +
623
+ `is in neither tasks/ready/ nor tasks/backlog/ on ${arbiter}/main, or ` +
624
+ 'main kept moving). Releasing the lock anyway — the requeue still ' +
625
+ 'recovers the item; only the note was skipped.',
626
+ );
627
+ }
628
+ }
629
+
630
+ // RELEASE the held lock (`stuck → released` / give up the hold): the item
631
+ // returns to the claimable pool (its body already rests in `backlog/`). Use the
632
+ // tolerant {@link releaseItemLock} (idempotent) so requeue recovers BOTH a
633
+ // `stuck` hold (the resolved-recovery path) and an `active` hold (a killed run
634
+ // that never surfaced) — the human asserting "put it back".
635
+ const released = await releaseItemLock({
636
+ item: `task:${slug}`,
637
+ cwd,
638
+ arbiter,
639
+ env,
640
+ });
641
+ if (released.outcome === 'error') {
642
+ const message =
643
+ `requeue for '${slug}': could not release the per-item lock ` +
644
+ `(${released.message}). The item is left stuck. Try again shortly.`;
645
+ note(message);
646
+ return {moved: false, reasonNotMoved: message};
647
+ }
648
+ note(
649
+ `Returned '${slug}' to backlog (released the lock; body rests in pool).`,
650
+ );
651
+ return {moved: true, commitMessage, deletedRemoteBranch};
652
+ }
653
+
654
+ /**
655
+ * **Promote a STAGED task into the agent-eligible pool** (prd
656
+ * `staging-pool-position-gate-and-trust-model`, task
657
+ * `pre-backlog-staging-folder-and-promote-step-a`, governing ADR
658
+ * `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Moves
659
+ * `work/pre-backlog/<slug>.md → work/backlog/<slug>.md` as a durable `main`
660
+ * move, the same category as {@link returnToBacklog} (tree-less CAS via
661
+ * {@link runTreelessLedgerMove}). After this transition the task is in the
662
+ * pool and claimable.
663
+ *
664
+ * **RUNNER/human-owned.** There is no agent-facing path that performs this:
665
+ * the agent's tasking output lands STAGED in `work/pre-backlog/` (the runner's
666
+ * deterministic placement decision), and only a runner/human invocation moves
667
+ * it into the pool. The agent does no git here, as everywhere.
668
+ *
669
+ * Storage-agnostic: it names the slug + the arbiter, NOT *where* the move
670
+ * lands; the sole strategy publishes to `<arbiter>/main`. Like
671
+ * {@link returnToBacklog} the tree-less CAS needs a ref to push to, so an
672
+ * `arbiter` is REQUIRED. NEVER throws for the expected
673
+ * "not in pre-backlog/" / contention-exhausted cases — it returns
674
+ * `{moved: false, reasonNotMoved}` so callers can branch cleanly.
675
+ */
676
+ export interface PromoteFromPreBacklogOptions {
677
+ /**
678
+ * The working clone the move is ORIGINATED from — purely the ORIGIN SOURCE
679
+ * (it resolves the arbiter remote + holds the object store the plumbing
680
+ * writes into), NEVER a write TARGET. Tree-less: the cwd index/HEAD/working
681
+ * tree are never touched (parity with {@link returnToBacklog}).
682
+ */
683
+ cwd: string;
684
+ /** The slug of the staged task to promote into the pool. */
685
+ slug: string;
686
+ /**
687
+ * The arbiter remote the promotion is CAS-published to. REQUIRED — the
688
+ * tree-less CAS needs a ref to push to (parity with `requeue`/`claim`).
689
+ */
690
+ arbiter: string;
691
+ /** Environment for child git processes (identity etc.). */
692
+ env?: NodeJS.ProcessEnv;
693
+ /** Sink for human-readable progress notes. */
694
+ note?: (message: string) => void;
695
+ }
696
+
697
+ export interface PromoteFromPreBacklogResult {
698
+ /** True iff the staged task was moved into the pool + committed. */
699
+ moved: boolean;
700
+ /** When `moved`, the committed transition message. */
701
+ commitMessage?: string;
702
+ /** When NOT moved, why (no such pre-backlog item, already in backlog, contention). */
703
+ reasonNotMoved?: string;
704
+ }
705
+
706
+ export async function promoteFromPreBacklog(
707
+ options: PromoteFromPreBacklogOptions,
708
+ ): Promise<PromoteFromPreBacklogResult> {
709
+ const note = options.note ?? (() => {});
710
+ const {cwd, slug, env} = options;
711
+
712
+ if (!options.arbiter) {
713
+ return {
714
+ moved: false,
715
+ reasonNotMoved:
716
+ `promote for '${slug}' needs an --arbiter: the move is published as a ` +
717
+ 'tree-less compare-and-swap to the arbiter ref (like requeue/claim), so ' +
718
+ 'there is no local-only mode — pass --arbiter.',
719
+ };
720
+ }
721
+ const arbiter = options.arbiter;
722
+
723
+ if (
724
+ (await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
725
+ ) {
726
+ return {
727
+ moved: false,
728
+ reasonNotMoved: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
729
+ };
730
+ }
731
+
732
+ // Refresh `<arbiter>/main` so the residence probe + the CAS base see the
733
+ // arbiter's TRUTH. A fetch, not a checkout — the working tree is untouched.
734
+ await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
735
+
736
+ // UNIFIED PER-ITEM LOCK around the CAS window (prd
737
+ // `staging-surface-and-apply-promote-safety`, task
738
+ // `f3b-promote-takes-per-item-advancing-lock`): promote and apply BOTH key onto
739
+ // the item's `refs/dorfl/lock/<entry>` ref with `action: advance` (the
740
+ // SAME action `apply` takes via `advancing-lock.ts`), so an apply mid-flight
741
+ // and a promote attempt on the SAME item are mutually exclusive BY
742
+ // CONSTRUCTION (the second acquirer loses the create-only ref CAS). Reusing
743
+ // the existing `advance` action value (rather than introducing a distinct
744
+ // `'promote'` axis) is deliberate — the lock entry is keyed on the item
745
+ // identity, and what matters is that ALL three transitions of one item
746
+ // (implement/task/advance) serialise on ONE ref. A lock `lost` exits CLEAN
747
+ // (no partial state on `main`, mirroring claim-cas loss semantics); on success
748
+ // or failure we release the lock in `finally`. Crash-safe release mirrors the
749
+ // apply rung: a crashed promote leaves an `advance`-active lock that the
750
+ // existing `release-lock` / `gc --ledger` recovery surface clears.
751
+ const item = `task:${slug}`;
752
+ const acquired = await acquireItemLock({
753
+ item,
754
+ action: 'advance',
755
+ cwd,
756
+ arbiter,
757
+ env,
758
+ });
759
+ if (acquired.outcome !== 'acquired') {
760
+ const message =
761
+ acquired.outcome === 'lost'
762
+ ? `promote for '${slug}' lost the per-item lock race (another implement/task/advance hold is in flight). No move on ${arbiter}/main. Try again shortly.`
763
+ : `promote for '${slug}': could not acquire the per-item lock (${acquired.message}).`;
764
+ note(message);
765
+ return {moved: false, reasonNotMoved: message};
766
+ }
767
+ try {
768
+ const sourceRel = workItemRel('tasks-backlog', `${slug}.md`);
769
+ const destRel = workItemRel('tasks-ready', `${slug}.md`);
770
+
771
+ // Early-exit message: if NEITHER staged nor already-in-pool, there is
772
+ // nothing to promote (the per-attempt `plan` is the authoritative
773
+ // resolution against the live base).
774
+ const hasSource =
775
+ (
776
+ await gitSoftAsync(
777
+ ['cat-file', '-e', `${arbiter}/main:${sourceRel}`],
778
+ cwd,
779
+ env,
780
+ )
781
+ ).status === 0;
782
+ const hasDest =
783
+ (
784
+ await gitSoftAsync(
785
+ ['cat-file', '-e', `${arbiter}/main:${destRel}`],
786
+ cwd,
787
+ env,
788
+ )
789
+ ).status === 0;
790
+ if (!hasSource && !hasDest) {
791
+ const message =
792
+ `'${slug}' is not staged in work/pre-backlog/ on ${arbiter}/main (and not ` +
793
+ 'already in work/backlog/) — nothing to promote (wrong slug, or never ' +
794
+ 'staged?).';
795
+ note(message);
796
+ return {moved: false, reasonNotMoved: message};
797
+ }
798
+
799
+ const commitMessage = `chore(${slug}): promote work/pre-backlog/ -> work/backlog/`;
800
+ const moved = await runTreelessLedgerMove({
801
+ cwd,
802
+ slug,
803
+ arbiter,
804
+ kind: 'promote',
805
+ onContended: 'promote',
806
+ // The surface direction's main-only refspec works here too: this runs in
807
+ // the project checkout, but we only need `<arbiter>/main` resolved, and
808
+ // the explicit refspec is the safer default (mirrors `surface`).
809
+ explicitMainRefspec: true,
810
+ env,
811
+ note,
812
+ plan: (base) => {
813
+ // If already in the pool on this base, a prior attempt landed
814
+ // (idempotent).
815
+ if (pathInCommit(base, destRel, cwd, env)) {
816
+ return 'already-done';
817
+ }
818
+ if (!pathInCommit(base, sourceRel, cwd, env)) {
819
+ return 'missing';
820
+ }
821
+ return prepareTreelessMoveCommit({
822
+ cwd,
823
+ slug,
824
+ base,
825
+ sourceRel,
826
+ destRel,
827
+ // The body is carried byte-for-byte from pre-backlog into the
828
+ // pool — promotion is a placement decision, not a content transform.
829
+ transformBody: (body) => body,
830
+ commitMessage,
831
+ refNamespace: 'promote',
832
+ env,
833
+ });
834
+ },
835
+ });
836
+ if (moved) {
837
+ note(`Promoted '${slug}' from pre-backlog to backlog (claimable).`);
838
+ return {moved: true, commitMessage};
839
+ }
840
+
841
+ const message =
842
+ `promote for '${slug}': the arbiter's main kept moving (contended) after ` +
843
+ `${TREELESS_CONTENTION_ATTEMPTS} attempts — item left in pre-backlog (no ` +
844
+ 'move). Try again shortly.';
845
+ note(message);
846
+ return {moved: false, reasonNotMoved: message};
847
+ } finally {
848
+ await releaseItemLock({item, cwd, arbiter, env});
849
+ }
850
+ }
851
+
852
+ /**
853
+ * **Promote a STAGED prd into the auto-task pool** (prd
854
+ * `staging-pool-position-gate-and-trust-model`, task
855
+ * `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
856
+ * `placement-is-runner-deterministic-humanonly-is-agent-judgement`). The prd
857
+ * twin of {@link promoteFromPreBacklog}: moves
858
+ * `work/prds/proposed/<slug>.md → work/prds/ready/<slug>.md` as a durable `main` move
859
+ * (tree-less CAS via {@link runTreelessLedgerMove}). After this transition
860
+ * the prd is in the auto-task POOL and eligible to be auto-tasked (subject
861
+ * to the existing `autoTask`/`humanOnly`/`needsAnswers`/`taskedAfter` gates,
862
+ * which are UNCHANGED — the staging/pool split changes only WHICH folder is
863
+ * the auto-task pool, not the gates).
864
+ *
865
+ * **RUNNER/human-owned.** There is no agent-facing path that performs this:
866
+ * `intake`'s `prd` dispatch lands the prd STAGED in `work/prds/proposed/` (the
867
+ * runner's deterministic placement decision), and only a runner/human
868
+ * invocation moves it into the pool. The agent does no git here, as
869
+ * everywhere; this function is not reachable from any agent surface.
870
+ *
871
+ * Storage-agnostic + tree-less, exactly like {@link promoteFromPreBacklog}:
872
+ * cwd index/HEAD/working tree are never touched, an arbiter remote is
873
+ * REQUIRED, and "not in prds/proposed/" / contention-exhausted cases are returned
874
+ * (NEVER thrown) via `{moved: false, reasonNotMoved}` so callers branch
875
+ * cleanly. Idempotent: re-running after the move LANDED is a no-op success.
876
+ */
877
+ export interface PromoteFromPreSpecOptions {
878
+ /** The working clone the move is originated from (origin source only; never written). */
879
+ cwd: string;
880
+ /** The slug of the staged prd to promote into the pool. */
881
+ slug: string;
882
+ /** The arbiter remote the promotion is CAS-published to. REQUIRED. */
883
+ arbiter: string;
884
+ /** Environment for child git processes (identity etc.). */
885
+ env?: NodeJS.ProcessEnv;
886
+ /** Sink for human-readable progress notes. */
887
+ note?: (message: string) => void;
888
+ }
889
+
890
+ export interface PromoteFromPreSpecResult {
891
+ /** True iff the staged prd was moved into the pool + committed. */
892
+ moved: boolean;
893
+ /** When `moved`, the committed transition message. */
894
+ commitMessage?: string;
895
+ /** When NOT moved, why (no such prds/proposed item, already in prds/ready/, contention). */
896
+ reasonNotMoved?: string;
897
+ }
898
+
899
+ export async function promoteFromPreSpec(
900
+ options: PromoteFromPreSpecOptions,
901
+ ): Promise<PromoteFromPreSpecResult> {
902
+ const note = options.note ?? (() => {});
903
+ const {cwd, slug, env} = options;
904
+
905
+ if (!options.arbiter) {
906
+ return {
907
+ moved: false,
908
+ reasonNotMoved:
909
+ `promote for '${slug}' needs an --arbiter: the move is published as ` +
910
+ 'a tree-less compare-and-swap to the arbiter ref (like requeue/claim), so ' +
911
+ 'there is no local-only mode — pass --arbiter.',
912
+ };
913
+ }
914
+ const arbiter = options.arbiter;
915
+
916
+ if (
917
+ (await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
918
+ ) {
919
+ return {
920
+ moved: false,
921
+ reasonNotMoved: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
922
+ };
923
+ }
924
+
925
+ // Refresh `<arbiter>/main` so the residence probe + the CAS base see the
926
+ // arbiter's TRUTH. A fetch, not a checkout — the working tree is untouched.
927
+ await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
928
+
929
+ // UNIFIED PER-ITEM LOCK around the CAS window — symmetric with
930
+ // {@link promoteFromPreBacklog} (prd `staging-surface-and-apply-promote-safety`,
931
+ // task `f3b-promote-takes-per-item-advancing-lock`, decisive prd q4 answer:
932
+ // specs share the apply×promote mutual-exclusion fix with tasks). The lock
933
+ // keys on `spec:${slug}` (a distinct ref from a task with the same slug, via
934
+ // {@link lockEntryFor}'s `<type>-<slug>` encoding), with `action: advance` —
935
+ // the SAME action an apply for a spec would take — so spec promote and spec
936
+ // apply on the same item are mutually exclusive by construction. MIGRATE step
937
+ // (prd `prd-to-spec-vocabulary-cutover-and-migration-command`): the lock
938
+ // identity is `spec:${slug}` to match the `spec-<slug>` entry the tasking/apply
939
+ // path now acquires (`tasking.ts` releases under `spec:${slug}`); a stale
940
+ // `prd:${slug}` here would key a DIFFERENT ref and break the mutual exclusion.
941
+ // Loss / crash semantics mirror the task case.
942
+ const item = `spec:${slug}`;
943
+ const acquired = await acquireItemLock({
944
+ item,
945
+ action: 'advance',
946
+ cwd,
947
+ arbiter,
948
+ env,
949
+ });
950
+ if (acquired.outcome !== 'acquired') {
951
+ const message =
952
+ acquired.outcome === 'lost'
953
+ ? `promote for '${slug}' lost the per-item lock race (another implement/task/advance hold is in flight). No move on ${arbiter}/main. Try again shortly.`
954
+ : `promote for '${slug}': could not acquire the per-item lock (${acquired.message}).`;
955
+ note(message);
956
+ return {moved: false, reasonNotMoved: message};
957
+ }
958
+ try {
959
+ const sourceRel = workItemRel('specs-proposed', `${slug}.md`);
960
+ const destRel = workItemRel('specs-ready', `${slug}.md`);
961
+
962
+ const hasSource =
963
+ (
964
+ await gitSoftAsync(
965
+ ['cat-file', '-e', `${arbiter}/main:${sourceRel}`],
966
+ cwd,
967
+ env,
968
+ )
969
+ ).status === 0;
970
+ const hasDest =
971
+ (
972
+ await gitSoftAsync(
973
+ ['cat-file', '-e', `${arbiter}/main:${destRel}`],
974
+ cwd,
975
+ env,
976
+ )
977
+ ).status === 0;
978
+ if (!hasSource && !hasDest) {
979
+ const message =
980
+ `'${slug}' is not staged in ${workFolderPrefix('specs-proposed')} on ${arbiter}/main ` +
981
+ `(and not already in ${workFolderPrefix('specs-ready')}) — nothing to promote ` +
982
+ '(wrong slug, or never staged?).';
983
+ note(message);
984
+ return {moved: false, reasonNotMoved: message};
985
+ }
986
+
987
+ const commitMessage = `chore(${slug}): promote ${workFolderPrefix(
988
+ 'specs-proposed',
989
+ )} -> ${workFolderPrefix('specs-ready')}`;
990
+ const moved = await runTreelessLedgerMove({
991
+ cwd,
992
+ slug,
993
+ arbiter,
994
+ kind: 'promote',
995
+ onContended: 'promote',
996
+ explicitMainRefspec: true,
997
+ env,
998
+ note,
999
+ plan: (base) => {
1000
+ if (pathInCommit(base, destRel, cwd, env)) {
1001
+ return 'already-done';
1002
+ }
1003
+ if (!pathInCommit(base, sourceRel, cwd, env)) {
1004
+ return 'missing';
1005
+ }
1006
+ return prepareTreelessMoveCommit({
1007
+ cwd,
1008
+ slug,
1009
+ base,
1010
+ sourceRel,
1011
+ destRel,
1012
+ // The body is carried byte-for-byte from prds/proposed into the pool —
1013
+ // promotion is a placement decision, not a content transform.
1014
+ transformBody: (body) => body,
1015
+ commitMessage,
1016
+ refNamespace: 'promote',
1017
+ env,
1018
+ });
1019
+ },
1020
+ });
1021
+ if (moved) {
1022
+ note(
1023
+ `Promoted prd '${slug}' from prds/proposed to prds/ready (auto-taskable).`,
1024
+ );
1025
+ return {moved: true, commitMessage};
1026
+ }
1027
+
1028
+ const message =
1029
+ `promote for '${slug}': the arbiter's main kept moving (contended) ` +
1030
+ `after ${TREELESS_CONTENTION_ATTEMPTS} attempts — item left in prds/proposed ` +
1031
+ '(no move). Try again shortly.';
1032
+ note(message);
1033
+ return {moved: false, reasonNotMoved: message};
1034
+ } finally {
1035
+ await releaseItemLock({item, cwd, arbiter, env});
1036
+ }
1037
+ }
1038
+
1039
+ /** One staged item awaiting promotion (a task in `pre-backlog/` or a spec in `specs/proposed/`). */
1040
+ export interface PromotableItem {
1041
+ /** `'task'` (staged in `work/pre-backlog/`) or `'spec'` (staged in `work/specs/proposed/`). */
1042
+ namespace: 'task' | 'spec';
1043
+ /** The slug (filename minus `.md`). */
1044
+ slug: string;
1045
+ }
1046
+
1047
+ export interface ListPromotableOptions {
1048
+ /** The working clone the arbiter remote is resolved FROM (origin source only). */
1049
+ cwd: string;
1050
+ /** The arbiter remote whose `main` the staging folders are read from. REQUIRED. */
1051
+ arbiter: string;
1052
+ /** Environment for child git processes. */
1053
+ env?: NodeJS.ProcessEnv;
1054
+ }
1055
+
1056
+ export interface ListPromotableResult {
1057
+ /** Every staged item awaiting promotion, tasks then prds, each sorted by slug. */
1058
+ items: PromotableItem[];
1059
+ /** When the listing could not run (no such remote), why. */
1060
+ error?: string;
1061
+ }
1062
+
1063
+ /**
1064
+ * LIST every staged item awaiting a runner/human promotion — the tasks in
1065
+ * `work/pre-backlog/` and the prds in `work/prds/proposed/` on `<arbiter>/main` (the
1066
+ * discovery half of the `promote` verb, so `promote` with no argument answers
1067
+ * "what is staged waiting for me?"). It reads the ARBITER's truth (a fetch + a
1068
+ * tree read), NOT the local working tree (which may be stale) — the same source
1069
+ * the promotion functions act against, so the list and the move never disagree.
1070
+ * Read-only: it never fetches a checkout, never moves anything.
1071
+ */
1072
+ export async function listPromotable(
1073
+ options: ListPromotableOptions,
1074
+ ): Promise<ListPromotableResult> {
1075
+ const {cwd, arbiter, env} = options;
1076
+ if (
1077
+ (await gitSoftAsync(['remote', 'get-url', arbiter], cwd, env)).status !== 0
1078
+ ) {
1079
+ return {
1080
+ items: [],
1081
+ error: `no git remote named '${arbiter}' (set one, or pass --arbiter).`,
1082
+ };
1083
+ }
1084
+ // Refresh `<arbiter>/main` so the listing sees the arbiter's TRUTH (a fetch,
1085
+ // not a checkout — the working tree is untouched), exactly as the promote
1086
+ // functions do before their residence probe.
1087
+ await gitSoftAsync(['fetch', '--quiet', arbiter], cwd, env);
1088
+ const tasks = await listMarkdownSlugsInTree(
1089
+ `${arbiter}/main:${workFolderRel('tasks-backlog')}`,
1090
+ cwd,
1091
+ env,
1092
+ );
1093
+ const specs = await listMarkdownSlugsInTree(
1094
+ `${arbiter}/main:${workFolderRel('specs-proposed')}`,
1095
+ cwd,
1096
+ env,
1097
+ );
1098
+ return {
1099
+ items: [
1100
+ ...tasks.map((slug) => ({namespace: 'task' as const, slug})),
1101
+ ...specs.map((slug) => ({namespace: 'spec' as const, slug})),
1102
+ ],
1103
+ };
1104
+ }
1105
+
1106
+ /**
1107
+ * `git ls-tree --name-only <base>` → the `.md` filenames' SLUGS (filename minus
1108
+ * `.md`), sorted. An absent folder on the ref reads as empty (the staging folder
1109
+ * may not exist yet). The bare-repo-safe read (`ls-tree`, no working tree), the
1110
+ * SAME mechanism the ledger read seam uses.
1111
+ */
1112
+ async function listMarkdownSlugsInTree(
1113
+ base: string,
1114
+ cwd: string,
1115
+ env: NodeJS.ProcessEnv | undefined,
1116
+ ): Promise<string[]> {
1117
+ const tree = await gitSoftAsync(['ls-tree', '--name-only', base], cwd, env);
1118
+ if (tree.status !== 0) {
1119
+ return [];
1120
+ }
1121
+ return tree.stdout
1122
+ .split('\n')
1123
+ .map((s) => s.trim())
1124
+ .filter((name) => isWorkItemFile(name))
1125
+ .map((name) => name.replace(/\.md$/i, ''))
1126
+ .sort();
1127
+ }
1128
+
1129
+ /** The contention-retry cap shared by the tree-less requeue + surface moves. */
1130
+ const TREELESS_CONTENTION_ATTEMPTS = 5;
1131
+
1132
+ /**
1133
+ * The plan for ONE attempt of a tree-less move, computed FRESH against the
1134
+ * current (re-fetched) base so a retry never reuses a stale source/blob:
1135
+ * - `{ref, commit}` — a prepared move commit on a throwaway ref, ready to CAS.
1136
+ * - `'already-done'` — the item is ALREADY at the destination on this base (an
1137
+ * idempotent re-surface, or a prior attempt that actually landed but whose CAS
1138
+ * verify reported rejected) — treat as success, no push needed.
1139
+ * - `'missing'` — the item is in NEITHER the source nor the destination folder on
1140
+ * this base — nothing to move.
1141
+ */
1142
+ type TreelessAttemptPlan =
1143
+ | {ref: string; commit: string}
1144
+ | 'already-done'
1145
+ | 'missing';
1146
+
1147
+ /**
1148
+ * The SHARED tree-less ledger-move core (ONE mechanism for BOTH directions — the
1149
+ * requeue `needs-attention|in-progress → backlog` and the surface `in-progress →
1150
+ * needs-attention`). It runs the contention-retry loop: fetch `<arbiter>/main`,
1151
+ * resolve `expectedBase`, ask the caller's `plan(base)` to build the one-file move
1152
+ * on a SCRATCH INDEX via {@link prepareTreelessMoveCommit} (so the caller's
1153
+ * index/HEAD/working tree are NEVER touched), CAS-publish it THROUGH the shared
1154
+ * write seam (`ledgerWrite.applyTransition`, the very push+lease+verify `claim`
1155
+ * uses), drop the throwaway ref, and on a CONTENTION rejection refetch + REPLAN
1156
+ * against the advanced base and retry. Re-planning per attempt is what makes the
1157
+ * retry safe when the item itself moved under us (e.g. a prior attempt landed but
1158
+ * the CAS verify reported rejected): the next plan sees it `already-done`.
1159
+ */
1160
+ async function runTreelessLedgerMove(params: {
1161
+ cwd: string;
1162
+ slug: string;
1163
+ arbiter: string;
1164
+ kind: LedgerTransitionKind;
1165
+ /** Build (or short-circuit) the move against the current base. Called per attempt. */
1166
+ plan: (base: string) => TreelessAttemptPlan;
1167
+ /** A label for the contention-progress note (the verb the caller surfaces). */
1168
+ onContended: string;
1169
+ /**
1170
+ * Fetch the arbiter's `main` with an EXPLICIT refspec
1171
+ * (`+refs/heads/main:refs/remotes/<arbiter>/main`) instead of a plain
1172
+ * `fetch <arbiter>`. The surface direction sets this `true` because it runs from
1173
+ * a JOB WORKTREE whose remote's default fetch refspec may NOT map `main →
1174
+ * refs/remotes/<arbiter>/main` (a bare-mirror worktree), so the plain fetch can
1175
+ * leave `<arbiter>/main` unresolved. The requeue direction leaves it `false`: it
1176
+ * needs ALL refs (the continue-branch guard reads `<arbiter>/work/<slug>`), so a
1177
+ * main-only refspec would be too narrow there.
1178
+ */
1179
+ explicitMainRefspec: boolean;
1180
+ env: NodeJS.ProcessEnv | undefined;
1181
+ note: (message: string) => void;
1182
+ }): Promise<boolean> {
1183
+ const {
1184
+ cwd,
1185
+ arbiter,
1186
+ kind,
1187
+ plan,
1188
+ onContended,
1189
+ explicitMainRefspec,
1190
+ env,
1191
+ note,
1192
+ } = params;
1193
+ const fetchArgs = explicitMainRefspec
1194
+ ? [
1195
+ 'fetch',
1196
+ '--quiet',
1197
+ arbiter,
1198
+ `+refs/heads/main:refs/remotes/${arbiter}/main`,
1199
+ ]
1200
+ : ['fetch', '--quiet', arbiter];
1201
+
1202
+ for (let i = 0; i < TREELESS_CONTENTION_ATTEMPTS; i++) {
1203
+ if (i > 0) {
1204
+ await gitSoftAsync(fetchArgs, cwd, env);
1205
+ }
1206
+ const base = (
1207
+ await gitHardAsync(['rev-parse', `${arbiter}/main`], cwd, env)
1208
+ ).stdout.trim();
1209
+
1210
+ // Plan the move FRESH against this (possibly re-fetched) base. The item may
1211
+ // already be at the destination (idempotent / a prior landed-but-reported-
1212
+ // rejected attempt) or absent — both are terminal, no push.
1213
+ const prepared = plan(base);
1214
+ if (prepared === 'already-done') {
1215
+ return true;
1216
+ }
1217
+ if (prepared === 'missing') {
1218
+ return false;
1219
+ }
1220
+
1221
+ // Publish THROUGH the shared seam (the same `:main` push + force-with-lease +
1222
+ // verify `claim` uses). The transition's WHO stays the caller's ambient env
1223
+ // (threaded by `commit-tree` above) — tree-less is orthogonal to attribution.
1224
+ const result = await ledgerWrite.applyTransition({
1225
+ kind,
1226
+ arbiter,
1227
+ localBranch: prepared.ref,
1228
+ expectedBase: base,
1229
+ head: prepared.commit,
1230
+ cwd,
1231
+ env,
1232
+ note,
1233
+ });
1234
+ // Drop the throwaway ref either way (it served only as the push source).
1235
+ await gitSoftAsync(['update-ref', '-d', prepared.ref], cwd, env);
1236
+
1237
+ if (result.kind === 'published') {
1238
+ // Advance the LOCAL remote-tracking `<arbiter>/main` so it INCLUDES the
1239
+ // move (the push only moved the arbiter's main). Best-effort.
1240
+ await gitSoftAsync(fetchArgs, cwd, env);
1241
+ return true;
1242
+ }
1243
+ // rejected: main moved under us — refetch + REPLAN against the new base.
1244
+ note(
1245
+ `main advanced under us — ${onContended} refetch and retry (${i + 1}/${TREELESS_CONTENTION_ATTEMPTS})...`,
1246
+ );
1247
+ }
1248
+ return false;
1249
+ }
1250
+
1251
+ /** True iff `path` exists in the given commit's tree (a soft cat-file probe). */
1252
+ function pathInCommit(
1253
+ commit: string,
1254
+ path: string,
1255
+ cwd: string,
1256
+ env: NodeJS.ProcessEnv | undefined,
1257
+ ): boolean {
1258
+ return (
1259
+ run('git', ['cat-file', '-e', `${commit}:${path}`], cwd, {env}).status === 0
1260
+ );
1261
+ }
1262
+
1263
+ /**
1264
+ * Build a one-file ledger MOVE as a commit off the arbiter's `main`, using
1265
+ * PLUMBING on a SCRATCH INDEX — it never touches the caller's index, HEAD, or
1266
+ * working tree (so a concurrent writer's uncommitted cwd files can never be swept
1267
+ * in). It loads `base`'s tree into a throwaway index, relocates ONLY this slug's
1268
+ * ledger file from `sourceRel` to `destRel` (applying `transformBody` to its body
1269
+ * first — read from the blob on `main`, NOT from any cwd file: the requeue note,
1270
+ * or the needs-attention reason), writes the tree, commits it parented on `base`,
1271
+ * and points a throwaway local ref at the commit. Returns that ref + the commit
1272
+ * sha for the seam's CAS push. The SHARED prep for BOTH tree-less directions.
1273
+ */
1274
+ function prepareTreelessMoveCommit(params: {
1275
+ cwd: string;
1276
+ slug: string;
1277
+ base: string;
1278
+ sourceRel: string;
1279
+ destRel: string;
1280
+ transformBody: (body: string) => string;
1281
+ commitMessage: string;
1282
+ refNamespace: string;
1283
+ env: NodeJS.ProcessEnv | undefined;
1284
+ }): {ref: string; commit: string} {
1285
+ const {
1286
+ cwd,
1287
+ slug,
1288
+ base,
1289
+ sourceRel,
1290
+ destRel,
1291
+ transformBody,
1292
+ commitMessage,
1293
+ refNamespace,
1294
+ env,
1295
+ } = params;
1296
+
1297
+ // The item's body on `main`, with the caller's body transform applied.
1298
+ const original = catBlob(`${base}:${sourceRel}`, cwd, env);
1299
+ const content = transformBody(original);
1300
+ // Hash the (possibly transformed) blob INTO the cwd's object store. A blob
1301
+ // write does not touch the working tree.
1302
+ const blob = hashObject(content, cwd, env);
1303
+
1304
+ // A scratch index so read-tree/update-index never disturb the caller's index.
1305
+ const scratchIndex = join(
1306
+ tmpdir(),
1307
+ `dorfl-${refNamespace}-${process.pid}-${Date.now()}.index`,
1308
+ );
1309
+ const withIndex: NodeJS.ProcessEnv = {
1310
+ ...(env ?? process.env),
1311
+ GIT_INDEX_FILE: scratchIndex,
1312
+ };
1313
+ try {
1314
+ gitHard(['read-tree', base], cwd, withIndex);
1315
+ // Remove the item from its source folder, add it under the dest folder. When
1316
+ // source === dest (an idempotent re-surface), force-remove + re-add the same
1317
+ // path is a no-op move that still carries any body change. One file changes.
1318
+ gitHard(['update-index', '--force-remove', sourceRel], cwd, withIndex);
1319
+ gitHard(
1320
+ ['update-index', '--add', '--cacheinfo', `100644,${blob},${destRel}`],
1321
+ cwd,
1322
+ withIndex,
1323
+ );
1324
+ const tree = runHard(['write-tree'], cwd, withIndex).stdout.trim();
1325
+ // commit-tree threads the caller's ambient identity (env) — tree-less only
1326
+ // changed the WHERE-it-writes (the arbiter ref, not the cwd tree), not the WHO.
1327
+ const commit = runHard(
1328
+ ['commit-tree', tree, '-p', base, '-m', commitMessage],
1329
+ cwd,
1330
+ env,
1331
+ ).stdout.trim();
1332
+ // A throwaway local ref the seam's push uses as its source (`<ref>:main`).
1333
+ const ref = `refs/dorfl/${refNamespace}/${slug}`;
1334
+ gitHard(['update-ref', ref, commit], cwd, env);
1335
+ return {ref, commit};
1336
+ } finally {
1337
+ rmSync(scratchIndex, {force: true});
1338
+ }
1339
+ }
1340
+
1341
+ /** The heading that opens an appended requeue handoff note in the item body. */
1342
+ const REQUEUE_HEADING_PREFIX = '## Requeue';
1343
+
1344
+ /**
1345
+ * Append a dated `## Requeue YYYY-MM-DD` handoff section to an item body's TEXT
1346
+ * (append-only — never overwrites; repeated requeues accumulate a handoff log).
1347
+ * Body prose only (never a frontmatter field — WORK-CONTRACT rule 3). The date is
1348
+ * UTC `YYYY-MM-DD`; multiple notes on the same day are distinct appended blocks.
1349
+ *
1350
+ * A PURE string transform (it operates on the body CONTENT, not a file path) so
1351
+ * the tree-less requeue can apply it to the blob read from `<arbiter>/main`
1352
+ * without touching the cwd working tree.
1353
+ */
1354
+ function appendRequeueNoteText(content: string, message: string): string {
1355
+ const date = new Date().toISOString().slice(0, 10);
1356
+ const base = content.replace(/\s*$/, '');
1357
+ return [base, '', `${REQUEUE_HEADING_PREFIX} ${date}`, '', message, ''].join(
1358
+ '\n',
1359
+ );
1360
+ }
1361
+
1362
+ /**
1363
+ * Extract the prose written under the `## Needs attention` heading from an item
1364
+ * body. Returns the first non-empty line(s) of the block as a single line
1365
+ * (stops at the next `## ` heading); '' when no block is present. The
1366
+ * needs-attention REASON is now recorded on the per-item lock entry (task
1367
+ * `cutover-needs-attention-becomes-lock-stuck-recovery-surface`, decision i+),
1368
+ * NOT in the body, but this extractor stays for any historical body text that
1369
+ * still carries the heading (a tolerant best-effort read).
1370
+ */
1371
+ export function extractReason(content: string): string {
1372
+ const normalized = content.replace(/\r\n/g, '\n');
1373
+ const lines = normalized.split('\n');
1374
+ const start = lines.findIndex((l) => l.trim() === REASON_HEADING);
1375
+ if (start === -1) {
1376
+ return '';
1377
+ }
1378
+ const collected: string[] = [];
1379
+ for (let i = start + 1; i < lines.length; i++) {
1380
+ const line = lines[i];
1381
+ if (/^##\s/.test(line)) {
1382
+ break;
1383
+ }
1384
+ if (/^###\s/.test(line)) {
1385
+ // The questions sub-section starts here; the reason itself is above it.
1386
+ break;
1387
+ }
1388
+ if (line.trim() === '') {
1389
+ if (collected.length > 0) {
1390
+ // Stop at the first blank line AFTER we captured the reason text.
1391
+ break;
1392
+ }
1393
+ continue;
1394
+ }
1395
+ collected.push(line.trim());
1396
+ }
1397
+ return collected.join(' ').trim();
1398
+ }
1399
+
1400
+ /** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
1401
+ function gitHard(
1402
+ args: string[],
1403
+ cwd: string,
1404
+ env: NodeJS.ProcessEnv | undefined,
1405
+ ): void {
1406
+ runHard(args, cwd, env);
1407
+ }
1408
+
1409
+ /** Like {@link gitHard} but returns the raw result (for plumbing that emits stdout). */
1410
+ function runHard(
1411
+ args: string[],
1412
+ cwd: string,
1413
+ env: NodeJS.ProcessEnv | undefined,
1414
+ ): RunResult {
1415
+ const result = run('git', args, cwd, {env});
1416
+ if (result.status !== 0) {
1417
+ throw new Error(
1418
+ `git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
1419
+ );
1420
+ }
1421
+ return result;
1422
+ }
1423
+
1424
+ /** Async soft git (no throw) — for the tree-less requeue's remote checks. */
1425
+ function gitSoftAsync(
1426
+ args: string[],
1427
+ cwd: string,
1428
+ env: NodeJS.ProcessEnv | undefined,
1429
+ ): Promise<RunResult> {
1430
+ return runAsync('git', args, cwd, {env});
1431
+ }
1432
+
1433
+ /** Async git; throw on non-zero (unexpected plumbing failures). */
1434
+ async function gitHardAsync(
1435
+ args: string[],
1436
+ cwd: string,
1437
+ env: NodeJS.ProcessEnv | undefined,
1438
+ ): Promise<RunResult> {
1439
+ const result = await runAsync('git', args, cwd, {env});
1440
+ if (result.status !== 0) {
1441
+ throw new Error(
1442
+ `git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
1443
+ );
1444
+ }
1445
+ return result;
1446
+ }
1447
+
1448
+ /** Read an object's content (`git cat-file -p <object>`) from the cwd's store. */
1449
+ function catBlob(
1450
+ object: string,
1451
+ cwd: string,
1452
+ env: NodeJS.ProcessEnv | undefined,
1453
+ ): string {
1454
+ return runHard(['cat-file', '-p', object], cwd, env).stdout;
1455
+ }
1456
+
1457
+ /** Write a blob into the cwd's object store (`git hash-object -w`), return its sha. */
1458
+ function hashObject(
1459
+ content: string,
1460
+ cwd: string,
1461
+ env: NodeJS.ProcessEnv | undefined,
1462
+ ): string {
1463
+ const result = run('git', ['hash-object', '-w', '--stdin'], cwd, {
1464
+ env,
1465
+ input: content,
1466
+ });
1467
+ if (result.status !== 0) {
1468
+ throw new Error(
1469
+ `git hash-object failed (exit ${result.status}): ${result.stderr.trim()}`,
1470
+ );
1471
+ }
1472
+ return result.stdout.trim();
1473
+ }
1474
+
1475
+ /**
1476
+ * Run git, returning the raw result (no throw) — for soft checks like the
1477
+ * `--reset` remote-branch delete, whose non-zero exit is a meaningful outcome
1478
+ * (the requeue aborts) rather than an unexpected plumbing failure.
1479
+ */
1480
+ function gitSoftRun(
1481
+ args: string[],
1482
+ cwd: string,
1483
+ env: NodeJS.ProcessEnv | undefined,
1484
+ ): {status: number; stdout: string; stderr: string} {
1485
+ return run('git', args, cwd, {env});
1486
+ }
1487
+
1488
+ /** True when the index has no staged changes against HEAD (nothing to commit). */
1489
+ function nothingStaged(
1490
+ cwd: string,
1491
+ env: NodeJS.ProcessEnv | undefined,
1492
+ ): boolean {
1493
+ // `diff --cached --quiet` exits 0 when NOTHING is staged, 1 when there is.
1494
+ return run('git', ['diff', '--cached', '--quiet'], cwd, {env}).status === 0;
1495
+ }
1496
+
1497
+ /** The current HEAD commit sha (the just-made commit's tip). */
1498
+ function revParseHead(cwd: string, env: NodeJS.ProcessEnv | undefined): string {
1499
+ const result = run('git', ['rev-parse', 'HEAD'], cwd, {env});
1500
+ if (result.status !== 0) {
1501
+ throw new Error(
1502
+ `git rev-parse HEAD failed (exit ${result.status}): ${result.stderr.trim()}`,
1503
+ );
1504
+ }
1505
+ return result.stdout.trim();
1506
+ }