@cleocode/core 2026.9.7 → 2026.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +5100 -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 +64 -0
- package/dist/git/work-tree.d.ts.map +1 -0
- package/dist/git/work-tree.js +111 -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 +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -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 +4018 -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 +31 -1
- package/dist/memory/brain-writer-thread.d.ts.map +1 -1
- package/dist/memory/brain-writer-thread.js +166 -24
- 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 +16 -41
- 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 +16 -3
- package/dist/release/pr-evidence.d.ts.map +1 -1
- package/dist/release/pr-evidence.js +53 -7
- 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 +4026 -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 +6 -2
- 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 +91 -35
- 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 +54 -26
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +292 -40
- 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-runner.d.ts +88 -14
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +596 -253
- 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/teardown-signal.d.ts +72 -0
- package/dist/teardown-signal.d.ts.map +1 -0
- package/dist/teardown-signal.js +108 -0
- package/dist/teardown-signal.js.map +1 -0
- 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/templates/CLEO-INJECTION.md +103 -101
|
@@ -12,27 +12,33 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Design constraints:
|
|
14
14
|
* - Each gate is self-contained (no cross-gate state).
|
|
15
|
-
* - Gates run sequentially
|
|
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 } 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,578 @@ const CHECKED_BY = process.env['CLEO_AGENT_ID'] ?? 'cleo-verify';
|
|
|
44
50
|
* @param gates - Typed gate objects (strings pre-filtered by caller).
|
|
45
51
|
* @param options - Execution options.
|
|
46
52
|
* @returns Ordered `AcceptanceGateResult[]`, one per gate.
|
|
53
|
+
* @throws If explicit project authority conflicts, metadata is invalid, or admission options are invalid.
|
|
54
|
+
* @remarks Results retain the original gate requirement ID. Process outcomes include target,
|
|
55
|
+
* transport and cleanup observations; they do not by themselves authorize task completion.
|
|
56
|
+
* Declared operation bytes reserve the per-gate capture ceiling before admission. Filesystem
|
|
57
|
+
* calls and regular-expression CPU work are cooperatively checked, not synchronously preempted.
|
|
58
|
+
* Canonical attachment retrieval validates its declared size but is not a streaming read API.
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const results = await runGates([{ kind: 'command', description: 'check', cmd: 'node', args: ['check.mjs'] }], { projectRoot: '/project' });
|
|
62
|
+
* ```
|
|
47
63
|
*
|
|
48
64
|
* @epic T760
|
|
49
65
|
* @task T781
|
|
50
66
|
*/
|
|
51
67
|
export async function runGates(gates, options = {}) {
|
|
52
|
-
const
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
// For 'pass' mode, check for obvious failure indicators in stdout
|
|
140
|
-
if (gate.expect === 'pass') {
|
|
141
|
-
const failPattern = /\bFAIL\b|failing|Error:/i;
|
|
142
|
-
if (failPattern.test(stdout)) {
|
|
143
|
-
return makeResult(index, gate, 'fail', durationMs, combined, 'Failure pattern detected in output');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return makeResult(index, gate, 'pass', durationMs, combined);
|
|
506
|
+
/** Execute a process gate through the existing captured resource service. */
|
|
507
|
+
async function captureGateCommand(command, args, cwd, timeoutMs, context, overlay) {
|
|
508
|
+
const env = gateEnvironment(context.env ?? process.env, overlay);
|
|
509
|
+
return captureWrapped(command, args, {
|
|
510
|
+
...context,
|
|
511
|
+
cwd,
|
|
512
|
+
env,
|
|
513
|
+
execution: {
|
|
514
|
+
...context.execution,
|
|
515
|
+
deadlineAt: Math.min(context.execution.deadlineAt, Date.now() + timeoutMs),
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/** Preserve target failure independently from launch, cancellation and cleanup failures. */
|
|
520
|
+
function processGateResult(index, gate, captured, verdict, reason) {
|
|
521
|
+
const incomplete = !captured.started ||
|
|
522
|
+
!captured.targetCloseObserved ||
|
|
523
|
+
captured.exitCode === null ||
|
|
524
|
+
captured.signal !== null ||
|
|
525
|
+
captured.error !== null ||
|
|
526
|
+
captured.stopped !== null ||
|
|
527
|
+
captured.outputTruncated ||
|
|
528
|
+
captured.cleanupErrors.length > 0;
|
|
529
|
+
return {
|
|
530
|
+
...makeResult(index, gate, incomplete ? 'error' : verdict, captured.durationMs, truncateString(`${captured.stdout}\n${captured.stderr}`.trim(), MAX_EVIDENCE_CHARS), incomplete
|
|
531
|
+
? `Gate did not finish: ${captured.error ?? captured.stopped ?? captured.signal ?? (captured.cleanupErrors.join('; ') || 'target outcome missing')}. This is NOT a failure verdict.`
|
|
532
|
+
: reason),
|
|
533
|
+
execution: captured,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async function runTestGate(gate, index, projectRoot, timeoutMs, context) {
|
|
537
|
+
if (gate.minCount !== undefined && gate.minCount > 0)
|
|
538
|
+
throw new Error('Minimum test count requires a supported structured count result; exit0 alone cannot prove it');
|
|
539
|
+
const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
|
|
540
|
+
const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context, gate.env);
|
|
541
|
+
const passed = captured.exitCode === 0 &&
|
|
542
|
+
(gate.expect === 'exit0' || !/\bFAIL\b|failing|Error:/i.test(captured.stdout));
|
|
543
|
+
return processGateResult(index, gate, captured, passed ? 'pass' : 'fail', captured.exitCode === 0
|
|
544
|
+
? 'Failure pattern detected in output'
|
|
545
|
+
: `Exit code ${captured.exitCode}`);
|
|
147
546
|
}
|
|
148
547
|
// ─── File gate ────────────────────────────────────────────────────────────────
|
|
149
|
-
async function runFileGate(gate, index, projectRoot) {
|
|
548
|
+
async function runFileGate(gate, index, projectRoot, context) {
|
|
150
549
|
const startMs = Date.now();
|
|
151
|
-
|
|
550
|
+
let attachmentBytes;
|
|
152
551
|
let filePath;
|
|
153
552
|
if (gate.attachmentSha256) {
|
|
154
|
-
// Resolve via AttachmentStore — fail fast if the attachment is missing.
|
|
155
553
|
const store = createAttachmentStore();
|
|
156
|
-
const
|
|
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}`);
|
|
554
|
+
const metadata = await store.getMetadata(gate.attachmentSha256, projectRoot);
|
|
555
|
+
assertGateActive(context);
|
|
556
|
+
if (!metadata)
|
|
557
|
+
return makeResult(index, gate, 'fail', Date.now() - startMs, undefined, 'Attachment not found');
|
|
558
|
+
const attachment = metadata.attachment;
|
|
559
|
+
if (!('size' in attachment) || !Number.isSafeInteger(attachment.size) || attachment.size < 0)
|
|
560
|
+
throw new Error('Attachment gate requires declared byte size for bounded retrieval');
|
|
561
|
+
if (attachment.size > (context.maxOutputBytes ?? 1_048_576))
|
|
562
|
+
throw new Error('Attachment gate exceeds declared byte limit');
|
|
563
|
+
const result = await store.get(gate.attachmentSha256, projectRoot);
|
|
564
|
+
assertGateActive(context);
|
|
565
|
+
if (!result)
|
|
566
|
+
throw new Error('Attachment metadata exists but authenticated bytes are unavailable');
|
|
567
|
+
if (result.bytes.length > (context.maxOutputBytes ?? 1_048_576))
|
|
568
|
+
throw new Error('Attachment bytes exceed declared limit');
|
|
569
|
+
attachmentBytes = result.bytes;
|
|
570
|
+
filePath = `sha256:${gate.attachmentSha256}`;
|
|
179
571
|
}
|
|
180
572
|
else if (gate.path) {
|
|
181
573
|
filePath = isAbsolute(gate.path) ? gate.path : join(projectRoot, gate.path);
|
|
182
574
|
}
|
|
183
|
-
else
|
|
184
|
-
|
|
185
|
-
return makeResult(index, gate, 'fail', durationMs, 'FileGate requires either path or attachmentSha256', 'FileGate missing path and attachmentSha256');
|
|
186
|
-
}
|
|
575
|
+
else
|
|
576
|
+
throw new Error('FileGate requires path or attachmentSha256');
|
|
187
577
|
const failures = [];
|
|
188
|
-
|
|
578
|
+
let fileContent = attachmentBytes;
|
|
189
579
|
let fileSize = 0;
|
|
190
580
|
let fileExists = false;
|
|
191
581
|
// Check existence first
|
|
192
582
|
try {
|
|
193
|
-
const st = await stat(filePath);
|
|
583
|
+
const st = attachmentBytes ? { size: attachmentBytes.length } : await stat(filePath);
|
|
194
584
|
fileExists = true;
|
|
195
585
|
fileSize = st.size;
|
|
196
586
|
}
|
|
197
|
-
catch {
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
589
|
+
throw error;
|
|
198
590
|
fileExists = false;
|
|
199
591
|
}
|
|
200
592
|
for (const assertion of gate.assertions) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
593
|
+
assertGateActive(context);
|
|
594
|
+
if (['contains', 'matches', 'sha256'].includes(assertion.type) &&
|
|
595
|
+
fileSize > (context.maxOutputBytes ?? 1_048_576))
|
|
596
|
+
throw new Error('File gate content exceeds declared byte limit');
|
|
597
|
+
const failure = await checkFileAssertion(assertion, filePath, fileExists, fileSize, async () => {
|
|
598
|
+
if (fileContent === undefined) {
|
|
599
|
+
const limit = context.maxOutputBytes ?? 1_048_576;
|
|
600
|
+
const handle = await open(filePath, 'r');
|
|
601
|
+
try {
|
|
602
|
+
const chunks = [];
|
|
603
|
+
let bytes = 0;
|
|
604
|
+
for (;;) {
|
|
605
|
+
assertGateActive(context);
|
|
606
|
+
const chunk = Buffer.alloc(Math.min(65536, limit - bytes + 1));
|
|
607
|
+
const read = await handle.read(chunk, 0, chunk.length, null);
|
|
608
|
+
assertGateActive(context);
|
|
609
|
+
if (read.bytesRead === 0)
|
|
610
|
+
break;
|
|
611
|
+
bytes += read.bytesRead;
|
|
612
|
+
if (bytes > limit)
|
|
613
|
+
throw new Error('File content exceeds declared byte limit');
|
|
614
|
+
chunks.push(chunk.subarray(0, read.bytesRead));
|
|
615
|
+
}
|
|
616
|
+
fileContent = Buffer.concat(chunks);
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
await handle.close();
|
|
620
|
+
}
|
|
204
621
|
}
|
|
205
|
-
return
|
|
622
|
+
return fileContent;
|
|
206
623
|
});
|
|
624
|
+
assertGateActive(context);
|
|
207
625
|
if (failure) {
|
|
208
626
|
failures.push(failure);
|
|
209
627
|
}
|
|
@@ -211,7 +629,7 @@ async function runFileGate(gate, index, projectRoot) {
|
|
|
211
629
|
const durationMs = Date.now() - startMs;
|
|
212
630
|
if (failures.length > 0) {
|
|
213
631
|
const evidence = `path=${filePath}\n${failures.join('\n')}`;
|
|
214
|
-
return makeResult(index, gate, 'fail', durationMs, truncateString(evidence,
|
|
632
|
+
return makeResult(index, gate, 'fail', durationMs, truncateString(evidence, MAX_EVIDENCE_CHARS), failures[0]);
|
|
215
633
|
}
|
|
216
634
|
return makeResult(index, gate, 'pass', durationMs, `path=${filePath} — all assertions passed`);
|
|
217
635
|
}
|
|
@@ -245,7 +663,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
|
|
|
245
663
|
case 'contains': {
|
|
246
664
|
if (!fileExists)
|
|
247
665
|
return `File does not exist: ${filePath}`;
|
|
248
|
-
const content = await getContent();
|
|
666
|
+
const content = (await getContent()).toString('utf8');
|
|
249
667
|
return content.includes(assertion.value)
|
|
250
668
|
? null
|
|
251
669
|
: `File does not contain: ${JSON.stringify(assertion.value)}`;
|
|
@@ -253,7 +671,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
|
|
|
253
671
|
case 'matches': {
|
|
254
672
|
if (!fileExists)
|
|
255
673
|
return `File does not exist: ${filePath}`;
|
|
256
|
-
const content = await getContent();
|
|
674
|
+
const content = (await getContent()).toString('utf8');
|
|
257
675
|
const re = new RegExp(assertion.regex, assertion.flags);
|
|
258
676
|
return re.test(content)
|
|
259
677
|
? null
|
|
@@ -262,7 +680,7 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
|
|
|
262
680
|
case 'sha256': {
|
|
263
681
|
if (!fileExists)
|
|
264
682
|
return `File does not exist: ${filePath}`;
|
|
265
|
-
const raw = await
|
|
683
|
+
const raw = await getContent();
|
|
266
684
|
const hash = createHash('sha256').update(raw).digest('hex');
|
|
267
685
|
return hash === assertion.value
|
|
268
686
|
? null
|
|
@@ -276,60 +694,19 @@ async function checkFileAssertion(assertion, filePath, fileExists, fileSize, get
|
|
|
276
694
|
}
|
|
277
695
|
}
|
|
278
696
|
// ─── Command gate ─────────────────────────────────────────────────────────────
|
|
279
|
-
async function runCommandGate(gate, index, projectRoot, timeoutMs) {
|
|
280
|
-
const
|
|
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);
|
|
697
|
+
async function runCommandGate(gate, index, projectRoot, timeoutMs, context) {
|
|
698
|
+
const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
|
|
699
|
+
const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context, gate.env);
|
|
700
|
+
const expected = gate.exitCode ?? 0;
|
|
701
|
+
let reason = captured.exitCode === expected
|
|
702
|
+
? undefined
|
|
703
|
+
: `Exit code ${captured.exitCode} (expected ${expected})`;
|
|
704
|
+
if (!reason && gate.stdoutMatches && !new RegExp(gate.stdoutMatches).test(captured.stdout))
|
|
705
|
+
reason = 'stdout pattern did not match';
|
|
706
|
+
if (!reason && gate.stderrMatches && !new RegExp(gate.stderrMatches).test(captured.stderr))
|
|
707
|
+
reason = 'stderr pattern did not match';
|
|
708
|
+
return processGateResult(index, gate, captured, reason ? 'fail' : 'pass', reason);
|
|
331
709
|
}
|
|
332
|
-
// ─── Lint gate ────────────────────────────────────────────────────────────────
|
|
333
710
|
/** Tool-specific CLI arguments and failure patterns. */
|
|
334
711
|
const LINT_TOOL_DEFAULTS = {
|
|
335
712
|
biome: { cmd: 'biome', defaultArgs: ['check', '.'] },
|
|
@@ -343,81 +720,57 @@ const LINT_TOOL_DEFAULTS = {
|
|
|
343
720
|
errorPattern: /^error/m,
|
|
344
721
|
},
|
|
345
722
|
};
|
|
346
|
-
async function runLintGate(gate, index, projectRoot, timeoutMs) {
|
|
347
|
-
const
|
|
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);
|
|
723
|
+
async function runLintGate(gate, index, projectRoot, timeoutMs, context) {
|
|
724
|
+
const tool = LINT_TOOL_DEFAULTS[gate.tool];
|
|
725
|
+
const invocation = executableInvocation(gate, projectRoot, context.env ?? process.env);
|
|
726
|
+
const captured = await captureGateCommand(invocation.command, invocation.args, invocation.cwd, timeoutMs, context);
|
|
727
|
+
const passed = captured.exitCode === 0 &&
|
|
728
|
+
(gate.expect === 'noErrors' || !tool.errorPattern?.test(captured.stdout + captured.stderr));
|
|
729
|
+
return processGateResult(index, gate, captured, passed ? 'pass' : 'fail', `${gate.tool} reported errors (exit ${captured.exitCode})`);
|
|
392
730
|
}
|
|
393
731
|
// ─── HTTP gate ────────────────────────────────────────────────────────────────
|
|
394
|
-
async function runHttpGate(gate, index, _projectRoot, timeoutMs) {
|
|
732
|
+
async function runHttpGate(gate, index, _projectRoot, timeoutMs, context) {
|
|
395
733
|
const startMs = Date.now();
|
|
396
|
-
|
|
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
|
-
}
|
|
734
|
+
if (gate.startCommand)
|
|
735
|
+
throw new Error('HTTP server startup requires an admitted owned service lifetime; unowned detached startup is unavailable');
|
|
407
736
|
let statusCode = 0;
|
|
408
737
|
let body = '';
|
|
409
738
|
let errorMsg;
|
|
410
739
|
try {
|
|
411
740
|
const controller = new AbortController();
|
|
412
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
741
|
+
const timer = setTimeout(() => controller.abort(), Math.max(0, Math.min(timeoutMs, context.execution.deadlineAt - Date.now())));
|
|
413
742
|
try {
|
|
414
743
|
const response = await fetch(gate.url, {
|
|
415
744
|
method: gate.method ?? 'GET',
|
|
416
745
|
headers: gate.headers,
|
|
417
|
-
signal:
|
|
746
|
+
signal: context.execution.signal
|
|
747
|
+
? AbortSignal.any([controller.signal, context.execution.signal])
|
|
748
|
+
: controller.signal,
|
|
418
749
|
});
|
|
419
750
|
statusCode = response.status;
|
|
420
|
-
|
|
751
|
+
if (response.body) {
|
|
752
|
+
const reader = response.body.getReader();
|
|
753
|
+
const chunks = [];
|
|
754
|
+
let bytes = 0;
|
|
755
|
+
try {
|
|
756
|
+
for (;;) {
|
|
757
|
+
assertGateActive(context);
|
|
758
|
+
const next = await reader.read();
|
|
759
|
+
if (next.done)
|
|
760
|
+
break;
|
|
761
|
+
bytes += next.value.byteLength;
|
|
762
|
+
if (bytes > (context.maxOutputBytes ?? 1_048_576))
|
|
763
|
+
throw new Error('HTTP body exceeds declared byte limit');
|
|
764
|
+
chunks.push(next.value);
|
|
765
|
+
}
|
|
766
|
+
body = Buffer.concat(chunks).toString('utf8');
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
await reader.cancel();
|
|
770
|
+
reader.releaseLock();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
assertGateActive(context);
|
|
421
774
|
}
|
|
422
775
|
finally {
|
|
423
776
|
clearTimeout(timer);
|
|
@@ -426,19 +779,9 @@ async function runHttpGate(gate, index, _projectRoot, timeoutMs) {
|
|
|
426
779
|
catch (err) {
|
|
427
780
|
errorMsg = err instanceof Error ? err.message : String(err);
|
|
428
781
|
}
|
|
429
|
-
finally {
|
|
430
|
-
if (serverProcess) {
|
|
431
|
-
try {
|
|
432
|
-
serverProcess.kill();
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
// Best-effort cleanup
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
782
|
const durationMs = Date.now() - startMs;
|
|
440
783
|
if (errorMsg) {
|
|
441
|
-
return makeResult(index, gate, '
|
|
784
|
+
return makeResult(index, gate, 'error', durationMs, errorMsg, `HTTP gate did not finish: ${errorMsg}`);
|
|
442
785
|
}
|
|
443
786
|
if (statusCode !== gate.status) {
|
|
444
787
|
return makeResult(index, gate, 'fail', durationMs, `HTTP ${statusCode}`, `Expected status ${gate.status}, got ${statusCode}`);
|