dorfl 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +309 -489
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +46 -14
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +123 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -0
  162. package/dist/install-ci-action-pins.js +190 -0
  163. package/dist/install-ci-action-pins.js.map +1 -0
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +155 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +304 -200
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -292
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +8 -4
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +375 -538
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +52 -15
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +209 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +388 -233
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -327
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +8 -4
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
@@ -0,0 +1,906 @@
1
+ /**
2
+ * **The tasking path split into the three CI phases** (spec
3
+ * `ci-agent-job-without-write-token` §3 tasking row, ADR
4
+ * `ci-agent-job-holds-no-write-token`, task `ci-split-tasking`).
5
+ *
6
+ * `do spec:<slug>` / `advance spec:<slug>` with `--phase` run ONE of three
7
+ * halves of the one tasking pipeline (`tasking.ts`: {@link runTaskingAgentHalf}
8
+ * is the agent half, {@link integrateTaskingCandidates} /
9
+ * {@link surfaceTaskingBlock} / {@link persistTaskingCandidates} the write half),
10
+ * never a second implementation:
11
+ *
12
+ * - **lock** ({@link performTaskingLockPhase}): fetch `<arbiter>/main` (its tip
13
+ * is `baseSha`); in a throwaway worktree of it, check the spec rests in
14
+ * `work/specs/ready/` and passes the agent tasking gate (and, for `advance`,
15
+ * classifies on the `task-spec` rung); take the tasking lock
16
+ * (`acquireTaskingLock`, the unified `spec:<slug>` lock, `action: task`);
17
+ * publish `acquired`, `needsAgent`, `rung`, `baseSha`, `lockSha`,
18
+ * `handoffName` and `agentTimeoutMinutes`. No agent, no repository code.
19
+ * - **agent** ({@link performTaskingAgentPhase}): check, read-only, that the
20
+ * lock ref still equals `lockSha`; on a `work/spec-<slug>` branch cut from
21
+ * `baseSha`, run the tasker, its review rounds, and the one-round task-set
22
+ * review (which the laptop path runs inside `performIntegration`; it is an
23
+ * agent, so it runs HERE), then hand over `tasking-land` (the candidates, the
24
+ * PR body, the review verdict and prose, the trimmed spec body) or
25
+ * `tasking-surface` (the candidates to save, the reason). Writes nothing to
26
+ * the arbiter: every write seam records into a recorder that knows no write.
27
+ * A tasker failure writes no handoff and fails the job.
28
+ * - **apply** ({@link performTaskingApplyPhase}): refuse to write unless the
29
+ * lock ref still equals `lockSha`; act on the agent job's result first
30
+ * (`failure` / timeout surfaces the spec, a real cancel only releases the
31
+ * lock); read the handoff as hostile and never take a bundle: it re-commits
32
+ * only VALIDATED candidate files ({@link checkTaskingCandidate}) and the
33
+ * validated trimmed spec body ({@link checkTrimmedSpecBody}) on a fresh
34
+ * `work/spec-<slug>` branch cut from `baseSha`, then integrates with the spec
35
+ * move and the review OFF (it already ran), or saves the candidates on the
36
+ * work branch, closes a stale PR in propose mode and surfaces. Every lock
37
+ * release is leased on `lockSha`.
38
+ */
39
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
40
+ import { basename, dirname, join } from 'node:path';
41
+ import { classifyTick } from './advance-classify.js';
42
+ import { readItemSignals } from './advance.js';
43
+ import { HANDOFF_LIMITS, HandoffRejected, handoffName as deriveHandoffName, } from './ci-handoff-format.js';
44
+ import { readHandoff, writeHandoff } from './ci-handoff.js';
45
+ import { PhaseDriverError, agentTimeoutMinutesAt, arbiterLockSha, boundHandoffText, acquireNotingOnce, checkLockOwnership, emitLockOutputs, fetchArbiterMain, noSmudgeEnv, releaseLockLeased, requireHeldLock, resolveAgentResult, runnerTempFrom, withBaseWorktree, } from './ci-phase-driver.js';
46
+ import { FrontmatterRenderError, assertFrontmatterFields, parseFrontmatter, unquoteYamlScalar, } from './frontmatter.js';
47
+ import { runAsync } from './git.js';
48
+ import { identityEnv } from './identity.js';
49
+ import { runReviewRounds } from './integration-core.js';
50
+ import { leaseLockReleases } from './item-lock.js';
51
+ import { formatBlockReason } from './review-gate.js';
52
+ import { ledgerRead } from './ledger-read.js';
53
+ import { createPhaseRecorder, runAgentPhase } from './phase-recorder.js';
54
+ import { isSafeSlug } from './slug-safety.js';
55
+ import { SlugResolutionError, resolveAdvanceArg, resolveSlug, } from './slug-namespace.js';
56
+ import { STAGED_TASKS_DIR, collectEmittedTasks, composeTaskingProposeBody, decompositionUnclearReason, gateRefusalReason, integrateTaskingCandidates, newOrChangedStagedTasks, persistTaskingCandidates, resolveAgentGate, reviewFailureReason, runTaskingAgentHalf, snapshotPool, snapshotStagedTasks, surfaceTaskingBlock, switchToWorkBranch, taskGateBlockedReason, taskGateUnparseableReason, taskPlacementDir, } from './tasking.js';
57
+ import { acquireTaskingLock, releaseTaskingLock } from './tasking-lock.js';
58
+ import { workFolderRel, workItemPath, workItemRel } from './work-layout.js';
59
+ const DEFAULT_ARBITER = 'origin';
60
+ /**
61
+ * Run one phase of the tasking path. The CLI calls this for `do spec:<slug>
62
+ * --phase` and `advance spec:<slug> --phase`.
63
+ */
64
+ export async function performTaskingPhase(options) {
65
+ try {
66
+ switch (options.phase) {
67
+ case 'lock':
68
+ return await performTaskingLockPhase(options);
69
+ case 'agent':
70
+ return await performTaskingAgentPhase(options);
71
+ case 'apply':
72
+ return await performTaskingApplyPhase(options);
73
+ }
74
+ }
75
+ catch (err) {
76
+ if (err instanceof PhaseDriverError || err instanceof SlugResolutionError) {
77
+ return { exitCode: 1, outcome: 'usage-error', message: err.message };
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+ /** The spec slug a tasking phase acts on, from the (trusted) workflow argument. */
83
+ function taskingSlug(options, repoPath) {
84
+ const read = options.read ?? ledgerRead;
85
+ const resolved = options.verb === 'advance'
86
+ ? resolveAdvanceArg({ arg: options.arg, repoPath, read })
87
+ : resolveSlug({ arg: options.arg, repoPath, read });
88
+ if (resolved.namespace !== 'spec') {
89
+ throw new PhaseDriverError(`the tasking phases take a spec; ${options.arg} is not one`);
90
+ }
91
+ return resolved.slug;
92
+ }
93
+ /**
94
+ * The `performTask` options the tasking halves read, mapped from the `do`
95
+ * options exactly as `performDo` maps them for `do spec:<slug>`.
96
+ */
97
+ function taskOptionsFor(options, slug, cwd, env, agentEnv) {
98
+ return {
99
+ slug,
100
+ cwd,
101
+ arbiter: options.arbiter ?? DEFAULT_ARBITER,
102
+ doer: 'agent',
103
+ autoTask: options.autoTask,
104
+ explicit: true,
105
+ dorfl: options.dorfl,
106
+ harness: options.harness,
107
+ agentCmd: options.agentCmd,
108
+ model: options.model,
109
+ sessionsDir: options.sessionsDir,
110
+ integration: options.taskingIntegration ?? options.integration,
111
+ mergeRetries: options.mergeRetries,
112
+ tasksLandIn: options.tasksLandIn,
113
+ untrustedTasksLandIn: options.untrustedTasksLandIn,
114
+ explicitTasksLandIn: options.explicitTasksLandIn,
115
+ noPR: options.noPR,
116
+ providerInstance: options.providerInstance,
117
+ reviewLoop: options.reviewLoop,
118
+ taskerLoopMax: options.taskerLoopMax,
119
+ reviewExecutions: options.reviewExecutions,
120
+ taskerLoopModel: options.taskerLoopModel,
121
+ review: options.review,
122
+ reviewGate: options.taskReviewGate,
123
+ acceptanceReviewModel: options.reviewModel,
124
+ env,
125
+ agentEnv,
126
+ note: options.note,
127
+ };
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // lock
131
+ // ---------------------------------------------------------------------------
132
+ /**
133
+ * The lock phase: check the spec at the arbiter's `main`, take the tasking lock
134
+ * and publish the trusted facts. Runs no agent and no repository code. A spec
135
+ * that is not taskable at `baseSha` is refused BEFORE any write.
136
+ */
137
+ export async function performTaskingLockPhase(options) {
138
+ const note = options.note ?? (() => { });
139
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
140
+ const cwd = options.cwd;
141
+ const env = identityEnv(options.identity, options.env ?? process.env);
142
+ const githubOutput = options.githubOutput ?? env.GITHUB_OUTPUT;
143
+ const publish = (facts) => {
144
+ emitLockOutputs(facts, { githubOutput, note });
145
+ return facts;
146
+ };
147
+ const baseSha = await fetchArbiterMain({ cwd, arbiter, env });
148
+ const verdict = await withBaseWorktree({ cwd, baseSha, env }, async (base) => {
149
+ const slug = taskingSlug(options, base);
150
+ const item = `spec:${slug}`;
151
+ if (options.verb === 'advance') {
152
+ const signals = readItemSignals({
153
+ repoPath: base,
154
+ type: 'spec',
155
+ slug,
156
+ item,
157
+ });
158
+ const kind = classifyTick({ type: 'spec', ...signals }).kind;
159
+ if (kind !== 'task-spec') {
160
+ if (kind === 'no-op') {
161
+ return { slug, skip: `no-op for ${item} at ${baseSha}` };
162
+ }
163
+ throw new PhaseDriverError(`--phase splits only the build and tasking paths so far; ${item} ` +
164
+ `classifies as ${kind} at ${baseSha}`);
165
+ }
166
+ }
167
+ const specPath = workItemPath(base, 'specs-ready', slug);
168
+ if (!existsSync(specPath)) {
169
+ return {
170
+ slug,
171
+ skip: `'${slug}' is not in ${workFolderRel('specs-ready')}/ at ` +
172
+ `${arbiter}/main (${baseSha}): it was already tasked, or removed`,
173
+ };
174
+ }
175
+ const specFm = parseFrontmatter(readFileSync(specPath, 'utf8'));
176
+ const eligibility = resolveAgentGate(base, slug, specFm, options.autoTask, true);
177
+ if (!eligibility.taskable) {
178
+ return {
179
+ slug,
180
+ refused: gateRefusalReason(slug, specFm, eligibility, {
181
+ slug,
182
+ cwd: base,
183
+ autoTask: options.autoTask,
184
+ explicit: true,
185
+ }),
186
+ };
187
+ }
188
+ return { slug };
189
+ });
190
+ const slug = verdict.slug;
191
+ const item = `spec:${slug}`;
192
+ if (verdict.skip !== undefined) {
193
+ return {
194
+ exitCode: 0,
195
+ outcome: 'no-op',
196
+ slug,
197
+ message: verdict.skip,
198
+ lockOutputs: publish({ acquired: false, baseSha }),
199
+ };
200
+ }
201
+ if (verdict.refused !== undefined) {
202
+ return {
203
+ exitCode: 1,
204
+ outcome: 'gate-refused',
205
+ slug,
206
+ message: verdict.refused,
207
+ lockOutputs: publish({ acquired: false, rung: 'task-spec', baseSha }),
208
+ };
209
+ }
210
+ if ((await arbiterLockSha({ cwd, arbiter, item, env })) !== undefined) {
211
+ const message = `'${slug}' is already locked on ${arbiter}; backing off.`;
212
+ return {
213
+ exitCode: 0,
214
+ outcome: 'lost',
215
+ slug,
216
+ message,
217
+ lockOutputs: publish({ acquired: false, rung: 'task-spec', baseSha }),
218
+ };
219
+ }
220
+ const acquired = await acquireNotingOnce(note, (acquireNote) => acquireTaskingLock({ slug, cwd, arbiter, env, note: acquireNote }));
221
+ if (acquired.exitCode !== 0) {
222
+ const outcome = acquired.outcome === 'lost' || acquired.outcome === 'contended'
223
+ ? 'lost'
224
+ : 'usage-error';
225
+ return {
226
+ // A lock lost to another run is handled, so green ("A handled
227
+ // outcome is green", `ci-phase-driver.ts`).
228
+ exitCode: outcome === 'lost' ? 0 : acquired.exitCode,
229
+ outcome,
230
+ slug,
231
+ message: acquired.message,
232
+ lockOutputs: publish({ acquired: false, rung: 'task-spec', baseSha }),
233
+ };
234
+ }
235
+ const lockSha = await arbiterLockSha({ cwd, arbiter, item, env });
236
+ if (lockSha === undefined) {
237
+ throw new Error(`the lock of ${item} vanished right after the acquire`);
238
+ }
239
+ const facts = publish({
240
+ acquired: true,
241
+ needsAgent: true,
242
+ rung: 'task-spec',
243
+ baseSha,
244
+ lockSha,
245
+ handoffName: deriveHandoffName(item, options.runAttempt ?? env.GITHUB_RUN_ATTEMPT ?? '1'),
246
+ agentTimeoutMinutes: agentTimeoutMinutesAt(cwd, baseSha, env),
247
+ });
248
+ return {
249
+ exitCode: 0,
250
+ outcome: 'locked',
251
+ slug,
252
+ message: `locked ${item} at ${baseSha} (lock ${lockSha})`,
253
+ lockOutputs: facts,
254
+ };
255
+ }
256
+ // ---------------------------------------------------------------------------
257
+ // agent
258
+ // ---------------------------------------------------------------------------
259
+ /** The candidate task files keyed by slug (their file name without `.md`). */
260
+ function candidatesBySlug(emitTasks) {
261
+ const out = {};
262
+ for (const [rel, content] of Object.entries(emitTasks)) {
263
+ out[basename(rel).replace(/\.md$/i, '')] = content;
264
+ }
265
+ return out;
266
+ }
267
+ /** A bounded reason for the handoff record. */
268
+ function reasonText(text) {
269
+ return boundHandoffText(text, HANDOFF_LIMITS.reasonChars);
270
+ }
271
+ /**
272
+ * The agent half's work under the recorder: the tasker, the loop and the
273
+ * task-set review, mapped to the handoff record. Throws on a tasker failure.
274
+ */
275
+ async function taskingHandover(params) {
276
+ const { options, slug, item, cwd, env, note } = params;
277
+ const specPath = workItemPath(cwd, 'specs-ready', slug);
278
+ if (!existsSync(specPath)) {
279
+ throw new Error(`the spec ${workItemRel('specs-ready', `${slug}.md`)} is missing at the base`);
280
+ }
281
+ const specFm = parseFrontmatter(readFileSync(specPath, 'utf8'));
282
+ const taskOptions = taskOptionsFor(options, slug, cwd, env, env);
283
+ const half = await runTaskingAgentHalf(taskOptions, {
284
+ cwd,
285
+ slug,
286
+ specFm,
287
+ doer: 'agent',
288
+ note,
289
+ });
290
+ const surface = (reason, candidates) => ({
291
+ schema: 1,
292
+ item,
293
+ intent: { kind: 'tasking-surface' },
294
+ products: {
295
+ candidates,
296
+ reason: reasonText(reason),
297
+ },
298
+ });
299
+ switch (half.kind) {
300
+ case 'agent-failed':
301
+ throw new Error(`Agent failed tasking '${slug}' (${half.detail}).`);
302
+ case 'review-leg-failed': {
303
+ const paths = newOrChangedStagedTasks(cwd, half.before);
304
+ return surface(reviewFailureReason(slug, half.error, paths), candidatesBySlug(collectEmittedTasks(cwd, paths)));
305
+ }
306
+ case 'decomposition-unclear':
307
+ return surface(decompositionUnclearReason(slug, half.questions), {});
308
+ case 'candidates':
309
+ break;
310
+ }
311
+ const emitTasks = collectEmittedTasks(cwd, newOrChangedStagedTasks(cwd, half.before));
312
+ // THE TASK-SET ACCEPTANCE GATE, one round (the laptop path runs it inside
313
+ // `performIntegration` with `reviewMaxRounds: 1`). It launches an agent, so
314
+ // it runs here; the apply phase integrates with the review OFF.
315
+ let reviewVerdict;
316
+ let reviewProse;
317
+ if (taskOptions.review === true) {
318
+ if (taskOptions.reviewGate === undefined) {
319
+ throw new Error(`review is on but no task-set review gate is configured for '${slug}' ` +
320
+ '(a wiring bug; the gate must not be skipped)');
321
+ }
322
+ note('Running the task-set acceptance review…');
323
+ const rounds = await runReviewRounds({
324
+ reviewGate: taskOptions.reviewGate,
325
+ slug,
326
+ reviewCwd: cwd,
327
+ maxRounds: 1,
328
+ reviewModel: taskOptions.acceptanceReviewModel,
329
+ sessionsDir: taskOptions.sessionsDir,
330
+ env,
331
+ });
332
+ if (rounds.kind === 'unparseable') {
333
+ return surface(taskGateUnparseableReason(slug, rounds.reason), {});
334
+ }
335
+ if (rounds.kind === 'blocked') {
336
+ const findings = rounds.verdict === undefined ? '' : formatBlockReason(rounds.verdict);
337
+ return surface(taskGateBlockedReason(slug, findings === '' ? undefined : findings), {});
338
+ }
339
+ reviewVerdict = 'approve';
340
+ reviewProse = rounds.verdict?.review;
341
+ }
342
+ return {
343
+ schema: 1,
344
+ item,
345
+ intent: { kind: 'tasking-land' },
346
+ products: {
347
+ candidates: candidatesBySlug(emitTasks),
348
+ prBody: boundHandoffText(composeTaskingProposeBody(slug, emitTasks) ?? '', HANDOFF_LIMITS.commentChars),
349
+ ...(reviewVerdict === undefined ? {} : { reviewVerdict }),
350
+ ...(reviewProse === undefined
351
+ ? {}
352
+ : {
353
+ reviewProse: boundHandoffText(reviewProse, HANDOFF_LIMITS.commentChars),
354
+ }),
355
+ specBody: readFileSync(specPath, 'utf8'),
356
+ },
357
+ };
358
+ }
359
+ /**
360
+ * The agent phase: after a read-only lock-ownership check, run the tasker, its
361
+ * review rounds and the task-set review on a `work/spec-<slug>` branch cut from
362
+ * `baseSha`, and write the handoff. Holds a read-only token: every write seam
363
+ * records into a recorder that knows no write, so a write attempt fails the
364
+ * job. Any failure writes no handoff and fails the job (the apply job then
365
+ * surfaces the spec).
366
+ */
367
+ export async function performTaskingAgentPhase(options) {
368
+ const note = options.note ?? (() => { });
369
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
370
+ const cwd = options.cwd;
371
+ const env = options.env ?? process.env;
372
+ const held = requireHeldLock({
373
+ lockOutputs: options.lockOutputs,
374
+ env,
375
+ rung: 'task-spec',
376
+ });
377
+ if (options.handoffDir === undefined) {
378
+ throw new PhaseDriverError('the agent phase needs a handoff directory');
379
+ }
380
+ const slug = taskingSlug(options, options.repoPath ?? cwd);
381
+ const item = `spec:${slug}`;
382
+ const ownership = await checkLockOwnership({
383
+ cwd,
384
+ arbiter,
385
+ item,
386
+ lockSha: held.lockSha,
387
+ env,
388
+ });
389
+ if (!ownership.owned) {
390
+ return {
391
+ exitCode: 0,
392
+ outcome: 'stale-lock',
393
+ slug,
394
+ message: ownership.message,
395
+ };
396
+ }
397
+ let record;
398
+ try {
399
+ await switchToWorkBranch(cwd, arbiter, slug, env, held.baseSha);
400
+ const outcome = await runAgentPhase(createPhaseRecorder({ record: [] }), () => taskingHandover({ options, slug, item, cwd, env, note }));
401
+ if (outcome.halted) {
402
+ throw new Error(`the tasking reached a write (${outcome.intent.seam}.${outcome.intent.method})`);
403
+ }
404
+ record = outcome.result;
405
+ writeHandoff({ dir: options.handoffDir, rung: 'task-spec', record });
406
+ }
407
+ catch (err) {
408
+ const message = err instanceof Error ? err.message : String(err);
409
+ return { exitCode: 1, outcome: 'agent-failed', slug, message };
410
+ }
411
+ const kind = record.intent.kind;
412
+ const message = `handed over ${kind} for ${item}`;
413
+ return { exitCode: 0, outcome: 'handed-over', slug, message, intent: kind };
414
+ }
415
+ /** A top-level `key: value` frontmatter line (the grammar `parseFrontmatter` reads). */
416
+ const TOP_LEVEL_KEY_RE = /^([A-Za-z0-9_.]+)\s*:\s*(.*)$/;
417
+ /**
418
+ * The frontmatter of `content` split into its block lines and everything from
419
+ * the closing fence on, or `undefined` when it does not parse: the document must
420
+ * open with a `---` line and close the fence (the rule `parseFrontmatter` uses;
421
+ * an unclosed fence reads as NO frontmatter, so every key in it is silently
422
+ * ignored). A leading BOM and CRLF line ends are normalised away.
423
+ */
424
+ function splitFrontmatter(content) {
425
+ const normalized = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
426
+ if (!normalized.startsWith('---\n'))
427
+ return undefined;
428
+ const lines = normalized.split('\n');
429
+ const closing = lines.indexOf('---', 1);
430
+ if (closing === -1)
431
+ return undefined;
432
+ return { block: lines.slice(1, closing), rest: lines.slice(closing) };
433
+ }
434
+ /** Every top-level key of a frontmatter block, in order (duplicates kept). */
435
+ function keyEntries(block) {
436
+ const out = [];
437
+ for (let i = 0; i < block.length; i++) {
438
+ const line = block[i];
439
+ if (line.trim() === '' || line.trimStart().startsWith('#'))
440
+ continue;
441
+ const m = TOP_LEVEL_KEY_RE.exec(line);
442
+ if (m === null)
443
+ continue;
444
+ let end = i + 1;
445
+ while (end < block.length && /^\s+\S/.test(block[end]))
446
+ end++;
447
+ out.push({
448
+ key: m[1],
449
+ raw: [m[2].trim(), ...block.slice(i + 1, end).map((l) => l.trim())],
450
+ start: i,
451
+ end,
452
+ });
453
+ i = end - 1;
454
+ }
455
+ return out;
456
+ }
457
+ /** The gate booleans a candidate may set; the apply job checks only that they parse. */
458
+ const CANDIDATE_BOOLEAN_KEYS = ['humanOnly', 'needsAnswers'];
459
+ /** The origin stamp keys the apply job overwrites on every candidate. */
460
+ const STAMP_KEYS = ['origin', 'originTrust'];
461
+ /**
462
+ * Check ONE candidate task file of a hostile handoff and return the content the
463
+ * apply job commits (spec §3 "Structured products", task `ci-split-tasking`).
464
+ *
465
+ * - The path is derived, never taken: `work/tasks/backlog/<slug>.md`, and the
466
+ * slug must be safe (`slug-safety.ts`; the record validation already refused
467
+ * any other key, so `..` or a folder can never reach here).
468
+ * - It must be NEW relative to `baseSha`: a candidate that names a staged task
469
+ * already on the base is an edit to a task this run did not produce.
470
+ * - Its frontmatter must parse (an opening AND a closing `---`): an unclosed
471
+ * fence reads as no frontmatter, which would silently drop the stamp.
472
+ * - `humanOnly` / `needsAnswers` must each appear at most once and read as a
473
+ * boolean. Their VALUES are not judged: the review loop that sets them ran in
474
+ * the hostile job, and every candidate lands staged for a human to promote.
475
+ * - `origin` / `originTrust` are OVERWRITTEN with the spec's stamp at `baseSha`:
476
+ * every such line is dropped (with its continuation lines) and exactly one of
477
+ * each is written, so a candidate that repeats `originTrust: trusted` cannot
478
+ * keep one past the stamp (observation
479
+ * `tasker-drafted-duplicate-origintrust-launders-propagated-stamp`); then the
480
+ * result is re-parsed with `assertFrontmatterFields`, and any duplicate or
481
+ * mismatch rejects it.
482
+ *
483
+ * Throws {@link HandoffRejected} on any breach.
484
+ */
485
+ export function checkTaskingCandidate(params) {
486
+ const { slug, content, stamp } = params;
487
+ const path = `${STAGED_TASKS_DIR}/${slug}.md`;
488
+ if (!isSafeSlug(slug)) {
489
+ throw new HandoffRejected('field', `candidate '${slug}' is not a safe slug`);
490
+ }
491
+ if (params.existsAtBase) {
492
+ throw new HandoffRejected('ledger', `candidate ${path} already exists at the base: an edit to a ` +
493
+ 'pre-existing staged task this run did not produce');
494
+ }
495
+ const fm = splitFrontmatter(content);
496
+ if (fm === undefined) {
497
+ throw new HandoffRejected('field', `candidate ${path}: its frontmatter does not parse (it must open and ` +
498
+ 'close a --- fence at the top)');
499
+ }
500
+ const entries = keyEntries(fm.block);
501
+ for (const key of CANDIDATE_BOOLEAN_KEYS) {
502
+ const found = entries.filter((e) => e.key === key);
503
+ if (found.length > 1) {
504
+ throw new HandoffRejected('field', `candidate ${path}: '${key}' appears ${found.length} times`);
505
+ }
506
+ const value = found[0];
507
+ if (value === undefined)
508
+ continue;
509
+ const v = unquoteYamlScalar(value.raw[0]).toLowerCase();
510
+ if (value.raw.length > 1 || (v !== 'true' && v !== 'false')) {
511
+ throw new HandoffRejected('field', `candidate ${path}: '${key}' does not read as a boolean`);
512
+ }
513
+ }
514
+ const drop = new Set();
515
+ for (const e of entries) {
516
+ if (STAMP_KEYS.includes(e.key)) {
517
+ for (let i = e.start; i < e.end; i++)
518
+ drop.add(i);
519
+ }
520
+ }
521
+ const block = fm.block.filter((_, i) => !drop.has(i));
522
+ if (stamp.origin !== undefined)
523
+ block.push(`origin: ${stamp.origin}`);
524
+ if (stamp.originTrust !== undefined) {
525
+ block.push(`originTrust: ${stamp.originTrust}`);
526
+ }
527
+ const stamped = ['---', ...block, ...fm.rest].join('\n');
528
+ try {
529
+ assertFrontmatterFields(stamped, {
530
+ origin: stamp.origin,
531
+ originTrust: stamp.originTrust,
532
+ });
533
+ }
534
+ catch (err) {
535
+ if (!(err instanceof FrontmatterRenderError))
536
+ throw err;
537
+ throw new HandoffRejected('field', `candidate ${path}: ${err.message}`);
538
+ }
539
+ return stamped;
540
+ }
541
+ /**
542
+ * The spec frontmatter keys a trimmed body must keep EXACTLY as they are at
543
+ * `baseSha`: the gate keys (`humanOnly`, `needsAnswers`, `taskedAfter`), the
544
+ * issue link and the origin stamp, plus `slug` (the tasked-spec identity
545
+ * `taskedAfter` of other specs resolves against).
546
+ */
547
+ export const SPEC_PINNED_KEYS = [
548
+ 'humanOnly',
549
+ 'needsAnswers',
550
+ 'taskedAfter',
551
+ 'issue',
552
+ 'origin',
553
+ 'originTrust',
554
+ 'slug',
555
+ ];
556
+ /** The headings a trimmed spec keeps (its durable framing, TASKING-PROTOCOL §6). */
557
+ export const SPEC_REQUIRED_HEADINGS = [
558
+ 'Problem Statement',
559
+ 'Solution',
560
+ 'User Stories',
561
+ ];
562
+ function hasHeading(content, heading) {
563
+ return content
564
+ .replace(/\r\n/g, '\n')
565
+ .split('\n')
566
+ .some((line) => {
567
+ const m = /^#{1,6}[ \t]+(.*?)[ \t#]*$/.exec(line);
568
+ return m !== null && m[1] === heading;
569
+ });
570
+ }
571
+ /**
572
+ * Check the trimmed spec body of a hostile `tasking-land` against the spec at
573
+ * `baseSha`, and return the body the apply job commits as the tasked spec. Its
574
+ * frontmatter must parse; every {@link SPEC_PINNED_KEYS} key must read EXACTLY
575
+ * as at `baseSha` (the same top-level lines, in the same order, with the same
576
+ * continuation lines, so neither a first-wins nor a last-wins reader can see
577
+ * another value); and every {@link SPEC_REQUIRED_HEADINGS} heading the base
578
+ * spec has must still be there. Throws {@link HandoffRejected} otherwise.
579
+ */
580
+ export function checkTrimmedSpecBody(params) {
581
+ const { slug, body, baseSpec } = params;
582
+ const what = `the trimmed spec body of '${slug}'`;
583
+ const fm = splitFrontmatter(body);
584
+ if (fm === undefined) {
585
+ throw new HandoffRejected('field', `${what}: its frontmatter does not parse (it must open and close a --- fence at the top)`);
586
+ }
587
+ const baseEntries = keyEntries(splitFrontmatter(baseSpec)?.block ?? []);
588
+ const bodyEntries = keyEntries(fm.block);
589
+ for (const key of SPEC_PINNED_KEYS) {
590
+ const want = baseEntries.filter((e) => e.key === key).map((e) => e.raw);
591
+ const got = bodyEntries.filter((e) => e.key === key).map((e) => e.raw);
592
+ if (JSON.stringify(want) !== JSON.stringify(got)) {
593
+ throw new HandoffRejected('field', `${what} changes the gate key '${key}' (it must keep its value at the base)`);
594
+ }
595
+ }
596
+ for (const heading of SPEC_REQUIRED_HEADINGS) {
597
+ if (hasHeading(baseSpec, heading) && !hasHeading(body, heading)) {
598
+ throw new HandoffRejected('field', `${what} drops the required '## ${heading}' heading`);
599
+ }
600
+ }
601
+ return body;
602
+ }
603
+ /**
604
+ * The apply phase: check the lock is still this run's, act on the agent job's
605
+ * result, validate the handoff as hostile, then resume at the tasking write
606
+ * half. Runs no agent and no repository code.
607
+ */
608
+ export async function performTaskingApplyPhase(options) {
609
+ const note = options.note ?? (() => { });
610
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
611
+ const cwd = options.cwd;
612
+ const env = noSmudgeEnv(identityEnv(options.identity, options.env ?? process.env));
613
+ const held = requireHeldLock({
614
+ lockOutputs: options.lockOutputs,
615
+ env,
616
+ rung: 'task-spec',
617
+ });
618
+ if (options.agentResult === undefined) {
619
+ throw new PhaseDriverError('the apply phase needs the agent job result (--agent-result, ' +
620
+ 'needs.agent.result): only success reads the handoff');
621
+ }
622
+ const agentResult = options.agentResult;
623
+ const slug = taskingSlug(options, options.repoPath ?? cwd);
624
+ const item = `spec:${slug}`;
625
+ // Lock ownership BEFORE the first write (spec §8): a lock that was released,
626
+ // reaped or re-taken since the lock job ran means this run writes nothing.
627
+ const ownership = await checkLockOwnership({
628
+ cwd,
629
+ arbiter,
630
+ item,
631
+ lockSha: held.lockSha,
632
+ env,
633
+ });
634
+ if (!ownership.owned) {
635
+ return {
636
+ exitCode: 0,
637
+ outcome: 'stale-lock',
638
+ slug,
639
+ message: ownership.message,
640
+ };
641
+ }
642
+ // Every lock release from here on (the merge land's, the surface's) is
643
+ // leased on the sha this run owns.
644
+ const restoreLease = leaseLockReleases(item, held.lockSha);
645
+ try {
646
+ const ctx = {
647
+ options,
648
+ held,
649
+ slug,
650
+ item,
651
+ arbiter,
652
+ cwd,
653
+ env,
654
+ note,
655
+ };
656
+ const decision = await resolveAgentResult({
657
+ result: agentResult,
658
+ held,
659
+ agentTimeoutMinutes: options.agentTimeoutMinutes,
660
+ api: options.actionsApi,
661
+ env,
662
+ });
663
+ switch (decision.action) {
664
+ case 'read-handoff':
665
+ return await applyOwned(ctx);
666
+ case 'deterministic':
667
+ return await surfaceSpec(ctx, 'the agent job was skipped (needsAgent: false), but the tasking ' +
668
+ 'path has no rung that runs without an agent; the handoff was not read');
669
+ case 'surface':
670
+ return await surfaceSpec(ctx, decision.reason);
671
+ case 'release': {
672
+ const released = await releaseLockLeased({
673
+ cwd,
674
+ arbiter,
675
+ item,
676
+ expectedSha: held.lockSha,
677
+ env,
678
+ });
679
+ note(released.message);
680
+ const reason = decision.reason;
681
+ return released.released
682
+ ? {
683
+ exitCode: 0,
684
+ outcome: 'released',
685
+ slug,
686
+ message: `Released '${slug}': ${reason}.`,
687
+ }
688
+ : {
689
+ exitCode: 1,
690
+ outcome: 'release-refused',
691
+ slug,
692
+ message: `Could not release '${slug}' (${released.message}): ${reason}.`,
693
+ };
694
+ }
695
+ }
696
+ }
697
+ finally {
698
+ restoreLease();
699
+ }
700
+ }
701
+ /**
702
+ * Surface the spec to needs-attention because the agent job did not succeed
703
+ * (decision 5): the question sidecar and `needsAnswers: true` on `main`, the lock
704
+ * released (leased). Nothing from the agent job is read or pushed.
705
+ */
706
+ async function surfaceSpec(ctx, reason) {
707
+ const { slug, cwd, arbiter, env, note } = ctx;
708
+ const routed = await releaseTaskingLock({
709
+ slug,
710
+ cwd,
711
+ arbiter,
712
+ routeToNeedsAttention: { reason },
713
+ env,
714
+ note,
715
+ });
716
+ const moved = routed.outcome === 'released';
717
+ const message = moved
718
+ ? `Surfaced '${slug}' to needs-attention: ${reason}`
719
+ : `Could not surface '${slug}' (${routed.message}): ${reason}`;
720
+ return {
721
+ exitCode: moved ? 0 : 1,
722
+ outcome: moved ? 'surfaced' : 'surface-unmoved',
723
+ slug,
724
+ message,
725
+ };
726
+ }
727
+ /** The spec at `baseSha` (trusted), or a usage error when it is not there. */
728
+ async function specAtBase(ctx) {
729
+ const rel = workItemRel('specs-ready', `${ctx.slug}.md`);
730
+ const show = await runAsync('git', ['cat-file', 'blob', `${ctx.held.baseSha}:${rel}`], ctx.cwd, { env: ctx.env });
731
+ const blob = await runAsync('git', ['rev-parse', `${ctx.held.baseSha}:${rel}`], ctx.cwd, { env: ctx.env });
732
+ if (show.status !== 0 || blob.status !== 0) {
733
+ throw new PhaseDriverError(`the spec ${rel} is not at the lock job's base ${ctx.held.baseSha}`);
734
+ }
735
+ return { content: show.stdout, blob: blob.stdout.trim() };
736
+ }
737
+ /** Whether `work/tasks/backlog/<slug>.md` exists at `baseSha`. */
738
+ async function stagedTaskAtBase(ctx, slug) {
739
+ const r = await runAsync('git', ['cat-file', '-e', `${ctx.held.baseSha}:${STAGED_TASKS_DIR}/${slug}.md`], ctx.cwd, { env: ctx.env });
740
+ return r.status === 0;
741
+ }
742
+ /** Validate every candidate of the record (throws {@link HandoffRejected}). */
743
+ async function checkCandidates(ctx, candidates, stamp) {
744
+ const out = {};
745
+ for (const [slug, content] of Object.entries(candidates)) {
746
+ out[slug] = checkTaskingCandidate({
747
+ slug,
748
+ content,
749
+ stamp,
750
+ existsAtBase: await stagedTaskAtBase(ctx, slug),
751
+ });
752
+ }
753
+ return out;
754
+ }
755
+ /** Write the checked candidates to `work/tasks/backlog/<slug>.md`. */
756
+ function writeCandidates(cwd, candidates) {
757
+ for (const [slug, content] of Object.entries(candidates)) {
758
+ const abs = join(cwd, STAGED_TASKS_DIR, `${slug}.md`);
759
+ mkdirSync(dirname(abs), { recursive: true });
760
+ writeFileSync(abs, content);
761
+ }
762
+ }
763
+ /**
764
+ * The apply phase once the lock is known to be this run's: read the handoff,
765
+ * check it, rebuild the commit on a fresh `work/spec-<slug>` branch cut from
766
+ * `baseSha`, and resume at the write half.
767
+ */
768
+ async function applyOwned(ctx) {
769
+ const { options, held, slug, item, arbiter, cwd, env, note } = ctx;
770
+ const mode = options.taskingIntegration ?? options.integration ?? 'propose';
771
+ const rejected = async (reason) => {
772
+ note(reason);
773
+ const r = await surfaceTaskingBlock({
774
+ slug,
775
+ cwd,
776
+ arbiter,
777
+ reason,
778
+ message: `the handoff was rejected; parked '${slug}' for your attention`,
779
+ lockedBlob: undefined,
780
+ release: releaseTaskingLock,
781
+ mode,
782
+ provider: options.providerInstance,
783
+ env,
784
+ note,
785
+ });
786
+ // A rejected handoff whose surface landed is handled, so green ("A
787
+ // handled outcome is green", `ci-phase-driver.ts`).
788
+ const surfaced = r.outcome === 'needs-attention';
789
+ return {
790
+ exitCode: surfaced ? 0 : 1,
791
+ outcome: surfaced ? 'rejected' : 'surface-unmoved',
792
+ slug,
793
+ message: `${reason}; ${r.message}`,
794
+ };
795
+ };
796
+ const base = await specAtBase(ctx);
797
+ const baseFm = parseFrontmatter(base.content);
798
+ const stamp = {
799
+ origin: baseFm.origin,
800
+ originTrust: baseFm.originTrust,
801
+ };
802
+ let record;
803
+ let candidates;
804
+ let specBody;
805
+ try {
806
+ if (options.handoffDir === undefined) {
807
+ throw new HandoffRejected('layout', 'no handoff directory was given');
808
+ }
809
+ record = readHandoff({
810
+ dir: options.handoffDir,
811
+ runnerTemp: runnerTempFrom(options.runnerTemp, env),
812
+ trust: { item, rung: 'task-spec' },
813
+ }).record;
814
+ const products = record.products;
815
+ candidates = await checkCandidates(ctx, products.candidates, stamp);
816
+ if (record.intent.kind === 'tasking-land') {
817
+ const land = record.products;
818
+ if (land.reviewVerdict !== undefined &&
819
+ land.reviewVerdict !== 'approve') {
820
+ throw new HandoffRejected('field', `products.reviewVerdict is ${land.reviewVerdict}: a blocked task-set ` +
821
+ 'review is a tasking-surface, never a tasking-land');
822
+ }
823
+ specBody = checkTrimmedSpecBody({
824
+ slug,
825
+ body: land.specBody,
826
+ baseSpec: base.content,
827
+ });
828
+ }
829
+ }
830
+ catch (err) {
831
+ if (!(err instanceof HandoffRejected))
832
+ throw err;
833
+ return rejected(err.message);
834
+ }
835
+ // A FRESH work branch cut from `baseSha`: nothing from the agent job but the
836
+ // checked files above reaches it.
837
+ await switchToWorkBranch(cwd, arbiter, slug, env, held.baseSha);
838
+ const before = snapshotStagedTasks(cwd);
839
+ const poolBefore = snapshotPool(cwd);
840
+ writeCandidates(cwd, candidates);
841
+ if (record.intent.kind === 'tasking-surface') {
842
+ const surfaceProducts = record.products;
843
+ await persistTaskingCandidates(cwd, slug, before, arbiter, env, note);
844
+ const r = await surfaceTaskingBlock({
845
+ slug,
846
+ cwd,
847
+ arbiter,
848
+ reason: surfaceProducts.reason,
849
+ message: `parked '${slug}' for your attention (no tasks landed)`,
850
+ lockedBlob: base.blob,
851
+ release: releaseTaskingLock,
852
+ mode,
853
+ provider: options.providerInstance,
854
+ env,
855
+ note,
856
+ });
857
+ return fromTaskResult(r, mode);
858
+ }
859
+ const land = record.products;
860
+ writeFileSync(workItemPath(cwd, 'specs-ready', slug), specBody);
861
+ const stagedEmitted = newOrChangedStagedTasks(cwd, before);
862
+ const r = await integrateTaskingCandidates({
863
+ slug,
864
+ cwd,
865
+ arbiter,
866
+ stagedEmitted,
867
+ emitTasks: collectEmittedTasks(cwd, stagedEmitted),
868
+ poolBefore,
869
+ placementDir: taskPlacementDir(options, baseFm),
870
+ loopTag: undefined,
871
+ lockedBlob: base.blob,
872
+ release: releaseTaskingLock,
873
+ mode,
874
+ // The task-set review already ran in the agent phase.
875
+ review: false,
876
+ approvedReviewProse: land.reviewProse,
877
+ mergeRetries: options.mergeRetries,
878
+ mergeJitterMs: options.mergeJitterMs,
879
+ noPR: options.noPR,
880
+ providerInstance: options.providerInstance,
881
+ env,
882
+ agentEnv: env,
883
+ note,
884
+ });
885
+ return fromTaskResult(r, mode);
886
+ }
887
+ /** Map the tasking write half's {@link TaskResult} to the apply phase's. */
888
+ function fromTaskResult(r, mode) {
889
+ const outcome = r.outcome === 'tasked'
890
+ ? mode === 'merge'
891
+ ? 'landed'
892
+ : 'proposed'
893
+ : r.outcome === 'needs-attention'
894
+ ? 'surfaced'
895
+ : r.outcome === 'stale'
896
+ ? 'stale'
897
+ : 'usage-error';
898
+ return {
899
+ exitCode: r.exitCode,
900
+ outcome,
901
+ slug: r.slug,
902
+ message: r.message,
903
+ ...(r.emitted === undefined ? {} : { emitted: r.emitted }),
904
+ };
905
+ }
906
+ //# sourceMappingURL=ci-phase-tasking.js.map