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
@@ -35,6 +34,11 @@ import {
35
34
  import {dirname, join, relative} from 'node:path';
36
35
  import {fileURLToPath} from 'node:url';
37
36
  import type {HarnessAdapter} from './config.js';
37
+ import {NO_FORWARD_ENV} from './bootstrap-forward.js';
38
+ import {
39
+ REPO_CONFIG_FILENAME,
40
+ REPO_CONFIG_FILENAME_LEGACY,
41
+ } from './repo-config.js';
38
42
  import {
39
43
  ACTION_PINS,
40
44
  pinnedUses,
@@ -44,16 +48,31 @@ import {
44
48
  // ─── Config model ──────────────────────────────────────────────────────────
45
49
 
46
50
  /**
47
- * The two auth modes, mirroring whitesmith:
48
- * - `models-json` (DEFAULT): one GitHub secret per provider API key; the harness
49
- * `models.json` is generated inline and references the env var names. No
50
- * OAuth-refresh machinery, no `GH_PAT` — the conservative default.
51
- * - `auth-json`: a single `PI_AUTH_JSON` secret + a `GH_PAT` for OAuth-token
52
- * refresh + an OAuth-refresh script (the `pi-mono#2743` workaround, the SINGLE
53
- * CI→repo mutation in the whole design). The known SHARP EDGE; default AWAY
54
- * from it.
51
+ * The auth mode. `models-json` is the ONLY mode: one GitHub secret per provider
52
+ * API key; the harness `models.json` is generated inline and references the env
53
+ * vars (`$NAME`). Credential rotation (e.g. OAuth) is NOT done in CI: point a
54
+ * provider's `baseUrl` at a proxy that rotates credentials outside GitHub.
55
+ *
56
+ * The former `auth-json` mode (a `PI_AUTH_JSON` secret + an OAuth-refresh
57
+ * script that wrote the rotated token back with a write-capable `GH_PAT`) was
58
+ * REMOVED: its refresh needed a write token in the agent job, which ADR
59
+ * `ci-agent-job-holds-no-write-token` forbids, and without the refresh the
60
+ * OAuth token expires within hours. A config still naming it is refused by
61
+ * {@link loadCIConfigFile} with {@link AUTH_JSON_REMOVED_MESSAGE}. The type
62
+ * (and the `authMode` config key) is kept so existing `models-json` configs
63
+ * still parse and export unchanged.
64
+ */
65
+ export type AuthMode = 'models-json';
66
+
67
+ /**
68
+ * The refusal a config naming the removed `auth-json` mode gets: it says the
69
+ * mode is gone and names the replacement (`models-json` + a proxy `baseUrl`).
55
70
  */
56
- export type AuthMode = 'auth-json' | 'models-json';
71
+ export const AUTH_JSON_REMOVED_MESSAGE =
72
+ 'config file "authMode": "auth-json" is no longer supported (the mode was ' +
73
+ 'removed: its OAuth refresh needed a write-capable GH_PAT in the agent job). ' +
74
+ 'Use "authMode": "models-json" and point a provider\'s "baseUrl" at a proxy ' +
75
+ 'that handles credential rotation outside GitHub.';
57
76
 
58
77
  /**
59
78
  * Where the composite setup action gets the `dorfl` CLI from:
@@ -95,7 +114,7 @@ export interface ProviderEntry {
95
114
  export interface CIConfigFile {
96
115
  /** The auth mode (`models-json` default). */
97
116
  authMode: AuthMode;
98
- /** The configured providers (empty for `auth-json` mode). */
117
+ /** The configured providers (non-empty). */
99
118
  providers: ProviderEntry[];
100
119
  /** The default provider name. */
101
120
  defaultProvider: string;
@@ -208,6 +227,20 @@ export const DEFAULT_INSTALL_SOURCE: InstallSource = 'registry';
208
227
 
209
228
  // ─── The CI-provider SEAM (provider-agnostic; GitHub is the first adapter) ───
210
229
 
230
+ /**
231
+ * A repository's GitHub Actions workflow permissions, as
232
+ * `GET /repos/{owner}/{repo}/actions/permissions/workflow` answers them (the
233
+ * same two fields the PUT accepts). `defaultWorkflowPermissions` is `read` or
234
+ * `write` on GitHub; typed as a string so an unexpected value is carried
235
+ * through untouched rather than rewritten.
236
+ */
237
+ export interface ActionsWorkflowPermissions {
238
+ /** The GITHUB_TOKEN's default scope (`read` / `write`); preserved on set. */
239
+ defaultWorkflowPermissions: string;
240
+ /** "Allow GitHub Actions to create and approve pull requests". */
241
+ canApprovePullRequestReviews: boolean;
242
+ }
243
+
211
244
  /**
212
245
  * The thin CI-provider seam (whitesmith's proven `GitHubCIContext` shape,
213
246
  * generalised so a second provider could slot in without touching the core). A
@@ -288,6 +321,25 @@ export interface CIProviderContext {
288
321
  * GitHub provider. Throws on failure (caller catches + logs).
289
322
  */
290
323
  setBranchRuleset?(ruleset: unknown): Promise<void>;
324
+ /**
325
+ * OPTIONALLY read the repo's Actions workflow permissions (GitHub:
326
+ * `GET /repos/{owner}/{repo}/actions/permissions/workflow`), whose
327
+ * `can_approve_pull_request_reviews` is the "Allow GitHub Actions to create
328
+ * and approve pull requests" setting propose mode needs. Returns `undefined`
329
+ * when the read fails. Absent on a provider with no such setting.
330
+ */
331
+ getActionsWorkflowPermissions?(): Promise<
332
+ ActionsWorkflowPermissions | undefined
333
+ >;
334
+ /**
335
+ * OPTIONALLY set the repo's Actions workflow permissions (GitHub:
336
+ * `PUT /repos/{owner}/{repo}/actions/permissions/workflow`). A field left
337
+ * `undefined` is not sent, so GitHub keeps its current value. Throws on
338
+ * failure (caller catches + logs). Absent on a provider with no such setting.
339
+ */
340
+ setActionsWorkflowPermissions?(
341
+ permissions: Partial<ActionsWorkflowPermissions>,
342
+ ): Promise<void>;
291
343
  /**
292
344
  * OPTIONALLY render the user's project-setup payload (the value of
293
345
  * {@link CIConfigFile.projectSetup}[providerId]) to the NATIVE-SYNTAX
@@ -416,9 +468,12 @@ function pathToFileUrl(absPath: string): string {
416
468
 
417
469
  /**
418
470
  * Build the harness `models.json` object from the providers. Built-in providers
419
- * (`anthropic`/`openai`) get `{apiKey: <ENV_VAR>}` (the harness resolves the env
420
- * var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
471
+ * (`anthropic`/`openai`) get `{apiKey: "$<ENV_VAR>"}` (the harness resolves the
472
+ * env var at runtime); a custom provider carries its `baseUrl`/`api`/`models`/`compat`.
421
473
  * Mirrors whitesmith's `buildModelsJson`.
474
+ *
475
+ * The `apiKey` is an env-var REFERENCE (see {@link modelsJsonEnvRef}), never the
476
+ * bare env-var name: pi treats a bare value as a LITERAL key.
422
477
  */
423
478
  export function buildModelsJson(providers: ProviderEntry[]): {
424
479
  providers: Record<string, unknown>;
@@ -426,14 +481,16 @@ export function buildModelsJson(providers: ProviderEntry[]): {
426
481
  const providersObj: Record<string, unknown> = {};
427
482
  for (const p of providers) {
428
483
  if (p.builtin) {
429
- const entry: Record<string, unknown> = {apiKey: p.apiKeyEnvVar};
484
+ const entry: Record<string, unknown> = {
485
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
486
+ };
430
487
  if (p.baseUrl) entry.baseUrl = p.baseUrl;
431
488
  providersObj[p.name] = entry;
432
489
  } else {
433
490
  const entry: Record<string, unknown> = {
434
491
  baseUrl: p.baseUrl,
435
492
  api: p.api,
436
- apiKey: p.apiKeyEnvVar,
493
+ apiKey: modelsJsonEnvRef(p.apiKeyEnvVar),
437
494
  models: p.models,
438
495
  };
439
496
  if (p.compat) entry.compat = p.compat;
@@ -443,19 +500,27 @@ export function buildModelsJson(providers: ProviderEntry[]): {
443
500
  return {providers: providersObj};
444
501
  }
445
502
 
503
+ /**
504
+ * The `models.json` config value that makes the pi harness read `envVarName`
505
+ * from the environment: `"$NAME"`. The pinned harness
506
+ * (`@earendil-works/pi-coding-agent`, `dist/core/resolve-config-value.js`)
507
+ * resolves a config value as a shell command when it starts with `!`, as an env
508
+ * var when it is `$NAME` / `${NAME}`, and as a LITERAL otherwise. Writing the bare
509
+ * name therefore sent the string `ANTHROPIC_API_KEY` as the token (`401
510
+ * Unauthorized` on every CI launch).
511
+ */
512
+ export function modelsJsonEnvRef(envVarName: string): string {
513
+ return `$${envVarName}`;
514
+ }
515
+
446
516
  // ─── secret-orchestration LOGIC (which secrets, dedup) ───────────────────────
447
517
 
448
518
  /**
449
519
  * The DEDUPLICATED set of secret env-var names the config requires, in first-seen
450
- * order. For `models-json` mode it is one per distinct provider `apiKeyEnvVar`;
451
- * for `auth-json` mode it is the fixed `PI_AUTH_JSON` + `GH_PAT` pair (the sharp
452
- * edge). This is the pure secret-orchestration LOGIC the adapter's setter
520
+ * order: one per distinct provider `apiKeyEnvVar`. This is the pure secret-orchestration LOGIC the adapter's setter
453
521
  * consumes — which secrets, deduped — separated from any provider I/O.
454
522
  */
455
523
  export function requiredSecretNames(config: ResolvedCIConfig): string[] {
456
- if (config.authMode === 'auth-json') {
457
- return ['PI_AUTH_JSON', 'GH_PAT'];
458
- }
459
524
  const seen = new Set<string>();
460
525
  const names: string[] = [];
461
526
  for (const p of config.providers) {
@@ -468,13 +533,13 @@ export function requiredSecretNames(config: ResolvedCIConfig): string[] {
468
533
 
469
534
  /**
470
535
  * The GitHub secret the propose/merge CI legs read as `GH_TOKEN` to open PRs
471
- * under a CHOSEN identity (a PAT / App token). It is the PR-IDENTITY token, a
472
- * distinct purpose from `auth-json` mode's `GH_PAT` (which exists only to rotate
473
- * the OAuth `PI_AUTH_JSON` secret). The generated workflows reference it as
536
+ * under a CHOSEN identity (a PAT / App token). The generated workflows reference it as
474
537
  * `${{ secrets.DORFL_GH_TOKEN || secrets.GITHUB_TOKEN }}` so it is
475
538
  * OPTIONAL: unset ⇒ the legs fall back to the built-in `GITHUB_TOKEN` (PRs are
476
- * `github-actions[bot]` and do not trigger downstream `on: pull_request`
477
- * workflows); set ⇒ PRs carry the token's identity and DO trigger them.
539
+ * `github-actions[bot]`, and GitHub holds their `on: pull_request` runs in
540
+ * `action_required` until someone approves them); set ⇒ PRs carry the token's
541
+ * identity and trigger those workflows on their own. `intake.yml` never passes
542
+ * it, so intake PRs always need the approval (`docs/ci/README.md`).
478
543
  */
479
544
  export const PR_IDENTITY_SECRET_NAME = 'DORFL_GH_TOKEN';
480
545
 
@@ -501,14 +566,13 @@ export function optionalSecretNames(config: ResolvedCIConfig): string[] {
501
566
  * entry). Each entry maps the action input (named identically to the secret) to
502
567
  * `${{ secrets.<NAME> }}`.
503
568
  *
504
- * Empty string when there are no provider keys (auth-json mode, or no providers),
569
+ * Empty string when there are no provider keys (no providers),
505
570
  * so the `uses:` line is emitted bare and unchanged. This is the SINGLE place the
506
571
  * provider→`with:` shape lives, shared by every capability workflow generator, so
507
572
  * no template re-derives it.
508
573
  */
509
574
  export function providerSecretsWithBlock(config: ResolvedCIConfig): string {
510
- const names =
511
- config.authMode === 'models-json' ? requiredSecretNames(config) : [];
575
+ const names = providerSecretNames(config);
512
576
  if (names.length === 0) {
513
577
  return '';
514
578
  }
@@ -518,6 +582,17 @@ export function providerSecretsWithBlock(config: ResolvedCIConfig): string {
518
582
  return `\n with:\n${entries}`;
519
583
  }
520
584
 
585
+ /**
586
+ * The provider API key secrets the AGENT-running jobs forward to the setup
587
+ * action: one per distinct provider key (the same set as
588
+ * {@link requiredSecretNames}). The split item workflow declares exactly these (plus
589
+ * {@link PR_IDENTITY_SECRET_NAME}) as its `workflow_call` secrets, and hands
590
+ * them to its agent job only.
591
+ */
592
+ export function providerSecretNames(config: ResolvedCIConfig): string[] {
593
+ return requiredSecretNames(config);
594
+ }
595
+
521
596
  /** The outcome of orchestrating one secret through the provider seam. */
522
597
  export interface SecretSetResult {
523
598
  /** The secret env-var name. */
@@ -615,12 +690,17 @@ export function loadCIConfigFile(filePath: string): CIConfigFile {
615
690
  (err instanceof Error ? err.message : String(err)),
616
691
  );
617
692
  }
618
- const authMode = data.authMode ?? 'models-json';
619
- if (authMode !== 'models-json' && authMode !== 'auth-json') {
620
- throw new CIConfigError(
621
- `config file "authMode" must be "models-json" or "auth-json"`,
622
- );
693
+ // `authMode` is kept as a key (so existing configs still parse) but the only
694
+ // accepted value is `models-json`. The removed `auth-json` mode gets a refusal
695
+ // that names its replacement rather than the generic "must be" message.
696
+ const rawAuthMode: unknown = (data as {authMode?: unknown}).authMode;
697
+ if (rawAuthMode === 'auth-json') {
698
+ throw new CIConfigError(AUTH_JSON_REMOVED_MESSAGE);
699
+ }
700
+ if (rawAuthMode !== undefined && rawAuthMode !== 'models-json') {
701
+ throw new CIConfigError(`config file "authMode" must be "models-json"`);
623
702
  }
703
+ const authMode: AuthMode = 'models-json';
624
704
  if (!data.defaultProvider) {
625
705
  throw new CIConfigError('config file must contain "defaultProvider"');
626
706
  }
@@ -649,20 +729,18 @@ export function loadCIConfigFile(filePath: string): CIConfigFile {
649
729
  );
650
730
  }
651
731
  }
652
- if (authMode === 'models-json') {
653
- if (
654
- !data.providers ||
655
- !Array.isArray(data.providers) ||
656
- data.providers.length === 0
657
- ) {
658
- throw new CIConfigError(
659
- 'config file (models-json) must contain a non-empty "providers" array',
660
- );
661
- }
732
+ if (
733
+ !data.providers ||
734
+ !Array.isArray(data.providers) ||
735
+ data.providers.length === 0
736
+ ) {
737
+ throw new CIConfigError(
738
+ 'config file (models-json) must contain a non-empty "providers" array',
739
+ );
662
740
  }
663
741
  return {
664
742
  authMode,
665
- providers: data.providers ?? [],
743
+ providers: data.providers,
666
744
  defaultProvider: data.defaultProvider,
667
745
  defaultModel: data.defaultModel,
668
746
  harness: data.harness,
@@ -718,7 +796,7 @@ export function exportCIConfig(
718
796
  return JSON.stringify(file, null, 2) + '\n';
719
797
  }
720
798
 
721
- // ─── composite setup action + OAuth-refresh script generation ────────────────
799
+ // ─── composite setup action generation ───────────────────────────────────────
722
800
 
723
801
  /** Indent every non-blank line of `text` by `spaces` spaces. */
724
802
  function indent(text: string, spaces: number): string {
@@ -761,6 +839,55 @@ export const PI_HARNESS_PACKAGE = '@earendil-works/pi-coding-agent';
761
839
  */
762
840
  export const PI_HARNESS_VERSION = '0.80.6';
763
841
 
842
+ /**
843
+ * The `pi` packages the harness LOADS besides itself, each pinned to an exact
844
+ * version that is known to load with {@link PI_HARNESS_VERSION}.
845
+ *
846
+ * `pi-coding-agent` declares its sibling packages with a caret range
847
+ * (`@earendil-works/pi-ai: ^0.80.6`), which on a `0.x` version floats within
848
+ * the minor. `pi-ai@0.80.10` removed the `getOAuthApiKey` export that
849
+ * `pi-coding-agent@0.80.6` imports, so a resolver that honours the caret kills
850
+ * `pi` on start. npm happens to be protected by the `npm-shrinkwrap.json` the
851
+ * package publishes; pnpm ignores that file and was broken. Pinning the family
852
+ * explicitly (as npm `overrides`, see {@link piHarnessManifest}) makes the
853
+ * install reproducible whatever the resolver.
854
+ *
855
+ * The set is `pi-coding-agent`'s own `@earendil-works/*` dependencies plus
856
+ * theirs (`npm view @earendil-works/pi-coding-agent@<v> dependencies`,
857
+ * recursively): at 0.80.6 that is pi-ai, pi-agent-core (which depends on pi-ai)
858
+ * and pi-tui. Re-derive it on every {@link PI_HARNESS_VERSION} bump: a newer
859
+ * harness may add a package (0.87 adds `@earendil-works/chord`).
860
+ */
861
+ export const PI_HARNESS_PINNED_DEPENDENCIES: Readonly<Record<string, string>> =
862
+ {
863
+ '@earendil-works/pi-agent-core': PI_HARNESS_VERSION,
864
+ '@earendil-works/pi-ai': PI_HARNESS_VERSION,
865
+ '@earendil-works/pi-tui': PI_HARNESS_VERSION,
866
+ };
867
+
868
+ /**
869
+ * The throwaway `package.json` the setup action installs the harness from: the
870
+ * harness at {@link PI_HARNESS_VERSION} as the only dependency, and every pi
871
+ * package it loads forced to its exact pin through npm `overrides`.
872
+ *
873
+ * A project-local install (not `npm install -g`) because `overrides` only apply
874
+ * to the ROOT project, and a global install makes each package its own root. The
875
+ * same file is used in `registry` and `workspace` mode: pnpm 11 also isolates
876
+ * every global package, so listing the pins next to the harness in
877
+ * `pnpm add -g` would not constrain the harness's own tree either.
878
+ */
879
+ export function piHarnessManifest(): {
880
+ private: true;
881
+ dependencies: Record<string, string>;
882
+ overrides: Record<string, string>;
883
+ } {
884
+ return {
885
+ private: true,
886
+ dependencies: {[PI_HARNESS_PACKAGE]: PI_HARNESS_VERSION},
887
+ overrides: {...PI_HARNESS_PINNED_DEPENDENCIES},
888
+ };
889
+ }
890
+
764
891
  /**
765
892
  * The version of THIS dorfl package, read at runtime from its own
766
893
  * `package.json` (the single source changesets bumps on release). The composite
@@ -798,29 +925,48 @@ export function dorflPackageVersion(): string {
798
925
  return version;
799
926
  }
800
927
 
928
+ /** Where the setup action installs the harness (under the job's temp dir). */
929
+ const PI_HARNESS_DIR = '$RUNNER_TEMP/dorfl-harness';
930
+
801
931
  /**
802
- * The install step for the configured harness (the `pi` CLI; `''` ⇒ none).
803
- * `registry` mode installs via `npm install -g`; `workspace` mode installs via
804
- * `pnpm add -g` so the harness lands on the pnpm global bin already on
805
- * `$GITHUB_PATH` (mirroring whitesmith's dev mode).
932
+ * The install + smoke steps for the configured harness (the `pi` CLI; `''` ⇒
933
+ * none). Identical in `registry` and `workspace` mode: the harness is installed
934
+ * with npm from {@link piHarnessManifest} into a job-local directory whose
935
+ * `node_modules/.bin` is added to `$GITHUB_PATH`, so every pi package it loads
936
+ * is pinned whatever package manager the repository uses. The smoke step then
937
+ * runs `pi --version`, which imports the harness's whole module graph, so a
938
+ * broken install fails setup with a clear message instead of failing inside a
939
+ * dorfl agent launch.
806
940
  */
807
- function harnessInstallStep(
808
- harness: HarnessAdapter,
809
- installSource: InstallSource,
810
- ): string {
941
+ function harnessInstallStep(harness: HarnessAdapter): string {
811
942
  if (harness === 'pi') {
812
- const spec = `${PI_HARNESS_PACKAGE}@${PI_HARNESS_VERSION}`;
813
- const run =
814
- installSource === 'workspace'
815
- ? `pnpm add -g ${spec}`
816
- : `npm install -g ${spec}`;
943
+ const manifest = JSON.stringify(piHarnessManifest(), null, 2);
817
944
  return `
818
945
  # Pinned to the exact harness version this dorfl release declares
819
- # (PI_HARNESS_VERSION): the job holds write access and a provider key, so the
820
- # version must not move on its own. Re-run \`dorfl install-ci\` to upgrade.
946
+ # (PI_HARNESS_VERSION), with every pi package it loads pinned too (npm
947
+ # \`overrides\`; a caret range on a 0.x version floats within the minor): the
948
+ # job holds a provider key, so no version may move on its own. Re-run
949
+ # \`dorfl install-ci\` to upgrade.
821
950
  - name: Install agent harness (pi)
822
951
  shell: bash
823
- run: ${run}`;
952
+ run: |
953
+ mkdir -p "${PI_HARNESS_DIR}"
954
+ cd "${PI_HARNESS_DIR}"
955
+ cat > package.json << 'HARNESS_EOF'
956
+ ${indent(manifest, 8)}
957
+ HARNESS_EOF
958
+ npm install --no-audit --no-fund
959
+ echo "${PI_HARNESS_DIR}/node_modules/.bin" >> "$GITHUB_PATH"
960
+
961
+ # \`pi --version\` loads the harness's module graph, so an install whose pi
962
+ # packages do not load together fails HERE, not inside a dorfl agent launch.
963
+ - name: Check the agent harness loads (pi)
964
+ shell: bash
965
+ run: |
966
+ if ! pi --version; then
967
+ 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."
968
+ exit 1
969
+ fi`;
824
970
  }
825
971
  return '';
826
972
  }
@@ -833,14 +979,11 @@ function harnessInstallStep(
833
979
  * `uses: ./.github/actions/dorfl-setup`. The advance-loop seed template
834
980
  * already references this exact action name + path (`docs/ci/README.md`).
835
981
  *
836
- * The auth step branches on the mode: `models-json` writes a generated
837
- * `~/.pi/agent/models.json` inline (the conservative default) AND exports each
982
+ * The auth step writes a generated `~/.pi/agent/models.json` inline AND exports each
838
983
  * configured provider's API key from a same-named ACTION INPUT to `$GITHUB_ENV`,
839
984
  * so the agent step (`pi`) can resolve the env var `models.json` references; the
840
985
  * workflow passes the secrets ONCE via `with:` on the `uses:` line (no
841
- * per-step / per-capability provider enumeration). `auth-json` writes
842
- * `~/.pi/agent/auth.json` from `$PI_AUTH_JSON` + runs the OAuth-refresh script
843
- * (the sharp edge). Deterministic: the same config produces byte-identical output.
986
+ * per-step / per-capability provider enumeration). Deterministic: the same config produces byte-identical output.
844
987
  */
845
988
  export function generateSetupAction(
846
989
  config: ResolvedCIConfig,
@@ -854,51 +997,32 @@ export function generateSetupAction(
854
997
  */
855
998
  projectSetupSteps?: string,
856
999
  ): string {
857
- let authStep: string;
858
- // models-json mode declares one ACTION INPUT per distinct provider key (named
859
- // identically to the secret / env var) and exports it to `$GITHUB_ENV`, so every
860
- // later step inherits it. auth-json mode has no provider keys (it uses auth.json).
861
- const providerKeyNames =
862
- config.authMode === 'models-json' ? requiredSecretNames(config) : [];
863
- if (config.authMode === 'auth-json') {
864
- authStep = `\
865
- - name: Configure agent auth (auth.json)
866
- shell: bash
867
- run: |
868
- if [ -z "$PI_AUTH_JSON" ]; then
869
- echo "ERROR: PI_AUTH_JSON secret is not set" >&2; exit 1
870
- fi
871
- mkdir -p ~/.pi/agent
872
- echo "$PI_AUTH_JSON" > ~/.pi/agent/auth.json
873
- chmod 600 ~/.pi/agent/auth.json
874
-
875
- # Workaround for https://github.com/badlogic/pi-mono/issues/2743 — the SINGLE
876
- # CI→repo mutation in the design; needs GH_PAT to rotate PI_AUTH_JSON.
877
- - name: Refresh OAuth token
878
- shell: bash
879
- run: node .github/scripts/refresh-oauth-token.mjs`;
880
- } else {
881
- const modelsJsonStr = JSON.stringify(
882
- buildModelsJson(config.providers),
883
- null,
884
- 2,
885
- );
886
- // Map each provider-key INPUT into the step env, then append the non-empty
887
- // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
888
- // (never interpolated into the script body), and Actions masks the secret in
889
- // logs. `models.json`'s `apiKey` is the env-var NAME; this puts the value there.
890
- const exportEnvLines = providerKeyNames
891
- .map((name) => ` ${name}: \${{ inputs.${name} }}`)
892
- .join('\n');
893
- const exportRunLines = providerKeyNames
894
- .map(
895
- (name) =>
896
- ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`,
897
- )
898
- .join('\n');
899
- const exportStep =
900
- providerKeyNames.length > 0
901
- ? `\n
1000
+ // models-json mode (the only mode) declares one ACTION INPUT per distinct
1001
+ // provider key (named identically to the secret / env var) and exports it to
1002
+ // `$GITHUB_ENV`, so every later step inherits it.
1003
+ const providerKeyNames = providerSecretNames(config);
1004
+ const modelsJsonStr = JSON.stringify(
1005
+ buildModelsJson(config.providers),
1006
+ null,
1007
+ 2,
1008
+ );
1009
+ // Map each provider-key INPUT into the step env, then append the non-empty
1010
+ // ones to `$GITHUB_ENV` so the agent step inherits them. Values flow via env
1011
+ // (never interpolated into the script body), and Actions masks the secret in
1012
+ // logs. `models.json`'s `apiKey` is a `$NAME` env-var REFERENCE (pi resolves it at
1013
+ // runtime; a bare name would be a literal key); this puts the value there.
1014
+ const exportEnvLines = providerKeyNames
1015
+ .map((name) => ` ${name}: \${{ inputs.${name} }}`)
1016
+ .join('\n');
1017
+ const exportRunLines = providerKeyNames
1018
+ .map(
1019
+ (name) =>
1020
+ ` if [ -n "\${${name}}" ]; then echo "${name}=\${${name}}" >> "$GITHUB_ENV"; fi`,
1021
+ )
1022
+ .join('\n');
1023
+ const exportStep =
1024
+ providerKeyNames.length > 0
1025
+ ? `\n
902
1026
  # Surface the configured provider API key(s) to the environment so the agent
903
1027
  # step (\`pi\`) can resolve the env var \`models.json\` references. The workflow
904
1028
  # passes the secret(s) via \`with:\` on the \`uses:\` line; we forward the
@@ -909,8 +1033,8 @@ export function generateSetupAction(
909
1033
  ${exportEnvLines}
910
1034
  run: |
911
1035
  ${exportRunLines}`
912
- : '';
913
- authStep = `\
1036
+ : '';
1037
+ const authStep = `\
914
1038
  - name: Configure agent models (models.json)
915
1039
  shell: bash
916
1040
  run: |
@@ -918,20 +1042,16 @@ ${exportRunLines}`
918
1042
  cat > ~/.pi/agent/models.json << 'MODELS_EOF'
919
1043
  ${indent(modelsJsonStr, 8)}
920
1044
  MODELS_EOF${exportStep}`;
921
- }
922
1045
 
923
- const installHarness = harnessInstallStep(
924
- config.harness,
925
- config.installSource,
926
- );
1046
+ const installHarness = harnessInstallStep(config.harness);
927
1047
 
928
1048
  // The CLI-install block branches on installSource. `registry` installs the
929
1049
  // published CLI via `npm install -g dorfl` (the default for every
930
1050
  // consumer repo). `workspace` builds the CLI from the checked-out source and
931
1051
  // links it onto PATH — for the self-hosting dorfl monorepo, which is
932
1052
  // not published under that npm name. We add pnpm's global bin to $GITHUB_PATH
933
- // so the linked `dorfl` (and the pnpm-installed harness) are on PATH in
934
- // all subsequent steps; we always rebuild because source changes per commit.
1053
+ // so the linked `dorfl` is on PATH in all subsequent steps (the harness adds
1054
+ // its own bin dir, the same way in both modes); we always rebuild because source changes per commit.
935
1055
  let installSteps: string;
936
1056
  if (config.installSource === 'workspace') {
937
1057
  installSteps = `\
@@ -986,10 +1106,9 @@ ${indent(modelsJsonStr, 8)}
986
1106
  // double-resolution. The install step below leaves `dorfl` on `$PATH` as the
987
1107
  // bootstrap; the forward does the pinning.
988
1108
 
989
- // One optional ACTION INPUT per provider key (models-json mode), named
990
- // identically to the secret/env var. Optional + default '' so a workflow that
991
- // does not pass it (or auth-json mode, which declares none) is valid; the export
992
- // step skips an empty value. auth-json mode declares no inputs.
1109
+ // One optional ACTION INPUT per provider key, named identically to the
1110
+ // secret/env var. Optional + default '' so a workflow that does not pass it is
1111
+ // valid; the export step skips an empty value.
993
1112
  const inputsBlock =
994
1113
  providerKeyNames.length > 0
995
1114
  ? `inputs:
@@ -1056,111 +1175,234 @@ ${authStep}
1056
1175
  `;
1057
1176
  }
1058
1177
 
1178
+ /** The local path a workflow step `uses:` for the agent-role setup action. */
1179
+ export const SETUP_ACTION_USES = './.github/actions/dorfl-setup';
1180
+
1181
+ /** The local path a workflow step `uses:` for the writer-role setup action. */
1182
+ export const WRITER_SETUP_ACTION_USES = './.github/actions/dorfl-setup-writer';
1183
+
1059
1184
  /**
1060
- * The OAuth-refresh script emitted ONLY in `auth-json` mode (whitesmith's
1061
- * `pi-mono#2743` workaround): refreshes the Anthropic OAuth token in
1062
- * `~/.pi/agent/auth.json` before the agent runs, then writes the rotated token
1063
- * BACK via `gh secret set PI_AUTH_JSON` (the single CI→repo mutation, needing a
1064
- * `GH_PAT`). This is the documented SHARP EDGE; `models-json` mode avoids it.
1065
- */
1066
- export const REFRESH_OAUTH_SCRIPT = `\
1067
- #!/usr/bin/env node
1068
- /**
1069
- * Refresh OAuth tokens in the agent's auth.json before it runs.
1185
+ * Generate the WRITER-ROLE composite setup action (`dorfl-setup-writer`, spec
1186
+ * `ci-agent-job-without-write-token` §9, ADR
1187
+ * `ci-agent-job-holds-no-write-token`; task `ci-split-generate-workflows`).
1188
+ *
1189
+ * The jobs that hold a write token and run no agent (the split item
1190
+ * workflow's lock and apply jobs, `reap-merged-branches`, `close-job`, and the
1191
+ * read-only `enumerate`) install Node and dorfl ONLY: no harness, no provider
1192
+ * key, no `models.json`, no project-setup hook, no install of the project's
1193
+ * dependencies, and no Actions cache. In `registry` mode the dorfl install runs
1194
+ * from `$RUNNER_TEMP`, outside the checkout, with `--ignore-scripts`, so a
1195
+ * repository `.npmrc` cannot redirect the registry and no install script runs.
1070
1196
  *
1071
- * Workaround for https://github.com/badlogic/pi-mono/issues/2743.
1072
- * After refreshing, updates the PI_AUTH_JSON GitHub secret so the next run has
1073
- * the latest rotated refresh token (requires GH_PAT with repo scope). This is the
1074
- * known SHARP EDGE of auth-json mode; models-json mode avoids it entirely.
1197
+ * In `workspace` mode (dorfl's own repository) dorfl is built from source
1198
+ * (decision 8): the tree of `source-ref` (default the checkout's `HEAD`) is
1199
+ * exported with `git archive` into `$RUNNER_TEMP` and built there with
1200
+ * `--ignore-scripts`. In merge mode that is code an earlier agent landed on
1201
+ * `main`, which is the trust merge mode already implies.
1075
1202
  *
1076
- * Remove this script once the upstream fix is released.
1203
+ * In both modes it then exports `DORFL_NO_FORWARD=1` for the rest of the job
1204
+ * ({@link writerNoForwardStep}): the writer role never forwards to the
1205
+ * repository's `dorflCmd`, which usually points into dependencies this role
1206
+ * never installs. In `registry` mode it also warns when the repository pins a
1207
+ * different dorfl version ({@link WRITER_PIN_CHECK_SCRIPT}).
1208
+ *
1209
+ * It is a SIBLING action rather than a `role` input on `dorfl-setup`: the
1210
+ * project-setup hook is an opaque fragment spliced first into `dorfl-setup`
1211
+ * (ADR `install-ci-project-provisioning-native-passthrough`), and a composite
1212
+ * action cannot skip an opaque block of steps by input. `dorfl-setup` stays the
1213
+ * agent role, byte for byte.
1077
1214
  */
1078
- import { readFileSync, writeFileSync, chmodSync } from "fs";
1079
- import { join } from "path";
1080
- import { execSync } from "child_process";
1215
+ export function generateWriterSetupAction(config: ResolvedCIConfig): string {
1216
+ let installSteps: string;
1217
+ let inputsBlock = '';
1218
+ // Registry mode only: warn when the repository's `dorflCmd` names an exact
1219
+ // `dorfl@<version>` that is not the version pinned here. Workspace mode builds
1220
+ // dorfl from the checked-out source, so there is no second version to compare.
1221
+ let pinCheckStep = '';
1222
+ if (config.installSource === 'workspace') {
1223
+ inputsBlock = `inputs:
1224
+ source-ref:
1225
+ description: 'The commit to build dorfl from (workspace mode). Blank: the checkout HEAD.'
1226
+ required: false
1227
+ default: ''
1081
1228
 
1082
- const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1083
- const ANTHROPIC_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
1229
+ `;
1230
+ installSteps = `\
1231
+ - name: Setup pnpm
1232
+ uses: ${pinnedUses(ACTION_PINS.pnpmSetup)}
1084
1233
 
1085
- const authPath = join(process.env.HOME, ".pi", "agent", "auth.json");
1086
- const auth = JSON.parse(readFileSync(authPath, "utf-8"));
1087
- const cred = auth.anthropic;
1234
+ - name: Add pnpm global bin to PATH
1235
+ shell: bash
1236
+ run: |
1237
+ pnpm setup
1238
+ echo "$HOME/.local/share/pnpm" >> "$GITHUB_PATH"
1088
1239
 
1089
- if (!cred || cred.type !== "oauth") {
1090
- console.log("No OAuth credentials for anthropic, skipping refresh");
1091
- process.exit(0);
1092
- }
1240
+ # Workspace mode (dorfl's own repository, decision 8 of ADR
1241
+ # ci-agent-job-holds-no-write-token): build dorfl from the tree of
1242
+ # \`source-ref\`, exported into $RUNNER_TEMP (outside the checkout), with
1243
+ # \`--ignore-scripts\`. In merge mode this is code an earlier agent landed on
1244
+ # main running in a write job, the trust merge mode already implies.
1245
+ # Only dorfl and its workspace dependencies (\`--filter 'dorfl...'\`) are
1246
+ # installed and built: the other workspace packages (the website) are not
1247
+ # needed to run dorfl, so they do not run next to the write token.
1248
+ - name: Build dorfl from the base commit (outside the checkout)
1249
+ shell: bash
1250
+ env:
1251
+ SOURCE_REF: \${{ inputs.source-ref }}
1252
+ run: |
1253
+ set -euo pipefail
1254
+ src="$RUNNER_TEMP/dorfl-src"
1255
+ rm -rf "$src"
1256
+ mkdir -p "$src"
1257
+ git archive "$(git rev-parse --verify "\${SOURCE_REF:-HEAD}^{commit}")" | tar -x -C "$src"
1258
+ cd "$src"
1259
+ pnpm install --frozen-lockfile --ignore-scripts --filter 'dorfl...'
1260
+ pnpm --filter 'dorfl...' build
1261
+ cd packages/dorfl && pnpm link --global`;
1262
+ } else {
1263
+ installSteps = `\
1264
+ # Pinned to the dorfl version that generated this action. Installed from
1265
+ # $RUNNER_TEMP (outside the checkout, so a repository .npmrc cannot redirect
1266
+ # the registry) with --ignore-scripts (no install script runs next to the
1267
+ # write token).
1268
+ - name: Install dorfl (outside the checkout, no install scripts)
1269
+ shell: bash
1270
+ run: |
1271
+ cd "$RUNNER_TEMP"
1272
+ npm install -g --ignore-scripts dorfl@${dorflPackageVersion()}`;
1273
+ pinCheckStep = writerPinCheckStep(dorflPackageVersion());
1274
+ }
1275
+ return `\
1276
+ name: Setup dorfl (writer role)
1277
+ description: Install Node.js and dorfl only, for the CI jobs that run no agent (lock, apply, reap, close, enumerate)
1278
+
1279
+ ${inputsBlock}# THE WRITER ROLE (ADR ci-agent-job-holds-no-write-token): the jobs that use
1280
+ # this action hold a write token (or run next to one) and run NO agent and NO
1281
+ # repository code. So it installs Node.js and dorfl ONLY: no agent harness, no
1282
+ # provider API key, no models.json, no project-setup hook, no install of the
1283
+ # project's dependencies, and no Actions cache (an agent can write
1284
+ # default-branch cache entries, so a restore here would run its bytes with
1285
+ # write access). The agent role is ./.github/actions/dorfl-setup.
1286
+ runs:
1287
+ using: composite
1288
+ steps:
1289
+ - name: Setup Node.js
1290
+ uses: ${pinnedUses(ACTION_PINS.setupNode)}
1291
+ with:
1292
+ node-version: '22'
1293
+ # No cache in the writer role (see above).
1294
+ package-manager-cache: false
1295
+
1296
+ - name: Configure git identity
1297
+ shell: bash
1298
+ run: |
1299
+ git config user.name "dorfl[bot]"
1300
+ git config user.email "dorfl[bot]@users.noreply.github.com"
1093
1301
 
1094
- if (Date.now() < cred.expires) {
1095
- console.log("Token still valid until", new Date(cred.expires).toISOString());
1096
- process.exit(0);
1302
+ ${installSteps}
1303
+
1304
+ ${writerNoForwardStep()}${pinCheckStep}
1305
+ `;
1097
1306
  }
1098
1307
 
1099
- console.log(
1100
- "Token expired at",
1101
- new Date(cred.expires).toISOString(),
1102
- "- refreshing..."
1103
- );
1104
-
1105
- const response = await fetch(ANTHROPIC_TOKEN_URL, {
1106
- method: "POST",
1107
- headers: {
1108
- "Content-Type": "application/x-www-form-urlencoded",
1109
- Accept: "application/json",
1110
- },
1111
- body: new URLSearchParams({
1112
- grant_type: "refresh_token",
1113
- client_id: ANTHROPIC_CLIENT_ID,
1114
- refresh_token: cred.refresh,
1115
- }).toString(),
1116
- signal: AbortSignal.timeout(30_000),
1117
- });
1118
-
1119
- const data = await response.json();
1120
-
1121
- if (!response.ok) {
1122
- console.error("Refresh failed:", response.status, JSON.stringify(data));
1123
- process.exit(1);
1308
+ /**
1309
+ * The writer-role step that turns OFF the bootstrap self-forward for every later
1310
+ * step of the job, by exporting {@link NO_FORWARD_ENV}`=1` to `$GITHUB_ENV`.
1311
+ *
1312
+ * Why: a repository that pins dorfl through its dependencies declares
1313
+ * `"dorflCmd": "node_modules/.bin/dorfl"`. The writer role deliberately installs
1314
+ * no project dependency, so that path does not exist, and a forwarding dorfl
1315
+ * would fail every writer job (0.15.0 regression). The dorfl installed above is
1316
+ * already the exact version `install-ci` generated these workflows with (or,
1317
+ * in workspace mode, dorfl built from the checked-out source), so forwarding
1318
+ * gains nothing here. Set ONCE in the action so no job template can forget it.
1319
+ * The agent-role `dorfl-setup` does install dependencies and keeps forwarding.
1320
+ */
1321
+ function writerNoForwardStep(): string {
1322
+ return `\
1323
+ # The writer role runs the dorfl installed above and never forwards to the
1324
+ # repository's dorflCmd: that command usually points into the project's
1325
+ # dependencies (node_modules/.bin/dorfl), which this role never installs.
1326
+ # Exported to $GITHUB_ENV so it holds for every later step of the job.
1327
+ - name: Run the dorfl installed above, not the repository's dorflCmd
1328
+ shell: bash
1329
+ run: echo "${NO_FORWARD_ENV}=1" >> "$GITHUB_ENV"`;
1124
1330
  }
1125
1331
 
1126
- auth.anthropic = {
1127
- type: "oauth",
1128
- refresh: data.refresh_token,
1129
- access: data.access_token,
1130
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
1332
+ /**
1333
+ * The JavaScript the registry-mode writer step runs with `node -e '<script>'
1334
+ * <pinned>` to WARN (never fail) when the repository pins a different dorfl
1335
+ * than the one the writer role installed. Exported so a test can run it.
1336
+ *
1337
+ * It reads JSON only (`dorfl.json`, and `package.json` for the devDependency
1338
+ * case) and runs no project code, so it is safe next to a write token. It
1339
+ * recognises two version-bearing shapes of `dorflCmd`:
1340
+ *
1341
+ * - an exact `dorfl@<version>` inside the command (`npx dorfl@0.16.0`,
1342
+ * `mise exec dorfl@0.16.0 --`);
1343
+ * - `node_modules/.bin/dorfl`, when `package.json` declares `dorfl` in
1344
+ * `devDependencies` (or `dependencies`) at an EXACT version.
1345
+ *
1346
+ * Anything else (a caret range, `./bin/dorfl`, no `dorflCmd`) is skipped
1347
+ * silently: resolving it would need the project's dependencies installed,
1348
+ * which the writer role never does. No single quote may appear in it (it is
1349
+ * wrapped in a single-quoted bash string).
1350
+ */
1351
+ export const WRITER_PIN_CHECK_SCRIPT = `\
1352
+ const fs = require("fs");
1353
+ const pinned = process.argv[1];
1354
+ const readJson = (p) => {
1355
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) { return undefined; }
1131
1356
  };
1132
-
1133
- writeFileSync(authPath, JSON.stringify(auth, null, 2));
1134
- chmodSync(authPath, 0o600);
1135
- console.log(
1136
- "Token refreshed, new expiry:",
1137
- new Date(auth.anthropic.expires).toISOString()
1138
- );
1139
-
1140
- // Update the GitHub secret so the next run has the latest refresh token.
1141
- const repo = process.env.GITHUB_REPOSITORY;
1142
- const token = process.env.GH_PAT;
1143
- if (repo && token) {
1144
- try {
1145
- execSync(\`gh secret set PI_AUTH_JSON --repo "\${repo}"\`, {
1146
- input: JSON.stringify(auth),
1147
- env: { ...process.env, GH_TOKEN: token },
1148
- stdio: ["pipe", "pipe", "pipe"],
1149
- });
1150
- console.log("PI_AUTH_JSON secret updated");
1151
- } catch (err) {
1152
- console.warn("Failed to update secret (non-fatal):", err.stderr?.toString() || err.message);
1357
+ const semver = "\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:-[0-9A-Za-z.-]+)?";
1358
+ const config = readJson("${REPO_CONFIG_FILENAME}") || readJson("${REPO_CONFIG_FILENAME_LEGACY}") || {};
1359
+ const cmd = config.dorflCmd;
1360
+ if (typeof cmd !== "string") process.exit(0);
1361
+ let found;
1362
+ let where;
1363
+ const inCmd = new RegExp("(?:^|\\\\s)dorfl@(" + semver + ")(?=\\\\s|$)").exec(cmd);
1364
+ if (inCmd) {
1365
+ found = inCmd[1];
1366
+ where = "${REPO_CONFIG_FILENAME} dorflCmd";
1367
+ } else if (/(?:^|[\\s/])node_modules\\/\\.bin\\/dorfl(?=\\s|$)/.test(cmd)) {
1368
+ const pkg = readJson("package.json") || {};
1369
+ const spec = (pkg.devDependencies || {}).dorfl || (pkg.dependencies || {}).dorfl;
1370
+ if (typeof spec === "string" && new RegExp("^" + semver + "$").test(spec)) {
1371
+ found = spec;
1372
+ where = "package.json (the dorflCmd " + cmd + ")";
1153
1373
  }
1154
- } else {
1155
- console.log("Skipping secret update (no GH_PAT or GITHUB_REPOSITORY)");
1156
1374
  }
1157
- `;
1375
+ if (found !== undefined && found !== pinned) {
1376
+ const esc = (s) => s.replace(/%/g, "%25").replace(/\\r/g, "%0D").replace(/\\n/g, "%0A");
1377
+ 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."));
1378
+ }`;
1379
+
1380
+ /**
1381
+ * The registry-mode writer step that runs {@link WRITER_PIN_CHECK_SCRIPT}. With
1382
+ * the forward off (see {@link writerNoForwardStep}) a disagreement between the
1383
+ * pinned global and the repository's own pin would otherwise go unnoticed: the
1384
+ * agent job forwards to `dorflCmd` while the writer jobs run the pin.
1385
+ */
1386
+ function writerPinCheckStep(pinned: string): string {
1387
+ return `
1388
+
1389
+ # Warn (never fail) when the repository pins a dorfl version other than the
1390
+ # one installed above: the agent job forwards to dorflCmd, these writer jobs
1391
+ # do not, so the two roles would run different versions. Reads dorfl.json
1392
+ # and package.json as JSON only; runs no project code.
1393
+ - name: Warn if the repository pins another dorfl version
1394
+ shell: bash
1395
+ run: |
1396
+ node -e '
1397
+ ${indent(WRITER_PIN_CHECK_SCRIPT, 10)}
1398
+ ' "${pinned}"`;
1399
+ }
1158
1400
 
1159
1401
  // ─── the --fake snapshot mechanism + artifact assembly ───────────────────────
1160
1402
 
1161
1403
  /**
1162
- * The setup ARTIFACTS this task generates: the composite setup action + (in
1163
- * `auth-json` mode) the OAuth-refresh script, plus any selected capabilities'
1404
+ * The setup ARTIFACTS this task generates: the composite setup actions, plus
1405
+ * any selected capabilities'
1164
1406
  * workflow files. This task emits NO capability workflow itself (those are the
1165
1407
  * sibling tasks); `capabilities` defaults to none.
1166
1408
  */
@@ -1180,13 +1422,11 @@ export function buildSetupArtifacts(
1180
1422
  path: join('actions', 'dorfl-setup', 'action.yml'),
1181
1423
  content: generateSetupAction(config, options?.projectSetupSteps),
1182
1424
  },
1425
+ {
1426
+ path: join('actions', 'dorfl-setup-writer', 'action.yml'),
1427
+ content: generateWriterSetupAction(config),
1428
+ },
1183
1429
  ];
1184
- if (config.authMode === 'auth-json') {
1185
- files.push({
1186
- path: join('scripts', 'refresh-oauth-token.mjs'),
1187
- content: REFRESH_OAUTH_SCRIPT,
1188
- });
1189
- }
1190
1430
  for (const cap of capabilities) {
1191
1431
  files.push(...cap.emit(config));
1192
1432
  }