@cleocode/core 2026.9.8 → 2026.9.10

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 (569) 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/compliance/protocol-rules.d.ts.map +1 -1
  9. package/dist/compliance/protocol-rules.js +3 -1
  10. package/dist/compliance/protocol-rules.js.map +1 -1
  11. package/dist/dispatch/mutate-projection.d.ts.map +1 -1
  12. package/dist/dispatch/mutate-projection.js +12 -0
  13. package/dist/dispatch/mutate-projection.js.map +1 -1
  14. package/dist/dispatch/mvi-projection.d.ts +29 -6
  15. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  16. package/dist/dispatch/mvi-projection.js +109 -139
  17. package/dist/dispatch/mvi-projection.js.map +1 -1
  18. package/dist/docs/docs-read-model.d.ts +3 -2
  19. package/dist/docs/docs-read-model.d.ts.map +1 -1
  20. package/dist/docs/docs-read-model.js +4 -3
  21. package/dist/docs/docs-read-model.js.map +1 -1
  22. package/dist/docs/export-document.js +5129 -2297
  23. package/dist/docs/export-document.js.map +3 -3
  24. package/dist/docs/projection.d.ts +67 -0
  25. package/dist/docs/projection.d.ts.map +1 -0
  26. package/dist/docs/projection.js +370 -0
  27. package/dist/docs/projection.js.map +1 -0
  28. package/dist/doctor/acceptance-drift.d.ts +6 -2
  29. package/dist/doctor/acceptance-drift.d.ts.map +1 -1
  30. package/dist/doctor/acceptance-drift.js +43 -5
  31. package/dist/doctor/acceptance-drift.js.map +1 -1
  32. package/dist/doctor/knowledge-summary.d.ts +41 -0
  33. package/dist/doctor/knowledge-summary.d.ts.map +1 -0
  34. package/dist/doctor/knowledge-summary.js +81 -0
  35. package/dist/doctor/knowledge-summary.js.map +1 -0
  36. package/dist/doctor/knowledge.d.ts +192 -0
  37. package/dist/doctor/knowledge.d.ts.map +1 -0
  38. package/dist/doctor/knowledge.js +1864 -0
  39. package/dist/doctor/knowledge.js.map +1 -0
  40. package/dist/git/work-tree.d.ts +169 -0
  41. package/dist/git/work-tree.d.ts.map +1 -0
  42. package/dist/git/work-tree.js +341 -0
  43. package/dist/git/work-tree.js.map +1 -0
  44. package/dist/hooks/handlers/task-hooks.d.ts +8 -5
  45. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  46. package/dist/hooks/handlers/task-hooks.js +15 -60
  47. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  48. package/dist/identity/cleo-identity.d.ts +13 -0
  49. package/dist/identity/cleo-identity.d.ts.map +1 -1
  50. package/dist/identity/cleo-identity.js +20 -6
  51. package/dist/identity/cleo-identity.js.map +1 -1
  52. package/dist/injection.d.ts.map +1 -1
  53. package/dist/injection.js +29 -13
  54. package/dist/injection.js.map +1 -1
  55. package/dist/internal.d.ts +3 -0
  56. package/dist/internal.d.ts.map +1 -1
  57. package/dist/internal.js +4 -0
  58. package/dist/internal.js.map +1 -1
  59. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  60. package/dist/lifecycle/verification/satisfies-validator.js +124 -120
  61. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  62. package/dist/llm/plugin-facade.js +4047 -1726
  63. package/dist/llm/plugin-facade.js.map +3 -3
  64. package/dist/memory/brain-backfill.d.ts +27 -12
  65. package/dist/memory/brain-backfill.d.ts.map +1 -1
  66. package/dist/memory/brain-backfill.js +325 -134
  67. package/dist/memory/brain-backfill.js.map +1 -1
  68. package/dist/memory/brain-doctor.d.ts +21 -0
  69. package/dist/memory/brain-doctor.d.ts.map +1 -1
  70. package/dist/memory/brain-doctor.js +111 -53
  71. package/dist/memory/brain-doctor.js.map +1 -1
  72. package/dist/memory/brain-links.d.ts.map +1 -1
  73. package/dist/memory/brain-links.js +31 -4
  74. package/dist/memory/brain-links.js.map +1 -1
  75. package/dist/memory/brain-reasoning.d.ts.map +1 -1
  76. package/dist/memory/brain-reasoning.js +28 -2
  77. package/dist/memory/brain-reasoning.js.map +1 -1
  78. package/dist/memory/brain-reconciler.d.ts +14 -19
  79. package/dist/memory/brain-reconciler.d.ts.map +1 -1
  80. package/dist/memory/brain-reconciler.js +70 -97
  81. package/dist/memory/brain-reconciler.js.map +1 -1
  82. package/dist/memory/brain-search.d.ts +2 -0
  83. package/dist/memory/brain-search.d.ts.map +1 -1
  84. package/dist/memory/brain-search.js +59 -29
  85. package/dist/memory/brain-search.js.map +1 -1
  86. package/dist/memory/brain-similarity.d.ts.map +1 -1
  87. package/dist/memory/brain-similarity.js +5 -4
  88. package/dist/memory/brain-similarity.js.map +1 -1
  89. package/dist/memory/brain-stub-prune.d.ts +31 -9
  90. package/dist/memory/brain-stub-prune.d.ts.map +1 -1
  91. package/dist/memory/brain-stub-prune.js +65 -18
  92. package/dist/memory/brain-stub-prune.js.map +1 -1
  93. package/dist/memory/brain-writer-handlers.d.ts +10 -1
  94. package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
  95. package/dist/memory/brain-writer-handlers.js +23 -3
  96. package/dist/memory/brain-writer-handlers.js.map +1 -1
  97. package/dist/memory/brain-writer-thread.d.ts +11 -1
  98. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  99. package/dist/memory/brain-writer-thread.js +87 -20
  100. package/dist/memory/brain-writer-thread.js.map +1 -1
  101. package/dist/memory/brain-writer-worker.js +8 -1
  102. package/dist/memory/brain-writer-worker.js.map +1 -1
  103. package/dist/memory/decision-cross-link.d.ts +39 -0
  104. package/dist/memory/decision-cross-link.d.ts.map +1 -1
  105. package/dist/memory/decision-cross-link.js +316 -0
  106. package/dist/memory/decision-cross-link.js.map +1 -1
  107. package/dist/memory/decisions.d.ts +4 -1
  108. package/dist/memory/decisions.d.ts.map +1 -1
  109. package/dist/memory/decisions.js +7 -24
  110. package/dist/memory/decisions.js.map +1 -1
  111. package/dist/memory/dispatch-trace.d.ts +12 -5
  112. package/dist/memory/dispatch-trace.d.ts.map +1 -1
  113. package/dist/memory/dispatch-trace.js +17 -7
  114. package/dist/memory/dispatch-trace.js.map +1 -1
  115. package/dist/memory/eligibility.d.ts +51 -0
  116. package/dist/memory/eligibility.d.ts.map +1 -0
  117. package/dist/memory/eligibility.js +77 -0
  118. package/dist/memory/eligibility.js.map +1 -0
  119. package/dist/memory/engine-compat.d.ts +2 -0
  120. package/dist/memory/engine-compat.d.ts.map +1 -1
  121. package/dist/memory/engine-compat.js +10 -0
  122. package/dist/memory/engine-compat.js.map +1 -1
  123. package/dist/memory/extraction-gate.d.ts +21 -7
  124. package/dist/memory/extraction-gate.d.ts.map +1 -1
  125. package/dist/memory/extraction-gate.js +178 -216
  126. package/dist/memory/extraction-gate.js.map +1 -1
  127. package/dist/memory/graph-auto-populate.d.ts +38 -5
  128. package/dist/memory/graph-auto-populate.d.ts.map +1 -1
  129. package/dist/memory/graph-auto-populate.js +182 -60
  130. package/dist/memory/graph-auto-populate.js.map +1 -1
  131. package/dist/memory/graph-memory-bridge.d.ts +14 -1
  132. package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
  133. package/dist/memory/graph-memory-bridge.js +66 -28
  134. package/dist/memory/graph-memory-bridge.js.map +1 -1
  135. package/dist/memory/index.d.ts +1 -19
  136. package/dist/memory/index.d.ts.map +1 -1
  137. package/dist/memory/index.js +1 -50
  138. package/dist/memory/index.js.map +1 -1
  139. package/dist/memory/learnings.d.ts.map +1 -1
  140. package/dist/memory/learnings.js +2 -23
  141. package/dist/memory/learnings.js.map +1 -1
  142. package/dist/memory/manifest-filter.d.ts +21 -0
  143. package/dist/memory/manifest-filter.d.ts.map +1 -0
  144. package/dist/memory/manifest-filter.js +51 -0
  145. package/dist/memory/manifest-filter.js.map +1 -0
  146. package/dist/memory/manifest-ingestion.d.ts +28 -30
  147. package/dist/memory/manifest-ingestion.d.ts.map +1 -1
  148. package/dist/memory/manifest-ingestion.js +104 -235
  149. package/dist/memory/manifest-ingestion.js.map +1 -1
  150. package/dist/memory/observer-reflector.d.ts +2 -0
  151. package/dist/memory/observer-reflector.d.ts.map +1 -1
  152. package/dist/memory/observer-reflector.js +6 -49
  153. package/dist/memory/observer-reflector.js.map +1 -1
  154. package/dist/memory/patterns.d.ts +10 -0
  155. package/dist/memory/patterns.d.ts.map +1 -1
  156. package/dist/memory/patterns.js +25 -33
  157. package/dist/memory/patterns.js.map +1 -1
  158. package/dist/memory/pipeline-manifest-sqlite.d.ts +72 -9
  159. package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
  160. package/dist/memory/pipeline-manifest-sqlite.js +508 -412
  161. package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
  162. package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
  163. package/dist/memory/retrieval/build-retrieval-bundle.js +16 -15
  164. package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
  165. package/dist/memory/retrieval/fetch.d.ts +7 -0
  166. package/dist/memory/retrieval/fetch.d.ts.map +1 -1
  167. package/dist/memory/retrieval/fetch.js +14 -11
  168. package/dist/memory/retrieval/fetch.js.map +1 -1
  169. package/dist/memory/retrieval/observe.d.ts +7 -1
  170. package/dist/memory/retrieval/observe.d.ts.map +1 -1
  171. package/dist/memory/retrieval/observe.js +58 -4
  172. package/dist/memory/retrieval/observe.js.map +1 -1
  173. package/dist/memory/retrieval/search.d.ts +14 -1
  174. package/dist/memory/retrieval/search.d.ts.map +1 -1
  175. package/dist/memory/retrieval/search.js +133 -69
  176. package/dist/memory/retrieval/search.js.map +1 -1
  177. package/dist/memory/retrieval/telemetry.d.ts +31 -0
  178. package/dist/memory/retrieval/telemetry.d.ts.map +1 -0
  179. package/dist/memory/retrieval/telemetry.js +53 -0
  180. package/dist/memory/retrieval/telemetry.js.map +1 -0
  181. package/dist/nexus/analyze-orchestrator.d.ts +41 -9
  182. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  183. package/dist/nexus/analyze-orchestrator.js +257 -58
  184. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  185. package/dist/nexus/api-contracts.d.ts +2 -6
  186. package/dist/nexus/api-contracts.d.ts.map +1 -1
  187. package/dist/nexus/api-contracts.js +11 -2
  188. package/dist/nexus/api-contracts.js.map +1 -1
  189. package/dist/nexus/context.d.ts.map +1 -1
  190. package/dist/nexus/context.js +14 -37
  191. package/dist/nexus/context.js.map +1 -1
  192. package/dist/nexus/diff.d.ts.map +1 -1
  193. package/dist/nexus/diff.js +2 -1
  194. package/dist/nexus/diff.js.map +1 -1
  195. package/dist/nexus/identity.d.ts +29 -3
  196. package/dist/nexus/identity.d.ts.map +1 -1
  197. package/dist/nexus/identity.js +63 -12
  198. package/dist/nexus/identity.js.map +1 -1
  199. package/dist/nexus/impact.d.ts +21 -19
  200. package/dist/nexus/impact.d.ts.map +1 -1
  201. package/dist/nexus/impact.js +96 -192
  202. package/dist/nexus/impact.js.map +1 -1
  203. package/dist/nexus/knowledge.d.ts +80 -0
  204. package/dist/nexus/knowledge.d.ts.map +1 -0
  205. package/dist/nexus/knowledge.js +786 -0
  206. package/dist/nexus/knowledge.js.map +1 -0
  207. package/dist/nexus/living-brain.d.ts +8 -5
  208. package/dist/nexus/living-brain.d.ts.map +1 -1
  209. package/dist/nexus/living-brain.js +252 -188
  210. package/dist/nexus/living-brain.js.map +1 -1
  211. package/dist/nexus/registry.d.ts +11 -2
  212. package/dist/nexus/registry.d.ts.map +1 -1
  213. package/dist/nexus/registry.js +144 -128
  214. package/dist/nexus/registry.js.map +1 -1
  215. package/dist/nexus/source-roots.d.ts +16 -0
  216. package/dist/nexus/source-roots.d.ts.map +1 -0
  217. package/dist/nexus/source-roots.js +174 -0
  218. package/dist/nexus/source-roots.js.map +1 -0
  219. package/dist/nexus/task-evidence.d.ts +22 -0
  220. package/dist/nexus/task-evidence.d.ts.map +1 -0
  221. package/dist/nexus/task-evidence.js +210 -0
  222. package/dist/nexus/task-evidence.js.map +1 -0
  223. package/dist/nexus/tasks-bridge.d.ts +14 -7
  224. package/dist/nexus/tasks-bridge.d.ts.map +1 -1
  225. package/dist/nexus/tasks-bridge.js +81 -40
  226. package/dist/nexus/tasks-bridge.js.map +1 -1
  227. package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
  228. package/dist/orchestrate/lifecycle-ops.js +170 -120
  229. package/dist/orchestrate/lifecycle-ops.js.map +1 -1
  230. package/dist/orchestrate/query-ops.d.ts +6 -7
  231. package/dist/orchestrate/query-ops.d.ts.map +1 -1
  232. package/dist/orchestrate/query-ops.js +509 -532
  233. package/dist/orchestrate/query-ops.js.map +1 -1
  234. package/dist/orchestration/bootstrap.d.ts +15 -1
  235. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  236. package/dist/orchestration/bootstrap.js +21 -10
  237. package/dist/orchestration/bootstrap.js.map +1 -1
  238. package/dist/orchestration/index.d.ts +35 -3
  239. package/dist/orchestration/index.d.ts.map +1 -1
  240. package/dist/orchestration/index.js +64 -35
  241. package/dist/orchestration/index.js.map +1 -1
  242. package/dist/orchestration/lead-rollup.d.ts +24 -1
  243. package/dist/orchestration/lead-rollup.d.ts.map +1 -1
  244. package/dist/orchestration/lead-rollup.js +31 -11
  245. package/dist/orchestration/lead-rollup.js.map +1 -1
  246. package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
  247. package/dist/orchestration/spawn-prompt.js +25 -45
  248. package/dist/orchestration/spawn-prompt.js.map +1 -1
  249. package/dist/orchestration/unblock.d.ts +15 -1
  250. package/dist/orchestration/unblock.d.ts.map +1 -1
  251. package/dist/orchestration/unblock.js +23 -12
  252. package/dist/orchestration/unblock.js.map +1 -1
  253. package/dist/orchestration/validate-spawn.d.ts.map +1 -1
  254. package/dist/orchestration/validate-spawn.js +3 -3
  255. package/dist/orchestration/validate-spawn.js.map +1 -1
  256. package/dist/orchestration/waves.d.ts +29 -13
  257. package/dist/orchestration/waves.d.ts.map +1 -1
  258. package/dist/orchestration/waves.js +43 -55
  259. package/dist/orchestration/waves.js.map +1 -1
  260. package/dist/paths.d.ts +17 -163
  261. package/dist/paths.d.ts.map +1 -1
  262. package/dist/paths.js +145 -496
  263. package/dist/paths.js.map +1 -1
  264. package/dist/project-info.d.ts +2 -11
  265. package/dist/project-info.d.ts.map +1 -1
  266. package/dist/project-info.js +3 -31
  267. package/dist/project-info.js.map +1 -1
  268. package/dist/project-scope.d.ts +239 -0
  269. package/dist/project-scope.d.ts.map +1 -0
  270. package/dist/project-scope.js +473 -0
  271. package/dist/project-scope.js.map +1 -0
  272. package/dist/release/pr-evidence.d.ts +23 -4
  273. package/dist/release/pr-evidence.d.ts.map +1 -1
  274. package/dist/release/pr-evidence.js +65 -9
  275. package/dist/release/pr-evidence.js.map +1 -1
  276. package/dist/render/nexus/graph/index.d.ts.map +1 -1
  277. package/dist/render/nexus/graph/index.js +4 -2
  278. package/dist/render/nexus/graph/index.js.map +1 -1
  279. package/dist/render/orchestration/audit-reconstruct.d.ts +11 -0
  280. package/dist/render/orchestration/audit-reconstruct.d.ts.map +1 -1
  281. package/dist/render/orchestration/audit-reconstruct.js +21 -6
  282. package/dist/render/orchestration/audit-reconstruct.js.map +1 -1
  283. package/dist/render/tasks/show.js +2 -1
  284. package/dist/render/tasks/show.js.map +1 -1
  285. package/dist/resources/spawn-wrapper.d.ts +82 -14
  286. package/dist/resources/spawn-wrapper.d.ts.map +1 -1
  287. package/dist/resources/spawn-wrapper.js +585 -27
  288. package/dist/resources/spawn-wrapper.js.map +1 -1
  289. package/dist/selfimprove/fix-gen.js +4055 -1734
  290. package/dist/selfimprove/fix-gen.js.map +3 -3
  291. package/dist/sequence/index.d.ts.map +1 -1
  292. package/dist/sequence/index.js +76 -68
  293. package/dist/sequence/index.js.map +1 -1
  294. package/dist/session/canon-lint.d.ts +24 -3
  295. package/dist/session/canon-lint.d.ts.map +1 -1
  296. package/dist/session/canon-lint.js +72 -31
  297. package/dist/session/canon-lint.js.map +1 -1
  298. package/dist/session/engine-ops.d.ts +1 -4
  299. package/dist/session/engine-ops.d.ts.map +1 -1
  300. package/dist/session/engine-ops.js +42 -6
  301. package/dist/session/engine-ops.js.map +1 -1
  302. package/dist/sessions/briefing.d.ts +11 -1
  303. package/dist/sessions/briefing.d.ts.map +1 -1
  304. package/dist/sessions/briefing.js +76 -8
  305. package/dist/sessions/briefing.js.map +1 -1
  306. package/dist/sessions/handoff.d.ts +3 -1
  307. package/dist/sessions/handoff.d.ts.map +1 -1
  308. package/dist/sessions/handoff.js +2 -0
  309. package/dist/sessions/handoff.js.map +1 -1
  310. package/dist/shutdown-deadline.d.ts +24 -33
  311. package/dist/shutdown-deadline.d.ts.map +1 -1
  312. package/dist/shutdown-deadline.js +71 -14
  313. package/dist/shutdown-deadline.js.map +1 -1
  314. package/dist/shutdown.d.ts +21 -10
  315. package/dist/shutdown.d.ts.map +1 -1
  316. package/dist/shutdown.js +89 -42
  317. package/dist/shutdown.js.map +1 -1
  318. package/dist/skills/index.d.ts +1 -1
  319. package/dist/skills/index.d.ts.map +1 -1
  320. package/dist/skills/index.js +1 -1
  321. package/dist/skills/index.js.map +1 -1
  322. package/dist/skills/orchestrator/validator.d.ts +87 -20
  323. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  324. package/dist/skills/orchestrator/validator.js +330 -241
  325. package/dist/skills/orchestrator/validator.js.map +1 -1
  326. package/dist/store/agent-resolver.d.ts.map +1 -1
  327. package/dist/store/agent-resolver.js +16 -9
  328. package/dist/store/agent-resolver.js.map +1 -1
  329. package/dist/store/attachment-store.d.ts +4 -1
  330. package/dist/store/attachment-store.d.ts.map +1 -1
  331. package/dist/store/attachment-store.js +20 -3
  332. package/dist/store/attachment-store.js.map +1 -1
  333. package/dist/store/background-jobs.d.ts +426 -105
  334. package/dist/store/background-jobs.d.ts.map +1 -1
  335. package/dist/store/background-jobs.js +1208 -252
  336. package/dist/store/background-jobs.js.map +1 -1
  337. package/dist/store/background-ops.d.ts +145 -8
  338. package/dist/store/background-ops.d.ts.map +1 -1
  339. package/dist/store/background-ops.js +459 -15
  340. package/dist/store/background-ops.js.map +1 -1
  341. package/dist/store/backup-inspect.d.ts +48 -4
  342. package/dist/store/backup-inspect.d.ts.map +1 -1
  343. package/dist/store/backup-inspect.js +384 -4
  344. package/dist/store/backup-inspect.js.map +1 -1
  345. package/dist/store/converters.d.ts.map +1 -1
  346. package/dist/store/converters.js +19 -2
  347. package/dist/store/converters.js.map +1 -1
  348. package/dist/store/data-accessor.d.ts +9 -1
  349. package/dist/store/data-accessor.d.ts.map +1 -1
  350. package/dist/store/data-accessor.js +22 -5
  351. package/dist/store/data-accessor.js.map +1 -1
  352. package/dist/store/data-safety-central.d.ts.map +1 -1
  353. package/dist/store/data-safety-central.js +272 -232
  354. package/dist/store/data-safety-central.js.map +1 -1
  355. package/dist/store/db-helpers.d.ts +19 -3
  356. package/dist/store/db-helpers.d.ts.map +1 -1
  357. package/dist/store/db-helpers.js +42 -58
  358. package/dist/store/db-helpers.js.map +1 -1
  359. package/dist/store/dual-scope-db.d.ts +63 -4
  360. package/dist/store/dual-scope-db.d.ts.map +1 -1
  361. package/dist/store/dual-scope-db.js +166 -19
  362. package/dist/store/dual-scope-db.js.map +1 -1
  363. package/dist/store/exodus/archive.d.ts +21 -7
  364. package/dist/store/exodus/archive.d.ts.map +1 -1
  365. package/dist/store/exodus/archive.js +60 -18
  366. package/dist/store/exodus/archive.js.map +1 -1
  367. package/dist/store/exodus/migrate.d.ts.map +1 -1
  368. package/dist/store/exodus/migrate.js +27 -23
  369. package/dist/store/exodus/migrate.js.map +1 -1
  370. package/dist/store/exodus/on-open.d.ts +16 -27
  371. package/dist/store/exodus/on-open.d.ts.map +1 -1
  372. package/dist/store/exodus/on-open.js +121 -169
  373. package/dist/store/exodus/on-open.js.map +1 -1
  374. package/dist/store/exodus/recovery.d.ts +90 -0
  375. package/dist/store/exodus/recovery.d.ts.map +1 -0
  376. package/dist/store/exodus/recovery.js +434 -0
  377. package/dist/store/exodus/recovery.js.map +1 -0
  378. package/dist/store/exodus/types.d.ts +15 -0
  379. package/dist/store/exodus/types.d.ts.map +1 -1
  380. package/dist/store/memory-accessor.d.ts +52 -3
  381. package/dist/store/memory-accessor.d.ts.map +1 -1
  382. package/dist/store/memory-accessor.js +122 -7
  383. package/dist/store/memory-accessor.js.map +1 -1
  384. package/dist/store/nexus-sqlite.d.ts +24 -6
  385. package/dist/store/nexus-sqlite.d.ts.map +1 -1
  386. package/dist/store/nexus-sqlite.js +39 -20
  387. package/dist/store/nexus-sqlite.js.map +1 -1
  388. package/dist/store/ports/domain-binding.d.ts.map +1 -1
  389. package/dist/store/ports/domain-binding.js +27 -5
  390. package/dist/store/ports/domain-binding.js.map +1 -1
  391. package/dist/store/schema/background-jobs.d.ts +181 -2
  392. package/dist/store/schema/background-jobs.d.ts.map +1 -1
  393. package/dist/store/schema/background-jobs.js +27 -3
  394. package/dist/store/schema/background-jobs.js.map +1 -1
  395. package/dist/store/schema/cleo-global/skills.d.ts +2 -2
  396. package/dist/store/schema/cleo-project/lifecycle.d.ts +2 -2
  397. package/dist/store/schema/cleo-project/runtime.d.ts +1 -1
  398. package/dist/store/schema/cleo-project/tasks-core.d.ts +1 -1
  399. package/dist/store/schema/lifecycle.d.ts +2 -2
  400. package/dist/store/schema/skills-schema.d.ts +2 -2
  401. package/dist/store/schema/tasks.d.ts +1 -1
  402. package/dist/store/session-store.d.ts +3 -1
  403. package/dist/store/session-store.d.ts.map +1 -1
  404. package/dist/store/session-store.js +252 -207
  405. package/dist/store/session-store.js.map +1 -1
  406. package/dist/store/sqlite-data-accessor.d.ts +39 -4
  407. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  408. package/dist/store/sqlite-data-accessor.js +689 -527
  409. package/dist/store/sqlite-data-accessor.js.map +1 -1
  410. package/dist/store/tasks-sqlite.d.ts +3 -1
  411. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  412. package/dist/store/tasks-sqlite.js +309 -279
  413. package/dist/store/tasks-sqlite.js.map +1 -1
  414. package/dist/store/validation-schemas.d.ts +8 -8
  415. package/dist/store/writer-lease.d.ts +80 -8
  416. package/dist/store/writer-lease.d.ts.map +1 -1
  417. package/dist/store/writer-lease.js +503 -134
  418. package/dist/store/writer-lease.js.map +1 -1
  419. package/dist/system/audit.d.ts +2 -1
  420. package/dist/system/audit.d.ts.map +1 -1
  421. package/dist/system/audit.js +118 -71
  422. package/dist/system/audit.js.map +1 -1
  423. package/dist/system/backup.d.ts.map +1 -1
  424. package/dist/system/backup.js +5 -5
  425. package/dist/system/backup.js.map +1 -1
  426. package/dist/system/inject-generate.js +1 -1
  427. package/dist/task-work/index.d.ts +3 -0
  428. package/dist/task-work/index.d.ts.map +1 -1
  429. package/dist/task-work/index.js +34 -37
  430. package/dist/task-work/index.js.map +1 -1
  431. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  432. package/dist/tasks/ac-coverage-gate.js +4 -0
  433. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  434. package/dist/tasks/ac-immutability.d.ts +7 -4
  435. package/dist/tasks/ac-immutability.d.ts.map +1 -1
  436. package/dist/tasks/ac-immutability.js +19 -17
  437. package/dist/tasks/ac-immutability.js.map +1 -1
  438. package/dist/tasks/ac-table.d.ts +5 -5
  439. package/dist/tasks/ac-table.d.ts.map +1 -1
  440. package/dist/tasks/ac-table.js +26 -12
  441. package/dist/tasks/ac-table.js.map +1 -1
  442. package/dist/tasks/acceptance-input.d.ts +52 -0
  443. package/dist/tasks/acceptance-input.d.ts.map +1 -0
  444. package/dist/tasks/acceptance-input.js +259 -0
  445. package/dist/tasks/acceptance-input.js.map +1 -0
  446. package/dist/tasks/add.d.ts +67 -29
  447. package/dist/tasks/add.d.ts.map +1 -1
  448. package/dist/tasks/add.js +255 -97
  449. package/dist/tasks/add.js.map +1 -1
  450. package/dist/tasks/complete.d.ts +16 -1
  451. package/dist/tasks/complete.d.ts.map +1 -1
  452. package/dist/tasks/complete.js +152 -55
  453. package/dist/tasks/complete.js.map +1 -1
  454. package/dist/tasks/decompose.d.ts +103 -0
  455. package/dist/tasks/decompose.d.ts.map +1 -0
  456. package/dist/tasks/decompose.js +224 -0
  457. package/dist/tasks/decompose.js.map +1 -0
  458. package/dist/tasks/delete.d.ts +5 -8
  459. package/dist/tasks/delete.d.ts.map +1 -1
  460. package/dist/tasks/delete.js +4 -3
  461. package/dist/tasks/delete.js.map +1 -1
  462. package/dist/tasks/dependency-check.d.ts +59 -0
  463. package/dist/tasks/dependency-check.d.ts.map +1 -1
  464. package/dist/tasks/dependency-check.js +73 -0
  465. package/dist/tasks/dependency-check.js.map +1 -1
  466. package/dist/tasks/deps-ready.d.ts +13 -1
  467. package/dist/tasks/deps-ready.d.ts.map +1 -1
  468. package/dist/tasks/deps-ready.js +16 -5
  469. package/dist/tasks/deps-ready.js.map +1 -1
  470. package/dist/tasks/engine-converters.d.ts +7 -0
  471. package/dist/tasks/engine-converters.d.ts.map +1 -1
  472. package/dist/tasks/engine-converters.js +8 -1
  473. package/dist/tasks/engine-converters.js.map +1 -1
  474. package/dist/tasks/evidence.d.ts +88 -27
  475. package/dist/tasks/evidence.d.ts.map +1 -1
  476. package/dist/tasks/evidence.js +344 -43
  477. package/dist/tasks/evidence.js.map +1 -1
  478. package/dist/tasks/find.d.ts +16 -12
  479. package/dist/tasks/find.d.ts.map +1 -1
  480. package/dist/tasks/find.js +107 -93
  481. package/dist/tasks/find.js.map +1 -1
  482. package/dist/tasks/gate-preview.d.ts +73 -0
  483. package/dist/tasks/gate-preview.d.ts.map +1 -0
  484. package/dist/tasks/gate-preview.js +119 -0
  485. package/dist/tasks/gate-preview.js.map +1 -0
  486. package/dist/tasks/gate-runner.d.ts +88 -14
  487. package/dist/tasks/gate-runner.d.ts.map +1 -1
  488. package/dist/tasks/gate-runner.js +719 -250
  489. package/dist/tasks/gate-runner.js.map +1 -1
  490. package/dist/tasks/hierarchy-policy.d.ts +8 -0
  491. package/dist/tasks/hierarchy-policy.d.ts.map +1 -1
  492. package/dist/tasks/hierarchy-policy.js +9 -2
  493. package/dist/tasks/hierarchy-policy.js.map +1 -1
  494. package/dist/tasks/hierarchy.d.ts +67 -1
  495. package/dist/tasks/hierarchy.d.ts.map +1 -1
  496. package/dist/tasks/hierarchy.js +81 -1
  497. package/dist/tasks/hierarchy.js.map +1 -1
  498. package/dist/tasks/index.d.ts +3 -0
  499. package/dist/tasks/index.d.ts.map +1 -1
  500. package/dist/tasks/index.js +3 -0
  501. package/dist/tasks/index.js.map +1 -1
  502. package/dist/tasks/infer-add-params.d.ts +1 -20
  503. package/dist/tasks/infer-add-params.d.ts.map +1 -1
  504. package/dist/tasks/infer-add-params.js +2 -203
  505. package/dist/tasks/infer-add-params.js.map +1 -1
  506. package/dist/tasks/list.d.ts +7 -2
  507. package/dist/tasks/list.d.ts.map +1 -1
  508. package/dist/tasks/list.js +24 -36
  509. package/dist/tasks/list.js.map +1 -1
  510. package/dist/tasks/ops.d.ts +6 -61
  511. package/dist/tasks/ops.d.ts.map +1 -1
  512. package/dist/tasks/ops.js +5 -53
  513. package/dist/tasks/ops.js.map +1 -1
  514. package/dist/tasks/plan.d.ts +15 -1
  515. package/dist/tasks/plan.d.ts.map +1 -1
  516. package/dist/tasks/plan.js +23 -25
  517. package/dist/tasks/plan.js.map +1 -1
  518. package/dist/tasks/population.d.ts +46 -0
  519. package/dist/tasks/population.d.ts.map +1 -0
  520. package/dist/tasks/population.js +88 -0
  521. package/dist/tasks/population.js.map +1 -0
  522. package/dist/tasks/reconcile-scope.d.ts +157 -0
  523. package/dist/tasks/reconcile-scope.d.ts.map +1 -0
  524. package/dist/tasks/reconcile-scope.js +242 -0
  525. package/dist/tasks/reconcile-scope.js.map +1 -0
  526. package/dist/tasks/req.d.ts +51 -12
  527. package/dist/tasks/req.d.ts.map +1 -1
  528. package/dist/tasks/req.js +125 -69
  529. package/dist/tasks/req.js.map +1 -1
  530. package/dist/tasks/session-scope.d.ts +6 -31
  531. package/dist/tasks/session-scope.d.ts.map +1 -1
  532. package/dist/tasks/session-scope.js +3 -17
  533. package/dist/tasks/session-scope.js.map +1 -1
  534. package/dist/tasks/severity-attestation.d.ts +39 -10
  535. package/dist/tasks/severity-attestation.d.ts.map +1 -1
  536. package/dist/tasks/severity-attestation.js +75 -36
  537. package/dist/tasks/severity-attestation.js.map +1 -1
  538. package/dist/tasks/task-next.d.ts +5 -0
  539. package/dist/tasks/task-next.d.ts.map +1 -1
  540. package/dist/tasks/task-next.js +10 -9
  541. package/dist/tasks/task-next.js.map +1 -1
  542. package/dist/tasks/task-reparent.d.ts.map +1 -1
  543. package/dist/tasks/task-reparent.js +3 -13
  544. package/dist/tasks/task-reparent.js.map +1 -1
  545. package/dist/tasks/update.d.ts +20 -43
  546. package/dist/tasks/update.d.ts.map +1 -1
  547. package/dist/tasks/update.js +64 -49
  548. package/dist/tasks/update.js.map +1 -1
  549. package/dist/validation/engine-ops.d.ts +17 -1
  550. package/dist/validation/engine-ops.d.ts.map +1 -1
  551. package/dist/validation/engine-ops.js +149 -11
  552. package/dist/validation/engine-ops.js.map +1 -1
  553. package/dist/workgraph/reparent-preflight.d.ts +1 -1
  554. package/dist/workgraph/reparent-preflight.d.ts.map +1 -1
  555. package/dist/workgraph/reparent-preflight.js +6 -1
  556. package/dist/workgraph/reparent-preflight.js.map +1 -1
  557. package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
  558. package/dist/workgraph/scaffold-apply.js +81 -76
  559. package/dist/workgraph/scaffold-apply.js.map +1 -1
  560. package/dist/workgraph/structure-validation.d.ts +1 -1
  561. package/dist/workgraph/structure-validation.d.ts.map +1 -1
  562. package/dist/workgraph/structure-validation.js +6 -1
  563. package/dist/workgraph/structure-validation.js.map +1 -1
  564. package/migrations/drizzle-tasks/20260919180000_t12263-job-ownership-fences/migration.sql +34 -0
  565. package/migrations/drizzle-tasks/20260919184500_t12265-durable-pending-proposals/migration.sql +4 -0
  566. package/package.json +18 -13
  567. package/schemas/config.schema.json +2 -2
  568. package/schemas/project-context.schema.json +12 -0
  569. package/templates/CLEO-INJECTION.md +114 -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, testCountReportSchema, } 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,704 @@ 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;
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
+ /**
537
+ * Index of the `}` closing the `{` at `start`, or `-1`.
538
+ *
539
+ * String literals are tracked so a brace inside a test NAME — which reports
540
+ * are full of — cannot terminate the object early.
541
+ */
542
+ function matchingBrace(text, start) {
543
+ let depth = 0;
544
+ let inString = false;
545
+ let escaped = false;
546
+ for (let i = start; i < text.length; i++) {
547
+ const ch = text[i];
548
+ if (inString) {
549
+ if (escaped)
550
+ escaped = false;
551
+ else if (ch === '\\')
552
+ escaped = true;
553
+ else if (ch === '"')
554
+ inString = false;
555
+ continue;
124
556
  }
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');
557
+ if (ch === '"')
558
+ inString = true;
559
+ else if (ch === '{')
560
+ depth++;
561
+ else if (ch === '}' && --depth === 0)
562
+ return i;
563
+ }
564
+ return -1;
565
+ }
566
+ /**
567
+ * Structured test counts emitted by THIS gate's own process, or `null`.
568
+ *
569
+ * Validated with {@link testCountReportSchema} — the contract written for
570
+ * exactly this purpose, which cross-checks every summary counter against the
571
+ * individual assertion results, so a report cannot claim a total its own
572
+ * details contradict.
573
+ *
574
+ * The whole captured stream is searched rather than requiring `stdout` to BE
575
+ * the document, because a runner that writes a JSON report to stdout routinely
576
+ * prefixes it with progress output. Candidates are tried newest-first, since a
577
+ * summary is emitted after its noise.
578
+ *
579
+ * Provenance is the reason this reads the gate's OWN capture and nothing else:
580
+ * a report file named by the task could have been produced by any run, of any
581
+ * code, at any time. Only the bytes this invocation emitted are bound to it.
582
+ *
583
+ * @param captured - Result of running the gate command.
584
+ * @returns Validated counts, or `null` when the run emitted no usable report.
585
+ * @task T12308
586
+ */
587
+ function structuredTestCounts(captured) {
588
+ const text = `${captured.stdout}\n${captured.stderr}`.trim();
589
+ if (text.length === 0)
590
+ return null;
591
+ const tryParse = (candidate) => {
592
+ try {
593
+ const report = testCountReportSchema.safeParse(JSON.parse(candidate));
594
+ return report.success ? report.data : null;
595
+ }
596
+ catch {
597
+ return null;
144
598
  }
599
+ };
600
+ const direct = tryParse(text);
601
+ if (direct !== null)
602
+ return direct;
603
+ // `lastIndexOf(s, -1)` searches from index 0 and returns 0 for a leading
604
+ // brace, so decrementing past the start yields the SAME index forever. The
605
+ // loop is synchronous, so a vitest `testTimeout` cannot interrupt it — the
606
+ // run just dies on the job's wall clock with no failing assertion. Terminate
607
+ // on `start === 0` explicitly rather than relying on the search to run out.
608
+ let start = text.lastIndexOf('{');
609
+ while (start > -1) {
610
+ const end = matchingBrace(text, start);
611
+ if (end !== -1) {
612
+ const parsed = tryParse(text.slice(start, end + 1));
613
+ if (parsed !== null)
614
+ return parsed;
615
+ }
616
+ if (start === 0)
617
+ break;
618
+ start = text.lastIndexOf('{', start - 1);
145
619
  }
146
- return makeResult(index, gate, 'pass', durationMs, combined);
620
+ return null;
621
+ }
622
+ /**
623
+ * The `minCount` remediation, naming the exact reporter flag for this runner.
624
+ *
625
+ * The message this replaced said only that a structured count was required. A
626
+ * reader had no way to learn what "supported" meant, what shape was expected,
627
+ * or which flag produced it — so the reported resolution was to delete the
628
+ * `minCount` from the gate, which removes the guarantee rather than meeting it.
629
+ *
630
+ * @param gate - Gate whose command needs a machine-readable reporter.
631
+ * @task T12308
632
+ */
633
+ function describeMissingTestCount(gate) {
634
+ const invoked = [gate.command, ...(gate.args ?? [])].join(' ');
635
+ const flag = /jest/.test(invoked)
636
+ ? '--json'
637
+ : /vitest|pnpm|npm|yarn|bun/.test(invoked)
638
+ ? '--reporter=json'
639
+ : '<your runner\u2019s JSON reporter flag>';
640
+ return (`minCount ${gate.minCount} cannot be proved: \`${invoked}\` emitted no machine-readable ` +
641
+ `test report, and an exit code alone carries no count. Add a JSON reporter to the gate ` +
642
+ `command itself (e.g. \`${invoked} ${flag}\`) so the run that is being attested is the ` +
643
+ `run that is counted. A report file produced by a separate invocation is deliberately not ` +
644
+ `accepted here \u2014 it is not bound to this execution; record that as \`test-run:<path>\` ` +
645
+ `evidence instead, or drop minCount if an exit code is the guarantee you want.`);
646
+ }
647
+ async function runTestGate(gate, index, projectRoot, timeoutMs, context) {
648
+ const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
649
+ const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context, gate.env);
650
+ const exitOk = captured.exitCode === 0 &&
651
+ (gate.expect === 'exit0' || !/\bFAIL\b|failing|Error:/i.test(captured.stdout));
652
+ // T12308: `minCount` was declarable, storable and never satisfiable — the
653
+ // runner rejected any positive value outright, so a task could carry a gate
654
+ // that no amount of passing tests could turn green. The counts now come from
655
+ // this run's own validated report; only their ABSENCE is still an error,
656
+ // because inferring a count from exit zero is the thing that must not happen.
657
+ if (gate.minCount !== undefined && gate.minCount > 0) {
658
+ const report = structuredTestCounts(captured);
659
+ if (report === null)
660
+ throw new Error(describeMissingTestCount(gate));
661
+ if (report.numPassedTests < gate.minCount)
662
+ return processGateResult(index, gate, captured, 'fail', `Structured report counts ${report.numPassedTests} passing tests, below the required minimum of ${gate.minCount}`);
663
+ if (!exitOk)
664
+ return processGateResult(index, gate, captured, 'fail', captured.exitCode === 0
665
+ ? 'Failure pattern detected in output'
666
+ : `Exit code ${captured.exitCode}`);
667
+ return processGateResult(index, gate, captured, 'pass');
668
+ }
669
+ return processGateResult(index, gate, captured, exitOk ? 'pass' : 'fail', captured.exitCode === 0
670
+ ? 'Failure pattern detected in output'
671
+ : `Exit code ${captured.exitCode}`);
147
672
  }
148
673
  // ─── File gate ────────────────────────────────────────────────────────────────
149
- async function runFileGate(gate, index, projectRoot) {
674
+ async function runFileGate(gate, index, projectRoot, context) {
150
675
  const startMs = Date.now();
151
- // Resolve file path: either from gate.path or via AttachmentStore by sha256.
676
+ let attachmentBytes;
152
677
  let filePath;
153
678
  if (gate.attachmentSha256) {
154
- // Resolve via AttachmentStore — fail fast if the attachment is missing.
155
679
  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}`);
680
+ const metadata = await store.getMetadata(gate.attachmentSha256, projectRoot);
681
+ assertGateActive(context);
682
+ if (!metadata)
683
+ return makeResult(index, gate, 'fail', Date.now() - startMs, undefined, 'Attachment not found');
684
+ const attachment = metadata.attachment;
685
+ if (!('size' in attachment) || !Number.isSafeInteger(attachment.size) || attachment.size < 0)
686
+ throw new Error('Attachment gate requires declared byte size for bounded retrieval');
687
+ if (attachment.size > (context.maxOutputBytes ?? 1_048_576))
688
+ throw new Error('Attachment gate exceeds declared byte limit');
689
+ const result = await store.get(gate.attachmentSha256, projectRoot);
690
+ assertGateActive(context);
691
+ if (!result)
692
+ throw new Error('Attachment metadata exists but authenticated bytes are unavailable');
693
+ if (result.bytes.length > (context.maxOutputBytes ?? 1_048_576))
694
+ throw new Error('Attachment bytes exceed declared limit');
695
+ attachmentBytes = result.bytes;
696
+ filePath = `sha256:${gate.attachmentSha256}`;
179
697
  }
180
698
  else if (gate.path) {
181
699
  filePath = isAbsolute(gate.path) ? gate.path : join(projectRoot, gate.path);
182
700
  }
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
- }
701
+ else
702
+ throw new Error('FileGate requires path or attachmentSha256');
187
703
  const failures = [];
188
- const fileContent = null;
704
+ let fileContent = attachmentBytes;
189
705
  let fileSize = 0;
190
706
  let fileExists = false;
191
707
  // Check existence first
192
708
  try {
193
- const st = await stat(filePath);
709
+ const st = attachmentBytes ? { size: attachmentBytes.length } : await stat(filePath);
194
710
  fileExists = true;
195
711
  fileSize = st.size;
196
712
  }
197
- catch {
713
+ catch (error) {
714
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
715
+ throw error;
198
716
  fileExists = false;
199
717
  }
200
718
  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(() => '');
719
+ assertGateActive(context);
720
+ if (['contains', 'matches', 'sha256'].includes(assertion.type) &&
721
+ fileSize > (context.maxOutputBytes ?? 1_048_576))
722
+ throw new Error('File gate content exceeds declared byte limit');
723
+ const failure = await checkFileAssertion(assertion, filePath, fileExists, fileSize, async () => {
724
+ if (fileContent === undefined) {
725
+ const limit = context.maxOutputBytes ?? 1_048_576;
726
+ const handle = await open(filePath, 'r');
727
+ try {
728
+ const chunks = [];
729
+ let bytes = 0;
730
+ for (;;) {
731
+ assertGateActive(context);
732
+ const chunk = Buffer.alloc(Math.min(65536, limit - bytes + 1));
733
+ const read = await handle.read(chunk, 0, chunk.length, null);
734
+ assertGateActive(context);
735
+ if (read.bytesRead === 0)
736
+ break;
737
+ bytes += read.bytesRead;
738
+ if (bytes > limit)
739
+ throw new Error('File content exceeds declared byte limit');
740
+ chunks.push(chunk.subarray(0, read.bytesRead));
741
+ }
742
+ fileContent = Buffer.concat(chunks);
743
+ }
744
+ finally {
745
+ await handle.close();
746
+ }
204
747
  }
205
- return Promise.resolve(fileContent);
748
+ return fileContent;
206
749
  });
750
+ assertGateActive(context);
207
751
  if (failure) {
208
752
  failures.push(failure);
209
753
  }
@@ -211,7 +755,7 @@ async function runFileGate(gate, index, projectRoot) {
211
755
  const durationMs = Date.now() - startMs;
212
756
  if (failures.length > 0) {
213
757
  const evidence = `path=${filePath}\n${failures.join('\n')}`;
214
- return makeResult(index, gate, 'fail', durationMs, truncateString(evidence, MAX_EVIDENCE_BYTES), failures[0]);
758
+ return makeResult(index, gate, 'fail', durationMs, truncateString(evidence, MAX_EVIDENCE_CHARS), failures[0]);
215
759
  }
216
760
  return makeResult(index, gate, 'pass', durationMs, `path=${filePath} — all assertions passed`);
217
761
  }
@@ -245,7 +789,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
245
789
  case 'contains': {
246
790
  if (!fileExists)
247
791
  return `File does not exist: ${filePath}`;
248
- const content = await getContent();
792
+ const content = (await getContent()).toString('utf8');
249
793
  return content.includes(assertion.value)
250
794
  ? null
251
795
  : `File does not contain: ${JSON.stringify(assertion.value)}`;
@@ -253,7 +797,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
253
797
  case 'matches': {
254
798
  if (!fileExists)
255
799
  return `File does not exist: ${filePath}`;
256
- const content = await getContent();
800
+ const content = (await getContent()).toString('utf8');
257
801
  const re = new RegExp(assertion.regex, assertion.flags);
258
802
  return re.test(content)
259
803
  ? null
@@ -262,7 +806,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
262
806
  case 'sha256': {
263
807
  if (!fileExists)
264
808
  return `File does not exist: ${filePath}`;
265
- const raw = await readFile(filePath);
809
+ const raw = await getContent();
266
810
  const hash = createHash('sha256').update(raw).digest('hex');
267
811
  return hash === assertion.value
268
812
  ? null
@@ -276,60 +820,19 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
276
820
  }
277
821
  }
278
822
  // ─── 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);
823
+ async function runCommandGate(gate, index, projectRoot, timeoutMs, context) {
824
+ const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
825
+ const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context, gate.env);
826
+ const expected = gate.exitCode ?? 0;
827
+ let reason = captured.exitCode === expected
828
+ ? undefined
829
+ : `Exit code ${captured.exitCode} (expected ${expected})`;
830
+ if (!reason && gate.stdoutMatches && !new RegExp(gate.stdoutMatches).test(captured.stdout))
831
+ reason = 'stdout pattern did not match';
832
+ if (!reason && gate.stderrMatches && !new RegExp(gate.stderrMatches).test(captured.stderr))
833
+ reason = 'stderr pattern did not match';
834
+ return processGateResult(index, gate, captured, reason ? 'fail' : 'pass', reason);
331
835
  }
332
- // ─── Lint gate ────────────────────────────────────────────────────────────────
333
836
  /** Tool-specific CLI arguments and failure patterns. */
334
837
  const LINT_TOOL_DEFAULTS = {
335
838
  biome: { cmd: 'biome', defaultArgs: ['check', '.'] },
@@ -343,81 +846,57 @@ const LINT_TOOL_DEFAULTS = {
343
846
  errorPattern: /^error/m,
344
847
  },
345
848
  };
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);
849
+ async function runLintGate(gate, index, projectRoot, timeoutMs, context) {
850
+ const tool = LINT_TOOL_DEFAULTS[gate.tool];
851
+ const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
852
+ const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context);
853
+ const passed = captured.exitCode === 0 &&
854
+ (gate.expect === 'noErrors' || !tool.errorPattern?.test(captured.stdout + captured.stderr));
855
+ return processGateResult(index, gate, captured, passed ? 'pass' : 'fail', `${gate.tool} reported errors (exit ${captured.exitCode})`);
392
856
  }
393
857
  // ─── HTTP gate ────────────────────────────────────────────────────────────────
394
- async function runHttpGate(gate, index, _projectRoot, timeoutMs) {
858
+ async function runHttpGate(gate, index, _projectRoot, timeoutMs, context) {
395
859
  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
- }
860
+ if (gate.startCommand)
861
+ throw new Error('HTTP server startup requires an admitted owned service lifetime; unowned detached startup is unavailable');
407
862
  let statusCode = 0;
408
863
  let body = '';
409
864
  let errorMsg;
410
865
  try {
411
866
  const controller = new AbortController();
412
- const timer = setTimeout(() => controller.abort(), timeoutMs);
867
+ const timer = setTimeout(() => controller.abort(), Math.max(0, Math.min(timeoutMs, context.execution.deadlineAt - Date.now())));
413
868
  try {
414
869
  const response = await fetch(gate.url, {
415
870
  method: gate.method ?? 'GET',
416
871
  headers: gate.headers,
417
- signal: controller.signal,
872
+ signal: context.execution.signal
873
+ ? AbortSignal.any([controller.signal, context.execution.signal])
874
+ : controller.signal,
418
875
  });
419
876
  statusCode = response.status;
420
- body = await response.text();
877
+ if (response.body) {
878
+ const reader = response.body.getReader();
879
+ const chunks = [];
880
+ let bytes = 0;
881
+ try {
882
+ for (;;) {
883
+ assertGateActive(context);
884
+ const next = await reader.read();
885
+ if (next.done)
886
+ break;
887
+ bytes += next.value.byteLength;
888
+ if (bytes > (context.maxOutputBytes ?? 1_048_576))
889
+ throw new Error('HTTP body exceeds declared byte limit');
890
+ chunks.push(next.value);
891
+ }
892
+ body = Buffer.concat(chunks).toString('utf8');
893
+ }
894
+ finally {
895
+ await reader.cancel();
896
+ reader.releaseLock();
897
+ }
898
+ }
899
+ assertGateActive(context);
421
900
  }
422
901
  finally {
423
902
  clearTimeout(timer);
@@ -426,19 +905,9 @@ async function runHttpGate(gate, index, _projectRoot, timeoutMs) {
426
905
  catch (err) {
427
906
  errorMsg = err instanceof Error ? err.message : String(err);
428
907
  }
429
- finally {
430
- if (serverProcess) {
431
- try {
432
- serverProcess.kill();
433
- }
434
- catch {
435
- // Best-effort cleanup
436
- }
437
- }
438
- }
439
908
  const durationMs = Date.now() - startMs;
440
909
  if (errorMsg) {
441
- return makeResult(index, gate, 'fail', durationMs, errorMsg, errorMsg);
910
+ return makeResult(index, gate, 'error', durationMs, errorMsg, `HTTP gate did not finish: ${errorMsg}`);
442
911
  }
443
912
  if (statusCode !== gate.status) {
444
913
  return makeResult(index, gate, 'fail', durationMs, `HTTP ${statusCode}`, `Expected status ${gate.status}, got ${statusCode}`);