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/do.js CHANGED
@@ -21,8 +21,159 @@ import { shouldFailProposePrIntent, PROPOSE_PR_INTENT_GH_UNAVAILABLE_MESSAGE, }
21
21
  import { checkGatePreconditions, detectLockfileOnDisk, detectLockfileOnMirrorMain, } from './gate-readiness.js';
22
22
  import { git, run, runAsync, localMainAheadCount } from './git.js';
23
23
  import { identityEnv, assertTransportAllowed, } from './identity.js';
24
- import { parseStopSentinel, isWorkBranchDiffEmpty, emptyDiffStopReason, } from './agent-stop.js';
24
+ import { parseStopSentinel, isWorkBranchDiffEmpty, emptyDiffStopReason, emptyDiffDisposeEnvelope, } from './agent-stop.js';
25
+ import { surfaceStuckToNeedsAttention, routeToNeedsAttention, } from './needs-attention.js';
26
+ import { resolveItemPathByIdentity } from './item-path.js';
25
27
  import { classifyFailureCause, failureCauseLabel, } from './failure-cause.js';
28
+ /**
29
+ * `dorfl do <slug>` (in-place form) — the per-repo, in-place WORKER that
30
+ * claims + builds + gates + integrates in ONE checkout, then EXITS (ADR §3).
31
+ * **This is the CI command** (CI has a checkout, is one repo, is one triggered
32
+ * invocation, exits) and it ABSORBS the manual `ar-run.sh` test-driver.
33
+ *
34
+ * In-place `do` is on the ISOLATION SEAM (`do-run-share-isolation-seam`): it is
35
+ * the FIRST production consumer of `selectIsolationStrategy`/`inPlaceStrategy`,
36
+ * so all THREE `do`/`run` forms (in-place `do`, `do --remote`, `run`) share the
37
+ * ONE `IsolatedTree`-handle post-claim shape. The composition mirrors
38
+ * `do --remote`/`run` exactly: keep the two in-place GUARDS (dirty-tree refusal
39
+ * + pre-flight diverged-main guard) in this driver BEFORE onboarding, CLAIM
40
+ * explicitly via the CAS (`performClaim`), then let
41
+ * `selectIsolationStrategy({checkout}).prepare()` do the ONBOARDING half (fetch +
42
+ * continue-detection + fresh-main `work/<slug>` switch, incl. the §14
43
+ * continue/rebase + §10 conflict path) WITHOUT re-claiming. The agent run is the
44
+ * ONLY new middle step (the one `ar-run.sh` shelled out for, `prompt | pi`). The
45
+ * runner owns EVERY git-state transition (claim, done-move, completion commit,
46
+ * integration); the agent only edits code.
47
+ *
48
+ * The in-place ISOLATION (ADR §3): the current checkout / CI container IS the
49
+ * isolation — no hub mirror, no external worktree. `inPlaceStrategy.prepare()`
50
+ * puts the checkout on `work/<slug>` cut from the freshly-fetched
51
+ * `<arbiter>/main`; its handle's `teardown` is a NO-OP (the checkout is left in a
52
+ * defined state on `work/<slug>`, NEVER reaped). `do --remote` (the job-worktree
53
+ * strategy) is the SEPARATE `do-remote` task; auto-pick / multi-arg / `-n` is
54
+ * `do-autopick`.
55
+ *
56
+ * CLAIM SEMANTICS (autonomous, same as `do --remote`/`run`): the claim is
57
+ * explicit and claim-or-lose. An item that is NOT in the pool (`tasks-ready`) on
58
+ * the arbiter (already in-progress / done / absent) is not claimable, so the CAS
59
+ * returns
60
+ * `lost` (exit 2) and the run skips cleanly — `do`, the unattended CI worker,
61
+ * never re-claims an item someone else holds and never silently picks up a
62
+ * needs-attention item (a human does that through the human face).
63
+ *
64
+ * **CRITICAL — `do` is AUTONOMOUS, so its failure path is `run`'s, NOT
65
+ * `complete`'s.** On a red gate / rebase conflict, `performComplete` routes the
66
+ * item to needs-attention via the SAME seam call as `run`'s `runOneItem`, but
67
+ * `complete` calls it WITHOUT an arbiter (the human path: a human is right
68
+ * there). `do` runs UNATTENDED, so it MUST get the autonomous, arbiter-passed
69
+ * surfacing like `run` (the on-`main` cherry-pick that makes a stuck CI run
70
+ * visible to `scan`/`status`/another machine). We achieve this by passing
71
+ * `surfaceArbiter` into `performComplete` (resolution (a) from the task): the
72
+ * success path reuses `complete`'s machinery; only the NEEDS-ATTENTION routing
73
+ * becomes the autonomous variant.
74
+ *
75
+ * `--propose` (default) / `--merge` is resolved at integrate-time exactly like
76
+ * `complete` (the caller threads the resolved mode in as `integration`).
77
+ */
78
+ /**
79
+ * The commit-message subject the deadline-checkpoint save writes, and the
80
+ * pattern used to COUNT consecutive checkpoints on the branch (spec
81
+ * `graceful-pre-timeout-wip-checkpoint` — the anti-loop ceiling). Living
82
+ * on-branch means the counter naturally RESETS when the item completes (the
83
+ * branch is discarded on integration) and when a `--reset` requeue discards
84
+ * the branch: a genuinely-progressing task cannot silently accumulate a
85
+ * ceiling breach across generations.
86
+ */
87
+ const DEADLINE_CHECKPOINT_COMMIT_SUBJECT_PREFIX = 'chore(deadline-checkpoint)';
88
+ /**
89
+ * Distinct reason strings for the two deadline-checkpoint branches (spec
90
+ * `graceful-pre-timeout-wip-checkpoint` — the CI/human report must NEVER
91
+ * collapse to a generic "agent-failed"). Auto-continue and surface carry
92
+ * different phrasing so the counter + ceiling are legible.
93
+ */
94
+ function deadlineAutoContinueReason(params) {
95
+ return (`deadline checkpoint (auto-continued ${params.count}/${params.max}): ` +
96
+ `'${params.slug}' hit the dorfl-internal deadline with new work-branch ` +
97
+ `progress this session; saved the WIP and released the lock so the next ` +
98
+ `tick continues from the branch tip.`);
99
+ }
100
+ function deadlineSurfaceReason(params) {
101
+ if (params.kind === 'no-progress') {
102
+ return (`deadline checkpoint (no progress / ceiling): '${params.slug}' hit ` +
103
+ `the dorfl-internal deadline WITHOUT any work-branch progress this ` +
104
+ `session — indistinguishable from a wedge; surfaced for a human to ` +
105
+ `decide (continue / split / cancel).`);
106
+ }
107
+ return (`deadline checkpoint (no progress / ceiling): '${params.slug}' has hit ` +
108
+ `the dorfl-internal deadline ${params.count}/${params.max} times ` +
109
+ `consecutively — it may be too big for one CI leg. Split it or run ` +
110
+ `it locally; surfaced for a human to decide.`);
111
+ }
112
+ /**
113
+ * Count how many DEADLINE-CHECKPOINT commits (subject prefix
114
+ * `chore(deadline-checkpoint)`) currently sit on the work branch ahead of
115
+ * `<arbiter>/main` — the branch-based anti-loop counter (spec
116
+ * `graceful-pre-timeout-wip-checkpoint`). A best-effort count: an unreachable
117
+ * arbiter / a bare mirror reads as 0 (the SAFE direction — never falsely block
118
+ * an auto-continue) so a genuine plumbing fault degrades to "under the ceiling"
119
+ * rather than surfacing a spurious ceiling breach. Resets naturally when the
120
+ * item completes (the branch is discarded on integration).
121
+ */
122
+ async function countDeadlineCheckpointsOnBranch(params) {
123
+ const { cwd, arbiter, branch, env } = params;
124
+ const range = `${arbiter}/main..HEAD`;
125
+ void branch;
126
+ const r = await runAsync('git', ['log', '--format=%s', range], cwd, { env });
127
+ if (r.status !== 0) {
128
+ return 0;
129
+ }
130
+ const lines = r.stdout.split('\n').filter((s) => s.length > 0);
131
+ return lines.filter((s) => s.startsWith(DEADLINE_CHECKPOINT_COMMIT_SUBJECT_PREFIX)).length;
132
+ }
133
+ /**
134
+ * SAVE the WIP on a deadline stop (spec `graceful-pre-timeout-wip-checkpoint`,
135
+ * build step 3): commit the tree as-is + push `work/<slug>`. Reuses the SAVE
136
+ * HALF of the needs-attention mechanism ({@link routeToNeedsAttention}) but
137
+ * ADDS a distinguishing checkpoint-marker commit on top so
138
+ * {@link countDeadlineCheckpointsOnBranch} can count auto-continues. Distinct
139
+ * from `applyNeedsAttentionTransition` (the whole thing) which ALSO marks the
140
+ * lock stuck — auto-continue must NOT mark stuck; only the surface branch does.
141
+ */
142
+ async function saveDeadlineCheckpoint(params) {
143
+ const { slug, branch, cwd, arbiter, reason, env, note } = params;
144
+ void reason;
145
+ // Save any uncommitted work first (the routeToNeedsAttention save half also
146
+ // commits + pushes; but we ALSO want a distinct checkpoint-marker commit so
147
+ // countDeadlineCheckpointsOnBranch can enumerate them). Stage everything and
148
+ // commit a MARKER commit if there's anything to record OR if there is not
149
+ // already a marker on the tip — the marker's job is to make the checkpoint
150
+ // COUNTABLE, not to represent new work.
151
+ const addResult = await runAsync('git', ['add', '-A'], cwd, { env });
152
+ void addResult;
153
+ const diffCached = await runAsync('git', ['diff', '--cached', '--quiet'], cwd, { env });
154
+ const hasStaged = diffCached.status !== 0;
155
+ if (hasStaged) {
156
+ await runAsync('git', [
157
+ 'commit',
158
+ '-q',
159
+ '-m',
160
+ `${DEADLINE_CHECKPOINT_COMMIT_SUBJECT_PREFIX}: save wip for '${slug}'`,
161
+ ], cwd, { env });
162
+ }
163
+ // Push the branch to the arbiter (best-effort). Reuse routeToNeedsAttention's
164
+ // save-half exact contract: it commits any residue + pushes the branch with
165
+ // bounded backoff. We already committed above; the wip commit inside
166
+ // routeToNeedsAttention will be a no-op (clean tree) and the push runs.
167
+ return routeToNeedsAttention({
168
+ cwd,
169
+ slug,
170
+ reason: `deadline-checkpoint save for '${slug}' (see branch)`,
171
+ arbiter,
172
+ branch,
173
+ note,
174
+ env,
175
+ });
176
+ }
26
177
  const DEFAULT_ARBITER = 'origin';
27
178
  /**
28
179
  * Map a `do spec:<slug>` {@link TaskResult} onto the `do` {@link DoResult}
@@ -176,6 +327,11 @@ export async function performDo(options) {
176
327
  // `integration:'propose'` + `taskingIntegration:'merge'` lands the task FILES
177
328
  // on main here while the BUILD path below still threads plain `integration`.
178
329
  integration: options.taskingIntegration ?? options.integration,
330
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
331
+ // threaded so the `do spec:` tasking-transition's land tail respects the
332
+ // same per-repo cap the build path uses (task
333
+ // `thread-merge-retries-cross-task-and-ratify-default`).
334
+ mergeRetries: options.mergeRetries,
179
335
  // The per-repo TASK-PLACEMENT default + the operator's explicit
180
336
  // override (task `runner-deterministic-slice-placement-policy-and-
181
337
  // precedence`). The tasker reads them as the configured-default + the
@@ -362,14 +518,17 @@ export async function performDo(options) {
362
518
  // auto-resolved). Surface to needs-attention TREE-LESSLY via the SAME `#89`
363
519
  // mechanism `requeue` uses for the reverse direction — the rebase was
364
520
  // ABORTED, so the kept `work/<slug>` tip == the arbiter tip (already on the
365
- // arbiter, after-commit, recoverable). The surface is purely the one-file
366
- // `in-progress/ needs-attention/` ledger move + reason (no branch push, no
367
- // worktree mutation) instead of running the agent — the §10 path. The work
521
+ // arbiter, after-commit, recoverable). The surface is purely the lock amend
522
+ // to `state: stuck` with the reason (post lock-cutover no folder move, no
523
+ // branch push, no worktree mutation) instead of running the agent — the §10
524
+ // path. The work
368
525
  // did NOT onboard; the runner owns the bounce.
369
526
  if (tree.continueRebaseConflict) {
370
527
  const reason = `continuing the kept ${tree.branch}: rebase onto the latest main ` +
371
- 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
372
- 'main, or `requeue --reset` to discard and start fresh';
528
+ 'conflicted (aborted, never auto-resolved) — run `requeue --reconcile` ' +
529
+ 'to non-destructively re-sync the mirror and retry the rebase (keeps ' +
530
+ 'the work). Last resort: `requeue --reset` DESTRUCTIVELY discards the ' +
531
+ 'branch and starts fresh.';
373
532
  const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
374
533
  cwd: tree.dir,
375
534
  slug,
@@ -401,8 +560,10 @@ export async function performDo(options) {
401
560
  if (tree.continuePushFailure !== undefined) {
402
561
  const reason = `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
403
562
  `to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
404
- 'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
405
- 'once the churn settles, or `requeue --reset` to discard and start fresh';
563
+ 'branch is left intact on the arbiter (recoverable); `requeue --reconcile` ' +
564
+ 'to non-destructively re-sync the mirror and retry once the churn ' +
565
+ 'settles (keeps the work). Last resort: `requeue --reset` DESTRUCTIVELY ' +
566
+ 'discards the branch and starts fresh.';
406
567
  const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
407
568
  cwd: tree.dir,
408
569
  slug,
@@ -502,6 +663,23 @@ export async function performDo(options) {
502
663
  note,
503
664
  });
504
665
  }
666
+ // DEADLINE CHECKPOINT (spec `graceful-pre-timeout-wip-checkpoint`): the
667
+ // dorfl-internal deadline fired — stop is graceful, NOT a failure. Route
668
+ // through the shared checkpoint path (save WIP + auto-continue on progress
669
+ // under the ceiling, else surface). Distinct from `saveAgentFailure` on
670
+ // purpose — the deadline path must NOT mark the item stuck when it
671
+ // auto-continues.
672
+ if (agent.timedOut) {
673
+ return await routeDeadlineCheckpoint({
674
+ slug,
675
+ branch,
676
+ cwd: tree.dir,
677
+ arbiter: tree.arbiterRemote,
678
+ maxAutoCheckpoints: options.maxAutoCheckpoints ?? 5,
679
+ env,
680
+ note,
681
+ });
682
+ }
505
683
  if (!agent.ok) {
506
684
  const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
507
685
  return await saveAgentFailure({
@@ -535,7 +713,8 @@ export async function performDo(options) {
535
713
  branch,
536
714
  cwd: tree.dir,
537
715
  arbiter: tree.arbiterRemote,
538
- reason: stopReason,
716
+ reason: stopReason.reason,
717
+ kind: stopReason.kind,
539
718
  env,
540
719
  note,
541
720
  });
@@ -682,8 +861,10 @@ export async function performDo(options) {
682
861
  // (`runRemotePipeline`) uses, so `advance task:<slug>` (via the default
683
862
  // `doDriver = performDo`) agrees with the remote path on the caller-visible
684
863
  // label.
864
+ //
865
+ // PR-2b D3: a clean surface (routedToNeedsAttention:true) is GREEN.
685
866
  return {
686
- exitCode: 1,
867
+ exitCode: completed.routedToNeedsAttention ? 0 : 1,
687
868
  outcome: 'needs-attention',
688
869
  slug,
689
870
  branch,
@@ -770,7 +951,9 @@ function routeReport(routed, branch) {
770
951
  // so loudly, the work is saved LOCALLY only, and how to recover.
771
952
  return {
772
953
  fragment: `push of ${branch} FAILED — the work is saved LOCALLY only; push it ` +
773
- 'when online, then `requeue` (continue), or `requeue --reset` to discard',
954
+ 'when online, then `requeue` (continue) or `requeue --reconcile` ' +
955
+ '(non-destructive retry). Last resort: `requeue --reset` ' +
956
+ 'DESTRUCTIVELY discards.',
774
957
  pushFailed: true,
775
958
  };
776
959
  }
@@ -787,7 +970,9 @@ function routeReport(routed, branch) {
787
970
  landed = 'saved locally';
788
971
  }
789
972
  return {
790
- fragment: `${landed}. Recover via \`requeue\` (continue) or \`requeue --reset\` to discard`,
973
+ fragment: `${landed}. Recover via \`requeue\` (continue) or \`requeue --reconcile\` ` +
974
+ '(non-destructive retry). Last resort: `requeue --reset` ' +
975
+ 'DESTRUCTIVELY discards.',
791
976
  pushFailed: false,
792
977
  };
793
978
  }
@@ -800,10 +985,12 @@ function routeReport(routed, branch) {
800
985
  *
801
986
  * This routes it through the SAME work-preserving machinery a RED GATE uses: the
802
987
  * ledger write seam's needs-attention transition (`git add -A` + a wip commit
803
- * capturing the agent's work + the `git mv needs-attention/` move-only commit
804
- * with the failure detail recorded as the reason in the body), surfaced on the
805
- * arbiter's `main` (the autonomous, cross-machine-visible mode-M surfacing `do`
806
- * already uses for the gate-fail path) so `scan`/`status`/another machine see it.
988
+ * capturing the agent's work, then post lock-cutover a LOCK AMEND to
989
+ * `state: stuck` with the failure detail as the reason on the lock entry, in
990
+ * place of the retired `git mv → needs-attention/` move-only commit), surfaced on
991
+ * the arbiter's `main` (the autonomous, cross-machine-visible mode-M surfacing
992
+ * `do` already uses for the gate-fail path) so `scan`/`status`/another machine
993
+ * see it.
807
994
  *
808
995
  * It ALSO pushes the `work/<slug>` branch to the arbiter so the saved partial
809
996
  * commits travel cross-machine and the item is RECOVERABLE via `requeue`
@@ -826,7 +1013,8 @@ function routeReport(routed, branch) {
826
1013
  * contract stays coherent). Only the WORK-PRESERVING side-effect (unchanged here)
827
1014
  * matches the gate-failure path. We do NOT validate or "fix" the partial work — a broken
828
1015
  * tree committed + surfaced (with the reason) is recoverable; the human chooses
829
- * `requeue` (continue) vs `requeue --reset` (discard).
1016
+ * `requeue` (continue) / `requeue --reconcile` (non-destructive retry) vs
1017
+ * `requeue --reset` (destructive last resort).
830
1018
  */
831
1019
  async function saveAgentFailure(params) {
832
1020
  const { slug, cwd, arbiter, detail, env, note } = params;
@@ -864,8 +1052,10 @@ async function saveAgentFailure(params) {
864
1052
  : `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
865
1053
  `mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
866
1054
  note(message);
1055
+ // PR-2b D3: a cleanly-surfaced bounce is GREEN (exitCode 0); a FAILED surface
1056
+ // stays non-zero.
867
1057
  return {
868
- exitCode: 1,
1058
+ exitCode: routed.moved ? 0 : 1,
869
1059
  outcome: failureCauseToDoOutcome(cause),
870
1060
  slug,
871
1061
  branch,
@@ -884,7 +1074,9 @@ function failureCauseToDoOutcome(cause) {
884
1074
  }
885
1075
  /**
886
1076
  * Build the HONEST result for a CONTINUE-site surface that did NOT land on the
887
- * arbiter (`{moved: false}`). The tree-less `in-progress/ needs-attention/` move
1077
+ * arbiter (`{moved: false}`). The tree-less stuck-lock amend (post lock-cutover
1078
+ * `state: stuck` with the reason, in place of the retired `in-progress/ →
1079
+ * needs-attention/` move)
888
1080
  * lost the CAS race against a busy arbiter (its contention-retry cap exhausted) or
889
1081
  * had no arbiter to publish to, so the item is STILL in-progress on the arbiter —
890
1082
  * a clean `needs-attention` would mislead (it claims the surface landed). Distinct
@@ -920,10 +1112,10 @@ async function resolveStopReason(params) {
920
1112
  const { output, slug, cwd, arbiter, env } = params;
921
1113
  const sentinel = parseStopSentinel(output);
922
1114
  if (sentinel !== undefined) {
923
- return sentinel.reason;
1115
+ return { kind: 'sentinel', reason: sentinel.reason };
924
1116
  }
925
1117
  if (await isWorkBranchDiffEmpty({ cwd, arbiter, env })) {
926
- return emptyDiffStopReason(slug);
1118
+ return { kind: 'empty-diff', reason: emptyDiffStopReason(slug) };
927
1119
  }
928
1120
  return undefined;
929
1121
  }
@@ -936,8 +1128,59 @@ async function resolveStopReason(params) {
936
1128
  * needs-attention reason. The acceptance gate AND Gate-2 are NEVER reached.
937
1129
  */
938
1130
  async function saveAgentStop(params) {
939
- const { slug, cwd, arbiter, reason, env, note } = params;
1131
+ const { slug, cwd, arbiter, reason, kind, env, note } = params;
940
1132
  const branch = params.branch ?? workBranchRef('task', slug);
1133
+ // EMPTY-DIFF branch (spec resolved decision #2, task
1134
+ // `empty-diff-bounce-surfaces-dispose-defaulted-question`): surface a sidecar
1135
+ // on `<arbiter>/main` carrying an engine-authored DISPOSE-DEFAULTED envelope
1136
+ // question, THEN release the lock. "Nothing to do" is a non-deterministic LLM
1137
+ // judgement, so this seam MUST NOT blindly requeue (that would infinite-loop);
1138
+ // the surfaced sidecar + `needsAnswers:true` on `main` parks the item as a
1139
+ // human-visible question a `dispose` answer settles to `tasks/cancelled/`.
1140
+ if (kind === 'empty-diff') {
1141
+ const item = `task:${slug}`;
1142
+ const itemPath = resolveItemPathByIdentity(cwd, item);
1143
+ if (itemPath === undefined) {
1144
+ const message = `The agent STOPPED building '${slug}' (empty diff) but its body could ` +
1145
+ `not be located on the arbiter to surface a sidecar. Reason: ${reason}`;
1146
+ note(message);
1147
+ return {
1148
+ exitCode: 1,
1149
+ outcome: 'agent-stopped',
1150
+ slug,
1151
+ branch,
1152
+ routedToNeedsAttention: false,
1153
+ message,
1154
+ };
1155
+ }
1156
+ const surfaced = await surfaceStuckToNeedsAttention({
1157
+ cwd,
1158
+ slug,
1159
+ item,
1160
+ itemPath,
1161
+ reason,
1162
+ envelope: emptyDiffDisposeEnvelope({ item, reason }),
1163
+ arbiter,
1164
+ env,
1165
+ note,
1166
+ });
1167
+ const message = surfaced.surfaced
1168
+ ? `The agent STOPPED building '${slug}' (empty diff); surfaced a ` +
1169
+ `dispose-defaulted question on ${arbiter}/main and released the lock. ` +
1170
+ `Reason: ${reason}`
1171
+ : `The agent STOPPED building '${slug}' (empty diff) but the surface ` +
1172
+ `did not land (${surfaced.reasonNotSurfaced ?? 'unknown'}). ` +
1173
+ `Reason: ${reason}`;
1174
+ note(message);
1175
+ return {
1176
+ exitCode: 1,
1177
+ outcome: 'agent-stopped',
1178
+ slug,
1179
+ branch,
1180
+ routedToNeedsAttention: surfaced.surfaced,
1181
+ message,
1182
+ };
1183
+ }
941
1184
  const routed = await ledgerWrite.applyNeedsAttentionTransition({
942
1185
  cwd,
943
1186
  slug,
@@ -954,8 +1197,11 @@ async function saveAgentStop(params) {
954
1197
  : `The agent STOPPED building '${slug}' but it could not be marked stuck ` +
955
1198
  `(${routed.reasonNotMoved ?? 'unknown'}). Reason: ${reason}`;
956
1199
  note(message);
1200
+ // PR-2b D3: agent-stopped (sentinel) on a clean surface is GREEN (exitCode 0);
1201
+ // a FAILED surface stays non-zero. The empty-diff branch above is UNCHANGED
1202
+ // (owned by `empty-diff-bounce-surfaces-dispose-defaulted-question`).
957
1203
  return {
958
- exitCode: 1,
1204
+ exitCode: routed.moved ? 0 : 1,
959
1205
  outcome: 'agent-stopped',
960
1206
  slug,
961
1207
  branch,
@@ -986,6 +1232,15 @@ async function runDoAgent(options, cwd, prompt, slug) {
986
1232
  return options.dorfl({ cwd, prompt, slug, env: options.env });
987
1233
  }
988
1234
  const harness = options.harness ?? new NullHarness();
1235
+ // Convert the dorfl-internal deadline (minutes) into a wall-clock epoch-ms so
1236
+ // the harness (`launchAsync`) can race the child against it (spec
1237
+ // `graceful-pre-timeout-wip-checkpoint`). Absent ⇒ no deadline; a run that
1238
+ // finishes before it is byte-for-byte unchanged (the harness clears the
1239
+ // timer on normal exit). Forcing the async path when a deadline is set is
1240
+ // {@link launchWithOptionalWatch}'s job.
1241
+ const deadlineMs = options.agentDeadlineMinutes !== undefined
1242
+ ? Date.now() + options.agentDeadlineMinutes * 60_000
1243
+ : undefined;
989
1244
  const launched = await launchWithOptionalWatch({
990
1245
  harness,
991
1246
  dir: cwd,
@@ -997,6 +1252,7 @@ async function runDoAgent(options, cwd, prompt, slug) {
997
1252
  sessionId: slug,
998
1253
  sessionsDir: options.sessionsDir,
999
1254
  watch: options.watch,
1255
+ deadlineMs,
1000
1256
  watchSink: options.watchSink,
1001
1257
  color: options.color,
1002
1258
  env: options.env,
@@ -1004,7 +1260,115 @@ async function runDoAgent(options, cwd, prompt, slug) {
1004
1260
  // Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
1005
1261
  // for the propose-mode PR body — instead of dropping it. Absent (no parseable
1006
1262
  // assistant text) ⇒ undefined ⇒ the body degrades to `--fill` (no regression).
1007
- return { ok: launched.ok, detail: launched.detail, output: launched.output };
1263
+ return {
1264
+ ok: launched.ok,
1265
+ detail: launched.detail,
1266
+ output: launched.output,
1267
+ timedOut: launched.timedOut,
1268
+ };
1269
+ }
1270
+ /**
1271
+ * Route a DEADLINE STOP (spec `graceful-pre-timeout-wip-checkpoint`) through
1272
+ * the checkpoint machinery: ALWAYS save the WIP (commit + push), then
1273
+ * decide — progress AND under ceiling ⇒ AUTO-CONTINUE (release lock, keep
1274
+ * branch, NO sidecar, exit 0); else ⇒ SURFACE a `needsAnswers:true` question.
1275
+ * Shared by {@link performDo} and {@link runRemotePipeline} so the in-place
1276
+ * and remote forms route a deadline stop identically.
1277
+ */
1278
+ async function routeDeadlineCheckpoint(params) {
1279
+ const { slug, branch, cwd, arbiter, maxAutoCheckpoints, env, note } = params;
1280
+ // 1. ALWAYS save the WIP first: commit any residue + push the work branch.
1281
+ const savedReason = `deadline checkpoint save for '${slug}'`;
1282
+ const saved = await saveDeadlineCheckpoint({
1283
+ slug,
1284
+ branch,
1285
+ cwd,
1286
+ arbiter,
1287
+ reason: savedReason,
1288
+ env,
1289
+ note,
1290
+ });
1291
+ void saved;
1292
+ // 2. Progress this session? Reuse the SAME primitive the STOP backstop uses.
1293
+ const emptyDiff = await isWorkBranchDiffEmpty({ cwd, arbiter, env });
1294
+ const madeProgressThisSession = !emptyDiff;
1295
+ // 3. Count consecutive deadline checkpoints on the branch (ceiling gate). The
1296
+ // marker commit we just saved is included — so the count is the number
1297
+ // of checkpoints INCLUDING this one.
1298
+ const checkpointCount = await countDeadlineCheckpointsOnBranch({
1299
+ cwd,
1300
+ arbiter,
1301
+ branch,
1302
+ env,
1303
+ });
1304
+ if (madeProgressThisSession && checkpointCount <= maxAutoCheckpoints) {
1305
+ // AUTO-CONTINUE: release the lock via the SAME default keep+continue path
1306
+ // `requeue` uses (no --reset, no --reconcile, no sidecar). The branch is
1307
+ // KEPT on the arbiter so the next claim continues from its tip.
1308
+ const reason = deadlineAutoContinueReason({
1309
+ slug,
1310
+ count: checkpointCount,
1311
+ max: maxAutoCheckpoints,
1312
+ });
1313
+ const returned = await ledgerWrite.applyReturnToBacklogTransition({
1314
+ cwd,
1315
+ slug,
1316
+ arbiter,
1317
+ env,
1318
+ note,
1319
+ });
1320
+ if (returned.moved) {
1321
+ const message = `Auto-continued '${slug}' at the dorfl-internal deadline (checkpoint ` +
1322
+ `${checkpointCount}/${maxAutoCheckpoints}): WIP saved + branch pushed, ` +
1323
+ `lock released so the next tick continues from ${branch}. ${reason}`;
1324
+ note(message);
1325
+ return {
1326
+ exitCode: 0,
1327
+ outcome: 'deadline-auto-continued',
1328
+ slug,
1329
+ branch,
1330
+ message,
1331
+ };
1332
+ }
1333
+ // The auto-continue lock release did not land — fall through to SURFACE.
1334
+ note(`Auto-continue lock release for '${slug}' did not land ` +
1335
+ `(${returned.reasonNotMoved ?? 'unknown'}); surfacing instead.`);
1336
+ }
1337
+ // SURFACE: mark the lock stuck via the whole applyNeedsAttentionTransition
1338
+ // (save + push + stuck). The WIP was already saved above; a second save is
1339
+ // idempotent (empty commit is skipped inside routeToNeedsAttention).
1340
+ const surfaceReason = madeProgressThisSession
1341
+ ? deadlineSurfaceReason({
1342
+ slug,
1343
+ kind: 'ceiling',
1344
+ count: checkpointCount,
1345
+ max: maxAutoCheckpoints,
1346
+ })
1347
+ : deadlineSurfaceReason({ slug, kind: 'no-progress' });
1348
+ const routed = await ledgerWrite.applyNeedsAttentionTransition({
1349
+ cwd,
1350
+ slug,
1351
+ reason: surfaceReason,
1352
+ arbiter,
1353
+ env,
1354
+ note,
1355
+ });
1356
+ const report = routed.moved ? routeReport(routed, branch) : undefined;
1357
+ const message = routed.moved
1358
+ ? `Surfaced '${slug}' at the deadline checkpoint (${madeProgressThisSession
1359
+ ? `ceiling ${checkpointCount}/${maxAutoCheckpoints}`
1360
+ : 'no progress this session'}); ${report.fragment}. ${surfaceReason}`
1361
+ : `Could not surface '${slug}' at the deadline checkpoint ` +
1362
+ `(${routed.reasonNotMoved ?? 'unknown'}). ${surfaceReason}`;
1363
+ note(message);
1364
+ return {
1365
+ exitCode: 1,
1366
+ outcome: 'deadline-surfaced',
1367
+ slug,
1368
+ branch,
1369
+ routedToNeedsAttention: routed.moved,
1370
+ message,
1371
+ };
1008
1372
  }
1009
1373
  /**
1010
1374
  * True when the working tree has uncommitted (unstaged OR staged) changes — the
@@ -1247,6 +1611,10 @@ export async function performDoRemote(options) {
1247
1611
  // `taskingIntegration ?? integration`, so the `--remote spec:` output ALSO
1248
1612
  // routes through the shared core with the tasking-resolved mode.
1249
1613
  integration: options.taskingIntegration ?? options.integration,
1614
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
1615
+ // threaded on the no-checkout `do --remote spec:` tasking path too,
1616
+ // mirroring the in-place site above.
1617
+ mergeRetries: options.mergeRetries,
1250
1618
  // The per-repo TASK-PLACEMENT default + the operator's explicit
1251
1619
  // override (task `runner-deterministic-slice-placement-policy-and-
1252
1620
  // precedence`). Same threading as the in-place `do spec:` path.
@@ -1402,8 +1770,10 @@ async function runRemotePipeline(options, tree, slug, note, env) {
1402
1770
  // agent — the §10 path `run` uses.
1403
1771
  if (tree.continueRebaseConflict) {
1404
1772
  const reason = `continuing the kept ${tree.branch}: rebase onto the latest main ` +
1405
- 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
1406
- 'main, or `requeue --reset` to discard and start fresh';
1773
+ 'conflicted (aborted, never auto-resolved) — run `requeue --reconcile` ' +
1774
+ 'to non-destructively re-sync the mirror and retry the rebase (keeps ' +
1775
+ 'the work). Last resort: `requeue --reset` DESTRUCTIVELY discards the ' +
1776
+ 'branch and starts fresh.';
1407
1777
  await ledgerWrite.applyNeedsAttentionTransition({
1408
1778
  cwd,
1409
1779
  slug,
@@ -1433,8 +1803,10 @@ async function runRemotePipeline(options, tree, slug, note, env) {
1433
1803
  if (tree.continuePushFailure !== undefined) {
1434
1804
  const reason = `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
1435
1805
  `to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
1436
- 'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
1437
- 'once the churn settles, or `requeue --reset` to discard and start fresh';
1806
+ 'branch is left intact on the arbiter (recoverable); `requeue --reconcile` ' +
1807
+ 'to non-destructively re-sync the mirror and retry once the churn ' +
1808
+ 'settles (keeps the work). Last resort: `requeue --reset` DESTRUCTIVELY ' +
1809
+ 'discards the branch and starts fresh.';
1438
1810
  await ledgerWrite.applyNeedsAttentionTransition({
1439
1811
  cwd,
1440
1812
  slug,
@@ -1574,6 +1946,19 @@ async function runRemotePipeline(options, tree, slug, note, env) {
1574
1946
  note,
1575
1947
  });
1576
1948
  }
1949
+ // Deadline checkpoint on the no-checkout `do --remote` path — same routing
1950
+ // as the in-place path (see performDo).
1951
+ if (agent.timedOut) {
1952
+ return await routeDeadlineCheckpoint({
1953
+ slug,
1954
+ branch: branch ?? workBranchRef('task', slug),
1955
+ cwd,
1956
+ arbiter: arbiterRemote,
1957
+ maxAutoCheckpoints: options.maxAutoCheckpoints ?? 5,
1958
+ env,
1959
+ note,
1960
+ });
1961
+ }
1577
1962
  if (!agent.ok) {
1578
1963
  const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
1579
1964
  return await saveRemoteAgentFailure({
@@ -1604,7 +1989,8 @@ async function runRemotePipeline(options, tree, slug, note, env) {
1604
1989
  branch,
1605
1990
  cwd,
1606
1991
  arbiter: arbiterRemote,
1607
- reason: stopReason,
1992
+ reason: stopReason.reason,
1993
+ kind: stopReason.kind,
1608
1994
  env,
1609
1995
  note,
1610
1996
  });
@@ -1775,8 +2161,9 @@ async function saveRemoteAgentFailure(params) {
1775
2161
  : `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
1776
2162
  `mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
1777
2163
  note(message);
2164
+ // PR-2b D3: cleanly-surfaced remote agent failure is GREEN.
1778
2165
  return {
1779
- exitCode: 1,
2166
+ exitCode: routed.moved ? 0 : 1,
1780
2167
  outcome: failureCauseToDoOutcome(cause),
1781
2168
  slug,
1782
2169
  branch,
@@ -1871,6 +2258,8 @@ export function jobWorktreeDoDriver(closure) {
1871
2258
  watch: options.watch,
1872
2259
  watchSink: options.watchSink,
1873
2260
  color: options.color,
2261
+ agentDeadlineMinutes: options.agentDeadlineMinutes,
2262
+ maxAutoCheckpoints: options.maxAutoCheckpoints,
1874
2263
  read: options.read,
1875
2264
  env: options.env,
1876
2265
  note: options.note,