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,1858 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {runAsync, type RunResult} from './git.js';
3
+ import {resolveSidecarIdentity, type SidecarType} from './sidecar.js';
4
+ import {workItemRel} from './work-layout.js';
5
+
6
+ /**
7
+ * The **unified item-lock module** (prd `ledger-status-per-item-lock-refs`, ADR
8
+ * `ledger-status-on-per-item-lock-refs`). The runner's ONE lock primitive: ONE
9
+ * lock per item, on a PER-ITEM hidden ref `refs/dorfl/lock/<entry>`,
10
+ * acquired by an ATOMIC create-only push and released by DELETING the ref, with a
11
+ * two-axis (`action` × `state`) entry. It collapses the old transient status
12
+ * folders (`in-progress`, `needs-attention`, `tasking`, `advancing`) into ONE
13
+ * lock keyed by item identity; `in-progress` = lock held active for `implement`,
14
+ * `needs-attention` = lock held `stuck`.
15
+ *
16
+ * It GENERALISES the green tracer that proved the dangerous core end-to-end on a
17
+ * bare `file://` arbiter (the tracer is now this file). The one production
18
+ * difference from the tracer is the IDENTITY SEAM: callers pass a NAMESPACED item
19
+ * identity (`task:<slug>` / `prd:<slug>` / `observation:<slug>` / `obs:<slug>`,
20
+ * or a bare `<slug>` = task), and this module derives the type-encoded lock
21
+ * `<entry>` (`<type>-<slug>`) through {@link resolveSidecarIdentity} — the SAME
22
+ * single source of truth the sidecar (`work/questions/<type>-<slug>.md`) and the
23
+ * work branch (`work/<type>-<slug>`) already use. There is deliberately NO second
24
+ * identity scheme: a task, a prd,
25
+ * and an observation that share a slug get DISTINCT lock refs, and the SAME item
26
+ * under different actions shares ONE ref (so implement / task / advance on one
27
+ * item are mutually exclusive by construction).
28
+ *
29
+ * It is NOT yet wired into claim/task/advance — those are separate, dependent
30
+ * tasks — and deliberately does NOT touch `main`.
31
+ *
32
+ * WHY a per-item ref (not a marker on `main` or a tree on one shared ref):
33
+ * - **No false contention, no retry.** The ONLY writer that can contend on item
34
+ * X's lock is another writer FOR X — a GENUINE conflict the loser SHOULD lose.
35
+ * Two writers for DIFFERENT items touch DIFFERENT refs and never serialise. So
36
+ * acquire needs NO refetch-retry budget (contrast the shared-`main` CAS, which
37
+ * falsely-contends under parallelism and exhausts its retry cap → exit 3).
38
+ * - **Self-cleaning, no storage growth.** Acquire CREATES the ref; release
39
+ * DELETES it (not "empties" it), so the live ref set = currently-held items
40
+ * only. The lock commit is PARENTLESS (no `main` parent), so on ref-delete it
41
+ * is immediately unreachable and reclaimed by normal git gc — and the lock ref
42
+ * is fully decoupled from `main`'s object graph.
43
+ * - **Branch-inheritance impossible.** Nothing is in `main`'s tree, so a work
44
+ * branch cut from `main` inherits no lock state.
45
+ * - **Provider-agnostic.** A ref is a ref: the create-only / delete pushes work
46
+ * identically on a `--bare file://` arbiter and a real remote.
47
+ *
48
+ * ATOMICITY is the `--force-with-lease=<ref>:` (EMPTY expected) create-only push:
49
+ * the arbiter accepts it ONLY if the ref is still absent; a racer who lost finds
50
+ * the ref present and is rejected. No nonce gymnastics are needed here (unlike the
51
+ * shared-`main` CAS) because the ref NAME is the identity — two acquires for the
52
+ * same item race the SAME ref, and ref-level create-only is itself the mutex.
53
+ */
54
+
55
+ /** The ref namespace for per-item locks. HIDDEN (not `refs/heads/*`): invisible
56
+ * in the GitHub UI, not swept by branch automation, not fetched by a default
57
+ * clone. Deletion = "all locks released" (recoverable; work is on the work
58
+ * branches + `main`). */
59
+ export const LOCK_REF_PREFIX = 'refs/dorfl/lock';
60
+
61
+ /**
62
+ * The single IDENTITY SEAM for the lock: derive the type-encoded lock `<entry>`
63
+ * (`<type>-<slug>`) from a NAMESPACED item identity, through the shared
64
+ * {@link resolveSidecarIdentity} resolver (the single source of truth, which the
65
+ * sidecar filename + the advancing-lock marker also key onto). Accepts the same
66
+ * forms as that resolver: `task:<slug>` / `prd:<slug>` / `observation:<slug>` /
67
+ * `obs:<slug>`, or a bare `<slug>` (= task). Acquire/release/read ALL key
68
+ * through THIS function, so there is one — and only one — addressing scheme.
69
+ */
70
+ export function lockEntryFor(item: string): string {
71
+ const {type, slug} = resolveSidecarIdentity(item);
72
+ return `${type}-${slug}`;
73
+ }
74
+
75
+ /** The lock ref for a type-encoded `<entry>` (`<type>-<slug>`). */
76
+ export function itemLockRef(entry: string): string {
77
+ return `${LOCK_REF_PREFIX}/${entry}`;
78
+ }
79
+
80
+ /** WHAT holds the lock — the three mutually-exclusive actions over one item. */
81
+ export type LockAction = 'implement' | 'task' | 'advance';
82
+
83
+ /** Health of the hold: `active` (in-progress) or `stuck` (needs-attention). */
84
+ export type LockState = 'active' | 'stuck';
85
+
86
+ /**
87
+ * The two-axis lock entry. `action` and `state` are INDEPENDENT axes (so
88
+ * "advanced-and-stuck" and "building-and-stuck" are both representable, which a
89
+ * single action-field could not do). `reason` is present IFF `state === 'stuck'`.
90
+ *
91
+ * Since the lock entry is the SOLE stuck record (task
92
+ * `cutover-needs-attention-becomes-lock-stuck-recovery-surface`, decision i+: the
93
+ * `needs-attention/` folder is retired), `reason` is the FULL bounce prose (it may
94
+ * span multiple lines — a red-gate excerpt, a rebase-conflict report, an agent's
95
+ * ambiguity note), and `questions` carries any agent-surfaced questions for the
96
+ * human. Both ride in the lock blob BODY (not a single frontmatter field) so they
97
+ * round-trip richly, in a shape a future advance-surface rung (the
98
+ * surface-stuck-locks-as-questions direction captured in
99
+ * `work/notes/observations/needs-attention-may-have-no-human-visible-outcome-after-lock-cutover-surface-as-questions-2026-06-21.md`)
100
+ * can render into a `work/questions/` sidecar. `questions` is present (non-empty) only
101
+ * for a stuck entry that recorded them.
102
+ */
103
+ export interface LockEntry {
104
+ entry: string;
105
+ action: LockAction;
106
+ state: LockState;
107
+ holder: string;
108
+ since: string;
109
+ reason?: string;
110
+ questions?: string[];
111
+ }
112
+
113
+ /** Outcome of an acquire attempt. `acquired` = we hold it; `lost` = someone else
114
+ * does (a genuine same-item conflict). `error` = environment/usage. */
115
+ export type AcquireOutcome = 'acquired' | 'lost' | 'error';
116
+ export type ReleaseOutcome = 'released' | 'not-held' | 'error';
117
+
118
+ export interface AcquireResult {
119
+ outcome: AcquireOutcome;
120
+ /** The type-encoded lock entry (`<type>-<slug>`) the acquire keyed onto. */
121
+ entry: string;
122
+ ref: string;
123
+ message: string;
124
+ }
125
+ export interface ReleaseResult {
126
+ outcome: ReleaseOutcome;
127
+ /** The type-encoded lock entry (`<type>-<slug>`) the release keyed onto. */
128
+ entry: string;
129
+ ref: string;
130
+ message: string;
131
+ }
132
+
133
+ /**
134
+ * Outcome of an AMEND-style transition (mark-stuck / resume / requeue) — the
135
+ * lock-entry STATE MACHINE's interior moves (prd `ledger-status-per-item-lock-refs`,
136
+ * the C8 lock-entry state machine in the design trail). Each is a single CAS on the
137
+ * held ref (no retry loop), so the verdict is definitive:
138
+ * - `transitioned` — we won the CAS; the entry now holds the target `(action, state)`.
139
+ * - `not-held` — there is no entry to transition (the move's precondition is
140
+ * "a held entry in the right state"; absent ⇒ illegal here).
141
+ * - `wrong-state` — an entry exists but in the wrong `state` for this move
142
+ * (e.g. resume on an `active` entry, mark-stuck on a `stuck` one).
143
+ * An ILLEGAL transition, rejected, not coerced.
144
+ * - `lost` — the leased CAS was rejected because a CONCURRENT writer changed
145
+ * the ref between our read and our push (a genuine same-item race).
146
+ * - `error` — environment/usage (missing item, missing reason for mark-stuck, …).
147
+ */
148
+ export type TransitionOutcome =
149
+ | 'transitioned'
150
+ | 'not-held'
151
+ | 'wrong-state'
152
+ | 'lost'
153
+ | 'error';
154
+
155
+ export interface TransitionResult {
156
+ outcome: TransitionOutcome;
157
+ /** The type-encoded lock entry (`<type>-<slug>`) the transition keyed onto. */
158
+ entry: string;
159
+ ref: string;
160
+ message: string;
161
+ /** The lock entry AFTER a successful `transitioned` (absent for requeue's removal). */
162
+ lock?: LockEntry;
163
+ }
164
+
165
+ function gitSoft(
166
+ args: string[],
167
+ cwd: string,
168
+ env: NodeJS.ProcessEnv | undefined,
169
+ ): Promise<RunResult> {
170
+ return runAsync('git', args, cwd, {env});
171
+ }
172
+ async function gitHard(
173
+ args: string[],
174
+ cwd: string,
175
+ env: NodeJS.ProcessEnv | undefined,
176
+ ): Promise<RunResult> {
177
+ const r = await runAsync('git', args, cwd, {env});
178
+ if (r.status !== 0) {
179
+ throw new Error(
180
+ `git ${args.join(' ')} failed (exit ${r.status}): ${r.stderr.trim()}`,
181
+ );
182
+ }
183
+ return r;
184
+ }
185
+
186
+ /** The body heading that opens the (possibly multi-line) stuck reason prose. */
187
+ const LOCK_REASON_HEADING = '## Reason';
188
+ /** The body heading that opens the agent-surfaced questions list. */
189
+ const LOCK_QUESTIONS_HEADING = '## Questions';
190
+
191
+ /**
192
+ * Serialise a lock entry to the ref's blob body (markdown frontmatter, like the
193
+ * advancing marker, so it round-trips and is previewable). The two-axis state
194
+ * (`entry`/`action`/`state`/`holder`/`since`) lives in the frontmatter; a stuck
195
+ * entry's FULL reason prose + any surfaced questions live in the BODY (under
196
+ * `## Reason` / `## Questions`) so they round-trip RICHLY (multi-line reason,
197
+ * bulleted questions) — the lock entry is the SOLE stuck record now, in a shape a
198
+ * future advance-surface rung can render. {@link parseLockEntry} is the exact
199
+ * inverse.
200
+ */
201
+ export function serialiseLockEntry(e: LockEntry): string {
202
+ const lines = [
203
+ '---',
204
+ `entry: ${e.entry}`,
205
+ `action: ${e.action}`,
206
+ `state: ${e.state}`,
207
+ `holder: ${e.holder}`,
208
+ `since: ${e.since}`,
209
+ '---',
210
+ '',
211
+ `Lock held for \`${e.entry}\` (${e.action}/${e.state}).`,
212
+ ];
213
+ if (e.state === 'stuck' && e.reason) {
214
+ lines.push('', LOCK_REASON_HEADING, '', ...e.reason.split('\n'));
215
+ }
216
+ if (e.state === 'stuck' && e.questions && e.questions.length > 0) {
217
+ lines.push('', LOCK_QUESTIONS_HEADING, '');
218
+ for (const q of e.questions) {
219
+ lines.push(`- ${q}`);
220
+ }
221
+ }
222
+ lines.push('');
223
+ return lines.join('\n');
224
+ }
225
+
226
+ /**
227
+ * Build a PARENTLESS commit whose tree contains the single lock-entry blob, and
228
+ * return its sha. Parentless (no `-p`) so it is decoupled from `main` and becomes
229
+ * unreachable the instant the ref is deleted (gc reclaims it). Uses plumbing
230
+ * (`hash-object` → `mktree` → `commit-tree`), never the working tree/index/HEAD,
231
+ * so it is safe to call from any worktree.
232
+ */
233
+ async function buildLockCommit(
234
+ entry: LockEntry,
235
+ cwd: string,
236
+ env: NodeJS.ProcessEnv | undefined,
237
+ ): Promise<string> {
238
+ const body = serialiseLockEntry(entry);
239
+ const blob = (
240
+ await gitHardInput(['hash-object', '-w', '--stdin'], cwd, env, body)
241
+ ).stdout.trim();
242
+ // One tree entry: `lock.md` → the blob.
243
+ const treeInput = `100644 blob ${blob}\tlock.md\n`;
244
+ const tree = (
245
+ await gitHardInput(['mktree'], cwd, env, treeInput)
246
+ ).stdout.trim();
247
+ const message = `lock: ${entry.entry} (${entry.action}/${entry.state})`;
248
+ // PARENTLESS commit (no -p): the lock graph never joins main's history.
249
+ const commit = (
250
+ await gitHard(['commit-tree', tree, '-m', message], cwd, env)
251
+ ).stdout.trim();
252
+ return commit;
253
+ }
254
+
255
+ /** `gitHard` variant that pipes `input` to stdin (for hash-object/mktree). */
256
+ async function gitHardInput(
257
+ args: string[],
258
+ cwd: string,
259
+ env: NodeJS.ProcessEnv | undefined,
260
+ input: string,
261
+ ): Promise<RunResult> {
262
+ const r = await runAsync('git', args, cwd, {env, input});
263
+ if (r.status !== 0) {
264
+ throw new Error(
265
+ `git ${args.join(' ')} failed (exit ${r.status}): ${r.stderr.trim()}`,
266
+ );
267
+ }
268
+ return r;
269
+ }
270
+ export interface AcquireOptions {
271
+ /**
272
+ * The NAMESPACED item identity to lock (`task:<slug>` / `prd:<slug>` /
273
+ * `observation:<slug>` / `obs:<slug>`, or a bare `<slug>` = task). Resolved to
274
+ * the type-encoded lock `<entry>` through {@link lockEntryFor}.
275
+ */
276
+ item: string;
277
+ action: LockAction;
278
+ cwd: string;
279
+ arbiter?: string;
280
+ holder?: string;
281
+ env?: NodeJS.ProcessEnv;
282
+ }
283
+
284
+ /**
285
+ * Acquire the per-item lock atomically: build a parentless lock commit and push it
286
+ * to `refs/dorfl/lock/<entry>` with `--force-with-lease=<ref>:` (the EMPTY
287
+ * expected value = create-only). The arbiter accepts ONLY if the ref is still
288
+ * absent. A racer who lost finds the ref present → its lease fails → `lost`. No
289
+ * retry loop: a rejection here is a GENUINE same-item conflict, not false
290
+ * contention.
291
+ */
292
+ export async function acquireItemLock(
293
+ opts: AcquireOptions,
294
+ ): Promise<AcquireResult> {
295
+ const arbiter = opts.arbiter ?? 'origin';
296
+ const env = opts.env;
297
+ const cwd = opts.cwd;
298
+ if (!opts.item) {
299
+ return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
300
+ }
301
+ const entry = lockEntryFor(opts.item);
302
+ const ref = itemLockRef(entry);
303
+ try {
304
+ // Fetch the current lock refs so the lease sees the real state.
305
+ await gitHard(
306
+ [
307
+ 'fetch',
308
+ '--quiet',
309
+ arbiter,
310
+ `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`,
311
+ ],
312
+ cwd,
313
+ env,
314
+ );
315
+ const holder = opts.holder ?? (await resolveHolder(cwd, env));
316
+ const commit = await buildLockCommit(
317
+ {
318
+ entry,
319
+ action: opts.action,
320
+ state: 'active',
321
+ holder,
322
+ since: new Date().toISOString(),
323
+ },
324
+ cwd,
325
+ env,
326
+ );
327
+ // CREATE-ONLY push: --force-with-lease=<ref>: (empty) succeeds iff ref absent.
328
+ const push = await gitSoft(
329
+ ['push', arbiter, `${commit}:${ref}`, `--force-with-lease=${ref}:`],
330
+ cwd,
331
+ env,
332
+ );
333
+ if (push.status === 0) {
334
+ return {outcome: 'acquired', entry, ref, message: `locked ${entry}`};
335
+ }
336
+ // Rejected: the ref already exists (held by someone for the SAME item).
337
+ return {
338
+ outcome: 'lost',
339
+ entry,
340
+ ref,
341
+ message: `'${entry}' is already locked (held by another). Back off.`,
342
+ };
343
+ } catch (err) {
344
+ return {
345
+ outcome: 'error',
346
+ entry,
347
+ ref,
348
+ message: err instanceof Error ? err.message : String(err),
349
+ };
350
+ }
351
+ }
352
+
353
+ export interface ReleaseOptions {
354
+ /** The NAMESPACED item identity (same forms as {@link AcquireOptions.item}). */
355
+ item: string;
356
+ cwd: string;
357
+ arbiter?: string;
358
+ env?: NodeJS.ProcessEnv;
359
+ }
360
+
361
+ /**
362
+ * Release the per-item lock by DELETING the ref (`git push <arbiter>
363
+ * :refs/dorfl/lock/<entry>`). Deleting (not emptying) is what makes the
364
+ * lock set self-cleaning: a released item has NO ref, and its parentless commit
365
+ * becomes unreachable for gc. Idempotent: deleting an absent ref is `not-held`.
366
+ */
367
+ export async function releaseItemLock(
368
+ opts: ReleaseOptions,
369
+ ): Promise<ReleaseResult> {
370
+ const arbiter = opts.arbiter ?? 'origin';
371
+ const env = opts.env;
372
+ const cwd = opts.cwd;
373
+ if (!opts.item) {
374
+ return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
375
+ }
376
+ const entry = lockEntryFor(opts.item);
377
+ const ref = itemLockRef(entry);
378
+ try {
379
+ await gitHard(
380
+ [
381
+ 'fetch',
382
+ '--quiet',
383
+ arbiter,
384
+ `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`,
385
+ ],
386
+ cwd,
387
+ env,
388
+ );
389
+ const held =
390
+ (await gitSoft(['rev-parse', '--verify', '--quiet', ref], cwd, env))
391
+ .status === 0;
392
+ if (!held) {
393
+ return {
394
+ outcome: 'not-held',
395
+ entry,
396
+ ref,
397
+ message: `'${entry}' not locked`,
398
+ };
399
+ }
400
+ // Delete the ref on the arbiter. Lease on the current value guards against a
401
+ // concurrent change between our fetch and the delete.
402
+ const cur = (await gitHard(['rev-parse', ref], cwd, env)).stdout.trim();
403
+ const del = await gitSoft(
404
+ ['push', arbiter, '--delete', ref, `--force-with-lease=${ref}:${cur}`],
405
+ cwd,
406
+ env,
407
+ );
408
+ if (del.status === 0) {
409
+ // Drop our local copy of the ref too (best-effort).
410
+ await gitSoft(['update-ref', '-d', ref], cwd, env);
411
+ return {outcome: 'released', entry, ref, message: `released ${entry}`};
412
+ }
413
+ return {
414
+ outcome: 'error',
415
+ entry,
416
+ ref,
417
+ message: `release push rejected: ${del.stderr.trim()}`,
418
+ };
419
+ } catch (err) {
420
+ return {
421
+ outcome: 'error',
422
+ entry,
423
+ ref,
424
+ message: err instanceof Error ? err.message : String(err),
425
+ };
426
+ }
427
+ }
428
+
429
+ /** Outcome of a GUARDED release ({@link releaseHeldItemLock}): the caller HELD the
430
+ * lock, so an absent ref is NOT benign — it is `vanished` (the ref was deleted
431
+ * under us, an abort signal). */
432
+ export type ReleaseHeldOutcome = 'released' | 'vanished' | 'error';
433
+
434
+ export interface ReleaseHeldResult {
435
+ outcome: ReleaseHeldOutcome;
436
+ /** The type-encoded lock entry (`<type>-<slug>`) the release keyed onto. */
437
+ entry: string;
438
+ ref: string;
439
+ message: string;
440
+ }
441
+
442
+ /**
443
+ * GUARDED release for a runner that KNOWS it acquired and HELD the lock (prd
444
+ * `ledger-status-per-item-lock-refs` US #13): unlike {@link releaseItemLock} —
445
+ * whose `not-held` is a BENIGN idempotent case the complete/tasking/needs-attention
446
+ * callers tolerate (the body may predate the lock, or a crash-recovery may have
447
+ * already cleared it) — here the absence of OUR ref is an ABORT SIGNAL. A held
448
+ * runner whose own lock VANISHED mid-build (someone `release-lock`-ed it, or a
449
+ * `gc`/recovery cleared it) must DETECT it on release and route to
450
+ * needs-attention rather than silently "clean-release" a lock it no longer holds:
451
+ * the work it just did was NOT protected by the exclusion it thought it had.
452
+ *
453
+ * So this maps {@link releaseItemLock}'s `not-held` to a DISTINCT `vanished`
454
+ * outcome the caller branches on (abort / needs-attention), while a genuine
455
+ * `released` is the happy path and `error` stays `error`. It is otherwise the
456
+ * SAME leased delete (no second mechanism): a clean `released` deletes the ref we
457
+ * held. Use this ONLY where the caller provably HELD the lock (the in-flight
458
+ * runner's own release); the tolerant idempotent callers keep using
459
+ * {@link releaseItemLock}.
460
+ */
461
+ export async function releaseHeldItemLock(
462
+ opts: ReleaseOptions,
463
+ ): Promise<ReleaseHeldResult> {
464
+ const rel = await releaseItemLock(opts);
465
+ if (rel.outcome === 'not-held') {
466
+ return {
467
+ outcome: 'vanished',
468
+ entry: rel.entry,
469
+ ref: rel.ref,
470
+ message: `'${rel.entry}' lock VANISHED before our release (the ref is gone) — our hold was lost mid-build; abort / route to needs-attention rather than clean-release.`,
471
+ };
472
+ }
473
+ return {
474
+ outcome: rel.outcome,
475
+ entry: rel.entry,
476
+ ref: rel.ref,
477
+ message: rel.message,
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Fetch the lock refs and return the held entry + its current ref sha, or
483
+ * `undefined` when the item is at REST (no ref). Shared read-before-CAS step for
484
+ * the amend-style transitions (mark-stuck / resume / requeue): they all need BOTH
485
+ * the current entry (to check the state precondition + carry forward `action` /
486
+ * `holder` / `since`) and the current sha (to lease the CAS on it).
487
+ */
488
+ async function fetchHeldEntry(
489
+ entry: string,
490
+ ref: string,
491
+ cwd: string,
492
+ arbiter: string,
493
+ env: NodeJS.ProcessEnv | undefined,
494
+ ): Promise<{lock: LockEntry; sha: string} | undefined> {
495
+ await gitHard(
496
+ ['fetch', '--quiet', arbiter, `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`],
497
+ cwd,
498
+ env,
499
+ );
500
+ const rev = await gitSoft(
501
+ ['rev-parse', '--verify', '--quiet', ref],
502
+ cwd,
503
+ env,
504
+ );
505
+ if (rev.status !== 0 || rev.stdout.trim() === '') {
506
+ return undefined;
507
+ }
508
+ const sha = rev.stdout.trim();
509
+ const show = await gitSoft(['show', `${ref}:lock.md`], cwd, env);
510
+ const lock = show.status === 0 ? parseLockEntry(show.stdout) : undefined;
511
+ if (!lock) {
512
+ return undefined;
513
+ }
514
+ return {lock, sha};
515
+ }
516
+
517
+ /**
518
+ * AMEND the held entry in place via a leased CAS: build a NEW parentless commit
519
+ * carrying `next` and push it to the SAME ref with `--force-with-lease=<ref>:<sha>`
520
+ * (the sha we just read). The arbiter accepts ONLY if the ref is unchanged since
521
+ * our read — a concurrent writer who moved it makes our lease fail (`lost`). No
522
+ * retry loop: a rejection is a genuine same-item race the caller should lose.
523
+ */
524
+ async function amendHeldEntry(
525
+ next: LockEntry,
526
+ ref: string,
527
+ expectedSha: string,
528
+ cwd: string,
529
+ arbiter: string,
530
+ env: NodeJS.ProcessEnv | undefined,
531
+ ): Promise<TransitionResult> {
532
+ const commit = await buildLockCommit(next, cwd, env);
533
+ const push = await gitSoft(
534
+ [
535
+ 'push',
536
+ arbiter,
537
+ `${commit}:${ref}`,
538
+ `--force-with-lease=${ref}:${expectedSha}`,
539
+ ],
540
+ cwd,
541
+ env,
542
+ );
543
+ if (push.status === 0) {
544
+ // Move our local copy to the new commit too (best-effort) so a subsequent
545
+ // read in the same clone sees the amended entry without a refetch.
546
+ await gitSoft(['update-ref', ref, commit], cwd, env);
547
+ return {
548
+ outcome: 'transitioned',
549
+ entry: next.entry,
550
+ ref,
551
+ message: `${next.entry} → ${next.action}/${next.state}`,
552
+ lock: next,
553
+ };
554
+ }
555
+ return {
556
+ outcome: 'lost',
557
+ entry: next.entry,
558
+ ref,
559
+ message: `'${next.entry}' lock changed concurrently (CAS lost). Back off.`,
560
+ };
561
+ }
562
+
563
+ /** Verdict of the SHARED leased delete {@link leasedDeleteLockRef}: `deleted` (the
564
+ * ref was removed), `lost` (a concurrent change moved the ref between the read and
565
+ * the delete — the lease was REJECTED, never `--force`d), or `error`. */
566
+ export type LeasedDeleteOutcome = 'deleted' | 'lost' | 'error';
567
+
568
+ /**
569
+ * The ONE leased-delete CLEAR path shared by every code path that removes a held
570
+ * lock ref by lease (the recovery {@link reconcileItemLockAgainstMain}, the
571
+ * human-invoked {@link reapStaleItemLocks} sweep, …): delete `ref` on the arbiter
572
+ * with `--force-with-lease=<ref>:<expectedSha>`, so the arbiter accepts ONLY if the
573
+ * ref is UNCHANGED since the caller read `expectedSha`. A concurrent writer who
574
+ * moved the ref (e.g. a racer who just marked it `stuck`) makes the lease FAIL →
575
+ * `lost` (reported, NEVER force-deleted). On success the local copy is dropped too
576
+ * (best-effort). It is the SAME leased delete `release-lock` / requeue use — there
577
+ * is no second clear mechanism.
578
+ */
579
+ async function leasedDeleteLockRef(
580
+ ref: string,
581
+ expectedSha: string,
582
+ cwd: string,
583
+ arbiter: string,
584
+ env: NodeJS.ProcessEnv | undefined,
585
+ ): Promise<LeasedDeleteOutcome> {
586
+ const del = await gitSoft(
587
+ [
588
+ 'push',
589
+ arbiter,
590
+ '--delete',
591
+ ref,
592
+ `--force-with-lease=${ref}:${expectedSha}`,
593
+ ],
594
+ cwd,
595
+ env,
596
+ );
597
+ if (del.status !== 0) {
598
+ return 'lost';
599
+ }
600
+ // Drop our local copy of the ref too (best-effort) so a subsequent read in the
601
+ // same clone does not see the now-deleted lock.
602
+ await gitSoft(['update-ref', '-d', ref], cwd, env);
603
+ return 'deleted';
604
+ }
605
+
606
+ export interface MarkStuckOptions {
607
+ /** The NAMESPACED item identity (same forms as {@link AcquireOptions.item}). */
608
+ item: string;
609
+ /** The needs-attention prose. REQUIRED — a `stuck` entry always carries a reason. */
610
+ reason: string;
611
+ /**
612
+ * Any agent-surfaced QUESTIONS for the human, recorded on the stuck entry's
613
+ * body (the lock is the SOLE stuck record now). Optional; empty/absent ⇒ no
614
+ * questions block. A future advance-surface rung renders these into a
615
+ * `work/questions/` sidecar.
616
+ */
617
+ questions?: string[];
618
+ cwd: string;
619
+ arbiter?: string;
620
+ env?: NodeJS.ProcessEnv;
621
+ }
622
+
623
+ /**
624
+ * mark-stuck (transition 2): `[action, active] -> [action, stuck] + reason`. The
625
+ * runner bounces (red gate, agent failure, decomposition-unclear). A leased CAS
626
+ * amend of the SAME entry's `state` + `reason`, keeping `action`/`holder`/`since`.
627
+ * It is the source of the needs-attention SURFACE (now read from the lock ref,
628
+ * not a `work/needs-attention/` folder).
629
+ *
630
+ * PRECONDITIONS (the state machine + invariants):
631
+ * - the entry must be HELD and `active` (`not-held` / `wrong-state` otherwise) —
632
+ * stuck is reachable only FROM active, never from absent or already-stuck.
633
+ * - `reason` must be non-empty (the `reason` PRESENT iff `state: stuck` invariant).
634
+ */
635
+ export async function markStuckItemLock(
636
+ opts: MarkStuckOptions,
637
+ ): Promise<TransitionResult> {
638
+ const arbiter = opts.arbiter ?? 'origin';
639
+ const env = opts.env;
640
+ const cwd = opts.cwd;
641
+ if (!opts.item) {
642
+ return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
643
+ }
644
+ const entry = lockEntryFor(opts.item);
645
+ const ref = itemLockRef(entry);
646
+ if (!opts.reason || opts.reason.trim() === '') {
647
+ return {
648
+ outcome: 'error',
649
+ entry,
650
+ ref,
651
+ message: 'mark-stuck requires a reason (reason iff stuck)',
652
+ };
653
+ }
654
+ try {
655
+ const held = await fetchHeldEntry(entry, ref, cwd, arbiter, env);
656
+ if (!held) {
657
+ return {
658
+ outcome: 'not-held',
659
+ entry,
660
+ ref,
661
+ message: `'${entry}' not locked`,
662
+ };
663
+ }
664
+ if (held.lock.state !== 'active') {
665
+ return {
666
+ outcome: 'wrong-state',
667
+ entry,
668
+ ref,
669
+ message: `'${entry}' is ${held.lock.state}, not active; cannot mark-stuck`,
670
+ };
671
+ }
672
+ const next: LockEntry = {
673
+ ...held.lock,
674
+ state: 'stuck',
675
+ reason: opts.reason.trim(),
676
+ };
677
+ const questions = (opts.questions ?? [])
678
+ .map((q) => q.trim())
679
+ .filter((q) => q !== '');
680
+ if (questions.length > 0) {
681
+ next.questions = questions;
682
+ } else {
683
+ delete next.questions;
684
+ }
685
+ return await amendHeldEntry(next, ref, held.sha, cwd, arbiter, env);
686
+ } catch (err) {
687
+ return {
688
+ outcome: 'error',
689
+ entry,
690
+ ref,
691
+ message: err instanceof Error ? err.message : String(err),
692
+ };
693
+ }
694
+ }
695
+
696
+ export interface ResumeOptions {
697
+ /** The NAMESPACED item identity (same forms as {@link AcquireOptions.item}). */
698
+ item: string;
699
+ cwd: string;
700
+ arbiter?: string;
701
+ /** Optionally reassign the holder on resume (a human/continue picks it up). */
702
+ holder?: string;
703
+ env?: NodeJS.ProcessEnv;
704
+ }
705
+
706
+ /**
707
+ * resume (transition 3): `[action, stuck] -> [action, active]`. A human (or a
708
+ * `continue`) picks the stuck item up: amend `state` back to `active` and CLEAR
709
+ * `reason` (the `reason` iff `stuck` invariant — an active entry never carries a
710
+ * stuck reason). Keeps the same `action`; `holder` may be reassigned. The
711
+ * lock-entry analogue of the old `needs-attention -> in-progress` folder move.
712
+ *
713
+ * PRECONDITION: the entry must be HELD and `stuck` (`not-held` / `wrong-state`
714
+ * otherwise) — active is reachable from stuck only, not from absent.
715
+ */
716
+ export async function resumeItemLock(
717
+ opts: ResumeOptions,
718
+ ): Promise<TransitionResult> {
719
+ const arbiter = opts.arbiter ?? 'origin';
720
+ const env = opts.env;
721
+ const cwd = opts.cwd;
722
+ if (!opts.item) {
723
+ return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
724
+ }
725
+ const entry = lockEntryFor(opts.item);
726
+ const ref = itemLockRef(entry);
727
+ try {
728
+ const held = await fetchHeldEntry(entry, ref, cwd, arbiter, env);
729
+ if (!held) {
730
+ return {
731
+ outcome: 'not-held',
732
+ entry,
733
+ ref,
734
+ message: `'${entry}' not locked`,
735
+ };
736
+ }
737
+ if (held.lock.state !== 'stuck') {
738
+ return {
739
+ outcome: 'wrong-state',
740
+ entry,
741
+ ref,
742
+ message: `'${entry}' is ${held.lock.state}, not stuck; nothing to resume`,
743
+ };
744
+ }
745
+ const next: LockEntry = {
746
+ ...held.lock,
747
+ state: 'active',
748
+ holder: opts.holder ?? held.lock.holder,
749
+ };
750
+ // reason + questions are PRESENT iff stuck: drop them on the way to active.
751
+ delete next.reason;
752
+ delete next.questions;
753
+ return await amendHeldEntry(next, ref, held.sha, cwd, arbiter, env);
754
+ } catch (err) {
755
+ return {
756
+ outcome: 'error',
757
+ entry,
758
+ ref,
759
+ message: err instanceof Error ? err.message : String(err),
760
+ };
761
+ }
762
+ }
763
+
764
+ /**
765
+ * requeue (transition 4): `[action, stuck] -> (absent)`. Give up on a STUCK hold
766
+ * and return the item to the pool by REMOVING the entry. The body never moved
767
+ * (Amendment 5 — it is already resting in `backlog/` on `main`), so requeue is
768
+ * purely "release the lock"; the kept `work/<slug>` branch remains for recovery.
769
+ *
770
+ * Distinct from {@link releaseItemLock} (transition 6, abort from ACTIVE): requeue
771
+ * is the GUARDED give-up from `stuck` only, so it rejects (`wrong-state`) an
772
+ * `active` entry — abandoning an in-flight active hold goes through `release`, not
773
+ * `requeue`. The removal itself is a leased delete (a concurrent change ⇒ `lost`).
774
+ */
775
+ export async function requeueItemLock(
776
+ opts: ReleaseOptions,
777
+ ): Promise<TransitionResult> {
778
+ const arbiter = opts.arbiter ?? 'origin';
779
+ const env = opts.env;
780
+ const cwd = opts.cwd;
781
+ if (!opts.item) {
782
+ return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
783
+ }
784
+ const entry = lockEntryFor(opts.item);
785
+ const ref = itemLockRef(entry);
786
+ try {
787
+ const held = await fetchHeldEntry(entry, ref, cwd, arbiter, env);
788
+ if (!held) {
789
+ return {
790
+ outcome: 'not-held',
791
+ entry,
792
+ ref,
793
+ message: `'${entry}' not locked`,
794
+ };
795
+ }
796
+ if (held.lock.state !== 'stuck') {
797
+ return {
798
+ outcome: 'wrong-state',
799
+ entry,
800
+ ref,
801
+ message: `'${entry}' is ${held.lock.state}, not stuck; use release to abort an active hold`,
802
+ };
803
+ }
804
+ const del = await gitSoft(
805
+ [
806
+ 'push',
807
+ arbiter,
808
+ '--delete',
809
+ ref,
810
+ `--force-with-lease=${ref}:${held.sha}`,
811
+ ],
812
+ cwd,
813
+ env,
814
+ );
815
+ if (del.status === 0) {
816
+ await gitSoft(['update-ref', '-d', ref], cwd, env);
817
+ return {
818
+ outcome: 'transitioned',
819
+ entry,
820
+ ref,
821
+ message: `requeued ${entry} (lock released, body still in pool)`,
822
+ };
823
+ }
824
+ return {
825
+ outcome: 'lost',
826
+ entry,
827
+ ref,
828
+ message: `'${entry}' lock changed concurrently (CAS lost). Back off.`,
829
+ };
830
+ } catch (err) {
831
+ return {
832
+ outcome: 'error',
833
+ entry,
834
+ ref,
835
+ message: err instanceof Error ? err.message : String(err),
836
+ };
837
+ }
838
+ }
839
+
840
+ /**
841
+ * Read the current lock entry for an item from the arbiter (the `status`/`scan`
842
+ * read path): fetch the lock refs, read `lock.md` from the ref's tree. Returns
843
+ * `undefined` when the item is not locked.
844
+ */
845
+ export async function readItemLock(
846
+ opts: ReleaseOptions,
847
+ ): Promise<LockEntry | undefined> {
848
+ const arbiter = opts.arbiter ?? 'origin';
849
+ const env = opts.env;
850
+ const cwd = opts.cwd;
851
+ const ref = itemLockRef(lockEntryFor(opts.item));
852
+ await gitHard(
853
+ ['fetch', '--quiet', arbiter, `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`],
854
+ cwd,
855
+ env,
856
+ );
857
+ const show = await gitSoft(['show', `${ref}:lock.md`], cwd, env);
858
+ if (show.status !== 0) {
859
+ return undefined;
860
+ }
861
+ return parseLockEntry(show.stdout);
862
+ }
863
+
864
+ /**
865
+ * The DURABLE-`main` terminal record paths for an item, by its type — the
866
+ * authoritative resting records the cross-substrate reconciliation reads. An
867
+ * item is TERMINAL on `main` iff ANY of these paths exists on `<arbiter>/main`.
868
+ * The won't-proceed terminal is PER-REGIME (the slug-collision correctness fix:
869
+ * a dropped task and a dropped prd sharing a slug used to collide on one
870
+ * bare-slug `work/dropped/<slug>.md`):
871
+ * - a TASK: `work/tasks/done/<slug>.md` (completed) OR
872
+ * `work/tasks/cancelled/<slug>.md` (the task regime's won't-proceed terminal).
873
+ * - a PRD: `work/prds/tasked/<slug>.md` (tasked) OR `work/prds/dropped/<slug>.md`
874
+ * (the prd regime's won't-proceed terminal).
875
+ * - an OBSERVATION: NONE. A note has no durable terminal folder — it leaves by
876
+ * deletion (its absence, not a terminal record, is the end state). A promoted
877
+ * observation becomes a NEW task/prd with its own ref.
878
+ */
879
+ export function terminalMainPaths(type: SidecarType, slug: string): string[] {
880
+ const file = `${slug}.md`;
881
+ switch (type) {
882
+ case 'task':
883
+ return [workItemRel('done', file), workItemRel('cancelled', file)];
884
+ // The parent-spec regime's durable terminals (`specs/tasked` tasked,
885
+ // `specs/dropped` won't-proceed) — a `spec:<slug>` lock resolves to these
886
+ // `work/specs/*` records.
887
+ case 'spec':
888
+ return [
889
+ workItemRel('specs-tasked', file),
890
+ workItemRel('specs-dropped', file),
891
+ ];
892
+ case 'observation':
893
+ return [];
894
+ }
895
+ }
896
+
897
+ /** The outcome of a cross-substrate reconciliation of one item's lock against
898
+ * the authoritative `main` durable record (prd `ledger-status-per-item-lock-refs`
899
+ * US #9/#10; ADR `ledger-status-on-per-item-lock-refs`). */
900
+ export type ReconcileOutcome =
901
+ | 'cleared-stale' // `main` is terminal + the lock was `active` (stranded) → cleared
902
+ | 'kept-stuck' // `main` is terminal + the lock is `stuck` → kept (co-exists, wins human attention)
903
+ | 'kept-in-flight' // `main` is NOT terminal + a lock is held → the normal in-flight state, kept
904
+ | 'no-lock' // there is no lock to reconcile (already at rest)
905
+ | 'error'; // environment/usage problem (best-effort; never throws)
906
+
907
+ export interface ReconcileResult {
908
+ outcome: ReconcileOutcome;
909
+ /** The type-encoded lock entry (`<type>-<slug>`) reconciled. */
910
+ entry: string;
911
+ ref: string;
912
+ /** Whether `<arbiter>/main` shows the item terminal (per {@link terminalMainPaths}:
913
+ * a task at `tasks/done`/`tasks/cancelled`, a prd at `prds/tasked`/`prds/dropped`). */
914
+ terminalOnMain: boolean;
915
+ message: string;
916
+ }
917
+
918
+ /**
919
+ * Reconcile ONE item's per-item lock against the AUTHORITATIVE `main` durable
920
+ * record — the heart of complete's cross-substrate crash-safety (prd
921
+ * `ledger-status-per-item-lock-refs` US #9/#10; ADR
922
+ * `ledger-status-on-per-item-lock-refs`; the design trail's Amendment 6).
923
+ *
924
+ * complete's order is hold lock → land the DURABLE `main` move FIRST → release
925
+ * the lock SECOND. A crash BETWEEN the move and the release leaves a
926
+ * terminal-on-`main` item (a completed/cancelled task or a tasked/dropped prd,
927
+ * per {@link terminalMainPaths}) with a STILL-HELD lock — a stale lock with no
928
+ * in-flight work behind it. This is the recovery that converges it.
929
+ *
930
+ * THE RECOVERY RULE (the `main` record is authoritative over a stale lock):
931
+ * - `main` is TERMINAL + the held lock is `active` → the item is RESTED, the
932
+ * lock is STALE (the crash was after the move) → CLEAR it (`cleared-stale`).
933
+ * - `main` is TERMINAL + the held lock is `stuck` → KEEP it (`kept-stuck`).
934
+ * `done` + `stuck` may legitimately CO-EXIST (a rebase-conflict bounce of a
935
+ * just-completed item — US #10). The stuck lock wins the human's attention;
936
+ * the `main` record wins dependency resolution. NOT corruption, never cleared
937
+ * here (a human resolves it via `resume`/`requeue`/`release-lock`).
938
+ * - `main` is NOT terminal + a lock is held → the NORMAL in-flight
939
+ * state (`kept-in-flight`); the lock is doing its job, leave it.
940
+ * - no lock at all → `no-lock` (at rest).
941
+ *
942
+ * Best-effort + idempotent: it NEVER throws (a fetch/read fault degrades to
943
+ * `error`, leaving the lock untouched — the safe direction), and re-running it
944
+ * on an already-reconciled item is a clean `no-lock`. The clear is the SAME
945
+ * leased delete {@link releaseItemLock} uses, so it cannot race off a concurrent
946
+ * change.
947
+ *
948
+ * BROADENED CONTRACT (leased-delete-rejection arm, task
949
+ * `reaper-no-lock-outcome-benign-not-lost`, promote-slice follow-up
950
+ * `reconcile-item-lock-broadened-contract-audit`, review 2026-06-20): when the
951
+ * SHARED leased delete is REJECTED (the arbiter ref moved between our read and
952
+ * our write) this function performs an EXTRA `git ls-remote <arbiter> <ref>`
953
+ * round-trip to distinguish the two rejection sub-cases, and applies to ALL
954
+ * callers — NOT only the reaper:
955
+ * - REMOTE REF IS EMPTY (a concurrent reaper / release-lock / requeue cleared
956
+ * the SAME stale lock first): the desired end state — benign. This function
957
+ * ALSO `git update-ref -d`s the LOCAL stale tracking ref as a SIDE-EFFECT
958
+ * (so a subsequent read in this clone sees the correct deleted state; a
959
+ * non-pruning fetch would otherwise leave a dangling local ref), then
960
+ * returns `no-lock`. Note the outcome-shape change: this rejection USED TO
961
+ * surface as `error` — it now surfaces as `no-lock` for EVERY caller. A
962
+ * caller that keys recovery / surfacing off `error` on this arm must be
963
+ * audited (see the follow-up task's `## Decisions` block).
964
+ * - REMOTE REF STILL EXISTS AT A DIFFERENT SHA (a genuine concurrent mutation
965
+ * — e.g. a racer just marked it `stuck`): back off rather than force —
966
+ * `error`.
967
+ * Because the function name reads "reconcile" (read-style), the local
968
+ * `update-ref -d` is a deliberate SIDE-EFFECT on the local clone's refs — it
969
+ * only fires on this specific rejection arm and is otherwise invisible.
970
+ */
971
+ export async function reconcileItemLockAgainstMain(
972
+ opts: ReleaseOptions,
973
+ ): Promise<ReconcileResult> {
974
+ const arbiter = opts.arbiter ?? 'origin';
975
+ const env = opts.env;
976
+ const cwd = opts.cwd;
977
+ if (!opts.item) {
978
+ return {
979
+ outcome: 'error',
980
+ entry: '',
981
+ ref: '',
982
+ terminalOnMain: false,
983
+ message: 'missing item',
984
+ };
985
+ }
986
+ const {type, slug} = resolveSidecarIdentity(opts.item);
987
+ const entry = lockEntryFor(opts.item);
988
+ const ref = itemLockRef(entry);
989
+ try {
990
+ // One fetch refreshes BOTH the lock refs and `<arbiter>/main` so the lock and
991
+ // the durable record are read from the SAME live arbiter snapshot.
992
+ await gitHard(
993
+ [
994
+ 'fetch',
995
+ '--quiet',
996
+ arbiter,
997
+ `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`,
998
+ ],
999
+ cwd,
1000
+ env,
1001
+ );
1002
+ await gitHard(['fetch', '--quiet', arbiter], cwd, env);
1003
+
1004
+ const held = await fetchHeldEntry(entry, ref, cwd, arbiter, env);
1005
+ const terminalOnMain = await isTerminalOnMain(
1006
+ type,
1007
+ slug,
1008
+ arbiter,
1009
+ cwd,
1010
+ env,
1011
+ );
1012
+ if (!held) {
1013
+ return {
1014
+ outcome: 'no-lock',
1015
+ entry,
1016
+ ref,
1017
+ terminalOnMain,
1018
+ message: `'${entry}' has no lock to reconcile`,
1019
+ };
1020
+ }
1021
+ if (!terminalOnMain) {
1022
+ // A held lock + a non-terminal `main` is the NORMAL in-flight state.
1023
+ return {
1024
+ outcome: 'kept-in-flight',
1025
+ entry,
1026
+ ref,
1027
+ terminalOnMain,
1028
+ message: `'${entry}' is in flight (held, not terminal on ${arbiter}/main)`,
1029
+ };
1030
+ }
1031
+ if (held.lock.state === 'stuck') {
1032
+ // a terminal-on-main record + `stuck` co-exist legitimately (US #10) —
1033
+ // NOT corruption. Keep the stuck lock (it wins the human's attention).
1034
+ return {
1035
+ outcome: 'kept-stuck',
1036
+ entry,
1037
+ ref,
1038
+ terminalOnMain,
1039
+ message: `'${entry}' is terminal on ${arbiter}/main but STUCK — kept for human attention (resume/requeue/release-lock)`,
1040
+ };
1041
+ }
1042
+ // Terminal on `main` + an `active` lock = a STALE lock (the crash was after
1043
+ // the durable move, before the release). The `main` record is authoritative:
1044
+ // clear the stale lock with the SHARED leased delete (the SAME one
1045
+ // `release-lock` / requeue / the reaper use).
1046
+ const cleared = await leasedDeleteLockRef(ref, held.sha, cwd, arbiter, env);
1047
+ if (cleared === 'deleted') {
1048
+ return {
1049
+ outcome: 'cleared-stale',
1050
+ entry,
1051
+ ref,
1052
+ terminalOnMain,
1053
+ message: `cleared the stale lock for '${entry}' (terminal on ${arbiter}/main; the durable record is authoritative)`,
1054
+ };
1055
+ }
1056
+ // The leased delete was REJECTED. Distinguish two sub-cases at the recovery
1057
+ // boundary so callers (the reaper) can route them differently:
1058
+ // (a) the remote ref is ALREADY GONE — a concurrent reaper / release-lock /
1059
+ // requeue cleared the SAME stale lock first. The desired end state;
1060
+ // benign — we report `no-lock` (the existing "already at rest" verdict).
1061
+ // (b) the remote ref still exists but at a DIFFERENT sha than we leased
1062
+ // against — a genuine concurrent mutation (e.g. a racer just marked it
1063
+ // `stuck`). Back off rather than force; this is the real `error`.
1064
+ const remote = await gitSoft(['ls-remote', arbiter, ref], cwd, env);
1065
+ const remoteEmpty = remote.status === 0 && remote.stdout.trim() === '';
1066
+ if (remoteEmpty) {
1067
+ // Drop our stale local copy too — a non-pruning fetch left it pointing at
1068
+ // the now-gone sha; with it gone locally a subsequent read in this clone
1069
+ // sees the correct (deleted) state.
1070
+ await gitSoft(['update-ref', '-d', ref], cwd, env);
1071
+ return {
1072
+ outcome: 'no-lock',
1073
+ entry,
1074
+ ref,
1075
+ terminalOnMain,
1076
+ message: `'${entry}' has no lock to reconcile (already cleared by another reaper / release-lock / requeue)`,
1077
+ };
1078
+ }
1079
+ return {
1080
+ outcome: 'error',
1081
+ entry,
1082
+ ref,
1083
+ terminalOnMain,
1084
+ message: `stale-lock clear for '${entry}' rejected (changed concurrently to a different value); a racer may have moved the ref. Re-run after re-checking.`,
1085
+ };
1086
+ } catch (err) {
1087
+ return {
1088
+ outcome: 'error',
1089
+ entry,
1090
+ ref,
1091
+ terminalOnMain: false,
1092
+ message: err instanceof Error ? err.message : String(err),
1093
+ };
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * The READ-ONLY classifier behind {@link reconcileItemLockAgainstMain}: it derives
1099
+ * the SAME {@link ReconcileOutcome} verdict, but NEVER mutates the arbiter — it
1100
+ * does NOT clear a stale-active lock, it only REPORTS that the lock IS
1101
+ * reconcilable. This is what the `gc --ledger` stuck-lock report keys off (the
1102
+ * forward-pointer's wiring): per the no-auto-sweep ADR rule, the plain report
1103
+ * surfaces a stale-active lock as `cleared-stale`-eligible WITHOUT clearing it —
1104
+ * a human still asserts the clear via `release-lock` (or runs complete's recovery,
1105
+ * which DOES clear via {@link reconcileItemLockAgainstMain}). The classification
1106
+ * is identical so the report and the recovery never disagree about WHAT a lock is;
1107
+ * only the ACTION differs (report vs clear).
1108
+ *
1109
+ * A `cleared-stale` outcome here therefore means "would be cleared by recovery /
1110
+ * `release-lock`" (the lock is terminal-on-`main` + `active` = stale), NOT that
1111
+ * anything was cleared. Best-effort + never throws (a fetch/read fault degrades to
1112
+ * `error`).
1113
+ */
1114
+ export async function classifyItemLockAgainstMain(
1115
+ opts: ReleaseOptions,
1116
+ ): Promise<ReconcileResult> {
1117
+ const arbiter = opts.arbiter ?? 'origin';
1118
+ const env = opts.env;
1119
+ const cwd = opts.cwd;
1120
+ if (!opts.item) {
1121
+ return {
1122
+ outcome: 'error',
1123
+ entry: '',
1124
+ ref: '',
1125
+ terminalOnMain: false,
1126
+ message: 'missing item',
1127
+ };
1128
+ }
1129
+ const {type, slug} = resolveSidecarIdentity(opts.item);
1130
+ const entry = lockEntryFor(opts.item);
1131
+ const ref = itemLockRef(entry);
1132
+ try {
1133
+ await gitHard(
1134
+ [
1135
+ 'fetch',
1136
+ '--quiet',
1137
+ arbiter,
1138
+ `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`,
1139
+ ],
1140
+ cwd,
1141
+ env,
1142
+ );
1143
+ await gitHard(['fetch', '--quiet', arbiter], cwd, env);
1144
+ const held = await fetchHeldEntry(entry, ref, cwd, arbiter, env);
1145
+ const terminalOnMain = await isTerminalOnMain(
1146
+ type,
1147
+ slug,
1148
+ arbiter,
1149
+ cwd,
1150
+ env,
1151
+ );
1152
+ if (!held) {
1153
+ return {
1154
+ outcome: 'no-lock',
1155
+ entry,
1156
+ ref,
1157
+ terminalOnMain,
1158
+ message: `'${entry}' has no lock to reconcile`,
1159
+ };
1160
+ }
1161
+ if (!terminalOnMain) {
1162
+ return {
1163
+ outcome: 'kept-in-flight',
1164
+ entry,
1165
+ ref,
1166
+ terminalOnMain,
1167
+ message: `'${entry}' is in flight (held, not terminal on ${arbiter}/main)`,
1168
+ };
1169
+ }
1170
+ if (held.lock.state === 'stuck') {
1171
+ return {
1172
+ outcome: 'kept-stuck',
1173
+ entry,
1174
+ ref,
1175
+ terminalOnMain,
1176
+ message: `'${entry}' is terminal on ${arbiter}/main but STUCK — kept for human attention (resume/requeue/release-lock)`,
1177
+ };
1178
+ }
1179
+ // Terminal on `main` + an `active` lock = a STALE lock. Unlike
1180
+ // `reconcileItemLockAgainstMain` we do NOT clear it here — the report only
1181
+ // names it as reconcilable; the human asserts the clear (no auto-sweep).
1182
+ return {
1183
+ outcome: 'cleared-stale',
1184
+ entry,
1185
+ ref,
1186
+ terminalOnMain,
1187
+ message: `'${entry}' is terminal on ${arbiter}/main but the lock is ACTIVE — reconcilable (stale); clear via 'release-lock' (NOT auto-cleared by the report)`,
1188
+ };
1189
+ } catch (err) {
1190
+ return {
1191
+ outcome: 'error',
1192
+ entry,
1193
+ ref,
1194
+ terminalOnMain: false,
1195
+ message: err instanceof Error ? err.message : String(err),
1196
+ };
1197
+ }
1198
+ }
1199
+
1200
+ /** One lingering lock in the `gc --ledger` stuck-lock REPORT: the held entry plus
1201
+ * the read-only cross-substrate {@link ReconcileOutcome} classification of it
1202
+ * against the authoritative `main` durable record (held/stuck vs stale-active over
1203
+ * a terminal item). Reported, NEVER cleared by the report itself. */
1204
+ export interface LockReportEntry {
1205
+ /** The full held lock entry (action × state + holder/since/reason). */
1206
+ lock: LockEntry;
1207
+ /** The lock ref (`refs/dorfl/lock/<entry>`). */
1208
+ ref: string;
1209
+ /**
1210
+ * The READ-ONLY {@link classifyItemLockAgainstMain} verdict:
1211
+ * - `kept-in-flight` — a normal in-flight hold (not terminal on `main`).
1212
+ * - `kept-stuck` — terminal on `main` + `stuck` (the `done`+`stuck`
1213
+ * co-existence; wins the human's attention, never cleared here).
1214
+ * - `cleared-stale` — terminal on `main` + `active` = a STALE lock the report
1215
+ * names as reconcilable; a human clears it via `release-lock` (NOT
1216
+ * auto-cleared by the report — no auto-sweep).
1217
+ * - `error` — a per-item classification fault (kept verbatim).
1218
+ */
1219
+ reconcile: ReconcileOutcome;
1220
+ }
1221
+
1222
+ /** The `gc --ledger` stuck/orphaned-lock REPORT (prd
1223
+ * `ledger-status-per-item-lock-refs` US #14): every lingering per-item lock on the
1224
+ * arbiter, each classified read-only against `main`. An EMPTY list = no locks held
1225
+ * (an absent ref namespace reads as `[]`, the recoverable "all locks released"
1226
+ * state — US #12). The report NEVER clears anything. */
1227
+ export interface ItemLockReport {
1228
+ locks: LockReportEntry[];
1229
+ }
1230
+
1231
+ /**
1232
+ * Build the `gc --ledger` stuck/orphaned-lock REPORT (prd
1233
+ * `ledger-status-per-item-lock-refs` US #12/#13/#14; ADR
1234
+ * `ledger-status-on-per-item-lock-refs`): enumerate every per-item lock currently
1235
+ * held on the arbiter ({@link listItemLockEntries} — held active + stuck, with
1236
+ * holder/since/reason) and classify EACH read-only against the authoritative
1237
+ * `main` durable record via {@link classifyItemLockAgainstMain} (the wiring the
1238
+ * `complete-lock-then-durable-main-move-crash-safe` task's
1239
+ * `reconcileItemLockAgainstMain` had no production caller for). This is the
1240
+ * generalisation of the (now-retired) advancing-marker report from advancing-only
1241
+ * to the UNIFIED lock (the `gc --ledger` stuck-lock report).
1242
+ *
1243
+ * It is a REPORT, never a sweep: it CLEARS nothing (no liveness heartbeat, no
1244
+ * auto-sweep — the same trust model as the advancing report; a human asserts a
1245
+ * lock is dead via `release-lock`). A stale-active lock over a terminal-on-`main`
1246
+ * item is surfaced as `cleared-stale`-eligible ("reconcilable") but left in place.
1247
+ *
1248
+ * Best-effort + degrades safely: an absent lock-ref namespace ⇒ an EMPTY report
1249
+ * ({@link listItemLockEntries} returns `[]`) — so a deleted lock ref reads as "all
1250
+ * locks released" (recoverable; work is safe on the `work/<slug>` branches +
1251
+ * `main`).
1252
+ */
1253
+ export async function reportItemLocks(
1254
+ cwd: string,
1255
+ arbiter = 'origin',
1256
+ env?: NodeJS.ProcessEnv,
1257
+ ): Promise<ItemLockReport> {
1258
+ const entries = await listItemLockEntries(cwd, arbiter, env);
1259
+ const locks: LockReportEntry[] = [];
1260
+ for (const lock of entries) {
1261
+ const ref = itemLockRef(lock.entry);
1262
+ // Read-only classification against `main` — NEVER clears (the report is
1263
+ // advisory; a human asserts the clear via `release-lock`).
1264
+ const verdict = await classifyItemLockAgainstMain({
1265
+ item: itemFromLockEntry(lock.entry),
1266
+ cwd,
1267
+ arbiter,
1268
+ env,
1269
+ });
1270
+ locks.push({lock, ref, reconcile: verdict.outcome});
1271
+ }
1272
+ return {locks};
1273
+ }
1274
+
1275
+ /**
1276
+ * Format the `gc --ledger` stuck/orphaned-lock REPORT for the terminal: one block
1277
+ * per lingering lock (entry, action/state, holder, since, the stuck `reason`, the
1278
+ * read-only `main`-reconciliation note, and a copy-pasteable `release-lock`
1279
+ * hint). An EMPTY report yields NO lines (a clean lock set is silent here, like
1280
+ * the duplicate-slug surface). It REPORTS only — the matching wording makes the
1281
+ * no-auto-sweep contract explicit (a human asserts a lock is dead).
1282
+ */
1283
+ /**
1284
+ * Does the `gc --ledger` lock report contain a lock that NEEDS HUMAN ATTENTION
1285
+ * (prd US#14/#21; ADR `ledger-status-on-per-item-lock-refs`: this surface is the
1286
+ * STUCK / crash-orphaned lock, NOT every held one)? TRUE iff some lock is
1287
+ * `kept-stuck` (terminal-on-`main` + stuck) or `cleared-stale`-eligible
1288
+ * (terminal-on-`main` + a stale active orphan). A `kept-in-flight` (active,
1289
+ * non-terminal) lock is the NORMAL in-flight state of a healthy concurrent build
1290
+ * (read by `status` as healthy) and does NOT count — so a routine `gc --ledger`
1291
+ * health check whose only locks are healthy in-flight holds exits 0. This is the
1292
+ * fail-loud EXIT predicate for the gc lock surface (the report itself still lists
1293
+ * every held lock, in-flight ones informationally).
1294
+ */
1295
+ export function itemLockReportNeedsAttention(report: ItemLockReport): boolean {
1296
+ return report.locks.some(
1297
+ (l) => l.reconcile === 'kept-stuck' || l.reconcile === 'cleared-stale',
1298
+ );
1299
+ }
1300
+
1301
+ export function formatItemLockReport(report: ItemLockReport): string[] {
1302
+ if (report.locks.length === 0) {
1303
+ return [];
1304
+ }
1305
+ const lines = [
1306
+ `Per-item locks: ${report.locks.length} lock(s) held on the arbiter ` +
1307
+ '(REPORT only — no automatic sweep; the lock has no liveness heartbeat, so ' +
1308
+ 'a human asserts a stuck/stale lock is dead via `release-lock`):',
1309
+ ];
1310
+ for (const {lock, reconcile} of report.locks) {
1311
+ const item = itemFromLockEntry(lock.entry);
1312
+ lines.push(` ${lock.entry} [${lock.action}/${lock.state}]`);
1313
+ lines.push(
1314
+ ` holder: ${lock.holder || '(unknown)'} since: ${lock.since || '(unknown)'}`,
1315
+ );
1316
+ if (lock.state === 'stuck' && lock.reason) {
1317
+ lines.push(` reason: ${lock.reason}`);
1318
+ }
1319
+ lines.push(` ${reconcileNote(reconcile)}`);
1320
+ lines.push(
1321
+ ` resolve (if the lock is dead): \`dorfl release-lock ${item}\` (never --force).`,
1322
+ );
1323
+ }
1324
+ return lines;
1325
+ }
1326
+
1327
+ /** The human-readable line for a lock's read-only `main`-reconciliation verdict. */
1328
+ function reconcileNote(reconcile: ReconcileOutcome): string {
1329
+ switch (reconcile) {
1330
+ case 'kept-in-flight':
1331
+ return 'in flight (held, not terminal on main) — normal; left untouched.';
1332
+ case 'kept-stuck':
1333
+ return 'terminal on main + STUCK (done+stuck co-exist) — kept for human attention.';
1334
+ case 'cleared-stale':
1335
+ return 'terminal on main + ACTIVE = STALE (reconcilable) — NOT auto-cleared; a human clears it.';
1336
+ case 'no-lock':
1337
+ return 'no lock (already at rest).';
1338
+ case 'error':
1339
+ return 'reconciliation against main could not be determined (left untouched — the safe direction).';
1340
+ }
1341
+ }
1342
+
1343
+ /** Per-lock outcome of the human-invoked {@link reapStaleItemLocks} SWEEP:
1344
+ * - `reaped` — a `cleared-stale` lock (terminal-on-main + active) cleared
1345
+ * via the SHARED leased delete.
1346
+ * - `already-reaped` — BENIGN: the lock was already gone by the time the sweep
1347
+ * re-read the ref (`no-lock`) — another reaper / a
1348
+ * `release-lock` / a `requeue` got there first; the ref is
1349
+ * at the desired end state. NOT `lost` and does NOT count
1350
+ * as needs-attention (see the exit-code contract recorded
1351
+ * in this task's done record).
1352
+ * - `kept-stuck` — left untouched (terminal + stuck — human attention; US #10).
1353
+ * - `kept-in-flight` — left untouched (active, non-terminal — a healthy build).
1354
+ * - `lost` — a `cleared-stale` candidate whose leased delete was REJECTED
1355
+ * (the ref changed concurrently to a DIFFERENT value);
1356
+ * REPORTED, never `--force`d.
1357
+ * - `error` — a per-item classification/clear fault (left untouched). */
1358
+ export type ReapOutcome =
1359
+ | 'reaped'
1360
+ | 'already-reaped'
1361
+ | 'kept-stuck'
1362
+ | 'kept-in-flight'
1363
+ | 'lost'
1364
+ | 'error';
1365
+
1366
+ /** One lock's disposition in the {@link ReapReport}. */
1367
+ export interface ReapEntry {
1368
+ /** The full lock entry as the report saw it (action × state + holder/since). */
1369
+ lock: LockEntry;
1370
+ /** The lock ref (`refs/dorfl/lock/<entry>`). */
1371
+ ref: string;
1372
+ /** What the sweep DID with this lock. */
1373
+ outcome: ReapOutcome;
1374
+ /** A human-readable note (the clear message, or why it was kept). */
1375
+ message: string;
1376
+ }
1377
+
1378
+ /** The result of {@link reapStaleItemLocks}: every held lock with what the sweep
1379
+ * did to it, plus convenience counts. A lock NEEDS HUMAN ATTENTION after the sweep
1380
+ * iff it is `kept-stuck` or `lost` (a `cleared-stale` whose leased delete lost the
1381
+ * race); a `kept-in-flight` is the normal healthy state and does NOT. */
1382
+ export interface ReapReport {
1383
+ entries: ReapEntry[];
1384
+ reaped: number;
1385
+ /** BENIGN already-reaped count: the sweep found the ref already gone (`no-lock`)
1386
+ * — another reaper / release-lock / requeue cleared it first. The desired end
1387
+ * state; NOT `lost`, does NOT contribute to needs-attention / a non-zero exit. */
1388
+ alreadyReaped: number;
1389
+ kept: number;
1390
+ lost: number;
1391
+ }
1392
+
1393
+ /**
1394
+ * The OPT-IN `gc --ledger --reap-stale-locks` SWEEP (prd
1395
+ * `ledger-status-per-item-lock-refs` US #14): a human asserting "clear the dead
1396
+ * TERMINAL locks now", so one command sweeps every orphaned terminal lock instead
1397
+ * of N hand-run `release-lock`s. It is the WRITE twin of {@link reportItemLocks}
1398
+ * (the default report-only surface): it enumerates the SAME held locks, classifies
1399
+ * each read-only, and for EXACTLY the `cleared-stale` class (terminal-on-`main` +
1400
+ * `active` = stranded) performs the SHARED leased delete via
1401
+ * {@link reconcileItemLockAgainstMain} (the recovery's clear, re-checked fresh per
1402
+ * item) — there is NO parallel clear mechanism.
1403
+ *
1404
+ * SCOPE FENCE (the trust model the default preserves):
1405
+ * - it clears ONLY `cleared-stale`. A `kept-stuck` (terminal + stuck — human
1406
+ * attention) and a `kept-in-flight` (active + non-terminal — a healthy build)
1407
+ * are NEVER reaped, even here. Because each clear goes through
1408
+ * {@link reconcileItemLockAgainstMain}, which RE-reads + RE-classifies before
1409
+ * deleting, a lock that turned stuck/in-flight between the report and the sweep
1410
+ * is still safe (reconcile returns `kept-*`, not a delete).
1411
+ * - the clear is a LEASED delete: a concurrent change to the ref makes it REJECT
1412
+ * (`lost`), reported — never a blind `--force`.
1413
+ *
1414
+ * The DEFAULT `gc --ledger` (no flag) never calls this: it stays report-only,
1415
+ * fail-loud, delete-nothing. This is gated behind the explicit `--reap-stale-locks`
1416
+ * flag (a human authorising the clear), exactly as `release-lock`'s trust model.
1417
+ */
1418
+ export async function reapStaleItemLocks(
1419
+ cwd: string,
1420
+ arbiter = 'origin',
1421
+ env?: NodeJS.ProcessEnv,
1422
+ ): Promise<ReapReport> {
1423
+ const report = await reportItemLocks(cwd, arbiter, env);
1424
+ const entries: ReapEntry[] = [];
1425
+ let reaped = 0;
1426
+ let alreadyReaped = 0;
1427
+ let kept = 0;
1428
+ let lost = 0;
1429
+ for (const {lock, ref, reconcile} of report.locks) {
1430
+ const item = itemFromLockEntry(lock.entry);
1431
+ if (reconcile === 'cleared-stale') {
1432
+ // Re-check + clear through the recovery's SHARED leased delete. Reconcile
1433
+ // re-reads the live ref, so a lock that turned stuck/in-flight since the
1434
+ // report is left alone; a concurrent change to the ref makes the lease lose.
1435
+ const rec = await reconcileItemLockAgainstMain({
1436
+ item,
1437
+ cwd,
1438
+ arbiter,
1439
+ env,
1440
+ });
1441
+ if (rec.outcome === 'cleared-stale') {
1442
+ reaped++;
1443
+ entries.push({lock, ref, outcome: 'reaped', message: rec.message});
1444
+ } else if (rec.outcome === 'no-lock') {
1445
+ // BENIGN: the ref is already gone — the desired end state. The LOSER of
1446
+ // a concurrent double-reap (another reaper deleted the ref between our
1447
+ // report and our re-read), or a `release-lock`/`requeue` that cleared
1448
+ // the same stale lock in the meantime. NOT a lost lease (the lease was
1449
+ // not REJECTED; there was simply nothing left to delete), so this does
1450
+ // NOT count as needs-attention. Kept SEPARATE from `reaped` so the
1451
+ // summary does not lie about who did the deleting.
1452
+ alreadyReaped++;
1453
+ entries.push({
1454
+ lock,
1455
+ ref,
1456
+ outcome: 'already-reaped',
1457
+ message: rec.message,
1458
+ });
1459
+ } else if (
1460
+ rec.outcome === 'kept-stuck' ||
1461
+ rec.outcome === 'kept-in-flight'
1462
+ ) {
1463
+ // The lock changed between the report and the sweep — no longer stale.
1464
+ kept++;
1465
+ entries.push({lock, ref, outcome: rec.outcome, message: rec.message});
1466
+ } else {
1467
+ // A lost lease (the ref was REJECTED because it changed concurrently to
1468
+ // a DIFFERENT value) or a per-item error — REPORTED, never forced.
1469
+ // Counts as needing attention after the sweep.
1470
+ lost++;
1471
+ entries.push({lock, ref, outcome: 'lost', message: rec.message});
1472
+ }
1473
+ continue;
1474
+ }
1475
+ // NOT a cleared-stale candidate: a stuck or in-flight lock the reaper must
1476
+ // NEVER touch, even with the flag.
1477
+ if (reconcile === 'kept-stuck' || reconcile === 'kept-in-flight') {
1478
+ kept++;
1479
+ entries.push({
1480
+ lock,
1481
+ ref,
1482
+ outcome: reconcile,
1483
+ message: reconcileNote(reconcile),
1484
+ });
1485
+ } else {
1486
+ // no-lock / error from the classifier — nothing to reap; surface verbatim.
1487
+ lost++;
1488
+ entries.push({
1489
+ lock,
1490
+ ref,
1491
+ outcome: 'error',
1492
+ message: reconcileNote(reconcile),
1493
+ });
1494
+ }
1495
+ }
1496
+ return {entries, reaped, alreadyReaped, kept, lost};
1497
+ }
1498
+
1499
+ /**
1500
+ * Does a {@link reapStaleItemLocks} sweep leave a lock that STILL needs human
1501
+ * attention? TRUE iff some entry is `kept-stuck` (a stuck lock the reaper rightly
1502
+ * left) or `lost`/`error` (a `cleared-stale` whose leased delete lost the race, or
1503
+ * an unresolvable fault). A `reaped` (successfully cleared) or a `kept-in-flight`
1504
+ * (healthy build) lock does NOT count — so a sweep that cleared every stale lock
1505
+ * and left only healthy in-flight holds exits 0. This is the post-sweep analogue of
1506
+ * {@link itemLockReportNeedsAttention}.
1507
+ */
1508
+ export function reapReportNeedsAttention(report: ReapReport): boolean {
1509
+ // EXIT-CODE CONTRACT (recorded in this task's done record): the reaper exits 0
1510
+ // when all stale locks are reaped and only healthy in-flight locks remain;
1511
+ // exits 1 when a `kept-stuck` survives or a delete genuinely lost the race /
1512
+ // errored. An `already-reaped` (the loser of a concurrent double-reap saw the
1513
+ // ref already gone via `no-lock`) is BENIGN — the desired end state — and is
1514
+ // NOT in this set.
1515
+ return report.entries.some(
1516
+ (e) =>
1517
+ e.outcome === 'kept-stuck' ||
1518
+ e.outcome === 'lost' ||
1519
+ e.outcome === 'error',
1520
+ );
1521
+ }
1522
+
1523
+ /**
1524
+ * Format the `gc --ledger --reap-stale-locks` sweep for the terminal: a header
1525
+ * line with the reaped/kept counts, then one line per lock (what was reaped vs
1526
+ * kept vs lost). An EMPTY sweep (no locks held) yields NO lines (silent, like the
1527
+ * report). The wording keeps the no-blind-force contract explicit.
1528
+ */
1529
+ export function formatReapReport(report: ReapReport): string[] {
1530
+ if (report.entries.length === 0) {
1531
+ return [];
1532
+ }
1533
+ const lines = [
1534
+ `Per-item lock sweep (--reap-stale-locks): reaped ${report.reaped} stale ` +
1535
+ `terminal lock(s), kept ${report.kept} (stuck/in-flight, never reaped)` +
1536
+ (report.alreadyReaped > 0
1537
+ ? `, ${report.alreadyReaped} already reaped by another sweep (no-lock — benign, the desired end state)`
1538
+ : '') +
1539
+ (report.lost > 0
1540
+ ? `, ${report.lost} could not be cleared (lease lost / error — reported, NEVER forced)`
1541
+ : '') +
1542
+ ':',
1543
+ ];
1544
+ for (const {lock, outcome, message} of report.entries) {
1545
+ const tag =
1546
+ outcome === 'reaped'
1547
+ ? '[reaped] '
1548
+ : outcome === 'already-reaped'
1549
+ ? '[already] '
1550
+ : outcome === 'lost'
1551
+ ? '[lost] '
1552
+ : outcome === 'error'
1553
+ ? '[error] '
1554
+ : '[kept] ';
1555
+ lines.push(
1556
+ ` ${tag} ${lock.entry} [${lock.action}/${lock.state}] ${message}`,
1557
+ );
1558
+ }
1559
+ return lines;
1560
+ }
1561
+
1562
+ /**
1563
+ * Convert a type-encoded lock `<entry>` (`<type>-<slug>`) back into the namespaced
1564
+ * item form (`<namespace>:<slug>`) that {@link lockEntryFor} /
1565
+ * {@link resolveSidecarIdentity} accept — so the report's suggested `release-lock`
1566
+ * command is copy-pasteable and the classifier can re-key off the SAME identity.
1567
+ * Unknown prefixes fall back to the raw entry (still copyable). The inverse of
1568
+ * {@link lockEntryFor} for the three known namespaces.
1569
+ */
1570
+ export function itemFromLockEntry(entry: string): string {
1571
+ // The lock-entry prefixes: `spec` is the parent-spec type token (the legacy
1572
+ // `prd` token is GONE after the hard cutover), so a `spec-<slug>` lock entry
1573
+ // round-trips back to its namespaced `spec:<slug>` form (the inverse of
1574
+ // `lockEntryFor('spec:<slug>')`).
1575
+ for (const prefix of ['task', 'spec', 'observation'] as const) {
1576
+ const tag = `${prefix}-`;
1577
+ if (entry.startsWith(tag)) {
1578
+ return `${prefix}:${entry.slice(tag.length)}`;
1579
+ }
1580
+ }
1581
+ return entry;
1582
+ }
1583
+
1584
+ /** True iff `<arbiter>/main` shows the item TERMINAL — any of
1585
+ * {@link terminalMainPaths} present in `<arbiter>/main`'s tree. */
1586
+ async function isTerminalOnMain(
1587
+ type: SidecarType,
1588
+ slug: string,
1589
+ arbiter: string,
1590
+ cwd: string,
1591
+ env: NodeJS.ProcessEnv | undefined,
1592
+ ): Promise<boolean> {
1593
+ for (const path of terminalMainPaths(type, slug)) {
1594
+ const exists =
1595
+ (await gitSoft(['cat-file', '-e', `${arbiter}/main:${path}`], cwd, env))
1596
+ .status === 0;
1597
+ if (exists) {
1598
+ return true;
1599
+ }
1600
+ }
1601
+ return false;
1602
+ }
1603
+
1604
+ /** List the entries (`<type>-<slug>`) currently locked on the arbiter (the
1605
+ * stuck-lock report / `status` read path). */
1606
+ export async function listItemLocks(
1607
+ cwd: string,
1608
+ arbiter = 'origin',
1609
+ env?: NodeJS.ProcessEnv,
1610
+ ): Promise<string[]> {
1611
+ await gitHard(
1612
+ ['fetch', '--quiet', arbiter, `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`],
1613
+ cwd,
1614
+ env,
1615
+ );
1616
+ const out = await gitSoft(
1617
+ ['for-each-ref', '--format=%(refname)', `${LOCK_REF_PREFIX}/*`],
1618
+ cwd,
1619
+ env,
1620
+ );
1621
+ if (out.status !== 0) {
1622
+ return [];
1623
+ }
1624
+ return out.stdout
1625
+ .split('\n')
1626
+ .map((l) => l.trim())
1627
+ .filter((l) => l.startsWith(`${LOCK_REF_PREFIX}/`))
1628
+ .map((l) => l.slice(`${LOCK_REF_PREFIX}/`.length))
1629
+ .sort();
1630
+ }
1631
+
1632
+ /**
1633
+ * Read the FULL lock entries currently held on the arbiter — the `status`/`scan`
1634
+ * in-flight read path (prd `ledger-status-per-item-lock-refs` US #8; task
1635
+ * `needs-attention-as-stuck-lock-state`). One fetch of the lock refs, then read
1636
+ * each held entry's `lock.md` blob, returning the parsed {@link LockEntry} for
1637
+ * every ref (so a caller can surface `active` (in-progress) and `stuck`
1638
+ * (needs-attention) holds + their reasons WITHOUT N fetches). Sorted by `entry`.
1639
+ * Best-effort: a fetch/read fault yields an EMPTY list, so the read-only
1640
+ * `status`/`scan` views degrade to "no in-flight locks" rather than erroring
1641
+ * (parity with {@link heldTaskSlugs}).
1642
+ */
1643
+ export async function listItemLockEntries(
1644
+ cwd: string,
1645
+ arbiter = 'origin',
1646
+ env?: NodeJS.ProcessEnv,
1647
+ ): Promise<LockEntry[]> {
1648
+ try {
1649
+ await gitHard(
1650
+ [
1651
+ 'fetch',
1652
+ '--quiet',
1653
+ arbiter,
1654
+ `+${LOCK_REF_PREFIX}/*:${LOCK_REF_PREFIX}/*`,
1655
+ ],
1656
+ cwd,
1657
+ env,
1658
+ );
1659
+ const out = await gitSoft(
1660
+ ['for-each-ref', '--format=%(refname)', `${LOCK_REF_PREFIX}/*`],
1661
+ cwd,
1662
+ env,
1663
+ );
1664
+ if (out.status !== 0) {
1665
+ return [];
1666
+ }
1667
+ const refs = out.stdout
1668
+ .split('\n')
1669
+ .map((l) => l.trim())
1670
+ .filter((l) => l.startsWith(`${LOCK_REF_PREFIX}/`))
1671
+ .sort();
1672
+ const entries: LockEntry[] = [];
1673
+ for (const ref of refs) {
1674
+ const show = await gitSoft(['show', `${ref}:lock.md`], cwd, env);
1675
+ if (show.status !== 0) {
1676
+ continue;
1677
+ }
1678
+ const lock = parseLockEntry(show.stdout);
1679
+ if (lock) {
1680
+ entries.push(lock);
1681
+ }
1682
+ }
1683
+ return entries;
1684
+ } catch {
1685
+ return [];
1686
+ }
1687
+ }
1688
+
1689
+ /**
1690
+ * List the TASK slugs currently lock-held on the arbiter — the held-slug set the
1691
+ * `ready/` pool readers SUBTRACT (prd `ledger-status-per-item-lock-refs` US #15;
1692
+ * task `claim-acquires-unified-lock-no-body-move`). Enumerates {@link listItemLocks}
1693
+ * and keeps only the `task-<slug>` entries (a prd/observation lock does not gate
1694
+ * the TASK pool), mapping each to its bare `<slug>`.
1695
+ *
1696
+ * LOAD-BEARING since the lock cut-over: the claim NO LONGER moves the body to
1697
+ * `in-progress/` (it stays in the pool on `main`; the held lock IS the claim), so
1698
+ * this held-slug set is the ONLY signal that keeps a claimed / in-flight item out
1699
+ * of the eligible pool, NOT the redundant subtraction it was while the body-move
1700
+ * still removed claimed items.
1701
+ *
1702
+ * GRACEFUL (fail-OPEN) by design — this is the SURFACE reader: a fetch fault
1703
+ * yields an EMPTY set so the read-only `status`/`scan` views degrade to "no
1704
+ * in-flight locks" rather than erroring. That is WRONG for SELECTION (an empty set
1705
+ * on a read FAULT lets a continuously-held in-flight item leak back into the
1706
+ * eligible pool → re-claimed → empty diff → spurious `stuck`). The SELECTION path
1707
+ * must instead use {@link heldTaskSlugsStrict}, which THROWS on a read fault so
1708
+ * the caller can fail CLOSED (refuse to enumerate an untrusted pool) rather than
1709
+ * subtract nothing. This graceful variant delegates to the strict one and only
1710
+ * swallows the fault.
1711
+ */
1712
+ export async function heldTaskSlugs(
1713
+ cwd: string,
1714
+ arbiter = 'origin',
1715
+ env?: NodeJS.ProcessEnv,
1716
+ ): Promise<Set<string>> {
1717
+ try {
1718
+ return await heldTaskSlugsStrict(cwd, arbiter, env);
1719
+ } catch {
1720
+ return new Set();
1721
+ }
1722
+ }
1723
+
1724
+ /**
1725
+ * STRICT (fail-CLOSED) twin of {@link heldTaskSlugs} for the SELECTION path: read
1726
+ * the held TASK slugs from the arbiter and THROW on any read fault (offline / dead
1727
+ * arbiter / unreadable lock refs) instead of degrading to an empty set. The
1728
+ * held-lock set lives ONLY on the arbiter and is the LOAD-BEARING signal that
1729
+ * keeps a claimed / in-flight item out of the eligible pool, so a SELECTION read
1730
+ * that cannot reach the arbiter must NOT pretend "no locks held" — it must fail so
1731
+ * the caller refuses to enumerate a pool it cannot trust (the
1732
+ * `scan-cwd-selection-pool-read-local-skips-held-lock-subtraction-offline-must-fail`
1733
+ * decision: offline selection FAILS, there is no `--local` fallback). The
1734
+ * read-only surface keeps the graceful {@link heldTaskSlugs}.
1735
+ */
1736
+ export async function heldTaskSlugsStrict(
1737
+ cwd: string,
1738
+ arbiter = 'origin',
1739
+ env?: NodeJS.ProcessEnv,
1740
+ ): Promise<Set<string>> {
1741
+ // `listItemLocks` fetches the lock refs with `gitHard` (throws on a failed
1742
+ // fetch) and returns the held entries; we do NOT swallow that throw here.
1743
+ const entries = await listItemLocks(cwd, arbiter, env);
1744
+ const prefix = 'task-';
1745
+ return new Set(
1746
+ entries
1747
+ .filter((e) => e.startsWith(prefix))
1748
+ .map((e) => e.slice(prefix.length)),
1749
+ );
1750
+ }
1751
+
1752
+ /**
1753
+ * Parse a serialised lock entry body back into a {@link LockEntry} — the exact
1754
+ * inverse of {@link serialiseLockEntry}. Reads the two-axis state from the
1755
+ * frontmatter and, for a stuck entry, the FULL reason prose + any questions from
1756
+ * the body (`## Reason` / `## Questions`). Tolerates a LEGACY entry whose reason
1757
+ * lived in a one-line frontmatter `reason:` field (the pre-cutover shape) so a
1758
+ * lock written by an older binary still reads.
1759
+ */
1760
+ export function parseLockEntry(body: string): LockEntry | undefined {
1761
+ const normalized = body.replace(/\r\n/g, '\n');
1762
+ const fm = /^---\n([\s\S]*?)\n---/.exec(normalized);
1763
+ if (!fm) {
1764
+ return undefined;
1765
+ }
1766
+ const fields: Record<string, string> = {};
1767
+ for (const line of fm[1].split('\n')) {
1768
+ const m = /^([a-zA-Z]+):\s*(.*)$/.exec(line);
1769
+ if (m) {
1770
+ fields[m[1]] = m[2];
1771
+ }
1772
+ }
1773
+ if (!fields.entry || !fields.action || !fields.state) {
1774
+ return undefined;
1775
+ }
1776
+ const bodyText = normalized.slice(fm[0].length);
1777
+ const reason = extractBodyReason(bodyText) ?? fields.reason;
1778
+ const questions = extractBodyQuestions(bodyText);
1779
+ const entry: LockEntry = {
1780
+ entry: fields.entry,
1781
+ action: fields.action as LockAction,
1782
+ state: fields.state as LockState,
1783
+ holder: fields.holder ?? '',
1784
+ since: fields.since ?? '',
1785
+ };
1786
+ if (reason !== undefined && reason !== '') {
1787
+ entry.reason = reason;
1788
+ }
1789
+ if (questions.length > 0) {
1790
+ entry.questions = questions;
1791
+ }
1792
+ return entry;
1793
+ }
1794
+
1795
+ /** Extract the `## Reason` block's prose (joined multi-line), or undefined. */
1796
+ function extractBodyReason(bodyText: string): string | undefined {
1797
+ const lines = bodyText.split('\n');
1798
+ const start = lines.findIndex((l) => l.trim() === LOCK_REASON_HEADING);
1799
+ if (start === -1) {
1800
+ return undefined;
1801
+ }
1802
+ const collected: string[] = [];
1803
+ for (let i = start + 1; i < lines.length; i++) {
1804
+ if (/^##\s/.test(lines[i])) {
1805
+ break;
1806
+ }
1807
+ collected.push(lines[i]);
1808
+ }
1809
+ // Trim leading/trailing blank lines but PRESERVE interior newlines (rich prose).
1810
+ const text = collected.join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
1811
+ return text === '' ? undefined : text;
1812
+ }
1813
+
1814
+ /** Extract the `## Questions` block's bulleted list, or [] when absent. */
1815
+ function extractBodyQuestions(bodyText: string): string[] {
1816
+ const lines = bodyText.split('\n');
1817
+ const start = lines.findIndex((l) => l.trim() === LOCK_QUESTIONS_HEADING);
1818
+ if (start === -1) {
1819
+ return [];
1820
+ }
1821
+ const questions: string[] = [];
1822
+ for (let i = start + 1; i < lines.length; i++) {
1823
+ if (/^##\s/.test(lines[i])) {
1824
+ break;
1825
+ }
1826
+ const m = /^-\s+(.*)$/.exec(lines[i].trim());
1827
+ if (m) {
1828
+ questions.push(m[1]);
1829
+ }
1830
+ }
1831
+ return questions;
1832
+ }
1833
+
1834
+ /**
1835
+ * Advisory holder id: git user.name, else $USER, else a uuid fragment. Exported
1836
+ * as {@link resolveLockHolder} so callers (e.g. claim's stale-lock self-heal) can
1837
+ * resolve THE SAME holder string the lock would stamp, to compare against a held
1838
+ * entry's `holder` without duplicating the resolution order.
1839
+ */
1840
+ export async function resolveLockHolder(
1841
+ cwd: string,
1842
+ env?: NodeJS.ProcessEnv,
1843
+ ): Promise<string> {
1844
+ return resolveHolder(cwd, env);
1845
+ }
1846
+
1847
+ /** Advisory holder id: git user.name, else $USER, else a uuid fragment. */
1848
+ async function resolveHolder(
1849
+ cwd: string,
1850
+ env: NodeJS.ProcessEnv | undefined,
1851
+ ): Promise<string> {
1852
+ const name = await gitSoft(['config', 'user.name'], cwd, env);
1853
+ if (name.status === 0 && name.stdout.trim() !== '') {
1854
+ return name.stdout.trim();
1855
+ }
1856
+ const e = env ?? process.env;
1857
+ return e.USER ?? e.USERNAME ?? randomUUID().slice(0, 8);
1858
+ }