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/env-config.ts CHANGED
@@ -100,12 +100,12 @@ const KEY_COERCIONS: {[K in keyof Config]?: Coercion} = {
100
100
  taskingIntegration: {enum: ['propose', 'merge']},
101
101
  // `tasksLandIn` (the per-repo TASK-PLACEMENT default — spec
102
102
  // `staging-pool-position-gate-and-trust-model` US #5) coerces as the
103
- // `pre-backlog`/`ready` enum, so `DORFL_TASKS_LAND_IN=ready` works and a
103
+ // `backlog`/`ready` enum, so `DORFL_TASKS_LAND_IN=ready` works and a
104
104
  // typo FAILS LOUDLY. Same
105
105
  // precedence chain as `taskingIntegration` (flag > env > per-repo > global >
106
- // built-in `pre-backlog`); fed into the runner-deterministic placement
106
+ // built-in `backlog`); fed into the runner-deterministic placement
107
107
  // resolver (`src/placement.ts`) as the configured-default rung.
108
- tasksLandIn: {enum: ['pre-backlog', 'ready']},
108
+ tasksLandIn: {enum: ['backlog', 'ready']},
109
109
  // `specsLandIn` (the per-repo SPEC-PLACEMENT default — spec
110
110
  // `staging-pool-position-gate-and-trust-model` US #2/#5) coerces as the
111
111
  // `pre-proposed`/`ready` enum, so `DORFL_SPECS_LAND_IN=ready` works and a typo
@@ -113,7 +113,7 @@ const KEY_COERCIONS: {[K in keyof Config]?: Coercion} = {
113
113
  // > global > built-in `pre-proposed`); fed into the shared placement resolver
114
114
  // (`src/placement.ts`) as the configured-default rung for the spec lifecycle.
115
115
  // The legacy `prdsLandIn` key / `DORFL_PRDS_LAND_IN` env are GONE after the
116
- // `prd` → `spec` hard cutover (clean break).
116
+ // ''prd'' → `spec` hard cutover (clean break).
117
117
  specsLandIn: {enum: ['pre-proposed', 'ready']},
118
118
  // `noPR` (the PR-INTENT axis) is a BOOLEAN coercion (like `review`), so
119
119
  // `DORFL_NO_PR=true|false` works and a typo FAILS LOUDLY. The removed
@@ -152,6 +152,14 @@ const KEY_COERCIONS: {[K in keyof Config]?: Coercion} = {
152
152
  // works and a typo FAILS LOUDLY. Default OFF; ON re-surfaces the
153
153
  // merge-question on a merge-base change instead of auto-landing.
154
154
  strictMergeApproval: 'boolean',
155
+ // The dorfl-INTERNAL agent deadline (minutes) — spec
156
+ // `graceful-pre-timeout-wip-checkpoint`. NUMBER coercion (like
157
+ // `reviewMaxRounds`); range validation is fail-loud in `validateDeadlineConfig`
158
+ // (not clamped here). Env is a per-machine source, so CI can override the
159
+ // per-repo default without a config edit: `DORFL_AGENT_DEADLINE_MINUTES=90`.
160
+ agentDeadlineMinutes: 'number',
161
+ checkpointHeadroomMinutes: 'number',
162
+ maxAutoCheckpoints: 'number',
155
163
  // `promptGuidance` is a STRUCTURED (nested) namespace, so it has no scalar env
156
164
  // var of its own — each MEMBER carries its own env var (the nested-key form
157
165
  // `DORFL_PROMPT_GUIDANCE_<MEMBER>`), handled out-of-band in
@@ -2,7 +2,8 @@
2
2
  * **The failure-CAUSE classifier** (task `failure-cause-classification-model-vs-
3
3
  * git-vs-agent`).
4
4
  *
5
- * When a claimed item routes to `work/needs-attention/`, the runner records WHY.
5
+ * When a claimed item is marked stuck on its per-item lock (post lock-cutover
6
+ * `state: stuck`, no `work/needs-attention/` folder write), the runner records WHY.
6
7
  * Historically several DIFFERENT causes collapsed into one undifferentiated label
7
8
  * ("agent failed"), even though the CAUSE drives the RECOVERY:
8
9
  *
@@ -49,6 +50,7 @@
49
50
  */
50
51
  export type FailureCause =
51
52
  | 'transient-infra' // a harness-surfaced model/connection outage (post-retry), or a git/provider outage — RETRY the same work
53
+ | 'needs-reauth' // a credential expired / was revoked (e.g. OAuth refresh token) — RETRY CANNOT help; a human must RE-AUTH
52
54
  | 'config-error' // a thrown CORE wiring/config error (e.g. review on, no reviewGate) — fix the WIRING
53
55
  | 'agent-failed'; // the conservative generic: the agent ran but produced bad/empty output, OR the cause is UNKNOWN
54
56
 
@@ -65,6 +67,24 @@ const CONFIG_ERROR_SIGNATURES = [
65
67
  /no review gate is configured/i,
66
68
  ];
67
69
 
70
+ /**
71
+ * Substrings that mark a CREDENTIAL-EXPIRY failure surfaced by the harness: an
72
+ * OAuth refresh token has expired or been revoked, or a 401 came back tagged
73
+ * `authentication_required`. Retry cannot help these — only a human RE-AUTH can.
74
+ * Kept SEPARATE from `TRANSIENT_INFRA_SIGNATURES` (see ADR
75
+ * `transient-infra-and-needs-reauth-routing`) so downstream routing can branch
76
+ * cleanly on the cause: retry-with-backoff for infra, straight to a needs-reauth
77
+ * surface for credentials. Conservative: unrelated 401s that don't mention auth /
78
+ * token stay `agent-failed`.
79
+ */
80
+ const NEEDS_REAUTH_SIGNATURES = [
81
+ /authentication_required/i,
82
+ /OAuth (?:refresh )?token (?:expired|revoked|invalid)/i,
83
+ /refresh token (?:expired|revoked|invalid)/i,
84
+ /\b401\b[^\n]{0,80}(?:auth|token|credential|unauthori[sz]ed)/i,
85
+ /(?:auth|token|credential)[^\n]{0,40}\b401\b/i,
86
+ ];
87
+
68
88
  /**
69
89
  * Substrings that mark a TRANSIENT-INFRA failure: a model endpoint / connection
70
90
  * outage the harness surfaced AFTER exhausting its own retries, or a git/provider
@@ -117,6 +137,13 @@ export function classifyFailureCause(detail: string | undefined): FailureCause {
117
137
  if (CONFIG_ERROR_SIGNATURES.some((re) => re.test(text))) {
118
138
  return 'config-error';
119
139
  }
140
+ // Credential-expiry is checked BEFORE `transient-infra` because a 401 body may
141
+ // also mention words like "unavailable"/network, and no amount of retry helps
142
+ // — a human must re-auth. Kept as its own cause per ADR
143
+ // `transient-infra-and-needs-reauth-routing`.
144
+ if (NEEDS_REAUTH_SIGNATURES.some((re) => re.test(text))) {
145
+ return 'needs-reauth';
146
+ }
120
147
  if (TRANSIENT_INFRA_SIGNATURES.some((re) => re.test(text))) {
121
148
  return 'transient-infra';
122
149
  }
@@ -134,6 +161,8 @@ export function failureCauseLabel(cause: FailureCause): string {
134
161
  switch (cause) {
135
162
  case 'transient-infra':
136
163
  return 'transient infra';
164
+ case 'needs-reauth':
165
+ return 'needs re-auth (credential expired)';
137
166
  case 'config-error':
138
167
  return 'config error';
139
168
  case 'agent-failed':
package/src/format.ts CHANGED
@@ -117,40 +117,20 @@ function cwdDivergenceLine(
117
117
  * consistency rule): the two reads have different freshness + storage models.
118
118
  */
119
119
  /**
120
- * Render ONE held per-item lock entry (action × state + reason/questions) as
121
- * indented lines, the SHARED renderer for the in-flight lock surface on BOTH the
122
- * registry view (`formatStatus`) and the cwd section (`formatCwdSection`), so the
123
- * two substrates present a held/stuck item identically (NOT a forked renderer). An
124
- * `active` hold reads as `in-progress`; a `stuck` hold as `needs-attention` and
125
- * carries its (possibly multi-line) reason + any surfaced questions.
120
+ * Render ONE held per-item lock entry as an indented line, the SHARED renderer
121
+ * for the in-flight lock surface on BOTH the registry view (`formatStatus`) and
122
+ * the cwd section (`formatCwdSection`). Post-`retire-stuck-lock-state` the
123
+ * lock state is degenerate (`active` only) a held lock is always an
124
+ * in-progress hold; a parked item is a `needsAnswers:true` pool item on `main`
125
+ * with a sidecar, rendered by the pool categoriser (NOT here).
126
126
  */
127
127
  export function formatLockEntryLines(
128
128
  entry: LockEntry,
129
129
  indent: string,
130
130
  ): string[] {
131
- const lines: string[] = [];
132
- const view = entry.state === 'stuck' ? 'needs-attention' : 'in-progress';
133
- lines.push(
134
- `${indent}${entry.entry} (${entry.action}/${entry.state} = ${view})`,
135
- );
136
- if (entry.state === 'stuck') {
137
- const reason =
138
- entry.reason && entry.reason !== ''
139
- ? entry.reason
140
- : '(no reason recorded)';
141
- const reasonLines = reason.split('\n');
142
- lines.push(`${indent} reason: ${reasonLines[0]}`);
143
- for (const extra of reasonLines.slice(1)) {
144
- lines.push(`${indent} ${extra}`);
145
- }
146
- if (entry.questions && entry.questions.length > 0) {
147
- lines.push(`${indent} questions:`);
148
- for (const q of entry.questions) {
149
- lines.push(`${indent} - ${q}`);
150
- }
151
- }
152
- }
153
- return lines;
131
+ return [
132
+ `${indent}${entry.entry} (${entry.action}/${entry.state} = in-progress)`,
133
+ ];
154
134
  }
155
135
 
156
136
  export function formatCwdSection(section: CwdSection): string[] {
@@ -214,7 +194,7 @@ export function formatCwdSection(section: CwdSection): string[] {
214
194
  const lockHeld = section.repo.lockHeld ?? [];
215
195
  if (lockHeld.length > 0) {
216
196
  lines.push('');
217
- lines.push(' In progress / stuck (lock held — refs/dorfl/lock/*):');
197
+ lines.push(' In progress (lock held — refs/dorfl/lock/*):');
218
198
  for (const entry of lockHeld) {
219
199
  lines.push(...formatLockEntryLines(entry, ' '));
220
200
  }
@@ -7,8 +7,8 @@
7
7
 
8
8
  /**
9
9
  * **Origin-trust PROVENANCE** (task `untrusted-origin-forces-build-propose`).
10
- * How a prd/task was BORN and the AUTHOR-TRUST verdict at birth, stamped so the
11
- * signal SURVIVES the prd/task merge boundary (a landed-on-`main` artifact would
10
+ * How a spec/task was BORN and the AUTHOR-TRUST verdict at birth, stamped so the
11
+ * signal SURVIVES the spec/task merge boundary (a landed-on-`main` artifact would
12
12
  * otherwise erase how it was created — the laundering gap).
13
13
  *
14
14
  * - `origin` — `human` (the default / unset: a human authored it locally; a local
@@ -48,8 +48,8 @@ export interface Frontmatter {
48
48
  * spec lives at `work/specs/ready/<spec>.md`. The `spec` vocabulary name (spec
49
49
  * `prd-to-spec-vocabulary-cutover-and-migration-command`). `parseFrontmatter`
50
50
  * populates it from the canonical `spec:` key ONLY — the HARD CUTOVER (the last
51
- * `prd` back-compat surface removed): the legacy `prd:` KEY read is GONE, so an
52
- * un-migrated `prd:` field no longer silently resolves. A downstream repo
51
+ * ''prd'' back-compat surface removed): the legacy ''prd:'' KEY read is GONE, so
52
+ * an un-migrated ''prd:'' field no longer silently resolves. A downstream repo
53
53
  * migrates its data with `dorfl prd-to-spec` (a purely TEXTUAL `prd: → spec:`
54
54
  * rewrite that does NOT go through this parser), after which the field reads
55
55
  * canonically. There is no `fm.prd` FIELD.
@@ -292,6 +292,36 @@ export function setFrontmatterMarker(
292
292
  return lines.join('\n');
293
293
  }
294
294
 
295
+ /**
296
+ * Read ONE arbitrary top-level scalar frontmatter key by name, returning its
297
+ * unquoted value (or `undefined` when the doc has no fence, or the key is absent).
298
+ *
299
+ * A general-purpose companion to {@link parseFrontmatter} for keys that are NOT in
300
+ * the modelled {@link Frontmatter} shape — e.g. the observation→task provenance
301
+ * back-reference `promotedFrom:` the triage-promote path stamps and the create-CAS
302
+ * lost-race disambiguation reads (task
303
+ * `observation-triage-already-triaged-benign-skip`). Only top-level `key: value`
304
+ * lines are matched (block-list / nested values are out of scope for a scalar read).
305
+ */
306
+ export function readFrontmatterField(
307
+ content: string,
308
+ key: string,
309
+ ): string | undefined {
310
+ const block = extractBlock(content);
311
+ if (block === undefined) {
312
+ return undefined;
313
+ }
314
+ const pattern = new RegExp(`^${key}\\s*:\\s*(.*)$`);
315
+ for (const line of block.split('\n')) {
316
+ const match = pattern.exec(line);
317
+ if (match) {
318
+ const value = unquote(match[1]);
319
+ return value === '' ? undefined : value;
320
+ }
321
+ }
322
+ return undefined;
323
+ }
324
+
295
325
  /**
296
326
  * PROPAGATE the origin-trust PROVENANCE (`origin` + `originTrust`) from a SOURCE
297
327
  * artifact (a spec) onto a TARGET artifact's content (an emitted task), returning
@@ -374,7 +404,7 @@ export function parseFrontmatter(content: string): Frontmatter {
374
404
  } else if (key === 'spec') {
375
405
  // The parent-spec pointer, read from the canonical `spec:` key ONLY into the
376
406
  // `fm.spec` field (spec `prd-to-spec-vocabulary-cutover-and-migration-command`,
377
- // HARD CUTOVER). The legacy `prd:` KEY read is GONE — an un-migrated `prd:`
407
+ // HARD CUTOVER). The legacy ''prd:'' KEY read is GONE — an un-migrated ''prd:''
378
408
  // field no longer resolves here; a repo converts its data via the TEXTUAL
379
409
  // `dorfl prd-to-spec` rewrite (which does not use this parser). An empty value
380
410
  // leaves the field undefined.
package/src/gc.ts CHANGED
@@ -8,7 +8,9 @@ import {
8
8
  removeJobRecord,
9
9
  type JobRecord,
10
10
  } from './workspace.js';
11
- import {workBranchRef} from './slug-namespace.js';
11
+ import {parseWorkBranchRef, workBranchRef} from './slug-namespace.js';
12
+ import {terminalMainPaths} from './item-lock.js';
13
+ import type {SidecarType} from './sidecar.js';
12
14
 
13
15
  /**
14
16
  * The **reaper** for job worktrees, governed by the **provably-safe deletion
@@ -140,8 +142,23 @@ export function evaluateDeletionSafety(
140
142
 
141
143
  const localTip = revParse(input.dir, 'HEAD', env);
142
144
 
143
- // 2a. Merged: the tip is an ancestor of <arbiter>/main.
144
- if (isAncestor(input.dir, localTip, `refs/remotes/${arbiter}/main`, env)) {
145
+ // 2a. Merged: the tip is provably on <arbiter>/main — either a plain
146
+ // ancestor (fast path) OR squash-aware-equivalent (the durable done/
147
+ // dropped record is on main AND the branch carries nothing main lacks).
148
+ const arbiterMainRef = `refs/remotes/${arbiter}/main`;
149
+ const reapIdentity = reapIdentityFromBranch(input.branch);
150
+ if (
151
+ reapIdentity !== undefined
152
+ ? isProvablyMergedForReap({
153
+ cwd: input.dir,
154
+ tip: localTip,
155
+ arbiterMain: arbiterMainRef,
156
+ namespace: reapIdentity.namespace,
157
+ slug: reapIdentity.slug,
158
+ env,
159
+ })
160
+ : isAncestor(input.dir, localTip, arbiterMainRef, env)
161
+ ) {
145
162
  return {safe: true, reachableVia: 'merged'};
146
163
  }
147
164
 
@@ -432,14 +449,160 @@ function fetchTracking(
432
449
  );
433
450
  }
434
451
 
452
+ /**
453
+ * Extract the `{namespace, slug}` identity a reap decision needs from a work
454
+ * branch ref (`work/[<producer>-]<type>-<slug>`). Returns `undefined` for a
455
+ * non-namespaced branch — the caller then falls back to the ancestry-only
456
+ * fast-path (the squash-aware fallback has no anchor without an identity to
457
+ * look up on `<arbiter>/main`, so the safe direction is the strict old rule).
458
+ */
459
+ function reapIdentityFromBranch(
460
+ branch: string,
461
+ ): {namespace: SidecarType; slug: string} | undefined {
462
+ const parsed = parseWorkBranchRef(branch);
463
+ if (parsed === undefined) {
464
+ return undefined;
465
+ }
466
+ // `SlugNamespace` and `SidecarType` share the same three members
467
+ // (`'task' | 'spec' | 'observation'`); the cast is structural.
468
+ return {namespace: parsed.namespace as SidecarType, slug: parsed.slug};
469
+ }
470
+
471
+ /** Input for {@link isProvablyMergedForReap}. */
472
+ export interface ProvablyMergedForReapInput {
473
+ /** The local repo the git predicates run in. */
474
+ cwd: string;
475
+ /** The candidate branch tip (a 40-hex sha or a resolvable ref). */
476
+ tip: string;
477
+ /**
478
+ * The arbiter's main as a ref RESOLVABLE INSIDE `cwd` — a fetched
479
+ * remote-tracking ref (`refs/remotes/<arbiter>/main`) or short-form
480
+ * (`<arbiter>/main`). Callers are responsible for the fetch (best-effort;
481
+ * an unresolvable ref reads as not-provable ⇒ false).
482
+ */
483
+ arbiterMain: string;
484
+ /**
485
+ * The item's type (task/spec/observation) — used to look up the durable
486
+ * terminal records on `<arbiter>/main`. Reap decisions on a
487
+ * `work/<type>-<slug>` branch pass the parsed type from
488
+ * {@link parseWorkBranchRef}.
489
+ */
490
+ namespace: SidecarType;
491
+ /** The item's bare slug — the anchor for the squash-aware fallback. */
492
+ slug: string;
493
+ env?: NodeJS.ProcessEnv;
494
+ }
495
+
496
+ /**
497
+ * The **squash-aware, provably-merged predicate** used to decide reap-safety
498
+ * across the workspace: `gc.ts`'s worktree reaper, `reap-branches.ts`'s remote
499
+ * `work/*` sweep, `integrator.ts`'s post-merge remote-head reap, and
500
+ * `complete.ts`'s local-branch reap. ONE decision point so the two rules can
501
+ * never drift apart, and NEVER `--force` — a merged-for-reap ref needs none.
502
+ *
503
+ * A remote/local `work/<slug>` branch is reapable iff EITHER:
504
+ *
505
+ * 1. **Fast path (unchanged).** `git merge-base --is-ancestor <tip>
506
+ * <arbiter>/main` succeeds — the fast-forward / true-merge land case.
507
+ * 2. **Squash-aware fallback (new).** BOTH of the following hold:
508
+ * - the item is TERMINAL on `<arbiter>/main` — i.e. the matching
509
+ * {@link terminalMainPaths} (`work/tasks/done/<slug>.md` or
510
+ * `work/tasks/cancelled/<slug>.md` for a task; `work/specs/tasked/…`
511
+ * or `work/specs/dropped/…` for a spec) exists in the fetched main
512
+ * tree; AND
513
+ * - the branch carries NOTHING main lacks — either `git diff --quiet
514
+ * <arbiter>/main <tip>` reads clean (content-equal / content-subset)
515
+ * OR `git cherry <arbiter>/main <tip>` reports every commit as
516
+ * already applied by patch-id (`-` on every line, catching
517
+ * rebase-lands the tree check misses).
518
+ *
519
+ * Grounding: the durable `<arbiter>/main` record is authoritative. It is what
520
+ * `complete` writes FIRST, so if the terminal record is there the item's work
521
+ * is at rest, and a branch that carries nothing main lacks has no in-flight
522
+ * work to protect. The safety floor is unchanged: NEVER reap an in-flight /
523
+ * unmerged branch (no terminal record ⇒ retain), NEVER reap a branch carrying
524
+ * commits main lacks (content check fails ⇒ retain), NEVER `--force`.
525
+ *
526
+ * This is the branch-side twin of the lock-side terminal-on-main recovery
527
+ * (`isTerminalOnMain` in `item-lock.ts`): same anchor, different subject.
528
+ */
529
+ export function isProvablyMergedForReap(
530
+ input: ProvablyMergedForReapInput,
531
+ ): boolean {
532
+ const {cwd, tip, arbiterMain, namespace, slug, env} = input;
533
+
534
+ // 1. Fast path — the fast-forward / true-merge land case. Cheap: one
535
+ // `git merge-base --is-ancestor`.
536
+ if (isAncestor(cwd, tip, arbiterMain, env)) {
537
+ return true;
538
+ }
539
+
540
+ // 2. Squash-aware fallback. Anchor: the durable terminal record on
541
+ // `<arbiter>/main`. No terminal record ⇒ the item is still in-flight (or
542
+ // an observation, which has no terminal folder) ⇒ retain (never reap
543
+ // in-flight work).
544
+ if (!terminalRecordOnMain(cwd, arbiterMain, namespace, slug, env)) {
545
+ return false;
546
+ }
547
+
548
+ // 2a. Content-subset: `git diff --quiet <arbiterMain> <tip>` clean ⇒ the
549
+ // two trees are identical, so the branch carries nothing main lacks
550
+ // (the incident shape: the branch is a since-lagged snapshot of a
551
+ // squash-landed item, no new content on it).
552
+ const diff = run('git', ['diff', '--quiet', arbiterMain, tip], cwd, {env});
553
+ if (diff.status === 0) {
554
+ return true;
555
+ }
556
+
557
+ // 2b. Patch-id equivalence via `git cherry <upstream> <head>`: every commit
558
+ // reachable from `<tip>` but not `<arbiterMain>` is reported as `-`
559
+ // (already applied upstream by patch-id). This catches rebase-lands
560
+ // where the tree diff is non-empty but every commit is already on main
561
+ // under a different sha.
562
+ const cherry = run('git', ['cherry', arbiterMain, tip], cwd, {env});
563
+ if (cherry.status === 0) {
564
+ const lines = cherry.stdout
565
+ .split('\n')
566
+ .map((l) => l.trim())
567
+ .filter((l) => l !== '');
568
+ if (lines.length > 0 && lines.every((l) => l.startsWith('- '))) {
569
+ return true;
570
+ }
571
+ }
572
+
573
+ return false;
574
+ }
575
+
576
+ /** True iff `<arbiterMain>` carries ANY of the item's durable terminal records
577
+ * ({@link terminalMainPaths}). Best-effort: `cat-file -e` is a cheap tree
578
+ * probe; an unresolvable `<arbiterMain>` reads as no-record ⇒ retain (the safe
579
+ * direction). Observations have no terminal folder ⇒ always false. */
580
+ function terminalRecordOnMain(
581
+ cwd: string,
582
+ arbiterMain: string,
583
+ namespace: SidecarType,
584
+ slug: string,
585
+ env: NodeJS.ProcessEnv | undefined,
586
+ ): boolean {
587
+ for (const path of terminalMainPaths(namespace, slug)) {
588
+ const res = run('git', ['cat-file', '-e', `${arbiterMain}:${path}`], cwd, {
589
+ env,
590
+ });
591
+ if (res.status === 0) {
592
+ return true;
593
+ }
594
+ }
595
+ return false;
596
+ }
597
+
435
598
  /**
436
599
  * True iff `ancestor` is an ancestor of (or equal to) `descendant`, both
437
- * resolved IN the local repo `dir` (`git merge-base --is-ancestor`). This is the
438
- * ONE provably-merged reachability predicate in the system: the worktree reaper
439
- * above uses it for `<tip> --is-ancestor <arbiter>/main`, and the remote
440
- * merged-branch sweep (`reap-branches.ts`) REUSES it (not a second predicate) to
441
- * decide a remote `work/<slug>` branch is provably merged before deleting it.
442
- * EXPORTED so that single source of truth is shared, never forked.
600
+ * resolved IN the local repo `dir` (`git merge-base --is-ancestor`). The
601
+ * low-level fast-path building block for {@link isProvablyMergedForReap}
602
+ * reap-safety decisions across the workspace go through the squash-aware
603
+ * wrapper, never this raw predicate. EXPORTED so callers that legitimately
604
+ * need pure ancestry (non-reap idempotency checks, claim-reachability guards)
605
+ * keep the one shared implementation.
443
606
  */
444
607
  export function isAncestor(
445
608
  dir: string,
package/src/github.ts CHANGED
@@ -209,6 +209,26 @@ export class GitHubProvider implements ReviewProvider {
209
209
  return this.parseOpened(input, first);
210
210
  }
211
211
 
212
+ // PR ALREADY EXISTS (task `committed-recovery-completes-existing-pr`): a
213
+ // re-pushed branch whose PR is still open makes `gh pr create` fail with
214
+ // "a pull request for branch ... already exists". This is NOT an outage and
215
+ // NOT a missing/unauth `gh` — retrying is pointless (it fails identically)
216
+ // and degrading to push-only would DROP the refreshed body + the Gate-2
217
+ // review comment on a PR that genuinely exists (the committed-recovery re-run
218
+ // case). Instead RESOLVE the existing PR, UPDATE its title/body to match what
219
+ // this run would have created, and return it as `opened` so the caller's
220
+ // review-comment path fires normally. NEVER throws; a failed edit still
221
+ // returns the resolved PR (the comment is the more important audit trail).
222
+ if (prAlreadyExists(first)) {
223
+ const existing = this.updateExistingRequest(input);
224
+ if (existing !== undefined) {
225
+ return existing;
226
+ }
227
+ // Could not resolve the existing PR's url (e.g. a transient `gh pr view`
228
+ // failure) — fall through to the degrade path rather than crashing; the
229
+ // branch is already pushed, so the work is safe.
230
+ }
231
+
212
232
  // Failed once — distinguish DETERMINISTIC (missing/unauth) from a TRANSIENT
213
233
  // outage by an availability probe. Missing/unauth ⇒ degrade now (no retry).
214
234
  // Thread the FAILED first attempt so the degrade surfaces the REAL `gh` cause
@@ -246,6 +266,47 @@ export class GitHubProvider implements ReviewProvider {
246
266
  return this.parseOpened(input, attempt.value);
247
267
  }
248
268
 
269
+ /**
270
+ * The PR for this branch ALREADY EXISTS (task
271
+ * `committed-recovery-completes-existing-pr`): resolve its url, REFRESH its
272
+ * title/body via `gh pr edit` (so a committed-recovery re-run's synthesised
273
+ * title + task-pointer body replace the earlier run's blank `--fill` ones),
274
+ * and return it as `{opened: true, url}` so the caller's review-comment path
275
+ * fires on the existing PR. Returns `undefined` when the url cannot be
276
+ * resolved (the caller then falls through to the degrade path). The `gh pr
277
+ * edit` is best-effort: a failed edit still returns the resolved PR (posting
278
+ * the review comment matters more than refreshing the body), and it is only
279
+ * attempted when this run actually has explicit title/body to set (else the
280
+ * existing PR's content is left untouched).
281
+ */
282
+ private updateExistingRequest(
283
+ input: OpenRequestInput,
284
+ ): OpenRequestResult | undefined {
285
+ const url = this.resolvePrUrlForBranch(input.branch, input.cwd, input.env);
286
+ if (url === undefined) {
287
+ return undefined;
288
+ }
289
+ // Refresh the PR body/title only when this run supplies them (a committed-
290
+ // recovery re-run does; an unset run leaves the existing content alone).
291
+ if (input.title !== undefined || input.body !== undefined) {
292
+ const editArgs = ['pr', 'edit', url];
293
+ if (input.title !== undefined) {
294
+ editArgs.push('--title', input.title);
295
+ }
296
+ if (input.body !== undefined) {
297
+ editArgs.push('--body', input.body);
298
+ }
299
+ // Best-effort: ignore the result. A failed edit must not lose the PR (or
300
+ // the upcoming review comment) — NEVER throws (runGh swallows spawn errors).
301
+ this.runGh(editArgs, input.cwd, input.env);
302
+ }
303
+ return {
304
+ opened: true,
305
+ url,
306
+ instruction: `Updated the existing GitHub PR for ${input.branch}: ${url}`,
307
+ };
308
+ }
309
+
249
310
  /** Map a successful `gh pr create` RunResult to an OpenRequestResult. */
250
311
  private parseOpened(
251
312
  input: OpenRequestInput,
@@ -457,6 +518,23 @@ export function prCreateContentArgs(input: {
457
518
  return ['--title', input.title ?? '', '--body', input.body ?? ''];
458
519
  }
459
520
 
521
+ /**
522
+ * Does a FAILED `gh pr create` result mean "a PR for this branch already
523
+ * exists" (task `committed-recovery-completes-existing-pr`)? `gh` exits non-zero
524
+ * with a message like `a pull request for branch "work/task-x" into branch
525
+ * "main" already exists: https://github.com/.../pull/357`. We match on the
526
+ * stable "already exists" phrase in `gh`'s stderr/stdout so a re-pushed branch
527
+ * whose PR is still open is treated as an UPDATE, not a transient outage. A
528
+ * missing `gh` (`undefined`) or a success is not this case.
529
+ */
530
+ function prAlreadyExists(result: RunResult | undefined): boolean {
531
+ if (result === undefined || result.status === 0) {
532
+ return false;
533
+ }
534
+ const text = `${result.stderr ?? ''}\n${result.stdout ?? ''}`.toLowerCase();
535
+ return text.includes('already exists');
536
+ }
537
+
460
538
  /**
461
539
  * Parse the PR URL `gh pr create` prints to stdout. `gh` prints the PR URL as a
462
540
  * line (often the only/last line); we pick the first GitHub PR URL we find. Any
package/src/harness.ts CHANGED
@@ -61,6 +61,21 @@ export interface LaunchInput {
61
61
  * `undefined` ⇒ the pi adapter falls back to a generated default for its cwd.
62
62
  */
63
63
  session?: string;
64
+ /**
65
+ * **Optional agent-session DEADLINE** (absolute epoch-ms wall-clock timestamp)
66
+ * for the ASYNC {@link Harness} launch (spec
67
+ * `graceful-pre-timeout-wip-checkpoint`). When set AND the future arrives
68
+ * before the child exits, the harness sends SIGTERM (a ~10s grace, then
69
+ * SIGKILL) and resolves the {@link LaunchResult} with `timedOut: true` — so
70
+ * the caller can route the run as a CHECKPOINT (save WIP + auto-continue /
71
+ * surface) rather than a generic agent-failure. Ignored on the synchronous
72
+ * {@link Harness.launch} path (`spawnSync` cannot be interrupted from the
73
+ * same event loop; the async path is where CI's `--watch` legs run and where
74
+ * the checkpoint discipline lives). Absent ⇒ no deadline (byte-for-byte the
75
+ * pre-task behaviour: a run finishing before its deadline is unchanged; on
76
+ * `exit` the timer is cleared).
77
+ */
78
+ deadlineMs?: number;
64
79
  env?: NodeJS.ProcessEnv;
65
80
  }
66
81
 
@@ -170,6 +185,16 @@ export interface LaunchResult {
170
185
  record: HarnessRecord;
171
186
  /** Failure detail when `ok` is false (the stderr/failure channel). */
172
187
  detail?: string;
188
+ /**
189
+ * **True iff the launch was stopped by the dorfl-internal DEADLINE**
190
+ * ({@link LaunchInput.deadlineMs}) rather than exiting on its own (spec
191
+ * `graceful-pre-timeout-wip-checkpoint`). Distinct from `ok` (a deadline
192
+ * stop is neither a success nor a generic failure — it is a CHECKPOINT the
193
+ * caller routes through the save+auto-continue/surface path). Absent/false
194
+ * when the child exited before the deadline, so a run that finishes
195
+ * normally is byte-for-byte unchanged.
196
+ */
197
+ timedOut?: boolean;
173
198
  /**
174
199
  * The agent's final ANSWER (task `harness-agent-output`): the concatenated
175
200
  * `text` of the LAST assistant turn the invocation produced. This is the
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ export type {
26
26
  } from './repo-config.js';
27
27
  export {
28
28
  REPO_CONFIG_FILENAME,
29
+ REPO_CONFIG_FILENAME_LEGACY,
30
+ resolveRepoConfigPath,
29
31
  REPO_ALLOWED_KEYS,
30
32
  REPO_REJECTED_KEYS,
31
33
  repoConfigPath,
@@ -783,3 +785,11 @@ export type {
783
785
  InstallCIResult,
784
786
  } from './install-ci.js';
785
787
  export {installCI, runWizard} from './install-ci.js';
788
+
789
+ export type {
790
+ InstallSkillsOptions,
791
+ InstallSkillsResult,
792
+ InstalledAgent,
793
+ } from './install-skills.js';
794
+ export {installSkills, resolveSkillsSourceDir} from './install-skills.js';
795
+ export type {Agent} from './vendor/incur/agents.js';