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
@@ -1,5 +1,6 @@
1
1
  import {existsSync, mkdirSync} from 'node:fs';
2
- import {dirname, join} from 'node:path';
2
+ import {dirname, isAbsolute, join} from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
3
4
  import {git, run} from './git.js';
4
5
  import type {Config, PartialConfig} from './config.js';
5
6
  import type {ConfigOverrideMap} from './config-override.js';
@@ -35,6 +36,27 @@ export function readOriginUrl(
35
36
  return url === '' ? undefined : url;
36
37
  }
37
38
 
39
+ /**
40
+ * Is `mirrorPath` inside a `--reconcile-locks` write scope? `undefined` scope
41
+ * means every mirror (the explicit `--all-arbiters`); otherwise only the mirror
42
+ * whose `origin` URL hub-keys to `arbiterKey` (the SAME keying `gc` uses for its
43
+ * per-job arbiter scope). A mirror whose `origin` is unreadable is OUT of scope:
44
+ * the safe direction for a write. Shared by `status --reconcile-locks` (task
45
+ * `reconcile-locks-stays-within-the-current-arbiter`) and `scan
46
+ * --reconcile-locks` (task `status-no-arbiter-is-honoured`).
47
+ */
48
+ export function mirrorInReconcileScope(
49
+ mirrorPath: string,
50
+ arbiterKey: string | undefined,
51
+ env: NodeJS.ProcessEnv | undefined,
52
+ ): boolean {
53
+ if (arbiterKey === undefined) {
54
+ return true;
55
+ }
56
+ const url = readOriginUrl(mirrorPath, env);
57
+ return url !== undefined && encodeRepoKey(url) === arbiterKey;
58
+ }
59
+
38
60
  /**
39
61
  * The shared **hub-mirror primitive**: one bare mirror per repo under
40
62
  * `<workspacesDir>/repos/<key>.git`, re-fetched from the arbiter. Both the
@@ -409,3 +431,87 @@ export function fetchMirrorMainOrWarn(options: {
409
431
  return false;
410
432
  }
411
433
  }
434
+
435
+ /**
436
+ * The mirror's `origin` URL when it names a LOCAL arbiter (a `file://` URL or an
437
+ * absolute path) that no longer exists on disk; `undefined` otherwise (a live
438
+ * local arbiter, a network remote, or an unreadable origin). Read-only.
439
+ */
440
+ export function missingLocalOrigin(
441
+ mirrorPath: string,
442
+ env: NodeJS.ProcessEnv | undefined,
443
+ ): string | undefined {
444
+ const url = readOriginUrl(mirrorPath, env);
445
+ if (url === undefined) {
446
+ return undefined;
447
+ }
448
+ let localPath: string | undefined;
449
+ if (url.startsWith('file://')) {
450
+ try {
451
+ localPath = fileURLToPath(url);
452
+ } catch {
453
+ return undefined;
454
+ }
455
+ } else if (isAbsolute(url)) {
456
+ localPath = url;
457
+ }
458
+ return localPath !== undefined && !existsSync(localPath) ? url : undefined;
459
+ }
460
+
461
+ /** How many dead mirrors the summary line names before "and N more". */
462
+ const DEAD_MIRROR_EXAMPLES = 3;
463
+
464
+ /**
465
+ * The fetch-first refresh `status` and `scan` run over every registered mirror,
466
+ * with DEAD LOCAL ARBITERS summarised on ONE line instead of one warning each.
467
+ *
468
+ * A mirror whose origin is a local path that no longer exists (typically a
469
+ * throwaway `/tmp` arbiter, or a disk that is not mounted) cannot be fetched, and
470
+ * warning once per mirror buried the real output under hundreds of identical
471
+ * git errors. Such a mirror is NOT fetched and NOT removed: it is still read
472
+ * from its last-known state exactly like an offline mirror, because a read-only
473
+ * command must never delete state (an unmounted disk comes back). `flush()`
474
+ * then emits a single line with the count, a few examples and the removal
475
+ * command. Every other fetch failure keeps its own warning, as before.
476
+ */
477
+ export function createMirrorRefresher(options: {
478
+ warn?: (message: string) => void;
479
+ env?: NodeJS.ProcessEnv;
480
+ }): {refresh(mirrorPath: string): void; flush(): void} {
481
+ const dead: {mirrorPath: string; url: string}[] = [];
482
+ return {
483
+ refresh(mirrorPath: string): void {
484
+ const url = missingLocalOrigin(mirrorPath, options.env);
485
+ if (url !== undefined) {
486
+ dead.push({mirrorPath, url});
487
+ return;
488
+ }
489
+ fetchMirrorMainOrWarn({
490
+ mirrorPath,
491
+ warn: options.warn,
492
+ env: options.env,
493
+ });
494
+ },
495
+ flush(): void {
496
+ if (dead.length === 0) {
497
+ return;
498
+ }
499
+ const shown = dead
500
+ .slice(0, DEAD_MIRROR_EXAMPLES)
501
+ .map((d) => d.url)
502
+ .join(', ');
503
+ const more =
504
+ dead.length > DEAD_MIRROR_EXAMPLES
505
+ ? ` and ${dead.length - DEAD_MIRROR_EXAMPLES} more`
506
+ : '';
507
+ const noun =
508
+ dead.length === 1
509
+ ? '1 registered mirror points at a local arbiter that no longer exists'
510
+ : `${dead.length} registered mirrors point at a local arbiter that no longer exists`;
511
+ options.warn?.(
512
+ `${noun} (${shown}${more}); skipped the fetch and read their last-known ` +
513
+ 'state. Remove each with `dorfl remote rm <origin-url>` if it is gone for good.',
514
+ );
515
+ },
516
+ };
517
+ }
@@ -124,7 +124,7 @@ export class ReviewParseError extends Error {}
124
124
  * shared the capped response with the verdict) rather than chasing a model flake.
125
125
  *
126
126
  * Detected at the harness seam: the adapter surfaces the last assistant turn's
127
- * `stop_reason` (null / `None` / `max_tokens` — the turn did not end naturally) and
127
+ * stop reason (pi's `length`, or a raw `max_tokens`: the turn hit the output cap) and
128
128
  * its `usage.output` token count; the gate throws this when a parse fails AND that
129
129
  * cap signal is present. When the adapter CANNOT see the signal (the null/shell
130
130
  * adapter, or a future adapter without usage telemetry), the parse still fails as a
package/src/run.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import {continueRebaseSentence} from './continue-branch.js';
1
2
  import {type Config, resolvePromptGuidance} from './config.js';
2
3
  import {resolveRepoConfig} from './repo-config.js';
3
4
  import type {ConfigOverrideMap} from './config-override.js';
@@ -16,7 +17,7 @@ import {
16
17
  type IsolationStrategy,
17
18
  } from './isolation.js';
18
19
  import {runConcurrent, createKeyedLock} from './concurrency.js';
19
- import type {Harness} from './harness.js';
20
+ import type {CutOffTurn, Harness} from './harness.js';
20
21
  import {createHarness} from './pi-harness.js';
21
22
  import {generateSessionPath} from './session-path.js';
22
23
  import {
@@ -32,6 +33,7 @@ import {
32
33
  isWorkBranchDiffEmpty,
33
34
  emptyDiffStopReason,
34
35
  emptyDiffDisposeEnvelope,
36
+ resolveCutOffTurnFailure,
35
37
  type AgentStopKind,
36
38
  } from './agent-stop.js';
37
39
  import {surfaceStuckToNeedsAttention} from './needs-attention.js';
@@ -233,6 +235,12 @@ export type Dorfl = (input: {
233
235
  * provider degrades to `--fill` (no regression).
234
236
  */
235
237
  output?: string;
238
+ /**
239
+ * The agent's final model turn was CUT OFF (`length` / `error` stop; the
240
+ * harness seam's `LaunchResult.cutOffTurn`, mirrors `DoDorfl.cutOffTurn`). An
241
+ * empty diff with this set routes `agent-failed`, never the empty-diff STOP.
242
+ */
243
+ cutOffTurn?: CutOffTurn;
236
244
  };
237
245
 
238
246
  export interface RunOnceOptions {
@@ -725,8 +733,9 @@ async function runOneItem(
725
733
  // predicate still HOLDS and this worktree is reaped — more §14-aligned.
726
734
  if (tree.continueRebaseConflict) {
727
735
  const reason =
728
- `continuing the kept ${tree.branch}: rebase onto the latest main ` +
729
- 'conflicted (aborted, never auto-resolved) — run `requeue --reconcile` ' +
736
+ `continuing the kept ${tree.branch}: ` +
737
+ `${continueRebaseSentence(tree.branch, tree.continueRebaseFailure)} — ` +
738
+ 'run `requeue --reconcile` ' +
730
739
  'to non-destructively re-sync the mirror and retry the rebase (keeps ' +
731
740
  'the work). Last resort: `requeue --reset` DESTRUCTIVELY discards the ' +
732
741
  'branch and starts fresh.';
@@ -824,6 +833,7 @@ async function runOneItem(
824
833
  cwd: tree.dir,
825
834
  continueContext,
826
835
  promptGuidance: itemGuidance,
836
+ taskFolder: task.folder,
827
837
  });
828
838
  } catch (err) {
829
839
  if (err instanceof PromptError) {
@@ -837,7 +847,12 @@ async function runOneItem(
837
847
  // resolved per-repo `buildModel` (ADR §13, which inherits the general
838
848
  // `model` when unset) flows through the seam to the adapter; a
839
849
  // `{model}`-in-agentCmd misconfiguration surfaces as agent-failed.
840
- let agent: {ok: boolean; detail?: string; output?: string};
850
+ let agent: {
851
+ ok: boolean;
852
+ detail?: string;
853
+ output?: string;
854
+ cutOffTurn?: CutOffTurn;
855
+ };
841
856
  try {
842
857
  agent = runAgent(
843
858
  ctx,
@@ -874,6 +889,21 @@ async function runOneItem(
874
889
  // catches a stop without one. Either routes to needs-attention (surfaced on
875
890
  // the arbiter) and SKIPS the gate + Gate-2 (the whole `performIntegration`
876
891
  // band) — a clean STOP is NOT "a build that changed nothing".
892
+ // A CUT-OFF final turn (`length`/`error`) with an empty diff is a harness
893
+ // failure, not a STOP: route it `agent-failed` before the empty-diff
894
+ // backstop (task `a-truncated-agent-turn-routes-as-agent-failed`, the
895
+ // same guard `do` runs).
896
+ const cutOffDetail = await resolveCutOffTurnFailure({
897
+ cutOffTurn: agent.cutOffTurn,
898
+ output: agent.output,
899
+ slug,
900
+ cwd: tree.dir,
901
+ arbiter: tree.arbiterRemote,
902
+ env: gitEnv,
903
+ });
904
+ if (cutOffDetail !== undefined) {
905
+ return await saveAgentFailure(base, tree, slug, cutOffDetail, ctx);
906
+ }
877
907
  const sentinel = parseStopSentinel(agent.output);
878
908
  const stopReason: {kind: AgentStopKind; reason: string} | undefined =
879
909
  sentinel !== undefined
@@ -1119,7 +1149,12 @@ function runAgent(
1119
1149
  agentCmd: string,
1120
1150
  model: string | undefined,
1121
1151
  sessionsDir: string | undefined,
1122
- ): {ok: boolean; detail?: string; output?: string} {
1152
+ ): {
1153
+ ok: boolean;
1154
+ detail?: string;
1155
+ output?: string;
1156
+ cutOffTurn?: CutOffTurn;
1157
+ } {
1123
1158
  if (ctx.dorfl) {
1124
1159
  return ctx.dorfl({cwd: tree.dir, prompt, slug, env: ctx.env});
1125
1160
  }
@@ -1148,7 +1183,13 @@ function runAgent(
1148
1183
  // Surface the agent's FINAL SUMMARY (`LaunchResult.output`) — the source channel
1149
1184
  // for the propose-mode PR body — instead of dropping it (mirrors `do`'s
1150
1185
  // `runDoAgent`). Absent (no parseable assistant text) ⇒ undefined ⇒ `--fill`.
1151
- return {ok: launched.ok, detail: launched.detail, output: launched.output};
1186
+ return {
1187
+ ok: launched.ok,
1188
+ detail: launched.detail,
1189
+ output: launched.output,
1190
+ // How the final model turn ended (the cut-off guard before the STOP route).
1191
+ cutOffTurn: launched.cutOffTurn,
1192
+ };
1152
1193
  }
1153
1194
 
1154
1195
  /**
package/src/scan.ts CHANGED
@@ -9,7 +9,8 @@ import {
9
9
  } from './ledger-read.js';
10
10
  import {listMirrors} from './registry.js';
11
11
  import {
12
- fetchMirrorMainOrWarn,
12
+ createMirrorRefresher,
13
+ mirrorInReconcileScope,
13
14
  resolveRepoConfigFromMirror,
14
15
  } from './repo-mirror.js';
15
16
  import {resolveRepoConfig} from './repo-config.js';
@@ -425,6 +426,16 @@ export async function scan(
425
426
  * claim path sweeps on every unit of work.
426
427
  */
427
428
  reconcileLocks?: boolean;
429
+ /**
430
+ * ARBITER SCOPE of the {@link reconcileLocks} write (task
431
+ * `status-no-arbiter-is-honoured`, the same scoping `status --reconcile-locks`
432
+ * has). When set, ONLY the registered mirror whose `origin` hub-keys to this
433
+ * key has its stale locks RELEASED; every other mirror is merely classified,
434
+ * exactly as without `reconcileLocks`. `undefined` reconciles EVERY mirror:
435
+ * the CLI leaves it undefined only behind the explicit `--all-arbiters`.
436
+ * Ignored when `reconcileLocks` is not `true`.
437
+ */
438
+ reconcileArbiterKey?: string;
428
439
  env?: NodeJS.ProcessEnv;
429
440
  /**
430
441
  * The per-machine {@link ConfigOverrideMap} (from `loadConfigOverride`),
@@ -444,15 +455,17 @@ export async function scan(
444
455
  const repos: RepoReport[] = [];
445
456
  const counts = {totalItems: 0, totalEligible: 0};
446
457
 
458
+ // Dead LOCAL arbiters are summarised on one line after the loop (see
459
+ // `createMirrorRefresher`), not warned once per mirror.
460
+ const refresher = createMirrorRefresher({
461
+ warn: options.warn,
462
+ env: options.env,
463
+ });
447
464
  for (const mirror of mirrors) {
448
465
  // Fetch-first (ADR §5/§6): refresh this mirror's `main` so the read below
449
466
  // sees the remote truth. Never fatal — a failed fetch WARNS and falls back to
450
467
  // the mirror's last-known `main` (the read strategy is unchanged).
451
- fetchMirrorMainOrWarn({
452
- mirrorPath: mirror.path,
453
- warn: options.warn,
454
- env: options.env,
455
- });
468
+ refresher.refresh(mirror.path);
456
469
  // Read the full `work/` lifecycle from the mirror's bare `main` ref through
457
470
  // the read seam (git ls-tree/show; NOT a working-tree read).
458
471
  const state = await ledgerRead.resolveMirrorState({
@@ -494,7 +507,14 @@ export async function scan(
494
507
  const MIRROR_MAIN = {mainRef: 'main'};
495
508
  let staleLockEntries: string[];
496
509
  let lockHeld: LockEntry[];
497
- if (options.reconcileLocks === true) {
510
+ if (
511
+ options.reconcileLocks === true &&
512
+ mirrorInReconcileScope(
513
+ mirror.path,
514
+ options.reconcileArbiterKey,
515
+ options.env,
516
+ )
517
+ ) {
498
518
  const swept = await reconcileTerminalItemLocks(
499
519
  mirror.path,
500
520
  'origin',
@@ -612,6 +632,7 @@ export async function scan(
612
632
  staleLocks: staleLockEntries,
613
633
  });
614
634
  }
635
+ refresher.flush();
615
636
 
616
637
  return {
617
638
  repos,
@@ -668,6 +689,14 @@ export function scanRepoPaths(
668
689
  * handle); DEFAULTS empty.
669
690
  */
670
691
  heldSpecSlugs: Set<string> = new Set(),
692
+ /**
693
+ * The held TASK slugs whose lock a finished propose build keeps (the
694
+ * `propose-pr` marker): passed to {@link gatherLifecycleInPlace}, which lets
695
+ * such a slug through the lifecycle subtraction when its merge question is
696
+ * answered `merge` (task `wire-merge-questions-into-the-advance-tick`,
697
+ * decision 6). The build pool is unaffected. DEFAULTS empty.
698
+ */
699
+ proposeKeptSlugs: Set<string> = new Set(),
671
700
  ): ScanReport {
672
701
  const repos: RepoReport[] = [];
673
702
  const counts = {totalItems: 0, totalEligible: 0};
@@ -705,6 +734,7 @@ export function scanRepoPaths(
705
734
  // symmetric to the `scoreItems` subtraction above, so a held task never
706
735
  // leaks into a surface/apply leg either.
707
736
  heldSlugs,
737
+ proposeKeptSlugs,
708
738
  }),
709
739
  );
710
740
  // The one-slug-one-folder LINT over THIS working tree's `work/` ledger.
package/src/sidecar.ts CHANGED
@@ -144,8 +144,23 @@ export interface SidecarEntry {
144
144
  * kind-based SUBFOLDERS.
145
145
  */
146
146
  kind?: SidecarKind;
147
+ /**
148
+ * The arbiter `main` commit this question was ASKED AGAINST (a full hex
149
+ * sha), set on a `kind: merge` entry by the merge-question surfacer and by
150
+ * the `strictMergeApproval` re-surface path (task
151
+ * `strict-merge-approval-restale-check-runs-before-the-continue-rebase`).
152
+ * With `strictMergeApproval` on, an answered merge re-surfaces when `main`'s
153
+ * code moved since this commit. Absent on every other entry and on a merge
154
+ * entry written before the field existed (which then never re-stales).
155
+ * Carried in the per-entry HTML comment as `askedAtMain=<sha>`; a malformed
156
+ * value reads as absent.
157
+ */
158
+ askedAtMain?: string;
147
159
  }
148
160
 
161
+ /** A full git object name: 40 hex (SHA-1) or 64 hex (SHA-256). */
162
+ const FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
163
+
149
164
  /** The parsed sidecar: identity frontmatter + ordered entries. */
150
165
  export interface SidecarModel {
151
166
  /** The NAMESPACED identity (`spec:autotask`, `task:foo`, `observation:bar`). */
@@ -434,6 +449,7 @@ interface EntryFields {
434
449
  id?: string;
435
450
  answered?: boolean;
436
451
  kind?: SidecarKind;
452
+ askedAtMain?: string;
437
453
  }
438
454
 
439
455
  /** Parse a per-entry `<!-- qN fields: key=val … -->` HTML comment. */
@@ -467,6 +483,11 @@ function parseEntryComment(line: string): EntryFields | undefined {
467
483
  if (SIDECAR_KINDS.has(value as SidecarKind)) {
468
484
  out.kind = value as SidecarKind;
469
485
  }
486
+ } else if (key === 'askedAtMain') {
487
+ // Silent-on-malformed, as for `kind`: only a full sha is kept.
488
+ if (FULL_SHA.test(value)) {
489
+ out.askedAtMain = value;
490
+ }
470
491
  }
471
492
  }
472
493
  return out;
@@ -481,6 +502,7 @@ function parseEntrySection(lines: string[]): SidecarEntry {
481
502
  let defaultVal: string | undefined;
482
503
  let answeredOverride: boolean | undefined;
483
504
  let kind: SidecarKind | undefined;
505
+ let askedAtMain: string | undefined;
484
506
  let answerStart = -1;
485
507
 
486
508
  for (let i = 0; i < lines.length; i++) {
@@ -500,6 +522,9 @@ function parseEntrySection(lines: string[]): SidecarEntry {
500
522
  if (entryFields.kind !== undefined) {
501
523
  kind = entryFields.kind;
502
524
  }
525
+ if (entryFields.askedAtMain !== undefined) {
526
+ askedAtMain = entryFields.askedAtMain;
527
+ }
503
528
  inBlockquote = false;
504
529
  continue;
505
530
  }
@@ -568,6 +593,9 @@ function parseEntrySection(lines: string[]): SidecarEntry {
568
593
  if (kind !== undefined) {
569
594
  entry.kind = kind;
570
595
  }
596
+ if (askedAtMain !== undefined) {
597
+ entry.askedAtMain = askedAtMain;
598
+ }
571
599
  return entry;
572
600
  }
573
601
 
@@ -718,6 +746,9 @@ export function serialiseSidecar(
718
746
  if (entry.kind !== undefined) {
719
747
  fields.push(`kind=${entry.kind}`);
720
748
  }
749
+ if (entry.askedAtMain !== undefined && FULL_SHA.test(entry.askedAtMain)) {
750
+ fields.push(`askedAtMain=${entry.askedAtMain}`);
751
+ }
721
752
  out.push(`<!-- ${entry.id} fields: ${fields.join(' ')} -->`);
722
753
  out.push('');
723
754
  out.push(ANSWER_MARKER);
@@ -775,6 +806,8 @@ export interface NewQuestion {
775
806
  * agentic `decide()` path. INTERIM — removable once kind-subfolders land.
776
807
  */
777
808
  kind?: SidecarKind;
809
+ /** The arbiter `main` sha the question is asked against (see {@link SidecarEntry.askedAtMain}). */
810
+ askedAtMain?: string;
778
811
  }
779
812
 
780
813
  /**
@@ -802,6 +835,9 @@ export function appendQuestions(
802
835
  if (q.kind !== undefined) {
803
836
  entry.kind = q.kind;
804
837
  }
838
+ if (q.askedAtMain !== undefined) {
839
+ entry.askedAtMain = q.askedAtMain;
840
+ }
805
841
  entries.push(entry);
806
842
  }
807
843
  return {...model, entries};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The **old single-job workflow warning** (decision 9 of ADR
3
+ * `ci-agent-job-holds-no-write-token`, task `ci-split-warn-single-job-workflows`).
4
+ *
5
+ * A consumer that upgrades dorfl but keeps a workflow generated before the CI
6
+ * split runs an agent verb in ONE job whose checkout persisted the write token
7
+ * in `.git/config`. The agent can read that credential. dorfl cannot fix the
8
+ * workflow from inside it, so it warns loudly: in GitHub Actions, when an
9
+ * agent-spawning verb ({@link AGENT_SPAWNING_VERBS}) runs WITHOUT `--phase` in a
10
+ * checkout whose local git config holds a credential. Behaviour is otherwise
11
+ * unchanged; the next minor version turns this warning into a refusal (an
12
+ * intent recorded in the warning text and the changeset, not in code).
13
+ *
14
+ * The credential itself is NEVER printed: detection returns only a boolean.
15
+ */
16
+
17
+ import {inGitHubActions} from './agent-env.js';
18
+ import {run} from './git.js';
19
+ import {AGENT_SPAWNING_VERBS, type Phase} from './phase.js';
20
+
21
+ /**
22
+ * Agent-spawning verbs this warning deliberately skips. `verify` is in
23
+ * {@link AGENT_SPAWNING_VERBS} because it runs repository code, but it launches
24
+ * no agent, and the generated `verify.yml` persists only a `contents: read`
25
+ * credential. Warning there would flag every private consumer's PR check, and
26
+ * the planned refusal would break it. Task `ci-split-generate-workflows` (the
27
+ * workflow guard) must apply the same exemption.
28
+ */
29
+ export const SINGLE_JOB_WARNING_EXEMPT_VERBS: ReadonlySet<string> = new Set([
30
+ 'verify',
31
+ ]);
32
+
33
+ /** An HTTP(S) URL carrying userinfo (`https://user:token@host/...`). */
34
+ const URL_WITH_USERINFO = /https?:\/\/[^/@\s]+@/i;
35
+
36
+ /** An `http.extraheader` / `http.<url>.extraheader` key. */
37
+ const EXTRAHEADER_KEY = /^http\.(?:.*\.)?extraheader$/i;
38
+
39
+ /**
40
+ * Does this `git config --null --list` output hold a credential: a non-empty
41
+ * `http.*.extraheader`, or any value that is an HTTP(S) URL with userinfo?
42
+ * Returns a boolean only, so no caller can print the secret by accident.
43
+ */
44
+ export function gitConfigListHoldsCredential(nullList: string): boolean {
45
+ for (const entry of nullList.split('\0')) {
46
+ if (entry === '') {
47
+ continue;
48
+ }
49
+ const newline = entry.indexOf('\n');
50
+ const key = newline === -1 ? entry : entry.slice(0, newline);
51
+ const value = newline === -1 ? '' : entry.slice(newline + 1);
52
+ if (EXTRAHEADER_KEY.test(key) && value.trim() !== '') {
53
+ return true;
54
+ }
55
+ if (URL_WITH_USERINFO.test(value)) {
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Does the checkout at `cwd` persist a credential in its LOCAL git config
64
+ * (`.git/config` and the files it includes: `actions/checkout` v6+ writes the
65
+ * header to a separate file pulled in by an `includeIf`)? Global, system and
66
+ * environment (`GIT_CONFIG_*`) config are not read: the agent phase hands its
67
+ * read token to dorfl's git through the environment, which is not a persisted
68
+ * credential. Any failure (not a repository, no git) answers `false`: the
69
+ * warning never breaks a run.
70
+ */
71
+ export function checkoutPersistsCredential(cwd: string): boolean {
72
+ try {
73
+ const result = run(
74
+ 'git',
75
+ ['config', '--local', '--includes', '--null', '--list'],
76
+ cwd,
77
+ );
78
+ return result.status === 0 && gitConfigListHoldsCredential(result.stdout);
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ /** The warning text (never contains the credential). */
85
+ export function singleJobWarningText(verb: string): string {
86
+ return [
87
+ '',
88
+ '!! WARNING: unsafe single-job CI workflow',
89
+ `!! 'dorfl ${verb}' is running in GitHub Actions without --phase, in a checkout`,
90
+ '!! whose git config persists a credential. Any agent this run launches can read',
91
+ '!! and use that token. Re-run `dorfl install-ci` to upgrade to the split',
92
+ '!! lock / agent / apply workflow, where the agent job holds no write token.',
93
+ '!! The next minor version of dorfl will REFUSE to run in this shape.',
94
+ '',
95
+ ].join('\n');
96
+ }
97
+
98
+ /** Inputs to {@link singleJobWarning}. */
99
+ export interface SingleJobWarningInput {
100
+ /** The top-level CLI verb that is about to run. */
101
+ verb: string;
102
+ /** Its `--phase`, or `undefined` when none was given. */
103
+ phase: Phase | undefined;
104
+ env: NodeJS.ProcessEnv;
105
+ /** The checkout to inspect. */
106
+ cwd: string;
107
+ /** Injectable credential probe (default {@link checkoutPersistsCredential}). */
108
+ persistsCredential?: (cwd: string) => boolean;
109
+ }
110
+
111
+ /**
112
+ * The warning to print for this invocation, or `undefined` when the shape is
113
+ * safe (a `--phase` run, not in GitHub Actions, a verb that spawns no agent or
114
+ * is exempt, or no persisted credential). The git config is read last, only
115
+ * when every cheap condition already says "unsafe".
116
+ */
117
+ export function singleJobWarning(
118
+ input: SingleJobWarningInput,
119
+ ): string | undefined {
120
+ const {verb, phase, env, cwd} = input;
121
+ if (phase !== undefined) {
122
+ return undefined;
123
+ }
124
+ if (!AGENT_SPAWNING_VERBS.has(verb)) {
125
+ return undefined;
126
+ }
127
+ if (SINGLE_JOB_WARNING_EXEMPT_VERBS.has(verb)) {
128
+ return undefined;
129
+ }
130
+ if (!inGitHubActions(env)) {
131
+ return undefined;
132
+ }
133
+ const probe = input.persistsCredential ?? checkoutPersistsCredential;
134
+ return probe(cwd) ? singleJobWarningText(verb) : undefined;
135
+ }
@@ -12,8 +12,13 @@ import {
12
12
  /**
13
13
  * The read-only **"is this spec complete?"** core query (spec `issue-intake`, US #8 —
14
14
  * the closure-linkage half). Given a spec slug + a `work/` tree, a spec is COMPLETE
15
- * iff there is **≥1 task carrying `spec:<slug>`** AND **all such tasks reside in
16
- * `work/done/`**. Pure `work/`-folder logic — no seam, no git, no `gh`, no mutation.
15
+ * iff there is **≥1 task carrying `spec:<slug>` in `work/tasks/done/`** AND **every
16
+ * other such task is in `work/tasks/cancelled/`**. A task that names the spec
17
+ * ANYWHERE else (staged in `tasks/backlog/`, in `tasks/ready/`, in progress) keeps
18
+ * the spec incomplete; a cancelled task does not block completion, but a spec whose
19
+ * tasks are ALL cancelled (none done) is NOT complete (reported as
20
+ * {@link SpecCompleteResult.allCancelled}, so the close-job never closes its issue
21
+ * as `completed`). Pure `work/`-folder logic — no seam, no git, no `gh`, no mutation.
17
22
  *
18
23
  * This is the LINKAGE the intake engine emits for CI to ACT on: a spec fans out to N
19
24
  * tasks = N PRs whose tasks carry `spec:` ONLY (no `Refs #N` keyword is emitted),
@@ -26,9 +31,11 @@ import {
26
31
  * It is a `work/`-FOLDER RESIDENCE scan keyed on the parsed `spec:` field — NOT the
27
32
  * claim ledger (`ledger-read.ts` resolves claim-STATE, a different concern). It
28
33
  * reuses {@link parseFrontmatter} (the `spec:` field) rather than hand-rolling a YAML
29
- * parse, and scans the task lifecycle folders directly: `work/tasks/ready/`,
30
- * `work/in-progress/`, and `work/done/`. A task that has NOT yet landed in
31
- * `work/done/` (still in backlog / in-progress) means the spec is not yet complete.
34
+ * parse, and scans EVERY task lifecycle folder directly ({@link TASK_LIFECYCLE_FOLDERS}:
35
+ * backlog, ready, in-progress, done, cancelled). Task
36
+ * `spec-complete-counts-staged-and-cancelled-tasks`: the scan used to skip
37
+ * `tasks/backlog/`, so a spec whose tasks were still staged read as complete as
38
+ * soon as its first task landed.
32
39
  */
33
40
 
34
41
  /** The task lifecycle folders a `spec:<slug>` task can reside in. */
@@ -62,11 +69,18 @@ export interface SpecTask {
62
69
  */
63
70
  export interface SpecCompleteResult {
64
71
  /**
65
- * `true` iff ≥1 task carries `spec:<slug>` AND every such task resides in
66
- * `work/done/`. `false` when no task carries the slug (≥1 is REQUIRED) OR when
67
- * any matching task is still outside `work/done/`.
72
+ * `true` iff ≥1 `spec:<slug>` task resides in `work/tasks/done/` AND every other
73
+ * such task resides in `work/tasks/cancelled/`. `false` when no task carries the
74
+ * slug, when any matching task is still open (backlog / ready / in-progress), or
75
+ * when every matching task is cancelled (see {@link allCancelled}).
68
76
  */
69
77
  complete: boolean;
78
+ /**
79
+ * `true` iff ≥1 task carries `spec:<slug>` AND every one of them is in
80
+ * `work/tasks/cancelled/` (none done): the spec did not complete, it was
81
+ * abandoned. Never `true` together with {@link complete}.
82
+ */
83
+ allCancelled: boolean;
70
84
  /** Every task carrying `spec:<slug>`, across all task folders, sorted by slug. */
71
85
  tasks: SpecTask[];
72
86
  }
@@ -84,10 +98,10 @@ function listMarkdown(repoPath: string, folder: TaskFolder): string[] {
84
98
  }
85
99
 
86
100
  /**
87
- * Is this spec COMPLETE? Read-only: scan the task lifecycle folders, parse each
101
+ * Is this spec COMPLETE? Read-only: scan every task lifecycle folder, parse each
88
102
  * task's `spec:` via {@link parseFrontmatter}, keep those whose `spec:` equals
89
- * `slug`, and return COMPLETE iff that set is NON-EMPTY and EVERY member resides in
90
- * `work/done/`. Touches no git, no network, no mutation — a pure `work/`-folder
103
+ * `slug`, and return COMPLETE iff ≥1 member resides in `work/tasks/done/` and EVERY
104
+ * member resides in `work/tasks/done/` or `work/tasks/cancelled/`. Touches no git, no network, no mutation — a pure `work/`-folder
91
105
  * residence scan keyed on the parsed `spec:` field.
92
106
  */
93
107
  export function isSpecComplete(input: SpecCompleteInput): SpecCompleteResult {
@@ -111,7 +125,15 @@ export function isSpecComplete(input: SpecCompleteInput): SpecCompleteResult {
111
125
  }
112
126
  tasks.sort((a, b) => a.slug.localeCompare(b.slug));
113
127
 
114
- // COMPLETE iff ≥1 such task AND every one of them is in `work/done/`.
115
- const complete = tasks.length > 0 && tasks.every((s) => s.folder === 'done');
116
- return {complete, tasks};
128
+ // COMPLETE iff ≥1 such task is done AND every one of them is done or
129
+ // cancelled. A staged/ready/in-progress task keeps it open; an all-cancelled
130
+ // set is NOT complete (nothing was delivered).
131
+ const settled = tasks.every(
132
+ (s) => s.folder === 'done' || s.folder === 'cancelled',
133
+ );
134
+ const anyDone = tasks.some((s) => s.folder === 'done');
135
+ const complete = settled && anyDone;
136
+ const allCancelled =
137
+ tasks.length > 0 && tasks.every((s) => s.folder === 'cancelled');
138
+ return {complete, allCancelled, tasks};
117
139
  }