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
package/src/tasking.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  writeFileSync,
8
8
  } from 'node:fs';
9
9
  import {basename, dirname, join} from 'node:path';
10
- import {parseFrontmatter} from './frontmatter.js';
10
+ import {parseFrontmatter, unquoteYamlScalar} from './frontmatter.js';
11
11
  import {runAsync, type RunResult} from './git.js';
12
12
  import {
13
13
  workFolderRel,
@@ -18,10 +18,11 @@ import {
18
18
  } from './work-layout.js';
19
19
  import {
20
20
  performIntegration,
21
+ proposePointer,
21
22
  type IntegrationCoreResult,
22
23
  } from './integration-core.js';
23
24
  import type {IntegrationMode} from './config.js';
24
- import type {ReviewProvider} from './integrator.js';
25
+ import {proposeRequestNotOpened, type ReviewProvider} from './integrator.js';
25
26
  import {
26
27
  resolveTaskingEligibility,
27
28
  type TaskingEligibilityResult,
@@ -35,6 +36,7 @@ import {
35
36
  type ReleaseTaskingLockResult,
36
37
  } from './tasking-lock.js';
37
38
  import {releaseItemLock} from './item-lock.js';
39
+ import {refWrite} from './ref-write.js';
38
40
  import {NullHarness, type Harness} from './harness.js';
39
41
  import {launchWithOptionalWatch} from './agent-launch.js';
40
42
  import {placementFolder, resolvePlacement} from './placement.js';
@@ -511,32 +513,20 @@ export async function performTask(
511
513
  await switchToWorkBranch(cwd, arbiter, slug, env);
512
514
  }
513
515
 
514
- // 3. INVOKE THE AGENT with the to-task spec. It WRITES
515
- // `work/tasks/backlog/*.md` task files (the STAGED area — NOT `work/tasks/ready/`,
516
- // which is the agent-eligible pool the runner owns the promotion into; task
517
- // `pre-backlog-staging-folder-and-promote-step-a`); it does NO git. We
518
- // snapshot the staged-tasks folder before/after so the runner (not the
519
- // agent) captures + commits exactly what was produced.
520
- const before = snapshotStagedTasks(cwd);
521
- // Also snapshot the POOL `work/tasks/ready/` BEFORE the agent runs: the runner's
522
- // final commit must scrub any agent writes there (an attempt to self-place into
523
- // the pool, spec US #4) before `git add -A` would sweep them in.
524
- const poolBefore = snapshotPool(cwd);
525
- // Read the parent-spec self-pointer off `specFm.spec` (populated from the
526
- // `spec:` key).
527
- const prompt = buildTaskingSpec(slug, specFm.spec);
528
- let agent: {ok: boolean; detail?: string};
529
- try {
530
- agent = await runTaskAgent(options, cwd, prompt, slug);
531
- } catch (err) {
532
- agent = {
533
- ok: false,
534
- detail: err instanceof Error ? err.message : String(err),
535
- };
536
- }
537
- if (!agent.ok) {
538
- const detail = agent.detail ?? `the agent failed to task '${slug}'.`;
539
- let message = `Agent failed tasking '${slug}' (${detail}).`;
516
+ // 3. + 3.5 THE AGENT HALF (task `ci-split-tasking`): the tasker agent and the
517
+ // tasker review→edit→converge loop. Shared, unchanged, with the CI agent
518
+ // phase (`ci-phase-tasking.ts`), which runs this SAME half in the job that
519
+ // holds no write token and hands the result over instead of integrating.
520
+ const half = await runTaskingAgentHalf(options, {
521
+ cwd,
522
+ slug,
523
+ specFm,
524
+ doer,
525
+ note,
526
+ });
527
+
528
+ if (half.kind === 'agent-failed') {
529
+ let message = `Agent failed tasking '${slug}' (${half.detail}).`;
540
530
  // RELEASE THE LOCK THE FAILED RUN TOOK (observation
541
531
  // `crashed-do-spec-strands-a-tasking-lock-no-verb-releases`).
542
532
  //
@@ -592,13 +582,218 @@ export async function performTask(
592
582
  return {exitCode: 1, outcome: 'agent-failed', slug, message};
593
583
  }
594
584
 
585
+ if (half.kind === 'review-leg-failed') {
586
+ // REVIEW-LEG FAILURE handling (observation
587
+ // `tasker-review-edits-payload-caps-the-verdict-response`): PERSIST the
588
+ // candidate tasks, and BOUNCE through the SAME surface the
589
+ // decomposition-unclear path uses (release the lock + a question sidecar).
590
+ // NEVER a silent approve — a parse failure is always a needs-attention route.
591
+ const candidatePaths = await persistTaskingCandidates(
592
+ cwd,
593
+ slug,
594
+ half.before,
595
+ arbiter,
596
+ env,
597
+ note,
598
+ );
599
+ const reason = reviewFailureReason(slug, half.error, candidatePaths);
600
+ const message = reviewFailureMessage(slug, half.error);
601
+ if (useLock) {
602
+ return await surfaceTaskingBlock({
603
+ slug,
604
+ cwd,
605
+ arbiter,
606
+ reason,
607
+ message,
608
+ lockedBlob,
609
+ release: lock.release,
610
+ mode: resolvedMode,
611
+ provider: options.providerInstance,
612
+ env,
613
+ note,
614
+ });
615
+ }
616
+ note(reason);
617
+ // Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
618
+ return {exitCode: 0, outcome: 'needs-attention', slug, message};
619
+ }
620
+
621
+ if (half.kind === 'decomposition-unclear') {
622
+ // DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
623
+ // needs-attention with the questions as the reason. The lock release surfaces
624
+ // the spec (a question sidecar + `needsAnswers: true` on main) and releases the
625
+ // `spec:<slug>` lock; the spec body stays in `work/specs/ready/`.
626
+ const reason = decompositionUnclearReason(slug, half.questions);
627
+ if (useLock) {
628
+ return await surfaceTaskingBlock({
629
+ slug,
630
+ cwd,
631
+ arbiter,
632
+ reason,
633
+ message: half.message,
634
+ lockedBlob,
635
+ release: lock.release,
636
+ mode: resolvedMode,
637
+ provider: options.providerInstance,
638
+ env,
639
+ note,
640
+ });
641
+ }
642
+ note(half.message);
643
+ // Human, no-lock path: nothing to release/surface via the seam; a clean
644
+ // park-for-human is still a success terminal (exit 0).
645
+ return {
646
+ exitCode: 0,
647
+ outcome: 'needs-attention',
648
+ slug,
649
+ message: half.message,
650
+ };
651
+ }
652
+
653
+ // 4. The RUNNER commits the COMPLETING transition: drop the produced backlog
654
+ // tasks IN + move the spec specs/ready/ -> specs/tasked/ (residence = tasked-ness) — now
655
+ // through the SHARED integrate core (`--propose` PR / `--merge` main), NOT a
656
+ // direct commit to `main`. The agent never does git. (The backlog snapshot is
657
+ // taken AFTER any loop edits, so the runner integrates the IMPROVED tasks,
658
+ // not the pre-loop candidates.)
659
+ const stagedEmitted = newOrChangedStagedTasks(cwd, half.before);
660
+ const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
661
+ const placementDir = taskPlacementDir(options, specFm);
662
+ const loopTag = half.loopTag;
663
+
664
+ if (useLock) {
665
+ return await integrateTaskingCandidates({
666
+ slug,
667
+ cwd,
668
+ arbiter,
669
+ stagedEmitted,
670
+ emitTasks,
671
+ poolBefore: half.poolBefore,
672
+ placementDir,
673
+ loopTag,
674
+ lockedBlob,
675
+ release: lock.release,
676
+ mode: resolvedMode,
677
+ review: options.review,
678
+ reviewGate: options.reviewGate,
679
+ acceptanceReviewModel: options.acceptanceReviewModel,
680
+ mergeRetries: options.mergeRetries,
681
+ noPR: options.noPR,
682
+ providerInstance: options.providerInstance,
683
+ env,
684
+ agentEnv,
685
+ note,
686
+ });
687
+ }
688
+
689
+ // HUMAN, no-lock path: the human commits on `main` directly (the runner does
690
+ // not own the human's git). We report the produced tasks; moving the spec into
691
+ // `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
692
+ // do, as with the human `complete`.
693
+ const emitted = stagedEmitted.map(
694
+ (rel) => `${placementDir}/${basename(rel)}`,
695
+ );
696
+ const message =
697
+ `Tasked '${slug}' -> ${emitted.length} backlog task` +
698
+ `${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
699
+ `the produced files (and move the spec into work/specs/tasked/) yourself.`;
700
+ note(message);
701
+ return {
702
+ exitCode: 0,
703
+ outcome: 'tasked',
704
+ slug,
705
+ emitted,
706
+ loop: loopTag,
707
+ message,
708
+ };
709
+ }
710
+
711
+ /**
712
+ * How {@link runTaskingAgentHalf} ended (task `ci-split-tasking`): the data the
713
+ * write half acts on. The laptop `performTask` acts on it in-process; the CI
714
+ * agent phase (`ci-phase-tasking.ts`) hands it over to the apply phase.
715
+ */
716
+ export type TaskingAgentHalf =
717
+ /** The tasker agent itself errored (nothing was produced to trust). */
718
+ | {kind: 'agent-failed'; detail: string}
719
+ /**
720
+ * The improver loop's review leg returned no parseable verdict; the candidate
721
+ * tasks (new-or-changed vs `before`) are on disk for recovery.
722
+ */
723
+ | {
724
+ kind: 'review-leg-failed';
725
+ error: ReviewParseError;
726
+ before: Map<string, string>;
727
+ }
728
+ /** The improver loop found the decomposition unclear: no tasks land. */
729
+ | {kind: 'decomposition-unclear'; questions: string[]; message: string}
730
+ /** The candidate tasks are on disk (improved, uncertain ones marked). */
731
+ | {
732
+ kind: 'candidates';
733
+ /** The staged-tasks snapshot taken BEFORE the agent ran (the fence). */
734
+ before: Map<string, string>;
735
+ /** The pool snapshot taken BEFORE the agent ran (the scrub fence). */
736
+ poolBefore: Map<string, string>;
737
+ loopTag: 'converged' | 'uncertain-tasks' | undefined;
738
+ };
739
+
740
+ /**
741
+ * The AGENT half of the tasking path (steps 3 and 3.5): run the tasker agent in
742
+ * `cwd` (on the work branch), then, when a loop seam is wired and the doer is
743
+ * the agent, the tasker review→edit→converge loop, and report what is on disk.
744
+ * It performs NO write to the arbiter and no git transition: only the agents'
745
+ * file edits, the loop's local edits and markers, and the scratch cleanup.
746
+ */
747
+ export async function runTaskingAgentHalf(
748
+ options: PerformTaskOptions,
749
+ params: {
750
+ cwd: string;
751
+ slug: string;
752
+ specFm: {spec?: string};
753
+ doer: 'agent' | 'human';
754
+ note: (message: string) => void;
755
+ },
756
+ ): Promise<TaskingAgentHalf> {
757
+ const {cwd, slug, specFm, doer, note} = params;
758
+ const agentEnv = options.agentEnv ?? options.env;
759
+
760
+ // 3. INVOKE THE AGENT with the to-task spec. It WRITES
761
+ // `work/tasks/backlog/*.md` task files (the STAGED area — NOT `work/tasks/ready/`,
762
+ // which is the agent-eligible pool the runner owns the promotion into; task
763
+ // `pre-backlog-staging-folder-and-promote-step-a`); it does NO git. We
764
+ // snapshot the staged-tasks folder before/after so the runner (not the
765
+ // agent) captures + commits exactly what was produced.
766
+ const before = snapshotStagedTasks(cwd);
767
+ // Also snapshot the POOL `work/tasks/ready/` BEFORE the agent runs: the runner's
768
+ // final commit must scrub any agent writes there (an attempt to self-place into
769
+ // the pool, spec US #4) before `git add -A` would sweep them in.
770
+ const poolBefore = snapshotPool(cwd);
771
+ // Read the parent-spec self-pointer off `specFm.spec` (populated from the
772
+ // `spec:` key).
773
+ const prompt = buildTaskingSpec(slug, specFm.spec);
774
+ let agent: {ok: boolean; detail?: string};
775
+ try {
776
+ agent = await runTaskAgent(options, cwd, prompt, slug);
777
+ } catch (err) {
778
+ agent = {
779
+ ok: false,
780
+ detail: err instanceof Error ? err.message : String(err),
781
+ };
782
+ }
783
+ if (!agent.ok) {
784
+ return {
785
+ kind: 'agent-failed',
786
+ detail: agent.detail ?? `the agent failed to task '${slug}'.`,
787
+ };
788
+ }
789
+
595
790
  // 3.5 THE TASKER REVIEW→EDIT→CONVERGE LOOP (`slicer-review-edit-loop`, Shape 2 /
596
791
  // insertion point A): when a loop seam is wired, run the `review` SKILL as a
597
792
  // review→edit→re-review loop that IMPROVES the candidate tasks in place, then
598
793
  // determines the disposition (the three outcomes). This plugs in AFTER the
599
794
  // candidate tasks are produced and BEFORE they are finalised. The agent makes
600
795
  // the review/edit JUDGEMENTS; the loop applies edits to the candidate files
601
- // and routes the verdict; the runner (below) owns the git transition. Only the
796
+ // and routes the verdict; the runner owns the git transition. Only the
602
797
  // AGENT path runs the loop — the human tasking path is unaffected.
603
798
  let loopDisposition: RunTaskReviewLoopResult | undefined;
604
799
  if (options.reviewLoop && doer === 'agent') {
@@ -608,10 +803,8 @@ export async function performTask(
608
803
  // parseable verdict (a generic parse failure, or the NAMED cap-truncation
609
804
  // class). The pre-fix behaviour THREW here, crashing the run, LEAVING THE
610
805
  // LOCK HELD, writing NO question sidecar, and DISCARDING the tasker's
611
- // candidate tasks. Now: catch it, PERSIST the candidate tasks, and BOUNCE
612
- // through the SAME surface the decomposition-unclear path uses (release the
613
- // lock + a question sidecar). NEVER a silent approve — a parse failure is
614
- // always a needs-attention route. A non-review throw (genuine wiring error)
806
+ // candidate tasks. Now: catch it and report it, so the write half PERSISTS
807
+ // the candidate tasks and BOUNCES. A non-review throw (genuine wiring error)
615
808
  // is re-thrown.
616
809
  try {
617
810
  loopDisposition = await runTaskReviewLoop({
@@ -634,7 +827,6 @@ export async function performTask(
634
827
  if (!(err instanceof ReviewParseError)) {
635
828
  throw err;
636
829
  }
637
- const reviewErr = err as ReviewParseError;
638
830
  // Reap any review-edits SCRATCH the failed pass left on disk so it is never
639
831
  // swept into a later run's integrate commit.
640
832
  try {
@@ -645,66 +837,12 @@ export async function performTask(
645
837
  } catch {
646
838
  // Best-effort.
647
839
  }
648
- const candidatePaths = await persistTaskingCandidates(
649
- cwd,
650
- slug,
651
- before,
652
- arbiter,
653
- env,
654
- note,
655
- );
656
- const reason = reviewFailureReason(slug, reviewErr, candidatePaths);
657
- const message = reviewFailureMessage(slug, reviewErr);
658
- if (useLock) {
659
- return await surfaceTaskingBlock({
660
- slug,
661
- cwd,
662
- arbiter,
663
- reason,
664
- message,
665
- lockedBlob,
666
- release: lock.release,
667
- mode: resolvedMode,
668
- provider: options.providerInstance,
669
- env,
670
- note,
671
- });
672
- }
673
- note(reason);
674
- // Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
675
- return {exitCode: 0, outcome: 'needs-attention', slug, message};
840
+ return {kind: 'review-leg-failed', error: err, before};
676
841
  }
677
- // DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
678
- // needs-attention with the questions as the reason. The lock release amends the
679
- // `spec:<slug>` unified lock `active → stuck` (the tasking needs-attention surface
680
- // is the stuck lock now — NO folder write; the spec body stays in `work/specs/ready/`).
681
842
  if (loopDisposition.outcome === 'decomposition-unclear') {
682
- const reason = decompositionUnclearReason(
683
- slug,
684
- loopDisposition.specQuestions,
685
- );
686
- if (useLock) {
687
- return await surfaceTaskingBlock({
688
- slug,
689
- cwd,
690
- arbiter,
691
- reason,
692
- message: loopDisposition.message,
693
- lockedBlob,
694
- release: lock.release,
695
- mode: resolvedMode,
696
- provider: options.providerInstance,
697
- env,
698
- note,
699
- });
700
- }
701
- note(loopDisposition.message);
702
- // Human, no-lock path: nothing to release/surface via the seam; a clean
703
- // park-for-human is still a success terminal (exit 0).
704
843
  return {
705
- exitCode: 0,
706
- outcome: 'needs-attention',
707
- slug,
844
+ kind: 'decomposition-unclear',
845
+ questions: loopDisposition.specQuestions,
708
846
  message: loopDisposition.message,
709
847
  };
710
848
  }
@@ -717,31 +855,42 @@ export async function performTask(
717
855
  }
718
856
  }
719
857
  }
858
+ return {
859
+ kind: 'candidates',
860
+ before,
861
+ poolBefore,
862
+ loopTag:
863
+ loopDisposition?.outcome === 'converged'
864
+ ? 'converged'
865
+ : loopDisposition?.outcome === 'uncertain-tasks'
866
+ ? 'uncertain-tasks'
867
+ : undefined,
868
+ };
869
+ }
720
870
 
721
- // 4. The RUNNER commits the COMPLETING transition: drop the produced backlog
722
- // tasks IN + move the spec specs/ready/ -> specs/tasked/ (residence = tasked-ness) — now
723
- // through the SHARED integrate core (`--propose` PR / `--merge` main), NOT a
724
- // direct commit to `main`. The agent never does git. (The backlog snapshot is
725
- // taken AFTER any loop edits, so the runner integrates the IMPROVED tasks,
726
- // not the pre-loop candidates.)
727
- const stagedEmitted = newOrChangedStagedTasks(cwd, before);
728
- const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
729
- // RUNNER-DETERMINISTIC PLACEMENT (task
730
- // `runner-deterministic-slice-placement-policy-and-precedence`; the
731
- // untrusted-forces-staging rung was RETIRED by ADR
732
- // `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
733
- // folder the runner lands the emitted tasks in BEFORE handing them to the
734
- // shared integrate band: precedence `explicit > configured default >
735
- // built-in (staging)`. THIS caller selects the trusted-vs-untrusted
736
- // configured default by reading the spec's propagated `originTrust:` stamp:
737
- // an untrusted-origin spec selects `untrustedTasksLandIn` (default staging;
738
- // `ready` when configured), a trusted/unset spec selects `tasksLandIn`. One
739
- // knob (`untrustedTasksLandIn`) governs an untrusted task's destination
740
- // whether born from an issue directly (intake) or from an untrusted spec
741
- // here. The agent NEVER influences this; it always writes to
742
- // `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety
743
- // for an untrusted task landing in `ready` is the carried stamp (its BUILD is
744
- // forced to a code PR), not the folder.
871
+ /**
872
+ * The RUNNER-DETERMINISTIC PLACEMENT folder for a spec's emitted tasks (task
873
+ * `runner-deterministic-slice-placement-policy-and-precedence`; the
874
+ * untrusted-forces-staging rung was RETIRED by ADR
875
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`): precedence
876
+ * `explicit > configured default > built-in (staging)`. This caller selects the
877
+ * trusted-vs-untrusted configured default by reading the spec's propagated
878
+ * `originTrust:` stamp: an untrusted-origin spec selects `untrustedTasksLandIn`
879
+ * (default staging; `ready` when configured), a trusted/unset spec selects
880
+ * `tasksLandIn`. One knob (`untrustedTasksLandIn`) governs an untrusted task's
881
+ * destination whether born from an issue directly (intake) or from an untrusted
882
+ * spec here. The agent NEVER influences this; it always writes to
883
+ * `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety for
884
+ * an untrusted task landing in `ready` is the carried stamp (its BUILD is forced
885
+ * to a code PR), not the folder.
886
+ */
887
+ export function taskPlacementDir(
888
+ options: Pick<
889
+ PerformTaskOptions,
890
+ 'tasksLandIn' | 'untrustedTasksLandIn' | 'explicitTasksLandIn'
891
+ >,
892
+ specFm: {originTrust?: string},
893
+ ): string {
745
894
  const configuredTasksLanding =
746
895
  specFm.originTrust === 'untrusted'
747
896
  ? options.untrustedTasksLandIn
@@ -750,265 +899,289 @@ export async function performTask(
750
899
  explicit: landingToSide(options.explicitTasksLandIn),
751
900
  configuredDefault: landingToSide(configuredTasksLanding),
752
901
  });
753
- const placementDir = placementFolder(
754
- TASK_PLACEMENT_SLOTS,
755
- placementDecision.choice,
756
- );
902
+ return placementFolder(TASK_PLACEMENT_SLOTS, placementDecision.choice);
903
+ }
904
+
905
+ /** The inputs of {@link integrateTaskingCandidates}. */
906
+ export interface IntegrateTaskingInput {
907
+ slug: string;
908
+ cwd: string;
909
+ arbiter: string;
910
+ /** The candidate tasks' repo-relative paths under `work/tasks/backlog/`. */
911
+ stagedEmitted: string[];
912
+ /** Their content, keyed by those paths. */
913
+ emitTasks: Record<string, string>;
914
+ /** The pool snapshot the stage scrubs agent writes against. */
915
+ poolBefore: Map<string, string>;
916
+ /** The runner-resolved destination folder ({@link taskPlacementDir}). */
917
+ placementDir: string;
918
+ loopTag: 'converged' | 'uncertain-tasks' | undefined;
919
+ /** The spec blob the lock took (the read-stability backstop). */
920
+ lockedBlob: string | undefined;
921
+ release: TaskingLockSeam['release'];
922
+ mode: IntegrationMode;
923
+ /** Run the task-set acceptance gate inside the core (the laptop path). */
924
+ review?: boolean;
925
+ reviewGate?: ReviewGate;
926
+ acceptanceReviewModel?: string;
927
+ /**
928
+ * The approved task-set review prose when the review already ran elsewhere
929
+ * (the CI agent phase); posted as the PR comment. `review` is then off.
930
+ */
931
+ approvedReviewProse?: string;
932
+ mergeRetries?: number;
933
+ mergeJitterMs?: number;
934
+ noPR?: boolean;
935
+ providerInstance?: ReviewProvider;
936
+ env: NodeJS.ProcessEnv | undefined;
937
+ agentEnv: NodeJS.ProcessEnv | undefined;
938
+ note: (message: string) => void;
939
+ }
940
+
941
+ /**
942
+ * The WRITE half of a tasking run that produced candidate tasks (step 4): the
943
+ * read-stability stale check, the integrate through the shared core (the tasks
944
+ * + the `specs/ready → specs/tasked` move, `--propose` PR / `--merge` main), the
945
+ * needs-attention routing of a not-landed transition, and the mode-dependent lock
946
+ * release. Shared by the laptop `performTask` (review ON inside the core) and the
947
+ * CI apply phase (`ci-phase-tasking.ts`, review OFF: it ran in the agent phase).
948
+ */
949
+ export async function integrateTaskingCandidates(
950
+ params: IntegrateTaskingInput,
951
+ ): Promise<TaskResult> {
952
+ const {
953
+ slug,
954
+ cwd,
955
+ arbiter,
956
+ stagedEmitted,
957
+ emitTasks,
958
+ poolBefore,
959
+ placementDir,
960
+ loopTag,
961
+ lockedBlob,
962
+ mode: resolvedMode,
963
+ env,
964
+ agentEnv,
965
+ note,
966
+ } = params;
757
967
  // REWRITE the emitted list to the RUNNER-RESOLVED destination so callers see
758
968
  // where the runner actually placed the files (not where the agent wrote them).
759
969
  const emitted = stagedEmitted.map(
760
970
  (rel) => `${placementDir}/${basename(rel)}`,
761
971
  );
762
- const loopTag: 'converged' | 'uncertain-tasks' | undefined =
763
- loopDisposition?.outcome === 'converged'
764
- ? 'converged'
765
- : loopDisposition?.outcome === 'uncertain-tasks'
766
- ? 'uncertain-tasks'
767
- : undefined;
768
972
 
769
- if (useLock) {
770
- // READ-STABILITY BACKSTOP (the lock's content-identity check, now owned at the
771
- // integrate seam): the OUTPUT no longer rides the lock release, so the band
772
- // below would otherwise rebase a concurrent edit of the held spec body CLEANLY
773
- // into spec/ (a rename+edit merge) while the tasks were cut from the OLD body —
774
- // the exact silent stale-task drift the lock forbids
775
- // (`work/notes/observations/tasking-lock-does-not-stabilise-spec-content.md`). So we
776
- // compare the CURRENTLY held `work/specs/ready/<slug>.md` blob on the arbiter against
777
- // the snapshot the lock TOOK (`lockedBlob`); ANY change ⇒ STALE ⇒ fail loud,
778
- // touch NOTHING (the lock stays held; a human re-tasks or routes to
779
- // needs-attention). It is the SAME content-identity check `releaseTaskingLock`
780
- // runs — relocated here because this transition, not the release, owns the
781
- // completing commit now.
782
- const stale = await heldSpecIsStale(cwd, arbiter, slug, lockedBlob, env);
783
- if (stale) {
784
- const specRel = workItemRel('specs-ready', `${slug}.md`);
785
- const message =
786
- `RELEASE CONFLICT for '${slug}': the spec was edited (${specRel} ` +
787
- `changed on ${arbiter}/main) while the tasking lock was held. The tasking is ` +
788
- `STALE — re-task from the edited spec or route it to needs-attention. ` +
789
- `The arbiter was NOT modified (lock still held).`;
790
- note(message);
791
- return {exitCode: 4, outcome: 'stale', slug, message};
792
- }
973
+ // READ-STABILITY BACKSTOP (the lock's content-identity check, now owned at the
974
+ // integrate seam): the OUTPUT no longer rides the lock release, so the band
975
+ // below would otherwise rebase a concurrent edit of the held spec body CLEANLY
976
+ // into spec/ (a rename+edit merge) while the tasks were cut from the OLD body —
977
+ // the exact silent stale-task drift the lock forbids
978
+ // (`work/notes/observations/tasking-lock-does-not-stabilise-spec-content.md`). So we
979
+ // compare the CURRENTLY held `work/specs/ready/<slug>.md` blob on the arbiter against
980
+ // the snapshot the lock TOOK (`lockedBlob`); ANY change ⇒ STALE ⇒ fail loud,
981
+ // touch NOTHING (the lock stays held; a human re-tasks or routes to
982
+ // needs-attention). It is the SAME content-identity check `releaseTaskingLock`
983
+ // runs — relocated here because this transition, not the release, owns the
984
+ // completing commit now.
985
+ const stale = await heldSpecIsStale(cwd, arbiter, slug, lockedBlob, env);
986
+ if (stale) {
987
+ const specRel = workItemRel('specs-ready', `${slug}.md`);
988
+ const message =
989
+ `RELEASE CONFLICT for '${slug}': the spec was edited (${specRel} ` +
990
+ `changed on ${arbiter}/main) while the tasking lock was held. The tasking is ` +
991
+ `STALE — re-task from the edited spec or route it to needs-attention. ` +
992
+ `The arbiter was NOT modified (lock still held).`;
993
+ note(message);
994
+ return {exitCode: 4, outcome: 'stale', slug, message};
995
+ }
996
+
997
+ // Route the OUTPUT through the SHARED integrate back-half (task
998
+ // `slice-output-through-integration`): the produced backlog tasks + the spec
999
+ // lifecycle move (`work/specs/ready/ -> work/specs/tasked/`, residence = tasked-ness) integrate
1000
+ // via `performIntegration` honoring `--propose` (push the work branch + open a
1001
+ // PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
1002
+ // args resolve ONCE in the shared core, every `do task:` arg applies here by
1003
+ // construction. The agent did NO git; the runner (the band) owns the ONE commit.
1004
+ const core = await performIntegration({
1005
+ cwd,
1006
+ arbiter,
1007
+ slug,
1008
+ // `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set
1009
+ // (a tasking transition never recovers a surfaced needs-attention move).
1010
+ source: 'in-progress',
1011
+ recovering: false,
1012
+ // Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
1013
+ // no `verify` floor (the tasker review loop above is its quality gate).
1014
+ skipVerify: true,
1015
+ // THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
1016
+ // task-path mirror of the build Gate-2, riding THIS shared core's
1017
+ // review-before-integrate block. When `review` resolves on, the wired
1018
+ // `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
1019
+ // prompt) runs a FRESH-CONTEXT review of the produced task SET before it
1020
+ // integrates: `approve` lands it, `block` routes the set to needs-attention
1021
+ // via the SAME machinery the build block uses (mapped to the tasking
1022
+ // `needs-attention` outcome below). It is ONE-SHOT: we pin
1023
+ // `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
1024
+ // (terminal pass/fail). The task path NEVER exposes/consults
1025
+ // `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
1026
+ // that belongs to a future revise↔review loop (see
1027
+ // `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
1028
+ // independently controllable from the tasker improver loop (`reviewLoop` /
1029
+ // the `--tasker-loop*` family); toggling one does not affect the other.
1030
+ // The CI apply phase passes it OFF (it ran in the agent phase) and hands
1031
+ // its approved prose in as `approvedReviewProse`.
1032
+ review: params.review,
1033
+ reviewGate: params.reviewGate,
1034
+ reviewModel: params.acceptanceReviewModel,
1035
+ reviewMaxRounds: 1,
1036
+ approvedReviewProse: params.approvedReviewProse,
1037
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
1038
+ // threaded so a wide-matrix CI's raised cap reaches the tasking-
1039
+ // transition's land tail too (task
1040
+ // `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
1041
+ // through to the engine default (byte-for-byte unchanged).
1042
+ mergeRetries: params.mergeRetries,
1043
+ ...(params.mergeJitterMs === undefined
1044
+ ? {}
1045
+ : {mergeJitterMs: params.mergeJitterMs}),
1046
+ // The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
1047
+ // `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
1048
+ // path's merge-vs-propose decision is the `integration` mode the user typed;
1049
+ // `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
1050
+ // The task gate family is `--review`/`--no-review`/`--review-model` only
1051
+ // (spec US #6).
1052
+ mode: resolvedMode,
1053
+ noPR: params.noPR,
1054
+ providerInstance: params.providerInstance,
1055
+ // PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
1056
+ // BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
1057
+ // agent produces no `LaunchResult.output` we can carry, but the slice-SET
1058
+ // itself is a summary-worthy artifact: the emitted task slugs+titles, their
1059
+ // coverage of the spec's user stories, the dependency graph (keystone +
1060
+ // blockedBy edges), and any `needsAnswers` open questions the tasker review
1061
+ // loop flagged. Compose it here and thread it into the shared core as `body`
1062
+ // so the propose-mode PR carries the summary instead of degrading to
1063
+ // `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
1064
+ // provider's `--fill` fallback (no regression on that edge).
1065
+ body: composeTaskingProposeBody(slug, emitTasks),
1066
+ type: 'tasking',
1067
+ lifecycle: {
1068
+ // Read the PR title / commit summary from the held spec (before it moves).
1069
+ titlePath: workItemPath(cwd, 'specs-ready', slug),
1070
+ commitTag: 'tasked',
1071
+ // The spec RESTS in `work/specs/tasked/` once this lands (never
1072
+ // `work/tasks/done/`, the task-build default), so point the reviewer there.
1073
+ proposeHeader: proposePointer(
1074
+ 'Spec',
1075
+ workItemRel('specs-tasked', `${slug}.md`),
1076
+ ),
1077
+ stage: () =>
1078
+ stageTaskingLifecycle({
1079
+ cwd,
1080
+ slug,
1081
+ emitTasks,
1082
+ poolBefore,
1083
+ placementDir,
1084
+ note,
1085
+ env,
1086
+ }),
1087
+ },
1088
+ env,
1089
+ // The task-SET acceptance review AGENT launches AMBIENT, never the
1090
+ // identity-scoped `env` (an agent must not act as the bot).
1091
+ agentEnv,
1092
+ note,
1093
+ });
793
1094
 
794
- // Route the OUTPUT through the SHARED integrate back-half (task
795
- // `slice-output-through-integration`): the produced backlog tasks + the spec
796
- // lifecycle move (`work/specs/ready/ -> work/specs/tasked/`, residence = tasked-ness) integrate
797
- // via `performIntegration` honoring `--propose` (push the work branch + open a
798
- // PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
799
- // args resolve ONCE in the shared core, every `do task:` arg applies here by
800
- // construction. The agent did NO git; the runner (the band) owns the ONE commit.
801
- const core = await performIntegration({
1095
+ const surfaceBlock = (reason: string, message: string) =>
1096
+ surfaceTaskingBlock({
1097
+ slug,
802
1098
  cwd,
803
1099
  arbiter,
804
- slug,
805
- // `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set
806
- // (a tasking transition never recovers a surfaced needs-attention move).
807
- source: 'in-progress',
808
- recovering: false,
809
- // Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
810
- // no `verify` floor (the tasker review loop above is its quality gate).
811
- skipVerify: true,
812
- // THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
813
- // task-path mirror of the build Gate-2, riding THIS shared core's
814
- // review-before-integrate block. When `review` resolves on, the wired
815
- // `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
816
- // prompt) runs a FRESH-CONTEXT review of the produced task SET before it
817
- // integrates: `approve` lands it, `block` routes the set to needs-attention
818
- // via the SAME machinery the build block uses (mapped to the tasking
819
- // `needs-attention` outcome below). It is ONE-SHOT: we pin
820
- // `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
821
- // (terminal pass/fail). The task path NEVER exposes/consults
822
- // `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
823
- // that belongs to a future revise↔review loop (see
824
- // `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
825
- // independently controllable from the tasker improver loop (`reviewLoop` /
826
- // the `--tasker-loop*` family); toggling one does not affect the other.
827
- review: options.review,
828
- reviewGate: options.reviewGate,
829
- reviewModel: options.acceptanceReviewModel,
830
- reviewMaxRounds: 1,
831
- // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
832
- // threaded so a wide-matrix CI's raised cap reaches the tasking-
833
- // transition's land tail too (task
834
- // `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
835
- // through to the engine default (byte-for-byte unchanged).
836
- mergeRetries: options.mergeRetries,
837
- // The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
838
- // `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
839
- // path's merge-vs-propose decision is the `integration` mode the user typed;
840
- // `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
841
- // The task gate family is `--review`/`--no-review`/`--review-model` only
842
- // (spec US #6).
1100
+ reason,
1101
+ message,
1102
+ lockedBlob,
1103
+ release: params.release,
843
1104
  mode: resolvedMode,
844
- noPR: options.noPR,
845
- providerInstance: options.providerInstance,
846
- // PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
847
- // BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
848
- // agent produces no `LaunchResult.output` we can carry, but the slice-SET
849
- // itself is a summary-worthy artifact: the emitted task slugs+titles, their
850
- // coverage of the spec's user stories, the dependency graph (keystone +
851
- // blockedBy edges), and any `needsAnswers` open questions the tasker review
852
- // loop flagged. Compose it here and thread it into the shared core as `body`
853
- // so the propose-mode PR carries the summary instead of degrading to
854
- // `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
855
- // provider's `--fill` fallback (no regression on that edge).
856
- body: composeTaskingProposeBody(slug, emitTasks),
857
- type: 'tasking',
858
- lifecycle: {
859
- // Read the PR title / commit summary from the held spec (before it moves).
860
- titlePath: workItemPath(cwd, 'specs-ready', slug),
861
- commitTag: 'tasked',
862
- stage: () =>
863
- stageTaskingLifecycle({
864
- cwd,
865
- slug,
866
- emitTasks,
867
- poolBefore,
868
- placementDir,
869
- note,
870
- env,
871
- }),
872
- },
1105
+ provider: params.providerInstance,
873
1106
  env,
874
- // The task-SET acceptance review AGENT launches AMBIENT, never the
875
- // identity-scoped `env` (an agent must not act as the bot).
876
- agentEnv,
877
1107
  note,
878
1108
  });
879
1109
 
880
- // THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
881
- // fresh-context review of the produced SET returned `block`, so the core ran
882
- // the review BEFORE the stage/integrate and did NOT integrate the tasks
883
- // (correct). The CORRECT task-path destination is the SAME needs-attention
884
- // route the lock release owns for the decomposition-unclear verdict: it amends
885
- // the `spec:<slug>` unified lock `active -> stuck` with the block reason (the
886
- // tasking needs-attention surface is the stuck lock now — NO folder write; the
887
- // spec body stays in `work/specs/ready/`). So on a block we route the held spec to
888
- // needs-attention THROUGH the lock release — the set never lands.
889
- if (core.outcome === 'review-blocked') {
890
- const reason = taskGateBlockedReason(slug, core.reviewBlockReason);
891
- return await surfaceTaskingBlock({
892
- slug,
893
- cwd,
894
- arbiter,
895
- reason,
896
- message:
897
- `The task acceptance gate disapproved the set produced for '${slug}'; ` +
898
- `parked it for your attention (no tasks landed). ` +
899
- `Resolve the findings, then re-task.`,
900
- lockedBlob,
901
- release: lock.release,
902
- mode: resolvedMode,
903
- provider: options.providerInstance,
904
- env,
905
- note,
906
- });
907
- }
908
- if (core.outcome === 'sidecar-violation') {
909
- // A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
910
- // (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
911
- // `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
912
- // before the stage/integrate; route the held spec to needs-attention through
913
- // the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
914
- // carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
915
- const reason =
916
- `The spec '${slug}' carries a co-located asset sidecar: ` +
917
- `${core.reviewBlockReason ?? core.reason ?? ''}`;
918
- return await surfaceTaskingBlock({
919
- slug,
920
- cwd,
921
- arbiter,
922
- reason,
923
- message:
924
- `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
925
- `rule 8); parked it for your attention (no tasks landed; relocate it ` +
926
- `to docs/spikes/${slug}/ and reference by path).`,
927
- lockedBlob,
928
- release: lock.release,
929
- mode: resolvedMode,
930
- provider: options.providerInstance,
931
- env,
932
- note,
933
- });
934
- }
935
- if (core.outcome === 'review-unparseable') {
936
- // The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
937
- // JSON). Route the held spec to needs-attention through the SAME lock-release
938
- // seam the block path uses (the tasking needs-attention surface is the stuck
939
- // `spec:<slug>` lock; no folder write). It is NOT a block (the gate output was
940
- // unreadable) — record it as the transient-infra-class re-run signal so the
941
- // stuck reason reads correctly; nothing landed.
942
- const reason =
943
- `The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
944
- `(re-run — transient): ${core.reason ?? ''}`;
945
- return await surfaceTaskingBlock({
946
- slug,
947
- cwd,
948
- arbiter,
949
- reason,
950
- message:
951
- `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
952
- `parked it for your attention (no tasks landed; re-run).`,
953
- lockedBlob,
954
- release: lock.release,
955
- mode: resolvedMode,
956
- provider: options.providerInstance,
957
- env,
958
- note,
959
- });
960
- }
961
- if (core.outcome === 'completed') {
962
- // LOCK LIFETIME IS MODE-DEPENDENT (fix
963
- // `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
964
- // the "hold-across-the-PR" capstone the older interim half deferred).
965
- //
966
- // `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
967
- // residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
968
- // has done its job — RELEASE it (delete the ref). Best-effort + idempotent
969
- // (`not-held` is fine).
970
- //
971
- // `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
972
- // still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
973
- // tasked-ness. If we released here, the next in-place scan would see the spec
974
- // eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
975
- // force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
976
- // regenerating its review forever until a human merges/closes it. So we KEEP
977
- // the `spec:<slug>` lock HELD across the open PR: the held lock IS the
978
- // "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
979
- // specs from the taskable pool (symmetric to the task-pool held-slug
980
- // subtraction). The lock is reaped when the PR resolves: a MERGE lands the
981
- // durable move (which now carries tasked-ness on its own) and the reap/gc path
982
- // deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
983
- // recovery the human owns via `release-lock` (the same model as any other
984
- // abandoned in-flight item — no liveness heartbeat/auto-steal).
985
- //
986
- // MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
987
- // `spec-<slug>` lock entry the acquire took).
988
- if (useLock && resolvedMode === 'merge') {
989
- await releaseItemLock({item: `spec:${slug}`, cwd, arbiter, env});
990
- }
1110
+ // THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
1111
+ // fresh-context review of the produced SET returned `block`, so the core ran
1112
+ // the review BEFORE the stage/integrate and did NOT integrate the tasks
1113
+ // (correct). The CORRECT task-path destination is the SAME needs-attention
1114
+ // route the lock release owns for the decomposition-unclear verdict: it surfaces
1115
+ // the spec and releases the `spec:<slug>` lock with the block reason. So on a
1116
+ // block we route the held spec to needs-attention THROUGH the lock release —
1117
+ // the set never lands.
1118
+ if (core.outcome === 'review-blocked') {
1119
+ return await surfaceBlock(
1120
+ taskGateBlockedReason(slug, core.reviewBlockReason),
1121
+ `The task acceptance gate disapproved the set produced for '${slug}'; ` +
1122
+ `parked it for your attention (no tasks landed). ` +
1123
+ `Resolve the findings, then re-task.`,
1124
+ );
1125
+ }
1126
+ if (core.outcome === 'sidecar-violation') {
1127
+ // A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
1128
+ // (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
1129
+ // `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
1130
+ // before the stage/integrate; route the held spec to needs-attention through
1131
+ // the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
1132
+ // carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
1133
+ return await surfaceBlock(
1134
+ `The spec '${slug}' carries a co-located asset sidecar: ` +
1135
+ `${core.reviewBlockReason ?? core.reason ?? ''}`,
1136
+ `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
1137
+ `rule 8); parked it for your attention (no tasks landed; relocate it ` +
1138
+ `to docs/spikes/${slug}/ and reference by path).`,
1139
+ );
1140
+ }
1141
+ if (core.outcome === 'review-unparseable') {
1142
+ // The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
1143
+ // JSON). Route the held spec to needs-attention through the SAME lock-release
1144
+ // seam the block path uses. It is NOT a block (the gate output was
1145
+ // unreadable) — record it as the transient-infra-class re-run signal so the
1146
+ // stuck reason reads correctly; nothing landed.
1147
+ return await surfaceBlock(
1148
+ taskGateUnparseableReason(slug, core.reason),
1149
+ `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
1150
+ `parked it for your attention (no tasks landed; re-run).`,
1151
+ );
1152
+ }
1153
+ if (core.outcome === 'completed') {
1154
+ // LOCK LIFETIME IS MODE-DEPENDENT (fix
1155
+ // `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
1156
+ // the "hold-across-the-PR" capstone the older interim half deferred).
1157
+ //
1158
+ // `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
1159
+ // residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
1160
+ // has done its job — RELEASE it (delete the ref). Best-effort + idempotent
1161
+ // (`not-held` is fine).
1162
+ //
1163
+ // `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
1164
+ // still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
1165
+ // tasked-ness. If we released here, the next in-place scan would see the spec
1166
+ // eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
1167
+ // force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
1168
+ // regenerating its review forever until a human merges/closes it. So we KEEP
1169
+ // the `spec:<slug>` lock HELD across the open PR: the held lock IS the
1170
+ // "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
1171
+ // specs from the taskable pool (symmetric to the task-pool held-slug
1172
+ // subtraction). The lock is reaped when the PR resolves: a MERGE lands the
1173
+ // durable move (which now carries tasked-ness on its own) and the reap/gc path
1174
+ // deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
1175
+ // recovery the human owns via `release-lock` (the same model as any other
1176
+ // abandoned in-flight item — no liveness heartbeat/auto-steal).
1177
+ //
1178
+ // MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
1179
+ // `spec-<slug>` lock entry the acquire took).
1180
+ if (resolvedMode === 'merge') {
1181
+ await releaseItemLock({item: `spec:${slug}`, cwd, arbiter, env});
991
1182
  }
992
- return integrationToTaskResult(core, {slug, emitted, loop: loopTag});
993
1183
  }
994
-
995
- // HUMAN, no-lock path: the human commits on `main` directly (the runner does
996
- // not own the human's git). We report the produced tasks; moving the spec into
997
- // `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
998
- // do, as with the human `complete`.
999
- const message =
1000
- `Tasked '${slug}' -> ${emitted.length} backlog task` +
1001
- `${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
1002
- `the produced files (and move the spec into work/specs/tasked/) yourself.`;
1003
- note(message);
1004
- return {
1005
- exitCode: 0,
1006
- outcome: 'tasked',
1007
- slug,
1008
- emitted,
1009
- loop: loopTag,
1010
- message,
1011
- };
1184
+ return integrationToTaskResult(core, {slug, emitted, loop: loopTag});
1012
1185
  }
1013
1186
 
1014
1187
  /**
@@ -1059,10 +1232,17 @@ function integrationToTaskResult(
1059
1232
  ): TaskResult {
1060
1233
  const {slug, emitted, loop} = ctx;
1061
1234
  if (core.outcome === 'completed') {
1235
+ // Say "opened a PR" ONLY when the provider opened one (task
1236
+ // `intake-reports-the-pr-it-actually-opened`); a degraded `gh pr create`
1237
+ // pushed the branch but opened nothing, and the shared core printed why.
1238
+ const notOpened = proposeRequestNotOpened(core.integration);
1062
1239
  const landed =
1063
1240
  core.integration?.mode === 'merge'
1064
1241
  ? 'landed them on the arbiter main'
1065
- : 'opened a PR carrying them (main untouched)';
1242
+ : notOpened !== undefined
1243
+ ? `pushed them on branch ${core.integration?.pushedRef ?? core.branch} ` +
1244
+ 'but opened NO PR (main untouched; see the provider note above)'
1245
+ : 'opened a PR carrying them (main untouched)';
1066
1246
  const message =
1067
1247
  `Tasked '${slug}' -> ${emitted.length} backlog task` +
1068
1248
  `${emitted.length === 1 ? '' : 's'}; the runner integrated the transition ` +
@@ -1100,11 +1280,18 @@ function integrationToTaskResult(
1100
1280
  * pre-existing local `work/<slug>` (a re-run) is force-recreated off fresh main.
1101
1281
  * The agent runs in-place on this branch (branch ≠ worktree).
1102
1282
  */
1103
- async function switchToWorkBranch(
1283
+ export async function switchToWorkBranch(
1104
1284
  cwd: string,
1105
1285
  arbiter: string,
1106
1286
  slug: string,
1107
1287
  env: NodeJS.ProcessEnv | undefined,
1288
+ /**
1289
+ * The commit to cut the branch from; default the freshly fetched
1290
+ * `<arbiter>/main`. The CI phases pass the lock job's `baseSha` (task
1291
+ * `ci-split-tasking`), so the agent and apply jobs work on the tree the lock
1292
+ * job classified.
1293
+ */
1294
+ base?: string,
1108
1295
  ): Promise<void> {
1109
1296
  // The tasking path is the parent-spec namespace (`do spec:<slug>`): the branch
1110
1297
  // is `work/spec-<slug>`, distinct from a same-slug task-build's `work/task-<slug>`.
@@ -1114,7 +1301,7 @@ async function switchToWorkBranch(
1114
1301
  const branch = workBranchRef('spec', slug);
1115
1302
  await gitHard(['fetch', '--quiet', arbiter], cwd, env);
1116
1303
  await gitHard(
1117
- ['switch', '--quiet', '-C', branch, `${arbiter}/main`],
1304
+ ['switch', '--quiet', '-C', branch, base ?? `${arbiter}/main`],
1118
1305
  cwd,
1119
1306
  env,
1120
1307
  );
@@ -1331,7 +1518,7 @@ async function gitHard(
1331
1518
  * generic line when absent. DISTINCT from the improver loop's
1332
1519
  * {@link decompositionUnclearReason} (which carries the loop's open questions).
1333
1520
  */
1334
- function taskGateBlockedReason(
1521
+ export function taskGateBlockedReason(
1335
1522
  slug: string,
1336
1523
  findingsReason: string | undefined,
1337
1524
  ): string {
@@ -1342,6 +1529,21 @@ function taskGateBlockedReason(
1342
1529
  return findingsReason ? `${head}\n\n${findingsReason}` : head;
1343
1530
  }
1344
1531
 
1532
+ /**
1533
+ * The needs-attention REASON for a task-SET ACCEPTANCE GATE whose verdict was
1534
+ * UNPARSEABLE (the gate ran; its output could not be read): NOT a block, the
1535
+ * transient-infra re-run signal. `detail` is the gate's parse-failure reason.
1536
+ */
1537
+ export function taskGateUnparseableReason(
1538
+ slug: string,
1539
+ detail: string | undefined,
1540
+ ): string {
1541
+ return (
1542
+ `The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
1543
+ `(re-run — transient): ${detail ?? ''}`
1544
+ );
1545
+ }
1546
+
1345
1547
  /**
1346
1548
  * Shared terminal for a tasking transition that DID NOT land — a disapproving
1347
1549
  * task-set review, a co-located sidecar violation, an unparseable verdict, or a
@@ -1363,7 +1565,7 @@ function taskGateBlockedReason(
1363
1565
  * is GREEN — a park-for-human is not a failure). Only a surface that FAILED to
1364
1566
  * publish maps to a non-zero {@link releaseFailureToResult}.
1365
1567
  */
1366
- async function surfaceTaskingBlock(params: {
1568
+ export async function surfaceTaskingBlock(params: {
1367
1569
  slug: string;
1368
1570
  cwd: string;
1369
1571
  arbiter: string;
@@ -1427,7 +1629,10 @@ async function surfaceTaskingBlock(params: {
1427
1629
  * spec is parked for the human with these open questions + a question sidecar on
1428
1630
  * main, no guessed tasks). Prose only — recorded as the parked question's reason.
1429
1631
  */
1430
- function decompositionUnclearReason(slug: string, questions: string[]): string {
1632
+ export function decompositionUnclearReason(
1633
+ slug: string,
1634
+ questions: string[],
1635
+ ): string {
1431
1636
  const head =
1432
1637
  `The tasker review→edit loop could not converge on a sound decomposition of ` +
1433
1638
  `'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is parked ` +
@@ -1450,7 +1655,7 @@ function decompositionUnclearReason(slug: string, questions: string[]): string {
1450
1655
  * The candidate tasks the tasker produced are PERSISTED on the work branch (see
1451
1656
  * {@link persistTaskingCandidates}) so a human can recover them.
1452
1657
  */
1453
- function reviewFailureReason(
1658
+ export function reviewFailureReason(
1454
1659
  slug: string,
1455
1660
  err: ReviewParseError,
1456
1661
  candidatePaths: string[],
@@ -1476,7 +1681,10 @@ function reviewFailureReason(
1476
1681
  }
1477
1682
 
1478
1683
  /** The human-readable terminal MESSAGE for a review-leg failure (the result's `message`). */
1479
- function reviewFailureMessage(slug: string, err: ReviewParseError): string {
1684
+ export function reviewFailureMessage(
1685
+ slug: string,
1686
+ err: ReviewParseError,
1687
+ ): string {
1480
1688
  if (err instanceof ReviewOutputCappedError) {
1481
1689
  return (
1482
1690
  `Tasking '${slug}' parked: the review leg hit the model output cap ` +
@@ -1502,7 +1710,7 @@ function reviewFailureMessage(slug: string, err: ReviewParseError): string {
1502
1710
  * candidate paths persisted (empty when there were none). Never throws — a git
1503
1711
  * failure is noted and the bounce proceeds (the lock is still released).
1504
1712
  */
1505
- async function persistTaskingCandidates(
1713
+ export async function persistTaskingCandidates(
1506
1714
  cwd: string,
1507
1715
  slug: string,
1508
1716
  before: Map<string, string>,
@@ -1545,7 +1753,12 @@ async function persistTaskingCandidates(
1545
1753
  // teardown on an isolated run (the branch ref survives locally too, but a push
1546
1754
  // makes them remote-recoverable). A push failure is noted, never fatal.
1547
1755
  try {
1548
- const pushed = await gitSoft(['push', arbiter, branch], cwd, env);
1756
+ const pushed = await refWrite.pushTaskingCandidatesBranch({
1757
+ arbiter,
1758
+ branch,
1759
+ cwd,
1760
+ env,
1761
+ });
1549
1762
  if (pushed.status !== 0) {
1550
1763
  note(
1551
1764
  `Could not push the candidate-tasks work branch '${branch}' to ${arbiter} ` +
@@ -1619,7 +1832,7 @@ function appendQuestionsBlock(content: string, questions: string[]): string {
1619
1832
  * cross-spec `taskedAfter` ordering, resolved against `work/specs/tasked/` residence of
1620
1833
  * the specs present in the checkout.
1621
1834
  */
1622
- function resolveAgentGate(
1835
+ export function resolveAgentGate(
1623
1836
  cwd: string,
1624
1837
  slug: string,
1625
1838
  specFm: {humanOnly?: boolean; needsAnswers?: boolean; taskedAfter: string[]},
@@ -1637,7 +1850,7 @@ function resolveAgentGate(
1637
1850
  }
1638
1851
 
1639
1852
  /** Build an HONEST gate-refusal message naming WHY the agent skipped the spec. */
1640
- function gateRefusalReason(
1853
+ export function gateRefusalReason(
1641
1854
  slug: string,
1642
1855
  specFm: {humanOnly?: boolean; needsAnswers?: boolean},
1643
1856
  eligibility: TaskingEligibilityResult,
@@ -1796,7 +2009,7 @@ async function runTaskAgent(
1796
2009
  }
1797
2010
 
1798
2011
  /** A snapshot of {@link STAGED_TASKS_DIR}: filename → file content. */
1799
- function snapshotStagedTasks(cwd: string): Map<string, string> {
2012
+ export function snapshotStagedTasks(cwd: string): Map<string, string> {
1800
2013
  const dir = join(cwd, STAGED_TASKS_DIR);
1801
2014
  const snap = new Map<string, string>();
1802
2015
  for (const file of listMarkdown(dir)) {
@@ -1812,7 +2025,7 @@ function snapshotStagedTasks(cwd: string): Map<string, string> {
1812
2025
  * re-committed.) The agent's staging folder is `work/tasks/backlog/`; writes to
1813
2026
  * the pool `work/tasks/ready/` are scrubbed at stage time, never picked up here.
1814
2027
  */
1815
- function newOrChangedStagedTasks(
2028
+ export function newOrChangedStagedTasks(
1816
2029
  cwd: string,
1817
2030
  before: Map<string, string>,
1818
2031
  ): string[] {
@@ -1828,7 +2041,7 @@ function newOrChangedStagedTasks(
1828
2041
  }
1829
2042
 
1830
2043
  /** Snapshot the POOL `work/tasks/ready/` (for the agent-write fence at stage time). */
1831
- function snapshotPool(cwd: string): Map<string, string> {
2044
+ export function snapshotPool(cwd: string): Map<string, string> {
1832
2045
  const dir = workFolderPath(cwd, 'tasks-ready');
1833
2046
  const snap = new Map<string, string>();
1834
2047
  for (const file of listMarkdown(dir)) {
@@ -1838,7 +2051,7 @@ function snapshotPool(cwd: string): Map<string, string> {
1838
2051
  }
1839
2052
 
1840
2053
  /** Read the produced backlog tasks' content keyed by repo-relative path. */
1841
- function collectEmittedTasks(
2054
+ export function collectEmittedTasks(
1842
2055
  cwd: string,
1843
2056
  relPaths: string[],
1844
2057
  ): Record<string, string> {
@@ -2001,12 +2214,14 @@ function readTitleField(content: string): string | undefined {
2001
2214
  if (!m) continue;
2002
2215
  let value = m[1].trim();
2003
2216
  if (value === '') return undefined;
2004
- // Quoted ⇒ return inner text verbatim; unquoted ⇒ strip trailing `# ...`.
2217
+ // Quoted ⇒ return the inner text (a single-quoted `''` decodes to `'`, the
2218
+ // shape `quoteYamlScalar` writes); unquoted ⇒ strip trailing `# ...`.
2005
2219
  if (
2006
- (value.startsWith('"') && value.endsWith('"')) ||
2007
- (value.startsWith("'") && value.endsWith("'"))
2220
+ value.length >= 2 &&
2221
+ ((value.startsWith('"') && value.endsWith('"')) ||
2222
+ (value.startsWith("'") && value.endsWith("'")))
2008
2223
  ) {
2009
- return value.slice(1, -1);
2224
+ return unquoteYamlScalar(value);
2010
2225
  }
2011
2226
  const hash = value.indexOf('#');
2012
2227
  if (hash !== -1) value = value.slice(0, hash).trimEnd();