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
@@ -24,9 +23,18 @@
24
23
  * policy is the SAME engine gate family resolved via the generated workflow's
25
24
  * `DORFL_*` env block, NOT a new config knob minted here.
26
25
  */
27
- import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
26
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, } from 'node:fs';
28
27
  import { dirname, join, relative } from 'node:path';
29
28
  import { fileURLToPath } from 'node:url';
29
+ import { ACTION_PINS, pinnedUses, preserveExistingPins, } from './install-ci-action-pins.js';
30
+ /**
31
+ * The refusal a config naming the removed `auth-json` mode gets: it says the
32
+ * mode is gone and names the replacement (`models-json` + a proxy `baseUrl`).
33
+ */
34
+ export const AUTH_JSON_REMOVED_MESSAGE = 'config file "authMode": "auth-json" is no longer supported (the mode was ' +
35
+ 'removed: its OAuth refresh needed a write-capable GH_PAT in the agent job). ' +
36
+ 'Use "authMode": "models-json" and point a provider\'s "baseUrl" at a proxy ' +
37
+ 'that handles credential rotation outside GitHub.';
30
38
  /** Parse a provider's visibility answer (`PUBLIC`, `private`, ...); unknown ⇒ `undefined`. */
31
39
  export function parseRepoVisibility(raw) {
32
40
  const v = raw.trim().toLowerCase();
@@ -125,15 +133,20 @@ function pathToFileUrl(absPath) {
125
133
  // ─── models.json generation ──────────────────────────────────────────────────
126
134
  /**
127
135
  * Build the harness `models.json` object from the providers. Built-in providers
128
- * (`anthropic`/`openai`) get `{apiKey: <ENV_VAR>}` (the harness resolves the env
129
- * var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
136
+ * (`anthropic`/`openai`) get `{apiKey: "$<ENV_VAR>"}` (the harness resolves the
137
+ * env var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
130
138
  * Mirrors whitesmith's `buildModelsJson`.
139
+ *
140
+ * The `apiKey` is an env-var REFERENCE (see {@link modelsJsonEnvRef}), never the
141
+ * bare env-var name: pi treats a bare value as a LITERAL key.
131
142
  */
132
143
  export function buildModelsJson(providers) {
133
144
  const providersObj = {};
134
145
  for (const p of providers) {
135
146
  if (p.builtin) {
136
- const entry = { apiKey: p.apiKeyEnvVar };
147
+ const entry = {
148
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
149
+ };
137
150
  if (p.baseUrl)
138
151
  entry.baseUrl = p.baseUrl;
139
152
  providersObj[p.name] = entry;
@@ -142,7 +155,7 @@ export function buildModelsJson(providers) {
142
155
  const entry = {
143
156
  baseUrl: p.baseUrl,
144
157
  api: p.api,
145
- apiKey: p.apiKeyEnvVar,
158
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
146
159
  models: p.models,
147
160
  };
148
161
  if (p.compat)
@@ -152,18 +165,25 @@ export function buildModelsJson(providers) {
152
165
  }
153
166
  return { providers: providersObj };
154
167
  }
168
+ /**
169
+ * The `models.json` config value that makes the pi harness read `envVarName`
170
+ * from the environment: `"$NAME"`. The pinned harness
171
+ * (`@earendil-works/pi-coding-agent`, `dist/core/resolve-config-value.js`)
172
+ * resolves a config value as a shell command when it starts with `!`, as an env
173
+ * var when it is `$NAME` / `${NAME}`, and as a LITERAL otherwise. Writing the bare
174
+ * name therefore sent the string `ANTHROPIC_API_KEY` as the token (`401
175
+ * Unauthorized` on every CI launch).
176
+ */
177
+ export function modelsJsonEnvRef(envVarName) {
178
+ return `$${envVarName}`;
179
+ }
155
180
  // ─── secret-orchestration LOGIC (which secrets, dedup) ───────────────────────
156
181
  /**
157
182
  * The DEDUPLICATED set of secret env-var names the config requires, in first-seen
158
- * order. For `models-json` mode it is one per distinct provider `apiKeyEnvVar`;
159
- * for `auth-json` mode it is the fixed `PI_AUTH_JSON` + `GH_PAT` pair (the sharp
160
- * edge). This is the pure secret-orchestration LOGIC the adapter's setter
183
+ * order: one per distinct provider `apiKeyEnvVar`. This is the pure secret-orchestration LOGIC the adapter's setter
161
184
  * consumes — which secrets, deduped — separated from any provider I/O.
162
185
  */
163
186
  export function requiredSecretNames(config) {
164
- if (config.authMode === 'auth-json') {
165
- return ['PI_AUTH_JSON', 'GH_PAT'];
166
- }
167
187
  const seen = new Set();
168
188
  const names = [];
169
189
  for (const p of config.providers) {
@@ -176,13 +196,13 @@ export function requiredSecretNames(config) {
176
196
  }
177
197
  /**
178
198
  * The GitHub secret the propose/merge CI legs read as `GH_TOKEN` to open PRs
179
- * under a CHOSEN identity (a PAT / App token). It is the PR-IDENTITY token, a
180
- * distinct purpose from `auth-json` mode's `GH_PAT` (which exists only to rotate
181
- * the OAuth `PI_AUTH_JSON` secret). The generated workflows reference it as
199
+ * under a CHOSEN identity (a PAT / App token). The generated workflows reference it as
182
200
  * `${{ secrets.DORFL_GH_TOKEN || secrets.GITHUB_TOKEN }}` so it is
183
201
  * OPTIONAL: unset ⇒ the legs fall back to the built-in `GITHUB_TOKEN` (PRs are
184
- * `github-actions[bot]` and do not trigger downstream `on: pull_request`
185
- * workflows); set ⇒ PRs carry the token's identity and DO trigger them.
202
+ * `github-actions[bot]`, and GitHub holds their `on: pull_request` runs in
203
+ * `action_required` until someone approves them); set ⇒ PRs carry the token's
204
+ * identity and trigger those workflows on their own. `intake.yml` never passes
205
+ * it, so intake PRs always need the approval (`docs/ci/README.md`).
186
206
  */
187
207
  export const PR_IDENTITY_SECRET_NAME = 'DORFL_GH_TOKEN';
188
208
  /**
@@ -207,13 +227,13 @@ export function optionalSecretNames(config) {
207
227
  * entry). Each entry maps the action input (named identically to the secret) to
208
228
  * `${{ secrets.<NAME> }}`.
209
229
  *
210
- * Empty string when there are no provider keys (auth-json mode, or no providers),
230
+ * Empty string when there are no provider keys (no providers),
211
231
  * so the `uses:` line is emitted bare and unchanged. This is the SINGLE place the
212
232
  * provider→`with:` shape lives, shared by every capability workflow generator, so
213
233
  * no template re-derives it.
214
234
  */
215
235
  export function providerSecretsWithBlock(config) {
216
- const names = config.authMode === 'models-json' ? requiredSecretNames(config) : [];
236
+ const names = providerSecretNames(config);
217
237
  if (names.length === 0) {
218
238
  return '';
219
239
  }
@@ -222,6 +242,16 @@ export function providerSecretsWithBlock(config) {
222
242
  .join('\n');
223
243
  return `\n with:\n${entries}`;
224
244
  }
245
+ /**
246
+ * The provider API key secrets the AGENT-running jobs forward to the setup
247
+ * action: one per distinct provider key (the same set as
248
+ * {@link requiredSecretNames}). The split item workflow declares exactly these (plus
249
+ * {@link PR_IDENTITY_SECRET_NAME}) as its `workflow_call` secrets, and hands
250
+ * them to its agent job only.
251
+ */
252
+ export function providerSecretNames(config) {
253
+ return requiredSecretNames(config);
254
+ }
225
255
  /**
226
256
  * Orchestrate the required secrets through the provider seam: for each
227
257
  * DEDUPLICATED required name, take its value from `knownSecrets` (the
@@ -289,10 +319,17 @@ export function loadCIConfigFile(filePath) {
289
319
  throw new CIConfigError(`could not read/parse config file ${filePath}: ` +
290
320
  (err instanceof Error ? err.message : String(err)));
291
321
  }
292
- const authMode = data.authMode ?? 'models-json';
293
- if (authMode !== 'models-json' && authMode !== 'auth-json') {
294
- throw new CIConfigError(`config file "authMode" must be "models-json" or "auth-json"`);
322
+ // `authMode` is kept as a key (so existing configs still parse) but the only
323
+ // accepted value is `models-json`. The removed `auth-json` mode gets a refusal
324
+ // that names its replacement rather than the generic "must be" message.
325
+ const rawAuthMode = data.authMode;
326
+ if (rawAuthMode === 'auth-json') {
327
+ throw new CIConfigError(AUTH_JSON_REMOVED_MESSAGE);
295
328
  }
329
+ if (rawAuthMode !== undefined && rawAuthMode !== 'models-json') {
330
+ throw new CIConfigError(`config file "authMode" must be "models-json"`);
331
+ }
332
+ const authMode = 'models-json';
296
333
  if (!data.defaultProvider) {
297
334
  throw new CIConfigError('config file must contain "defaultProvider"');
298
335
  }
@@ -313,16 +350,14 @@ export function loadCIConfigFile(filePath) {
313
350
  'the per-provider payload is OPAQUE to the core');
314
351
  }
315
352
  }
316
- if (authMode === 'models-json') {
317
- if (!data.providers ||
318
- !Array.isArray(data.providers) ||
319
- data.providers.length === 0) {
320
- throw new CIConfigError('config file (models-json) must contain a non-empty "providers" array');
321
- }
353
+ if (!data.providers ||
354
+ !Array.isArray(data.providers) ||
355
+ data.providers.length === 0) {
356
+ throw new CIConfigError('config file (models-json) must contain a non-empty "providers" array');
322
357
  }
323
358
  return {
324
359
  authMode,
325
- providers: data.providers ?? [],
360
+ providers: data.providers,
326
361
  defaultProvider: data.defaultProvider,
327
362
  defaultModel: data.defaultModel,
328
363
  harness: data.harness,
@@ -372,7 +407,7 @@ export function exportCIConfig(config, secrets) {
372
407
  }
373
408
  return JSON.stringify(file, null, 2) + '\n';
374
409
  }
375
- // ─── composite setup action + OAuth-refresh script generation ────────────────
410
+ // ─── composite setup action generation ───────────────────────────────────────
376
411
  /** Indent every non-blank line of `text` by `spaces` spaces. */
377
412
  function indent(text, spaces) {
378
413
  const pad = ' '.repeat(spaces);
@@ -411,6 +446,48 @@ export const PI_HARNESS_PACKAGE = '@earendil-works/pi-coding-agent';
411
446
  * `node-version: '22'` satisfies (setup-node resolves the latest 22.x).
412
447
  */
413
448
  export const PI_HARNESS_VERSION = '0.80.6';
449
+ /**
450
+ * The `pi` packages the harness LOADS besides itself, each pinned to an exact
451
+ * version that is known to load with {@link PI_HARNESS_VERSION}.
452
+ *
453
+ * `pi-coding-agent` declares its sibling packages with a caret range
454
+ * (`@earendil-works/pi-ai: ^0.80.6`), which on a `0.x` version floats within
455
+ * the minor. `pi-ai@0.80.10` removed the `getOAuthApiKey` export that
456
+ * `pi-coding-agent@0.80.6` imports, so a resolver that honours the caret kills
457
+ * `pi` on start. npm happens to be protected by the `npm-shrinkwrap.json` the
458
+ * package publishes; pnpm ignores that file and was broken. Pinning the family
459
+ * explicitly (as npm `overrides`, see {@link piHarnessManifest}) makes the
460
+ * install reproducible whatever the resolver.
461
+ *
462
+ * The set is `pi-coding-agent`'s own `@earendil-works/*` dependencies plus
463
+ * theirs (`npm view @earendil-works/pi-coding-agent@<v> dependencies`,
464
+ * recursively): at 0.80.6 that is pi-ai, pi-agent-core (which depends on pi-ai)
465
+ * and pi-tui. Re-derive it on every {@link PI_HARNESS_VERSION} bump: a newer
466
+ * harness may add a package (0.87 adds `@earendil-works/chord`).
467
+ */
468
+ export const PI_HARNESS_PINNED_DEPENDENCIES = {
469
+ '@earendil-works/pi-agent-core': PI_HARNESS_VERSION,
470
+ '@earendil-works/pi-ai': PI_HARNESS_VERSION,
471
+ '@earendil-works/pi-tui': PI_HARNESS_VERSION,
472
+ };
473
+ /**
474
+ * The throwaway `package.json` the setup action installs the harness from: the
475
+ * harness at {@link PI_HARNESS_VERSION} as the only dependency, and every pi
476
+ * package it loads forced to its exact pin through npm `overrides`.
477
+ *
478
+ * A project-local install (not `npm install -g`) because `overrides` only apply
479
+ * to the ROOT project, and a global install makes each package its own root. The
480
+ * same file is used in `registry` and `workspace` mode: pnpm 11 also isolates
481
+ * every global package, so listing the pins next to the harness in
482
+ * `pnpm add -g` would not constrain the harness's own tree either.
483
+ */
484
+ export function piHarnessManifest() {
485
+ return {
486
+ private: true,
487
+ dependencies: { [PI_HARNESS_PACKAGE]: PI_HARNESS_VERSION },
488
+ overrides: { ...PI_HARNESS_PINNED_DEPENDENCIES },
489
+ };
490
+ }
414
491
  /**
415
492
  * The version of THIS dorfl package, read at runtime from its own
416
493
  * `package.json` (the single source changesets bumps on release). The composite
@@ -442,25 +519,47 @@ export function dorflPackageVersion() {
442
519
  }
443
520
  return version;
444
521
  }
522
+ /** Where the setup action installs the harness (under the job's temp dir). */
523
+ const PI_HARNESS_DIR = '$RUNNER_TEMP/dorfl-harness';
445
524
  /**
446
- * The install step for the configured harness (the `pi` CLI; `''` ⇒ none).
447
- * `registry` mode installs via `npm install -g`; `workspace` mode installs via
448
- * `pnpm add -g` so the harness lands on the pnpm global bin already on
449
- * `$GITHUB_PATH` (mirroring whitesmith's dev mode).
525
+ * The install + smoke steps for the configured harness (the `pi` CLI; `''` ⇒
526
+ * none). Identical in `registry` and `workspace` mode: the harness is installed
527
+ * with npm from {@link piHarnessManifest} into a job-local directory whose
528
+ * `node_modules/.bin` is added to `$GITHUB_PATH`, so every pi package it loads
529
+ * is pinned whatever package manager the repository uses. The smoke step then
530
+ * runs `pi --version`, which imports the harness's whole module graph, so a
531
+ * broken install fails setup with a clear message instead of failing inside a
532
+ * dorfl agent launch.
450
533
  */
451
- function harnessInstallStep(harness, installSource) {
534
+ function harnessInstallStep(harness) {
452
535
  if (harness === 'pi') {
453
- const spec = `${PI_HARNESS_PACKAGE}@${PI_HARNESS_VERSION}`;
454
- const run = installSource === 'workspace'
455
- ? `pnpm add -g ${spec}`
456
- : `npm install -g ${spec}`;
536
+ const manifest = JSON.stringify(piHarnessManifest(), null, 2);
457
537
  return `
458
538
  # Pinned to the exact harness version this dorfl release declares
459
- # (PI_HARNESS_VERSION): the job holds write access and a provider key, so the
460
- # version must not move on its own. Re-run \`dorfl install-ci\` to upgrade.
539
+ # (PI_HARNESS_VERSION), with every pi package it loads pinned too (npm
540
+ # \`overrides\`; a caret range on a 0.x version floats within the minor): the
541
+ # job holds a provider key, so no version may move on its own. Re-run
542
+ # \`dorfl install-ci\` to upgrade.
461
543
  - name: Install agent harness (pi)
462
544
  shell: bash
463
- run: ${run}`;
545
+ run: |
546
+ mkdir -p "${PI_HARNESS_DIR}"
547
+ cd "${PI_HARNESS_DIR}"
548
+ cat > package.json << 'HARNESS_EOF'
549
+ ${indent(manifest, 8)}
550
+ HARNESS_EOF
551
+ npm install --no-audit --no-fund
552
+ echo "${PI_HARNESS_DIR}/node_modules/.bin" >> "$GITHUB_PATH"
553
+
554
+ # \`pi --version\` loads the harness's module graph, so an install whose pi
555
+ # packages do not load together fails HERE, not inside a dorfl agent launch.
556
+ - name: Check the agent harness loads (pi)
557
+ shell: bash
558
+ run: |
559
+ if ! pi --version; then
560
+ 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."
561
+ exit 1
562
+ fi`;
464
563
  }
465
564
  return '';
466
565
  }
@@ -472,14 +571,11 @@ function harnessInstallStep(harness, installSource) {
472
571
  * `uses: ./.github/actions/dorfl-setup`. The advance-loop seed template
473
572
  * already references this exact action name + path (`docs/ci/README.md`).
474
573
  *
475
- * The auth step branches on the mode: `models-json` writes a generated
476
- * `~/.pi/agent/models.json` inline (the conservative default) AND exports each
574
+ * The auth step writes a generated `~/.pi/agent/models.json` inline AND exports each
477
575
  * configured provider's API key from a same-named ACTION INPUT to `$GITHUB_ENV`,
478
576
  * so the agent step (`pi`) can resolve the env var `models.json` references; the
479
577
  * workflow passes the secrets ONCE via `with:` on the `uses:` line (no
480
- * per-step / per-capability provider enumeration). `auth-json` writes
481
- * `~/.pi/agent/auth.json` from `$PI_AUTH_JSON` + runs the OAuth-refresh script
482
- * (the sharp edge). Deterministic: the same config produces byte-identical output.
578
+ * per-step / per-capability provider enumeration). Deterministic: the same config produces byte-identical output.
483
579
  */
484
580
  export function generateSetupAction(config,
485
581
  /**
@@ -491,43 +587,24 @@ export function generateSetupAction(config,
491
587
  * baseline (the load-bearing absent-⇒-identical contract).
492
588
  */
493
589
  projectSetupSteps) {
494
- let authStep;
495
- // models-json mode declares one ACTION INPUT per distinct provider key (named
496
- // identically to the secret / env var) and exports it to `$GITHUB_ENV`, so every
497
- // later step inherits it. auth-json mode has no provider keys (it uses auth.json).
498
- const providerKeyNames = config.authMode === 'models-json' ? requiredSecretNames(config) : [];
499
- if (config.authMode === 'auth-json') {
500
- authStep = `\
501
- - name: Configure agent auth (auth.json)
502
- shell: bash
503
- run: |
504
- if [ -z "$PI_AUTH_JSON" ]; then
505
- echo "ERROR: PI_AUTH_JSON secret is not set" >&2; exit 1
506
- fi
507
- mkdir -p ~/.pi/agent
508
- echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
509
- chmod 600 ~/.pi/agent/auth.json
510
-
511
- # Workaround for https://github.com/badlogic/pi-mono/issues/2743 — the SINGLE
512
- # CI→repo mutation in the design; needs GH_PAT to rotate PI_AUTH_JSON.
513
- - name: Refresh OAuth token
514
- shell: bash
515
- run: node .github/scripts/refresh-oauth-token.mjs`;
516
- }
517
- else {
518
- const modelsJsonStr = JSON.stringify(buildModelsJson(config.providers), null, 2);
519
- // Map each provider-key INPUT into the step env, then append the non-empty
520
- // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
521
- // (never interpolated into the script body), and Actions masks the secret in
522
- // logs. `models.json`'s `apiKey` is the env-var NAME; this puts the value there.
523
- const exportEnvLines = providerKeyNames
524
- .map((name) => ` ${name}: \${{ inputs.${name} }}`)
525
- .join('\n');
526
- const exportRunLines = providerKeyNames
527
- .map((name) => ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`)
528
- .join('\n');
529
- const exportStep = providerKeyNames.length > 0
530
- ? `\n
590
+ // models-json mode (the only mode) declares one ACTION INPUT per distinct
591
+ // provider key (named identically to the secret / env var) and exports it to
592
+ // `$GITHUB_ENV`, so every later step inherits it.
593
+ const providerKeyNames = providerSecretNames(config);
594
+ const modelsJsonStr = JSON.stringify(buildModelsJson(config.providers), null, 2);
595
+ // Map each provider-key INPUT into the step env, then append the non-empty
596
+ // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
597
+ // (never interpolated into the script body), and Actions masks the secret in
598
+ // logs. `models.json`'s `apiKey` is a `$NAME` env-var REFERENCE (pi resolves it at
599
+ // runtime; a bare name would be a literal key); this puts the value there.
600
+ const exportEnvLines = providerKeyNames
601
+ .map((name) => ` ${name}: \${{ inputs.${name} }}`)
602
+ .join('\n');
603
+ const exportRunLines = providerKeyNames
604
+ .map((name) => ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`)
605
+ .join('\n');
606
+ const exportStep = providerKeyNames.length > 0
607
+ ? `\n
531
608
  # Surface the configured provider API key(s) to the environment so the agent
532
609
  # step (\`pi\`) can resolve the env var \`models.json\` references. The workflow
533
610
  # passes the secret(s) via \`with:\` on the \`uses:\` line; we forward the
@@ -538,8 +615,8 @@ projectSetupSteps) {
538
615
  ${exportEnvLines}
539
616
  run: |
540
617
  ${exportRunLines}`
541
- : '';
542
- authStep = `\
618
+ : '';
619
+ const authStep = `\
543
620
  - name: Configure agent models (models.json)
544
621
  shell: bash
545
622
  run: |
@@ -547,20 +624,19 @@ ${exportRunLines}`
547
624
  cat > ~/.pi/agent/models.json << 'MODELS_EOF'
548
625
  ${indent(modelsJsonStr, 8)}
549
626
  MODELS_EOF${exportStep}`;
550
- }
551
- const installHarness = harnessInstallStep(config.harness, config.installSource);
627
+ const installHarness = harnessInstallStep(config.harness);
552
628
  // The CLI-install block branches on installSource. `registry` installs the
553
629
  // published CLI via `npm install -g dorfl` (the default for every
554
630
  // consumer repo). `workspace` builds the CLI from the checked-out source and
555
631
  // links it onto PATH — for the self-hosting dorfl monorepo, which is
556
632
  // not published under that npm name. We add pnpm's global bin to $GITHUB_PATH
557
- // so the linked `dorfl` (and the pnpm-installed harness) are on PATH in
558
- // all subsequent steps; we always rebuild because source changes per commit.
633
+ // so the linked `dorfl` is on PATH in all subsequent steps (the harness adds
634
+ // its own bin dir, the same way in both modes); we always rebuild because source changes per commit.
559
635
  let installSteps;
560
636
  if (config.installSource === 'workspace') {
561
637
  installSteps = `\
562
638
  - name: Setup pnpm
563
- uses: pnpm/action-setup@v5
639
+ uses: ${pinnedUses(ACTION_PINS.pnpmSetup)}
564
640
 
565
641
  - name: Add pnpm global bin to PATH
566
642
  shell: bash
@@ -609,10 +685,9 @@ ${indent(modelsJsonStr, 8)}
609
685
  // would exec the local bin which then forwards AGAIN — a confusing
610
686
  // double-resolution. The install step below leaves `dorfl` on `$PATH` as the
611
687
  // bootstrap; the forward does the pinning.
612
- // One optional ACTION INPUT per provider key (models-json mode), named
613
- // identically to the secret/env var. Optional + default '' so a workflow that
614
- // does not pass it (or auth-json mode, which declares none) is valid; the export
615
- // step skips an empty value. auth-json mode declares no inputs.
688
+ // One optional ACTION INPUT per provider key, named identically to the
689
+ // secret/env var. Optional + default '' so a workflow that does not pass it is
690
+ // valid; the export step skips an empty value.
616
691
  const inputsBlock = providerKeyNames.length > 0
617
692
  ? `inputs:
618
693
  ${providerKeyNames
@@ -648,15 +723,16 @@ runs:
648
723
  using: composite
649
724
  steps:
650
725
  ${projectSetupBlock} - name: Setup Node.js
651
- uses: actions/setup-node@v5
726
+ uses: ${pinnedUses(ACTION_PINS.setupNode)}
652
727
  with:
653
728
  node-version: '22'
654
- # setup-node@v5 auto-enables package-manager caching, which probes the
655
- # project package manager DURING its own run when a lockfile is present,
656
- # before that manager is on PATH, so the probe crashes ("Unable to
657
- # locate executable file"). dorfl provisions ONLY its own runtime and
658
- # leaves the project package-manager cache to the project (toolchain
659
- # boundary), so disable it explicitly.
729
+ # setup-node (v5 and later) auto-enables package-manager caching. In v5
730
+ # it probed whatever manager package.json's packageManager named, during
731
+ # its own run and before that manager was on PATH, and crashed ("Unable
732
+ # to locate executable file"). Since v6 it only fires when package.json
733
+ # declares npm, and then fails if there is no npm lockfile. Either way
734
+ # it is the project's cache, not dorfl's: dorfl provisions ONLY its own
735
+ # runtime (toolchain boundary), so disable it explicitly.
660
736
  package-manager-cache: false
661
737
 
662
738
  - name: Configure git identity
@@ -670,109 +746,125 @@ ${installSteps}
670
746
  ${authStep}
671
747
  `;
672
748
  }
749
+ /** The local path a workflow step `uses:` for the agent-role setup action. */
750
+ export const SETUP_ACTION_USES = './.github/actions/dorfl-setup';
751
+ /** The local path a workflow step `uses:` for the writer-role setup action. */
752
+ export const WRITER_SETUP_ACTION_USES = './.github/actions/dorfl-setup-writer';
673
753
  /**
674
- * The OAuth-refresh script emitted ONLY in `auth-json` mode (whitesmith's
675
- * `pi-mono#2743` workaround): refreshes the Anthropic OAuth token in
676
- * `~/.pi/agent/auth.json` before the agent runs, then writes the rotated token
677
- * BACK via `gh secret set PI_AUTH_JSON` (the single CI→repo mutation, needing a
678
- * `GH_PAT`). This is the documented SHARP EDGE; `models-json` mode avoids it.
679
- */
680
- export const REFRESH_OAUTH_SCRIPT = `\
681
- #!/usr/bin/env node
682
- /**
683
- * Refresh OAuth tokens in the agent's auth.json before it runs.
754
+ * Generate the WRITER-ROLE composite setup action (`dorfl-setup-writer`, spec
755
+ * `ci-agent-job-without-write-token` §9, ADR
756
+ * `ci-agent-job-holds-no-write-token`; task `ci-split-generate-workflows`).
757
+ *
758
+ * The jobs that hold a write token and run no agent (the split item
759
+ * workflow's lock and apply jobs, `reap-merged-branches`, `close-job`, and the
760
+ * read-only `enumerate`) install Node and dorfl ONLY: no harness, no provider
761
+ * key, no `models.json`, no project-setup hook, no install of the project's
762
+ * dependencies, and no Actions cache. In `registry` mode the dorfl install runs
763
+ * from `$RUNNER_TEMP`, outside the checkout, with `--ignore-scripts`, so a
764
+ * repository `.npmrc` cannot redirect the registry and no install script runs.
684
765
  *
685
- * Workaround for https://github.com/badlogic/pi-mono/issues/2743.
686
- * After refreshing, updates the PI_AUTH_JSON GitHub secret so the next run has
687
- * the latest rotated refresh token (requires GH_PAT with repo scope). This is the
688
- * known SHARP EDGE of auth-json mode; models-json mode avoids it entirely.
766
+ * In `workspace` mode (dorfl's own repository) dorfl is built from source
767
+ * (decision 8): the tree of `source-ref` (default the checkout's `HEAD`) is
768
+ * exported with `git archive` into `$RUNNER_TEMP` and built there with
769
+ * `--ignore-scripts`. In merge mode that is code an earlier agent landed on
770
+ * `main`, which is the trust merge mode already implies.
689
771
  *
690
- * Remove this script once the upstream fix is released.
772
+ * It is a SIBLING action rather than a `role` input on `dorfl-setup`: the
773
+ * project-setup hook is an opaque fragment spliced first into `dorfl-setup`
774
+ * (ADR `install-ci-project-provisioning-native-passthrough`), and a composite
775
+ * action cannot skip an opaque block of steps by input. `dorfl-setup` stays the
776
+ * agent role, byte for byte.
691
777
  */
692
- import { readFileSync, writeFileSync, chmodSync } from "fs";
693
- import { join } from "path";
694
- import { execSync } from "child_process";
695
-
696
- const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
697
- const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
698
-
699
- const authPath = join(process.env.HOME, ".pi", "agent", "auth.json");
700
- const auth = JSON.parse(readFileSync(authPath, "utf-8"));
701
- const cred = auth.anthropic;
702
-
703
- if (!cred || cred.type !== "oauth") {
704
- console.log("No OAuth credentials for anthropic, skipping refresh");
705
- process.exit(0);
706
- }
707
-
708
- if (Date.now() < cred.expires) {
709
- console.log("Token still valid until", new Date(cred.expires).toISOString());
710
- process.exit(0);
711
- }
712
-
713
- console.log(
714
- "Token expired at",
715
- new Date(cred.expires).toISOString(),
716
- "- refreshing..."
717
- );
778
+ export function generateWriterSetupAction(config) {
779
+ let installSteps;
780
+ let inputsBlock = '';
781
+ if (config.installSource === 'workspace') {
782
+ inputsBlock = `inputs:
783
+ source-ref:
784
+ description: 'The commit to build dorfl from (workspace mode). Blank: the checkout HEAD.'
785
+ required: false
786
+ default: ''
718
787
 
719
- const response = await fetch(ANTHROPIC_TOKEN_URL, {
720
- method: "POST",
721
- headers: {
722
- "Content-Type": "application/x-www-form-urlencoded",
723
- Accept: "application/json",
724
- },
725
- body: new URLSearchParams({
726
- grant_type: "refresh_token",
727
- client_id: ANTHROPIC_CLIENT_ID,
728
- refresh_token: cred.refresh,
729
- }).toString(),
730
- signal: AbortSignal.timeout(30_000),
731
- });
788
+ `;
789
+ installSteps = `\
790
+ - name: Setup pnpm
791
+ uses: ${pinnedUses(ACTION_PINS.pnpmSetup)}
732
792
 
733
- const data = await response.json();
793
+ - name: Add pnpm global bin to PATH
794
+ shell: bash
795
+ run: |
796
+ pnpm setup
797
+ echo "$HOME/.local/share/pnpm" >> "$GITHUB_PATH"
734
798
 
735
- if (!response.ok) {
736
- console.error("Refresh failed:", response.status, JSON.stringify(data));
737
- process.exit(1);
738
- }
799
+ # Workspace mode (dorfl's own repository, decision 8 of ADR
800
+ # ci-agent-job-holds-no-write-token): build dorfl from the tree of
801
+ # \`source-ref\`, exported into $RUNNER_TEMP (outside the checkout), with
802
+ # \`--ignore-scripts\`. In merge mode this is code an earlier agent landed on
803
+ # main running in a write job, the trust merge mode already implies.
804
+ # Only dorfl and its workspace dependencies (\`--filter 'dorfl...'\`) are
805
+ # installed and built: the other workspace packages (the website) are not
806
+ # needed to run dorfl, so they do not run next to the write token.
807
+ - name: Build dorfl from the base commit (outside the checkout)
808
+ shell: bash
809
+ env:
810
+ SOURCE_REF: \${{ inputs.source-ref }}
811
+ run: |
812
+ set -euo pipefail
813
+ src="$RUNNER_TEMP/dorfl-src"
814
+ rm -rf "$src"
815
+ mkdir -p "$src"
816
+ git archive "$(git rev-parse --verify "\${SOURCE_REF:-HEAD}^{commit}")" | tar -x -C "$src"
817
+ cd "$src"
818
+ pnpm install --frozen-lockfile --ignore-scripts --filter 'dorfl...'
819
+ pnpm --filter 'dorfl...' build
820
+ cd packages/dorfl && pnpm link --global`;
821
+ }
822
+ else {
823
+ installSteps = `\
824
+ # Pinned to the dorfl version that generated this action. Installed from
825
+ # $RUNNER_TEMP (outside the checkout, so a repository .npmrc cannot redirect
826
+ # the registry) with --ignore-scripts (no install script runs next to the
827
+ # write token).
828
+ - name: Install dorfl (outside the checkout, no install scripts)
829
+ shell: bash
830
+ run: |
831
+ cd "$RUNNER_TEMP"
832
+ npm install -g --ignore-scripts dorfl@${dorflPackageVersion()}`;
833
+ }
834
+ return `\
835
+ name: Setup dorfl (writer role)
836
+ description: Install Node.js and dorfl only, for the CI jobs that run no agent (lock, apply, reap, close, enumerate)
739
837
 
740
- auth.anthropic = {
741
- type: "oauth",
742
- refresh: data.refresh_token,
743
- access: data.access_token,
744
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
745
- };
838
+ ${inputsBlock}# THE WRITER ROLE (ADR ci-agent-job-holds-no-write-token): the jobs that use
839
+ # this action hold a write token (or run next to one) and run NO agent and NO
840
+ # repository code. So it installs Node.js and dorfl ONLY: no agent harness, no
841
+ # provider API key, no models.json, no project-setup hook, no install of the
842
+ # project's dependencies, and no Actions cache (an agent can write
843
+ # default-branch cache entries, so a restore here would run its bytes with
844
+ # write access). The agent role is ./.github/actions/dorfl-setup.
845
+ runs:
846
+ using: composite
847
+ steps:
848
+ - name: Setup Node.js
849
+ uses: ${pinnedUses(ACTION_PINS.setupNode)}
850
+ with:
851
+ node-version: '22'
852
+ # No cache in the writer role (see above).
853
+ package-manager-cache: false
746
854
 
747
- writeFileSync(authPath, JSON.stringify(auth, null, 2));
748
- chmodSync(authPath, 0o600);
749
- console.log(
750
- "Token refreshed, new expiry:",
751
- new Date(auth.anthropic.expires).toISOString()
752
- );
855
+ - name: Configure git identity
856
+ shell: bash
857
+ run: |
858
+ git config user.name "dorfl[bot]"
859
+ git config user.email "dorfl[bot]@users.noreply.github.com"
753
860
 
754
- // Update the GitHub secret so the next run has the latest refresh token.
755
- const repo = process.env.GITHUB_REPOSITORY;
756
- const token = process.env.GH_PAT;
757
- if (repo && token) {
758
- try {
759
- execSync(\`gh secret set PI_AUTH_JSON --repo "\${repo}"\`, {
760
- input: JSON.stringify(auth),
761
- env: { ...process.env, GH_TOKEN: token },
762
- stdio: ["pipe", "pipe", "pipe"],
763
- });
764
- console.log("PI_AUTH_JSON secret updated");
765
- } catch (err) {
766
- console.warn("Failed to update secret (non-fatal):", err.stderr?.toString() || err.message);
767
- }
768
- } else {
769
- console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
770
- }
861
+ ${installSteps}
771
862
  `;
863
+ }
772
864
  // ─── the --fake snapshot mechanism + artifact assembly ───────────────────────
773
865
  /**
774
- * The setup ARTIFACTS this task generates: the composite setup action + (in
775
- * `auth-json` mode) the OAuth-refresh script, plus any selected capabilities'
866
+ * The setup ARTIFACTS this task generates: the composite setup actions, plus
867
+ * any selected capabilities'
776
868
  * workflow files. This task emits NO capability workflow itself (those are the
777
869
  * sibling tasks); `capabilities` defaults to none.
778
870
  */
@@ -789,13 +881,11 @@ options) {
789
881
  path: join('actions', 'dorfl-setup', 'action.yml'),
790
882
  content: generateSetupAction(config, options?.projectSetupSteps),
791
883
  },
884
+ {
885
+ path: join('actions', 'dorfl-setup-writer', 'action.yml'),
886
+ content: generateWriterSetupAction(config),
887
+ },
792
888
  ];
793
- if (config.authMode === 'auth-json') {
794
- files.push({
795
- path: join('scripts', 'refresh-oauth-token.mjs'),
796
- content: REFRESH_OAUTH_SCRIPT,
797
- });
798
- }
799
889
  for (const cap of capabilities) {
800
890
  files.push(...cap.emit(config));
801
891
  }
@@ -814,14 +904,28 @@ export function outputBaseName(fake) {
814
904
  * Write the artifacts under `<workDir>/<base>/` (base = `.fake` when `fake`, else
815
905
  * `.github`). Returns the repo-relative paths actually written. Pure filesystem
816
906
  * I/O — no secrets, no git. Creates parent dirs as needed.
907
+ *
908
+ * PIN PRESERVATION. Before writing a YAML artifact, any `uses:` of an action the
909
+ * repository's EXISTING `.github/<path>` already pins to a full commit SHA keeps
910
+ * that reference and its trailing comment (see {@link preserveExistingPins}),
911
+ * so a Dependabot bump survives regeneration and a pin is never swapped for a
912
+ * tag. The existing file is always read from `.github/`, also in `--fake` mode,
913
+ * so the `.fake/` preview shows what a real run would write.
817
914
  */
818
915
  export function writeArtifacts(options) {
819
916
  const base = outputBaseName(options.fake);
820
917
  const written = [];
821
918
  for (const file of options.files) {
822
919
  const abs = join(options.workDir, base, file.path);
920
+ let content = file.content;
921
+ if (/\.ya?ml$/.test(file.path)) {
922
+ const current = join(options.workDir, outputBaseName(false), file.path);
923
+ if (existsSync(current)) {
924
+ content = preserveExistingPins(content, readFileSync(current, 'utf8'));
925
+ }
926
+ }
823
927
  mkdirSync(dirname(abs), { recursive: true });
824
- writeFileSync(abs, file.content, 'utf8');
928
+ writeFileSync(abs, content, 'utf8');
825
929
  written.push(relative(options.workDir, abs));
826
930
  }
827
931
  return written;