dorfl 0.14.2 → 0.15.0

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