dorfl 0.1.1 → 0.2.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 (492) hide show
  1. package/dist/advance-ci-template.d.ts +3 -3
  2. package/dist/advance-ci-template.js +1 -1
  3. package/dist/advance-ci-template.js.map +1 -1
  4. package/dist/advance-classify.d.ts +5 -5
  5. package/dist/advance-classify.d.ts.map +1 -1
  6. package/dist/advance-classify.js +4 -4
  7. package/dist/advance-drivers.d.ts +8 -8
  8. package/dist/advance-drivers.d.ts.map +1 -1
  9. package/dist/advance-drivers.js +20 -8
  10. package/dist/advance-drivers.js.map +1 -1
  11. package/dist/advance-isolated.d.ts +3 -3
  12. package/dist/advance-isolated.d.ts.map +1 -1
  13. package/dist/advance-isolated.js +1 -1
  14. package/dist/advance-lifecycle-template.d.ts +2 -2
  15. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  16. package/dist/advance-lifecycle-template.js +85 -13
  17. package/dist/advance-lifecycle-template.js.map +1 -1
  18. package/dist/advance-loop-driver.d.ts +3 -3
  19. package/dist/advance-loop-driver.d.ts.map +1 -1
  20. package/dist/advance-loop-driver.js +1 -1
  21. package/dist/advance-treeless-publish.d.ts +24 -1
  22. package/dist/advance-treeless-publish.d.ts.map +1 -1
  23. package/dist/advance-treeless-publish.js +41 -0
  24. package/dist/advance-treeless-publish.js.map +1 -1
  25. package/dist/advance.d.ts +69 -17
  26. package/dist/advance.d.ts.map +1 -1
  27. package/dist/advance.js +409 -102
  28. package/dist/advance.js.map +1 -1
  29. package/dist/advancing-lock.d.ts +31 -3
  30. package/dist/advancing-lock.d.ts.map +1 -1
  31. package/dist/advancing-lock.js +55 -5
  32. package/dist/advancing-lock.js.map +1 -1
  33. package/dist/agent-launch.d.ts +12 -0
  34. package/dist/agent-launch.d.ts.map +1 -1
  35. package/dist/agent-launch.js +22 -12
  36. package/dist/agent-launch.js.map +1 -1
  37. package/dist/agent-stop.d.ts +40 -2
  38. package/dist/agent-stop.d.ts.map +1 -1
  39. package/dist/agent-stop.js +30 -2
  40. package/dist/agent-stop.js.map +1 -1
  41. package/dist/apply-decide.d.ts +19 -5
  42. package/dist/apply-decide.d.ts.map +1 -1
  43. package/dist/apply-decide.js +38 -9
  44. package/dist/apply-decide.js.map +1 -1
  45. package/dist/apply-merge-action.d.ts +20 -8
  46. package/dist/apply-merge-action.d.ts.map +1 -1
  47. package/dist/apply-merge-action.js +44 -9
  48. package/dist/apply-merge-action.js.map +1 -1
  49. package/dist/apply-persist.d.ts +62 -31
  50. package/dist/apply-persist.d.ts.map +1 -1
  51. package/dist/apply-persist.js +173 -44
  52. package/dist/apply-persist.js.map +1 -1
  53. package/dist/apply-stuck-action.d.ts +151 -0
  54. package/dist/apply-stuck-action.d.ts.map +1 -0
  55. package/dist/apply-stuck-action.js +125 -0
  56. package/dist/apply-stuck-action.js.map +1 -0
  57. package/dist/brand.d.ts +12 -1
  58. package/dist/brand.d.ts.map +1 -1
  59. package/dist/brand.js +2 -1
  60. package/dist/brand.js.map +1 -1
  61. package/dist/buildable-body.d.ts +14 -14
  62. package/dist/buildable-body.js +9 -9
  63. package/dist/claim-cas.d.ts +3 -3
  64. package/dist/claim-cas.js +1 -1
  65. package/dist/claim-cas.js.map +1 -1
  66. package/dist/cli-spinner.d.ts +1 -1
  67. package/dist/cli-spinner.js +1 -1
  68. package/dist/cli.d.ts +10 -0
  69. package/dist/cli.d.ts.map +1 -1
  70. package/dist/cli.js +385 -128
  71. package/dist/cli.js.map +1 -1
  72. package/dist/close-job-template.d.ts +3 -3
  73. package/dist/close-job-template.js +9 -9
  74. package/dist/close-job.d.ts +8 -8
  75. package/dist/close-job.js +25 -25
  76. package/dist/close-job.js.map +1 -1
  77. package/dist/complete.d.ts +9 -6
  78. package/dist/complete.d.ts.map +1 -1
  79. package/dist/complete.js +99 -45
  80. package/dist/complete.js.map +1 -1
  81. package/dist/concurrency.d.ts +1 -1
  82. package/dist/concurrency.js +1 -1
  83. package/dist/config.d.ts +86 -40
  84. package/dist/config.d.ts.map +1 -1
  85. package/dist/config.js +61 -9
  86. package/dist/config.js.map +1 -1
  87. package/dist/continue-branch.d.ts +1 -1
  88. package/dist/continue-branch.d.ts.map +1 -1
  89. package/dist/continue-branch.js +23 -1
  90. package/dist/continue-branch.js.map +1 -1
  91. package/dist/cwd-section.js +1 -1
  92. package/dist/cwd-section.js.map +1 -1
  93. package/dist/decision-engine.d.ts +39 -11
  94. package/dist/decision-engine.d.ts.map +1 -1
  95. package/dist/decision-engine.js +10 -6
  96. package/dist/decision-engine.js.map +1 -1
  97. package/dist/do-autopick.d.ts +6 -6
  98. package/dist/do-autopick.d.ts.map +1 -1
  99. package/dist/do-autopick.js +16 -6
  100. package/dist/do-autopick.js.map +1 -1
  101. package/dist/do-config.d.ts +4 -4
  102. package/dist/do-config.js +3 -3
  103. package/dist/do-config.js.map +1 -1
  104. package/dist/do-remote-auto.d.ts +2 -2
  105. package/dist/do-remote-auto.js +1 -1
  106. package/dist/do.d.ts +74 -81
  107. package/dist/do.d.ts.map +1 -1
  108. package/dist/do.js +439 -50
  109. package/dist/do.js.map +1 -1
  110. package/dist/drop-source.d.ts +2 -2
  111. package/dist/env-config.d.ts.map +1 -1
  112. package/dist/env-config.js +19 -11
  113. package/dist/env-config.js.map +1 -1
  114. package/dist/failure-cause.d.ts +3 -2
  115. package/dist/failure-cause.d.ts.map +1 -1
  116. package/dist/failure-cause.js +28 -1
  117. package/dist/failure-cause.js.map +1 -1
  118. package/dist/format.d.ts +6 -6
  119. package/dist/format.d.ts.map +1 -1
  120. package/dist/format.js +13 -30
  121. package/dist/format.js.map +1 -1
  122. package/dist/frontmatter.d.ts +28 -15
  123. package/dist/frontmatter.d.ts.map +1 -1
  124. package/dist/frontmatter.js +38 -24
  125. package/dist/frontmatter.js.map +1 -1
  126. package/dist/gc.d.ts +65 -6
  127. package/dist/gc.d.ts.map +1 -1
  128. package/dist/gc.js +126 -9
  129. package/dist/gc.js.map +1 -1
  130. package/dist/github.d.ts +14 -0
  131. package/dist/github.d.ts.map +1 -1
  132. package/dist/github.js +73 -0
  133. package/dist/github.js.map +1 -1
  134. package/dist/harness.d.ts +25 -0
  135. package/dist/harness.d.ts.map +1 -1
  136. package/dist/harness.js.map +1 -1
  137. package/dist/index.d.ts +4 -1
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +2 -1
  140. package/dist/index.js.map +1 -1
  141. package/dist/install-ci-branch-protection.d.ts +133 -39
  142. package/dist/install-ci-branch-protection.d.ts.map +1 -1
  143. package/dist/install-ci-branch-protection.js +191 -49
  144. package/dist/install-ci-branch-protection.js.map +1 -1
  145. package/dist/install-ci-capabilities/advance-lifecycle.d.ts +1 -1
  146. package/dist/install-ci-capabilities/advance-lifecycle.js +1 -1
  147. package/dist/install-ci-capabilities/close-job.d.ts +1 -1
  148. package/dist/install-ci-capabilities/close-job.js +1 -1
  149. package/dist/install-ci-capabilities/example-noop.d.ts +1 -1
  150. package/dist/install-ci-capabilities/example-noop.js +1 -1
  151. package/dist/install-ci-capabilities/intake.d.ts +2 -2
  152. package/dist/install-ci-capabilities/intake.js +2 -2
  153. package/dist/install-ci-capabilities/verify.d.ts +1 -1
  154. package/dist/install-ci-capabilities/verify.js +1 -1
  155. package/dist/install-ci-core.d.ts +24 -4
  156. package/dist/install-ci-core.d.ts.map +1 -1
  157. package/dist/install-ci-core.js +12 -5
  158. package/dist/install-ci-core.js.map +1 -1
  159. package/dist/install-ci-github.d.ts +33 -1
  160. package/dist/install-ci-github.d.ts.map +1 -1
  161. package/dist/install-ci-github.js +74 -1
  162. package/dist/install-ci-github.js.map +1 -1
  163. package/dist/install-ci.d.ts +1 -1
  164. package/dist/install-ci.js +4 -4
  165. package/dist/install-ci.js.map +1 -1
  166. package/dist/install-skills.d.ts +123 -0
  167. package/dist/install-skills.d.ts.map +1 -0
  168. package/dist/install-skills.js +100 -0
  169. package/dist/install-skills.js.map +1 -0
  170. package/dist/intake-event.d.ts +6 -6
  171. package/dist/intake-event.js +6 -6
  172. package/dist/intake-marker.d.ts +3 -3
  173. package/dist/intake-marker.d.ts.map +1 -1
  174. package/dist/intake-triage.d.ts +1 -1
  175. package/dist/intake-triage.js +2 -2
  176. package/dist/intake-trigger-template.d.ts +8 -8
  177. package/dist/intake-trigger-template.d.ts.map +1 -1
  178. package/dist/intake-trigger-template.js +17 -17
  179. package/dist/intake-trigger-template.js.map +1 -1
  180. package/dist/intake.d.ts +65 -52
  181. package/dist/intake.d.ts.map +1 -1
  182. package/dist/intake.js +92 -76
  183. package/dist/intake.js.map +1 -1
  184. package/dist/integration-core.d.ts +27 -24
  185. package/dist/integration-core.d.ts.map +1 -1
  186. package/dist/integration-core.js +191 -77
  187. package/dist/integration-core.js.map +1 -1
  188. package/dist/integrator.d.ts +1 -1
  189. package/dist/integrator.d.ts.map +1 -1
  190. package/dist/integrator.js +19 -3
  191. package/dist/integrator.js.map +1 -1
  192. package/dist/isolation.d.ts +4 -4
  193. package/dist/isolation.d.ts.map +1 -1
  194. package/dist/isolation.js +9 -0
  195. package/dist/isolation.js.map +1 -1
  196. package/dist/issue-provider.d.ts +4 -4
  197. package/dist/issue-provider.js +1 -1
  198. package/dist/item-lock.d.ts +216 -149
  199. package/dist/item-lock.d.ts.map +1 -1
  200. package/dist/item-lock.js +345 -270
  201. package/dist/item-lock.js.map +1 -1
  202. package/dist/item-path.d.ts +2 -2
  203. package/dist/item-path.js +2 -2
  204. package/dist/ledger-lint.d.ts +9 -9
  205. package/dist/ledger-lint.js +9 -9
  206. package/dist/ledger-read.d.ts +57 -57
  207. package/dist/ledger-read.d.ts.map +1 -1
  208. package/dist/ledger-read.js +24 -24
  209. package/dist/ledger-read.js.map +1 -1
  210. package/dist/ledger-write.d.ts +33 -28
  211. package/dist/ledger-write.d.ts.map +1 -1
  212. package/dist/ledger-write.js +100 -122
  213. package/dist/ledger-write.js.map +1 -1
  214. package/dist/lifecycle-gather.d.ts +18 -1
  215. package/dist/lifecycle-gather.d.ts.map +1 -1
  216. package/dist/lifecycle-gather.js +23 -17
  217. package/dist/lifecycle-gather.js.map +1 -1
  218. package/dist/lifecycle-pools.d.ts +44 -7
  219. package/dist/lifecycle-pools.d.ts.map +1 -1
  220. package/dist/lifecycle-pools.js +27 -7
  221. package/dist/lifecycle-pools.js.map +1 -1
  222. package/dist/merge-question-surfacer.d.ts +18 -4
  223. package/dist/merge-question-surfacer.d.ts.map +1 -1
  224. package/dist/merge-question-surfacer.js +20 -5
  225. package/dist/merge-question-surfacer.js.map +1 -1
  226. package/dist/migrate-stuck-locks.d.ts +129 -0
  227. package/dist/migrate-stuck-locks.d.ts.map +1 -0
  228. package/dist/migrate-stuck-locks.js +355 -0
  229. package/dist/migrate-stuck-locks.js.map +1 -0
  230. package/dist/mint-adr.js +4 -4
  231. package/dist/mirror-pool-scan.d.ts +2 -2
  232. package/dist/mirror-pool-scan.js +3 -3
  233. package/dist/mirror-pool-scan.js.map +1 -1
  234. package/dist/needs-attention.d.ts +304 -27
  235. package/dist/needs-attention.d.ts.map +1 -1
  236. package/dist/needs-attention.js +542 -67
  237. package/dist/needs-attention.js.map +1 -1
  238. package/dist/orphan-sidecar.d.ts +10 -6
  239. package/dist/orphan-sidecar.d.ts.map +1 -1
  240. package/dist/orphan-sidecar.js +35 -2
  241. package/dist/orphan-sidecar.js.map +1 -1
  242. package/dist/pi-harness.d.ts +16 -0
  243. package/dist/pi-harness.d.ts.map +1 -1
  244. package/dist/pi-harness.js +82 -2
  245. package/dist/pi-harness.js.map +1 -1
  246. package/dist/placement.d.ts +8 -8
  247. package/dist/placement.js +3 -3
  248. package/dist/prd-to-spec.d.ts.map +1 -1
  249. package/dist/prd-to-spec.js +9 -5
  250. package/dist/prd-to-spec.js.map +1 -1
  251. package/dist/prompt.d.ts +16 -19
  252. package/dist/prompt.d.ts.map +1 -1
  253. package/dist/prompt.js +17 -19
  254. package/dist/prompt.js.map +1 -1
  255. package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
  256. package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
  257. package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
  258. package/dist/protocol/TASKING-PROTOCOL.md +3 -1
  259. package/dist/protocol/WORK-CONTRACT.md +22 -18
  260. package/dist/protocol/task-template.md +1 -1
  261. package/dist/readiness.d.ts +1 -1
  262. package/dist/reap-branches.d.ts +12 -9
  263. package/dist/reap-branches.d.ts.map +1 -1
  264. package/dist/reap-branches.js +25 -7
  265. package/dist/reap-branches.js.map +1 -1
  266. package/dist/recover-isolated.d.ts +12 -0
  267. package/dist/recover-isolated.d.ts.map +1 -1
  268. package/dist/recover-isolated.js +6 -1
  269. package/dist/recover-isolated.js.map +1 -1
  270. package/dist/repo-config.d.ts +23 -2
  271. package/dist/repo-config.d.ts.map +1 -1
  272. package/dist/repo-config.js +64 -18
  273. package/dist/repo-config.js.map +1 -1
  274. package/dist/repo-mirror.d.ts.map +1 -1
  275. package/dist/repo-mirror.js +18 -2
  276. package/dist/repo-mirror.js.map +1 -1
  277. package/dist/review-gate.d.ts +3 -3
  278. package/dist/review-gate.d.ts.map +1 -1
  279. package/dist/review-gate.js +11 -10
  280. package/dist/review-gate.js.map +1 -1
  281. package/dist/review-verdict.d.ts +2 -2
  282. package/dist/review-verdict.d.ts.map +1 -1
  283. package/dist/review-verdict.js +2 -2
  284. package/dist/review-verdict.js.map +1 -1
  285. package/dist/run.d.ts +2 -2
  286. package/dist/run.d.ts.map +1 -1
  287. package/dist/run.js +58 -23
  288. package/dist/run.js.map +1 -1
  289. package/dist/scan.d.ts +26 -17
  290. package/dist/scan.d.ts.map +1 -1
  291. package/dist/scan.js +29 -15
  292. package/dist/scan.js.map +1 -1
  293. package/dist/select-order.d.ts +1 -1
  294. package/dist/select-priority.d.ts +14 -14
  295. package/dist/select-priority.d.ts.map +1 -1
  296. package/dist/select-priority.js +5 -5
  297. package/dist/select-priority.js.map +1 -1
  298. package/dist/sidecar-apply.js +1 -1
  299. package/dist/sidecar-apply.js.map +1 -1
  300. package/dist/sidecar.d.ts +47 -12
  301. package/dist/sidecar.d.ts.map +1 -1
  302. package/dist/sidecar.js +84 -4
  303. package/dist/sidecar.js.map +1 -1
  304. package/dist/skills/answer-questions/SKILL.md +89 -0
  305. package/dist/skills/capture-signal/SKILL.md +52 -0
  306. package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
  307. package/dist/skills/drive-tasks/SKILL.md +218 -0
  308. package/dist/skills/from-idea/SKILL.md +83 -0
  309. package/dist/skills/merge-prs/SKILL.md +70 -0
  310. package/dist/skills/orchestrate/SKILL.md +101 -0
  311. package/dist/skills/promote/SKILL.md +35 -0
  312. package/dist/skills/review/SKILL.md +16 -0
  313. package/dist/skills/setup/SKILL.md +258 -0
  314. package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
  315. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
  316. package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
  317. package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
  318. package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
  319. package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
  320. package/dist/skills/setup/protocol/spec-template.md +71 -0
  321. package/dist/skills/setup/protocol/task-template.md +65 -0
  322. package/dist/skills/surface-questions/SKILL.md +16 -0
  323. package/dist/skills/to-spec/SKILL.md +34 -0
  324. package/dist/skills/to-task/SKILL.md +19 -0
  325. package/dist/skills/triage-observations/SKILL.md +78 -0
  326. package/dist/skills/work/SKILL.md +51 -0
  327. package/dist/slug-namespace.d.ts +6 -6
  328. package/dist/slug-namespace.js +8 -8
  329. package/dist/slug-namespace.js.map +1 -1
  330. package/dist/spec-complete.d.ts.map +1 -1
  331. package/dist/spec-complete.js +4 -5
  332. package/dist/spec-complete.js.map +1 -1
  333. package/dist/start.d.ts +1 -1
  334. package/dist/start.d.ts.map +1 -1
  335. package/dist/start.js +54 -60
  336. package/dist/start.js.map +1 -1
  337. package/dist/status.d.ts +3 -3
  338. package/dist/status.js +4 -4
  339. package/dist/status.js.map +1 -1
  340. package/dist/surface-gate.d.ts +2 -2
  341. package/dist/surface-gate.d.ts.map +1 -1
  342. package/dist/surface-gate.js +10 -3
  343. package/dist/surface-gate.js.map +1 -1
  344. package/dist/surface-persist.d.ts +2 -2
  345. package/dist/surface-persist.d.ts.map +1 -1
  346. package/dist/surface-persist.js +1 -1
  347. package/dist/surface-persist.js.map +1 -1
  348. package/dist/tasker-review-loop.d.ts +11 -10
  349. package/dist/tasker-review-loop.d.ts.map +1 -1
  350. package/dist/tasker-review-loop.js +8 -8
  351. package/dist/tasker-review-loop.js.map +1 -1
  352. package/dist/tasking-eligibility.d.ts +21 -21
  353. package/dist/tasking-eligibility.d.ts.map +1 -1
  354. package/dist/tasking-eligibility.js +10 -10
  355. package/dist/tasking-lock.d.ts +10 -10
  356. package/dist/tasking-lock.d.ts.map +1 -1
  357. package/dist/tasking-lock.js +69 -49
  358. package/dist/tasking-lock.js.map +1 -1
  359. package/dist/tasking.d.ts +73 -39
  360. package/dist/tasking.d.ts.map +1 -1
  361. package/dist/tasking.js +309 -95
  362. package/dist/tasking.js.map +1 -1
  363. package/dist/triage-gate.d.ts +1 -1
  364. package/dist/triage-persist.d.ts +15 -11
  365. package/dist/triage-persist.d.ts.map +1 -1
  366. package/dist/triage-persist.js +48 -18
  367. package/dist/triage-persist.js.map +1 -1
  368. package/dist/vendor/incur/agents.d.ts +58 -0
  369. package/dist/vendor/incur/agents.d.ts.map +1 -0
  370. package/dist/vendor/incur/agents.js +343 -0
  371. package/dist/vendor/incur/agents.js.map +1 -0
  372. package/dist/verify-workflow-template.d.ts +1 -1
  373. package/dist/verify-workflow-template.js +3 -3
  374. package/dist/watch-session.d.ts +11 -3
  375. package/dist/watch-session.d.ts.map +1 -1
  376. package/dist/watch-session.js +94 -7
  377. package/dist/watch-session.js.map +1 -1
  378. package/dist/work-layout.d.ts +15 -9
  379. package/dist/work-layout.d.ts.map +1 -1
  380. package/dist/work-layout.js +14 -9
  381. package/dist/work-layout.js.map +1 -1
  382. package/dist/workspace.d.ts +2 -2
  383. package/package.json +2 -2
  384. package/src/advance-ci-template.ts +4 -4
  385. package/src/advance-classify.ts +5 -5
  386. package/src/advance-drivers.ts +26 -13
  387. package/src/advance-isolated.ts +3 -3
  388. package/src/advance-lifecycle-template.ts +98 -13
  389. package/src/advance-loop-driver.ts +3 -3
  390. package/src/advance-treeless-publish.ts +46 -1
  391. package/src/advance.ts +495 -115
  392. package/src/advancing-lock.ts +102 -7
  393. package/src/agent-launch.ts +37 -12
  394. package/src/agent-stop.ts +60 -2
  395. package/src/apply-decide.ts +38 -9
  396. package/src/apply-merge-action.ts +49 -11
  397. package/src/apply-persist.ts +236 -62
  398. package/src/apply-stuck-action.ts +260 -0
  399. package/src/brand.ts +14 -2
  400. package/src/buildable-body.ts +14 -14
  401. package/src/claim-cas.ts +4 -4
  402. package/src/cli-spinner.ts +1 -1
  403. package/src/cli.ts +472 -141
  404. package/src/close-job-template.ts +9 -9
  405. package/src/close-job.ts +26 -26
  406. package/src/complete.ts +121 -63
  407. package/src/concurrency.ts +1 -1
  408. package/src/config.ts +143 -49
  409. package/src/continue-branch.ts +23 -1
  410. package/src/cwd-section.ts +1 -1
  411. package/src/decision-engine.ts +57 -19
  412. package/src/do-autopick.ts +21 -10
  413. package/src/do-config.ts +7 -7
  414. package/src/do-remote-auto.ts +2 -2
  415. package/src/do.ts +558 -85
  416. package/src/drop-source.ts +2 -2
  417. package/src/env-config.ts +19 -11
  418. package/src/failure-cause.ts +30 -1
  419. package/src/format.ts +13 -33
  420. package/src/frontmatter.ts +55 -35
  421. package/src/gc.ts +172 -9
  422. package/src/github.ts +78 -0
  423. package/src/harness.ts +25 -0
  424. package/src/index.ts +10 -0
  425. package/src/install-ci-branch-protection.ts +283 -58
  426. package/src/install-ci-capabilities/advance-lifecycle.ts +1 -1
  427. package/src/install-ci-capabilities/close-job.ts +1 -1
  428. package/src/install-ci-capabilities/example-noop.ts +1 -1
  429. package/src/install-ci-capabilities/intake.ts +2 -2
  430. package/src/install-ci-capabilities/verify.ts +1 -1
  431. package/src/install-ci-core.ts +34 -7
  432. package/src/install-ci-github.ts +87 -1
  433. package/src/install-ci.ts +4 -4
  434. package/src/install-skills.ts +166 -0
  435. package/src/intake-event.ts +6 -6
  436. package/src/intake-marker.ts +3 -3
  437. package/src/intake-triage.ts +2 -2
  438. package/src/intake-trigger-template.ts +18 -18
  439. package/src/intake.ts +138 -103
  440. package/src/integration-core.ts +234 -100
  441. package/src/integrator.ts +22 -9
  442. package/src/isolation.ts +13 -4
  443. package/src/issue-provider.ts +4 -4
  444. package/src/item-lock.ts +505 -361
  445. package/src/item-path.ts +2 -2
  446. package/src/ledger-lint.ts +9 -9
  447. package/src/ledger-read.ts +81 -81
  448. package/src/ledger-write.ts +137 -152
  449. package/src/lifecycle-gather.ts +45 -22
  450. package/src/lifecycle-pools.ts +71 -13
  451. package/src/merge-question-surfacer.ts +32 -8
  452. package/src/migrate-stuck-locks.ts +451 -0
  453. package/src/mint-adr.ts +4 -4
  454. package/src/mirror-pool-scan.ts +5 -5
  455. package/src/needs-attention.ts +877 -104
  456. package/src/orphan-sidecar.ts +49 -8
  457. package/src/pi-harness.ts +82 -2
  458. package/src/placement.ts +8 -8
  459. package/src/prd-to-spec.ts +13 -5
  460. package/src/prompt.ts +20 -25
  461. package/src/readiness.ts +1 -1
  462. package/src/reap-branches.ts +38 -14
  463. package/src/recover-isolated.ts +18 -1
  464. package/src/repo-config.ts +66 -17
  465. package/src/repo-mirror.ts +23 -1
  466. package/src/review-gate.ts +12 -11
  467. package/src/review-verdict.ts +3 -3
  468. package/src/run.ts +69 -23
  469. package/src/scan.ts +36 -22
  470. package/src/select-order.ts +1 -1
  471. package/src/select-priority.ts +15 -15
  472. package/src/sidecar-apply.ts +1 -1
  473. package/src/sidecar.ts +141 -14
  474. package/src/slug-namespace.ts +10 -10
  475. package/src/spec-complete.ts +4 -5
  476. package/src/start.ts +57 -63
  477. package/src/status.ts +7 -7
  478. package/src/surface-gate.ts +12 -5
  479. package/src/surface-persist.ts +3 -3
  480. package/src/tasker-review-loop.ts +18 -17
  481. package/src/tasking-eligibility.ts +21 -21
  482. package/src/tasking-lock.ts +75 -60
  483. package/src/tasking.ts +381 -135
  484. package/src/triage-gate.ts +1 -1
  485. package/src/triage-persist.ts +71 -27
  486. package/src/vendor/incur/LICENSE +21 -0
  487. package/src/vendor/incur/README.md +19 -0
  488. package/src/vendor/incur/agents.ts +392 -0
  489. package/src/verify-workflow-template.ts +3 -3
  490. package/src/watch-session.ts +102 -7
  491. package/src/work-layout.ts +14 -9
  492. package/src/workspace.ts +2 -2
@@ -24,9 +24,13 @@ import type {LifecycleSelectedItem} from './select-priority.js';
24
24
  * **Three lifecycle sub-pools, gated by PURPOSE (ADR `ci-config-policy-and-gate-
25
25
  * family.md` \u00a74 \u2014 gates govern CREATE only; CONSUME is always-on):**
26
26
  *
27
- * - **triage** \u2014 UNTRIAGED observations (no `triaged:` settled marker). A CREATE
28
- * act (the bot mints a promote/keep/delete question), so gated by
29
- * {@link LifecyclePoolGates.triage} (the future `observationTriage` gate).
27
+ * - **triage** \u2014 UNTRIAGED observations (no `triaged:` settled marker) with NO
28
+ * sidecar yet. A CREATE act (the bot mints the deterministic triage question),
29
+ * so gated by {@link LifecyclePoolGates.triage} (the future `observationTriage`
30
+ * gate). Like SURFACE, a PENDING sidecar is NOT enumerated (already surfaced +
31
+ * awaiting the human, so re-enumerating only re-spawns the additive surface
32
+ * agent for a no-outcome no-op); it re-enters only when ANSWERED (to apply) or
33
+ * settled by a `triaged:` marker.
30
34
  * - **surface** \u2014 `needsAnswers`-blocked tasks/prds with NO all-answered sidecar
31
35
  * (no sidecar yet, or a still-pending one). A CREATE act (the bot mints the
32
36
  * blocker question), so gated by {@link LifecyclePoolGates.surface} (the future
@@ -70,7 +74,7 @@ export interface LifecyclePoolGates {
70
74
  surface?: boolean;
71
75
  /**
72
76
  * Does the SURFACE candidate set widen to include STAGING (`tasks/backlog/` +
73
- * `prds/proposed/`), or stay POOL-ONLY? This field is CONSUMED BY THE
77
+ * `specs/proposed/`), or stay POOL-ONLY? This field is CONSUMED BY THE
74
78
  * GATHER (`lifecycle-gather.ts`), NOT by {@link buildLifecyclePools} — the
75
79
  * builder is pure and only routes already-resolved candidates between surface
76
80
  * / apply by sidecar-answered-state. The gather decides WHICH items become
@@ -80,6 +84,39 @@ export interface LifecyclePoolGates {
80
84
  * default is `true`). Spec `staging-surface-and-apply-promote-safety` F2.
81
85
  * BUILD/claim eligibility is UNCHANGED in either mode — staging items stay
82
86
  * non-claimable, the trust model is untouched.
87
+ *
88
+ * CREATE-vs-CONSUME EDGE (accepted, spec
89
+ * `staging-surface-and-apply-promote-safety` F2 follow-up): `surfaceStaging`
90
+ * gates the CREATE half (surface-side ENUMERATION of staged candidates).
91
+ * APPLY (the consume half) reads through the SAME enumeration, so flipping
92
+ * `surfaceStaging` true→false AFTER a surface tick has minted+answered a
93
+ * staged sidecar can STRAND that sidecar in staging until the flag is flipped
94
+ * back on. The realistic flip direction is off→on (opt-in widening), and an
95
+ * answered staged sidecar only exists if surfacing (opt-in) minted it, so the
96
+ * strand window is narrow and accepted. Revisit only if the create-vs-consume
97
+ * invariant must hold strictly (e.g. re-route `apply` to consume regardless of
98
+ * the surface gate).
99
+ *
100
+ * ## Decisions (ratified by task
101
+ * `f2-mirror-path-staging-test-and-ratify-decisions`, follow-up to slice
102
+ * `f2-surface-staging-config-and-pool-extension` — gate-2 review nits)
103
+ *
104
+ * 1. **Split default is intentional.** `LifecyclePoolGates.surfaceStaging`
105
+ * defaults `false` at the library boundary; `Config.surfaceStaging` defaults
106
+ * `true` at the user-visible layer. The calm library default is load-bearing
107
+ * for any direct caller of `gatherLifecycle*` that does NOT thread CLI gates
108
+ * — they get pool-only behaviour by default and opt IN to staging.
109
+ * 2. **Four new public methods on `LedgerReadStrategy` are intentional**
110
+ * (`resolveLocalTaskStaging`, `resolveLocalSpecStaging`,
111
+ * `resolveMirrorTaskStaging`, `resolveMirrorSpecStaging`), rather than
112
+ * overloading `resolveLocalState` / `resolveMirrorState` with a flag. Keeps
113
+ * the state vs. staging axes orthogonal at the interface.
114
+ * 3. **The `surfaceStaging` gate is consumed by the GATHER, not by pure
115
+ * `buildLifecyclePools`,** even though the field lives on
116
+ * `LifecyclePoolGates`. Placement acknowledged above; ratified as intentional
117
+ * so the gate colocates with its sibling create-side gates.
118
+ * 4. **`surfaceStaging` in `REPO_ALLOWED_KEYS` is intentional** so a repo's
119
+ * `.dorfl.json` can flip it via the normal resolution chain.
83
120
  */
84
121
  surfaceStaging?: boolean;
85
122
  }
@@ -112,7 +149,7 @@ export interface NeedsAnswersCandidate {
112
149
  * to the always-on APPLY pool (a CONSUME act — the human's answer is never
113
150
  * stranded), while a NO/PENDING sidecar UNTRIAGED observation stays a TRIAGE
114
151
  * candidate (gated) as today. PURE: the caller resolves the active sidecar (via
115
- * {@link sidecarPathFor} — the SAME resolver the task/prd `needsAnswers`
152
+ * {@link sidecarPathFor} — the SAME resolver the task/spec `needsAnswers`
116
153
  * candidates use); this unit only routes.
117
154
  */
118
155
  export interface ObservationCandidate {
@@ -142,7 +179,7 @@ export interface LifecyclePoolsInput {
142
179
  */
143
180
  observations: ObservationCandidate[];
144
181
  /**
145
- * Every `needsAnswers:true` task/prd (the create-side blocked items), each
182
+ * Every `needsAnswers:true` task/spec (the create-side blocked items), each
146
183
  * with its resolved active sidecar. This unit routes each to SURFACE or APPLY.
147
184
  */
148
185
  needsAnswers: NeedsAnswersCandidate[];
@@ -172,7 +209,7 @@ export interface LifecyclePools {
172
209
  * Build the three lifecycle sub-pools from the raw candidates + the gates, PURE.
173
210
  * Each selected item carries the lifecycle `namespace` discriminator so the
174
211
  * driver's per-item dispatch can map it to the right TICK arg (observation \u2192
175
- * `obs:<slug>`; a blocked task/prd \u2192 `task:`/`prd:<slug>`, which the tick then
212
+ * `obs:<slug>`; a blocked task/spec \u2192 `task:`/`spec:<slug>`, which the tick then
176
213
  * classifies into surface/apply/no-op). This unit decides WHICH sub-pool each item
177
214
  * belongs to (and whether its gate lets it through); the TICK re-classifies and
178
215
  * runs the actual rung (the classifier + rung bodies are unchanged).
@@ -201,23 +238,44 @@ export function buildLifecyclePools(
201
238
  // with the triage create-gate off (the create-vs-consume invariant, ADR
202
239
  // `ci-config-policy-and-gate-family` §4). An answered sidecar wins even
203
240
  // when the observation body also carries a `triaged:` marker: a human's
204
- // answer must never be stranded (task `## Decisions`).
241
+ // answer must never be stranded (ADR
242
+ // `answered-observation-sidecar-dominates-triaged-marker.md`).
205
243
  apply.push({
206
244
  repoPath: input.repoPath,
207
245
  slug: obs.slug,
208
246
  namespace: 'observation',
209
247
  });
210
- } else if (obs.triaged === undefined && triageOn) {
211
- // CREATE (mint the promote/keep/delete question) — gated by
212
- // `observationTriage`. An untriaged observation with NO sidecar or a
213
- // PENDING sidecar is a triage candidate as today.
248
+ } else if (
249
+ obs.triaged === undefined &&
250
+ obs.sidecar === undefined &&
251
+ triageOn
252
+ ) {
253
+ // CREATE (mint the deterministic triage question) — gated by
254
+ // `observationTriage`. ONLY an untriaged observation with NO sidecar yet is
255
+ // a triage candidate: it has never been surfaced. A PENDING sidecar (surfaced
256
+ // already, awaiting the human's answer) is DELIBERATELY DROPPED here — mirror
257
+ // of the SURFACE pool. The triage question is DETERMINISTIC (engine-built) +
258
+ // already present, so re-enumerating a pending observation every tick would
259
+ // only re-run a leg + re-spawn the additive surface agent for a NO-OUTCOME
260
+ // no-op (the persist appends no duplicate). It re-enters only when it is
261
+ // ANSWERED (→ apply, above) or a `triaged:` marker settles it. This is the
262
+ // enumerate-smartness fix: do NOT re-evaluate items that produce no outcome.
214
263
  triage.push({
215
264
  repoPath: input.repoPath,
216
265
  slug: obs.slug,
217
266
  namespace: 'observation',
218
267
  });
268
+ } else if (obs.triaged !== undefined) {
269
+ // EXPLICIT DROP: SETTLED (`triaged:` non-empty) with no answered sidecar
270
+ // — deliberately NOT enumerated. A pending sidecar on a settled
271
+ // observation would only no-op if re-enumerated, and a settled body is
272
+ // already resolved on the create side (ADR
273
+ // `answered-observation-sidecar-dominates-triaged-marker.md`). Codified
274
+ // as an explicit branch so a future reader does not resurface this cell.
275
+ continue;
219
276
  }
220
- // else: SETTLED (triaged:) with no answered sidecar — NOT enumerated.
277
+ // else: UNTRIAGED with no answered sidecar + triage gate OFF implicit
278
+ // drop (the gate deliberately silences the pool).
221
279
  }
222
280
 
223
281
  // --- surface / apply: split `needsAnswers` items by sidecar answered-state ---
@@ -16,7 +16,7 @@ import {
16
16
  import {workItemRel, type WorkFolderKey} from './work-layout.js';
17
17
 
18
18
  /**
19
- * The **MERGE-QUESTION SURFACER** (prd `land-time-reverify-and-parallel-merge-ceiling`,
19
+ * The **MERGE-QUESTION SURFACER** (spec `land-time-reverify-and-parallel-merge-ceiling`,
20
20
  * task `merge-question-surfacer`, US #14) — the SECOND, STATE-sourced surfacer
21
21
  * in the advance loop. It is a clean SIBLING to the existing
22
22
  * `surface-questions` JUDGEMENT surfacer (`surface-gate.ts` +
@@ -41,7 +41,7 @@ import {workItemRel, type WorkFolderKey} from './work-layout.js';
41
41
  * layered on top when a GitHub host is configured; the surfacer functions
42
42
  * identically without it.
43
43
  *
44
- * OUT OF SCOPE for this task (covered by sibling tasks of the same prd):
44
+ * OUT OF SCOPE for this task (covered by sibling tasks of the same spec):
45
45
  *
46
46
  * - The APPLY rung dispatch that lands an answered `kind: merge` through the
47
47
  * land primitive (rebase → re-verify → advance). Task
@@ -139,10 +139,19 @@ export interface MergeQuestionSurfaced {
139
139
  prUrl?: string;
140
140
  }
141
141
 
142
- /** One branch the surfacer considered but did not surface (with the reason). */
142
+ /**
143
+ * One branch the surfacer considered but did not surface (with the reason).
144
+ *
145
+ * PROVISIONAL vocabulary. The `reason` union is scoped to this surfacer — no
146
+ * sibling surfacer exists yet, so it is deliberately NOT lifted to a shared
147
+ * skip-reason type. When a second STATE-sourced surfacer lands (e.g. a
148
+ * stuck-lock surfacer), promote this to a shared skip-reason vocabulary via a
149
+ * dedicated decision; until then it may change without notice.
150
+ */
143
151
  export interface MergeQuestionSkipped {
144
152
  ref: string;
145
153
  slug: string;
154
+ /** PROVISIONAL vocabulary — see {@link MergeQuestionSkipped}. */
146
155
  reason: 'no-item-body' | 'already-pending-merge-question' | 'persist-nothing';
147
156
  }
148
157
 
@@ -164,9 +173,19 @@ export class MergeQuestionSurfacerError extends Error {
164
173
  }
165
174
  }
166
175
 
167
- /** The lifecycle folders a task body may rest in (the FOLDERS the persist
168
- * needs to find the item file to set `needsAnswers:true` on). Matches
169
- * `advance.ts`'s `findItemPath` task-folder set. */
176
+ /**
177
+ * The lifecycle folders {@link findTaskItemPath} scans for a task body to flip
178
+ * `needsAnswers:true` on. This set DELIBERATELY DIVERGES from `advance.ts`'s
179
+ * `FOLDERS_FOR_TYPE.task` (`['tasks-backlog','tasks-ready','in-progress','done']`):
180
+ *
181
+ * - OMITS `in-progress` (and `needs-attention`): an unmerged `work/<slug>`
182
+ * branch whose body is mid-build should NOT trigger a merge-question — the
183
+ * build is still active, and surfacing a land-decision now would race the
184
+ * builder. Such tasks fall through to the `no-item-body` skip.
185
+ * - ADDS `cancelled`: a cancelled task with a lingering unmerged `work/*`
186
+ * branch SHOULD surface a merge-question so the operator explicitly decides
187
+ * whether to merge the branch or drop it.
188
+ */
170
189
  const TASK_FOLDERS: readonly WorkFolderKey[] = [
171
190
  'tasks-ready',
172
191
  'tasks-backlog',
@@ -185,7 +204,7 @@ const TASK_FOLDERS: readonly WorkFolderKey[] = [
185
204
  * Idempotency: a branch whose sidecar ALREADY carries a PENDING `kind: merge`
186
205
  * entry is SKIPPED (with `already-pending-merge-question`). A branch with no
187
206
  * item body on `main` is SKIPPED (with `no-item-body`) — the cross-cutting
188
- * branch-keyed sidecar identity (PRD sidecar Q5-i) is OOS for this task; an
207
+ * branch-keyed sidecar identity (SPEC sidecar Q5-i) is OOS for this task; an
189
208
  * unmerged-branch-with-no-body lands on the same skip path until that
190
209
  * generalisation arrives.
191
210
  */
@@ -248,7 +267,7 @@ export function surfaceMergeQuestions(
248
267
 
249
268
  const itemPath = findTaskItemPath(cwd, branch.slug);
250
269
  if (itemPath === undefined) {
251
- // The `branch:`/`ref:`-keyed sidecar identity (PRD sidecar Q5-i, the
270
+ // The `branch:`/`ref:`-keyed sidecar identity (SPEC sidecar Q5-i, the
252
271
  // cross-cutting open question SHARED with the stuck-lock surfacer) is
253
272
  // OUT OF SCOPE for this task — without a body to flip `needsAnswers`
254
273
  // on, persist would tear the invariant. Skip with the reason so the
@@ -440,6 +459,11 @@ export function listUnmergedWorkBranchesViaGit(
440
459
  * results by their `headRefName` so the surfacer can enrich the matching
441
460
  * branch's question. A non-zero / missing `gh` is treated as "no host
442
461
  * metadata available" — the floor still surfaces every unmerged branch.
462
+ *
463
+ * Best-effort enrichment. The git-reachability FLOOR is authoritative; the
464
+ * `--state open`, `--base <base>`, and `--limit 200` arguments are DELIBERATE
465
+ * ceilings — a PR targeting a non-`main` base (e.g. a stacked PR) or the case
466
+ * of >200 open PRs degrades to floor-only output, never corrupts it.
443
467
  */
444
468
  export function listOpenPullRequestsViaGh(
445
469
  input: ListPullRequestsInput,
@@ -0,0 +1,451 @@
1
+ /**
2
+ * ONE-SHOT migration of pre-existing `stuck` per-item lock refs into the
3
+ * post-`retire-stuck-lock-state` resting shape (surface-on-`main` +
4
+ * `needsAnswers:true` + release the lock).
5
+ *
6
+ * Context (spec `surface-stuck-as-questions-and-retire-stuck-lock-state`,
7
+ * resolved decision #3, user story 5; task
8
+ * `migrate-existing-stuck-locks-one-shot`):
9
+ * - Before the retirement, a bounced item was recorded as a `state: stuck`
10
+ * lock ref (`refs/dorfl/lock/<entry>`) with the bounce reason (+ any
11
+ * agent-surfaced questions) on the ref's blob body.
12
+ * - After the retirement (task `retire-stuck-lock-state`), `LockState`
13
+ * admits only `active`. {@link parseLockEntry} silently COERCES a legacy
14
+ * `state: stuck` blob to `active` on read, and the `stuck`-only body
15
+ * sections (`## Reason` / `## Questions`) are no longer emitted or
16
+ * parsed. So a legacy stuck ref is now invisible to the runner (it looks
17
+ * like a healthy in-flight `active` hold) and its reason/questions would
18
+ * rot forever if the state were not migrated at rollout.
19
+ *
20
+ * This module runs the migration ONCE per arbiter at rollout: for every held
21
+ * lock ref whose RAW blob body still carries `state: stuck` in its
22
+ * frontmatter, it extracts the reason (+ questions) directly from the legacy
23
+ * body (bypassing the coercing {@link parseLockEntry}) and drives the SAME
24
+ * ordered surface-first-release-second transition the new bounce uses
25
+ * ({@link surfaceStuckToNeedsAttention}) — writing the `stuck`-kind
26
+ * `work/questions/<entry>.md` sidecar + flipping `needsAnswers:true` on the
27
+ * item body on `<arbiter>/main`, then releasing the ref. After the migration
28
+ * no `stuck` lock ref remains and every previously-stuck item rests as a
29
+ * `needsAnswers:true` pool item with a sidecar (the same resting shape a
30
+ * fresh bounce produces).
31
+ *
32
+ * IDEMPOTENT by construction: the legacy shape is identified from the raw
33
+ * blob's `state: stuck` frontmatter line, which the new lock module NEVER
34
+ * writes; a re-run therefore finds no legacy stuck ref → the report has zero
35
+ * `migrated` entries and no arbiter mutation occurs. Bare-arbiter safe (a ref
36
+ * is a ref): every operation goes through the same lock-ref plumbing the
37
+ * live path uses, so it works on a `--bare file://` arbiter identically to a
38
+ * real remote.
39
+ *
40
+ * SCOPE FENCE: this migrates ONLY lock entries whose name reverse-derives to
41
+ * a CURRENT item-form (`task-*` / `spec-*` / `observation-*` via
42
+ * {@link hasCurrentItemForm}); a pre-cutover `slice-*` / `prd-*` legacy stuck
43
+ * ref has no on-`main` body to flip and is out of scope for the surface
44
+ * transition — such entries are reported as `skipped-no-item-form` and left
45
+ * for the existing `release-lock --entry <literal>` escape hatch to clear (a
46
+ * human still asserts the clear on those; the migration never `--force`s or
47
+ * deletes them). See the CLI verb `dorfl migrate-stuck-locks` for the
48
+ * user-facing surface + its help text.
49
+ */
50
+
51
+ import {runAsync, type RunResult} from './git.js';
52
+ import {
53
+ itemFromLockEntry,
54
+ itemLockRef,
55
+ hasCurrentItemForm,
56
+ listItemLocks,
57
+ } from './item-lock.js';
58
+ import {
59
+ surfaceStuckToNeedsAttention,
60
+ type SurfaceStuckToNeedsAttentionResult,
61
+ } from './needs-attention.js';
62
+ import {resolveSidecarIdentity} from './sidecar.js';
63
+
64
+ /** Per-lock outcome of the migration sweep. */
65
+ export type MigrateStuckLockOutcome =
66
+ | 'migrated' // legacy stuck ref → surfaced on main + released
67
+ | 'migrated-body-absent' // legacy stuck ref → lock released, but no `main` body existed to flip (bounce D1 probe found nothing); still a no-op-safe drain of the stale lock
68
+ | 'not-stuck' // ref present but its blob is not the legacy `state: stuck` shape (healthy active hold, or already migrated)
69
+ | 'skipped-no-item-form' // legacy stuck ref but its `<entry>` does not reverse-derive to a current item-form (pre-cutover `slice-*` / `prd-*`); clear via `release-lock --entry <literal>`
70
+ | 'lost' // the surface CAS or the release lost the race to a concurrent writer; reported, never `--force`d
71
+ | 'error'; // read/plumbing fault on this ref; reported, ref left untouched
72
+
73
+ /** One lock's disposition in the {@link MigrateStuckLocksReport}. */
74
+ export interface MigrateStuckLockEntry {
75
+ /** The lock-entry name (`<type>-<slug>` or a pre-cutover `slice-*` / `prd-*`). */
76
+ entry: string;
77
+ /** The lock ref (`refs/dorfl/lock/<entry>`). */
78
+ ref: string;
79
+ /**
80
+ * The namespaced item form the entry reverse-derives to (`task:<slug>` /
81
+ * `spec:<slug>` / `observation:<slug>`), or the raw entry when it does not
82
+ * carry a current item-form prefix.
83
+ */
84
+ item: string;
85
+ outcome: MigrateStuckLockOutcome;
86
+ /** Human-readable note (why it was left, or the surface + release result). */
87
+ message: string;
88
+ }
89
+
90
+ /** Result of {@link migrateStuckLocks}. A one-shot sweep report. */
91
+ export interface MigrateStuckLocksReport {
92
+ entries: MigrateStuckLockEntry[];
93
+ /** Count of legacy stuck refs surfaced on main + released. */
94
+ migrated: number;
95
+ /** Count of legacy stuck refs whose lock was released but had no on-`main` body
96
+ * to flip (body-absent D1 probe). Still a successful drain. */
97
+ migratedBodyAbsent: number;
98
+ /** Count of refs that were not the legacy stuck shape (healthy active holds
99
+ * or already-migrated). No mutation. */
100
+ notStuck: number;
101
+ /** Count of legacy stuck refs skipped because their entry has no current
102
+ * item-form (pre-cutover `slice-*` / `prd-*`). */
103
+ skippedNoItemForm: number;
104
+ /** Count of refs whose surface/release lost the CAS race. */
105
+ lost: number;
106
+ /** Count of refs where a read/plumbing fault prevented migration. */
107
+ errors: number;
108
+ }
109
+
110
+ export interface MigrateStuckLocksOptions {
111
+ cwd: string;
112
+ arbiter?: string;
113
+ env?: NodeJS.ProcessEnv;
114
+ /** Optional progress note sink (parity with `surfaceStuckToNeedsAttention`). */
115
+ note?: (message: string) => void;
116
+ }
117
+
118
+ /**
119
+ * Run the one-shot migration against the arbiter. Enumerates every held
120
+ * per-item lock ref, and for each whose RAW blob body carries `state: stuck`
121
+ * (the legacy shape the retired `stuck` state produced) drives the shared
122
+ * surface-first-release-second transition (reason + questions extracted from
123
+ * the legacy body). Idempotent: a re-run finds no legacy stuck refs and is a
124
+ * clean no-op.
125
+ */
126
+ export async function migrateStuckLocks(
127
+ options: MigrateStuckLocksOptions,
128
+ ): Promise<MigrateStuckLocksReport> {
129
+ const cwd = options.cwd;
130
+ const arbiter = options.arbiter ?? 'origin';
131
+ const env = options.env;
132
+ const note = options.note ?? (() => {});
133
+
134
+ const entries: MigrateStuckLockEntry[] = [];
135
+ let migrated = 0;
136
+ let migratedBodyAbsent = 0;
137
+ let notStuck = 0;
138
+ let skippedNoItemForm = 0;
139
+ let lost = 0;
140
+ let errors = 0;
141
+
142
+ // Enumerate every held lock entry on the arbiter. `listItemLocks` fetches the
143
+ // lock refs first (hard-fails on a fetch fault) so the enumeration reads a
144
+ // fresh arbiter snapshot; if the fetch throws we surface it as a single
145
+ // terminal error rather than degrading to `[]` (which would silently claim
146
+ // "nothing to migrate").
147
+ let heldEntries: string[];
148
+ try {
149
+ heldEntries = await listItemLocks(cwd, arbiter, env);
150
+ } catch (err) {
151
+ return {
152
+ entries: [
153
+ {
154
+ entry: '',
155
+ ref: '',
156
+ item: '',
157
+ outcome: 'error',
158
+ message: `failed to enumerate lock refs on ${arbiter}: ${
159
+ err instanceof Error ? err.message : String(err)
160
+ }`,
161
+ },
162
+ ],
163
+ migrated: 0,
164
+ migratedBodyAbsent: 0,
165
+ notStuck: 0,
166
+ skippedNoItemForm: 0,
167
+ lost: 0,
168
+ errors: 1,
169
+ };
170
+ }
171
+
172
+ for (const entry of heldEntries) {
173
+ const ref = itemLockRef(entry);
174
+ const item = itemFromLockEntry(entry);
175
+ let body: string;
176
+ try {
177
+ body = await readLockBlob(ref, cwd, env);
178
+ } catch (err) {
179
+ errors++;
180
+ entries.push({
181
+ entry,
182
+ ref,
183
+ item,
184
+ outcome: 'error',
185
+ message: `read failed: ${err instanceof Error ? err.message : String(err)}`,
186
+ });
187
+ continue;
188
+ }
189
+ if (!isLegacyStuckBlob(body)) {
190
+ notStuck++;
191
+ entries.push({
192
+ entry,
193
+ ref,
194
+ item,
195
+ outcome: 'not-stuck',
196
+ message: `${entry} is not a legacy stuck lock (state != stuck on the ref blob) — left untouched.`,
197
+ });
198
+ continue;
199
+ }
200
+ if (!hasCurrentItemForm(entry)) {
201
+ // A pre-cutover `slice-*` / `prd-*` stuck ref has no current
202
+ // item-form and therefore no on-`main` body path to flip. The
203
+ // existing `release-lock --entry <literal>` escape hatch is the
204
+ // supported clear here; the migration NEVER `--force`s such a ref.
205
+ skippedNoItemForm++;
206
+ entries.push({
207
+ entry,
208
+ ref,
209
+ item,
210
+ outcome: 'skipped-no-item-form',
211
+ message:
212
+ `'${entry}' is a legacy stuck ref with no current item-form ` +
213
+ `(pre-cutover slice-*/prd-*); clear via ` +
214
+ `\`dorfl release-lock --entry ${entry}\` (no auto-force).`,
215
+ });
216
+ continue;
217
+ }
218
+ const {slug} = resolveSidecarIdentity(item);
219
+ const reason = extractLegacyReason(body) ?? '(no reason recorded)';
220
+ const questions = extractLegacyQuestions(body).map((q) => ({question: q}));
221
+ let result: SurfaceStuckToNeedsAttentionResult;
222
+ try {
223
+ result = await surfaceStuckToNeedsAttention({
224
+ cwd,
225
+ slug,
226
+ item,
227
+ reason,
228
+ questions,
229
+ arbiter,
230
+ env,
231
+ note,
232
+ });
233
+ } catch (err) {
234
+ errors++;
235
+ entries.push({
236
+ entry,
237
+ ref,
238
+ item,
239
+ outcome: 'error',
240
+ message: `surface transition threw: ${
241
+ err instanceof Error ? err.message : String(err)
242
+ }`,
243
+ });
244
+ continue;
245
+ }
246
+ if (result.surfaced && result.released) {
247
+ migrated++;
248
+ entries.push({
249
+ entry,
250
+ ref,
251
+ item,
252
+ outcome: 'migrated',
253
+ message:
254
+ `migrated '${entry}' → surfaced on ${arbiter}/main ` +
255
+ '(needsAnswers:true + sidecar) and released the lock.',
256
+ });
257
+ continue;
258
+ }
259
+ if (!result.surfaced && result.released && result.bodyAbsent === true) {
260
+ // D1 body-absent probe: no `main` body existed to flip. The bounce
261
+ // primitive STILL released the lock so the legacy stuck ref no longer
262
+ // dangles; there is just nothing to surface. Counted as a drained
263
+ // migration (the goal — no stuck ref remains).
264
+ migratedBodyAbsent++;
265
+ entries.push({
266
+ entry,
267
+ ref,
268
+ item,
269
+ outcome: 'migrated-body-absent',
270
+ message:
271
+ `released legacy stuck lock '${entry}': no body for '${item}' on ` +
272
+ `${arbiter}/main (D1 probe) — sidecar surface skipped, but the ` +
273
+ 'stale lock was drained (the migration goal).',
274
+ });
275
+ continue;
276
+ }
277
+ // The surface CAS or the release did not converge (contention exhausted,
278
+ // or the leased release lost the race). Reported, never forced.
279
+ lost++;
280
+ entries.push({
281
+ entry,
282
+ ref,
283
+ item,
284
+ outcome: 'lost',
285
+ message:
286
+ result.reasonNotSurfaced ??
287
+ `surface/release for '${entry}' did not converge (surfaced=${result.surfaced}, released=${result.released}); re-run.`,
288
+ });
289
+ }
290
+
291
+ return {
292
+ entries,
293
+ migrated,
294
+ migratedBodyAbsent,
295
+ notStuck,
296
+ skippedNoItemForm,
297
+ lost,
298
+ errors,
299
+ };
300
+ }
301
+
302
+ /**
303
+ * True iff the migration report leaves a state a human should look at: a
304
+ * `lost` (CAS race) or an `error` (read/plumbing fault). A `not-stuck` /
305
+ * `skipped-no-item-form` outcome is INFORMATIONAL (the former is the normal
306
+ * healthy-hold state; the latter is deferred to `release-lock --entry`) and
307
+ * does NOT count. A pure "0 migrated, 0 lost, 0 error" report is exit-0.
308
+ */
309
+ export function migrateStuckLocksNeedsAttention(
310
+ report: MigrateStuckLocksReport,
311
+ ): boolean {
312
+ return report.lost > 0 || report.errors > 0;
313
+ }
314
+
315
+ /** Format the sweep for the terminal. An empty enumeration (no lock refs at
316
+ * all) yields no lines (silent, like the gc lock report). */
317
+ export function formatMigrateStuckLocksReport(
318
+ report: MigrateStuckLocksReport,
319
+ ): string[] {
320
+ if (report.entries.length === 0) {
321
+ return [
322
+ 'Migrate stuck locks: no per-item lock refs held on the arbiter — nothing to migrate.',
323
+ ];
324
+ }
325
+ const lines = [
326
+ `Migrate stuck locks: migrated ${report.migrated}` +
327
+ (report.migratedBodyAbsent > 0
328
+ ? ` (+${report.migratedBodyAbsent} body-absent — lock drained, no on-main body to surface)`
329
+ : '') +
330
+ `, left ${report.notStuck} healthy active hold(s) untouched` +
331
+ (report.skippedNoItemForm > 0
332
+ ? `, skipped ${report.skippedNoItemForm} pre-cutover entry (clear via release-lock --entry)`
333
+ : '') +
334
+ (report.lost > 0
335
+ ? `, ${report.lost} could not converge (lease lost — re-run)`
336
+ : '') +
337
+ (report.errors > 0
338
+ ? `, ${report.errors} error(s) (left untouched)`
339
+ : '') +
340
+ ':',
341
+ ];
342
+ for (const e of report.entries) {
343
+ const tag =
344
+ e.outcome === 'migrated'
345
+ ? '[migrated] '
346
+ : e.outcome === 'migrated-body-absent'
347
+ ? '[migrated:body-absent]'
348
+ : e.outcome === 'not-stuck'
349
+ ? '[healthy] '
350
+ : e.outcome === 'skipped-no-item-form'
351
+ ? '[skipped:no-item-form]'
352
+ : e.outcome === 'lost'
353
+ ? '[lost] '
354
+ : '[error] ';
355
+ lines.push(` ${tag} ${e.entry} ${e.message}`);
356
+ }
357
+ return lines;
358
+ }
359
+
360
+ /** Read the raw `lock.md` blob body from a lock ref, or throw. Bypasses
361
+ * {@link parseLockEntry} on purpose — the caller needs the untouched legacy
362
+ * text (which the coercing parser strips). */
363
+ async function readLockBlob(
364
+ ref: string,
365
+ cwd: string,
366
+ env: NodeJS.ProcessEnv | undefined,
367
+ ): Promise<string> {
368
+ const show: RunResult = await runAsync(
369
+ 'git',
370
+ ['show', `${ref}:lock.md`],
371
+ cwd,
372
+ {env},
373
+ );
374
+ if (show.status !== 0) {
375
+ throw new Error(
376
+ `git show ${ref}:lock.md failed (exit ${show.status}): ${show.stderr.trim()}`,
377
+ );
378
+ }
379
+ return show.stdout;
380
+ }
381
+
382
+ /** True iff `body` is the legacy `state: stuck` shape (the pre-retirement
383
+ * lock-entry blob). Matched on the raw frontmatter line so this is unaffected
384
+ * by the current parser's coercion to `active`. */
385
+ export function isLegacyStuckBlob(body: string): boolean {
386
+ const normalized = body.replace(/\r\n/g, '\n');
387
+ const fm = /^---\n([\s\S]*?)\n---/.exec(normalized);
388
+ if (!fm) {
389
+ return false;
390
+ }
391
+ return /^state:\s*stuck\s*$/m.test(fm[1]);
392
+ }
393
+
394
+ /**
395
+ * Extract the bounce reason from a legacy stuck lock blob. Preference order
396
+ * (the two shapes the retired serialiser produced):
397
+ * 1. A `## Reason` block in the body (multi-line, blank-trimmed).
398
+ * 2. A one-line `reason:` field in the frontmatter (pre-cutover shape).
399
+ * Returns `undefined` when neither is present.
400
+ */
401
+ export function extractLegacyReason(body: string): string | undefined {
402
+ const normalized = body.replace(/\r\n/g, '\n');
403
+ const fm = /^---\n([\s\S]*?)\n---/.exec(normalized);
404
+ const bodyText = fm ? normalized.slice(fm[0].length) : normalized;
405
+ const lines = bodyText.split('\n');
406
+ const start = lines.findIndex((l) => l.trim() === '## Reason');
407
+ if (start !== -1) {
408
+ const collected: string[] = [];
409
+ for (let i = start + 1; i < lines.length; i++) {
410
+ if (/^##\s/.test(lines[i])) {
411
+ break;
412
+ }
413
+ collected.push(lines[i]);
414
+ }
415
+ const text = collected.join('\n').replace(/^\n+/, '').replace(/\n+$/, '');
416
+ if (text !== '') {
417
+ return text;
418
+ }
419
+ }
420
+ if (fm) {
421
+ const m = /^reason:\s*(.*)$/m.exec(fm[1]);
422
+ if (m && m[1].trim() !== '') {
423
+ return m[1].trim();
424
+ }
425
+ }
426
+ return undefined;
427
+ }
428
+
429
+ /** Extract the `## Questions` bulleted list from a legacy stuck lock blob (or
430
+ * `[]` when absent). Each bullet becomes one question string. */
431
+ export function extractLegacyQuestions(body: string): string[] {
432
+ const normalized = body.replace(/\r\n/g, '\n');
433
+ const fm = /^---\n[\s\S]*?\n---/.exec(normalized);
434
+ const bodyText = fm ? normalized.slice(fm[0].length) : normalized;
435
+ const lines = bodyText.split('\n');
436
+ const start = lines.findIndex((l) => l.trim() === '## Questions');
437
+ if (start === -1) {
438
+ return [];
439
+ }
440
+ const questions: string[] = [];
441
+ for (let i = start + 1; i < lines.length; i++) {
442
+ if (/^##\s/.test(lines[i])) {
443
+ break;
444
+ }
445
+ const m = /^-\s+(.*)$/.exec(lines[i].trim());
446
+ if (m) {
447
+ questions.push(m[1]);
448
+ }
449
+ }
450
+ return questions;
451
+ }