dorfl 0.14.3 → 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 +305 -486
  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 +44 -13
  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 +25 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -1
  162. package/dist/install-ci-action-pins.js +25 -0
  163. package/dist/install-ci-action-pins.js.map +1 -1
  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 +148 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +278 -190
  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 -293
  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 +6 -3
  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 +371 -535
  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 +50 -14
  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 +25 -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 +352 -223
  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 -328
  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 +6 -3
  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
@@ -17,38 +17,41 @@
17
17
  *
18
18
  * - the LIFECYCLE rungs (triage observations / surface declared blockers / apply
19
19
  * a committed answer), gated by the two calm-default lifecycle gates below;
20
- * - the on-answer-committed trigger `on: push work/questions/**`, which re-runs
21
- * the loop promptly when a question sidecar is answered;
20
+ * - the on-answer-committed trigger `on: push work/questions/**` (on the
21
+ * default branch only), which re-runs the loop promptly when a question
22
+ * sidecar is answered;
22
23
  * - (already in the absorbed seed) capability F — the `reap-merged-branches` job
23
24
  * (`gc --remote-branches`) on the same `schedule:` cron, opt-out via the
24
- * `sweepMergedBranches` dispatch input.
25
+ * `sweepMergedBranches` dispatch input;
26
+ * - (also in the seed) the no-agent `surface-merge-questions` job (`dorfl
27
+ * surface-merge-questions`, task `wire-merge-questions-into-the-advance-tick`),
28
+ * which asks a merge question for each unmerged work branch with no open PR,
29
+ * behind the `mergeQuestions` config gate.
25
30
  *
26
- * The discipline is VERBATIM from the seed:
31
+ * THE SPLIT (spec `ci-agent-job-without-write-token`, ADR
32
+ * `ci-agent-job-holds-no-write-token`, task `ci-split-generate-workflows`): the
33
+ * tick runs NO agent itself. `enumerate` (`contents: read`) lists the eligible
34
+ * items; a `dispatch` job (`actions: write` ONLY, no checkout, no setup) starts
35
+ * one `dorfl-item-dispatch.yml` run per item, which calls `dorfl-item.yml`
36
+ * (lock, agent, apply; `dorfl-item-template.ts`). There is no matrix any more:
37
+ * a matrix would put every item's artifact in one shared namespace (decision 1).
27
38
  *
28
- * - CI ALWAYS invokes `advance` (ADR `ci-config-policy-and-gate-family` §1):
29
- * `advance` is a strict superset of `do`, and with the lifecycle gates at their
30
- * calm defaults it degrades to exactly `do`'s build/task behaviour. The verb
31
- * is NEVER a user decision; the workflow tunes behaviour via the
32
- * `DORFL_*` env block, not by swapping the verb. There is NO
33
- * `autoAdvance` gate.
34
- * - The two LIFECYCLE gates are ORTHOGONAL peers, both calm by default:
35
- * `DORFL_OBSERVATION_TRIAGE` (`off`/`ask`/`auto`, calm `off`) governs the
36
- * raw observation INBOX; `DORFL_SURFACE_BLOCKERS` (`true`/`false`, calm
37
- * `false`) governs DECLARED `needsAnswers` work. "Groom my observations but
38
- * leave my blocked work alone" = `OBSERVATION_TRIAGE: ask` + `SURFACE_BLOCKERS:
39
- * false`. Applying a committed answer has NO gate.
40
- * - ONE word `integrationMode` drives BOTH the job SHAPE AND the
41
- * `--propose`/`--merge` flag (they can never desync): `propose` ⇒ a DYNAMIC
42
- * matrix (`dorfl scan --json | jq` enumerates one leg per eligible id,
43
- * each leg carries `--propose` so it can NEVER merge to `main`); `merge` ⇒ ALSO
44
- * a MATRIX (one leg per item; build/gate/review fan out, the LAND tail is
45
- * serialised by the engine's `mergeRetries` CAS-retry loop — the git-alone
46
- * floor — NOT by the workflow shape; SPEC
47
- * `land-time-reverify-and-parallel-merge-ceiling`). This is integration-mode
48
- * behaviour, IDENTICAL to the build tick (integration mode is
49
- * verb-independent).
39
+ * The discipline:
40
+ *
41
+ * - CI ALWAYS invokes `advance` (ADR `ci-config-policy-and-gate-family` §1),
42
+ * now inside the per-item workflow. There is NO `autoAdvance` gate.
43
+ * - The two LIFECYCLE gates are ORTHOGONAL peers, both calm by default
44
+ * (`observationTriage` off, `surfaceBlockers` false). Applying a committed
45
+ * answer has NO gate.
46
+ * - ONE word `integrationMode` drives the `--propose`/`--merge` flag every
47
+ * item run passes (the `dispatch` job forwards it). In merge mode the LAND
48
+ * tail (rebase + CAS push to `main`) is serialised by the engine's
49
+ * `mergeRetries` CAS-retry loop in the apply job, NOT by the workflow
50
+ * shape; a lost CAS re-runs the rebase and the push only, never the gate
51
+ * (decision 2), and the apply phase reports every land whose tree differs
52
+ * from the gated one.
50
53
  * - CI runs IN-PLACE (the CI container IS the isolation): no
51
- * `--isolated`/`--remote`/registry. A CI concurrency group (per-ref) prevents
54
+ * `--isolated`/`--remote`/registry. A per-ref concurrency group prevents
52
55
  * overlapping ticks; the claim CAS is the real cross-run serialiser.
53
56
  * - All invocations use explicit slug prefixes (`task:`/`spec:`), never bare
54
57
  * (ADR `command-surface-and-journeys` §3a).
@@ -60,36 +63,63 @@
60
63
  * the test generates this artifact under `--fake` and asserts every invariant.
61
64
  */
62
65
  import { ACTION_PINS, pinnedUses } from './install-ci-action-pins.js';
63
- import { providerSecretsWithBlock } from './install-ci-core.js';
66
+ import { WRITER_SETUP_ACTION_USES } from './install-ci-core.js';
67
+ import { GATE_OVERRIDES, ITEM_DISPATCH_WORKFLOW_FILE, ITEM_RUN_NAME_PREFIX, gateOverridesStep, } from './dorfl-item-template.js';
64
68
  /** The capability id (the registry key + the emitted workflow file stem). */
65
69
  export const ADVANCE_LIFECYCLE_CAPABILITY_ID = 'advance-lifecycle';
66
70
  /** The wizard-facing label for the advance-lifecycle capability. */
67
71
  export const ADVANCE_LIFECYCLE_CAPABILITY_LABEL = 'Auto-triage observations + surface declared blockers + apply committed answers (the advance lifecycle loop: cron + dispatch + on-answer-committed)';
68
72
  /** The repo-relative path (under the output base) of the emitted workflow. */
69
73
  export const ADVANCE_LIFECYCLE_WORKFLOW_PATH = 'workflows/advance-lifecycle.yml';
74
+ /**
75
+ * The `dispatch` job's shell (a `run: |` body, 10-space indented): list the
76
+ * `dorfl-item-dispatch.yml` runs that are not completed, then start one run per
77
+ * item of `ITEMS` (slot = index mod `MAX_PARALLEL`), skipping an item whose
78
+ * `dorfl-item <item>` run is still queued, pending or in progress. Every value
79
+ * arrives through `env:`; it prints one line per item (dispatched or skipped).
80
+ * Exported for the template test, which runs it with `gh` stubbed.
81
+ */
82
+ export const DISPATCH_SCRIPT = `\
83
+ set -euo pipefail
84
+ # The item runs that have not completed yet (queued, waiting in a
85
+ # slot, or in progress), by run name.
86
+ active="$(gh run list -R "\${REPO}" --workflow ${ITEM_DISPATCH_WORKFLOW_FILE} --limit 1000 --json displayTitle,status --jq '.[] | select(.status != "completed") | .displayTitle')"
87
+ mapfile -t items < <(printf '%s' "\${ITEMS}" | jq -r '.[]')
88
+ i=0
89
+ for item in "\${items[@]}"; do
90
+ slot=$(( i % MAX_PARALLEL ))
91
+ i=$(( i + 1 ))
92
+ if printf '%s\\n' "\${active}" | grep -Fxq -- "${ITEM_RUN_NAME_PREFIX} \${item}"; then
93
+ echo "skipped \${item} slot \${slot} (its '${ITEM_RUN_NAME_PREFIX} \${item}' run is not completed)"
94
+ continue
95
+ fi
96
+ args=(-f "item=\${item}" -f "slot=\${slot}" -f "integrationMode=\${INTEGRATION_MODE}")
97
+ ${GATE_OVERRIDES.map((g) => ` if [ -n "\${${g.env.replace(/^DORFL_/, 'DISPATCH_')}}" ]; then args+=(-f "${g.input}=\${${g.env.replace(/^DORFL_/, 'DISPATCH_')}}"); fi`).join('\n')}
98
+ gh workflow run ${ITEM_DISPATCH_WORKFLOW_FILE} -R "\${REPO}" --ref "\${DEFAULT_BRANCH}" "\${args[@]}"
99
+ echo "dispatched \${item} slot \${slot} (run '${ITEM_RUN_NAME_PREFIX} \${item}')"
100
+ done`;
70
101
  /**
71
102
  * Generate the advance-lifecycle workflow YAML by PARAMETERISING the seed
72
103
  * `docs/ci/advance-loop.yml.template`. Deterministic: the same config produces
73
- * byte-identical output. The workflow is a FIXED shell (ADR §6: all policy is
74
- * env/config, so the artifact carries no config-derived policy beyond the env-block
75
- * scaffolding) — `config` is accepted for parity with the `CapabilityEmitter` seam
76
- * and future per-config wiring, but the advance-lifecycle shape itself is
77
- * config-independent.
78
- *
79
- * This is the absorbed seed, with two parameterisations: (1) it wires the SHARED
80
- * composite setup action (`./.github/actions/dorfl-setup`, emitted by the
81
- * core task) into every job; (2) it surfaces the engine gate family — including
82
- * the two calm-default LIFECYCLE gates — as the `DORFL_*` env block, so the
83
- * out-of-the-box tick asks nothing until the user opts in. The seed's capability-F
84
- * reap job + `sweepMergedBranches` input are PRESERVED verbatim (NOT stripped); no
85
- * separate gc-sweep workflow is emitted.
104
+ * byte-identical output. The only config it reads is `maxParallel` (the number
105
+ * of concurrency slots the `dispatch` job spreads the item runs over).
86
106
  */
87
107
  export function generateAdvanceLifecycleWorkflow(config) {
88
- // The agent-running jobs (propose / merge) pass the configured provider
89
- // secret(s) to the setup action ONCE via `with:`; the action forwards them to
90
- // `$GITHUB_ENV` so `pi` can authenticate (models-json mode). auth-json mode has
91
- // no provider keys here (it uses auth.json), so the block is empty.
92
- const setupWith = providerSecretsWithBlock(config);
108
+ const enumerateGate = gateOverridesStep({
109
+ source: 'github.event.inputs',
110
+ ifDispatch: true,
111
+ comment: `\
112
+ # MUST run BEFORE \`scan\`: scan's pools are gated by the SAME engine
113
+ # gate family (autoTask/autoBuild + lifecycle observationTriage/
114
+ # surfaceBlockers), so an override that does not reach this job empties
115
+ # the item list and is silently inert. \`if:\` + the inner \`[ -n ... ]\`
116
+ # guard keep schedule/push (and a blank dispatch field) exporting
117
+ # NOTHING: an empty DORFL_* would make env-config coercion throw. The
118
+ # inputs reach the shell through \`env:\` as DATA, never as \`\${{ }}\` text
119
+ # spliced into the script, and the \`case\` guards stop a value from
120
+ # appending its own lines to $GITHUB_ENV.`,
121
+ });
122
+ const dispatchGateEnv = GATE_OVERRIDES.map((g) => ` ${g.env.replace(/^DORFL_/, 'DISPATCH_')}: \${{ github.event.inputs.${g.input} }}`).join('\n');
93
123
  return `\
94
124
  # dorfl — the ADVANCE LIFECYCLE loop in CI (capability C: auto-triage
95
125
  # observations + surface declared blockers + apply committed answers, spec
@@ -103,36 +133,40 @@ export function generateAdvanceLifecycleWorkflow(config) {
103
133
  # on their own time.
104
134
  #
105
135
  # CI ALWAYS invokes \`advance\` (NEVER a user-chosen verb): \`advance\` is a strict
106
- # superset of \`do\`, and with the lifecycle gates at their calm defaults (below) it
136
+ # superset of \`do\`, and with the lifecycle gates at their calm defaults it
107
137
  # degrades to exactly \`do\`'s build/task behaviour (ADR
108
- # ci-config-policy-and-gate-family §1). The triage/surface/apply rungs + the
109
- # on-answer-committed trigger are EXACTLY what \`advance\` adds over the build/task
110
- # tick. There is NO \`autoAdvance\` gate — the lifecycle decomposes into the gate
111
- # family in the env block.
138
+ # ci-config-policy-and-gate-family §1). There is NO \`autoAdvance\` gate.
139
+ #
140
+ # THE SPLIT (ADR ci-agent-job-holds-no-write-token): THIS workflow runs no agent.
141
+ # \`enumerate\` lists the eligible items; \`dispatch\` starts ONE
142
+ # dorfl-item-dispatch.yml run per item, which calls dorfl-item.yml: a lock job
143
+ # (write token, no agent), an agent job (read-only token, no persisted
144
+ # credential) and an apply job (write token, no agent). One run per item gives
145
+ # each item its own artifact namespace. Parallelism: the item runs spread over
146
+ # maxParallel concurrency slots (\`queue: max\`), so at most that many run at
147
+ # once and the rest wait without using runner minutes.
112
148
  #
113
- # ONE WORD, ONE MEANING — \`integrationMode\` drives BOTH the job SHAPE AND the
114
- # \`--propose\`/\`--merge\` flag the legs pass, so they can NEVER desync:
115
- # * propose ⇒ a DYNAMIC matrix (one leg per eligible id, enumerated via
116
- # \`dorfl scan --json | jq\`), each leg \`advance --propose\`
117
- # opening its OWN PR — a leg can NEVER merge to main.
118
- # * merge ⇒ ALSO a MATRIX (one leg per item; build/gate/review fan out, the
119
- # LAND tail — rebase + CAS push to \`main\` — is serialised by the
120
- # engine's \`mergeRetries\` CAS-retry loop, NOT by the workflow shape.
121
- # SPEC \`land-time-reverify-and-parallel-merge-ceiling\`: a
122
- # non-fast-forward push triggers re-rebase + re-gate + retry, never
123
- # a \`--force\`. The cross-job serialiser is the CAS-retry FLOOR;
124
- # there is NO host-specific \`concurrency:\` group on the merge job).
125
- # This is integration-mode behaviour, IDENTICAL to the build/task tick
126
- # (integration mode is verb-independent). The CLAIM CAS, not the matrix, is the
127
- # real cross-run serialiser: a leg that loses the claim race exits clean.
149
+ # ONE WORD, ONE MEANING — \`integrationMode\` is forwarded to every item run,
150
+ # which passes it to \`advance\` as \`--propose\`/\`--merge\`:
151
+ # * propose ⇒ each item opens its OWN PR; an item run can NEVER merge to main.
152
+ # * merge ⇒ each item lands on main. The LAND tail (rebase + CAS push) is
153
+ # serialised by the engine's \`mergeRetries\` CAS-retry loop in the
154
+ # apply job, NOT by a workflow shape: a non-fast-forward push
155
+ # re-rebases and retries, never a \`--force\`. A lost CAS does NOT
156
+ # re-run the gate (the gate ran in the agent job, minutes
157
+ # earlier); the apply phase reports every land whose tree differs
158
+ # from the gated tree ("landed without re-gate after N lost
159
+ # races") in its output and the landed commit's trailer.
160
+ # The CLAIM CAS, not the workflow, is the real cross-run serialiser: an item run
161
+ # that loses the claim race exits clean, and a duplicate run re-classifies at the
162
+ # fresh arbiter tip.
128
163
  #
129
164
  # CI runs IN-PLACE (the CI container IS the isolation): NO --isolated/--remote/
130
165
  # registry (laptop-only affordances). The concurrency group below stops
131
166
  # overlapping ticks of the same ref from colliding.
132
167
  #
133
- # SAFETY (US #9): the running job is FORBIDDEN from editing the workflows tree
134
- # under .github. It requests NO \`workflows\` permission, so it can never rewrite
135
- # its own triggers.
168
+ # SAFETY (US #9): no job here requests a \`workflows\` permission, so none can
169
+ # rewrite its own triggers.
136
170
 
137
171
  name: advance-lifecycle
138
172
 
@@ -143,15 +177,18 @@ on:
143
177
  # (here: hourly).
144
178
  - cron: '0 * * * *'
145
179
  push:
146
- # On-answer-committed: a push that touches an answered question sidecar
147
- # (\`work/questions/**\`) re-runs the loop so the human's answer is applied
148
- # promptly. This is the lifecycle-specific trigger \`do\` cannot provide.
180
+ # On-answer-committed: a push to the DEFAULT BRANCH that touches an answered
181
+ # question sidecar (\`work/questions/**\`) re-runs the loop so the human's
182
+ # answer is applied promptly. Pinned to main: a work-branch push touching a
183
+ # sidecar must never run the lifecycle on that branch.
184
+ branches:
185
+ - main
149
186
  paths:
150
187
  - 'work/questions/**'
151
188
  workflow_dispatch:
152
189
  inputs:
153
190
  integrationMode:
154
- description: "Integration mode (drives BOTH the integration flag passed to \`advance\` AND the job shape): propose ⇒ a matrix, each leg \`advance --propose\` (one PR per item); merge ⇒ a matrix, each leg \`advance --merge\` (one item — build/gate/review fan out in parallel; the LAND tail is serialised by the engine's \`mergeRetries\` CAS-retry loop, not by the workflow shape)."
191
+ description: "Integration mode, forwarded to every item run as \`advance --propose\`/\`--merge\`: propose ⇒ one PR per item; merge ⇒ each item lands on main (the LAND tail is serialised by the engine's \`mergeRetries\` CAS-retry loop, not by the workflow shape)."
155
192
  required: false
156
193
  default: 'propose'
157
194
  type: choice
@@ -167,10 +204,9 @@ on:
167
204
  # Override an engine gate for THIS manual run only, riding the env layer of
168
205
  # flag > env > per-repo > global > default. Modelled as \`type: choice\` with a
169
206
  # BLANK first option (not \`type: boolean\`, which cannot represent "unset"):
170
- # blank ⇒ emit NOTHING (the committed dorfl.json wins, today's
171
- # behaviour); a non-blank choice ⇒ export the matching DORFL_* for this
172
- # run. The blank is load-bearing: env-config coercion THROWS on an empty
173
- # string, so the per-job step below only writes when the input is non-blank.
207
+ # blank ⇒ emit NOTHING (the committed dorfl.json wins); a non-blank choice ⇒
208
+ # export the matching DORFL_* in \`enumerate\` and forward it to every item
209
+ # run (its lock, agent and apply jobs).
174
210
  autoBuild:
175
211
  description: 'One-shot override of the autoBuild gate (DORFL_AUTO_BUILD) for THIS dispatch run only. Blank ⇒ no override (config wins).'
176
212
  required: false
@@ -215,126 +251,60 @@ concurrency:
215
251
  group: advance-lifecycle-\${{ github.ref }}
216
252
  cancel-in-progress: false
217
253
 
218
- # NO \`workflows\` permission: the running job can NEVER edit the workflows tree
219
- # under .github (US #9). \`contents: write\` + \`pull-requests: write\` are all this
220
- # tick needs (commit work, open PRs); it never rewrites its triggers.
221
- permissions:
222
- contents: write
223
- pull-requests: write
254
+ # Nothing at workflow level: each job grants its own scopes. NO job requests a
255
+ # \`workflows\` permission (US #9).
256
+ permissions: {}
224
257
 
225
- env:
226
- # The resolved integration mode: the dispatch input when present, else \`propose\`
227
- # (the conservative default — one PR per item, a human merges). This single value
228
- # selects BOTH the job shape (via the \`if:\` guards below) AND the integration flag
229
- # the \`advance\` legs pass (\`--propose\`/\`--merge\`), so they can never desync.
230
- INTEGRATION_MODE: \${{ github.event.inputs.integrationMode || 'propose' }}
231
- # Reap merged remote work/* branches on the scheduled tick (capability F). ON by
232
- # default (the empty dispatch input on a \`schedule\`/\`push\` trigger reads as the
233
- # default); set the \`sweepMergedBranches\` dispatch input to \`false\` to opt out.
234
- # This is the provider-agnostic hygiene sweep (\`gc --remote-branches\`) for
235
- # out-of-band human/UI PR merges, whose work/<slug> branch nothing else deletes.
236
- SWEEP_MERGED_BRANCHES: \${{ github.event.inputs.sweepMergedBranches || 'true' }}
237
-
238
- # ── The engine GATE FAMILY is resolved FROM CONFIG, not carried here ─────────
239
- # CI is NOT a special policy surface (ADR ci-config-policy-and-gate-family §5):
240
- # it runs the SAME engine gates, resolved through flag > env > per-repo > global
241
- # > default. The SAME dorfl.json the laptop uses applies here. This
242
- # workflow emits NO DORFL_AUTO_BUILD / DORFL_AUTO_TASK /
243
- # DORFL_OBSERVATION_TRIAGE / DORFL_SURFACE_BLOCKERS line on a
244
- # SCHEDULE/PUSH tick, so the env layer carries NO defaults there — your committed
245
- # dorfl.json wins (then the global config, then the strict built-in
246
- # defaults autoBuild:false / autoTask:false / observationTriage:'off' /
247
- # surfaceBlockers:false). The ONE exception is a manual \`workflow_dispatch\` where
248
- # you fill a gate override input (above): the per-job step then exports that ONE
249
- # DORFL_* for that run only (blank input ⇒ still nothing). The override
250
- # MUST be wired into every job that resolves the gate family — including the
251
- # \`enumerate\` scan (it gates the matrix pools via observationTriage/surfaceBlockers
252
- # /autoTask/autoBuild), not just the agent-running jobs — or the override is inert
253
- # for the very pools it targets. To enable CI autonomy durably, set the gate(s) in
254
- # dorfl.json (applies everywhere) — NOT by re-running install-ci (ADR §6:
255
- # install-ci is one-time).
258
+ # ── The engine GATE FAMILY is resolved FROM CONFIG, not carried here ─────────
259
+ # CI is NOT a special policy surface (ADR ci-config-policy-and-gate-family §5):
260
+ # it runs the SAME engine gates, resolved through flag > env > per-repo > global
261
+ # > default. This workflow emits NO DORFL_AUTO_BUILD / DORFL_AUTO_TASK /
262
+ # DORFL_OBSERVATION_TRIAGE / DORFL_SURFACE_BLOCKERS line on a SCHEDULE/PUSH tick,
263
+ # so your committed dorfl.json wins (then the global config, then the strict
264
+ # built-in defaults autoBuild:false / autoTask:false / observationTriage:'off' /
265
+ # surfaceBlockers:false). The ONE exception is a manual \`workflow_dispatch\` where
266
+ # you fill a gate override input (above): \`enumerate\` exports it for its scan and
267
+ # \`dispatch\` forwards it to every item run. To enable CI autonomy durably, set
268
+ # the gate(s) in dorfl.json (applies everywhere) — NOT by re-running install-ci
269
+ # (ADR §6: install-ci is one-time).
256
270
 
257
271
  jobs:
258
- # ── ENUMERATE (propose only) ────────────────────────────────────────────────
259
- # Build the DYNAMIC matrix from the eligible-pool scan. \`scan --json\` reports
260
- # BOTH the registry/hub-mirror pool (\`repos[].items[]\`, \`repos[].specs[]\`,
261
- # \`repos[].lifecycle\`) AND the in-place working checkout (\`cwd.repo.items[]\`,
262
- # \`cwd.repo.specs[]\`, \`cwd.repo.lifecycle\`); CI runs IN-PLACE so the items live
263
- # in the latter (a fresh runner has no registered mirror). \`jq\` unions + dedups
264
- # the build/task pools AND the LIFECYCLE pools into a deduplicated GitHub
265
- # Actions matrix list of explicit \`task:<slug>\` / \`spec:<slug>\` / \`obs:<slug>\`
266
- # ids (CI MUST use explicit prefixes). The lifecycle legs run the WHOLE
267
- # answer-loop in propose mode (not only merge): \`obs:\` (triage untriaged
268
- # observations), surface \`task:\`/\`spec:\` (\`needsAnswers\`, no answered sidecar)
269
- # AND apply \`task:\`/\`spec:\` (\`needsAnswers\`, answered sidecar — consume the
270
- # committed answer, closing the on-answer \`push: work/questions/**\` loop). Each
271
- # becomes one matrix leg → one independent \`advance --propose\`. Skipped in merge
272
- # mode. Inert with calm-default gates (empty triage/surface pools).
272
+ # ── ENUMERATE ───────────────────────────────────────────────────────────────
273
+ # List the eligible items. \`scan --json\` reports BOTH the registry/hub-mirror
274
+ # pool (\`repos[].items[]\`, \`repos[].specs[]\`, \`repos[].lifecycle\`) AND the
275
+ # in-place working checkout (\`cwd.repo.items[]\`, \`cwd.repo.specs[]\`,
276
+ # \`cwd.repo.lifecycle\`); CI runs IN-PLACE so the items live in the latter (a
277
+ # fresh runner has no registered mirror). \`jq\` unions + dedups the build/task
278
+ # pools AND the LIFECYCLE pools into a list of explicit \`task:<slug>\` /
279
+ # \`spec:<slug>\` / \`obs:<slug>\` ids (CI MUST use explicit prefixes): \`obs:\`
280
+ # (triage untriaged observations), surface \`task:\`/\`spec:\` (\`needsAnswers\`,
281
+ # no answered sidecar) AND apply \`task:\`/\`spec:\`/\`observation:\` (an answered
282
+ # sidecar — consume the committed answer, closing the on-answer
283
+ # \`push: work/questions/**\` loop). Read-only: it runs no agent and writes
284
+ # nothing.
273
285
  enumerate:
274
286
  runs-on: ubuntu-latest
287
+ permissions:
288
+ contents: read
275
289
  outputs:
276
290
  items: \${{ steps.scan.outputs.items }}
277
291
  any: \${{ steps.scan.outputs.any }}
278
- # Dynamic GitHub-backstop cap (spec
279
- # \`graceful-pre-timeout-wip-checkpoint\`): the agent-leg jobs
280
- # (advance-propose / advance-merge) read this as their
281
- # \`timeout-minutes\` — replacing the retired static
282
- # \`legTimeoutMinutes\`. Computed at run time from the committed
283
- # \`dorfl.json\` (via \`dorfl config --json\`) as
284
- # \`agentDeadlineMinutes + checkpointHeadroomMinutes\`, so an edit to
285
- # \`dorfl.json\` reflects EVERYWHERE (the internal deadline the legs
286
- # honour AND this GitHub cap) on the NEXT tick — no \`install-ci\`
287
- # re-run, no baked-in YAML number.
288
- githubTimeout: \${{ steps.timeout.outputs.githubTimeout }}
289
292
  steps:
290
293
  - uses: ${pinnedUses(ACTION_PINS.checkout)}
291
294
  with:
292
295
  fetch-depth: 0
293
- - uses: ./.github/actions/dorfl-setup
294
- - name: apply dispatch gate overrides (one-shot, this run only)
295
- # MUST run BEFORE \`scan\`: scan's matrix pools are gated by the SAME engine
296
- # gate family (autoTask/autoBuild + lifecycle observationTriage/
297
- # surfaceBlockers), so an override that does not reach this job produces an
298
- # empty matrix and is silently inert. \`if:\` + the inner \`[ -n ... ]\` guard
299
- # keep schedule/push (and a blank dispatch field) exporting NOTHING — an
300
- # empty DORFL_* would make env-config coercion throw. The inputs reach the
301
- # shell through \`env:\` as DATA, never as \`\${{ }}\` text spliced into the
302
- # script (GitHub's script-injection guidance), and the \`case\` guard stops a
303
- # value from appending its own lines to $GITHUB_ENV. Both only matter if an
304
- # input ever stops being a fixed \`type: choice\`; for the choices below the
305
- # step writes exactly what it always did.
306
- if: \${{ github.event_name == 'workflow_dispatch' }}
307
- env:
308
- DISPATCH_AUTO_BUILD: \${{ github.event.inputs.autoBuild }}
309
- DISPATCH_AUTO_TASK: \${{ github.event.inputs.autoTask }}
310
- DISPATCH_OBSERVATION_TRIAGE: \${{ github.event.inputs.observationTriage }}
311
- DISPATCH_SURFACE_BLOCKERS: \${{ github.event.inputs.surfaceBlockers }}
312
- run: |
313
- case "\${DISPATCH_AUTO_BUILD}\${DISPATCH_AUTO_TASK}\${DISPATCH_OBSERVATION_TRIAGE}\${DISPATCH_SURFACE_BLOCKERS}" in
314
- *$'\\n'*|*$'\\r'*) echo "::error::a dispatch gate override contains a line break"; exit 1 ;;
315
- esac
316
- [ -n "\${DISPATCH_AUTO_BUILD}" ] && echo "DORFL_AUTO_BUILD=\${DISPATCH_AUTO_BUILD}" >> "$GITHUB_ENV"
317
- [ -n "\${DISPATCH_AUTO_TASK}" ] && echo "DORFL_AUTO_TASK=\${DISPATCH_AUTO_TASK}" >> "$GITHUB_ENV"
318
- [ -n "\${DISPATCH_OBSERVATION_TRIAGE}" ] && echo "DORFL_OBSERVATION_TRIAGE=\${DISPATCH_OBSERVATION_TRIAGE}" >> "$GITHUB_ENV"
319
- [ -n "\${DISPATCH_SURFACE_BLOCKERS}" ] && echo "DORFL_SURFACE_BLOCKERS=\${DISPATCH_SURFACE_BLOCKERS}" >> "$GITHUB_ENV"
320
- true
296
+ - uses: ${WRITER_SETUP_ACTION_USES}
297
+ ${enumerateGate}
321
298
  - id: scan
322
- # Enumerate eligible items as namespaced ids, one matrix leg per id. CI
323
- # uses explicit \`task:\` / \`spec:\` / \`obs:\` prefixes, never bare (ADR
324
- # command-surface §3a). Eligible TASKS ⇒ \`task:<slug>\` legs (\`advance\`
325
- # builds them); TASKABLE SPECS ⇒ \`spec:<slug>\` legs (\`advance\` auto-tasks
326
- # them, capability B — \`DORFL_AUTO_TASK\` above). LIFECYCLE pools:
327
- # \`lifecycle.triage[]\` ⇒ \`obs:<slug>\` (the \`obs:\` prefix is fixed here),
328
- # \`lifecycle.surface[]\` + \`lifecycle.apply[]\` ⇒ \`.namespace + ":" + .slug\`
329
- # (surface: \`task:\`/\`spec:\` blocker-question legs; apply: \`task:\`/\`spec:\`
330
- # AND \`observation:\` legs — an answered observation sidecar is consumed by
331
- # the apply rung too, so it MUST carry through here). CI runs IN-PLACE, so we use
332
- # \`scan --here\`: it reports ONLY the cwd checkout (\`cwd.repo.*\`) and SKIPS
333
- # the cross-repo registry loop (no N-mirror fetches; a fresh runner has no
334
- # registered mirror anyway). The \`cwd\` arbiter is the checkout's own clone,
335
- # so the fetch-first is a near-noop. \`jq\` still reads the (now-empty)
336
- # \`repos[]\` branches harmlessly; we union + dedup so the matrix has one
337
- # leg per item.
299
+ # Enumerate eligible items as namespaced ids, one item run per id.
300
+ # Eligible TASKS ⇒ \`task:<slug>\` (\`advance\` builds them); TASKABLE SPECS
301
+ # ⇒ \`spec:<slug>\` (\`advance\` auto-tasks them, capability B). LIFECYCLE
302
+ # pools: \`lifecycle.triage[]\` ⇒ \`obs:<slug>\`, \`lifecycle.surface[]\` +
303
+ # \`lifecycle.apply[]\` ⇒ \`.namespace + ":" + .slug\` (an answered
304
+ # observation sidecar is consumed by the apply rung too, so
305
+ # \`observation:\` MUST carry through here). \`scan --here\` reports ONLY
306
+ # the cwd checkout (\`cwd.repo.*\`) and skips the cross-repo registry
307
+ # loop; \`jq\` still reads the (now-empty) \`repos[]\` branches harmlessly.
338
308
  run: |
339
309
  items="$(dorfl scan --json --here \\
340
310
  | jq -c '[(.repos[].items[]?, .cwd.repo.items[]?) | select(.eligibility.eligible == true) | "task:" + .slug] + [(.repos[].specs[]?, .cwd.repo.specs[]?) | select(.eligibility.eligible == true) | "spec:" + .slug] + [(.repos[].lifecycle.triage[]?, .cwd.repo.lifecycle.triage[]?) | "obs:" + .slug] + [(.repos[].lifecycle.surface[]?, .cwd.repo.lifecycle.surface[]?, .repos[].lifecycle.apply[]?, .cwd.repo.lifecycle.apply[]?) | .namespace + ":" + .slug] | unique')"
@@ -344,192 +314,76 @@ jobs:
344
314
  else
345
315
  echo "any=false" >> "$GITHUB_OUTPUT"
346
316
  fi
347
- - id: timeout
348
- # Dynamic GitHub-backstop cap (spec
349
- # \`graceful-pre-timeout-wip-checkpoint\` — step 5): read the committed
350
- # \`dorfl.json\` via \`dorfl config --json\` (in-place, so the resolution
351
- # chain reads THIS repo's file exactly like \`scan --here\`), and emit
352
- # \`githubTimeout = agentDeadlineMinutes + checkpointHeadroomMinutes\` as
353
- # a job OUTPUT the agent-leg jobs consume via
354
- # \`timeout-minutes: \${{ needs.enumerate.outputs.githubTimeout }}\`. So
355
- # the SAME committed config drives BOTH the legs' internal deadline AND
356
- # this GitHub cap — edit + commit, next tick uses it for both, no
357
- # \`install-ci\` re-run, no baked YAML number. \`dorfl.json\` is the
358
- # single source read at run time by BOTH consumers.
359
- run: |
360
- cap="$(dorfl config --json \\
361
- | jq '.agentDeadlineMinutes + .checkpointHeadroomMinutes')"
362
- echo "githubTimeout=\${cap}" >> "$GITHUB_OUTPUT"
363
317
 
364
- # ── PROPOSE: a MATRIX of independent jobs (one PR per item) ──────────────────
365
- # True parallelism — each item is an independent PR, so a matrix is the right
366
- # parallel shape and \`-n\` is NOT needed. Each leg passes \`--propose\`, tying the
367
- # integration mode to THIS shape: it sits at the top of the precedence chain, so
368
- # the workflow mode always wins over the repo config default and a leg can NEVER
369
- # merge to main. Explicit \`task:\`/\`spec:\` prefixes only, never bare.
370
- advance-propose:
318
+ # ── DISPATCH: one dorfl-item-dispatch.yml run per item ──────────────────────
319
+ # \`actions: write\` and NOTHING else, NO checkout and NO setup: no repository
320
+ # code runs next to \`actions: write\`, only the runner's preinstalled \`gh\` and
321
+ # \`jq\`. Each item run joins the slot dorfl-slot-<index mod maxParallel>
322
+ # (\`queue: max\`), so at most maxParallel (install-ci --max-parallel) item runs
323
+ # execute at once: each spawns a full agent session, and an unbounded fan-out
324
+ # exhausts the model provider's rate limit and thrashes the arbiter-main CAS.
325
+ # An item whose \`dorfl-item <item>\` run has not completed yet is SKIPPED (a
326
+ # tick ends as soon as it has dispatched, so the next tick can come while
327
+ # earlier runs still wait in a slot); a duplicate that slips through a race is
328
+ # harmless (its lock job re-classifies at the fresh arbiter tip). Every value
329
+ # reaches the script through \`env:\`; the log lists each item, its slot and
330
+ # whether it was dispatched or skipped.
331
+ dispatch:
371
332
  needs: enumerate
372
- if: \${{ (github.event.inputs.integrationMode || 'propose') == 'propose' && needs.enumerate.outputs.any == 'true' }}
333
+ if: \${{ needs.enumerate.outputs.any == 'true' }}
373
334
  runs-on: ubuntu-latest
374
- # Per-leg wall-clock cap: the DYNAMIC GitHub backstop the enumerate job
375
- # emits (spec \`graceful-pre-timeout-wip-checkpoint\`), computed at run
376
- # time from the committed \`dorfl.json\` as
377
- # \`agentDeadlineMinutes + checkpointHeadroomMinutes\`. It is a STRICTLY
378
- # LARGER backstop than the dorfl-internal deadline the leg self-stops at
379
- # (which SAVES its WIP + auto-continues on the next tick), so the GitHub
380
- # SIGKILL only fires if the graceful save itself wedges. Editing
381
- # \`dorfl.json\` flips BOTH the internal deadline AND this cap on the
382
- # next tick — no \`install-ci\` re-run, no baked-in YAML number.
383
- # A job output is always a STRING; \`timeout-minutes\` needs an INTEGER, so
384
- # coerce it with \`fromJson(...)\` (the same pattern the \`item\` matrix uses).
385
- # A raw \`\${{ ... }}\` substitution makes GitHub reject it ("Unexpected
386
- # value '45'").
387
- timeout-minutes: \${{ fromJson(needs.enumerate.outputs.githubTimeout) }}
388
- strategy:
389
- # Independent PRs: one failing item must NOT cancel the others.
390
- fail-fast: false
391
- # Cap concurrent legs (config \`maxParallel\`, install-ci --max-parallel):
392
- # each leg spawns a FULL agent session (build + Gate-2 review), so an
393
- # unbounded fan-out over a large item set exhausts the model-provider API
394
- # rate limit (429s that strand legs as transient-infra stuck) AND thrashes
395
- # the arbiter-main CAS. A bounded fan-out drains steadily instead.
396
- max-parallel: ${config.maxParallel}
397
- matrix:
398
- item: \${{ fromJson(needs.enumerate.outputs.items) }}
335
+ permissions:
336
+ actions: write
399
337
  steps:
400
- - uses: ${pinnedUses(ACTION_PINS.checkout)}
401
- with:
402
- fetch-depth: 0
403
- - uses: ./.github/actions/dorfl-setup${setupWith}
404
- - name: apply dispatch gate overrides (one-shot, this run only)
405
- # Mirror of the enumerate-job override (see there): export each gate's
406
- # DORFL_* ONLY on a workflow_dispatch with a non-blank input, so the
407
- # one-shot override also reaches the \`advance\` leg that builds the item.
408
- if: \${{ github.event_name == 'workflow_dispatch' }}
338
+ - name: dispatch one item run per item
409
339
  env:
410
- DISPATCH_AUTO_BUILD: \${{ github.event.inputs.autoBuild }}
411
- DISPATCH_AUTO_TASK: \${{ github.event.inputs.autoTask }}
412
- DISPATCH_OBSERVATION_TRIAGE: \${{ github.event.inputs.observationTriage }}
413
- DISPATCH_SURFACE_BLOCKERS: \${{ github.event.inputs.surfaceBlockers }}
340
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
341
+ REPO: \${{ github.repository }}
342
+ DEFAULT_BRANCH: \${{ github.event.repository.default_branch || 'main' }}
343
+ ITEMS: \${{ needs.enumerate.outputs.items }}
344
+ MAX_PARALLEL: '${config.maxParallel}'
345
+ INTEGRATION_MODE: \${{ github.event.inputs.integrationMode || 'propose' }}
346
+ ${dispatchGateEnv}
414
347
  run: |
415
- case "\${DISPATCH_AUTO_BUILD}\${DISPATCH_AUTO_TASK}\${DISPATCH_OBSERVATION_TRIAGE}\${DISPATCH_SURFACE_BLOCKERS}" in
416
- *$'\\n'*|*$'\\r'*) echo "::error::a dispatch gate override contains a line break"; exit 1 ;;
417
- esac
418
- [ -n "\${DISPATCH_AUTO_BUILD}" ] && echo "DORFL_AUTO_BUILD=\${DISPATCH_AUTO_BUILD}" >> "$GITHUB_ENV"
419
- [ -n "\${DISPATCH_AUTO_TASK}" ] && echo "DORFL_AUTO_TASK=\${DISPATCH_AUTO_TASK}" >> "$GITHUB_ENV"
420
- [ -n "\${DISPATCH_OBSERVATION_TRIAGE}" ] && echo "DORFL_OBSERVATION_TRIAGE=\${DISPATCH_OBSERVATION_TRIAGE}" >> "$GITHUB_ENV"
421
- [ -n "\${DISPATCH_SURFACE_BLOCKERS}" ] && echo "DORFL_SURFACE_BLOCKERS=\${DISPATCH_SURFACE_BLOCKERS}" >> "$GITHUB_ENV"
422
- true
423
- - name: advance one item in-place (propose ⇒ opens a PR)
424
- # In-place in this checkout (no --isolated/--remote): the CI container IS
425
- # the isolation. \`--propose\` can ONLY ride a matrix leg, never the merge
426
- # job, so a parallel merge-to-main is structurally impossible.
427
- # \`--propose\` shells \`gh\` to open the PR, which reads \`GH_TOKEN\` from the
428
- # env (a workflow PERMISSION is not a credential). Prefer a dedicated
429
- # \`DORFL_GH_TOKEN\` secret (a PAT / App token) so PRs carry YOUR
430
- # identity AND trigger downstream workflows (PRs opened with the built-in
431
- # \`GITHUB_TOKEN\` are \`github-actions[bot]\` and do NOT trigger further
432
- # \`on: pull_request\` runs). Falls back to the auto \`GITHUB_TOKEN\` so it
433
- # works zero-config; the job's \`pull-requests: write\` scopes that token.
434
- env:
435
- GH_TOKEN: \${{ secrets.DORFL_GH_TOKEN || secrets.GITHUB_TOKEN }}
436
- # The item id is a slug read from work/ (frontmatter \`slug:\` or the
437
- # file name). dorfl sanitises the slugs it mints, but a hand-written
438
- # one is only checked when \`advance\` resolves it, so it reaches the
439
- # shell as DATA through env, never as \`\${{ matrix.item }}\` text
440
- # spliced into the script.
441
- WORK_ITEM: \${{ matrix.item }}
442
- # \`--watch\` streams the build agent's high-signal turns (assistant text,
443
- # tool calls, finish) into THIS job log live, so the run shows the agent
444
- # working instead of freezing after "Start work". A read-only observer (no
445
- # outcome/gate/git effect). It fits because each matrix leg names ONE item
446
- # (one pi session to tail) — the merge job is now a per-item matrix too
447
- # (see below) and streams the same way.
448
- run: dorfl advance "\${WORK_ITEM}" --propose --watch --arbiter origin
348
+ ${DISPATCH_SCRIPT}
449
349
 
450
- # ── MERGE: a MATRIX of independent jobs (parallel build/gate/review, serialised land) ──
451
- # SPEC \`land-time-reverify-and-parallel-merge-ceiling\` (stories 4 + 6): each item
452
- # gets its own leg; build/gate/review run concurrently across siblings; the LAND
453
- # TAIL is serialised by the engine's \`mergeRetries\` CAS-retry loop — the
454
- # git-alone floor — NOT by the workflow shape. A non-fast-forward push triggers
455
- # re-rebase + re-gate + retry up to the resolved \`mergeRetries\` cap; never a
456
- # \`--force\`. NO host-specific \`concurrency:\` group on this job (it would be
457
- # load-bearing for cross-job safety, breaking the floor framing).
458
- # The fully-autonomous-to-main path (this job + all gates on) is still a LOUD,
459
- # NON-DEFAULT opt-in: the default integrationMode is \`propose\`, so reaching
460
- # merge-to-main requires deliberately dispatching/pinning \`merge\`.
461
- advance-merge:
462
- needs: enumerate
463
- if: \${{ (github.event.inputs.integrationMode || 'propose') == 'merge' && needs.enumerate.outputs.any == 'true' }}
350
+ # ── SURFACE MERGE QUESTIONS (no agent) ──────────────────────────────────────
351
+ # Ask a merge question (a \`kind: merge\` sidecar entry the human answers
352
+ # \`merge | hold | drop\`) for every unmerged \`work/task-<slug>\` branch with no
353
+ # open PR (an open PR already is the land decision), whose task rests in
354
+ # tasks/ready/ or tasks/backlog/, whose lock is free or kept by its finished
355
+ # propose build, and whose tip carries the done-move; then publish the
356
+ # questions to main. An answered \`merge\` is enumerated like any answered
357
+ # sidecar and lands through the item run's apply rung. Gated by the
358
+ # \`mergeQuestions\` config (off|ask, default ask): \`off\` surfaces nothing.
359
+ # Deterministic: it runs no agent, so the writer-role setup (Node and dorfl
360
+ # only). It fetches the arbiter before listing the branches. \`gh\` reads the
361
+ # open PRs with the built-in token (\`pull-requests: read\`); a failed PR
362
+ # listing fails the job loudly rather than skipping every branch silently.
363
+ surface-merge-questions:
464
364
  runs-on: ubuntu-latest
465
- # Per-leg wall-clock cap: the DYNAMIC GitHub backstop the enumerate job
466
- # emits (see advance-propose for the rationale). \`fromJson(...)\` coerces the
467
- # string job output to the INTEGER \`timeout-minutes\` requires.
468
- timeout-minutes: \${{ fromJson(needs.enumerate.outputs.githubTimeout) }}
469
- strategy:
470
- # Independent landings: one failing item must NOT cancel the others; a
471
- # loser of the CAS race re-rebases + re-gates + retries.
472
- fail-fast: false
473
- # Cap concurrent legs (config \`maxParallel\`, see advance-propose): a FULL
474
- # agent session per leg, so bound the fan-out to avoid API-429s + CAS thrash.
475
- max-parallel: ${config.maxParallel}
476
- matrix:
477
- item: \${{ fromJson(needs.enumerate.outputs.items) }}
365
+ permissions:
366
+ contents: write
367
+ pull-requests: read
478
368
  steps:
479
369
  - uses: ${pinnedUses(ACTION_PINS.checkout)}
480
370
  with:
481
371
  fetch-depth: 0
482
- - uses: ./.github/actions/dorfl-setup${setupWith}
483
- - name: apply dispatch gate overrides (one-shot, this run only)
484
- # Mirror of the enumerate-job override (see there): export each gate's
485
- # DORFL_* ONLY on a workflow_dispatch with a non-blank input. The
486
- # merge job re-scans the pool inside \`advance -n\`, so it needs the override
487
- # too for the lifecycle/task pools to reflect it.
488
- if: \${{ github.event_name == 'workflow_dispatch' }}
489
- env:
490
- DISPATCH_AUTO_BUILD: \${{ github.event.inputs.autoBuild }}
491
- DISPATCH_AUTO_TASK: \${{ github.event.inputs.autoTask }}
492
- DISPATCH_OBSERVATION_TRIAGE: \${{ github.event.inputs.observationTriage }}
493
- DISPATCH_SURFACE_BLOCKERS: \${{ github.event.inputs.surfaceBlockers }}
494
- run: |
495
- case "\${DISPATCH_AUTO_BUILD}\${DISPATCH_AUTO_TASK}\${DISPATCH_OBSERVATION_TRIAGE}\${DISPATCH_SURFACE_BLOCKERS}" in
496
- *$'\\n'*|*$'\\r'*) echo "::error::a dispatch gate override contains a line break"; exit 1 ;;
497
- esac
498
- [ -n "\${DISPATCH_AUTO_BUILD}" ] && echo "DORFL_AUTO_BUILD=\${DISPATCH_AUTO_BUILD}" >> "$GITHUB_ENV"
499
- [ -n "\${DISPATCH_AUTO_TASK}" ] && echo "DORFL_AUTO_TASK=\${DISPATCH_AUTO_TASK}" >> "$GITHUB_ENV"
500
- [ -n "\${DISPATCH_OBSERVATION_TRIAGE}" ] && echo "DORFL_OBSERVATION_TRIAGE=\${DISPATCH_OBSERVATION_TRIAGE}" >> "$GITHUB_ENV"
501
- [ -n "\${DISPATCH_SURFACE_BLOCKERS}" ] && echo "DORFL_SURFACE_BLOCKERS=\${DISPATCH_SURFACE_BLOCKERS}" >> "$GITHUB_ENV"
502
- true
503
- - name: advance one item in-place (merge ⇒ rebase + CAS land on main)
504
- # In-place (no --isolated/--remote). \`--merge\` ties the integration mode
505
- # to THIS (matrix) shape: it sits at the top of the precedence chain, so
506
- # the workflow mode always wins over the repo's \`dorfl.json\` default.
507
- # Cross-job land safety comes from the engine's CAS-retry loop, not from
508
- # this workflow's job shape. \`gh\` (merge / PR housekeeping) reads
509
- # \`GH_TOKEN\` from the env.
372
+ - uses: ${WRITER_SETUP_ACTION_USES}
373
+ - name: surface merge questions for unmerged work/* branches (surface-merge-questions)
510
374
  env:
511
- GH_TOKEN: \${{ secrets.DORFL_GH_TOKEN || secrets.GITHUB_TOKEN }}
512
- # The item id is a slug read from work/ (frontmatter \`slug:\` or the
513
- # file name). dorfl sanitises the slugs it mints, but a hand-written
514
- # one is only checked when \`advance\` resolves it, so it reaches the
515
- # shell as DATA through env, never as \`\${{ matrix.item }}\` text
516
- # spliced into the script.
517
- WORK_ITEM: \${{ matrix.item }}
518
- # \`--watch\` streams the build agent's high-signal turns into THIS job
519
- # log live; each leg names ONE item, so it fits the same way it does on
520
- # the propose legs.
521
- run: dorfl advance "\${WORK_ITEM}" --merge --watch --arbiter origin
375
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
376
+ run: dorfl surface-merge-questions --arbiter origin
522
377
 
523
378
  # ── REAP merged remote work/* branches (capability F, the hygiene sweep) ─────
524
379
  # PRESERVED from the seed (NOT a separate gc-sweep workflow): the provider-
525
380
  # agnostic counterpart of the worktree reaper. Deletes remote \`work/<slug>\`
526
381
  # branches that are PROVABLY MERGED into \`origin/main\`, so an out-of-band
527
382
  # human/UI PR merge stops leaving its branch lingering on the arbiter.
528
- # Independent of the integration mode (both propose- and merge-mode repos
529
- # accumulate out-of-band-merged branches), so it runs on EVERY tick regardless
383
+ # Independent of the integration mode, so it runs on EVERY tick regardless
530
384
  # of \`integrationMode\` — gated ONLY by the \`sweepMergedBranches\` opt-out (ON by
531
385
  # default). It NEVER touches an un-merged/in-flight branch and NEVER \`--force\`s.
532
- # Plain git, so it works against any provider (incl. a \`--bare\` arbiter).
386
+ # It runs no agent: the writer-role setup (Node and dorfl only).
533
387
  #
534
388
  # OPTIONAL belt-and-suspenders for a GitHub arbiter: enable the repo-level
535
389
  # "Automatically delete head branches" setting (\`delete_branch_on_merge\`), owned
@@ -539,11 +393,13 @@ jobs:
539
393
  reap-merged-branches:
540
394
  if: \${{ (github.event.inputs.sweepMergedBranches || 'true') == 'true' }}
541
395
  runs-on: ubuntu-latest
396
+ permissions:
397
+ contents: write
542
398
  steps:
543
399
  - uses: ${pinnedUses(ACTION_PINS.checkout)}
544
400
  with:
545
401
  fetch-depth: 0
546
- - uses: ./.github/actions/dorfl-setup
402
+ - uses: ${WRITER_SETUP_ACTION_USES}
547
403
  - name: reap merged remote work/* branches + orphan sidecars (gc --remote-branches)
548
404
  # Deletes ONLY branches provably merged into origin/main; reports
549
405
  # deleted-vs-retained-with-reason. Safe to run every tick (idempotent).
@@ -576,10 +432,15 @@ export function validateAdvanceLifecycleWorkflow(text) {
576
432
  .split('\n')
577
433
  .filter((line) => !/^\s*#/.test(line))
578
434
  .join('\n');
579
- // --- Always invokes `advance`, NEVER `do` ----------------------------------
580
- require('invokes-advance', /dorfl advance\b/.test(text), 'the workflow must invoke the `advance` driver (CI always runs `advance`).');
435
+ // --- The tick runs NO agent: it dispatches one item run per item ------------
436
+ require('dispatches-item-runs', /gh workflow run dorfl-item-dispatch\.yml\b/.test(operative), 'the `dispatch` job must start one `dorfl-item-dispatch.yml` run per item ' +
437
+ '(`gh workflow run dorfl-item-dispatch.yml`), which calls dorfl-item.yml.');
438
+ require('no-agent-verb-in-tick', !/dorfl (?:advance|do|intake|run)\b/.test(operative), 'the tick itself must run NO agent-spawning verb: `advance` runs inside ' +
439
+ 'the per-item workflow (lock, agent, apply jobs).');
581
440
  require('never-invokes-do', !/dorfl do\b/.test(operative), 'the workflow must NEVER invoke `do` directly (CI always invokes `advance`, ' +
582
441
  'ADR ci-config-policy-and-gate-family §1).');
442
+ require('no-matrix', !/\bstrategy:\s*[\s\S]*?matrix:/.test(operative), 'no job may use a `strategy.matrix`: one item per workflow run (a matrix ' +
443
+ 'shares one artifact namespace across items, decision 1).');
583
444
  // --- Triggers: cron + workflow_dispatch + the on-answer-committed push -------
584
445
  require('trigger-cron', /\bschedule:\s*[\s\S]*?-\s*cron:/.test(text), 'must trigger on a cron schedule (`on.schedule[].cron`).');
585
446
  require('trigger-workflow-dispatch', /\bworkflow_dispatch:/.test(text), 'must trigger on `workflow_dispatch` (manual catch-up/debug).');
@@ -589,95 +450,68 @@ export function validateAdvanceLifecycleWorkflow(text) {
589
450
  // promptly. This is what `advance` adds over the build/task tick.
590
451
  require('trigger-on-answer-committed', /\bpush:\s*[\s\S]*?paths:[\s\S]*?work\/questions\//.test(text), 'must trigger on-answer-committed (a push touching `work/questions/**`) — ' +
591
452
  'the lifecycle answer loop.');
592
- // --- integrationMode drives BOTH shape and flag (one word, one meaning) -----
453
+ // ... on the DEFAULT BRANCH only: a work-branch push touching a sidecar must
454
+ // never run the lifecycle (observation
455
+ // `advance-lifecycle-push-trigger-lands-an-unreviewed-work-branch-on-main`).
456
+ require('push-pinned-to-main', /\bpush:\s*\n\s+(?:#[^\n]*\n\s+)*branches:\s*\n\s+-\s*main\b/.test(text), 'the on-answer-committed `push` trigger must be pinned to the default ' +
457
+ 'branch (`branches: [main]`).');
458
+ // --- integrationMode is forwarded to every item run (one word) -------------
593
459
  require('integration-mode-one-word', /integrationMode:/.test(text) &&
594
- /github\.event\.inputs\.integrationMode/.test(text), 'the dispatch input must be `integrationMode` (one word driving BOTH the ' +
595
- 'flag and the derived job shape).');
596
- // --- propose ⇒ a DYNAMIC matrix enumerated via `scan --json` ----------------
597
- require('propose-matrix', /strategy:\s*[\s\S]*?matrix:/.test(text), '`propose` mode must emit a MATRIX of jobs (`strategy.matrix`).');
598
- require('propose-enumerates-via-scan', /dorfl scan --json/.test(text), 'the matrix items must be ENUMERATED via the eligible-pool scan ' +
599
- '(`dorfl scan --json`).');
600
- require('propose-one-advance-per-item', /WORK_ITEM:\s*\$\{\{\s*matrix\.item\s*\}\}[\s\S]*?dorfl advance "\$\{WORK_ITEM\}"/.test(text), 'each matrix leg must run one `dorfl advance <matrix item>` ' +
601
- '(one PR per item), reading the item from the step env (`WORK_ITEM`).');
602
- // The item id is a slug that may be hand-written (only minted slugs are
603
- // sanitised), so it must reach the shell as DATA through `env:`, never as `${{ }}` text
604
- // spliced into the `run:` script (GitHub Actions script injection).
605
- require('matrix-item-not-spliced-into-run', !/dorfl advance "?\$\{\{/.test(operative), 'the matrix item must NOT be interpolated into the `run:` script as ' +
606
- '`${{ matrix.item }}` (script injection): pass it through the step `env:` ' +
607
- 'as `WORK_ITEM` and quote it as `"${WORK_ITEM}"`.');
608
- require('propose-leg-carries-propose-flag', /advance-propose:[\s\S]*?dorfl advance "\$\{WORK_ITEM\}"[^\n]*--propose\b/.test(text), 'each `propose` matrix leg must pass `--propose` so the integration mode is ' +
609
- 'TIED to the matrix shape (a leg can never merge to main / desync from the ' +
610
- 'dispatch mode).');
611
- // --- merge ⇒ a MATRIX per item (parallel build/gate/review, serialised land) -
612
- // SPEC `land-time-reverify-and-parallel-merge-ceiling` stories 4 + 6: build/
613
- // gate/review fan out; the LAND tail is serialised by the engine's
614
- // `mergeRetries` CAS-retry loop (the git-alone floor), NOT the workflow.
615
- require('merge-matrix', /advance-merge:[\s\S]*?strategy:\s*[\s\S]*?matrix:/.test(text), 'the `merge` job must use a MATRIX (parallel build/gate/review per item; ' +
616
- "the land tail is serialised by the engine's `mergeRetries` CAS-retry " +
617
- "loop, not by the workflow's job shape).");
618
- require('merge-leg-carries-merge-flag', /advance-merge:[\s\S]*?dorfl advance "\$\{WORK_ITEM\}"[^\n]*--merge\b/.test(text), 'each `merge` matrix leg must pass `--merge` so the integration mode is ' +
619
- 'TIED to the matrix shape (a leg can never propose-only / desync from the ' +
620
- 'dispatch mode).');
621
- // No host-specific cross-job serialiser on the merge job: a GitHub Actions
622
- // `concurrency:` block there would make cross-job land safety depend on a host
623
- // feature, breaking the git-alone-floor framing (Applied Answer q1).
624
- require('merge-no-host-concurrency-serialiser', !/advance-merge:[\s\S]*?\n\s{4}concurrency:/.test(text), 'the `merge` job must NOT carry a `concurrency:` group: a host-specific ' +
625
- 'serialiser would make the cross-job land safety depend on a GitHub Actions ' +
626
- "feature; the engine's `mergeRetries` CAS-retry loop is the git-alone floor.");
460
+ /github\.event\.inputs\.integrationMode/.test(text), 'the dispatch input must be `integrationMode` (one word, forwarded to ' +
461
+ 'every item run as `--propose`/`--merge`).');
462
+ require('dispatch-forwards-integration-mode', /-f "integrationMode=\$\{INTEGRATION_MODE\}"/.test(operative), 'the `dispatch` job must forward `integrationMode` to each item run.');
463
+ require('dispatch-slot', /-f "slot=\$\{slot\}"/.test(operative) &&
464
+ /i % MAX_PARALLEL/.test(operative), 'the `dispatch` job must give each item run a slot (index mod ' +
465
+ 'maxParallel), the concurrency group that caps the parallelism.');
466
+ require('dispatch-skips-active-runs', /gh run list[^\n]*--workflow dorfl-item-dispatch\.yml[^\n]*displayTitle,status/.test(operative), 'the `dispatch` job must list the non-completed `dorfl-item-dispatch.yml` ' +
467
+ 'runs and skip an item whose `dorfl-item <item>` run is still active.');
468
+ require('dispatch-actions-write-only', /\n {2}dispatch:[\s\S]*?\n {4}permissions:\s*\n {6}actions: write\s*\n {4}steps:/.test(text), 'the `dispatch` job must hold `actions: write` and nothing else.');
469
+ require('dispatch-no-checkout-no-setup', !/\n {2}dispatch:[\s\S]*?(?:\n {2}\S|$)/
470
+ .exec(operative)?.[0]
471
+ .match(/uses:/), 'the `dispatch` job must have NO checkout and NO setup ' +
472
+ '(no repository code runs next to `actions: write`).');
473
+ require('item-values-not-spliced-into-run', !/gh workflow run[^\n]*\$\{\{/.test(operative), 'the item list and inputs must reach the dispatch script through `env:`, ' +
474
+ 'never as `${{ }}` text in the `run:` script.');
627
475
  // --- The DORFL_* gate family must NOT be carried as workflow env -----
628
476
  // The workflow emits NO active gate env line for any of AUTO_BUILD / AUTO_TASK
629
477
  // / OBSERVATION_TRIAGE / SURFACE_BLOCKERS: the env layer is the OPTIONAL CI-only
630
478
  // override layer, NOT the carrier of defaults. Emitting any of them would FORCE
631
479
  // env to win over the repo's own dorfl.json (the precedence is
632
480
  // flag > env > per-repo > global > default), silently shadowing per-repo gate
633
- // config in CI — exactly the bug this task closes. A user who genuinely wants a
634
- // CI-SPECIFIC override adds the env var themselves (the opt-in CI override the
635
- // env layer is FOR). Check the OPERATIVE (non-comment) lines so the explanatory
481
+ // config in CI. Check the OPERATIVE (non-comment) lines so the explanatory
636
482
  // header comment that NAMES these keys is not a false positive.
637
- require('no-gate-env-auto-build', !/DORFL_AUTO_BUILD\s*:/.test(operative), 'the workflow must NOT emit an `DORFL_AUTO_BUILD:` env assignment ' +
638
- '(env carries no defaults; the gate is resolved from per-repo config / built-in default).');
639
- require('no-gate-env-auto-task', !/DORFL_AUTO_TASK\s*:/.test(operative), 'the workflow must NOT emit an `DORFL_AUTO_TASK:` env assignment ' +
640
- '(env carries no defaults; the gate is resolved from per-repo config / built-in default).');
641
- require('no-gate-env-observation-triage', !/DORFL_OBSERVATION_TRIAGE\s*:/.test(operative), 'the workflow must NOT emit an `DORFL_OBSERVATION_TRIAGE:` env ' +
642
- 'assignment (env carries no defaults; resolved from per-repo config / built-in default).');
643
- require('no-gate-env-surface-blockers', !/DORFL_SURFACE_BLOCKERS\s*:/.test(operative), 'the workflow must NOT emit an `DORFL_SURFACE_BLOCKERS:` env ' +
644
- 'assignment (env carries no defaults; resolved from per-repo config / built-in default).');
645
- // --- Gate-family DISPATCH OVERRIDES (one-shot, dispatch only) ---------------
646
- // Each gate is exposed as a `workflow_dispatch` input AND its override is wired
647
- // (as a guarded `$GITHUB_ENV` write, NOT a YAML `env:` key — so the
648
- // `no-gate-env-*` invariants above still hold) into EVERY job that resolves the
649
- // gate family: `enumerate` (it gates the matrix pools via `scan`), plus the two
650
- // agent-running jobs. The enumerate wiring is the load-bearing one: without it a
651
- // dispatch override of `observationTriage`/`surfaceBlockers`/`autoTask` produces
652
- // an empty matrix and is silently inert (the bug this task's review caught).
653
- for (const input of [
654
- 'autoBuild',
655
- 'autoTask',
656
- 'observationTriage',
657
- 'surfaceBlockers',
483
+ for (const [id, envVar] of [
484
+ ['no-gate-env-auto-build', 'DORFL_AUTO_BUILD'],
485
+ ['no-gate-env-auto-task', 'DORFL_AUTO_TASK'],
486
+ ['no-gate-env-observation-triage', 'DORFL_OBSERVATION_TRIAGE'],
487
+ ['no-gate-env-surface-blockers', 'DORFL_SURFACE_BLOCKERS'],
658
488
  ]) {
659
- require(`dispatch-${input}-input`, new RegExp(`workflow_dispatch:[\\s\\S]*?inputs:[\\s\\S]*?\\b${input}:`).test(text), `the \`workflow_dispatch\` must carry a \`${input}\` gate-override input.`);
489
+ require(id, !new RegExp(`${envVar}\\s*:`).test(operative), `the workflow must NOT emit an \`${envVar}:\` env assignment ` +
490
+ '(env carries no defaults; the gate is resolved from per-repo config / built-in default).');
660
491
  }
492
+ // --- Gate-family DISPATCH OVERRIDES (one-shot, dispatch only) ---------------
493
+ // Each gate is a `workflow_dispatch` input, exported (a guarded `$GITHUB_ENV`
494
+ // write, NOT a YAML `env:` key) in `enumerate` BEFORE its scan, and forwarded
495
+ // by `dispatch` to every item run (whose lock, agent and apply jobs export it).
661
496
  for (const [input, envVar] of [
662
497
  ['autoBuild', 'DORFL_AUTO_BUILD'],
663
498
  ['autoTask', 'DORFL_AUTO_TASK'],
664
499
  ['observationTriage', 'DORFL_OBSERVATION_TRIAGE'],
665
500
  ['surfaceBlockers', 'DORFL_SURFACE_BLOCKERS'],
666
501
  ]) {
667
- // The override is a guarded shell write of a step-env copy of the input:
668
- // `env: DISPATCH_<X>: ${{ inputs.<x> }}` then
669
- // `[ -n "${DISPATCH_<X>}" ] && echo "<ENV>=${DISPATCH_<X>}" >> $GITHUB_ENV`.
670
- // The input never appears as `${{ }}` text inside the `run:` script.
502
+ require(`dispatch-${input}-input`, new RegExp(`workflow_dispatch:[\\s\\S]*?inputs:[\\s\\S]*?\\b${input}:`).test(text), `the \`workflow_dispatch\` must carry a \`${input}\` gate-override input.`);
671
503
  const dispatchVar = envVar.replace(/^DORFL_/, 'DISPATCH_');
672
504
  const guardedWrite = new RegExp(`${dispatchVar}:\\s*\\$\\{\\{ github\\.event\\.inputs\\.${input} \\}\\}` +
673
505
  `[\\s\\S]*?\\[ -n "\\$\\{${dispatchVar}\\}" \\] && echo "${envVar}=\\$\\{${dispatchVar}\\}"`);
674
506
  require(`dispatch-${input}-guarded-write`, guardedWrite.test(text), `the \`${input}\` override must be a blank-guarded write of \`${envVar}\` ` +
675
507
  'to `$GITHUB_ENV` (blank dispatch input / schedule / push emit nothing).');
508
+ require(`dispatch-forwards-${input}`, new RegExp(`args\\+=\\(-f "${input}=\\$\\{${dispatchVar}\\}"\\)`).test(operative), `the \`dispatch\` job must forward a non-blank \`${input}\` override to ` +
509
+ 'each item run.');
676
510
  }
677
511
  // The ENUMERATE job MUST carry the override before its `scan` step — else the
678
- // matrix pools are built from the un-overridden gates and the override is inert.
512
+ // item list is built from the un-overridden gates and the override is inert.
679
513
  require('enumerate-carries-gate-override', /enumerate:[\s\S]*?DORFL_OBSERVATION_TRIAGE=[\s\S]*?id: scan/.test(text), 'the `enumerate` job must apply the dispatch gate override BEFORE its `scan` ' +
680
- 'step (scan gates the matrix pools by the gate family; otherwise the ' +
514
+ 'step (scan gates the item pools by the gate family; otherwise the ' +
681
515
  'override is silently inert for the lifecycle/task pools).');
682
516
  // The override must be GUARDED by the workflow_dispatch event so schedule/push
683
517
  // runs never even enter the write step.
@@ -694,100 +528,85 @@ export function validateAdvanceLifecycleWorkflow(text) {
694
528
  'agnostic merged-branch sweep).');
695
529
  require('reap-sweep-dispatch-input', /sweepMergedBranches:/.test(text), 'the `sweepMergedBranches` dispatch input (capability F, opt-out) must be ' +
696
530
  'preserved.');
697
- // The ORPHAN-SIDECAR reap (spec
698
- // `agentic-question-resolution-retire-disposition-vocabulary`, US #10) rides
699
- // the SAME scheduled `dorfl gc --remote-branches` invocation, so it provably
700
- // FIRES on the cron tick (not behind an un-passed flag). The reap step that
701
- // runs `gc --remote-branches` MUST be checked out WITH a working tree (the
702
- // orphan sweep is working-tree based: it reads `work/questions/` + the
703
- // lifecycle folders from the checkout). `actions/checkout` provides that, and
704
- // the step's name/comment names the orphan-sidecar duty so the linkage is not
705
- // silently lost on a future template edit.
531
+ // The ORPHAN-SIDECAR reap rides the SAME scheduled `dorfl gc
532
+ // --remote-branches` invocation, which reads the working tree, so the reap
533
+ // job must check one out, and its step must name the orphan-sidecar duty.
706
534
  require('reap-checks-out-working-tree', /reap-merged-branches:[\s\S]*?uses:\s*actions\/checkout/.test(text), 'the reap job must `actions/checkout` a working tree before ' +
707
535
  '`gc --remote-branches`: the orphan-sidecar sweep that rides that invocation ' +
708
536
  'reads `work/questions/` + the lifecycle folders from the checkout.');
709
537
  require('reap-names-orphan-sidecars', /reap-merged-branches:[\s\S]*?orphan sidecar/i.test(text), 'the reap step (running `gc --remote-branches`) must name the ORPHAN ' +
710
538
  'SIDECAR reap it ALSO performs (US #10), so the scheduled invocation that ' +
711
539
  'fires it is visible and not silently dropped on a future edit.');
540
+ require('reap-writer-role', /reap-merged-branches:[\s\S]*?uses:\s*\.\/\.github\/actions\/dorfl-setup-writer\b/.test(text), 'the reap job runs no agent: it must use the writer-role setup ' +
541
+ '(`./.github/actions/dorfl-setup-writer`: Node and dorfl only).');
542
+ // --- The merge-question writer job (no agent) -------------------------------
543
+ // Task `wire-merge-questions-into-the-advance-tick` (decision 5): surfacing is
544
+ // deterministic, so a no-agent writer job runs it; `enumerate` stays
545
+ // read-only.
546
+ require('surface-merge-questions-job', /\n {2}surface-merge-questions:\s*\n {4}runs-on: ubuntu-latest\s*\n {4}permissions:\s*\n {6}contents: write\s*\n {6}pull-requests: read\s*\n {4}steps:/.test(text), 'the workflow must carry the `surface-merge-questions` job (contents: write, ' +
547
+ 'pull-requests: read, no agent) that asks the merge questions.');
548
+ // Gate-3 (PR #459): `gh pr list` needs a credential, or every branch is
549
+ // skipped as `pr-state-unknown` on a GitHub arbiter. The built-in token
550
+ // suffices (a read), so the step carries `GH_TOKEN: secrets.GITHUB_TOKEN`.
551
+ require('surface-merge-questions-gh-token', /surface-merge-questions:[\s\S]*?env:\s*\n\s*GH_TOKEN: \$\{\{ secrets\.GITHUB_TOKEN \}\}\s*\n\s*run: dorfl surface-merge-questions\b/.test(operative), 'the `surface-merge-questions` step must give `gh` a credential ' +
552
+ '(`env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`), else the open-PR listing ' +
553
+ 'fails and every branch is skipped.');
554
+ require('surface-merge-questions-runs-the-command', /surface-merge-questions:[\s\S]*?run: dorfl surface-merge-questions\b/.test(operative), 'the `surface-merge-questions` job must run `dorfl surface-merge-questions`.');
555
+ require('surface-merge-questions-writer-role', /surface-merge-questions:[\s\S]*?uses:\s*\.\/\.github\/actions\/dorfl-setup-writer\b/.test(text), 'the `surface-merge-questions` job runs no agent: it must use the ' +
556
+ 'writer-role setup (`./.github/actions/dorfl-setup-writer`).');
712
557
  // --- CI runs IN-PLACE: no isolation machinery ------------------------------
713
558
  require('no-isolated-flag', !/--isolated\b/.test(operative), 'CI runs IN-PLACE (the container IS the isolation): no `--isolated` flag.');
714
559
  // Guard `--remote` the LAPTOP affordance, but NOT `--remote-branches` (the
715
- // preserved capability-F `gc --remote-branches` sweep): a negative lookahead
716
- // for a following `-`/word char so the gc flag is not a false positive.
560
+ // preserved capability-F `gc --remote-branches` sweep).
717
561
  require('no-remote-flag', !/--remote(?![-\w])/.test(operative), 'CI runs IN-PLACE: no `--remote` flag (laptop-only affordance).');
718
562
  // --- A CI concurrency group ------------------------------------------------
719
563
  require('concurrency-group', /\bconcurrency:\s*[\s\S]*?group:/.test(text), 'must carry a CI `concurrency.group` so overlapping ticks never collide.');
720
- // --- US #9: NO `workflows` permission; cannot self-edit triggers ------------
564
+ // --- Permissions: nothing at workflow level; NO `workflows` permission -----
565
+ require('workflow-permissions-empty', /^permissions: \{\}$/m.test(text), 'the workflow must grant nothing at workflow level (`permissions: {}`); ' +
566
+ 'each job grants its own scopes.');
567
+ require('enumerate-contents-read', /\n {2}enumerate:\s*\n {4}runs-on: ubuntu-latest\s*\n {4}permissions:\s*\n {6}contents: read\s*\n {4}outputs:/.test(text), 'the `enumerate` job must hold `contents: read` only.');
721
568
  require('no-workflows-permission', !/\bworkflows:\s*write\b/.test(text), 'the running job must request NO `workflows` permission (US #9: it can ' +
722
569
  'never edit `.github/workflows/**` / rewrite its own triggers).');
723
570
  require('never-edits-dot-github-workflows', !/\.github\/workflows\//.test(operative), 'no emitted job step may touch `.github/workflows/**` (US #9).');
724
571
  // --- Explicit slug prefixes, never bare ------------------------------------
725
- require('explicit-task-prefix', /"task:" \+ \.slug/.test(text) ||
726
- /task:/.test(text), 'CI must use explicit `task:`/`spec:` slug prefixes, never bare (ADR ' +
572
+ require('explicit-task-prefix', /"task:" \+ \.slug/.test(text), 'CI must use explicit `task:`/`spec:` slug prefixes, never bare (ADR ' +
727
573
  'command-surface-and-journeys §3a).');
728
- // --- The propose `enumerate` matrix must UNION taskable prds --------------
574
+ // --- The `enumerate` list must UNION taskable specs -------------------------
729
575
  // (`ci-propose-matrix-must-enumerate-sliceable-prds-not-only-slices`): a
730
- // task-only `jq` would render `DORFL_AUTO_TASK: 'true'` dead on the
731
- // hourly cron — a ready ungated SPEC would never become a matrix leg. The `jq`
732
- // must enumerate `spec:<slug>` ids from `scan --json`'s taskable-SPEC pool
733
- // (`repos[].specs[]` + `cwd.repo.specs[]`) alongside the eligible-task legs.
576
+ // task-only `jq` would render `DORFL_AUTO_TASK: 'true'` dead on the hourly
577
+ // cron — a ready ungated SPEC would never become an item run.
734
578
  require('propose-enumerates-taskable-specs', /"spec:" \+ \.slug/.test(text) &&
735
- /\.specs\[\]/.test(text), 'the propose-mode `enumerate` `jq` must union taskable specs into the ' +
736
- "matrix as `spec:<slug>` legs (read from `scan --json`'s `repos[].specs[]` " +
737
- '+ `cwd.repo.specs[]` pools), so a ready ungated SPEC becomes one auto-task ' +
738
- 'matrix leg per item alongside the eligible-task legs ' +
579
+ /\.specs\[\]/.test(text), 'the `enumerate` `jq` must union taskable specs into the item list as ' +
580
+ "`spec:<slug>` ids (read from `scan --json`'s `repos[].specs[]` + " +
581
+ '`cwd.repo.specs[]` pools), so a ready ungated SPEC becomes one auto-task ' +
582
+ 'item run alongside the eligible-task items ' +
739
583
  '(`ci-propose-matrix-must-enumerate-sliceable-prds-not-only-slices`).');
740
- // --- The propose `enumerate` matrix must UNION the LIFECYCLE pools ----------
741
- // (`ci-propose-matrix-enumerates-lifecycle-items`): a build/task-only `jq`
742
- // runs the answer-loop ONLY in merge mode — a `needsAnswers` item is
743
- // `eligible:false` and untriaged observations are not in the task/spec pools at
744
- // all, so NO lifecycle rung (triage/surface/apply) ever gets a propose leg. The
745
- // `jq` must enumerate the three lifecycle sub-pools from `scan --json`
746
- // (`repos[].lifecycle.*` + `cwd.repo.lifecycle.*`): `triage[]` → `obs:<slug>`,
747
- // and `surface[]`/`apply[]` → `.namespace + ":" + .slug`. Match all four shape
748
- // signals so a regression that drops any one leg is caught.
584
+ require('propose-enumerates-via-scan', /dorfl scan --json/.test(text), 'the items must be ENUMERATED via the eligible-pool scan ' +
585
+ '(`dorfl scan --json`).');
586
+ // --- The `enumerate` list must UNION the LIFECYCLE pools --------------------
587
+ // (`ci-propose-matrix-enumerates-lifecycle-items`): `triage[]` → `obs:<slug>`,
588
+ // and `surface[]`/`apply[]` → `.namespace + ":" + .slug`, so the WHOLE
589
+ // answer-loop runs in propose mode too.
749
590
  require('propose-enumerates-lifecycle-items', /"obs:" \+ \.slug/.test(text) &&
750
591
  /\.lifecycle\.triage\[\]/.test(text) &&
751
592
  /\.lifecycle\.surface\[\]/.test(text) &&
752
593
  /\.lifecycle\.apply\[\]/.test(text) &&
753
- /\.namespace \+ ":" \+ \.slug/.test(text), 'the propose-mode `enumerate` `jq` must union the LIFECYCLE pools into the ' +
754
- "matrix (read from `scan --json`'s `repos[].lifecycle.*` + " +
755
- '`cwd.repo.lifecycle.*`): `triage[]` as `obs:<slug>` legs, and ' +
756
- '`surface[]`/`apply[]` as `.namespace + ":" + .slug` (`task:`/`spec:`, plus ' +
757
- '`observation:` for an answered-observation apply leg) legs, ' +
758
- 'so the WHOLE answer-loop (triage + surface + apply) runs in propose mode, ' +
759
- 'not only in merge mode ' +
594
+ /\.namespace \+ ":" \+ \.slug/.test(text), 'the `enumerate` `jq` must union the LIFECYCLE pools into the item list ' +
595
+ "(read from `scan --json`'s `repos[].lifecycle.*` + " +
596
+ '`cwd.repo.lifecycle.*`): `triage[]` as `obs:<slug>`, and ' +
597
+ '`surface[]`/`apply[]` as `.namespace + ":" + .slug` ' +
760
598
  '(`ci-propose-matrix-enumerates-lifecycle-items`).');
761
- // --- DYNAMIC GitHub backstop (spec `graceful-pre-timeout-wip-checkpoint`) ---
762
- // The agent-leg jobs (advance-propose / advance-merge) must consume the
763
- // enumerate job's dynamic `githubTimeout` output, NOT a baked-in number. This
764
- // closes the drift where the static `legTimeoutMinutes` render only refreshed
765
- // on `install-ci` re-run; now the same committed `dorfl.json` drives BOTH the
766
- // internal deadline AND the GitHub cap on every tick.
767
- require('enumerate-emits-github-timeout-output', /enumerate:[\s\S]*?outputs:[\s\S]*?githubTimeout:/.test(text), 'the `enumerate` job must declare a `githubTimeout` job OUTPUT (computed ' +
768
- 'from the committed `dorfl.json` as ' +
769
- '`agentDeadlineMinutes + checkpointHeadroomMinutes`).');
770
- require('enumerate-reads-dorfl-config-json', /dorfl config --json/.test(text), 'the `enumerate` job must read the committed config via ' +
771
- '`dorfl config --json` at run time (so an edit to `dorfl.json` ' +
772
- 'reflects on the next tick without an `install-ci` re-run).');
773
- require('enumerate-computes-deadline-plus-headroom', /agentDeadlineMinutes \+ checkpointHeadroomMinutes/.test(text), 'the `enumerate` job must compute `githubTimeout` as ' +
774
- '`agentDeadlineMinutes + checkpointHeadroomMinutes` (the backstop is ' +
775
- 'DELIBERATELY head-room above the internal deadline).');
776
- require('propose-uses-dynamic-github-timeout', /advance-propose:[\s\S]*?timeout-minutes:\s*\$\{\{\s*fromJson\(needs\.enumerate\.outputs\.githubTimeout\)\s*\}\}/.test(text), 'the `advance-propose` job must set `timeout-minutes: ${{ ' +
777
- 'fromJson(needs.enumerate.outputs.githubTimeout) }}` (the DYNAMIC GitHub ' +
778
- 'backstop, fromJson-coerced to an integer, not a baked-in number).');
779
- require('merge-uses-dynamic-github-timeout', /advance-merge:[\s\S]*?timeout-minutes:\s*\$\{\{\s*fromJson\(needs\.enumerate\.outputs\.githubTimeout\)\s*\}\}/.test(text), 'the `advance-merge` job must set `timeout-minutes: ${{ ' +
780
- 'fromJson(needs.enumerate.outputs.githubTimeout) }}` (the DYNAMIC GitHub ' +
781
- 'backstop, fromJson-coerced to an integer).');
782
- // A baked static `timeout-minutes: <number>` on an agent-leg job is the
783
- // retired `legTimeoutMinutes` render — forbid it so a future edit cannot
784
- // silently re-introduce the drift the dynamic backstop is fixing.
785
- require('no-static-agent-leg-timeout', !/(?:advance-propose|advance-merge):[\s\S]*?timeout-minutes:\s*\d/.test(text), 'no agent-leg job may set a baked-in numeric `timeout-minutes` ' +
786
- '(the retired `legTimeoutMinutes` render); the GitHub backstop is ' +
787
- 'DYNAMIC (read from `dorfl.json` at run time).');
788
- // --- Wires the SHARED composite setup action -------------------------------
789
- require('uses-shared-setup-action', /uses:\s*\.\/\.github\/actions\/dorfl-setup\b/.test(text), 'every job must wire the shared composite setup action ' +
790
- '(`./.github/actions/dorfl-setup`, emitted by the core task).');
599
+ // --- No timeout is computed here: the item's lock job owns it ---------------
600
+ // The agent job's `timeout-minutes` is the lock job's `agentTimeoutMinutes`
601
+ // output (dorfl.json at the item's base), in dorfl-item.yml; no job here may
602
+ // bake a numeric `timeout-minutes` either (the retired `legTimeoutMinutes`).
603
+ require('no-static-timeout', !/timeout-minutes:\s*\d/.test(operative), 'no job may set a baked-in numeric `timeout-minutes`: the agent timeout ' +
604
+ "is the item lock job's `agentTimeoutMinutes` (dorfl.json at the base).");
605
+ // --- Wires the writer-role setup action (no agent runs here) ---------------
606
+ require('uses-writer-setup-action', /uses:\s*\.\/\.github\/actions\/dorfl-setup-writer\b/.test(text), 'the jobs that set up dorfl here run no agent, so they must use the ' +
607
+ 'writer-role setup (`./.github/actions/dorfl-setup-writer`).');
608
+ require('no-agent-setup-action', !/uses:\s*\.\/\.github\/actions\/dorfl-setup\s*$/m.test(operative), 'no job here runs an agent, so none may use the agent-role setup ' +
609
+ '(`./.github/actions/dorfl-setup`: the harness and the provider key).');
791
610
  return { ok: problems.length === 0, problems };
792
611
  }
793
612
  //# sourceMappingURL=advance-lifecycle-template.js.map