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/dist/tasking.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
2
  import { basename, dirname, join } from 'node:path';
3
- import { parseFrontmatter } from './frontmatter.js';
3
+ import { parseFrontmatter, unquoteYamlScalar } from './frontmatter.js';
4
4
  import { runAsync } from './git.js';
5
5
  import { workFolderRel, workFolderPath, workItemPath, workItemRel, isWorkItemFile, } from './work-layout.js';
6
- import { performIntegration, } from './integration-core.js';
6
+ import { performIntegration, proposePointer, } from './integration-core.js';
7
+ import { proposeRequestNotOpened } from './integrator.js';
7
8
  import { resolveTaskingEligibility, } from './tasking-eligibility.js';
8
9
  import { acquireTaskingLock, releaseTaskingLock, } from './tasking-lock.js';
9
10
  import { releaseItemLock } from './item-lock.js';
11
+ import { refWrite } from './ref-write.js';
10
12
  import { NullHarness } from './harness.js';
11
13
  import { launchWithOptionalWatch } from './agent-launch.js';
12
14
  import { placementFolder, resolvePlacement } from './placement.js';
@@ -162,33 +164,19 @@ export async function performTask(options) {
162
164
  if (useLock) {
163
165
  await switchToWorkBranch(cwd, arbiter, slug, env);
164
166
  }
165
- // 3. INVOKE THE AGENT with the to-task spec. It WRITES
166
- // `work/tasks/backlog/*.md` task files (the STAGED area — NOT `work/tasks/ready/`,
167
- // which is the agent-eligible pool the runner owns the promotion into; task
168
- // `pre-backlog-staging-folder-and-promote-step-a`); it does NO git. We
169
- // snapshot the staged-tasks folder before/after so the runner (not the
170
- // agent) captures + commits exactly what was produced.
171
- const before = snapshotStagedTasks(cwd);
172
- // Also snapshot the POOL `work/tasks/ready/` BEFORE the agent runs: the runner's
173
- // final commit must scrub any agent writes there (an attempt to self-place into
174
- // the pool, spec US #4) before `git add -A` would sweep them in.
175
- const poolBefore = snapshotPool(cwd);
176
- // Read the parent-spec self-pointer off `specFm.spec` (populated from the
177
- // `spec:` key).
178
- const prompt = buildTaskingSpec(slug, specFm.spec);
179
- let agent;
180
- try {
181
- agent = await runTaskAgent(options, cwd, prompt, slug);
182
- }
183
- catch (err) {
184
- agent = {
185
- ok: false,
186
- detail: err instanceof Error ? err.message : String(err),
187
- };
188
- }
189
- if (!agent.ok) {
190
- const detail = agent.detail ?? `the agent failed to task '${slug}'.`;
191
- let message = `Agent failed tasking '${slug}' (${detail}).`;
167
+ // 3. + 3.5 THE AGENT HALF (task `ci-split-tasking`): the tasker agent and the
168
+ // tasker review→edit→converge loop. Shared, unchanged, with the CI agent
169
+ // phase (`ci-phase-tasking.ts`), which runs this SAME half in the job that
170
+ // holds no write token and hands the result over instead of integrating.
171
+ const half = await runTaskingAgentHalf(options, {
172
+ cwd,
173
+ slug,
174
+ specFm,
175
+ doer,
176
+ note,
177
+ });
178
+ if (half.kind === 'agent-failed') {
179
+ let message = `Agent failed tasking '${slug}' (${half.detail}).`;
192
180
  // RELEASE THE LOCK THE FAILED RUN TOOK (observation
193
181
  // `crashed-do-spec-strands-a-tasking-lock-no-verb-releases`).
194
182
  //
@@ -243,13 +231,164 @@ export async function performTask(options) {
243
231
  note(message);
244
232
  return { exitCode: 1, outcome: 'agent-failed', slug, message };
245
233
  }
234
+ if (half.kind === 'review-leg-failed') {
235
+ // REVIEW-LEG FAILURE handling (observation
236
+ // `tasker-review-edits-payload-caps-the-verdict-response`): PERSIST the
237
+ // candidate tasks, and BOUNCE through the SAME surface the
238
+ // decomposition-unclear path uses (release the lock + a question sidecar).
239
+ // NEVER a silent approve — a parse failure is always a needs-attention route.
240
+ const candidatePaths = await persistTaskingCandidates(cwd, slug, half.before, arbiter, env, note);
241
+ const reason = reviewFailureReason(slug, half.error, candidatePaths);
242
+ const message = reviewFailureMessage(slug, half.error);
243
+ if (useLock) {
244
+ return await surfaceTaskingBlock({
245
+ slug,
246
+ cwd,
247
+ arbiter,
248
+ reason,
249
+ message,
250
+ lockedBlob,
251
+ release: lock.release,
252
+ mode: resolvedMode,
253
+ provider: options.providerInstance,
254
+ env,
255
+ note,
256
+ });
257
+ }
258
+ note(reason);
259
+ // Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
260
+ return { exitCode: 0, outcome: 'needs-attention', slug, message };
261
+ }
262
+ if (half.kind === 'decomposition-unclear') {
263
+ // DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
264
+ // needs-attention with the questions as the reason. The lock release surfaces
265
+ // the spec (a question sidecar + `needsAnswers: true` on main) and releases the
266
+ // `spec:<slug>` lock; the spec body stays in `work/specs/ready/`.
267
+ const reason = decompositionUnclearReason(slug, half.questions);
268
+ if (useLock) {
269
+ return await surfaceTaskingBlock({
270
+ slug,
271
+ cwd,
272
+ arbiter,
273
+ reason,
274
+ message: half.message,
275
+ lockedBlob,
276
+ release: lock.release,
277
+ mode: resolvedMode,
278
+ provider: options.providerInstance,
279
+ env,
280
+ note,
281
+ });
282
+ }
283
+ note(half.message);
284
+ // Human, no-lock path: nothing to release/surface via the seam; a clean
285
+ // park-for-human is still a success terminal (exit 0).
286
+ return {
287
+ exitCode: 0,
288
+ outcome: 'needs-attention',
289
+ slug,
290
+ message: half.message,
291
+ };
292
+ }
293
+ // 4. The RUNNER commits the COMPLETING transition: drop the produced backlog
294
+ // tasks IN + move the spec specs/ready/ -> specs/tasked/ (residence = tasked-ness) — now
295
+ // through the SHARED integrate core (`--propose` PR / `--merge` main), NOT a
296
+ // direct commit to `main`. The agent never does git. (The backlog snapshot is
297
+ // taken AFTER any loop edits, so the runner integrates the IMPROVED tasks,
298
+ // not the pre-loop candidates.)
299
+ const stagedEmitted = newOrChangedStagedTasks(cwd, half.before);
300
+ const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
301
+ const placementDir = taskPlacementDir(options, specFm);
302
+ const loopTag = half.loopTag;
303
+ if (useLock) {
304
+ return await integrateTaskingCandidates({
305
+ slug,
306
+ cwd,
307
+ arbiter,
308
+ stagedEmitted,
309
+ emitTasks,
310
+ poolBefore: half.poolBefore,
311
+ placementDir,
312
+ loopTag,
313
+ lockedBlob,
314
+ release: lock.release,
315
+ mode: resolvedMode,
316
+ review: options.review,
317
+ reviewGate: options.reviewGate,
318
+ acceptanceReviewModel: options.acceptanceReviewModel,
319
+ mergeRetries: options.mergeRetries,
320
+ noPR: options.noPR,
321
+ providerInstance: options.providerInstance,
322
+ env,
323
+ agentEnv,
324
+ note,
325
+ });
326
+ }
327
+ // HUMAN, no-lock path: the human commits on `main` directly (the runner does
328
+ // not own the human's git). We report the produced tasks; moving the spec into
329
+ // `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
330
+ // do, as with the human `complete`.
331
+ const emitted = stagedEmitted.map((rel) => `${placementDir}/${basename(rel)}`);
332
+ const message = `Tasked '${slug}' -> ${emitted.length} backlog task` +
333
+ `${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
334
+ `the produced files (and move the spec into work/specs/tasked/) yourself.`;
335
+ note(message);
336
+ return {
337
+ exitCode: 0,
338
+ outcome: 'tasked',
339
+ slug,
340
+ emitted,
341
+ loop: loopTag,
342
+ message,
343
+ };
344
+ }
345
+ /**
346
+ * The AGENT half of the tasking path (steps 3 and 3.5): run the tasker agent in
347
+ * `cwd` (on the work branch), then, when a loop seam is wired and the doer is
348
+ * the agent, the tasker review→edit→converge loop, and report what is on disk.
349
+ * It performs NO write to the arbiter and no git transition: only the agents'
350
+ * file edits, the loop's local edits and markers, and the scratch cleanup.
351
+ */
352
+ export async function runTaskingAgentHalf(options, params) {
353
+ const { cwd, slug, specFm, doer, note } = params;
354
+ const agentEnv = options.agentEnv ?? options.env;
355
+ // 3. INVOKE THE AGENT with the to-task spec. It WRITES
356
+ // `work/tasks/backlog/*.md` task files (the STAGED area — NOT `work/tasks/ready/`,
357
+ // which is the agent-eligible pool the runner owns the promotion into; task
358
+ // `pre-backlog-staging-folder-and-promote-step-a`); it does NO git. We
359
+ // snapshot the staged-tasks folder before/after so the runner (not the
360
+ // agent) captures + commits exactly what was produced.
361
+ const before = snapshotStagedTasks(cwd);
362
+ // Also snapshot the POOL `work/tasks/ready/` BEFORE the agent runs: the runner's
363
+ // final commit must scrub any agent writes there (an attempt to self-place into
364
+ // the pool, spec US #4) before `git add -A` would sweep them in.
365
+ const poolBefore = snapshotPool(cwd);
366
+ // Read the parent-spec self-pointer off `specFm.spec` (populated from the
367
+ // `spec:` key).
368
+ const prompt = buildTaskingSpec(slug, specFm.spec);
369
+ let agent;
370
+ try {
371
+ agent = await runTaskAgent(options, cwd, prompt, slug);
372
+ }
373
+ catch (err) {
374
+ agent = {
375
+ ok: false,
376
+ detail: err instanceof Error ? err.message : String(err),
377
+ };
378
+ }
379
+ if (!agent.ok) {
380
+ return {
381
+ kind: 'agent-failed',
382
+ detail: agent.detail ?? `the agent failed to task '${slug}'.`,
383
+ };
384
+ }
246
385
  // 3.5 THE TASKER REVIEW→EDIT→CONVERGE LOOP (`slicer-review-edit-loop`, Shape 2 /
247
386
  // insertion point A): when a loop seam is wired, run the `review` SKILL as a
248
387
  // review→edit→re-review loop that IMPROVES the candidate tasks in place, then
249
388
  // determines the disposition (the three outcomes). This plugs in AFTER the
250
389
  // candidate tasks are produced and BEFORE they are finalised. The agent makes
251
390
  // the review/edit JUDGEMENTS; the loop applies edits to the candidate files
252
- // and routes the verdict; the runner (below) owns the git transition. Only the
391
+ // and routes the verdict; the runner owns the git transition. Only the
253
392
  // AGENT path runs the loop — the human tasking path is unaffected.
254
393
  let loopDisposition;
255
394
  if (options.reviewLoop && doer === 'agent') {
@@ -259,10 +398,8 @@ export async function performTask(options) {
259
398
  // parseable verdict (a generic parse failure, or the NAMED cap-truncation
260
399
  // class). The pre-fix behaviour THREW here, crashing the run, LEAVING THE
261
400
  // LOCK HELD, writing NO question sidecar, and DISCARDING the tasker's
262
- // candidate tasks. Now: catch it, PERSIST the candidate tasks, and BOUNCE
263
- // through the SAME surface the decomposition-unclear path uses (release the
264
- // lock + a question sidecar). NEVER a silent approve — a parse failure is
265
- // always a needs-attention route. A non-review throw (genuine wiring error)
401
+ // candidate tasks. Now: catch it and report it, so the write half PERSISTS
402
+ // the candidate tasks and BOUNCES. A non-review throw (genuine wiring error)
266
403
  // is re-thrown.
267
404
  try {
268
405
  loopDisposition = await runTaskReviewLoop({
@@ -286,7 +423,6 @@ export async function performTask(options) {
286
423
  if (!(err instanceof ReviewParseError)) {
287
424
  throw err;
288
425
  }
289
- const reviewErr = err;
290
426
  // Reap any review-edits SCRATCH the failed pass left on disk so it is never
291
427
  // swept into a later run's integrate commit.
292
428
  try {
@@ -298,56 +434,12 @@ export async function performTask(options) {
298
434
  catch {
299
435
  // Best-effort.
300
436
  }
301
- const candidatePaths = await persistTaskingCandidates(cwd, slug, before, arbiter, env, note);
302
- const reason = reviewFailureReason(slug, reviewErr, candidatePaths);
303
- const message = reviewFailureMessage(slug, reviewErr);
304
- if (useLock) {
305
- return await surfaceTaskingBlock({
306
- slug,
307
- cwd,
308
- arbiter,
309
- reason,
310
- message,
311
- lockedBlob,
312
- release: lock.release,
313
- mode: resolvedMode,
314
- provider: options.providerInstance,
315
- env,
316
- note,
317
- });
318
- }
319
- note(reason);
320
- // Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
321
- return { exitCode: 0, outcome: 'needs-attention', slug, message };
437
+ return { kind: 'review-leg-failed', error: err, before };
322
438
  }
323
- // DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
324
- // needs-attention with the questions as the reason. The lock release amends the
325
- // `spec:<slug>` unified lock `active → stuck` (the tasking needs-attention surface
326
- // is the stuck lock now — NO folder write; the spec body stays in `work/specs/ready/`).
327
439
  if (loopDisposition.outcome === 'decomposition-unclear') {
328
- const reason = decompositionUnclearReason(slug, loopDisposition.specQuestions);
329
- if (useLock) {
330
- return await surfaceTaskingBlock({
331
- slug,
332
- cwd,
333
- arbiter,
334
- reason,
335
- message: loopDisposition.message,
336
- lockedBlob,
337
- release: lock.release,
338
- mode: resolvedMode,
339
- provider: options.providerInstance,
340
- env,
341
- note,
342
- });
343
- }
344
- note(loopDisposition.message);
345
- // Human, no-lock path: nothing to release/surface via the seam; a clean
346
- // park-for-human is still a success terminal (exit 0).
347
440
  return {
348
- exitCode: 0,
349
- outcome: 'needs-attention',
350
- slug,
441
+ kind: 'decomposition-unclear',
442
+ questions: loopDisposition.specQuestions,
351
443
  message: loopDisposition.message,
352
444
  };
353
445
  }
@@ -360,30 +452,34 @@ export async function performTask(options) {
360
452
  }
361
453
  }
362
454
  }
363
- // 4. The RUNNER commits the COMPLETING transition: drop the produced backlog
364
- // tasks IN + move the spec specs/ready/ -> specs/tasked/ (residence = tasked-ness) — now
365
- // through the SHARED integrate core (`--propose` PR / `--merge` main), NOT a
366
- // direct commit to `main`. The agent never does git. (The backlog snapshot is
367
- // taken AFTER any loop edits, so the runner integrates the IMPROVED tasks,
368
- // not the pre-loop candidates.)
369
- const stagedEmitted = newOrChangedStagedTasks(cwd, before);
370
- const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
371
- // RUNNER-DETERMINISTIC PLACEMENT (task
372
- // `runner-deterministic-slice-placement-policy-and-precedence`; the
373
- // untrusted-forces-staging rung was RETIRED by ADR
374
- // `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
375
- // folder the runner lands the emitted tasks in BEFORE handing them to the
376
- // shared integrate band: precedence `explicit > configured default >
377
- // built-in (staging)`. THIS caller selects the trusted-vs-untrusted
378
- // configured default by reading the spec's propagated `originTrust:` stamp:
379
- // an untrusted-origin spec selects `untrustedTasksLandIn` (default staging;
380
- // `ready` when configured), a trusted/unset spec selects `tasksLandIn`. One
381
- // knob (`untrustedTasksLandIn`) governs an untrusted task's destination
382
- // whether born from an issue directly (intake) or from an untrusted spec
383
- // here. The agent NEVER influences this; it always writes to
384
- // `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety
385
- // for an untrusted task landing in `ready` is the carried stamp (its BUILD is
386
- // forced to a code PR), not the folder.
455
+ return {
456
+ kind: 'candidates',
457
+ before,
458
+ poolBefore,
459
+ loopTag: loopDisposition?.outcome === 'converged'
460
+ ? 'converged'
461
+ : loopDisposition?.outcome === 'uncertain-tasks'
462
+ ? 'uncertain-tasks'
463
+ : undefined,
464
+ };
465
+ }
466
+ /**
467
+ * The RUNNER-DETERMINISTIC PLACEMENT folder for a spec's emitted tasks (task
468
+ * `runner-deterministic-slice-placement-policy-and-precedence`; the
469
+ * untrusted-forces-staging rung was RETIRED by ADR
470
+ * `untrusted-origin-carries-via-stamp-not-forced-staging`): precedence
471
+ * `explicit > configured default > built-in (staging)`. This caller selects the
472
+ * trusted-vs-untrusted configured default by reading the spec's propagated
473
+ * `originTrust:` stamp: an untrusted-origin spec selects `untrustedTasksLandIn`
474
+ * (default staging; `ready` when configured), a trusted/unset spec selects
475
+ * `tasksLandIn`. One knob (`untrustedTasksLandIn`) governs an untrusted task's
476
+ * destination whether born from an issue directly (intake) or from an untrusted
477
+ * spec here. The agent NEVER influences this; it always writes to
478
+ * `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety for
479
+ * an untrusted task landing in `ready` is the carried stamp (its BUILD is forced
480
+ * to a code PR), not the folder.
481
+ */
482
+ export function taskPlacementDir(options, specFm) {
387
483
  const configuredTasksLanding = specFm.originTrust === 'untrusted'
388
484
  ? options.untrustedTasksLandIn
389
485
  : options.tasksLandIn;
@@ -391,247 +487,215 @@ export async function performTask(options) {
391
487
  explicit: landingToSide(options.explicitTasksLandIn),
392
488
  configuredDefault: landingToSide(configuredTasksLanding),
393
489
  });
394
- const placementDir = placementFolder(TASK_PLACEMENT_SLOTS, placementDecision.choice);
490
+ return placementFolder(TASK_PLACEMENT_SLOTS, placementDecision.choice);
491
+ }
492
+ /**
493
+ * The WRITE half of a tasking run that produced candidate tasks (step 4): the
494
+ * read-stability stale check, the integrate through the shared core (the tasks
495
+ * + the `specs/ready → specs/tasked` move, `--propose` PR / `--merge` main), the
496
+ * needs-attention routing of a not-landed transition, and the mode-dependent lock
497
+ * release. Shared by the laptop `performTask` (review ON inside the core) and the
498
+ * CI apply phase (`ci-phase-tasking.ts`, review OFF: it ran in the agent phase).
499
+ */
500
+ export async function integrateTaskingCandidates(params) {
501
+ const { slug, cwd, arbiter, stagedEmitted, emitTasks, poolBefore, placementDir, loopTag, lockedBlob, mode: resolvedMode, env, agentEnv, note, } = params;
395
502
  // REWRITE the emitted list to the RUNNER-RESOLVED destination so callers see
396
503
  // where the runner actually placed the files (not where the agent wrote them).
397
504
  const emitted = stagedEmitted.map((rel) => `${placementDir}/${basename(rel)}`);
398
- const loopTag = loopDisposition?.outcome === 'converged'
399
- ? 'converged'
400
- : loopDisposition?.outcome === 'uncertain-tasks'
401
- ? 'uncertain-tasks'
402
- : undefined;
403
- if (useLock) {
404
- // READ-STABILITY BACKSTOP (the lock's content-identity check, now owned at the
405
- // integrate seam): the OUTPUT no longer rides the lock release, so the band
406
- // below would otherwise rebase a concurrent edit of the held spec body CLEANLY
407
- // into spec/ (a rename+edit merge) while the tasks were cut from the OLD body —
408
- // the exact silent stale-task drift the lock forbids
409
- // (`work/notes/observations/tasking-lock-does-not-stabilise-spec-content.md`). So we
410
- // compare the CURRENTLY held `work/specs/ready/<slug>.md` blob on the arbiter against
411
- // the snapshot the lock TOOK (`lockedBlob`); ANY change ⇒ STALE ⇒ fail loud,
412
- // touch NOTHING (the lock stays held; a human re-tasks or routes to
413
- // needs-attention). It is the SAME content-identity check `releaseTaskingLock`
414
- // runs — relocated here because this transition, not the release, owns the
415
- // completing commit now.
416
- const stale = await heldSpecIsStale(cwd, arbiter, slug, lockedBlob, env);
417
- if (stale) {
418
- const specRel = workItemRel('specs-ready', `${slug}.md`);
419
- const message = `RELEASE CONFLICT for '${slug}': the spec was edited (${specRel} ` +
420
- `changed on ${arbiter}/main) while the tasking lock was held. The tasking is ` +
421
- `STALE — re-task from the edited spec or route it to needs-attention. ` +
422
- `The arbiter was NOT modified (lock still held).`;
423
- note(message);
424
- return { exitCode: 4, outcome: 'stale', slug, message };
425
- }
426
- // Route the OUTPUT through the SHARED integrate back-half (task
427
- // `slice-output-through-integration`): the produced backlog tasks + the spec
428
- // lifecycle move (`work/specs/ready/ -> work/specs/tasked/`, residence = tasked-ness) integrate
429
- // via `performIntegration` honoring `--propose` (push the work branch + open a
430
- // PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
431
- // args resolve ONCE in the shared core, every `do task:` arg applies here by
432
- // construction. The agent did NO git; the runner (the band) owns the ONE commit.
433
- const core = await performIntegration({
434
- cwd,
435
- arbiter,
436
- slug,
437
- // `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set
438
- // (a tasking transition never recovers a surfaced needs-attention move).
439
- source: 'in-progress',
440
- recovering: false,
441
- // Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
442
- // no `verify` floor (the tasker review loop above is its quality gate).
443
- skipVerify: true,
444
- // THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
445
- // task-path mirror of the build Gate-2, riding THIS shared core's
446
- // review-before-integrate block. When `review` resolves on, the wired
447
- // `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
448
- // prompt) runs a FRESH-CONTEXT review of the produced task SET before it
449
- // integrates: `approve` lands it, `block` routes the set to needs-attention
450
- // via the SAME machinery the build block uses (mapped to the tasking
451
- // `needs-attention` outcome below). It is ONE-SHOT: we pin
452
- // `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
453
- // (terminal pass/fail). The task path NEVER exposes/consults
454
- // `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
455
- // that belongs to a future revise↔review loop (see
456
- // `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
457
- // independently controllable from the tasker improver loop (`reviewLoop` /
458
- // the `--tasker-loop*` family); toggling one does not affect the other.
459
- review: options.review,
460
- reviewGate: options.reviewGate,
461
- reviewModel: options.acceptanceReviewModel,
462
- reviewMaxRounds: 1,
463
- // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
464
- // threaded so a wide-matrix CI's raised cap reaches the tasking-
465
- // transition's land tail too (task
466
- // `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
467
- // through to the engine default (byte-for-byte unchanged).
468
- mergeRetries: options.mergeRetries,
469
- // The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
470
- // `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
471
- // path's merge-vs-propose decision is the `integration` mode the user typed;
472
- // `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
473
- // The task gate family is `--review`/`--no-review`/`--review-model` only
474
- // (spec US #6).
475
- mode: resolvedMode,
476
- noPR: options.noPR,
477
- providerInstance: options.providerInstance,
478
- // PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
479
- // BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
480
- // agent produces no `LaunchResult.output` we can carry, but the slice-SET
481
- // itself is a summary-worthy artifact: the emitted task slugs+titles, their
482
- // coverage of the spec's user stories, the dependency graph (keystone +
483
- // blockedBy edges), and any `needsAnswers` open questions the tasker review
484
- // loop flagged. Compose it here and thread it into the shared core as `body`
485
- // so the propose-mode PR carries the summary instead of degrading to
486
- // `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
487
- // provider's `--fill` fallback (no regression on that edge).
488
- body: composeTaskingProposeBody(slug, emitTasks),
489
- type: 'tasking',
490
- lifecycle: {
491
- // Read the PR title / commit summary from the held spec (before it moves).
492
- titlePath: workItemPath(cwd, 'specs-ready', slug),
493
- commitTag: 'tasked',
494
- stage: () => stageTaskingLifecycle({
495
- cwd,
496
- slug,
497
- emitTasks,
498
- poolBefore,
499
- placementDir,
500
- note,
501
- env,
502
- }),
503
- },
504
- env,
505
- // The task-SET acceptance review AGENT launches AMBIENT, never the
506
- // identity-scoped `env` (an agent must not act as the bot).
507
- agentEnv,
508
- note,
509
- });
510
- // THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
511
- // fresh-context review of the produced SET returned `block`, so the core ran
512
- // the review BEFORE the stage/integrate and did NOT integrate the tasks
513
- // (correct). The CORRECT task-path destination is the SAME needs-attention
514
- // route the lock release owns for the decomposition-unclear verdict: it amends
515
- // the `spec:<slug>` unified lock `active -> stuck` with the block reason (the
516
- // tasking needs-attention surface is the stuck lock now — NO folder write; the
517
- // spec body stays in `work/specs/ready/`). So on a block we route the held spec to
518
- // needs-attention THROUGH the lock release — the set never lands.
519
- if (core.outcome === 'review-blocked') {
520
- const reason = taskGateBlockedReason(slug, core.reviewBlockReason);
521
- return await surfaceTaskingBlock({
522
- slug,
505
+ // READ-STABILITY BACKSTOP (the lock's content-identity check, now owned at the
506
+ // integrate seam): the OUTPUT no longer rides the lock release, so the band
507
+ // below would otherwise rebase a concurrent edit of the held spec body CLEANLY
508
+ // into spec/ (a rename+edit merge) while the tasks were cut from the OLD body —
509
+ // the exact silent stale-task drift the lock forbids
510
+ // (`work/notes/observations/tasking-lock-does-not-stabilise-spec-content.md`). So we
511
+ // compare the CURRENTLY held `work/specs/ready/<slug>.md` blob on the arbiter against
512
+ // the snapshot the lock TOOK (`lockedBlob`); ANY change ⇒ STALE ⇒ fail loud,
513
+ // touch NOTHING (the lock stays held; a human re-tasks or routes to
514
+ // needs-attention). It is the SAME content-identity check `releaseTaskingLock`
515
+ // runs — relocated here because this transition, not the release, owns the
516
+ // completing commit now.
517
+ const stale = await heldSpecIsStale(cwd, arbiter, slug, lockedBlob, env);
518
+ if (stale) {
519
+ const specRel = workItemRel('specs-ready', `${slug}.md`);
520
+ const message = `RELEASE CONFLICT for '${slug}': the spec was edited (${specRel} ` +
521
+ `changed on ${arbiter}/main) while the tasking lock was held. The tasking is ` +
522
+ `STALE — re-task from the edited spec or route it to needs-attention. ` +
523
+ `The arbiter was NOT modified (lock still held).`;
524
+ note(message);
525
+ return { exitCode: 4, outcome: 'stale', slug, message };
526
+ }
527
+ // Route the OUTPUT through the SHARED integrate back-half (task
528
+ // `slice-output-through-integration`): the produced backlog tasks + the spec
529
+ // lifecycle move (`work/specs/ready/ -> work/specs/tasked/`, residence = tasked-ness) integrate
530
+ // via `performIntegration` honoring `--propose` (push the work branch + open a
531
+ // PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
532
+ // args resolve ONCE in the shared core, every `do task:` arg applies here by
533
+ // construction. The agent did NO git; the runner (the band) owns the ONE commit.
534
+ const core = await performIntegration({
535
+ cwd,
536
+ arbiter,
537
+ slug,
538
+ // `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set
539
+ // (a tasking transition never recovers a surfaced needs-attention move).
540
+ source: 'in-progress',
541
+ recovering: false,
542
+ // Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
543
+ // no `verify` floor (the tasker review loop above is its quality gate).
544
+ skipVerify: true,
545
+ // THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
546
+ // task-path mirror of the build Gate-2, riding THIS shared core's
547
+ // review-before-integrate block. When `review` resolves on, the wired
548
+ // `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
549
+ // prompt) runs a FRESH-CONTEXT review of the produced task SET before it
550
+ // integrates: `approve` lands it, `block` routes the set to needs-attention
551
+ // via the SAME machinery the build block uses (mapped to the tasking
552
+ // `needs-attention` outcome below). It is ONE-SHOT: we pin
553
+ // `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
554
+ // (terminal pass/fail). The task path NEVER exposes/consults
555
+ // `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
556
+ // that belongs to a future revise↔review loop (see
557
+ // `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
558
+ // independently controllable from the tasker improver loop (`reviewLoop` /
559
+ // the `--tasker-loop*` family); toggling one does not affect the other.
560
+ // The CI apply phase passes it OFF (it ran in the agent phase) and hands
561
+ // its approved prose in as `approvedReviewProse`.
562
+ review: params.review,
563
+ reviewGate: params.reviewGate,
564
+ reviewModel: params.acceptanceReviewModel,
565
+ reviewMaxRounds: 1,
566
+ approvedReviewProse: params.approvedReviewProse,
567
+ // The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
568
+ // threaded so a wide-matrix CI's raised cap reaches the tasking-
569
+ // transition's land tail too (task
570
+ // `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
571
+ // through to the engine default (byte-for-byte unchanged).
572
+ mergeRetries: params.mergeRetries,
573
+ ...(params.mergeJitterMs === undefined
574
+ ? {}
575
+ : { mergeJitterMs: params.mergeJitterMs }),
576
+ // The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
577
+ // `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
578
+ // path's merge-vs-propose decision is the `integration` mode the user typed;
579
+ // `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
580
+ // The task gate family is `--review`/`--no-review`/`--review-model` only
581
+ // (spec US #6).
582
+ mode: resolvedMode,
583
+ noPR: params.noPR,
584
+ providerInstance: params.providerInstance,
585
+ // PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
586
+ // BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
587
+ // agent produces no `LaunchResult.output` we can carry, but the slice-SET
588
+ // itself is a summary-worthy artifact: the emitted task slugs+titles, their
589
+ // coverage of the spec's user stories, the dependency graph (keystone +
590
+ // blockedBy edges), and any `needsAnswers` open questions the tasker review
591
+ // loop flagged. Compose it here and thread it into the shared core as `body`
592
+ // so the propose-mode PR carries the summary instead of degrading to
593
+ // `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
594
+ // provider's `--fill` fallback (no regression on that edge).
595
+ body: composeTaskingProposeBody(slug, emitTasks),
596
+ type: 'tasking',
597
+ lifecycle: {
598
+ // Read the PR title / commit summary from the held spec (before it moves).
599
+ titlePath: workItemPath(cwd, 'specs-ready', slug),
600
+ commitTag: 'tasked',
601
+ // The spec RESTS in `work/specs/tasked/` once this lands (never
602
+ // `work/tasks/done/`, the task-build default), so point the reviewer there.
603
+ proposeHeader: proposePointer('Spec', workItemRel('specs-tasked', `${slug}.md`)),
604
+ stage: () => stageTaskingLifecycle({
523
605
  cwd,
524
- arbiter,
525
- reason,
526
- message: `The task acceptance gate disapproved the set produced for '${slug}'; ` +
527
- `parked it for your attention (no tasks landed). ` +
528
- `Resolve the findings, then re-task.`,
529
- lockedBlob,
530
- release: lock.release,
531
- mode: resolvedMode,
532
- provider: options.providerInstance,
533
- env,
534
- note,
535
- });
536
- }
537
- if (core.outcome === 'sidecar-violation') {
538
- // A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
539
- // (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
540
- // `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
541
- // before the stage/integrate; route the held spec to needs-attention through
542
- // the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
543
- // carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
544
- const reason = `The spec '${slug}' carries a co-located asset sidecar: ` +
545
- `${core.reviewBlockReason ?? core.reason ?? ''}`;
546
- return await surfaceTaskingBlock({
547
606
  slug,
548
- cwd,
549
- arbiter,
550
- reason,
551
- message: `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
552
- `rule 8); parked it for your attention (no tasks landed; relocate it ` +
553
- `to docs/spikes/${slug}/ and reference by path).`,
554
- lockedBlob,
555
- release: lock.release,
556
- mode: resolvedMode,
557
- provider: options.providerInstance,
558
- env,
607
+ emitTasks,
608
+ poolBefore,
609
+ placementDir,
559
610
  note,
560
- });
561
- }
562
- if (core.outcome === 'review-unparseable') {
563
- // The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
564
- // JSON). Route the held spec to needs-attention through the SAME lock-release
565
- // seam the block path uses (the tasking needs-attention surface is the stuck
566
- // `spec:<slug>` lock; no folder write). It is NOT a block (the gate output was
567
- // unreadable) — record it as the transient-infra-class re-run signal so the
568
- // stuck reason reads correctly; nothing landed.
569
- const reason = `The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
570
- `(re-run — transient): ${core.reason ?? ''}`;
571
- return await surfaceTaskingBlock({
572
- slug,
573
- cwd,
574
- arbiter,
575
- reason,
576
- message: `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
577
- `parked it for your attention (no tasks landed; re-run).`,
578
- lockedBlob,
579
- release: lock.release,
580
- mode: resolvedMode,
581
- provider: options.providerInstance,
582
611
  env,
583
- note,
584
- });
585
- }
586
- if (core.outcome === 'completed') {
587
- // LOCK LIFETIME IS MODE-DEPENDENT (fix
588
- // `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
589
- // the "hold-across-the-PR" capstone the older interim half deferred).
590
- //
591
- // `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
592
- // residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
593
- // has done its job — RELEASE it (delete the ref). Best-effort + idempotent
594
- // (`not-held` is fine).
595
- //
596
- // `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
597
- // still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
598
- // tasked-ness. If we released here, the next in-place scan would see the spec
599
- // eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
600
- // force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
601
- // regenerating its review forever until a human merges/closes it. So we KEEP
602
- // the `spec:<slug>` lock HELD across the open PR: the held lock IS the
603
- // "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
604
- // specs from the taskable pool (symmetric to the task-pool held-slug
605
- // subtraction). The lock is reaped when the PR resolves: a MERGE lands the
606
- // durable move (which now carries tasked-ness on its own) and the reap/gc path
607
- // deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
608
- // recovery the human owns via `release-lock` (the same model as any other
609
- // abandoned in-flight item — no liveness heartbeat/auto-steal).
610
- //
611
- // MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
612
- // `spec-<slug>` lock entry the acquire took).
613
- if (useLock && resolvedMode === 'merge') {
614
- await releaseItemLock({ item: `spec:${slug}`, cwd, arbiter, env });
615
- }
616
- }
617
- return integrationToTaskResult(core, { slug, emitted, loop: loopTag });
618
- }
619
- // HUMAN, no-lock path: the human commits on `main` directly (the runner does
620
- // not own the human's git). We report the produced tasks; moving the spec into
621
- // `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
622
- // do, as with the human `complete`.
623
- const message = `Tasked '${slug}' -> ${emitted.length} backlog task` +
624
- `${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
625
- `the produced files (and move the spec into work/specs/tasked/) yourself.`;
626
- note(message);
627
- return {
628
- exitCode: 0,
629
- outcome: 'tasked',
612
+ }),
613
+ },
614
+ env,
615
+ // The task-SET acceptance review AGENT launches AMBIENT, never the
616
+ // identity-scoped `env` (an agent must not act as the bot).
617
+ agentEnv,
618
+ note,
619
+ });
620
+ const surfaceBlock = (reason, message) => surfaceTaskingBlock({
630
621
  slug,
631
- emitted,
632
- loop: loopTag,
622
+ cwd,
623
+ arbiter,
624
+ reason,
633
625
  message,
634
- };
626
+ lockedBlob,
627
+ release: params.release,
628
+ mode: resolvedMode,
629
+ provider: params.providerInstance,
630
+ env,
631
+ note,
632
+ });
633
+ // THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
634
+ // fresh-context review of the produced SET returned `block`, so the core ran
635
+ // the review BEFORE the stage/integrate and did NOT integrate the tasks
636
+ // (correct). The CORRECT task-path destination is the SAME needs-attention
637
+ // route the lock release owns for the decomposition-unclear verdict: it surfaces
638
+ // the spec and releases the `spec:<slug>` lock with the block reason. So on a
639
+ // block we route the held spec to needs-attention THROUGH the lock release —
640
+ // the set never lands.
641
+ if (core.outcome === 'review-blocked') {
642
+ return await surfaceBlock(taskGateBlockedReason(slug, core.reviewBlockReason), `The task acceptance gate disapproved the set produced for '${slug}'; ` +
643
+ `parked it for your attention (no tasks landed). ` +
644
+ `Resolve the findings, then re-task.`);
645
+ }
646
+ if (core.outcome === 'sidecar-violation') {
647
+ // A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
648
+ // (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
649
+ // `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
650
+ // before the stage/integrate; route the held spec to needs-attention through
651
+ // the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
652
+ // carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
653
+ return await surfaceBlock(`The spec '${slug}' carries a co-located asset sidecar: ` +
654
+ `${core.reviewBlockReason ?? core.reason ?? ''}`, `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
655
+ `rule 8); parked it for your attention (no tasks landed; relocate it ` +
656
+ `to docs/spikes/${slug}/ and reference by path).`);
657
+ }
658
+ if (core.outcome === 'review-unparseable') {
659
+ // The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
660
+ // JSON). Route the held spec to needs-attention through the SAME lock-release
661
+ // seam the block path uses. It is NOT a block (the gate output was
662
+ // unreadable) — record it as the transient-infra-class re-run signal so the
663
+ // stuck reason reads correctly; nothing landed.
664
+ return await surfaceBlock(taskGateUnparseableReason(slug, core.reason), `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
665
+ `parked it for your attention (no tasks landed; re-run).`);
666
+ }
667
+ if (core.outcome === 'completed') {
668
+ // LOCK LIFETIME IS MODE-DEPENDENT (fix
669
+ // `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
670
+ // the "hold-across-the-PR" capstone the older interim half deferred).
671
+ //
672
+ // `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
673
+ // residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
674
+ // has done its job — RELEASE it (delete the ref). Best-effort + idempotent
675
+ // (`not-held` is fine).
676
+ //
677
+ // `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
678
+ // still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
679
+ // tasked-ness. If we released here, the next in-place scan would see the spec
680
+ // eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
681
+ // force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
682
+ // regenerating its review forever until a human merges/closes it. So we KEEP
683
+ // the `spec:<slug>` lock HELD across the open PR: the held lock IS the
684
+ // "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
685
+ // specs from the taskable pool (symmetric to the task-pool held-slug
686
+ // subtraction). The lock is reaped when the PR resolves: a MERGE lands the
687
+ // durable move (which now carries tasked-ness on its own) and the reap/gc path
688
+ // deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
689
+ // recovery the human owns via `release-lock` (the same model as any other
690
+ // abandoned in-flight item — no liveness heartbeat/auto-steal).
691
+ //
692
+ // MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
693
+ // `spec-<slug>` lock entry the acquire took).
694
+ if (resolvedMode === 'merge') {
695
+ await releaseItemLock({ item: `spec:${slug}`, cwd, arbiter, env });
696
+ }
697
+ }
698
+ return integrationToTaskResult(core, { slug, emitted, loop: loopTag });
635
699
  }
636
700
  /**
637
701
  * Map a non-`released` lock-release result onto the {@link TaskResult} contract
@@ -670,9 +734,16 @@ function releaseFailureToResult(released, slug) {
670
734
  function integrationToTaskResult(core, ctx) {
671
735
  const { slug, emitted, loop } = ctx;
672
736
  if (core.outcome === 'completed') {
737
+ // Say "opened a PR" ONLY when the provider opened one (task
738
+ // `intake-reports-the-pr-it-actually-opened`); a degraded `gh pr create`
739
+ // pushed the branch but opened nothing, and the shared core printed why.
740
+ const notOpened = proposeRequestNotOpened(core.integration);
673
741
  const landed = core.integration?.mode === 'merge'
674
742
  ? 'landed them on the arbiter main'
675
- : 'opened a PR carrying them (main untouched)';
743
+ : notOpened !== undefined
744
+ ? `pushed them on branch ${core.integration?.pushedRef ?? core.branch} ` +
745
+ 'but opened NO PR (main untouched; see the provider note above)'
746
+ : 'opened a PR carrying them (main untouched)';
676
747
  const message = `Tasked '${slug}' -> ${emitted.length} backlog task` +
677
748
  `${emitted.length === 1 ? '' : 's'}; the runner integrated the transition ` +
678
749
  `through the shared core (moved work/specs/ready/ -> work/specs/tasked/, the ` +
@@ -706,7 +777,14 @@ function integrationToTaskResult(core, ctx) {
706
777
  * pre-existing local `work/<slug>` (a re-run) is force-recreated off fresh main.
707
778
  * The agent runs in-place on this branch (branch ≠ worktree).
708
779
  */
709
- async function switchToWorkBranch(cwd, arbiter, slug, env) {
780
+ export async function switchToWorkBranch(cwd, arbiter, slug, env,
781
+ /**
782
+ * The commit to cut the branch from; default the freshly fetched
783
+ * `<arbiter>/main`. The CI phases pass the lock job's `baseSha` (task
784
+ * `ci-split-tasking`), so the agent and apply jobs work on the tree the lock
785
+ * job classified.
786
+ */
787
+ base) {
710
788
  // The tasking path is the parent-spec namespace (`do spec:<slug>`): the branch
711
789
  // is `work/spec-<slug>`, distinct from a same-slug task-build's `work/task-<slug>`.
712
790
  // MIGRATE step (spec `prd-to-spec-vocabulary-cutover-and-migration-command`):
@@ -714,7 +792,7 @@ async function switchToWorkBranch(cwd, arbiter, slug, env) {
714
792
  // parses the legacy `work/prd-<slug>` form, so in-flight branches keep resolving).
715
793
  const branch = workBranchRef('spec', slug);
716
794
  await gitHard(['fetch', '--quiet', arbiter], cwd, env);
717
- await gitHard(['switch', '--quiet', '-C', branch, `${arbiter}/main`], cwd, env);
795
+ await gitHard(['switch', '--quiet', '-C', branch, base ?? `${arbiter}/main`], cwd, env);
718
796
  }
719
797
  /**
720
798
  * The READ-STABILITY content-identity STALE CHECK (the lock's backstop, owned at
@@ -876,12 +954,21 @@ async function gitHard(args, cwd, env) {
876
954
  * generic line when absent. DISTINCT from the improver loop's
877
955
  * {@link decompositionUnclearReason} (which carries the loop's open questions).
878
956
  */
879
- function taskGateBlockedReason(slug, findingsReason) {
957
+ export function taskGateBlockedReason(slug, findingsReason) {
880
958
  const head = `The task acceptance gate (fresh-context review of the produced SET) blocked ` +
881
959
  `'${slug}'. The spec is parked for your attention with no tasks landed; ` +
882
960
  `resolve the blocking findings, then re-task.`;
883
961
  return findingsReason ? `${head}\n\n${findingsReason}` : head;
884
962
  }
963
+ /**
964
+ * The needs-attention REASON for a task-SET ACCEPTANCE GATE whose verdict was
965
+ * UNPARSEABLE (the gate ran; its output could not be read): NOT a block, the
966
+ * transient-infra re-run signal. `detail` is the gate's parse-failure reason.
967
+ */
968
+ export function taskGateUnparseableReason(slug, detail) {
969
+ return (`The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
970
+ `(re-run — transient): ${detail ?? ''}`);
971
+ }
885
972
  /**
886
973
  * Shared terminal for a tasking transition that DID NOT land — a disapproving
887
974
  * task-set review, a co-located sidecar violation, an unparseable verdict, or a
@@ -903,7 +990,7 @@ function taskGateBlockedReason(slug, findingsReason) {
903
990
  * is GREEN — a park-for-human is not a failure). Only a surface that FAILED to
904
991
  * publish maps to a non-zero {@link releaseFailureToResult}.
905
992
  */
906
- async function surfaceTaskingBlock(params) {
993
+ export async function surfaceTaskingBlock(params) {
907
994
  const { slug, cwd, arbiter, reason, message, lockedBlob, release, mode, provider, env, note, } = params;
908
995
  // 1. Close a stale OPEN PR (propose only, only-if-exists) with the review as
909
996
  // the closing comment; keep the branch for a later approving re-task to
@@ -940,7 +1027,7 @@ async function surfaceTaskingBlock(params) {
940
1027
  * spec is parked for the human with these open questions + a question sidecar on
941
1028
  * main, no guessed tasks). Prose only — recorded as the parked question's reason.
942
1029
  */
943
- function decompositionUnclearReason(slug, questions) {
1030
+ export function decompositionUnclearReason(slug, questions) {
944
1031
  const head = `The tasker review→edit loop could not converge on a sound decomposition of ` +
945
1032
  `'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is parked ` +
946
1033
  `for your attention with no guessed tasks; resolve:`;
@@ -960,7 +1047,7 @@ function decompositionUnclearReason(slug, questions) {
960
1047
  * The candidate tasks the tasker produced are PERSISTED on the work branch (see
961
1048
  * {@link persistTaskingCandidates}) so a human can recover them.
962
1049
  */
963
- function reviewFailureReason(slug, err, candidatePaths) {
1050
+ export function reviewFailureReason(slug, err, candidatePaths) {
964
1051
  const named = err instanceof ReviewOutputCappedError
965
1052
  ? `The tasker review leg failed on '${slug}': ${err.message}. This is a ` +
966
1053
  "STRUCTURAL cap-truncation (the review edits payload shared the model's " +
@@ -979,7 +1066,7 @@ function reviewFailureReason(slug, err, candidatePaths) {
979
1066
  return `${named}${tasks}`;
980
1067
  }
981
1068
  /** The human-readable terminal MESSAGE for a review-leg failure (the result's `message`). */
982
- function reviewFailureMessage(slug, err) {
1069
+ export function reviewFailureMessage(slug, err) {
983
1070
  if (err instanceof ReviewOutputCappedError) {
984
1071
  return (`Tasking '${slug}' parked: the review leg hit the model output cap ` +
985
1072
  `(${err.outputTokens} tokens) and was truncated before emitting its ` +
@@ -1000,7 +1087,7 @@ function reviewFailureMessage(slug, err) {
1000
1087
  * candidate paths persisted (empty when there were none). Never throws — a git
1001
1088
  * failure is noted and the bounce proceeds (the lock is still released).
1002
1089
  */
1003
- async function persistTaskingCandidates(cwd, slug, before, arbiter, env, note) {
1090
+ export async function persistTaskingCandidates(cwd, slug, before, arbiter, env, note) {
1004
1091
  const candidatePaths = newOrChangedStagedTasks(cwd, before);
1005
1092
  if (candidatePaths.length === 0) {
1006
1093
  return [];
@@ -1029,7 +1116,12 @@ async function persistTaskingCandidates(cwd, slug, before, arbiter, env, note) {
1029
1116
  // teardown on an isolated run (the branch ref survives locally too, but a push
1030
1117
  // makes them remote-recoverable). A push failure is noted, never fatal.
1031
1118
  try {
1032
- const pushed = await gitSoft(['push', arbiter, branch], cwd, env);
1119
+ const pushed = await refWrite.pushTaskingCandidatesBranch({
1120
+ arbiter,
1121
+ branch,
1122
+ cwd,
1123
+ env,
1124
+ });
1033
1125
  if (pushed.status !== 0) {
1034
1126
  note(`Could not push the candidate-tasks work branch '${branch}' to ${arbiter} ` +
1035
1127
  `(best-effort; the local commit still persists for recovery).`);
@@ -1085,7 +1177,7 @@ function appendQuestionsBlock(content, questions) {
1085
1177
  * cross-spec `taskedAfter` ordering, resolved against `work/specs/tasked/` residence of
1086
1178
  * the specs present in the checkout.
1087
1179
  */
1088
- function resolveAgentGate(cwd, slug, specFm, autoTask, explicit) {
1180
+ export function resolveAgentGate(cwd, slug, specFm, autoTask, explicit) {
1089
1181
  return resolveTaskingEligibility({
1090
1182
  humanOnly: specFm.humanOnly,
1091
1183
  needsAnswers: specFm.needsAnswers,
@@ -1096,7 +1188,7 @@ function resolveAgentGate(cwd, slug, specFm, autoTask, explicit) {
1096
1188
  });
1097
1189
  }
1098
1190
  /** Build an HONEST gate-refusal message naming WHY the agent skipped the spec. */
1099
- function gateRefusalReason(slug, specFm, eligibility, options) {
1191
+ export function gateRefusalReason(slug, specFm, eligibility, options) {
1100
1192
  const reasons = [];
1101
1193
  if (specFm.humanOnly === true) {
1102
1194
  reasons.push('the spec is humanOnly (a human must drive its tasking)');
@@ -1234,7 +1326,7 @@ async function runTaskAgent(options, cwd, prompt, slug) {
1234
1326
  return { ok: launched.ok, detail: launched.detail };
1235
1327
  }
1236
1328
  /** A snapshot of {@link STAGED_TASKS_DIR}: filename → file content. */
1237
- function snapshotStagedTasks(cwd) {
1329
+ export function snapshotStagedTasks(cwd) {
1238
1330
  const dir = join(cwd, STAGED_TASKS_DIR);
1239
1331
  const snap = new Map();
1240
1332
  for (const file of listMarkdown(dir)) {
@@ -1249,7 +1341,7 @@ function snapshotStagedTasks(cwd) {
1249
1341
  * re-committed.) The agent's staging folder is `work/tasks/backlog/`; writes to
1250
1342
  * the pool `work/tasks/ready/` are scrubbed at stage time, never picked up here.
1251
1343
  */
1252
- function newOrChangedStagedTasks(cwd, before) {
1344
+ export function newOrChangedStagedTasks(cwd, before) {
1253
1345
  const dir = join(cwd, STAGED_TASKS_DIR);
1254
1346
  const changed = [];
1255
1347
  for (const file of listMarkdown(dir)) {
@@ -1261,7 +1353,7 @@ function newOrChangedStagedTasks(cwd, before) {
1261
1353
  return changed.sort();
1262
1354
  }
1263
1355
  /** Snapshot the POOL `work/tasks/ready/` (for the agent-write fence at stage time). */
1264
- function snapshotPool(cwd) {
1356
+ export function snapshotPool(cwd) {
1265
1357
  const dir = workFolderPath(cwd, 'tasks-ready');
1266
1358
  const snap = new Map();
1267
1359
  for (const file of listMarkdown(dir)) {
@@ -1270,7 +1362,7 @@ function snapshotPool(cwd) {
1270
1362
  return snap;
1271
1363
  }
1272
1364
  /** Read the produced backlog tasks' content keyed by repo-relative path. */
1273
- function collectEmittedTasks(cwd, relPaths) {
1365
+ export function collectEmittedTasks(cwd, relPaths) {
1274
1366
  const out = {};
1275
1367
  for (const rel of relPaths) {
1276
1368
  out[rel] = readFileSync(join(cwd, rel), 'utf8');
@@ -1410,10 +1502,12 @@ function readTitleField(content) {
1410
1502
  let value = m[1].trim();
1411
1503
  if (value === '')
1412
1504
  return undefined;
1413
- // Quoted ⇒ return inner text verbatim; unquoted ⇒ strip trailing `# ...`.
1414
- if ((value.startsWith('"') && value.endsWith('"')) ||
1415
- (value.startsWith("'") && value.endsWith("'"))) {
1416
- return value.slice(1, -1);
1505
+ // Quoted ⇒ return the inner text (a single-quoted `''` decodes to `'`, the
1506
+ // shape `quoteYamlScalar` writes); unquoted ⇒ strip trailing `# ...`.
1507
+ if (value.length >= 2 &&
1508
+ ((value.startsWith('"') && value.endsWith('"')) ||
1509
+ (value.startsWith("'") && value.endsWith("'")))) {
1510
+ return unquoteYamlScalar(value);
1417
1511
  }
1418
1512
  const hash = value.indexOf('#');
1419
1513
  if (hash !== -1)