@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decision point asked inside an Op run (#2749), and in a steward's turn in
|
|
3
|
+
* particular.
|
|
4
|
+
*
|
|
5
|
+
* An Op that needs a decision (the factory's builder tier, whether to ship)
|
|
6
|
+
* calls {@link askPointInRun} from one of its activities, such as the decide
|
|
7
|
+
* activity (#2740). It asks the point through `askPoint` (ws-058,
|
|
8
|
+
* `../workspace/decide.ts`), which records the question and its answer as a
|
|
9
|
+
* record, and then:
|
|
10
|
+
*
|
|
11
|
+
* - an `answered` question returns its answer, and the Op goes on;
|
|
12
|
+
* - an `escalated` or `proposed` question is open, so the activity throws
|
|
13
|
+
* {@link PointWait}. The executor ends the run there with status `waiting`,
|
|
14
|
+
* as a gate ends it with `gated`: no later step runs, no `onFailure` phase
|
|
15
|
+
* runs, and the run ledger records the question the run is waiting on.
|
|
16
|
+
*
|
|
17
|
+
* The question stays open in the workspace, where `chant workspace points
|
|
18
|
+
* --open`, MCP's `workspace-points` and hud read it, and a person answers it
|
|
19
|
+
* with `points answer`. Nothing waits in the meantime: the next run asks the
|
|
20
|
+
* same point with the same inputs, finds the answer record, and goes on. A
|
|
21
|
+
* local steward re-runs a waiting Op on the first round after its question is
|
|
22
|
+
* answered (`./operator.ts`).
|
|
23
|
+
*
|
|
24
|
+
* ## In a steward's turn
|
|
25
|
+
*
|
|
26
|
+
* A steward runs unattended, so two more rules hold when the process is a
|
|
27
|
+
* steward's turn (`./steward-turn.ts`):
|
|
28
|
+
*
|
|
29
|
+
* - The model call goes through the broker. The caller supplies the call as a
|
|
30
|
+
* {@link BrokeredModelAsk}, which is handed the box capability to reach the
|
|
31
|
+
* model through, and only a capability the steward declares
|
|
32
|
+
* (`declareSteward({ capabilities })`, #2726). A steward that names none,
|
|
33
|
+
* or holds a vault, makes no model call: the model decider is recorded as
|
|
34
|
+
* not reached and the question goes to people.
|
|
35
|
+
* - The question is written on the `chant/lifecycle` branch, not in the
|
|
36
|
+
* checkout (#2786): the working tree is the coding agent's, and an Op's
|
|
37
|
+
* leased worktree goes when the run ends. `points` reads it from there
|
|
38
|
+
* (`../workspace/answers-ledger.ts`), and a person's answer goes there too.
|
|
39
|
+
* - The question names the steward and its run in its source block, and
|
|
40
|
+
* neither the steward's turn nor the steward's name can answer it: `points
|
|
41
|
+
* answer` refuses inside a steward's turn (`answer-in-steward-turn`) and
|
|
42
|
+
* leaves the asking steward out of the quorum.
|
|
43
|
+
*
|
|
44
|
+
* The question never goes to the steward's own thread: on Fountain that thread
|
|
45
|
+
* is the steward's, and nobody reads it for questions.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { ModelAsk, ModelRequest, WireAnswer } from "../workspace/points";
|
|
49
|
+
import type { QuestionView } from "../workspace/decide";
|
|
50
|
+
import { currentStewardTurn, type StewardTurn } from "./steward-turn";
|
|
51
|
+
|
|
52
|
+
/** The box capability a model call goes through when the caller names none. */
|
|
53
|
+
export const DEFAULT_INFERENCE_CAPABILITY = "inference";
|
|
54
|
+
|
|
55
|
+
/** The open question a run stopped on, as the run ledger records it. */
|
|
56
|
+
export interface WaitingPoint {
|
|
57
|
+
/** The answer record's id: what `points answer` and hud name. */
|
|
58
|
+
id: string;
|
|
59
|
+
/** The point asked. */
|
|
60
|
+
point: string;
|
|
61
|
+
/** Its state when the run stopped: `escalated` to people, or `proposed` by a model and waiting for a person to confirm it. */
|
|
62
|
+
state: "escalated" | "proposed";
|
|
63
|
+
/** The answer record, from the repository root. */
|
|
64
|
+
path: string;
|
|
65
|
+
/** What the question is about, or null. */
|
|
66
|
+
subject: string | null;
|
|
67
|
+
/** The steward whose turn asked it, or null. */
|
|
68
|
+
steward: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The marker {@link PointWait} carries, so a copy of core linked twice still recognises it. */
|
|
72
|
+
const POINT_WAIT = Symbol.for("chant.op.PointWait");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Thrown by an activity whose decision point is open. Not a failure: the
|
|
76
|
+
* executor ends the run with status `waiting` (see the module doc).
|
|
77
|
+
*/
|
|
78
|
+
export class PointWait extends Error {
|
|
79
|
+
readonly [POINT_WAIT] = true;
|
|
80
|
+
constructor(readonly question: WaitingPoint) {
|
|
81
|
+
super(
|
|
82
|
+
`waiting on decision point ${question.point} (${question.id}, ${question.state}): ` +
|
|
83
|
+
`a person answers it through hud or \`chant workspace points answer ${question.id}\``,
|
|
84
|
+
);
|
|
85
|
+
this.name = "PointWait";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Is this a {@link PointWait}? Duck-typed on the shared symbol. */
|
|
90
|
+
export function isPointWait(err: unknown): err is PointWait {
|
|
91
|
+
return !!err && typeof err === "object" && (err as Record<symbol, unknown>)[POINT_WAIT] === true && "question" in err;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The model call, supplied by the caller (the decide activity, #2740, or a
|
|
96
|
+
* test's stub), made through a box capability. `via.capability` is the
|
|
97
|
+
* capability to reach the model through and `via.broker` the broker the
|
|
98
|
+
* member's box block names for it, when that is known. The function holds no
|
|
99
|
+
* credential: the broker does.
|
|
100
|
+
*/
|
|
101
|
+
export type BrokeredModelAsk = (
|
|
102
|
+
request: ModelRequest,
|
|
103
|
+
via: { capability: string; broker: string | null },
|
|
104
|
+
) => Promise<{ model: string; answer: WireAnswer }>;
|
|
105
|
+
|
|
106
|
+
export interface AskPointInRunOptions {
|
|
107
|
+
/** Where the workspace and the answer kind are found. */
|
|
108
|
+
cwd: string;
|
|
109
|
+
point: string;
|
|
110
|
+
inputs: unknown;
|
|
111
|
+
subject?: string;
|
|
112
|
+
/** The answer kind file, or a declared kind's name. */
|
|
113
|
+
kind?: string;
|
|
114
|
+
/** The model call. Without it, a model decider is not asked. */
|
|
115
|
+
ask?: BrokeredModelAsk;
|
|
116
|
+
/** The box capability the model call goes through. Default `inference`. */
|
|
117
|
+
capability?: string;
|
|
118
|
+
/** The broker the member's box block names for the capability, when the caller has read it. */
|
|
119
|
+
broker?: string | null;
|
|
120
|
+
/** The date written as asked_on, YYYY-MM-DD. */
|
|
121
|
+
on?: string;
|
|
122
|
+
/** The steward turn, when the caller has it. Default: this process's (`currentStewardTurn`). */
|
|
123
|
+
turn?: StewardTurn | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** What a steward reaches through its broker, read from its declaration when the turn named only the steward. */
|
|
127
|
+
async function stewardCapabilities(turn: StewardTurn, cwd: string): Promise<{ capabilities: readonly string[]; vault: string | null }> {
|
|
128
|
+
if (turn.capabilities !== undefined) return { capabilities: turn.capabilities, vault: turn.vault ?? null };
|
|
129
|
+
const { discoverStewards } = await import("./discover");
|
|
130
|
+
const found = (await discoverStewards({ cwd })).stewards.get(turn.steward)?.declaration;
|
|
131
|
+
return { capabilities: found?.capabilities ?? [], vault: found?.vault ?? null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The {@link ModelAsk} a point is asked with. In a steward's turn the call is
|
|
136
|
+
* made only through a capability the steward declares; otherwise it throws,
|
|
137
|
+
* which the chain records as the model not being reached.
|
|
138
|
+
*/
|
|
139
|
+
export async function brokeredModelAsk(
|
|
140
|
+
ask: BrokeredModelAsk,
|
|
141
|
+
opts: { capability?: string; broker?: string | null; turn?: StewardTurn | null; cwd: string },
|
|
142
|
+
): Promise<ModelAsk> {
|
|
143
|
+
const capability = opts.capability ?? DEFAULT_INFERENCE_CAPABILITY;
|
|
144
|
+
const via = { capability, broker: opts.broker ?? null };
|
|
145
|
+
const turn = opts.turn === undefined ? currentStewardTurn() : opts.turn;
|
|
146
|
+
if (!turn) return (request) => ask(request, via);
|
|
147
|
+
const { capabilities, vault } = await stewardCapabilities(turn, opts.cwd);
|
|
148
|
+
if (!capabilities.includes(capability)) {
|
|
149
|
+
const why =
|
|
150
|
+
vault !== null
|
|
151
|
+
? `the steward ${turn.steward} holds the vault ${vault} and names no brokered capabilities, and a steward's model call goes through the broker`
|
|
152
|
+
: `the steward ${turn.steward} does not name the brokered capability ${capability}` +
|
|
153
|
+
(capabilities.length ? ` (it names ${capabilities.join(", ")})` : "") +
|
|
154
|
+
`, and a steward holds no credential of its own`;
|
|
155
|
+
return async () => {
|
|
156
|
+
throw new Error(why);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return (request) => ask(request, via);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Ask a decision point from inside an Op run. Resolves with the answer when
|
|
164
|
+
* the question is answered, and throws {@link PointWait} when it is open.
|
|
165
|
+
* Any other problem (the points can't be read, the inputs are wrong, a model
|
|
166
|
+
* declared `unreachable: "fail"` could not answer) throws an ordinary error,
|
|
167
|
+
* which fails the step.
|
|
168
|
+
*/
|
|
169
|
+
export async function askPointInRun(opts: AskPointInRunOptions): Promise<{ answer: string | boolean; question: QuestionView }> {
|
|
170
|
+
const turn = opts.turn === undefined ? currentStewardTurn() : opts.turn;
|
|
171
|
+
const ask = opts.ask ? await brokeredModelAsk(opts.ask, { capability: opts.capability, broker: opts.broker, turn, cwd: opts.cwd }) : undefined;
|
|
172
|
+
const { askPoint } = await import("../workspace/decide");
|
|
173
|
+
const doc = await askPoint({
|
|
174
|
+
cwd: opts.cwd,
|
|
175
|
+
point: opts.point,
|
|
176
|
+
inputs: opts.inputs,
|
|
177
|
+
...(opts.subject !== undefined ? { subject: opts.subject } : {}),
|
|
178
|
+
...(opts.kind !== undefined ? { kind: opts.kind } : {}),
|
|
179
|
+
...(ask ? { ask } : {}),
|
|
180
|
+
...(opts.on !== undefined ? { on: opts.on } : {}),
|
|
181
|
+
...(turn ? { steward: { name: turn.steward, ...(turn.run ? { run: turn.run } : {}) } } : {}),
|
|
182
|
+
});
|
|
183
|
+
if ("error" in doc) throw new Error(`decision point ${opts.point}: ${doc.error.code}: ${doc.error.message}`);
|
|
184
|
+
const q = doc.question;
|
|
185
|
+
if (q.state === "answered" && q.answer !== null) return { answer: q.answer, question: q };
|
|
186
|
+
throw new PointWait({
|
|
187
|
+
id: q.id,
|
|
188
|
+
point: q.point,
|
|
189
|
+
state: q.state === "proposed" ? "proposed" : "escalated",
|
|
190
|
+
path: q.path,
|
|
191
|
+
subject: q.subject,
|
|
192
|
+
steward: q.askedBy?.steward ?? null,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this process is a steward's turn, and whose (#2749).
|
|
3
|
+
*
|
|
4
|
+
* A steward runs unattended. In the local form, `chant operator --steward`
|
|
5
|
+
* is the steward for as long as it runs; on Fountain, the Agent's `chant acp
|
|
6
|
+
* --steward <name>` session is. Two things read this: a decision point asked
|
|
7
|
+
* during the turn (`./steward-points.ts`), which records the steward on the
|
|
8
|
+
* question and makes its model call through the steward's brokered
|
|
9
|
+
* capability, and `points answer`, which refuses to record an answer from a
|
|
10
|
+
* steward's own turn, as the gate ledger's origin rule refuses a gate
|
|
11
|
+
* resolved on the channel that reached it.
|
|
12
|
+
*
|
|
13
|
+
* Like the gate origin (`../lifecycle/gate-origin.ts`) this is a property of
|
|
14
|
+
* the process, set once at its entry point, not a parameter threaded through
|
|
15
|
+
* every executor and activity. It is also exported to child processes as
|
|
16
|
+
* `CHANT_STEWARD`, so a `chant workspace points answer` an Op shells out to
|
|
17
|
+
* is refused the same way.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The environment variable a steward's turn carries into child processes. */
|
|
21
|
+
export const STEWARD_ENV = "CHANT_STEWARD";
|
|
22
|
+
|
|
23
|
+
/** A steward's turn, as the process knows it. */
|
|
24
|
+
export interface StewardTurn {
|
|
25
|
+
/** The steward's name. */
|
|
26
|
+
steward: string;
|
|
27
|
+
/**
|
|
28
|
+
* The box capabilities the steward reaches through the broker (#2726), or
|
|
29
|
+
* undefined when the process was told only the steward's name (a child
|
|
30
|
+
* process, or `chant acp --steward <name>`): the declaration is then read
|
|
31
|
+
* from the project's `*.op.ts` files when a model call needs it.
|
|
32
|
+
*/
|
|
33
|
+
capabilities?: readonly string[];
|
|
34
|
+
/** The vault the steward holds, by name, when it is not behind a broker. */
|
|
35
|
+
vault?: string | null;
|
|
36
|
+
/** The run the steward is in, when an Op is running. */
|
|
37
|
+
run?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let ambient: StewardTurn | undefined;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Declare that this process is a steward's turn. Called once by `chant
|
|
44
|
+
* operator --steward` and `chant acp --steward`; also exports
|
|
45
|
+
* {@link STEWARD_ENV} so a child process knows it too.
|
|
46
|
+
*/
|
|
47
|
+
export function setStewardTurn(turn: StewardTurn): void {
|
|
48
|
+
ambient = { ...turn };
|
|
49
|
+
process.env[STEWARD_ENV] = turn.steward;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Be a steward's turn until the returned function is called, which puts back
|
|
54
|
+
* whatever was there before. `chant operator --steward` enters one per Op run,
|
|
55
|
+
* naming the run, so a question asked during it names the run too.
|
|
56
|
+
*/
|
|
57
|
+
export function enterStewardTurn(turn: StewardTurn): () => void {
|
|
58
|
+
const before = ambient;
|
|
59
|
+
const envBefore = process.env[STEWARD_ENV];
|
|
60
|
+
setStewardTurn(turn);
|
|
61
|
+
return () => {
|
|
62
|
+
ambient = before;
|
|
63
|
+
if (envBefore === undefined) delete process.env[STEWARD_ENV];
|
|
64
|
+
else process.env[STEWARD_ENV] = envBefore;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The steward whose turn this process is, or undefined. */
|
|
69
|
+
export function currentStewardTurn(): StewardTurn | undefined {
|
|
70
|
+
if (ambient) return ambient;
|
|
71
|
+
const name = process.env[STEWARD_ENV];
|
|
72
|
+
return name && name.trim() !== "" ? { steward: name.trim() } : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Forget the turn. For tests, which must not leak a steward into each other. */
|
|
76
|
+
export function resetStewardTurn(): void {
|
|
77
|
+
ambient = undefined;
|
|
78
|
+
delete process.env[STEWARD_ENV];
|
|
79
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A steward's declaration and its local form (#2731): `declareSteward`'s
|
|
3
|
+
* refusals, discovery beside the Ops, a local round that runs the steward's
|
|
4
|
+
* scheduled Ops under its own lease, and the rule that a steward turn and a
|
|
5
|
+
* coding agent share one checkout without colliding.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect, vi } from "vitest";
|
|
8
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
13
|
+
import type { OpConfig } from "./types";
|
|
14
|
+
import { declareSteward, isStewardDeclaration, pickSteward, stewardFormFor, stewardLeaseName, stewardTurnLeaseName } from "./steward";
|
|
15
|
+
import { discoverOps, discoverStewards } from "./discover";
|
|
16
|
+
import { runOperatorRound, acquireStewardLease, acquireStewardTurn, formatRoundLine } from "./operator";
|
|
17
|
+
import { readLease } from "../lifecycle/lease";
|
|
18
|
+
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
19
|
+
|
|
20
|
+
const PROFILES: Record<string, ActivityProfile> = {};
|
|
21
|
+
|
|
22
|
+
function op(name: string, cron?: string, fn = "fakeTurn"): OpConfig {
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
overview: `${name} fixture`,
|
|
26
|
+
phases: [{ name: "Run", steps: [{ kind: "activity", fn, args: {} }] }],
|
|
27
|
+
...(cron ? { schedule: { cron, overlap: "skip" as const } } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function git(args: string[], cwd: string): { stdout: string; status: number } {
|
|
32
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
33
|
+
return { stdout: r.stdout ?? "", status: r.status ?? -1 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function initRepo(dir: string): Promise<void> {
|
|
37
|
+
git(["init", "-q", "-b", "main"], dir);
|
|
38
|
+
git(["config", "user.email", "test@chant.dev"], dir);
|
|
39
|
+
git(["config", "user.name", "Test"], dir);
|
|
40
|
+
mkdirSync(join(dir, "app"), { recursive: true });
|
|
41
|
+
writeFileSync(join(dir, "app", "index.ts"), "export const hello = 1;\n");
|
|
42
|
+
git(["add", "app/index.ts"], dir);
|
|
43
|
+
git(["commit", "-q", "-m", "init"], dir);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An `*.op.ts` file exporting a steward as a literal: the shape `declareSteward` returns, no imports. */
|
|
47
|
+
function writeStewardFile(dir: string, file: string, name: string, ops: OpConfig[], form: unknown = { default: "local", environments: {} }): void {
|
|
48
|
+
mkdirSync(join(dir, "ops"), { recursive: true });
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(dir, "ops", file),
|
|
51
|
+
`export const steward = ${JSON.stringify({ kind: "Chant::Steward", name, ops, form })};\n`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function turns(log: string[]): Map<string, ActivityFn> {
|
|
56
|
+
return new Map<string, ActivityFn>([
|
|
57
|
+
["fakeTurn", async () => { log.push("turn"); return { ok: true }; }],
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("declareSteward", () => {
|
|
62
|
+
test("normalises the form, local by default", () => {
|
|
63
|
+
const s = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
|
|
64
|
+
expect(isStewardDeclaration(s)).toBe(true);
|
|
65
|
+
expect(s.form).toEqual({ default: "local", environments: {} });
|
|
66
|
+
expect(stewardFormFor(s)).toBe("local");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("chooses the form per environment", () => {
|
|
70
|
+
const s = declareSteward({
|
|
71
|
+
name: "box-steward",
|
|
72
|
+
ops: [],
|
|
73
|
+
form: { default: "local", environments: { "fountain-k3d": "fountain" } },
|
|
74
|
+
});
|
|
75
|
+
expect(stewardFormFor(s, "minimal")).toBe("local");
|
|
76
|
+
expect(stewardFormFor(s, "fountain-k3d")).toBe("fountain");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("reads an Op declaration through its props", () => {
|
|
80
|
+
const s = declareSteward({ name: "box-steward", ops: [{ props: op("box-release") }] });
|
|
81
|
+
expect(s.ops.map((o) => o.name)).toEqual(["box-release"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a brokered steward names its capabilities and holds no vault (#2726)", () => {
|
|
85
|
+
const s = declareSteward({ name: "a", ops: [], capabilities: ["fountain", "inference", "fountain"] });
|
|
86
|
+
expect(s.capabilities).toEqual(["fountain", "inference"]);
|
|
87
|
+
expect(s.vault).toBeNull();
|
|
88
|
+
expect(declareSteward({ name: "b", ops: [], vault: "creds" }).vault).toBe("creds");
|
|
89
|
+
expect(() => declareSteward({ name: "c", ops: [], capabilities: ["fountain"], vault: "creds" })).toThrow(/holds no credential/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("refuses what would make two writers or a promise it can't keep", () => {
|
|
93
|
+
expect(() => declareSteward({ name: "a", ops: [op("x"), op("x")] })).toThrow(/listed twice/);
|
|
94
|
+
expect(() => declareSteward({ name: "a", ops: [{ ...op("x"), schedule: { cron: "* * * * *", overlap: "buffer" as never } }] })).toThrow(/overlap "buffer"/);
|
|
95
|
+
expect(() => declareSteward({ name: "a", ops: [op("x", "not a cron")] })).toThrow(/Steward "a": op "x"/);
|
|
96
|
+
expect(() => declareSteward({ name: "../a", ops: [] })).toThrow(/steward's name/);
|
|
97
|
+
expect(() => declareSteward({ name: "a", ops: [], form: "cloud" as never })).toThrow(/"local" or "fountain"/);
|
|
98
|
+
expect(() => declareSteward({ name: "a", ops: [], form: { default: "local", environments: { "a/b": "fountain" } } })).toThrow(/can't be an environment/);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("discoverStewards", () => {
|
|
103
|
+
test("finds a steward in an *.op.ts file, and discoverOps doesn't call that file an error", async () => {
|
|
104
|
+
await withTestDir(async (dir) => {
|
|
105
|
+
await initRepo(dir);
|
|
106
|
+
writeStewardFile(dir, "steward.op.ts", "box-steward", [op("box-converge", "* * * * *")]);
|
|
107
|
+
const { stewards, errors } = await discoverStewards({ cwd: dir });
|
|
108
|
+
expect(errors).toEqual([]);
|
|
109
|
+
expect([...stewards.keys()]).toEqual(["box-steward"]);
|
|
110
|
+
expect(stewards.get("box-steward")!.exportName).toBe("steward");
|
|
111
|
+
expect((await discoverOps({ cwd: dir })).errors).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("refuses one Op listed by two stewards", async () => {
|
|
116
|
+
await withTestDir(async (dir) => {
|
|
117
|
+
await initRepo(dir);
|
|
118
|
+
writeStewardFile(dir, "a.op.ts", "first", [op("box-converge", "* * * * *")]);
|
|
119
|
+
writeStewardFile(dir, "b.op.ts", "second", [op("box-converge", "* * * * *")]);
|
|
120
|
+
const { stewards, errors, conflicts } = await discoverStewards({ cwd: dir });
|
|
121
|
+
expect([...stewards.keys()]).toEqual(["first"]);
|
|
122
|
+
expect(errors).toEqual([]);
|
|
123
|
+
expect(conflicts).toHaveLength(1);
|
|
124
|
+
expect(conflicts[0]).toMatch(/steward "second" lists op "box-converge", which steward "first" already runs/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("pickSteward takes the only steward, or the named one, and refuses otherwise", () => {
|
|
129
|
+
const one = new Map([["box-steward", { declaration: declareSteward({ name: "box-steward", ops: [] }) }]]);
|
|
130
|
+
expect(pickSteward(one, "")).toMatchObject({ name: "box-steward" });
|
|
131
|
+
expect(pickSteward(one, "other")).toMatch(/No steward "other"/);
|
|
132
|
+
expect(pickSteward(new Map(), "")).toMatch(/No steward is declared/);
|
|
133
|
+
const two = new Map([...one, ["b", { declaration: declareSteward({ name: "b", ops: [] }) }]]);
|
|
134
|
+
expect(pickSteward(two, "")).toMatch(/2 stewards are declared/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("a local steward round", () => {
|
|
139
|
+
test("runs only the scheduled Ops, each on its cron, and records each run", async () => {
|
|
140
|
+
await withTestDir(async (dir) => {
|
|
141
|
+
await initRepo(dir);
|
|
142
|
+
const steward = declareSteward({
|
|
143
|
+
name: "box-steward",
|
|
144
|
+
ops: [op("box-converge", "* * * * *"), op("box-dispatch", "*/5 * * * *"), op("box-release")],
|
|
145
|
+
});
|
|
146
|
+
const log: string[] = [];
|
|
147
|
+
// 10:01 fires every minute, not every five.
|
|
148
|
+
const now = () => new Date(2026, 8, 25, 10, 1, 0);
|
|
149
|
+
const events = await runOperatorRound({ cwd: dir, holder: "h1", steward, activities: turns(log), profiles: PROFILES, now });
|
|
150
|
+
expect(events.map((e) => [e.kind, e.op])).toEqual([
|
|
151
|
+
["ticked", "box-converge"],
|
|
152
|
+
["skipped-not-due", "box-dispatch"],
|
|
153
|
+
]);
|
|
154
|
+
// box-release has no schedule: it runs when asked, never on a round.
|
|
155
|
+
expect(log).toEqual(["turn"]);
|
|
156
|
+
expect((await readRunLedger("local", "box-converge", { cwd: dir })).records).toHaveLength(1);
|
|
157
|
+
expect((await readLease(stewardLeaseName("box-steward"), { cwd: dir })).record?.holder).toBe("h1");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a second holder of the same steward ticks nothing", async () => {
|
|
162
|
+
await withTestDir(async (dir) => {
|
|
163
|
+
await initRepo(dir);
|
|
164
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
|
|
165
|
+
expect((await acquireStewardLease("box-steward", "h1", { cwd: dir })).acquired).toBe(true);
|
|
166
|
+
const log: string[] = [];
|
|
167
|
+
const events = await runOperatorRound({ cwd: dir, holder: "h2", steward, activities: turns(log), profiles: PROFILES });
|
|
168
|
+
expect(events).toEqual([{ kind: "steward-busy", op: "_stewards/box-steward", env: "-", steward: "box-steward", heldBy: "h1" }]);
|
|
169
|
+
expect(formatRoundLine(events[0])).toBe("operator: steward box-steward skipped=1(steward-lease-held:h1)");
|
|
170
|
+
expect(log).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a coding agent edits app/ and holds the index lock during a steward turn, and neither collides", async () => {
|
|
175
|
+
await withTestDir(async (dir) => {
|
|
176
|
+
await initRepo(dir);
|
|
177
|
+
const head = git(["rev-parse", "HEAD"], dir).stdout.trim();
|
|
178
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *", "agentEditsMidTurn")] });
|
|
179
|
+
const edited = "export const hello = 2; // the coding agent's edit\n";
|
|
180
|
+
const lock = join(dir, ".git", "index.lock");
|
|
181
|
+
const activities = new Map<string, ActivityFn>([
|
|
182
|
+
[
|
|
183
|
+
"agentEditsMidTurn",
|
|
184
|
+
async () => {
|
|
185
|
+
// The coding agent, mid-turn: an edit to the app, and a commit in
|
|
186
|
+
// progress that holds git's index lock.
|
|
187
|
+
writeFileSync(join(dir, "app", "index.ts"), edited);
|
|
188
|
+
writeFileSync(lock, "");
|
|
189
|
+
return { ok: true };
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const events = await runOperatorRound({ cwd: dir, holder: "h1", steward, activities, profiles: PROFILES });
|
|
195
|
+
expect(events.map((e) => e.kind)).toEqual(["ticked"]);
|
|
196
|
+
|
|
197
|
+
// The steward's writes landed where chant writes: the run ledger and the lease refs.
|
|
198
|
+
expect((await readRunLedger("local", "box-converge", { cwd: dir })).records).toHaveLength(1);
|
|
199
|
+
expect(git(["rev-parse", "--verify", "refs/heads/chant/lifecycle"], dir).status).toBe(0);
|
|
200
|
+
|
|
201
|
+
// And none of them touched the checkout: the agent's edit is intact, its
|
|
202
|
+
// lock is still its own, and HEAD and the index are where they were.
|
|
203
|
+
expect(readFileSync(join(dir, "app", "index.ts"), "utf-8")).toBe(edited);
|
|
204
|
+
expect(existsSync(lock)).toBe(true);
|
|
205
|
+
rmSync(lock); // the agent's commit finishes; git status needs the lock gone
|
|
206
|
+
|
|
207
|
+
expect(git(["rev-parse", "HEAD"], dir).stdout.trim()).toBe(head);
|
|
208
|
+
expect(git(["status", "--porcelain"], dir).stdout).toBe(" M app/index.ts\n");
|
|
209
|
+
expect(git(["diff", "--cached", "--name-only"], dir).stdout).toBe("");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("a hand run waits its turn (#2750)", () => {
|
|
215
|
+
test("a round holds the turn lease for exactly the length of its tick — a concurrent hand run sees it held, then free again once the tick ends", async () => {
|
|
216
|
+
await withTestDir(async (dir) => {
|
|
217
|
+
await initRepo(dir);
|
|
218
|
+
let releaseTurn: () => void = () => {};
|
|
219
|
+
const gate = new Promise<void>((resolve) => { releaseTurn = resolve; });
|
|
220
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *", "slowTurn")] });
|
|
221
|
+
const activities = new Map<string, ActivityFn>([
|
|
222
|
+
["slowTurn", async () => { await gate; return { ok: true }; }],
|
|
223
|
+
]);
|
|
224
|
+
// Real time on both sides here, deliberately: a fixed `now` on only one
|
|
225
|
+
// of the round's and the hand run's leases would make one see the
|
|
226
|
+
// other's lease as already expired by wall-clock time, which is not
|
|
227
|
+
// the race this test is about.
|
|
228
|
+
const roundPromise = runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities, profiles: PROFILES });
|
|
229
|
+
|
|
230
|
+
// Wait for the round to actually be mid-tick, holding the turn lease.
|
|
231
|
+
await vi.waitFor(async () => {
|
|
232
|
+
const turn = await readLease(stewardTurnLeaseName("box-steward"), { cwd: dir });
|
|
233
|
+
expect(turn.record?.holder).toBe("steward-proc");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// A hand run's own attempt (no `waitMs`) sees it taken and gives up
|
|
237
|
+
// without retrying — this is what `chant run <op>` names in its refusal.
|
|
238
|
+
const midTurn = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
|
|
239
|
+
expect(midTurn.acquired).toBe(false);
|
|
240
|
+
expect(midTurn.heldBy?.holder).toBe("steward-proc");
|
|
241
|
+
|
|
242
|
+
releaseTurn();
|
|
243
|
+
const events = await roundPromise;
|
|
244
|
+
expect(events.map((e) => e.kind)).toEqual(["ticked"]);
|
|
245
|
+
|
|
246
|
+
// The tick released the turn the moment it finished — succeeds again
|
|
247
|
+
// right away, not only once the whole steward process stops.
|
|
248
|
+
const afterTurn = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
|
|
249
|
+
expect(afterTurn.acquired).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a hand run's turn blocks a round: it reports turn-busy and ticks nothing, and releases the op lease it had just taken", async () => {
|
|
254
|
+
await withTestDir(async (dir) => {
|
|
255
|
+
await initRepo(dir);
|
|
256
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
|
|
257
|
+
const held = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
|
|
258
|
+
expect(held.acquired).toBe(true);
|
|
259
|
+
|
|
260
|
+
const log: string[] = [];
|
|
261
|
+
const now = () => new Date(2026, 8, 25, 10, 1, 0);
|
|
262
|
+
const events = await runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities: turns(log), profiles: PROFILES, now });
|
|
263
|
+
|
|
264
|
+
expect(events).toEqual([{ kind: "turn-busy", op: "box-converge", env: "local", steward: "box-steward", heldBy: "hand-run" }]);
|
|
265
|
+
expect(formatRoundLine(events[0])).toContain("turn-held:hand-run");
|
|
266
|
+
expect(log).toEqual([]);
|
|
267
|
+
// The op's own lease, freshly acquired before the turn check, was let go again.
|
|
268
|
+
expect((await readLease("box-converge", { cwd: dir })).record).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("a stale .lock on the turn ref reports lease-error, never a thrown exception out of the round, and still releases the op lease it had just taken (#1959 finding 2, applied to #2750's turn lease)", async () => {
|
|
273
|
+
await withTestDir(async (dir) => {
|
|
274
|
+
await initRepo(dir);
|
|
275
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
|
|
276
|
+
|
|
277
|
+
// A previous holder of the turn lease was killed mid-write — its
|
|
278
|
+
// `.lock` file is still sitting there, blocking every future write to
|
|
279
|
+
// `_turns/box-steward`, the same way #1959 finding 2 covers the
|
|
280
|
+
// per-op lease.
|
|
281
|
+
mkdirSync(join(dir, ".git", "refs", "chant", "lease", "_turns"), { recursive: true });
|
|
282
|
+
writeFileSync(join(dir, ".git", "refs", "chant", "lease", "_turns", "box-steward.lock"), "");
|
|
283
|
+
|
|
284
|
+
const log: string[] = [];
|
|
285
|
+
const now = () => new Date(2026, 8, 25, 10, 1, 0);
|
|
286
|
+
const events = await runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities: turns(log), profiles: PROFILES, now });
|
|
287
|
+
|
|
288
|
+
expect(events).toHaveLength(1);
|
|
289
|
+
expect(events[0]).toMatchObject({ kind: "lease-error", op: "_turns/box-steward", env: "local" });
|
|
290
|
+
expect((events[0] as { error: string }).error).toContain("box-steward.lock");
|
|
291
|
+
expect(log).toEqual([]);
|
|
292
|
+
// The op's own lease, freshly acquired before the failed turn attempt, was let go again.
|
|
293
|
+
expect((await readLease("box-converge", { cwd: dir })).record).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|