dorfl 0.0.0 → 0.1.1

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 (619) hide show
  1. package/dist/advance-ci-template.d.ts +73 -0
  2. package/dist/advance-ci-template.d.ts.map +1 -0
  3. package/dist/advance-ci-template.js +104 -0
  4. package/dist/advance-ci-template.js.map +1 -0
  5. package/dist/advance-classify.d.ts +132 -0
  6. package/dist/advance-classify.d.ts.map +1 -0
  7. package/dist/advance-classify.js +120 -0
  8. package/dist/advance-classify.js.map +1 -0
  9. package/dist/advance-drivers.d.ts +182 -0
  10. package/dist/advance-drivers.d.ts.map +1 -0
  11. package/dist/advance-drivers.js +231 -0
  12. package/dist/advance-drivers.js.map +1 -0
  13. package/dist/advance-isolated.d.ts +156 -0
  14. package/dist/advance-isolated.d.ts.map +1 -0
  15. package/dist/advance-isolated.js +256 -0
  16. package/dist/advance-isolated.js.map +1 -0
  17. package/dist/advance-lifecycle-template.d.ts +107 -0
  18. package/dist/advance-lifecycle-template.d.ts.map +1 -0
  19. package/dist/advance-lifecycle-template.js +668 -0
  20. package/dist/advance-lifecycle-template.js.map +1 -0
  21. package/dist/advance-loop-driver.d.ts +325 -0
  22. package/dist/advance-loop-driver.d.ts.map +1 -0
  23. package/dist/advance-loop-driver.js +437 -0
  24. package/dist/advance-loop-driver.js.map +1 -0
  25. package/dist/advance-treeless-publish.d.ts +108 -0
  26. package/dist/advance-treeless-publish.d.ts.map +1 -0
  27. package/dist/advance-treeless-publish.js +71 -0
  28. package/dist/advance-treeless-publish.js.map +1 -0
  29. package/dist/advance.d.ts +340 -0
  30. package/dist/advance.d.ts.map +1 -0
  31. package/dist/advance.js +1122 -0
  32. package/dist/advance.js.map +1 -0
  33. package/dist/advancing-lock.d.ts +294 -0
  34. package/dist/advancing-lock.d.ts.map +1 -0
  35. package/dist/advancing-lock.js +594 -0
  36. package/dist/advancing-lock.js.map +1 -0
  37. package/dist/agent-launch.d.ts +79 -0
  38. package/dist/agent-launch.d.ts.map +1 -0
  39. package/dist/agent-launch.js +61 -0
  40. package/dist/agent-launch.js.map +1 -0
  41. package/dist/agent-stop.d.ts +149 -0
  42. package/dist/agent-stop.d.ts.map +1 -0
  43. package/dist/agent-stop.js +307 -0
  44. package/dist/agent-stop.js.map +1 -0
  45. package/dist/apply-decide.d.ts +127 -0
  46. package/dist/apply-decide.d.ts.map +1 -0
  47. package/dist/apply-decide.js +176 -0
  48. package/dist/apply-decide.js.map +1 -0
  49. package/dist/apply-merge-action.d.ts +206 -0
  50. package/dist/apply-merge-action.d.ts.map +1 -0
  51. package/dist/apply-merge-action.js +307 -0
  52. package/dist/apply-merge-action.js.map +1 -0
  53. package/dist/apply-persist.d.ts +174 -0
  54. package/dist/apply-persist.d.ts.map +1 -0
  55. package/dist/apply-persist.js +359 -0
  56. package/dist/apply-persist.js.map +1 -0
  57. package/dist/arbiter.d.ts +120 -0
  58. package/dist/arbiter.d.ts.map +1 -0
  59. package/dist/arbiter.js +255 -0
  60. package/dist/arbiter.js.map +1 -0
  61. package/dist/brand.d.ts +70 -0
  62. package/dist/brand.d.ts.map +1 -0
  63. package/dist/brand.js +84 -0
  64. package/dist/brand.js.map +1 -0
  65. package/dist/buildable-body.d.ts +132 -0
  66. package/dist/buildable-body.d.ts.map +1 -0
  67. package/dist/buildable-body.js +131 -0
  68. package/dist/buildable-body.js.map +1 -0
  69. package/dist/categorise.d.ts +66 -0
  70. package/dist/categorise.d.ts.map +1 -0
  71. package/dist/categorise.js +106 -0
  72. package/dist/categorise.js.map +1 -0
  73. package/dist/claim-cas.d.ts +117 -0
  74. package/dist/claim-cas.d.ts.map +1 -0
  75. package/dist/claim-cas.js +312 -0
  76. package/dist/claim-cas.js.map +1 -0
  77. package/dist/cli-spinner.d.ts +112 -0
  78. package/dist/cli-spinner.d.ts.map +1 -0
  79. package/dist/cli-spinner.js +157 -0
  80. package/dist/cli-spinner.js.map +1 -0
  81. package/dist/cli.d.ts +11 -0
  82. package/dist/cli.d.ts.map +1 -0
  83. package/dist/cli.js +3100 -0
  84. package/dist/cli.js.map +1 -0
  85. package/dist/close-job-template.d.ts +70 -0
  86. package/dist/close-job-template.d.ts.map +1 -0
  87. package/dist/close-job-template.js +180 -0
  88. package/dist/close-job-template.js.map +1 -0
  89. package/dist/close-job.d.ts +95 -0
  90. package/dist/close-job.d.ts.map +1 -0
  91. package/dist/close-job.js +226 -0
  92. package/dist/close-job.js.map +1 -0
  93. package/dist/complete.d.ts +361 -0
  94. package/dist/complete.d.ts.map +1 -0
  95. package/dist/complete.js +885 -0
  96. package/dist/complete.js.map +1 -0
  97. package/dist/concurrency.d.ts +68 -0
  98. package/dist/concurrency.d.ts.map +1 -0
  99. package/dist/concurrency.js +112 -0
  100. package/dist/concurrency.js.map +1 -0
  101. package/dist/config-override.d.ts +76 -0
  102. package/dist/config-override.d.ts.map +1 -0
  103. package/dist/config-override.js +50 -0
  104. package/dist/config-override.js.map +1 -0
  105. package/dist/config.d.ts +668 -0
  106. package/dist/config.d.ts.map +1 -0
  107. package/dist/config.js +241 -0
  108. package/dist/config.js.map +1 -0
  109. package/dist/continue-branch.d.ts +249 -0
  110. package/dist/continue-branch.d.ts.map +1 -0
  111. package/dist/continue-branch.js +389 -0
  112. package/dist/continue-branch.js.map +1 -0
  113. package/dist/cwd-section.d.ts +186 -0
  114. package/dist/cwd-section.d.ts.map +1 -0
  115. package/dist/cwd-section.js +209 -0
  116. package/dist/cwd-section.js.map +1 -0
  117. package/dist/decision-engine.d.ts +170 -0
  118. package/dist/decision-engine.d.ts.map +1 -0
  119. package/dist/decision-engine.js +136 -0
  120. package/dist/decision-engine.js.map +1 -0
  121. package/dist/detect.d.ts +17 -0
  122. package/dist/detect.d.ts.map +1 -0
  123. package/dist/detect.js +118 -0
  124. package/dist/detect.js.map +1 -0
  125. package/dist/do-autopick.d.ts +85 -0
  126. package/dist/do-autopick.d.ts.map +1 -0
  127. package/dist/do-autopick.js +112 -0
  128. package/dist/do-autopick.js.map +1 -0
  129. package/dist/do-config.d.ts +312 -0
  130. package/dist/do-config.d.ts.map +1 -0
  131. package/dist/do-config.js +358 -0
  132. package/dist/do-config.js.map +1 -0
  133. package/dist/do-remote-auto.d.ts +75 -0
  134. package/dist/do-remote-auto.d.ts.map +1 -0
  135. package/dist/do-remote-auto.js +111 -0
  136. package/dist/do-remote-auto.js.map +1 -0
  137. package/dist/do.d.ts +621 -0
  138. package/dist/do.d.ts.map +1 -0
  139. package/dist/do.js +1882 -0
  140. package/dist/do.js.map +1 -0
  141. package/dist/drop-source.d.ts +96 -0
  142. package/dist/drop-source.d.ts.map +1 -0
  143. package/dist/drop-source.js +91 -0
  144. package/dist/drop-source.js.map +1 -0
  145. package/dist/eligibility.d.ts +46 -0
  146. package/dist/eligibility.d.ts.map +1 -0
  147. package/dist/eligibility.js +34 -0
  148. package/dist/eligibility.js.map +1 -0
  149. package/dist/env-config.d.ts +51 -0
  150. package/dist/env-config.d.ts.map +1 -0
  151. package/dist/env-config.js +272 -0
  152. package/dist/env-config.js.map +1 -0
  153. package/dist/failure-cause.d.ts +70 -0
  154. package/dist/failure-cause.d.ts.map +1 -0
  155. package/dist/failure-cause.js +126 -0
  156. package/dist/failure-cause.js.map +1 -0
  157. package/dist/format.d.ts +43 -0
  158. package/dist/format.d.ts.map +1 -0
  159. package/dist/format.js +256 -0
  160. package/dist/format.js.map +1 -0
  161. package/dist/frontmatter.d.ts +208 -0
  162. package/dist/frontmatter.d.ts.map +1 -0
  163. package/dist/frontmatter.js +344 -0
  164. package/dist/frontmatter.js.map +1 -0
  165. package/dist/gate-readiness.d.ts +84 -0
  166. package/dist/gate-readiness.d.ts.map +1 -0
  167. package/dist/gate-readiness.js +103 -0
  168. package/dist/gate-readiness.js.map +1 -0
  169. package/dist/gc.d.ts +165 -0
  170. package/dist/gc.d.ts.map +1 -0
  171. package/dist/gc.js +313 -0
  172. package/dist/gc.js.map +1 -0
  173. package/dist/gh-failure.d.ts +42 -0
  174. package/dist/gh-failure.d.ts.map +1 -0
  175. package/dist/gh-failure.js +49 -0
  176. package/dist/gh-failure.js.map +1 -0
  177. package/dist/git.d.ts +75 -0
  178. package/dist/git.d.ts.map +1 -0
  179. package/dist/git.js +130 -0
  180. package/dist/git.js.map +1 -0
  181. package/dist/github.d.ts +187 -0
  182. package/dist/github.d.ts.map +1 -0
  183. package/dist/github.js +343 -0
  184. package/dist/github.js.map +1 -0
  185. package/dist/harness.d.ts +242 -0
  186. package/dist/harness.d.ts.map +1 -0
  187. package/dist/harness.js +157 -0
  188. package/dist/harness.js.map +1 -0
  189. package/dist/identity.d.ts +167 -0
  190. package/dist/identity.d.ts.map +1 -0
  191. package/dist/identity.js +231 -0
  192. package/dist/identity.js.map +1 -0
  193. package/dist/index.d.ts +147 -0
  194. package/dist/index.d.ts.map +1 -0
  195. package/dist/index.js +76 -0
  196. package/dist/index.js.map +1 -0
  197. package/dist/install-ci-branch-protection.d.ts +147 -0
  198. package/dist/install-ci-branch-protection.d.ts.map +1 -0
  199. package/dist/install-ci-branch-protection.js +166 -0
  200. package/dist/install-ci-branch-protection.js.map +1 -0
  201. package/dist/install-ci-capabilities/advance-lifecycle.d.ts +15 -0
  202. package/dist/install-ci-capabilities/advance-lifecycle.d.ts.map +1 -0
  203. package/dist/install-ci-capabilities/advance-lifecycle.js +28 -0
  204. package/dist/install-ci-capabilities/advance-lifecycle.js.map +1 -0
  205. package/dist/install-ci-capabilities/close-job.d.ts +13 -0
  206. package/dist/install-ci-capabilities/close-job.d.ts.map +1 -0
  207. package/dist/install-ci-capabilities/close-job.js +26 -0
  208. package/dist/install-ci-capabilities/close-job.js.map +1 -0
  209. package/dist/install-ci-capabilities/example-noop.d.ts +16 -0
  210. package/dist/install-ci-capabilities/example-noop.d.ts.map +1 -0
  211. package/dist/install-ci-capabilities/example-noop.js +23 -0
  212. package/dist/install-ci-capabilities/example-noop.js.map +1 -0
  213. package/dist/install-ci-capabilities/intake.d.ts +15 -0
  214. package/dist/install-ci-capabilities/intake.d.ts.map +1 -0
  215. package/dist/install-ci-capabilities/intake.js +28 -0
  216. package/dist/install-ci-capabilities/intake.js.map +1 -0
  217. package/dist/install-ci-capabilities/verify.d.ts +14 -0
  218. package/dist/install-ci-capabilities/verify.d.ts.map +1 -0
  219. package/dist/install-ci-capabilities/verify.js +27 -0
  220. package/dist/install-ci-capabilities/verify.js.map +1 -0
  221. package/dist/install-ci-core.d.ts +446 -0
  222. package/dist/install-ci-core.d.ts.map +1 -0
  223. package/dist/install-ci-core.js +760 -0
  224. package/dist/install-ci-core.js.map +1 -0
  225. package/dist/install-ci-github.d.ts +167 -0
  226. package/dist/install-ci-github.d.ts.map +1 -0
  227. package/dist/install-ci-github.js +315 -0
  228. package/dist/install-ci-github.js.map +1 -0
  229. package/dist/install-ci.d.ts +105 -0
  230. package/dist/install-ci.d.ts.map +1 -0
  231. package/dist/install-ci.js +363 -0
  232. package/dist/install-ci.js.map +1 -0
  233. package/dist/intake-event.d.ts +88 -0
  234. package/dist/intake-event.d.ts.map +1 -0
  235. package/dist/intake-event.js +66 -0
  236. package/dist/intake-event.js.map +1 -0
  237. package/dist/intake-marker.d.ts +95 -0
  238. package/dist/intake-marker.d.ts.map +1 -0
  239. package/dist/intake-marker.js +127 -0
  240. package/dist/intake-marker.js.map +1 -0
  241. package/dist/intake-triage.d.ts +48 -0
  242. package/dist/intake-triage.d.ts.map +1 -0
  243. package/dist/intake-triage.js +95 -0
  244. package/dist/intake-triage.js.map +1 -0
  245. package/dist/intake-trigger-template.d.ts +185 -0
  246. package/dist/intake-trigger-template.d.ts.map +1 -0
  247. package/dist/intake-trigger-template.js +449 -0
  248. package/dist/intake-trigger-template.js.map +1 -0
  249. package/dist/intake.d.ts +569 -0
  250. package/dist/intake.d.ts.map +1 -0
  251. package/dist/intake.js +1628 -0
  252. package/dist/intake.js.map +1 -0
  253. package/dist/integration-core.d.ts +539 -0
  254. package/dist/integration-core.d.ts.map +1 -0
  255. package/dist/integration-core.js +2195 -0
  256. package/dist/integration-core.js.map +1 -0
  257. package/dist/integrator.d.ts +343 -0
  258. package/dist/integrator.d.ts.map +1 -0
  259. package/dist/integrator.js +400 -0
  260. package/dist/integrator.js.map +1 -0
  261. package/dist/isolation.d.ts +219 -0
  262. package/dist/isolation.d.ts.map +1 -0
  263. package/dist/isolation.js +261 -0
  264. package/dist/isolation.js.map +1 -0
  265. package/dist/issue-provider.d.ts +349 -0
  266. package/dist/issue-provider.d.ts.map +1 -0
  267. package/dist/issue-provider.js +360 -0
  268. package/dist/issue-provider.js.map +1 -0
  269. package/dist/item-lock.d.ts +626 -0
  270. package/dist/item-lock.d.ts.map +1 -0
  271. package/dist/item-lock.js +1381 -0
  272. package/dist/item-lock.js.map +1 -0
  273. package/dist/item-path.d.ts +49 -0
  274. package/dist/item-path.d.ts.map +1 -0
  275. package/dist/item-path.js +66 -0
  276. package/dist/item-path.js.map +1 -0
  277. package/dist/ledger-lint.d.ts +129 -0
  278. package/dist/ledger-lint.d.ts.map +1 -0
  279. package/dist/ledger-lint.js +249 -0
  280. package/dist/ledger-lint.js.map +1 -0
  281. package/dist/ledger-read.d.ts +357 -0
  282. package/dist/ledger-read.d.ts.map +1 -0
  283. package/dist/ledger-read.js +442 -0
  284. package/dist/ledger-read.js.map +1 -0
  285. package/dist/ledger-write.d.ts +330 -0
  286. package/dist/ledger-write.d.ts.map +1 -0
  287. package/dist/ledger-write.js +411 -0
  288. package/dist/ledger-write.js.map +1 -0
  289. package/dist/lifecycle-gather.d.ts +30 -0
  290. package/dist/lifecycle-gather.d.ts.map +1 -0
  291. package/dist/lifecycle-gather.js +205 -0
  292. package/dist/lifecycle-gather.js.map +1 -0
  293. package/dist/lifecycle-pools.d.ts +180 -0
  294. package/dist/lifecycle-pools.d.ts.map +1 -0
  295. package/dist/lifecycle-pools.js +78 -0
  296. package/dist/lifecycle-pools.js.map +1 -0
  297. package/dist/merge-question-surfacer.d.ts +166 -0
  298. package/dist/merge-question-surfacer.d.ts.map +1 -0
  299. package/dist/merge-question-surfacer.js +297 -0
  300. package/dist/merge-question-surfacer.js.map +1 -0
  301. package/dist/mint-adr.d.ts +126 -0
  302. package/dist/mint-adr.d.ts.map +1 -0
  303. package/dist/mint-adr.js +257 -0
  304. package/dist/mint-adr.js.map +1 -0
  305. package/dist/mirror-pool-scan.d.ts +125 -0
  306. package/dist/mirror-pool-scan.d.ts.map +1 -0
  307. package/dist/mirror-pool-scan.js +104 -0
  308. package/dist/mirror-pool-scan.js.map +1 -0
  309. package/dist/needs-attention.d.ts +341 -0
  310. package/dist/needs-attention.d.ts.map +1 -0
  311. package/dist/needs-attention.js +900 -0
  312. package/dist/needs-attention.js.map +1 -0
  313. package/dist/orphan-sidecar.d.ts +79 -0
  314. package/dist/orphan-sidecar.d.ts.map +1 -0
  315. package/dist/orphan-sidecar.js +71 -0
  316. package/dist/orphan-sidecar.js.map +1 -0
  317. package/dist/output.d.ts +48 -0
  318. package/dist/output.d.ts.map +1 -0
  319. package/dist/output.js +66 -0
  320. package/dist/output.js.map +1 -0
  321. package/dist/pi-harness.d.ts +179 -0
  322. package/dist/pi-harness.d.ts.map +1 -0
  323. package/dist/pi-harness.js +342 -0
  324. package/dist/pi-harness.js.map +1 -0
  325. package/dist/placement.d.ts +99 -0
  326. package/dist/placement.d.ts.map +1 -0
  327. package/dist/placement.js +67 -0
  328. package/dist/placement.js.map +1 -0
  329. package/dist/prd-to-spec.d.ts +329 -0
  330. package/dist/prd-to-spec.d.ts.map +1 -0
  331. package/dist/prd-to-spec.js +706 -0
  332. package/dist/prd-to-spec.js.map +1 -0
  333. package/dist/prepare.d.ts +121 -0
  334. package/dist/prepare.d.ts.map +1 -0
  335. package/dist/prepare.js +140 -0
  336. package/dist/prepare.js.map +1 -0
  337. package/dist/prompt.d.ts +363 -0
  338. package/dist/prompt.d.ts.map +1 -0
  339. package/dist/prompt.js +499 -0
  340. package/dist/prompt.js.map +1 -0
  341. package/dist/protocol/ADR-FORMAT.md +47 -0
  342. package/dist/protocol/CLAIM-PROTOCOL.md +217 -0
  343. package/dist/protocol/REVIEW-PROTOCOL.md +119 -0
  344. package/dist/protocol/SURFACE-PROTOCOL.md +121 -0
  345. package/dist/protocol/TASKING-PROTOCOL.md +122 -0
  346. package/dist/protocol/WORK-CONTRACT.md +276 -0
  347. package/dist/protocol/spec-template.md +71 -0
  348. package/dist/protocol/task-template.md +65 -0
  349. package/dist/readiness.d.ts +66 -0
  350. package/dist/readiness.d.ts.map +1 -0
  351. package/dist/readiness.js +36 -0
  352. package/dist/readiness.js.map +1 -0
  353. package/dist/reap-branches.d.ts +102 -0
  354. package/dist/reap-branches.d.ts.map +1 -0
  355. package/dist/reap-branches.js +149 -0
  356. package/dist/reap-branches.js.map +1 -0
  357. package/dist/recover-isolated.d.ts +72 -0
  358. package/dist/recover-isolated.d.ts.map +1 -0
  359. package/dist/recover-isolated.js +188 -0
  360. package/dist/recover-isolated.js.map +1 -0
  361. package/dist/registry.d.ts +172 -0
  362. package/dist/registry.d.ts.map +1 -0
  363. package/dist/registry.js +296 -0
  364. package/dist/registry.js.map +1 -0
  365. package/dist/repo-config.d.ts +201 -0
  366. package/dist/repo-config.d.ts.map +1 -0
  367. package/dist/repo-config.js +414 -0
  368. package/dist/repo-config.js.map +1 -0
  369. package/dist/repo-key.d.ts +20 -0
  370. package/dist/repo-key.d.ts.map +1 -0
  371. package/dist/repo-key.js +68 -0
  372. package/dist/repo-key.js.map +1 -0
  373. package/dist/repo-mirror.d.ts +177 -0
  374. package/dist/repo-mirror.d.ts.map +1 -0
  375. package/dist/repo-mirror.js +271 -0
  376. package/dist/repo-mirror.js.map +1 -0
  377. package/dist/retry-backoff.d.ts +90 -0
  378. package/dist/retry-backoff.d.ts.map +1 -0
  379. package/dist/retry-backoff.js +98 -0
  380. package/dist/retry-backoff.js.map +1 -0
  381. package/dist/review-gate.d.ts +173 -0
  382. package/dist/review-gate.d.ts.map +1 -0
  383. package/dist/review-gate.js +261 -0
  384. package/dist/review-gate.js.map +1 -0
  385. package/dist/review-verdict.d.ts +149 -0
  386. package/dist/review-verdict.d.ts.map +1 -0
  387. package/dist/review-verdict.js +332 -0
  388. package/dist/review-verdict.js.map +1 -0
  389. package/dist/run.d.ts +221 -0
  390. package/dist/run.d.ts.map +1 -0
  391. package/dist/run.js +963 -0
  392. package/dist/run.js.map +1 -0
  393. package/dist/scan.d.ts +308 -0
  394. package/dist/scan.d.ts.map +1 -0
  395. package/dist/scan.js +374 -0
  396. package/dist/scan.js.map +1 -0
  397. package/dist/select-order.d.ts +75 -0
  398. package/dist/select-order.d.ts.map +1 -0
  399. package/dist/select-order.js +108 -0
  400. package/dist/select-order.js.map +1 -0
  401. package/dist/select-priority.d.ts +188 -0
  402. package/dist/select-priority.d.ts.map +1 -0
  403. package/dist/select-priority.js +80 -0
  404. package/dist/select-priority.js.map +1 -0
  405. package/dist/select.d.ts +25 -0
  406. package/dist/select.d.ts.map +1 -0
  407. package/dist/select.js +43 -0
  408. package/dist/select.js.map +1 -0
  409. package/dist/session-path.d.ts +36 -0
  410. package/dist/session-path.d.ts.map +1 -0
  411. package/dist/session-path.js +129 -0
  412. package/dist/session-path.js.map +1 -0
  413. package/dist/sidecar-apply.d.ts +83 -0
  414. package/dist/sidecar-apply.d.ts.map +1 -0
  415. package/dist/sidecar-apply.js +111 -0
  416. package/dist/sidecar-apply.js.map +1 -0
  417. package/dist/sidecar.d.ts +245 -0
  418. package/dist/sidecar.d.ts.map +1 -0
  419. package/dist/sidecar.js +481 -0
  420. package/dist/sidecar.js.map +1 -0
  421. package/dist/slug-namespace.d.ts +204 -0
  422. package/dist/slug-namespace.d.ts.map +1 -0
  423. package/dist/slug-namespace.js +229 -0
  424. package/dist/slug-namespace.js.map +1 -0
  425. package/dist/spec-complete.d.ts +44 -0
  426. package/dist/spec-complete.d.ts.map +1 -0
  427. package/dist/spec-complete.js +69 -0
  428. package/dist/spec-complete.js.map +1 -0
  429. package/dist/start.d.ts +97 -0
  430. package/dist/start.d.ts.map +1 -0
  431. package/dist/start.js +633 -0
  432. package/dist/start.js.map +1 -0
  433. package/dist/status.d.ts +199 -0
  434. package/dist/status.d.ts.map +1 -0
  435. package/dist/status.js +228 -0
  436. package/dist/status.js.map +1 -0
  437. package/dist/surface-gate.d.ts +162 -0
  438. package/dist/surface-gate.d.ts.map +1 -0
  439. package/dist/surface-gate.js +206 -0
  440. package/dist/surface-gate.js.map +1 -0
  441. package/dist/surface-persist.d.ts +86 -0
  442. package/dist/surface-persist.d.ts.map +1 -0
  443. package/dist/surface-persist.js +129 -0
  444. package/dist/surface-persist.js.map +1 -0
  445. package/dist/tasker-review-loop.d.ts +249 -0
  446. package/dist/tasker-review-loop.d.ts.map +1 -0
  447. package/dist/tasker-review-loop.js +369 -0
  448. package/dist/tasker-review-loop.js.map +1 -0
  449. package/dist/tasking-eligibility.d.ts +74 -0
  450. package/dist/tasking-eligibility.d.ts.map +1 -0
  451. package/dist/tasking-eligibility.js +52 -0
  452. package/dist/tasking-eligibility.js.map +1 -0
  453. package/dist/tasking-lock.d.ts +111 -0
  454. package/dist/tasking-lock.d.ts.map +1 -0
  455. package/dist/tasking-lock.js +256 -0
  456. package/dist/tasking-lock.js.map +1 -0
  457. package/dist/tasking.d.ts +275 -0
  458. package/dist/tasking.d.ts.map +1 -0
  459. package/dist/tasking.js +951 -0
  460. package/dist/tasking.js.map +1 -0
  461. package/dist/triage-gate.d.ts +127 -0
  462. package/dist/triage-gate.d.ts.map +1 -0
  463. package/dist/triage-gate.js +139 -0
  464. package/dist/triage-gate.js.map +1 -0
  465. package/dist/triage-persist.d.ts +163 -0
  466. package/dist/triage-persist.d.ts.map +1 -0
  467. package/dist/triage-persist.js +387 -0
  468. package/dist/triage-persist.js.map +1 -0
  469. package/dist/verdict-json.d.ts +32 -0
  470. package/dist/verdict-json.d.ts.map +1 -0
  471. package/dist/verdict-json.js +74 -0
  472. package/dist/verdict-json.js.map +1 -0
  473. package/dist/verify-workflow-template.d.ts +60 -0
  474. package/dist/verify-workflow-template.d.ts.map +1 -0
  475. package/dist/verify-workflow-template.js +126 -0
  476. package/dist/verify-workflow-template.js.map +1 -0
  477. package/dist/verify.d.ts +60 -0
  478. package/dist/verify.d.ts.map +1 -0
  479. package/dist/verify.js +62 -0
  480. package/dist/verify.js.map +1 -0
  481. package/dist/watch-session.d.ts +112 -0
  482. package/dist/watch-session.d.ts.map +1 -0
  483. package/dist/watch-session.js +347 -0
  484. package/dist/watch-session.js.map +1 -0
  485. package/dist/work-layout.d.ts +198 -0
  486. package/dist/work-layout.d.ts.map +1 -0
  487. package/dist/work-layout.js +217 -0
  488. package/dist/work-layout.js.map +1 -0
  489. package/dist/work-on.d.ts +154 -0
  490. package/dist/work-on.d.ts.map +1 -0
  491. package/dist/work-on.js +387 -0
  492. package/dist/work-on.js.map +1 -0
  493. package/dist/workspace.d.ts +224 -0
  494. package/dist/workspace.d.ts.map +1 -0
  495. package/dist/workspace.js +325 -0
  496. package/dist/workspace.js.map +1 -0
  497. package/package.json +46 -2
  498. package/src/advance-ci-template.ts +203 -0
  499. package/src/advance-classify.ts +197 -0
  500. package/src/advance-drivers.ts +414 -0
  501. package/src/advance-isolated.ts +432 -0
  502. package/src/advance-lifecycle-template.ts +791 -0
  503. package/src/advance-loop-driver.ts +745 -0
  504. package/src/advance-treeless-publish.ts +177 -0
  505. package/src/advance.ts +1564 -0
  506. package/src/advancing-lock.ts +988 -0
  507. package/src/agent-launch.ts +137 -0
  508. package/src/agent-stop.ts +361 -0
  509. package/src/apply-decide.ts +242 -0
  510. package/src/apply-merge-action.ts +502 -0
  511. package/src/apply-persist.ts +518 -0
  512. package/src/arbiter.ts +372 -0
  513. package/src/brand.ts +111 -0
  514. package/src/buildable-body.ts +196 -0
  515. package/src/categorise.ts +158 -0
  516. package/src/claim-cas.ts +513 -0
  517. package/src/cli-spinner.ts +225 -0
  518. package/src/cli.ts +4379 -0
  519. package/src/close-job-template.ts +236 -0
  520. package/src/close-job.ts +319 -0
  521. package/src/complete.ts +1379 -0
  522. package/src/concurrency.ts +151 -0
  523. package/src/config-override.ts +116 -0
  524. package/src/config.ts +883 -0
  525. package/src/continue-branch.ts +542 -0
  526. package/src/cwd-section.ts +392 -0
  527. package/src/decision-engine.ts +272 -0
  528. package/src/detect.ts +124 -0
  529. package/src/do-autopick.ts +223 -0
  530. package/src/do-config.ts +589 -0
  531. package/src/do-remote-auto.ts +197 -0
  532. package/src/do.ts +2623 -0
  533. package/src/drop-source.ts +194 -0
  534. package/src/eligibility.ts +79 -0
  535. package/src/env-config.ts +305 -0
  536. package/src/failure-cause.ts +142 -0
  537. package/src/format.ts +313 -0
  538. package/src/frontmatter.ts +477 -0
  539. package/src/gate-readiness.ts +147 -0
  540. package/src/gc.ts +510 -0
  541. package/src/gh-failure.ts +53 -0
  542. package/src/git.ts +186 -0
  543. package/src/github.ts +468 -0
  544. package/src/harness.ts +355 -0
  545. package/src/identity.ts +322 -0
  546. package/src/index.ts +785 -0
  547. package/src/install-ci-branch-protection.ts +255 -0
  548. package/src/install-ci-capabilities/advance-lifecycle.ts +34 -0
  549. package/src/install-ci-capabilities/close-job.ts +32 -0
  550. package/src/install-ci-capabilities/example-noop.ts +24 -0
  551. package/src/install-ci-capabilities/intake.ts +34 -0
  552. package/src/install-ci-capabilities/verify.ts +33 -0
  553. package/src/install-ci-core.ts +1088 -0
  554. package/src/install-ci-github.ts +376 -0
  555. package/src/install-ci.ts +552 -0
  556. package/src/intake-event.ts +102 -0
  557. package/src/intake-marker.ts +195 -0
  558. package/src/intake-triage.ts +138 -0
  559. package/src/intake-trigger-template.ts +591 -0
  560. package/src/intake.ts +2445 -0
  561. package/src/integration-core.ts +3065 -0
  562. package/src/integrator.ts +771 -0
  563. package/src/isolation.ts +484 -0
  564. package/src/issue-provider.ts +733 -0
  565. package/src/item-lock.ts +1858 -0
  566. package/src/item-path.ts +75 -0
  567. package/src/ledger-lint.ts +332 -0
  568. package/src/ledger-read.ts +924 -0
  569. package/src/ledger-write.ts +865 -0
  570. package/src/lifecycle-gather.ts +298 -0
  571. package/src/lifecycle-pools.ts +250 -0
  572. package/src/merge-question-surfacer.ts +496 -0
  573. package/src/mint-adr.ts +362 -0
  574. package/src/mirror-pool-scan.ts +240 -0
  575. package/src/needs-attention.ts +1506 -0
  576. package/src/orphan-sidecar.ts +150 -0
  577. package/src/output.ts +89 -0
  578. package/src/pi-harness.ts +403 -0
  579. package/src/placement.ts +131 -0
  580. package/src/prd-to-spec.ts +1059 -0
  581. package/src/prepare.ts +230 -0
  582. package/src/prompt.ts +763 -0
  583. package/src/readiness.ts +98 -0
  584. package/src/reap-branches.ts +278 -0
  585. package/src/recover-isolated.ts +276 -0
  586. package/src/registry.ts +475 -0
  587. package/src/repo-config.ts +550 -0
  588. package/src/repo-key.ts +74 -0
  589. package/src/repo-mirror.ts +367 -0
  590. package/src/retry-backoff.ts +130 -0
  591. package/src/review-gate.ts +389 -0
  592. package/src/review-verdict.ts +422 -0
  593. package/src/run.ts +1430 -0
  594. package/src/scan.ts +611 -0
  595. package/src/select-order.ts +143 -0
  596. package/src/select-priority.ts +266 -0
  597. package/src/select.ts +62 -0
  598. package/src/session-path.ts +153 -0
  599. package/src/sidecar-apply.ts +216 -0
  600. package/src/sidecar.ts +700 -0
  601. package/src/slug-namespace.ts +367 -0
  602. package/src/spec-complete.ts +118 -0
  603. package/src/start.ts +974 -0
  604. package/src/status.ts +441 -0
  605. package/src/surface-gate.ts +337 -0
  606. package/src/surface-persist.ts +241 -0
  607. package/src/tasker-review-loop.ts +671 -0
  608. package/src/tasking-eligibility.ts +114 -0
  609. package/src/tasking-lock.ts +416 -0
  610. package/src/tasking.ts +1437 -0
  611. package/src/triage-gate.ts +248 -0
  612. package/src/triage-persist.ts +570 -0
  613. package/src/verdict-json.ts +73 -0
  614. package/src/verify-workflow-template.ts +159 -0
  615. package/src/verify.ts +123 -0
  616. package/src/watch-session.ts +397 -0
  617. package/src/work-layout.ts +262 -0
  618. package/src/work-on.ts +660 -0
  619. package/src/workspace.ts +502 -0
package/src/intake.ts ADDED
@@ -0,0 +1,2445 @@
1
+ import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
2
+ import {dirname, join} from 'node:path';
3
+ import {runAsync, type RunResult} from './git.js';
4
+ import {workFolderRel, workItemRel} from './work-layout.js';
5
+ import {paramCase} from './brand.js';
6
+ import {
7
+ performIntegration,
8
+ type IntegrationCoreResult,
9
+ } from './integration-core.js';
10
+ import type {IntegrateResult, ReviewProvider} from './integrator.js';
11
+ import {integrationFromFlags} from './complete.js';
12
+ import type {IntegrationMode, SpecsLandIn} from './config.js';
13
+ import type {OriginTrust} from './frontmatter.js';
14
+ import {
15
+ placementFolder,
16
+ resolvePlacement,
17
+ type PlacementSlots,
18
+ } from './placement.js';
19
+ import {
20
+ identityEnv,
21
+ assertTransportAllowed,
22
+ type Identity,
23
+ } from './identity.js';
24
+ import {NullHarness, type Harness} from './harness.js';
25
+ import {workBranchRef, type SlugNamespace} from './slug-namespace.js';
26
+ import {launchWithOptionalWatch} from './agent-launch.js';
27
+ import {
28
+ GitHubIssueProvider,
29
+ PROCESSING_LOCK_LABEL,
30
+ type Issue,
31
+ type IssueComment,
32
+ type IssueProvider,
33
+ } from './issue-provider.js';
34
+ import {extractJsonObjectSpan} from './verdict-json.js';
35
+ import {
36
+ parseReviewVerdict,
37
+ reviewDisciplinePrompt,
38
+ verdictContractPrompt,
39
+ type ReviewFinding,
40
+ type ReviewVerdict,
41
+ } from './review-verdict.js';
42
+ import {
43
+ stampIntakeMarker,
44
+ computeSeenDelta,
45
+ type IntakeMarkerKind,
46
+ } from './intake-marker.js';
47
+ import {triageIntake, type IntakeTriageDecision} from './intake-triage.js';
48
+ import {renderTaskBody, renderSpecBody} from './buildable-body.js';
49
+
50
+ /**
51
+ * **`intake <N>`** (prd `issue-intake`, task `intake-tracer-slice-outcome`): the
52
+ * KEYSTONE of the issue front-door. A new, GATE-FREE command — explicit invocation
53
+ * IS the authorization (precedent: `explicit-do-prd-not-gated-by-autoslice`), so
54
+ * `autoTask`/`autoBuild` config does NOT apply — that reads a GitHub issue + its
55
+ * thread through the {@link IssueProvider} seam, runs the decision as a
56
+ * **prompt → VERDICT**, and DISPATCHES on the verdict.
57
+ *
58
+ * The engine shape MIRRORS the review gate (prompt → `approve|block` → dispatch):
59
+ * the decision prompt is an INLINE builder ({@link buildIntakeDecisionSpec}, like
60
+ * `buildTaskingPrd`); the **dispatcher is the testable seam** — a STUBBED verdict
61
+ * (injected, no model/network) drives it, exactly as `ReviewGate` is injected. The
62
+ * prompt's JUDGEMENT is NOT unit-tested (like the review prompt's is not); only the
63
+ * dispatch is.
64
+ *
65
+ * The dispatcher implements the FULL four-outcome decision table (prd
66
+ * `issue-intake` — the source of truth):
67
+ * - **ASK** (not clear enough to act on): `postIssueComment` the next clarifying
68
+ * question; emit NOTHING; STOP.
69
+ * - **TASK** (clear AND fits ONE tracer-bullet task): write
70
+ * `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the
71
+ * lone-task closure link, NOT `Fixes #N`), integrate via {@link
72
+ * performIntegration} (default `propose`).
73
+ * - **PRD** (clear AND coherent but >1 task — INCLUDING a coupled-but-SMALL pair,
74
+ * which is NEVER bounced): write the prd file (`work/prds/ready/<slug>.md`) with `issue: N` (+ the gate
75
+ * axes the verdict carried), integrate, STOP (tasking is the separate `do prd:`
76
+ * step).
77
+ * - **BOUNCE** (genuinely UNRELATED concerns — no shared vision): the bounce is
78
+ * TERMINAL (the asks are unrelated and must be re-filed), so intake CLOSES the
79
+ * issue ATOMICALLY — the "file separate issues" text as the closing comment +
80
+ * `reason: not planned` (the honest GitHub-native signal) in ONE `closeIssue`
81
+ * call; emit NOTHING. Intake closes on BOUNCE (as not planned); NEVER on
82
+ * task/prd (CI's close-job closes those via the `issue:` field) / ask.
83
+ *
84
+ * The per-outcome integration KNOBS, the processing LOCK, and event-classification
85
+ * are LATER tasks and are NOT built here (default `propose` is fine here).
86
+ *
87
+ * The AGENT only DRAFTS (returns the verdict object); the RUNNER (this dispatcher)
88
+ * owns every git/seam side-effect — the write + integrate (and, in later tasks,
89
+ * the comment + label ops). The agent is git-free AND seam-free: the in-band
90
+ * boundary (the SAME discipline the build/tasker agents follow).
91
+ */
92
+
93
+ /**
94
+ * The four outcomes the decision prompt classifies an issue into (the decision
95
+ * table). EXPAND step (prd
96
+ * `prd-to-spec-vocabulary-cutover-and-migration-command`): the `spec` outcome
97
+ * names the "clear + coherent but >1 task" classification (a parent SPEC). HARD
98
+ * CUTOVER (contract step): the legacy `prd` outcome token is GONE — the prompt
99
+ * emits `spec` and the parser rejects any other token.
100
+ */
101
+ export type IntakeOutcome = 'ask' | 'task' | 'spec' | 'bounce';
102
+
103
+ /**
104
+ * The VERDICT the decision prompt returns — `{ask,task,spec,bounce}` + the drafted
105
+ * content for the chosen outcome. THIS code path consumes only the `task` branch's
106
+ * fields (`taskSlug` / `taskTitle` / `taskBody`); the `ask`/`spec`/`bounce`
107
+ * fields are carried on the shape (so the type is stable for the next task) but
108
+ * not dispatched here.
109
+ */
110
+ export interface IntakeVerdict {
111
+ /** Which outcome the prompt chose for the issue. */
112
+ outcome: IntakeOutcome;
113
+ /**
114
+ * The drafted task's content-derived slug (`task` outcome). The dispatcher
115
+ * SANITISES it (a content-derived slug, never a counter) before writing
116
+ * `work/backlog/<slug>.md`. Falls back to a slug derived from {@link taskTitle}
117
+ * when absent/empty.
118
+ */
119
+ taskSlug?: string;
120
+ /** The drafted task's `title:` (`task` outcome). */
121
+ taskTitle?: string;
122
+ /**
123
+ * The drafted task BODY (`task` outcome) — the markdown AFTER the frontmatter
124
+ * (the `## What to build` / `## Acceptance criteria` / `## Prompt` sections). The
125
+ * dispatcher writes the frontmatter (slug/title/`covers: []`, NO `prd:`) carrying
126
+ * the lone-task `issue: N` closure link itself; the agent never writes
127
+ * git-visible files.
128
+ */
129
+ taskBody?: string;
130
+ /**
131
+ * The drafted clarifying question (`ask` outcome) — the dispatcher posts it via
132
+ * `postIssueComment`, emits nothing, and STOPS (a later run resumes from the
133
+ * updated thread).
134
+ */
135
+ question?: string;
136
+ /**
137
+ * The drafted spec's content-derived slug (`spec` outcome). The dispatcher
138
+ * SANITISES it through `paramCase` (never a counter) before writing the spec
139
+ * file (`work/specs/ready/<slug>.md`). Falls back to a slug derived from {@link specTitle} when
140
+ * absent/empty.
141
+ */
142
+ specSlug?: string;
143
+ /** The drafted spec's `title:` (`spec` outcome). */
144
+ specTitle?: string;
145
+ /**
146
+ * The drafted spec BODY (`spec` outcome) — the markdown AFTER the frontmatter
147
+ * (`## Problem Statement` / `## Solution` / `## User Stories` / …). The dispatcher
148
+ * writes the frontmatter (title/slug/`issue: N` + the gate axes) itself; the
149
+ * agent never writes git-visible files.
150
+ */
151
+ specBody?: string;
152
+ /**
153
+ * The spec's gate axes (`spec` outcome) AS THE PROMPT JUDGED THEM — surfaced onto
154
+ * the emitted spec frontmatter (prd `issue-intake` US #8: "the emitted artifact
155
+ * carries … its own gate axes"). Both omitted (undeclared) by default; the prompt
156
+ * sets `specHumanOnly: true` when a human should drive the TASKING and/or
157
+ * `specNeedsAnswers: true` when open questions remain.
158
+ */
159
+ specHumanOnly?: boolean;
160
+ specNeedsAnswers?: boolean;
161
+ /**
162
+ * The drafted bounce message (`bounce` outcome) — the dispatcher carries it as
163
+ * the CLOSING COMMENT on the atomic `closeIssue` ("please file separate issues")
164
+ * with `reason: not planned`, then emits nothing. A bounce is TERMINAL, so the
165
+ * issue is CLOSED (not left open).
166
+ */
167
+ bounceMessage?: string;
168
+ }
169
+
170
+ /** The terminal status of one `intake <N>` run. */
171
+ export type IntakeRunOutcome =
172
+ | 'tasked' // a `task` verdict → backlog task written + integrated
173
+ | 'asked' // an `ask` verdict → clarifying question posted, nothing emitted
174
+ | 'spec-written' // a `spec` verdict → the spec file (`work/specs/ready/<slug>.md`) written + integrated
175
+ | 'bounced' // a `bounce` verdict → split-issues comment posted, nothing emitted
176
+ | 'no-new-input' // the TRIAGE saw intake had the last word + nothing unseen → SKIP (ran, deliberately did nothing)
177
+ | 'already-terminal' // the TRIAGE saw the issue was already transformed (a `bounced`/`created` marker) → SKIP
178
+ | 'locked' // the `processing` lock was already held → backed off (did nothing)
179
+ | 'lock-failed' // the lock could not be ACQUIRED on a label-supporting provider → fail (do NOT proceed lock-less)
180
+ | 'agent-failed' // the decision agent invocation itself errored
181
+ | 'stale' // the integrate rebase conflicted against an advanced main
182
+ | 'usage-error'; // usage / environment problem
183
+
184
+ export interface IntakeResult {
185
+ exitCode: 0 | 1 | 4;
186
+ outcome: IntakeRunOutcome;
187
+ /** The issue number acted on. */
188
+ issueNumber: number;
189
+ /** The slug of the emitted artifact (task OR prd outcome). */
190
+ emittedSlug?: string;
191
+ /** Repo-relative path of the emitted artifact (task OR prd outcome). */
192
+ emitted?: string;
193
+ /** True iff a comment was posted on the issue (ask / bounce outcomes). */
194
+ commented?: boolean;
195
+ /**
196
+ * True iff the ISSUE was closed (the BOUNCE outcome — a terminal bounce closes
197
+ * the issue atomically as `not planned`). Additive (mirrors {@link commented}),
198
+ * so CI / callers can observe the close. Never set on ask/task/prd.
199
+ */
200
+ closed?: boolean;
201
+ /** Human-readable summary of the terminal condition. */
202
+ message: string;
203
+ }
204
+
205
+ /**
206
+ * The DECISION step: given the issue + thread, return a VERDICT. Tests inject a
207
+ * canned verdict (the STUBBED seam that drives the dispatcher, no model/network).
208
+ * Production wires the harness through {@link harnessIntakeDecision}.
209
+ */
210
+ export type IntakeDecider = (input: {
211
+ cwd: string;
212
+ issue: Issue;
213
+ comments: IssueComment[];
214
+ prompt: string;
215
+ env?: NodeJS.ProcessEnv;
216
+ }) => Promise<IntakeVerdict>;
217
+
218
+ export interface PerformIntakeOptions {
219
+ /** The issue number to intake (`intake <N>`). */
220
+ issueNumber: number;
221
+ /** The working clone/checkout the intake runs in. */
222
+ cwd: string;
223
+ /** Name of the arbiter git remote. Defaults to `origin`. */
224
+ arbiter?: string;
225
+ /**
226
+ * The issue seam (read the issue + thread). Tests inject a STUB; production
227
+ * defaults to {@link GitHubIssueProvider} (the only place `gh` is shelled out).
228
+ */
229
+ issueProvider?: IssueProvider;
230
+ /**
231
+ * The DECISION seam (prompt → verdict). Tests inject a CANNED verdict (no
232
+ * model/network) — this is the unit-test target. Production wires the harness.
233
+ */
234
+ decide?: IntakeDecider;
235
+ /**
236
+ * The LONE-TASK bounded-review seam (prompt → review verdict). After a `task`
237
+ * verdict and BEFORE the write/integrate, {@link dispatchTask} runs a bounded
238
+ * (3-round, HARD-CAPPED) adversarial self-review on the SINGLE drafted task
239
+ * through this seam (observation
240
+ * `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C).
241
+ * Tests inject a CANNED review verdict (no model/network) — the new testable
242
+ * seam; production wires the harness ({@link harnessLoneTaskReviewGate}). It
243
+ * mirrors {@link decide}'s injectable shape, NOT the tasker loop (which is a
244
+ * SET-level reviewer this never imports/calls).
245
+ */
246
+ reviewTask?: LoneTaskReviewGate;
247
+ /** The harness seam used when {@link decide} is omitted; defaults to the null adapter. */
248
+ harness?: Harness;
249
+ /** The configured agent command the harness shells out to (null adapter). */
250
+ agentCmd?: string;
251
+ /** The model routing intent forwarded to the harness (ADR §13). */
252
+ model?: string;
253
+ /** The HOST-ONLY sessions root for the pi session file. */
254
+ sessionsDir?: string;
255
+ /**
256
+ * The PER-OUTCOME integration modes (prd `issue-intake` US #9) the emitted artifact integrates
257
+ * THROUGH the shared core with. Because `intake` decides the artifact TYPE at
258
+ * RUNTIME, the mode is keyed per type: an emitted task integrates with
259
+ * `integration.task`, an emitted prd with `integration.prd` (`propose` =
260
+ * push the `work/<slug>` branch + open a PR, NO `main` touch; `merge` = land on
261
+ * `main`). The CLI resolves this from the granular + aggregate flags via
262
+ * {@link resolveIntakeIntegrationModes}; ask/bounce emit nothing, so the modes
263
+ * are no-ops for them. Unset ⇒ propose for both.
264
+ */
265
+ integration?: IntakeIntegrationModes;
266
+ /**
267
+ * **The ORIGIN-TRUST verdict, passed IN** (task
268
+ * `untrusted-origin-forces-build-propose`; the `--origin-trust <trusted|untrusted>`
269
+ * CLI flag). `intake` STAMPS `origin: issue` + this `originTrust` onto every prd/
270
+ * task it emits, so the author-trust signal SURVIVES the prd/task merge
271
+ * boundary (a landed-on-main artifact otherwise erases how it was born, the
272
+ * laundering gap). `intake` does NOT resolve trust itself: the verdict is CI's
273
+ * POLICY, computed in the `intake.yml` shell from the SAME `author_association`
274
+ * case as the integration flags and threaded IN here (preserving the ~L296
275
+ * boundary). UNSET means the artifact is emitted UNSTAMPED, read as `human`/trusted:
276
+ * a LOCAL `dorfl intake <N>` (no CI shell, no `--origin-trust`) is the
277
+ * human-IS-the-checkpoint path, gate-free exactly as `do`.
278
+ */
279
+ originTrust?: OriginTrust;
280
+ /**
281
+ * **The PR-INTENT axis** (config `noPR`, ADR §6): when `true`, intake's propose
282
+ * emissions push the branch but skip the PR (the explicit suppress-PR intent).
283
+ * NOT a provider choice — the provider is purely arbiter-derived. Unset/false ⇒
284
+ * the PR opens normally.
285
+ */
286
+ noPR?: boolean;
287
+ /**
288
+ * **The per-repo PRD-PLACEMENT default, passed IN** (prd
289
+ * `staging-pool-position-gate-and-trust-model` US #2/#5, task
290
+ * `pre-prd-staging-pool-split-and-untrusted-prd-placement`). The resolved
291
+ * per-repo default landing for `intake`-authored prds (`pre-proposed` =
292
+ * staging; `ready` = the auto-tasking pool), fed as the CONFIGURED-DEFAULT rung
293
+ * into the shared placement resolver (`src/placement.ts`). The resolver
294
+ * overlays an EXPLICIT operator flag ({@link explicitSpecsLandIn}, top) and
295
+ * the UNTRUSTED-ORIGIN force (`originTrust: untrusted` ⇒ staging) on top.
296
+ * Unset ⇒ the resolver's built-in floor applies (`staging` = `prds/proposed/`,
297
+ * the conservative landing). The PRD TWIN of `tasksLandIn` on the tasker
298
+ * path — one resolver, two lifecycles.
299
+ */
300
+ specsLandIn?: SpecsLandIn;
301
+ /**
302
+ * **The OPERATOR's EXPLICIT spec-placement override** (the TOP precedence
303
+ * rung). When set, the runner-deterministic resolver lands the spec HERE
304
+ * regardless of `originTrust` or {@link specsLandIn} — the positional
305
+ * analogue of `explicitMerge` overriding the untrusted-origin
306
+ * build-propose rule ("the operator is present; CLI always wins, no
307
+ * special force-key"). Set ONLY when the operator typed
308
+ * `--specs-land-in <where>`; never when the value came from config.
309
+ */
310
+ explicitSpecsLandIn?: SpecsLandIn;
311
+ /**
312
+ * Optional FULLY-FORMED review provider INSTANCE used VERBATIM (the SAME seam
313
+ * `run`/`do` expose; forwarded to `performIntegration` as `providerInstance`).
314
+ * Tests/embeddings inject a stubbed `GitHubProvider` (a custom `gh` path) to
315
+ * drive intake's propose pipeline OFFLINE. The resolved provider OBJECT, NOT a
316
+ * config override. Unset ⇒ the core selects from the arbiter URL.
317
+ */
318
+ providerInstance?: ReviewProvider;
319
+ /**
320
+ * The optional runner IDENTITY (a bot), threaded from host-only
321
+ * `config.identity`. It scopes intake's GIT + provider operations — the `gh`
322
+ * issue ops (read/label/comment/close), the push, and the PR — via process-
323
+ * scoped env overrides. It is NEVER applied to the intake AGENT launches (the
324
+ * decision agent + the lone-task review agent), which stay ambient: an agent
325
+ * must not act as the bot. Absent ⇒ ambient (today's behaviour).
326
+ */
327
+ identity?: Identity;
328
+ /** Environment for child git/agent processes (the AGENT-launch ambient env). */
329
+ env?: NodeJS.ProcessEnv;
330
+ /** Sink for human-readable progress notes. */
331
+ note?: (message: string) => void;
332
+ }
333
+
334
+ const DEFAULT_ARBITER = 'origin';
335
+
336
+ /**
337
+ * **The STAGED-prds dir** (prd `staging-pool-position-gate-and-trust-model`,
338
+ * task `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing
339
+ * ADR `placement-is-runner-deterministic-humanonly-is-agent-judgement`). When
340
+ * the runner-deterministic placement resolver picks the staging side for an
341
+ * `intake`-authored prd, the runner writes the prd file HERE instead of in
342
+ * `work/prds/ready/`. An item born in `prds/proposed/` is durable + readable but NOT in
343
+ * the tasking candidate POOL (`work/prds/ready/` is the pool). A runner/human-owned promotion
344
+ * ({@link promoteFromPreSpec} in `needs-attention.ts`) moves an approved prd
345
+ * `prds/proposed/ → prds/ready/` to make it taskable.
346
+ */
347
+ export const STAGED_SPECS_DIR = workFolderRel('specs-proposed');
348
+
349
+ /**
350
+ * The POOL folder specs land in when the runner-deterministic placement
351
+ * resolver chooses the pool side (`specsLandIn: 'ready'` + a trusted origin, or
352
+ * an `--specs-land-in ready` operator override). This is `work/specs/ready/`,
353
+ * the tasking candidate pool.
354
+ */
355
+ const POOL_SPECS_DIR = workFolderRel('specs-ready');
356
+
357
+ /** The placement slots for the spec lifecycle (folder names). */
358
+ const SPEC_PLACEMENT_SLOTS: PlacementSlots = {
359
+ staging: STAGED_SPECS_DIR,
360
+ pool: POOL_SPECS_DIR,
361
+ };
362
+
363
+ /**
364
+ * Map the `specsLandIn` value spelling (`pre-proposed` | `ready`) onto the
365
+ * resolver's lifecycle-generic side enum (`staging` | `pool`). Returns
366
+ * `undefined` when no value is set, so the resolver's next precedence rung
367
+ * applies (the built-in floor). The spec twin of `landingToSide` on the
368
+ * tasker path — same shape, different slots.
369
+ */
370
+ function specLandingToSide(
371
+ landing: SpecsLandIn | undefined,
372
+ ): 'staging' | 'pool' | undefined {
373
+ if (landing === 'pre-proposed') return 'staging';
374
+ if (landing === 'ready') return 'pool';
375
+ return undefined;
376
+ }
377
+
378
+ /**
379
+ * The emitted artifact TYPE `intake` decides at RUNTIME — a `task` verdict emits
380
+ * `work/backlog/<slug>.md`, a `prd` verdict emits the prd file (`work/prds/ready/<slug>.md`). The two
381
+ * granular flag axes (`--merge-task`/`--propose-task` vs `--merge-spec`/
382
+ * `--propose-spec`) are keyed on this. (ask/bounce emit NOTHING, so the modes are
383
+ * no-ops for them.)
384
+ */
385
+ export type IntakeArtifactType = 'task' | 'spec';
386
+ // prd → spec cutover (MIGRATE batch): `'spec'` is the CANONICAL artifact type;
387
+ // `'prd'` stays as an accepted ALIAS until the contract task removes it.
388
+
389
+ /**
390
+ * The PER-OUTCOME integration mode FLAG SET (prd `issue-intake` US #9). Because
391
+ * `intake` decides the artifact TYPE at runtime, a single `--merge`/`--propose`
392
+ * cannot express a type-conditional policy ("merge a prd but propose a task") —
393
+ * hence the four GRANULAR per-type flags layered over the two AGGREGATES:
394
+ *
395
+ * - **granular:** `--merge-spec`/`--propose-spec` apply iff the outcome is a spec;
396
+ * `--merge-task`/`--propose-task` apply iff it is a task.
397
+ * - **aggregates:** `--merge` = merge BOTH types; `--propose` = propose BOTH.
398
+ *
399
+ * `intake` owns only these KNOBS; WHICH knobs CI sets (from gate state +
400
+ * author-trust) is CI's POLICY, authored in `runner-in-ci` — NOT here.
401
+ */
402
+ export interface IntakeIntegrationFlags {
403
+ /** Aggregate: merge BOTH a task and a prd (the broad knob, overridden per type). */
404
+ merge?: boolean;
405
+ /** Aggregate: propose BOTH a task and a prd. */
406
+ propose?: boolean;
407
+ /** Granular: merge a spec (overrides the aggregate for the spec outcome). */
408
+ mergeSpec?: boolean;
409
+ /** Granular: propose a spec (overrides the aggregate for the spec outcome). */
410
+ proposeSpec?: boolean;
411
+ /** Granular: merge a task (overrides the aggregate for the task outcome). */
412
+ mergeTask?: boolean;
413
+ /** Granular: propose a task (overrides the aggregate for the task outcome). */
414
+ proposeTask?: boolean;
415
+ }
416
+
417
+ /** Both per-type integration modes, resolved from the flag set in ONE eager pass. */
418
+ export interface IntakeIntegrationModes {
419
+ /** The mode an EMITTED task integrates with. */
420
+ task: IntegrationMode;
421
+ /**
422
+ * The mode an EMITTED spec integrates with. `spec` is the CANONICAL key (prd →
423
+ * spec cutover); the user-facing `--merge-spec`/`--propose-spec` flags that FEED
424
+ * it carry the same `spec` spelling (the cli-flag rename landed in batch 4f).
425
+ */
426
+ spec: IntegrationMode;
427
+ }
428
+
429
+ /** Default per-outcome integration mode when no flag selects one — propose (matches `do`). */
430
+ const DEFAULT_INTEGRATION: IntegrationMode = 'propose';
431
+
432
+ /**
433
+ * Resolve the GRANULAR per-type axis (`--merge-<t>` / `--propose-<t>`) for ONE
434
+ * artifact type, REUSING {@link integrationFromFlags} for its mutual-exclusion +
435
+ * "mutually exclusive" error message (the same-type-both usage error) — so the
436
+ * granular axis is NOT a forked second resolver, just `integrationFromFlags`
437
+ * applied to the per-type pair. Returns the granular mode, or `undefined` when
438
+ * neither granular flag for this type was given (the aggregate/default then
439
+ * decides). The error message is reworded to name the granular flag pair.
440
+ */
441
+ function granularFromFlags(
442
+ type: IntakeArtifactType,
443
+ merge: boolean | undefined,
444
+ propose: boolean | undefined,
445
+ ): IntegrationMode | undefined {
446
+ try {
447
+ return integrationFromFlags({merge, propose});
448
+ } catch {
449
+ throw new Error(
450
+ `--merge-${type} and --propose-${type} are mutually exclusive; pass at most one.`,
451
+ );
452
+ }
453
+ }
454
+
455
+ /**
456
+ * The PURE per-outcome integration mode resolution (prd `issue-intake` US #9 —
457
+ * the canonical table). Given ONLY the flag set, resolve BOTH per-type modes in
458
+ * one eager pass (so a usage error is caught before the runtime verdict is even
459
+ * known). The rules, all decided in the prd:
460
+ *
461
+ * - **unset ⇒ propose for BOTH** (conservative default; matches `do`).
462
+ * - **aggregates:** `--merge` ⇒ merge both; `--propose` ⇒ propose both (this axis
463
+ * COMPOSES the existing {@link integrationFromFlags}, reusing its mutual
464
+ * exclusion + error message).
465
+ * - **granular routes per type:** `--merge-spec` merges a spec (and leaves a task at
466
+ * the aggregate/default), etc.
467
+ * - **GRANULAR OVERRIDES AGGREGATE:** `--merge --propose-task` ⇒ merge a spec,
468
+ * propose a task.
469
+ * - **same type + both modes is a usage ERROR:** `--merge-spec --propose-spec` (and
470
+ * `--merge-task --propose-task`), and the aggregate `--merge --propose`.
471
+ *
472
+ * Throws (a usage error) on any mutually-exclusive pair. The dispatcher picks the
473
+ * field matching the runtime verdict's type; ask/bounce never integrate, so the
474
+ * modes are no-ops for them.
475
+ *
476
+ * `defaultMode` is the FALLBACK when NEITHER a granular nor the aggregate flag
477
+ * selects a mode for a type — it defaults to `propose` (so the pure table reads
478
+ * "unset ⇒ propose for both"), but the CLI passes the per-repo/global
479
+ * config-resolved mode so the established precedence chain (flag > per-repo >
480
+ * global > default) is preserved, exactly as `do`/`complete` resolve it.
481
+ */
482
+ export function resolveIntakeIntegrationModes(
483
+ flags: IntakeIntegrationFlags,
484
+ defaultMode: IntegrationMode = DEFAULT_INTEGRATION,
485
+ ): IntakeIntegrationModes {
486
+ // AGGREGATE axis — reuse the existing resolver (its mutual exclusion + the
487
+ // "--merge and --propose are mutually exclusive" message). `undefined` ⇒ unset.
488
+ const aggregate = integrationFromFlags({
489
+ merge: flags.merge,
490
+ propose: flags.propose,
491
+ });
492
+ // GRANULAR axes — `integrationFromFlags` per type (the same-type-both error).
493
+ const specGranular = granularFromFlags(
494
+ 'spec',
495
+ flags.mergeSpec,
496
+ flags.proposeSpec,
497
+ );
498
+ const taskGranular = granularFromFlags(
499
+ 'task',
500
+ flags.mergeTask,
501
+ flags.proposeTask,
502
+ );
503
+ // GRANULAR OVERRIDES AGGREGATE; aggregate over the (config/propose) default.
504
+ // The result KEYS carry the `spec` vocabulary (canonical); the per-type flag axes
505
+ // (`--merge-spec`/`--merge-task`) carry the same `spec` spelling (the cli-flag
506
+ // rename landed in batch 4f).
507
+ return {
508
+ spec: specGranular ?? aggregate ?? defaultMode,
509
+ task: taskGranular ?? aggregate ?? defaultMode,
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Run `intake <N>` end-to-end (the LOCAL one-shot). Never throws for the expected
515
+ * agent-failed / stale / usage cases — those are returned with the corresponding
516
+ * exit code and outcome. The runner owns all git/seam side-effects; the agent only
517
+ * DRAFTS the verdict.
518
+ */
519
+ export async function performIntake(
520
+ options: PerformIntakeOptions,
521
+ ): Promise<IntakeResult> {
522
+ const note = options.note ?? (() => {});
523
+ const arbiter = options.arbiter ?? DEFAULT_ARBITER;
524
+ const cwd = options.cwd;
525
+ // `env` is intake's GIT + provider env, scoped to the configured identity (the
526
+ // `gh` issue ops, the push, the PR). The intake AGENT launches (decision agent
527
+ // + lone-task review agent) read `options.env` directly — they stay AMBIENT
528
+ // (an agent must not act as the bot). Absent identity ⇒ `options.env` unchanged.
529
+ // A configured identity that cannot be resolved (e.g. `tokenEnv` names an unset
530
+ // env var) is a clean usage error, never a crash or a silent ambient fallback.
531
+ const issueNumber = options.issueNumber;
532
+ let env: NodeJS.ProcessEnv;
533
+ try {
534
+ env = identityEnv(options.identity, options.env ?? process.env);
535
+ } catch (err) {
536
+ const message = err instanceof Error ? err.message : String(err);
537
+ note(message);
538
+ return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
539
+ }
540
+ const issueProvider = options.issueProvider ?? new GitHubIssueProvider();
541
+
542
+ // Push-time transport-coherence guard (identity): if a configured identity
543
+ // forbids the arbiter's transport, fail with a clear message rather than
544
+ // silently pushing under an ambient credential. Resolve the arbiter URL softly
545
+ // (a non-zero/unknown URL is skipped — the guard is a no-op without an identity
546
+ // or a resolvable URL).
547
+ if (options.identity !== undefined) {
548
+ const urlRes = await runAsync('git', ['remote', 'get-url', arbiter], cwd, {
549
+ env,
550
+ });
551
+ if (urlRes.status === 0) {
552
+ try {
553
+ assertTransportAllowed(options.identity, urlRes.stdout.trim());
554
+ } catch (err) {
555
+ const message = err instanceof Error ? err.message : String(err);
556
+ note(message);
557
+ return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
558
+ }
559
+ }
560
+ }
561
+
562
+ // 1. READ the issue + thread via the seam (the core never imports `gh`; only the
563
+ // adapter shells out). A read failure surfaces as a usage error — `intake`
564
+ // cannot decide without the issue.
565
+ let issue: Issue;
566
+ let comments: IssueComment[];
567
+ try {
568
+ issue = await issueProvider.getIssue({cwd, issueNumber, env});
569
+ comments = await issueProvider.listComments({cwd, issueNumber, env});
570
+ } catch (err) {
571
+ const detail = err instanceof Error ? err.message : String(err);
572
+ const message = `Could not read issue #${issueNumber}: ${detail}`;
573
+ note(message);
574
+ return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
575
+ }
576
+
577
+ // 2. ACQUIRE the `processing` LOCK (prd `issue-intake` US #10): a TRANSIENT concurrency mutex
578
+ // that serialises two concurrent runs on the SAME issue. Read the labels; if
579
+ // the lock is ALREADY present, BACK OFF (do nothing — another run owns it). The
580
+ // winner ADDS the label and proceeds; the label is REMOVED on finish (success
581
+ // OR handled failure, in the `finally` below). It is NOT a `work/` CAS and NOT a
582
+ // label state-machine (ADR §12) — ONE transient lock label.
583
+ //
584
+ // Fail-vs-degrade (maintainer decision): a lock that is MEANINGFUL but cannot
585
+ // be taken must NOT silently proceed lock-less. Only a genuinely-UNSUPPORTED
586
+ // provider (no label concept at all) legitimately degrades to best-effort (the
587
+ // spec's provider-pluggability; CI's per-issue concurrency group is then the
588
+ // only serialiser — out of scope here). A real FAILURE on a label-supporting
589
+ // provider (e.g. `gh` unauthenticated) FAILS the run with the REAL cause
590
+ // surfaced, rather than misattributing it or proceeding without serialisation.
591
+ const labels = await issueProvider.getLabels({cwd, issueNumber, env});
592
+ if (labels.outcome === 'failed') {
593
+ // The provider HAS labels but we could not READ the lock state — we cannot tell
594
+ // whether another run holds it, so guessing "free" could let two runs proceed.
595
+ // FAIL with the real cause (the actual `gh` stderr), not a hard-coded guess.
596
+ const message =
597
+ `Intake of issue #${issueNumber} could not acquire the ` +
598
+ `\`${PROCESSING_LOCK_LABEL}\` lock: ${labels.instruction}`;
599
+ note(message);
600
+ return {exitCode: 1, outcome: 'lock-failed', issueNumber, message};
601
+ }
602
+ if (
603
+ labels.outcome === 'ok' &&
604
+ labels.labels.includes(PROCESSING_LOCK_LABEL)
605
+ ) {
606
+ const message =
607
+ `Intake of issue #${issueNumber} backed off: the \`${PROCESSING_LOCK_LABEL}\` ` +
608
+ `lock is already held by a concurrent run; doing nothing.`;
609
+ note(message);
610
+ return {exitCode: 0, outcome: 'locked', issueNumber, message};
611
+ }
612
+ let locked = false;
613
+ if (labels.outcome === 'ok') {
614
+ const acquired = await issueProvider.addLabel({
615
+ cwd,
616
+ issueNumber,
617
+ label: PROCESSING_LOCK_LABEL,
618
+ env,
619
+ });
620
+ if (acquired.outcome === 'failed') {
621
+ // The provider HAS labels but the ACQUIRE failed for a real reason (e.g. `gh`
622
+ // lost auth, or the label could not be created on a fresh repo). The lock is
623
+ // meaningful but unacquirable → FAIL with the real cause, do NOT proceed
624
+ // lock-less (which would let a concurrent run race us).
625
+ const message =
626
+ `Intake of issue #${issueNumber} could not acquire the ` +
627
+ `\`${PROCESSING_LOCK_LABEL}\` lock: ${acquired.instruction}`;
628
+ note(message);
629
+ return {exitCode: 1, outcome: 'lock-failed', issueNumber, message};
630
+ }
631
+ locked = acquired.applied;
632
+ } else {
633
+ // Non-label provider (genuinely UNSUPPORTED) → the ONLY legitimate degrade:
634
+ // proceed without the lock, surfaced honestly.
635
+ note(`Processing lock degraded: ${labels.instruction}`);
636
+ }
637
+
638
+ // INTERRUPTION-SAFETY (maintainer point 3): the `finally` below releases on every
639
+ // EXCEPTION path, but a SIGINT/SIGTERM (Ctrl-C, kill) unwinds the process WITHOUT
640
+ // running `finally` — which would LEAK the lock label and block all future intake
641
+ // runs on this issue. While the lock is held we install signal handlers that
642
+ // release it best-effort before the process exits. A leaked lock must ALSO be
643
+ // recoverable by hand and that recovery must be DISCOVERABLE, so we surface the
644
+ // exact manual command (`gh issue edit <N> --remove-label <label>`) whenever the
645
+ // best-effort release does not confirm.
646
+ const manualRecovery =
647
+ `If the \`${PROCESSING_LOCK_LABEL}\` lock is left behind, release it with: ` +
648
+ `gh issue edit ${issueNumber} --remove-label '${PROCESSING_LOCK_LABEL}'`;
649
+ const releaseLock = createLockReleaser({
650
+ locked,
651
+ issueProvider,
652
+ cwd,
653
+ issueNumber,
654
+ env,
655
+ note,
656
+ manualRecovery,
657
+ });
658
+ const onSignal = (signal: NodeJS.Signals) => {
659
+ // Synchronous best-effort release on interruption, then re-raise the default
660
+ // disposition so the process still exits with the conventional signal code.
661
+ if (locked) {
662
+ note(
663
+ `Received ${signal}; releasing the \`${PROCESSING_LOCK_LABEL}\` lock on issue #${issueNumber} before exit.`,
664
+ );
665
+ }
666
+ releaseLock.releaseSync();
667
+ process.removeListener('SIGINT', onSignal);
668
+ process.removeListener('SIGTERM', onSignal);
669
+ process.kill(process.pid, signal);
670
+ };
671
+ if (locked) {
672
+ process.once('SIGINT', onSignal);
673
+ process.once('SIGTERM', onSignal);
674
+ }
675
+
676
+ try {
677
+ return await decideAndDispatch(options, cwd, issue, comments, {
678
+ arbiter,
679
+ issueProvider,
680
+ note,
681
+ // The identity-scoped GIT/provider env (the `gh` ops, push, PR). The AGENT
682
+ // launches inside dispatch read `options.env` (ambient) — not this.
683
+ gitEnv: env,
684
+ });
685
+ } finally {
686
+ // RELEASE the lock on FINISH (success OR handled failure). Only the winner that
687
+ // actually acquired it releases it — a degraded/best-effort run holds nothing.
688
+ process.removeListener('SIGINT', onSignal);
689
+ process.removeListener('SIGTERM', onSignal);
690
+ await releaseLock.release();
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Build the lock RELEASER for {@link performIntake}: one `release()` (the normal
696
+ * async finish path) and one `releaseSync()` (the signal-handler path — a
697
+ * best-effort synchronous release that must run inside a signal handler). Both are
698
+ * no-ops when the run never held the lock (a degraded/unsupported run holds
699
+ * nothing). When a release does not CONFIRM, the manual-recovery hint is surfaced
700
+ * so a leaked lock stays recoverable AND discoverable (maintainer point 3).
701
+ */
702
+ function createLockReleaser(params: {
703
+ locked: boolean;
704
+ issueProvider: IssueProvider;
705
+ cwd: string;
706
+ issueNumber: number;
707
+ env: NodeJS.ProcessEnv | undefined;
708
+ note: (message: string) => void;
709
+ manualRecovery: string;
710
+ }): {release: () => Promise<void>; releaseSync: () => void} {
711
+ const {locked, issueProvider, cwd, issueNumber, env, note, manualRecovery} =
712
+ params;
713
+ let released = false;
714
+ const surfaceFailure = (instruction: string) => {
715
+ note(`Processing lock release degraded: ${instruction}`);
716
+ note(manualRecovery);
717
+ };
718
+ return {
719
+ async release() {
720
+ if (!locked || released) {
721
+ return;
722
+ }
723
+ released = true;
724
+ const result = await issueProvider.removeLabel({
725
+ cwd,
726
+ issueNumber,
727
+ label: PROCESSING_LOCK_LABEL,
728
+ env,
729
+ });
730
+ if (!result.applied) {
731
+ surfaceFailure(result.instruction);
732
+ }
733
+ },
734
+ releaseSync() {
735
+ if (!locked || released) {
736
+ return;
737
+ }
738
+ released = true;
739
+ // A signal handler cannot await. The GitHub adapter's `removeLabel` shells out
740
+ // SYNCHRONOUSLY (spawnSync) inside its async wrapper, so firing it here still
741
+ // runs the `gh` call before the process exits — but we cannot READ the result
742
+ // synchronously through the async seam, so we ALWAYS surface the manual-recovery
743
+ // hint too. That keeps a leaked lock both recoverable AND discoverable even if
744
+ // the in-handler release did not complete (maintainer point 3).
745
+ void issueProvider.removeLabel({
746
+ cwd,
747
+ issueNumber,
748
+ label: PROCESSING_LOCK_LABEL,
749
+ env,
750
+ });
751
+ note(manualRecovery);
752
+ },
753
+ };
754
+ }
755
+
756
+ /**
757
+ * The DECIDE (prompt → verdict) + DISPATCH (the four-outcome table) band, run
758
+ * INSIDE the `processing` lock {@link performIntake} acquires/releases around it.
759
+ * Split out so the lock release is a clean `try`/`finally` in the caller (the lock
760
+ * MUST release on every terminal path — success or handled failure). The agent
761
+ * DRAFTS only; the runner owns every git/seam side-effect here.
762
+ */
763
+ async function decideAndDispatch(
764
+ options: PerformIntakeOptions,
765
+ cwd: string,
766
+ issue: Issue,
767
+ comments: IssueComment[],
768
+ ctx: {
769
+ arbiter: string;
770
+ issueProvider: IssueProvider;
771
+ note: (message: string) => void;
772
+ /** The identity-scoped GIT/provider env (the `gh` ops, push, PR). */
773
+ gitEnv: NodeJS.ProcessEnv | undefined;
774
+ },
775
+ ): Promise<IntakeResult> {
776
+ const {arbiter, issueProvider, note} = ctx;
777
+ const issueNumber = issue.number;
778
+ // `env` here is the identity-scoped GIT/provider env (the runner's `gh`/git
779
+ // ops). The AGENT launches (decision agent, lone-task review) use the AMBIENT
780
+ // `options.env` — an agent must not act as the bot.
781
+ const env = ctx.gitEnv;
782
+
783
+ // TRIAGE (deterministic, under the lock, BEFORE the prompt): decide whether to run
784
+ // the decision at all, built ENTIRELY on intake's own MARKER on the thread (no
785
+ // sidecar/cursor/bot-identity). It SKIPS when intake has the last word
786
+ // (`no-new-input`) or the issue is already terminal (`already-terminal`), and runs
787
+ // the prompt ONLY on genuine new human input. This is also the COMPLETE fix for the
788
+ // self-trigger hazard: intake's own freshly-posted comment carries a marker, so it
789
+ // is excluded from the human-comment check by construction.
790
+ const triage = triageIntake(comments);
791
+ if (triage.action === 'skip') {
792
+ const message =
793
+ triage.outcome === 'no-new-input'
794
+ ? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
795
+ `on the thread and has already seen every human comment up to it; doing ` +
796
+ `nothing (the decision prompt did not run).`
797
+ : `Intake of issue #${issueNumber} skipped: the issue was already ` +
798
+ `transformed (a terminal intake marker is on the thread); a later human ` +
799
+ `comment does not re-open it (the decision prompt did not run).`;
800
+ note(message);
801
+ return {exitCode: 0, outcome: triage.outcome, issueNumber, message};
802
+ }
803
+
804
+ // DECIDE: prompt → VERDICT. The agent DRAFTS only (no git, no seam ops). Tests
805
+ // inject a canned verdict (the dispatcher's testable seam); production wires the
806
+ // harness. The prompt's judgement is not unit-tested — only the dispatch.
807
+ const prompt = buildIntakeDecisionSpec(issue, comments, triage);
808
+ let verdict: IntakeVerdict;
809
+ try {
810
+ verdict = await runDecision(options, cwd, issue, comments, prompt);
811
+ } catch (err) {
812
+ const detail = err instanceof Error ? err.message : String(err);
813
+ const message = `Intake decision failed for issue #${issueNumber}: ${detail}`;
814
+ note(message);
815
+ return {exitCode: 1, outcome: 'agent-failed', issueNumber, message};
816
+ }
817
+
818
+ // DISPATCH on the verdict — the FULL four-outcome decision table (prd
819
+ // `issue-intake`). The agent only DRAFTED the verdict; the runner owns every
820
+ // git/seam side-effect below (the in-band boundary): the write + integrate
821
+ // (task/prd) and the `postIssueComment` (ask/bounce).
822
+ //
823
+ // PER-OUTCOME integration (prd `issue-intake` US #9): the resolved mode is keyed on the runtime
824
+ // artifact TYPE — a `task` verdict integrates with the task mode, a `spec`
825
+ // verdict with the spec mode. Unset ⇒ propose for both. ask/bounce never
826
+ // integrate, so the modes are no-ops for them.
827
+ const modes = options.integration ?? {task: 'propose', spec: 'propose'};
828
+ // The per-run `seen=` DELTA (the HUMAN comment ids intake READ this run, excluding
829
+ // its own marker-comments + already-seen ids) the marker records on every comment
830
+ // intake posts — the chain-model primitive the TRIAGE unions into `seenSet`.
831
+ const seenDelta = computeSeenDelta(comments);
832
+ switch (verdict.outcome) {
833
+ case 'task':
834
+ return dispatchTask({
835
+ verdict,
836
+ issueNumber,
837
+ cwd,
838
+ arbiter,
839
+ integration: modes.task,
840
+ // The origin-trust STAMP, passed IN (not resolved here): the emitted task
841
+ // carries `origin: issue` + this verdict so the becomes-code checkpoint is
842
+ // not laundered. Unset ⇒ unstamped (a local intake ⇒ human/trusted).
843
+ originTrust: options.originTrust,
844
+ noPR: options.noPR,
845
+ providerInstance: options.providerInstance,
846
+ issueProvider,
847
+ // The bounded lone-task review seam (tests inject a canned verdict;
848
+ // production wires the harness via the default below).
849
+ reviewTask: resolveLoneTaskReviewGate(options),
850
+ seen: seenDelta,
851
+ env,
852
+ // The lone-task review AGENT launches AMBIENT (an agent must not act as
853
+ // the bot); `env` above is the identity-scoped git/provider env.
854
+ agentEnv: options.env,
855
+ note,
856
+ });
857
+ // The `spec` outcome is the parent-spec verdict; it dispatches through
858
+ // `modes.spec`. HARD CUTOVER: the legacy `prd` outcome case is GONE.
859
+ case 'spec':
860
+ return dispatchSpec({
861
+ verdict,
862
+ issueNumber,
863
+ cwd,
864
+ arbiter,
865
+ integration: modes.spec,
866
+ // Same origin-trust stamp on the prd outcome (propagated onto its tasks
867
+ // later by the tasker). Passed IN; not resolved here.
868
+ originTrust: options.originTrust,
869
+ noPR: options.noPR,
870
+ // RUNNER-DETERMINISTIC PLACEMENT (task
871
+ // `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the
872
+ // configured-default + explicit-flag rungs, fed into the SHARED placement
873
+ // resolver alongside the `originTrust` stamp above. The resolver decides
874
+ // `prds/proposed/` (staging) vs `prds/ready/` (the tasking pool); `intake` never
875
+ // places itself.
876
+ specsLandIn: options.specsLandIn,
877
+ explicitSpecsLandIn: options.explicitSpecsLandIn,
878
+ providerInstance: options.providerInstance,
879
+ issueProvider,
880
+ seen: seenDelta,
881
+ env,
882
+ note,
883
+ });
884
+ case 'ask':
885
+ return dispatchComment({
886
+ outcome: 'asked',
887
+ cwd,
888
+ issueNumber,
889
+ issueProvider,
890
+ // The drafted clarifying question; a thin fallback keeps the comment
891
+ // non-empty if the agent left it blank.
892
+ body:
893
+ verdict.question && verdict.question.trim() !== ''
894
+ ? verdict.question
895
+ : `Could you clarify issue #${issueNumber} so it can be acted on?`,
896
+ // STAMP the MARKER recording `kind=ask` (non-terminal — the TRIAGE owns
897
+ // that) + the `seen=` delta, so a re-run recognises this as intake's own
898
+ // turn and resumes only on genuine new human input.
899
+ markerKind: 'ask',
900
+ seen: seenDelta,
901
+ env,
902
+ note,
903
+ });
904
+ case 'bounce':
905
+ return dispatchComment({
906
+ outcome: 'bounced',
907
+ cwd,
908
+ issueNumber,
909
+ issueProvider,
910
+ // The drafted bounce message; a thin fallback restates the "file separate
911
+ // issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically (this
912
+ // text as the closing comment + reason not planned).
913
+ body:
914
+ verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
915
+ ? verdict.bounceMessage
916
+ : `This issue looks like multiple unrelated concerns — please file ` +
917
+ `separate issues so each can be intaken on its own.`,
918
+ // STAMP `kind=bounced` (TERMINAL — the TRIAGE then SKIPS `already-terminal`
919
+ // on a later human comment) + the `seen=` delta.
920
+ markerKind: 'bounced',
921
+ seen: seenDelta,
922
+ env,
923
+ note,
924
+ });
925
+ }
926
+ }
927
+
928
+ /**
929
+ * DISPATCH the `ask` / `bounce` outcomes — the SHARED comment band, which now
930
+ * BRANCHES on the outcome:
931
+ *
932
+ * - **ask** (non-terminal): `postIssueComment` the drafted question, emit NOTHING,
933
+ * and LEAVE THE ISSUE OPEN — it waits for the thread to be answered (a later run
934
+ * resumes from it). The task/prd path also never closes (CI's close-job does,
935
+ * via the `issue:` field). Intake closes ONLY on BOUNCE.
936
+ * - **bounce** (TERMINAL): the asks are unrelated and must be re-filed, so an OPEN
937
+ * issue is a dishonest "still in play" signal. Intake CLOSES the issue
938
+ * ATOMICALLY via a single `closeIssue` carrying the bounce text as the closing
939
+ * comment + `reason: not planned` (one call — no post-then-close partial-failure
940
+ * window). The result's `closed` reflects it.
941
+ *
942
+ * Both the comment poster and the atomic close are advisory and DEGRADE (a
943
+ * missing/unauthenticated `gh` never throws — the text/real cause is surfaced via
944
+ * `ghFailureReason`, never a hard-coded guess), so the terminal outcome is
945
+ * unchanged (`asked`/`bounced`, exit 0) and the run still terminates cleanly.
946
+ */
947
+ async function dispatchComment(params: {
948
+ outcome: 'asked' | 'bounced';
949
+ cwd: string;
950
+ issueNumber: number;
951
+ issueProvider: IssueProvider;
952
+ body: string;
953
+ /** The neutral `kind` the MARKER records (`ask` for an ask, `bounced` for a bounce). */
954
+ markerKind: IntakeMarkerKind;
955
+ /** The per-run `seen=` delta of HUMAN comment ids intake read this run. */
956
+ seen: string[];
957
+ env: NodeJS.ProcessEnv | undefined;
958
+ note: (message: string) => void;
959
+ }): Promise<IntakeResult> {
960
+ const {
961
+ outcome,
962
+ cwd,
963
+ issueNumber,
964
+ issueProvider,
965
+ body,
966
+ markerKind,
967
+ seen,
968
+ env,
969
+ note,
970
+ } = params;
971
+ // STAMP the intake MARKER onto the body so a re-run recognises this as intake's
972
+ // own comment (the SOLE self-recognition signal — no author identity). Hidden HTML
973
+ // comment; renders as nothing, present in the raw markdown the TRIAGE parses.
974
+ const stamped = stampIntakeMarker(body, {kind: markerKind, seen});
975
+
976
+ if (outcome === 'bounced') {
977
+ // BOUNCE is TERMINAL: CLOSE the issue ATOMICALLY (bounce text as the closing
978
+ // comment + reason not planned) in ONE call — no separate postIssueComment, no
979
+ // post-then-close window. The close DEGRADES (never throws) on a missing/
980
+ // unauthenticated `gh`, surfacing the REAL cause; the terminal outcome stays
981
+ // `bounced`/exit 0 regardless.
982
+ const close = await issueProvider.closeIssue({
983
+ cwd,
984
+ issueNumber,
985
+ comment: stamped,
986
+ reason: 'not planned',
987
+ env,
988
+ });
989
+ const tail = close.closed
990
+ ? 'the issue was closed (as not planned) with the bounce comment'
991
+ : `the issue could NOT be closed (${close.instruction})`;
992
+ const message =
993
+ `Intake bounced issue #${issueNumber}; emitted no artifact and closed the ` +
994
+ `issue as not planned — ${tail}.`;
995
+ note(message);
996
+ return {
997
+ exitCode: 0,
998
+ outcome,
999
+ issueNumber,
1000
+ commented: close.closed,
1001
+ closed: close.closed,
1002
+ message,
1003
+ };
1004
+ }
1005
+
1006
+ // ASK (non-terminal): post the clarifying question and LEAVE THE ISSUE OPEN.
1007
+ const posted = await issueProvider.postIssueComment({
1008
+ cwd,
1009
+ issueNumber,
1010
+ body: stamped,
1011
+ env,
1012
+ });
1013
+ const tail = posted.posted
1014
+ ? 'the comment was posted'
1015
+ : `the comment could NOT be posted (${posted.instruction})`;
1016
+ const message =
1017
+ `Intake asked a clarifying question on issue #${issueNumber}; emitted no ` +
1018
+ `artifact and left the issue open — ${tail}.`;
1019
+ note(message);
1020
+ return {
1021
+ exitCode: 0,
1022
+ outcome,
1023
+ issueNumber,
1024
+ commented: posted.posted,
1025
+ message,
1026
+ };
1027
+ }
1028
+
1029
+ /**
1030
+ * DISPATCH the `task` outcome: derive a content-derived slug, write
1031
+ * `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the
1032
+ * lone-task closure link, NOT `Fixes #N`), and integrate via {@link
1033
+ * performIntegration}. The runner owns the git: it onboards a
1034
+ * `work/<slug>` branch off fresh `<arbiter>/main`, then the lifecycle `stage`
1035
+ * writes + stages the task and the band commits + rebases + integrates it. The
1036
+ * agent did NO git/seam ops.
1037
+ */
1038
+ async function dispatchTask(params: {
1039
+ verdict: IntakeVerdict;
1040
+ issueNumber: number;
1041
+ cwd: string;
1042
+ arbiter: string;
1043
+ integration: IntegrationMode;
1044
+ /** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
1045
+ originTrust: OriginTrust | undefined;
1046
+ noPR: boolean | undefined;
1047
+ providerInstance: ReviewProvider | undefined;
1048
+ /** The issue seam the completion comment is posted back through (runner-owned). */
1049
+ issueProvider: IssueProvider;
1050
+ /** The bounded lone-task review seam (tests inject a canned verdict; prod: harness). */
1051
+ reviewTask: LoneTaskReviewGate;
1052
+ /** The per-run `seen=` delta of HUMAN comment ids the completion marker records. */
1053
+ seen: string[];
1054
+ /** The identity-scoped GIT/provider env (push, PR, completion comment). */
1055
+ env: NodeJS.ProcessEnv | undefined;
1056
+ /** The AMBIENT env for the lone-task review AGENT launch (never the identity). */
1057
+ agentEnv: NodeJS.ProcessEnv | undefined;
1058
+ note: (message: string) => void;
1059
+ }): Promise<IntakeResult> {
1060
+ const {
1061
+ verdict,
1062
+ issueNumber,
1063
+ cwd,
1064
+ arbiter,
1065
+ integration,
1066
+ originTrust,
1067
+ noPR,
1068
+ providerInstance,
1069
+ issueProvider,
1070
+ reviewTask,
1071
+ seen,
1072
+ env,
1073
+ agentEnv,
1074
+ note,
1075
+ } = params;
1076
+
1077
+ // A content-derived slug — NEVER a counter (prd `issue-intake` US #8). Prefer the drafted
1078
+ // `taskSlug`, else derive from the drafted title; sanitise either through
1079
+ // `paramCase` so the filename + frontmatter slug are well-formed.
1080
+ const slug = resolveSlug(verdict);
1081
+ if (slug === '') {
1082
+ const message =
1083
+ `Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
1084
+ `slug/title to derive a content-derived slug from (never a counter).`;
1085
+ note(message);
1086
+ return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
1087
+ }
1088
+ const relPath = workItemRel('tasks-ready', `${slug}.md`);
1089
+
1090
+ // BOUNDED INTERNAL REVIEW (observation
1091
+ // `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C):
1092
+ // the `do prd:` path gets `runTaskReviewLoop`; the lone-TASK path got NOTHING.
1093
+ // AFTER the `task` verdict and BEFORE the write/integrate, run a bounded (3-round,
1094
+ // HARD-CAPPED) adversarial self-review on the SINGLE drafted task. It mutates the
1095
+ // candidate body IN MEMORY (no `work/backlog/` write pre-convergence). A launch/
1096
+ // parse failure THROWS — `decideAndDispatch`'s try/catch maps it onto `agent-failed`
1097
+ // (never a silent emit of the un-reviewed task).
1098
+ let review: LoneTaskReviewResult;
1099
+ try {
1100
+ review = await runLoneTaskReview({
1101
+ slug,
1102
+ issueNumber,
1103
+ draftTitle: verdict.taskTitle ?? slug,
1104
+ draftBody: verdict.taskBody,
1105
+ gate: reviewTask,
1106
+ cwd,
1107
+ // The review AGENT launches AMBIENT (never the identity-scoped env).
1108
+ env: agentEnv,
1109
+ note,
1110
+ });
1111
+ } catch (err) {
1112
+ // A review-agent launch/parse FAILURE DEGRADES honestly onto the EXISTING
1113
+ // `agent-failed` outcome (exit 1) — NEVER a silent emit of the un-reviewed
1114
+ // task. The SAME try/catch discipline the decision step uses.
1115
+ const detail = err instanceof Error ? err.message : String(err);
1116
+ const message = `Intake lone-task review failed for issue #${issueNumber}: ${detail}`;
1117
+ note(message);
1118
+ return {exitCode: 1, outcome: 'agent-failed', issueNumber, message};
1119
+ }
1120
+
1121
+ if (review.outcome === 'non-converge') {
1122
+ // NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
1123
+ // `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the proposed
1124
+ // task DRAFT and the open question(s) in its BODY (NOT a new marker kind) — the
1125
+ // human reacts to a concrete draft, strictly richer than a blank-question ask.
1126
+ // NEVER write `work/backlog/<slug>.md`; NEVER silently emit the under-refined
1127
+ // task. The next intake run resumes via the already-built triage gate.
1128
+ note(
1129
+ `Intake's lone-task review did not converge for issue #${issueNumber} ` +
1130
+ `(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
1131
+ `question(s) in the comment body.`,
1132
+ );
1133
+ return dispatchComment({
1134
+ outcome: 'asked',
1135
+ cwd,
1136
+ issueNumber,
1137
+ issueProvider,
1138
+ body: composeLoneTaskAskComment({
1139
+ issueNumber,
1140
+ slug,
1141
+ draftTitle: review.title,
1142
+ draftBody: review.body,
1143
+ questions: review.questions,
1144
+ }),
1145
+ markerKind: 'ask',
1146
+ seen,
1147
+ env,
1148
+ note,
1149
+ });
1150
+ }
1151
+
1152
+ // CONVERGED: the (possibly edited) task is emitted via the EXISTING write/integrate
1153
+ // path below + the existing `task created` completion comment. The refined body
1154
+ // replaces the agent's first draft.
1155
+ const reviewedBody = review.body;
1156
+
1157
+ // ONBOARD the task write onto a `work/intake-task-<slug>` branch cut from the
1158
+ // freshly-fetched `<arbiter>/main` (the SAME runner-owns-git discipline the
1159
+ // tasking path uses): the lifecycle `stage` writes the file ON THIS BRANCH and
1160
+ // the shared integrate core (`--propose` PR / `--merge` main) lands it. The
1161
+ // intake- producer prefix keeps it distinct from a later `do task:<slug>`
1162
+ // build branch for the same slug. The agent ran no git.
1163
+ await switchToWorkBranch(cwd, arbiter, 'task', slug, env);
1164
+
1165
+ const taskContent = renderBacklogTask({
1166
+ slug,
1167
+ title: review.title,
1168
+ body: reviewedBody,
1169
+ issueNumber,
1170
+ originTrust,
1171
+ });
1172
+
1173
+ const core = await performIntegration({
1174
+ cwd,
1175
+ arbiter,
1176
+ slug,
1177
+ // `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
1178
+ source: 'in-progress',
1179
+ recovering: false,
1180
+ // An intake-emitted task has no `verify` floor of its own (it is a new
1181
+ // backlog item, not a build); skip the acceptance gate, exactly as the
1182
+ // tasking transition does.
1183
+ skipVerify: true,
1184
+ // Default `propose` (the per-outcome KNOBS are a later task). The
1185
+ // EXPLICITLY-chosen mode proceeds as-is: a future `--merge-task` lands on main
1186
+ // (`merge` IS the auto-land mode, never downgraded).
1187
+ mode: integration,
1188
+ noPR,
1189
+ providerInstance,
1190
+ type: 'feat',
1191
+ lifecycle: {
1192
+ // The emitted task IS the title source. Pass the DRAFTED title EXPLICITLY
1193
+ // (not a read-from-path): `stage()` WRITES `work/backlog/<slug>.md` AFTER the
1194
+ // core reads the title, so a `titlePath` read would race the write and degrade
1195
+ // the commit subject / PR title to the generic fallback. `titlePath` stays set
1196
+ // (the lifecycle contract requires it) but is IGNORED while `title` is present.
1197
+ titlePath: join(cwd, relPath),
1198
+ title: review.title,
1199
+ commitTag: 'intake',
1200
+ stage: () =>
1201
+ stageIntakeContent({cwd, relPath, content: taskContent, env}),
1202
+ },
1203
+ env,
1204
+ note,
1205
+ });
1206
+
1207
+ return integrationToIntakeResult(core, {
1208
+ issueNumber,
1209
+ slug,
1210
+ relPath,
1211
+ cwd,
1212
+ issueProvider,
1213
+ seen,
1214
+ env,
1215
+ note,
1216
+ });
1217
+ }
1218
+
1219
+ /**
1220
+ * DISPATCH the `spec` outcome (canonical; the legacy `prd` outcome routes here too
1221
+ * through the cutover): derive a content-derived slug, write the spec file
1222
+ * (`work/specs/ready/<slug>.md`) carrying `issue: N` (the loop-closure linkage the
1223
+ * close JOB reaches via `task.spec: → spec issue:`; on a fanned spec the number
1224
+ * lives ONLY on the spec — a fanned task uses `spec:`, NOT its own `issue:`, which
1225
+ * is the lone-task outcome's link) + the gate axes the prompt JUDGED, integrate it
1226
+ * via {@link performIntegration}, then STOP. Tasking the emitted spec is the
1227
+ * SEPARATE `do spec:` step (NOT done here). A coupled-but-SMALL pair lands here too
1228
+ * (the spec vs BOUNCE line is SHARED VISION, not size — the over-bounce guard). The
1229
+ * runner owns the git exactly as the task branch does; the agent did NO git/seam ops.
1230
+ */
1231
+ async function dispatchSpec(params: {
1232
+ verdict: IntakeVerdict;
1233
+ issueNumber: number;
1234
+ cwd: string;
1235
+ arbiter: string;
1236
+ integration: IntegrationMode;
1237
+ /** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
1238
+ originTrust: OriginTrust | undefined;
1239
+ noPR: boolean | undefined;
1240
+ /** The per-repo SPEC-PLACEMENT default (configured-default rung of the placement chain). */
1241
+ specsLandIn: SpecsLandIn | undefined;
1242
+ /** The OPERATOR's EXPLICIT spec-placement override (the TOP rung). */
1243
+ explicitSpecsLandIn: SpecsLandIn | undefined;
1244
+ providerInstance: ReviewProvider | undefined;
1245
+ /** The issue seam the completion comment is posted back through (runner-owned). */
1246
+ issueProvider: IssueProvider;
1247
+ /** The per-run `seen=` delta of HUMAN comment ids the completion marker records. */
1248
+ seen: string[];
1249
+ env: NodeJS.ProcessEnv | undefined;
1250
+ note: (message: string) => void;
1251
+ }): Promise<IntakeResult> {
1252
+ const {
1253
+ verdict,
1254
+ issueNumber,
1255
+ cwd,
1256
+ arbiter,
1257
+ integration,
1258
+ originTrust,
1259
+ noPR,
1260
+ specsLandIn,
1261
+ explicitSpecsLandIn,
1262
+ providerInstance,
1263
+ issueProvider,
1264
+ seen,
1265
+ env,
1266
+ note,
1267
+ } = params;
1268
+
1269
+ // A content-derived slug — NEVER a counter (prd `issue-intake` US #8). Prefer the drafted
1270
+ // `specSlug`, else derive from the drafted title.
1271
+ const slug = resolveSpecSlug(verdict);
1272
+ if (slug === '') {
1273
+ const message =
1274
+ `Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
1275
+ `slug/title to derive a content-derived slug from (never a counter).`;
1276
+ note(message);
1277
+ return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
1278
+ }
1279
+ // RUNNER-DETERMINISTIC PLACEMENT (task
1280
+ // `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
1281
+ // `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Resolve
1282
+ // which folder the runner writes the intake-authored prd into BEFORE handing
1283
+ // it to the shared integrate band: the SAME precedence chain the tasker uses
1284
+ // (`explicit > untrusted-origin ⇒ staging > specsLandIn > built-in (staging)`),
1285
+ // the SAME shared resolver — only the lifecycle SLOTS differ. The agent
1286
+ // (the intake decider) never influences placement; it returns the verdict and
1287
+ // the runner computes the destination from unforgeable inputs.
1288
+ const placementDecision = resolvePlacement({
1289
+ explicit: specLandingToSide(explicitSpecsLandIn),
1290
+ originTrust,
1291
+ configuredDefault: specLandingToSide(specsLandIn),
1292
+ });
1293
+ const placementDir = placementFolder(
1294
+ SPEC_PLACEMENT_SLOTS,
1295
+ placementDecision.choice,
1296
+ );
1297
+ const relPath = `${placementDir}/${slug}.md`;
1298
+
1299
+ // ONBOARD onto a `work/intake-spec-<slug>` branch off fresh `<arbiter>/main` —
1300
+ // the SAME runner-owns-git discipline the task branch uses; the intake-
1301
+ // producer prefix keeps it distinct from a `do spec:<slug>` tasking branch.
1302
+ await switchToWorkBranch(cwd, arbiter, 'spec', slug, env);
1303
+
1304
+ const specContent = renderSpec({
1305
+ slug,
1306
+ title: verdict.specTitle ?? slug,
1307
+ body: verdict.specBody,
1308
+ issueNumber,
1309
+ humanOnly: verdict.specHumanOnly,
1310
+ needsAnswers: verdict.specNeedsAnswers,
1311
+ originTrust,
1312
+ });
1313
+
1314
+ const core = await performIntegration({
1315
+ cwd,
1316
+ arbiter,
1317
+ slug,
1318
+ source: 'in-progress',
1319
+ recovering: false,
1320
+ // An intake-emitted prd has no `verify` floor of its own (it is a new spec,
1321
+ // not a build), exactly as the task branch + the tasking transition skip it.
1322
+ skipVerify: true,
1323
+ mode: integration,
1324
+ noPR,
1325
+ providerInstance,
1326
+ type: 'feat',
1327
+ lifecycle: {
1328
+ // The emitted prd IS the title source. Pass the DRAFTED title EXPLICITLY (same
1329
+ // race as the task path: `stage()` writes the prd file AFTER the title
1330
+ // read). `titlePath` stays set but is IGNORED while `title` is present.
1331
+ titlePath: join(cwd, relPath),
1332
+ title: verdict.specTitle ?? slug,
1333
+ commitTag: 'intake',
1334
+ stage: () =>
1335
+ stageIntakeContent({cwd, relPath, content: specContent, env}),
1336
+ },
1337
+ env,
1338
+ note,
1339
+ });
1340
+
1341
+ return integrationToIntakeResult(core, {
1342
+ issueNumber,
1343
+ slug,
1344
+ relPath,
1345
+ kind: 'spec',
1346
+ cwd,
1347
+ issueProvider,
1348
+ seen,
1349
+ env,
1350
+ note,
1351
+ });
1352
+ }
1353
+
1354
+ /**
1355
+ * Map the shared integrate band's {@link IntegrationCoreResult} onto the intake
1356
+ * {@link IntakeResult}. On `completed` the artifact was written + integrated; a
1357
+ * `rebase-conflict` against an advanced `main` maps to `stale` (the analogue of
1358
+ * "the backlog moved under us"); everything else maps defensively to a usage error
1359
+ * (the intake task path passes `skipVerify` + has no review gate, so neither
1360
+ * `gate-failed` nor `review-blocked` can occur).
1361
+ */
1362
+ async function integrationToIntakeResult(
1363
+ core: IntegrationCoreResult,
1364
+ ctx: {
1365
+ issueNumber: number;
1366
+ slug: string;
1367
+ relPath: string;
1368
+ kind?: 'task' | 'spec';
1369
+ /** The working checkout the issue seam shells `gh` in. */
1370
+ cwd: string;
1371
+ /** The issue seam the completion comment is posted back through. */
1372
+ issueProvider: IssueProvider;
1373
+ /** The per-run `seen=` delta the completion marker records (chain model). */
1374
+ seen: string[];
1375
+ env: NodeJS.ProcessEnv | undefined;
1376
+ note: (message: string) => void;
1377
+ },
1378
+ ): Promise<IntakeResult> {
1379
+ const {issueNumber, slug, relPath, cwd, issueProvider, seen, env, note} = ctx;
1380
+ const kind = ctx.kind ?? 'task';
1381
+ const artifact = kind === 'spec' ? 'spec' : 'task';
1382
+ if (core.outcome === 'completed') {
1383
+ const landed =
1384
+ core.integration?.mode === 'merge'
1385
+ ? 'landed it on the arbiter main'
1386
+ : 'opened a PR carrying it (main untouched)';
1387
+ // Both a lone task and a prd carry `issue: N` as their closure link (the task
1388
+ // closes its own issue; a prd is reached via `task.prd: → prd issue:`). On the
1389
+ // task/prd path `intake` never closes the issue (CI's close-job does, via the
1390
+ // `issue:` field; intake closes ONLY on BOUNCE) and emits no `Fixes #N` (a
1391
+ // deferred GitHub-only optimisation).
1392
+ const link = `issue: ${issueNumber}`;
1393
+ const message =
1394
+ `Intake of issue #${issueNumber} → wrote ${relPath} (${link}); ` +
1395
+ `the runner integrated it through the shared core and ${landed}.`;
1396
+ // CLOSE THE LOOP (this task): post ONE INFORMATIONAL completion comment back on
1397
+ // the issue for the SUCCESSFUL outcome — the confirmation the ASK/BOUNCE comments
1398
+ // already give the author. It reports `task created` / `spec created` (NEVER
1399
+ // "issue resolved"; intake never closes on task/spec — CI's close-job does, via
1400
+ // the `issue:` field) and links the artifact by integration mode: the PR `url` in
1401
+ // propose, the landed `commit` in merge. The marker carries `kind=created` (the
1402
+ // TRIAGE treats it as TERMINAL → `already-terminal`), so the comment cannot
1403
+ // re-trigger intake. ADVISORY — it DEGRADES (a missing/unauthenticated `gh` never
1404
+ // throws), so a degrade leaves the run's success outcome unchanged.
1405
+ const posted = await postCompletionComment({
1406
+ issueProvider,
1407
+ issueNumber,
1408
+ kind,
1409
+ slug,
1410
+ integration: core.integration,
1411
+ seen,
1412
+ cwd,
1413
+ env,
1414
+ note,
1415
+ });
1416
+ return {
1417
+ exitCode: 0,
1418
+ outcome: kind === 'spec' ? 'spec-written' : 'tasked',
1419
+ issueNumber,
1420
+ emittedSlug: slug,
1421
+ emitted: relPath,
1422
+ commented: posted,
1423
+ message,
1424
+ };
1425
+ }
1426
+ if (core.outcome === 'rebase-conflict') {
1427
+ return {
1428
+ exitCode: 4,
1429
+ outcome: 'stale',
1430
+ issueNumber,
1431
+ message:
1432
+ core.reason ??
1433
+ `Integrating the intake ${artifact} for issue #${issueNumber} conflicted ` +
1434
+ `against the latest main; re-run intake.`,
1435
+ };
1436
+ }
1437
+ return {
1438
+ exitCode: 1,
1439
+ outcome: 'usage-error',
1440
+ issueNumber,
1441
+ message:
1442
+ core.reason ??
1443
+ `Integrating the intake ${artifact} for issue #${issueNumber} failed unexpectedly.`,
1444
+ };
1445
+ }
1446
+
1447
+ /**
1448
+ * Build the INFORMATIONAL completion-comment BODY (with its FULL `created` marker)
1449
+ * for a SUCCESSFUL `task` / `spec` outcome — the PURE, seam-free core of
1450
+ * {@link postCompletionComment}, exported so both link variants are unit-testable
1451
+ * without a live seam. The comment:
1452
+ *
1453
+ * - reports `task created` / `spec created` framed as CREATED — NEVER "issue
1454
+ * resolved/closed" (intake never closes on the task/spec path).
1455
+ * - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
1456
+ * `commit` (the additive {@link IntegrateResult.commit}) in merge. A degraded
1457
+ * propose (no `url`) or a failed merge-tip read (no `commit`) simply OMITS the
1458
+ * link — the comment still confirms what was created (the artifact is safe on the
1459
+ * branch/main regardless). No prd link beyond the slug.
1460
+ * - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
1461
+ * (`kind=created slug=<slug> seen=<id>,…`) so the triage's `already-terminal`
1462
+ * branch consumes it — the comment cannot re-trigger intake.
1463
+ */
1464
+ export function composeIntakeCompletionComment(params: {
1465
+ kind: 'task' | 'spec';
1466
+ slug: string;
1467
+ integration: IntegrateResult | undefined;
1468
+ seen: string[];
1469
+ }): string {
1470
+ const {kind, slug, integration, seen} = params;
1471
+ const artifact = kind === 'spec' ? 'spec' : 'task';
1472
+ const link =
1473
+ integration?.mode === 'merge'
1474
+ ? integration.commit !== undefined
1475
+ ? `\n\nIt landed on \`main\` in commit ${integration.commit}.`
1476
+ : ''
1477
+ : integration?.url !== undefined
1478
+ ? `\n\nIt is carried by the PR: ${integration.url}`
1479
+ : '';
1480
+ const body =
1481
+ `Created ${artifact} \`${slug}\` from this issue.${link}\n\n` +
1482
+ `This is an informational update — the issue stays open (it remains in play ` +
1483
+ `until the ${artifact} lands; intake does not change the issue's state).`;
1484
+ // STAMP the FULL marker (incl. `seen=`) via the SHARED helper, so the triage's
1485
+ // `already-terminal` branch recognises this terminal `created` comment.
1486
+ return stampIntakeMarker(body, {kind: 'created', seen, slug});
1487
+ }
1488
+
1489
+ /**
1490
+ * Post the INFORMATIONAL completion comment for a SUCCESSFUL `task` / `spec`
1491
+ * outcome (this task). It closes the loop the ASK/BOUNCE comments already close
1492
+ * for the other outcomes: the issue author gets a confirmation when intake did the
1493
+ * useful thing. The comment:
1494
+ *
1495
+ * - reports `task created` / `spec created` — NEVER "issue resolved/closed".
1496
+ * Intake never closes the issue on the task/spec path (CI's future close-job
1497
+ * does, via the `issue:` field); this comment changes NO issue state.
1498
+ * - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
1499
+ * `commit` (the additive {@link IntegrateResult.commit} this task surfaces) in
1500
+ * merge. No prd link beyond the slug.
1501
+ * - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
1502
+ * (`kind=created slug=<slug> seen=<id>,…`). `kind=created` is TERMINAL, so the
1503
+ * triage's `already-terminal` branch then treats the issue as already-transformed
1504
+ * — the completion comment cannot re-trigger intake.
1505
+ *
1506
+ * ADVISORY — it DEGRADES (a missing/unauthenticated `gh` surfaces the text, never
1507
+ * throws), so a degrade does NOT change the run's success outcome. Returns whether
1508
+ * a comment was actually posted (for {@link IntakeResult.commented}).
1509
+ */
1510
+ async function postCompletionComment(params: {
1511
+ issueProvider: IssueProvider;
1512
+ issueNumber: number;
1513
+ kind: 'task' | 'spec';
1514
+ slug: string;
1515
+ /** The integrate result — carries the propose `url` / the merge `commit` link. */
1516
+ integration: IntegrateResult | undefined;
1517
+ /** The per-run `seen=` delta the marker records (the chain-model primitive). */
1518
+ seen: string[];
1519
+ /** The working checkout the issue seam shells `gh` in. */
1520
+ cwd: string;
1521
+ env: NodeJS.ProcessEnv | undefined;
1522
+ note: (message: string) => void;
1523
+ }): Promise<boolean> {
1524
+ const {
1525
+ issueProvider,
1526
+ issueNumber,
1527
+ kind,
1528
+ slug,
1529
+ integration,
1530
+ seen,
1531
+ cwd,
1532
+ env,
1533
+ note,
1534
+ } = params;
1535
+ const artifact = kind === 'spec' ? 'spec' : 'task';
1536
+ // Build the full stamped body (CREATED wording + mode-keyed link + the FULL
1537
+ // `created` marker) via the exported pure builder — unit-tested directly for both
1538
+ // link variants (propose `url` / merge `commit`).
1539
+ const stamped = composeIntakeCompletionComment({
1540
+ kind,
1541
+ slug,
1542
+ integration,
1543
+ seen,
1544
+ });
1545
+ const posted = await issueProvider.postIssueComment({
1546
+ cwd,
1547
+ issueNumber,
1548
+ body: stamped,
1549
+ env,
1550
+ });
1551
+ note(
1552
+ posted.posted
1553
+ ? `Posted a '${artifact} created' completion comment on issue #${issueNumber}.`
1554
+ : `Could not post the completion comment on issue #${issueNumber} ` +
1555
+ `(${posted.instruction}); the ${artifact} was still created.`,
1556
+ );
1557
+ return posted.posted;
1558
+ }
1559
+
1560
+ /**
1561
+ * Resolve a content-derived slug from the verdict — NEVER a counter (prd `issue-intake` US #8).
1562
+ * Prefer the drafted `taskSlug`, else derive from the drafted title; both go
1563
+ * through `paramCase` (the brand case-transform) so the result is a clean
1564
+ * lowercase-`-`-joined slug. An empty result (no slug AND no title) signals the
1565
+ * caller to refuse (a counter fallback is forbidden).
1566
+ */
1567
+ function resolveSlug(verdict: IntakeVerdict): string {
1568
+ const candidate =
1569
+ verdict.taskSlug && verdict.taskSlug.trim() !== ''
1570
+ ? verdict.taskSlug
1571
+ : (verdict.taskTitle ?? '');
1572
+ return paramCase(candidate);
1573
+ }
1574
+
1575
+ /**
1576
+ * Resolve a content-derived slug for the spec outcome — NEVER a counter (prd `issue-intake` US #8).
1577
+ * Prefer the drafted `specSlug`, else derive from the drafted spec title; both go
1578
+ * through `paramCase`. An empty result signals the caller to refuse.
1579
+ */
1580
+ function resolveSpecSlug(verdict: IntakeVerdict): string {
1581
+ const candidate =
1582
+ verdict.specSlug && verdict.specSlug.trim() !== ''
1583
+ ? verdict.specSlug
1584
+ : (verdict.specTitle ?? '');
1585
+ return paramCase(candidate);
1586
+ }
1587
+
1588
+ /**
1589
+ * Render the backlog task file: the frontmatter (`title`/`slug`/`covers: []`, NO
1590
+ * `prd:` — its own source of truth, prd `issue-intake` decision table) carrying the lone-task
1591
+ * `issue: N` closure link + the drafted body. The task closes its source issue
1592
+ * via its `issue:` field (the provider-agnostic link a FUTURE CI close-job reads
1593
+ * from folder + field state); it carries NO `Fixes #N` (a deferred GitHub-only
1594
+ * optimisation, structurally unplaceable on the `--merge` path). The number is
1595
+ * the task's own closure path — `issue:` XOR `prd:`; a lone task never carries a
1596
+ * `prd:` (prd `issue-intake` decision table). When the agent drafted no body, a thin default
1597
+ * scaffold keeps the file a valid task.
1598
+ */
1599
+ export function renderBacklogTask(params: {
1600
+ slug: string;
1601
+ title: string;
1602
+ body: string | undefined;
1603
+ issueNumber: number;
1604
+ /**
1605
+ * The origin-trust STAMP (task `untrusted-origin-forces-build-propose`).
1606
+ * Present ⇒ emit `origin: issue` + `originTrust: <value>` so the becomes-code
1607
+ * checkpoint survives the merge boundary. UNSET (a local intake, no CI shell)
1608
+ * ⇒ NO stamp (the human running intake IS the checkpoint ⇒ human/trusted).
1609
+ */
1610
+ originTrust?: OriginTrust;
1611
+ }): string {
1612
+ const {slug, title, body, issueNumber, originTrust} = params;
1613
+ const lines = [
1614
+ '---',
1615
+ `title: ${title}`,
1616
+ `slug: ${slug}`,
1617
+ `issue: ${issueNumber}`,
1618
+ ];
1619
+ if (originTrust !== undefined) {
1620
+ lines.push('origin: issue', `originTrust: ${originTrust}`);
1621
+ }
1622
+ lines.push('covers: []', 'blockedBy: []', '---');
1623
+ const frontmatter = lines.join('\n');
1624
+ // The drafted body (agent-authored, headings and all) is wrapped VERBATIM.
1625
+ // Only the empty-body DEFAULT SCAFFOLD is sourced from the shared section
1626
+ // skeleton owner (`renderTaskBody`, prd
1627
+ // `centralize-buildable-task-renderer-shared-by-intake-and-promotion` US #2),
1628
+ // so intake's fallback and promotion's body cannot drift on section
1629
+ // names/order. The shared renderer ends its body with a trailing newline (its
1630
+ // last line is a blank); intake owns the single trailing `\n` in the join
1631
+ // below, so we `trimEnd()` the renderer output to stay byte-for-byte identical
1632
+ // to the pre-rewire literal.
1633
+ const drafted =
1634
+ body && body.trim() !== ''
1635
+ ? body.trim()
1636
+ : renderTaskBody({
1637
+ whatToBuild: title,
1638
+ acceptanceCriteria: '- [ ] the issue is resolved',
1639
+ prompt: `Resolve issue #${issueNumber}: ${title}`,
1640
+ }).trimEnd();
1641
+ return `${frontmatter}\n\n${drafted}\n`;
1642
+ }
1643
+
1644
+ /**
1645
+ * Render the emitted prd file: the frontmatter (`title`/`slug` + the loop-closure
1646
+ * `issue: N` + the gate axes the prompt JUDGED) followed by the drafted prd body.
1647
+ * For a FANNED prd the `issue: N` lives ONLY on the prd — never duplicated across
1648
+ * the N fanned tasks, which reach it via `task.prd: → prd issue:` (a fanned
1649
+ * task carries `prd:`, NOT its own `issue:`; the lone-task outcome is the only
1650
+ * one that puts `issue:` on a task). The close JOB reaches the prd's number via
1651
+ * `task.prd: → prd issue:`. The gate axes (`humanOnly`/`needsAnswers`) are emitted ONLY when the
1652
+ * verdict declared them `true` — an omitted axis is `undefined` (undeclared), the
1653
+ * same convention `frontmatter.ts` parses. When the agent drafted no body, a thin
1654
+ * default scaffold keeps the file a valid prd that `do prd:` can later task.
1655
+ */
1656
+ export function renderSpec(params: {
1657
+ slug: string;
1658
+ title: string;
1659
+ body: string | undefined;
1660
+ issueNumber: number;
1661
+ humanOnly: boolean | undefined;
1662
+ needsAnswers: boolean | undefined;
1663
+ /**
1664
+ * The origin-trust STAMP (task `untrusted-origin-forces-build-propose`).
1665
+ * Present ⇒ emit `origin: issue` + `originTrust: <value>`, PROPAGATED onto every
1666
+ * emitted task by the tasker so the build transition can read it. UNSET (a
1667
+ * local intake) ⇒ NO stamp (human/trusted).
1668
+ */
1669
+ originTrust?: OriginTrust;
1670
+ }): string {
1671
+ const {slug, title, body, issueNumber, humanOnly, needsAnswers, originTrust} =
1672
+ params;
1673
+ const lines = [
1674
+ '---',
1675
+ `title: ${title}`,
1676
+ `slug: ${slug}`,
1677
+ `issue: ${issueNumber}`,
1678
+ ];
1679
+ // The origin-trust stamp (only when passed IN from the CI shell): the
1680
+ // becomes-code checkpoint that survives the merge boundary. A local intake
1681
+ // leaves it unset ⇒ no stamp ⇒ human/trusted.
1682
+ if (originTrust !== undefined) {
1683
+ lines.push('origin: issue', `originTrust: ${originTrust}`);
1684
+ }
1685
+ // Surface the gate axes AS THE PROMPT JUDGED THEM (prd `issue-intake` US #8). Only emit a `true`
1686
+ // axis — an undeclared axis stays absent (parsed as `undefined`).
1687
+ if (humanOnly === true) {
1688
+ lines.push('humanOnly: true');
1689
+ }
1690
+ if (needsAnswers === true) {
1691
+ lines.push('needsAnswers: true');
1692
+ }
1693
+ lines.push('---');
1694
+ const frontmatter = lines.join('\n');
1695
+ // As with `renderBacklogTask`: the drafted PRD body is wrapped VERBATIM; only
1696
+ // the empty-body DEFAULT SCAFFOLD is sourced from the shared section skeleton
1697
+ // owner (`renderSpecBody` with `solution` + `userStories`, prd
1698
+ // `centralize-buildable-task-renderer-shared-by-intake-and-promotion` US #2),
1699
+ // so intake's PRD fallback and promotion's PRD body cannot drift. `trimEnd()`
1700
+ // drops the renderer's trailing blank line so intake's single trailing `\n`
1701
+ // (owned by the join below) keeps the bytes identical to the pre-rewire literal.
1702
+ const drafted =
1703
+ body && body.trim() !== ''
1704
+ ? body.trim()
1705
+ : renderSpecBody({
1706
+ problemStatement: `Transformed from issue #${issueNumber}: ${title}`,
1707
+ solution: '(to be detailed; this prd needs tasking via `do prd:`).',
1708
+ userStories: `1. As a user, I want issue #${issueNumber} addressed.`,
1709
+ }).trimEnd();
1710
+ return `${frontmatter}\n\n${drafted}\n`;
1711
+ }
1712
+
1713
+ /**
1714
+ * STAGE the intake artifact content into the index on the `work/<slug>` branch (the
1715
+ * {@link performIntegration} lifecycle seam): write the `work/backlog/<slug>.md`
1716
+ * file (runner-owned; the agent never writes git-visible files) and `git add` it.
1717
+ * The band's subsequent `git add -A` + atomic commit folds it into ONE runner-owned
1718
+ * commit.
1719
+ */
1720
+ async function stageIntakeContent(params: {
1721
+ cwd: string;
1722
+ relPath: string;
1723
+ content: string;
1724
+ env: NodeJS.ProcessEnv | undefined;
1725
+ }): Promise<void> {
1726
+ const {cwd, relPath, content, env} = params;
1727
+ const abs = join(cwd, relPath);
1728
+ mkdirSync(dirname(abs), {recursive: true});
1729
+ writeFileSync(abs, content);
1730
+ await gitHard(['add', '--', relPath], cwd, env);
1731
+ }
1732
+
1733
+ /**
1734
+ * ONBOARD the intake write onto a NAMESPACED, INTAKE-PRODUCED branch
1735
+ * (`work/intake-task-<slug>` / `work/intake-prd-<slug>`) cut from the freshly-
1736
+ * fetched `<arbiter>/main` (the SAME discipline `tasking.ts` uses). The
1737
+ * `intake-` PRODUCER prefix keeps this short-lived "create the item" branch
1738
+ * DISTINCT from the later build branch (`work/task-<slug>`) for the same slug
1739
+ * — the firing `intake` × `do task:` collision the observation traced. The
1740
+ * task-emit path passes `'task'`, the prd-emit path `'prd'`. A pre-existing
1741
+ * local branch (a re-run) is force-recreated off fresh main.
1742
+ */
1743
+ async function switchToWorkBranch(
1744
+ cwd: string,
1745
+ arbiter: string,
1746
+ type: SlugNamespace,
1747
+ slug: string,
1748
+ env: NodeJS.ProcessEnv | undefined,
1749
+ ): Promise<void> {
1750
+ const branch = workBranchRef(type, slug, {producer: 'intake'});
1751
+ await gitHard(['fetch', '--quiet', arbiter], cwd, env);
1752
+ await gitHard(
1753
+ ['switch', '--quiet', '-C', branch, `${arbiter}/main`],
1754
+ cwd,
1755
+ env,
1756
+ );
1757
+ }
1758
+
1759
+ /** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
1760
+ async function gitHard(
1761
+ args: string[],
1762
+ cwd: string,
1763
+ env: NodeJS.ProcessEnv | undefined,
1764
+ ): Promise<RunResult> {
1765
+ const result = await runAsync('git', args, cwd, {env});
1766
+ if (result.status !== 0) {
1767
+ throw new Error(
1768
+ `git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`,
1769
+ );
1770
+ }
1771
+ return result;
1772
+ }
1773
+
1774
+ /** Run the decision step. Prefers the injected decider; else the harness seam. */
1775
+ async function runDecision(
1776
+ options: PerformIntakeOptions,
1777
+ cwd: string,
1778
+ issue: Issue,
1779
+ comments: IssueComment[],
1780
+ prompt: string,
1781
+ ): Promise<IntakeVerdict> {
1782
+ if (options.decide) {
1783
+ return options.decide({cwd, issue, comments, prompt, env: options.env});
1784
+ }
1785
+ // PRODUCTION: launch the harness with the decision prd, then PARSE the verdict
1786
+ // the agent emitted out of its ANSWER channel (`launched.output`) — the SAME wire
1787
+ // the review gate runs (launch → `parseReviewVerdict(readOutput(launched.output))`;
1788
+ // `harnessReviewGate`). The agent emits a single fenced ```json block (the OUTPUT
1789
+ // CONTRACT {@link buildIntakeDecisionSpec} appends); {@link parseIntakeVerdict}
1790
+ // extracts + validates it. The model's JUDGEMENT is not unit-tested — only the
1791
+ // parse + dispatch — exactly as the review prompt's judgement is not.
1792
+ const harness = options.harness ?? new NullHarness();
1793
+ const launched = await launchWithOptionalWatch({
1794
+ harness,
1795
+ dir: cwd,
1796
+ slug: `intake-${issue.number}`,
1797
+ command: options.agentCmd ?? '',
1798
+ prompt,
1799
+ model: options.model,
1800
+ sessionId: `intake-${issue.number}`,
1801
+ sessionsDir: options.sessionsDir,
1802
+ env: options.env,
1803
+ });
1804
+ if (!launched.ok) {
1805
+ throw new Error(launched.detail ?? 'the intake decision agent failed.');
1806
+ }
1807
+ // Read the verdict from the agent's ANSWER channel (`output`), NOT `detail` (the
1808
+ // failure channel, empty on success) — the SAME `output ?? ''` normalisation the
1809
+ // review gate's `readOutput` default applies. A malformed/absent verdict throws,
1810
+ // which `decideAndDispatch`'s try/catch already maps onto `agent-failed` (exit 1).
1811
+ return parseIntakeVerdict(launched.output ?? '');
1812
+ }
1813
+
1814
+ /**
1815
+ * Parse the decision agent's emitted VERDICT out of its (possibly prose-wrapped /
1816
+ * fenced) textual output into an {@link IntakeVerdict} — the PRODUCTION wire
1817
+ * between the launched agent and the already-built dispatcher, modeled 1:1 on the
1818
+ * review gate's `parseReviewVerdict` twin (`review-gate.ts`). It pulls the first
1819
+ * JSON object carrying an `"outcome"` field via the SHARED
1820
+ * {@link extractJsonObjectSpan} (NOT a forked second "first JSON object in agent
1821
+ * prose" extractor — the review gates anchor on `"verdict"`, intake on
1822
+ * `"outcome"`; same need, one implementation — coherence), `JSON.parse`s it, and
1823
+ * validates the shape: `outcome ∈ {ask,task,spec,bounce}`.
1824
+ *
1825
+ * The per-outcome fields map 1:1 onto {@link IntakeVerdict} (`task` →
1826
+ * taskSlug?/taskTitle/taskBody, `spec` →
1827
+ * specSlug?/specTitle/specBody/specHumanOnly?/specNeedsAnswers?, `ask` → question,
1828
+ * `bounce` → bounceMessage). Missing OPTIONALS are tolerated — the dispatcher
1829
+ * already has fallbacks (slug-from-title, the thin comment/scaffold defaults).
1830
+ *
1831
+ * THROWS a clear error on: no JSON object present, invalid JSON, or an `outcome`
1832
+ * not in the set. The caller (`decideAndDispatch`) maps any throw onto the
1833
+ * `agent-failed` outcome (exit 1) — a malformed verdict degrades honestly, never
1834
+ * a crash and never a silent dispatch.
1835
+ */
1836
+ export function parseIntakeVerdict(output: string): IntakeVerdict {
1837
+ const span = extractJsonObjectSpan(output, 'outcome');
1838
+ if (span === undefined) {
1839
+ throw new Error(
1840
+ 'intake decision agent produced no parseable {outcome, …} verdict.',
1841
+ );
1842
+ }
1843
+ let parsed: unknown;
1844
+ try {
1845
+ parsed = JSON.parse(output.slice(span.start, span.end));
1846
+ } catch (err) {
1847
+ throw new Error(
1848
+ `intake verdict was not valid JSON: ${(err as Error).message}`,
1849
+ );
1850
+ }
1851
+ if (typeof parsed !== 'object' || parsed === null) {
1852
+ throw new Error('intake verdict was not an object.');
1853
+ }
1854
+ const obj = parsed as Record<string, unknown>;
1855
+ const outcome = obj.outcome;
1856
+ if (
1857
+ outcome !== 'ask' &&
1858
+ outcome !== 'task' &&
1859
+ outcome !== 'spec' &&
1860
+ outcome !== 'bounce'
1861
+ ) {
1862
+ // The prompt teaches the LLM to emit `spec`, so the accepted outcome set is
1863
+ // `ask|task|spec|bounce`. HARD CUTOVER: the legacy `prd` outcome token is
1864
+ // fully gone (rejected here + removed from the `IntakeOutcome` type + the
1865
+ // dispatch `case`).
1866
+ throw new Error(
1867
+ `intake verdict 'outcome' was not one of ask|task|spec|bounce (got ` +
1868
+ `${JSON.stringify(outcome)}).`,
1869
+ );
1870
+ }
1871
+ // Map the per-outcome fields onto the verdict shape, keeping ONLY the strings/
1872
+ // booleans the dispatcher consumes (a missing optional stays absent — the
1873
+ // dispatcher's fallbacks cover it). Every field is optional on the type, so the
1874
+ // `task`/`spec` content + the `ask`/`bounce` text are carried verbatim when present.
1875
+ const str = (v: unknown): string | undefined =>
1876
+ typeof v === 'string' ? v : undefined;
1877
+ const bool = (v: unknown): boolean | undefined =>
1878
+ typeof v === 'boolean' ? v : undefined;
1879
+ return {
1880
+ outcome,
1881
+ ...(str(obj.taskSlug) !== undefined ? {taskSlug: str(obj.taskSlug)} : {}),
1882
+ ...(str(obj.taskTitle) !== undefined
1883
+ ? {taskTitle: str(obj.taskTitle)}
1884
+ : {}),
1885
+ ...(str(obj.taskBody) !== undefined ? {taskBody: str(obj.taskBody)} : {}),
1886
+ ...(str(obj.question) !== undefined ? {question: str(obj.question)} : {}),
1887
+ ...(str(obj.specSlug) !== undefined ? {specSlug: str(obj.specSlug)} : {}),
1888
+ ...(str(obj.specTitle) !== undefined
1889
+ ? {specTitle: str(obj.specTitle)}
1890
+ : {}),
1891
+ ...(str(obj.specBody) !== undefined ? {specBody: str(obj.specBody)} : {}),
1892
+ ...(bool(obj.specHumanOnly) !== undefined
1893
+ ? {specHumanOnly: bool(obj.specHumanOnly)}
1894
+ : {}),
1895
+ ...(bool(obj.specNeedsAnswers) !== undefined
1896
+ ? {specNeedsAnswers: bool(obj.specNeedsAnswers)}
1897
+ : {}),
1898
+ ...(str(obj.bounceMessage) !== undefined
1899
+ ? {bounceMessage: str(obj.bounceMessage)}
1900
+ : {}),
1901
+ };
1902
+ }
1903
+
1904
+ // ---------------------------------------------------------------------------
1905
+ // The LONE-TASK bounded internal review (observation
1906
+ // `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C).
1907
+ //
1908
+ // Give intake's lone-TASK outcome the adversarial refinement the `do prd:` path
1909
+ // already gets — but as a small intake-NATIVE bounded review, NOT by integrating
1910
+ // the tasker loop. This is a NEW prompt + a small loop + an injectable gate seam,
1911
+ // MIRRORING the tasker loop's verdict/output CONVENTIONS (fenced JSON
1912
+ // `{verdict, findings, edit}` parsed via the shared `extractJsonObjectSpan`) WITHOUT
1913
+ // importing or calling `runTaskReviewLoop`. The differences are load-bearing: this
1914
+ // reviews ONE drafted task (N=1 — the SET/graph/overlap lenses are OFF), it never
1915
+ // touches disk pre-convergence (the task has not been emitted yet), and its only
1916
+ // non-converge sink is the EXISTING `asked` outcome (verdict flips TASK→ASK with
1917
+ // the draft + question(s) in the comment body — ruling C).
1918
+ // ---------------------------------------------------------------------------
1919
+
1920
+ /** The HARD-CODED round cap for the lone-task review (ruling A — a literal, no config/flag). */
1921
+ const LONE_TASK_REVIEW_MAX_ROUNDS = 3;
1922
+
1923
+ /**
1924
+ * Backwards-compatible alias for {@link ReviewFinding} (task
1925
+ * `review-protocol-doc-and-shared-machinery`). Existing imports keep compiling;
1926
+ * new code should reach for `ReviewFinding` from `review-verdict.ts`.
1927
+ */
1928
+ export type LoneTaskReviewFinding = ReviewFinding;
1929
+
1930
+ /**
1931
+ * The lone-task review verdict shape is now the UNIFIED {@link ReviewVerdict}.
1932
+ * The lone-task caller consumes the `edit` / `questions` channels of the wide
1933
+ * type; other review callers consume different channels. The SET/graph/overlap
1934
+ * lenses are still N=1-OFF in the PROMPT (intake never spawns the tasker loop's
1935
+ * set-level sinks); the type itself is shared.
1936
+ */
1937
+ export type LoneTaskReviewVerdict = ReviewVerdict;
1938
+
1939
+ /** What the lone-task review gate needs to launch / answer ONE review round. */
1940
+ export interface LoneTaskReviewGateInput {
1941
+ /** The drafted task's slug (the candidate under review). */
1942
+ slug: string;
1943
+ /** The source issue number (the destination check's target behaviour). */
1944
+ issueNumber: number;
1945
+ /** The drafted task's title (the candidate under review). */
1946
+ title: string;
1947
+ /** The drafted task BODY as it stands THIS round (after any prior in-memory edits). */
1948
+ body: string;
1949
+ /** Which review ROUND this is (1-based, 1..{@link LONE_TASK_REVIEW_MAX_ROUNDS}). */
1950
+ round: number;
1951
+ /** The working clone/checkout the review runs in. */
1952
+ cwd: string;
1953
+ /** Environment for the review-agent launch. */
1954
+ env?: NodeJS.ProcessEnv;
1955
+ }
1956
+
1957
+ /**
1958
+ * The lone-task review SEAM: run ONE adversarial review round on the SINGLE
1959
+ * drafted task and return a parsed verdict (incl. an optional in-memory edit).
1960
+ * Tests inject a canned verdict (no model/network) — the new testable seam, mirroring
1961
+ * {@link IntakeDecider}. Production uses {@link harnessLoneTaskReviewGate}.
1962
+ */
1963
+ export type LoneTaskReviewGate = (
1964
+ input: LoneTaskReviewGateInput,
1965
+ ) => Promise<LoneTaskReviewVerdict>;
1966
+
1967
+ /** The terminal disposition of the bounded lone-task review. */
1968
+ interface LoneTaskReviewResult {
1969
+ /** `converge` → emit the (edited) task; `non-converge` → flip TASK→ASK. */
1970
+ outcome: 'converge' | 'non-converge';
1971
+ /** The task TITLE after the review (unchanged today; carried for symmetry). */
1972
+ title: string;
1973
+ /** The task BODY after all applied in-memory edits (the body to emit / carry). */
1974
+ body: string | undefined;
1975
+ /** On `non-converge`: the open question(s) to carry into the ASK comment body. */
1976
+ questions: string[];
1977
+ /** How many review rounds ran. */
1978
+ passes: number;
1979
+ }
1980
+
1981
+ /**
1982
+ * Run the BOUNDED lone-task adversarial self-review over the SINGLE drafted task.
1983
+ * Each round runs the gate (the `review` skill's per-task + destination lenses on
1984
+ * the ONE task); a round may propose an EDIT (full replacement body) applied IN
1985
+ * MEMORY and re-reviewed. The cap is the HARD-CODED literal
1986
+ * {@link LONE_TASK_REVIEW_MAX_ROUNDS} = 3 (ruling A — no config/flag).
1987
+ *
1988
+ * - CONVERGE — a round returns `approve` with no NEW blocking issue → emit the
1989
+ * improved task (the caller's existing write/integrate + completion comment).
1990
+ * - NON-CONVERGE — a round `block`s with an open question (no clear thread answer)
1991
+ * OR the cap is hit with an unresolved blocker → flip TASK→ASK carrying the
1992
+ * draft + the open question(s) (ruling C).
1993
+ *
1994
+ * A gate launch/parse failure THROWS (the caller's try/catch maps it onto
1995
+ * `agent-failed`) — never a silent emit of the un-reviewed task.
1996
+ */
1997
+ async function runLoneTaskReview(params: {
1998
+ slug: string;
1999
+ issueNumber: number;
2000
+ draftTitle: string;
2001
+ draftBody: string | undefined;
2002
+ gate: LoneTaskReviewGate;
2003
+ cwd: string;
2004
+ env: NodeJS.ProcessEnv | undefined;
2005
+ note: (message: string) => void;
2006
+ }): Promise<LoneTaskReviewResult> {
2007
+ const {slug, issueNumber, draftTitle, draftBody, gate, cwd, env, note} =
2008
+ params;
2009
+ let body = draftBody;
2010
+ let lastVerdict: LoneTaskReviewVerdict = {verdict: 'block', findings: []};
2011
+ let passes = 0;
2012
+ for (let round = 1; round <= LONE_TASK_REVIEW_MAX_ROUNDS; round++) {
2013
+ const verdict = await gate({
2014
+ slug,
2015
+ issueNumber,
2016
+ title: draftTitle,
2017
+ // The body the reviewer sees this round (after any prior in-memory edit);
2018
+ // fall back to the rendered scaffold-input the emit path also tolerates.
2019
+ body: body ?? '',
2020
+ round,
2021
+ cwd,
2022
+ env,
2023
+ });
2024
+ passes = round;
2025
+ lastVerdict = verdict;
2026
+ // APPLY the proposed EDIT IN MEMORY (no `work/backlog/` write pre-convergence).
2027
+ if (verdict.edit !== undefined && verdict.edit.trim() !== '') {
2028
+ body = verdict.edit;
2029
+ }
2030
+ if (verdict.verdict === 'approve') {
2031
+ return {
2032
+ outcome: 'converge',
2033
+ title: draftTitle,
2034
+ body,
2035
+ questions: [],
2036
+ passes,
2037
+ };
2038
+ }
2039
+ // EARLY FLIP → ASK (the non-converge trigger the source observation names
2040
+ // FIRST: "a blocking question with NO clear answer in the issue thread"). When a
2041
+ // round BLOCKS, carries open `questions`, and proposes NO `edit`, the agent is
2042
+ // saying "this needs the HUMAN, I have nothing left to tighten" — so flip to ASK
2043
+ // NOW rather than burning the remaining rounds (which cannot resolve a question
2044
+ // only the human can answer). Symmetric with the early CONVERGE return above; it
2045
+ // retires the "flips only at the cap" behaviour (PR #62 review nit #1). A `block`
2046
+ // that DID propose an `edit` is still iterated — the edit may converge it; only a
2047
+ // no-edit blocking question short-circuits.
2048
+ const hasEdit = verdict.edit !== undefined && verdict.edit.trim() !== '';
2049
+ const earlyQuestions = loneTaskBlockingQuestions(verdict);
2050
+ if (!hasEdit && earlyQuestions.length > 0) {
2051
+ note(
2052
+ `Intake lone-task review round ${round}/${LONE_TASK_REVIEW_MAX_ROUNDS} ` +
2053
+ `surfaced a blocking question with no edit to apply; flipping TASK→ASK ` +
2054
+ `early (no clear thread answer — the human must decide).`,
2055
+ );
2056
+ return {
2057
+ outcome: 'non-converge',
2058
+ title: draftTitle,
2059
+ body,
2060
+ questions: earlyQuestions,
2061
+ passes,
2062
+ };
2063
+ }
2064
+ note(
2065
+ `Intake lone-task review round ${round}/${LONE_TASK_REVIEW_MAX_ROUNDS} ` +
2066
+ `found ${loneTaskBlockingCount(verdict)} blocking issue(s)` +
2067
+ `${hasEdit ? ' (an edit was applied)' : ''}.`,
2068
+ );
2069
+ }
2070
+ // The cap was hit with a still-`block` verdict → NON-CONVERGE (flip TASK→ASK).
2071
+ return {
2072
+ outcome: 'non-converge',
2073
+ title: draftTitle,
2074
+ body,
2075
+ questions: loneTaskBlockingQuestions(lastVerdict),
2076
+ passes,
2077
+ };
2078
+ }
2079
+
2080
+ /** Count blocking findings in a lone-task review verdict. */
2081
+ function loneTaskBlockingCount(verdict: LoneTaskReviewVerdict): number {
2082
+ return verdict.findings.filter((f) => f.severity === 'blocking').length;
2083
+ }
2084
+
2085
+ /**
2086
+ * The open question(s) for the ASK comment body on a non-converge: prefer the
2087
+ * verdict's explicit `questions`, else fall back to the blocking findings'
2088
+ * questions (so the human always gets a concrete question, never a blank ask).
2089
+ */
2090
+ function loneTaskBlockingQuestions(verdict: LoneTaskReviewVerdict): string[] {
2091
+ if (verdict.questions && verdict.questions.length > 0) {
2092
+ return verdict.questions;
2093
+ }
2094
+ const blocking = verdict.findings.filter((f) => f.severity === 'blocking');
2095
+ const source = blocking.length > 0 ? blocking : verdict.findings;
2096
+ return source.map((f) =>
2097
+ f.context ? `${f.question} (${f.context})` : f.question,
2098
+ );
2099
+ }
2100
+
2101
+ /**
2102
+ * Compose the NON-CONVERGE ASK comment BODY (ruling C): it carries BOTH the proposed
2103
+ * task DRAFT and the open question(s) that arose, so the human reacts to a concrete
2104
+ * draft ("yes, yes, but…"), strictly richer than a blank-question ask. The draft
2105
+ * rides in the comment BODY — NOT a new marker kind; {@link dispatchComment} stamps
2106
+ * the EXISTING `kind=ask` marker around it.
2107
+ */
2108
+ function composeLoneTaskAskComment(params: {
2109
+ issueNumber: number;
2110
+ slug: string;
2111
+ draftTitle: string;
2112
+ draftBody: string | undefined;
2113
+ questions: string[];
2114
+ }): string {
2115
+ const {issueNumber, slug, draftTitle, draftBody, questions} = params;
2116
+ const draft = renderBacklogTask({
2117
+ slug,
2118
+ title: draftTitle,
2119
+ body: draftBody,
2120
+ issueNumber,
2121
+ });
2122
+ const questionLines =
2123
+ questions.length > 0
2124
+ ? questions.map((q) => `- ${q}`).join('\n')
2125
+ : '- (the draft below needs a clarification before it can be built)';
2126
+ return [
2127
+ `I drafted a task for issue #${issueNumber} but the internal review surfaced`,
2128
+ `open question(s) it could not resolve from the thread. Please weigh in on the`,
2129
+ `draft below — once the question(s) are answered, a later run can emit it.`,
2130
+ '',
2131
+ '## Open question(s)',
2132
+ '',
2133
+ questionLines,
2134
+ '',
2135
+ '## Proposed task draft',
2136
+ '',
2137
+ '```markdown',
2138
+ draft.trimEnd(),
2139
+ '```',
2140
+ ].join('\n');
2141
+ }
2142
+
2143
+ /**
2144
+ * Resolve the lone-task review GATE from the intake options: the injected
2145
+ * {@link PerformIntakeOptions.reviewTask} (tests' canned seam) when present, else
2146
+ * the production harness-backed gate ({@link harnessLoneTaskReviewGate}) wired to
2147
+ * the same harness/agent the decision step uses. Mirrors how {@link runDecision}
2148
+ * prefers the injected `decide`.
2149
+ */
2150
+ function resolveLoneTaskReviewGate(
2151
+ options: PerformIntakeOptions,
2152
+ ): LoneTaskReviewGate {
2153
+ if (options.reviewTask) {
2154
+ return options.reviewTask;
2155
+ }
2156
+ return harnessLoneTaskReviewGate({
2157
+ harness: options.harness,
2158
+ agentCmd: options.agentCmd,
2159
+ model: options.model,
2160
+ sessionsDir: options.sessionsDir,
2161
+ });
2162
+ }
2163
+
2164
+ /** Options for the production harness-backed lone-task review gate. */
2165
+ export interface HarnessLoneTaskReviewGateOptions {
2166
+ /** The harness seam used to launch the fresh-context review agent. */
2167
+ harness?: Harness;
2168
+ /** The configured agent command the harness shells out to. */
2169
+ agentCmd?: string;
2170
+ /** The model routing intent forwarded to the harness (ADR §13). */
2171
+ model?: string;
2172
+ /** The HOST-ONLY sessions root for the review session file. */
2173
+ sessionsDir?: string;
2174
+ }
2175
+
2176
+ /**
2177
+ * The PRODUCTION lone-task review gate: launch the `review` SKILL as an agent
2178
+ * through the EXISTING harness seam (the SAME wire {@link runDecision} uses), then
2179
+ * PARSE the emitted `{verdict, findings, edit, questions}` via
2180
+ * {@link parseLoneTaskReviewVerdict}. The agent makes the review JUDGEMENT (the
2181
+ * per-task + destination lenses on the ONE task); this gate launches it and parses
2182
+ * its verdict. A launch failure THROWS (the dispatcher's try/catch maps it onto
2183
+ * `agent-failed`). MIRRORS {@link harnessTaskReviewGate} WITHOUT importing it.
2184
+ */
2185
+ export function harnessLoneTaskReviewGate(
2186
+ options: HarnessLoneTaskReviewGateOptions = {},
2187
+ ): LoneTaskReviewGate {
2188
+ const harness = options.harness ?? new NullHarness();
2189
+ return async (
2190
+ input: LoneTaskReviewGateInput,
2191
+ ): Promise<LoneTaskReviewVerdict> => {
2192
+ const launched = await launchWithOptionalWatch({
2193
+ harness,
2194
+ dir: input.cwd,
2195
+ slug: `intake-task-review-${input.slug}`,
2196
+ command: options.agentCmd ?? '',
2197
+ prompt: buildLoneTaskReviewPrompt(input),
2198
+ model: options.model,
2199
+ // A DISTINCT session id per round so launches never collide.
2200
+ sessionId: `intake-task-review-${input.slug}-r${input.round}`,
2201
+ sessionsDir: options.sessionsDir,
2202
+ env: input.env,
2203
+ });
2204
+ if (!launched.ok) {
2205
+ throw new Error(
2206
+ `intake lone-task review agent launch failed${
2207
+ launched.detail ? `: ${launched.detail}` : ''
2208
+ }`,
2209
+ );
2210
+ }
2211
+ return parseReviewVerdict(launched.output ?? '');
2212
+ };
2213
+ }
2214
+
2215
+ /**
2216
+ * Build the LONE-TASK review PROMPT: instruct a fresh-context agent to apply
2217
+ * the **review discipline** (`work/protocol/REVIEW-PROTOCOL.md`) to the SINGLE
2218
+ * drafted task — per-task well-formedness + the destination check ("if this
2219
+ * task is built exactly as written, do we end up with the behaviour issue #N
2220
+ * asks for?"). The SET / graph / overlap lenses are N=1 and EXPLICITLY OFF.
2221
+ * A round may propose an `edit` (the FULL replacement task body) the runner
2222
+ * applies IN MEMORY and re-reviews; converge when a round finds NO new blocking
2223
+ * issue, else carry the open `questions` into the ASK comment for the human.
2224
+ *
2225
+ * The discipline body and the JSON-emitted-shape contract are SHARED helpers
2226
+ * (task `review-protocol-doc-and-shared-machinery`); this builder owns ONLY
2227
+ * the lone-task-specific framing.
2228
+ */
2229
+ export function buildLoneTaskReviewPrompt(
2230
+ input: LoneTaskReviewGateInput,
2231
+ ): string {
2232
+ return [
2233
+ `You are a FRESH-CONTEXT reviewer in intake's BOUNDED lone-task review. A`,
2234
+ `single task has just been drafted from GitHub issue #${input.issueNumber}.`,
2235
+ `Review THIS ONE task adversarially (round ${input.round} of at most ${LONE_TASK_REVIEW_MAX_ROUNDS}).`,
2236
+ '',
2237
+ reviewDisciplinePrompt(),
2238
+ '',
2239
+ `Drafted task slug: ${input.slug}`,
2240
+ `Drafted task title: ${input.title}`,
2241
+ '',
2242
+ 'Drafted task body (the markdown AFTER the frontmatter):',
2243
+ '```markdown',
2244
+ input.body.trim() === ''
2245
+ ? '(empty — only a scaffold was drafted)'
2246
+ : input.body,
2247
+ '```',
2248
+ '',
2249
+ '## Which lenses apply (N=1 — this is ONE task, not a SET)',
2250
+ '',
2251
+ 'Apply ONLY the per-task lenses, ENDING in the destination check:',
2252
+ '- **Per-task well-formedness** — is it a single tracer-bullet vertical task',
2253
+ ' (one thin end-to-end path)? Are the `## What to build`, `## Acceptance',
2254
+ ' criteria`, and `## Prompt` present, concrete, and self-contained (an AFK',
2255
+ ' agent could start from the file alone)? Are claims/paths/“reuse X” real?',
2256
+ '- **The DESTINATION check** — if this task is built EXACTLY as written, do we',
2257
+ ` end up with the behaviour issue #${input.issueNumber} asks for? A hole here is`,
2258
+ ' the highest-value thing to flag.',
2259
+ '',
2260
+ 'The SET / graph / overlap / goal-COMPOSITION lenses are OFF: there is only ONE',
2261
+ 'task (N=1), so there is no dependency graph, no set-level gap, and no',
2262
+ 'duplicate/overlap to assess. Do NOT invent a decomposition.',
2263
+ '',
2264
+ '## How to iterate',
2265
+ '',
2266
+ 'You do NOT edit files or run git — you EMIT a verdict and the runner applies it',
2267
+ 'in memory, then re-reviews. If a finding can be FIXED by tightening the draft,',
2268
+ 'propose an `edit` (the FULL replacement task body — the markdown AFTER the',
2269
+ 'frontmatter; the runner writes the frontmatter + the issue link). CONVERGE',
2270
+ '(`approve`, no blocking findings) when a round finds NO new blocking issue.',
2271
+ 'When a BLOCKING question has NO clear answer in the issue thread — it needs the',
2272
+ 'human, not another edit — `block` and put it in `questions`: the runner asks the',
2273
+ 'human, carrying this draft. Flag, do not guess.',
2274
+ '',
2275
+ verdictContractPrompt(),
2276
+ '',
2277
+ 'Fill the channels appropriate to THIS caller (the lone-task review):',
2278
+ ' - `edit` — a single full-replacement task body (NOT a path; the task is',
2279
+ ' not yet emitted) when tightening the draft fixes the finding.',
2280
+ ' - `questions` — the open question(s) for the human when a blocking issue',
2281
+ ' has no clear thread answer.',
2282
+ 'Do NOT fill `review` / `edits` / `uncertainTasks` / `decompositionUnclear`',
2283
+ "— those are other callers' channels.",
2284
+ ].join('\n');
2285
+ }
2286
+
2287
+ /**
2288
+ * Backwards-compatible alias for the unified {@link parseReviewVerdict}
2289
+ * (task `review-protocol-doc-and-shared-machinery`). The lone-task review
2290
+ * verdict is now the unified {@link ReviewVerdict}; the alias keeps existing
2291
+ * tests/callers compiling.
2292
+ */
2293
+ export const parseLoneTaskReviewVerdict = parseReviewVerdict;
2294
+
2295
+ /**
2296
+ * Build the intake decision PRD (an inline prompt builder, like `buildTaskingPrd`
2297
+ * in `tasking.ts` / the reviewer prompts in `review-gate.ts` — NOT a standalone
2298
+ * asset/`.md` file; no such convention exists in this package). It encodes the FULL
2299
+ * four-outcome decision table (prd `issue-intake` — the source of truth) and the
2300
+ * three DECISION AIDS stated once there:
2301
+ *
2302
+ * 1. the **"clear?" bar** = `to-task`/`needsAnswers`' "would I build the wrong
2303
+ * thing if I guessed?" — if a material requirement/scope/acceptance question is
2304
+ * unanswered, ASK (never guess a spec from a vague issue);
2305
+ * 2. the **"one task?" bar** = `to-task`' tracer-bullet test (one thin end-to-end
2306
+ * path, demoable on its own) — fits → TASK, needs splitting → PRD;
2307
+ * 3. **PRD vs BOUNCE** turns on a **SHARED VISION**: coupled (even if small) → PRD;
2308
+ * genuinely unrelated → BOUNCE. Size NEVER forces a bounce — only unrelatedness
2309
+ * (the over-bounce guard: a coupled-but-small pair gets a light PRD, never a
2310
+ * bounce).
2311
+ *
2312
+ * The prompt anchors to `to-task`/`to-spec` for the task/spec SHAPES it drafts. Its
2313
+ * JUDGEMENT is NOT unit-tested (exactly as the review prompt's is not) — only the
2314
+ * dispatch is. The agent only DRAFTS the verdict + its content; it does NO git/seam
2315
+ * ops (the runner owns every postComment / write / integrate — the in-band boundary).
2316
+ */
2317
+ export function buildIntakeDecisionSpec(
2318
+ issue: Issue,
2319
+ comments: IssueComment[],
2320
+ triage?: IntakeTriageDecision,
2321
+ ): string {
2322
+ const thread =
2323
+ comments.length === 0
2324
+ ? '(no comments yet)'
2325
+ : comments
2326
+ .map(
2327
+ (c, i) =>
2328
+ `#${i + 1} ${c.author ? `@${c.author}` : '(unknown)'}: ${c.body}`,
2329
+ )
2330
+ .join('\n\n');
2331
+ // TRIAGE ENRICHMENT (prd `issue-intake`): on the raced PROCEED path the prompt is
2332
+ // told which comment(s) PRE-DATE intake's last turn (context for a prior state,
2333
+ // not necessarily a fresh answer) and — only then — how many previously-SEEN
2334
+ // comments were DELETED (a flag + count; the bodies are gone, so do not name them).
2335
+ const triageNotes: string[] = [];
2336
+ if (triage?.action === 'proceed' && triage.predatingIds.length > 0) {
2337
+ triageNotes.push(
2338
+ '',
2339
+ '## Triage note — raced comment(s) that PRE-DATE intake’s last turn',
2340
+ '',
2341
+ `${triage.predatingIds.length} comment(s) landed AFTER intake last read the`,
2342
+ 'thread but BEFORE it posted its last turn, so they pre-date that turn',
2343
+ '(possibly concurrent). Treat them as possibly-already-addressed context for a',
2344
+ 'PRIOR state — NOT necessarily a direct answer to intake’s latest question.',
2345
+ );
2346
+ if (triage.deletedSeenCount > 0) {
2347
+ triageNotes.push(
2348
+ '',
2349
+ `ALSO: ${triage.deletedSeenCount} previously-seen comment(s) were DELETED since`,
2350
+ 'intake last read the thread. Their content is gone and not recoverable; do',
2351
+ 'NOT assume your prior reasoning’s premises still hold — reassess from the',
2352
+ 'current thread.',
2353
+ );
2354
+ }
2355
+ }
2356
+ return [
2357
+ `You are the dorfl INTAKE agent. Decide what to do with GitHub issue`,
2358
+ `#${issue.number}: "${issue.title}". You read the issue + its full comment`,
2359
+ `thread and return ONE verdict (the runner DISPATCHES on it deterministically).`,
2360
+ '',
2361
+ 'Issue body:',
2362
+ issue.body.trim() === '' ? '(empty)' : issue.body,
2363
+ '',
2364
+ 'Comment thread (oldest first):',
2365
+ thread,
2366
+ ...triageNotes,
2367
+ '',
2368
+ '## The decision — classify the issue into exactly ONE of four verdicts',
2369
+ '',
2370
+ '- **ASK** — the issue is NOT clear enough to act on: a material requirement,',
2371
+ ' scope, or acceptance question is unanswered. Use the same bar `to-task`',
2372
+ ' uses for `needsAnswers`: "would I build the WRONG thing if I guessed now?"',
2373
+ ' If yes → ASK. Draft the SINGLE next clarifying question (do NOT guess a spec',
2374
+ ' from a vague issue). The runner posts it and stops; a later run resumes from',
2375
+ ' the updated thread.',
2376
+ '',
2377
+ '- **TASK** — the issue is CLEAR *and* it fits ONE tracer-bullet vertical task',
2378
+ ' (a single thin end-to-end path, demoable on its own — `to-task`’ criterion).',
2379
+ ' Draft that ONE task in the `to-task` shape (a `## What to build`,',
2380
+ ' `## Acceptance criteria`, and `## Prompt`). The runner writes',
2381
+ ' `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the',
2382
+ ' lone-task closure link, NOT `Fixes #N`) and integrates it.',
2383
+ '',
2384
+ '- **PRD** — the issue is CLEAR *and* coherent but needs MORE THAN ONE task (it',
2385
+ ' cannot be one tracer-bullet path — it splits for scope/architecture). >1 task',
2386
+ ' ⟺ a shared vision worth recording ⟺ a spec. Draft a spec in the `to-spec` shape',
2387
+ ' (`## Problem Statement`, `## Solution`, `## User Stories`, `## Out of Scope`).',
2388
+ ' The runner writes the prd file (`work/prds/ready/<slug>.md`) with `issue: N` and integrates it;',
2389
+ ' TASKING the prd is a SEPARATE later step (`do prd:`) — do not task it here.',
2390
+ ' **INCLUDES a coupled-but-SMALL pair: if two asks share a vision they get a',
2391
+ ' (light) prd — they are NEVER bounced.**',
2392
+ '',
2393
+ '- **BOUNCE** — the issue is really MULTIPLE UNRELATED concerns wearing one issue:',
2394
+ ' you cannot articulate a SINGLE shared vision tying them together. Draft a short',
2395
+ ' message asking the author to file separate issues. A bounce is TERMINAL, so the',
2396
+ ' runner CLOSES the issue ATOMICALLY — your message as the closing comment +',
2397
+ ' reason "not planned" (the honest signal that the asks must be re-filed). Intake',
2398
+ ' closes on BOUNCE only; never on task/prd (CI’s close-job) / ask.',
2399
+ '',
2400
+ '## The three decision aids (apply them in order)',
2401
+ '',
2402
+ '1. **"clear?"** (ASK vs the rest): the `needsAnswers` bar — would acting now risk',
2403
+ ' building the wrong thing? If yes → ASK. Otherwise it is clear; continue.',
2404
+ '2. **"one task?"** (TASK vs PRD): the `to-task` tracer-bullet test — one thin',
2405
+ ' end-to-end path, demoable alone? Fits → TASK; needs splitting → PRD.',
2406
+ '3. **"shared vision?"** (PRD vs BOUNCE): coupled (even if small) → PRD; genuinely',
2407
+ ' unrelated → BOUNCE. SIZE NEVER forces a bounce — only UNRELATEDNESS does. Do',
2408
+ ' not over-bounce a small coupled pair: it is a light prd.',
2409
+ '',
2410
+ '## Boundary',
2411
+ '',
2412
+ 'You only DRAFT the verdict + its content (the task/prd body, or the comment',
2413
+ 'text). You do NOT perform ANY git operation and you do NOT post any comment — the',
2414
+ 'runner owns every git/seam side-effect (write, integrate, postComment). For a prd',
2415
+ 'verdict, also judge its gate axes (humanOnly / needsAnswers) so the runner can',
2416
+ 'surface them on the emitted prd.',
2417
+ '',
2418
+ '## Output — hand the verdict back as ONE fenced JSON block',
2419
+ '',
2420
+ 'Emit your verdict as a SINGLE fenced ```json block (and nothing else that looks',
2421
+ 'like JSON). Its keys map 1:1 onto the verdict the runner dispatches on — always an',
2422
+ '`"outcome"` plus ONLY the fields for that outcome:',
2423
+ '',
2424
+ '```json',
2425
+ '{"outcome": "task", "taskSlug": "<content-derived-slug>", "taskTitle": "<title>", "taskBody": "<the markdown AFTER the frontmatter>"}',
2426
+ '```',
2427
+ '',
2428
+ '- **task** → `taskTitle` + `taskBody` (the `## What to build` / `## Acceptance',
2429
+ ' criteria` / `## Prompt` markdown — NOT the frontmatter; the runner writes the',
2430
+ ' frontmatter + the `issue: N` link) and an optional `taskSlug` (the runner',
2431
+ ' derives one from the title if you omit it — never a counter).',
2432
+ '- **spec** → `specTitle` + `specBody` (the `## Problem Statement` / `## Solution` / …',
2433
+ ' markdown AFTER the frontmatter; the runner writes the frontmatter + `issue: N`),',
2434
+ ' an optional `specSlug`, and the gate axes `specHumanOnly` / `specNeedsAnswers`',
2435
+ ' (booleans — set `true` when a human should drive the TASKING and/or open',
2436
+ ' questions remain; omit otherwise).',
2437
+ '- **ask** → `question` (the single next clarifying question).',
2438
+ '- **bounce** → `bounceMessage` (the “file separate issues” message).',
2439
+ '',
2440
+ '`outcome` MUST be exactly one of `ask` | `task` | `spec` | `bounce`. Strings are',
2441
+ 'plain text inside the JSON (escape newlines as \\n). Do not wrap the JSON in any',
2442
+ 'other structure — the runner pulls the first `{"outcome": …}` object out and',
2443
+ 'dispatches on it.',
2444
+ ].join('\n');
2445
+ }