dorfl 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +309 -489
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +46 -14
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +123 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -0
  162. package/dist/install-ci-action-pins.js +190 -0
  163. package/dist/install-ci-action-pins.js.map +1 -0
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +155 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +304 -200
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -292
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +8 -4
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +375 -538
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +52 -15
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +209 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +388 -233
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -327
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +8 -4
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
@@ -14,6 +14,15 @@
14
14
  * a normal (non-fork-restricted) `GITHUB_TOKEN` that can actually close issues
15
15
  * — whereas a `pull_request` event from a FORK gets a read-only token and
16
16
  * cannot close. (See the `## Decisions` block in the task.)
17
+ * - PLUS an hourly `schedule:` (the same `'0 * * * *'` cron the generated
18
+ * `advance-lifecycle` uses) and `workflow_dispatch:` (task
19
+ * `close-job-also-runs-on-a-schedule`): GitHub starts NO workflow for a push
20
+ * made with a job's `GITHUB_TOKEN`, so a land the CI apply job pushes itself
21
+ * (merge mode, an answered `merge` question, a tree-less publish) never fires
22
+ * the `push` trigger. `close-merged-issues` keeps no state (every run
23
+ * re-derives from `main` what is complete) and skips an issue that is already
24
+ * closed, so the timed run is safe and quiet; it catches those lands within
25
+ * the hour. The `push` trigger stays so a human-merged PR closes at once.
17
26
  * - The job INVOKES the close machinery via `dorfl close-merged-issues`,
18
27
  * which CONSUMES the UNCHANGED engine pieces: the resolution
19
28
  * (`resolveClosingIssue`), the "spec complete?" query (`prd-complete-query`,
@@ -33,15 +42,19 @@
33
42
  * the test generates this artifact under `--fake` and asserts every invariant.
34
43
  */
35
44
 
45
+ import {ACTION_PINS, pinnedUses} from './install-ci-action-pins.js';
36
46
  import type {ResolvedCIConfig} from './install-ci-core.js';
37
- import {shouldDropCheckoutCredentials} from './install-ci-core.js';
47
+ import {
48
+ WRITER_SETUP_ACTION_USES,
49
+ shouldDropCheckoutCredentials,
50
+ } from './install-ci-core.js';
38
51
 
39
52
  /** The capability id (the registry key + the emitted workflow file stem). */
40
53
  export const CLOSE_JOB_CAPABILITY_ID = 'close-job';
41
54
 
42
55
  /** The wizard-facing label for the close-job capability. */
43
56
  export const CLOSE_JOB_CAPABILITY_LABEL =
44
- 'Close issues when their work lands (the close-job: on a merge to main)';
57
+ 'Close issues when their work lands (the close-job: on a merge to main, and hourly)';
45
58
 
46
59
  /** The repo-relative path (under the output base) of the emitted workflow. */
47
60
  export const CLOSE_JOB_WORKFLOW_PATH = 'workflows/close-job.yml';
@@ -62,8 +75,7 @@ export function generateCloseJobWorkflow(config: ResolvedCIConfig): string {
62
75
  ? `
63
76
  # Public repository: every git read works without a token, and this
64
77
  # job cannot push, so do not leave the token in .git/config for later
65
- # steps (the setup action, including any project-setup hook, and
66
- # close-merged-issues) to read.
78
+ # steps (the setup action and close-merged-issues) to read.
67
79
  persist-credentials: false`
68
80
  : '';
69
81
  return `\
@@ -78,6 +90,13 @@ export function generateCloseJobWorkflow(config: ResolvedCIConfig): string {
78
90
  # actually CLOSE issues — a \`pull_request\` event from a FORK gets a read-only
79
91
  # token and could not close (a real limitation we deliberately avoid).
80
92
  #
93
+ # PLUS an HOURLY schedule (and workflow_dispatch for a manual catch-up). GitHub
94
+ # starts NO workflow for a push made with a job's GITHUB_TOKEN, so a land the CI
95
+ # pushes itself (merge mode, an answered \`merge\` question, a tree-less publish)
96
+ # never fires the push trigger above. The schedule closes those issues within the
97
+ # hour. It is safe to repeat: \`close-merged-issues\` keeps no state (every run
98
+ # re-derives from main what is complete) and skips an issue already closed.
99
+ #
81
100
  # WHAT IT DOES — \`dorfl close-merged-issues\` resolves which source issue(s)
82
101
  # the landed work closes and closes them. CI owns ONLY this job + the trigger; the
83
102
  # command CONSUMES the engine's UNCHANGED pieces and re-implements none of them:
@@ -106,6 +125,12 @@ on:
106
125
  push:
107
126
  branches:
108
127
  - main
128
+ # Hourly (the same cadence as advance-lifecycle): a land the CI pushes with its
129
+ # GITHUB_TOKEN starts no workflow, so this catches the issues those lands close.
130
+ schedule:
131
+ - cron: '0 * * * *'
132
+ # Manual catch-up.
133
+ workflow_dispatch:
109
134
 
110
135
  # Serialise overlapping close ticks on main; the close-job mutates only the ISSUE
111
136
  # (not the main-CAS), so this just avoids redundant concurrent passes.
@@ -113,20 +138,25 @@ concurrency:
113
138
  group: close-job-\${{ github.ref }}
114
139
  cancel-in-progress: false
115
140
 
116
- # NO \`workflows\` permission: the running job can NEVER edit the workflows tree
117
- # under .github (US #9). It needs only to READ the work/ tree and CLOSE issues.
118
- permissions:
119
- contents: read
120
- issues: write
141
+ # Nothing at workflow level: the job grants its own scopes. NO \`workflows\`
142
+ # permission: the running job can NEVER edit the workflows tree under .github
143
+ # (US #9).
144
+ permissions: {}
121
145
 
122
146
  jobs:
123
147
  close-merged-issues:
124
148
  runs-on: ubuntu-latest
149
+ # It needs only to READ the work/ tree and CLOSE issues.
150
+ permissions:
151
+ contents: read
152
+ issues: write
125
153
  steps:
126
- - uses: actions/checkout@v5
154
+ - uses: ${pinnedUses(ACTION_PINS.checkout)}
127
155
  with:
128
156
  fetch-depth: 0${checkoutCredentials}
129
- - uses: ./.github/actions/dorfl-setup
157
+ # The writer role (Node and dorfl only): this job runs no agent, so it
158
+ # installs no harness, no provider key and no project-setup hook.
159
+ - uses: ${WRITER_SETUP_ACTION_USES}
130
160
  - name: close issues whose work has landed on main
131
161
  # In-place in this checkout (no --isolated/--remote): the CI container IS
132
162
  # the isolation. Resolves the closing issue(s) from the work/ tree, runs
@@ -188,11 +218,18 @@ export function validateCloseJobWorkflow(text: string): CloseJobValidation {
188
218
  operative,
189
219
  ), 'must NOT use the `pull_request` trigger (a fork PR gets a read-only token ' +
190
220
  'that cannot close; `push: [main]` is the chosen trigger).');
191
- // And it must NOT be the build/task tick's cron/dispatch shape — the close-job
192
- // is event-driven on a merge, not a scheduled drain.
193
- require('no-cron-trigger', !/\bschedule:\s*[\s\S]*?-\s*cron:/.test(
221
+ // AND an hourly schedule + workflow_dispatch: GitHub starts no workflow for a
222
+ // push made with a job's GITHUB_TOKEN, so a land the CI pushes itself never
223
+ // fires the push trigger; the timed run closes those issues. Operative lines
224
+ // only: the header comment mentions the schedule.
225
+ require('trigger-cron', /\bschedule:\s*[\s\S]*?-\s*cron:/.test(
226
+ operative,
227
+ ), 'must ALSO trigger on a cron schedule (`on.schedule[].cron`): a land the ' +
228
+ 'CI pushes with its GITHUB_TOKEN starts no workflow, so only the timed run ' +
229
+ 'closes its issue.');
230
+ require('trigger-workflow-dispatch', /\bworkflow_dispatch:/.test(
194
231
  operative,
195
- ), 'the close-job is merge-triggered, NOT a cron drain (no `on.schedule.cron`).');
232
+ ), 'must trigger on `workflow_dispatch` (manual catch-up).');
196
233
  require('no-answer-loop-push-trigger', !/work\/questions\//.test(
197
234
  text,
198
235
  ), 'the close-job must NOT carry the `push work/questions/**` (answer-loop) ' +
package/src/close-job.ts CHANGED
@@ -10,8 +10,9 @@
10
10
  * fanned task reaches the number via `task.spec: → work/specs/<spec>.md spec
11
11
  * issue:`, and `spec:` WINS on a (hand-edited) conflict;
12
12
  * - the QUERY — {@link isSpecComplete} (`spec-complete.ts`, task
13
- * `prd-complete-query`, done): a spec is COMPLETE iff ≥1 `spec:<slug>` task AND
14
- * all such tasks are in `work/done/`;
13
+ * `prd-complete-query`, done): a spec is COMPLETE iff ≥1 `spec:<slug>` task is
14
+ * in `work/tasks/done/` AND every other one is in `work/tasks/cancelled/` (a
15
+ * task staged in backlog, ready or in progress keeps it open);
15
16
  * - the CLOSE — {@link IssueProvider.closeIssue} (`issue-provider.ts`, the atomic
16
17
  * comment+close seam intake already uses): NO direct `gh` in this core; any
17
18
  * informational comment rides the SAME close call (never the PR seam
@@ -25,13 +26,27 @@
25
26
  * `spec:<slug>` tasks are in `work/done/` (reason `completed`).
26
27
  *
27
28
  * A spec whose query is NOT yet complete is left OPEN (the final fanned task's
28
- * merge tick closes it). A lone task still outside `work/done/` is skipped. The
29
+ * merge tick closes it). A lone task still outside `work/done/` is skipped.
30
+ *
31
+ * CANCELLED work never closes an issue (task
32
+ * `spec-complete-counts-staged-and-cancelled-tasks`): a spec whose tasks are ALL
33
+ * cancelled, and a lone task that was cancelled, are left OPEN with decision
34
+ * `cancelled`, not closed as `completed` (nothing was delivered) and not closed
35
+ * as `not planned` either: abandoning an issue is a human's call (drop the spec,
36
+ * close the issue by hand), not something a merge-to-main job infers. The
29
37
  * close DEGRADES (never throws) on a missing/unauthenticated provider, exactly
30
38
  * like intake's bounce close — the run reports the real cause and stays exit-0.
31
39
  *
40
+ * An issue the provider already reports CLOSED is skipped with decision
41
+ * `already-closed` (no close call, no comment, not counted in `closed`): the
42
+ * workflow also runs HOURLY, so without this every pass would re-close and
43
+ * re-report issues closed long ago.
44
+ *
32
45
  * This is the JOB the close-job WORKFLOW (capability E) invokes via
33
46
  * `dorfl close-merged-issues`; the workflow is triggered on a merge to
34
- * `main` (`push: {branches: [main]}`). The driver itself is provider-pluggable
47
+ * `main` (`push: {branches: [main]}`) AND on an hourly `schedule:`, because a
48
+ * land the CI pushes with its `GITHUB_TOKEN` starts no workflow. Every pass
49
+ * re-derives everything from the `work/` tree (no state), so repeating is safe. The driver itself is provider-pluggable
35
50
  * (it takes an {@link IssueProvider}) and reads only the `work/` tree.
36
51
  */
37
52
 
@@ -64,6 +79,8 @@ export type CloseDecision =
64
79
  | 'closed' // the issue was closed via the provider seam
65
80
  | 'not-complete' // a spec whose query says it is not yet complete → left open
66
81
  | 'not-landed' // a lone task not yet in work/done/ → left open
82
+ | 'cancelled' // a spec whose tasks are ALL cancelled, or a cancelled lone task → left open
83
+ | 'already-closed' // closure condition held but the issue is already closed → no close call
67
84
  | 'close-failed'; // closure condition held but the provider close degraded
68
85
 
69
86
  /** One candidate the close-job considered, with the decision it reached. */
@@ -199,11 +216,15 @@ function resolveCandidates(repoPath: string): {
199
216
  return [...specCandidates, ...taskCandidates];
200
217
  }
201
218
 
202
- /** True iff the lone task with this slug resides in `work/done/`. */
203
- function loneTaskLanded(repoPath: string, taskSlug: string): boolean {
204
- for (const file of listMarkdown(repoPath, 'done')) {
219
+ /** True iff the lone task with this slug resides in `work/<folder>/`. */
220
+ function loneTaskIn(
221
+ repoPath: string,
222
+ taskSlug: string,
223
+ folder: 'done' | 'cancelled',
224
+ ): boolean {
225
+ for (const file of listMarkdown(repoPath, folder)) {
205
226
  const fm = parseFrontmatter(
206
- readFileSync(join(repoPath, workItemRel('done', file)), 'utf8'),
227
+ readFileSync(join(repoPath, workItemRel(folder, file)), 'utf8'),
207
228
  );
208
229
  const slug = fm.slug ?? basename(file, '.md');
209
230
  if (slug === taskSlug) {
@@ -220,6 +241,32 @@ function closeComment(via: 'issue' | 'spec', slug: string): string {
220
241
  : `Closed by dorfl: the task \`${slug}\` has landed in \`work/done/\`.`;
221
242
  }
222
243
 
244
+ /**
245
+ * True iff the provider reports the issue as already `closed` (via the existing
246
+ * {@link IssueProvider.getIssue} read, whose `state` is lower-cased). A read that
247
+ * FAILS (the GitHub adapter throws on a `gh` failure) or reports no state answers
248
+ * `false`, so the pass falls through to the close attempt exactly as before: the
249
+ * close then either succeeds or reports `close-failed` with the real cause. A
250
+ * state read must never be the reason an issue stays open.
251
+ */
252
+ async function isAlreadyClosed(
253
+ issueProvider: IssueProvider,
254
+ repoPath: string,
255
+ issueNumber: number,
256
+ env: NodeJS.ProcessEnv | undefined,
257
+ ): Promise<boolean> {
258
+ try {
259
+ const issue = await issueProvider.getIssue({
260
+ cwd: repoPath,
261
+ issueNumber,
262
+ env,
263
+ });
264
+ return issue.state === 'closed';
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+
223
270
  /**
224
271
  * Run the close-job over a repo's `work/` tree: resolve the closure candidates,
225
272
  * apply the per-kind closure condition (a landed lone task; a spec whose
@@ -243,11 +290,14 @@ export async function runCloseJob(
243
290
  let shouldClose: boolean;
244
291
  let notReady: CloseDecision;
245
292
  if (cand.via === 'spec') {
246
- shouldClose = isSpecComplete({repoPath, slug: cand.slug}).complete;
247
- notReady = 'not-complete';
293
+ const query = isSpecComplete({repoPath, slug: cand.slug});
294
+ shouldClose = query.complete;
295
+ notReady = query.allCancelled ? 'cancelled' : 'not-complete';
248
296
  } else {
249
- shouldClose = loneTaskLanded(repoPath, cand.slug);
250
- notReady = 'not-landed';
297
+ shouldClose = loneTaskIn(repoPath, cand.slug, 'done');
298
+ notReady = loneTaskIn(repoPath, cand.slug, 'cancelled')
299
+ ? 'cancelled'
300
+ : 'not-landed';
251
301
  }
252
302
 
253
303
  if (!shouldClose) {
@@ -260,6 +310,21 @@ export async function runCloseJob(
260
310
  continue;
261
311
  }
262
312
 
313
+ // ALREADY CLOSED? The close-job also runs hourly (task
314
+ // `close-job-also-runs-on-a-schedule`), so most passes meet issues an
315
+ // earlier pass (or a human) already closed. Read the state through the
316
+ // provider seam (NO direct `gh`) and skip those: no second close, no second
317
+ // comment, not counted in `closed`.
318
+ if (await isAlreadyClosed(issueProvider, repoPath, cand.issueNumber, env)) {
319
+ candidates.push({
320
+ issueNumber: cand.issueNumber,
321
+ via: cand.via,
322
+ slug: cand.slug,
323
+ decision: 'already-closed',
324
+ });
325
+ continue;
326
+ }
327
+
263
328
  // CLOSE via the provider seam (NO direct `gh`): comment + reason + close in
264
329
  // ONE atomic call, exactly the seam intake's bounce uses.
265
330
  const result = await issueProvider.closeIssue({
package/src/complete.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  PR_TITLE_MAX,
12
12
  } from './integration-core.js';
13
13
  import {ledgerWrite} from './ledger-write.js';
14
- import {releaseItemLock} from './item-lock.js';
14
+ import {markLockKeptForProposePr, releaseItemLock} from './item-lock.js';
15
15
  import {workBranchRef, parseWorkBranchRef} from './slug-namespace.js';
16
16
  import {isProvablyMergedForReap} from './gc.js';
17
17
  import type {SidecarType} from './sidecar.js';
@@ -1272,6 +1272,24 @@ async function releaseClaimLockAfterDurableMove(
1272
1272
  'the lock is released by the next claim (or now, with `dorfl status ' +
1273
1273
  '--reconcile-locks`).',
1274
1274
  );
1275
+ // Say so ON the lock (task `wire-merge-questions-into-the-advance-tick`,
1276
+ // decision 4): the kept lock no longer covers a live build, so the
1277
+ // merge-question surfacer may ask about the branch (when no PR is open) and
1278
+ // an answered `merge` may take the lock over. Best-effort: an unmarked kept
1279
+ // lock only means the branch is treated as a live build (never surfaced).
1280
+ try {
1281
+ const marked = await markLockKeptForProposePr({
1282
+ item: `task:${slug}`,
1283
+ cwd,
1284
+ arbiter,
1285
+ env,
1286
+ });
1287
+ if (marked.outcome === 'lost' || marked.outcome === 'error') {
1288
+ note(marked.message);
1289
+ }
1290
+ } catch {
1291
+ // Never fail a completed propose on the marker.
1292
+ }
1275
1293
  return;
1276
1294
  }
1277
1295
  try {
package/src/config.ts CHANGED
@@ -81,31 +81,28 @@ export type SpecsLandIn = 'proposed' | 'ready';
81
81
  export type ObservationTriage = 'off' | 'ask' | 'auto';
82
82
 
83
83
  /**
84
- * **The `mergeQuestions` gate axis** — the 3-state member of the question-
84
+ * **The `mergeQuestions` gate axis**: the 2-state member of the question-
85
85
  * surfacing gate family that governs the MERGE-QUESTION surfacer (spec
86
86
  * `land-time-reverify-and-parallel-merge-ceiling` Story 17 / task
87
- * `merge-questions-gate-axis`). MIRRORS `observationTriage`'s shape
88
- * (`off | ask | auto`) but is a SEPARATE axis — "should this built work merge?"
89
- * is materially more consequential than "is this observation worth promoting?",
90
- * so a merge-question must NEVER ride `observationTriage` and must NOT default
91
- * `off` (a silently-dropped merge-question means finished, pushed work never
92
- * lands). The states:
87
+ * `merge-questions-gate-axis`). A SEPARATE axis from `observationTriage`:
88
+ * "should this built work merge?" is materially more consequential than "is
89
+ * this observation worth promoting?", so a merge-question must NEVER ride
90
+ * `observationTriage` and must NOT default `off` (a silently-dropped
91
+ * merge-question means finished, pushed work never lands). The states:
93
92
  * - `off` ⇒ the merge-question surfacer is not invoked (only correct for a
94
93
  * repo that lands by some other means);
95
- * - `ask` (DEFAULT) ⇒ the surfacer enumerates unmerged `work/*` branches +
96
- * emits a merge-question sidecar; a human's plain `merge | hold | drop`
97
- * answer is required before the land. The conservative default that honours
98
- * propose semantics and never silently drops pushed work;
99
- * - `auto` ⇒ the runner self-supplies the `merge` answer without surfacing
100
- * and lands through the SAME deterministic answer-driven runner-action
101
- * dispatch + apply-time re-verify (the merge-mode-like fast path). Does NOT
102
- * invoke the agentic decider — a merge-land is never an agent decision.
103
- * Resolved through the SAME precedence chain as the other gates (flag > env >
104
- * per-repo > global > default `ask`). The exact name + default + shape were
105
- * answered in this task's Applied answers 2026-06-26 (q1/q2/q3) + SPEC sidecar
106
- * Q3.
94
+ * - `ask` (DEFAULT) ⇒ the surfacer enumerates unmerged `work/*` branches with
95
+ * no open PR + emits a merge-question sidecar; a human's plain
96
+ * `merge | hold | drop` answer is required before the land. The
97
+ * conservative default that honours propose semantics and never silently
98
+ * drops pushed work.
99
+ * There is NO `auto` (task `wire-merge-questions-into-the-advance-tick`,
100
+ * decision 3): unattended landing is what merge mode (`integration: merge`) is
101
+ * for. Resolved through the precedence chain env > per-repo > global > default
102
+ * `ask` (there is no CLI flag). The surfacer runs in the laptop bare `advance`
103
+ * tick and in the CI `surface-merge-questions` job (`merge-question-tick.ts`).
107
104
  */
108
- export type MergeQuestions = 'off' | 'ask' | 'auto';
105
+ export type MergeQuestions = 'off' | 'ask';
109
106
 
110
107
  /**
111
108
  * Which harness adapter (ADR §5) launches a job's agent and reports its
@@ -229,23 +226,20 @@ export interface Config {
229
226
  */
230
227
  observationTriage: ObservationTriage;
231
228
  /**
232
- * Per-repo policy governing the MERGE-QUESTION SURFACER — the 3-state member
229
+ * Per-repo policy governing the MERGE-QUESTION SURFACER, the 2-state member
233
230
  * of the question-surfacing gate family (spec
234
231
  * `land-time-reverify-and-parallel-merge-ceiling` Story 17 / task
235
- * `merge-questions-gate-axis`). MIRRORS `observationTriage`'s SHAPE
236
- * (`off | ask | auto`) but is a SEPARATE axis with a DIFFERENT default —
237
- * `observationTriage` defaults `off` (a dropped observation is safely
238
- * ignorable), `mergeQuestions` defaults `ask` (a dropped merge-question means
239
- * pushed work never lands). `off` ⇒ the surfacer is NOT invoked (only for a
240
- * repo that lands by some other means); `ask` (default) ⇒ the surfacer
241
- * enumerates unmerged `work/*` branches and emits a merge-question sidecar a
242
- * human answers; `auto` ⇒ the runner self-supplies the `merge` answer without
243
- * surfacing and lands via the SAME deterministic answer-driven runner-action
244
- * dispatch + apply-time re-verify (the merge-mode-like fast path; NOT the
245
- * agentic decider). Resolved like `observationTriage`/`integration`: flag
246
- * (`--merge-questions`) > `DORFL_MERGE_QUESTIONS` env > per-repo > global >
247
- * default `ask`. Gates the SURFACE phase of the merge-question loop only;
248
- * APPLY (consume a committed merge-answer) stays ALWAYS allowed (the
232
+ * `merge-questions-gate-axis`). A SEPARATE axis from `observationTriage` with
233
+ * a DIFFERENT default: `observationTriage` defaults `off` (a dropped
234
+ * observation is safely ignorable), `mergeQuestions` defaults `ask` (a
235
+ * dropped merge-question means pushed work never lands). `off` ⇒ the
236
+ * surfacer is NOT invoked (only for a repo that lands by some other means);
237
+ * `ask` (default) ⇒ the surfacer enumerates unmerged `work/*` branches with no
238
+ * open PR and emits a merge-question sidecar a human answers. No `auto` (task
239
+ * `wire-merge-questions-into-the-advance-tick`, decision 3: unattended landing
240
+ * is merge mode's job). Resolved `DORFL_MERGE_QUESTIONS` env > per-repo >
241
+ * global > default `ask`. Gates the SURFACE phase of the merge-question loop
242
+ * only; APPLY (consume a committed merge-answer) stays ALWAYS allowed (the
249
243
  * create-vs-consume invariant the gate family obeys, ADR
250
244
  * `ci-config-policy-and-gate-family` §4).
251
245
  */
@@ -746,8 +740,10 @@ export interface Config {
746
740
  * INTEGRATES in ONE process; across separate CI jobs the CAS loop IS the queue,
747
741
  * and this cap is what determines how wide a matrix burst converges before any
748
742
  * spurious bounce. A wide-matrix CI raises it; the default stays modest. Race-1
749
- * safety is unchanged (a lost CAS costs only a re-rebase + re-gate retry, never a
750
- * `--force`, never a both-land-broken) — scaling the cap only changes WHEN a
743
+ * safety is unchanged (a lost CAS costs only a re-rebase and a retried push, never
744
+ * a `--force`, never a both-land-broken; it does NOT re-run the gate, and the CI
745
+ * apply phase reports a land whose tree differs from the gated tree, ADR
746
+ * `ci-agent-job-holds-no-write-token` decision 2) — scaling the cap only changes WHEN a
751
747
  * genuinely-stuck loser gives up. Resolved per-repo like `freshWorktreeGate`:
752
748
  * flag (`--merge-retries`) > env (`DORFL_MERGE_RETRIES`) > per-repo > global >
753
749
  * default. The default matches `integration-core.ts`'s built-in fallback (1000 —
@@ -762,13 +758,14 @@ export interface Config {
762
758
  * **Per-repo OPT-IN strictness layered on the OQ6 stale-approval default**
763
759
  * (spec `land-time-reverify-and-parallel-merge-ceiling`, sidecar OQ6 / task
764
760
  * `strict-merge-approval-gate`). Controls how the apply rung treats a prior
765
- * merge-answer when the merge-base CHANGED between the human's answer and
766
- * the apply step. The default (`false`) honours the prior answer and lands
767
- * when the rebased tip re-verifies GREEN — a green re-verify is trusted as
768
- * sufficient (the cheap fast-path). `true` re-surfaces the merge-question
769
- * (clears the answer back to no-answer and re-authors the question on
770
- * `main`/runner under the `advancing` lock — no branch-side mutation)
771
- * instead of auto-landing on a merge-base change, even on a green re-verify
761
+ * merge-answer when `main`'s code (outside `work/`) moved since the question
762
+ * was asked (the `askedAtMain` the question records). The default (`false`)
763
+ * honours the prior answer and lands when the rebased tip re-verifies GREEN
764
+ * (a green re-verify is trusted as sufficient: the cheap fast-path). `true`
765
+ * re-surfaces the merge-question (appends a follow-up asked against the
766
+ * current `main`, under the `advancing` lock, before any checkout or
767
+ * rebase: no branch-side mutation) instead of auto-landing when `main`
768
+ * moved, even on a green re-verify
772
769
  * — the host-agnostic analogue of GitHub's "dismiss stale approvals when
773
770
  * the base changes". Story #16's RED-re-verify refusal is UNCHANGED and
774
771
  * independent of this flag.
@@ -905,10 +902,8 @@ export const DEFAULT_CONFIG: Config = {
905
902
  // a silently-dropped merge-question means finished, pushed work never lands —
906
903
  // strictly more consequential than a dropped observation-promote prompt, so
907
904
  // this gate axis is DELIBERATELY HIGHER than `observationTriage`'s `off`. A
908
- // trusted-repo fast path opts into `auto` (runner self-supplies the `merge`
909
- // answer + lands via the SAME deterministic apply-time re-verify); a repo that
910
- // lands by some other means opts into `off`. Resolved per-repo through the
911
- // SAME precedence chain (flag > env > per-repo > global > default).
905
+ // repo that lands by some other means opts into `off`. Resolved per-repo
906
+ // through the SAME precedence chain (env > per-repo > global > default).
912
907
  mergeQuestions: 'ask',
913
908
  // DECLARED blocked work is calm by default (`false`): the `needsAnswers`-blocked
914
909
  // pool is dropped from the auto-pick selection, so `advance` does NOT proactively
@@ -165,11 +165,91 @@ export function branchAheadOfArbiter(options: {
165
165
  return branchAheadOf(cwd, branchRef, mainRef, env);
166
166
  }
167
167
 
168
+ /**
169
+ * Why a continue rebase did not apply (task
170
+ * `a-failed-rebase-reports-its-real-cause`). Either way the rebase was
171
+ * `--abort`ed (NEVER auto-resolved) and the kept branch is back on its
172
+ * un-rebased tip; the two kinds differ only in what the report says:
173
+ *
174
+ * - `conflict`: git stopped on conflicting paths (`paths`, the unmerged index
175
+ * entries; empty only when git printed a `CONFLICT (...)` line but left no
176
+ * unmerged entry). A genuine content conflict for a human to resolve.
177
+ * - `failed`: the rebase exited non-zero WITHOUT a conflict (a missing
178
+ * committer identity, an unresolvable `main`, a dirty tree, a hook, ...).
179
+ * Calling it "conflicted" hid the real cause (observation
180
+ * `ci-answered-merge-reports-a-conflict-a-plain-rebase-does-not-have`).
181
+ *
182
+ * `onto` is the sha `mainRef` resolved to when the rebase ran (`''` when it did
183
+ * not resolve), and `output` is git's own report (stderr, then stdout),
184
+ * condensed to one line by {@link condenseGitOutput}.
185
+ */
186
+ export type ContinueRebaseFailure =
187
+ | {kind: 'conflict'; onto: string; paths: string[]; output: string}
188
+ | {kind: 'failed'; onto: string; output: string};
189
+
168
190
  /** The result of rebasing a continued branch onto the freshly-fetched main. */
169
- export interface ContinueRebaseResult {
170
- /** `clean` — the rebase replayed onto main with no conflict. */
171
- /** `conflict` — the rebase conflicted; it was `--abort`ed (never auto-resolved). */
172
- kind: 'clean' | 'conflict';
191
+ export type ContinueRebaseResult = {kind: 'clean'} | ContinueRebaseFailure;
192
+
193
+ /** The longest git report a {@link ContinueRebaseFailure} carries. */
194
+ const MAX_GIT_OUTPUT = 600;
195
+
196
+ /**
197
+ * Condense git's multi-line report into one line for a reason / note: split
198
+ * on newlines and on the carriage returns of git's progress lines, drop blank
199
+ * and `hint:` lines, join the rest with ` / `, and cap the length.
200
+ */
201
+ export function condenseGitOutput(text: string): string {
202
+ const lines = text
203
+ .split(/[\r\n]+/)
204
+ .map((l) => l.trim())
205
+ .filter((l) => l !== '' && !l.startsWith('hint:'));
206
+ const joined = lines.join(' / ');
207
+ return joined.length > MAX_GIT_OUTPUT
208
+ ? `${joined.slice(0, MAX_GIT_OUTPUT)}...`
209
+ : joined;
210
+ }
211
+
212
+ /**
213
+ * What happened to a continue rebase, as the tail of a sentence that starts
214
+ * "rebasing `<branch>` onto current main ...". A conflict names its paths and
215
+ * the `main` sha; any other failure says so and quotes git. Both say the rebase
216
+ * was aborted, so the kept work is intact.
217
+ */
218
+ export function describeContinueRebaseFailure(
219
+ failure: ContinueRebaseFailure,
220
+ ): string {
221
+ const onto = failure.onto === '' ? 'an unresolved main' : failure.onto;
222
+ if (failure.kind === 'conflict') {
223
+ const on =
224
+ failure.paths.length > 0 ? ` on ${failure.paths.join(', ')}` : '';
225
+ return (
226
+ `(${onto}) conflicted${on} (aborted, never auto-resolved)` +
227
+ (failure.paths.length > 0 || failure.output === ''
228
+ ? ''
229
+ : `; git: ${failure.output}`)
230
+ );
231
+ }
232
+ return (
233
+ `(${onto}) failed without a conflict (aborted); git: ` +
234
+ (failure.output === '' ? '(no output)' : failure.output)
235
+ );
236
+ }
237
+
238
+ /**
239
+ * The whole sentence for a continue rebase that did not apply: "rebasing
240
+ * `<branch>` onto current main (<sha>) conflicted on <paths> ..." or "... failed
241
+ * without a conflict (aborted); git: <report>". `failure` is optional only so a
242
+ * caller holding just the legacy boolean still gets a sentence.
243
+ */
244
+ export function continueRebaseSentence(
245
+ branch: string,
246
+ failure: ContinueRebaseFailure | undefined,
247
+ ): string {
248
+ const tail =
249
+ failure === undefined
250
+ ? 'did not apply (aborted, never auto-resolved)'
251
+ : describeContinueRebaseFailure(failure);
252
+ return `rebasing \`${branch}\` onto current main ${tail}`;
173
253
  }
174
254
 
175
255
  /**
@@ -188,10 +268,13 @@ export interface ContinueRebaseResult {
188
268
  * rename/rename ledger conflict that inheritance caused) is deleted. The ONLY
189
269
  * commits replayed are the agent's own wip / `→done` commits.
190
270
  *
191
- * A CLEAN rebase returns `{kind: 'clean'}`; a CONFLICTING rebase (a GENUINE code
192
- * conflict) is `--abort`ed (NEVER auto-resolved) and returns `{kind: 'conflict'}`
193
- * so the caller can route the item to needs-attention via the §10 path. Must be
194
- * called while HEAD is the continued branch.
271
+ * A CLEAN rebase returns `{kind: 'clean'}`. Any other outcome is `--abort`ed
272
+ * (NEVER auto-resolved) and returns a {@link ContinueRebaseFailure} carrying the
273
+ * `main` sha rebased onto and git's report: `conflict` (with the conflicting
274
+ * paths) when git stopped on a GENUINE conflict, `failed` for every other
275
+ * non-zero exit. The caller routes either to needs-attention via the §10 path,
276
+ * naming the real cause ({@link describeContinueRebaseFailure}). Must be called
277
+ * while HEAD is the continued branch.
195
278
  *
196
279
  * RENAME-DETECTION-OFF (task `disable-rename-detection-on-continue-rebase`): the
197
280
  * rebase is invoked with `-c merge.directoryRenames=false` SCOPED to the
@@ -210,6 +293,11 @@ export function rebaseContinuedBranchOntoMain(
210
293
  mainRef: string,
211
294
  env: NodeJS.ProcessEnv | undefined,
212
295
  ): ContinueRebaseResult {
296
+ const onto = gitSoft(
297
+ ['rev-parse', '--verify', '--quiet', `${mainRef}^{commit}`],
298
+ cwd,
299
+ env,
300
+ ).stdout.trim();
213
301
  const rebase = gitSoft(
214
302
  ['-c', 'merge.directoryRenames=false', 'rebase', mainRef],
215
303
  cwd,
@@ -218,9 +306,20 @@ export function rebaseContinuedBranchOntoMain(
218
306
  if (rebase.status === 0) {
219
307
  return {kind: 'clean'};
220
308
  }
221
- // NEVER auto-resolve: abort back to a clean continued-branch tip.
309
+ // Read the conflicting paths (the unmerged index entries) BEFORE the abort
310
+ // clears them. git prints its `CONFLICT (...)` lines on stdout.
311
+ const paths = gitSoft(['diff', '--name-only', '--diff-filter=U'], cwd, env)
312
+ .stdout.split('\n')
313
+ .map((p) => p.trim())
314
+ .filter((p) => p !== '');
315
+ const output = condenseGitOutput([rebase.stderr, rebase.stdout].join('\n'));
316
+ // NEVER auto-resolve: abort back to a clean continued-branch tip. A failure
317
+ // that never started a rebase leaves nothing to abort; that exit is harmless.
222
318
  gitSoft(['rebase', '--abort'], cwd, env);
223
- return {kind: 'conflict'};
319
+ if (paths.length > 0 || /^CONFLICT \(/m.test(rebase.stdout)) {
320
+ return {kind: 'conflict', onto, paths, output};
321
+ }
322
+ return {kind: 'failed', onto, output};
224
323
  }
225
324
 
226
325
  /**
@@ -376,6 +475,9 @@ export function pushProposeBranchWithStaleLeaseRetry(options: {
376
475
  // shape is the same create-only `--force-with-lease=<ref>:` form
377
476
  // `item-lock.ts` uses for the CAS create.
378
477
  const lease = observed === '' ? `${branch}:` : `${branch}:${observed}`;
478
+ // Write-seam implementation: reached only from the integrator, behind
479
+ // `ledgerWrite.applyCompleteTransition` (guard:
480
+ // `write-sites-through-seams.test.ts`).
379
481
  const push = gitSoft(
380
482
  ['push', arbiter, `${branch}:${branch}`, `--force-with-lease=${lease}`],
381
483
  cwd,
@@ -423,11 +525,12 @@ export type ContinuedPushResult =
423
525
  /** The work branch landed on the arbiter (first try or after a clean re-rebase). */
424
526
  | {kind: 'pushed'}
425
527
  /**
426
- * A re-rebase onto the freshly-fetched main CONFLICTED on a retry; it was
427
- * `--abort`ed (NEVER auto-resolved). The caller routes the item to
428
- * needs-attention via the §10 path — the green work stays intact on the branch.
528
+ * A re-rebase onto the freshly-fetched main did not apply on a retry (a
529
+ * `conflict`, or a non-conflict `failed`); it was `--abort`ed (NEVER
530
+ * auto-resolved). The caller routes the item to needs-attention via the §10
531
+ * path, naming the cause; the green work stays intact on the branch.
429
532
  */
430
- | {kind: 'conflict'};
533
+ | ContinueRebaseFailure;
431
534
 
432
535
  /**
433
536
  * Push the CONTINUED `work/<slug>` branch to the arbiter with
@@ -493,6 +596,8 @@ export function pushContinuedBranchWithStaleLeaseRetry(options: {
493
596
  let attempt = 0;
494
597
  // eslint-disable-next-line no-constant-condition
495
598
  while (true) {
599
+ // Write-seam implementation: callers reach this through
600
+ // `refWrite.pushContinuedBranch` (guard: `write-sites-through-seams.test.ts`).
496
601
  const push = gitSoft(
497
602
  [
498
603
  'push',
@@ -556,8 +661,8 @@ export function pushContinuedBranchWithStaleLeaseRetry(options: {
556
661
  // merge). A CONFLICT is the EXISTING abort → needs-attention path — the retry
557
662
  // handles ONLY the clean-rebase stale-lease case (never auto-resolves).
558
663
  const rebase = rebaseContinuedBranchOntoMain(cwd, mainRef, env);
559
- if (rebase.kind === 'conflict') {
560
- return {kind: 'conflict'};
664
+ if (rebase.kind !== 'clean') {
665
+ return rebase;
561
666
  }
562
667
  // Loop: retry the push, now re-leased against the freshly-fetched remote tip.
563
668
  }