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
@@ -10,8 +10,7 @@
10
10
  * This module is the PROVIDER-AGNOSTIC half: the config model (`ProviderEntry` /
11
11
  * `AuthMode` / `CIConfigFile`), the `models.json` builder, the secret-orchestration
12
12
  * LOGIC (which secrets, dedup, prompt-or-take-from-config), the `--export-config`
13
- * (+ `--include-secrets`) round-trip, the composite-setup-action + OAuth-refresh-
14
- * script generators, the `--fake` snapshot mechanism (write to `.fake/` instead of
13
+ * (+ `--include-secrets`) round-trip, the composite-setup-action generators, the `--fake` snapshot mechanism (write to `.fake/` instead of
15
14
  * `.github/`), and the capability-emitter REGISTRY seam. It imports NOTHING
16
15
  * GitHub-specific: the thin GitHub adapter (`github-ci.ts`) plugs into the
17
16
  * {@link CIProviderContext} seam, so a second provider could slot in WITHOUT
@@ -25,24 +24,50 @@
25
24
  * `DORFL_*` env block, NOT a new config knob minted here.
26
25
  */
27
26
 
28
- import {readFileSync, writeFileSync, mkdirSync, readdirSync} from 'node:fs';
27
+ import {
28
+ readFileSync,
29
+ writeFileSync,
30
+ mkdirSync,
31
+ readdirSync,
32
+ existsSync,
33
+ } from 'node:fs';
29
34
  import {dirname, join, relative} from 'node:path';
30
35
  import {fileURLToPath} from 'node:url';
31
36
  import type {HarnessAdapter} from './config.js';
37
+ import {
38
+ ACTION_PINS,
39
+ pinnedUses,
40
+ preserveExistingPins,
41
+ } from './install-ci-action-pins.js';
32
42
 
33
43
  // ─── Config model ──────────────────────────────────────────────────────────
34
44
 
35
45
  /**
36
- * The two auth modes, mirroring whitesmith:
37
- * - `models-json` (DEFAULT): one GitHub secret per provider API key; the harness
38
- * `models.json` is generated inline and references the env var names. No
39
- * OAuth-refresh machinery, no `GH_PAT` — the conservative default.
40
- * - `auth-json`: a single `PI_AUTH_JSON` secret + a `GH_PAT` for OAuth-token
41
- * refresh + an OAuth-refresh script (the `pi-mono#2743` workaround, the SINGLE
42
- * CI→repo mutation in the whole design). The known SHARP EDGE; default AWAY
43
- * from it.
46
+ * The auth mode. `models-json` is the ONLY mode: one GitHub secret per provider
47
+ * API key; the harness `models.json` is generated inline and references the env
48
+ * vars (`$NAME`). Credential rotation (e.g. OAuth) is NOT done in CI: point a
49
+ * provider's `baseUrl` at a proxy that rotates credentials outside GitHub.
50
+ *
51
+ * The former `auth-json` mode (a `PI_AUTH_JSON` secret + an OAuth-refresh
52
+ * script that wrote the rotated token back with a write-capable `GH_PAT`) was
53
+ * REMOVED: its refresh needed a write token in the agent job, which ADR
54
+ * `ci-agent-job-holds-no-write-token` forbids, and without the refresh the
55
+ * OAuth token expires within hours. A config still naming it is refused by
56
+ * {@link loadCIConfigFile} with {@link AUTH_JSON_REMOVED_MESSAGE}. The type
57
+ * (and the `authMode` config key) is kept so existing `models-json` configs
58
+ * still parse and export unchanged.
59
+ */
60
+ export type AuthMode = 'models-json';
61
+
62
+ /**
63
+ * The refusal a config naming the removed `auth-json` mode gets: it says the
64
+ * mode is gone and names the replacement (`models-json` + a proxy `baseUrl`).
44
65
  */
45
- export type AuthMode = 'auth-json' | 'models-json';
66
+ export const AUTH_JSON_REMOVED_MESSAGE =
67
+ 'config file "authMode": "auth-json" is no longer supported (the mode was ' +
68
+ 'removed: its OAuth refresh needed a write-capable GH_PAT in the agent job). ' +
69
+ 'Use "authMode": "models-json" and point a provider\'s "baseUrl" at a proxy ' +
70
+ 'that handles credential rotation outside GitHub.';
46
71
 
47
72
  /**
48
73
  * Where the composite setup action gets the `dorfl` CLI from:
@@ -84,7 +109,7 @@ export interface ProviderEntry {
84
109
  export interface CIConfigFile {
85
110
  /** The auth mode (`models-json` default). */
86
111
  authMode: AuthMode;
87
- /** The configured providers (empty for `auth-json` mode). */
112
+ /** The configured providers (non-empty). */
88
113
  providers: ProviderEntry[];
89
114
  /** The default provider name. */
90
115
  defaultProvider: string;
@@ -197,6 +222,20 @@ export const DEFAULT_INSTALL_SOURCE: InstallSource = 'registry';
197
222
 
198
223
  // ─── The CI-provider SEAM (provider-agnostic; GitHub is the first adapter) ───
199
224
 
225
+ /**
226
+ * A repository's GitHub Actions workflow permissions, as
227
+ * `GET /repos/{owner}/{repo}/actions/permissions/workflow` answers them (the
228
+ * same two fields the PUT accepts). `defaultWorkflowPermissions` is `read` or
229
+ * `write` on GitHub; typed as a string so an unexpected value is carried
230
+ * through untouched rather than rewritten.
231
+ */
232
+ export interface ActionsWorkflowPermissions {
233
+ /** The GITHUB_TOKEN's default scope (`read` / `write`); preserved on set. */
234
+ defaultWorkflowPermissions: string;
235
+ /** "Allow GitHub Actions to create and approve pull requests". */
236
+ canApprovePullRequestReviews: boolean;
237
+ }
238
+
200
239
  /**
201
240
  * The thin CI-provider seam (whitesmith's proven `GitHubCIContext` shape,
202
241
  * generalised so a second provider could slot in without touching the core). A
@@ -277,6 +316,25 @@ export interface CIProviderContext {
277
316
  * GitHub provider. Throws on failure (caller catches + logs).
278
317
  */
279
318
  setBranchRuleset?(ruleset: unknown): Promise<void>;
319
+ /**
320
+ * OPTIONALLY read the repo's Actions workflow permissions (GitHub:
321
+ * `GET /repos/{owner}/{repo}/actions/permissions/workflow`), whose
322
+ * `can_approve_pull_request_reviews` is the "Allow GitHub Actions to create
323
+ * and approve pull requests" setting propose mode needs. Returns `undefined`
324
+ * when the read fails. Absent on a provider with no such setting.
325
+ */
326
+ getActionsWorkflowPermissions?(): Promise<
327
+ ActionsWorkflowPermissions | undefined
328
+ >;
329
+ /**
330
+ * OPTIONALLY set the repo's Actions workflow permissions (GitHub:
331
+ * `PUT /repos/{owner}/{repo}/actions/permissions/workflow`). A field left
332
+ * `undefined` is not sent, so GitHub keeps its current value. Throws on
333
+ * failure (caller catches + logs). Absent on a provider with no such setting.
334
+ */
335
+ setActionsWorkflowPermissions?(
336
+ permissions: Partial<ActionsWorkflowPermissions>,
337
+ ): Promise<void>;
280
338
  /**
281
339
  * OPTIONALLY render the user's project-setup payload (the value of
282
340
  * {@link CIConfigFile.projectSetup}[providerId]) to the NATIVE-SYNTAX
@@ -405,9 +463,12 @@ function pathToFileUrl(absPath: string): string {
405
463
 
406
464
  /**
407
465
  * Build the harness `models.json` object from the providers. Built-in providers
408
- * (`anthropic`/`openai`) get `{apiKey: <ENV_VAR>}` (the harness resolves the env
409
- * var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
466
+ * (`anthropic`/`openai`) get `{apiKey: "$<ENV_VAR>"}` (the harness resolves the
467
+ * env var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
410
468
  * Mirrors whitesmith's `buildModelsJson`.
469
+ *
470
+ * The `apiKey` is an env-var REFERENCE (see {@link modelsJsonEnvRef}), never the
471
+ * bare env-var name: pi treats a bare value as a LITERAL key.
411
472
  */
412
473
  export function buildModelsJson(providers: ProviderEntry[]): {
413
474
  providers: Record<string, unknown>;
@@ -415,14 +476,16 @@ export function buildModelsJson(providers: ProviderEntry[]): {
415
476
  const providersObj: Record<string, unknown> = {};
416
477
  for (const p of providers) {
417
478
  if (p.builtin) {
418
- const entry: Record<string, unknown> = {apiKey: p.apiKeyEnvVar};
479
+ const entry: Record<string, unknown> = {
480
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
481
+ };
419
482
  if (p.baseUrl) entry.baseUrl = p.baseUrl;
420
483
  providersObj[p.name] = entry;
421
484
  } else {
422
485
  const entry: Record<string, unknown> = {
423
486
  baseUrl: p.baseUrl,
424
487
  api: p.api,
425
- apiKey: p.apiKeyEnvVar,
488
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
426
489
  models: p.models,
427
490
  };
428
491
  if (p.compat) entry.compat = p.compat;
@@ -432,19 +495,27 @@ export function buildModelsJson(providers: ProviderEntry[]): {
432
495
  return {providers: providersObj};
433
496
  }
434
497
 
498
+ /**
499
+ * The `models.json` config value that makes the pi harness read `envVarName`
500
+ * from the environment: `"$NAME"`. The pinned harness
501
+ * (`@earendil-works/pi-coding-agent`, `dist/core/resolve-config-value.js`)
502
+ * resolves a config value as a shell command when it starts with `!`, as an env
503
+ * var when it is `$NAME` / `${NAME}`, and as a LITERAL otherwise. Writing the bare
504
+ * name therefore sent the string `ANTHROPIC_API_KEY` as the token (`401
505
+ * Unauthorized` on every CI launch).
506
+ */
507
+ export function modelsJsonEnvRef(envVarName: string): string {
508
+ return `$${envVarName}`;
509
+ }
510
+
435
511
  // ─── secret-orchestration LOGIC (which secrets, dedup) ───────────────────────
436
512
 
437
513
  /**
438
514
  * The DEDUPLICATED set of secret env-var names the config requires, in first-seen
439
- * order. For `models-json` mode it is one per distinct provider `apiKeyEnvVar`;
440
- * for `auth-json` mode it is the fixed `PI_AUTH_JSON` + `GH_PAT` pair (the sharp
441
- * edge). This is the pure secret-orchestration LOGIC the adapter's setter
515
+ * order: one per distinct provider `apiKeyEnvVar`. This is the pure secret-orchestration LOGIC the adapter's setter
442
516
  * consumes — which secrets, deduped — separated from any provider I/O.
443
517
  */
444
518
  export function requiredSecretNames(config: ResolvedCIConfig): string[] {
445
- if (config.authMode === 'auth-json') {
446
- return ['PI_AUTH_JSON', 'GH_PAT'];
447
- }
448
519
  const seen = new Set<string>();
449
520
  const names: string[] = [];
450
521
  for (const p of config.providers) {
@@ -457,13 +528,13 @@ export function requiredSecretNames(config: ResolvedCIConfig): string[] {
457
528
 
458
529
  /**
459
530
  * The GitHub secret the propose/merge CI legs read as `GH_TOKEN` to open PRs
460
- * under a CHOSEN identity (a PAT / App token). It is the PR-IDENTITY token, a
461
- * distinct purpose from `auth-json` mode's `GH_PAT` (which exists only to rotate
462
- * the OAuth `PI_AUTH_JSON` secret). The generated workflows reference it as
531
+ * under a CHOSEN identity (a PAT / App token). The generated workflows reference it as
463
532
  * `${{ secrets.DORFL_GH_TOKEN || secrets.GITHUB_TOKEN }}` so it is
464
533
  * OPTIONAL: unset ⇒ the legs fall back to the built-in `GITHUB_TOKEN` (PRs are
465
- * `github-actions[bot]` and do not trigger downstream `on: pull_request`
466
- * workflows); set ⇒ PRs carry the token's identity and DO trigger them.
534
+ * `github-actions[bot]`, and GitHub holds their `on: pull_request` runs in
535
+ * `action_required` until someone approves them); set ⇒ PRs carry the token's
536
+ * identity and trigger those workflows on their own. `intake.yml` never passes
537
+ * it, so intake PRs always need the approval (`docs/ci/README.md`).
467
538
  */
468
539
  export const PR_IDENTITY_SECRET_NAME = 'DORFL_GH_TOKEN';
469
540
 
@@ -490,14 +561,13 @@ export function optionalSecretNames(config: ResolvedCIConfig): string[] {
490
561
  * entry). Each entry maps the action input (named identically to the secret) to
491
562
  * `${{ secrets.<NAME> }}`.
492
563
  *
493
- * Empty string when there are no provider keys (auth-json mode, or no providers),
564
+ * Empty string when there are no provider keys (no providers),
494
565
  * so the `uses:` line is emitted bare and unchanged. This is the SINGLE place the
495
566
  * provider→`with:` shape lives, shared by every capability workflow generator, so
496
567
  * no template re-derives it.
497
568
  */
498
569
  export function providerSecretsWithBlock(config: ResolvedCIConfig): string {
499
- const names =
500
- config.authMode === 'models-json' ? requiredSecretNames(config) : [];
570
+ const names = providerSecretNames(config);
501
571
  if (names.length === 0) {
502
572
  return '';
503
573
  }
@@ -507,6 +577,17 @@ export function providerSecretsWithBlock(config: ResolvedCIConfig): string {
507
577
  return `\n with:\n${entries}`;
508
578
  }
509
579
 
580
+ /**
581
+ * The provider API key secrets the AGENT-running jobs forward to the setup
582
+ * action: one per distinct provider key (the same set as
583
+ * {@link requiredSecretNames}). The split item workflow declares exactly these (plus
584
+ * {@link PR_IDENTITY_SECRET_NAME}) as its `workflow_call` secrets, and hands
585
+ * them to its agent job only.
586
+ */
587
+ export function providerSecretNames(config: ResolvedCIConfig): string[] {
588
+ return requiredSecretNames(config);
589
+ }
590
+
510
591
  /** The outcome of orchestrating one secret through the provider seam. */
511
592
  export interface SecretSetResult {
512
593
  /** The secret env-var name. */
@@ -604,12 +685,17 @@ export function loadCIConfigFile(filePath: string): CIConfigFile {
604
685
  (err instanceof Error ? err.message : String(err)),
605
686
  );
606
687
  }
607
- const authMode = data.authMode ?? 'models-json';
608
- if (authMode !== 'models-json' && authMode !== 'auth-json') {
609
- throw new CIConfigError(
610
- `config file "authMode" must be "models-json" or "auth-json"`,
611
- );
688
+ // `authMode` is kept as a key (so existing configs still parse) but the only
689
+ // accepted value is `models-json`. The removed `auth-json` mode gets a refusal
690
+ // that names its replacement rather than the generic "must be" message.
691
+ const rawAuthMode: unknown = (data as {authMode?: unknown}).authMode;
692
+ if (rawAuthMode === 'auth-json') {
693
+ throw new CIConfigError(AUTH_JSON_REMOVED_MESSAGE);
694
+ }
695
+ if (rawAuthMode !== undefined && rawAuthMode !== 'models-json') {
696
+ throw new CIConfigError(`config file "authMode" must be "models-json"`);
612
697
  }
698
+ const authMode: AuthMode = 'models-json';
613
699
  if (!data.defaultProvider) {
614
700
  throw new CIConfigError('config file must contain "defaultProvider"');
615
701
  }
@@ -638,20 +724,18 @@ export function loadCIConfigFile(filePath: string): CIConfigFile {
638
724
  );
639
725
  }
640
726
  }
641
- if (authMode === 'models-json') {
642
- if (
643
- !data.providers ||
644
- !Array.isArray(data.providers) ||
645
- data.providers.length === 0
646
- ) {
647
- throw new CIConfigError(
648
- 'config file (models-json) must contain a non-empty "providers" array',
649
- );
650
- }
727
+ if (
728
+ !data.providers ||
729
+ !Array.isArray(data.providers) ||
730
+ data.providers.length === 0
731
+ ) {
732
+ throw new CIConfigError(
733
+ 'config file (models-json) must contain a non-empty "providers" array',
734
+ );
651
735
  }
652
736
  return {
653
737
  authMode,
654
- providers: data.providers ?? [],
738
+ providers: data.providers,
655
739
  defaultProvider: data.defaultProvider,
656
740
  defaultModel: data.defaultModel,
657
741
  harness: data.harness,
@@ -707,7 +791,7 @@ export function exportCIConfig(
707
791
  return JSON.stringify(file, null, 2) + '\n';
708
792
  }
709
793
 
710
- // ─── composite setup action + OAuth-refresh script generation ────────────────
794
+ // ─── composite setup action generation ───────────────────────────────────────
711
795
 
712
796
  /** Indent every non-blank line of `text` by `spaces` spaces. */
713
797
  function indent(text: string, spaces: number): string {
@@ -750,6 +834,55 @@ export const PI_HARNESS_PACKAGE = '@earendil-works/pi-coding-agent';
750
834
  */
751
835
  export const PI_HARNESS_VERSION = '0.80.6';
752
836
 
837
+ /**
838
+ * The `pi` packages the harness LOADS besides itself, each pinned to an exact
839
+ * version that is known to load with {@link PI_HARNESS_VERSION}.
840
+ *
841
+ * `pi-coding-agent` declares its sibling packages with a caret range
842
+ * (`@earendil-works/pi-ai: ^0.80.6`), which on a `0.x` version floats within
843
+ * the minor. `pi-ai@0.80.10` removed the `getOAuthApiKey` export that
844
+ * `pi-coding-agent@0.80.6` imports, so a resolver that honours the caret kills
845
+ * `pi` on start. npm happens to be protected by the `npm-shrinkwrap.json` the
846
+ * package publishes; pnpm ignores that file and was broken. Pinning the family
847
+ * explicitly (as npm `overrides`, see {@link piHarnessManifest}) makes the
848
+ * install reproducible whatever the resolver.
849
+ *
850
+ * The set is `pi-coding-agent`'s own `@earendil-works/*` dependencies plus
851
+ * theirs (`npm view @earendil-works/pi-coding-agent@<v> dependencies`,
852
+ * recursively): at 0.80.6 that is pi-ai, pi-agent-core (which depends on pi-ai)
853
+ * and pi-tui. Re-derive it on every {@link PI_HARNESS_VERSION} bump: a newer
854
+ * harness may add a package (0.87 adds `@earendil-works/chord`).
855
+ */
856
+ export const PI_HARNESS_PINNED_DEPENDENCIES: Readonly<Record<string, string>> =
857
+ {
858
+ '@earendil-works/pi-agent-core': PI_HARNESS_VERSION,
859
+ '@earendil-works/pi-ai': PI_HARNESS_VERSION,
860
+ '@earendil-works/pi-tui': PI_HARNESS_VERSION,
861
+ };
862
+
863
+ /**
864
+ * The throwaway `package.json` the setup action installs the harness from: the
865
+ * harness at {@link PI_HARNESS_VERSION} as the only dependency, and every pi
866
+ * package it loads forced to its exact pin through npm `overrides`.
867
+ *
868
+ * A project-local install (not `npm install -g`) because `overrides` only apply
869
+ * to the ROOT project, and a global install makes each package its own root. The
870
+ * same file is used in `registry` and `workspace` mode: pnpm 11 also isolates
871
+ * every global package, so listing the pins next to the harness in
872
+ * `pnpm add -g` would not constrain the harness's own tree either.
873
+ */
874
+ export function piHarnessManifest(): {
875
+ private: true;
876
+ dependencies: Record<string, string>;
877
+ overrides: Record<string, string>;
878
+ } {
879
+ return {
880
+ private: true,
881
+ dependencies: {[PI_HARNESS_PACKAGE]: PI_HARNESS_VERSION},
882
+ overrides: {...PI_HARNESS_PINNED_DEPENDENCIES},
883
+ };
884
+ }
885
+
753
886
  /**
754
887
  * The version of THIS dorfl package, read at runtime from its own
755
888
  * `package.json` (the single source changesets bumps on release). The composite
@@ -787,29 +920,48 @@ export function dorflPackageVersion(): string {
787
920
  return version;
788
921
  }
789
922
 
923
+ /** Where the setup action installs the harness (under the job's temp dir). */
924
+ const PI_HARNESS_DIR = '$RUNNER_TEMP/dorfl-harness';
925
+
790
926
  /**
791
- * The install step for the configured harness (the `pi` CLI; `''` ⇒ none).
792
- * `registry` mode installs via `npm install -g`; `workspace` mode installs via
793
- * `pnpm add -g` so the harness lands on the pnpm global bin already on
794
- * `$GITHUB_PATH` (mirroring whitesmith's dev mode).
927
+ * The install + smoke steps for the configured harness (the `pi` CLI; `''` ⇒
928
+ * none). Identical in `registry` and `workspace` mode: the harness is installed
929
+ * with npm from {@link piHarnessManifest} into a job-local directory whose
930
+ * `node_modules/.bin` is added to `$GITHUB_PATH`, so every pi package it loads
931
+ * is pinned whatever package manager the repository uses. The smoke step then
932
+ * runs `pi --version`, which imports the harness's whole module graph, so a
933
+ * broken install fails setup with a clear message instead of failing inside a
934
+ * dorfl agent launch.
795
935
  */
796
- function harnessInstallStep(
797
- harness: HarnessAdapter,
798
- installSource: InstallSource,
799
- ): string {
936
+ function harnessInstallStep(harness: HarnessAdapter): string {
800
937
  if (harness === 'pi') {
801
- const spec = `${PI_HARNESS_PACKAGE}@${PI_HARNESS_VERSION}`;
802
- const run =
803
- installSource === 'workspace'
804
- ? `pnpm add -g ${spec}`
805
- : `npm install -g ${spec}`;
938
+ const manifest = JSON.stringify(piHarnessManifest(), null, 2);
806
939
  return `
807
940
  # Pinned to the exact harness version this dorfl release declares
808
- # (PI_HARNESS_VERSION): the job holds write access and a provider key, so the
809
- # version must not move on its own. Re-run \`dorfl install-ci\` to upgrade.
941
+ # (PI_HARNESS_VERSION), with every pi package it loads pinned too (npm
942
+ # \`overrides\`; a caret range on a 0.x version floats within the minor): the
943
+ # job holds a provider key, so no version may move on its own. Re-run
944
+ # \`dorfl install-ci\` to upgrade.
810
945
  - name: Install agent harness (pi)
811
946
  shell: bash
812
- run: ${run}`;
947
+ run: |
948
+ mkdir -p "${PI_HARNESS_DIR}"
949
+ cd "${PI_HARNESS_DIR}"
950
+ cat > package.json << 'HARNESS_EOF'
951
+ ${indent(manifest, 8)}
952
+ HARNESS_EOF
953
+ npm install --no-audit --no-fund
954
+ echo "${PI_HARNESS_DIR}/node_modules/.bin" >> "$GITHUB_PATH"
955
+
956
+ # \`pi --version\` loads the harness's module graph, so an install whose pi
957
+ # packages do not load together fails HERE, not inside a dorfl agent launch.
958
+ - name: Check the agent harness loads (pi)
959
+ shell: bash
960
+ run: |
961
+ if ! pi --version; then
962
+ echo "::error title=dorfl-setup::The agent harness (pi) is installed but cannot start, so its pi packages do not load together. Re-run dorfl install-ci with a dorfl release that pins a working harness."
963
+ exit 1
964
+ fi`;
813
965
  }
814
966
  return '';
815
967
  }
@@ -822,14 +974,11 @@ function harnessInstallStep(
822
974
  * `uses: ./.github/actions/dorfl-setup`. The advance-loop seed template
823
975
  * already references this exact action name + path (`docs/ci/README.md`).
824
976
  *
825
- * The auth step branches on the mode: `models-json` writes a generated
826
- * `~/.pi/agent/models.json` inline (the conservative default) AND exports each
977
+ * The auth step writes a generated `~/.pi/agent/models.json` inline AND exports each
827
978
  * configured provider's API key from a same-named ACTION INPUT to `$GITHUB_ENV`,
828
979
  * so the agent step (`pi`) can resolve the env var `models.json` references; the
829
980
  * workflow passes the secrets ONCE via `with:` on the `uses:` line (no
830
- * per-step / per-capability provider enumeration). `auth-json` writes
831
- * `~/.pi/agent/auth.json` from `$PI_AUTH_JSON` + runs the OAuth-refresh script
832
- * (the sharp edge). Deterministic: the same config produces byte-identical output.
981
+ * per-step / per-capability provider enumeration). Deterministic: the same config produces byte-identical output.
833
982
  */
834
983
  export function generateSetupAction(
835
984
  config: ResolvedCIConfig,
@@ -843,51 +992,32 @@ export function generateSetupAction(
843
992
  */
844
993
  projectSetupSteps?: string,
845
994
  ): string {
846
- let authStep: string;
847
- // models-json mode declares one ACTION INPUT per distinct provider key (named
848
- // identically to the secret / env var) and exports it to `$GITHUB_ENV`, so every
849
- // later step inherits it. auth-json mode has no provider keys (it uses auth.json).
850
- const providerKeyNames =
851
- config.authMode === 'models-json' ? requiredSecretNames(config) : [];
852
- if (config.authMode === 'auth-json') {
853
- authStep = `\
854
- - name: Configure agent auth (auth.json)
855
- shell: bash
856
- run: |
857
- if [ -z "$PI_AUTH_JSON" ]; then
858
- echo "ERROR: PI_AUTH_JSON secret is not set" >&2; exit 1
859
- fi
860
- mkdir -p ~/.pi/agent
861
- echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
862
- chmod 600 ~/.pi/agent/auth.json
863
-
864
- # Workaround for https://github.com/badlogic/pi-mono/issues/2743 — the SINGLE
865
- # CI→repo mutation in the design; needs GH_PAT to rotate PI_AUTH_JSON.
866
- - name: Refresh OAuth token
867
- shell: bash
868
- run: node .github/scripts/refresh-oauth-token.mjs`;
869
- } else {
870
- const modelsJsonStr = JSON.stringify(
871
- buildModelsJson(config.providers),
872
- null,
873
- 2,
874
- );
875
- // Map each provider-key INPUT into the step env, then append the non-empty
876
- // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
877
- // (never interpolated into the script body), and Actions masks the secret in
878
- // logs. `models.json`'s `apiKey` is the env-var NAME; this puts the value there.
879
- const exportEnvLines = providerKeyNames
880
- .map((name) => ` ${name}: \${{ inputs.${name} }}`)
881
- .join('\n');
882
- const exportRunLines = providerKeyNames
883
- .map(
884
- (name) =>
885
- ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`,
886
- )
887
- .join('\n');
888
- const exportStep =
889
- providerKeyNames.length > 0
890
- ? `\n
995
+ // models-json mode (the only mode) declares one ACTION INPUT per distinct
996
+ // provider key (named identically to the secret / env var) and exports it to
997
+ // `$GITHUB_ENV`, so every later step inherits it.
998
+ const providerKeyNames = providerSecretNames(config);
999
+ const modelsJsonStr = JSON.stringify(
1000
+ buildModelsJson(config.providers),
1001
+ null,
1002
+ 2,
1003
+ );
1004
+ // Map each provider-key INPUT into the step env, then append the non-empty
1005
+ // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
1006
+ // (never interpolated into the script body), and Actions masks the secret in
1007
+ // logs. `models.json`'s `apiKey` is a `$NAME` env-var REFERENCE (pi resolves it at
1008
+ // runtime; a bare name would be a literal key); this puts the value there.
1009
+ const exportEnvLines = providerKeyNames
1010
+ .map((name) => ` ${name}: \${{ inputs.${name} }}`)
1011
+ .join('\n');
1012
+ const exportRunLines = providerKeyNames
1013
+ .map(
1014
+ (name) =>
1015
+ ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`,
1016
+ )
1017
+ .join('\n');
1018
+ const exportStep =
1019
+ providerKeyNames.length > 0
1020
+ ? `\n
891
1021
  # Surface the configured provider API key(s) to the environment so the agent
892
1022
  # step (\`pi\`) can resolve the env var \`models.json\` references. The workflow
893
1023
  # passes the secret(s) via \`with:\` on the \`uses:\` line; we forward the
@@ -898,8 +1028,8 @@ export function generateSetupAction(
898
1028
  ${exportEnvLines}
899
1029
  run: |
900
1030
  ${exportRunLines}`
901
- : '';
902
- authStep = `\
1031
+ : '';
1032
+ const authStep = `\
903
1033
  - name: Configure agent models (models.json)
904
1034
  shell: bash
905
1035
  run: |
@@ -907,25 +1037,21 @@ ${exportRunLines}`
907
1037
  cat > ~/.pi/agent/models.json << 'MODELS_EOF'
908
1038
  ${indent(modelsJsonStr, 8)}
909
1039
  MODELS_EOF${exportStep}`;
910
- }
911
1040
 
912
- const installHarness = harnessInstallStep(
913
- config.harness,
914
- config.installSource,
915
- );
1041
+ const installHarness = harnessInstallStep(config.harness);
916
1042
 
917
1043
  // The CLI-install block branches on installSource. `registry` installs the
918
1044
  // published CLI via `npm install -g dorfl` (the default for every
919
1045
  // consumer repo). `workspace` builds the CLI from the checked-out source and
920
1046
  // links it onto PATH — for the self-hosting dorfl monorepo, which is
921
1047
  // not published under that npm name. We add pnpm's global bin to $GITHUB_PATH
922
- // so the linked `dorfl` (and the pnpm-installed harness) are on PATH in
923
- // all subsequent steps; we always rebuild because source changes per commit.
1048
+ // so the linked `dorfl` is on PATH in all subsequent steps (the harness adds
1049
+ // its own bin dir, the same way in both modes); we always rebuild because source changes per commit.
924
1050
  let installSteps: string;
925
1051
  if (config.installSource === 'workspace') {
926
1052
  installSteps = `\
927
1053
  - name: Setup pnpm
928
- uses: pnpm/action-setup@v5
1054
+ uses: ${pinnedUses(ACTION_PINS.pnpmSetup)}
929
1055
 
930
1056
  - name: Add pnpm global bin to PATH
931
1057
  shell: bash
@@ -975,10 +1101,9 @@ ${indent(modelsJsonStr, 8)}
975
1101
  // double-resolution. The install step below leaves `dorfl` on `$PATH` as the
976
1102
  // bootstrap; the forward does the pinning.
977
1103
 
978
- // One optional ACTION INPUT per provider key (models-json mode), named
979
- // identically to the secret/env var. Optional + default '' so a workflow that
980
- // does not pass it (or auth-json mode, which declares none) is valid; the export
981
- // step skips an empty value. auth-json mode declares no inputs.
1104
+ // One optional ACTION INPUT per provider key, named identically to the
1105
+ // secret/env var. Optional + default '' so a workflow that does not pass it is
1106
+ // valid; the export step skips an empty value.
982
1107
  const inputsBlock =
983
1108
  providerKeyNames.length > 0
984
1109
  ? `inputs:
@@ -1021,15 +1146,16 @@ runs:
1021
1146
  using: composite
1022
1147
  steps:
1023
1148
  ${projectSetupBlock} - name: Setup Node.js
1024
- uses: actions/setup-node@v5
1149
+ uses: ${pinnedUses(ACTION_PINS.setupNode)}
1025
1150
  with:
1026
1151
  node-version: '22'
1027
- # setup-node@v5 auto-enables package-manager caching, which probes the
1028
- # project package manager DURING its own run when a lockfile is present,
1029
- # before that manager is on PATH, so the probe crashes ("Unable to
1030
- # locate executable file"). dorfl provisions ONLY its own runtime and
1031
- # leaves the project package-manager cache to the project (toolchain
1032
- # boundary), so disable it explicitly.
1152
+ # setup-node (v5 and later) auto-enables package-manager caching. In v5
1153
+ # it probed whatever manager package.json's packageManager named, during
1154
+ # its own run and before that manager was on PATH, and crashed ("Unable
1155
+ # to locate executable file"). Since v6 it only fires when package.json
1156
+ # declares npm, and then fails if there is no npm lockfile. Either way
1157
+ # it is the project's cache, not dorfl's: dorfl provisions ONLY its own
1158
+ # runtime (toolchain boundary), so disable it explicitly.
1033
1159
  package-manager-cache: false
1034
1160
 
1035
1161
  - name: Configure git identity
@@ -1044,111 +1170,128 @@ ${authStep}
1044
1170
  `;
1045
1171
  }
1046
1172
 
1173
+ /** The local path a workflow step `uses:` for the agent-role setup action. */
1174
+ export const SETUP_ACTION_USES = './.github/actions/dorfl-setup';
1175
+
1176
+ /** The local path a workflow step `uses:` for the writer-role setup action. */
1177
+ export const WRITER_SETUP_ACTION_USES = './.github/actions/dorfl-setup-writer';
1178
+
1047
1179
  /**
1048
- * The OAuth-refresh script emitted ONLY in `auth-json` mode (whitesmith's
1049
- * `pi-mono#2743` workaround): refreshes the Anthropic OAuth token in
1050
- * `~/.pi/agent/auth.json` before the agent runs, then writes the rotated token
1051
- * BACK via `gh secret set PI_AUTH_JSON` (the single CI→repo mutation, needing a
1052
- * `GH_PAT`). This is the documented SHARP EDGE; `models-json` mode avoids it.
1053
- */
1054
- export const REFRESH_OAUTH_SCRIPT = `\
1055
- #!/usr/bin/env node
1056
- /**
1057
- * Refresh OAuth tokens in the agent's auth.json before it runs.
1180
+ * Generate the WRITER-ROLE composite setup action (`dorfl-setup-writer`, spec
1181
+ * `ci-agent-job-without-write-token` §9, ADR
1182
+ * `ci-agent-job-holds-no-write-token`; task `ci-split-generate-workflows`).
1183
+ *
1184
+ * The jobs that hold a write token and run no agent (the split item
1185
+ * workflow's lock and apply jobs, `reap-merged-branches`, `close-job`, and the
1186
+ * read-only `enumerate`) install Node and dorfl ONLY: no harness, no provider
1187
+ * key, no `models.json`, no project-setup hook, no install of the project's
1188
+ * dependencies, and no Actions cache. In `registry` mode the dorfl install runs
1189
+ * from `$RUNNER_TEMP`, outside the checkout, with `--ignore-scripts`, so a
1190
+ * repository `.npmrc` cannot redirect the registry and no install script runs.
1058
1191
  *
1059
- * Workaround for https://github.com/badlogic/pi-mono/issues/2743.
1060
- * After refreshing, updates the PI_AUTH_JSON GitHub secret so the next run has
1061
- * the latest rotated refresh token (requires GH_PAT with repo scope). This is the
1062
- * known SHARP EDGE of auth-json mode; models-json mode avoids it entirely.
1192
+ * In `workspace` mode (dorfl's own repository) dorfl is built from source
1193
+ * (decision 8): the tree of `source-ref` (default the checkout's `HEAD`) is
1194
+ * exported with `git archive` into `$RUNNER_TEMP` and built there with
1195
+ * `--ignore-scripts`. In merge mode that is code an earlier agent landed on
1196
+ * `main`, which is the trust merge mode already implies.
1063
1197
  *
1064
- * Remove this script once the upstream fix is released.
1198
+ * It is a SIBLING action rather than a `role` input on `dorfl-setup`: the
1199
+ * project-setup hook is an opaque fragment spliced first into `dorfl-setup`
1200
+ * (ADR `install-ci-project-provisioning-native-passthrough`), and a composite
1201
+ * action cannot skip an opaque block of steps by input. `dorfl-setup` stays the
1202
+ * agent role, byte for byte.
1065
1203
  */
1066
- import { readFileSync, writeFileSync, chmodSync } from "fs";
1067
- import { join } from "path";
1068
- import { execSync } from "child_process";
1069
-
1070
- const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1071
- const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
1204
+ export function generateWriterSetupAction(config: ResolvedCIConfig): string {
1205
+ let installSteps: string;
1206
+ let inputsBlock = '';
1207
+ if (config.installSource === 'workspace') {
1208
+ inputsBlock = `inputs:
1209
+ source-ref:
1210
+ description: 'The commit to build dorfl from (workspace mode). Blank: the checkout HEAD.'
1211
+ required: false
1212
+ default: ''
1072
1213
 
1073
- const authPath = join(process.env.HOME, ".pi", "agent", "auth.json");
1074
- const auth = JSON.parse(readFileSync(authPath, "utf-8"));
1075
- const cred = auth.anthropic;
1214
+ `;
1215
+ installSteps = `\
1216
+ - name: Setup pnpm
1217
+ uses: ${pinnedUses(ACTION_PINS.pnpmSetup)}
1076
1218
 
1077
- if (!cred || cred.type !== "oauth") {
1078
- console.log("No OAuth credentials for anthropic, skipping refresh");
1079
- process.exit(0);
1080
- }
1219
+ - name: Add pnpm global bin to PATH
1220
+ shell: bash
1221
+ run: |
1222
+ pnpm setup
1223
+ echo "$HOME/.local/share/pnpm" >> "$GITHUB_PATH"
1081
1224
 
1082
- if (Date.now() < cred.expires) {
1083
- console.log("Token still valid until", new Date(cred.expires).toISOString());
1084
- process.exit(0);
1085
- }
1225
+ # Workspace mode (dorfl's own repository, decision 8 of ADR
1226
+ # ci-agent-job-holds-no-write-token): build dorfl from the tree of
1227
+ # \`source-ref\`, exported into $RUNNER_TEMP (outside the checkout), with
1228
+ # \`--ignore-scripts\`. In merge mode this is code an earlier agent landed on
1229
+ # main running in a write job, the trust merge mode already implies.
1230
+ # Only dorfl and its workspace dependencies (\`--filter 'dorfl...'\`) are
1231
+ # installed and built: the other workspace packages (the website) are not
1232
+ # needed to run dorfl, so they do not run next to the write token.
1233
+ - name: Build dorfl from the base commit (outside the checkout)
1234
+ shell: bash
1235
+ env:
1236
+ SOURCE_REF: \${{ inputs.source-ref }}
1237
+ run: |
1238
+ set -euo pipefail
1239
+ src="$RUNNER_TEMP/dorfl-src"
1240
+ rm -rf "$src"
1241
+ mkdir -p "$src"
1242
+ git archive "$(git rev-parse --verify "\${SOURCE_REF:-HEAD}^{commit}")" | tar -x -C "$src"
1243
+ cd "$src"
1244
+ pnpm install --frozen-lockfile --ignore-scripts --filter 'dorfl...'
1245
+ pnpm --filter 'dorfl...' build
1246
+ cd packages/dorfl && pnpm link --global`;
1247
+ } else {
1248
+ installSteps = `\
1249
+ # Pinned to the dorfl version that generated this action. Installed from
1250
+ # $RUNNER_TEMP (outside the checkout, so a repository .npmrc cannot redirect
1251
+ # the registry) with --ignore-scripts (no install script runs next to the
1252
+ # write token).
1253
+ - name: Install dorfl (outside the checkout, no install scripts)
1254
+ shell: bash
1255
+ run: |
1256
+ cd "$RUNNER_TEMP"
1257
+ npm install -g --ignore-scripts dorfl@${dorflPackageVersion()}`;
1258
+ }
1259
+ return `\
1260
+ name: Setup dorfl (writer role)
1261
+ description: Install Node.js and dorfl only, for the CI jobs that run no agent (lock, apply, reap, close, enumerate)
1262
+
1263
+ ${inputsBlock}# THE WRITER ROLE (ADR ci-agent-job-holds-no-write-token): the jobs that use
1264
+ # this action hold a write token (or run next to one) and run NO agent and NO
1265
+ # repository code. So it installs Node.js and dorfl ONLY: no agent harness, no
1266
+ # provider API key, no models.json, no project-setup hook, no install of the
1267
+ # project's dependencies, and no Actions cache (an agent can write
1268
+ # default-branch cache entries, so a restore here would run its bytes with
1269
+ # write access). The agent role is ./.github/actions/dorfl-setup.
1270
+ runs:
1271
+ using: composite
1272
+ steps:
1273
+ - name: Setup Node.js
1274
+ uses: ${pinnedUses(ACTION_PINS.setupNode)}
1275
+ with:
1276
+ node-version: '22'
1277
+ # No cache in the writer role (see above).
1278
+ package-manager-cache: false
1086
1279
 
1087
- console.log(
1088
- "Token expired at",
1089
- new Date(cred.expires).toISOString(),
1090
- "- refreshing..."
1091
- );
1092
-
1093
- const response = await fetch(ANTHROPIC_TOKEN_URL, {
1094
- method: "POST",
1095
- headers: {
1096
- "Content-Type": "application/x-www-form-urlencoded",
1097
- Accept: "application/json",
1098
- },
1099
- body: new URLSearchParams({
1100
- grant_type: "refresh_token",
1101
- client_id: ANTHROPIC_CLIENT_ID,
1102
- refresh_token: cred.refresh,
1103
- }).toString(),
1104
- signal: AbortSignal.timeout(30_000),
1105
- });
1106
-
1107
- const data = await response.json();
1108
-
1109
- if (!response.ok) {
1110
- console.error("Refresh failed:", response.status, JSON.stringify(data));
1111
- process.exit(1);
1112
- }
1280
+ - name: Configure git identity
1281
+ shell: bash
1282
+ run: |
1283
+ git config user.name "dorfl[bot]"
1284
+ git config user.email "dorfl[bot]@users.noreply.github.com"
1113
1285
 
1114
- auth.anthropic = {
1115
- type: "oauth",
1116
- refresh: data.refresh_token,
1117
- access: data.access_token,
1118
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
1119
- };
1120
-
1121
- writeFileSync(authPath, JSON.stringify(auth, null, 2));
1122
- chmodSync(authPath, 0o600);
1123
- console.log(
1124
- "Token refreshed, new expiry:",
1125
- new Date(auth.anthropic.expires).toISOString()
1126
- );
1127
-
1128
- // Update the GitHub secret so the next run has the latest refresh token.
1129
- const repo = process.env.GITHUB_REPOSITORY;
1130
- const token = process.env.GH_PAT;
1131
- if (repo && token) {
1132
- try {
1133
- execSync(\`gh secret set PI_AUTH_JSON --repo "\${repo}"\`, {
1134
- input: JSON.stringify(auth),
1135
- env: { ...process.env, GH_TOKEN: token },
1136
- stdio: ["pipe", "pipe", "pipe"],
1137
- });
1138
- console.log("PI_AUTH_JSON secret updated");
1139
- } catch (err) {
1140
- console.warn("Failed to update secret (non-fatal):", err.stderr?.toString() || err.message);
1141
- }
1142
- } else {
1143
- console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
1144
- }
1286
+ ${installSteps}
1145
1287
  `;
1288
+ }
1146
1289
 
1147
1290
  // ─── the --fake snapshot mechanism + artifact assembly ───────────────────────
1148
1291
 
1149
1292
  /**
1150
- * The setup ARTIFACTS this task generates: the composite setup action + (in
1151
- * `auth-json` mode) the OAuth-refresh script, plus any selected capabilities'
1293
+ * The setup ARTIFACTS this task generates: the composite setup actions, plus
1294
+ * any selected capabilities'
1152
1295
  * workflow files. This task emits NO capability workflow itself (those are the
1153
1296
  * sibling tasks); `capabilities` defaults to none.
1154
1297
  */
@@ -1168,13 +1311,11 @@ export function buildSetupArtifacts(
1168
1311
  path: join('actions', 'dorfl-setup', 'action.yml'),
1169
1312
  content: generateSetupAction(config, options?.projectSetupSteps),
1170
1313
  },
1314
+ {
1315
+ path: join('actions', 'dorfl-setup-writer', 'action.yml'),
1316
+ content: generateWriterSetupAction(config),
1317
+ },
1171
1318
  ];
1172
- if (config.authMode === 'auth-json') {
1173
- files.push({
1174
- path: join('scripts', 'refresh-oauth-token.mjs'),
1175
- content: REFRESH_OAUTH_SCRIPT,
1176
- });
1177
- }
1178
1319
  for (const cap of capabilities) {
1179
1320
  files.push(...cap.emit(config));
1180
1321
  }
@@ -1195,6 +1336,13 @@ export function outputBaseName(fake: boolean): string {
1195
1336
  * Write the artifacts under `<workDir>/<base>/` (base = `.fake` when `fake`, else
1196
1337
  * `.github`). Returns the repo-relative paths actually written. Pure filesystem
1197
1338
  * I/O — no secrets, no git. Creates parent dirs as needed.
1339
+ *
1340
+ * PIN PRESERVATION. Before writing a YAML artifact, any `uses:` of an action the
1341
+ * repository's EXISTING `.github/<path>` already pins to a full commit SHA keeps
1342
+ * that reference and its trailing comment (see {@link preserveExistingPins}),
1343
+ * so a Dependabot bump survives regeneration and a pin is never swapped for a
1344
+ * tag. The existing file is always read from `.github/`, also in `--fake` mode,
1345
+ * so the `.fake/` preview shows what a real run would write.
1198
1346
  */
1199
1347
  export function writeArtifacts(options: {
1200
1348
  workDir: string;
@@ -1205,8 +1353,15 @@ export function writeArtifacts(options: {
1205
1353
  const written: string[] = [];
1206
1354
  for (const file of options.files) {
1207
1355
  const abs = join(options.workDir, base, file.path);
1356
+ let content = file.content;
1357
+ if (/\.ya?ml$/.test(file.path)) {
1358
+ const current = join(options.workDir, outputBaseName(false), file.path);
1359
+ if (existsSync(current)) {
1360
+ content = preserveExistingPins(content, readFileSync(current, 'utf8'));
1361
+ }
1362
+ }
1208
1363
  mkdirSync(dirname(abs), {recursive: true});
1209
- writeFileSync(abs, file.content, 'utf8');
1364
+ writeFileSync(abs, content, 'utf8');
1210
1365
  written.push(relative(options.workDir, abs));
1211
1366
  }
1212
1367
  return written;