dorfl 0.14.3 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +305 -486
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +44 -13
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +25 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -1
  162. package/dist/install-ci-action-pins.js +25 -0
  163. package/dist/install-ci-action-pins.js.map +1 -1
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +173 -36
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +379 -186
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -293
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +6 -3
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +371 -535
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +50 -14
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +25 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +458 -218
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -328
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +6 -3
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
@@ -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
@@ -27,7 +26,17 @@
27
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 { NO_FORWARD_ENV } from './bootstrap-forward.js';
30
+ import { REPO_CONFIG_FILENAME, REPO_CONFIG_FILENAME_LEGACY, } from './repo-config.js';
30
31
  import { ACTION_PINS, pinnedUses, preserveExistingPins, } from './install-ci-action-pins.js';
32
+ /**
33
+ * The refusal a config naming the removed `auth-json` mode gets: it says the
34
+ * mode is gone and names the replacement (`models-json` + a proxy `baseUrl`).
35
+ */
36
+ export const AUTH_JSON_REMOVED_MESSAGE = 'config file "authMode": "auth-json" is no longer supported (the mode was ' +
37
+ 'removed: its OAuth refresh needed a write-capable GH_PAT in the agent job). ' +
38
+ 'Use "authMode": "models-json" and point a provider\'s "baseUrl" at a proxy ' +
39
+ 'that handles credential rotation outside GitHub.';
31
40
  /** Parse a provider's visibility answer (`PUBLIC`, `private`, ...); unknown ⇒ `undefined`. */
32
41
  export function parseRepoVisibility(raw) {
33
42
  const v = raw.trim().toLowerCase();
@@ -126,15 +135,20 @@ function pathToFileUrl(absPath) {
126
135
  // ─── models.json generation ──────────────────────────────────────────────────
127
136
  /**
128
137
  * Build the harness `models.json` object from the providers. Built-in providers
129
- * (`anthropic`/`openai`) get `{apiKey: <ENV_VAR>}` (the harness resolves the env
130
- * var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
138
+ * (`anthropic`/`openai`) get `{apiKey: "$<ENV_VAR>"}` (the harness resolves the
139
+ * env var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
131
140
  * Mirrors whitesmith's `buildModelsJson`.
141
+ *
142
+ * The `apiKey` is an env-var REFERENCE (see {@link modelsJsonEnvRef}), never the
143
+ * bare env-var name: pi treats a bare value as a LITERAL key.
132
144
  */
133
145
  export function buildModelsJson(providers) {
134
146
  const providersObj = {};
135
147
  for (const p of providers) {
136
148
  if (p.builtin) {
137
- const entry = { apiKey: p.apiKeyEnvVar };
149
+ const entry = {
150
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
151
+ };
138
152
  if (p.baseUrl)
139
153
  entry.baseUrl = p.baseUrl;
140
154
  providersObj[p.name] = entry;
@@ -143,7 +157,7 @@ export function buildModelsJson(providers) {
143
157
  const entry = {
144
158
  baseUrl: p.baseUrl,
145
159
  api: p.api,
146
- apiKey: p.apiKeyEnvVar,
160
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
147
161
  models: p.models,
148
162
  };
149
163
  if (p.compat)
@@ -153,18 +167,25 @@ export function buildModelsJson(providers) {
153
167
  }
154
168
  return { providers: providersObj };
155
169
  }
170
+ /**
171
+ * The `models.json` config value that makes the pi harness read `envVarName`
172
+ * from the environment: `"$NAME"`. The pinned harness
173
+ * (`@earendil-works/pi-coding-agent`, `dist/core/resolve-config-value.js`)
174
+ * resolves a config value as a shell command when it starts with `!`, as an env
175
+ * var when it is `$NAME` / `${NAME}`, and as a LITERAL otherwise. Writing the bare
176
+ * name therefore sent the string `ANTHROPIC_API_KEY` as the token (`401
177
+ * Unauthorized` on every CI launch).
178
+ */
179
+ export function modelsJsonEnvRef(envVarName) {
180
+ return `$${envVarName}`;
181
+ }
156
182
  // ─── secret-orchestration LOGIC (which secrets, dedup) ───────────────────────
157
183
  /**
158
184
  * The DEDUPLICATED set of secret env-var names the config requires, in first-seen
159
- * order. For `models-json` mode it is one per distinct provider `apiKeyEnvVar`;
160
- * for `auth-json` mode it is the fixed `PI_AUTH_JSON` + `GH_PAT` pair (the sharp
161
- * edge). This is the pure secret-orchestration LOGIC the adapter's setter
185
+ * order: one per distinct provider `apiKeyEnvVar`. This is the pure secret-orchestration LOGIC the adapter's setter
162
186
  * consumes — which secrets, deduped — separated from any provider I/O.
163
187
  */
164
188
  export function requiredSecretNames(config) {
165
- if (config.authMode === 'auth-json') {
166
- return ['PI_AUTH_JSON', 'GH_PAT'];
167
- }
168
189
  const seen = new Set();
169
190
  const names = [];
170
191
  for (const p of config.providers) {
@@ -177,13 +198,13 @@ export function requiredSecretNames(config) {
177
198
  }
178
199
  /**
179
200
  * The GitHub secret the propose/merge CI legs read as `GH_TOKEN` to open PRs
180
- * under a CHOSEN identity (a PAT / App token). It is the PR-IDENTITY token, a
181
- * distinct purpose from `auth-json` mode's `GH_PAT` (which exists only to rotate
182
- * the OAuth `PI_AUTH_JSON` secret). The generated workflows reference it as
201
+ * under a CHOSEN identity (a PAT / App token). The generated workflows reference it as
183
202
  * `${{ secrets.DORFL_GH_TOKEN || secrets.GITHUB_TOKEN }}` so it is
184
203
  * OPTIONAL: unset ⇒ the legs fall back to the built-in `GITHUB_TOKEN` (PRs are
185
- * `github-actions[bot]` and do not trigger downstream `on: pull_request`
186
- * workflows); set ⇒ PRs carry the token's identity and DO trigger them.
204
+ * `github-actions[bot]`, and GitHub holds their `on: pull_request` runs in
205
+ * `action_required` until someone approves them); set ⇒ PRs carry the token's
206
+ * identity and trigger those workflows on their own. `intake.yml` never passes
207
+ * it, so intake PRs always need the approval (`docs/ci/README.md`).
187
208
  */
188
209
  export const PR_IDENTITY_SECRET_NAME = 'DORFL_GH_TOKEN';
189
210
  /**
@@ -208,13 +229,13 @@ export function optionalSecretNames(config) {
208
229
  * entry). Each entry maps the action input (named identically to the secret) to
209
230
  * `${{ secrets.<NAME> }}`.
210
231
  *
211
- * Empty string when there are no provider keys (auth-json mode, or no providers),
232
+ * Empty string when there are no provider keys (no providers),
212
233
  * so the `uses:` line is emitted bare and unchanged. This is the SINGLE place the
213
234
  * provider→`with:` shape lives, shared by every capability workflow generator, so
214
235
  * no template re-derives it.
215
236
  */
216
237
  export function providerSecretsWithBlock(config) {
217
- const names = config.authMode === 'models-json' ? requiredSecretNames(config) : [];
238
+ const names = providerSecretNames(config);
218
239
  if (names.length === 0) {
219
240
  return '';
220
241
  }
@@ -223,6 +244,16 @@ export function providerSecretsWithBlock(config) {
223
244
  .join('\n');
224
245
  return `\n with:\n${entries}`;
225
246
  }
247
+ /**
248
+ * The provider API key secrets the AGENT-running jobs forward to the setup
249
+ * action: one per distinct provider key (the same set as
250
+ * {@link requiredSecretNames}). The split item workflow declares exactly these (plus
251
+ * {@link PR_IDENTITY_SECRET_NAME}) as its `workflow_call` secrets, and hands
252
+ * them to its agent job only.
253
+ */
254
+ export function providerSecretNames(config) {
255
+ return requiredSecretNames(config);
256
+ }
226
257
  /**
227
258
  * Orchestrate the required secrets through the provider seam: for each
228
259
  * DEDUPLICATED required name, take its value from `knownSecrets` (the
@@ -290,10 +321,17 @@ export function loadCIConfigFile(filePath) {
290
321
  throw new CIConfigError(`could not read/parse config file ${filePath}: ` +
291
322
  (err instanceof Error ? err.message : String(err)));
292
323
  }
293
- const authMode = data.authMode ?? 'models-json';
294
- if (authMode !== 'models-json' && authMode !== 'auth-json') {
295
- throw new CIConfigError(`config file "authMode" must be "models-json" or "auth-json"`);
324
+ // `authMode` is kept as a key (so existing configs still parse) but the only
325
+ // accepted value is `models-json`. The removed `auth-json` mode gets a refusal
326
+ // that names its replacement rather than the generic "must be" message.
327
+ const rawAuthMode = data.authMode;
328
+ if (rawAuthMode === 'auth-json') {
329
+ throw new CIConfigError(AUTH_JSON_REMOVED_MESSAGE);
330
+ }
331
+ if (rawAuthMode !== undefined && rawAuthMode !== 'models-json') {
332
+ throw new CIConfigError(`config file "authMode" must be "models-json"`);
296
333
  }
334
+ const authMode = 'models-json';
297
335
  if (!data.defaultProvider) {
298
336
  throw new CIConfigError('config file must contain "defaultProvider"');
299
337
  }
@@ -314,16 +352,14 @@ export function loadCIConfigFile(filePath) {
314
352
  'the per-provider payload is OPAQUE to the core');
315
353
  }
316
354
  }
317
- if (authMode === 'models-json') {
318
- if (!data.providers ||
319
- !Array.isArray(data.providers) ||
320
- data.providers.length === 0) {
321
- throw new CIConfigError('config file (models-json) must contain a non-empty "providers" array');
322
- }
355
+ if (!data.providers ||
356
+ !Array.isArray(data.providers) ||
357
+ data.providers.length === 0) {
358
+ throw new CIConfigError('config file (models-json) must contain a non-empty "providers" array');
323
359
  }
324
360
  return {
325
361
  authMode,
326
- providers: data.providers ?? [],
362
+ providers: data.providers,
327
363
  defaultProvider: data.defaultProvider,
328
364
  defaultModel: data.defaultModel,
329
365
  harness: data.harness,
@@ -373,7 +409,7 @@ export function exportCIConfig(config, secrets) {
373
409
  }
374
410
  return JSON.stringify(file, null, 2) + '\n';
375
411
  }
376
- // ─── composite setup action + OAuth-refresh script generation ────────────────
412
+ // ─── composite setup action generation ───────────────────────────────────────
377
413
  /** Indent every non-blank line of `text` by `spaces` spaces. */
378
414
  function indent(text, spaces) {
379
415
  const pad = ' '.repeat(spaces);
@@ -412,6 +448,48 @@ export const PI_HARNESS_PACKAGE = '@earendil-works/pi-coding-agent';
412
448
  * `node-version: '22'` satisfies (setup-node resolves the latest 22.x).
413
449
  */
414
450
  export const PI_HARNESS_VERSION = '0.80.6';
451
+ /**
452
+ * The `pi` packages the harness LOADS besides itself, each pinned to an exact
453
+ * version that is known to load with {@link PI_HARNESS_VERSION}.
454
+ *
455
+ * `pi-coding-agent` declares its sibling packages with a caret range
456
+ * (`@earendil-works/pi-ai: ^0.80.6`), which on a `0.x` version floats within
457
+ * the minor. `pi-ai@0.80.10` removed the `getOAuthApiKey` export that
458
+ * `pi-coding-agent@0.80.6` imports, so a resolver that honours the caret kills
459
+ * `pi` on start. npm happens to be protected by the `npm-shrinkwrap.json` the
460
+ * package publishes; pnpm ignores that file and was broken. Pinning the family
461
+ * explicitly (as npm `overrides`, see {@link piHarnessManifest}) makes the
462
+ * install reproducible whatever the resolver.
463
+ *
464
+ * The set is `pi-coding-agent`'s own `@earendil-works/*` dependencies plus
465
+ * theirs (`npm view @earendil-works/pi-coding-agent@<v> dependencies`,
466
+ * recursively): at 0.80.6 that is pi-ai, pi-agent-core (which depends on pi-ai)
467
+ * and pi-tui. Re-derive it on every {@link PI_HARNESS_VERSION} bump: a newer
468
+ * harness may add a package (0.87 adds `@earendil-works/chord`).
469
+ */
470
+ export const PI_HARNESS_PINNED_DEPENDENCIES = {
471
+ '@earendil-works/pi-agent-core': PI_HARNESS_VERSION,
472
+ '@earendil-works/pi-ai': PI_HARNESS_VERSION,
473
+ '@earendil-works/pi-tui': PI_HARNESS_VERSION,
474
+ };
475
+ /**
476
+ * The throwaway `package.json` the setup action installs the harness from: the
477
+ * harness at {@link PI_HARNESS_VERSION} as the only dependency, and every pi
478
+ * package it loads forced to its exact pin through npm `overrides`.
479
+ *
480
+ * A project-local install (not `npm install -g`) because `overrides` only apply
481
+ * to the ROOT project, and a global install makes each package its own root. The
482
+ * same file is used in `registry` and `workspace` mode: pnpm 11 also isolates
483
+ * every global package, so listing the pins next to the harness in
484
+ * `pnpm add -g` would not constrain the harness's own tree either.
485
+ */
486
+ export function piHarnessManifest() {
487
+ return {
488
+ private: true,
489
+ dependencies: { [PI_HARNESS_PACKAGE]: PI_HARNESS_VERSION },
490
+ overrides: { ...PI_HARNESS_PINNED_DEPENDENCIES },
491
+ };
492
+ }
415
493
  /**
416
494
  * The version of THIS dorfl package, read at runtime from its own
417
495
  * `package.json` (the single source changesets bumps on release). The composite
@@ -443,25 +521,47 @@ export function dorflPackageVersion() {
443
521
  }
444
522
  return version;
445
523
  }
524
+ /** Where the setup action installs the harness (under the job's temp dir). */
525
+ const PI_HARNESS_DIR = '$RUNNER_TEMP/dorfl-harness';
446
526
  /**
447
- * The install step for the configured harness (the `pi` CLI; `''` ⇒ none).
448
- * `registry` mode installs via `npm install -g`; `workspace` mode installs via
449
- * `pnpm add -g` so the harness lands on the pnpm global bin already on
450
- * `$GITHUB_PATH` (mirroring whitesmith's dev mode).
527
+ * The install + smoke steps for the configured harness (the `pi` CLI; `''` ⇒
528
+ * none). Identical in `registry` and `workspace` mode: the harness is installed
529
+ * with npm from {@link piHarnessManifest} into a job-local directory whose
530
+ * `node_modules/.bin` is added to `$GITHUB_PATH`, so every pi package it loads
531
+ * is pinned whatever package manager the repository uses. The smoke step then
532
+ * runs `pi --version`, which imports the harness's whole module graph, so a
533
+ * broken install fails setup with a clear message instead of failing inside a
534
+ * dorfl agent launch.
451
535
  */
452
- function harnessInstallStep(harness, installSource) {
536
+ function harnessInstallStep(harness) {
453
537
  if (harness === 'pi') {
454
- const spec = `${PI_HARNESS_PACKAGE}@${PI_HARNESS_VERSION}`;
455
- const run = installSource === 'workspace'
456
- ? `pnpm add -g ${spec}`
457
- : `npm install -g ${spec}`;
538
+ const manifest = JSON.stringify(piHarnessManifest(), null, 2);
458
539
  return `
459
540
  # Pinned to the exact harness version this dorfl release declares
460
- # (PI_HARNESS_VERSION): the job holds write access and a provider key, so the
461
- # version must not move on its own. Re-run \`dorfl install-ci\` to upgrade.
541
+ # (PI_HARNESS_VERSION), with every pi package it loads pinned too (npm
542
+ # \`overrides\`; a caret range on a 0.x version floats within the minor): the
543
+ # job holds a provider key, so no version may move on its own. Re-run
544
+ # \`dorfl install-ci\` to upgrade.
462
545
  - name: Install agent harness (pi)
463
546
  shell: bash
464
- run: ${run}`;
547
+ run: |
548
+ mkdir -p "${PI_HARNESS_DIR}"
549
+ cd "${PI_HARNESS_DIR}"
550
+ cat > package.json << 'HARNESS_EOF'
551
+ ${indent(manifest, 8)}
552
+ HARNESS_EOF
553
+ npm install --no-audit --no-fund
554
+ echo "${PI_HARNESS_DIR}/node_modules/.bin" >> "$GITHUB_PATH"
555
+
556
+ # \`pi --version\` loads the harness's module graph, so an install whose pi
557
+ # packages do not load together fails HERE, not inside a dorfl agent launch.
558
+ - name: Check the agent harness loads (pi)
559
+ shell: bash
560
+ run: |
561
+ if ! pi --version; then
562
+ 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."
563
+ exit 1
564
+ fi`;
465
565
  }
466
566
  return '';
467
567
  }
@@ -473,14 +573,11 @@ function harnessInstallStep(harness, installSource) {
473
573
  * `uses: ./.github/actions/dorfl-setup`. The advance-loop seed template
474
574
  * already references this exact action name + path (`docs/ci/README.md`).
475
575
  *
476
- * The auth step branches on the mode: `models-json` writes a generated
477
- * `~/.pi/agent/models.json` inline (the conservative default) AND exports each
576
+ * The auth step writes a generated `~/.pi/agent/models.json` inline AND exports each
478
577
  * configured provider's API key from a same-named ACTION INPUT to `$GITHUB_ENV`,
479
578
  * so the agent step (`pi`) can resolve the env var `models.json` references; the
480
579
  * workflow passes the secrets ONCE via `with:` on the `uses:` line (no
481
- * per-step / per-capability provider enumeration). `auth-json` writes
482
- * `~/.pi/agent/auth.json` from `$PI_AUTH_JSON` + runs the OAuth-refresh script
483
- * (the sharp edge). Deterministic: the same config produces byte-identical output.
580
+ * per-step / per-capability provider enumeration). Deterministic: the same config produces byte-identical output.
484
581
  */
485
582
  export function generateSetupAction(config,
486
583
  /**
@@ -492,43 +589,24 @@ export function generateSetupAction(config,
492
589
  * baseline (the load-bearing absent-⇒-identical contract).
493
590
  */
494
591
  projectSetupSteps) {
495
- let authStep;
496
- // models-json mode declares one ACTION INPUT per distinct provider key (named
497
- // identically to the secret / env var) and exports it to `$GITHUB_ENV`, so every
498
- // later step inherits it. auth-json mode has no provider keys (it uses auth.json).
499
- const providerKeyNames = config.authMode === 'models-json' ? requiredSecretNames(config) : [];
500
- if (config.authMode === 'auth-json') {
501
- authStep = `\
502
- - name: Configure agent auth (auth.json)
503
- shell: bash
504
- run: |
505
- if [ -z "$PI_AUTH_JSON" ]; then
506
- echo "ERROR: PI_AUTH_JSON secret is not set" >&2; exit 1
507
- fi
508
- mkdir -p ~/.pi/agent
509
- echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
510
- chmod 600 ~/.pi/agent/auth.json
511
-
512
- # Workaround for https://github.com/badlogic/pi-mono/issues/2743 — the SINGLE
513
- # CI→repo mutation in the design; needs GH_PAT to rotate PI_AUTH_JSON.
514
- - name: Refresh OAuth token
515
- shell: bash
516
- run: node .github/scripts/refresh-oauth-token.mjs`;
517
- }
518
- else {
519
- const modelsJsonStr = JSON.stringify(buildModelsJson(config.providers), null, 2);
520
- // Map each provider-key INPUT into the step env, then append the non-empty
521
- // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
522
- // (never interpolated into the script body), and Actions masks the secret in
523
- // logs. `models.json`'s `apiKey` is the env-var NAME; this puts the value there.
524
- const exportEnvLines = providerKeyNames
525
- .map((name) => ` ${name}: \${{ inputs.${name} }}`)
526
- .join('\n');
527
- const exportRunLines = providerKeyNames
528
- .map((name) => ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`)
529
- .join('\n');
530
- const exportStep = providerKeyNames.length > 0
531
- ? `\n
592
+ // models-json mode (the only mode) declares one ACTION INPUT per distinct
593
+ // provider key (named identically to the secret / env var) and exports it to
594
+ // `$GITHUB_ENV`, so every later step inherits it.
595
+ const providerKeyNames = providerSecretNames(config);
596
+ const modelsJsonStr = JSON.stringify(buildModelsJson(config.providers), null, 2);
597
+ // Map each provider-key INPUT into the step env, then append the non-empty
598
+ // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
599
+ // (never interpolated into the script body), and Actions masks the secret in
600
+ // logs. `models.json`'s `apiKey` is a `$NAME` env-var REFERENCE (pi resolves it at
601
+ // runtime; a bare name would be a literal key); this puts the value there.
602
+ const exportEnvLines = providerKeyNames
603
+ .map((name) => ` ${name}: \${{ inputs.${name} }}`)
604
+ .join('\n');
605
+ const exportRunLines = providerKeyNames
606
+ .map((name) => ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`)
607
+ .join('\n');
608
+ const exportStep = providerKeyNames.length > 0
609
+ ? `\n
532
610
  # Surface the configured provider API key(s) to the environment so the agent
533
611
  # step (\`pi\`) can resolve the env var \`models.json\` references. The workflow
534
612
  # passes the secret(s) via \`with:\` on the \`uses:\` line; we forward the
@@ -539,8 +617,8 @@ projectSetupSteps) {
539
617
  ${exportEnvLines}
540
618
  run: |
541
619
  ${exportRunLines}`
542
- : '';
543
- authStep = `\
620
+ : '';
621
+ const authStep = `\
544
622
  - name: Configure agent models (models.json)
545
623
  shell: bash
546
624
  run: |
@@ -548,15 +626,14 @@ ${exportRunLines}`
548
626
  cat > ~/.pi/agent/models.json << 'MODELS_EOF'
549
627
  ${indent(modelsJsonStr, 8)}
550
628
  MODELS_EOF${exportStep}`;
551
- }
552
- const installHarness = harnessInstallStep(config.harness, config.installSource);
629
+ const installHarness = harnessInstallStep(config.harness);
553
630
  // The CLI-install block branches on installSource. `registry` installs the
554
631
  // published CLI via `npm install -g dorfl` (the default for every
555
632
  // consumer repo). `workspace` builds the CLI from the checked-out source and
556
633
  // links it onto PATH — for the self-hosting dorfl monorepo, which is
557
634
  // not published under that npm name. We add pnpm's global bin to $GITHUB_PATH
558
- // so the linked `dorfl` (and the pnpm-installed harness) are on PATH in
559
- // all subsequent steps; we always rebuild because source changes per commit.
635
+ // so the linked `dorfl` is on PATH in all subsequent steps (the harness adds
636
+ // its own bin dir, the same way in both modes); we always rebuild because source changes per commit.
560
637
  let installSteps;
561
638
  if (config.installSource === 'workspace') {
562
639
  installSteps = `\
@@ -610,10 +687,9 @@ ${indent(modelsJsonStr, 8)}
610
687
  // would exec the local bin which then forwards AGAIN — a confusing
611
688
  // double-resolution. The install step below leaves `dorfl` on `$PATH` as the
612
689
  // bootstrap; the forward does the pinning.
613
- // One optional ACTION INPUT per provider key (models-json mode), named
614
- // identically to the secret/env var. Optional + default '' so a workflow that
615
- // does not pass it (or auth-json mode, which declares none) is valid; the export
616
- // step skips an empty value. auth-json mode declares no inputs.
690
+ // One optional ACTION INPUT per provider key, named identically to the
691
+ // secret/env var. Optional + default '' so a workflow that does not pass it is
692
+ // valid; the export step skips an empty value.
617
693
  const inputsBlock = providerKeyNames.length > 0
618
694
  ? `inputs:
619
695
  ${providerKeyNames
@@ -672,109 +748,228 @@ ${installSteps}
672
748
  ${authStep}
673
749
  `;
674
750
  }
751
+ /** The local path a workflow step `uses:` for the agent-role setup action. */
752
+ export const SETUP_ACTION_USES = './.github/actions/dorfl-setup';
753
+ /** The local path a workflow step `uses:` for the writer-role setup action. */
754
+ export const WRITER_SETUP_ACTION_USES = './.github/actions/dorfl-setup-writer';
675
755
  /**
676
- * The OAuth-refresh script emitted ONLY in `auth-json` mode (whitesmith's
677
- * `pi-mono#2743` workaround): refreshes the Anthropic OAuth token in
678
- * `~/.pi/agent/auth.json` before the agent runs, then writes the rotated token
679
- * BACK via `gh secret set PI_AUTH_JSON` (the single CI→repo mutation, needing a
680
- * `GH_PAT`). This is the documented SHARP EDGE; `models-json` mode avoids it.
681
- */
682
- export const REFRESH_OAUTH_SCRIPT = `\
683
- #!/usr/bin/env node
684
- /**
685
- * Refresh OAuth tokens in the agent's auth.json before it runs.
756
+ * Generate the WRITER-ROLE composite setup action (`dorfl-setup-writer`, spec
757
+ * `ci-agent-job-without-write-token` §9, ADR
758
+ * `ci-agent-job-holds-no-write-token`; task `ci-split-generate-workflows`).
759
+ *
760
+ * The jobs that hold a write token and run no agent (the split item
761
+ * workflow's lock and apply jobs, `reap-merged-branches`, `close-job`, and the
762
+ * read-only `enumerate`) install Node and dorfl ONLY: no harness, no provider
763
+ * key, no `models.json`, no project-setup hook, no install of the project's
764
+ * dependencies, and no Actions cache. In `registry` mode the dorfl install runs
765
+ * from `$RUNNER_TEMP`, outside the checkout, with `--ignore-scripts`, so a
766
+ * repository `.npmrc` cannot redirect the registry and no install script runs.
686
767
  *
687
- * Workaround for https://github.com/badlogic/pi-mono/issues/2743.
688
- * After refreshing, updates the PI_AUTH_JSON GitHub secret so the next run has
689
- * the latest rotated refresh token (requires GH_PAT with repo scope). This is the
690
- * known SHARP EDGE of auth-json mode; models-json mode avoids it entirely.
768
+ * In `workspace` mode (dorfl's own repository) dorfl is built from source
769
+ * (decision 8): the tree of `source-ref` (default the checkout's `HEAD`) is
770
+ * exported with `git archive` into `$RUNNER_TEMP` and built there with
771
+ * `--ignore-scripts`. In merge mode that is code an earlier agent landed on
772
+ * `main`, which is the trust merge mode already implies.
691
773
  *
692
- * Remove this script once the upstream fix is released.
774
+ * In both modes it then exports `DORFL_NO_FORWARD=1` for the rest of the job
775
+ * ({@link writerNoForwardStep}): the writer role never forwards to the
776
+ * repository's `dorflCmd`, which usually points into dependencies this role
777
+ * never installs. In `registry` mode it also warns when the repository pins a
778
+ * different dorfl version ({@link WRITER_PIN_CHECK_SCRIPT}).
779
+ *
780
+ * It is a SIBLING action rather than a `role` input on `dorfl-setup`: the
781
+ * project-setup hook is an opaque fragment spliced first into `dorfl-setup`
782
+ * (ADR `install-ci-project-provisioning-native-passthrough`), and a composite
783
+ * action cannot skip an opaque block of steps by input. `dorfl-setup` stays the
784
+ * agent role, byte for byte.
693
785
  */
694
- import { readFileSync, writeFileSync, chmodSync } from "fs";
695
- import { join } from "path";
696
- import { execSync } from "child_process";
697
-
698
- const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
699
- const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
786
+ export function generateWriterSetupAction(config) {
787
+ let installSteps;
788
+ let inputsBlock = '';
789
+ // Registry mode only: warn when the repository's `dorflCmd` names an exact
790
+ // `dorfl@<version>` that is not the version pinned here. Workspace mode builds
791
+ // dorfl from the checked-out source, so there is no second version to compare.
792
+ let pinCheckStep = '';
793
+ if (config.installSource === 'workspace') {
794
+ inputsBlock = `inputs:
795
+ source-ref:
796
+ description: 'The commit to build dorfl from (workspace mode). Blank: the checkout HEAD.'
797
+ required: false
798
+ default: ''
700
799
 
701
- const authPath = join(process.env.HOME, ".pi", "agent", "auth.json");
702
- const auth = JSON.parse(readFileSync(authPath, "utf-8"));
703
- const cred = auth.anthropic;
800
+ `;
801
+ installSteps = `\
802
+ - name: Setup pnpm
803
+ uses: ${pinnedUses(ACTION_PINS.pnpmSetup)}
704
804
 
705
- if (!cred || cred.type !== "oauth") {
706
- console.log("No OAuth credentials for anthropic, skipping refresh");
707
- process.exit(0);
708
- }
805
+ - name: Add pnpm global bin to PATH
806
+ shell: bash
807
+ run: |
808
+ pnpm setup
809
+ echo "$HOME/.local/share/pnpm" >> "$GITHUB_PATH"
709
810
 
710
- if (Date.now() < cred.expires) {
711
- console.log("Token still valid until", new Date(cred.expires).toISOString());
712
- process.exit(0);
713
- }
811
+ # Workspace mode (dorfl's own repository, decision 8 of ADR
812
+ # ci-agent-job-holds-no-write-token): build dorfl from the tree of
813
+ # \`source-ref\`, exported into $RUNNER_TEMP (outside the checkout), with
814
+ # \`--ignore-scripts\`. In merge mode this is code an earlier agent landed on
815
+ # main running in a write job, the trust merge mode already implies.
816
+ # Only dorfl and its workspace dependencies (\`--filter 'dorfl...'\`) are
817
+ # installed and built: the other workspace packages (the website) are not
818
+ # needed to run dorfl, so they do not run next to the write token.
819
+ - name: Build dorfl from the base commit (outside the checkout)
820
+ shell: bash
821
+ env:
822
+ SOURCE_REF: \${{ inputs.source-ref }}
823
+ run: |
824
+ set -euo pipefail
825
+ src="$RUNNER_TEMP/dorfl-src"
826
+ rm -rf "$src"
827
+ mkdir -p "$src"
828
+ git archive "$(git rev-parse --verify "\${SOURCE_REF:-HEAD}^{commit}")" | tar -x -C "$src"
829
+ cd "$src"
830
+ pnpm install --frozen-lockfile --ignore-scripts --filter 'dorfl...'
831
+ pnpm --filter 'dorfl...' build
832
+ cd packages/dorfl && pnpm link --global`;
833
+ }
834
+ else {
835
+ installSteps = `\
836
+ # Pinned to the dorfl version that generated this action. Installed from
837
+ # $RUNNER_TEMP (outside the checkout, so a repository .npmrc cannot redirect
838
+ # the registry) with --ignore-scripts (no install script runs next to the
839
+ # write token).
840
+ - name: Install dorfl (outside the checkout, no install scripts)
841
+ shell: bash
842
+ run: |
843
+ cd "$RUNNER_TEMP"
844
+ npm install -g --ignore-scripts dorfl@${dorflPackageVersion()}`;
845
+ pinCheckStep = writerPinCheckStep(dorflPackageVersion());
846
+ }
847
+ return `\
848
+ name: Setup dorfl (writer role)
849
+ description: Install Node.js and dorfl only, for the CI jobs that run no agent (lock, apply, reap, close, enumerate)
714
850
 
715
- console.log(
716
- "Token expired at",
717
- new Date(cred.expires).toISOString(),
718
- "- refreshing..."
719
- );
851
+ ${inputsBlock}# THE WRITER ROLE (ADR ci-agent-job-holds-no-write-token): the jobs that use
852
+ # this action hold a write token (or run next to one) and run NO agent and NO
853
+ # repository code. So it installs Node.js and dorfl ONLY: no agent harness, no
854
+ # provider API key, no models.json, no project-setup hook, no install of the
855
+ # project's dependencies, and no Actions cache (an agent can write
856
+ # default-branch cache entries, so a restore here would run its bytes with
857
+ # write access). The agent role is ./.github/actions/dorfl-setup.
858
+ runs:
859
+ using: composite
860
+ steps:
861
+ - name: Setup Node.js
862
+ uses: ${pinnedUses(ACTION_PINS.setupNode)}
863
+ with:
864
+ node-version: '22'
865
+ # No cache in the writer role (see above).
866
+ package-manager-cache: false
720
867
 
721
- const response = await fetch(ANTHROPIC_TOKEN_URL, {
722
- method: "POST",
723
- headers: {
724
- "Content-Type": "application/x-www-form-urlencoded",
725
- Accept: "application/json",
726
- },
727
- body: new URLSearchParams({
728
- grant_type: "refresh_token",
729
- client_id: ANTHROPIC_CLIENT_ID,
730
- refresh_token: cred.refresh,
731
- }).toString(),
732
- signal: AbortSignal.timeout(30_000),
733
- });
868
+ - name: Configure git identity
869
+ shell: bash
870
+ run: |
871
+ git config user.name "dorfl[bot]"
872
+ git config user.email "dorfl[bot]@users.noreply.github.com"
734
873
 
735
- const data = await response.json();
874
+ ${installSteps}
736
875
 
737
- if (!response.ok) {
738
- console.error("Refresh failed:", response.status, JSON.stringify(data));
739
- process.exit(1);
876
+ ${writerNoForwardStep()}${pinCheckStep}
877
+ `;
740
878
  }
741
-
742
- auth.anthropic = {
743
- type: "oauth",
744
- refresh: data.refresh_token,
745
- access: data.access_token,
746
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
879
+ /**
880
+ * The writer-role step that turns OFF the bootstrap self-forward for every later
881
+ * step of the job, by exporting {@link NO_FORWARD_ENV}`=1` to `$GITHUB_ENV`.
882
+ *
883
+ * Why: a repository that pins dorfl through its dependencies declares
884
+ * `"dorflCmd": "node_modules/.bin/dorfl"`. The writer role deliberately installs
885
+ * no project dependency, so that path does not exist, and a forwarding dorfl
886
+ * would fail every writer job (0.15.0 regression). The dorfl installed above is
887
+ * already the exact version `install-ci` generated these workflows with (or,
888
+ * in workspace mode, dorfl built from the checked-out source), so forwarding
889
+ * gains nothing here. Set ONCE in the action so no job template can forget it.
890
+ * The agent-role `dorfl-setup` does install dependencies and keeps forwarding.
891
+ */
892
+ function writerNoForwardStep() {
893
+ return `\
894
+ # The writer role runs the dorfl installed above and never forwards to the
895
+ # repository's dorflCmd: that command usually points into the project's
896
+ # dependencies (node_modules/.bin/dorfl), which this role never installs.
897
+ # Exported to $GITHUB_ENV so it holds for every later step of the job.
898
+ - name: Run the dorfl installed above, not the repository's dorflCmd
899
+ shell: bash
900
+ run: echo "${NO_FORWARD_ENV}=1" >> "$GITHUB_ENV"`;
901
+ }
902
+ /**
903
+ * The JavaScript the registry-mode writer step runs with `node -e '<script>'
904
+ * <pinned>` to WARN (never fail) when the repository pins a different dorfl
905
+ * than the one the writer role installed. Exported so a test can run it.
906
+ *
907
+ * It reads JSON only (`dorfl.json`, and `package.json` for the devDependency
908
+ * case) and runs no project code, so it is safe next to a write token. It
909
+ * recognises two version-bearing shapes of `dorflCmd`:
910
+ *
911
+ * - an exact `dorfl@<version>` inside the command (`npx dorfl@0.16.0`,
912
+ * `mise exec dorfl@0.16.0 --`);
913
+ * - `node_modules/.bin/dorfl`, when `package.json` declares `dorfl` in
914
+ * `devDependencies` (or `dependencies`) at an EXACT version.
915
+ *
916
+ * Anything else (a caret range, `./bin/dorfl`, no `dorflCmd`) is skipped
917
+ * silently: resolving it would need the project's dependencies installed,
918
+ * which the writer role never does. No single quote may appear in it (it is
919
+ * wrapped in a single-quoted bash string).
920
+ */
921
+ export const WRITER_PIN_CHECK_SCRIPT = `\
922
+ const fs = require("fs");
923
+ const pinned = process.argv[1];
924
+ const readJson = (p) => {
925
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) { return undefined; }
747
926
  };
748
-
749
- writeFileSync(authPath, JSON.stringify(auth, null, 2));
750
- chmodSync(authPath, 0o600);
751
- console.log(
752
- "Token refreshed, new expiry:",
753
- new Date(auth.anthropic.expires).toISOString()
754
- );
755
-
756
- // Update the GitHub secret so the next run has the latest refresh token.
757
- const repo = process.env.GITHUB_REPOSITORY;
758
- const token = process.env.GH_PAT;
759
- if (repo && token) {
760
- try {
761
- execSync(\`gh secret set PI_AUTH_JSON --repo "\${repo}"\`, {
762
- input: JSON.stringify(auth),
763
- env: { ...process.env, GH_TOKEN: token },
764
- stdio: ["pipe", "pipe", "pipe"],
765
- });
766
- console.log("PI_AUTH_JSON secret updated");
767
- } catch (err) {
768
- console.warn("Failed to update secret (non-fatal):", err.stderr?.toString() || err.message);
927
+ const semver = "\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:-[0-9A-Za-z.-]+)?";
928
+ const config = readJson("${REPO_CONFIG_FILENAME}") || readJson("${REPO_CONFIG_FILENAME_LEGACY}") || {};
929
+ const cmd = config.dorflCmd;
930
+ if (typeof cmd !== "string") process.exit(0);
931
+ let found;
932
+ let where;
933
+ const inCmd = new RegExp("(?:^|\\\\s)dorfl@(" + semver + ")(?=\\\\s|$)").exec(cmd);
934
+ if (inCmd) {
935
+ found = inCmd[1];
936
+ where = "${REPO_CONFIG_FILENAME} dorflCmd";
937
+ } else if (/(?:^|[\\s/])node_modules\\/\\.bin\\/dorfl(?=\\s|$)/.test(cmd)) {
938
+ const pkg = readJson("package.json") || {};
939
+ const spec = (pkg.devDependencies || {}).dorfl || (pkg.dependencies || {}).dorfl;
940
+ if (typeof spec === "string" && new RegExp("^" + semver + "$").test(spec)) {
941
+ found = spec;
942
+ where = "package.json (the dorflCmd " + cmd + ")";
769
943
  }
770
- } else {
771
- console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
772
944
  }
773
- `;
945
+ if (found !== undefined && found !== pinned) {
946
+ const esc = (s) => s.replace(/%/g, "%25").replace(/\\r/g, "%0D").replace(/\\n/g, "%0A");
947
+ console.log("::warning title=dorfl-setup-writer::" + esc(where + " pins dorfl@" + found + " but the writer-role jobs run dorfl@" + pinned + ", the version install-ci generated these workflows with (writer jobs do not forward to dorflCmd). Re-run dorfl install-ci with dorfl " + found + " so every job runs the same version."));
948
+ }`;
949
+ /**
950
+ * The registry-mode writer step that runs {@link WRITER_PIN_CHECK_SCRIPT}. With
951
+ * the forward off (see {@link writerNoForwardStep}) a disagreement between the
952
+ * pinned global and the repository's own pin would otherwise go unnoticed: the
953
+ * agent job forwards to `dorflCmd` while the writer jobs run the pin.
954
+ */
955
+ function writerPinCheckStep(pinned) {
956
+ return `
957
+
958
+ # Warn (never fail) when the repository pins a dorfl version other than the
959
+ # one installed above: the agent job forwards to dorflCmd, these writer jobs
960
+ # do not, so the two roles would run different versions. Reads dorfl.json
961
+ # and package.json as JSON only; runs no project code.
962
+ - name: Warn if the repository pins another dorfl version
963
+ shell: bash
964
+ run: |
965
+ node -e '
966
+ ${indent(WRITER_PIN_CHECK_SCRIPT, 10)}
967
+ ' "${pinned}"`;
968
+ }
774
969
  // ─── the --fake snapshot mechanism + artifact assembly ───────────────────────
775
970
  /**
776
- * The setup ARTIFACTS this task generates: the composite setup action + (in
777
- * `auth-json` mode) the OAuth-refresh script, plus any selected capabilities'
971
+ * The setup ARTIFACTS this task generates: the composite setup actions, plus
972
+ * any selected capabilities'
778
973
  * workflow files. This task emits NO capability workflow itself (those are the
779
974
  * sibling tasks); `capabilities` defaults to none.
780
975
  */
@@ -791,13 +986,11 @@ options) {
791
986
  path: join('actions', 'dorfl-setup', 'action.yml'),
792
987
  content: generateSetupAction(config, options?.projectSetupSteps),
793
988
  },
989
+ {
990
+ path: join('actions', 'dorfl-setup-writer', 'action.yml'),
991
+ content: generateWriterSetupAction(config),
992
+ },
794
993
  ];
795
- if (config.authMode === 'auth-json') {
796
- files.push({
797
- path: join('scripts', 'refresh-oauth-token.mjs'),
798
- content: REFRESH_OAUTH_SCRIPT,
799
- });
800
- }
801
994
  for (const cap of capabilities) {
802
995
  files.push(...cap.emit(config));
803
996
  }