dorfl 0.14.3 → 0.15.1

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