@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.
- package/dist/audit/reconstruct.d.ts +13 -43
- package/dist/audit/reconstruct.d.ts.map +1 -1
- package/dist/audit/reconstruct.js +295 -290
- package/dist/audit/reconstruct.js.map +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +8 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/compliance/protocol-rules.d.ts.map +1 -1
- package/dist/compliance/protocol-rules.js +3 -1
- package/dist/compliance/protocol-rules.js.map +1 -1
- package/dist/dispatch/mutate-projection.d.ts.map +1 -1
- package/dist/dispatch/mutate-projection.js +12 -0
- package/dist/dispatch/mutate-projection.js.map +1 -1
- package/dist/dispatch/mvi-projection.d.ts +29 -6
- package/dist/dispatch/mvi-projection.d.ts.map +1 -1
- package/dist/dispatch/mvi-projection.js +109 -139
- package/dist/dispatch/mvi-projection.js.map +1 -1
- package/dist/docs/docs-read-model.d.ts +3 -2
- package/dist/docs/docs-read-model.d.ts.map +1 -1
- package/dist/docs/docs-read-model.js +4 -3
- package/dist/docs/docs-read-model.js.map +1 -1
- package/dist/docs/export-document.js +5129 -2297
- package/dist/docs/export-document.js.map +3 -3
- package/dist/docs/projection.d.ts +67 -0
- package/dist/docs/projection.d.ts.map +1 -0
- package/dist/docs/projection.js +370 -0
- package/dist/docs/projection.js.map +1 -0
- package/dist/doctor/acceptance-drift.d.ts +6 -2
- package/dist/doctor/acceptance-drift.d.ts.map +1 -1
- package/dist/doctor/acceptance-drift.js +43 -5
- package/dist/doctor/acceptance-drift.js.map +1 -1
- package/dist/doctor/knowledge-summary.d.ts +41 -0
- package/dist/doctor/knowledge-summary.d.ts.map +1 -0
- package/dist/doctor/knowledge-summary.js +81 -0
- package/dist/doctor/knowledge-summary.js.map +1 -0
- package/dist/doctor/knowledge.d.ts +192 -0
- package/dist/doctor/knowledge.d.ts.map +1 -0
- package/dist/doctor/knowledge.js +1864 -0
- package/dist/doctor/knowledge.js.map +1 -0
- package/dist/git/work-tree.d.ts +169 -0
- package/dist/git/work-tree.d.ts.map +1 -0
- package/dist/git/work-tree.js +341 -0
- package/dist/git/work-tree.js.map +1 -0
- package/dist/hooks/handlers/task-hooks.d.ts +8 -5
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +15 -60
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/identity/cleo-identity.d.ts +13 -0
- package/dist/identity/cleo-identity.d.ts.map +1 -1
- package/dist/identity/cleo-identity.js +20 -6
- package/dist/identity/cleo-identity.js.map +1 -1
- package/dist/injection.d.ts.map +1 -1
- package/dist/injection.js +29 -13
- package/dist/injection.js.map +1 -1
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.js +124 -120
- package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
- package/dist/llm/plugin-facade.js +4047 -1726
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/memory/brain-backfill.d.ts +27 -12
- package/dist/memory/brain-backfill.d.ts.map +1 -1
- package/dist/memory/brain-backfill.js +325 -134
- package/dist/memory/brain-backfill.js.map +1 -1
- package/dist/memory/brain-doctor.d.ts +21 -0
- package/dist/memory/brain-doctor.d.ts.map +1 -1
- package/dist/memory/brain-doctor.js +111 -53
- package/dist/memory/brain-doctor.js.map +1 -1
- package/dist/memory/brain-links.d.ts.map +1 -1
- package/dist/memory/brain-links.js +31 -4
- package/dist/memory/brain-links.js.map +1 -1
- package/dist/memory/brain-reasoning.d.ts.map +1 -1
- package/dist/memory/brain-reasoning.js +28 -2
- package/dist/memory/brain-reasoning.js.map +1 -1
- package/dist/memory/brain-reconciler.d.ts +14 -19
- package/dist/memory/brain-reconciler.d.ts.map +1 -1
- package/dist/memory/brain-reconciler.js +70 -97
- package/dist/memory/brain-reconciler.js.map +1 -1
- package/dist/memory/brain-search.d.ts +2 -0
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-search.js +59 -29
- package/dist/memory/brain-search.js.map +1 -1
- package/dist/memory/brain-similarity.d.ts.map +1 -1
- package/dist/memory/brain-similarity.js +5 -4
- package/dist/memory/brain-similarity.js.map +1 -1
- package/dist/memory/brain-stub-prune.d.ts +31 -9
- package/dist/memory/brain-stub-prune.d.ts.map +1 -1
- package/dist/memory/brain-stub-prune.js +65 -18
- package/dist/memory/brain-stub-prune.js.map +1 -1
- package/dist/memory/brain-writer-handlers.d.ts +10 -1
- package/dist/memory/brain-writer-handlers.d.ts.map +1 -1
- package/dist/memory/brain-writer-handlers.js +23 -3
- package/dist/memory/brain-writer-handlers.js.map +1 -1
- package/dist/memory/brain-writer-thread.d.ts +11 -1
- package/dist/memory/brain-writer-thread.d.ts.map +1 -1
- package/dist/memory/brain-writer-thread.js +87 -20
- package/dist/memory/brain-writer-thread.js.map +1 -1
- package/dist/memory/brain-writer-worker.js +8 -1
- package/dist/memory/brain-writer-worker.js.map +1 -1
- package/dist/memory/decision-cross-link.d.ts +39 -0
- package/dist/memory/decision-cross-link.d.ts.map +1 -1
- package/dist/memory/decision-cross-link.js +316 -0
- package/dist/memory/decision-cross-link.js.map +1 -1
- package/dist/memory/decisions.d.ts +4 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/decisions.js +7 -24
- package/dist/memory/decisions.js.map +1 -1
- package/dist/memory/dispatch-trace.d.ts +12 -5
- package/dist/memory/dispatch-trace.d.ts.map +1 -1
- package/dist/memory/dispatch-trace.js +17 -7
- package/dist/memory/dispatch-trace.js.map +1 -1
- package/dist/memory/eligibility.d.ts +51 -0
- package/dist/memory/eligibility.d.ts.map +1 -0
- package/dist/memory/eligibility.js +77 -0
- package/dist/memory/eligibility.js.map +1 -0
- package/dist/memory/engine-compat.d.ts +2 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +10 -0
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/extraction-gate.d.ts +21 -7
- package/dist/memory/extraction-gate.d.ts.map +1 -1
- package/dist/memory/extraction-gate.js +178 -216
- package/dist/memory/extraction-gate.js.map +1 -1
- package/dist/memory/graph-auto-populate.d.ts +38 -5
- package/dist/memory/graph-auto-populate.d.ts.map +1 -1
- package/dist/memory/graph-auto-populate.js +182 -60
- package/dist/memory/graph-auto-populate.js.map +1 -1
- package/dist/memory/graph-memory-bridge.d.ts +14 -1
- package/dist/memory/graph-memory-bridge.d.ts.map +1 -1
- package/dist/memory/graph-memory-bridge.js +66 -28
- package/dist/memory/graph-memory-bridge.js.map +1 -1
- package/dist/memory/index.d.ts +1 -19
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +1 -50
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +2 -23
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/manifest-filter.d.ts +21 -0
- package/dist/memory/manifest-filter.d.ts.map +1 -0
- package/dist/memory/manifest-filter.js +51 -0
- package/dist/memory/manifest-filter.js.map +1 -0
- package/dist/memory/manifest-ingestion.d.ts +28 -30
- package/dist/memory/manifest-ingestion.d.ts.map +1 -1
- package/dist/memory/manifest-ingestion.js +104 -235
- package/dist/memory/manifest-ingestion.js.map +1 -1
- package/dist/memory/observer-reflector.d.ts +2 -0
- package/dist/memory/observer-reflector.d.ts.map +1 -1
- package/dist/memory/observer-reflector.js +6 -49
- package/dist/memory/observer-reflector.js.map +1 -1
- package/dist/memory/patterns.d.ts +10 -0
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/patterns.js +25 -33
- package/dist/memory/patterns.js.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.d.ts +72 -9
- package/dist/memory/pipeline-manifest-sqlite.d.ts.map +1 -1
- package/dist/memory/pipeline-manifest-sqlite.js +508 -412
- package/dist/memory/pipeline-manifest-sqlite.js.map +1 -1
- package/dist/memory/retrieval/build-retrieval-bundle.d.ts.map +1 -1
- package/dist/memory/retrieval/build-retrieval-bundle.js +16 -15
- package/dist/memory/retrieval/build-retrieval-bundle.js.map +1 -1
- package/dist/memory/retrieval/fetch.d.ts +7 -0
- package/dist/memory/retrieval/fetch.d.ts.map +1 -1
- package/dist/memory/retrieval/fetch.js +14 -11
- package/dist/memory/retrieval/fetch.js.map +1 -1
- package/dist/memory/retrieval/observe.d.ts +7 -1
- package/dist/memory/retrieval/observe.d.ts.map +1 -1
- package/dist/memory/retrieval/observe.js +58 -4
- package/dist/memory/retrieval/observe.js.map +1 -1
- package/dist/memory/retrieval/search.d.ts +14 -1
- package/dist/memory/retrieval/search.d.ts.map +1 -1
- package/dist/memory/retrieval/search.js +133 -69
- package/dist/memory/retrieval/search.js.map +1 -1
- package/dist/memory/retrieval/telemetry.d.ts +31 -0
- package/dist/memory/retrieval/telemetry.d.ts.map +1 -0
- package/dist/memory/retrieval/telemetry.js +53 -0
- package/dist/memory/retrieval/telemetry.js.map +1 -0
- package/dist/nexus/analyze-orchestrator.d.ts +41 -9
- package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
- package/dist/nexus/analyze-orchestrator.js +257 -58
- package/dist/nexus/analyze-orchestrator.js.map +1 -1
- package/dist/nexus/api-contracts.d.ts +2 -6
- package/dist/nexus/api-contracts.d.ts.map +1 -1
- package/dist/nexus/api-contracts.js +11 -2
- package/dist/nexus/api-contracts.js.map +1 -1
- package/dist/nexus/context.d.ts.map +1 -1
- package/dist/nexus/context.js +14 -37
- package/dist/nexus/context.js.map +1 -1
- package/dist/nexus/diff.d.ts.map +1 -1
- package/dist/nexus/diff.js +2 -1
- package/dist/nexus/diff.js.map +1 -1
- package/dist/nexus/identity.d.ts +29 -3
- package/dist/nexus/identity.d.ts.map +1 -1
- package/dist/nexus/identity.js +63 -12
- package/dist/nexus/identity.js.map +1 -1
- package/dist/nexus/impact.d.ts +21 -19
- package/dist/nexus/impact.d.ts.map +1 -1
- package/dist/nexus/impact.js +96 -192
- package/dist/nexus/impact.js.map +1 -1
- package/dist/nexus/knowledge.d.ts +80 -0
- package/dist/nexus/knowledge.d.ts.map +1 -0
- package/dist/nexus/knowledge.js +786 -0
- package/dist/nexus/knowledge.js.map +1 -0
- package/dist/nexus/living-brain.d.ts +8 -5
- package/dist/nexus/living-brain.d.ts.map +1 -1
- package/dist/nexus/living-brain.js +252 -188
- package/dist/nexus/living-brain.js.map +1 -1
- package/dist/nexus/registry.d.ts +11 -2
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +144 -128
- package/dist/nexus/registry.js.map +1 -1
- package/dist/nexus/source-roots.d.ts +16 -0
- package/dist/nexus/source-roots.d.ts.map +1 -0
- package/dist/nexus/source-roots.js +174 -0
- package/dist/nexus/source-roots.js.map +1 -0
- package/dist/nexus/task-evidence.d.ts +22 -0
- package/dist/nexus/task-evidence.d.ts.map +1 -0
- package/dist/nexus/task-evidence.js +210 -0
- package/dist/nexus/task-evidence.js.map +1 -0
- package/dist/nexus/tasks-bridge.d.ts +14 -7
- package/dist/nexus/tasks-bridge.d.ts.map +1 -1
- package/dist/nexus/tasks-bridge.js +81 -40
- package/dist/nexus/tasks-bridge.js.map +1 -1
- package/dist/orchestrate/lifecycle-ops.d.ts.map +1 -1
- package/dist/orchestrate/lifecycle-ops.js +170 -120
- package/dist/orchestrate/lifecycle-ops.js.map +1 -1
- package/dist/orchestrate/query-ops.d.ts +6 -7
- package/dist/orchestrate/query-ops.d.ts.map +1 -1
- package/dist/orchestrate/query-ops.js +509 -532
- package/dist/orchestrate/query-ops.js.map +1 -1
- package/dist/orchestration/bootstrap.d.ts +15 -1
- package/dist/orchestration/bootstrap.d.ts.map +1 -1
- package/dist/orchestration/bootstrap.js +21 -10
- package/dist/orchestration/bootstrap.js.map +1 -1
- package/dist/orchestration/index.d.ts +35 -3
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +64 -35
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/lead-rollup.d.ts +24 -1
- package/dist/orchestration/lead-rollup.d.ts.map +1 -1
- package/dist/orchestration/lead-rollup.js +31 -11
- package/dist/orchestration/lead-rollup.js.map +1 -1
- package/dist/orchestration/spawn-prompt.d.ts.map +1 -1
- package/dist/orchestration/spawn-prompt.js +25 -45
- package/dist/orchestration/spawn-prompt.js.map +1 -1
- package/dist/orchestration/unblock.d.ts +15 -1
- package/dist/orchestration/unblock.d.ts.map +1 -1
- package/dist/orchestration/unblock.js +23 -12
- package/dist/orchestration/unblock.js.map +1 -1
- package/dist/orchestration/validate-spawn.d.ts.map +1 -1
- package/dist/orchestration/validate-spawn.js +3 -3
- package/dist/orchestration/validate-spawn.js.map +1 -1
- package/dist/orchestration/waves.d.ts +29 -13
- package/dist/orchestration/waves.d.ts.map +1 -1
- package/dist/orchestration/waves.js +43 -55
- package/dist/orchestration/waves.js.map +1 -1
- package/dist/paths.d.ts +17 -163
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +145 -496
- package/dist/paths.js.map +1 -1
- package/dist/project-info.d.ts +2 -11
- package/dist/project-info.d.ts.map +1 -1
- package/dist/project-info.js +3 -31
- package/dist/project-info.js.map +1 -1
- package/dist/project-scope.d.ts +239 -0
- package/dist/project-scope.d.ts.map +1 -0
- package/dist/project-scope.js +473 -0
- package/dist/project-scope.js.map +1 -0
- package/dist/release/pr-evidence.d.ts +23 -4
- package/dist/release/pr-evidence.d.ts.map +1 -1
- package/dist/release/pr-evidence.js +65 -9
- package/dist/release/pr-evidence.js.map +1 -1
- package/dist/render/nexus/graph/index.d.ts.map +1 -1
- package/dist/render/nexus/graph/index.js +4 -2
- package/dist/render/nexus/graph/index.js.map +1 -1
- package/dist/render/orchestration/audit-reconstruct.d.ts +11 -0
- package/dist/render/orchestration/audit-reconstruct.d.ts.map +1 -1
- package/dist/render/orchestration/audit-reconstruct.js +21 -6
- package/dist/render/orchestration/audit-reconstruct.js.map +1 -1
- package/dist/render/tasks/show.js +2 -1
- package/dist/render/tasks/show.js.map +1 -1
- package/dist/resources/spawn-wrapper.d.ts +82 -14
- package/dist/resources/spawn-wrapper.d.ts.map +1 -1
- package/dist/resources/spawn-wrapper.js +585 -27
- package/dist/resources/spawn-wrapper.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +4055 -1734
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sequence/index.d.ts.map +1 -1
- package/dist/sequence/index.js +76 -68
- package/dist/sequence/index.js.map +1 -1
- package/dist/session/canon-lint.d.ts +24 -3
- package/dist/session/canon-lint.d.ts.map +1 -1
- package/dist/session/canon-lint.js +72 -31
- package/dist/session/canon-lint.js.map +1 -1
- package/dist/session/engine-ops.d.ts +1 -4
- package/dist/session/engine-ops.d.ts.map +1 -1
- package/dist/session/engine-ops.js +42 -6
- package/dist/session/engine-ops.js.map +1 -1
- package/dist/sessions/briefing.d.ts +11 -1
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/briefing.js +76 -8
- package/dist/sessions/briefing.js.map +1 -1
- package/dist/sessions/handoff.d.ts +3 -1
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/handoff.js +2 -0
- package/dist/sessions/handoff.js.map +1 -1
- package/dist/shutdown-deadline.d.ts +24 -33
- package/dist/shutdown-deadline.d.ts.map +1 -1
- package/dist/shutdown-deadline.js +71 -14
- package/dist/shutdown-deadline.js.map +1 -1
- package/dist/shutdown.d.ts +21 -10
- package/dist/shutdown.d.ts.map +1 -1
- package/dist/shutdown.js +89 -42
- package/dist/shutdown.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/orchestrator/validator.d.ts +87 -20
- package/dist/skills/orchestrator/validator.d.ts.map +1 -1
- package/dist/skills/orchestrator/validator.js +330 -241
- package/dist/skills/orchestrator/validator.js.map +1 -1
- package/dist/store/agent-resolver.d.ts.map +1 -1
- package/dist/store/agent-resolver.js +16 -9
- package/dist/store/agent-resolver.js.map +1 -1
- package/dist/store/attachment-store.d.ts +4 -1
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +20 -3
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/store/background-jobs.d.ts +426 -105
- package/dist/store/background-jobs.d.ts.map +1 -1
- package/dist/store/background-jobs.js +1208 -252
- package/dist/store/background-jobs.js.map +1 -1
- package/dist/store/background-ops.d.ts +145 -8
- package/dist/store/background-ops.d.ts.map +1 -1
- package/dist/store/background-ops.js +459 -15
- package/dist/store/background-ops.js.map +1 -1
- package/dist/store/backup-inspect.d.ts +48 -4
- package/dist/store/backup-inspect.d.ts.map +1 -1
- package/dist/store/backup-inspect.js +384 -4
- package/dist/store/backup-inspect.js.map +1 -1
- package/dist/store/converters.d.ts.map +1 -1
- package/dist/store/converters.js +19 -2
- package/dist/store/converters.js.map +1 -1
- package/dist/store/data-accessor.d.ts +9 -1
- package/dist/store/data-accessor.d.ts.map +1 -1
- package/dist/store/data-accessor.js +22 -5
- package/dist/store/data-accessor.js.map +1 -1
- package/dist/store/data-safety-central.d.ts.map +1 -1
- package/dist/store/data-safety-central.js +272 -232
- package/dist/store/data-safety-central.js.map +1 -1
- package/dist/store/db-helpers.d.ts +19 -3
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/db-helpers.js +42 -58
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts +63 -4
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +166 -19
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/archive.d.ts +21 -7
- package/dist/store/exodus/archive.d.ts.map +1 -1
- package/dist/store/exodus/archive.js +60 -18
- package/dist/store/exodus/archive.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +27 -23
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/exodus/on-open.d.ts +16 -27
- package/dist/store/exodus/on-open.d.ts.map +1 -1
- package/dist/store/exodus/on-open.js +121 -169
- package/dist/store/exodus/on-open.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts +90 -0
- package/dist/store/exodus/recovery.d.ts.map +1 -0
- package/dist/store/exodus/recovery.js +434 -0
- package/dist/store/exodus/recovery.js.map +1 -0
- package/dist/store/exodus/types.d.ts +15 -0
- package/dist/store/exodus/types.d.ts.map +1 -1
- package/dist/store/memory-accessor.d.ts +52 -3
- package/dist/store/memory-accessor.d.ts.map +1 -1
- package/dist/store/memory-accessor.js +122 -7
- package/dist/store/memory-accessor.js.map +1 -1
- package/dist/store/nexus-sqlite.d.ts +24 -6
- package/dist/store/nexus-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-sqlite.js +39 -20
- package/dist/store/nexus-sqlite.js.map +1 -1
- package/dist/store/ports/domain-binding.d.ts.map +1 -1
- package/dist/store/ports/domain-binding.js +27 -5
- package/dist/store/ports/domain-binding.js.map +1 -1
- package/dist/store/schema/background-jobs.d.ts +181 -2
- package/dist/store/schema/background-jobs.d.ts.map +1 -1
- package/dist/store/schema/background-jobs.js +27 -3
- package/dist/store/schema/background-jobs.js.map +1 -1
- package/dist/store/schema/cleo-global/skills.d.ts +2 -2
- package/dist/store/schema/cleo-project/lifecycle.d.ts +2 -2
- package/dist/store/schema/cleo-project/runtime.d.ts +1 -1
- package/dist/store/schema/cleo-project/tasks-core.d.ts +1 -1
- package/dist/store/schema/lifecycle.d.ts +2 -2
- package/dist/store/schema/skills-schema.d.ts +2 -2
- package/dist/store/schema/tasks.d.ts +1 -1
- package/dist/store/session-store.d.ts +3 -1
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +252 -207
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts +39 -4
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +689 -527
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/tasks-sqlite.d.ts +3 -1
- package/dist/store/tasks-sqlite.d.ts.map +1 -1
- package/dist/store/tasks-sqlite.js +309 -279
- package/dist/store/tasks-sqlite.js.map +1 -1
- package/dist/store/validation-schemas.d.ts +8 -8
- package/dist/store/writer-lease.d.ts +80 -8
- package/dist/store/writer-lease.d.ts.map +1 -1
- package/dist/store/writer-lease.js +503 -134
- package/dist/store/writer-lease.js.map +1 -1
- package/dist/system/audit.d.ts +2 -1
- package/dist/system/audit.d.ts.map +1 -1
- package/dist/system/audit.js +118 -71
- package/dist/system/audit.js.map +1 -1
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/backup.js +5 -5
- package/dist/system/backup.js.map +1 -1
- package/dist/system/inject-generate.js +1 -1
- package/dist/task-work/index.d.ts +3 -0
- package/dist/task-work/index.d.ts.map +1 -1
- package/dist/task-work/index.js +34 -37
- package/dist/task-work/index.js.map +1 -1
- package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
- package/dist/tasks/ac-coverage-gate.js +4 -0
- package/dist/tasks/ac-coverage-gate.js.map +1 -1
- package/dist/tasks/ac-immutability.d.ts +7 -4
- package/dist/tasks/ac-immutability.d.ts.map +1 -1
- package/dist/tasks/ac-immutability.js +19 -17
- package/dist/tasks/ac-immutability.js.map +1 -1
- package/dist/tasks/ac-table.d.ts +5 -5
- package/dist/tasks/ac-table.d.ts.map +1 -1
- package/dist/tasks/ac-table.js +26 -12
- package/dist/tasks/ac-table.js.map +1 -1
- package/dist/tasks/acceptance-input.d.ts +52 -0
- package/dist/tasks/acceptance-input.d.ts.map +1 -0
- package/dist/tasks/acceptance-input.js +259 -0
- package/dist/tasks/acceptance-input.js.map +1 -0
- package/dist/tasks/add.d.ts +67 -29
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +255 -97
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts +16 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +152 -55
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/decompose.d.ts +103 -0
- package/dist/tasks/decompose.d.ts.map +1 -0
- package/dist/tasks/decompose.js +224 -0
- package/dist/tasks/decompose.js.map +1 -0
- package/dist/tasks/delete.d.ts +5 -8
- package/dist/tasks/delete.d.ts.map +1 -1
- package/dist/tasks/delete.js +4 -3
- package/dist/tasks/delete.js.map +1 -1
- package/dist/tasks/dependency-check.d.ts +59 -0
- package/dist/tasks/dependency-check.d.ts.map +1 -1
- package/dist/tasks/dependency-check.js +73 -0
- package/dist/tasks/dependency-check.js.map +1 -1
- package/dist/tasks/deps-ready.d.ts +13 -1
- package/dist/tasks/deps-ready.d.ts.map +1 -1
- package/dist/tasks/deps-ready.js +16 -5
- package/dist/tasks/deps-ready.js.map +1 -1
- package/dist/tasks/engine-converters.d.ts +7 -0
- package/dist/tasks/engine-converters.d.ts.map +1 -1
- package/dist/tasks/engine-converters.js +8 -1
- package/dist/tasks/engine-converters.js.map +1 -1
- package/dist/tasks/evidence.d.ts +88 -27
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +344 -43
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/find.d.ts +16 -12
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +107 -93
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/gate-preview.d.ts +73 -0
- package/dist/tasks/gate-preview.d.ts.map +1 -0
- package/dist/tasks/gate-preview.js +119 -0
- package/dist/tasks/gate-preview.js.map +1 -0
- package/dist/tasks/gate-runner.d.ts +88 -14
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +719 -250
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/hierarchy-policy.d.ts +8 -0
- package/dist/tasks/hierarchy-policy.d.ts.map +1 -1
- package/dist/tasks/hierarchy-policy.js +9 -2
- package/dist/tasks/hierarchy-policy.js.map +1 -1
- package/dist/tasks/hierarchy.d.ts +67 -1
- package/dist/tasks/hierarchy.d.ts.map +1 -1
- package/dist/tasks/hierarchy.js +81 -1
- package/dist/tasks/hierarchy.js.map +1 -1
- package/dist/tasks/index.d.ts +3 -0
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +3 -0
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/infer-add-params.d.ts +1 -20
- package/dist/tasks/infer-add-params.d.ts.map +1 -1
- package/dist/tasks/infer-add-params.js +2 -203
- package/dist/tasks/infer-add-params.js.map +1 -1
- package/dist/tasks/list.d.ts +7 -2
- package/dist/tasks/list.d.ts.map +1 -1
- package/dist/tasks/list.js +24 -36
- package/dist/tasks/list.js.map +1 -1
- package/dist/tasks/ops.d.ts +6 -61
- package/dist/tasks/ops.d.ts.map +1 -1
- package/dist/tasks/ops.js +5 -53
- package/dist/tasks/ops.js.map +1 -1
- package/dist/tasks/plan.d.ts +15 -1
- package/dist/tasks/plan.d.ts.map +1 -1
- package/dist/tasks/plan.js +23 -25
- package/dist/tasks/plan.js.map +1 -1
- package/dist/tasks/population.d.ts +46 -0
- package/dist/tasks/population.d.ts.map +1 -0
- package/dist/tasks/population.js +88 -0
- package/dist/tasks/population.js.map +1 -0
- package/dist/tasks/reconcile-scope.d.ts +157 -0
- package/dist/tasks/reconcile-scope.d.ts.map +1 -0
- package/dist/tasks/reconcile-scope.js +242 -0
- package/dist/tasks/reconcile-scope.js.map +1 -0
- package/dist/tasks/req.d.ts +51 -12
- package/dist/tasks/req.d.ts.map +1 -1
- package/dist/tasks/req.js +125 -69
- package/dist/tasks/req.js.map +1 -1
- package/dist/tasks/session-scope.d.ts +6 -31
- package/dist/tasks/session-scope.d.ts.map +1 -1
- package/dist/tasks/session-scope.js +3 -17
- package/dist/tasks/session-scope.js.map +1 -1
- package/dist/tasks/severity-attestation.d.ts +39 -10
- package/dist/tasks/severity-attestation.d.ts.map +1 -1
- package/dist/tasks/severity-attestation.js +75 -36
- package/dist/tasks/severity-attestation.js.map +1 -1
- package/dist/tasks/task-next.d.ts +5 -0
- package/dist/tasks/task-next.d.ts.map +1 -1
- package/dist/tasks/task-next.js +10 -9
- package/dist/tasks/task-next.js.map +1 -1
- package/dist/tasks/task-reparent.d.ts.map +1 -1
- package/dist/tasks/task-reparent.js +3 -13
- package/dist/tasks/task-reparent.js.map +1 -1
- package/dist/tasks/update.d.ts +20 -43
- package/dist/tasks/update.d.ts.map +1 -1
- package/dist/tasks/update.js +64 -49
- package/dist/tasks/update.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +17 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +149 -11
- package/dist/validation/engine-ops.js.map +1 -1
- package/dist/workgraph/reparent-preflight.d.ts +1 -1
- package/dist/workgraph/reparent-preflight.d.ts.map +1 -1
- package/dist/workgraph/reparent-preflight.js +6 -1
- package/dist/workgraph/reparent-preflight.js.map +1 -1
- package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
- package/dist/workgraph/scaffold-apply.js +81 -76
- package/dist/workgraph/scaffold-apply.js.map +1 -1
- package/dist/workgraph/structure-validation.d.ts +1 -1
- package/dist/workgraph/structure-validation.d.ts.map +1 -1
- package/dist/workgraph/structure-validation.js +6 -1
- package/dist/workgraph/structure-validation.js.map +1 -1
- package/migrations/drizzle-tasks/20260919180000_t12263-job-ownership-fences/migration.sql +34 -0
- package/migrations/drizzle-tasks/20260919184500_t12265-durable-pending-proposals/migration.sql +4 -0
- package/package.json +18 -13
- package/schemas/config.schema.json +2 -2
- package/schemas/project-context.schema.json +12 -0
- 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
|
|
16
|
-
* -
|
|
17
|
-
*
|
|
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 {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { isAbsolute, join } from 'node:path';
|
|
26
|
-
import {
|
|
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
|
-
|
|
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
|
|
35
|
-
const
|
|
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
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
428
|
+
before = await lstat(path);
|
|
61
429
|
}
|
|
62
|
-
catch (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return makeResult(index, gate, 'fail',
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
let
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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:
|
|
872
|
+
signal: context.execution.signal
|
|
873
|
+
? AbortSignal.any([controller.signal, context.execution.signal])
|
|
874
|
+
: controller.signal,
|
|
418
875
|
});
|
|
419
876
|
statusCode = response.status;
|
|
420
|
-
|
|
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, '
|
|
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}`);
|