dorfl 0.14.2 → 0.15.0

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