dorfl 0.1.2 → 0.2.1

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 (364) hide show
  1. package/dist/advance-drivers.d.ts +1 -1
  2. package/dist/advance-drivers.d.ts.map +1 -1
  3. package/dist/advance-drivers.js +15 -3
  4. package/dist/advance-drivers.js.map +1 -1
  5. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  6. package/dist/advance-lifecycle-template.js +76 -4
  7. package/dist/advance-lifecycle-template.js.map +1 -1
  8. package/dist/advance-treeless-publish.d.ts +23 -0
  9. package/dist/advance-treeless-publish.d.ts.map +1 -1
  10. package/dist/advance-treeless-publish.js +41 -0
  11. package/dist/advance-treeless-publish.js.map +1 -1
  12. package/dist/advance.d.ts +59 -7
  13. package/dist/advance.d.ts.map +1 -1
  14. package/dist/advance.js +389 -82
  15. package/dist/advance.js.map +1 -1
  16. package/dist/advancing-lock.d.ts +30 -2
  17. package/dist/advancing-lock.d.ts.map +1 -1
  18. package/dist/advancing-lock.js +50 -0
  19. package/dist/advancing-lock.js.map +1 -1
  20. package/dist/agent-launch.d.ts +12 -0
  21. package/dist/agent-launch.d.ts.map +1 -1
  22. package/dist/agent-launch.js +22 -12
  23. package/dist/agent-launch.js.map +1 -1
  24. package/dist/agent-stop.d.ts +40 -2
  25. package/dist/agent-stop.d.ts.map +1 -1
  26. package/dist/agent-stop.js +30 -2
  27. package/dist/agent-stop.js.map +1 -1
  28. package/dist/apply-decide.d.ts +17 -3
  29. package/dist/apply-decide.d.ts.map +1 -1
  30. package/dist/apply-decide.js +36 -7
  31. package/dist/apply-decide.js.map +1 -1
  32. package/dist/apply-merge-action.d.ts +17 -5
  33. package/dist/apply-merge-action.d.ts.map +1 -1
  34. package/dist/apply-merge-action.js +43 -8
  35. package/dist/apply-merge-action.js.map +1 -1
  36. package/dist/apply-persist.d.ts +57 -26
  37. package/dist/apply-persist.d.ts.map +1 -1
  38. package/dist/apply-persist.js +166 -37
  39. package/dist/apply-persist.js.map +1 -1
  40. package/dist/apply-stuck-action.d.ts +151 -0
  41. package/dist/apply-stuck-action.d.ts.map +1 -0
  42. package/dist/apply-stuck-action.js +125 -0
  43. package/dist/apply-stuck-action.js.map +1 -0
  44. package/dist/brand.d.ts +12 -1
  45. package/dist/brand.d.ts.map +1 -1
  46. package/dist/brand.js +2 -1
  47. package/dist/brand.js.map +1 -1
  48. package/dist/cli-spinner.d.ts +1 -1
  49. package/dist/cli-spinner.js +1 -1
  50. package/dist/cli.d.ts +10 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +320 -63
  53. package/dist/cli.js.map +1 -1
  54. package/dist/close-job-template.js +1 -1
  55. package/dist/complete.d.ts +6 -3
  56. package/dist/complete.d.ts.map +1 -1
  57. package/dist/complete.js +91 -37
  58. package/dist/complete.js.map +1 -1
  59. package/dist/config.d.ts +55 -9
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +56 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/continue-branch.d.ts.map +1 -1
  64. package/dist/continue-branch.js +22 -0
  65. package/dist/continue-branch.js.map +1 -1
  66. package/dist/decision-engine.d.ts +38 -10
  67. package/dist/decision-engine.d.ts.map +1 -1
  68. package/dist/decision-engine.js +10 -6
  69. package/dist/decision-engine.js.map +1 -1
  70. package/dist/do-autopick.d.ts.map +1 -1
  71. package/dist/do-autopick.js +11 -1
  72. package/dist/do-autopick.js.map +1 -1
  73. package/dist/do-config.d.ts +1 -1
  74. package/dist/do.d.ts +49 -56
  75. package/dist/do.d.ts.map +1 -1
  76. package/dist/do.js +419 -30
  77. package/dist/do.js.map +1 -1
  78. package/dist/env-config.d.ts.map +1 -1
  79. package/dist/env-config.js +12 -4
  80. package/dist/env-config.js.map +1 -1
  81. package/dist/failure-cause.d.ts +3 -2
  82. package/dist/failure-cause.d.ts.map +1 -1
  83. package/dist/failure-cause.js +28 -1
  84. package/dist/failure-cause.js.map +1 -1
  85. package/dist/format.d.ts +6 -6
  86. package/dist/format.d.ts.map +1 -1
  87. package/dist/format.js +10 -27
  88. package/dist/format.js.map +1 -1
  89. package/dist/frontmatter.d.ts +16 -4
  90. package/dist/frontmatter.d.ts.map +1 -1
  91. package/dist/frontmatter.js +27 -1
  92. package/dist/frontmatter.js.map +1 -1
  93. package/dist/gc.d.ts +65 -6
  94. package/dist/gc.d.ts.map +1 -1
  95. package/dist/gc.js +126 -9
  96. package/dist/gc.js.map +1 -1
  97. package/dist/github.d.ts +14 -0
  98. package/dist/github.d.ts.map +1 -1
  99. package/dist/github.js +73 -0
  100. package/dist/github.js.map +1 -1
  101. package/dist/harness.d.ts +25 -0
  102. package/dist/harness.d.ts.map +1 -1
  103. package/dist/harness.js.map +1 -1
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +2 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/install-ci-branch-protection.d.ts +132 -38
  109. package/dist/install-ci-branch-protection.d.ts.map +1 -1
  110. package/dist/install-ci-branch-protection.js +190 -48
  111. package/dist/install-ci-branch-protection.js.map +1 -1
  112. package/dist/install-ci-core.d.ts +21 -1
  113. package/dist/install-ci-core.d.ts.map +1 -1
  114. package/dist/install-ci-core.js +10 -3
  115. package/dist/install-ci-core.js.map +1 -1
  116. package/dist/install-ci-github.d.ts +32 -0
  117. package/dist/install-ci-github.d.ts.map +1 -1
  118. package/dist/install-ci-github.js +73 -0
  119. package/dist/install-ci-github.js.map +1 -1
  120. package/dist/install-skills.d.ts +123 -0
  121. package/dist/install-skills.d.ts.map +1 -0
  122. package/dist/install-skills.js +100 -0
  123. package/dist/install-skills.js.map +1 -0
  124. package/dist/intake-trigger-template.js +1 -1
  125. package/dist/intake.d.ts +17 -4
  126. package/dist/intake.d.ts.map +1 -1
  127. package/dist/intake.js +23 -7
  128. package/dist/intake.js.map +1 -1
  129. package/dist/integration-core.d.ts +10 -7
  130. package/dist/integration-core.d.ts.map +1 -1
  131. package/dist/integration-core.js +174 -60
  132. package/dist/integration-core.js.map +1 -1
  133. package/dist/integrator.d.ts +1 -1
  134. package/dist/integrator.d.ts.map +1 -1
  135. package/dist/integrator.js +19 -3
  136. package/dist/integrator.js.map +1 -1
  137. package/dist/isolation.d.ts +2 -2
  138. package/dist/isolation.d.ts.map +1 -1
  139. package/dist/isolation.js +9 -0
  140. package/dist/isolation.js.map +1 -1
  141. package/dist/item-lock.d.ts +196 -129
  142. package/dist/item-lock.d.ts.map +1 -1
  143. package/dist/item-lock.js +329 -254
  144. package/dist/item-lock.js.map +1 -1
  145. package/dist/ledger-write.d.ts +31 -26
  146. package/dist/ledger-write.d.ts.map +1 -1
  147. package/dist/ledger-write.js +100 -122
  148. package/dist/ledger-write.js.map +1 -1
  149. package/dist/lifecycle-gather.d.ts +17 -0
  150. package/dist/lifecycle-gather.d.ts.map +1 -1
  151. package/dist/lifecycle-gather.js +7 -1
  152. package/dist/lifecycle-gather.js.map +1 -1
  153. package/dist/lifecycle-pools.d.ts +40 -3
  154. package/dist/lifecycle-pools.d.ts.map +1 -1
  155. package/dist/lifecycle-pools.js +26 -6
  156. package/dist/lifecycle-pools.js.map +1 -1
  157. package/dist/merge-question-surfacer.d.ts +15 -1
  158. package/dist/merge-question-surfacer.d.ts.map +1 -1
  159. package/dist/merge-question-surfacer.js +18 -3
  160. package/dist/merge-question-surfacer.js.map +1 -1
  161. package/dist/migrate-stuck-locks.d.ts +129 -0
  162. package/dist/migrate-stuck-locks.d.ts.map +1 -0
  163. package/dist/migrate-stuck-locks.js +355 -0
  164. package/dist/migrate-stuck-locks.js.map +1 -0
  165. package/dist/needs-attention.d.ts +293 -16
  166. package/dist/needs-attention.d.ts.map +1 -1
  167. package/dist/needs-attention.js +532 -57
  168. package/dist/needs-attention.js.map +1 -1
  169. package/dist/orphan-sidecar.d.ts +8 -4
  170. package/dist/orphan-sidecar.d.ts.map +1 -1
  171. package/dist/orphan-sidecar.js +35 -2
  172. package/dist/orphan-sidecar.js.map +1 -1
  173. package/dist/pi-harness.d.ts +16 -0
  174. package/dist/pi-harness.d.ts.map +1 -1
  175. package/dist/pi-harness.js +82 -2
  176. package/dist/pi-harness.js.map +1 -1
  177. package/dist/placement.d.ts +3 -3
  178. package/dist/placement.js +1 -1
  179. package/dist/prd-to-spec.d.ts.map +1 -1
  180. package/dist/prd-to-spec.js +9 -5
  181. package/dist/prd-to-spec.js.map +1 -1
  182. package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
  183. package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
  184. package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
  185. package/dist/protocol/TASKING-PROTOCOL.md +3 -1
  186. package/dist/protocol/WORK-CONTRACT.md +22 -18
  187. package/dist/protocol/task-template.md +1 -1
  188. package/dist/reap-branches.d.ts +12 -9
  189. package/dist/reap-branches.d.ts.map +1 -1
  190. package/dist/reap-branches.js +25 -7
  191. package/dist/reap-branches.js.map +1 -1
  192. package/dist/recover-isolated.d.ts +12 -0
  193. package/dist/recover-isolated.d.ts.map +1 -1
  194. package/dist/recover-isolated.js +5 -0
  195. package/dist/recover-isolated.js.map +1 -1
  196. package/dist/repo-config.d.ts +23 -2
  197. package/dist/repo-config.d.ts.map +1 -1
  198. package/dist/repo-config.js +53 -7
  199. package/dist/repo-config.js.map +1 -1
  200. package/dist/repo-mirror.d.ts.map +1 -1
  201. package/dist/repo-mirror.js +18 -2
  202. package/dist/repo-mirror.js.map +1 -1
  203. package/dist/review-verdict.js +1 -1
  204. package/dist/review-verdict.js.map +1 -1
  205. package/dist/run.d.ts +1 -1
  206. package/dist/run.d.ts.map +1 -1
  207. package/dist/run.js +54 -19
  208. package/dist/run.js.map +1 -1
  209. package/dist/scan.d.ts +17 -8
  210. package/dist/scan.d.ts.map +1 -1
  211. package/dist/scan.js +22 -8
  212. package/dist/scan.js.map +1 -1
  213. package/dist/select-priority.d.ts +2 -2
  214. package/dist/select-priority.js +1 -1
  215. package/dist/select-priority.js.map +1 -1
  216. package/dist/sidecar-apply.js +1 -1
  217. package/dist/sidecar-apply.js.map +1 -1
  218. package/dist/sidecar.d.ts +45 -10
  219. package/dist/sidecar.d.ts.map +1 -1
  220. package/dist/sidecar.js +82 -2
  221. package/dist/sidecar.js.map +1 -1
  222. package/dist/skills/answer-questions/SKILL.md +89 -0
  223. package/dist/skills/capture-signal/SKILL.md +52 -0
  224. package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
  225. package/dist/skills/drive-tasks/SKILL.md +218 -0
  226. package/dist/skills/from-idea/SKILL.md +83 -0
  227. package/dist/skills/merge-prs/SKILL.md +70 -0
  228. package/dist/skills/orchestrate/SKILL.md +101 -0
  229. package/dist/skills/promote/SKILL.md +35 -0
  230. package/dist/skills/review/SKILL.md +16 -0
  231. package/dist/skills/setup/SKILL.md +258 -0
  232. package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
  233. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
  234. package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
  235. package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
  236. package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
  237. package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
  238. package/dist/skills/setup/protocol/spec-template.md +71 -0
  239. package/dist/skills/setup/protocol/task-template.md +65 -0
  240. package/dist/skills/surface-questions/SKILL.md +16 -0
  241. package/dist/skills/to-spec/SKILL.md +34 -0
  242. package/dist/skills/to-task/SKILL.md +19 -0
  243. package/dist/skills/triage-observations/SKILL.md +78 -0
  244. package/dist/skills/work/SKILL.md +51 -0
  245. package/dist/slug-namespace.d.ts +5 -5
  246. package/dist/slug-namespace.js +5 -5
  247. package/dist/slug-namespace.js.map +1 -1
  248. package/dist/spec-complete.d.ts.map +1 -1
  249. package/dist/spec-complete.js +2 -3
  250. package/dist/spec-complete.js.map +1 -1
  251. package/dist/start.d.ts.map +1 -1
  252. package/dist/start.js +51 -57
  253. package/dist/start.js.map +1 -1
  254. package/dist/surface-gate.d.ts.map +1 -1
  255. package/dist/surface-gate.js +10 -3
  256. package/dist/surface-gate.js.map +1 -1
  257. package/dist/surface-persist.js +1 -1
  258. package/dist/surface-persist.js.map +1 -1
  259. package/dist/tasker-review-loop.d.ts +4 -3
  260. package/dist/tasker-review-loop.d.ts.map +1 -1
  261. package/dist/tasker-review-loop.js.map +1 -1
  262. package/dist/tasking-lock.d.ts +2 -2
  263. package/dist/tasking-lock.d.ts.map +1 -1
  264. package/dist/tasking-lock.js +53 -32
  265. package/dist/tasking-lock.js.map +1 -1
  266. package/dist/tasking.d.ts +40 -6
  267. package/dist/tasking.d.ts.map +1 -1
  268. package/dist/tasking.js +224 -12
  269. package/dist/tasking.js.map +1 -1
  270. package/dist/triage-persist.d.ts +10 -6
  271. package/dist/triage-persist.d.ts.map +1 -1
  272. package/dist/triage-persist.js +34 -4
  273. package/dist/triage-persist.js.map +1 -1
  274. package/dist/vendor/incur/agents.d.ts +58 -0
  275. package/dist/vendor/incur/agents.d.ts.map +1 -0
  276. package/dist/vendor/incur/agents.js +343 -0
  277. package/dist/vendor/incur/agents.js.map +1 -0
  278. package/dist/verify-workflow-template.js +1 -1
  279. package/dist/watch-session.d.ts +11 -3
  280. package/dist/watch-session.d.ts.map +1 -1
  281. package/dist/watch-session.js +94 -7
  282. package/dist/watch-session.js.map +1 -1
  283. package/dist/work-layout.d.ts +7 -3
  284. package/dist/work-layout.d.ts.map +1 -1
  285. package/dist/work-layout.js +6 -3
  286. package/dist/work-layout.js.map +1 -1
  287. package/dist/workspace.d.ts +1 -1
  288. package/package.json +2 -2
  289. package/src/advance-drivers.ts +16 -3
  290. package/src/advance-lifecycle-template.ts +89 -4
  291. package/src/advance-treeless-publish.ts +45 -0
  292. package/src/advance.ts +467 -87
  293. package/src/advancing-lock.ts +96 -1
  294. package/src/agent-launch.ts +37 -12
  295. package/src/agent-stop.ts +60 -2
  296. package/src/apply-decide.ts +36 -7
  297. package/src/apply-merge-action.ts +45 -7
  298. package/src/apply-persist.ts +228 -54
  299. package/src/apply-stuck-action.ts +260 -0
  300. package/src/brand.ts +14 -2
  301. package/src/cli-spinner.ts +1 -1
  302. package/src/cli.ts +401 -70
  303. package/src/close-job-template.ts +1 -1
  304. package/src/complete.ts +110 -52
  305. package/src/config.ts +107 -13
  306. package/src/continue-branch.ts +22 -0
  307. package/src/decision-engine.ts +55 -17
  308. package/src/do-autopick.ts +12 -1
  309. package/src/do-config.ts +1 -1
  310. package/src/do.ts +511 -38
  311. package/src/env-config.ts +12 -4
  312. package/src/failure-cause.ts +30 -1
  313. package/src/format.ts +10 -30
  314. package/src/frontmatter.ts +35 -5
  315. package/src/gc.ts +172 -9
  316. package/src/github.ts +78 -0
  317. package/src/harness.ts +25 -0
  318. package/src/index.ts +10 -0
  319. package/src/install-ci-branch-protection.ts +282 -57
  320. package/src/install-ci-core.ts +30 -3
  321. package/src/install-ci-github.ts +86 -0
  322. package/src/install-skills.ts +166 -0
  323. package/src/intake-trigger-template.ts +1 -1
  324. package/src/intake.ts +46 -11
  325. package/src/integration-core.ts +199 -65
  326. package/src/integrator.ts +22 -9
  327. package/src/isolation.ts +11 -2
  328. package/src/item-lock.ts +485 -341
  329. package/src/ledger-write.ts +135 -150
  330. package/src/lifecycle-gather.ts +29 -6
  331. package/src/lifecycle-pools.ts +67 -9
  332. package/src/merge-question-surfacer.ts +28 -4
  333. package/src/migrate-stuck-locks.ts +451 -0
  334. package/src/needs-attention.ts +857 -84
  335. package/src/orphan-sidecar.ts +47 -6
  336. package/src/pi-harness.ts +82 -2
  337. package/src/placement.ts +3 -3
  338. package/src/prd-to-spec.ts +13 -5
  339. package/src/reap-branches.ts +38 -14
  340. package/src/recover-isolated.ts +17 -0
  341. package/src/repo-config.ts +55 -6
  342. package/src/repo-mirror.ts +23 -1
  343. package/src/review-verdict.ts +1 -1
  344. package/src/run.ts +64 -18
  345. package/src/scan.ts +22 -8
  346. package/src/select-priority.ts +3 -3
  347. package/src/sidecar-apply.ts +1 -1
  348. package/src/sidecar.ts +138 -11
  349. package/src/slug-namespace.ts +7 -7
  350. package/src/spec-complete.ts +2 -3
  351. package/src/start.ts +53 -59
  352. package/src/surface-gate.ts +10 -3
  353. package/src/surface-persist.ts +1 -1
  354. package/src/tasker-review-loop.ts +4 -3
  355. package/src/tasking-lock.ts +52 -36
  356. package/src/tasking.ts +261 -17
  357. package/src/triage-persist.ts +56 -12
  358. package/src/vendor/incur/LICENSE +21 -0
  359. package/src/vendor/incur/README.md +19 -0
  360. package/src/vendor/incur/agents.ts +392 -0
  361. package/src/verify-workflow-template.ts +1 -1
  362. package/src/watch-session.ts +102 -7
  363. package/src/work-layout.ts +6 -3
  364. package/src/workspace.ts +1 -1
package/dist/cli.js CHANGED
@@ -51,7 +51,8 @@ import { sweepOrphanSidecars } from './orphan-sidecar.js';
51
51
  import { sweepLedgerDuplicates, formatLedgerSweep } from './ledger-lint.js';
52
52
  import { status, formatStatus } from './status.js';
53
53
  import { ledgerWrite } from './ledger-write.js';
54
- import { releaseItemLock, reportItemLocks, formatItemLockReport, itemLockReportNeedsAttention, reapStaleItemLocks, formatReapReport, reapReportNeedsAttention, } from './item-lock.js';
54
+ import { releaseItemLock, releaseLiteralLockEntry, reportItemLocks, formatItemLockReport, itemLockReportNeedsAttention, reapStaleItemLocks, formatReapReport, reapReportNeedsAttention, } from './item-lock.js';
55
+ import { migrateStuckLocks, formatMigrateStuckLocksReport, migrateStuckLocksNeedsAttention, } from './migrate-stuck-locks.js';
55
56
  import { promoteFromPreBacklog, promoteFromPreSpec, listPromotable, } from './needs-attention.js';
56
57
  import { parseSlugArg } from './slug-namespace.js';
57
58
  import { dropSource } from './drop-source.js';
@@ -62,6 +63,7 @@ import { installCI } from './install-ci.js';
62
63
  import { GitHubCIContext } from './install-ci-github.js';
63
64
  import { loadCapabilityRegistry } from './install-ci-core.js';
64
65
  import { performCloseMergedIssues } from './close-job.js';
66
+ import { installSkills } from './install-skills.js';
65
67
  /**
66
68
  * Whether `--auto-build` / `--no-auto-build` was explicitly passed on the command
67
69
  * line. Commander gives a negatable boolean option a default of `true`, so we
@@ -253,7 +255,7 @@ function buildRegistrySetAdvanceTick(options) {
253
255
  // `taskingIntegration ?? integration`; the build path stays on `integration`.
254
256
  taskingIntegration: config.taskingIntegration,
255
257
  // The TASK-PLACEMENT configured default (`do spec:` tasking output:
256
- // `pre-backlog` staged vs `ready` pool). No operator flag on this
258
+ // `backlog` staged vs `ready` pool). No operator flag on this
257
259
  // registry-driven advance context, so only the configured default rung is
258
260
  // threaded (the resolver still layers untrusted-origin force + built-in floor).
259
261
  tasksLandIn: config.tasksLandIn,
@@ -422,8 +424,8 @@ function explicitTasksLandInFromFlag(raw) {
422
424
  if (raw === undefined) {
423
425
  return undefined;
424
426
  }
425
- if (raw !== 'pre-backlog' && raw !== 'ready') {
426
- throw new Error(`--tasks-land-in must be 'pre-backlog' or 'ready' (got '${raw}').`);
427
+ if (raw !== 'backlog' && raw !== 'ready') {
428
+ throw new Error(`--tasks-land-in must be 'backlog' or 'ready' (got '${raw}').`);
427
429
  }
428
430
  return raw;
429
431
  }
@@ -516,9 +518,9 @@ function printLeak(leak) {
516
518
  * REJECT `spec:` with a clear "operates on tasks, not specs" error. On rejection
517
519
  * it prints the error to stderr and exits 1 (the task-only commands never act on
518
520
  * a spec). An OMITTED slug (`start`/`complete`/`prompt` infer it from the branch)
519
- * passes through untouched. (The legacy `prd:` prefix is a DEAD namespace token
521
+ * passes through untouched. (The legacy ''prd:'' prefix is a DEAD namespace token
520
522
  * after the hard cutover — it is neither special-cased nor rejected here; a
521
- * `prd:<slug>` arg resolves as a bare literal task slug.)
523
+ * ''prd:<slug>'' arg resolves as a bare literal task slug.)
522
524
  *
523
525
  * `do` is the ONE command that spans both namespaces; it consumes the full
524
526
  * `resolveSlug` (with the cross-namespace collision check) in the `do-in-place`
@@ -676,6 +678,34 @@ export function buildProgram() {
676
678
  program
677
679
  .name(brand.bin)
678
680
  .description('Autonomous parallel agents over file-based work/ queues.');
681
+ // `dorfl config --json` (spec `graceful-pre-timeout-wip-checkpoint` — step 5):
682
+ // print the RESOLVED per-repo config as JSON, so BOTH the advance-lifecycle
683
+ // workflow's `enumerate` job (which reads it to emit the dynamic
684
+ // `githubTimeout` output) AND a human debugging a config-precedence question
685
+ // share ONE honest primitive. A focused honest primitive — NOT overloaded
686
+ // onto `scan`. Runs in the cwd (in-place), so the resolution chain reads THIS
687
+ // repo's committed `dorfl.json` exactly like `scan --here`.
688
+ program
689
+ .command('config')
690
+ .helpGroup(HEADLINE_GROUP)
691
+ .description("Print the resolved config for the current repo (flag > env > per-repo > global > default). Consumed by the advance-lifecycle workflow's enumerate job to emit the dynamic GitHub timeout backstop (agentDeadlineMinutes + checkpointHeadroomMinutes) at run time from the committed dorfl.json — so editing dorfl.json reflects everywhere (internal deadline AND GitHub cap) on the next tick, no install-ci re-run.")
692
+ .option('-c, --config <path>', 'config file path', defaultConfigPath())
693
+ .option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
694
+ .option('--json', 'output the resolved config as JSON')
695
+ .action(async (flags) => {
696
+ if (flags.json !== true) {
697
+ console.error('error: `dorfl config` currently supports only --json output. Pass --json.');
698
+ process.exit(1);
699
+ }
700
+ const cwd = process.cwd();
701
+ const { global, override } = loadGlobalAndOverride(flags.config);
702
+ const resolved = resolveRepoConfig({
703
+ repoPath: cwd,
704
+ global,
705
+ override,
706
+ }).config;
707
+ console.log(JSON.stringify(resolved, null, 2));
708
+ });
679
709
  program
680
710
  .command('scan')
681
711
  .helpGroup(HEADLINE_GROUP)
@@ -908,7 +938,7 @@ export function buildProgram() {
908
938
  program
909
939
  .command('claim')
910
940
  .helpGroup(ADVANCED_GROUP)
911
- .description('Atomically claim a work/backlog/<slug>.md item via a compare-and-swap push to the arbiter (in-process; mirrors scripts/claim.sh).')
941
+ .description(`Atomically claim a ${workFolderPrefix('tasks-ready')}<slug>.md item via a compare-and-swap push to the arbiter (in-process; mirrors scripts/claim.sh).`)
912
942
  .argument('<slug>', 'the slug of the backlog item to claim')
913
943
  .option('--arbiter <remote>', 'name of the arbiter git remote (default: origin)', 'origin')
914
944
  .option('--retries <n>', 'cap on push retries when main advances', '3')
@@ -1067,7 +1097,7 @@ export function buildProgram() {
1067
1097
  program
1068
1098
  .command('prompt')
1069
1099
  .helpGroup(ADVANCED_GROUP)
1070
- .description("Print to stdout the work-agent prompt for a task: the canonical CLAIM-PROTOCOL wrapper + the task's own ## Prompt (with <slug> and source spec substituted). Resolves work/in-progress/<slug>.md then work/backlog/<slug>.md; infers <slug> from a work/<slug> branch when omitted. Read-only, stdout only — the same assembly the autonomous runner feeds agentCmd.")
1100
+ .description(`Print to stdout the work-agent prompt for a task: the canonical CLAIM-PROTOCOL wrapper + the task's own ## Prompt (with <slug> and source spec substituted). Resolves work/in-progress/<slug>.md then ${workFolderPrefix('tasks-ready')}<slug>.md; infers <slug> from a work/<slug> branch when omitted. Read-only, stdout only — the same assembly the autonomous runner feeds agentCmd.`)
1071
1101
  .argument('[slug]', 'the slug to render (inferred from a work/<slug> branch if omitted)')
1072
1102
  .action((rawSlug) => {
1073
1103
  // Task-only command (§3a): accept bare + `task:`, reject `spec:`.
@@ -1134,6 +1164,11 @@ export function buildProgram() {
1134
1164
  override,
1135
1165
  flags: {
1136
1166
  ...(flagMode ? { integration: flagMode } : {}),
1167
+ // `--merge-retries <n>` rides the SAME gate-family precedence chain on
1168
+ // the isolated-recovery path so a wide-matrix CI's raised cap actually
1169
+ // reaches the land tail here too (task
1170
+ // `thread-merge-retries-cross-task-and-ratify-default`).
1171
+ ...mergeRetriesFlagOverrides(flags),
1137
1172
  ...noPRFlagOverrides(flags),
1138
1173
  },
1139
1174
  });
@@ -1148,6 +1183,9 @@ export function buildProgram() {
1148
1183
  workspacesDir: flags.workspace ?? isoConfig.workspacesDir,
1149
1184
  integration: isoConfig.integration,
1150
1185
  noPR: isoConfig.noPR,
1186
+ // The resolved cross-job CAS-retry cap threaded through so the isolated-
1187
+ // recovery land tail respects the per-repo cap (same chain as `complete`).
1188
+ mergeRetries: isoConfig.mergeRetries,
1151
1189
  note: (message) => console.error(`>> ${message}`),
1152
1190
  env: process.env,
1153
1191
  });
@@ -1244,7 +1282,7 @@ export function buildProgram() {
1244
1282
  program
1245
1283
  .command('do')
1246
1284
  .helpGroup(HEADLINE_GROUP)
1247
- .description('The per-repo WORKER (the CI command): claim + onboard onto work/<slug>, run the agent, gate, integrate, and exit. In the CURRENT checkout by default (refuses on a dirty tree, integrates in-place). With --remote <r>: against a REGISTERED repo with NO checkout — materialise a hub mirror + job worktree in the agents\u2019 area, run the same pipeline there, then reap. do <slug> | do task:<slug> | do spec:<slug> (the tasking path) | do (auto-pick one) | do <a> <b> (those, in sequence) | do -n <x> (x eligible, in sequence). Auto-pick draws TASKS-FIRST then SPECS-to-task by default (per-repo selectionOrder reorders the pools). --propose (default) / --merge resolved at integrate-time. Supersedes ar-run.sh.')
1285
+ .description("The per-repo WORKER (the CI command): claim + onboard onto work/<slug>, run the agent, gate, integrate, and exit. Builds in an ISOLATED job worktree off THIS repo's arbiter by DEFAULT (task `make-isolated-default-build-mode` — the cwd is the origin SOURCE only; the human working tree is never written). --in-place opts OUT (build in the current checkout, refusing a dirty tree today's pre-flip behaviour). --isolated remains accepted as a redundant explicit opt-in alias. With --remote <r>: against a REGISTERED repo with NO checkout — materialise a hub mirror + job worktree in the agents\u2019 area, run the same pipeline there, then reap. do <slug> | do task:<slug> | do spec:<slug> (the tasking path) | do (auto-pick one) | do <a> <b> (those, in sequence) | do -n <x> (x eligible, in sequence). Auto-pick draws TASKS-FIRST then SPECS-to-task by default (per-repo selectionOrder reorders the pools). --propose (default) / --merge resolved at integrate-time. Supersedes ar-run.sh.")
1248
1286
  // EXTENSIBLE argument grammar (the three do-* tasks grow this one block):
1249
1287
  // `do-autopick` widens the single optional positional into a VARIADIC one so
1250
1288
  // `do` (zero args = auto-pick), `do <a> <b> …` (named, in sequence), and
@@ -1256,10 +1294,11 @@ export function buildProgram() {
1256
1294
  .option('-n, --number <x>', 'AUTO-PICK x eligible items and do them IN SEQUENCE (ordered by selectionOrder, default drain = tasks-first then prds-to-task). Sequential — never a parallelism knob (that is `run`). Mutually exclusive with naming items.')
1257
1295
  .option('--selection-order <order>', 'order the auto-pick pools (build/task/surface/triage; apply is always first): a preset keyword (drain (default) | groom) or an explicit comma-separated pool list (e.g. build,task,surface,triage). Resolved flag > env > per-repo > global > default.')
1258
1296
  .option('--remote <r>', 'run against a REGISTERED repo with NO checkout: materialise a hub mirror + job worktree in the agents\u2019 area (auto-registers an unknown remote), run the pipeline there, then reap (never touches the human area)')
1259
- .option('--isolated', "build in an ISOLATED job worktree off THIS repo's arbiter (inferred from cwd) instead of taking over the current checkout, then integrate + reap \u2014 the in-place-but-isolated form. Shares the same grammar as the no-checkout forms: a single named item, multiple named items (in sequence), AND -n/auto-pick over the mirror-side eligible-pool scan. Always SEQUENTIAL (parallelism is `run` / the CI matrix). Orthogonal to --remote (a foreign repo); with --remote, remote wins (isolation is already implied).")
1297
+ .option('--isolated', "build in an ISOLATED job worktree off THIS repo's arbiter (inferred from cwd) instead of taking over the current checkout, then integrate + reap \u2014 NOW THE DEFAULT since `make-isolated-default-build-mode`; the flag remains accepted as a redundant explicit opt-IN alias (some scripts/skills pin it, and it is harmless once implied). Shares the same grammar as the no-checkout forms: a single named item, multiple named items (in sequence), AND -n/auto-pick over the mirror-side eligible-pool scan. Always SEQUENTIAL (parallelism is `run` / the CI matrix). Orthogonal to --remote (a foreign repo); with --remote, remote wins (isolation is already implied). Mutually exclusive with --in-place (contradictory intents).")
1298
+ .option('--in-place', "OPT OUT of the new isolated default: build in the CURRENT checkout (integrates in place, refuses a dirty tree) \u2014 today's pre-flip behaviour, now explicit. Mutually exclusive with --isolated (contradictory intents) and with --remote (there is no local checkout to take over). Use this for the edit-locally-then-build loop; the default (isolated off the arbiter) never writes the cwd working tree.")
1260
1299
  .option('--merge', 'integrate in merge mode this invocation (mutually exclusive with --propose; overrides config)')
1261
1300
  .option('--propose', 'integrate in propose mode this invocation (default; mutually exclusive with --merge; overrides config)')
1262
- .option('--tasks-land-in <where>', 'where `do spec:<slug>` tasking output lands: `pre-backlog` (staged, not agent-eligible) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > tasksLandIn default > built-in). Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
1301
+ .option('--tasks-land-in <where>', 'where `do spec:<slug>` tasking output lands: `backlog` (staged, not agent-eligible) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > tasksLandIn default > built-in). Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
1263
1302
  .option('--no-pr', 'propose without opening a PR: push the branch but deliberately skip the review request, even on an authed GitHub arbiter (the explicit suppress-PR intent). Resolved flag > env > per-repo > global > default off.')
1264
1303
  .option('--ignore-diverged-main', 'override the in-place divergence guard: run even when local main is ahead of <arbiter>/main (unpushed). The work still lands on the arbiter; local main is left for you to `git rebase`. In-place only; loud, never default.')
1265
1304
  .option('--allow-backlog', 'do task:<slug> ONLY: also FIND, CLAIM, and COMPLETE a task that lives in tasks/backlog/ (staging), driving it in place WITHOUT promoting it to the pool (so no advance leg / run daemon can claim it out from under you). The done-move goes tasks/backlog/ -> tasks/done/ directly (your explicit drive IS the promotion). EXPLICIT-INVOCATION-ONLY: default off, never set by run/auto-pick/advance or config/env.')
@@ -1332,13 +1371,47 @@ export function buildProgram() {
1332
1371
  // `remote-do-reads-per-repo-config-from-arbiter-main`). Only the whitelisted
1333
1372
  // `REPO_ALLOWED_KEYS` are layered (host-only keys stay global/flag/env-only,
1334
1373
  // rejected by the SAME `repo-config.ts` split).
1335
- const isolatedNoRemote = flags.isolated === true && flags.remote === undefined;
1374
+ // Mutually-exclusive intent guards for the three FORM flags (task
1375
+ // `make-isolated-default-build-mode`, D3): `--in-place` opts OUT of the new
1376
+ // isolated default, `--isolated` is the redundant explicit opt-IN alias,
1377
+ // `--remote <r>` targets a foreign repo. `--in-place` + `--isolated` is
1378
+ // contradictory (build here vs build off the arbiter); `--in-place` + `--remote`
1379
+ // is nonsensical (there is no local checkout to take over). Reject loudly
1380
+ // rather than silently prefer one.
1381
+ if (flags.inPlace === true && flags.isolated === true) {
1382
+ console.error('error: --in-place and --isolated are contradictory (build in the current ' +
1383
+ "checkout vs build in a worktree off this repo's arbiter). Pick one.");
1384
+ process.exit(1);
1385
+ }
1386
+ if (flags.inPlace === true && flags.remote !== undefined) {
1387
+ console.error('error: --in-place builds in the CURRENT checkout; --remote <r> targets a ' +
1388
+ 'REGISTERED repo with NO checkout. Pick one.');
1389
+ process.exit(1);
1390
+ }
1391
+ // The DEFAULT flip (task `make-isolated-default-build-mode`): with no
1392
+ // `--remote` and no `--in-place`, `do <slug>` builds in an ISOLATED job
1393
+ // worktree off THIS repo's arbiter — the SAME no-checkout path `--isolated`
1394
+ // already used. The cwd is the origin SOURCE only (arbiter-remote resolution
1395
+ // + per-repo config), never written. `--isolated` remains accepted as a
1396
+ // redundant explicit opt-IN alias (D3). `--in-place` opts out to today's
1397
+ // in-checkout path (the fall-through below). See ADR §3.
1398
+ const isolatedNoRemote = flags.remote === undefined && flags.inPlace !== true;
1399
+ // Whether the user DEFAULTED into isolated (no form flag typed) vs typed
1400
+ // `--isolated` explicitly. Only used to tune the no-arbiter error message so
1401
+ // the default path names `--in-place` as its natural escape (D2).
1402
+ const defaultedToIsolated = isolatedNoRemote && flags.isolated !== true;
1336
1403
  if (flags.remote !== undefined || isolatedNoRemote) {
1337
1404
  // The form's user-facing name + canonical usage, for the shared error
1338
- // messages below (so `--isolated` errors read in its own terms).
1339
- const form = isolatedNoRemote ? '--isolated' : '--remote';
1405
+ // messages below (so isolated errors read in the form's own terms).
1406
+ const form = isolatedNoRemote
1407
+ ? defaultedToIsolated
1408
+ ? 'do'
1409
+ : '--isolated'
1410
+ : '--remote';
1340
1411
  const usage = isolatedNoRemote
1341
- ? '`do --isolated <slug>`'
1412
+ ? defaultedToIsolated
1413
+ ? '`do <slug>`'
1414
+ : '`do --isolated <slug>`'
1342
1415
  : '`do --remote <r> <slug>`';
1343
1416
  // The no-checkout forms now support the SAME variadic grammar the in-place
1344
1417
  // form does: a single NAMED item, MULTIPLE named items (sequential), and
@@ -1360,10 +1433,30 @@ export function buildProgram() {
1360
1433
  resolveDefaultArbiterForCwd(cwd, global, remoteFlags, override);
1361
1434
  const resolvedUrl = resolveArbiterUrlFromCheckout(cwd, arbiterName, identityEnv(bootstrapIdentity, process.env));
1362
1435
  if (resolvedUrl === undefined) {
1363
- console.error(`error: --isolated builds in a worktree off this repo's arbiter ` +
1364
- `('${arbiterName}'), but no such arbiter remote is configured/found ` +
1365
- `here. Run inside a participating repo (a clone with an arbiter ` +
1366
- `remote), or use --remote <url> to target another repo.`);
1436
+ // Isolated needs an arbiter to build off. Two message variants:
1437
+ // - defaulted-to-isolated (no form flag typed) leads with the DEFAULT
1438
+ // and names `--in-place` as the natural local escape (D2). This is
1439
+ // the ERROR-not-silent-degrade behaviour the flip requires: a repo
1440
+ // with no arbiter must NOT be quietly built in-place invisibly.
1441
+ // - explicit `--isolated` keeps its own terms (the user asked for
1442
+ // isolation deliberately) but ALSO surfaces `--in-place` alongside
1443
+ // `--remote <url>` as the two escapes.
1444
+ if (defaultedToIsolated) {
1445
+ console.error(`error: no arbiter is configured for this repo ` +
1446
+ `(no '${arbiterName}' remote), and \`do\` now defaults to ` +
1447
+ `building in an ISOLATED worktree off THIS repo's arbiter ` +
1448
+ `(task \`make-isolated-default-build-mode\`). Configure an ` +
1449
+ `arbiter (e.g. \`dorfl remote add <url>\`), pass --in-place ` +
1450
+ `to build in the current checkout, or use --remote <url> to ` +
1451
+ `target another repo.`);
1452
+ }
1453
+ else {
1454
+ console.error(`error: --isolated builds in a worktree off this repo's arbiter ` +
1455
+ `('${arbiterName}'), but no such arbiter remote is configured/found ` +
1456
+ `here. Run inside a participating repo (a clone with an arbiter ` +
1457
+ `remote), pass --in-place to build in the current checkout, or ` +
1458
+ `use --remote <url> to target another repo.`);
1459
+ }
1367
1460
  process.exit(1);
1368
1461
  }
1369
1462
  effectiveRemote = resolvedUrl;
@@ -1470,6 +1563,11 @@ export function buildProgram() {
1470
1563
  })
1471
1564
  : undefined,
1472
1565
  watch: flags.watch === true,
1566
+ // The dorfl-internal agent deadline + anti-loop ceiling (spec
1567
+ // `graceful-pre-timeout-wip-checkpoint`). Threaded from the RESOLVED
1568
+ // remote-repo config (its committed `dorfl.json` on <arbiter>/main).
1569
+ agentDeadlineMinutes: remoteConfig.agentDeadlineMinutes,
1570
+ maxAutoCheckpoints: remoteConfig.maxAutoCheckpoints,
1473
1571
  color: shouldUseColor(process.stdout),
1474
1572
  note: (message) => console.error(`>> ${message}`),
1475
1573
  noteBlock: (message) => console.error(message),
@@ -1643,6 +1741,11 @@ export function buildProgram() {
1643
1741
  // `--watch`: tail the pi session log live (pi harness only; the
1644
1742
  // performDo guard errors clearly on any other adapter). READ-ONLY.
1645
1743
  watch: flags.watch === true,
1744
+ // The dorfl-internal agent deadline + anti-loop ceiling (spec
1745
+ // `graceful-pre-timeout-wip-checkpoint`). Threaded from the resolved
1746
+ // per-repo config so an edit to dorfl.json reflects on the NEXT tick.
1747
+ agentDeadlineMinutes: config.agentDeadlineMinutes,
1748
+ maxAutoCheckpoints: config.maxAutoCheckpoints,
1646
1749
  color: shouldUseColor(process.stdout),
1647
1750
  note: (message) => console.error(`>> ${message}`),
1648
1751
  noteBlock: (message) => console.error(message),
@@ -1727,7 +1830,7 @@ export function buildProgram() {
1727
1830
  .option('--merge-questions <mode>', 'the merge-question SURFACER gate (off|ask|auto): off drops the surfacer (only for a repo that lands by some other means); ask (default) enumerates unmerged `work/*` branches and surfaces a merge-question sidecar a human answers; auto self-supplies the `merge` answer and lands via the SAME deterministic apply-time re-verify (the merge-mode-like fast path). SEPARATE axis from --observation-triage with a HIGHER default (a dropped merge-question means pushed work never lands). Resolved flag > env > per-repo > global > default ask.')
1728
1831
  .option('--merge', 'integrate the advanced item(s) in merge mode this invocation (mutually exclusive with --propose; overrides config). The CI merge shape is a SINGLE SEQUENTIAL job, so this rides the `-n`/named-sequence path, never the matrix.')
1729
1832
  .option('--propose', 'integrate the advanced item(s) in propose mode this invocation (default; mutually exclusive with --merge; overrides config). The CI propose shape is the parallel matrix (one PR per item).')
1730
- .option('--tasks-land-in <where>', 'where `advance spec:<slug>` tasking output lands: `pre-backlog` (staged) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence. Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
1833
+ .option('--tasks-land-in <where>', 'where `advance spec:<slug>` tasking output lands: `backlog` (staged) or `ready` (the agent POOL). The EXPLICIT operator override at the top of the placement precedence. Resolved flag > env (DORFL_TASKS_LAND_IN) > per-repo > global > built-in.')
1731
1834
  .option('--watch', "stream the build agent's high-signal events live by tailing the pi session log (requires harness: pi; READ-ONLY observer — does not change outcome/gate/git). The same view `do --watch` gives, threaded through the build rung; CI uses it so the job log shows the agent working instead of freezing.")
1732
1835
  .action(async (rawSlugs, flags) => {
1733
1836
  // Variadic grammar (mirrors `do`): zero args = AUTO-PICK; one = the single
@@ -2014,6 +2117,10 @@ export function buildProgram() {
2014
2117
  taskReviewGate: config.review
2015
2118
  ? harnessTaskAcceptanceGate({ harness, agentCmd: config.agentCmd })
2016
2119
  : undefined,
2120
+ // The dorfl-internal agent deadline + anti-loop ceiling (spec
2121
+ // `graceful-pre-timeout-wip-checkpoint`). Threaded from resolved config.
2122
+ agentDeadlineMinutes: config.agentDeadlineMinutes,
2123
+ maxAutoCheckpoints: config.maxAutoCheckpoints,
2017
2124
  color: shouldUseColor(process.stdout),
2018
2125
  note: (message) => console.error(`>> ${message}`),
2019
2126
  noteBlock: (message) => console.error(message),
@@ -2110,7 +2217,7 @@ export function buildProgram() {
2110
2217
  .option('--force', 'OVERRIDE the predicate: remove worktrees even with un-saved work (requires --yes; never the default)')
2111
2218
  .option('--yes', 'confirm a destructive --force sweep non-interactively')
2112
2219
  .option('--ledger [repoPath]', 'SWEEP the work/ lifecycle LEDGER instead of job worktrees: REPORT (never delete) every slug present in more than one work/ status folder (the one-slug-one-folder belt-and-suspenders), with its folders + candidate canonical folder, for a HUMAN to resolve. Defaults to the cwd repo.')
2113
- .option('--reap-stale-locks', '(with --ledger) OPT-IN: also CLEAR every STALE terminal lock the report finds (a held `active` per-item lock whose item is already TERMINAL on <arbiter>/main — the `cleared-stale` class) via the SAME leased delete `release-lock` uses, so one command sweeps all orphaned terminal locks instead of N hand-run release-locks. SCOPED to `cleared-stale` ONLY: a `kept-stuck` (terminal + stuck) or a `kept-in-flight` (active, non-terminal) lock is NEVER reaped, even with this flag. A concurrent change to a lock ref makes its leased delete REJECT (reported), never --force. WITHOUT this flag `gc --ledger` stays report-only (fail-loud, deletes nothing).')
2220
+ .option('--reap-stale-locks', '(with --ledger) OPT-IN: also CLEAR every ORPHAN lock the report finds via the SAME leased delete `release-lock` uses, so one command sweeps all orphaned locks instead of N hand-run release-locks. Reaps the `cleared-stale` class: a held `active` lock that is EITHER terminal on `<arbiter>/main` (stranded between the durable move and the release) OR non-terminal + SURFACED (`needsAnswers:true` + sidecar on `<arbiter>/main`) = the crash-window orphan the ordered bounce transition leaves when step 1 lands but step 2 never runs. A `kept-in-flight` (active + non-terminal + NOT surfaced — a healthy build) is NEVER reaped, even with this flag. A concurrent change to a lock ref makes its leased delete REJECT (reported), never --force. WITHOUT this flag `gc --ledger` stays report-only (fail-loud, deletes nothing).')
2114
2221
  .option('--remote-branches', 'SWEEP the arbiter’s remote work/* BRANCHES instead of job worktrees: delete (via git push --delete, NEVER --force) exactly those PROVABLY MERGED into <arbiter>/main (git merge-base --is-ancestor, the SAME predicate the worktree reaper uses), and RETAIN the rest with a reason. An in-flight/un-merged branch (the recovery point) is NEVER touched. Provider-agnostic plain git — works on a --bare arbiter. The merged-only complement of `requeue --reset`.')
2115
2222
  .option('--arbiter <remote>', '(with --remote-branches) the arbiter git remote whose work/* branches to sweep (default: origin); resolved from --cwd')
2116
2223
  .option('--cwd <dir>', '(with --remote-branches) the local repo/clone whose --arbiter remote points at the arbiter to sweep (default: cwd); only remote refs are read + deleted, never the working tree')
@@ -2140,13 +2247,16 @@ export function buildProgram() {
2140
2247
  // clearing (no auto-sweep; a human asserts a lock is dead via
2141
2248
  // `release-lock`).
2142
2249
  // OPT-IN SWEEP (`--reap-stale-locks`): the WRITE twin of the report. A
2143
- // human asserting "clear the dead TERMINAL locks now": for EXACTLY the
2144
- // `cleared-stale` class (terminal-on-main + active = stranded) perform the
2145
- // SAME leased delete `release-lock` / the recovery use, so one command
2146
- // sweeps every orphaned terminal lock. A `kept-stuck` / `kept-in-flight`
2147
- // lock is NEVER reaped (scope fence); a concurrent change makes a clear
2148
- // REJECT (reported `lost`), never --force. WITHOUT the flag the surface
2149
- // below stays report-only (fail-loud, deletes nothing).
2250
+ // human asserting "clear the dead TERMINAL locks now": for BOTH
2251
+ // terminal-on-main orphan classes (task `reaper-reap-terminal-stuck-lock-orphans`;
2252
+ // ADR `ledger-status-on-per-item-lock-refs` § Addendum 2026-07-10) the
2253
+ // stranded `cleared-stale` AND the stuck-terminal `cleared-stuck-terminal`
2254
+ // crash-orphan perform the SAME leased delete `release-lock` / the
2255
+ // recovery use, so one command sweeps every orphaned terminal lock. A
2256
+ // `kept-stuck` (STUCK + NON-terminal) / `kept-in-flight` lock is NEVER
2257
+ // reaped (scope fence); a concurrent change makes a clear REJECT (reported
2258
+ // `lost`), never --force. WITHOUT the flag the surface below stays
2259
+ // report-only (fail-loud, deletes nothing).
2150
2260
  if (flags.reapStaleLocks) {
2151
2261
  const reap = await reapStaleItemLocks(flags.cwd ?? repoPath, flags.arbiter ?? 'origin', process.env);
2152
2262
  if (flags.json) {
@@ -2165,10 +2275,11 @@ export function buildProgram() {
2165
2275
  ? blocks.join('\n\n')
2166
2276
  : formatLedgerSweep(result));
2167
2277
  }
2168
- // Fail-loud AFTER the sweep: a `kept-stuck` (rightly left for a human) or
2169
- // a `lost`/`error` (a stale lock whose leased delete lost the race) still
2170
- // needs attention; a clean sweep that reaped every stale lock and left
2171
- // only healthy in-flight holds exits 0.
2278
+ // Fail-loud AFTER the sweep: a `kept-stuck` (STUCK + non-terminal
2279
+ // rightly left for a human) or a `lost`/`error` (an orphan lock whose
2280
+ // leased delete lost the race) still needs attention; a clean sweep
2281
+ // that reaped every orphan lock (stale OR stuck-terminal) and left only
2282
+ // healthy in-flight holds exits 0.
2172
2283
  process.exit(result.duplicates.length > 0 || reapReportNeedsAttention(reap)
2173
2284
  ? 1
2174
2285
  : 0);
@@ -2204,13 +2315,15 @@ export function buildProgram() {
2204
2315
  // human clears a NAMED unified lock via `release-lock`).
2205
2316
  //
2206
2317
  // SCOPED to the ATTENTION verdicts only (spec US#14/#21, ADR
2207
- // `ledger-status-on-per-item-lock-refs`: this surface is the STUCK /
2208
- // crash-orphaned lock, NOT every held one): a `kept-stuck` (terminal +
2209
- // stuck) or a `cleared-stale`-eligible (terminal + stale active = orphaned)
2210
- // lock fails loud, but a `kept-in-flight` (active, non-terminal) lock is the
2211
- // NORMAL in-flight state of a healthy concurrent build (read by `status` as
2212
- // healthy) it is reported informationally and does NOT make a routine
2213
- // `gc --ledger` health check exit non-zero.
2318
+ // `ledger-status-on-per-item-lock-refs` § Addendum 2026-07-10: this surface
2319
+ // is the STUCK / crash-orphaned lock, NOT every held one): a `kept-stuck`
2320
+ // (STUCK + NON-terminal the genuine human-attention case), a
2321
+ // `cleared-stale`-eligible (terminal + stale active = stranded orphan), or
2322
+ // a `cleared-stuck-terminal`-eligible (terminal + stuck = crash-orphan the
2323
+ // reaper can now auto-clear) fails loud, but a `kept-in-flight` (active,
2324
+ // non-terminal) lock is the NORMAL in-flight state of a healthy concurrent
2325
+ // build (read by `status` as healthy) — it is reported informationally
2326
+ // and does NOT make a routine `gc --ledger` health check exit non-zero.
2214
2327
  process.exit(result.duplicates.length > 0 ||
2215
2328
  itemLockReportNeedsAttention(lockReport)
2216
2329
  ? 1
@@ -2329,7 +2442,7 @@ export function buildProgram() {
2329
2442
  printPrdToSpecReport(result);
2330
2443
  }
2331
2444
  // Exit non-zero on a REFUSAL (quiescence gate) or a non-green leak scan
2332
- // (a converted tree that still carries a dangling `prd` ref). A clean
2445
+ // (a converted tree that still carries a dangling ''prd'' ref). A clean
2333
2446
  // dry-run or a green migration exits 0.
2334
2447
  if (result.refused || result.leaks.length > 0) {
2335
2448
  process.exit(1);
@@ -2421,11 +2534,13 @@ export function buildProgram() {
2421
2534
  program
2422
2535
  .command('requeue <slug>')
2423
2536
  .helpGroup(HEADLINE_GROUP)
2424
- .description('Requeue a STUCK task to the backlog for re-claiming (ADR §12/§14). Recovers a task whose per-item lock is held — stuck (the resolved-recovery path: a previously-routed needs-attention item, now lock `state: stuck`) OR active (a claim that never surfaced — an un-surfaced abort, a killed run, or an in-place requeue note). The body rests in work/tasks/backlog/<slug>.md (claim never moves it under the per-item-lock model); requeue releases the lock so the item is claimable again. The release is published as a TREE-LESS compare-and-swap to the arbiter ref, EXACTLY like claim — it NEVER stages or commits in the cwd working tree, so a requeue in a shared checkout can never sweep up a concurrent writer’s uncommitted files. DEFAULT = keep + continue: leave the work/<slug> branch UNTOUCHED so the next claim CONTINUES from its tip (rebased onto fresh main at onboard-time). --reset = discard + fresh: delete the remote work/<slug> branch FIRST (then release the lock) so the next claim starts fresh (guarded; never the default). -m/--message appends a dated handoff note to the item body (both modes; append-only).')
2537
+ .description('Requeue a STUCK task to the backlog for re-claiming (ADR §12/§14). Recovers a task whose per-item lock is held — stuck (the resolved-recovery path: a previously-routed needs-attention item, now lock `state: stuck`) OR active (a claim that never surfaced — an un-surfaced abort, a killed run, or an in-place requeue note). The body rests in work/tasks/backlog/<slug>.md (claim never moves it under the per-item-lock model); requeue releases the lock so the item is claimable again. The release is published as a TREE-LESS compare-and-swap to the arbiter ref, EXACTLY like claim — it NEVER stages or commits in the cwd working tree, so a requeue in a shared checkout can never sweep up a concurrent writer’s uncommitted files. Escalation ladder (least to most destructive): (1) DEFAULT = keep + continue: leave the work/<slug> branch UNTOUCHED so the next claim CONTINUES from its tip (rebased onto fresh main at onboard-time); if the work/<slug> branch is not on the arbiter (never pushed, or a prior --reset already deleted it), default requeue succeeds anyway with a fresh-claim note. (2) --reconcile (alias --rebase) = NON-DESTRUCTIVE recovery: re-sync the mirror to the arbiter (prune-fetch) and RETRY the rebase of the kept branch onto latest arbiter/main in a scratch worktree; on a clean rebase, push the reconciled tip back (--force-with-lease, work branch only) and release the lock; on a genuine content conflict AFTER the clean re-sync, leave the item stuck and the branch UNTOUCHED (nothing deleted) so the human can retry when the churn settles. NEVER deletes the remote branch. (3) --reset = DESTRUCTIVE last resort: delete the remote work/<slug> branch FIRST (then release the lock) so the next claim starts fresh (guarded; never the default). -m/--message appends a dated handoff note to the item body (all modes; append-only).')
2425
2538
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
2426
2539
  .option('--cwd <dir>', 'the repo/working clone whose work/ tree the arbiter remote is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree')
2427
2540
  .option('--arbiter <remote>', 'the arbiter git remote the tree-less move is CAS-published to (default: origin). --cwd resolves this remote; the move is never written to the cwd tree.')
2428
- .option('--reset', 'DISCARD the kept work: delete the remote work/<slug> branch FIRST, then move to backlog so the next claim starts FRESH (guarded; a deliberate departure from the never-delete-the-remote-branch invariant). Never the default.')
2541
+ .option('--reconcile', 'NON-DESTRUCTIVE recovery (middle rung, between the default keep+continue and the destructive --reset): re-sync the mirror to the arbiter (prune-fetch) and RETRY the rebase of the kept work/<slug> onto latest arbiter/main in a scratch worktree; on success, push the reconciled tip back with --force-with-lease and release the lock; on a genuine content conflict after the clean re-sync, leave the item stuck and the branch UNTOUCHED (nothing deleted). NEVER deletes the remote branch. Incompatible with --reset.')
2542
+ .option('--rebase', 'alias of --reconcile (canonical name)')
2543
+ .option('--reset', 'DESTRUCTIVE last resort: DISCARD the kept work by deleting the remote work/<slug> branch FIRST, then move to backlog so the next claim starts FRESH (guarded; a deliberate departure from the never-delete-the-remote-branch invariant). Never the default; try --reconcile first.')
2429
2544
  .option('-m, --message <note>', 'append a dated handoff note to the item body for the next agent (append-only; applies to both default and --reset)')
2430
2545
  .action(async (rawSlug, flags) => {
2431
2546
  // Task-only command (§3a): accept bare + `task:`, reject `spec:`.
@@ -2443,6 +2558,12 @@ export function buildProgram() {
2443
2558
  // call site, rather than relying on the seam's silent `?? process.env`
2444
2559
  // default by omission (the implicit fallback that made `requeue`'s human
2445
2560
  // attribution accidental rather than declared).
2561
+ const reconcile = flags.reconcile === true || flags.rebase === true;
2562
+ if (reconcile && flags.reset) {
2563
+ console.error('error: --reconcile and --reset are mutually exclusive ' +
2564
+ '(non-destructive recovery vs destructive last resort). Pick one.');
2565
+ process.exit(1);
2566
+ }
2446
2567
  const result = await ledgerWrite.applyReturnToBacklogTransition({
2447
2568
  cwd,
2448
2569
  slug,
@@ -2451,6 +2572,7 @@ export function buildProgram() {
2451
2572
  // `--cwd` is purely the ORIGIN SOURCE the remote is resolved from.
2452
2573
  arbiter: flags.arbiter ?? 'origin',
2453
2574
  reset: flags.reset,
2575
+ reconcile,
2454
2576
  message: flags.message,
2455
2577
  env: process.env,
2456
2578
  note: (message) => console.error(`>> ${message}`),
@@ -2461,14 +2583,16 @@ export function buildProgram() {
2461
2583
  }
2462
2584
  const how = result.deletedRemoteBranch
2463
2585
  ? ` (--reset: deleted the remote ${workBranchRef('task', slug)} branch; next claim starts fresh)`
2464
- : ' (kept the work branch; next claim continues from its tip)';
2586
+ : result.reconciled
2587
+ ? ` (--reconcile: re-synced mirror + rebased ${workBranchRef('task', slug)} onto latest main and pushed the reconciled tip back; next claim continues from it)`
2588
+ : ' (kept the work branch; next claim continues from its tip)';
2465
2589
  console.log(`Requeued '${slug}' to backlog for re-claiming.${how}`);
2466
2590
  });
2467
2591
  // `promote [item]` (spec `staging-pool-position-gate-and-trust-model`, tasks
2468
2592
  // `pre-backlog-staging-folder-and-promote-step-a` /
2469
2593
  // `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the HUMAN/runner-
2470
2594
  // owned verb that moves a STAGED item into its agent-eligible POOL — a task
2471
- // `work/pre-backlog/<slug>.md → work/backlog/<slug>.md`, a spec
2595
+ // `work/tasks/backlog/<slug>.md → work/tasks/ready/<slug>.md`, a spec
2472
2596
  // `work/specs/proposed/<slug>.md → work/specs/ready/<slug>.md` — as a tree-less CAS on the
2473
2597
  // arbiter, the SAME trust model + mechanism as `requeue`. The agent emits STAGED;
2474
2598
  // only this verb (a human, or the runner) admits it to the pool. With NO argument
@@ -2477,7 +2601,7 @@ export function buildProgram() {
2477
2601
  program
2478
2602
  .command('promote [item]')
2479
2603
  .helpGroup(HEADLINE_GROUP)
2480
- .description(`Admit a STAGED item into its agent-eligible POOL (the runner/human side of the staging gate): a task \`work/pre-backlog/<slug>.md → work/backlog/<slug>.md\`, a spec \`${workFolderPrefix('specs-proposed')}<slug>.md → ${workFolderPrefix('specs-ready')}<slug>.md\`, published as a TREE-LESS compare-and-swap to the arbiter ref (EXACTLY like requeue/claim — it never stages/commits in the cwd tree). The agent only ever CREATES staged; this verb is the gate a human (or the runner) opens. Accepts \`task:<slug>\` / \`spec:<slug>\` / a bare \`<slug>\` (= task). With NO argument, LISTS every promotable item (the tasks in pre-backlog/ + the specs in specs/proposed/ on the arbiter) so you can see what is staged waiting for promotion. Idempotent: promoting an already-pooled slug is a clean no-op success.`)
2604
+ .description(`Admit a STAGED item into its agent-eligible POOL (the runner/human side of the staging gate): a task \`${workFolderPrefix('tasks-backlog')}<slug>.md → ${workFolderPrefix('tasks-ready')}<slug>.md\`, a spec \`${workFolderPrefix('specs-proposed')}<slug>.md → ${workFolderPrefix('specs-ready')}<slug>.md\`, published as a TREE-LESS compare-and-swap to the arbiter ref (EXACTLY like requeue/claim — it never stages/commits in the cwd tree). The agent only ever CREATES staged; this verb is the gate a human (or the runner) opens. Accepts \`task:<slug>\` / \`spec:<slug>\` / a bare \`<slug>\` (= task). With NO argument, LISTS every promotable item (the tasks in ${workFolderPrefix('tasks-backlog')} + the specs in ${workFolderPrefix('specs-proposed')} on the arbiter) so you can see what is staged waiting for promotion. Idempotent: promoting an already-pooled slug is a clean no-op success.`)
2481
2605
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
2482
2606
  .option('--cwd <dir>', 'the repo/working clone whose arbiter remote the tree-less move is resolved FROM (default: cwd) — an ORIGIN SOURCE only; the move is published to the arbiter, never to this tree')
2483
2607
  .option('--arbiter <remote>', 'the arbiter git remote the promotion is CAS-published to / the staging folders are listed from (default: origin)')
@@ -2496,7 +2620,7 @@ export function buildProgram() {
2496
2620
  process.exit(1);
2497
2621
  }
2498
2622
  if (listed.items.length === 0) {
2499
- console.log(`Nothing staged to promote on ${arbiter}/main (work/pre-backlog/ and ${workFolderPrefix('specs-proposed')} are empty).`);
2623
+ console.log(`Nothing staged to promote on ${arbiter}/main (${workFolderPrefix('tasks-backlog')} and ${workFolderPrefix('specs-proposed')} are empty).`);
2500
2624
  return;
2501
2625
  }
2502
2626
  console.log('Staged, awaiting promotion (run `promote <item>`):');
@@ -2507,7 +2631,7 @@ export function buildProgram() {
2507
2631
  }
2508
2632
  // AN ITEM → promote it. `task:`/`spec:` are explicit; a bare slug defaults to
2509
2633
  // a task (mirrors `requeue`). An `obs:`/`observation:` prefix is rejected
2510
- // (observations have no pool). A legacy `prd:` prefix is a DEAD namespace token
2634
+ // (observations have no pool). A legacy ''prd:'' prefix is a DEAD namespace token
2511
2635
  // after the hard cutover — it resolves as a bare literal task slug, not a spec.
2512
2636
  const parsed = parseSlugArg(rawItem);
2513
2637
  if (parsed.explicit === 'observation') {
@@ -2548,31 +2672,67 @@ export function buildProgram() {
2548
2672
  // Idempotent: deleting an absent ref is a clean exit-0 "nothing to clear"
2549
2673
  // (`not-held`), NOT a failure — deleting the lock ref(s) is "all locks released"
2550
2674
  // and recoverable (the work is safe on the `work/<slug>` branches + `main`).
2675
+ //
2676
+ // ESCAPE HATCH (`--entry <literal>`, task
2677
+ // `release-lock-entry-escape-hatch-and-literal-entry-reporting`): the item
2678
+ // positional is OPTIONAL because a lock whose entry name is NOT derivable from
2679
+ // any current item-form (a PRE-CUTOVER `slice-<slug>` / `prd-<slug>` entry minted
2680
+ // before the slice→task / `prd-to-spec` vocabulary cutover) is UN-NAMEABLE through
2681
+ // the item-form path. `--entry` takes the literal `<entry>` VERBATIM (bypassing
2682
+ // the namespace mapping) so a human can still NAME + clear such an orphan without
2683
+ // the raw `git push origin --delete refs/dorfl/lock/…` plumbing. It reuses the
2684
+ // SAME leased-delete path (`releaseLiteralLockEntry` → the shared entry-keyed
2685
+ // core), so the trust model is unchanged: the human still asserts liveness.
2686
+ // EXACTLY ONE of {item positional, --entry} must be given.
2551
2687
  program
2552
- .command('release-lock <item>')
2688
+ .command('release-lock [item]')
2553
2689
  .helpGroup(HEADLINE_GROUP)
2554
- .description('Clear a NAMED stuck/orphaned UNIFIED per-item lock (refs/dorfl/lock/<entry>) by DELETING the ref on the arbiter — the recovery verb for a lock the system orphaned (a crashed build/task/advance that left the hold behind). The generalisation of `release-advancing` from the advancing marker to the ONE lock per item. Same trust model as `requeue`: a HUMAN asserts the lock is dead by NAMING it; the tool never guesses liveness (the lock has NO heartbeat, so there is NO automatic sweep / age-based reaper anywhere). Accepts the same item forms as the lock API: `task:<slug>` / `spec:<slug>` / `obs:<slug>` / a bare `<slug>` (= task). Idempotent — re-running on an already-cleared lock is a clean exit-0 no-op (deleting the lock ref is “all locks released”, recoverable). NEVER `--force`. Discoverable via `gc --ledger` (it REPORTS every lingering lock, never deletes).')
2690
+ .description('Clear a NAMED stuck/orphaned UNIFIED per-item lock (refs/dorfl/lock/<entry>) by DELETING the ref on the arbiter — the recovery verb for a lock the system orphaned (a crashed build/task/advance that left the hold behind). The generalisation of `release-advancing` from the advancing marker to the ONE lock per item. Same trust model as `requeue`: a HUMAN asserts the lock is dead by NAMING it; the tool never guesses liveness (the lock has NO heartbeat, so there is NO automatic sweep / age-based reaper anywhere). Accepts the same item forms as the lock API: `task:<slug>` / `spec:<slug>` / `obs:<slug>` / a bare `<slug>` (= task). ESCAPE HATCH: `--entry <literal>` (no item positional) targets a LITERAL entry name directly, bypassing the namespace mapping — for a lock whose entry name is not derivable from any current item-form (a pre-vocabulary-cutover `slice-<slug>` / `prd-<slug>` prefix, or a future rename); this is the supported way to clear such orphans, so the raw `git push origin --delete refs/dorfl/lock/…` plumbing is no longer required. Idempotent — re-running on an already-cleared lock is a clean exit-0 no-op (deleting the lock ref is “all locks released”, recoverable). NEVER `--force`. Discoverable via `gc --ledger` (it REPORTS every lingering lock and, for a pre-cutover entry, the exact `release-lock --entry <literal>` invocation).')
2555
2691
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
2692
+ .option('--entry <literal>', 'ESCAPE HATCH: the LITERAL lock-entry name (`refs/dorfl/lock/<literal>`) to release VERBATIM, bypassing the namespace mapping — for a lock whose entry name is not derivable from any current item-form (a pre-cutover `slice-<slug>` / `prd-<slug>` entry). Mutually exclusive with the item positional (give EXACTLY one). Validated to `[A-Za-z0-9._-]+` (no slashes/whitespace) so it cannot escape the lock namespace.')
2556
2693
  .option('--cwd <dir>', 'the repo/working clone whose arbiter remote the lock ref is DELETED on (default: cwd)')
2557
2694
  .option('--arbiter <remote>', 'the arbiter git remote the lock ref is deleted on (default: origin)')
2558
2695
  .action(async (item, flags) => {
2559
2696
  const cwd = flags.cwd ?? process.cwd();
2560
2697
  const arbiter = flags.arbiter ?? 'origin';
2561
- const result = await releaseItemLock({
2562
- item,
2563
- cwd,
2564
- arbiter,
2565
- env: process.env,
2566
- });
2698
+ // MUTUAL EXCLUSION: exactly one of {item positional, --entry}. BOTH or
2699
+ // NEITHER is a usage error (non-zero exit, actionable message) — the
2700
+ // item-form and the literal escape hatch are two ways to name ONE lock, not
2701
+ // combinable.
2702
+ if (item !== undefined && flags.entry !== undefined) {
2703
+ console.error('error: give EITHER an item (task:<slug> / spec:<slug> / obs:<slug> / <slug>) OR --entry <literal>, not both.');
2704
+ process.exit(1);
2705
+ }
2706
+ if (item === undefined && flags.entry === undefined) {
2707
+ console.error('error: name the lock to release — an item (task:<slug> / spec:<slug> / obs:<slug> / <slug>) OR --entry <literal> for a pre-cutover entry with no current item-form.');
2708
+ process.exit(1);
2709
+ }
2710
+ // The LITERAL escape-hatch path (`--entry`): take the entry name verbatim,
2711
+ // bypassing the namespace mapping, and reuse the SAME leased-delete core.
2712
+ const result = flags.entry !== undefined
2713
+ ? await releaseLiteralLockEntry({
2714
+ entry: flags.entry,
2715
+ cwd,
2716
+ arbiter,
2717
+ env: process.env,
2718
+ })
2719
+ : await releaseItemLock({
2720
+ item: item,
2721
+ cwd,
2722
+ arbiter,
2723
+ env: process.env,
2724
+ });
2567
2725
  if (result.outcome === 'released') {
2568
2726
  console.log(`Released lock '${result.entry}' (${result.ref} deleted on ${arbiter}; the item itself was untouched — it rests on main / its work/<slug> branch).`);
2569
2727
  return;
2570
2728
  }
2571
- // IDEMPOTENT exit semantics: `releaseItemLock` returns `not-held` when the
2729
+ // IDEMPOTENT exit semantics: the release returns `not-held` when the
2572
2730
  // ref is ALREADY absent. For a HUMAN re-running the verb on an
2573
2731
  // already-cleared lock that is the CORRECT "nothing to clear" outcome —
2574
2732
  // deleting the lock ref(s) is "all locks released" and recoverable — so map
2575
- // it to a clean exit-0 with an honest message (NOT a failure).
2733
+ // it to a clean exit-0 with an honest message (NOT a failure). The message
2734
+ // names the LITERAL entry (`refs/dorfl/lock/<entry>`) so an operator can
2735
+ // copy-paste it straight back into a `--entry` invocation.
2576
2736
  if (result.outcome === 'not-held') {
2577
2737
  console.log(`No lock to release for '${result.entry}' (${result.ref} is already absent on ${arbiter} — “all locks released”, recoverable).`);
2578
2738
  return;
@@ -2580,12 +2740,47 @@ export function buildProgram() {
2580
2740
  console.error(`error: ${result.message}`);
2581
2741
  process.exit(1);
2582
2742
  });
2743
+ // `migrate-stuck-locks` (spec `surface-stuck-as-questions-and-retire-stuck-lock-state`,
2744
+ // resolved decision #3, user story 5; task `migrate-existing-stuck-locks-one-shot`):
2745
+ // the ONE-SHOT ROLLOUT migration that converts every pre-existing `stuck` lock
2746
+ // ref (an item bounced under the OLD model, before `retire-stuck-lock-state`)
2747
+ // into the new resting shape (`needsAnswers:true` + `stuck`-kind sidecar on
2748
+ // `<arbiter>/main` + lock released) via the SAME surface-first-release-second
2749
+ // transition the new bounce uses. Idempotent (a re-run finds no legacy stuck
2750
+ // ref → clean no-op) and bare-arbiter-safe (a ref is a ref). Its own verb (not
2751
+ // folded into `gc --ledger`) because it is a one-shot rollout event with WRITE
2752
+ // semantics on `main`, distinct from `gc`'s report-oriented + terminal-orphan
2753
+ // reap surface — the two are different natures and their exit-code contracts
2754
+ // stay separate.
2755
+ program
2756
+ .command('migrate-stuck-locks')
2757
+ .helpGroup(ADVANCED_GROUP)
2758
+ .description('ONE-SHOT rollout migration: convert every pre-existing `stuck` per-item lock ref (`refs/dorfl/lock/<entry>` written under the retired `stuck` lock state) into the new resting shape — a `needsAnswers:true` item body + a `stuck`-kind `work/questions/<entry>.md` sidecar on `<arbiter>/main`, with the lock ref released — via the SAME surface-first-release-second transition a fresh bounce uses. Idempotent: re-running finds no legacy `stuck` ref (the current lock module never writes `state: stuck`) and is a clean no-op. Bare-arbiter safe (a ref is a ref). Skips a pre-cutover `slice-*` / `prd-*` entry with no current item-form (clear those via `release-lock --entry <literal>`) and NEVER --force-deletes a lock. Exits non-zero only when a surface CAS lost the race or a read/plumbing fault prevented migration.')
2759
+ .option('--cwd <dir>', 'the local repo/clone whose --arbiter remote the lock refs are read + mutated on (default: cwd); the working tree is never touched (the surface transition is tree-less)')
2760
+ .option('--arbiter <remote>', 'the arbiter git remote the migration reads + mutates (default: origin)')
2761
+ .action(async (flags) => {
2762
+ const cwd = flags.cwd ?? process.cwd();
2763
+ const arbiter = flags.arbiter ?? 'origin';
2764
+ const note = (message) => console.error(`>> ${message}`);
2765
+ const report = await migrateStuckLocks({
2766
+ cwd,
2767
+ arbiter,
2768
+ env: process.env,
2769
+ note,
2770
+ });
2771
+ for (const line of formatMigrateStuckLocksReport(report)) {
2772
+ console.log(line);
2773
+ }
2774
+ if (migrateStuckLocksNeedsAttention(report)) {
2775
+ process.exit(1);
2776
+ }
2777
+ });
2583
2778
  // `drop <slug>` (spec `agentic-question-resolution-retire-disposition-vocabulary`,
2584
2779
  // US #5/#11; task `direct-delete-question-cli-helper`): the DIRECT "throw it
2585
2780
  // away" verb — `git rm` a source item AND its question sidecar (when present) in
2586
2781
  // ONE revertible commit, the reason in the commit MESSAGE (git history is the
2587
2782
  // archive). It does NOT round-trip through the decision engine or spawn an agent
2588
- // (that is the SEPARATE agentic `delete-source` verdict in apply-persist.ts);
2783
+ // (that is the SEPARATE agentic `dispose` verdict in apply-persist.ts);
2589
2784
  // this is the human/skill/CLI no-ceremony delete of decision 7. DISTINCT from
2590
2785
  // the existing `remote rm` (the hub-MIRROR deleter) — different concern, no
2591
2786
  // collision. A LOCAL one-commit primitive over the working tree (like apply): it
@@ -2595,7 +2790,6 @@ export function buildProgram() {
2595
2790
  .command('drop <slug>')
2596
2791
  .helpGroup(HEADLINE_GROUP)
2597
2792
  .description('DIRECTLY delete a source item + its question sidecar (when present) in ONE revertible commit — the "I just want to throw this away" path that does NOT round-trip through the decision engine or any agent. Resolves the source by its namespaced identity (`task:<slug>` / `spec:<slug>` / `obs:<slug>` / a bare `<slug>` = task), `git rm`s the source AND its sidecar together, and records your --reason in the commit MESSAGE (git history is the archive). A single revertible commit, so a wrong delete is recoverable via `git revert`. DISTINCT from `remote rm` (the hub-mirror deleter). A LOCAL working-tree commit (like the apply rung); it does not touch the arbiter — push/integrate it as you normally would. If the named source is already gone it is a clean no-op (nothing to throw away).')
2598
- .option('-c, --config <path>', 'config file path', defaultConfigPath())
2599
2793
  .option('--cwd <dir>', 'the working clone the revertible delete commit is made in (default: cwd)')
2600
2794
  .option('--reason <text>', 'why you are throwing this away — recorded in the commit MESSAGE (git history is the archive). Optional; recorded as "(no reason given)" when omitted.')
2601
2795
  .action((slug, flags) => {
@@ -2622,7 +2816,7 @@ export function buildProgram() {
2622
2816
  program
2623
2817
  .command('intake')
2624
2818
  .helpGroup(HEADLINE_GROUP)
2625
- .description('Front-of-funnel: turn a GitHub issue into the right work/ artifact. Reads issue #N + its comment thread via the issue seam (gh), runs a prompt→verdict decision, and dispatches it: a clear, small issue → a proposed work/backlog/<slug>.md PR carrying an `issue: N` closure link (read by a future CI close-job; not `Fixes #N`). GATE-FREE — your explicit invocation IS the authorization (autoTask/autoBuild do NOT apply), exactly as `do`. A LOCAL one-shot AND the SAME command CI schedules. PER-OUTCOME integration modes (the artifact TYPE is decided at runtime): --merge/--propose set BOTH; --merge-spec/--propose-spec and --merge-task/--propose-task override per type; granular overrides the aggregate; unset ⇒ propose for both.')
2819
+ .description(`Front-of-funnel: turn a GitHub issue into the right work/ artifact. Reads issue #N + its comment thread via the issue seam (gh), runs a prompt→verdict decision, and dispatches it: a clear, small issue → a proposed ${workFolderPrefix('tasks-ready')}<slug>.md PR carrying an \`issue: N\` closure link (read by a future CI close-job; not \`Fixes #N\`). GATE-FREE — your explicit invocation IS the authorization (autoTask/autoBuild do NOT apply), exactly as \`do\`. A LOCAL one-shot AND the SAME command CI schedules. PER-OUTCOME integration modes (the artifact TYPE is decided at runtime): --merge/--propose set BOTH; --merge-spec/--propose-spec and --merge-task/--propose-task override per type; granular overrides the aggregate; unset ⇒ propose for both.`)
2626
2820
  .argument('<number>', 'the GitHub issue number to intake (e.g. `intake 42`)')
2627
2821
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
2628
2822
  .option('--arbiter <remote>', 'name of the arbiter git remote (default: per-repo/global defaultArbiter)')
@@ -2633,7 +2827,7 @@ export function buildProgram() {
2633
2827
  .option('--propose-spec', 'integrate a spec outcome in propose mode (granular; overrides --merge/--propose for a spec; mutually exclusive with --merge-spec)')
2634
2828
  .option('--merge-task', 'integrate a task outcome in merge mode (granular; overrides --merge/--propose for a task; mutually exclusive with --propose-task)')
2635
2829
  .option('--propose-task', 'integrate a task outcome in propose mode (granular; overrides --merge/--propose for a task; mutually exclusive with --merge-task)')
2636
- .option('--origin-trust <trusted|untrusted>', "the author-trust verdict to STAMP onto the emitted prd/task (origin: issue + originTrust: <value>), so an untrusted origin survives the merge boundary and later forces the task's BUILD transition to propose. CI's intake.yml derives it from the SAME author_association case as the integration flags. UNSET (a local intake) ⇒ emitted unstamped (human/trusted) — the human running intake IS the checkpoint.")
2830
+ .option('--origin-trust <trusted|untrusted>', "the author-trust verdict to STAMP onto the emitted spec/task (origin: issue + originTrust: <value>), so an untrusted origin survives the merge boundary and later forces the task's BUILD transition to propose. CI's intake.yml derives it from the SAME author_association case as the integration flags. UNSET (a local intake) ⇒ emitted unstamped (human/trusted) — the human running intake IS the checkpoint.")
2637
2831
  .option('--specs-land-in <where>', 'where an intake-authored spec lands: `pre-proposed` (staged, not auto-taskable) or `ready` (the auto-tasking pool). The EXPLICIT operator override at the top of the placement precedence (explicit flag > untrusted-origin forces staging > specsLandIn default > built-in). Resolved flag > env (DORFL_SPECS_LAND_IN) > per-repo > global > built-in.')
2638
2832
  .option('--agent-cmd <cmd>', 'command to run the decision agent')
2639
2833
  .option('--model <id>', 'model the decision agent runs on (routing intent; resolved flag > env > per-repo > global > default)')
@@ -2715,6 +2909,13 @@ export function buildProgram() {
2715
2909
  cwd,
2716
2910
  arbiter: flags.arbiter ?? config.defaultArbiter,
2717
2911
  integration: modes,
2912
+ // The resolved cross-job CAS-retry cap (config `mergeRetries`). `intake`
2913
+ // has no `--merge-retries` flag today (task
2914
+ // `thread-merge-retries-cross-task-and-ratify-default` deliberately does
2915
+ // NOT add one — intake is unattended-CI-first, env + per-repo cover the
2916
+ // need); env + per-repo + global fold into `config.mergeRetries` via the
2917
+ // SAME chain, and unset falls through to the engine default.
2918
+ mergeRetries: config.mergeRetries,
2718
2919
  // The origin-trust stamp the CI shell passes IN (unset ⇒ unstamped).
2719
2920
  originTrust,
2720
2921
  noPR: config.noPR,
@@ -2867,7 +3068,7 @@ export function buildProgram() {
2867
3068
  remote
2868
3069
  .command('find <folder>')
2869
3070
  .helpGroup(HEADLINE_GROUP)
2870
- .description('Discover work/-participating repos under <folder> (a populated work/backlog/), then toggle-add the chosen ones via `remote add`. Interactive multi-select by default; --yes adds ALL discovered repos non-interactively.')
3071
+ .description(`Discover work/-participating repos under <folder> (a populated ${workFolderPrefix('tasks-ready')}), then toggle-add the chosen ones via \`remote add\`. Interactive multi-select by default; --yes adds ALL discovered repos non-interactively.`)
2871
3072
  .option('-c, --config <path>', 'config file path', defaultConfigPath())
2872
3073
  .option('--yes', 'add all discovered participating repos (no prompt)')
2873
3074
  .action(async (folder, flags) => {
@@ -2918,7 +3119,7 @@ export function buildProgram() {
2918
3119
  .option('--gh-bin <bin>', 'the gh CLI binary (default: gh on PATH)')
2919
3120
  .option('--cwd <dir>', 'the target repo working dir (default: cwd)')
2920
3121
  .option('--install-source <registry|workspace>', 'where the CI installs the CLI from: `registry` (npm install -g, the default) or `workspace` (build from the checked-out source + link onto PATH, for the self-hosting monorepo). Overrides auto-detection in both directions.')
2921
- .option('--max-parallel <n>', 'cap on CONCURRENT advance-lifecycle matrix legs (the propose/merge `max-parallel`). Each leg is a full agent session, so a large fan-out can exhaust the model provider rate limit + thrash the CAS. Default 4.')
3122
+ .option('--max-parallel <n>', 'cap on CONCURRENT advance-lifecycle matrix legs (the propose/merge `max-parallel`). Each leg is a full agent session, so a large fan-out can exhaust the model provider rate limit + thrash the CAS. Default 2.')
2922
3123
  .action(async (flags) => {
2923
3124
  const workDir = flags.cwd ?? process.cwd();
2924
3125
  if (flags.installSource !== undefined &&
@@ -2964,8 +3165,64 @@ export function buildProgram() {
2964
3165
  log: (line) => console.error(line),
2965
3166
  });
2966
3167
  });
3168
+ // The `skills` group (ADR `skill-install-vendors-incur-agents-map`): install the
3169
+ // packaged dorfl skills (`from-idea`, `setup`, ...) into the OPERATOR's OWN
3170
+ // harness dirs. Distinct from any target-repo propagation (ADR
3171
+ // `methodology-and-skills` §6 — skills don't travel into target repos; only
3172
+ // `work/protocol/` does). Only `add` is exposed in this pass — `list`/`remove`
3173
+ // are deferred until we have a real use case (the vendored `install()` is
3174
+ // idempotent, so `add` alone covers upgrade + drift-repair; a future `remove`
3175
+ // would map onto the vendored `remove()` and is intentionally out of scope
3176
+ // here to keep the CLI surface minimal).
3177
+ const skills = program
3178
+ .command('skills')
3179
+ .helpGroup(HEADLINE_GROUP)
3180
+ .description("Install the packaged dorfl skills (`from-idea`, `setup`, ...) into the operator's own agent harness(es). Installs into your OWN harness dirs (~/.agents/skills/ + per-harness symlinks) — this is operator tooling, NOT target-repo propagation (skills don't travel into `work/` repos; only `work/protocol/` does).");
3181
+ skills
3182
+ .command('add')
3183
+ .helpGroup(HEADLINE_GROUP)
3184
+ .description('Copy the packaged dorfl skills into the canonical `~/.agents/skills/` (global; `--local` scopes to <cwd>/.agents/skills/) and symlink each detected non-universal harness (Claude Code, Cursor, Windsurf, ...) to it. Idempotent; re-run to pick up new/updated skills.')
3185
+ .option('--local', 'install project-locally into <cwd>/.agents/skills/ (and each detected harness\u2019s PROJECT dir) instead of globally into ~/.agents/skills/. Default: global.')
3186
+ .action((flags) => {
3187
+ const result = installSkills({ global: flags.local !== true });
3188
+ console.log(formatSkillsAddReport(result, flags.local === true));
3189
+ });
2967
3190
  return program;
2968
3191
  }
3192
+ /**
3193
+ * Human-readable report for `dorfl skills add`. Prints the SOURCE the skills
3194
+ * were read from, each canonical install path (one per skill), and each
3195
+ * non-universal harness placement (symlink | copy) with its absolute path.
3196
+ * Universal harnesses read from the canonical dir directly and need no line.
3197
+ * Output goes to stdout; the shape is stable enough for a human to audit "what
3198
+ * landed where" but is NOT a machine contract (no JSON mode in this pass).
3199
+ */
3200
+ export function formatSkillsAddReport(result, local) {
3201
+ const lines = [];
3202
+ const scope = local ? 'project-local' : 'global';
3203
+ lines.push(`Installed dorfl skills (${scope}) from ${result.sourceDir}:`);
3204
+ if (result.paths.length === 0) {
3205
+ lines.push(' (no skills found in source)');
3206
+ }
3207
+ else {
3208
+ for (const p of result.paths.slice().sort()) {
3209
+ lines.push(` ${p}`);
3210
+ }
3211
+ }
3212
+ if (result.agents.length === 0) {
3213
+ lines.push('Harnesses: no non-universal harness detected (universal harnesses read from the canonical dir directly).');
3214
+ }
3215
+ else {
3216
+ lines.push('Harnesses:');
3217
+ for (const a of result.agents.slice().sort((x, y) => {
3218
+ const byAgent = x.agent.localeCompare(y.agent);
3219
+ return byAgent !== 0 ? byAgent : x.path.localeCompare(y.path);
3220
+ })) {
3221
+ lines.push(` ${a.agent}: ${a.mode} -> ${a.path}`);
3222
+ }
3223
+ }
3224
+ return lines.join('\n');
3225
+ }
2969
3226
  /**
2970
3227
  * A readline-backed {@link WizardPrompts} for the interactive `install-ci`
2971
3228
  * wizard. Prompts go to stderr (stdout is reserved for any machine output); a