@cleocode/core 2026.9.8 → 2026.9.9

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 (561) hide show
  1. package/dist/audit/reconstruct.d.ts +13 -43
  2. package/dist/audit/reconstruct.d.ts.map +1 -1
  3. package/dist/audit/reconstruct.js +295 -290
  4. package/dist/audit/reconstruct.js.map +1 -1
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +8 -2
  7. package/dist/bootstrap.js.map +1 -1
  8. package/dist/dispatch/mutate-projection.d.ts.map +1 -1
  9. package/dist/dispatch/mutate-projection.js +12 -0
  10. package/dist/dispatch/mutate-projection.js.map +1 -1
  11. package/dist/dispatch/mvi-projection.d.ts +29 -6
  12. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  13. package/dist/dispatch/mvi-projection.js +109 -139
  14. package/dist/dispatch/mvi-projection.js.map +1 -1
  15. package/dist/docs/docs-read-model.d.ts +3 -2
  16. package/dist/docs/docs-read-model.d.ts.map +1 -1
  17. package/dist/docs/docs-read-model.js +4 -3
  18. package/dist/docs/docs-read-model.js.map +1 -1
  19. package/dist/docs/export-document.js +5100 -2297
  20. package/dist/docs/export-document.js.map +3 -3
  21. package/dist/docs/projection.d.ts +67 -0
  22. package/dist/docs/projection.d.ts.map +1 -0
  23. package/dist/docs/projection.js +370 -0
  24. package/dist/docs/projection.js.map +1 -0
  25. package/dist/doctor/acceptance-drift.d.ts +6 -2
  26. package/dist/doctor/acceptance-drift.d.ts.map +1 -1
  27. package/dist/doctor/acceptance-drift.js +43 -5
  28. package/dist/doctor/acceptance-drift.js.map +1 -1
  29. package/dist/doctor/knowledge-summary.d.ts +41 -0
  30. package/dist/doctor/knowledge-summary.d.ts.map +1 -0
  31. package/dist/doctor/knowledge-summary.js +81 -0
  32. package/dist/doctor/knowledge-summary.js.map +1 -0
  33. package/dist/doctor/knowledge.d.ts +192 -0
  34. package/dist/doctor/knowledge.d.ts.map +1 -0
  35. package/dist/doctor/knowledge.js +1864 -0
  36. package/dist/doctor/knowledge.js.map +1 -0
  37. package/dist/git/work-tree.d.ts +64 -0
  38. package/dist/git/work-tree.d.ts.map +1 -0
  39. package/dist/git/work-tree.js +111 -0
  40. package/dist/git/work-tree.js.map +1 -0
  41. package/dist/hooks/handlers/task-hooks.d.ts +8 -5
  42. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  43. package/dist/hooks/handlers/task-hooks.js +15 -60
  44. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  45. package/dist/identity/cleo-identity.d.ts +13 -0
  46. package/dist/identity/cleo-identity.d.ts.map +1 -1
  47. package/dist/identity/cleo-identity.js +20 -6
  48. package/dist/identity/cleo-identity.js.map +1 -1
  49. package/dist/injection.d.ts.map +1 -1
  50. package/dist/injection.js +29 -13
  51. package/dist/injection.js.map +1 -1
  52. package/dist/internal.d.ts +2 -0
  53. package/dist/internal.d.ts.map +1 -1
  54. package/dist/internal.js +2 -0
  55. package/dist/internal.js.map +1 -1
  56. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  57. package/dist/lifecycle/verification/satisfies-validator.js +124 -120
  58. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  59. package/dist/llm/plugin-facade.js +4018 -1726
  60. package/dist/llm/plugin-facade.js.map +3 -3
  61. package/dist/memory/brain-backfill.d.ts +27 -12
  62. package/dist/memory/brain-backfill.d.ts.map +1 -1
  63. package/dist/memory/brain-backfill.js +325 -134
  64. package/dist/memory/brain-backfill.js.map +1 -1
  65. package/dist/memory/brain-doctor.d.ts +21 -0
  66. package/dist/memory/brain-doctor.d.ts.map +1 -1
  67. package/dist/memory/brain-doctor.js +111 -53
  68. package/dist/memory/brain-doctor.js.map +1 -1
  69. package/dist/memory/brain-links.d.ts.map +1 -1
  70. package/dist/memory/brain-links.js +31 -4
  71. package/dist/memory/brain-links.js.map +1 -1
  72. package/dist/memory/brain-reasoning.d.ts.map +1 -1
  73. package/dist/memory/brain-reasoning.js +28 -2
  74. package/dist/memory/brain-reasoning.js.map +1 -1
  75. package/dist/memory/brain-reconciler.d.ts +14 -19
  76. package/dist/memory/brain-reconciler.d.ts.map +1 -1
  77. package/dist/memory/brain-reconciler.js +70 -97
  78. package/dist/memory/brain-reconciler.js.map +1 -1
  79. package/dist/memory/brain-search.d.ts +2 -0
  80. package/dist/memory/brain-search.d.ts.map +1 -1
  81. package/dist/memory/brain-search.js +59 -29
  82. package/dist/memory/brain-search.js.map +1 -1
  83. package/dist/memory/brain-similarity.d.ts.map +1 -1
  84. package/dist/memory/brain-similarity.js +5 -4
  85. package/dist/memory/brain-similarity.js.map +1 -1
  86. package/dist/memory/brain-stub-prune.d.ts +31 -9
  87. package/dist/memory/brain-stub-prune.d.ts.map +1 -1
  88. package/dist/memory/brain-stub-prune.js +65 -18
  89. package/dist/memory/brain-stub-prune.js.map +1 -1
  90. package/dist/memory/brain-writer-handlers.d.ts +10 -1
  91. package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
  92. package/dist/memory/brain-writer-handlers.js +23 -3
  93. package/dist/memory/brain-writer-handlers.js.map +1 -1
  94. package/dist/memory/brain-writer-thread.d.ts +11 -1
  95. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  96. package/dist/memory/brain-writer-thread.js +87 -20
  97. package/dist/memory/brain-writer-thread.js.map +1 -1
  98. package/dist/memory/brain-writer-worker.js +8 -1
  99. package/dist/memory/brain-writer-worker.js.map +1 -1
  100. package/dist/memory/decision-cross-link.d.ts +39 -0
  101. package/dist/memory/decision-cross-link.d.ts.map +1 -1
  102. package/dist/memory/decision-cross-link.js +316 -0
  103. package/dist/memory/decision-cross-link.js.map +1 -1
  104. package/dist/memory/decisions.d.ts +4 -1
  105. package/dist/memory/decisions.d.ts.map +1 -1
  106. package/dist/memory/decisions.js +7 -24
  107. package/dist/memory/decisions.js.map +1 -1
  108. package/dist/memory/dispatch-trace.d.ts +12 -5
  109. package/dist/memory/dispatch-trace.d.ts.map +1 -1
  110. package/dist/memory/dispatch-trace.js +17 -7
  111. package/dist/memory/dispatch-trace.js.map +1 -1
  112. package/dist/memory/eligibility.d.ts +51 -0
  113. package/dist/memory/eligibility.d.ts.map +1 -0
  114. package/dist/memory/eligibility.js +77 -0
  115. package/dist/memory/eligibility.js.map +1 -0
  116. package/dist/memory/engine-compat.d.ts +2 -0
  117. package/dist/memory/engine-compat.d.ts.map +1 -1
  118. package/dist/memory/engine-compat.js +10 -0
  119. package/dist/memory/engine-compat.js.map +1 -1
  120. package/dist/memory/extraction-gate.d.ts +21 -7
  121. package/dist/memory/extraction-gate.d.ts.map +1 -1
  122. package/dist/memory/extraction-gate.js +178 -216
  123. package/dist/memory/extraction-gate.js.map +1 -1
  124. package/dist/memory/graph-auto-populate.d.ts +38 -5
  125. package/dist/memory/graph-auto-populate.d.ts.map +1 -1
  126. package/dist/memory/graph-auto-populate.js +182 -60
  127. package/dist/memory/graph-auto-populate.js.map +1 -1
  128. package/dist/memory/graph-memory-bridge.d.ts +14 -1
  129. package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
  130. package/dist/memory/graph-memory-bridge.js +66 -28
  131. package/dist/memory/graph-memory-bridge.js.map +1 -1
  132. package/dist/memory/index.d.ts +1 -19
  133. package/dist/memory/index.d.ts.map +1 -1
  134. package/dist/memory/index.js +1 -50
  135. package/dist/memory/index.js.map +1 -1
  136. package/dist/memory/learnings.d.ts.map +1 -1
  137. package/dist/memory/learnings.js +2 -23
  138. package/dist/memory/learnings.js.map +1 -1
  139. package/dist/memory/manifest-filter.d.ts +21 -0
  140. package/dist/memory/manifest-filter.d.ts.map +1 -0
  141. package/dist/memory/manifest-filter.js +51 -0
  142. package/dist/memory/manifest-filter.js.map +1 -0
  143. package/dist/memory/manifest-ingestion.d.ts +28 -30
  144. package/dist/memory/manifest-ingestion.d.ts.map +1 -1
  145. package/dist/memory/manifest-ingestion.js +104 -235
  146. package/dist/memory/manifest-ingestion.js.map +1 -1
  147. package/dist/memory/observer-reflector.d.ts +2 -0
  148. package/dist/memory/observer-reflector.d.ts.map +1 -1
  149. package/dist/memory/observer-reflector.js +6 -49
  150. package/dist/memory/observer-reflector.js.map +1 -1
  151. package/dist/memory/patterns.d.ts +10 -0
  152. package/dist/memory/patterns.d.ts.map +1 -1
  153. package/dist/memory/patterns.js +25 -33
  154. package/dist/memory/patterns.js.map +1 -1
  155. package/dist/memory/pipeline-manifest-sqlite.d.ts +72 -9
  156. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  157. package/dist/memory/pipeline-manifest-sqlite.js +508 -412
  158. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  159. package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
  160. package/dist/memory/retrieval/build-retrieval-bundle.js +16 -15
  161. package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
  162. package/dist/memory/retrieval/fetch.d.ts +7 -0
  163. package/dist/memory/retrieval/fetch.d.ts.map +1 -1
  164. package/dist/memory/retrieval/fetch.js +14 -11
  165. package/dist/memory/retrieval/fetch.js.map +1 -1
  166. package/dist/memory/retrieval/observe.d.ts +7 -1
  167. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  168. package/dist/memory/retrieval/observe.js +58 -4
  169. package/dist/memory/retrieval/observe.js.map +1 -1
  170. package/dist/memory/retrieval/search.d.ts +14 -1
  171. package/dist/memory/retrieval/search.d.ts.map +1 -1
  172. package/dist/memory/retrieval/search.js +133 -69
  173. package/dist/memory/retrieval/search.js.map +1 -1
  174. package/dist/memory/retrieval/telemetry.d.ts +31 -0
  175. package/dist/memory/retrieval/telemetry.d.ts.map +1 -0
  176. package/dist/memory/retrieval/telemetry.js +53 -0
  177. package/dist/memory/retrieval/telemetry.js.map +1 -0
  178. package/dist/nexus/analyze-orchestrator.d.ts +41 -9
  179. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  180. package/dist/nexus/analyze-orchestrator.js +257 -58
  181. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  182. package/dist/nexus/api-contracts.d.ts +2 -6
  183. package/dist/nexus/api-contracts.d.ts.map +1 -1
  184. package/dist/nexus/api-contracts.js +11 -2
  185. package/dist/nexus/api-contracts.js.map +1 -1
  186. package/dist/nexus/context.d.ts.map +1 -1
  187. package/dist/nexus/context.js +14 -37
  188. package/dist/nexus/context.js.map +1 -1
  189. package/dist/nexus/diff.d.ts.map +1 -1
  190. package/dist/nexus/diff.js +2 -1
  191. package/dist/nexus/diff.js.map +1 -1
  192. package/dist/nexus/identity.d.ts +29 -3
  193. package/dist/nexus/identity.d.ts.map +1 -1
  194. package/dist/nexus/identity.js +63 -12
  195. package/dist/nexus/identity.js.map +1 -1
  196. package/dist/nexus/impact.d.ts +21 -19
  197. package/dist/nexus/impact.d.ts.map +1 -1
  198. package/dist/nexus/impact.js +96 -192
  199. package/dist/nexus/impact.js.map +1 -1
  200. package/dist/nexus/knowledge.d.ts +80 -0
  201. package/dist/nexus/knowledge.d.ts.map +1 -0
  202. package/dist/nexus/knowledge.js +786 -0
  203. package/dist/nexus/knowledge.js.map +1 -0
  204. package/dist/nexus/living-brain.d.ts +8 -5
  205. package/dist/nexus/living-brain.d.ts.map +1 -1
  206. package/dist/nexus/living-brain.js +252 -188
  207. package/dist/nexus/living-brain.js.map +1 -1
  208. package/dist/nexus/registry.d.ts +11 -2
  209. package/dist/nexus/registry.d.ts.map +1 -1
  210. package/dist/nexus/registry.js +144 -128
  211. package/dist/nexus/registry.js.map +1 -1
  212. package/dist/nexus/source-roots.d.ts +16 -0
  213. package/dist/nexus/source-roots.d.ts.map +1 -0
  214. package/dist/nexus/source-roots.js +174 -0
  215. package/dist/nexus/source-roots.js.map +1 -0
  216. package/dist/nexus/task-evidence.d.ts +22 -0
  217. package/dist/nexus/task-evidence.d.ts.map +1 -0
  218. package/dist/nexus/task-evidence.js +210 -0
  219. package/dist/nexus/task-evidence.js.map +1 -0
  220. package/dist/nexus/tasks-bridge.d.ts +14 -7
  221. package/dist/nexus/tasks-bridge.d.ts.map +1 -1
  222. package/dist/nexus/tasks-bridge.js +81 -40
  223. package/dist/nexus/tasks-bridge.js.map +1 -1
  224. package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
  225. package/dist/orchestrate/lifecycle-ops.js +170 -120
  226. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  227. package/dist/orchestrate/query-ops.d.ts +6 -7
  228. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  229. package/dist/orchestrate/query-ops.js +509 -532
  230. package/dist/orchestrate/query-ops.js.map +1 -1
  231. package/dist/orchestration/bootstrap.d.ts +15 -1
  232. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  233. package/dist/orchestration/bootstrap.js +21 -10
  234. package/dist/orchestration/bootstrap.js.map +1 -1
  235. package/dist/orchestration/index.d.ts +35 -3
  236. package/dist/orchestration/index.d.ts.map +1 -1
  237. package/dist/orchestration/index.js +64 -35
  238. package/dist/orchestration/index.js.map +1 -1
  239. package/dist/orchestration/lead-rollup.d.ts +24 -1
  240. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  241. package/dist/orchestration/lead-rollup.js +31 -11
  242. package/dist/orchestration/lead-rollup.js.map +1 -1
  243. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  244. package/dist/orchestration/spawn-prompt.js +16 -41
  245. package/dist/orchestration/spawn-prompt.js.map +1 -1
  246. package/dist/orchestration/unblock.d.ts +15 -1
  247. package/dist/orchestration/unblock.d.ts.map +1 -1
  248. package/dist/orchestration/unblock.js +23 -12
  249. package/dist/orchestration/unblock.js.map +1 -1
  250. package/dist/orchestration/validate-spawn.d.ts.map +1 -1
  251. package/dist/orchestration/validate-spawn.js +3 -3
  252. package/dist/orchestration/validate-spawn.js.map +1 -1
  253. package/dist/orchestration/waves.d.ts +29 -13
  254. package/dist/orchestration/waves.d.ts.map +1 -1
  255. package/dist/orchestration/waves.js +43 -55
  256. package/dist/orchestration/waves.js.map +1 -1
  257. package/dist/paths.d.ts +17 -163
  258. package/dist/paths.d.ts.map +1 -1
  259. package/dist/paths.js +145 -496
  260. package/dist/paths.js.map +1 -1
  261. package/dist/project-info.d.ts +2 -11
  262. package/dist/project-info.d.ts.map +1 -1
  263. package/dist/project-info.js +3 -31
  264. package/dist/project-info.js.map +1 -1
  265. package/dist/project-scope.d.ts +239 -0
  266. package/dist/project-scope.d.ts.map +1 -0
  267. package/dist/project-scope.js +473 -0
  268. package/dist/project-scope.js.map +1 -0
  269. package/dist/release/pr-evidence.d.ts +16 -3
  270. package/dist/release/pr-evidence.d.ts.map +1 -1
  271. package/dist/release/pr-evidence.js +53 -7
  272. package/dist/release/pr-evidence.js.map +1 -1
  273. package/dist/render/nexus/graph/index.d.ts.map +1 -1
  274. package/dist/render/nexus/graph/index.js +4 -2
  275. package/dist/render/nexus/graph/index.js.map +1 -1
  276. package/dist/render/orchestration/audit-reconstruct.d.ts +11 -0
  277. package/dist/render/orchestration/audit-reconstruct.d.ts.map +1 -1
  278. package/dist/render/orchestration/audit-reconstruct.js +21 -6
  279. package/dist/render/orchestration/audit-reconstruct.js.map +1 -1
  280. package/dist/render/tasks/show.js +2 -1
  281. package/dist/render/tasks/show.js.map +1 -1
  282. package/dist/resources/spawn-wrapper.d.ts +82 -14
  283. package/dist/resources/spawn-wrapper.d.ts.map +1 -1
  284. package/dist/resources/spawn-wrapper.js +585 -27
  285. package/dist/resources/spawn-wrapper.js.map +1 -1
  286. package/dist/selfimprove/fix-gen.js +4026 -1734
  287. package/dist/selfimprove/fix-gen.js.map +3 -3
  288. package/dist/sequence/index.d.ts.map +1 -1
  289. package/dist/sequence/index.js +76 -68
  290. package/dist/sequence/index.js.map +1 -1
  291. package/dist/session/canon-lint.d.ts +24 -3
  292. package/dist/session/canon-lint.d.ts.map +1 -1
  293. package/dist/session/canon-lint.js +72 -31
  294. package/dist/session/canon-lint.js.map +1 -1
  295. package/dist/session/engine-ops.d.ts +1 -4
  296. package/dist/session/engine-ops.d.ts.map +1 -1
  297. package/dist/session/engine-ops.js +6 -2
  298. package/dist/session/engine-ops.js.map +1 -1
  299. package/dist/sessions/briefing.d.ts +11 -1
  300. package/dist/sessions/briefing.d.ts.map +1 -1
  301. package/dist/sessions/briefing.js +76 -8
  302. package/dist/sessions/briefing.js.map +1 -1
  303. package/dist/sessions/handoff.d.ts +3 -1
  304. package/dist/sessions/handoff.d.ts.map +1 -1
  305. package/dist/sessions/handoff.js +2 -0
  306. package/dist/sessions/handoff.js.map +1 -1
  307. package/dist/shutdown-deadline.d.ts +24 -33
  308. package/dist/shutdown-deadline.d.ts.map +1 -1
  309. package/dist/shutdown-deadline.js +71 -14
  310. package/dist/shutdown-deadline.js.map +1 -1
  311. package/dist/shutdown.d.ts +21 -10
  312. package/dist/shutdown.d.ts.map +1 -1
  313. package/dist/shutdown.js +89 -42
  314. package/dist/shutdown.js.map +1 -1
  315. package/dist/skills/index.d.ts +1 -1
  316. package/dist/skills/index.d.ts.map +1 -1
  317. package/dist/skills/index.js +1 -1
  318. package/dist/skills/index.js.map +1 -1
  319. package/dist/skills/orchestrator/validator.d.ts +87 -20
  320. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  321. package/dist/skills/orchestrator/validator.js +330 -241
  322. package/dist/skills/orchestrator/validator.js.map +1 -1
  323. package/dist/store/agent-resolver.d.ts.map +1 -1
  324. package/dist/store/agent-resolver.js +16 -9
  325. package/dist/store/agent-resolver.js.map +1 -1
  326. package/dist/store/attachment-store.d.ts +4 -1
  327. package/dist/store/attachment-store.d.ts.map +1 -1
  328. package/dist/store/attachment-store.js +20 -3
  329. package/dist/store/attachment-store.js.map +1 -1
  330. package/dist/store/background-jobs.d.ts +426 -105
  331. package/dist/store/background-jobs.d.ts.map +1 -1
  332. package/dist/store/background-jobs.js +1208 -252
  333. package/dist/store/background-jobs.js.map +1 -1
  334. package/dist/store/background-ops.d.ts +145 -8
  335. package/dist/store/background-ops.d.ts.map +1 -1
  336. package/dist/store/background-ops.js +459 -15
  337. package/dist/store/background-ops.js.map +1 -1
  338. package/dist/store/backup-inspect.d.ts +48 -4
  339. package/dist/store/backup-inspect.d.ts.map +1 -1
  340. package/dist/store/backup-inspect.js +384 -4
  341. package/dist/store/backup-inspect.js.map +1 -1
  342. package/dist/store/converters.d.ts.map +1 -1
  343. package/dist/store/converters.js +19 -2
  344. package/dist/store/converters.js.map +1 -1
  345. package/dist/store/data-accessor.d.ts +9 -1
  346. package/dist/store/data-accessor.d.ts.map +1 -1
  347. package/dist/store/data-accessor.js +22 -5
  348. package/dist/store/data-accessor.js.map +1 -1
  349. package/dist/store/data-safety-central.d.ts.map +1 -1
  350. package/dist/store/data-safety-central.js +272 -232
  351. package/dist/store/data-safety-central.js.map +1 -1
  352. package/dist/store/db-helpers.d.ts +19 -3
  353. package/dist/store/db-helpers.d.ts.map +1 -1
  354. package/dist/store/db-helpers.js +42 -58
  355. package/dist/store/db-helpers.js.map +1 -1
  356. package/dist/store/dual-scope-db.d.ts +63 -4
  357. package/dist/store/dual-scope-db.d.ts.map +1 -1
  358. package/dist/store/dual-scope-db.js +166 -19
  359. package/dist/store/dual-scope-db.js.map +1 -1
  360. package/dist/store/exodus/archive.d.ts +21 -7
  361. package/dist/store/exodus/archive.d.ts.map +1 -1
  362. package/dist/store/exodus/archive.js +60 -18
  363. package/dist/store/exodus/archive.js.map +1 -1
  364. package/dist/store/exodus/migrate.d.ts.map +1 -1
  365. package/dist/store/exodus/migrate.js +27 -23
  366. package/dist/store/exodus/migrate.js.map +1 -1
  367. package/dist/store/exodus/on-open.d.ts +16 -27
  368. package/dist/store/exodus/on-open.d.ts.map +1 -1
  369. package/dist/store/exodus/on-open.js +121 -169
  370. package/dist/store/exodus/on-open.js.map +1 -1
  371. package/dist/store/exodus/recovery.d.ts +90 -0
  372. package/dist/store/exodus/recovery.d.ts.map +1 -0
  373. package/dist/store/exodus/recovery.js +434 -0
  374. package/dist/store/exodus/recovery.js.map +1 -0
  375. package/dist/store/exodus/types.d.ts +15 -0
  376. package/dist/store/exodus/types.d.ts.map +1 -1
  377. package/dist/store/memory-accessor.d.ts +52 -3
  378. package/dist/store/memory-accessor.d.ts.map +1 -1
  379. package/dist/store/memory-accessor.js +122 -7
  380. package/dist/store/memory-accessor.js.map +1 -1
  381. package/dist/store/nexus-sqlite.d.ts +24 -6
  382. package/dist/store/nexus-sqlite.d.ts.map +1 -1
  383. package/dist/store/nexus-sqlite.js +39 -20
  384. package/dist/store/nexus-sqlite.js.map +1 -1
  385. package/dist/store/ports/domain-binding.d.ts.map +1 -1
  386. package/dist/store/ports/domain-binding.js +27 -5
  387. package/dist/store/ports/domain-binding.js.map +1 -1
  388. package/dist/store/schema/background-jobs.d.ts +181 -2
  389. package/dist/store/schema/background-jobs.d.ts.map +1 -1
  390. package/dist/store/schema/background-jobs.js +27 -3
  391. package/dist/store/schema/background-jobs.js.map +1 -1
  392. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  393. package/dist/store/schema/cleo-project/lifecycle.d.ts +2 -2
  394. package/dist/store/schema/cleo-project/runtime.d.ts +1 -1
  395. package/dist/store/schema/cleo-project/tasks-core.d.ts +1 -1
  396. package/dist/store/schema/lifecycle.d.ts +2 -2
  397. package/dist/store/schema/skills-schema.d.ts +2 -2
  398. package/dist/store/schema/tasks.d.ts +1 -1
  399. package/dist/store/session-store.d.ts +3 -1
  400. package/dist/store/session-store.d.ts.map +1 -1
  401. package/dist/store/session-store.js +252 -207
  402. package/dist/store/session-store.js.map +1 -1
  403. package/dist/store/sqlite-data-accessor.d.ts +39 -4
  404. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  405. package/dist/store/sqlite-data-accessor.js +689 -527
  406. package/dist/store/sqlite-data-accessor.js.map +1 -1
  407. package/dist/store/tasks-sqlite.d.ts +3 -1
  408. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  409. package/dist/store/tasks-sqlite.js +309 -279
  410. package/dist/store/tasks-sqlite.js.map +1 -1
  411. package/dist/store/validation-schemas.d.ts +8 -8
  412. package/dist/store/writer-lease.d.ts +80 -8
  413. package/dist/store/writer-lease.d.ts.map +1 -1
  414. package/dist/store/writer-lease.js +503 -134
  415. package/dist/store/writer-lease.js.map +1 -1
  416. package/dist/system/audit.d.ts +2 -1
  417. package/dist/system/audit.d.ts.map +1 -1
  418. package/dist/system/audit.js +118 -71
  419. package/dist/system/audit.js.map +1 -1
  420. package/dist/system/backup.d.ts.map +1 -1
  421. package/dist/system/backup.js +5 -5
  422. package/dist/system/backup.js.map +1 -1
  423. package/dist/system/inject-generate.js +1 -1
  424. package/dist/task-work/index.d.ts +3 -0
  425. package/dist/task-work/index.d.ts.map +1 -1
  426. package/dist/task-work/index.js +34 -37
  427. package/dist/task-work/index.js.map +1 -1
  428. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  429. package/dist/tasks/ac-coverage-gate.js +4 -0
  430. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  431. package/dist/tasks/ac-immutability.d.ts +7 -4
  432. package/dist/tasks/ac-immutability.d.ts.map +1 -1
  433. package/dist/tasks/ac-immutability.js +19 -17
  434. package/dist/tasks/ac-immutability.js.map +1 -1
  435. package/dist/tasks/ac-table.d.ts +5 -5
  436. package/dist/tasks/ac-table.d.ts.map +1 -1
  437. package/dist/tasks/ac-table.js +26 -12
  438. package/dist/tasks/ac-table.js.map +1 -1
  439. package/dist/tasks/acceptance-input.d.ts +52 -0
  440. package/dist/tasks/acceptance-input.d.ts.map +1 -0
  441. package/dist/tasks/acceptance-input.js +259 -0
  442. package/dist/tasks/acceptance-input.js.map +1 -0
  443. package/dist/tasks/add.d.ts +67 -29
  444. package/dist/tasks/add.d.ts.map +1 -1
  445. package/dist/tasks/add.js +255 -97
  446. package/dist/tasks/add.js.map +1 -1
  447. package/dist/tasks/complete.d.ts +16 -1
  448. package/dist/tasks/complete.d.ts.map +1 -1
  449. package/dist/tasks/complete.js +152 -55
  450. package/dist/tasks/complete.js.map +1 -1
  451. package/dist/tasks/decompose.d.ts +103 -0
  452. package/dist/tasks/decompose.d.ts.map +1 -0
  453. package/dist/tasks/decompose.js +224 -0
  454. package/dist/tasks/decompose.js.map +1 -0
  455. package/dist/tasks/delete.d.ts +5 -8
  456. package/dist/tasks/delete.d.ts.map +1 -1
  457. package/dist/tasks/delete.js +4 -3
  458. package/dist/tasks/delete.js.map +1 -1
  459. package/dist/tasks/dependency-check.d.ts +59 -0
  460. package/dist/tasks/dependency-check.d.ts.map +1 -1
  461. package/dist/tasks/dependency-check.js +73 -0
  462. package/dist/tasks/dependency-check.js.map +1 -1
  463. package/dist/tasks/deps-ready.d.ts +13 -1
  464. package/dist/tasks/deps-ready.d.ts.map +1 -1
  465. package/dist/tasks/deps-ready.js +16 -5
  466. package/dist/tasks/deps-ready.js.map +1 -1
  467. package/dist/tasks/engine-converters.d.ts +7 -0
  468. package/dist/tasks/engine-converters.d.ts.map +1 -1
  469. package/dist/tasks/engine-converters.js +8 -1
  470. package/dist/tasks/engine-converters.js.map +1 -1
  471. package/dist/tasks/evidence.d.ts +54 -26
  472. package/dist/tasks/evidence.d.ts.map +1 -1
  473. package/dist/tasks/evidence.js +292 -40
  474. package/dist/tasks/evidence.js.map +1 -1
  475. package/dist/tasks/find.d.ts +16 -12
  476. package/dist/tasks/find.d.ts.map +1 -1
  477. package/dist/tasks/find.js +107 -93
  478. package/dist/tasks/find.js.map +1 -1
  479. package/dist/tasks/gate-runner.d.ts +88 -14
  480. package/dist/tasks/gate-runner.d.ts.map +1 -1
  481. package/dist/tasks/gate-runner.js +596 -253
  482. package/dist/tasks/gate-runner.js.map +1 -1
  483. package/dist/tasks/hierarchy-policy.d.ts +8 -0
  484. package/dist/tasks/hierarchy-policy.d.ts.map +1 -1
  485. package/dist/tasks/hierarchy-policy.js +9 -2
  486. package/dist/tasks/hierarchy-policy.js.map +1 -1
  487. package/dist/tasks/hierarchy.d.ts +67 -1
  488. package/dist/tasks/hierarchy.d.ts.map +1 -1
  489. package/dist/tasks/hierarchy.js +81 -1
  490. package/dist/tasks/hierarchy.js.map +1 -1
  491. package/dist/tasks/index.d.ts +3 -0
  492. package/dist/tasks/index.d.ts.map +1 -1
  493. package/dist/tasks/index.js +3 -0
  494. package/dist/tasks/index.js.map +1 -1
  495. package/dist/tasks/infer-add-params.d.ts +1 -20
  496. package/dist/tasks/infer-add-params.d.ts.map +1 -1
  497. package/dist/tasks/infer-add-params.js +2 -203
  498. package/dist/tasks/infer-add-params.js.map +1 -1
  499. package/dist/tasks/list.d.ts +7 -2
  500. package/dist/tasks/list.d.ts.map +1 -1
  501. package/dist/tasks/list.js +24 -36
  502. package/dist/tasks/list.js.map +1 -1
  503. package/dist/tasks/ops.d.ts +6 -61
  504. package/dist/tasks/ops.d.ts.map +1 -1
  505. package/dist/tasks/ops.js +5 -53
  506. package/dist/tasks/ops.js.map +1 -1
  507. package/dist/tasks/plan.d.ts +15 -1
  508. package/dist/tasks/plan.d.ts.map +1 -1
  509. package/dist/tasks/plan.js +23 -25
  510. package/dist/tasks/plan.js.map +1 -1
  511. package/dist/tasks/population.d.ts +46 -0
  512. package/dist/tasks/population.d.ts.map +1 -0
  513. package/dist/tasks/population.js +88 -0
  514. package/dist/tasks/population.js.map +1 -0
  515. package/dist/tasks/reconcile-scope.d.ts +157 -0
  516. package/dist/tasks/reconcile-scope.d.ts.map +1 -0
  517. package/dist/tasks/reconcile-scope.js +242 -0
  518. package/dist/tasks/reconcile-scope.js.map +1 -0
  519. package/dist/tasks/req.d.ts +51 -12
  520. package/dist/tasks/req.d.ts.map +1 -1
  521. package/dist/tasks/req.js +125 -69
  522. package/dist/tasks/req.js.map +1 -1
  523. package/dist/tasks/session-scope.d.ts +6 -31
  524. package/dist/tasks/session-scope.d.ts.map +1 -1
  525. package/dist/tasks/session-scope.js +3 -17
  526. package/dist/tasks/session-scope.js.map +1 -1
  527. package/dist/tasks/severity-attestation.d.ts +39 -10
  528. package/dist/tasks/severity-attestation.d.ts.map +1 -1
  529. package/dist/tasks/severity-attestation.js +75 -36
  530. package/dist/tasks/severity-attestation.js.map +1 -1
  531. package/dist/tasks/task-next.d.ts +5 -0
  532. package/dist/tasks/task-next.d.ts.map +1 -1
  533. package/dist/tasks/task-next.js +10 -9
  534. package/dist/tasks/task-next.js.map +1 -1
  535. package/dist/tasks/task-reparent.d.ts.map +1 -1
  536. package/dist/tasks/task-reparent.js +3 -13
  537. package/dist/tasks/task-reparent.js.map +1 -1
  538. package/dist/tasks/update.d.ts +20 -43
  539. package/dist/tasks/update.d.ts.map +1 -1
  540. package/dist/tasks/update.js +64 -49
  541. package/dist/tasks/update.js.map +1 -1
  542. package/dist/validation/engine-ops.d.ts +17 -1
  543. package/dist/validation/engine-ops.d.ts.map +1 -1
  544. package/dist/validation/engine-ops.js +149 -11
  545. package/dist/validation/engine-ops.js.map +1 -1
  546. package/dist/workgraph/reparent-preflight.d.ts +1 -1
  547. package/dist/workgraph/reparent-preflight.d.ts.map +1 -1
  548. package/dist/workgraph/reparent-preflight.js +6 -1
  549. package/dist/workgraph/reparent-preflight.js.map +1 -1
  550. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  551. package/dist/workgraph/scaffold-apply.js +81 -76
  552. package/dist/workgraph/scaffold-apply.js.map +1 -1
  553. package/dist/workgraph/structure-validation.d.ts +1 -1
  554. package/dist/workgraph/structure-validation.d.ts.map +1 -1
  555. package/dist/workgraph/structure-validation.js +6 -1
  556. package/dist/workgraph/structure-validation.js.map +1 -1
  557. package/migrations/drizzle-tasks/20260919180000_t12263-job-ownership-fences/migration.sql +34 -0
  558. package/migrations/drizzle-tasks/20260919184500_t12265-durable-pending-proposals/migration.sql +4 -0
  559. package/package.json +18 -13
  560. package/schemas/config.schema.json +2 -2
  561. package/templates/CLEO-INJECTION.md +103 -101
@@ -12,27 +12,33 @@
12
12
  *
13
13
  * Design constraints:
14
14
  * - Each gate is self-contained (no cross-gate state).
15
- * - Gates run sequentially unless the caller sets `parallel` options.
16
- * - Default timeout is 60 000 ms per gate (overridable via `timeoutMs`
17
- * on the gate or the `CLEO_GATE_TIMEOUT_MS` env variable).
15
+ * - Gates run sequentially under the original shared deadline (two seconds by default).
16
+ * - Per-gate timeouts can only tighten the shared deadline. Long work needs runtime admission.
17
+ * - Structured test-count evidence and HTTP service startup require separate capabilities.
18
+ * - Results are observations; this module does not persist completion authority.
18
19
  *
19
20
  * @epic T760
20
21
  * @task T781
21
22
  */
22
- import { execFile } from 'node:child_process';
23
- import { createHash } from 'node:crypto';
24
- import { readFile, stat } from 'node:fs/promises';
25
- import { isAbsolute, join } from 'node:path';
26
- import { promisify } from 'node:util';
23
+ import { createHash, randomUUID } from 'node:crypto';
24
+ import { constants } from 'node:fs';
25
+ import { lstat, open, realpath, stat } from 'node:fs/promises';
26
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
27
+ import { isDeepStrictEqual } from 'node:util';
28
+ import { acceptanceGateResultSchema, acceptanceGateSchema } from '@cleocode/contracts';
27
29
  import { getProjectRoot } from '../paths.js';
30
+ import { captureProjectScope, worktreeScope } from '../project-scope.js';
28
31
  import { truncateString } from '../render/helpers.js';
32
+ import { captureWrapped } from '../resources/spawn-wrapper.js';
29
33
  import { createAttachmentStore } from '../store/attachment-store.js';
30
- const execFileAsync = promisify(execFile);
34
+ import { registerTeardownAbort } from '../teardown-signal.js';
35
+ import { acItemToText, acTextHash } from './ac-table.js';
36
+ import { heavyToolEnv } from './heavy-tool-env.js';
31
37
  // ─── Constants ────────────────────────────────────────────────────────────────
32
38
  /** Default gate timeout in milliseconds. Overridden by env or per-gate `timeoutMs`. */
33
39
  const DEFAULT_TIMEOUT_MS = Number(process.env['CLEO_GATE_TIMEOUT_MS'] ?? 60_000);
34
- /** Maximum evidence string length stored per gate result. */
35
- const MAX_EVIDENCE_BYTES = 2_000;
40
+ /** Maximum evidence string character count retained per gate result. */
41
+ const MAX_EVIDENCE_CHARS = 2_000;
36
42
  /** Agent identifier written into `checkedBy`. */
37
43
  const CHECKED_BY = process.env['CLEO_AGENT_ID'] ?? 'cleo-verify';
38
44
  /**
@@ -44,166 +50,578 @@ const CHECKED_BY = process.env['CLEO_AGENT_ID'] ?? 'cleo-verify';
44
50
  * @param gates - Typed gate objects (strings pre-filtered by caller).
45
51
  * @param options - Execution options.
46
52
  * @returns Ordered `AcceptanceGateResult[]`, one per gate.
53
+ * @throws If explicit project authority conflicts, metadata is invalid, or admission options are invalid.
54
+ * @remarks Results retain the original gate requirement ID. Process outcomes include target,
55
+ * transport and cleanup observations; they do not by themselves authorize task completion.
56
+ * Declared operation bytes reserve the per-gate capture ceiling before admission. Filesystem
57
+ * calls and regular-expression CPU work are cooperatively checked, not synchronously preempted.
58
+ * Canonical attachment retrieval validates its declared size but is not a streaming read API.
59
+ * @example
60
+ * ```typescript
61
+ * const results = await runGates([{ kind: 'command', description: 'check', cmd: 'node', args: ['check.mjs'] }], { projectRoot: '/project' });
62
+ * ```
47
63
  *
48
64
  * @epic T760
49
65
  * @task T781
50
66
  */
51
67
  export async function runGates(gates, options = {}) {
52
- const projectRoot = options.projectRoot ?? getProjectRoot();
53
- const skipManual = options.skipManual ?? true;
68
+ const inherited = worktreeScope.getStore();
69
+ if (inherited?.execution && options.execution && inherited.execution !== options.execution)
70
+ throw new Error('Gate execution cannot replace an active captured operation');
71
+ const execution = options.execution ?? inherited?.execution;
72
+ const projectRoot = resolve(options.projectRoot ?? execution?.identity.projectRoot ?? getProjectRoot());
73
+ if (execution && resolve(execution.identity.projectRoot) !== projectRoot)
74
+ throw new Error('Gate execution project does not match captured operation authority');
75
+ const scope = captureProjectScope(projectRoot, {
76
+ ...captureProjectScope(projectRoot, inherited),
77
+ execution,
78
+ });
79
+ const maxOutputBytes = options.maxOutputBytes ?? 1_048_576;
80
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
81
+ throw new RangeError('Gate capture byte limit must be a positive safe integer');
82
+ const snapshot = structuredClone(gates);
83
+ const controller = new AbortController();
84
+ const deregister = registerTeardownAbort(controller);
85
+ const batch = {
86
+ cwd: projectRoot,
87
+ env: { ...(options.env ?? process.env) },
88
+ execution: {
89
+ deadlineAt: execution?.deadlineAt ?? Date.now() + 2000,
90
+ signal: execution
91
+ ? AbortSignal.any([execution.signal, controller.signal])
92
+ : controller.signal,
93
+ },
94
+ maxOutputBytes,
95
+ memoryMaxMb: options.memoryMaxMb,
96
+ tasksMax: options.tasksMax,
97
+ systemdControl: options.systemdControl,
98
+ };
99
+ const checkedBy = execution?.identity.actor ?? batch.env['CLEO_AGENT_ID'] ?? CHECKED_BY;
54
100
  const results = [];
55
- for (let i = 0; i < gates.length; i++) {
56
- const gate = gates[i];
57
- const startMs = Date.now();
58
- let result;
101
+ try {
102
+ return await worktreeScope.run(scope, async () => {
103
+ for (let i = 0; i < snapshot.length; i++) {
104
+ const gate = snapshot[i];
105
+ const startMs = Date.now();
106
+ try {
107
+ execution?.assertActive();
108
+ execution?.consume({
109
+ items: 1,
110
+ bytes: gate.kind === 'manual' ? 0 : maxOutputBytes,
111
+ });
112
+ assertGateActive(batch);
113
+ const parsed = acceptanceGateSchema.parse(gate);
114
+ if (!isDeepStrictEqual(parsed, gate))
115
+ throw new Error('Gate includes unsupported or noncanonical fields');
116
+ results.push(await runOneGate(parsed, i, projectRoot, options.skipManual ?? true, batch));
117
+ }
118
+ catch (error) {
119
+ results.push(makeResult(i, gate, 'error', Date.now() - startMs, undefined, error instanceof Error ? error.message : String(error)));
120
+ }
121
+ }
122
+ return results.map((result) => ({ ...result, checkedBy }));
123
+ });
124
+ }
125
+ finally {
126
+ deregister();
127
+ }
128
+ }
129
+ /**
130
+ * Run task requirements with their canonical AC and bounded input snapshots.
131
+ * @param task - Captured task, including its complete mixed acceptance array.
132
+ * @param criteria - Current normalized acceptance rows from the same task snapshot.
133
+ * @param options - Existing explicitly admitted verification lifetime and process limits.
134
+ * @returns Results bound to original mixed-array indexes, identities and input bytes.
135
+ * @remarks This function never writes a result. The canonical verifier must compare
136
+ * task/criterion freshness again and persist results plus receipt in one transaction.
137
+ * Untracked harnesses are included; undeclared runtime dependencies are not inferred.
138
+ * @example
139
+ * ```typescript
140
+ * const results = await runTaskGates(task, rows, { projectRoot, execution });
141
+ * ```
142
+ */
143
+ export async function runTaskGates(task, criteria, options) {
144
+ const execution = options.execution ?? worktreeScope.getStore()?.execution;
145
+ if (!execution || execution.identity.operation !== 'check.gate.verify')
146
+ throw new Error('Typed verification requires an explicitly admitted check.gate.verify lifetime');
147
+ const root = resolve(options.projectRoot ?? execution.identity.projectRoot);
148
+ const scope = captureProjectScope(root, {
149
+ ...captureProjectScope(root, worktreeScope.getStore()),
150
+ execution,
151
+ });
152
+ const snapshot = structuredClone(task);
153
+ const rows = structuredClone([...criteria]).sort((a, b) => a.ordinal - b.ordinal);
154
+ assertCriterionProjection(snapshot, rows);
155
+ const env = { ...(options.env ?? process.env) };
156
+ const verificationId = randomUUID();
157
+ return worktreeScope.run(scope, async () => {
158
+ const results = [];
159
+ for (const [index, gate] of (snapshot.acceptance ?? []).entries()) {
160
+ if (typeof gate === 'string')
161
+ continue;
162
+ execution.assertActive();
163
+ const capturedAt = new Date().toISOString();
164
+ const invocation = executableInvocation(gate, root, env);
165
+ const artifacts = await snapshotGateInputs(snapshot, gate, invocation, execution);
166
+ const binding = {
167
+ version: 1,
168
+ verificationId,
169
+ identity: { ...execution.identity },
170
+ taskId: snapshot.id,
171
+ criterionId: rows[index].id,
172
+ criterionHash: acTextHash(rows[index].text),
173
+ gateHash: createHash('sha256').update(acItemToText(gate)).digest('hex'),
174
+ capturedAt,
175
+ deadlineAt: execution.deadlineAt,
176
+ ...(invocation ? { invocation } : {}),
177
+ artifacts,
178
+ };
179
+ const observed = (await runGates([gate], { ...options, projectRoot: root, env, execution }))[0];
180
+ let result = { ...observed, index, binding };
181
+ try {
182
+ const after = await snapshotGateInputs(snapshot, gate, invocation, execution);
183
+ if (!isDeepStrictEqual(after, artifacts))
184
+ throw new Error('Verification inputs changed during execution');
185
+ }
186
+ catch (error) {
187
+ result = {
188
+ ...result,
189
+ result: 'error',
190
+ errorMessage: error instanceof Error ? error.message : String(error),
191
+ };
192
+ }
193
+ results.push(acceptanceGateResultSchema.parse(result));
194
+ }
195
+ return results;
196
+ });
197
+ }
198
+ /**
199
+ * Build the existing audit detail payload for one validated typed-result batch.
200
+ * @param results - Exact result array that is persisted alongside this receipt.
201
+ * @param passed - Overall verification outcome at the time of recording.
202
+ * @returns The canonical receipt detail shape with deterministic result-byte hash.
203
+ * @throws Error when results are absent, unbound, duplicated or belong to different batches.
204
+ * @remarks This serializer establishes shape and byte identity, not execution authority.
205
+ * The caller must persist the receipt and results in the same owning transaction.
206
+ * @example
207
+ * ```ts
208
+ * const details = createTaskGateReceipt(results, verification.passed);
209
+ * await transaction.appendLog({ action: 'gate.verify.typed', taskId, details });
210
+ * ```
211
+ */
212
+ export function createTaskGateReceipt(results, passed) {
213
+ const first = results[0]?.binding;
214
+ if (!first)
215
+ throw new Error('Typed receipt requires a bound result batch');
216
+ const indexes = new Set();
217
+ const criteria = new Set();
218
+ for (const candidate of results) {
219
+ const result = acceptanceGateResultSchema.parse(candidate);
220
+ const binding = result.binding;
221
+ if (!binding ||
222
+ binding.verificationId !== first.verificationId ||
223
+ binding.taskId !== first.taskId ||
224
+ !isDeepStrictEqual(binding.identity, first.identity) ||
225
+ binding.deadlineAt !== first.deadlineAt ||
226
+ indexes.has(result.index) ||
227
+ criteria.has(binding.criterionId))
228
+ throw new Error('Typed receipt requires one batch owner and unique criterion results');
229
+ indexes.add(result.index);
230
+ criteria.add(binding.criterionId);
231
+ }
232
+ return {
233
+ verificationId: first.verificationId,
234
+ resultHash: createHash('sha256').update(JSON.stringify(results)).digest('hex'),
235
+ operation: 'check.gate.verify',
236
+ passed,
237
+ };
238
+ }
239
+ /**
240
+ * Revalidate persisted typed results against current canonical task and input state.
241
+ * @param task - Current task read under the caller's transaction ownership.
242
+ * @param criteria - Current normalized AC rows from that transaction.
243
+ * @param results - Authentic stored results, never caller-submitted verdicts.
244
+ * @param options - Current captured lifetime bounding revalidation work.
245
+ * @param requirePassing - True for completion; false only when recording actual unmet verification outcomes.
246
+ * @returns Resolves when bound inputs are current and the requested verdict policy holds.
247
+ * @remarks Historical unbound results and generic text-AC evidence cannot substitute.
248
+ * Filesystem checks are cooperative and cannot make external file writes atomic with SQLite.
249
+ * @example
250
+ * ```typescript
251
+ * await revalidateTaskGateResults(task, rows, task.verification?.gateResults ?? [], { execution });
252
+ * ```
253
+ */
254
+ export async function revalidateTaskGateResults(task, criteria, results, options, requirePassing = true) {
255
+ const execution = options.execution ?? worktreeScope.getStore()?.execution;
256
+ if (!execution)
257
+ throw new Error('Typed result revalidation requires a captured execution lifetime');
258
+ const root = resolve(options.projectRoot ?? execution.identity.projectRoot);
259
+ const scope = captureProjectScope(root, {
260
+ ...captureProjectScope(root, worktreeScope.getStore()),
261
+ execution,
262
+ });
263
+ const rows = [...criteria].sort((a, b) => a.ordinal - b.ordinal);
264
+ assertCriterionProjection(task, rows);
265
+ const env = { ...(options.env ?? process.env) };
266
+ await worktreeScope.run(scope, async () => {
267
+ for (const [index, gate] of (task.acceptance ?? []).entries()) {
268
+ if (typeof gate === 'string' || (requirePassing && gate.advisory))
269
+ continue;
270
+ execution.assertActive();
271
+ const matches = results.filter((result) => result.index === index);
272
+ if (matches.length !== 1)
273
+ throw new Error(`Typed requirement ${gate.req ?? index} has no unique verified result`);
274
+ const result = acceptanceGateResultSchema.parse(matches[0]);
275
+ const binding = result.binding;
276
+ if ((requirePassing && result.result !== 'pass') ||
277
+ !binding ||
278
+ result.kind !== gate.kind ||
279
+ result.req !== gate.req)
280
+ throw new Error(`Typed requirement ${gate.req ?? index} lacks a passing bound result`);
281
+ if (binding.taskId !== task.id ||
282
+ binding.identity.projectId !== execution.identity.projectId ||
283
+ binding.identity.projectRoot !== root ||
284
+ binding.criterionId !== rows[index].id ||
285
+ binding.criterionHash !== acTextHash(rows[index].text) ||
286
+ binding.gateHash !== createHash('sha256').update(acItemToText(gate)).digest('hex'))
287
+ throw new Error(`Typed requirement ${gate.req ?? index} binding is stale or belongs to another owner`);
288
+ const invocation = executableInvocation(gate, root, env);
289
+ if (!isDeepStrictEqual(binding.invocation, invocation))
290
+ throw new Error(`Typed requirement ${gate.req ?? index} invocation or environment changed`);
291
+ const artifacts = await snapshotGateInputs(task, gate, invocation, execution);
292
+ if (!isDeepStrictEqual(binding.artifacts, artifacts))
293
+ throw new Error(`Typed requirement ${gate.req ?? index} input bytes changed after verification`);
294
+ }
295
+ });
296
+ }
297
+ /**
298
+ * Validate completion authority from current typed inputs and the canonical verification receipt.
299
+ * @param task - Task snapshot owned by the caller's write transaction.
300
+ * @param criteria - Normalized criteria from that same transaction.
301
+ * @param options - Captured original execution lifetime and project scope.
302
+ * @param accessor - Canonical receipt reader bound to the same project store.
303
+ * @returns Resolves only when every hard typed requirement has current authentic proof.
304
+ * @throws When typed representations, results, inputs or canonical receipts disagree.
305
+ * @remarks Callers retain transaction ownership and must check their execution context before
306
+ * committing. Advisory gates retain their existing semantics; generic evidence cannot replace
307
+ * hard typed proof. Filesystem hashing is cooperative, not atomic with external file writes.
308
+ * @example
309
+ * ```typescript
310
+ * await validateTaskGateCompletion(task, rows, { execution }, accessor);
311
+ * ```
312
+ */
313
+ export async function validateTaskGateCompletion(task, criteria, options, accessor) {
314
+ const results = task.verification?.gateResults ?? [];
315
+ await revalidateTaskGateResults(task, criteria, results, options);
316
+ if (!(task.acceptance ?? []).some((item) => typeof item !== 'string' && !item.advisory))
317
+ return;
318
+ const passingDetails = JSON.stringify(createTaskGateReceipt(results, true));
319
+ const failingDetails = JSON.stringify(createTaskGateReceipt(results, false));
320
+ const receipts = await accessor.queryAuditLog({
321
+ taskIds: [task.id],
322
+ actions: ['gate.verify.typed'],
323
+ limit: 100,
324
+ });
325
+ if (!receipts.some((receipt) => receipt.actor === results[0].binding.identity.actor &&
326
+ (receipt.detailsJson === passingDetails || receipt.detailsJson === failingDetails)))
327
+ throw new Error('Typed requirement result has no authentic matching canonical receipt');
328
+ (options.execution ?? worktreeScope.getStore()?.execution)?.assertActive();
329
+ }
330
+ /** Require the stored mixed acceptance array and ordered normalized rows to agree. */
331
+ function assertCriterionProjection(task, rows) {
332
+ const acceptance = task.acceptance ?? [];
333
+ if (rows.length !== acceptance.length ||
334
+ rows.some((row, index) => row.taskId !== task.id ||
335
+ row.text !== acItemToText(acceptance[index]) ||
336
+ (row.kind === 'evidence_bound') !== (typeof acceptance[index] !== 'string') ||
337
+ (row.kind !== 'child_task' &&
338
+ row.contentHash !== null &&
339
+ row.contentHash !== acTextHash(row.text))))
340
+ throw new Error('Typed verification refused inconsistent acceptance JSON and normalized AC rows');
341
+ }
342
+ /** Effective child environment shared by launch and verification input hashing. */
343
+ function gateEnvironment(env, overlay) {
344
+ const merged = { ...env, ...overlay };
345
+ return { ...merged, ...heavyToolEnv('test', merged) };
346
+ }
347
+ /** Resolve the actual executable inputs once using the same rules as process launch. */
348
+ function executableInvocation(gate, root, env) {
349
+ if (gate.kind !== 'test' && gate.kind !== 'command' && gate.kind !== 'lint')
350
+ return undefined;
351
+ const [testCommand, ...testArgs] = gate.kind === 'test' ? gate.command.trim().split(/\s+/) : [];
352
+ const command = gate.kind === 'test'
353
+ ? testCommand
354
+ : gate.kind === 'command'
355
+ ? gate.cmd
356
+ : LINT_TOOL_DEFAULTS[gate.tool].cmd;
357
+ const args = gate.args ??
358
+ (gate.kind === 'test'
359
+ ? testArgs
360
+ : gate.kind === 'lint'
361
+ ? LINT_TOOL_DEFAULTS[gate.tool].defaultArgs
362
+ : []);
363
+ const effective = gateEnvironment(env, gate.kind === 'lint' ? undefined : gate.env);
364
+ return {
365
+ command,
366
+ args: [...args],
367
+ cwd: resolveCwd(root, gate.cwd),
368
+ environmentHash: createHash('sha256')
369
+ .update(JSON.stringify(Object.entries(effective)
370
+ .filter(([, value]) => value !== undefined)
371
+ .sort(([a], [b]) => a.localeCompare(b))))
372
+ .digest('hex'),
373
+ };
374
+ }
375
+ /** Capture declared task inputs and an interpreter's explicit harness argument. */
376
+ async function snapshotGateInputs(task, gate, invocation, execution) {
377
+ const root = resolve(execution.identity.projectRoot);
378
+ if (invocation) {
379
+ const cwdRelative = relative(root, invocation.cwd);
380
+ if (cwdRelative === '..' || cwdRelative.startsWith('../') || isAbsolute(cwdRelative))
381
+ throw new Error('Typed invocation working directory escapes the captured project');
382
+ if ((await realpath(invocation.cwd)) !== invocation.cwd)
383
+ throw new Error('Typed invocation working directory has ambiguous symlink ownership');
384
+ }
385
+ const paths = new Set((task.files ?? []).map((path) => resolve(root, path)));
386
+ if (gate.kind === 'file' && gate.path)
387
+ paths.add(resolve(root, gate.path));
388
+ if (invocation &&
389
+ /^(node(?:\.exe)?|python[0-9.]*(?:\.exe)?|bun|deno|tsx|ts-node|bash|sh|ruby|perl)$/.test(basename(invocation.command))) {
390
+ if (!invocation.args.some((arg) => ['-e', '--eval', '-c', '--print', '-p'].includes(arg))) {
391
+ const script = invocation.args.find((arg) => !arg.startsWith('-'));
392
+ if (script)
393
+ paths.add(resolve(invocation.cwd, script));
394
+ for (const arg of invocation.args) {
395
+ if (!arg.startsWith('-') &&
396
+ (arg.includes('/') || /\.(?:[cm]?[jt]sx?|py|sh|rb|pl)$/.test(arg)))
397
+ paths.add(resolve(invocation.cwd, arg));
398
+ }
399
+ }
400
+ }
401
+ const artifacts = [];
402
+ for (const path of [...paths].sort()) {
403
+ execution.assertActive();
404
+ execution.consume({ items: 1 });
405
+ if (/[?*[\]{}]/.test(path))
406
+ throw new Error(`Typed input requires an explicit file path: ${path}`);
407
+ const rel = relative(root, path);
408
+ if (!rel || rel === '..' || rel.startsWith('../') || isAbsolute(rel))
409
+ throw new Error(`Typed input escapes the captured project: ${path}`);
410
+ // Refuse redirected parents even for absent files; absence must belong to this project.
411
+ let parent = dirname(path);
412
+ for (;;) {
413
+ try {
414
+ if ((await realpath(parent)) !== parent)
415
+ throw new Error(`Typed input has an ambiguous symlink parent: ${path}`);
416
+ break;
417
+ }
418
+ catch (error) {
419
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
420
+ throw error;
421
+ if (parent === root)
422
+ throw error;
423
+ parent = dirname(parent);
424
+ }
425
+ }
426
+ let before;
59
427
  try {
60
- result = await runOneGate(gate, i, projectRoot, skipManual);
428
+ before = await lstat(path);
61
429
  }
62
- catch (err) {
63
- const durationMs = Date.now() - startMs;
64
- result = {
65
- index: i,
66
- req: gate.req,
67
- kind: gate.kind,
68
- result: 'error',
69
- durationMs,
70
- errorMessage: err instanceof Error ? err.message : String(err),
71
- checkedAt: new Date().toISOString(),
72
- checkedBy: CHECKED_BY,
73
- };
430
+ catch (error) {
431
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
432
+ artifacts.push({ path, sha256: null, bytes: null });
433
+ continue;
434
+ }
435
+ throw error;
436
+ }
437
+ if (!before.isFile() || before.isSymbolicLink())
438
+ throw new Error(`Typed input is not an unambiguous regular file: ${path}`);
439
+ const ceiling = execution.resources.maxBytes ?? 1_048_576;
440
+ if (before.size > ceiling)
441
+ throw new Error(`Typed input exceeds admitted byte limit: ${path}`);
442
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
443
+ try {
444
+ const opened = await file.stat();
445
+ if (opened.dev !== before.dev || opened.ino !== before.ino)
446
+ throw new Error(`Typed input replaced while opening: ${path}`);
447
+ const hash = createHash('sha256');
448
+ const buffer = Buffer.alloc(65_536);
449
+ let bytes = 0;
450
+ for (;;) {
451
+ execution.assertActive();
452
+ const chunk = await file.read(buffer, 0, buffer.length, null);
453
+ if (chunk.bytesRead === 0)
454
+ break;
455
+ bytes += chunk.bytesRead;
456
+ if (bytes > ceiling)
457
+ throw new Error(`Typed input grew beyond admitted byte limit: ${path}`);
458
+ execution.consume({ bytes: chunk.bytesRead });
459
+ hash.update(buffer.subarray(0, chunk.bytesRead));
460
+ }
461
+ const after = await file.stat();
462
+ const named = await lstat(path);
463
+ if (bytes !== before.size ||
464
+ after.size !== before.size ||
465
+ after.mtimeMs !== before.mtimeMs ||
466
+ after.ctimeMs !== before.ctimeMs ||
467
+ named.dev !== before.dev ||
468
+ named.ino !== before.ino)
469
+ throw new Error(`Typed input changed while hashing: ${path}`);
470
+ execution.assertActive();
471
+ artifacts.push({ path, sha256: hash.digest('hex'), bytes });
472
+ }
473
+ finally {
474
+ await file.close();
74
475
  }
75
- results.push(result);
76
476
  }
77
- return results;
477
+ return artifacts;
478
+ }
479
+ /** Check the shared boundary without pretending to preempt synchronous operations. */
480
+ function assertGateActive(context) {
481
+ context.execution.signal?.throwIfAborted();
482
+ if (Date.now() >= context.execution.deadlineAt)
483
+ throw new Error('Gate did not finish: original shared deadline expired. This is NOT a failure verdict.');
78
484
  }
79
485
  // ─── Internal dispatcher ──────────────────────────────────────────────────────
80
- async function runOneGate(gate, index, projectRoot, skipManual) {
486
+ async function runOneGate(gate, index, projectRoot, skipManual, context) {
81
487
  const timeout = gate.timeoutMs ?? DEFAULT_TIMEOUT_MS;
488
+ if (!Number.isSafeInteger(timeout) || timeout < 1)
489
+ throw new Error('Gate timeout must be a positive safe integer');
82
490
  switch (gate.kind) {
83
491
  case 'test':
84
- return runTestGate(gate, index, projectRoot, timeout);
492
+ return runTestGate(gate, index, projectRoot, timeout, context);
85
493
  case 'file':
86
- return runFileGate(gate, index, projectRoot);
494
+ return runFileGate(gate, index, projectRoot, context);
87
495
  case 'command':
88
- return runCommandGate(gate, index, projectRoot, timeout);
496
+ return runCommandGate(gate, index, projectRoot, timeout, context);
89
497
  case 'lint':
90
- return runLintGate(gate, index, projectRoot, timeout);
498
+ return runLintGate(gate, index, projectRoot, timeout, context);
91
499
  case 'http':
92
- return runHttpGate(gate, index, projectRoot, timeout);
500
+ return runHttpGate(gate, index, projectRoot, timeout, context);
93
501
  case 'manual':
94
502
  return runManualGate(gate, index, skipManual);
95
503
  }
96
504
  }
97
505
  // ─── Test gate ────────────────────────────────────────────────────────────────
98
- async function runTestGate(gate, index, projectRoot, timeoutMs) {
99
- const startMs = Date.now();
100
- const cwd = resolveCwd(projectRoot, gate.cwd);
101
- // Split command string into binary + args if no explicit args provided
102
- const [bin, ...defaultArgs] = gate.command.split(' ');
103
- const args = gate.args ?? defaultArgs;
104
- let stdout = '';
105
- let stderr = '';
106
- let exitCode = 0;
107
- let timedOut = false;
108
- try {
109
- const out = await execFileAsync(bin, args, {
110
- cwd,
111
- env: { ...process.env, ...gate.env },
112
- timeout: timeoutMs,
113
- });
114
- stdout = out.stdout;
115
- stderr = out.stderr;
116
- }
117
- catch (err) {
118
- const e = err;
119
- stdout = e.stdout ?? '';
120
- stderr = e.stderr ?? '';
121
- exitCode = typeof e.status === 'number' ? e.status : 1;
122
- if (e.killed || e.code === 'ETIMEDOUT') {
123
- timedOut = true;
124
- }
125
- }
126
- const durationMs = Date.now() - startMs;
127
- const combined = truncateString(`${stdout}\n${stderr}`.trim(), MAX_EVIDENCE_BYTES);
128
- if (timedOut) {
129
- // T12137 (gh#1270): 'error', NOT 'fail'. A gate that was killed produced no
130
- // verdict — it did not find a problem, it never finished looking. Recording
131
- // it as 'fail' makes a false red indistinguishable from a real one, which
132
- // costs an agent the work it actually completed. The distinction already
133
- // existed in `AcceptanceGateResult['result']` and was simply unused here.
134
- return makeResult(index, gate, 'error', durationMs, combined, `Gate did not finish: killed after ${timeoutMs}ms. This is NOT a failure — the check never produced a verdict. Re-run it, or raise the timeout.`);
135
- }
136
- if (exitCode !== 0) {
137
- return makeResult(index, gate, 'fail', durationMs, combined, `Exit code ${exitCode}`);
138
- }
139
- // For 'pass' mode, check for obvious failure indicators in stdout
140
- if (gate.expect === 'pass') {
141
- const failPattern = /\bFAIL\b|failing|Error:/i;
142
- if (failPattern.test(stdout)) {
143
- return makeResult(index, gate, 'fail', durationMs, combined, 'Failure pattern detected in output');
144
- }
145
- }
146
- return makeResult(index, gate, 'pass', durationMs, combined);
506
+ /** Execute a process gate through the existing captured resource service. */
507
+ async function captureGateCommand(command, args, cwd, timeoutMs, context, overlay) {
508
+ const env = gateEnvironment(context.env ?? process.env, overlay);
509
+ return captureWrapped(command, args, {
510
+ ...context,
511
+ cwd,
512
+ env,
513
+ execution: {
514
+ ...context.execution,
515
+ deadlineAt: Math.min(context.execution.deadlineAt, Date.now() + timeoutMs),
516
+ },
517
+ });
518
+ }
519
+ /** Preserve target failure independently from launch, cancellation and cleanup failures. */
520
+ function processGateResult(index, gate, captured, verdict, reason) {
521
+ const incomplete = !captured.started ||
522
+ !captured.targetCloseObserved ||
523
+ captured.exitCode === null ||
524
+ captured.signal !== null ||
525
+ captured.error !== null ||
526
+ captured.stopped !== null ||
527
+ captured.outputTruncated ||
528
+ captured.cleanupErrors.length > 0;
529
+ return {
530
+ ...makeResult(index, gate, incomplete ? 'error' : verdict, captured.durationMs, truncateString(`${captured.stdout}\n${captured.stderr}`.trim(), MAX_EVIDENCE_CHARS), incomplete
531
+ ? `Gate did not finish: ${captured.error ?? captured.stopped ?? captured.signal ?? (captured.cleanupErrors.join('; ') || 'target outcome missing')}. This is NOT a failure verdict.`
532
+ : reason),
533
+ execution: captured,
534
+ };
535
+ }
536
+ async function runTestGate(gate, index, projectRoot, timeoutMs, context) {
537
+ if (gate.minCount !== undefined && gate.minCount > 0)
538
+ throw new Error('Minimum test count requires a supported structured count result; exit0 alone cannot prove it');
539
+ const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
540
+ const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context, gate.env);
541
+ const passed = captured.exitCode === 0 &&
542
+ (gate.expect === 'exit0' || !/\bFAIL\b|failing|Error:/i.test(captured.stdout));
543
+ return processGateResult(index, gate, captured, passed ? 'pass' : 'fail', captured.exitCode === 0
544
+ ? 'Failure pattern detected in output'
545
+ : `Exit code ${captured.exitCode}`);
147
546
  }
148
547
  // ─── File gate ────────────────────────────────────────────────────────────────
149
- async function runFileGate(gate, index, projectRoot) {
548
+ async function runFileGate(gate, index, projectRoot, context) {
150
549
  const startMs = Date.now();
151
- // Resolve file path: either from gate.path or via AttachmentStore by sha256.
550
+ let attachmentBytes;
152
551
  let filePath;
153
552
  if (gate.attachmentSha256) {
154
- // Resolve via AttachmentStore — fail fast if the attachment is missing.
155
553
  const store = createAttachmentStore();
156
- const result = await store.get(gate.attachmentSha256);
157
- if (!result) {
158
- const durationMs = Date.now() - startMs;
159
- return makeResult(index, gate, 'fail', durationMs, `attachment sha256=${gate.attachmentSha256} not found`, `Attachment not found: ${gate.attachmentSha256}`);
160
- }
161
- // Derive the on-disk path from AttachmentStore internals via the metadata.
162
- const mime = 'mime' in result.metadata.attachment
163
- ? result.metadata.attachment.mime
164
- : 'application/octet-stream';
165
- // Reconstruct path: .cleo/attachments/sha256/<prefix>/<rest>.<ext>
166
- const { getCleoDirAbsolute } = await import('../paths.js');
167
- const { join: pathJoin } = await import('node:path');
168
- const cleoDir = getCleoDirAbsolute(projectRoot);
169
- const sha256 = gate.attachmentSha256;
170
- const extMap = {
171
- 'text/markdown': '.md',
172
- 'text/plain': '.txt',
173
- 'application/json': '.json',
174
- 'application/pdf': '.pdf',
175
- 'text/html': '.html',
176
- };
177
- const ext = extMap[mime] ?? '.bin';
178
- filePath = pathJoin(cleoDir, 'attachments', 'sha256', sha256.slice(0, 2), `${sha256.slice(2)}${ext}`);
554
+ const metadata = await store.getMetadata(gate.attachmentSha256, projectRoot);
555
+ assertGateActive(context);
556
+ if (!metadata)
557
+ return makeResult(index, gate, 'fail', Date.now() - startMs, undefined, 'Attachment not found');
558
+ const attachment = metadata.attachment;
559
+ if (!('size' in attachment) || !Number.isSafeInteger(attachment.size) || attachment.size < 0)
560
+ throw new Error('Attachment gate requires declared byte size for bounded retrieval');
561
+ if (attachment.size > (context.maxOutputBytes ?? 1_048_576))
562
+ throw new Error('Attachment gate exceeds declared byte limit');
563
+ const result = await store.get(gate.attachmentSha256, projectRoot);
564
+ assertGateActive(context);
565
+ if (!result)
566
+ throw new Error('Attachment metadata exists but authenticated bytes are unavailable');
567
+ if (result.bytes.length > (context.maxOutputBytes ?? 1_048_576))
568
+ throw new Error('Attachment bytes exceed declared limit');
569
+ attachmentBytes = result.bytes;
570
+ filePath = `sha256:${gate.attachmentSha256}`;
179
571
  }
180
572
  else if (gate.path) {
181
573
  filePath = isAbsolute(gate.path) ? gate.path : join(projectRoot, gate.path);
182
574
  }
183
- else {
184
- const durationMs = Date.now() - startMs;
185
- return makeResult(index, gate, 'fail', durationMs, 'FileGate requires either path or attachmentSha256', 'FileGate missing path and attachmentSha256');
186
- }
575
+ else
576
+ throw new Error('FileGate requires path or attachmentSha256');
187
577
  const failures = [];
188
- const fileContent = null;
578
+ let fileContent = attachmentBytes;
189
579
  let fileSize = 0;
190
580
  let fileExists = false;
191
581
  // Check existence first
192
582
  try {
193
- const st = await stat(filePath);
583
+ const st = attachmentBytes ? { size: attachmentBytes.length } : await stat(filePath);
194
584
  fileExists = true;
195
585
  fileSize = st.size;
196
586
  }
197
- catch {
587
+ catch (error) {
588
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
589
+ throw error;
198
590
  fileExists = false;
199
591
  }
200
592
  for (const assertion of gate.assertions) {
201
- const failure = await checkFileAssertion(assertion, filePath, fileExists, fileSize, () => {
202
- if (fileContent === null) {
203
- return readFile(filePath, 'utf-8').catch(() => '');
593
+ assertGateActive(context);
594
+ if (['contains', 'matches', 'sha256'].includes(assertion.type) &&
595
+ fileSize > (context.maxOutputBytes ?? 1_048_576))
596
+ throw new Error('File gate content exceeds declared byte limit');
597
+ const failure = await checkFileAssertion(assertion, filePath, fileExists, fileSize, async () => {
598
+ if (fileContent === undefined) {
599
+ const limit = context.maxOutputBytes ?? 1_048_576;
600
+ const handle = await open(filePath, 'r');
601
+ try {
602
+ const chunks = [];
603
+ let bytes = 0;
604
+ for (;;) {
605
+ assertGateActive(context);
606
+ const chunk = Buffer.alloc(Math.min(65536, limit - bytes + 1));
607
+ const read = await handle.read(chunk, 0, chunk.length, null);
608
+ assertGateActive(context);
609
+ if (read.bytesRead === 0)
610
+ break;
611
+ bytes += read.bytesRead;
612
+ if (bytes > limit)
613
+ throw new Error('File content exceeds declared byte limit');
614
+ chunks.push(chunk.subarray(0, read.bytesRead));
615
+ }
616
+ fileContent = Buffer.concat(chunks);
617
+ }
618
+ finally {
619
+ await handle.close();
620
+ }
204
621
  }
205
- return Promise.resolve(fileContent);
622
+ return fileContent;
206
623
  });
624
+ assertGateActive(context);
207
625
  if (failure) {
208
626
  failures.push(failure);
209
627
  }
@@ -211,7 +629,7 @@ async function runFileGate(gate, index, projectRoot) {
211
629
  const durationMs = Date.now() - startMs;
212
630
  if (failures.length > 0) {
213
631
  const evidence = `path=${filePath}\n${failures.join('\n')}`;
214
- return makeResult(index, gate, 'fail', durationMs, truncateString(evidence, MAX_EVIDENCE_BYTES), failures[0]);
632
+ return makeResult(index, gate, 'fail', durationMs, truncateString(evidence, MAX_EVIDENCE_CHARS), failures[0]);
215
633
  }
216
634
  return makeResult(index, gate, 'pass', durationMs, `path=${filePath} — all assertions passed`);
217
635
  }
@@ -245,7 +663,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
245
663
  case 'contains': {
246
664
  if (!fileExists)
247
665
  return `File does not exist: ${filePath}`;
248
- const content = await getContent();
666
+ const content = (await getContent()).toString('utf8');
249
667
  return content.includes(assertion.value)
250
668
  ? null
251
669
  : `File does not contain: ${JSON.stringify(assertion.value)}`;
@@ -253,7 +671,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
253
671
  case 'matches': {
254
672
  if (!fileExists)
255
673
  return `File does not exist: ${filePath}`;
256
- const content = await getContent();
674
+ const content = (await getContent()).toString('utf8');
257
675
  const re = new RegExp(assertion.regex, assertion.flags);
258
676
  return re.test(content)
259
677
  ? null
@@ -262,7 +680,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
262
680
  case 'sha256': {
263
681
  if (!fileExists)
264
682
  return `File does not exist: ${filePath}`;
265
- const raw = await readFile(filePath);
683
+ const raw = await getContent();
266
684
  const hash = createHash('sha256').update(raw).digest('hex');
267
685
  return hash === assertion.value
268
686
  ? null
@@ -276,60 +694,19 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
276
694
  }
277
695
  }
278
696
  // ─── Command gate ─────────────────────────────────────────────────────────────
279
- async function runCommandGate(gate, index, projectRoot, timeoutMs) {
280
- const startMs = Date.now();
281
- const cwd = resolveCwd(projectRoot, gate.cwd);
282
- const expectedExitCode = gate.exitCode ?? 0;
283
- let stdout = '';
284
- let stderr = '';
285
- let actualExitCode = 0;
286
- let timedOut = false;
287
- try {
288
- const out = await execFileAsync(gate.cmd, gate.args ?? [], {
289
- cwd,
290
- env: { ...process.env, ...gate.env },
291
- timeout: timeoutMs,
292
- });
293
- stdout = out.stdout;
294
- stderr = out.stderr;
295
- }
296
- catch (err) {
297
- const e = err;
298
- stdout = e.stdout ?? '';
299
- stderr = e.stderr ?? '';
300
- actualExitCode = typeof e.status === 'number' ? e.status : 1;
301
- if (e.killed || e.code === 'ETIMEDOUT') {
302
- timedOut = true;
303
- }
304
- }
305
- const durationMs = Date.now() - startMs;
306
- const combined = truncateString(`${stdout}\n${stderr}`.trim(), MAX_EVIDENCE_BYTES);
307
- if (timedOut) {
308
- // T12137 (gh#1270): 'error', NOT 'fail'. A gate that was killed produced no
309
- // verdict — it did not find a problem, it never finished looking. Recording
310
- // it as 'fail' makes a false red indistinguishable from a real one, which
311
- // costs an agent the work it actually completed. The distinction already
312
- // existed in `AcceptanceGateResult['result']` and was simply unused here.
313
- return makeResult(index, gate, 'error', durationMs, combined, `Gate did not finish: killed after ${timeoutMs}ms. This is NOT a failure — the check never produced a verdict. Re-run it, or raise the timeout.`);
314
- }
315
- if (actualExitCode !== expectedExitCode) {
316
- return makeResult(index, gate, 'fail', durationMs, combined, `Exit code ${actualExitCode} (expected ${expectedExitCode})`);
317
- }
318
- if (gate.stdoutMatches) {
319
- const re = new RegExp(gate.stdoutMatches);
320
- if (!re.test(stdout)) {
321
- return makeResult(index, gate, 'fail', durationMs, combined, `stdout did not match /${gate.stdoutMatches}/`);
322
- }
323
- }
324
- if (gate.stderrMatches) {
325
- const re = new RegExp(gate.stderrMatches);
326
- if (!re.test(stderr)) {
327
- return makeResult(index, gate, 'fail', durationMs, combined, `stderr did not match /${gate.stderrMatches}/`);
328
- }
329
- }
330
- return makeResult(index, gate, 'pass', durationMs, combined);
697
+ async function runCommandGate(gate, index, projectRoot, timeoutMs, context) {
698
+ const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
699
+ const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context, gate.env);
700
+ const expected = gate.exitCode ?? 0;
701
+ let reason = captured.exitCode === expected
702
+ ? undefined
703
+ : `Exit code ${captured.exitCode} (expected ${expected})`;
704
+ if (!reason && gate.stdoutMatches && !new RegExp(gate.stdoutMatches).test(captured.stdout))
705
+ reason = 'stdout pattern did not match';
706
+ if (!reason && gate.stderrMatches && !new RegExp(gate.stderrMatches).test(captured.stderr))
707
+ reason = 'stderr pattern did not match';
708
+ return processGateResult(index, gate, captured, reason ? 'fail' : 'pass', reason);
331
709
  }
332
- // ─── Lint gate ────────────────────────────────────────────────────────────────
333
710
  /** Tool-specific CLI arguments and failure patterns. */
334
711
  const LINT_TOOL_DEFAULTS = {
335
712
  biome: { cmd: 'biome', defaultArgs: ['check', '.'] },
@@ -343,81 +720,57 @@ const LINT_TOOL_DEFAULTS = {
343
720
  errorPattern: /^error/m,
344
721
  },
345
722
  };
346
- async function runLintGate(gate, index, projectRoot, timeoutMs) {
347
- const startMs = Date.now();
348
- const cwd = resolveCwd(projectRoot, gate.cwd);
349
- const toolDef = LINT_TOOL_DEFAULTS[gate.tool];
350
- const args = gate.args ?? toolDef.defaultArgs;
351
- let stdout = '';
352
- let stderr = '';
353
- let exitCode = 0;
354
- let timedOut = false;
355
- try {
356
- const out = await execFileAsync(toolDef.cmd, args, { cwd, timeout: timeoutMs });
357
- stdout = out.stdout;
358
- stderr = out.stderr;
359
- }
360
- catch (err) {
361
- const e = err;
362
- stdout = e.stdout ?? '';
363
- stderr = e.stderr ?? '';
364
- exitCode = typeof e.status === 'number' ? e.status : 1;
365
- if (e.killed || e.code === 'ETIMEDOUT') {
366
- timedOut = true;
367
- }
368
- }
369
- const durationMs = Date.now() - startMs;
370
- const combined = truncateString(`${stdout}\n${stderr}`.trim(), MAX_EVIDENCE_BYTES);
371
- if (timedOut) {
372
- // T12137 (gh#1270): 'error', NOT 'fail'. A gate that was killed produced no
373
- // verdict — it did not find a problem, it never finished looking. Recording
374
- // it as 'fail' makes a false red indistinguishable from a real one, which
375
- // costs an agent the work it actually completed. The distinction already
376
- // existed in `AcceptanceGateResult['result']` and was simply unused here.
377
- return makeResult(index, gate, 'error', durationMs, combined, `Gate did not finish: killed after ${timeoutMs}ms. This is NOT a failure — the check never produced a verdict. Re-run it, or raise the timeout.`);
378
- }
379
- // For 'noErrors' mode, exit code 0 = pass (warnings tolerated)
380
- if (gate.expect === 'noErrors') {
381
- const passed = exitCode === 0;
382
- return makeResult(index, gate, passed ? 'pass' : 'fail', durationMs, combined, passed ? undefined : `${gate.tool} reported errors (exit ${exitCode})`);
383
- }
384
- // For 'clean' mode, check exit code AND optional error pattern
385
- if (exitCode !== 0) {
386
- return makeResult(index, gate, 'fail', durationMs, combined, `${gate.tool} exited with ${exitCode}`);
387
- }
388
- if (toolDef.errorPattern?.test(combined)) {
389
- return makeResult(index, gate, 'fail', durationMs, combined, `${gate.tool} output matched error pattern`);
390
- }
391
- return makeResult(index, gate, 'pass', durationMs, combined);
723
+ async function runLintGate(gate, index, projectRoot, timeoutMs, context) {
724
+ const tool = LINT_TOOL_DEFAULTS[gate.tool];
725
+ const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
726
+ const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context);
727
+ const passed = captured.exitCode === 0 &&
728
+ (gate.expect === 'noErrors' || !tool.errorPattern?.test(captured.stdout + captured.stderr));
729
+ return processGateResult(index, gate, captured, passed ? 'pass' : 'fail', `${gate.tool} reported errors (exit ${captured.exitCode})`);
392
730
  }
393
731
  // ─── HTTP gate ────────────────────────────────────────────────────────────────
394
- async function runHttpGate(gate, index, _projectRoot, timeoutMs) {
732
+ async function runHttpGate(gate, index, _projectRoot, timeoutMs, context) {
395
733
  const startMs = Date.now();
396
- let serverProcess = null;
397
- // Start the server if configured
398
- if (gate.startCommand) {
399
- const { spawn } = await import('node:child_process');
400
- const [cmd, ...args] = gate.startCommand.split(' ');
401
- serverProcess = spawn(cmd, args, { detached: true, stdio: 'ignore' });
402
- serverProcess.unref();
403
- // Wait for startup delay
404
- const delayMs = gate.startupDelayMs ?? 2_000;
405
- await new Promise((resolve) => setTimeout(resolve, delayMs));
406
- }
734
+ if (gate.startCommand)
735
+ throw new Error('HTTP server startup requires an admitted owned service lifetime; unowned detached startup is unavailable');
407
736
  let statusCode = 0;
408
737
  let body = '';
409
738
  let errorMsg;
410
739
  try {
411
740
  const controller = new AbortController();
412
- const timer = setTimeout(() => controller.abort(), timeoutMs);
741
+ const timer = setTimeout(() => controller.abort(), Math.max(0, Math.min(timeoutMs, context.execution.deadlineAt - Date.now())));
413
742
  try {
414
743
  const response = await fetch(gate.url, {
415
744
  method: gate.method ?? 'GET',
416
745
  headers: gate.headers,
417
- signal: controller.signal,
746
+ signal: context.execution.signal
747
+ ? AbortSignal.any([controller.signal, context.execution.signal])
748
+ : controller.signal,
418
749
  });
419
750
  statusCode = response.status;
420
- body = await response.text();
751
+ if (response.body) {
752
+ const reader = response.body.getReader();
753
+ const chunks = [];
754
+ let bytes = 0;
755
+ try {
756
+ for (;;) {
757
+ assertGateActive(context);
758
+ const next = await reader.read();
759
+ if (next.done)
760
+ break;
761
+ bytes += next.value.byteLength;
762
+ if (bytes > (context.maxOutputBytes ?? 1_048_576))
763
+ throw new Error('HTTP body exceeds declared byte limit');
764
+ chunks.push(next.value);
765
+ }
766
+ body = Buffer.concat(chunks).toString('utf8');
767
+ }
768
+ finally {
769
+ await reader.cancel();
770
+ reader.releaseLock();
771
+ }
772
+ }
773
+ assertGateActive(context);
421
774
  }
422
775
  finally {
423
776
  clearTimeout(timer);
@@ -426,19 +779,9 @@ async function runHttpGate(gate, index, _projectRoot, timeoutMs) {
426
779
  catch (err) {
427
780
  errorMsg = err instanceof Error ? err.message : String(err);
428
781
  }
429
- finally {
430
- if (serverProcess) {
431
- try {
432
- serverProcess.kill();
433
- }
434
- catch {
435
- // Best-effort cleanup
436
- }
437
- }
438
- }
439
782
  const durationMs = Date.now() - startMs;
440
783
  if (errorMsg) {
441
- return makeResult(index, gate, 'fail', durationMs, errorMsg, errorMsg);
784
+ return makeResult(index, gate, 'error', durationMs, errorMsg, `HTTP gate did not finish: ${errorMsg}`);
442
785
  }
443
786
  if (statusCode !== gate.status) {
444
787
  return makeResult(index, gate, 'fail', durationMs, `HTTP ${statusCode}`, `Expected status ${gate.status}, got ${statusCode}`);