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
@@ -12,10 +12,8 @@ import {runConcurrent} from './concurrency.js';
12
12
  import {scan, type ScanReport} from './scan.js';
13
13
  import {run as runProcess} from './git.js';
14
14
  import {jobWorktreeDoDriver} from './do.js';
15
- import {
16
- pushTreelessResult,
17
- TREELESS_RUNGS,
18
- } from './advance-treeless-publish.js';
15
+ import {TREELESS_RUNGS} from './advance-treeless-publish.js';
16
+ import {refWrite} from './ref-write.js';
19
17
  import type {Config} from './config.js';
20
18
  import type {
21
19
  RunTick,
@@ -206,7 +204,7 @@ export async function advanceOnce(
206
204
  TREELESS_RUNGS.has(result.rung) &&
207
205
  options.context.cwd !== undefined
208
206
  ) {
209
- await pushTreelessResult({
207
+ await refWrite.publishTreelessResult({
210
208
  cwd: options.context.cwd,
211
209
  arbiter: options.context.arbiter ?? 'origin',
212
210
  // Large liveness ceiling (C2, task `c2-rebase-until-real-on-durable-main-
@@ -134,6 +134,8 @@ export async function pushTreelessResult(
134
134
  const {cwd, arbiter, retries, env, note} = params;
135
135
  const jitterMs = params.jitterMs ?? DEFAULT_TREELESS_JITTER_MS;
136
136
  for (let i = 0; i <= retries; i++) {
137
+ // Write-seam implementation: callers reach this through
138
+ // `refWrite.publishTreelessResult` (guard: `write-sites-through-seams.test.ts`).
137
139
  const push = await runAsync(
138
140
  'git',
139
141
  ['push', '--quiet', arbiter, 'HEAD:main'],
@@ -207,6 +209,65 @@ export async function pushTreelessResult(
207
209
  }
208
210
  }
209
211
 
212
+ /** The result of {@link checkTreelessPublishScope}. */
213
+ export type TreelessPublishScope =
214
+ /** `HEAD` is `base`: the rung committed nothing, so there is nothing to push. */
215
+ | {kind: 'nothing'}
216
+ /** `HEAD` is exactly one commit whose parent is `base`: the rung's own. */
217
+ | {kind: 'rung-commit'; commit: string}
218
+ /** Anything else: publishing `HEAD:main` would carry commits that are not the rung's. */
219
+ | {kind: 'refused'; message: string};
220
+
221
+ /**
222
+ * Check, BEFORE a tree-less publish, that `HEAD:main` would publish exactly the
223
+ * rung's own commit (task `ci-split-treeless-rungs`; observation
224
+ * `advance-lifecycle-push-trigger-lands-an-unreviewed-work-branch-on-main`).
225
+ * {@link pushTreelessResult} pushes `HEAD:main` from whatever is checked out, so
226
+ * a checkout not based on the fetched `<arbiter>/main` tip (`base`) would
227
+ * publish every commit between the two. The CI apply phase runs the rung on a
228
+ * fresh checkout of `base` and calls this first: the new commit's parent must be
229
+ * `base` (the publish's own retry may then rebase only that commit). Read-only.
230
+ */
231
+ export async function checkTreelessPublishScope(params: {
232
+ cwd: string;
233
+ base: string;
234
+ env: NodeJS.ProcessEnv | undefined;
235
+ }): Promise<TreelessPublishScope> {
236
+ const {cwd, base, env} = params;
237
+ const head = await runAsync('git', ['rev-parse', 'HEAD'], cwd, {env});
238
+ if (head.status !== 0) {
239
+ return {kind: 'refused', message: 'the checkout has no HEAD commit'};
240
+ }
241
+ const tip = head.stdout.trim();
242
+ if (tip === base) return {kind: 'nothing'};
243
+ const list = await runAsync('git', ['rev-list', `${base}..${tip}`], cwd, {
244
+ env,
245
+ });
246
+ const commits = list.stdout.split('\n').filter((l) => l.trim() !== '');
247
+ const parents = await runAsync(
248
+ 'git',
249
+ ['rev-list', '--parents', '-n', '1', tip],
250
+ cwd,
251
+ {env},
252
+ );
253
+ const parentList = parents.stdout.trim().split(/\s+/).slice(1);
254
+ if (
255
+ list.status !== 0 ||
256
+ commits.length !== 1 ||
257
+ parentList.length !== 1 ||
258
+ parentList[0] !== base
259
+ ) {
260
+ return {
261
+ kind: 'refused',
262
+ message:
263
+ `HEAD (${tip}) is not one commit on top of the fetched main (${base}): ` +
264
+ `${commits.length} commit(s) would be published, so the tree-less publish ` +
265
+ 'is refused (it would land commits that are not the rung result)',
266
+ };
267
+ }
268
+ return {kind: 'rung-commit', commit: tip};
269
+ }
270
+
210
271
  /**
211
272
  * The rung kinds that commit a tree-less result LOCALLY (sidecar / `needsAnswers`
212
273
  * / `triaged:` marker) and therefore need {@link pushTreelessResult} to reach the
package/src/advance.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type {Phase} from './phase.js';
1
2
  import {existsSync, readFileSync} from 'node:fs';
2
3
  import {join} from 'node:path';
3
4
  import {type WorkFolderKey, workItemPath, workItemRel} from './work-layout.js';
@@ -401,9 +402,10 @@ export interface AdvanceContext {
401
402
  * Resolved `strictMergeApproval` boolean (sibling task
402
403
  * `strict-merge-approval-gate`). Default OFF ⇒ honour the prior approval +
403
404
  * land on a green re-verify (cheap, the SPEC-applied OQ6 default). ON ⇒
404
- * re-surface the merge-question when the merge-base moved between the
405
- * surfacer's question and this apply (the host-agnostic analogue of
406
- * GitHub's "dismiss stale approvals when the base changes").
405
+ * re-surface the merge-question when `main`'s code (outside `work/`) moved
406
+ * since the question was asked, i.e. since its recorded `askedAtMain` (the
407
+ * host-agnostic analogue of GitHub's "dismiss stale approvals when the base
408
+ * changes").
407
409
  */
408
410
  strictMergeApproval?: boolean;
409
411
  /**
@@ -433,6 +435,13 @@ export interface AdvanceContext {
433
435
  stuckAction?: StuckActionHandler;
434
436
  /** Sink for human-readable progress notes. */
435
437
  note?: (message: string) => void;
438
+ /**
439
+ * The CI phase this run executes as (`--phase lock|agent|apply`, CI-only;
440
+ * see `phase.ts`). `undefined` ⇒ today's single process. Threaded next to
441
+ * `env` and `note` so the per-path phase splits can branch on it; the phase
442
+ * guards read the process-wide active phase.
443
+ */
444
+ phase?: Phase;
436
445
  }
437
446
 
438
447
  /** The options one `advance` tick consumes. */
@@ -494,7 +503,7 @@ export interface ReadSignalsInput {
494
503
  * to the `spec` sidecar type, so `advance spec:<slug>` resolves its
495
504
  * signals/folders (`FOLDERS_FOR_TYPE['spec']`) and orchestrates `do spec:<slug>`.
496
505
  */
497
- function sidecarTypeFor(namespace: SlugNamespace): SidecarType {
506
+ export function sidecarTypeFor(namespace: SlugNamespace): SidecarType {
498
507
  return namespace === 'observation'
499
508
  ? 'observation'
500
509
  : namespace === 'spec'
@@ -1186,8 +1195,8 @@ async function applyRung(input: RungExecInput): Promise<RungExecResult> {
1186
1195
  * the item to needs-attention through its own shared seam, so `main` never
1187
1196
  * received a failing tree. The apply rung SHORT-CIRCUITS — the sidecar is
1188
1197
  * LEFT IN PLACE so the open answer stays surfaced for a human follow-up.
1189
- * - `restale` ⇒ `strictMergeApproval` was ON and the merge-base moved
1190
- * between the surfacer's question and this apply; the apply rung appends
1198
+ * - `restale` ⇒ `strictMergeApproval` was ON and `main`'s code moved
1199
+ * since the question was asked; the apply rung appends
1191
1200
  * a follow-up question + re-pauses (the human re-confirms against the new
1192
1201
  * base).
1193
1202
  * - `hold` / `drop` ⇒ no land; fall through to the normal apply path so the
@@ -1263,7 +1272,9 @@ async function maybeRunMergeAction(
1263
1272
  if (result.outcome === 'restale') {
1264
1273
  // `strictMergeApproval` re-surface: append a follow-up question and
1265
1274
  // re-pause. The previous answer stays recorded in the entry; the human
1266
- // re-confirms against the new merge-base in the appended question.
1275
+ // re-confirms against the new `main` in the appended question, which
1276
+ // records that `main` as its `askedAtMain` (so a re-answer while `main`
1277
+ // stays put lands instead of re-staling forever).
1267
1278
  const apply = context.applyPersist ?? applyAnsweredQuestions;
1268
1279
  try {
1269
1280
  const applied = apply({
@@ -1273,11 +1284,12 @@ async function maybeRunMergeAction(
1273
1284
  appendQuestions: [
1274
1285
  {
1275
1286
  question:
1276
- `Merge-base for \`work/${input.slug}\` moved since your last ` +
1277
- `answer (strictMergeApproval is ON). Re-confirm: still land?`,
1287
+ `\`main\` moved since the merge question for \`work/${input.slug}\` ` +
1288
+ `was asked (strictMergeApproval is ON). Re-confirm: still land?`,
1278
1289
  context: result.message,
1279
1290
  default: 'merge | hold | drop',
1280
1291
  kind: 'merge',
1292
+ ...(result.main === undefined ? {} : {askedAtMain: result.main}),
1281
1293
  },
1282
1294
  ],
1283
1295
  note,
@@ -1432,6 +1444,87 @@ async function maybeRunStuckAction(
1432
1444
  }
1433
1445
  }
1434
1446
 
1447
+ /**
1448
+ * What a TREE-LESS rung needs to run on this item, read off the same state the
1449
+ * rung body reads (spec `ci-agent-job-without-write-token` decision 11, task
1450
+ * `ci-split-treeless-rungs`): the CI lock phase publishes it as `needsAgent`, and
1451
+ * the apply phase recomputes it on its fresh checkout. It mirrors the rung
1452
+ * bodies above branch for branch, so it lives next to them:
1453
+ *
1454
+ * - `triage-observation`: a note already carrying `triaged:`, or the legacy
1455
+ * back-fill (an engine-written `## Applied answers` record without the
1456
+ * marker), is deterministic; under `observationTriage: auto` the triage gate
1457
+ * agent runs; otherwise the surface path runs the `surface-questions` agent
1458
+ * unless the observation provably has nothing to ask (the surface
1459
+ * short-circuit, which then surfaces only the engine-built triage question);
1460
+ * - `surface`: the `surface-questions` agent, or the same short-circuit;
1461
+ * - `apply`: an answered `kind: merge` entry answered `merge` runs the agent
1462
+ * job (task `ci-split-answered-merge-action`: no model, but `prepare` and
1463
+ * `verify` run the branch's code on the rebased tip), and `hold` / `drop`
1464
+ * run none; the CI phases refine the `merge` case against the arbiter (a
1465
+ * work branch that is absent or already on `main` needs none, see
1466
+ * `ci-phase-treeless.ts`), which this tree-only read cannot see. An
1467
+ * answered `kind: stuck` entry and a task/spec content answer run no agent;
1468
+ * an answered observation runs the agentic decision.
1469
+ *
1470
+ * A missing item file needs no agent (the rung is a benign `vanished` skip).
1471
+ */
1472
+ export type TreelessAgentNeed =
1473
+ | {needsAgent: boolean}
1474
+ | {unsplit: string; needsAgent?: undefined};
1475
+
1476
+ /** See {@link TreelessAgentNeed}. */
1477
+ export function treelessAgentNeed(input: RungExecInput): TreelessAgentNeed {
1478
+ const {item, context} = input;
1479
+ const cwd = context.cwd;
1480
+ const itemPath = findItemPath(cwd, input.namespace, input.slug);
1481
+ if (itemPath === undefined) return {needsAgent: false};
1482
+ const sidecarExists = existsSync(join(cwd, sidecarPathFor(item)));
1483
+ switch (input.classification.kind) {
1484
+ case 'triage-observation': {
1485
+ const body = readFileSync(join(cwd, itemPath), 'utf8');
1486
+ const fm = parseFrontmatter(body);
1487
+ if (fm.triaged !== undefined && fm.triaged !== '') {
1488
+ return {needsAgent: false};
1489
+ }
1490
+ if (hasAppliedAnswersRecord(body)) return {needsAgent: false};
1491
+ if (context.observationTriage === 'auto') return {needsAgent: true};
1492
+ return {
1493
+ needsAgent: !isNothingToSurfaceObservation(
1494
+ cwd,
1495
+ input,
1496
+ itemPath,
1497
+ sidecarExists,
1498
+ ),
1499
+ };
1500
+ }
1501
+ case 'surface':
1502
+ return {
1503
+ needsAgent: !isNothingToSurfaceObservation(
1504
+ cwd,
1505
+ input,
1506
+ itemPath,
1507
+ sidecarExists,
1508
+ ),
1509
+ };
1510
+ case 'apply':
1511
+ const merge = detectAnsweredMergeAction(cwd, item);
1512
+ if (merge !== undefined) {
1513
+ return {needsAgent: merge.verb === 'merge'};
1514
+ }
1515
+ if (detectAnsweredStuckAction(cwd, item) !== undefined) {
1516
+ return {needsAgent: false};
1517
+ }
1518
+ return {
1519
+ needsAgent:
1520
+ input.namespace === 'observation' &&
1521
+ !(context.applyFollowups && context.applyFollowups.length > 0),
1522
+ };
1523
+ default:
1524
+ return {unsplit: `the ${input.classification.kind} rung`};
1525
+ }
1526
+ }
1527
+
1435
1528
  /**
1436
1529
  * The cross-tick-window BENIGN SKIP shared by all three rungs that need to
1437
1530
  * resolve an item file (surface / triage / apply). The lifecycle pool enumerated
@@ -1462,7 +1555,7 @@ function vanishedSkip(input: {
1462
1555
  * folders it may rest in (the SAME folder set {@link readNeedsAnswers} searches).
1463
1556
  * Returns the path RELATIVE to `cwd`, or `undefined` when no file exists.
1464
1557
  */
1465
- function findItemPath(
1558
+ export function findItemPath(
1466
1559
  cwd: string,
1467
1560
  namespace: SlugNamespace,
1468
1561
  slug: string,
@@ -1547,8 +1640,14 @@ async function applyAgenticDecision(
1547
1640
  if (verdict.outcome === 'ask') {
1548
1641
  // ask-follow-up → the EXISTING append/re-pause loop. One BATCH of follow-ups.
1549
1642
  const apply = context.applyPersist ?? applyAnsweredQuestions;
1550
- const question = (verdict.question ?? '').trim();
1551
- if (question === '') {
1643
+ const questions = (
1644
+ verdict.questions !== undefined && verdict.questions.length > 0
1645
+ ? verdict.questions
1646
+ : [verdict.question ?? '']
1647
+ )
1648
+ .map((q) => q.trim())
1649
+ .filter((q) => q !== '');
1650
+ if (questions.length === 0) {
1552
1651
  return {
1553
1652
  exitCode: 1,
1554
1653
  outcome: 'usage-error',
@@ -1560,7 +1659,7 @@ async function applyAgenticDecision(
1560
1659
  cwd,
1561
1660
  item,
1562
1661
  itemPath,
1563
- appendQuestions: [{question}],
1662
+ appendQuestions: questions.map((question) => ({question})),
1564
1663
  note,
1565
1664
  });
1566
1665
  return {
@@ -1857,9 +1956,21 @@ export async function performAdvance(
1857
1956
  // ITSELF acquires the SAME `task-<slug>`/`prd-<slug>` ref (the create-only CAS
1858
1957
  // with NO re-entrancy/auto-steal, per the ADR), so taking it here too would
1859
1958
  // DEADLOCK the tick against itself. The inner `do`'s claim/task lock IS the
1860
- // single exclusion point for those rungs. The `work/advancing/<entry>.md` marker
1861
- // CAS is KEPT for ALL rungs (its removal is the capstone task #9).
1959
+ // single exclusion point for those rungs. The legacy `work/advancing/<entry>.md`
1960
+ // marker is GONE (removed by the capstone cut-over, task
1961
+ // `cutover-retire-slicing-advancing-markers-and-trim-folder-sets`): there is no
1962
+ // marker CAS for any rung, only the per-item lock ref (see `advancing-lock.ts`).
1862
1963
  const unifiedForRung = isTreeLessRung(classification.kind);
1964
+ // An answered `merge` may take over the lock a finished propose build of the
1965
+ // SAME item keeps held (the `propose-pr` marker; task
1966
+ // `wire-merge-questions-into-the-advance-tick`, decisions 2 and 4). Any other
1967
+ // held lock still makes this tick back off.
1968
+ const takeOverProposeKept = answersMerge(
1969
+ repoPath,
1970
+ item,
1971
+ resolved.namespace,
1972
+ classification,
1973
+ );
1863
1974
  const acquire =
1864
1975
  options.acquireLock ??
1865
1976
  ((lockItem: string) =>
@@ -1868,6 +1979,7 @@ export async function performAdvance(
1868
1979
  cwd,
1869
1980
  arbiter,
1870
1981
  acquireUnified: unifiedForRung,
1982
+ takeOverProposeKept,
1871
1983
  note,
1872
1984
  }));
1873
1985
  const lock = await acquire(item);
@@ -1965,6 +2077,24 @@ function isTreeLessRung(kind: TickClassification['kind']): boolean {
1965
2077
  );
1966
2078
  }
1967
2079
 
2080
+ /**
2081
+ * Is this tick the apply of a task whose merge question is answered `merge`
2082
+ * (the case that may take over a propose-kept lock)? Read off the same tree the
2083
+ * classification read.
2084
+ */
2085
+ export function answersMerge(
2086
+ repoPath: string,
2087
+ item: string,
2088
+ namespace: SlugNamespace,
2089
+ classification: TickClassification,
2090
+ ): boolean {
2091
+ return (
2092
+ classification.kind === 'apply' &&
2093
+ namespace === 'task' &&
2094
+ detectAnsweredMergeAction(repoPath, item)?.verb === 'merge'
2095
+ );
2096
+ }
2097
+
1968
2098
  /** Dispatch the classified rung to the executor seam (winner-only). */
1969
2099
  function dispatchRung(
1970
2100
  executor: RungExecutor,
@@ -4,7 +4,11 @@ import {runAsync, type RunResult} from './git.js';
4
4
  import {ledgerWrite} from './ledger-write.js';
5
5
  import {resolveSidecarIdentity} from './sidecar.js';
6
6
  import {readFrontmatterField} from './frontmatter.js';
7
- import {acquireItemLock, releaseItemLock} from './item-lock.js';
7
+ import {
8
+ acquireItemLock,
9
+ releaseItemLock,
10
+ takeOverProposeKeptLock,
11
+ } from './item-lock.js';
8
12
  import {realSleep, type Sleep} from './retry-backoff.js';
9
13
 
10
14
  /**
@@ -110,6 +114,15 @@ export interface AcquireAdvancingLockOptions {
110
114
  * nothing). This module stays rung-agnostic — it only knows "unified or not".
111
115
  */
112
116
  acquireUnified?: boolean;
117
+ /**
118
+ * When the unified acquire finds the item locked, TAKE OVER the lock if (and
119
+ * only if) a finished propose build of the same item keeps it held (its entry
120
+ * carries the `propose-pr` marker, see `takeOverProposeKeptLock`). The advance
121
+ * tick sets it for an answered `merge` (task
122
+ * `wire-merge-questions-into-the-advance-tick`, decisions 2 and 4); any other
123
+ * held lock is still `lost`. Ignored unless `acquireUnified`.
124
+ */
125
+ takeOverProposeKept?: boolean;
113
126
  /** Environment for child git processes. */
114
127
  env?: NodeJS.ProcessEnv;
115
128
  /** Sink for human-readable progress notes. */
@@ -215,6 +228,28 @@ async function runAcquire(
215
228
  `failed to acquire the item lock for '${entry}': ${lock.message}`,
216
229
  );
217
230
  }
231
+ if (lock.outcome === 'lost' && options.takeOverProposeKept === true) {
232
+ const taken = await takeOverProposeKeptLock({
233
+ item: options.item,
234
+ action: 'advance',
235
+ cwd,
236
+ arbiter,
237
+ holder: by,
238
+ env,
239
+ });
240
+ if (taken.outcome === 'error') {
241
+ throw new AdvancingLockUsageError(
242
+ `failed to take over the item lock for '${entry}': ${taken.message}`,
243
+ );
244
+ }
245
+ if (taken.outcome === 'acquired') {
246
+ const message = `LOCKED '${entry}' for advancing on ${arbiter}: ${taken.message}.`;
247
+ note(message);
248
+ return {exitCode: 0, outcome: 'acquired', message, entry};
249
+ }
250
+ note(taken.message);
251
+ return {exitCode: 2, outcome: 'lost', message: taken.message, entry};
252
+ }
218
253
  if (lock.outcome === 'lost') {
219
254
  note(lock.message);
220
255
  return {exitCode: 2, outcome: 'lost', message: lock.message, entry};
@@ -0,0 +1,174 @@
1
+ /**
2
+ * **No GitHub token in a CI agent's environment.**
3
+ *
4
+ * In CI, dorfl runs an agent (with shell) over text any GitHub user can write:
5
+ * an issue body, a comment. Whatever the agent's environment holds, a prompt
6
+ * injection in that text can ask it to print. So every autonomous agent launch
7
+ * in GitHub Actions gets its environment with the GitHub credentials removed
8
+ * ({@link scrubAgentEnv}), whatever the workflow put in the step environment.
9
+ *
10
+ * This is only the cheap layer. It does not stop an agent from reading the
11
+ * token another way while it shares a job with one (its parent processes'
12
+ * environment, `.git/config` when the checkout persists a credential, `sudo` on
13
+ * a hosted runner). The real boundary is running the agent in a job whose
14
+ * token cannot write, see spec `ci-agent-job-without-write-token` (under
15
+ * `work/specs/`).
16
+ *
17
+ * On a laptop nothing is removed: it is the human's own machine, and a local
18
+ * tool the agent uses (an MCP server, a `gh` call) may legitimately need the
19
+ * human's token.
20
+ */
21
+
22
+ /**
23
+ * Names that carry a GitHub credential by convention (case-insensitive):
24
+ * `GH_TOKEN`, `GITHUB_TOKEN`, any `GH_*_TOKEN` / `GITHUB_*_TOKEN` (e.g.
25
+ * `GH_ENTERPRISE_TOKEN`), and the dorfl-specific `DORFL_GH_TOKEN`,
26
+ * `DORFL_GIT_TOKEN`, `DORFL_GITHUB_TOKEN` with any infix
27
+ * (`DORFL_*_GH_TOKEN` / `DORFL_*_GIT_TOKEN` / `DORFL_*_GITHUB_TOKEN`).
28
+ */
29
+ const GITHUB_TOKEN_NAME =
30
+ /^(?:(?:GH|GITHUB)_(?:\w+_)?TOKEN|DORFL_(?:\w+_)?(?:GH|GIT|GITHUB)_TOKEN)$/i;
31
+
32
+ /**
33
+ * The GitHub Actions runtime credentials a step can see: the OIDC request pair
34
+ * (it mints an ID token, which a cloud role may trust for writes) and the
35
+ * runtime token (artifact/cache service).
36
+ */
37
+ const ACTIONS_RUNTIME_NAME =
38
+ /^ACTIONS_(?:ID_TOKEN_REQUEST_(?:TOKEN|URL)|RUNTIME_TOKEN)$/i;
39
+
40
+ /**
41
+ * The runner's workflow-command FILES. Not credentials, but writing to them
42
+ * injects environment variables, PATH entries or outputs into LATER steps of
43
+ * the job, which may hold a token. The agent has no use for them.
44
+ */
45
+ const RUNNER_COMMAND_FILES = new Set([
46
+ 'GITHUB_ENV',
47
+ 'GITHUB_PATH',
48
+ 'GITHUB_OUTPUT',
49
+ 'GITHUB_STATE',
50
+ 'GITHUB_STEP_SUMMARY',
51
+ ]);
52
+
53
+ /**
54
+ * GitHub token VALUE shapes, anywhere in a value (a token inside a URL counts):
55
+ * personal (`ghp_`), OAuth (`gho_`), user-to-server (`ghu_`), server-to-server
56
+ * incl. the Actions `GITHUB_TOKEN` (`ghs_`), refresh (`ghr_`) and fine-grained
57
+ * PATs (`github_pat_`). Matching on the value catches a token the workflow
58
+ * exported under ANY name, which a name list alone would miss.
59
+ */
60
+ const GITHUB_TOKEN_VALUE =
61
+ /(?:^|[^A-Za-z0-9_])(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/;
62
+
63
+ /** `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>`. */
64
+ const GIT_CONFIG_ENV_ENTRY = /^GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)$/;
65
+
66
+ /**
67
+ * Git config text that carries or fetches a credential: an HTTP auth header, a
68
+ * credential helper or askpass command, or an HTTP(S) URL with userinfo
69
+ * (`https://user:token@host`). A plain `insteadOf` rewrite is NOT matched,
70
+ * including an SSH one (`ssh://git@github.com/`, whose userinfo is no secret).
71
+ */
72
+ const CREDENTIAL_CONFIG_TEXT =
73
+ /extraheader|authorization|credential\.|askpass|https?:\/\/[^/@\s]+@/i;
74
+
75
+ /** The result of {@link scrubAgentEnv}. */
76
+ export interface ScrubbedEnv {
77
+ /** The environment to launch the child with (a COPY; the input is untouched). */
78
+ env: NodeJS.ProcessEnv;
79
+ /** The NAMES that were removed (never the values), sorted, for diagnostics. */
80
+ removed: string[];
81
+ }
82
+
83
+ /**
84
+ * Should this single variable be withheld from an agent? True for the GitHub
85
+ * token names, the Actions runtime credentials, the runner command files, and
86
+ * any variable whose VALUE contains a GitHub token.
87
+ */
88
+ export function isWriteCredentialVar(
89
+ name: string,
90
+ value: string | undefined,
91
+ ): boolean {
92
+ if (GITHUB_TOKEN_NAME.test(name) || ACTIONS_RUNTIME_NAME.test(name)) {
93
+ return true;
94
+ }
95
+ if (RUNNER_COMMAND_FILES.has(name.toUpperCase())) {
96
+ return true;
97
+ }
98
+ return value !== undefined && GITHUB_TOKEN_VALUE.test(value);
99
+ }
100
+
101
+ /**
102
+ * Return a copy of `env` with every GitHub credential removed: the variables
103
+ * {@link isWriteCredentialVar} flags, any OTHER variable holding the SAME value
104
+ * as one of those (an alias such as `TOKEN=$GH_TOKEN` of a token whose shape is
105
+ * not recognised, e.g. a GitHub Enterprise Server token), and git's environment
106
+ * config (`GIT_CONFIG_PARAMETERS`, `GIT_CONFIG_COUNT/KEY_n/VALUE_n`) when it
107
+ * carries a credential. The `GIT_CONFIG_COUNT/KEY_n/VALUE_n` block is removed
108
+ * WHOLE whenever any part of it is: removing one entry would leave a `COUNT`
109
+ * pointing at a missing key, which makes every git command fail.
110
+ *
111
+ * A denylist on purpose: the agent needs an open-ended set of variables (PATH,
112
+ * HOME, the provider key whose name the consumer chooses, node/pnpm/proxy
113
+ * settings), and an allowlist would break a consumer's harness.
114
+ */
115
+ export function scrubAgentEnv(env: NodeJS.ProcessEnv): ScrubbedEnv {
116
+ const out: NodeJS.ProcessEnv = {...env};
117
+ const removed = new Set<string>();
118
+ const secretValues = new Set<string>();
119
+ for (const [name, value] of Object.entries(env)) {
120
+ if (isWriteCredentialVar(name, value)) {
121
+ delete out[name];
122
+ removed.add(name);
123
+ // Only a value long enough to be a secret is used for alias matching, so
124
+ // an empty or trivially short value cannot knock out unrelated variables.
125
+ if (value !== undefined && value.length >= 16) {
126
+ secretValues.add(value);
127
+ }
128
+ }
129
+ }
130
+ for (const [name, value] of Object.entries(out)) {
131
+ if (value !== undefined && secretValues.has(value)) {
132
+ delete out[name];
133
+ removed.add(name);
134
+ }
135
+ }
136
+ // Judge the block on the ORIGINAL env: an entry already removed above still
137
+ // condemns the whole block.
138
+ const gitConfigEntries = Object.keys(env).filter((k) =>
139
+ GIT_CONFIG_ENV_ENTRY.test(k),
140
+ );
141
+ if (
142
+ gitConfigEntries.some(
143
+ (k) => removed.has(k) || CREDENTIAL_CONFIG_TEXT.test(env[k] ?? ''),
144
+ )
145
+ ) {
146
+ for (const k of gitConfigEntries) {
147
+ delete out[k];
148
+ removed.add(k);
149
+ }
150
+ }
151
+ if (CREDENTIAL_CONFIG_TEXT.test(out.GIT_CONFIG_PARAMETERS ?? '')) {
152
+ delete out.GIT_CONFIG_PARAMETERS;
153
+ removed.add('GIT_CONFIG_PARAMETERS');
154
+ }
155
+ return {env: out, removed: [...removed].sort()};
156
+ }
157
+
158
+ /**
159
+ * Is dorfl running in GitHub Actions (where agents lose the GitHub tokens)?
160
+ * Checks the process env too, so a caller that builds a child env from scratch
161
+ * (without `GITHUB_ACTIONS`) cannot switch the filter off by accident.
162
+ */
163
+ export function inGitHubActions(env: NodeJS.ProcessEnv): boolean {
164
+ return env.GITHUB_ACTIONS === 'true' || process.env.GITHUB_ACTIONS === 'true';
165
+ }
166
+
167
+ /**
168
+ * The environment an autonomous agent is launched with: in GitHub Actions, the
169
+ * caller's env with every GitHub credential removed ({@link scrubAgentEnv}); on
170
+ * a laptop, the caller's env unchanged.
171
+ */
172
+ export function agentEnvFor(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
173
+ return inGitHubActions(env) ? scrubAgentEnv(env).env : env;
174
+ }
package/src/agent-stop.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {runAsync} from './git.js';
2
2
  import {JOB_RECORD_FILENAME} from './workspace.js';
3
3
  import type {NewQuestion} from './sidecar.js';
4
+ import type {CutOffTurn} from './harness.js';
4
5
 
5
6
  /**
6
7
  * **The build-agent → runner REPORTING CHANNEL** (task `agent-stop-signal`).
@@ -363,6 +364,86 @@ export function emptyDiffStopReason(slug: string): string {
363
364
  );
364
365
  }
365
366
 
367
+ /**
368
+ * The provider error text is capped at this many characters in the failure
369
+ * detail: pi records the raw provider body (often a JSON blob), and the detail
370
+ * becomes the needs-attention reason on the lock, which must stay one readable
371
+ * line rather than a pasted payload.
372
+ */
373
+ const MAX_CUT_OFF_ERROR_LENGTH = 300;
374
+
375
+ /**
376
+ * The `agent-failed` detail for a run whose FINAL model turn was CUT OFF
377
+ * ({@link CutOffTurn}) and which left no source change (task
378
+ * `a-truncated-agent-turn-routes-as-agent-failed`). Names the stop cause so the
379
+ * operator reads a harness failure, never "the agent found nothing to build".
380
+ * The provider's `errorMessage` (an `error` stop) is appended VERBATIM (bounded)
381
+ * so the shared failure-cause classifier can still recognise an infra signature
382
+ * in it (an overloaded provider reads `transient-infra`).
383
+ */
384
+ export function cutOffTurnFailureDetail(
385
+ slug: string,
386
+ cutOff: CutOffTurn,
387
+ ): string {
388
+ const cause =
389
+ cutOff.cause === 'length'
390
+ ? "stopReason 'length': the turn hit the model's per-turn output-token cap"
391
+ : "stopReason 'error': the provider ended the turn with an error";
392
+ const error =
393
+ cutOff.errorMessage === undefined
394
+ ? ''
395
+ : ` (${boundErrorMessage(cutOff.errorMessage)})`;
396
+ return (
397
+ `the agent's final model turn was cut off (${cause}${error}) before it ` +
398
+ `produced any source change building '${slug}'; this is a harness failure, ` +
399
+ 'not a judgement that there is nothing to build. Requeue to retry.'
400
+ );
401
+ }
402
+
403
+ function boundErrorMessage(text: string): string {
404
+ const oneLine = text.replace(/\s+/g, ' ').trim();
405
+ return oneLine.length <= MAX_CUT_OFF_ERROR_LENGTH
406
+ ? oneLine
407
+ : `${oneLine.slice(0, MAX_CUT_OFF_ERROR_LENGTH)}…`;
408
+ }
409
+
410
+ /**
411
+ * The CUT-OFF-TURN guard in front of the empty-diff STOP backstop (task
412
+ * `a-truncated-agent-turn-routes-as-agent-failed`): returns the `agent-failed`
413
+ * detail when the run's final model turn was cut off ({@link CutOffTurn}), the
414
+ * agent raised NO in-band STOP sentinel, AND the work branch is empty; else
415
+ * `undefined` (the caller proceeds to its usual STOP resolution / gate).
416
+ *
417
+ * Only a NORMAL end of turn may reach the empty-diff backstop: that route reads
418
+ * an empty diff as the agent's deliberate "nothing to build" and surfaces a
419
+ * DISPOSE-defaulted question, which for a truncated turn puts a real, unbuilt
420
+ * task one default answer away from `tasks/cancelled/`. A sentinel still wins
421
+ * (the agent DID report a deliberate STOP), and a non-empty diff still goes to
422
+ * the gate (the gate, not the stop reason, judges built work). Shared by `do`
423
+ * (in-place and remote, hence the CI agent phase) and `run` so every path with
424
+ * the empty-diff backstop applies the same rule.
425
+ */
426
+ export async function resolveCutOffTurnFailure(params: {
427
+ cutOffTurn: CutOffTurn | undefined;
428
+ output: string | undefined;
429
+ slug: string;
430
+ cwd: string;
431
+ arbiter: string;
432
+ env?: NodeJS.ProcessEnv;
433
+ }): Promise<string | undefined> {
434
+ const {cutOffTurn, output, slug, cwd, arbiter, env} = params;
435
+ if (cutOffTurn === undefined) {
436
+ return undefined;
437
+ }
438
+ if (parseStopSentinel(output) !== undefined) {
439
+ return undefined;
440
+ }
441
+ if (!(await isWorkBranchDiffEmpty({cwd, arbiter, env}))) {
442
+ return undefined;
443
+ }
444
+ return cutOffTurnFailureDetail(slug, cutOffTurn);
445
+ }
446
+
366
447
  /**
367
448
  * The DISCRIMINATED classification of a `saveAgentStop` reason (spec
368
449
  * `surface-stuck-as-questions-and-retire-stuck-lock-state` resolved decision #2,