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/src/do.ts CHANGED
@@ -59,7 +59,14 @@ import {
59
59
  parseStopSentinel,
60
60
  isWorkBranchDiffEmpty,
61
61
  emptyDiffStopReason,
62
+ emptyDiffDisposeEnvelope,
63
+ type AgentStopClass,
62
64
  } from './agent-stop.js';
65
+ import {
66
+ surfaceStuckToNeedsAttention,
67
+ routeToNeedsAttention,
68
+ } from './needs-attention.js';
69
+ import {resolveItemPathByIdentity} from './item-path.js';
63
70
  import {
64
71
  classifyFailureCause,
65
72
  failureCauseLabel,
@@ -117,6 +124,149 @@ import {
117
124
  * `complete` (the caller threads the resolved mode in as `integration`).
118
125
  */
119
126
 
127
+ /**
128
+ * The commit-message subject the deadline-checkpoint save writes, and the
129
+ * pattern used to COUNT consecutive checkpoints on the branch (spec
130
+ * `graceful-pre-timeout-wip-checkpoint` — the anti-loop ceiling). Living
131
+ * on-branch means the counter naturally RESETS when the item completes (the
132
+ * branch is discarded on integration) and when a `--reset` requeue discards
133
+ * the branch: a genuinely-progressing task cannot silently accumulate a
134
+ * ceiling breach across generations.
135
+ */
136
+ const DEADLINE_CHECKPOINT_COMMIT_SUBJECT_PREFIX = 'chore(deadline-checkpoint)';
137
+
138
+ /**
139
+ * Distinct reason strings for the two deadline-checkpoint branches (spec
140
+ * `graceful-pre-timeout-wip-checkpoint` — the CI/human report must NEVER
141
+ * collapse to a generic "agent-failed"). Auto-continue and surface carry
142
+ * different phrasing so the counter + ceiling are legible.
143
+ */
144
+ function deadlineAutoContinueReason(params: {
145
+ slug: string;
146
+ count: number;
147
+ max: number;
148
+ }): string {
149
+ return (
150
+ `deadline checkpoint (auto-continued ${params.count}/${params.max}): ` +
151
+ `'${params.slug}' hit the dorfl-internal deadline with new work-branch ` +
152
+ `progress this session; saved the WIP and released the lock so the next ` +
153
+ `tick continues from the branch tip.`
154
+ );
155
+ }
156
+ function deadlineSurfaceReason(params: {
157
+ slug: string;
158
+ kind: 'no-progress' | 'ceiling';
159
+ count?: number;
160
+ max?: number;
161
+ }): string {
162
+ if (params.kind === 'no-progress') {
163
+ return (
164
+ `deadline checkpoint (no progress / ceiling): '${params.slug}' hit ` +
165
+ `the dorfl-internal deadline WITHOUT any work-branch progress this ` +
166
+ `session — indistinguishable from a wedge; surfaced for a human to ` +
167
+ `decide (continue / split / cancel).`
168
+ );
169
+ }
170
+ return (
171
+ `deadline checkpoint (no progress / ceiling): '${params.slug}' has hit ` +
172
+ `the dorfl-internal deadline ${params.count}/${params.max} times ` +
173
+ `consecutively — it may be too big for one CI leg. Split it or run ` +
174
+ `it locally; surfaced for a human to decide.`
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Count how many DEADLINE-CHECKPOINT commits (subject prefix
180
+ * `chore(deadline-checkpoint)`) currently sit on the work branch ahead of
181
+ * `<arbiter>/main` — the branch-based anti-loop counter (spec
182
+ * `graceful-pre-timeout-wip-checkpoint`). A best-effort count: an unreachable
183
+ * arbiter / a bare mirror reads as 0 (the SAFE direction — never falsely block
184
+ * an auto-continue) so a genuine plumbing fault degrades to "under the ceiling"
185
+ * rather than surfacing a spurious ceiling breach. Resets naturally when the
186
+ * item completes (the branch is discarded on integration).
187
+ */
188
+ async function countDeadlineCheckpointsOnBranch(params: {
189
+ cwd: string;
190
+ arbiter: string;
191
+ branch: string;
192
+ env: NodeJS.ProcessEnv | undefined;
193
+ }): Promise<number> {
194
+ const {cwd, arbiter, branch, env} = params;
195
+ const range = `${arbiter}/main..HEAD`;
196
+ void branch;
197
+ const r = await runAsync('git', ['log', '--format=%s', range], cwd, {env});
198
+ if (r.status !== 0) {
199
+ return 0;
200
+ }
201
+ const lines = r.stdout.split('\n').filter((s) => s.length > 0);
202
+ return lines.filter((s) =>
203
+ s.startsWith(DEADLINE_CHECKPOINT_COMMIT_SUBJECT_PREFIX),
204
+ ).length;
205
+ }
206
+
207
+ /**
208
+ * SAVE the WIP on a deadline stop (spec `graceful-pre-timeout-wip-checkpoint`,
209
+ * build step 3): commit the tree as-is + push `work/<slug>`. Reuses the SAVE
210
+ * HALF of the needs-attention mechanism ({@link routeToNeedsAttention}) but
211
+ * ADDS a distinguishing checkpoint-marker commit on top so
212
+ * {@link countDeadlineCheckpointsOnBranch} can count auto-continues. Distinct
213
+ * from `applyNeedsAttentionTransition` (the whole thing) which ALSO marks the
214
+ * lock stuck — auto-continue must NOT mark stuck; only the surface branch does.
215
+ */
216
+ async function saveDeadlineCheckpoint(params: {
217
+ slug: string;
218
+ branch: string;
219
+ cwd: string;
220
+ arbiter: string;
221
+ reason: string;
222
+ env: NodeJS.ProcessEnv | undefined;
223
+ note: (message: string) => void;
224
+ }): Promise<import('./needs-attention.js').RouteToNeedsAttentionResult> {
225
+ const {slug, branch, cwd, arbiter, reason, env, note} = params;
226
+ void reason;
227
+ // Save any uncommitted work first (the routeToNeedsAttention save half also
228
+ // commits + pushes; but we ALSO want a distinct checkpoint-marker commit so
229
+ // countDeadlineCheckpointsOnBranch can enumerate them). Stage everything and
230
+ // commit a MARKER commit if there's anything to record OR if there is not
231
+ // already a marker on the tip — the marker's job is to make the checkpoint
232
+ // COUNTABLE, not to represent new work.
233
+ const addResult = await runAsync('git', ['add', '-A'], cwd, {env});
234
+ void addResult;
235
+ const diffCached = await runAsync(
236
+ 'git',
237
+ ['diff', '--cached', '--quiet'],
238
+ cwd,
239
+ {env},
240
+ );
241
+ const hasStaged = diffCached.status !== 0;
242
+ if (hasStaged) {
243
+ await runAsync(
244
+ 'git',
245
+ [
246
+ 'commit',
247
+ '-q',
248
+ '-m',
249
+ `${DEADLINE_CHECKPOINT_COMMIT_SUBJECT_PREFIX}: save wip for '${slug}'`,
250
+ ],
251
+ cwd,
252
+ {env},
253
+ );
254
+ }
255
+ // Push the branch to the arbiter (best-effort). Reuse routeToNeedsAttention's
256
+ // save-half exact contract: it commits any residue + pushes the branch with
257
+ // bounded backoff. We already committed above; the wip commit inside
258
+ // routeToNeedsAttention will be a no-op (clean tree) and the push runs.
259
+ return routeToNeedsAttention({
260
+ cwd,
261
+ slug,
262
+ reason: `deadline-checkpoint save for '${slug}' (see branch)`,
263
+ arbiter,
264
+ branch,
265
+ note,
266
+ env,
267
+ });
268
+ }
269
+
120
270
  /** The terminal status of one in-place `do` run. */
121
271
  export type DoOutcome =
122
272
  | 'completed' // claimed/onboarded → agent → gate green → integrated → exited
@@ -126,8 +276,11 @@ export type DoOutcome =
126
276
  | 'surface-unmoved' // the tree-less surface to needs-attention did NOT land on the arbiter (lost the CAS race / no arbiter) — the item is STILL in-progress on the arbiter; retry/resolve
127
277
  | 'agent-failed' // the agent ran but produced bad/empty output (the conservative generic), OR the cause is unknown — work SAVED + surfaced
128
278
  | 'transient-infra' // a harness-surfaced model/connection outage (post-retry) or a git/provider outage — RETRY the same work (FAILURE-CAUSE axis)
279
+ | 'needs-reauth' // a credential expired / was revoked (OAuth refresh) — retry cannot help; a human must RE-AUTH (FAILURE-CAUSE axis)
129
280
  | 'config-error' // a thrown CORE wiring/config error (e.g. review on, no reviewGate) — fix the WIRING, not the task (FAILURE-CAUSE axis)
130
281
  | 'agent-stopped' // the agent DELIBERATELY stopped (task drifted/ambiguous) OR produced no change → surfaced; gate + Gate-2 SKIPPED
282
+ | 'deadline-auto-continued' // the dorfl-internal deadline fired; WIP saved + branch pushed + lock RELEASED (no sidecar) so the next tick continues (spec `graceful-pre-timeout-wip-checkpoint`)
283
+ | 'deadline-surfaced' // the dorfl-internal deadline fired but auto-continue was refused (no progress this session, or the maxAutoCheckpoints ceiling was hit); WIP saved + question SURFACED for a human
131
284
  | 'refused' // refused (dirty tree, wrong folder, nothing to complete, …)
132
285
  | 'usage-error' // usage / environment problem, or a slug-resolution error
133
286
  | 'tasked' // `do spec:<slug>` — the spec was tasked into work/backlog/ (runner-owned)
@@ -168,6 +321,14 @@ export type DoDorfl = (input: {
168
321
  * Absent ⇒ no body ⇒ the provider degrades to `--fill` (no regression).
169
322
  */
170
323
  output?: string;
324
+ /**
325
+ * **True iff the injected agent simulates a dorfl-internal DEADLINE stop**
326
+ * (spec `graceful-pre-timeout-wip-checkpoint`). Test-only signal so an
327
+ * injected `DoDorfl` can drive the checkpoint routing (save WIP +
328
+ * auto-continue / surface). Production `runDoAgent` never sets this on the
329
+ * injected path — the real deadline race lives in `PiHarness.launchAsync`.
330
+ */
331
+ timedOut?: boolean;
171
332
  };
172
333
 
173
334
  export interface DoOptions {
@@ -251,10 +412,10 @@ export interface DoOptions {
251
412
  * CONFIGURED-DEFAULT rung into the runner-deterministic placement resolver
252
413
  * (`src/placement.ts`). Resolved per-repo through the SAME chain as
253
414
  * `taskingIntegration` (flag > env > per-repo > global > built-in
254
- * `pre-backlog`). The task-BUILD path ignores it (placement is a TASKING
415
+ * `backlog`). The task-BUILD path ignores it (placement is a TASKING
255
416
  * lifecycle concern).
256
417
  */
257
- tasksLandIn?: 'pre-backlog' | 'ready';
418
+ tasksLandIn?: 'backlog' | 'ready';
258
419
  /**
259
420
  * **The OPERATOR's EXPLICIT task-placement override** (the TOP precedence
260
421
  * rung in the placement resolver). Set ONLY when the operator typed
@@ -263,7 +424,7 @@ export interface DoOptions {
263
424
  * CLI always wins, no special force-key) — the positional analogue of
264
425
  * `explicitMerge` overriding the untrusted-origin build-propose rule.
265
426
  */
266
- explicitTasksLandIn?: 'pre-backlog' | 'ready';
427
+ explicitTasksLandIn?: 'backlog' | 'ready';
267
428
  /**
268
429
  * Override the pre-flight DIVERGENCE guard (`--ignore-diverged-main`, mirroring
269
430
  * `--ignore-not-ready`): proceed even when local `main` is ahead of
@@ -408,6 +569,22 @@ export interface DoOptions {
408
569
  * a sink to assert the surfaced lines without a real terminal.
409
570
  */
410
571
  watchSink?: (line: string) => void;
572
+ /**
573
+ * The dorfl-internal agent-session deadline in minutes (spec
574
+ * `graceful-pre-timeout-wip-checkpoint`). Threaded through {@link runDoAgent}
575
+ * to `launchWithOptionalWatch` — the harness races the child against it and
576
+ * flags a graceful stop on fire, which `performDo` routes as a CHECKPOINT
577
+ * (save WIP + auto-continue on progress under the ceiling, else surface).
578
+ * Absent ⇒ no deadline (a run that finishes on its own is byte-for-byte
579
+ * unchanged).
580
+ */
581
+ agentDeadlineMinutes?: number;
582
+ /**
583
+ * Anti-loop ceiling on CONSECUTIVE deadline auto-continues (spec
584
+ * `graceful-pre-timeout-wip-checkpoint`). On hitting this the next
585
+ * deadline checkpoint surfaces to a human instead of auto-continuing.
586
+ */
587
+ maxAutoCheckpoints?: number;
411
588
  }
412
589
 
413
590
  /**
@@ -433,6 +610,25 @@ interface DoAgentLaunchOptions {
433
610
  watch?: boolean;
434
611
  watchSink?: (line: string) => void;
435
612
  color?: boolean;
613
+ /**
614
+ * **The dorfl-internal agent deadline in minutes** (spec
615
+ * `graceful-pre-timeout-wip-checkpoint`). Threaded to {@link runDoAgent},
616
+ * which converts it to a wall-clock `deadlineMs` and passes it to
617
+ * {@link launchWithOptionalWatch} — the pi harness races the child against
618
+ * that deadline. When the deadline fires, the agent is gracefully stopped
619
+ * (SIGTERM + 10s grace + SIGKILL) and the caller routes the run as a
620
+ * CHECKPOINT (save WIP + auto-continue / surface). Absent ⇒ no deadline
621
+ * (byte-for-byte the pre-task behaviour).
622
+ */
623
+ agentDeadlineMinutes?: number;
624
+ /**
625
+ * **The anti-loop ceiling** for consecutive deadline auto-continues (spec
626
+ * `graceful-pre-timeout-wip-checkpoint`). On hitting this the next deadline
627
+ * checkpoint SURFACES to a human instead of auto-continuing. Absent ⇒
628
+ * unlimited (⇒ defensive callers should always thread the resolved config
629
+ * value; the default is 5).
630
+ */
631
+ maxAutoCheckpoints?: number;
436
632
  env?: NodeJS.ProcessEnv;
437
633
  /**
438
634
  * The optional runner IDENTITY (a bot). Threaded from the host-only
@@ -507,12 +703,12 @@ export interface DoRemoteOptions extends DoAgentLaunchOptions {
507
703
  * spec:<slug>` path: threaded into {@link performTask} as the
508
704
  * configured-default rung. See {@link DoOptions.tasksLandIn}.
509
705
  */
510
- tasksLandIn?: 'pre-backlog' | 'ready';
706
+ tasksLandIn?: 'backlog' | 'ready';
511
707
  /**
512
708
  * **The OPERATOR's EXPLICIT task-placement override** on the `do --remote
513
709
  * spec:` path. See {@link DoOptions.explicitTasksLandIn}.
514
710
  */
515
- explicitTasksLandIn?: 'pre-backlog' | 'ready';
711
+ explicitTasksLandIn?: 'backlog' | 'ready';
516
712
  /** The declared per-repo ENV-PREP step (string | list), run ONCE before the
517
713
  * first `verify` on a fresh worktree. Unset ⇒ a no-op (NO default install). */
518
714
  prepare?: VerifyConfig;
@@ -741,6 +937,11 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
741
937
  // `integration:'propose'` + `taskingIntegration:'merge'` lands the task FILES
742
938
  // on main here while the BUILD path below still threads plain `integration`.
743
939
  integration: options.taskingIntegration ?? options.integration,
940
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
941
+ // threaded so the `do spec:` tasking-transition's land tail respects the
942
+ // same per-repo cap the build path uses (task
943
+ // `thread-merge-retries-cross-task-and-ratify-default`).
944
+ mergeRetries: options.mergeRetries,
744
945
  // The per-repo TASK-PLACEMENT default + the operator's explicit
745
946
  // override (task `runner-deterministic-slice-placement-policy-and-
746
947
  // precedence`). The tasker reads them as the configured-default + the
@@ -942,15 +1143,18 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
942
1143
  // auto-resolved). Surface to needs-attention TREE-LESSLY via the SAME `#89`
943
1144
  // mechanism `requeue` uses for the reverse direction — the rebase was
944
1145
  // ABORTED, so the kept `work/<slug>` tip == the arbiter tip (already on the
945
- // arbiter, after-commit, recoverable). The surface is purely the one-file
946
- // `in-progress/ needs-attention/` ledger move + reason (no branch push, no
947
- // worktree mutation) instead of running the agent — the §10 path. The work
1146
+ // arbiter, after-commit, recoverable). The surface is purely the lock amend
1147
+ // to `state: stuck` with the reason (post lock-cutover no folder move, no
1148
+ // branch push, no worktree mutation) instead of running the agent — the §10
1149
+ // path. The work
948
1150
  // did NOT onboard; the runner owns the bounce.
949
1151
  if (tree.continueRebaseConflict) {
950
1152
  const reason =
951
1153
  `continuing the kept ${tree.branch}: rebase onto the latest main ` +
952
- 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
953
- 'main, or `requeue --reset` to discard and start fresh';
1154
+ 'conflicted (aborted, never auto-resolved) — run `requeue --reconcile` ' +
1155
+ 'to non-destructively re-sync the mirror and retry the rebase (keeps ' +
1156
+ 'the work). Last resort: `requeue --reset` DESTRUCTIVELY discards the ' +
1157
+ 'branch and starts fresh.';
954
1158
  const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
955
1159
  cwd: tree.dir,
956
1160
  slug,
@@ -984,8 +1188,10 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
984
1188
  const reason =
985
1189
  `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
986
1190
  `to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
987
- 'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
988
- 'once the churn settles, or `requeue --reset` to discard and start fresh';
1191
+ 'branch is left intact on the arbiter (recoverable); `requeue --reconcile` ' +
1192
+ 'to non-destructively re-sync the mirror and retry once the churn ' +
1193
+ 'settles (keeps the work). Last resort: `requeue --reset` DESTRUCTIVELY ' +
1194
+ 'discards the branch and starts fresh.';
989
1195
  const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
990
1196
  cwd: tree.dir,
991
1197
  slug,
@@ -1074,7 +1280,12 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
1074
1280
  throw err;
1075
1281
  }
1076
1282
 
1077
- let agent: {ok: boolean; detail?: string; output?: string};
1283
+ let agent: {
1284
+ ok: boolean;
1285
+ detail?: string;
1286
+ output?: string;
1287
+ timedOut?: boolean;
1288
+ };
1078
1289
  try {
1079
1290
  agent = await runDoAgent(options, tree.dir, prompt, slug);
1080
1291
  } catch (err) {
@@ -1089,6 +1300,23 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
1089
1300
  note,
1090
1301
  });
1091
1302
  }
1303
+ // DEADLINE CHECKPOINT (spec `graceful-pre-timeout-wip-checkpoint`): the
1304
+ // dorfl-internal deadline fired — stop is graceful, NOT a failure. Route
1305
+ // through the shared checkpoint path (save WIP + auto-continue on progress
1306
+ // under the ceiling, else surface). Distinct from `saveAgentFailure` on
1307
+ // purpose — the deadline path must NOT mark the item stuck when it
1308
+ // auto-continues.
1309
+ if (agent.timedOut) {
1310
+ return await routeDeadlineCheckpoint({
1311
+ slug,
1312
+ branch,
1313
+ cwd: tree.dir,
1314
+ arbiter: tree.arbiterRemote,
1315
+ maxAutoCheckpoints: options.maxAutoCheckpoints ?? 5,
1316
+ env,
1317
+ note,
1318
+ });
1319
+ }
1092
1320
  if (!agent.ok) {
1093
1321
  const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
1094
1322
  return await saveAgentFailure({
@@ -1123,7 +1351,8 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
1123
1351
  branch,
1124
1352
  cwd: tree.dir,
1125
1353
  arbiter: tree.arbiterRemote,
1126
- reason: stopReason,
1354
+ reason: stopReason.reason,
1355
+ kind: stopReason.kind,
1127
1356
  env,
1128
1357
  note,
1129
1358
  });
@@ -1275,8 +1504,10 @@ export async function performDo(options: DoOptions): Promise<DoResult> {
1275
1504
  // (`runRemotePipeline`) uses, so `advance task:<slug>` (via the default
1276
1505
  // `doDriver = performDo`) agrees with the remote path on the caller-visible
1277
1506
  // label.
1507
+ //
1508
+ // PR-2b D3: a clean surface (routedToNeedsAttention:true) is GREEN.
1278
1509
  return {
1279
- exitCode: 1,
1510
+ exitCode: completed.routedToNeedsAttention ? 0 : 1,
1280
1511
  outcome: 'needs-attention',
1281
1512
  slug,
1282
1513
  branch,
@@ -1373,7 +1604,9 @@ function routeReport(
1373
1604
  return {
1374
1605
  fragment:
1375
1606
  `push of ${branch} FAILED — the work is saved LOCALLY only; push it ` +
1376
- 'when online, then `requeue` (continue), or `requeue --reset` to discard',
1607
+ 'when online, then `requeue` (continue) or `requeue --reconcile` ' +
1608
+ '(non-destructive retry). Last resort: `requeue --reset` ' +
1609
+ 'DESTRUCTIVELY discards.',
1377
1610
  pushFailed: true,
1378
1611
  };
1379
1612
  }
@@ -1389,7 +1622,10 @@ function routeReport(
1389
1622
  landed = 'saved locally';
1390
1623
  }
1391
1624
  return {
1392
- fragment: `${landed}. Recover via \`requeue\` (continue) or \`requeue --reset\` to discard`,
1625
+ fragment:
1626
+ `${landed}. Recover via \`requeue\` (continue) or \`requeue --reconcile\` ` +
1627
+ '(non-destructive retry). Last resort: `requeue --reset` ' +
1628
+ 'DESTRUCTIVELY discards.',
1393
1629
  pushFailed: false,
1394
1630
  };
1395
1631
  }
@@ -1403,10 +1639,12 @@ function routeReport(
1403
1639
  *
1404
1640
  * This routes it through the SAME work-preserving machinery a RED GATE uses: the
1405
1641
  * ledger write seam's needs-attention transition (`git add -A` + a wip commit
1406
- * capturing the agent's work + the `git mv needs-attention/` move-only commit
1407
- * with the failure detail recorded as the reason in the body), surfaced on the
1408
- * arbiter's `main` (the autonomous, cross-machine-visible mode-M surfacing `do`
1409
- * already uses for the gate-fail path) so `scan`/`status`/another machine see it.
1642
+ * capturing the agent's work, then post lock-cutover a LOCK AMEND to
1643
+ * `state: stuck` with the failure detail as the reason on the lock entry, in
1644
+ * place of the retired `git mv → needs-attention/` move-only commit), surfaced on
1645
+ * the arbiter's `main` (the autonomous, cross-machine-visible mode-M surfacing
1646
+ * `do` already uses for the gate-fail path) so `scan`/`status`/another machine
1647
+ * see it.
1410
1648
  *
1411
1649
  * It ALSO pushes the `work/<slug>` branch to the arbiter so the saved partial
1412
1650
  * commits travel cross-machine and the item is RECOVERABLE via `requeue`
@@ -1429,7 +1667,8 @@ function routeReport(
1429
1667
  * contract stays coherent). Only the WORK-PRESERVING side-effect (unchanged here)
1430
1668
  * matches the gate-failure path. We do NOT validate or "fix" the partial work — a broken
1431
1669
  * tree committed + surfaced (with the reason) is recoverable; the human chooses
1432
- * `requeue` (continue) vs `requeue --reset` (discard).
1670
+ * `requeue` (continue) / `requeue --reconcile` (non-destructive retry) vs
1671
+ * `requeue --reset` (destructive last resort).
1433
1672
  */
1434
1673
  async function saveAgentFailure(params: {
1435
1674
  slug: string;
@@ -1477,8 +1716,10 @@ async function saveAgentFailure(params: {
1477
1716
  : `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
1478
1717
  `mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
1479
1718
  note(message);
1719
+ // PR-2b D3: a cleanly-surfaced bounce is GREEN (exitCode 0); a FAILED surface
1720
+ // stays non-zero.
1480
1721
  return {
1481
- exitCode: 1,
1722
+ exitCode: routed.moved ? 0 : 1,
1482
1723
  outcome: failureCauseToDoOutcome(cause),
1483
1724
  slug,
1484
1725
  branch,
@@ -1499,7 +1740,9 @@ function failureCauseToDoOutcome(cause: FailureCause): DoOutcome {
1499
1740
 
1500
1741
  /**
1501
1742
  * Build the HONEST result for a CONTINUE-site surface that did NOT land on the
1502
- * arbiter (`{moved: false}`). The tree-less `in-progress/ needs-attention/` move
1743
+ * arbiter (`{moved: false}`). The tree-less stuck-lock amend (post lock-cutover
1744
+ * `state: stuck` with the reason, in place of the retired `in-progress/ →
1745
+ * needs-attention/` move)
1503
1746
  * lost the CAS race against a busy arbiter (its contention-retry cap exhausted) or
1504
1747
  * had no arbiter to publish to, so the item is STILL in-progress on the arbiter —
1505
1748
  * a clean `needs-attention` would mislead (it claims the surface landed). Distinct
@@ -1544,14 +1787,14 @@ async function resolveStopReason(params: {
1544
1787
  cwd: string;
1545
1788
  arbiter: string;
1546
1789
  env: NodeJS.ProcessEnv | undefined;
1547
- }): Promise<string | undefined> {
1790
+ }): Promise<AgentStopClass | undefined> {
1548
1791
  const {output, slug, cwd, arbiter, env} = params;
1549
1792
  const sentinel = parseStopSentinel(output);
1550
1793
  if (sentinel !== undefined) {
1551
- return sentinel.reason;
1794
+ return {kind: 'sentinel', reason: sentinel.reason};
1552
1795
  }
1553
1796
  if (await isWorkBranchDiffEmpty({cwd, arbiter, env})) {
1554
- return emptyDiffStopReason(slug);
1797
+ return {kind: 'empty-diff', reason: emptyDiffStopReason(slug)};
1555
1798
  }
1556
1799
  return undefined;
1557
1800
  }
@@ -1570,12 +1813,66 @@ async function saveAgentStop(params: {
1570
1813
  cwd: string;
1571
1814
  arbiter: string;
1572
1815
  reason: string;
1816
+ kind: 'sentinel' | 'empty-diff';
1573
1817
  env: NodeJS.ProcessEnv | undefined;
1574
1818
  note: (message: string) => void;
1575
1819
  }): Promise<DoResult> {
1576
- const {slug, cwd, arbiter, reason, env, note} = params;
1820
+ const {slug, cwd, arbiter, reason, kind, env, note} = params;
1577
1821
  const branch = params.branch ?? workBranchRef('task', slug);
1578
1822
 
1823
+ // EMPTY-DIFF branch (spec resolved decision #2, task
1824
+ // `empty-diff-bounce-surfaces-dispose-defaulted-question`): surface a sidecar
1825
+ // on `<arbiter>/main` carrying an engine-authored DISPOSE-DEFAULTED envelope
1826
+ // question, THEN release the lock. "Nothing to do" is a non-deterministic LLM
1827
+ // judgement, so this seam MUST NOT blindly requeue (that would infinite-loop);
1828
+ // the surfaced sidecar + `needsAnswers:true` on `main` parks the item as a
1829
+ // human-visible question a `dispose` answer settles to `tasks/cancelled/`.
1830
+ if (kind === 'empty-diff') {
1831
+ const item = `task:${slug}`;
1832
+ const itemPath = resolveItemPathByIdentity(cwd, item);
1833
+ if (itemPath === undefined) {
1834
+ const message =
1835
+ `The agent STOPPED building '${slug}' (empty diff) but its body could ` +
1836
+ `not be located on the arbiter to surface a sidecar. Reason: ${reason}`;
1837
+ note(message);
1838
+ return {
1839
+ exitCode: 1,
1840
+ outcome: 'agent-stopped',
1841
+ slug,
1842
+ branch,
1843
+ routedToNeedsAttention: false,
1844
+ message,
1845
+ };
1846
+ }
1847
+ const surfaced = await surfaceStuckToNeedsAttention({
1848
+ cwd,
1849
+ slug,
1850
+ item,
1851
+ itemPath,
1852
+ reason,
1853
+ envelope: emptyDiffDisposeEnvelope({item, reason}),
1854
+ arbiter,
1855
+ env,
1856
+ note,
1857
+ });
1858
+ const message = surfaced.surfaced
1859
+ ? `The agent STOPPED building '${slug}' (empty diff); surfaced a ` +
1860
+ `dispose-defaulted question on ${arbiter}/main and released the lock. ` +
1861
+ `Reason: ${reason}`
1862
+ : `The agent STOPPED building '${slug}' (empty diff) but the surface ` +
1863
+ `did not land (${surfaced.reasonNotSurfaced ?? 'unknown'}). ` +
1864
+ `Reason: ${reason}`;
1865
+ note(message);
1866
+ return {
1867
+ exitCode: 1,
1868
+ outcome: 'agent-stopped',
1869
+ slug,
1870
+ branch,
1871
+ routedToNeedsAttention: surfaced.surfaced,
1872
+ message,
1873
+ };
1874
+ }
1875
+
1579
1876
  const routed = await ledgerWrite.applyNeedsAttentionTransition({
1580
1877
  cwd,
1581
1878
  slug,
@@ -1593,8 +1890,11 @@ async function saveAgentStop(params: {
1593
1890
  : `The agent STOPPED building '${slug}' but it could not be marked stuck ` +
1594
1891
  `(${routed.reasonNotMoved ?? 'unknown'}). Reason: ${reason}`;
1595
1892
  note(message);
1893
+ // PR-2b D3: agent-stopped (sentinel) on a clean surface is GREEN (exitCode 0);
1894
+ // a FAILED surface stays non-zero. The empty-diff branch above is UNCHANGED
1895
+ // (owned by `empty-diff-bounce-surfaces-dispose-defaulted-question`).
1596
1896
  return {
1597
- exitCode: 1,
1897
+ exitCode: routed.moved ? 0 : 1,
1598
1898
  outcome: 'agent-stopped',
1599
1899
  slug,
1600
1900
  branch,
@@ -1626,11 +1926,26 @@ async function runDoAgent(
1626
1926
  cwd: string,
1627
1927
  prompt: string,
1628
1928
  slug: string,
1629
- ): Promise<{ok: boolean; detail?: string; output?: string}> {
1929
+ ): Promise<{
1930
+ ok: boolean;
1931
+ detail?: string;
1932
+ output?: string;
1933
+ timedOut?: boolean;
1934
+ }> {
1630
1935
  if (options.dorfl) {
1631
1936
  return options.dorfl({cwd, prompt, slug, env: options.env});
1632
1937
  }
1633
1938
  const harness = options.harness ?? new NullHarness();
1939
+ // Convert the dorfl-internal deadline (minutes) into a wall-clock epoch-ms so
1940
+ // the harness (`launchAsync`) can race the child against it (spec
1941
+ // `graceful-pre-timeout-wip-checkpoint`). Absent ⇒ no deadline; a run that
1942
+ // finishes before it is byte-for-byte unchanged (the harness clears the
1943
+ // timer on normal exit). Forcing the async path when a deadline is set is
1944
+ // {@link launchWithOptionalWatch}'s job.
1945
+ const deadlineMs =
1946
+ options.agentDeadlineMinutes !== undefined
1947
+ ? Date.now() + options.agentDeadlineMinutes * 60_000
1948
+ : undefined;
1634
1949
  const launched = await launchWithOptionalWatch({
1635
1950
  harness,
1636
1951
  dir: cwd,
@@ -1642,6 +1957,7 @@ async function runDoAgent(
1642
1957
  sessionId: slug,
1643
1958
  sessionsDir: options.sessionsDir,
1644
1959
  watch: options.watch,
1960
+ deadlineMs,
1645
1961
  watchSink: options.watchSink,
1646
1962
  color: options.color,
1647
1963
  env: options.env,
@@ -1649,7 +1965,134 @@ async function runDoAgent(
1649
1965
  // Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
1650
1966
  // for the propose-mode PR body — instead of dropping it. Absent (no parseable
1651
1967
  // assistant text) ⇒ undefined ⇒ the body degrades to `--fill` (no regression).
1652
- return {ok: launched.ok, detail: launched.detail, output: launched.output};
1968
+ return {
1969
+ ok: launched.ok,
1970
+ detail: launched.detail,
1971
+ output: launched.output,
1972
+ timedOut: launched.timedOut,
1973
+ };
1974
+ }
1975
+
1976
+ /**
1977
+ * Route a DEADLINE STOP (spec `graceful-pre-timeout-wip-checkpoint`) through
1978
+ * the checkpoint machinery: ALWAYS save the WIP (commit + push), then
1979
+ * decide — progress AND under ceiling ⇒ AUTO-CONTINUE (release lock, keep
1980
+ * branch, NO sidecar, exit 0); else ⇒ SURFACE a `needsAnswers:true` question.
1981
+ * Shared by {@link performDo} and {@link runRemotePipeline} so the in-place
1982
+ * and remote forms route a deadline stop identically.
1983
+ */
1984
+ async function routeDeadlineCheckpoint(params: {
1985
+ slug: string;
1986
+ branch: string;
1987
+ cwd: string;
1988
+ arbiter: string;
1989
+ maxAutoCheckpoints: number;
1990
+ env: NodeJS.ProcessEnv | undefined;
1991
+ note: (message: string) => void;
1992
+ }): Promise<DoResult> {
1993
+ const {slug, branch, cwd, arbiter, maxAutoCheckpoints, env, note} = params;
1994
+
1995
+ // 1. ALWAYS save the WIP first: commit any residue + push the work branch.
1996
+ const savedReason = `deadline checkpoint save for '${slug}'`;
1997
+ const saved = await saveDeadlineCheckpoint({
1998
+ slug,
1999
+ branch,
2000
+ cwd,
2001
+ arbiter,
2002
+ reason: savedReason,
2003
+ env,
2004
+ note,
2005
+ });
2006
+ void saved;
2007
+
2008
+ // 2. Progress this session? Reuse the SAME primitive the STOP backstop uses.
2009
+ const emptyDiff = await isWorkBranchDiffEmpty({cwd, arbiter, env});
2010
+ const madeProgressThisSession = !emptyDiff;
2011
+
2012
+ // 3. Count consecutive deadline checkpoints on the branch (ceiling gate). The
2013
+ // marker commit we just saved is included — so the count is the number
2014
+ // of checkpoints INCLUDING this one.
2015
+ const checkpointCount = await countDeadlineCheckpointsOnBranch({
2016
+ cwd,
2017
+ arbiter,
2018
+ branch,
2019
+ env,
2020
+ });
2021
+
2022
+ if (madeProgressThisSession && checkpointCount <= maxAutoCheckpoints) {
2023
+ // AUTO-CONTINUE: release the lock via the SAME default keep+continue path
2024
+ // `requeue` uses (no --reset, no --reconcile, no sidecar). The branch is
2025
+ // KEPT on the arbiter so the next claim continues from its tip.
2026
+ const reason = deadlineAutoContinueReason({
2027
+ slug,
2028
+ count: checkpointCount,
2029
+ max: maxAutoCheckpoints,
2030
+ });
2031
+ const returned = await ledgerWrite.applyReturnToBacklogTransition({
2032
+ cwd,
2033
+ slug,
2034
+ arbiter,
2035
+ env,
2036
+ note,
2037
+ });
2038
+ if (returned.moved) {
2039
+ const message =
2040
+ `Auto-continued '${slug}' at the dorfl-internal deadline (checkpoint ` +
2041
+ `${checkpointCount}/${maxAutoCheckpoints}): WIP saved + branch pushed, ` +
2042
+ `lock released so the next tick continues from ${branch}. ${reason}`;
2043
+ note(message);
2044
+ return {
2045
+ exitCode: 0,
2046
+ outcome: 'deadline-auto-continued',
2047
+ slug,
2048
+ branch,
2049
+ message,
2050
+ };
2051
+ }
2052
+ // The auto-continue lock release did not land — fall through to SURFACE.
2053
+ note(
2054
+ `Auto-continue lock release for '${slug}' did not land ` +
2055
+ `(${returned.reasonNotMoved ?? 'unknown'}); surfacing instead.`,
2056
+ );
2057
+ }
2058
+
2059
+ // SURFACE: mark the lock stuck via the whole applyNeedsAttentionTransition
2060
+ // (save + push + stuck). The WIP was already saved above; a second save is
2061
+ // idempotent (empty commit is skipped inside routeToNeedsAttention).
2062
+ const surfaceReason = madeProgressThisSession
2063
+ ? deadlineSurfaceReason({
2064
+ slug,
2065
+ kind: 'ceiling',
2066
+ count: checkpointCount,
2067
+ max: maxAutoCheckpoints,
2068
+ })
2069
+ : deadlineSurfaceReason({slug, kind: 'no-progress'});
2070
+ const routed = await ledgerWrite.applyNeedsAttentionTransition({
2071
+ cwd,
2072
+ slug,
2073
+ reason: surfaceReason,
2074
+ arbiter,
2075
+ env,
2076
+ note,
2077
+ });
2078
+ const report = routed.moved ? routeReport(routed, branch) : undefined;
2079
+ const message = routed.moved
2080
+ ? `Surfaced '${slug}' at the deadline checkpoint (${
2081
+ madeProgressThisSession
2082
+ ? `ceiling ${checkpointCount}/${maxAutoCheckpoints}`
2083
+ : 'no progress this session'
2084
+ }); ${report!.fragment}. ${surfaceReason}`
2085
+ : `Could not surface '${slug}' at the deadline checkpoint ` +
2086
+ `(${routed.reasonNotMoved ?? 'unknown'}). ${surfaceReason}`;
2087
+ note(message);
2088
+ return {
2089
+ exitCode: 1,
2090
+ outcome: 'deadline-surfaced',
2091
+ slug,
2092
+ branch,
2093
+ routedToNeedsAttention: routed.moved,
2094
+ message,
2095
+ };
1653
2096
  }
1654
2097
 
1655
2098
  /**
@@ -1922,6 +2365,10 @@ export async function performDoRemote(
1922
2365
  // `taskingIntegration ?? integration`, so the `--remote spec:` output ALSO
1923
2366
  // routes through the shared core with the tasking-resolved mode.
1924
2367
  integration: options.taskingIntegration ?? options.integration,
2368
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
2369
+ // threaded on the no-checkout `do --remote spec:` tasking path too,
2370
+ // mirroring the in-place site above.
2371
+ mergeRetries: options.mergeRetries,
1925
2372
  // The per-repo TASK-PLACEMENT default + the operator's explicit
1926
2373
  // override (task `runner-deterministic-slice-placement-policy-and-
1927
2374
  // precedence`). Same threading as the in-place `do spec:` path.
@@ -2094,8 +2541,10 @@ async function runRemotePipeline(
2094
2541
  if (tree.continueRebaseConflict) {
2095
2542
  const reason =
2096
2543
  `continuing the kept ${tree.branch}: rebase onto the latest main ` +
2097
- 'conflicted (aborted, never auto-resolved) — resolve against the latest ' +
2098
- 'main, or `requeue --reset` to discard and start fresh';
2544
+ 'conflicted (aborted, never auto-resolved) — run `requeue --reconcile` ' +
2545
+ 'to non-destructively re-sync the mirror and retry the rebase (keeps ' +
2546
+ 'the work). Last resort: `requeue --reset` DESTRUCTIVELY discards the ' +
2547
+ 'branch and starts fresh.';
2099
2548
  await ledgerWrite.applyNeedsAttentionTransition({
2100
2549
  cwd,
2101
2550
  slug,
@@ -2127,8 +2576,10 @@ async function runRemotePipeline(
2127
2576
  const reason =
2128
2577
  `continuing the kept ${tree.branch}: publishing the rebased work branch ` +
2129
2578
  `to the arbiter failed terminally (${tree.continuePushFailure}) — the kept ` +
2130
- 'branch is left intact on the arbiter (recoverable); `requeue` to retry ' +
2131
- 'once the churn settles, or `requeue --reset` to discard and start fresh';
2579
+ 'branch is left intact on the arbiter (recoverable); `requeue --reconcile` ' +
2580
+ 'to non-destructively re-sync the mirror and retry once the churn ' +
2581
+ 'settles (keeps the work). Last resort: `requeue --reset` DESTRUCTIVELY ' +
2582
+ 'discards the branch and starts fresh.';
2132
2583
  await ledgerWrite.applyNeedsAttentionTransition({
2133
2584
  cwd,
2134
2585
  slug,
@@ -2265,7 +2716,12 @@ async function runRemotePipeline(
2265
2716
  throw err;
2266
2717
  }
2267
2718
 
2268
- let agent: {ok: boolean; detail?: string; output?: string};
2719
+ let agent: {
2720
+ ok: boolean;
2721
+ detail?: string;
2722
+ output?: string;
2723
+ timedOut?: boolean;
2724
+ };
2269
2725
  try {
2270
2726
  agent = await runDoAgent(options, cwd, prompt, slug);
2271
2727
  } catch (err) {
@@ -2280,6 +2736,19 @@ async function runRemotePipeline(
2280
2736
  note,
2281
2737
  });
2282
2738
  }
2739
+ // Deadline checkpoint on the no-checkout `do --remote` path — same routing
2740
+ // as the in-place path (see performDo).
2741
+ if (agent.timedOut) {
2742
+ return await routeDeadlineCheckpoint({
2743
+ slug,
2744
+ branch: branch ?? workBranchRef('task', slug),
2745
+ cwd,
2746
+ arbiter: arbiterRemote,
2747
+ maxAutoCheckpoints: options.maxAutoCheckpoints ?? 5,
2748
+ env,
2749
+ note,
2750
+ });
2751
+ }
2283
2752
  if (!agent.ok) {
2284
2753
  const detail = agent.detail ?? `the agent failed to build '${slug}'.`;
2285
2754
  return await saveRemoteAgentFailure({
@@ -2311,7 +2780,8 @@ async function runRemotePipeline(
2311
2780
  branch,
2312
2781
  cwd,
2313
2782
  arbiter: arbiterRemote,
2314
- reason: stopReason,
2783
+ reason: stopReason.reason,
2784
+ kind: stopReason.kind,
2315
2785
  env,
2316
2786
  note,
2317
2787
  });
@@ -2500,8 +2970,9 @@ async function saveRemoteAgentFailure(params: {
2500
2970
  : `Agent run failed building '${slug}' [${cause}] (${detail}); could not ` +
2501
2971
  `mark '${slug}' stuck (${routed.reasonNotMoved ?? 'unknown'}).`;
2502
2972
  note(message);
2973
+ // PR-2b D3: cleanly-surfaced remote agent failure is GREEN.
2503
2974
  return {
2504
- exitCode: 1,
2975
+ exitCode: routed.moved ? 0 : 1,
2505
2976
  outcome: failureCauseToDoOutcome(cause),
2506
2977
  slug,
2507
2978
  branch,
@@ -2613,6 +3084,8 @@ export function jobWorktreeDoDriver(closure: {
2613
3084
  watch: options.watch,
2614
3085
  watchSink: options.watchSink,
2615
3086
  color: options.color,
3087
+ agentDeadlineMinutes: options.agentDeadlineMinutes,
3088
+ maxAutoCheckpoints: options.maxAutoCheckpoints,
2616
3089
  read: options.read,
2617
3090
  env: options.env,
2618
3091
  note: options.note,