@intentius/chant 0.101.0 → 0.102.0
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/agents/checks.d.ts.map +1 -1
- package/dist/attrref.d.ts.map +1 -1
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/nginx.d.ts.map +1 -1
- package/dist/audit/prior-art.d.ts +48 -0
- package/dist/audit/prior-art.d.ts.map +1 -1
- package/dist/behaviour-kinds.d.ts.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +16 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts +8 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +19 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +9 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
- package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +9 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/build.d.ts.map +1 -1
- package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/tools/import.d.ts.map +1 -1
- package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
- package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
- package/dist/cli/mcp/tools/search.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +11 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts +67 -3
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/process-runner.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/verify.d.ts.map +1 -1
- package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts.map +1 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +19 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/cache.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -5
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +16 -0
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.d.ts +37 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +3 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-layout.d.ts.map +1 -1
- package/dist/import/base-parser.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +7 -0
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/import/parser-options.d.ts +14 -0
- package/dist/import/parser-options.d.ts.map +1 -0
- package/dist/import/parser.d.ts +17 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +3 -3
- package/dist/lexicon.d.ts +55 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +15 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +61 -2
- package/dist/lifecycle/work-lease.d.ts.map +1 -1
- package/dist/lint/config.d.ts +8 -8
- package/dist/lint/engine.d.ts +6 -0
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +11 -11
- package/dist/op/activities/apply.d.ts +79 -9
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +1 -1
- package/dist/op/activity-failure.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/apply-op.d.ts +12 -5
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
- package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts.map +1 -1
- package/dist/op/decide-read-inputs.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.d.ts +21 -2
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts +5 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts.map +1 -1
- package/dist/op/gate.d.ts +19 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +2 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +6 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/run-context.d.ts +39 -0
- package/dist/op/run-context.d.ts.map +1 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts.map +1 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +8 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +24 -4
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/project-root.d.ts.map +1 -1
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/pseudo-parameter.d.ts.map +1 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts.map +1 -1
- package/dist/terraform/emit-source.d.ts.map +1 -1
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
- package/dist/workspace/agent-cli.d.ts +5 -0
- package/dist/workspace/agent-cli.d.ts.map +1 -1
- package/dist/workspace/box-factory.d.ts +73 -0
- package/dist/workspace/box-factory.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts.map +1 -1
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +5 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +10 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +67 -6
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +12 -3
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +368 -0
- package/dist/workspace/conformance/writer.d.ts.map +1 -0
- package/dist/workspace/decide.d.ts +1 -1
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +91 -4
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +234 -5
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cli.d.ts +2 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/identity.d.ts +111 -0
- package/dist/workspace/identity.d.ts.map +1 -0
- package/dist/workspace/intent-cli.d.ts +7 -4
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +8 -1
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +149 -5
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/kinds.d.ts +18 -4
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lineage-hash-index.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +2 -2
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/principal-classes.d.ts +114 -0
- package/dist/workspace/principal-classes.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +21 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-checkout.d.ts +54 -0
- package/dist/workspace/records-checkout.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +27 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-since.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +5 -4
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +15 -0
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runs-cli.d.ts +164 -0
- package/dist/workspace/runs-cli.d.ts.map +1 -0
- package/dist/workspace/runs.d.ts +423 -0
- package/dist/workspace/runs.d.ts.map +1 -0
- package/dist/workspace/source-block.d.ts +1 -1
- package/dist/workspace/source-block.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +15 -3
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +10 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +52 -0
- package/dist/workspace/symbols.d.ts.map +1 -0
- package/dist/workspace/trailer-joins.d.ts +46 -0
- package/dist/workspace/trailer-joins.d.ts.map +1 -0
- package/dist/workspace/trailers.d.ts +89 -0
- package/dist/workspace/trailers.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.d.ts.map +1 -1
- package/dist/workspace/trust/seal.d.ts +33 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/test-repo.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +39 -3
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work.d.ts +26 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/dist/workspace/workspace-principals.schema.json +60 -0
- package/dist/workspace/write-scope.d.ts +79 -19
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/yaml.d.ts +21 -39
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/audit/prior-art.ts +15 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
- package/src/cli/commands/carve-emit-state.test.ts +4 -0
- package/src/cli/commands/carve-emit.ts +5 -19
- package/src/cli/commands/check-lexicon.test.ts +62 -1
- package/src/cli/commands/check-lexicon.ts +36 -6
- package/src/cli/commands/import-live.test.ts +18 -0
- package/src/cli/commands/import-parser-options.test.ts +133 -0
- package/src/cli/commands/import.ts +21 -3
- package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
- package/src/cli/commands/init.test.ts +8 -1
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/lint.test.ts +37 -0
- package/src/cli/commands/onboard.test.ts +53 -3
- package/src/cli/commands/onboard.ts +52 -4
- package/src/cli/commands/pinned-upgrade.ts +73 -33
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/dev.ts +8 -6
- package/src/cli/handlers/lifecycle.test.ts +67 -0
- package/src/cli/handlers/lifecycle.ts +6 -5
- package/src/cli/handlers/misc.ts +2 -0
- package/src/cli/handlers/operator.ts +87 -3
- package/src/cli/handlers/run.test.ts +111 -0
- package/src/cli/handlers/run.ts +85 -24
- package/src/cli/handlers/scenario.ts +2 -2
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +65 -11
- package/src/cli/mcp/workspace-tools.test.ts +5 -2
- package/src/cli/mcp/workspace-tools.ts +29 -2
- package/src/cli/registry.ts +11 -2
- package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
- package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
- package/src/codegen/pinned-upgrade.test.ts +104 -0
- package/src/codegen/pinned-upgrade.ts +250 -56
- package/src/codegen/testdata/postgres-tags.json +242 -0
- package/src/config.test.ts +21 -0
- package/src/config.ts +15 -0
- package/src/declarable.test.ts +21 -0
- package/src/declarable.ts +19 -0
- package/src/discovery/fold-composite.test.ts +41 -5
- package/src/discovery/fold-import.test.ts +129 -0
- package/src/discovery/fold-import.ts +643 -131
- package/src/discovery/fold-package-composite.test.ts +264 -0
- package/src/discovery/fold-specifiers.test.ts +191 -0
- package/src/discovery/param-deps.test.ts +66 -1
- package/src/discovery/param-deps.ts +45 -0
- package/src/discovery/specifier-resolve.ts +344 -0
- package/src/fold/fold.test.ts +39 -1
- package/src/fold/fold.ts +83 -9
- package/src/fold-provenance.test.ts +140 -0
- package/src/graph-ir.test.ts +83 -0
- package/src/graph-ir.ts +29 -0
- package/src/import/embedded.test.ts +20 -0
- package/src/import/embedded.ts +13 -0
- package/src/import/parser-options.ts +46 -0
- package/src/import/parser.ts +20 -0
- package/src/lexicon.ts +55 -3
- package/src/lifecycle/gate-ledger.ts +16 -0
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +6 -4
- package/src/lifecycle/snapshot.ts +2 -2
- package/src/lifecycle/work-lease.test.ts +1 -1
- package/src/lifecycle/work-lease.ts +93 -2
- package/src/lint/engine.test.ts +55 -0
- package/src/lint/engine.ts +30 -4
- package/src/lint/output-docs.test.ts +59 -56
- package/src/op/activities/apply.test.ts +189 -5
- package/src/op/activities/apply.ts +230 -8
- package/src/op/activities/index.ts +3 -0
- package/src/op/activities/lexicon-upgrade.test.ts +160 -6
- package/src/op/activities/lexicon-upgrade.ts +154 -51
- package/src/op/composites/apply-op.ts +12 -5
- package/src/op/composites/composites.test.ts +30 -3
- package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
- package/src/op/composites/lexicon-upgrade-op.ts +29 -8
- package/src/op/emulator-lifecycle.test.ts +23 -0
- package/src/op/emulator-lifecycle.ts +52 -14
- package/src/op/gate-approval.test.ts +74 -0
- package/src/op/gate-approval.ts +5 -1
- package/src/op/gate.ts +31 -5
- package/src/op/index.ts +2 -0
- package/src/op/local-executor.test.ts +15 -0
- package/src/op/local-executor.ts +40 -12
- package/src/op/local-output.ts +1 -1
- package/src/op/operator.ts +4 -0
- package/src/op/run-context.test.ts +141 -0
- package/src/op/run-context.ts +51 -0
- package/src/op/runtimes/local.test.ts +14 -0
- package/src/op/runtimes/local.ts +2 -0
- package/src/op/steward-beside.ts +2 -0
- package/src/op/types.ts +8 -3
- package/src/op/work-lease-run.test.ts +47 -0
- package/src/op/work-lease-run.ts +73 -11
- package/src/provenance.ts +35 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/agent-cli.ts +7 -3
- package/src/workspace/agent.schema.json +22 -0
- package/src/workspace/box-factory.test.ts +320 -0
- package/src/workspace/box-factory.ts +103 -0
- package/src/workspace/changes.schema.json +12 -20
- package/src/workspace/changes.ts +4 -1
- package/src/workspace/checks/boxes.test.ts +2 -0
- package/src/workspace/checks.ts +29 -5
- package/src/workspace/compose-graph.ts +7 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +27 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +95 -0
- package/src/workspace/conformance/__writer_fixture__/decisions/points.json +132 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +48 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +108 -0
- package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
- package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +578 -0
- package/src/workspace/conformance/conformance.e2e.test.ts +35 -3
- package/src/workspace/conformance/index.mjs +30 -3
- package/src/workspace/conformance/index.ts +103 -15
- package/src/workspace/conformance/vitest.ts +44 -2
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +131 -0
- package/src/workspace/conformance/writer.test.ts +156 -0
- package/src/workspace/conformance/writer.ts +880 -0
- package/src/workspace/decide.ts +7 -1
- package/src/workspace/declaration.schema.json +234 -5
- package/src/workspace/declaration.ts +174 -10
- package/src/workspace/graph-cli.ts +5 -2
- package/src/workspace/graph.schema.json +247 -0
- package/src/workspace/identity.test.ts +196 -0
- package/src/workspace/identity.ts +186 -0
- package/src/workspace/intent-cli.ts +64 -10
- package/src/workspace/intent-record.schema.json +208 -0
- package/src/workspace/intent-record.ts +35 -1
- package/src/workspace/intent-why.test.ts +274 -0
- package/src/workspace/intent.schema.json +718 -7
- package/src/workspace/intent.test.ts +11 -2
- package/src/workspace/intent.ts +495 -32
- package/src/workspace/kinds.ts +44 -37
- package/src/workspace/points-write.schema.json +1 -0
- package/src/workspace/principal-classes.test.ts +175 -0
- package/src/workspace/principal-classes.ts +307 -0
- package/src/workspace/reason-codes.test.ts +11 -3
- package/src/workspace/reason-codes.ts +24 -1
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-checkout.ts +106 -0
- package/src/workspace/records-cli.ts +103 -11
- package/src/workspace/records-close.schema.json +1 -0
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-uncommitted.test.ts +171 -0
- package/src/workspace/records-write.ts +28 -6
- package/src/workspace/records.schema.json +72 -1
- package/src/workspace/records.ts +27 -0
- package/src/workspace/runs-cli.ts +381 -0
- package/src/workspace/runs-write.schema.json +583 -0
- package/src/workspace/runs.schema.json +810 -0
- package/src/workspace/runs.test.ts +242 -0
- package/src/workspace/runs.ts +709 -0
- package/src/workspace/status-contract.test.ts +5 -0
- package/src/workspace/status-gates.ts +22 -8
- package/src/workspace/status.schema.json +127 -2
- package/src/workspace/status.ts +21 -1
- package/src/workspace/symbols.ts +180 -0
- package/src/workspace/trailer-joins.test.ts +171 -0
- package/src/workspace/trailer-joins.ts +82 -0
- package/src/workspace/trailers.ts +148 -0
- package/src/workspace/trust/seal.ts +52 -1
- package/src/workspace/work-cli.e2e.test.ts +28 -1
- package/src/workspace/work-cli.ts +133 -8
- package/src/workspace/work-evidence.schema.json +2 -0
- package/src/workspace/work-history.schema.json +73 -1
- package/src/workspace/work-history.test.ts +48 -5
- package/src/workspace/work-kind-fields.test.ts +154 -0
- package/src/workspace/work-lease.schema.json +14 -1
- package/src/workspace/work.ts +82 -0
- package/src/workspace/workspace-principals.schema.json +60 -0
- package/src/workspace/write-scope.test.ts +128 -2
- package/src/workspace/write-scope.ts +170 -45
- package/src/yaml.test.ts +111 -1
- package/src/yaml.ts +141 -549
|
@@ -16,8 +16,25 @@ export interface EmulatorSpec {
|
|
|
16
16
|
image: string;
|
|
17
17
|
/** Port the emulator listens on inside the container (e.g. 4566, 4280). */
|
|
18
18
|
containerPort: number;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Health path on the host port (e.g. `/_localstack/health`, `/_mudflaps/health`).
|
|
21
|
+
* Required unless {@link readyCommand} is set: an emulator is reached over
|
|
22
|
+
* HTTP or checks itself inside its container, one or the other.
|
|
23
|
+
*/
|
|
24
|
+
healthPath?: string;
|
|
25
|
+
/**
|
|
26
|
+
* A command run inside the container (`docker exec <name> ...`) whose exit
|
|
27
|
+
* status 0 means ready, for an emulator with no HTTP health endpoint: a
|
|
28
|
+
* database server answers its own client (`pg_isready`), not a URL. When
|
|
29
|
+
* set, it replaces the HTTP poll of {@link healthPath}.
|
|
30
|
+
*/
|
|
31
|
+
readyCommand?: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* The endpoint a tool connects to, for an emulator that is not reached over
|
|
34
|
+
* HTTP, e.g. a `postgres://` URL for a database server.
|
|
35
|
+
* Default: `http://localhost:<port>`.
|
|
36
|
+
*/
|
|
37
|
+
endpoint?: (port: number) => string;
|
|
21
38
|
/** Readiness predicate over the health body. Default: any 200 response is ready. */
|
|
22
39
|
ready?: (healthBody: string) => boolean;
|
|
23
40
|
/** Extra `docker run` args inserted before the image (e.g. a socket mount). */
|
|
@@ -102,6 +119,8 @@ export interface EmulatorLifecycle {
|
|
|
102
119
|
existsCommand(name: string): string;
|
|
103
120
|
rmCommand(name: string): string;
|
|
104
121
|
healthUrl(port: number): string;
|
|
122
|
+
/** `docker exec <name> <readyCommand...>`, or undefined for an emulator polled over HTTP. */
|
|
123
|
+
readyExecCommand(name: string): string | undefined;
|
|
105
124
|
endpoint(port: number): string;
|
|
106
125
|
up(args?: EmulatorUpArgs, signal?: AbortSignal): Promise<{ endpoint: string }>;
|
|
107
126
|
down(args?: { name?: string }, signal?: AbortSignal): Promise<void>;
|
|
@@ -116,6 +135,9 @@ export interface EmulatorLifecycle {
|
|
|
116
135
|
*/
|
|
117
136
|
export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
|
|
118
137
|
const ready = spec.ready ?? (() => true);
|
|
138
|
+
if (spec.healthPath === undefined && !spec.readyCommand?.length) {
|
|
139
|
+
throw new Error(`emulator "${spec.name}" declares neither a healthPath nor a readyCommand, so nothing can tell when it is ready`);
|
|
140
|
+
}
|
|
119
141
|
|
|
120
142
|
const runCommand = (args: EmulatorUpArgs = {}): string => {
|
|
121
143
|
const name = args.name ?? spec.name;
|
|
@@ -128,8 +150,30 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
|
|
|
128
150
|
};
|
|
129
151
|
const existsCommand = (name: string): string => `docker ps -q -f name=${name}`;
|
|
130
152
|
const rmCommand = (name: string): string => `docker rm -f ${name}`;
|
|
131
|
-
const healthUrl = (port: number): string => `http://localhost:${port}${spec.healthPath}`;
|
|
132
|
-
const endpoint = (port: number): string => `http://localhost:${port}
|
|
153
|
+
const healthUrl = (port: number): string => `http://localhost:${port}${spec.healthPath ?? ""}`;
|
|
154
|
+
const endpoint = (port: number): string => (spec.endpoint ? spec.endpoint(port) : `http://localhost:${port}`);
|
|
155
|
+
const readyExecCommand = (name: string): string | undefined =>
|
|
156
|
+
spec.readyCommand?.length ? ["docker", "exec", name, ...spec.readyCommand].join(" ") : undefined;
|
|
157
|
+
|
|
158
|
+
/** One readiness probe: the in-container command when declared, else the HTTP health path. */
|
|
159
|
+
async function probe(name: string, port: number, signal?: AbortSignal): Promise<boolean> {
|
|
160
|
+
const command = readyExecCommand(name);
|
|
161
|
+
if (command) {
|
|
162
|
+
try {
|
|
163
|
+
await execAsync(command, { signal });
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(healthUrl(port), { signal });
|
|
171
|
+
return res.ok && ready(await res.text());
|
|
172
|
+
} catch {
|
|
173
|
+
// Not up yet (connection refused / non-2xx) — retry.
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
133
177
|
|
|
134
178
|
async function up(args: EmulatorUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> {
|
|
135
179
|
const name = args.name ?? spec.name;
|
|
@@ -153,19 +197,13 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
|
|
|
153
197
|
await execAsync(runCommand({ ...args, name, port }), { signal });
|
|
154
198
|
}
|
|
155
199
|
|
|
156
|
-
const url = healthUrl(port);
|
|
157
200
|
const deadline = Date.now() + timeoutMs;
|
|
158
201
|
let ok = false;
|
|
159
202
|
while (Date.now() < deadline) {
|
|
160
203
|
if (signal?.aborted) throw new Error(`emulator "${name}" wait aborted`);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ok = true;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
} catch {
|
|
168
|
-
// Not up yet (connection refused / non-2xx) — retry.
|
|
204
|
+
if (await probe(name, port, signal)) {
|
|
205
|
+
ok = true;
|
|
206
|
+
break;
|
|
169
207
|
}
|
|
170
208
|
await sleep(intervalMs, signal);
|
|
171
209
|
}
|
|
@@ -187,5 +225,5 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
|
|
|
187
225
|
}
|
|
188
226
|
}
|
|
189
227
|
|
|
190
|
-
return { runCommand, existsCommand, rmCommand, healthUrl, endpoint, up, down };
|
|
228
|
+
return { runCommand, existsCommand, rmCommand, healthUrl, readyExecCommand, endpoint, up, down };
|
|
191
229
|
}
|
|
@@ -191,6 +191,80 @@ describe("gate approval — policy decisions (#2508)", () => {
|
|
|
191
191
|
expect(check.satisfied).toBe(false);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
test("log-only then enforce: a permit recorded under log-only does not pass the enforced gate (#2512)", async () => {
|
|
195
|
+
const logOnly: ResolvedGateApproval = { policy: POLICY, mode: "log-only", context: { risk: "low" } };
|
|
196
|
+
const enforce: ResolvedGateApproval = { ...logOnly, mode: "enforce" };
|
|
197
|
+
// The pending fact was recorded while the gate was log-only, and the agent's allow was recorded against it.
|
|
198
|
+
const port = memoryGateLedgerPort({
|
|
199
|
+
resolutions: [approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("log-only") })],
|
|
200
|
+
pending: [{ ...PENDING, approval: logOnly }],
|
|
201
|
+
});
|
|
202
|
+
const check = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: NOW });
|
|
203
|
+
expect(check.satisfied).toBe(false);
|
|
204
|
+
|
|
205
|
+
const tally = tallyGateApprovals(
|
|
206
|
+
[approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("log-only") })],
|
|
207
|
+
"ship", PENDING.timestamp, PLAN_A, enforce,
|
|
208
|
+
);
|
|
209
|
+
expect(tally.permit).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("log-only then enforce: a human's approval recorded under log-only still counts toward the quorum (#2512)", async () => {
|
|
213
|
+
const logOnly: ResolvedGateApproval = { quorum: { count: 1 }, policy: POLICY, mode: "log-only" };
|
|
214
|
+
const enforce: ResolvedGateApproval = { ...logOnly, mode: "enforce" };
|
|
215
|
+
const port = memoryGateLedgerPort({
|
|
216
|
+
resolutions: [approval({ resolvedBy: "alex", timestamp: "2026-09-01T01:00:00.000Z", policyDecision: allow("log-only") })],
|
|
217
|
+
pending: [{ ...PENDING, approval: logOnly }],
|
|
218
|
+
});
|
|
219
|
+
const check = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: NOW });
|
|
220
|
+
expect(check.satisfied).toBe(true);
|
|
221
|
+
if (!check.satisfied) return;
|
|
222
|
+
expect(check.via).toBe("quorum");
|
|
223
|
+
expect(check.resolution.resolvedBy).toBe("alex");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("log-only then enforce: the gated run re-records the pending fact under enforce, and only a fresh permit passes (#2512)", async () => {
|
|
227
|
+
const logOnly: ResolvedGateApproval = { policy: POLICY, mode: "log-only", context: { risk: "low" } };
|
|
228
|
+
const enforce: ResolvedGateApproval = { ...logOnly, mode: "enforce" };
|
|
229
|
+
const stale = approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("log-only") });
|
|
230
|
+
const port = memoryGateLedgerPort({ resolutions: [stale], pending: [{ ...PENDING, approval: logOnly }] });
|
|
231
|
+
|
|
232
|
+
const first = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: NOW });
|
|
233
|
+
expect(first.satisfied).toBe(false);
|
|
234
|
+
if (first.satisfied) return;
|
|
235
|
+
// The standing fact described the log-only gate, so a fresh one carries
|
|
236
|
+
// enforce, and `chant approve` evaluates the next agent approval under it.
|
|
237
|
+
expect(first.recorded).toBe(true);
|
|
238
|
+
expect(first.pending.approval?.mode).toBe("enforce");
|
|
239
|
+
|
|
240
|
+
// Re-running without a new approval still waits: the stale permit predates the new pending fact as well.
|
|
241
|
+
const again = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: "2026-09-01T12:30:00.000Z" });
|
|
242
|
+
expect(again.satisfied).toBe(false);
|
|
243
|
+
|
|
244
|
+
const fresh = approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T13:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("enforce") });
|
|
245
|
+
const after = await evaluateGate(
|
|
246
|
+
memoryGateLedgerPort({ resolutions: [stale, fresh], pending: [first.pending] }),
|
|
247
|
+
{ op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: "2026-09-01T14:00:00.000Z" },
|
|
248
|
+
);
|
|
249
|
+
expect(after.satisfied).toBe(true);
|
|
250
|
+
if (!after.satisfied) return;
|
|
251
|
+
expect(after.via).toBe("policy");
|
|
252
|
+
expect(after.resolution.timestamp).toBe(fresh.timestamp);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("enforce: a recorded decision with no mode does not pass the gate (#2512)", async () => {
|
|
256
|
+
const block: ResolvedGateApproval = { policy: POLICY, mode: "enforce" };
|
|
257
|
+
const { mode: _mode, ...noMode } = allow()!;
|
|
258
|
+
const { check } = await decide(
|
|
259
|
+
[approval({
|
|
260
|
+
resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" },
|
|
261
|
+
policyDecision: noMode as GateResolutionRecord["policyDecision"],
|
|
262
|
+
})],
|
|
263
|
+
block,
|
|
264
|
+
);
|
|
265
|
+
expect(check.satisfied).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
194
268
|
test("enforce: a deny-by-default policy keeps the old behaviour, where only a human's approval counts", async () => {
|
|
195
269
|
const block: ResolvedGateApproval = { policy: POLICY, mode: "enforce" };
|
|
196
270
|
const deny = { ...allow()!, decision: "deny" as const, determining: ["floor"] };
|
package/src/op/gate-approval.ts
CHANGED
|
@@ -119,7 +119,11 @@ export interface GatePolicyAnswer {
|
|
|
119
119
|
export interface GatePolicyDecision extends GatePolicyAnswer {
|
|
120
120
|
policy: string;
|
|
121
121
|
version: string;
|
|
122
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* The gate's mode when the decision was recorded. A permit passes a gate
|
|
124
|
+
* only when both this and the gate's current mode are `enforce` (#2512), so
|
|
125
|
+
* an allow recorded under `log-only` never binds after a switch.
|
|
126
|
+
*/
|
|
123
127
|
mode: GateApprovalMode;
|
|
124
128
|
}
|
|
125
129
|
|
package/src/op/gate.ts
CHANGED
|
@@ -74,6 +74,19 @@ export interface PendingGatePush {
|
|
|
74
74
|
export interface GateLedgerPort {
|
|
75
75
|
read(op: string): Promise<{ resolutions: GateResolutionRecord[]; pending: PendingGateRecord[] }>;
|
|
76
76
|
appendPending(input: PendingGateInput): Promise<PendingGatePush>;
|
|
77
|
+
/**
|
|
78
|
+
* The rule a workspace sets for approvals of `gate` (#3163, ws-080), or
|
|
79
|
+
* null when none applies: the declaration's `identity.gates`, read at base.
|
|
80
|
+
* Absent, every approval is judged as before.
|
|
81
|
+
*/
|
|
82
|
+
approvalRule?(gate: string): Promise<GateApprovalRule | null>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A workspace's rule for which approvals of a gate count (#3163): a signed approval, from a class when it names one. */
|
|
86
|
+
export interface GateApprovalRule {
|
|
87
|
+
requirement: { gate: string; class: string | null };
|
|
88
|
+
/** Why `approval` doesn't count, or null when it does. */
|
|
89
|
+
refuses(approval: GateResolutionRecord): string | null;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/**
|
|
@@ -102,6 +115,11 @@ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
|
|
|
102
115
|
const { resolutions, pending } = await readGateLedger(op, opts);
|
|
103
116
|
return { resolutions, pending };
|
|
104
117
|
},
|
|
118
|
+
async approvalRule(gate) {
|
|
119
|
+
// Loaded on demand: the rule lives in the workspace modules, which import the Op modules.
|
|
120
|
+
const { gateAdmission } = await import("../workspace/identity");
|
|
121
|
+
return gateAdmission(opts?.cwd ?? process.cwd(), gate);
|
|
122
|
+
},
|
|
105
123
|
async appendPending(input) {
|
|
106
124
|
const { record } = await appendPendingGate(input, opts);
|
|
107
125
|
try {
|
|
@@ -288,7 +306,7 @@ export interface GateTally {
|
|
|
288
306
|
counted: GateResolutionRecord[];
|
|
289
307
|
/** The quorum's count, 1 when the gate declares none. */
|
|
290
308
|
need: number;
|
|
291
|
-
/** The newest approval whose recorded permit passes the gate on its own. Only in `enforce` mode, and only under the gate's current policy version. */
|
|
309
|
+
/** The newest approval whose recorded permit passes the gate on its own. Only in `enforce` mode, only for a permit recorded under `enforce`, and only under the gate's current policy version. */
|
|
292
310
|
permit?: GateResolutionRecord;
|
|
293
311
|
/** The newest approval of this gate for a different plan, when the gate binds one. */
|
|
294
312
|
mismatched?: GateResolutionRecord;
|
|
@@ -305,8 +323,9 @@ export interface GateTally {
|
|
|
305
323
|
* Of those, only a human's counts toward the quorum, once per `resolvedBy`,
|
|
306
324
|
* and only with one of the quorum's roles when it names any. An agent counts
|
|
307
325
|
* only through a recorded `allow` in `enforce` mode, evaluated under the
|
|
308
|
-
* policy version the gate declares now
|
|
309
|
-
*
|
|
326
|
+
* policy version the gate declares now and recorded while the gate was in
|
|
327
|
+
* `enforce` (#2512). A `log-only` decision never changes the outcome, even
|
|
328
|
+
* after the gate switches to `enforce`, and a `deny` never removes a human's approval: a policy can add
|
|
310
329
|
* a way through the gate but cannot take one away.
|
|
311
330
|
*/
|
|
312
331
|
export function tallyGateApprovals(
|
|
@@ -331,8 +350,12 @@ export function tallyGateApprovals(
|
|
|
331
350
|
}
|
|
332
351
|
|
|
333
352
|
const decision = r.policyDecision;
|
|
353
|
+
// #2512: the decision must also have been recorded under `enforce`. An
|
|
354
|
+
// allow recorded while the gate was log-only was never binding, and a
|
|
355
|
+
// later switch to enforce does not make it binding.
|
|
334
356
|
if (
|
|
335
357
|
approval.mode === "enforce" && approval.policy && decision?.decision === "allow" &&
|
|
358
|
+
decision.mode === "enforce" &&
|
|
336
359
|
decision.version === approval.policy.version && (!permit || at(r) >= at(permit))
|
|
337
360
|
) {
|
|
338
361
|
permit = r;
|
|
@@ -394,10 +417,13 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
|
|
|
394
417
|
// a mismatch with a message instead of silently not counting.
|
|
395
418
|
const env = input.environment;
|
|
396
419
|
const pending = env === undefined ? ledger.pending : ledger.pending.filter((p) => p.environment === env);
|
|
397
|
-
|
|
420
|
+
// #3163: a gate the workspace's identity.gates names counts only signed approvals from its class.
|
|
421
|
+
const rule = (await port.approvalRule?.(input.gate)) ?? null;
|
|
422
|
+
const resolutions = (
|
|
398
423
|
env === undefined
|
|
399
424
|
? ledger.resolutions
|
|
400
|
-
: ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env)
|
|
425
|
+
: ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env)
|
|
426
|
+
).filter((r) => rule === null || r.gate !== input.gate || rule.refuses(r) === null);
|
|
401
427
|
|
|
402
428
|
const standing = latestPendingGate(pending, input.gate);
|
|
403
429
|
const since = standing?.timestamp ?? EPOCH;
|
package/src/op/index.ts
CHANGED
|
@@ -60,6 +60,8 @@ export type { ActivityProfile, ActivityProfileName } from "./activity-profiles";
|
|
|
60
60
|
export { NonRetryableActivityError, nonRetryableFailure } from "./activity-failure";
|
|
61
61
|
export { runOpLocally, parseDuration, OpRunFailure } from "./local-executor";
|
|
62
62
|
export type { StepRecord, OpRunResult, RunOpOptions } from "./local-executor";
|
|
63
|
+
export { currentOpRun, withOpRunContext } from "./run-context";
|
|
64
|
+
export type { OpRunContext, PassedGate } from "./run-context";
|
|
63
65
|
export { evaluateGate, gitGateLedgerPort, memoryGateLedgerPort, approveCommand, describeGateMismatch } from "./gate";
|
|
64
66
|
export type { GateLedgerPort, GateCheck, GateCheckInput, PendingGatePush, GateDigestMismatch, GateQuorumProgress, GateTally } from "./gate";
|
|
65
67
|
export { tallyGateApprovals, approverOf } from "./gate";
|
|
@@ -232,6 +232,21 @@ describe("runOpLocally — outcomeAttribute", () => {
|
|
|
232
232
|
expect(result.records[0].outcome).toEqual({ name: "Drift", value: true });
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
test("an attribute whose path the result does not hold is left off, not recorded as undefined", async () => {
|
|
236
|
+
const done: ActivityFn = async () => ({ state: "done", switched: false });
|
|
237
|
+
const config = op({
|
|
238
|
+
phases: [{ name: "Switch", steps: [
|
|
239
|
+
{ kind: "activity", fn: "migrationSwitch", outcomeAttribute: [{ name: "OldColumn", from: "oldColumn" }, { name: "State", from: "state" }] },
|
|
240
|
+
{ kind: "activity", fn: "migrationRetain", outcomeAttribute: { name: "RetainUntil", from: "retainUntil" } },
|
|
241
|
+
] }],
|
|
242
|
+
});
|
|
243
|
+
const result = await runOpLocally(config, new Map([["migrationSwitch", done], ["migrationRetain", done]]), PROFILES);
|
|
244
|
+
expect(result.records[0].outcomes).toEqual([{ name: "State", value: "done" }]);
|
|
245
|
+
expect(result.records[0].outcome).toEqual({ name: "State", value: "done" });
|
|
246
|
+
expect(result.records[1].outcomes).toBeUndefined();
|
|
247
|
+
expect(result.records[1].outcome).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
235
250
|
test("a step with no attribute leaves both fields absent", async () => {
|
|
236
251
|
const noop: ActivityFn = async () => ({ ok: true });
|
|
237
252
|
const config = op({ phases: [{ name: "Plan", steps: [{ kind: "activity", fn: "noop" }] }] });
|
package/src/op/local-executor.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { stepTimeoutProblem } from "./activity-profiles";
|
|
|
29
29
|
import { describeGateMismatch, evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "./gate";
|
|
30
30
|
import { gateName } from "./gate-name";
|
|
31
31
|
import type { ResolvedGateApproval } from "./gate-approval";
|
|
32
|
+
import { withOpRunContext, type OpRunContext, type PassedGate } from "./run-context";
|
|
32
33
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
33
34
|
import { isPointWait, type WaitingPoint } from "./steward-points";
|
|
34
35
|
import { isGateWait } from "./gate-wait";
|
|
@@ -416,9 +417,11 @@ async function runStep(
|
|
|
416
417
|
if (step.id) resultsById.set(step.id, result);
|
|
417
418
|
const record: StepRecord = { ...base, status: "ok", durationMs: Date.now() - start };
|
|
418
419
|
const attrs = outcomeAttributesOf(step);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
420
|
+
// An attribute whose path the result does not hold is left off, rather than recorded as undefined.
|
|
421
|
+
const outcomes = attrs.map((a) => ({ name: a.name, value: resolvePath(result, a.from) })).filter((o) => o.value !== undefined);
|
|
422
|
+
if (outcomes.length > 0) {
|
|
423
|
+
record.outcomes = outcomes;
|
|
424
|
+
record.outcome = outcomes[0];
|
|
422
425
|
}
|
|
423
426
|
return { record, result };
|
|
424
427
|
} catch (err) {
|
|
@@ -482,6 +485,8 @@ interface GateContext {
|
|
|
482
485
|
runId?: string;
|
|
483
486
|
/** Called once per settled step, in production order (#2121) — what `--progress-json` streams from. */
|
|
484
487
|
onRecord?: (record: StepRecord) => void;
|
|
488
|
+
/** The run context's list of passed gates (#2522). A gate that passes is appended here. */
|
|
489
|
+
passed?: PassedGate[];
|
|
485
490
|
/** The run's hold on its work item's lease (#2748). Absent in compensation phases, which run whatever the lease did. */
|
|
486
491
|
work?: RunWorkLease;
|
|
487
492
|
}
|
|
@@ -643,6 +648,17 @@ async function runGateStep(
|
|
|
643
648
|
|
|
644
649
|
if (check.satisfied) {
|
|
645
650
|
const { resolution } = check;
|
|
651
|
+
const passedApproval: NonNullable<StepRecord["approval"]> = {
|
|
652
|
+
gate: gateName(step),
|
|
653
|
+
resolvedBy: resolution.resolvedBy,
|
|
654
|
+
timestamp: resolution.timestamp,
|
|
655
|
+
...(resolution.url ? { url: resolution.url } : {}),
|
|
656
|
+
...(check.via ? { via: check.via } : {}),
|
|
657
|
+
...(check.approvals ? { approvers: check.approvals.map((r) => r.resolvedBy) } : {}),
|
|
658
|
+
};
|
|
659
|
+
// #2522: a later step reads this gate's approval from the run context
|
|
660
|
+
// rather than re-tallying the ledger, so it sees the decision this run made.
|
|
661
|
+
gates.passed?.push({ gate: gateName(step), approval: passedApproval });
|
|
646
662
|
return {
|
|
647
663
|
record: {
|
|
648
664
|
phase: phaseName,
|
|
@@ -650,14 +666,7 @@ async function runGateStep(
|
|
|
650
666
|
args: {},
|
|
651
667
|
status: "ok",
|
|
652
668
|
durationMs: Date.now() - start,
|
|
653
|
-
approval:
|
|
654
|
-
gate: gateName(step),
|
|
655
|
-
resolvedBy: resolution.resolvedBy,
|
|
656
|
-
timestamp: resolution.timestamp,
|
|
657
|
-
...(resolution.url ? { url: resolution.url } : {}),
|
|
658
|
-
...(check.via ? { via: check.via } : {}),
|
|
659
|
-
...(check.approvals ? { approvers: check.approvals.map((r) => r.resolvedBy) } : {}),
|
|
660
|
-
},
|
|
669
|
+
approval: passedApproval,
|
|
661
670
|
},
|
|
662
671
|
};
|
|
663
672
|
}
|
|
@@ -971,6 +980,12 @@ export interface RunOpOptions {
|
|
|
971
980
|
now?: string;
|
|
972
981
|
/** Identifies this run on any pending fact it records. */
|
|
973
982
|
runId?: string;
|
|
983
|
+
/**
|
|
984
|
+
* The environment the run was started for (`--env`, #2522). The executor
|
|
985
|
+
* only hands it to activities through the run context (`./run-context.ts`).
|
|
986
|
+
* The run ledger path still comes from the Op's `labels.Env`.
|
|
987
|
+
*/
|
|
988
|
+
env?: string;
|
|
974
989
|
/**
|
|
975
990
|
* Called once per settled step, in the order the records are produced
|
|
976
991
|
* (#2121) — what the local op runtime (./runtimes/local.ts) feeds
|
|
@@ -1039,6 +1054,13 @@ export async function runOpLocally(
|
|
|
1039
1054
|
const turn = currentStewardTurn();
|
|
1040
1055
|
const steward = options.steward ?? turn?.steward;
|
|
1041
1056
|
const restore = turn && turn.run !== runId ? enterStewardTurn({ ...turn, run: runId }) : undefined;
|
|
1057
|
+
// #2522: every activity in this run can read the run it is in.
|
|
1058
|
+
const run: OpRunContext = {
|
|
1059
|
+
op: config.name,
|
|
1060
|
+
runId,
|
|
1061
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
1062
|
+
passedGates: [],
|
|
1063
|
+
};
|
|
1042
1064
|
// A run that writes to the ledger also says what it is doing while it runs
|
|
1043
1065
|
// (./run-live.ts), and removes that once its record is written.
|
|
1044
1066
|
const live = options.ledger
|
|
@@ -1052,7 +1074,11 @@ export async function runOpLocally(
|
|
|
1052
1074
|
})
|
|
1053
1075
|
: undefined;
|
|
1054
1076
|
try {
|
|
1055
|
-
return await withLiveRun(live, () =>
|
|
1077
|
+
return await withLiveRun(live, () =>
|
|
1078
|
+
withOpRunContext(run, () =>
|
|
1079
|
+
runOpInTurn(config, activities, profiles, signal, { ...options, runId, ...(steward ? { steward } : {}) }, run),
|
|
1080
|
+
),
|
|
1081
|
+
);
|
|
1056
1082
|
} finally {
|
|
1057
1083
|
live?.close();
|
|
1058
1084
|
restore?.();
|
|
@@ -1065,6 +1091,7 @@ async function runOpInTurn(
|
|
|
1065
1091
|
profiles: Record<string, ActivityProfile>,
|
|
1066
1092
|
signal: AbortSignal | undefined,
|
|
1067
1093
|
options: RunOpOptions & { runId: string },
|
|
1094
|
+
run: OpRunContext,
|
|
1068
1095
|
): Promise<OpRunResult> {
|
|
1069
1096
|
const runId = options.runId;
|
|
1070
1097
|
|
|
@@ -1074,6 +1101,7 @@ async function runOpInTurn(
|
|
|
1074
1101
|
...(options.now ? { now: options.now } : {}),
|
|
1075
1102
|
runId,
|
|
1076
1103
|
...(options.onRecord ? { onRecord: options.onRecord } : {}),
|
|
1104
|
+
passed: run.passedGates,
|
|
1077
1105
|
};
|
|
1078
1106
|
|
|
1079
1107
|
// Effect steps are ordered (read-compare-run-write): refuse them in a
|
package/src/op/local-output.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function renderHuman(result: OpRunResult, write: Writer = stderr): void {
|
|
|
66
66
|
: work.released
|
|
67
67
|
? `released ${work.outcome}`
|
|
68
68
|
: "not released; it runs out on its own";
|
|
69
|
-
write(`[work] ${work.item} held by ${work.holder}${work.branch ? ` on ${work.branch}` : ""}: ${end}`);
|
|
69
|
+
write(`[work] ${work.item} held by ${work.holder}${work.branch ? ` on ${work.branch}` : ""}: ${end}${work.kept ? `; the attempt is kept at ${work.kept}` : ""}`);
|
|
70
70
|
} else if (work?.refusal) {
|
|
71
71
|
write(`[work] nothing claimed: ${work.refusal}`);
|
|
72
72
|
}
|
package/src/op/operator.ts
CHANGED
|
@@ -577,6 +577,10 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
577
577
|
try {
|
|
578
578
|
const result = await runOpLocally(config, opts.activities, opts.profiles, opts.signal, {
|
|
579
579
|
runId,
|
|
580
|
+
// #2522: the environment the operator was started for (`--env`, or a
|
|
581
|
+
// steward's environment) reaches the activities through the run
|
|
582
|
+
// context, as `chant run --env` does.
|
|
583
|
+
...(opts.env ?? opts.stewardEnv ? { env: opts.env ?? opts.stewardEnv } : {}),
|
|
580
584
|
...(steward ? { steward: steward.name } : {}),
|
|
581
585
|
ledger: { cwd: opts.cwd },
|
|
582
586
|
// A steward's turn claims work leases as `<steward>/<op>@<holder>`
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run context an activity reads (#2522): the Op, the run id the run
|
|
3
|
+
* ledger records, `--env`, and the gates the run has passed.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect } from "vitest";
|
|
6
|
+
import type { OpConfig } from "./types";
|
|
7
|
+
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
8
|
+
import { runOpLocally } from "./local-executor";
|
|
9
|
+
import { memoryGateLedgerPort } from "./gate";
|
|
10
|
+
import { currentOpRun, type OpRunContext } from "./run-context";
|
|
11
|
+
import { gatePolicyVersion, type GatePolicyRef, type ResolvedGateApproval } from "./gate-approval";
|
|
12
|
+
import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
|
|
13
|
+
|
|
14
|
+
const PROFILES: Record<string, ActivityProfile> = {
|
|
15
|
+
fastIdempotent: { timeout: "5m", retry: { maximumAttempts: 1 } },
|
|
16
|
+
};
|
|
17
|
+
const NOW = "2026-09-05T12:00:00.000Z";
|
|
18
|
+
|
|
19
|
+
function op(steps: OpConfig["phases"][number]["steps"]): OpConfig {
|
|
20
|
+
return { name: "release", overview: "", phases: [{ name: "main", steps }] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("the Op run context (#2522)", () => {
|
|
24
|
+
test("an activity reads the Op, the run id and --env", async () => {
|
|
25
|
+
let seen: OpRunContext | undefined;
|
|
26
|
+
const activities = new Map<string, ActivityFn>([
|
|
27
|
+
["deploy", async () => { await Promise.resolve(); seen = currentOpRun(); }],
|
|
28
|
+
]);
|
|
29
|
+
const result = await runOpLocally(op([{ kind: "activity", fn: "deploy" }]), activities, PROFILES, undefined, {
|
|
30
|
+
runId: "local-123",
|
|
31
|
+
env: "staging",
|
|
32
|
+
});
|
|
33
|
+
expect(seen).toEqual({ op: "release", runId: "local-123", env: "staging", passedGates: [] });
|
|
34
|
+
expect(result.record?.id).toBe("local-123");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("without --env the context carries none, and outside a run there is no context", async () => {
|
|
38
|
+
let seen: OpRunContext | undefined;
|
|
39
|
+
const activities = new Map<string, ActivityFn>([["deploy", async () => { seen = currentOpRun(); }]]);
|
|
40
|
+
await runOpLocally(op([{ kind: "activity", fn: "deploy" }]), activities, PROFILES, undefined, { runId: "r1" });
|
|
41
|
+
expect(seen).toEqual({ op: "release", runId: "r1", passedGates: [] });
|
|
42
|
+
expect(currentOpRun()).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a step after a passed gate reads the gate's approval", async () => {
|
|
46
|
+
const seen: OpRunContext[] = [];
|
|
47
|
+
const activities = new Map<string, ActivityFn>([
|
|
48
|
+
["plan", async () => { seen.push(currentOpRun()!); }],
|
|
49
|
+
["apply", async () => { seen.push(currentOpRun()!); }],
|
|
50
|
+
]);
|
|
51
|
+
const port = memoryGateLedgerPort({
|
|
52
|
+
pending: [{
|
|
53
|
+
version: 1, kind: "pending", op: "release", gate: "ship",
|
|
54
|
+
timestamp: "2026-09-05T10:00:00.000Z", expiresAt: "2026-09-07T10:00:00.000Z",
|
|
55
|
+
}],
|
|
56
|
+
resolutions: [{ version: 1, op: "release", gate: "ship", resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z" }],
|
|
57
|
+
});
|
|
58
|
+
const result = await runOpLocally(
|
|
59
|
+
op([
|
|
60
|
+
{ kind: "activity", fn: "plan" },
|
|
61
|
+
{ kind: "gate", gate: "ship" },
|
|
62
|
+
{ kind: "activity", fn: "apply" },
|
|
63
|
+
]),
|
|
64
|
+
activities,
|
|
65
|
+
PROFILES,
|
|
66
|
+
undefined,
|
|
67
|
+
{ gates: port, now: NOW, runId: "r2" },
|
|
68
|
+
);
|
|
69
|
+
expect(result.status).toBe("ok");
|
|
70
|
+
expect(seen[0]!.passedGates).toEqual([]);
|
|
71
|
+
expect(seen[1]!.passedGates).toEqual([
|
|
72
|
+
{ gate: "ship", approval: { gate: "ship", resolvedBy: "alex", timestamp: "2026-09-05T11:00:00.000Z" } },
|
|
73
|
+
]);
|
|
74
|
+
expect(seen[1]!.passedGates[0]!.approval).toEqual(result.records.find((r) => r.fn === "gate:ship")?.approval);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("a policy permit and the gates a run passed (#2512)", () => {
|
|
78
|
+
const TEXT = 'permit (principal is Chant::Agent, action, resource) when { context.risk == "low" };\n';
|
|
79
|
+
const POLICY: GatePolicyRef = { kind: "gate-policy", lexicon: "cedar", name: "ship", version: gatePolicyVersion(TEXT), text: TEXT };
|
|
80
|
+
const enforce: ResolvedGateApproval = { policy: POLICY, mode: "enforce" };
|
|
81
|
+
const permit = (mode: "log-only" | "enforce"): GateResolutionRecord => ({
|
|
82
|
+
version: 1, op: "release", gate: "ship", resolvedBy: "release-bot", timestamp: "2026-09-05T11:00:00.000Z",
|
|
83
|
+
approver: { kind: "agent" },
|
|
84
|
+
policyDecision: { policy: "ship", version: POLICY.version, mode, decision: "allow", determining: ["ship-0"], errors: [] },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
async function run(mode: "log-only" | "enforce") {
|
|
88
|
+
const seen: OpRunContext[] = [];
|
|
89
|
+
const activities = new Map<string, ActivityFn>([["apply", async () => { seen.push(currentOpRun()!); }]]);
|
|
90
|
+
const port = memoryGateLedgerPort({
|
|
91
|
+
pending: [{
|
|
92
|
+
version: 1, kind: "pending", op: "release", gate: "ship",
|
|
93
|
+
timestamp: "2026-09-05T10:00:00.000Z", expiresAt: "2026-09-07T10:00:00.000Z",
|
|
94
|
+
approval: { ...enforce, mode },
|
|
95
|
+
}],
|
|
96
|
+
resolutions: [permit(mode)],
|
|
97
|
+
});
|
|
98
|
+
const result = await runOpLocally(
|
|
99
|
+
op([
|
|
100
|
+
{ kind: "gate", gate: "ship", approval: { policy: POLICY, mode: "enforce" } },
|
|
101
|
+
{ kind: "activity", fn: "apply" },
|
|
102
|
+
]),
|
|
103
|
+
activities,
|
|
104
|
+
PROFILES,
|
|
105
|
+
undefined,
|
|
106
|
+
{ gates: port, now: NOW, runId: "r3" },
|
|
107
|
+
);
|
|
108
|
+
return { result, seen };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("an enforce-mode permit passes the gate, and the next step sees it as passed by policy", async () => {
|
|
112
|
+
const { result, seen } = await run("enforce");
|
|
113
|
+
expect(result.status).toBe("ok");
|
|
114
|
+
expect(seen[0]!.passedGates.map((g) => [g.gate, g.approval.via])).toEqual([["ship", "policy"]]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a permit recorded under log-only does not pass the enforced gate, so no step sees it as passed", async () => {
|
|
118
|
+
const { result, seen } = await run("log-only");
|
|
119
|
+
expect(result.status).toBe("gated");
|
|
120
|
+
expect(seen).toEqual([]);
|
|
121
|
+
expect(result.records.find((r) => r.fn === "gate:ship")?.approval).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("two runs in flight at once each see their own context", async () => {
|
|
126
|
+
const seen = new Map<string, string | undefined>();
|
|
127
|
+
const activities = new Map<string, ActivityFn>([
|
|
128
|
+
["deploy", async () => {
|
|
129
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
130
|
+
const ctx = currentOpRun()!;
|
|
131
|
+
seen.set(ctx.runId, ctx.env);
|
|
132
|
+
}],
|
|
133
|
+
]);
|
|
134
|
+
const config = op([{ kind: "activity", fn: "deploy" }]);
|
|
135
|
+
await Promise.all([
|
|
136
|
+
runOpLocally(config, activities, PROFILES, undefined, { runId: "a", env: "staging" }),
|
|
137
|
+
runOpLocally(config, activities, PROFILES, undefined, { runId: "b", env: "prod" }),
|
|
138
|
+
]);
|
|
139
|
+
expect(Object.fromEntries(seen)).toEqual({ a: "staging", b: "prod" });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run an activity is part of (#2522).
|
|
3
|
+
*
|
|
4
|
+
* An activity is called as `fn(args, signal)`, which says nothing about the
|
|
5
|
+
* run around it. An activity that deploys somewhere needs the environment the
|
|
6
|
+
* run was asked for, one that appends a ledger record of its own needs the run
|
|
7
|
+
* id to name the run that made it, and a step after a gate may need to know the
|
|
8
|
+
* gate passed and who approved it.
|
|
9
|
+
*
|
|
10
|
+
* `runOpLocally` (`./local-executor.ts`) sets this for the whole run, and
|
|
11
|
+
* {@link currentOpRun} reads it from inside an activity. It is backed by
|
|
12
|
+
* `AsyncLocalStorage`, so it follows the activity through every `await` and is
|
|
13
|
+
* `undefined` outside a run.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
import type { StepRecord } from "./local-executor";
|
|
18
|
+
|
|
19
|
+
/** A gate this run has passed, with the approval that passed it. */
|
|
20
|
+
export interface PassedGate {
|
|
21
|
+
gate: string;
|
|
22
|
+
approval: NonNullable<StepRecord["approval"]>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What an activity can read about the run it is in. */
|
|
26
|
+
export interface OpRunContext {
|
|
27
|
+
/** The Op's name. */
|
|
28
|
+
op: string;
|
|
29
|
+
/** The run id the run ledger record carries. */
|
|
30
|
+
runId: string;
|
|
31
|
+
/** `--env`, as handed to `OpRuntimeProvider.start`. Absent when the run was started without one. */
|
|
32
|
+
env?: string;
|
|
33
|
+
/** Every gate this run has passed so far, in the order it passed them. */
|
|
34
|
+
passedGates: PassedGate[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const store = new AsyncLocalStorage<OpRunContext>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The run the calling activity is part of, or `undefined` outside a run.
|
|
41
|
+
* `passedGates` is a copy taken at the time of the call.
|
|
42
|
+
*/
|
|
43
|
+
export function currentOpRun(): OpRunContext | undefined {
|
|
44
|
+
const ctx = store.getStore();
|
|
45
|
+
return ctx ? { ...ctx, passedGates: [...ctx.passedGates] } : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Run `fn` with `ctx` as the current run. The executor calls this. A runtime that calls activities itself can too. */
|
|
49
|
+
export function withOpRunContext<T>(ctx: OpRunContext, fn: () => T): T {
|
|
50
|
+
return store.run(ctx, fn);
|
|
51
|
+
}
|