dorfl 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +309 -489
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +46 -14
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +123 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -0
  162. package/dist/install-ci-action-pins.js +190 -0
  163. package/dist/install-ci-action-pins.js.map +1 -0
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +155 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +304 -200
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -292
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +8 -4
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +375 -538
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +52 -15
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +209 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +388 -233
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -327
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +8 -4
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
@@ -0,0 +1,350 @@
1
+ import {ledgerWrite} from './ledger-write.js';
2
+ import {refWrite} from './ref-write.js';
3
+ import {integrationLand} from './integration-core.js';
4
+ import type {ReviewProvider} from './integrator.js';
5
+ import type {IssueProvider} from './issue-provider.js';
6
+ import {enterPhase, type Phase} from './phase.js';
7
+
8
+ /**
9
+ * **Record at the seams, resume at the boundary** (spec
10
+ * `ci-agent-job-without-write-token` §4, task `ci-split-phase-mode-and-guards`).
11
+ *
12
+ * In the `agent` phase the job token cannot write, and dorfl must not try: every
13
+ * write seam is replaced by a RECORDING implementation. The first post-agent
14
+ * write call the recorder KNOWS captures its intent (seam, method, input) and
15
+ * halts the pipeline with a {@link PhaseHaltSentinel}, which the phase driver
16
+ * ({@link runAgentPhase}) catches. Nothing after the first write runs in the
17
+ * agent phase, so no agent-phase step ever depends on a write RESULT (a PR URL,
18
+ * `mergeNonFastForward`, `publishedHead`, a CAS rejection).
19
+ *
20
+ * A write seam call the recorder does NOT know throws
21
+ * {@link UnrecordedWriteError}: an unrouted write is a bug the tests catch.
22
+ *
23
+ * The write seams covered here are every seam a CI path writes through
24
+ * (task `ci-split-route-direct-writes-through-seams`): `ledgerWrite` (which also
25
+ * reaches the integrator, via `applyCompleteTransition`), `refWrite`, the review
26
+ * provider and the issue provider (whose READS pass through), plus the
27
+ * `integrationLand` seam (`integration-core.ts`): the land half of the build's
28
+ * rebase-to-integrate tail, whose call is the success path's boundary (task
29
+ * `ci-split-build-path`). Which seam calls
30
+ * each path records, and how the captured input maps to a handoff intent kind
31
+ * (`ci-handoff-format.ts`), is decided by the per-path split tasks; this module
32
+ * is the shared machinery.
33
+ *
34
+ * The captured `input` is the seam's in-memory argument, which may hold an env
35
+ * with credentials: it is NEVER serialised as-is. The per-path code builds the
36
+ * handoff record from it field by field.
37
+ */
38
+
39
+ /** The write seams a CI path reaches (the recorder replaces all of them). */
40
+ export type WriteSeamName =
41
+ | 'ledgerWrite'
42
+ | 'refWrite'
43
+ | 'integrationLand'
44
+ | 'reviewProvider'
45
+ | 'issueProvider';
46
+
47
+ /** A seam call id: `<seam>.<method>`, e.g. `refWrite.pushContinuedBranch`. */
48
+ export type WriteSeamCall = `${WriteSeamName}.${string}`;
49
+
50
+ /** The captured intent of the first recorded write. */
51
+ export interface RecordedWriteIntent {
52
+ /** The seam the write went through. */
53
+ seam: WriteSeamName;
54
+ /** The seam method called. */
55
+ method: string;
56
+ /** The call's argument, in memory only (see the module comment). */
57
+ input: unknown;
58
+ /**
59
+ * What the pipeline said the boundary IS, when it said so just before the
60
+ * write ({@link annotatePhaseBoundary}): several outcomes reach the same seam
61
+ * call (a red gate, an agent failure and a STOP all bounce through
62
+ * `ledgerWrite.applyNeedsAttentionTransition`), and the per-path code needs to
63
+ * tell them apart. In memory only, like {@link input}.
64
+ */
65
+ annotation?: unknown;
66
+ }
67
+
68
+ /**
69
+ * Thrown by a recording seam on the first recorded write: it halts the
70
+ * pipeline so nothing after the boundary runs. The phase driver catches it; it
71
+ * is never a failure.
72
+ */
73
+ export class PhaseHaltSentinel extends Error {
74
+ override readonly name = 'PhaseHaltSentinel';
75
+ constructor(readonly intent: RecordedWriteIntent) {
76
+ super(
77
+ `agent phase halted at the first write (${intent.seam}.${intent.method}); ` +
78
+ 'the apply phase performs it.',
79
+ );
80
+ }
81
+ }
82
+
83
+ /**
84
+ * A write seam call the recorder does not know, in the agent phase. Always a
85
+ * dorfl bug: the write is not routed through the phase split.
86
+ */
87
+ export class UnrecordedWriteError extends Error {
88
+ override readonly name = 'UnrecordedWriteError';
89
+ constructor(
90
+ readonly seam: WriteSeamName,
91
+ readonly method: string,
92
+ ) {
93
+ super(
94
+ `agent phase: unrecorded write ${seam}.${method} (the agent job holds a ` +
95
+ 'read-only token, and this write is not routed to the apply phase). ' +
96
+ 'This is a dorfl bug in the phase split.',
97
+ );
98
+ }
99
+ }
100
+
101
+ /** The recording state one agent-phase run shares across every seam. */
102
+ export interface PhaseRecorder {
103
+ /** The seam calls this recorder captures (every other write throws). */
104
+ readonly recordable: ReadonlySet<WriteSeamCall>;
105
+ /** The first recorded write, once the pipeline has reached it. */
106
+ captured(): RecordedWriteIntent | undefined;
107
+ /** The first unrecorded write, if the pipeline made one. */
108
+ violation(): UnrecordedWriteError | undefined;
109
+ /**
110
+ * Remember what the NEXT write is (see {@link annotatePhaseBoundary}); the
111
+ * first recorded write captures the latest annotation. Ignored once halted.
112
+ */
113
+ annotate(annotation: unknown): void;
114
+ /**
115
+ * Handle one write seam call: always throws. The first recorded call
116
+ * captures its intent and throws a {@link PhaseHaltSentinel}; any later write
117
+ * re-throws that same sentinel (nothing after the boundary may write). An
118
+ * unknown call throws (and remembers) an {@link UnrecordedWriteError}.
119
+ */
120
+ write(seam: WriteSeamName, method: string, input: unknown): never;
121
+ }
122
+
123
+ /** Create a {@link PhaseRecorder} that captures exactly the `record` calls. */
124
+ export function createPhaseRecorder(options: {
125
+ record: Iterable<WriteSeamCall>;
126
+ }): PhaseRecorder {
127
+ const recordable = new Set<WriteSeamCall>(options.record);
128
+ let sentinel: PhaseHaltSentinel | undefined;
129
+ let violation: UnrecordedWriteError | undefined;
130
+ let annotation: unknown;
131
+ return {
132
+ recordable,
133
+ captured: () => sentinel?.intent,
134
+ violation: () => violation,
135
+ annotate(next): void {
136
+ if (sentinel === undefined) annotation = next;
137
+ },
138
+ write(seam, method, input): never {
139
+ if (violation !== undefined) {
140
+ throw violation;
141
+ }
142
+ if (sentinel !== undefined) {
143
+ throw sentinel;
144
+ }
145
+ if (!recordable.has(`${seam}.${method}`)) {
146
+ violation = new UnrecordedWriteError(seam, method);
147
+ throw violation;
148
+ }
149
+ sentinel = new PhaseHaltSentinel({
150
+ seam,
151
+ method,
152
+ input,
153
+ ...(annotation === undefined ? {} : {annotation}),
154
+ });
155
+ throw sentinel;
156
+ },
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Replace EVERY method of the process-wide `ledgerWrite`, `refWrite` and
162
+ * `integrationLand` seams with a recording one, and return a function restoring the originals. Every
163
+ * method is enumerated from the seam object, so a seam method added later is
164
+ * covered (as an unrecorded write) without touching this function.
165
+ */
166
+ export function installRecordingSeams(recorder: PhaseRecorder): () => void {
167
+ const previousRecorder = activeRecorder;
168
+ activeRecorder = recorder;
169
+ const restores = [
170
+ () => {
171
+ activeRecorder = previousRecorder;
172
+ },
173
+ swapMethods('ledgerWrite', ledgerWrite, recorder),
174
+ swapMethods('refWrite', refWrite, recorder),
175
+ swapMethods('integrationLand', integrationLand, recorder),
176
+ ];
177
+ return () => {
178
+ for (const restore of restores.reverse()) {
179
+ restore();
180
+ }
181
+ };
182
+ }
183
+
184
+ /** The recorder the process-wide seams write into now (agent phase only). */
185
+ let activeRecorder: PhaseRecorder | undefined;
186
+
187
+ /**
188
+ * Say what the next write IS, for the agent-phase recorder to capture with it.
189
+ * A no-op outside the agent phase (no recorder is installed), so a pipeline
190
+ * calls it unconditionally right before a write seam call whose seam and method
191
+ * alone do not name the outcome (task `ci-split-build-path-non-integrate-intents`:
192
+ * an agent failure, a STOP and a red gate all bounce through the same seam).
193
+ * The annotation never reaches the arbiter; the per-path code maps it to a
194
+ * handoff intent.
195
+ */
196
+ export function annotatePhaseBoundary(annotation: unknown): void {
197
+ activeRecorder?.annotate(annotation);
198
+ }
199
+
200
+ function swapMethods(
201
+ seam: WriteSeamName,
202
+ target: object,
203
+ recorder: PhaseRecorder,
204
+ ): () => void {
205
+ const record = target as Record<string, unknown>;
206
+ const originals = new Map<string, unknown>();
207
+ for (const [method, value] of Object.entries(record)) {
208
+ if (typeof value !== 'function') {
209
+ continue;
210
+ }
211
+ originals.set(method, value);
212
+ record[method] = (input: unknown) => recorder.write(seam, method, input);
213
+ }
214
+ return () => {
215
+ for (const [method, value] of originals) {
216
+ record[method] = value;
217
+ }
218
+ };
219
+ }
220
+
221
+ /**
222
+ * A recording {@link ReviewProvider}: every method is a write, so every call
223
+ * goes to the recorder. `name` is the wrapped provider's.
224
+ */
225
+ export function recordingReviewProvider(
226
+ inner: ReviewProvider,
227
+ recorder: PhaseRecorder,
228
+ ): ReviewProvider {
229
+ const seam: WriteSeamName = 'reviewProvider';
230
+ return {
231
+ name: inner.name,
232
+ openRequest: (input) => recorder.write(seam, 'openRequest', input),
233
+ postPRComment: (input) => recorder.write(seam, 'postPRComment', input),
234
+ postPRCommentOnBranch: (input) =>
235
+ recorder.write(seam, 'postPRCommentOnBranch', input),
236
+ closeRequestOnBranch: (input) =>
237
+ recorder.write(seam, 'closeRequestOnBranch', input),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * A recording {@link IssueProvider}: the reads (`getIssue`, `listComments`,
243
+ * `getLabels`) pass through to `inner` (the agent job's read token serves
244
+ * them); every write goes to the recorder.
245
+ */
246
+ export function recordingIssueProvider(
247
+ inner: IssueProvider,
248
+ recorder: PhaseRecorder,
249
+ ): IssueProvider {
250
+ const seam: WriteSeamName = 'issueProvider';
251
+ return {
252
+ name: inner.name,
253
+ getIssue: (input) => inner.getIssue(input),
254
+ listComments: (input) => inner.listComments(input),
255
+ getLabels: (input) => inner.getLabels(input),
256
+ postIssueComment: (input) =>
257
+ recorder.write(seam, 'postIssueComment', input),
258
+ addLabel: (input) => recorder.write(seam, 'addLabel', input),
259
+ removeLabel: (input) => recorder.write(seam, 'removeLabel', input),
260
+ closeIssue: (input) => recorder.write(seam, 'closeIssue', input),
261
+ };
262
+ }
263
+
264
+ /** The outcome of one {@link runAgentPhase}. */
265
+ export type AgentPhaseOutcome<T> =
266
+ | {
267
+ /** The pipeline finished without reaching a write. */
268
+ halted: false;
269
+ result: T;
270
+ }
271
+ | {
272
+ /** The pipeline reached its first write and stopped there. */
273
+ halted: true;
274
+ /** The captured first write. */
275
+ intent: RecordedWriteIntent;
276
+ /**
277
+ * True when the pipeline CAUGHT the sentinel and returned normally
278
+ * instead of letting it propagate. The intent still stands (the recorder
279
+ * is authoritative); the flag lets the per-path tasks assert their
280
+ * pipeline stops cleanly at the boundary.
281
+ */
282
+ sentinelSwallowed: boolean;
283
+ };
284
+
285
+ /**
286
+ * The agent-phase driver: run `pipeline` in the `agent` phase with every write
287
+ * seam recording (the process-wide `ledgerWrite` / `refWrite`; the pipeline
288
+ * wraps its injected providers with {@link recordingReviewProvider} /
289
+ * {@link recordingIssueProvider} on the same `recorder`), catch the
290
+ * {@link PhaseHaltSentinel}, and restore the seams and the previous phase.
291
+ *
292
+ * The recorder, not the exception, is authoritative: a pipeline that swallows
293
+ * the sentinel in a broad `catch` is still reported as halted at the captured
294
+ * intent, and an {@link UnrecordedWriteError} is re-thrown even if the pipeline
295
+ * swallowed it.
296
+ */
297
+ export async function runAgentPhase<T>(
298
+ recorder: PhaseRecorder,
299
+ pipeline: () => Promise<T>,
300
+ ): Promise<AgentPhaseOutcome<T>> {
301
+ const restorePhase = enterPhase('agent');
302
+ const restoreSeams = installRecordingSeams(recorder);
303
+ let result: T | undefined;
304
+ let caught: unknown;
305
+ let threw = false;
306
+ try {
307
+ result = await pipeline();
308
+ } catch (err) {
309
+ threw = true;
310
+ caught = err;
311
+ } finally {
312
+ restoreSeams();
313
+ restorePhase();
314
+ }
315
+ const violation = recorder.violation();
316
+ if (violation !== undefined) {
317
+ throw violation;
318
+ }
319
+ const intent = recorder.captured();
320
+ if (intent !== undefined) {
321
+ if (threw && !(caught instanceof PhaseHaltSentinel)) {
322
+ throw caught;
323
+ }
324
+ return {halted: true, intent, sentinelSwallowed: !threw};
325
+ }
326
+ if (threw) {
327
+ throw caught;
328
+ }
329
+ return {halted: false, result: result as T};
330
+ }
331
+
332
+ /**
333
+ * Enter `phase` for the whole (one-shot) CLI process, the `--phase` entry
334
+ * point: the phase guards fire from here on, and in the `agent` phase every
335
+ * process-wide write seam records into a recorder that knows NO write, so any
336
+ * write that is not inside a per-path {@link runAgentPhase} (which installs its
337
+ * own recorder on top) throws {@link UnrecordedWriteError}. Returns a function
338
+ * restoring the previous state (tests; the CLI process simply exits).
339
+ */
340
+ export function activateProcessPhase(phase: Phase): () => void {
341
+ const restorePhase = enterPhase(phase);
342
+ const restoreSeams =
343
+ phase === 'agent'
344
+ ? installRecordingSeams(createPhaseRecorder({record: []}))
345
+ : () => {};
346
+ return () => {
347
+ restoreSeams();
348
+ restorePhase();
349
+ };
350
+ }
package/src/phase.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The **CI phase mode** (spec `ci-agent-job-without-write-token`, ADR
3
+ * `ci-agent-job-holds-no-write-token`, task `ci-split-phase-mode-and-guards`).
4
+ *
5
+ * In GitHub Actions every CI item runs as three jobs, and the SAME dorfl verb
6
+ * (`intake`, `advance`; `do` for parity) runs once per job with a hidden
7
+ * `--phase`:
8
+ *
9
+ * - **`lock`**: the write token, NO agent, NO repository code. Takes the lock
10
+ * (and the other pre-agent writes) and emits trusted facts.
11
+ * - **`agent`**: a read-only token (`persist-credentials: false`). Runs the
12
+ * agents, the gate and every local commit, and RECORDS the first write
13
+ * instead of performing it (see `phase-recorder.ts`).
14
+ * - **`apply`**: the write token, NO agent, NO repository code. Performs the
15
+ * recorded writes from the agent job's handoff, treated as hostile.
16
+ *
17
+ * Absent `--phase` is today's single process, byte for byte: the active phase
18
+ * is `undefined` and every guard below is a no-op.
19
+ *
20
+ * The active phase is PROCESS-GLOBAL on purpose: a phase run is a one-shot CI
21
+ * process, and the chokepoints it must guard (`agentLaunchEnv`, the harness
22
+ * adapters, `runVerify`, `runPrepare`, the write seams) are reached from deep
23
+ * inside pipelines that do not carry a context. The same value is ALSO threaded
24
+ * through the run context (`AdvanceContext.phase`, `DoOptions.phase`,
25
+ * `PerformIntakeOptions.phase`) so the pipelines can branch on it; the global
26
+ * is what the guards trust.
27
+ */
28
+
29
+ /** One of the three CI jobs a dorfl verb runs as (see the module comment). */
30
+ export type Phase = 'lock' | 'agent' | 'apply';
31
+
32
+ /** Every {@link Phase}, in pipeline order. */
33
+ export const PHASES: readonly Phase[] = ['lock', 'agent', 'apply'];
34
+
35
+ /**
36
+ * A `--phase` value that is not allowed: an unknown phase name, or any phase
37
+ * outside GitHub Actions. A usage error, raised before anything runs.
38
+ */
39
+ export class PhaseUsageError extends Error {
40
+ override readonly name = 'PhaseUsageError';
41
+ }
42
+
43
+ /**
44
+ * Parse and admit a `--phase` value. It must be one of {@link PHASES}, and it is
45
+ * only allowed when `GITHUB_ACTIONS=true` in `env`, so a phase cannot change
46
+ * laptop behaviour by accident. `allowOutsideActions` is the explicit TEST
47
+ * override (never wired to a flag, an env var or config).
48
+ */
49
+ export function parsePhase(
50
+ raw: string,
51
+ options: {env?: NodeJS.ProcessEnv; allowOutsideActions?: boolean} = {},
52
+ ): Phase {
53
+ const value = raw.trim();
54
+ if (!(PHASES as readonly string[]).includes(value)) {
55
+ throw new PhaseUsageError(
56
+ `--phase must be one of ${PHASES.join(', ')} (got '${raw}').`,
57
+ );
58
+ }
59
+ const env = options.env ?? process.env;
60
+ if (env.GITHUB_ACTIONS !== 'true' && options.allowOutsideActions !== true) {
61
+ throw new PhaseUsageError(
62
+ '--phase is CI-only: it splits one item into the lock, agent and apply ' +
63
+ 'jobs of a GitHub Actions workflow and requires GITHUB_ACTIONS=true. ' +
64
+ 'Run the verb without --phase for the normal single-process behaviour.',
65
+ );
66
+ }
67
+ return value as Phase;
68
+ }
69
+
70
+ let current: Phase | undefined;
71
+
72
+ /** The phase this process runs as, or `undefined` (no `--phase`: today's behaviour). */
73
+ export function activePhase(): Phase | undefined {
74
+ return current;
75
+ }
76
+
77
+ /**
78
+ * Make `phase` the active phase and return a function restoring the previous
79
+ * one. The CLI enters the phase once for the whole (one-shot) process; the
80
+ * phase drivers and tests scope it with the returned restore.
81
+ */
82
+ export function enterPhase(phase: Phase | undefined): () => void {
83
+ const previous = current;
84
+ current = phase;
85
+ return () => {
86
+ current = previous;
87
+ };
88
+ }
89
+
90
+ /** What a {@link PhaseGuardError} refused to do. */
91
+ export type GuardedOperation =
92
+ | 'agent-launch-env'
93
+ | 'harness-launch'
94
+ | 'verify'
95
+ | 'prepare';
96
+
97
+ /**
98
+ * A phase guard fired: dorfl tried to launch an agent or run repository code in
99
+ * the `lock` or `apply` phase, whose job holds the write token. Always a dorfl
100
+ * BUG (the pipeline split is wrong), never a user error; defence in depth that
101
+ * holds whatever the workflow shape.
102
+ */
103
+ export class PhaseGuardError extends Error {
104
+ override readonly name = 'PhaseGuardError';
105
+ constructor(
106
+ readonly phase: Phase,
107
+ readonly operation: GuardedOperation,
108
+ ) {
109
+ super(
110
+ `phase guard: '${operation}' is not allowed in the ${phase} phase ` +
111
+ '(the lock and apply jobs hold the write token, so they never launch ' +
112
+ 'an agent or run repository code). This is a dorfl bug in the phase split.',
113
+ );
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Throw a {@link PhaseGuardError} when the active phase is `lock` or `apply`.
119
+ * Called at every chokepoint that launches an agent or runs repository code:
120
+ * `agentLaunchEnv`, each harness adapter's launch, `runVerify` and
121
+ * `runPrepare`. A no-op without a phase and in the `agent` phase.
122
+ */
123
+ export function assertAgentOrRepoCodeAllowed(
124
+ operation: GuardedOperation,
125
+ ): void {
126
+ const phase = current;
127
+ if (phase === 'lock' || phase === 'apply') {
128
+ throw new PhaseGuardError(phase, operation);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Every CLI verb that can launch an agent (autonomous or interactive) or run
134
+ * repository code (`prepare` / `verify`). A CI job that runs one of these must
135
+ * be the agent job (read-only token, no persisted credential) or pass
136
+ * `--phase lock|apply`. The workflow guard (task `ci-split-generate-workflows`)
137
+ * and the single-job warning (task `ci-split-warn-single-job-workflows`) read
138
+ * this set; `phase-cli.test.ts` fails when a CLI verb is in neither this set
139
+ * nor {@link NON_AGENT_VERBS}.
140
+ */
141
+ export const AGENT_SPAWNING_VERBS: ReadonlySet<string> = new Set([
142
+ 'run',
143
+ 'verify',
144
+ 'start',
145
+ 'resume',
146
+ 'work-on',
147
+ 'complete',
148
+ 'do',
149
+ 'advance',
150
+ 'intake',
151
+ ]);
152
+
153
+ /**
154
+ * Every CLI verb that neither launches an agent nor runs repository code (it
155
+ * reads, or writes refs / the ledger / provider state only). The explicit
156
+ * complement of {@link AGENT_SPAWNING_VERBS}, so a new verb must be classified.
157
+ */
158
+ export const NON_AGENT_VERBS: ReadonlySet<string> = new Set([
159
+ 'config',
160
+ 'scan',
161
+ 'claim',
162
+ 'prompt',
163
+ 'gc',
164
+ 'sync',
165
+ 'prd-to-spec',
166
+ 'status',
167
+ 'requeue',
168
+ 'promote',
169
+ 'release-lock',
170
+ 'migrate-stuck-locks',
171
+ 'drop',
172
+ 'close-merged-issues',
173
+ 'surface-merge-questions',
174
+ 'remote',
175
+ 'install-ci',
176
+ 'skills',
177
+ ]);
package/src/pi-harness.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import {spawn, spawnSync} from 'node:child_process';
2
2
  import {existsSync, readFileSync, writeSync} from 'node:fs';
3
+ import {assertAgentOrRepoCodeAllowed} from './phase.js';
3
4
  import {
4
5
  NullHarness,
6
+ agentLaunchEnv,
5
7
  pidAlive,
6
8
  registerHarness,
9
+ type CutOffTurn,
7
10
  type Harness,
8
11
  type HarnessRecord,
9
12
  type InteractiveLaunchInput,
@@ -12,7 +15,11 @@ import {
12
15
  type LaunchResult,
13
16
  } from './harness.js';
14
17
  import {generateSessionPath} from './session-path.js';
15
- import {lastAssistantTurn, isOutputCappedTurn} from './watch-session.js';
18
+ import {
19
+ lastAssistantTurn,
20
+ isOutputCappedTurn,
21
+ cutOffTurnOf,
22
+ } from './watch-session.js';
16
23
  import {reapProcessGroup} from './reap-agent-tree.js';
17
24
  import type {HarnessAdapter} from './config.js';
18
25
 
@@ -246,6 +253,8 @@ export class PiHarness implements Harness {
246
253
  }
247
254
 
248
255
  launch(input: LaunchInput): LaunchResult {
256
+ // PHASE GUARD first: no launch at all in the CI lock/apply phases.
257
+ assertAgentOrRepoCodeAllowed('harness-launch');
249
258
  const sessionFile = this.resolveSessionFile(input);
250
259
  const args = this.buildArgs(input, sessionFile);
251
260
  const result = spawnSync(this.piBin, args, {
@@ -255,7 +264,8 @@ export class PiHarness implements Harness {
255
264
  cwd: input.dir,
256
265
  encoding: 'utf8',
257
266
  input: input.prompt,
258
- env: input.env ?? process.env,
267
+ // No GitHub token reaches a CI agent (see agentLaunchEnv).
268
+ env: agentLaunchEnv(input.env),
259
269
  maxBuffer: 64 * 1024 * 1024,
260
270
  });
261
271
  if (result.error) {
@@ -280,7 +290,7 @@ export class PiHarness implements Harness {
280
290
  // The agent's ANSWER (task `harness-agent-output`): the LAST assistant
281
291
  // turn's text read from the session `.jsonl` pi just wrote — NOT piped
282
292
  // stdout (which is drained). Shares `watch-session.ts`'s reader. The
283
- // same turn's stop_reason/usage feed the outputCapped cap-truncation
293
+ // same turn's stop reason/usage feed the outputCapped cap-truncation
284
294
  // signal (observation `tasker-review-edits-payload-caps-the-verdict-response`).
285
295
  ...readAssistantOutput(sessionFile),
286
296
  };
@@ -317,6 +327,8 @@ export class PiHarness implements Harness {
317
327
  * belt-and-suspenders backstop bounding any such residual leak.
318
328
  */
319
329
  launchAsync(input: LaunchInput): Promise<LaunchResult> {
330
+ // PHASE GUARD first: no launch at all in the CI lock/apply phases.
331
+ assertAgentOrRepoCodeAllowed('harness-launch');
320
332
  const sessionFile = this.resolveSessionFile(input);
321
333
  const args = this.buildArgs(input, sessionFile);
322
334
  const record: PiHarnessRecord = {
@@ -333,7 +345,8 @@ export class PiHarness implements Harness {
333
345
  // Same as `launch`: spawn in the repo/worktree dir so the session
334
346
  // header `cwd` groups the dashboard correctly (invariant #3).
335
347
  cwd: input.dir,
336
- env: input.env ?? process.env,
348
+ // No GitHub token reaches a CI agent (see agentLaunchEnv).
349
+ env: agentLaunchEnv(input.env),
337
350
  stdio: ['pipe', 'pipe', 'pipe'],
338
351
  // PROCESS-GROUP LEADER (observation
339
352
  // `checkpoint-releases-lock-while-predecessor-agent-still-writes`): pi's
@@ -483,7 +496,7 @@ export class PiHarness implements Harness {
483
496
  // Read the agent's ANSWER from the `.jsonl` at `exit` — the same
484
497
  // last-assistant-turn read `launch` does at return (task
485
498
  // `harness-agent-output`); the process has exited so the log is final.
486
- // The same turn's stop_reason/usage feed the outputCapped signal.
499
+ // The same turn's stop reason/usage feed the outputCapped signal.
487
500
  ...readAssistantOutput(sessionFile),
488
501
  });
489
502
  };
@@ -558,6 +571,8 @@ export class PiHarness implements Harness {
558
571
  * nothing to capture, so it returns only the exit code.
559
572
  */
560
573
  launchInteractive(input: InteractiveLaunchInput): InteractiveLaunchResult {
574
+ // PHASE GUARD first: no launch at all in the CI lock/apply phases.
575
+ assertAgentOrRepoCodeAllowed('harness-launch');
561
576
  const sessionFile = this.resolveSessionFile({
562
577
  dir: input.dir,
563
578
  slug: input.slug,
@@ -653,6 +668,7 @@ export function piSessionExists(record: HarnessRecord): boolean {
653
668
  function readAssistantOutput(sessionFile: string): {
654
669
  output?: string;
655
670
  outputCapped?: number;
671
+ cutOffTurn?: CutOffTurn;
656
672
  } {
657
673
  let jsonl: string;
658
674
  try {
@@ -664,6 +680,10 @@ function readAssistantOutput(sessionFile: string): {
664
680
  return {
665
681
  output: turn.text,
666
682
  outputCapped: isOutputCappedTurn(turn) ? turn.outputTokens : undefined,
683
+ // How the run's FINAL turn ended (task
684
+ // `a-truncated-agent-turn-routes-as-agent-failed`): a `length`/`error` stop
685
+ // makes an empty diff an agent failure, not a deliberate empty-diff STOP.
686
+ cutOffTurn: cutOffTurnOf(turn),
667
687
  };
668
688
  }
669
689
 
package/src/prepare.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {spawn} from 'node:child_process';
2
+ import {assertAgentOrRepoCodeAllowed} from './phase.js';
2
3
  import {existsSync, mkdirSync, writeFileSync} from 'node:fs';
3
4
  import {dirname, join} from 'node:path';
4
5
  import {run} from './git.js';
@@ -93,6 +94,9 @@ export interface RunPrepareResult {
93
94
  export async function runPrepare(
94
95
  options: RunPrepareOptions,
95
96
  ): Promise<RunPrepareResult> {
97
+ // PHASE GUARD: the gate runs repository code, never in the CI lock/apply
98
+ // phases (their jobs hold the write token; see `phase.ts`).
99
+ assertAgentOrRepoCodeAllowed('prepare');
96
100
  const commands = resolvePrepareCommands(options.prepare);
97
101
  if (commands.length === 0) {
98
102
  return {exitCode: 0, commands, passed: true, noop: true};