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,861 @@
1
+ /**
2
+ * **The intake path split into the three CI phases** (spec
3
+ * `ci-agent-job-without-write-token` §3 intake row, ADR
4
+ * `ci-agent-job-holds-no-write-token`, task `ci-split-intake`).
5
+ *
6
+ * `intake <N> --phase` runs ONE of three halves of the one intake pipeline
7
+ * (`intake.ts`: {@link decideIntake} is the agent half, {@link applyIntakeDecision}
8
+ * / {@link emitIntakeDocument} the write half), never a second implementation.
9
+ * The item is `issue:<N>`; its lock is the `processing` LABEL, not a lock ref.
10
+ *
11
+ * - **lock** ({@link performIntakeLockPhase}): fetch `<arbiter>/main` (its tip
12
+ * is `baseSha`); read the issue, its comments and its labels; take the
13
+ * `processing` label (the provider creates it on a fresh repository); run
14
+ * the deterministic triage (a skip needs no agent: `needsAgent: false`);
15
+ * derive the origin trust from the event (`comment.author_association`, else
16
+ * `issue.author_association`, as `intake.yml` does) and the document mode
17
+ * (`intakeIntegration ?? integration` from the config at `baseSha`); publish
18
+ * them with `seenCommentIds`, every comment id it read. No agent, no
19
+ * repository code.
20
+ * - **agent** ({@link performIntakeAgentPhase}): read the issue with the read
21
+ * token, but give the decision agent ONLY the comments whose ids are in
22
+ * `seenCommentIds` (a comment posted after the lock is neither read nor
23
+ * marked seen); run the decision agent and the lone-task review rounds; hand
24
+ * over the verdict (`intake-ask`, `intake-bounce`, `intake-task`,
25
+ * `intake-spec`) carrying only the record fields the intent table allows. Any
26
+ * failure writes no handoff and fails the job.
27
+ * - **apply** ({@link performIntakeApplyPhase}): refuse to write when the label
28
+ * is gone (a re-run of a finished run); act on the agent job's result first
29
+ * (anything but `success` never reads the handoff: remove the label, post
30
+ * nothing); validate the handoff as hostile; for a document, RECOMPUTE the
31
+ * slug (`slug-safety.ts`), the placement, the origin-trust stamp and the mode
32
+ * from trusted inputs, render the document with the title YAML-quoted and
33
+ * re-parse it to prove the stamp survived, then integrate
34
+ * (`performIntegration`, PR or merge with the CAS loop) and post the
35
+ * completion comment. The marker's `seen=` is `seenCommentIds`. The label is
36
+ * ALWAYS removed.
37
+ */
38
+
39
+ import {existsSync, readFileSync} from 'node:fs';
40
+ import {paramCase} from './brand.js';
41
+ import {
42
+ HANDOFF_LIMITS,
43
+ HandoffRejected,
44
+ handoffName as deriveHandoffName,
45
+ type HandoffIntentKind,
46
+ type HandoffRecord,
47
+ type IntakeAskProducts,
48
+ type IntakeBounceProducts,
49
+ type IntakeSpecProducts,
50
+ type IntakeTaskProducts,
51
+ } from './ci-handoff-format.js';
52
+ import {readHandoff, writeHandoff} from './ci-handoff.js';
53
+ import {
54
+ LockOutputRefused,
55
+ readLockOutputsFromEnv,
56
+ serializeLockOutputs,
57
+ type LockOutputs,
58
+ } from './ci-lock-outputs.js';
59
+ import type {AgentJobResult} from './ci-agent-result.js';
60
+ import {
61
+ PhaseDriverError,
62
+ agentTimeoutMinutesAt,
63
+ boundHandoffText,
64
+ emitLockOutputs,
65
+ fetchArbiterMain,
66
+ noSmudgeEnv,
67
+ repoConfigAt,
68
+ runnerTempFrom,
69
+ } from './ci-phase-driver.js';
70
+ import type {IntegrationMode} from './config.js';
71
+ import {singleLineScalarProblem, type OriginTrust} from './frontmatter.js';
72
+ import {identityEnv} from './identity.js';
73
+ import {
74
+ applyIntakeDecision,
75
+ decideIntake,
76
+ emitIntakeDocument,
77
+ intakePlacementInputs,
78
+ intakeSkipMessage,
79
+ intakeTransportProblem,
80
+ renderIntakeDocument,
81
+ type IntakeDecision,
82
+ type IntakeDocumentDecision,
83
+ type IntakeResult,
84
+ type IntakeWriteContext,
85
+ type PerformIntakeOptions,
86
+ } from './intake.js';
87
+ import {isAuthorTrusted} from './intake-trigger-template.js';
88
+ import {triageIntake} from './intake-triage.js';
89
+ import {
90
+ GitHubIssueProvider,
91
+ PROCESSING_LOCK_LABEL,
92
+ type Issue,
93
+ type IssueComment,
94
+ type IssueProvider,
95
+ } from './issue-provider.js';
96
+ import type {Phase} from './phase.js';
97
+ import {ensureSafeSlug, isSafeSlug} from './slug-safety.js';
98
+
99
+ const DEFAULT_ARBITER = 'origin';
100
+
101
+ /** Options of an intake phase run: the `intake` options plus the phase inputs. */
102
+ export interface IntakePhaseOptions extends PerformIntakeOptions {
103
+ /** Which of the three jobs this process is. */
104
+ phase: Phase;
105
+ /**
106
+ * The handoff directory: the agent phase writes the artifact here (empty or
107
+ * absent); the apply phase reads it (under {@link runnerTemp}).
108
+ */
109
+ handoffDir?: string;
110
+ /** `$RUNNER_TEMP` (the apply phase); defaults to the env's. */
111
+ runnerTemp?: string;
112
+ /** The lock job's outputs (agent / apply); defaults to `DORFL_LOCK_OUTPUTS`. */
113
+ lockOutputs?: LockOutputs;
114
+ /** `github.run_attempt` (names the artifact); default the env's, else 1. */
115
+ runAttempt?: string;
116
+ /** The `$GITHUB_OUTPUT` file the lock phase appends to; default the env's. */
117
+ githubOutput?: string;
118
+ /**
119
+ * lock: the event payload file (`$GITHUB_EVENT_PATH`) the origin trust is
120
+ * read from; default the env's. A called workflow sees its caller's event.
121
+ */
122
+ eventPath?: string;
123
+ /**
124
+ * apply: `needs.agent.result` (`--agent-result`), REQUIRED. Only `success`
125
+ * reads the handoff.
126
+ */
127
+ agentResult?: AgentJobResult;
128
+ /** The merge-mode CAS-loop jitter (tests pass 0). */
129
+ mergeJitterMs?: number;
130
+ }
131
+
132
+ /** How an intake phase run ended. */
133
+ export type IntakePhaseOutcome =
134
+ /** lock: the label is taken and the agent runs next. */
135
+ | 'locked'
136
+ /** lock: the triage skipped (label taken, `needsAgent: false`). */
137
+ | 'no-new-input'
138
+ | 'already-terminal'
139
+ /** lock: another run holds the label; nothing was written. */
140
+ | 'backed-off'
141
+ /** lock: the label could not be read or taken. */
142
+ | 'lock-failed'
143
+ /** agent: the handoff was written (see `intent`). */
144
+ | 'handed-over'
145
+ /** agent / apply: nothing to do (the lock job's triage skipped). */
146
+ | 'no-op'
147
+ /** apply: the label is gone (a finished run re-run); nothing was written. */
148
+ | 'stale-lock'
149
+ /** apply: the agent job did not succeed; the label was removed, nothing posted. */
150
+ | 'released'
151
+ /** apply: the handoff broke a rule; the label was removed, nothing posted. */
152
+ | 'rejected'
153
+ /** apply: the write half's outcomes (as the laptop `intake`). */
154
+ | 'asked'
155
+ | 'bounced'
156
+ | 'tasked'
157
+ | 'spec-written'
158
+ | 'stale'
159
+ /** agent: the decision or the review failed (no handoff; the job fails). */
160
+ | 'agent-failed'
161
+ /** A usage or environment problem. */
162
+ | 'usage-error';
163
+
164
+ /** The result of one intake phase run. */
165
+ export interface IntakePhaseResult {
166
+ /** See "A handled outcome is green" in `ci-phase-driver.ts`. */
167
+ exitCode: 0 | 1 | 4;
168
+ outcome: IntakePhaseOutcome;
169
+ issueNumber: number;
170
+ /**
171
+ * The result line: the CLI prints it (`>> ` or `error: `), so the phase does
172
+ * NOT also `note` it (each line appears once in the job log).
173
+ */
174
+ message: string;
175
+ /** lock: the facts published. */
176
+ lockOutputs?: LockOutputs;
177
+ /** agent: the handed-over intent kind. */
178
+ intent?: HandoffIntentKind;
179
+ /** apply: the emitted document (task / spec). */
180
+ emitted?: string;
181
+ emittedSlug?: string;
182
+ }
183
+
184
+ /**
185
+ * Run one phase of the intake path. The CLI calls this for `intake <N> --phase`.
186
+ */
187
+ export async function performIntakePhase(
188
+ options: IntakePhaseOptions,
189
+ ): Promise<IntakePhaseResult> {
190
+ try {
191
+ switch (options.phase) {
192
+ case 'lock':
193
+ return await performIntakeLockPhase(options);
194
+ case 'agent':
195
+ return await performIntakeAgentPhase(options);
196
+ case 'apply':
197
+ return await performIntakeApplyPhase(options);
198
+ }
199
+ } catch (err) {
200
+ if (err instanceof PhaseDriverError || err instanceof LockOutputRefused) {
201
+ return {
202
+ exitCode: 1,
203
+ outcome: 'usage-error',
204
+ issueNumber: options.issueNumber,
205
+ message: err.message,
206
+ };
207
+ }
208
+ throw err;
209
+ }
210
+ }
211
+
212
+ function itemOf(issueNumber: number): string {
213
+ return `issue:${issueNumber}`;
214
+ }
215
+
216
+ function manualRecovery(issueNumber: number): string {
217
+ return (
218
+ `If the \`${PROCESSING_LOCK_LABEL}\` label is left behind, release it with: ` +
219
+ `gh issue edit ${issueNumber} --remove-label '${PROCESSING_LOCK_LABEL}'`
220
+ );
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // lock
225
+ // ---------------------------------------------------------------------------
226
+
227
+ /**
228
+ * The origin trust from the event payload at `eventPath`: the COMMENT's
229
+ * `author_association` on an `issue_comment` event, else the ISSUE's (the rule
230
+ * `intake.yml` applies), classified by {@link isAuthorTrusted}. A payload that
231
+ * is missing, unreadable, or about another issue gives `untrusted` (the
232
+ * fail-safe side of a public front door).
233
+ */
234
+ export function originTrustFromEvent(
235
+ eventPath: string | undefined,
236
+ issueNumber: number,
237
+ ): OriginTrust {
238
+ if (eventPath === undefined || eventPath === '' || !existsSync(eventPath)) {
239
+ return 'untrusted';
240
+ }
241
+ let event: unknown;
242
+ try {
243
+ event = JSON.parse(readFileSync(eventPath, 'utf8'));
244
+ } catch {
245
+ return 'untrusted';
246
+ }
247
+ const e = (event ?? {}) as {
248
+ issue?: {number?: unknown; author_association?: unknown};
249
+ comment?: {author_association?: unknown};
250
+ };
251
+ if (e.issue?.number !== issueNumber) {
252
+ return 'untrusted';
253
+ }
254
+ const str = (v: unknown): string | undefined =>
255
+ typeof v === 'string' && v !== '' ? v : undefined;
256
+ const association =
257
+ str(e.comment?.author_association) ?? str(e.issue?.author_association);
258
+ return isAuthorTrusted(association) ? 'trusted' : 'untrusted';
259
+ }
260
+
261
+ /**
262
+ * The intake document mode from the repository config AT `baseSha`:
263
+ * `intakeIntegration ?? integration`, else `propose` (the conservative
264
+ * default). Never read from the agent job.
265
+ */
266
+ export function intakeDocumentModeAt(
267
+ cwd: string,
268
+ baseSha: string,
269
+ env: NodeJS.ProcessEnv | undefined,
270
+ ): IntegrationMode {
271
+ const config = repoConfigAt(cwd, baseSha, env);
272
+ const mode = config?.intakeIntegration ?? config?.integration;
273
+ return mode === 'merge' || mode === 'propose' ? mode : 'propose';
274
+ }
275
+
276
+ /**
277
+ * The lock phase: read the issue, its thread and its labels, take the
278
+ * `processing` label, run the deterministic triage and publish the trusted
279
+ * facts. Runs no agent and no repository code. Every fact is checked BEFORE the
280
+ * label is taken, so a refused output never leaves the label behind.
281
+ */
282
+ export async function performIntakeLockPhase(
283
+ options: IntakePhaseOptions,
284
+ ): Promise<IntakePhaseResult> {
285
+ const note = options.note ?? (() => {});
286
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
287
+ const cwd = options.cwd;
288
+ const issueNumber = options.issueNumber;
289
+ const item = itemOf(issueNumber);
290
+ const env = identityEnv(options.identity, options.env ?? process.env);
291
+ const githubOutput = options.githubOutput ?? env.GITHUB_OUTPUT;
292
+ const publish = (facts: LockOutputs): LockOutputs => {
293
+ emitLockOutputs(facts, {githubOutput, note});
294
+ return facts;
295
+ };
296
+ const done = (
297
+ exitCode: 0 | 1,
298
+ outcome: IntakePhaseOutcome,
299
+ message: string,
300
+ facts: LockOutputs,
301
+ ): IntakePhaseResult => {
302
+ return {
303
+ exitCode,
304
+ outcome,
305
+ issueNumber,
306
+ message,
307
+ lockOutputs: publish(facts),
308
+ };
309
+ };
310
+ const issueProvider = options.issueProvider ?? new GitHubIssueProvider();
311
+
312
+ const baseSha = await fetchArbiterMain({cwd, arbiter, env});
313
+ let comments: IssueComment[];
314
+ try {
315
+ await issueProvider.getIssue({cwd, issueNumber, env});
316
+ comments = await issueProvider.listComments({cwd, issueNumber, env});
317
+ } catch (err) {
318
+ const detail = err instanceof Error ? err.message : String(err);
319
+ return done(
320
+ 1,
321
+ 'usage-error',
322
+ `Could not read issue #${issueNumber}: ${detail}`,
323
+ {acquired: false, baseSha},
324
+ );
325
+ }
326
+
327
+ const labels = await issueProvider.getLabels({cwd, issueNumber, env});
328
+ if (labels.outcome === 'failed') {
329
+ return done(
330
+ 1,
331
+ 'lock-failed',
332
+ `Intake of issue #${issueNumber} could not acquire the ` +
333
+ `\`${PROCESSING_LOCK_LABEL}\` lock: ${labels.instruction}`,
334
+ {acquired: false, rung: 'intake', baseSha},
335
+ );
336
+ }
337
+ if (
338
+ labels.outcome === 'ok' &&
339
+ labels.labels.includes(PROCESSING_LOCK_LABEL)
340
+ ) {
341
+ return done(
342
+ 0,
343
+ 'backed-off',
344
+ `Intake of issue #${issueNumber} backed off: the \`${PROCESSING_LOCK_LABEL}\` ` +
345
+ `lock is already held by a concurrent run; doing nothing.`,
346
+ {acquired: false, rung: 'intake', baseSha},
347
+ );
348
+ }
349
+
350
+ const triage = triageIntake(comments);
351
+ const ids = [
352
+ ...new Set(
353
+ comments
354
+ .map((c) => c.id)
355
+ .filter((id): id is string => id !== undefined && id !== ''),
356
+ ),
357
+ ];
358
+ const facts: LockOutputs = {
359
+ acquired: true,
360
+ needsAgent: triage.action === 'proceed',
361
+ rung: 'intake',
362
+ baseSha,
363
+ handoffName: deriveHandoffName(
364
+ item,
365
+ options.runAttempt ?? env.GITHUB_RUN_ATTEMPT ?? '1',
366
+ ),
367
+ agentTimeoutMinutes: agentTimeoutMinutesAt(cwd, baseSha, env),
368
+ // An explicit `--origin-trust` (the workflow's own input) wins; else the
369
+ // event payload decides.
370
+ originTrust:
371
+ options.originTrust ??
372
+ originTrustFromEvent(
373
+ options.eventPath ?? env.GITHUB_EVENT_PATH,
374
+ issueNumber,
375
+ ),
376
+ documentMode: intakeDocumentModeAt(cwd, baseSha, env),
377
+ ...(ids.length > 0 ? {seenCommentIds: ids} : {}),
378
+ };
379
+ try {
380
+ serializeLockOutputs(facts);
381
+ } catch (err) {
382
+ if (!(err instanceof LockOutputRefused)) throw err;
383
+ return done(1, 'usage-error', err.message, {
384
+ acquired: false,
385
+ rung: 'intake',
386
+ baseSha,
387
+ });
388
+ }
389
+
390
+ if (labels.outcome === 'ok') {
391
+ const acquired = await issueProvider.addLabel({
392
+ cwd,
393
+ issueNumber,
394
+ label: PROCESSING_LOCK_LABEL,
395
+ env,
396
+ });
397
+ if (acquired.outcome === 'failed') {
398
+ return done(
399
+ 1,
400
+ 'lock-failed',
401
+ `Intake of issue #${issueNumber} could not acquire the ` +
402
+ `\`${PROCESSING_LOCK_LABEL}\` lock: ${acquired.instruction}`,
403
+ {acquired: false, rung: 'intake', baseSha},
404
+ );
405
+ }
406
+ } else {
407
+ note(`Processing lock degraded: ${labels.instruction}`);
408
+ }
409
+
410
+ if (triage.action === 'skip') {
411
+ return done(
412
+ 0,
413
+ triage.outcome,
414
+ `${intakeSkipMessage(issueNumber, triage.outcome)} The apply job only ` +
415
+ 'removes the label.',
416
+ facts,
417
+ );
418
+ }
419
+ return done(
420
+ 0,
421
+ 'locked',
422
+ `locked ${item} at ${baseSha} (the \`${PROCESSING_LOCK_LABEL}\` label; ` +
423
+ `${ids.length} comment(s) read)`,
424
+ facts,
425
+ );
426
+ }
427
+
428
+ // ---------------------------------------------------------------------------
429
+ // The trusted lock facts (agent / apply)
430
+ // ---------------------------------------------------------------------------
431
+
432
+ /** The lock outputs an intake agent or apply phase acts on. */
433
+ interface IntakeLock {
434
+ baseSha: string;
435
+ needsAgent?: boolean;
436
+ originTrust?: OriginTrust;
437
+ documentMode?: IntegrationMode;
438
+ /** The comment ids the lock job read, as tokens. */
439
+ seenCommentIds: string[];
440
+ }
441
+
442
+ function requireIntakeLock(
443
+ options: IntakePhaseOptions,
444
+ env: NodeJS.ProcessEnv,
445
+ ): IntakeLock {
446
+ const facts = options.lockOutputs ?? readLockOutputsFromEnv(env);
447
+ if (facts.acquired !== true) {
448
+ throw new PhaseDriverError(
449
+ 'the lock job did not acquire the issue (acquired != true); nothing to do',
450
+ );
451
+ }
452
+ if (facts.rung !== 'intake') {
453
+ throw new PhaseDriverError(
454
+ `the lock job classified the rung ${String(facts.rung)}, not intake`,
455
+ );
456
+ }
457
+ if (facts.baseSha === undefined) {
458
+ throw new PhaseDriverError('the lock outputs carry no baseSha');
459
+ }
460
+ return {
461
+ baseSha: facts.baseSha,
462
+ needsAgent: facts.needsAgent,
463
+ originTrust: facts.originTrust,
464
+ documentMode: facts.documentMode,
465
+ seenCommentIds: (facts.seenCommentIds ?? []).map(String),
466
+ };
467
+ }
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // agent
471
+ // ---------------------------------------------------------------------------
472
+
473
+ /**
474
+ * The handoff record of a decision: only the fields the intent table allows,
475
+ * each bounded to its limit. A task / spec carries its title and body (and a
476
+ * spec its two gate booleans), never a slug, a path, a stamp or a mode.
477
+ */
478
+ export function intakeHandoffRecord(
479
+ issueNumber: number,
480
+ decision: IntakeDecision,
481
+ ): HandoffRecord {
482
+ const item = itemOf(issueNumber);
483
+ const L = HANDOFF_LIMITS;
484
+ switch (decision.kind) {
485
+ case 'ask':
486
+ return {
487
+ schema: 1,
488
+ item,
489
+ intent: {kind: 'intake-ask'},
490
+ products: {
491
+ question: boundHandoffText(decision.body, L.reasonChars),
492
+ } satisfies IntakeAskProducts,
493
+ };
494
+ case 'bounce':
495
+ return {
496
+ schema: 1,
497
+ item,
498
+ intent: {kind: 'intake-bounce'},
499
+ products: {
500
+ bounceText: boundHandoffText(decision.body, L.commentChars),
501
+ } satisfies IntakeBounceProducts,
502
+ };
503
+ case 'task':
504
+ return {
505
+ schema: 1,
506
+ item,
507
+ intent: {kind: 'intake-task'},
508
+ products: {
509
+ title: boundTitle(decision.title),
510
+ body: boundHandoffText(decision.body ?? '', L.documentChars),
511
+ } satisfies IntakeTaskProducts,
512
+ };
513
+ case 'spec':
514
+ return {
515
+ schema: 1,
516
+ item,
517
+ intent: {kind: 'intake-spec'},
518
+ products: {
519
+ title: boundTitle(decision.title),
520
+ body: boundHandoffText(decision.body ?? '', L.documentChars),
521
+ ...(decision.humanOnly === undefined
522
+ ? {}
523
+ : {humanOnly: decision.humanOnly}),
524
+ ...(decision.needsAnswers === undefined
525
+ ? {}
526
+ : {needsAnswers: decision.needsAnswers}),
527
+ } satisfies IntakeSpecProducts,
528
+ };
529
+ }
530
+ }
531
+
532
+ /** Cut a (single-line) title to the handoff's document-title limit. */
533
+ function boundTitle(title: string): string {
534
+ return title.slice(0, HANDOFF_LIMITS.documentTitleChars).trim();
535
+ }
536
+
537
+ /**
538
+ * The agent phase: read the issue (read token) and ONLY the comments the lock
539
+ * job read, run the decision agent and the lone-task review, and write the
540
+ * handoff. Writes nothing anywhere else. Any failure writes no handoff and
541
+ * fails the job, so the apply job removes the label and posts nothing.
542
+ */
543
+ export async function performIntakeAgentPhase(
544
+ options: IntakePhaseOptions,
545
+ ): Promise<IntakePhaseResult> {
546
+ const note = options.note ?? (() => {});
547
+ const cwd = options.cwd;
548
+ const issueNumber = options.issueNumber;
549
+ const env = options.env ?? process.env;
550
+ const held = requireIntakeLock(options, env);
551
+ if (held.needsAgent === false) {
552
+ const message =
553
+ 'the lock job said no agent is needed (needsAgent: false); nothing to do';
554
+ return {exitCode: 0, outcome: 'no-op', issueNumber, message};
555
+ }
556
+ if (options.handoffDir === undefined) {
557
+ throw new PhaseDriverError('the agent phase needs a handoff directory');
558
+ }
559
+ const issueProvider = options.issueProvider ?? new GitHubIssueProvider();
560
+
561
+ let issue: Issue;
562
+ let thread: IssueComment[];
563
+ try {
564
+ issue = await issueProvider.getIssue({cwd, issueNumber, env});
565
+ thread = await issueProvider.listComments({cwd, issueNumber, env});
566
+ } catch (err) {
567
+ const detail = err instanceof Error ? err.message : String(err);
568
+ const message = `Could not read issue #${issueNumber}: ${detail}`;
569
+ return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
570
+ }
571
+ // ONLY the comments the lock job read: one posted since is neither shown to
572
+ // the agent nor (the apply job marks `seenCommentIds`) marked seen.
573
+ const seen = new Set(held.seenCommentIds);
574
+ const comments = thread.filter((c) => c.id !== undefined && seen.has(c.id));
575
+ if (comments.length < thread.length) {
576
+ note(
577
+ `${thread.length - comments.length} comment(s) the lock job did not read ` +
578
+ '(posted since) are not shown to the decision agent',
579
+ );
580
+ }
581
+ const triage = triageIntake(comments);
582
+
583
+ const decided = await decideIntake(
584
+ {...options, phase: 'agent'},
585
+ cwd,
586
+ issue,
587
+ comments,
588
+ triage.action === 'proceed' ? triage : undefined,
589
+ note,
590
+ );
591
+ if (!decided.ok) {
592
+ return {
593
+ exitCode: 1,
594
+ outcome: decided.result.outcome as IntakePhaseOutcome,
595
+ issueNumber,
596
+ message: decided.result.message,
597
+ };
598
+ }
599
+
600
+ const record = intakeHandoffRecord(issueNumber, decided.decision);
601
+ try {
602
+ writeHandoff({dir: options.handoffDir, rung: 'intake', record});
603
+ } catch (err) {
604
+ if (!(err instanceof HandoffRejected)) throw err;
605
+ return {
606
+ exitCode: 1,
607
+ outcome: 'agent-failed',
608
+ issueNumber,
609
+ message: err.message,
610
+ };
611
+ }
612
+ const kind = record.intent.kind;
613
+ const message = `handed over ${kind} for ${itemOf(issueNumber)}`;
614
+ return {
615
+ exitCode: 0,
616
+ outcome: 'handed-over',
617
+ issueNumber,
618
+ message,
619
+ intent: kind,
620
+ };
621
+ }
622
+
623
+ // ---------------------------------------------------------------------------
624
+ // apply
625
+ // ---------------------------------------------------------------------------
626
+
627
+ /**
628
+ * The document a `intake-task` / `intake-spec` record describes, with the slug
629
+ * RECOMPUTED from its title (`paramCase`, then `slug-safety.ts`). Throws
630
+ * {@link HandoffRejected} for a title that is not one line or holds a control
631
+ * character, or that leaves no safe slug.
632
+ */
633
+ export function intakeDocumentFromRecord(
634
+ record: HandoffRecord,
635
+ ): IntakeDocumentDecision {
636
+ if (
637
+ record.intent.kind !== 'intake-task' &&
638
+ record.intent.kind !== 'intake-spec'
639
+ ) {
640
+ throw new Error(`${record.intent.kind} is not an intake document`);
641
+ }
642
+ const products = record.products as IntakeTaskProducts & IntakeSpecProducts;
643
+ const title = products.title;
644
+ const problem = singleLineScalarProblem(title);
645
+ if (problem !== undefined || title.trim() === '') {
646
+ throw new HandoffRejected(
647
+ 'field',
648
+ `products.title ${problem ?? 'is blank'}`,
649
+ );
650
+ }
651
+ const slug = ensureSafeSlug(paramCase(title));
652
+ if (!isSafeSlug(slug)) {
653
+ throw new HandoffRejected(
654
+ 'field',
655
+ 'products.title leaves no safe slug to derive (never a counter)',
656
+ );
657
+ }
658
+ const body = products.body === '' ? undefined : products.body;
659
+ if (record.intent.kind === 'intake-task') {
660
+ return {kind: 'task', slug, title, body};
661
+ }
662
+ return {
663
+ kind: 'spec',
664
+ slug,
665
+ title,
666
+ body,
667
+ humanOnly: products.humanOnly,
668
+ needsAnswers: products.needsAnswers,
669
+ };
670
+ }
671
+
672
+ /**
673
+ * The apply phase: check the label is still held, act on the agent job's
674
+ * result, validate the handoff as hostile, then resume at the write half with
675
+ * every target, stamp and mode from trusted inputs. Runs no agent and no
676
+ * repository code. The label is ALWAYS removed once the check passed.
677
+ */
678
+ export async function performIntakeApplyPhase(
679
+ options: IntakePhaseOptions,
680
+ ): Promise<IntakePhaseResult> {
681
+ const note = options.note ?? (() => {});
682
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
683
+ const cwd = options.cwd;
684
+ const issueNumber = options.issueNumber;
685
+ const item = itemOf(issueNumber);
686
+ const env = noSmudgeEnv(
687
+ identityEnv(options.identity, options.env ?? process.env),
688
+ );
689
+ const held = requireIntakeLock(options, env);
690
+ if (options.agentResult === undefined) {
691
+ throw new PhaseDriverError(
692
+ 'the apply phase needs the agent job result (--agent-result, ' +
693
+ 'needs.agent.result): only success reads the handoff',
694
+ );
695
+ }
696
+ const agentResult = options.agentResult;
697
+ const issueProvider = options.issueProvider ?? new GitHubIssueProvider();
698
+ const result = (
699
+ exitCode: 0 | 1 | 4,
700
+ outcome: IntakePhaseOutcome,
701
+ message: string,
702
+ ): IntakePhaseResult => {
703
+ return {exitCode, outcome, issueNumber, message};
704
+ };
705
+
706
+ const transport = await intakeTransportProblem(
707
+ options.identity,
708
+ cwd,
709
+ arbiter,
710
+ env,
711
+ );
712
+ if (transport !== undefined) {
713
+ return result(
714
+ 1,
715
+ 'usage-error',
716
+ `${transport} ${manualRecovery(issueNumber)}`,
717
+ );
718
+ }
719
+
720
+ // Label ownership BEFORE the first write: the label is the intake lock. A
721
+ // label that is gone means the run already finished (a "Re-run failed jobs"
722
+ // of it replays the old handoff), so nothing is written.
723
+ const labels = await issueProvider.getLabels({cwd, issueNumber, env});
724
+ if (labels.outcome === 'failed') {
725
+ return result(
726
+ 1,
727
+ 'usage-error',
728
+ `could not read the labels of issue #${issueNumber} (${labels.instruction}); ` +
729
+ `nothing was written. ${manualRecovery(issueNumber)}`,
730
+ );
731
+ }
732
+ if (
733
+ labels.outcome === 'ok' &&
734
+ !labels.labels.includes(PROCESSING_LOCK_LABEL)
735
+ ) {
736
+ // A handled outcome, so green ("A handled outcome is green",
737
+ // `ci-phase-driver.ts`).
738
+ return result(
739
+ 0,
740
+ 'stale-lock',
741
+ `the \`${PROCESSING_LOCK_LABEL}\` label of issue #${issueNumber} is gone ` +
742
+ '(the run already finished, or another run released it); nothing was ' +
743
+ 'written. To retry the issue, start a NEW run.',
744
+ );
745
+ }
746
+
747
+ try {
748
+ if (agentResult !== 'success' || held.needsAgent === false) {
749
+ if (held.needsAgent === false) {
750
+ return result(
751
+ 0,
752
+ 'no-op',
753
+ `the lock job's triage skipped ${item} (needsAgent: false); only the ` +
754
+ 'label is removed',
755
+ );
756
+ }
757
+ return result(
758
+ 1,
759
+ 'released',
760
+ `the agent job ended ${agentResult} (a failure, a timeout or a cancel); ` +
761
+ 'the handoff was not read and nothing was posted; the label is removed',
762
+ );
763
+ }
764
+
765
+ let record: HandoffRecord;
766
+ let doc: IntakeDocumentDecision | undefined;
767
+ let content: string | undefined;
768
+ const originTrust: OriginTrust = held.originTrust ?? 'untrusted';
769
+ try {
770
+ if (options.handoffDir === undefined) {
771
+ throw new HandoffRejected('layout', 'no handoff directory was given');
772
+ }
773
+ record = readHandoff({
774
+ dir: options.handoffDir,
775
+ runnerTemp: runnerTempFrom(options.runnerTemp, env),
776
+ trust: {item, rung: 'intake'},
777
+ }).record;
778
+ if (
779
+ record.intent.kind === 'intake-task' ||
780
+ record.intent.kind === 'intake-spec'
781
+ ) {
782
+ doc = intakeDocumentFromRecord(record);
783
+ // Re-render from the verdict and the TRUSTED issue number and stamp,
784
+ // then re-parse: the stamp must read back as the trusted values.
785
+ try {
786
+ content = renderIntakeDocument(doc, {issueNumber, originTrust});
787
+ } catch (err) {
788
+ const detail = err instanceof Error ? err.message : String(err);
789
+ throw new HandoffRejected(
790
+ 'field',
791
+ `the re-rendered document does not carry the trusted stamp: ${detail}`,
792
+ );
793
+ }
794
+ }
795
+ } catch (err) {
796
+ if (!(err instanceof HandoffRejected)) throw err;
797
+ return result(
798
+ 1,
799
+ 'rejected',
800
+ `${err.message}; nothing was posted and the label is removed`,
801
+ );
802
+ }
803
+
804
+ const mode: IntegrationMode =
805
+ held.documentMode ?? options.integration?.task ?? 'propose';
806
+ const ctx: IntakeWriteContext = {
807
+ issueNumber,
808
+ cwd,
809
+ arbiter,
810
+ issueProvider,
811
+ integration: {task: mode, spec: mode},
812
+ mergeRetries: options.mergeRetries,
813
+ mergeJitterMs: options.mergeJitterMs,
814
+ originTrust,
815
+ noPR: options.noPR,
816
+ placement: intakePlacementInputs(options),
817
+ providerInstance: options.providerInstance,
818
+ seen: held.seenCommentIds,
819
+ env,
820
+ note,
821
+ };
822
+ let written: IntakeResult;
823
+ if (doc !== undefined && content !== undefined) {
824
+ written = await emitIntakeDocument(doc, content, ctx);
825
+ } else if (record.intent.kind === 'intake-ask') {
826
+ written = await applyIntakeDecision(
827
+ {kind: 'ask', body: (record.products as IntakeAskProducts).question},
828
+ ctx,
829
+ );
830
+ } else {
831
+ written = await applyIntakeDecision(
832
+ {
833
+ kind: 'bounce',
834
+ body: (record.products as IntakeBounceProducts).bounceText,
835
+ },
836
+ ctx,
837
+ );
838
+ }
839
+ return {
840
+ exitCode: written.exitCode,
841
+ outcome: written.outcome as IntakePhaseOutcome,
842
+ issueNumber,
843
+ message: written.message,
844
+ emitted: written.emitted,
845
+ emittedSlug: written.emittedSlug,
846
+ };
847
+ } finally {
848
+ if (labels.outcome === 'ok') {
849
+ const removed = await issueProvider.removeLabel({
850
+ cwd,
851
+ issueNumber,
852
+ label: PROCESSING_LOCK_LABEL,
853
+ env,
854
+ });
855
+ if (!removed.applied) {
856
+ note(`Processing lock release degraded: ${removed.instruction}`);
857
+ note(manualRecovery(issueNumber));
858
+ }
859
+ }
860
+ }
861
+ }