@intentius/chant 0.90.0 → 0.93.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/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +8 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +20 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +7 -4
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +42 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +72 -0
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +40 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +72 -16
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +3 -2
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts +114 -0
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +140 -0
- package/dist/lifecycle/work-lease.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +203 -2
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/discover.d.ts +25 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +18 -5
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/lifecycle-receipt-store.d.ts +34 -0
- package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
- package/dist/op/local-executor.d.ts +48 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +12 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +126 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +46 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +2 -2
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +161 -0
- package/dist/op/steward.d.ts.map +1 -0
- package/dist/op/types.d.ts +58 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts +18 -0
- package/dist/op/work-lease-decl.d.ts.map +1 -0
- package/dist/op/work-lease-run.d.ts +173 -0
- package/dist/op/work-lease-run.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/box-isolation.d.ts +99 -0
- package/dist/workspace/box-isolation.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/box-isolation.d.ts +18 -0
- package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +104 -0
- package/dist/workspace/checks/boxes.d.ts.map +1 -0
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +8 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +13 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +229 -0
- package/dist/workspace/decide.d.ts.map +1 -0
- package/dist/workspace/decision-points.schema.json +137 -0
- package/dist/workspace/declaration.d.ts +103 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +285 -0
- package/dist/workspace/declared-kinds.d.ts +32 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts +113 -0
- package/dist/workspace/points-cli.d.ts.map +1 -0
- package/dist/workspace/points.d.ts +320 -0
- package/dist/workspace/points.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +38 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +23 -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-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +42 -6
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +179 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +68 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +189 -0
- package/dist/workspace/work-cli.d.ts.map +1 -0
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/components.test.ts +93 -0
- package/src/cli/handlers/components.ts +44 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +141 -4
- package/src/cli/handlers/run.test.ts +173 -2
- package/src/cli/handlers/run.ts +174 -3
- package/src/cli/main.test.ts +50 -0
- package/src/cli/main.ts +97 -2
- package/src/cli/mcp/workspace-tools.test.ts +48 -19
- package/src/cli/mcp/workspace-tools.ts +110 -8
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +42 -1
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/components/verbs/vuln-scan.test.ts +124 -1
- package/src/components/verbs/vuln-scan.ts +142 -1
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/git.ts +65 -15
- package/src/lifecycle/lease.test.ts +22 -0
- package/src/lifecycle/lease.ts +133 -29
- package/src/lifecycle/member-ledger.ts +3 -2
- package/src/lifecycle/plan-ledger.test.ts +148 -0
- package/src/lifecycle/plan-ledger.ts +158 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lifecycle/work-lease.test.ts +236 -0
- package/src/lifecycle/work-lease.ts +426 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +124 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/discover.ts +71 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +34 -6
- package/src/op/lifecycle-receipt-store.test.ts +60 -0
- package/src/op/lifecycle-receipt-store.ts +61 -0
- package/src/op/local-executor.ts +367 -33
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +33 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +18 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +360 -10
- package/src/op/runtime.ts +37 -8
- package/src/op/runtimes/local.ts +6 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/step-output-ref.ts +6 -2
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +296 -0
- package/src/op/steward.ts +277 -0
- package/src/op/types.ts +60 -0
- package/src/op/work-lease-decl.ts +80 -0
- package/src/op/work-lease-run.test.ts +345 -0
- package/src/op/work-lease-run.ts +395 -0
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/box-isolation.test.ts +261 -0
- package/src/workspace/box-isolation.ts +205 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +5 -1
- package/src/workspace/check.schema.json +25 -7
- package/src/workspace/checks/box-isolation.ts +68 -0
- package/src/workspace/checks/boxes.test.ts +276 -0
- package/src/workspace/checks/boxes.ts +442 -0
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +72 -0
- package/src/workspace/checks.test.ts +7 -0
- package/src/workspace/checks.ts +27 -3
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +224 -0
- package/src/workspace/decide.ts +709 -0
- package/src/workspace/decision-points.schema.json +137 -0
- package/src/workspace/declaration.schema.json +285 -0
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +241 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +71 -3
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +12 -3
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/point-answer.schema.json +95 -0
- package/src/workspace/points-cli.ts +284 -0
- package/src/workspace/points-write.schema.json +499 -0
- package/src/workspace/points.schema.json +743 -0
- package/src/workspace/points.test.ts +264 -0
- package/src/workspace/points.ts +564 -0
- package/src/workspace/read-contract.test.ts +18 -1
- package/src/workspace/reason-codes.test.ts +24 -1
- package/src/workspace/reason-codes.ts +49 -0
- package/src/workspace/record-assets.test.ts +3 -2
- package/src/workspace/record-assets.ts +4 -1
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +28 -5
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-contract.test.ts +3 -2
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +53 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +142 -6
- package/src/workspace/status-contract.test.ts +189 -6
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +329 -0
- package/src/workspace/status.schema.json +335 -5
- package/src/workspace/status.ts +142 -6
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/work-cli.e2e.test.ts +180 -0
- package/src/workspace/work-cli.ts +429 -0
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work-lease.schema.json +233 -0
- package/src/workspace/work-readiness-chud.test.ts +145 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
package/src/op/runtime.ts
CHANGED
|
@@ -18,23 +18,27 @@
|
|
|
18
18
|
|
|
19
19
|
import type { OpConfig } from "./types";
|
|
20
20
|
import type { StepRecord, OpRunResult } from "./local-executor";
|
|
21
|
+
import type { WaitingPoint } from "./steward-points";
|
|
21
22
|
import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
|
|
22
23
|
import type { RunComponentsOptions, RunComponentsResult } from "../components/cli-support";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* How a run ended, or that it has not. `gated` is the state a run reaches when
|
|
26
|
-
* a gate has no recorded resolution — a fact, not a wait (#2119).
|
|
27
|
+
* a gate has no recorded resolution — a fact, not a wait (#2119). `waiting` is
|
|
28
|
+
* the state it reaches when an activity asked a decision point that is still
|
|
29
|
+
* open (#2749), a fact of the same kind: a person answers the question, and
|
|
30
|
+
* the next run reads the answer.
|
|
27
31
|
*
|
|
28
32
|
* This is the coarse state a *runtime* reports. The ledger records the
|
|
29
33
|
* executor's own three-state outcome instead ({@link OpRunRecord.status}),
|
|
30
34
|
* which has no `running` or `cancelled`: a record is only written once a run
|
|
31
35
|
* has settled.
|
|
32
36
|
*/
|
|
33
|
-
export type OpRunState = "running" | "completed" | "failed" | "gated" | "cancelled";
|
|
37
|
+
export type OpRunState = "running" | "completed" | "failed" | "gated" | "waiting" | "cancelled";
|
|
34
38
|
|
|
35
39
|
/** Map a settled run's ledger status onto the coarser runtime state. */
|
|
36
40
|
export function runStateOf(status: OpRunRecord["status"]): OpRunState {
|
|
37
|
-
return status === "ok" ? "completed" : status === "gated" ? "gated" : "failed";
|
|
41
|
+
return status === "ok" ? "completed" : status === "gated" ? "gated" : status === "waiting" ? "waiting" : "failed";
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** One step's outcome as the run ledger keeps it — `StepRecord` minus the fields only a live renderer needs. */
|
|
@@ -51,6 +55,10 @@ export interface OpRunStepRecord {
|
|
|
51
55
|
error?: string;
|
|
52
56
|
/** Why a gate declined a standing approval that was for another plan (#2300) — see {@link StepRecord.refusal}. */
|
|
53
57
|
refusal?: string;
|
|
58
|
+
/** The open decision point this step asked (#2749), for a step skipped because it is waiting on one. */
|
|
59
|
+
point?: WaitingPoint;
|
|
60
|
+
/** The gate this step's command stopped at (#2779), for a step skipped because of it. */
|
|
61
|
+
gate?: { op: string; gate: string };
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/** One phase's steps and the verdict they add up to. */
|
|
@@ -81,8 +89,10 @@ export interface OpRunStatus {
|
|
|
81
89
|
records?: StepRecord[];
|
|
82
90
|
/** The executor's own result, when the runtime is executing in this process. */
|
|
83
91
|
result?: OpRunResult;
|
|
84
|
-
/** The gate this run is waiting on, when `state` is `gated
|
|
85
|
-
gate?: { name: string; since: string };
|
|
92
|
+
/** The gate this run is waiting on, when `state` is `gated`; `op` names the op it is recorded under when that isn't the run's own (#2779). */
|
|
93
|
+
gate?: { name: string; since: string; op?: string };
|
|
94
|
+
/** The open decision point this run is waiting on, when `state` is `waiting` (#2749). */
|
|
95
|
+
point?: WaitingPoint & { since: string };
|
|
86
96
|
/** Free-text detail for a failed run. */
|
|
87
97
|
error?: string;
|
|
88
98
|
}
|
|
@@ -117,15 +127,28 @@ export interface OpRunRecord {
|
|
|
117
127
|
* failure: the run reached a gate nobody has approved, recorded the fact,
|
|
118
128
|
* and stopped (#2119).
|
|
119
129
|
*/
|
|
120
|
-
status: "ok" | "fail" | "gated";
|
|
130
|
+
status: "ok" | "fail" | "gated" | "waiting";
|
|
121
131
|
/** The Op's own `labels`, copied at run time. */
|
|
122
132
|
labels: Record<string, string>;
|
|
123
133
|
/** Every `outcomeAttribute` the run captured, name → value. */
|
|
124
134
|
outcomes: Record<string, unknown>;
|
|
125
135
|
/** Per-phase, per-step status, in execution order. */
|
|
126
136
|
phases: OpRunPhaseRecord[];
|
|
127
|
-
/**
|
|
128
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The gate the run stopped on, for `status: "gated"`. `op` is set when the
|
|
139
|
+
* gate is recorded under another op than the run's (#2779): a step whose
|
|
140
|
+
* command stopped at its own gate, such as `chant workspace upgrade`'s
|
|
141
|
+
* `workspace-upgrade` / `<scope>`.
|
|
142
|
+
*/
|
|
143
|
+
gate?: { name: string; since: string; op?: string };
|
|
144
|
+
/**
|
|
145
|
+
* The open decision point the run stopped on, for `status: "waiting"`
|
|
146
|
+
* (#2749): the answer record's id, the point, its state then, and when the
|
|
147
|
+
* run stopped. `points --open` and hud read the question itself.
|
|
148
|
+
*/
|
|
149
|
+
point?: WaitingPoint & { since: string };
|
|
150
|
+
/** The steward whose turn the run was, when a steward ran it (#2749). */
|
|
151
|
+
steward?: string;
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
/** An {@link OpRunRecord} before the ledger stamps its version and mints an id. */
|
|
@@ -156,6 +179,12 @@ export interface OpRunStartOptions {
|
|
|
156
179
|
progress?: (record: StepRecord) => void;
|
|
157
180
|
/** Aborts in-flight work (Ctrl-C). */
|
|
158
181
|
signal?: AbortSignal;
|
|
182
|
+
/**
|
|
183
|
+
* For an Op that declares `workLease` (#2748): the work item this run is
|
|
184
|
+
* for (`--work <id>`) and who holds its lease (`--holder`). Only the local
|
|
185
|
+
* runtime takes it; a hosted run's lease is taken where the run executes.
|
|
186
|
+
*/
|
|
187
|
+
work?: { item?: string; holder?: string };
|
|
159
188
|
}
|
|
160
189
|
|
|
161
190
|
/**
|
package/src/op/runtimes/local.ts
CHANGED
|
@@ -52,7 +52,8 @@ function statusFrom(
|
|
|
52
52
|
endedAt: new Date().toISOString(),
|
|
53
53
|
records: result.records,
|
|
54
54
|
result,
|
|
55
|
-
...(result.gate ? { gate: { name: result.gate.gate, since: result.gate.timestamp } } : {}),
|
|
55
|
+
...(result.gate ? { gate: { name: result.gate.gate, since: result.gate.timestamp, ...(result.gate.op !== op ? { op: result.gate.op } : {}) } } : {}),
|
|
56
|
+
...(result.record.point ? { point: result.record.point } : {}),
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -70,6 +71,7 @@ function statusFromRecord(record: OpRunRecord): OpRunStatus {
|
|
|
70
71
|
startedAt: record.started,
|
|
71
72
|
endedAt: record.ended,
|
|
72
73
|
...(record.gate ? { gate: record.gate } : {}),
|
|
74
|
+
...(record.point ? { point: record.point } : {}),
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -166,6 +168,9 @@ export function createLocalOpRuntime(opts: { projectPath?: string } = {}): OpRun
|
|
|
166
168
|
`appended to the run ledger: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
167
169
|
),
|
|
168
170
|
...(startOpts.progress ? { onRecord: startOpts.progress } : {}),
|
|
171
|
+
...(startOpts.work ? { work: startOpts.work } : {}),
|
|
172
|
+
onWorkLeaseWarning: (message) =>
|
|
173
|
+
process.stderr.write(`warning: "${op.name}" could not renew its work lease, retrying at the next beat: ${message}\n`),
|
|
169
174
|
},
|
|
170
175
|
);
|
|
171
176
|
const status = statusFrom(op.name, runId, startedAt, result);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A source tree as a release artifact (#2782): one directory of a commit,
|
|
3
|
+
* archived with `git archive`, and read back only when its bytes still hash
|
|
4
|
+
* to the digest a gate approved.
|
|
5
|
+
*
|
|
6
|
+
* `git archive <commit> -- <dir>` gives every entry the commit's own
|
|
7
|
+
* timestamp, so the same commit always archives to the same bytes, and the
|
|
8
|
+
* archive's sha256 names the tree exactly. A release plan carries that digest,
|
|
9
|
+
* the ship gate approves the plan, and whatever ships the release reads the
|
|
10
|
+
* archive through {@link readSourceArchive}, which refuses bytes that do not
|
|
11
|
+
* hash to the approved digest.
|
|
12
|
+
*
|
|
13
|
+
* Pure except for the git and file reads each function names.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** One file of an archived tree. */
|
|
22
|
+
export interface SourceFile {
|
|
23
|
+
/** Its path inside the archived directory, with `/` separators. */
|
|
24
|
+
path: string;
|
|
25
|
+
/** Whether git records it executable. */
|
|
26
|
+
executable: boolean;
|
|
27
|
+
data: Buffer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A directory of a commit, archived. */
|
|
31
|
+
export interface SourceArchive {
|
|
32
|
+
/** `sha256:<hex>` of the archive's bytes. */
|
|
33
|
+
digest: string;
|
|
34
|
+
/** Where the archive was written. */
|
|
35
|
+
archive: string;
|
|
36
|
+
/** The commit it was archived from. */
|
|
37
|
+
commit: string;
|
|
38
|
+
/** The archived directory, relative to the repository root (`.` for the root). */
|
|
39
|
+
dir: string;
|
|
40
|
+
/** Files in it. */
|
|
41
|
+
files: number;
|
|
42
|
+
/** Size of the archive in bytes. */
|
|
43
|
+
bytes: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const sha256Digest = (data: Buffer | string): string => `sha256:${createHash("sha256").update(data).digest("hex")}`;
|
|
47
|
+
|
|
48
|
+
function git(cwd: string, args: string[]): Buffer {
|
|
49
|
+
return execFileSync("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Archive `dir` (relative to `cwd`) as committed at `ref` (default HEAD) to
|
|
54
|
+
* `out` (default `<cwd>/dist/releases/<commit>.tar`). Uncommitted changes are
|
|
55
|
+
* not in it: a release ships what is committed. Running it again for the same
|
|
56
|
+
* commit writes the same bytes.
|
|
57
|
+
*/
|
|
58
|
+
export function archiveSourceTree(opts: { dir: string; ref?: string; out?: string; cwd?: string }): SourceArchive {
|
|
59
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
60
|
+
// git names the root by its real path, so the directory is resolved the same way.
|
|
61
|
+
const root = realpathSync(git(cwd, ["rev-parse", "--show-toplevel"]).toString("utf-8").trim());
|
|
62
|
+
const commit = git(cwd, ["rev-parse", "--verify", `${opts.ref ?? "HEAD"}^{commit}`]).toString("utf-8").trim();
|
|
63
|
+
const target = resolve(realpathSync(cwd), opts.dir);
|
|
64
|
+
const dir = relative(root, target).split("\\").join("/") || ".";
|
|
65
|
+
if (dir.startsWith("..") || isAbsolute(dir)) throw new Error(`${opts.dir} is outside the repository at ${root}`);
|
|
66
|
+
const spec = dir === "." ? commit : `${commit}:${dir}`;
|
|
67
|
+
try {
|
|
68
|
+
git(root, ["cat-file", "-e", spec]);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error(`${dir} is not in commit ${commit.slice(0, 12)}: commit it before releasing it`);
|
|
71
|
+
}
|
|
72
|
+
const data = git(root, ["archive", "--format=tar", commit, "--", dir]);
|
|
73
|
+
const archive = resolve(cwd, opts.out ?? join("dist", "releases", `${commit}.tar`));
|
|
74
|
+
mkdirSync(dirname(archive), { recursive: true });
|
|
75
|
+
writeFileSync(archive, data);
|
|
76
|
+
const files = tarEntries(data).filter((e) => e.type === "file").length;
|
|
77
|
+
return { digest: sha256Digest(data), archive, commit, dir, files, bytes: data.length };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface TarEntry {
|
|
81
|
+
type: "file" | "dir" | "symlink" | "other";
|
|
82
|
+
path: string;
|
|
83
|
+
mode: number;
|
|
84
|
+
data: Buffer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const field = (block: Buffer, at: number, len: number): string => {
|
|
88
|
+
const raw = block.subarray(at, at + len);
|
|
89
|
+
const end = raw.indexOf(0);
|
|
90
|
+
return raw.subarray(0, end < 0 ? raw.length : end).toString("utf-8");
|
|
91
|
+
};
|
|
92
|
+
const octal = (block: Buffer, at: number, len: number): number => parseInt(field(block, at, len).trim() || "0", 8);
|
|
93
|
+
|
|
94
|
+
/** The records of a pax header: `<len> key=value\n`. */
|
|
95
|
+
function paxRecords(data: Buffer): Record<string, string> {
|
|
96
|
+
const out: Record<string, string> = {};
|
|
97
|
+
let at = 0;
|
|
98
|
+
while (at < data.length) {
|
|
99
|
+
const space = data.indexOf(0x20, at);
|
|
100
|
+
if (space < 0) break;
|
|
101
|
+
const len = parseInt(data.subarray(at, space).toString("utf-8"), 10);
|
|
102
|
+
if (!Number.isFinite(len) || len <= 0) break;
|
|
103
|
+
const record = data.subarray(space + 1, at + len - 1).toString("utf-8");
|
|
104
|
+
const eq = record.indexOf("=");
|
|
105
|
+
if (eq > 0) out[record.slice(0, eq)] = record.slice(eq + 1);
|
|
106
|
+
at += len;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The entries of a ustar/pax archive, as `git archive` writes one. */
|
|
112
|
+
function tarEntries(tar: Buffer): TarEntry[] {
|
|
113
|
+
const out: TarEntry[] = [];
|
|
114
|
+
let at = 0;
|
|
115
|
+
let pax: Record<string, string> = {};
|
|
116
|
+
while (at + 512 <= tar.length) {
|
|
117
|
+
const header = tar.subarray(at, at + 512);
|
|
118
|
+
if (header.every((b) => b === 0)) break;
|
|
119
|
+
const size = octal(header, 124, 12);
|
|
120
|
+
const flag = String.fromCharCode(header[156] || 0x30);
|
|
121
|
+
const data = tar.subarray(at + 512, at + 512 + size);
|
|
122
|
+
at += 512 + Math.ceil(size / 512) * 512;
|
|
123
|
+
if (flag === "x") {
|
|
124
|
+
pax = paxRecords(data);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (flag === "g") continue;
|
|
128
|
+
const prefix = field(header, 345, 155);
|
|
129
|
+
const name = pax.path ?? (prefix ? `${prefix}/${field(header, 0, 100)}` : field(header, 0, 100));
|
|
130
|
+
pax = {};
|
|
131
|
+
const type = flag === "0" || flag === "\0" || flag === "7" ? "file" : flag === "5" ? "dir" : flag === "2" ? "symlink" : "other";
|
|
132
|
+
out.push({ type, path: name, mode: octal(header, 100, 8), data: Buffer.from(data) });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The files of an archive `archiveSourceTree` wrote, relative to the directory
|
|
139
|
+
* it archived, once its bytes hash to `digest`. Throws, naming both digests,
|
|
140
|
+
* when they do not: whatever would ship them is not what was approved.
|
|
141
|
+
*
|
|
142
|
+
* A symbolic link in the tree is refused rather than followed or dropped.
|
|
143
|
+
*/
|
|
144
|
+
export function readSourceArchive(opts: { archive: string; digest: string; dir?: string }): SourceFile[] {
|
|
145
|
+
const data = readFileSync(opts.archive);
|
|
146
|
+
const actual = sha256Digest(data);
|
|
147
|
+
if (actual !== opts.digest) {
|
|
148
|
+
throw new Error(`${opts.archive} is ${actual}, not the approved ${opts.digest}: it is not the tree the release was planned from`);
|
|
149
|
+
}
|
|
150
|
+
const entries = tarEntries(data);
|
|
151
|
+
const dir = opts.dir && opts.dir !== "." ? `${opts.dir.replace(/\/+$/, "")}/` : "";
|
|
152
|
+
const files: SourceFile[] = [];
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
if (e.type === "dir") continue;
|
|
155
|
+
if (!e.path.startsWith(dir)) throw new Error(`${opts.archive} holds ${e.path}, outside ${dir || "its root"}`);
|
|
156
|
+
const path = e.path.slice(dir.length);
|
|
157
|
+
if (e.type !== "file") throw new Error(`${opts.archive} holds ${path}, which is not a regular file; a source release ships regular files only`);
|
|
158
|
+
files.push({ path, executable: (e.mode & 0o111) !== 0, data: e.data });
|
|
159
|
+
}
|
|
160
|
+
return files;
|
|
161
|
+
}
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
|
|
88
88
|
import { z } from "zod";
|
|
89
89
|
import type { ActivityStep, OpConfig, PhaseDefinition } from "./types";
|
|
90
|
+
import { WORK_LEASE_STEP_ID } from "./types";
|
|
90
91
|
import { pathExistsInSchema, schemaAtPath, primitiveKindOf, type ActivityContract, type ActivityContractIssue } from "./activity-contract";
|
|
91
92
|
|
|
92
93
|
const STEP_OUTPUT_REF_BRAND = Symbol.for("chant.op.stepOutputRef");
|
|
@@ -308,7 +309,7 @@ function indexById(locations: StepLocation[]): { byId: Map<string, StepLocation>
|
|
|
308
309
|
* the same parallel phase).
|
|
309
310
|
*/
|
|
310
311
|
export function validateStepOutputRefScope(
|
|
311
|
-
config: Pick<OpConfig, "name" | "phases" | "onFailure"
|
|
312
|
+
config: Pick<OpConfig, "name" | "phases" | "onFailure"> & Partial<Pick<OpConfig, "workLease">>,
|
|
312
313
|
): ActivityContractIssue[] {
|
|
313
314
|
const issues: ActivityContractIssue[] = [];
|
|
314
315
|
|
|
@@ -339,6 +340,9 @@ export function validateStepOutputRefScope(
|
|
|
339
340
|
for (const consumer of locations) {
|
|
340
341
|
const refs = collectStepOutputRefs(consumer.step.args);
|
|
341
342
|
for (const ref of refs) {
|
|
343
|
+
// The run's work lease (#2748) is published under a reserved id once it
|
|
344
|
+
// is claimed; an Op that declares one may reference it from any step.
|
|
345
|
+
if (config.workLease && ref.step === WORK_LEASE_STEP_ID) continue;
|
|
342
346
|
if (duplicateIds.has(ref.step)) {
|
|
343
347
|
issues.push({
|
|
344
348
|
opName: config.name,
|
|
@@ -395,7 +399,7 @@ export function validateStepOutputRefScope(
|
|
|
395
399
|
* schema (an empty `path` — the whole return value — is always valid).
|
|
396
400
|
*/
|
|
397
401
|
export function validateStepOutputRefs(
|
|
398
|
-
config: Pick<OpConfig, "name" | "phases" | "onFailure"
|
|
402
|
+
config: Pick<OpConfig, "name" | "phases" | "onFailure"> & Partial<Pick<OpConfig, "workLease">>,
|
|
399
403
|
contracts: ReadonlyMap<string, ActivityContract>,
|
|
400
404
|
): ActivityContractIssue[] {
|
|
401
405
|
const issues = validateStepOutputRefScope(config);
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision points reached in a steward's turn (#2749): the model call goes
|
|
3
|
+
* through the steward's brokered capability, an open question stops the run
|
|
4
|
+
* as `waiting` and stays open in the workspace for a person, the steward's
|
|
5
|
+
* turn never answers it, and the local steward resumes the Op on the first
|
|
6
|
+
* round after a person answers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { cleanScratch, commitAll, contract, git, REPO, repo } from "../workspace/__fixtures__/contract-repo";
|
|
13
|
+
import statusSchema from "../workspace/status.schema.json";
|
|
14
|
+
import pointsSchema from "../workspace/points.schema.json";
|
|
15
|
+
import { answerPoint, askPoint } from "../workspace/decide";
|
|
16
|
+
import { workspacePoints } from "../workspace/points-cli";
|
|
17
|
+
import type { WireAnswer } from "../workspace/points";
|
|
18
|
+
import { readMemberStewards } from "../workspace/status-stewards";
|
|
19
|
+
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
20
|
+
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
21
|
+
import { runOpLocally } from "./local-executor";
|
|
22
|
+
import { formatRoundLine, runOperatorRound } from "./operator";
|
|
23
|
+
import { declareSteward } from "./steward";
|
|
24
|
+
import { askPointInRun, isPointWait, PointWait, type BrokeredModelAsk } from "./steward-points";
|
|
25
|
+
import { enterStewardTurn, resetStewardTurn, STEWARD_ENV } from "./steward-turn";
|
|
26
|
+
import type { OpConfig } from "./types";
|
|
27
|
+
|
|
28
|
+
const REF = join(REPO, "reference-workspace");
|
|
29
|
+
const PROFILES: Record<string, ActivityProfile> = {};
|
|
30
|
+
const on = "2026-09-25";
|
|
31
|
+
|
|
32
|
+
const points = {
|
|
33
|
+
points: {
|
|
34
|
+
"ship-now": {
|
|
35
|
+
title: "Ship this release now",
|
|
36
|
+
question: { type: "noul", instructions: "Is the release ready to ship?", criteria: { true: "Ship it.", false: "Hold it." } },
|
|
37
|
+
inputs: { release: "the release, as status lists it" },
|
|
38
|
+
deciders: [
|
|
39
|
+
{ kind: "table", rows: [{ when: { release: "hotfix" }, answer: true }] },
|
|
40
|
+
{ kind: "model", backend: "systemone", model: "jev-1.13.0", threshold: 0.8 },
|
|
41
|
+
{ kind: "quorum", count: 1 },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const confident: WireAnswer = { type: "noul", noul: 0.97 };
|
|
48
|
+
|
|
49
|
+
let root: string;
|
|
50
|
+
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
root = repo(
|
|
53
|
+
{
|
|
54
|
+
"chant.workspace.json": JSON.stringify(
|
|
55
|
+
{ name: "studio", schema: 1, members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }], records: [{ kind: "answers/answer.kind.mjs" }] },
|
|
56
|
+
null,
|
|
57
|
+
2,
|
|
58
|
+
),
|
|
59
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
60
|
+
"decisions/points.json": JSON.stringify(points, null, 2),
|
|
61
|
+
".chant/trust.json": JSON.stringify({ schema: 1, roles: { agent: ["bot"] } }),
|
|
62
|
+
},
|
|
63
|
+
false,
|
|
64
|
+
);
|
|
65
|
+
cpSync(join(REF, "answers", "answer.kind.mjs"), join(root, "answers", "answer.kind.mjs"));
|
|
66
|
+
cpSync(join(REF, "answers", "answer.schema.json"), join(root, "answers", "answer.schema.json"));
|
|
67
|
+
// The run ledger and leases are written with git plumbing, which needs an identity.
|
|
68
|
+
git(root, "config", "user.name", "Steward Test");
|
|
69
|
+
git(root, "config", "user.email", "steward@example.com");
|
|
70
|
+
commitAll(root, "c0");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => resetStewardTurn());
|
|
74
|
+
afterAll(cleanScratch);
|
|
75
|
+
|
|
76
|
+
/** A brokered model call that records what it was asked through. */
|
|
77
|
+
function brokered(answer: WireAnswer, calls: { capability: string }[]): BrokeredModelAsk {
|
|
78
|
+
return async (req, via) => {
|
|
79
|
+
calls.push({ capability: via.capability });
|
|
80
|
+
return { model: req.model, answer };
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A steward's question, as held on the lifecycle ledger (#2786). */
|
|
85
|
+
function ledgerText(id: string): string {
|
|
86
|
+
return git(root, "show", `chant/lifecycle:_answers/answer/${id}.md`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function waitOf(p: Promise<unknown>): Promise<PointWait> {
|
|
90
|
+
try {
|
|
91
|
+
await p;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (isPointWait(err)) return err;
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
throw new Error("expected the ask to wait");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("askPointInRun (#2749)", () => {
|
|
100
|
+
test("an answered question returns its answer, outside any steward", async () => {
|
|
101
|
+
const got = await askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "hotfix" }, on });
|
|
102
|
+
expect(got.answer).toBe(true);
|
|
103
|
+
expect(got.question).toMatchObject({ state: "answered", askedBy: null });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("in a steward's turn the model call goes through the steward's brokered capability, and a proposal waits for a person", async () => {
|
|
107
|
+
enterStewardTurn({ steward: "box-steward", capabilities: ["inference"], vault: null, run: "run-1" });
|
|
108
|
+
const calls: { capability: string }[] = [];
|
|
109
|
+
const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-1" }, subject: "r-1", ask: brokered(confident, calls), on }));
|
|
110
|
+
expect(calls).toEqual([{ capability: "inference" }]);
|
|
111
|
+
expect(wait.question).toMatchObject({ point: "ship-now", state: "proposed", subject: "r-1", steward: "box-steward" });
|
|
112
|
+
const text = ledgerText(wait.question.id);
|
|
113
|
+
expect(text).toContain('harness: "chant-steward"');
|
|
114
|
+
expect(text).toContain('name: "box-steward"');
|
|
115
|
+
expect(text).toContain('id: "run-1"');
|
|
116
|
+
// The question is open in the read contract, naming who waits on it.
|
|
117
|
+
const doc = await workspacePoints({ cwd: root, open: true });
|
|
118
|
+
if (!("questions" in doc)) throw new Error(doc.error.message);
|
|
119
|
+
expect(doc.questions.find((q) => q.id === wait.question.id)).toMatchObject({
|
|
120
|
+
open: true,
|
|
121
|
+
state: "proposed",
|
|
122
|
+
askedBy: { steward: "box-steward", run: "run-1" },
|
|
123
|
+
ledger: `chant/lifecycle:_answers/answer/${wait.question.id}.md`,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a steward that doesn't name the capability makes no model call, and the question goes to people", async () => {
|
|
128
|
+
enterStewardTurn({ steward: "box-steward", capabilities: ["fountain"], vault: null });
|
|
129
|
+
const calls: { capability: string }[] = [];
|
|
130
|
+
const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-2" }, ask: brokered(confident, calls), on }));
|
|
131
|
+
expect(calls).toEqual([]);
|
|
132
|
+
expect(wait.question.state).toBe("escalated");
|
|
133
|
+
const text = ledgerText(wait.question.id);
|
|
134
|
+
expect(text).toMatch(/does not name the brokered capability inference \(it names fountain\)/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a steward holding a vault makes no model call either", async () => {
|
|
138
|
+
enterStewardTurn({ steward: "vault-steward", capabilities: [], vault: "creds" });
|
|
139
|
+
const calls: { capability: string }[] = [];
|
|
140
|
+
const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-3" }, ask: brokered(confident, calls), on }));
|
|
141
|
+
expect(calls).toEqual([]);
|
|
142
|
+
expect(ledgerText(wait.question.id)).toMatch(/holds the vault creds/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("a steward's question stays out of the checkout (#2786)", () => {
|
|
147
|
+
const status = () => git(root, "status", "--porcelain", "--untracked-files=all");
|
|
148
|
+
|
|
149
|
+
test("asked in a steward's turn, it is written on chant/lifecycle, the checkout stays clean, and points --open lists it", async () => {
|
|
150
|
+
// Earlier tests asked outside a turn, into the tree; commit those so the checkout starts clean.
|
|
151
|
+
commitAll(root, "earlier answers");
|
|
152
|
+
expect(status()).toBe("");
|
|
153
|
+
enterStewardTurn({ steward: "box-steward", capabilities: [], vault: null, run: "run-9" });
|
|
154
|
+
const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-9" }, subject: "r-9", on }));
|
|
155
|
+
expect(status()).toBe("");
|
|
156
|
+
expect(existsSync(join(root, wait.question.path))).toBe(false);
|
|
157
|
+
expect(ledgerText(wait.question.id)).toContain(`id: "${wait.question.id}"`);
|
|
158
|
+
|
|
159
|
+
const doc = await workspacePoints({ cwd: root, open: true });
|
|
160
|
+
if (!("questions" in doc)) throw new Error(doc.error.message);
|
|
161
|
+
const listed = doc.questions.find((q) => q.id === wait.question.id);
|
|
162
|
+
expect(listed).toMatchObject({ state: "escalated", open: true, subject: "r-9", path: wait.question.path, ledger: `chant/lifecycle:_answers/answer/${wait.question.id}.md` });
|
|
163
|
+
const pointsShape = contract(pointsSchema);
|
|
164
|
+
pointsShape.expectValid(doc);
|
|
165
|
+
|
|
166
|
+
// Asked again in the same turn, the question is found on the ledger, not asked twice.
|
|
167
|
+
const again = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-9" }, subject: "r-9", on }));
|
|
168
|
+
expect(again.question.id).toBe(wait.question.id);
|
|
169
|
+
|
|
170
|
+
// A person answers it: the answer goes back on the ledger, and the checkout is still clean.
|
|
171
|
+
resetStewardTurn();
|
|
172
|
+
const answered = await answerPoint({ cwd: root, id: wait.question.id, answer: "no", by: ["alice"], on });
|
|
173
|
+
if ("error" in answered) throw new Error(answered.error.message);
|
|
174
|
+
expect(answered.question).toMatchObject({ state: "answered", answer: false, ledger: `chant/lifecycle:_answers/answer/${wait.question.id}.md` });
|
|
175
|
+
expect(status()).toBe("");
|
|
176
|
+
expect(ledgerText(wait.question.id)).toContain('state: "answered"');
|
|
177
|
+
|
|
178
|
+
// The steward's next turn reads the answer and goes on.
|
|
179
|
+
enterStewardTurn({ steward: "box-steward", capabilities: [], vault: null, run: "run-10" });
|
|
180
|
+
const got = await askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-9" }, subject: "r-9", on });
|
|
181
|
+
expect(got.answer).toBe(false);
|
|
182
|
+
expect(status()).toBe("");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("points ask shelled out from a steward's turn (CHANT_STEWARD) goes on the ledger too, naming the steward", async () => {
|
|
186
|
+
expect(status()).toBe("");
|
|
187
|
+
process.env[STEWARD_ENV] = "box-steward";
|
|
188
|
+
const doc = await askPoint({ cwd: root, point: "ship-now", inputs: { release: "r-11" }, subject: "r-11", on });
|
|
189
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
190
|
+
expect(doc.written).toBe(true);
|
|
191
|
+
expect(doc.question).toMatchObject({ state: "escalated", askedBy: { steward: "box-steward", run: null }, ledger: `chant/lifecycle:_answers/answer/${doc.id}.md` });
|
|
192
|
+
expect(status()).toBe("");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("outside a steward's turn a question is still written into the answer kind's directory", async () => {
|
|
196
|
+
const doc = await askPoint({ cwd: root, point: "ship-now", inputs: { release: "r-12" }, on });
|
|
197
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
198
|
+
expect(doc.question.ledger).toBeNull();
|
|
199
|
+
expect(existsSync(join(root, doc.path))).toBe(true);
|
|
200
|
+
rmSync(join(root, doc.path));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("a steward never answers a question (#2749)", () => {
|
|
205
|
+
test("points answer is refused inside a steward's turn, and in a process it started", async () => {
|
|
206
|
+
enterStewardTurn({ steward: "box-steward", capabilities: ["inference"], vault: null });
|
|
207
|
+
const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-4" }, on }));
|
|
208
|
+
const doc = await answerPoint({ cwd: root, id: wait.question.id, answer: "yes", by: ["alice"], on });
|
|
209
|
+
expect("error" in doc && doc.error.code).toBe("answer-in-steward-turn");
|
|
210
|
+
|
|
211
|
+
resetStewardTurn();
|
|
212
|
+
process.env[STEWARD_ENV] = "box-steward";
|
|
213
|
+
const child = await answerPoint({ cwd: root, id: wait.question.id, answer: "yes", by: ["alice"], on });
|
|
214
|
+
expect("error" in child && child.error.code).toBe("answer-in-steward-turn");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("the steward that asked never counts toward the quorum", async () => {
|
|
218
|
+
enterStewardTurn({ steward: "box-steward", capabilities: [], vault: null });
|
|
219
|
+
const wait = await waitOf(askPointInRun({ cwd: root, point: "ship-now", inputs: { release: "r-5" }, on }));
|
|
220
|
+
resetStewardTurn();
|
|
221
|
+
const doc = await answerPoint({ cwd: root, id: wait.question.id, answer: "yes", by: ["Box-Steward", "bot"], on });
|
|
222
|
+
if (!("error" in doc)) throw new Error("expected a refusal");
|
|
223
|
+
expect(doc.error.code).toBe("quorum-not-met");
|
|
224
|
+
expect(doc.error.message).toMatch(/Box-Steward, who is the steward that asked/);
|
|
225
|
+
expect(doc.error.message).toMatch(/bot, who holds the agent role/);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
function shipOp(name: string, release: string, cron?: string): OpConfig {
|
|
230
|
+
return {
|
|
231
|
+
name,
|
|
232
|
+
overview: `${name} fixture`,
|
|
233
|
+
phases: [
|
|
234
|
+
{ name: "Decide", steps: [{ kind: "activity", fn: "askShip", args: { release } }] },
|
|
235
|
+
{ name: "Ship", steps: [{ kind: "activity", fn: "ship", args: {} }] },
|
|
236
|
+
],
|
|
237
|
+
onFailure: [{ name: "Undo", steps: [{ kind: "activity", fn: "undo", args: {} }] }],
|
|
238
|
+
...(cron ? { schedule: { cron, overlap: "skip" as const } } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function shipActivities(log: string[]): Map<string, ActivityFn> {
|
|
243
|
+
return new Map<string, ActivityFn>([
|
|
244
|
+
["askShip", async (args) => (await askPointInRun({ cwd: root, point: "ship-now", inputs: { release: args.release }, on })).answer],
|
|
245
|
+
["ship", async () => { log.push("ship"); return { shipped: true }; }],
|
|
246
|
+
["undo", async () => { log.push("undo"); return {}; }],
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
describe("an Op waiting on a decision point (#2749)", () => {
|
|
251
|
+
test("stops as waiting, like a gate: no later phase, no compensation, and the ledger names the question", async () => {
|
|
252
|
+
const log: string[] = [];
|
|
253
|
+
const result = await runOpLocally(shipOp("ship-once", "r-6"), shipActivities(log), PROFILES, undefined, { ledger: { cwd: root } });
|
|
254
|
+
expect(result.status).toBe("waiting");
|
|
255
|
+
expect(log).toEqual([]);
|
|
256
|
+
expect(result.records.map((r) => [r.fn, r.status])).toEqual([["askShip", "skipped"], ["ship", "skipped"]]);
|
|
257
|
+
expect(result.point).toMatchObject({ point: "ship-now", state: "escalated", steward: null });
|
|
258
|
+
const newest = (await readRunLedger("local", "ship-once", { cwd: root })).records.at(-1)!;
|
|
259
|
+
expect(newest).toMatchObject({ status: "waiting", point: { id: result.point!.id, point: "ship-now", state: "escalated" } });
|
|
260
|
+
expect(newest.steward).toBeUndefined();
|
|
261
|
+
expect(newest.phases[0].steps[0]).toMatchObject({ fn: "askShip", status: "skipped", point: { id: result.point!.id } });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("a local steward records the wait, lists it in status, leaves it while open, and resumes once a person answers", async () => {
|
|
265
|
+
const config = shipOp("box-ship", "r-7", "0 0 1 1 *");
|
|
266
|
+
const steward = declareSteward({ name: "box-steward", ops: [config], capabilities: ["inference"] });
|
|
267
|
+
const log: string[] = [];
|
|
268
|
+
const scheduleState = new Map<string, Date>();
|
|
269
|
+
// Cron minutes are local time, so the rounds are too.
|
|
270
|
+
const round = (minute: number) =>
|
|
271
|
+
runOperatorRound({ cwd: root, steward, activities: shipActivities(log), profiles: PROFILES, holder: "box", now: () => new Date(2027, 0, 1, 0, minute, 10), scheduleState });
|
|
272
|
+
|
|
273
|
+
// The cron fires: the Op asks, nobody has answered, the run waits.
|
|
274
|
+
const first = await round(0);
|
|
275
|
+
expect(first).toHaveLength(1);
|
|
276
|
+
expect(first[0]).toMatchObject({ kind: "ticked", op: "box-ship" });
|
|
277
|
+
const ticked = first[0] as Extract<(typeof first)[number], { kind: "ticked" }>;
|
|
278
|
+
expect(ticked.result.status).toBe("waiting");
|
|
279
|
+
const question = ticked.result.point!.id;
|
|
280
|
+
expect(formatRoundLine(first[0])).toContain(`status=waiting point="${question}"`);
|
|
281
|
+
const newest = (await readRunLedger("local", "box-ship", { cwd: root })).records.at(-1)!;
|
|
282
|
+
expect(newest).toMatchObject({ status: "waiting", steward: "box-steward", point: { id: question } });
|
|
283
|
+
|
|
284
|
+
// workspace status lists what the steward waits on, beside its Ops.
|
|
285
|
+
mkdirSync(join(root, "ops"), { recursive: true });
|
|
286
|
+
writeFileSync(join(root, "chant.config.json"), "{}\n");
|
|
287
|
+
writeFileSync(join(root, "ops", "steward.op.ts"), `export const steward = ${JSON.stringify(steward)};\n`);
|
|
288
|
+
const status = await readMemberStewards(root, "local", "2027-01-01T00:01:00Z");
|
|
289
|
+
expect(status.reasons).toEqual([]);
|
|
290
|
+
const entry = status.stewards.find((s) => s.name === "box-steward")!;
|
|
291
|
+
// The entry keeps status.schema.json's steward shape (#2731, #2749).
|
|
292
|
+
const stewardShape = contract({ $schema: statusSchema.$schema, $id: "urn:test:status-steward", $defs: statusSchema.$defs, $ref: "#/$defs/steward" });
|
|
293
|
+
stewardShape.expectValid(entry);
|
|
294
|
+
expect(entry.ops[0].lastRun).toMatchObject({ status: "waiting", point: { id: question, point: "ship-now", state: "escalated" } });
|
|
295
|
+
expect(entry.waiting).toEqual([
|
|
296
|
+
{ op: "box-ship", run: newest.id, id: question, point: "ship-now", state: "escalated", path: newest.point!.path, subject: null, since: newest.point!.since },
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
// Still open: the steward does not run it, and says what it waits on.
|
|
300
|
+
const second = await round(5);
|
|
301
|
+
expect(second).toEqual([{ kind: "waiting-on-point", op: "box-ship", env: "local", point: "ship-now", question, state: "escalated" }]);
|
|
302
|
+
expect(formatRoundLine(second[0])).toBe(`operator: box-ship@local waiting=1(point:${question}:escalated)`);
|
|
303
|
+
|
|
304
|
+
// A person answers, through hud or at a shell; the next round resumes the Op.
|
|
305
|
+
const answered = await answerPoint({ cwd: root, id: question, answer: "yes", by: ["alice"], on });
|
|
306
|
+
expect("error" in answered).toBe(false);
|
|
307
|
+
const third = await round(10);
|
|
308
|
+
expect(third[0]).toMatchObject({ kind: "ticked", op: "box-ship", resumed: question });
|
|
309
|
+
expect((third[0] as { result: { status: string } }).result.status).toBe("ok");
|
|
310
|
+
expect(log).toEqual(["ship"]);
|
|
311
|
+
expect((await readMemberStewards(root, "local", "2027-01-01T00:11:00Z")).stewards[0].waiting).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
});
|