dorfl 0.1.2 → 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 (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
@@ -110,7 +110,7 @@ jobs:
110
110
  close-merged-issues:
111
111
  runs-on: ubuntu-latest
112
112
  steps:
113
- - uses: actions/checkout@v4
113
+ - uses: actions/checkout@v5
114
114
  with:
115
115
  fetch-depth: 0
116
116
  - uses: ./.github/actions/dorfl-setup
package/src/complete.ts CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  import {ledgerWrite} from './ledger-write.js';
14
14
  import {releaseItemLock} from './item-lock.js';
15
15
  import {workBranchRef, parseWorkBranchRef} from './slug-namespace.js';
16
+ import {isProvablyMergedForReap} from './gc.js';
17
+ import type {SidecarType} from './sidecar.js';
16
18
  import type {ReviewProvider} from './integrator.js';
17
19
  import type {IntegrationMode} from './config.js';
18
20
  import {runAsync, localMainAheadCount, type RunResult} from './git.js';
@@ -315,9 +317,12 @@ export interface CompleteResult {
315
317
  outcome: CompleteOutcome;
316
318
  /**
317
319
  * True iff a FAILURE outcome (gate-failed / rebase-conflict) was routed to
318
- * `work/needs-attention/` via the shared mechanism (ADR §12), rather than left
319
- * dangling. Undefined/false on the success, `--skip-verify`, refused, and
320
- * usage-error paths (none of which move the item to needs-attention).
320
+ * the shared needs-attention mechanism (ADR §12) post lock-cutover, the
321
+ * per-item lock is amended to `state: stuck` with the reason recorded on the
322
+ * lock entry (there is no `work/needs-attention/` folder write anymore),
323
+ * rather than the item being left dangling. Undefined/false on the success,
324
+ * `--skip-verify`, refused, and usage-error paths (none of which mark the
325
+ * item stuck).
321
326
  */
322
327
  routedToNeedsAttention?: boolean;
323
328
  /** The work branch that was completed, when one was resolved. */
@@ -489,13 +494,11 @@ function strandRefusalSlug(err: unknown): string | undefined {
489
494
  }
490
495
 
491
496
  /**
492
- * Surface an autonomous-path strand refusal to `needs-attention/` on the
493
- * arbiter via the TREE-LESS seam ({@link
494
- * ledgerWrite.applyTreelessNeedsAttentionTransition}). The mechanism is the
495
- * one `requeue`/`continue` already use in reverse: fetch `<arbiter>/main`,
496
- * resolve the slug's actual current folder ON THE ARBITER (arbiter-is-truth),
497
- * build the one-file `work/<src>/<slug>.md → work/needs-attention/<slug>.md`
498
- * move on a scratch index, and CAS-publish it. It NEVER touches the caller's
497
+ * Surface an autonomous-path strand refusal via the TREE-LESS seam ({@link
498
+ * ledgerWrite.applyTreelessNeedsAttentionTransition}). Post lock-cutover the
499
+ * seam is a pure LOCK AMEND — the item's per-item lock is moved to
500
+ * `state: stuck` with the reason on the lock entry; no folder move, no
501
+ * `work/needs-attention/` write. It NEVER touches the caller's
499
502
  * working tree / HEAD / index — important here because the refusal was raised
500
503
  * precisely because the working tree does NOT hold an `.md` to `git mv`. It is
501
504
  * idempotent (a re-surface of an already-surfaced slug is a no-op) and reports
@@ -523,11 +526,13 @@ async function surfaceAutonomousStrand(params: {
523
526
  });
524
527
  if (surfaced.moved) {
525
528
  const message =
526
- `'${slug}' refused (${reason}); surfaced to work/needs-attention/ on ` +
527
- `${arbiter}/main so the next autonomous tick does NOT re-claim it.`;
529
+ `'${slug}' refused (${reason}); surfaced on ${arbiter}/main as a stuck ` +
530
+ 'question (needsAnswers) so the next autonomous tick does NOT re-claim it.';
528
531
  note(message);
532
+ // PR-2b D3: a clean surface is GREEN (exitCode 0). The autonomous strand
533
+ // surface is one of the cleanly-surfaced bounce sites this policy applies to.
529
534
  return {
530
- exitCode: 1,
535
+ exitCode: 0,
531
536
  outcome: 'strand-surfaced',
532
537
  routedToNeedsAttention: true,
533
538
  message,
@@ -668,12 +673,14 @@ async function runComplete(
668
673
  // removes it from THERE, so the move can never disagree with what the arbiter
669
674
  // holds even when the local tree diverges (ledger-integrity defect 1). The
670
675
  // `source` we pass is the arbiter-content fallback for the degenerate "arbiter
671
- // holds nothing" case, not the authority. `in-progress/` is RETAINED below for
672
- // the legacy/bounce surfaces that may still source from it until its folder
673
- // removal (9c, OUT of this task's scope).
676
+ // holds nothing" case, not the authority. The legacy `in-progress/` FOLDER PROBE
677
+ // is GONE (task `finish-in-progress-folder-cutover-remove-legacy-recovery-readers`):
678
+ // the per-item-lock cutover moved claim OFF writing `work/in-progress/` (the body
679
+ // rests in `tasks/backlog/`, liveness is the held lock), so nothing writes that
680
+ // folder anymore and the `existsSync(inProgress)` arm could not fire. Liveness is
681
+ // the lock, not the folder.
674
682
  const backlog = workItemPath(cwd, 'tasks-ready', slug);
675
683
  const staged = workItemPath(cwd, 'tasks-backlog', slug);
676
- const inProgress = workItemPath(cwd, 'in-progress', slug);
677
684
  const done = workItemPath(cwd, 'done', slug);
678
685
  const onBacklog = existsSync(backlog);
679
686
  // `--allow-backlog` drive (spec
@@ -686,7 +693,6 @@ async function runComplete(
686
693
  // strand `complete` must REFUSE (the honest "not finished, not done" position),
687
694
  // NOT silently integrate — only the deliberate drive promotes from staging.
688
695
  const onPreBacklog = options.allowBacklog === true && existsSync(staged);
689
- const onInProgress = existsSync(inProgress);
690
696
  // SELF-RENAMING FOLDER task backstop: the binary-known `work/done/<slug>.md`,
691
697
  // OR — when none of the binary-known ledger folders hold the record — the slug
692
698
  // at a RENAMED done-position the migration agent placed it in (e.g.
@@ -695,13 +701,10 @@ async function runComplete(
695
701
  // resolution is byte-for-byte unchanged.
696
702
  const onDone =
697
703
  existsSync(done) ||
698
- (!onBacklog &&
699
- !onPreBacklog &&
700
- !onInProgress &&
701
- recordAtRenamedDonePosition(cwd, slug));
704
+ (!onBacklog && !onPreBacklog && recordAtRenamedDonePosition(cwd, slug));
702
705
  // STRANDED-DONE AUTO-RECOVER (spec `ledger-integrity` story 6, the autonomous
703
- // half of `finish-already-committed-branch`). When neither in-progress/ nor
704
- // needs-attention/ holds the slug on the BRANCH tree BUT done/ does, the work
706
+ // half of `finish-already-committed-branch`). When the pool/staging does NOT
707
+ // hold the slug on the BRANCH tree BUT done/ does, the work
705
708
  // branch was already built + done-moved + committed by a prior run that never
706
709
  // landed on the arbiter (a terminal push failed, or the PR never merged). The
707
710
  // autonomous `do`/`advance`/plain-`complete` path used to refuse this with
@@ -713,8 +716,7 @@ async function runComplete(
713
716
  // `<arbiter>/main` returns `already-integrated` (clean no-op, NEVER a
714
717
  // re-push/double-integrate). Mutually exclusive with the build-path source +
715
718
  // `recovering` flags — the core ignores them when `committedRecovery` is set.
716
- const folderShapeStranded =
717
- !onBacklog && !onPreBacklog && !onInProgress && onDone;
719
+ const folderShapeStranded = !onBacklog && !onPreBacklog && onDone;
718
720
  // DIRTY-CONTINUE GATE (task `recover-autodetect-gated-on-nothing-to-commit`).
719
721
  // The folder-shape stranded-done auto-detect is necessary but NOT sufficient on
720
722
  // a CONTINUE: a requeued task whose prior attempt already done-moved the slug
@@ -749,24 +751,19 @@ async function runComplete(
749
751
  // "nothing to complete" CompleteRefusal). We default `source` to `'tasks-ready'`
750
752
  // so `sourcePath` resolves to the canonical pool path the refusal message
751
753
  // names; the refusal fires because that file does not exist on disk.
752
- const source: 'tasks-ready' | 'tasks-backlog' | 'in-progress' | 'done' =
753
- dirtyContinue
754
- ? 'done'
755
- : onBacklog
756
- ? 'tasks-ready'
757
- : onInProgress
758
- ? 'in-progress'
759
- : onPreBacklog
760
- ? 'tasks-backlog'
761
- : 'tasks-ready';
754
+ const source: 'tasks-ready' | 'tasks-backlog' | 'done' = dirtyContinue
755
+ ? 'done'
756
+ : onBacklog
757
+ ? 'tasks-ready'
758
+ : onPreBacklog
759
+ ? 'tasks-backlog'
760
+ : 'tasks-ready';
762
761
  const sourcePath =
763
762
  source === 'tasks-ready'
764
763
  ? backlog
765
764
  : source === 'tasks-backlog'
766
765
  ? staged
767
- : source === 'in-progress'
768
- ? inProgress
769
- : done;
766
+ : done;
770
767
  if (dirtyContinue) {
771
768
  // Announce LOUDLY (parallel to the `committedRecovery` recovery note above):
772
769
  // the autonomous integrate path took the CONTINUE-BUILD branch, not the
@@ -924,9 +921,19 @@ async function runComplete(
924
921
 
925
922
  // The FAILURE outcomes map 1:1 onto `complete`'s — the core already note()'d
926
923
  // the reason and did any routing; the tail never runs for them.
924
+ // PR-2b exit-code policy (spec
925
+ // `surface-stuck-as-questions-and-retire-stuck-lock-state`, decision #4 / D3):
926
+ // a cleanly-surfaced bounce is GREEN (`exitCode: 0`) iff its surface transition
927
+ // SUCCEEDED (`routedToNeedsAttention: true`); a FAILED surface stays non-zero
928
+ // (retry/resolve). Applies to `prepare-failed`/`gate-failed`/`review-blocked`/
929
+ // `review-unparseable`/`rebase-conflict`. The empty-diff backstop is owned by
930
+ // task `empty-diff-bounce-surfaces-dispose-defaulted-question` and is NOT
931
+ // reached here.
932
+ const surfaceExit = (routed: boolean | undefined): 0 | 1 =>
933
+ routed === true ? 0 : 1;
927
934
  if (core.outcome === 'prepare-failed') {
928
935
  return {
929
- exitCode: 1,
936
+ exitCode: surfaceExit(core.routedToNeedsAttention),
930
937
  outcome: 'prepare-failed',
931
938
  routedToNeedsAttention: core.routedToNeedsAttention,
932
939
  branch: core.branch,
@@ -935,7 +942,7 @@ async function runComplete(
935
942
  }
936
943
  if (core.outcome === 'gate-failed') {
937
944
  return {
938
- exitCode: 1,
945
+ exitCode: surfaceExit(core.routedToNeedsAttention),
939
946
  outcome: 'gate-failed',
940
947
  routedToNeedsAttention: core.routedToNeedsAttention,
941
948
  branch: core.branch,
@@ -944,7 +951,7 @@ async function runComplete(
944
951
  }
945
952
  if (core.outcome === 'review-blocked') {
946
953
  return {
947
- exitCode: 1,
954
+ exitCode: surfaceExit(core.routedToNeedsAttention),
948
955
  outcome: 'review-blocked',
949
956
  routedToNeedsAttention: core.routedToNeedsAttention,
950
957
  branch: core.branch,
@@ -958,7 +965,7 @@ async function runComplete(
958
965
  // `do`/`run` tail re-labels its cause `transient-infra` (NOT a reviewer block) off
959
966
  // the parse-failure phrase in `core.reason`.
960
967
  return {
961
- exitCode: 1,
968
+ exitCode: surfaceExit(core.routedToNeedsAttention),
962
969
  outcome: 'review-unparseable',
963
970
  routedToNeedsAttention: core.routedToNeedsAttention,
964
971
  branch: core.branch,
@@ -967,7 +974,7 @@ async function runComplete(
967
974
  }
968
975
  if (core.outcome === 'rebase-conflict') {
969
976
  return {
970
- exitCode: 1,
977
+ exitCode: surfaceExit(core.routedToNeedsAttention),
971
978
  outcome: 'rebase-conflict',
972
979
  routedToNeedsAttention: core.routedToNeedsAttention,
973
980
  branch: core.branch,
@@ -1120,6 +1127,43 @@ async function runComplete(
1120
1127
  // ONE thing the human must act on, so it is emitted as a visually-distinct
1121
1128
  // block (blank lines + heading + TTY-aware color) ON TOP of the plain summary
1122
1129
  // note below — cosmetic only; the structured result is unchanged.
1130
+ //
1131
+ // BENIGN ALREADY-LANDED RACE TAIL (task `propose-push-survives-stale-lease-
1132
+ // on-reaped-work-ref`, follow-up
1133
+ // `complete-propose-honour-already-landed-and-rename-continue-branch-module`):
1134
+ // when the integrator's propose push observed a REAPED `work/<slug>` ref AND
1135
+ // the work was provably on `<arbiter>/main`, it returns `alreadyLanded: true`
1136
+ // with a custom `instruction`. In that case NOTHING was pushed and there is
1137
+ // NO ref for a PR to point at, so emitting the generic
1138
+ // `formatProposeNextStep` push+PR block ("Pushed ... Open a PR/MR ...") would
1139
+ // misreport a clean no-op as unfinished work. Surface the integrator's
1140
+ // `instruction` VERBATIM instead — it is already the correct clean-no-op
1141
+ // prose (the CI-dominant recovery path via `performIntegration` already does
1142
+ // the same; this is the sibling `complete.ts` caller's honest report). The
1143
+ // tail (switch-to-main / delete-local-branch / `--no-switch`) is UNCHANGED
1144
+ // — only the emitted next-step + summary text differ.
1145
+ if (result.alreadyLanded === true) {
1146
+ const instruction =
1147
+ result.instruction ??
1148
+ `${branch}: already on ${arbiter}/main (clean no-op).`;
1149
+ const tail = options.noSwitch
1150
+ ? `; left on ${branch} (--no-switch).`
1151
+ : deletedLocalBranch
1152
+ ? `; switched to main and deleted ${branch}.`
1153
+ : '; switched to main.';
1154
+ const message = `Completed '${slug}': ${instruction}${tail}`;
1155
+ note(message);
1156
+ return {
1157
+ exitCode: 0,
1158
+ outcome: 'completed',
1159
+ branch,
1160
+ commitMessage,
1161
+ mergedToMain: false,
1162
+ switchedTo,
1163
+ deletedLocalBranch,
1164
+ message,
1165
+ };
1166
+ }
1123
1167
  const color = options.color ?? shouldUseColor(process.stdout);
1124
1168
  const noteBlock = options.noteBlock ?? note;
1125
1169
  noteBlock(
@@ -1316,15 +1360,29 @@ export async function isLocalBranchProvablyOnArbiter(
1316
1360
  // Refresh remote-tracking refs against the live arbiter (best-effort).
1317
1361
  await gitSoft(['fetch', '--quiet', arbiter], cwd, env);
1318
1362
 
1319
- // Merged: tip is an ancestor of <arbiter>/main.
1363
+ // Merged: the tip is provably on <arbiter>/main — fast-path ancestor OR
1364
+ // squash-aware fallback (durable done/dropped record on main + branch
1365
+ // carries nothing main lacks). The SAME predicate `gc.ts`'s worktree reaper
1366
+ // and `reap-branches.ts`'s remote sweep use — do not fork it.
1367
+ const parsed = parseWorkBranchRef(branch);
1368
+ const arbiterMainRef = `${arbiter}/main`;
1320
1369
  const merged =
1321
- (
1322
- await gitSoft(
1323
- ['merge-base', '--is-ancestor', localTip, `${arbiter}/main`],
1324
- cwd,
1325
- env,
1326
- )
1327
- ).status === 0;
1370
+ parsed === undefined
1371
+ ? (
1372
+ await gitSoft(
1373
+ ['merge-base', '--is-ancestor', localTip, arbiterMainRef],
1374
+ cwd,
1375
+ env,
1376
+ )
1377
+ ).status === 0
1378
+ : isProvablyMergedForReap({
1379
+ cwd,
1380
+ tip: localTip,
1381
+ arbiterMain: arbiterMainRef,
1382
+ namespace: parsed.namespace as SidecarType,
1383
+ slug: parsed.slug,
1384
+ env,
1385
+ });
1328
1386
  if (merged) {
1329
1387
  return true;
1330
1388
  }
package/src/config.ts CHANGED
@@ -22,7 +22,7 @@ export type IntegrationMode = 'propose' | 'merge';
22
22
  * `runner-deterministic-slice-placement-policy-and-precedence`, governing ADR
23
23
  * `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Which
24
24
  * folder the runner lands the tasker's emitted task files in BY DEFAULT —
25
- * `'pre-backlog'` (staging — durable + readable but NOT in the agent-eligible
25
+ * `'backlog'` (staging — durable + readable but NOT in the agent-eligible
26
26
  * POOL; a runner/human promotion is needed to make an item claimable; the on-disk
27
27
  * folder for this value is `work/tasks/backlog/`) or `'ready'` (the agent-eligible
28
28
  * POOL — the trusted fast-path landing, on-disk `work/tasks/ready/`). The pool
@@ -30,12 +30,13 @@ export type IntegrationMode = 'propose' | 'merge';
30
30
  * `rename-task-pool-folder-todo-to-ready`, a CLEAN BREAK matching the on-disk
31
31
  * folder `tasks/ready/` and the spec-side `'ready'` pool spelling). The runner-deterministic
32
32
  * placement RESOLVER (`src/placement.ts`) layers on top: `explicit operator flag
33
- * > untrusted-origin ⇒ pre-backlog > tasksLandIn default > built-in
34
- * (pre-backlog)`. An untrusted-origin tasker output is FORCED to staging even in
33
+ * > untrusted-origin ⇒ backlog > tasksLandIn default > built-in
34
+ * (backlog)`. The staging value was renamed `'pre-backlog'` `'backlog'`
35
+ * (matching the on-disk folder `tasks/backlog/`). An untrusted-origin tasker output is FORCED to staging even in
35
36
  * a `'ready'` repo (the positional analogue of the existing
36
37
  * `untrusted-origin-forces-build-propose` rule).
37
38
  */
38
- export type TasksLandIn = 'pre-backlog' | 'ready';
39
+ export type TasksLandIn = 'backlog' | 'ready';
39
40
 
40
41
  /**
41
42
  * **Per-repo SPEC-PLACEMENT default** (spec
@@ -352,7 +353,7 @@ export interface Config {
352
353
  * {@link integration}: flag (`--merge`/`--propose`) > env
353
354
  * (`DORFL_TASKING_INTEGRATION`) > per-repo > global > (fall back to)
354
355
  * `integration` > default `propose`. DISTINCT from intake's per-EMITTED-TYPE
355
- * `{task, prd}` resolver (front door, author-trust-resolved): this is a
356
+ * `{task, spec}` resolver (front door, author-trust-resolved): this is a
356
357
  * per-LIFECYCLE-TRANSITION knob, inside the trust boundary, operator/config-only.
357
358
  */
358
359
  taskingIntegration?: IntegrationMode;
@@ -362,7 +363,7 @@ export interface Config {
362
363
  * `runner-deterministic-slice-placement-policy-and-precedence`). Resolved
363
364
  * per-repo EXACTLY like {@link taskingIntegration} (flag `--tasks-land-in`
364
365
  * > env `DORFL_TASKS_LAND_IN` > per-repo > global > built-in
365
- * `'pre-backlog'`). The tasking path reads it and passes it as the
366
+ * `'backlog'`). The tasking path reads it and passes it as the
366
367
  * CONFIGURED-DEFAULT rung into the shared placement resolver
367
368
  * (`src/placement.ts`); the resolver overlays an EXPLICIT operator flag
368
369
  * (top) and the UNTRUSTED-ORIGIN force (staging) on top, in that order. The
@@ -385,7 +386,7 @@ export interface Config {
385
386
  * influence it. KEY-LEVEL SYMMETRY with `tasksLandIn` — one resolver, two
386
387
  * lifecycles, one precedence change touches ONE place.
387
388
  *
388
- * The sole spec-placement key after the `prd` → `spec` HARD CUTOVER (spec
389
+ * The sole spec-placement key after the ''prd'' → `spec` HARD CUTOVER (spec
389
390
  * `prd-to-spec-vocabulary-cutover-and-migration-command`): the legacy
390
391
  * `prdsLandIn` config key + `--prds-land-in` flag are GONE (clean break).
391
392
  */
@@ -508,8 +509,8 @@ export interface Config {
508
509
  * re-rolled — re-reviewing an unchanged tip after a block would just be a
509
510
  * stochastic dice re-roll that could launder a real reject into a pass). So a
510
511
  * value > 1 makes a FALSE APPROVE harder to slip through (each extra round is a
511
- * veto), it does NOT give blocked work a second chance. A non-approve forces
512
- * `needs-attention/` (never silently merges or loops), per the maintainer
512
+ * veto), it does NOT give blocked work a second chance. A non-approve forces the
513
+ * item stuck (the lock `state: stuck`; never silently merges or loops), per the maintainer
513
514
  * decision. Default a small N (2): two independent approvals required. (A future
514
515
  * builder-REVISE step that MUTATES the tree between rounds is the only thing that
515
516
  * should make a block retryable, because it changes the artifact under review; it
@@ -626,6 +627,40 @@ export interface Config {
626
627
  * `observationTriage` default or shape — a SEPARATE, independent axis.
627
628
  */
628
629
  strictMergeApproval: boolean;
630
+ /**
631
+ * **The dorfl-INTERNAL agent deadline** (minutes). The agent session self-stops
632
+ * at this budget; dorfl then SAVES the WIP (commit + push `work/<slug>`) and
633
+ * routes it per the progress+ceiling decision (auto-continue, or surface). This
634
+ * is the PRIMARY stop (see spec `graceful-pre-timeout-wip-checkpoint`); the
635
+ * GitHub `timeout-minutes` cap is a BACKSTOP set above it as
636
+ * `agentDeadlineMinutes + checkpointHeadroomMinutes`. Default 60; MAX 240 (4h).
637
+ * FAIL-LOUD: values outside `[1, 240]` throw at config load (never silently
638
+ * clamped). Resolved per-repo through the SAME chain as `integration` (flag >
639
+ * env `DORFL_AGENT_DEADLINE_MINUTES` > per-repo > global > default).
640
+ */
641
+ agentDeadlineMinutes: number;
642
+ /**
643
+ * **The GitHub-backstop head-room above the internal deadline** (minutes). The
644
+ * enumerate job's dynamic `githubTimeout` output is
645
+ * `agentDeadlineMinutes + checkpointHeadroomMinutes`; the MIN 10 guarantees the
646
+ * GitHub cap is always at least 10 minutes above the internal deadline so the
647
+ * graceful save always has room to commit + push before the hard kill. Default
648
+ * 30; MIN 10; MAX 60 (1h). FAIL-LOUD: out-of-range throws at load. Resolved
649
+ * per-repo through the SAME chain as `agentDeadlineMinutes`.
650
+ */
651
+ checkpointHeadroomMinutes: number;
652
+ /**
653
+ * **The auto-continue ceiling** for the deadline-checkpoint anti-loop guard
654
+ * (spec `graceful-pre-timeout-wip-checkpoint`). Counts CONSECUTIVE deadline
655
+ * auto-continues; on hitting this cap the next deadline checkpoint SURFACES a
656
+ * `needsAnswers:true` question ("this task has hit the deadline N times — it
657
+ * may be too big for one CI leg; split it or run it locally?") instead of
658
+ * auto-continuing. The counter naturally resets when the item completes (the
659
+ * work branch is discarded on integration). Default 5. Must be a positive
660
+ * integer (fail-loud). Resolved per-repo through the SAME chain as its
661
+ * siblings.
662
+ */
663
+ maxAutoCheckpoints: number;
629
664
  /**
630
665
  * The optional runner **identity** (a bot): run the runner's git + provider
631
666
  * operations as a configured entity via process-scoped env overrides, without
@@ -741,7 +776,7 @@ export const DEFAULT_CONFIG: Config = {
741
776
  // always first. Reproduces today's tasks-first two-pool default. Subsumes the
742
777
  // removed `prdsFirst` boolean (`[task, build, ...]` reproduces `prdsFirst: true`).
743
778
  selectionOrder: DEFAULT_SELECTION_ORDER,
744
- maxParallel: 4,
779
+ maxParallel: 2,
745
780
  perRepoMax: 2,
746
781
  defaultArbiter: 'origin',
747
782
  workspacesDir: join(homedir(), brand.workdirName),
@@ -752,14 +787,14 @@ export const DEFAULT_CONFIG: Config = {
752
787
  // branch but deliberately skip the PR (the explicit suppress-PR intent that
753
788
  // re-homes the old `provider: none` use). NOT a provider choice.
754
789
  noPR: false,
755
- // The tasker's emitted tasks land STAGED (`pre-backlog/`) by default — the
790
+ // The tasker's emitted tasks land STAGED (`tasks/backlog/`) by default — the
756
791
  // conservative landing that preserves the tracer task's behaviour: an item is
757
792
  // durable + readable but NOT in the agent-eligible pool until a human/runner
758
793
  // promotes it. A repo opts into the trusted fast-path with `tasksLandIn:
759
794
  // 'ready'` (or `--tasks-land-in ready` / `DORFL_TASKS_LAND_IN=ready`).
760
795
  // The runner-deterministic resolver overlays explicit-flag + untrusted-origin
761
796
  // force on top of this default (`src/placement.ts`).
762
- tasksLandIn: 'pre-backlog',
797
+ tasksLandIn: 'backlog',
763
798
  // `intake`-authored specs land STAGED (`pre-proposed/`) by default — the
764
799
  // conservative landing that mirrors `tasksLandIn`'s built-in floor: a spec is
765
800
  // durable + readable but NOT in the auto-tasking POOL until a human/runner
@@ -806,8 +841,65 @@ export const DEFAULT_CONFIG: Config = {
806
841
  // `DORFL_STRICT_MERGE_APPROVAL=true`). The re-surface vs. land branch is
807
842
  // `apply-rung-merge-disposition`'s consumer, NOT here.
808
843
  strictMergeApproval: false,
844
+ // The dorfl-INTERNAL agent deadline defaults to 60 minutes (spec
845
+ // `graceful-pre-timeout-wip-checkpoint`). A legitimately-long task that runs
846
+ // past this checkpoints its WIP + auto-continues on the next tick (rather than
847
+ // losing everything to a 6h GitHub SIGKILL). MAX 240 (4h); fail-loud outside.
848
+ agentDeadlineMinutes: 60,
849
+ // The GitHub-backstop head-room above the internal deadline defaults to 30
850
+ // minutes: the enumerate job's dynamic `githubTimeout` output is
851
+ // deadline + headroom, so the default GitHub cap is 60 + 30 = 90 (1h30). MIN 10
852
+ // (guarantees the graceful save has room); MAX 60; fail-loud outside.
853
+ checkpointHeadroomMinutes: 30,
854
+ // The auto-continue ceiling defaults to 5 CONSECUTIVE deadline checkpoints;
855
+ // past that the next checkpoint SURFACES to a human (spec
856
+ // `graceful-pre-timeout-wip-checkpoint` anti-loop guard).
857
+ maxAutoCheckpoints: 5,
809
858
  };
810
859
 
860
+ /**
861
+ * Fail-loud range validation for the deadline / backstop / ceiling triple
862
+ * (spec `graceful-pre-timeout-wip-checkpoint`). Called at the two resolution
863
+ * FINAL points (`loadConfig` for the global chain; `resolveRepoConfigFromLoaded`
864
+ * for the per-repo chain), so an out-of-range value from ANY layer (flag / env /
865
+ * per-repo / global) surfaces as a clean throw naming the field + the valid
866
+ * range. Never clamps — the task's decision is fail-loud, since a silently
867
+ * clamped 400-minute deadline would look correct in `dorfl config --json` and
868
+ * quietly break the invariant that GitHub cap > internal deadline.
869
+ */
870
+ export function validateDeadlineConfig(config: Config): void {
871
+ const adm = config.agentDeadlineMinutes;
872
+ if (!Number.isInteger(adm) || adm < 1 || adm > 240) {
873
+ throw new Error(
874
+ `agentDeadlineMinutes must be a positive integer in [1, 240] ` +
875
+ `(got ${adm}). The dorfl-internal agent deadline caps a single ` +
876
+ `agent session; 240 minutes (4h) is the hard ceiling so the ` +
877
+ `GitHub backstop (deadline + headroom) still fits under GitHub's ` +
878
+ `6h job limit.`,
879
+ );
880
+ }
881
+ const chm = config.checkpointHeadroomMinutes;
882
+ if (!Number.isInteger(chm) || chm < 10 || chm > 60) {
883
+ throw new Error(
884
+ `checkpointHeadroomMinutes must be an integer in [10, 60] ` +
885
+ `(got ${chm}). This is the head-room the GitHub 'timeout-minutes' ` +
886
+ `backstop sits above the internal deadline (rendered dynamically ` +
887
+ `as agentDeadlineMinutes + checkpointHeadroomMinutes by the ` +
888
+ `advance-lifecycle 'enumerate' job); MIN 10 guarantees the ` +
889
+ `graceful save always has room to commit + push before the hard kill.`,
890
+ );
891
+ }
892
+ const cap = config.maxAutoCheckpoints;
893
+ if (!Number.isInteger(cap) || cap < 1) {
894
+ throw new Error(
895
+ `maxAutoCheckpoints must be a positive integer (got ${cap}). ` +
896
+ `It caps CONSECUTIVE deadline auto-continues; past this the ` +
897
+ `next checkpoint surfaces a needs-answers question to a human ` +
898
+ `(the anti-loop guard for the graceful pre-timeout checkpoint).`,
899
+ );
900
+ }
901
+ }
902
+
811
903
  /** The conventional config location (`~/.config/dorfl/config.json`). */
812
904
  export function defaultConfigPath(): string {
813
905
  return join(homedir(), '.config', brand.configDirName, 'config.json');
@@ -879,5 +971,7 @@ export function loadConfig(path: string = defaultConfigPath()): Config {
879
971
  );
880
972
  }
881
973
  }
882
- return mergeConfig(parsed);
974
+ const merged = mergeConfig(parsed);
975
+ validateDeadlineConfig(merged);
976
+ return merged;
883
977
  }
@@ -1,6 +1,28 @@
1
1
  import {run, type RunResult} from './git.js';
2
2
 
3
3
  /**
4
+ * BROADENED SCOPE NOTE (follow-up task
5
+ * `complete-propose-honour-already-landed-and-rename-continue-branch-module`):
6
+ * this module's name still reads as "continue-branch" (its ORIGINAL role: the
7
+ * continue-detection + rebase-onto-fresh-main helpers used by the onboarding
8
+ * paths, documented below), BUT it now ALSO hosts the shared
9
+ * **stale-lease work-branch push** helpers used by MULTIPLE callers:
10
+ *
11
+ * - {@link pushContinuedBranchWithStaleLeaseRetry} — the original
12
+ * continue/onboard caller (the mirror hub → arbiter reconcile push).
13
+ * - {@link pushProposeBranchWithStaleLeaseRetry} — the propose-mode
14
+ * integrator's push (task `propose-push-survives-stale-lease-on-reaped-work-
15
+ * ref`), which adds the BENIGN already-landed predicate (gone remote ref +
16
+ * work reachable from `<arbiter>/main`) on top of the shared `stale info`
17
+ * retry.
18
+ *
19
+ * A rename to something like `stale-lease-work-push.ts` was considered; the
20
+ * scope-note is preferred here because a rename would churn many imports
21
+ * across `src/` + `test/` for no behavioural gain, and the module already reads
22
+ * as a single conceptual bucket ("the work-branch push + continue-detection
23
+ * helpers"). Add new stale-lease work-ref push helpers here rather than
24
+ * spawning a sibling module.
25
+ *
4
26
  * The **continue-detection** shared by BOTH onboarding paths (the keystone of
5
27
  * the `requeue-continue-and-reset` task). `requeue` keeps the `work/<slug>`
6
28
  * branch so the NEXT claim CONTINUES from its tip instead of force-cutting a