@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
|
@@ -49,6 +49,16 @@ vi.mock("../../lifecycle/build-ledger-store", () => ({
|
|
|
49
49
|
readBuildManifest: (...args: unknown[]) => readBuildManifestMock(...args),
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
|
+
const persistReleasePlanMock = vi.fn();
|
|
53
|
+
|
|
54
|
+
vi.mock("../../lifecycle/plan-ledger", async () => {
|
|
55
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/plan-ledger")>("../../lifecycle/plan-ledger");
|
|
56
|
+
return {
|
|
57
|
+
...actual,
|
|
58
|
+
persistReleasePlan: (...args: unknown[]) => persistReleasePlanMock(...args),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
52
62
|
vi.mock("../../lifecycle/release-ledger", async () => {
|
|
53
63
|
const actual = await vi.importActual<typeof import("../../lifecycle/release-ledger")>("../../lifecycle/release-ledger");
|
|
54
64
|
return {
|
|
@@ -137,6 +147,7 @@ describe("components handlers", () => {
|
|
|
137
147
|
discoverComponentsMock.mockReset().mockResolvedValue({ components: new Map(), sourceFiles: [], errors: [] });
|
|
138
148
|
findBuildManifestByArtifactDigestMock.mockReset().mockResolvedValue(undefined);
|
|
139
149
|
readBuildManifestMock.mockReset().mockResolvedValue(null);
|
|
150
|
+
persistReleasePlanMock.mockReset().mockResolvedValue({ commit: "a".repeat(40), written: true });
|
|
140
151
|
|
|
141
152
|
delete process.env.GITHUB_RUN_ID;
|
|
142
153
|
delete process.env.CI_PIPELINE_ID;
|
|
@@ -222,6 +233,88 @@ describe("components handlers", () => {
|
|
|
222
233
|
await runComponentsReleaseRecord(ctx);
|
|
223
234
|
expect(JSON.parse(stdoutBuf.join(""))).toMatchObject(record);
|
|
224
235
|
});
|
|
236
|
+
|
|
237
|
+
describe("--plan (ws-055, #2733)", () => {
|
|
238
|
+
let planDir: string;
|
|
239
|
+
|
|
240
|
+
beforeEach(async () => {
|
|
241
|
+
planDir = await mkdtemp(join(tmpdir(), "chant-plan-"));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
afterEach(async () => {
|
|
245
|
+
await rm(planDir, { recursive: true, force: true });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("persists the plan and records its own digest, when --digest is omitted", async () => {
|
|
249
|
+
const planPath = join(planDir, "plan.json");
|
|
250
|
+
await writeFile(planPath, JSON.stringify({ digest: "sha256:fromplan", release: "r-1", units: [] }));
|
|
251
|
+
appendReleaseRecordMock.mockResolvedValue({
|
|
252
|
+
commit: "a".repeat(40),
|
|
253
|
+
record: { version: 1, component: "svc", env: "prod", digest: "sha256:fromplan", gitSha: "abc123headsha", runId: "local-123", timestamp: "2026-01-01T00:00:00.000Z", actor: "alice" },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
|
|
257
|
+
const exit = await runComponentsReleaseRecord(ctx);
|
|
258
|
+
|
|
259
|
+
expect(exit).toBe(0);
|
|
260
|
+
expect(persistReleasePlanMock).toHaveBeenCalledTimes(1);
|
|
261
|
+
expect(persistReleasePlanMock.mock.calls[0][0]).toEqual({ digest: "sha256:fromplan", release: "r-1", units: [] });
|
|
262
|
+
const [input] = appendReleaseRecordMock.mock.calls[0];
|
|
263
|
+
expect(input).toMatchObject({ digest: "sha256:fromplan" });
|
|
264
|
+
// The plan is persisted before the release record is appended, so the
|
|
265
|
+
// record's digest always names a plan this checkout can resolve.
|
|
266
|
+
expect(persistReleasePlanMock.mock.invocationCallOrder[0]).toBeLessThan(appendReleaseRecordMock.mock.invocationCallOrder[0]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("--digest agreeing with the plan's own digest is accepted", async () => {
|
|
270
|
+
const planPath = join(planDir, "plan.json");
|
|
271
|
+
await writeFile(planPath, JSON.stringify({ digest: "sha256:agree", release: "r-1" }));
|
|
272
|
+
appendReleaseRecordMock.mockResolvedValue({ commit: "a".repeat(40), record: { version: 1, component: "svc", env: "prod", digest: "sha256:agree", gitSha: "x", runId: "y", timestamp: "z", actor: "alice" } });
|
|
273
|
+
|
|
274
|
+
const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", digest: "sha256:agree", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
|
|
275
|
+
const exit = await runComponentsReleaseRecord(ctx);
|
|
276
|
+
expect(exit).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("--digest disagreeing with the plan's own digest is refused", async () => {
|
|
280
|
+
const planPath = join(planDir, "plan.json");
|
|
281
|
+
await writeFile(planPath, JSON.stringify({ digest: "sha256:plandigest" }));
|
|
282
|
+
|
|
283
|
+
const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", digest: "sha256:other", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
|
|
284
|
+
const exit = await runComponentsReleaseRecord(ctx);
|
|
285
|
+
expect(exit).toBe(1);
|
|
286
|
+
expect(stderrBuf.join("\n")).toContain("does not match --release-plan");
|
|
287
|
+
expect(persistReleasePlanMock).not.toHaveBeenCalled();
|
|
288
|
+
expect(appendReleaseRecordMock).not.toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a plan file with no digest field is refused", async () => {
|
|
292
|
+
const planPath = join(planDir, "plan.json");
|
|
293
|
+
await writeFile(planPath, JSON.stringify({ release: "r-1" }));
|
|
294
|
+
|
|
295
|
+
const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
|
|
296
|
+
const exit = await runComponentsReleaseRecord(ctx);
|
|
297
|
+
expect(exit).toBe(1);
|
|
298
|
+
expect(stderrBuf.join("\n")).toContain("missing its own \"digest\" field");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("a plan file that is not valid JSON is refused", async () => {
|
|
302
|
+
const planPath = join(planDir, "plan.json");
|
|
303
|
+
await writeFile(planPath, "{not json");
|
|
304
|
+
|
|
305
|
+
const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: planPath, actor: "alice" }), plugins: [], serializers: [] };
|
|
306
|
+
const exit = await runComponentsReleaseRecord(ctx);
|
|
307
|
+
expect(exit).toBe(1);
|
|
308
|
+
expect(stderrBuf.join("\n")).toContain("is not valid JSON");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("a --plan path that can't be read is refused", async () => {
|
|
312
|
+
const ctx = { args: makeArgs({ extraPositional: "prod", component: "svc", releasePlanFile: join(planDir, "missing.json"), actor: "alice" }), plugins: [], serializers: [] };
|
|
313
|
+
const exit = await runComponentsReleaseRecord(ctx);
|
|
314
|
+
expect(exit).toBe(1);
|
|
315
|
+
expect(stderrBuf.join("\n")).toContain("Could not read --release-plan");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
225
318
|
});
|
|
226
319
|
|
|
227
320
|
describe("runComponentsStatus", () => {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
resolveRunId,
|
|
39
39
|
InvalidReleaseRecordError,
|
|
40
40
|
} from "../../lifecycle/release-ledger";
|
|
41
|
+
import { persistReleasePlan, InvalidReleasePlanError, type ReleasePlan } from "../../lifecycle/plan-ledger";
|
|
41
42
|
import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence, type EntityLiveRead } from "../../lifecycle/status";
|
|
42
43
|
import { commandBuildParams } from "../build-params-cli";
|
|
43
44
|
import { buildChangeSet } from "../../lifecycle/change-set";
|
|
@@ -75,6 +76,14 @@ import { withoutReadFlags } from "../../lifecycle/legacy-digest";
|
|
|
75
76
|
* never threaded in from elsewhere and never mocked in production code, per
|
|
76
77
|
* #568: "the session cannot call Date.now() in some contexts; take the
|
|
77
78
|
* timestamp from the environment/CLI at record time."
|
|
79
|
+
*
|
|
80
|
+
* `--release-plan <file>` (ws-055, #2733) takes `--digest`'s place: the file is a
|
|
81
|
+
* release plan JSON object naming its own `digest` field, persisted
|
|
82
|
+
* content-addressed to `_plans/<digest>.json` (../../lifecycle/plan-
|
|
83
|
+
* ledger.ts) before the release record is appended, so the record's digest
|
|
84
|
+
* always names a plan this checkout can resolve. Passing both requires them
|
|
85
|
+
* to agree — a plan is content-addressed by its own digest, so a caller
|
|
86
|
+
* naming a different one is a mistake, not an override.
|
|
78
87
|
*/
|
|
79
88
|
export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<number> {
|
|
80
89
|
const { args } = ctx;
|
|
@@ -87,11 +96,42 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
const component = args.component;
|
|
90
|
-
|
|
99
|
+
let digest = args.digest;
|
|
100
|
+
let plan: ReleasePlan | undefined;
|
|
101
|
+
if (args.releasePlanFile) {
|
|
102
|
+
let raw: string;
|
|
103
|
+
try {
|
|
104
|
+
raw = await readFile(args.releasePlanFile, "utf-8");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(formatError({ message: `Could not read --release-plan ${args.releasePlanFile}: ${err instanceof Error ? err.message : String(err)}` }));
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(formatError({ message: `--release-plan ${args.releasePlanFile} is not valid JSON: ${err instanceof Error ? err.message : String(err)}` }));
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
117
|
+
console.error(formatError({ message: `--release-plan ${args.releasePlanFile} must be a JSON object` }));
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
plan = parsed as ReleasePlan;
|
|
121
|
+
if (typeof plan.digest !== "string" || plan.digest.length === 0) {
|
|
122
|
+
console.error(formatError({ message: `--release-plan ${args.releasePlanFile} is missing its own "digest" field — a release plan is content-addressed by its own digest` }));
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
if (digest !== undefined && digest !== plan.digest) {
|
|
126
|
+
console.error(formatError({ message: `--digest ${digest} does not match --release-plan ${args.releasePlanFile}'s own digest ${plan.digest}` }));
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
digest = plan.digest;
|
|
130
|
+
}
|
|
91
131
|
if (!component || !digest) {
|
|
92
132
|
console.error(formatError({
|
|
93
133
|
message: "--component and --digest are required",
|
|
94
|
-
hint: "chant components release <env> --component <name> --digest <sha256:...> [--git-sha <sha>] [--run-id <id>] [--actor <name>] [--approver <name>]",
|
|
134
|
+
hint: "chant components release <env> --component <name> --digest <sha256:...> [--release-plan <file>] [--git-sha <sha>] [--run-id <id>] [--actor <name>] [--approver <name>]",
|
|
95
135
|
}));
|
|
96
136
|
return 1;
|
|
97
137
|
}
|
|
@@ -121,6 +161,7 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
121
161
|
const timestamp = new Date().toISOString();
|
|
122
162
|
|
|
123
163
|
try {
|
|
164
|
+
if (plan) await persistReleasePlan(plan);
|
|
124
165
|
const { commit, record } = await appendReleaseRecord({
|
|
125
166
|
component,
|
|
126
167
|
env: environment,
|
|
@@ -142,7 +183,7 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
142
183
|
}
|
|
143
184
|
return 0;
|
|
144
185
|
} catch (err) {
|
|
145
|
-
if (err instanceof InvalidReleaseRecordError) {
|
|
186
|
+
if (err instanceof InvalidReleaseRecordError || err instanceof InvalidReleasePlanError) {
|
|
146
187
|
console.error(formatError({ message: err.message }));
|
|
147
188
|
return 1;
|
|
148
189
|
}
|
|
@@ -3,6 +3,7 @@ import type { ParsedArgs } from "../registry";
|
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
4
|
import { AttrRef } from "../../attrref";
|
|
5
5
|
import { GRAPH_IR_VERSION } from "../../graph-ir";
|
|
6
|
+
import { NO_LEXICON_DETECTED_MESSAGE } from "../../detectLexicon";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The aws emulator capability, as the real plugin declares it. `--live`
|
|
@@ -469,6 +470,25 @@ describe("runGraph", () => {
|
|
|
469
470
|
expect(exit).toBe(1);
|
|
470
471
|
expect(stderrBuf.join("\n")).toContain("boom");
|
|
471
472
|
});
|
|
473
|
+
|
|
474
|
+
// #2841 — an Ops-only project (`lexicons: []`, no lexicon import) has no
|
|
475
|
+
// lexicon to predict with. Detection's sentinel means "none", not a failed
|
|
476
|
+
// graph, so `chant workspace graph` does not report the member failed.
|
|
477
|
+
test("--format ir in a project with no lexicon graphs with no plugins", async () => {
|
|
478
|
+
lintClean(); discovered();
|
|
479
|
+
resolveLexMock.mockRejectedValue(new Error(NO_LEXICON_DETECTED_MESSAGE));
|
|
480
|
+
loadPluginsMock.mockResolvedValue([]);
|
|
481
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
482
|
+
expect(exit).toBe(0);
|
|
483
|
+
expect(loadPluginsMock).toHaveBeenCalledWith([]);
|
|
484
|
+
expect(JSON.parse(stdoutBuf.join("\n")).nodes.length).toBe(3);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("--format ir still fails on a lexicon error other than none detected", async () => {
|
|
488
|
+
lintClean(); discovered();
|
|
489
|
+
resolveLexMock.mockRejectedValue(new Error("lexicon @intentius/chant-lexicon-nope is not installed"));
|
|
490
|
+
await expect(runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] })).rejects.toThrow("not installed");
|
|
491
|
+
});
|
|
472
492
|
});
|
|
473
493
|
|
|
474
494
|
describe("component DAG view (--components --format ir|layout)", () => {
|
|
@@ -21,6 +21,7 @@ import { toDot } from "../../graph-dot";
|
|
|
21
21
|
import { getLayoutEngine, toLayoutInput, type NodeSize } from "../../graph-layout";
|
|
22
22
|
import { lintCommand } from "../commands/lint";
|
|
23
23
|
import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "../plugins";
|
|
24
|
+
import { isNoLexiconDetected } from "../../detectLexicon";
|
|
24
25
|
import { readFileSync } from "node:fs";
|
|
25
26
|
import { formatError, formatWarning, formatBold } from "../format";
|
|
26
27
|
import type { CommandContext, ParsedArgs } from "../registry";
|
|
@@ -31,6 +32,19 @@ import type { BuildParamProvenance } from "../../provenance";
|
|
|
31
32
|
import { discoverComponents } from "../../components/discover";
|
|
32
33
|
import { cfnDeployStacks } from "./components";
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The project's lexicons, or none for a project that declares none and imports
|
|
37
|
+
* none (an Ops-only member, `lexicons: []`): detection's NO_LEXICON_DETECTED
|
|
38
|
+
* sentinel means "no lexicon", as `chant lint` reads it, not a failed graph (#2841).
|
|
39
|
+
*/
|
|
40
|
+
async function projectLexiconsOrNone(projectPath: string): Promise<string[]> {
|
|
41
|
+
try {
|
|
42
|
+
return await resolveProjectLexicons(projectPath);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (isNoLexiconDetected(err)) return [];
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
/**
|
|
36
50
|
* Resolve this invocation's declared build-time parameters, the same way
|
|
@@ -223,7 +237,7 @@ async function runGraphLive(
|
|
|
223
237
|
// `graph` is not `requiresPlugins` (Op/source-graph modes must work without a
|
|
224
238
|
// lexicon), so `ctx.plugins` is empty. The live path needs the project's
|
|
225
239
|
// observation plugins — load them here, mirroring the lifecycle handlers.
|
|
226
|
-
const plugins = ctx.plugins.length > 0 ? ctx.plugins : await loadPlugins(await
|
|
240
|
+
const plugins = ctx.plugins.length > 0 ? ctx.plugins : await loadPlugins(await projectLexiconsOrNone(projectPath));
|
|
227
241
|
const declaredEnvNames = environmentNames(config.environments);
|
|
228
242
|
if (declaredEnvNames && !matchesDeclaredEnvironment(config.environments, environment)) {
|
|
229
243
|
console.error(formatError({
|
|
@@ -828,7 +842,7 @@ async function runGraphView(
|
|
|
828
842
|
{
|
|
829
843
|
const plugins = ctx.plugins.length > 0
|
|
830
844
|
? ctx.plugins
|
|
831
|
-
: await loadPlugins(await
|
|
845
|
+
: await loadPlugins(await projectLexiconsOrNone(projectPath));
|
|
832
846
|
const predicted = await predictOntoIr(ir, plugins as LexiconPlugin[], ctx.args, {
|
|
833
847
|
// No environment on the declared path: the file is not deployed anywhere
|
|
834
848
|
// yet, and naming one would claim it was.
|
|
@@ -11,7 +11,8 @@ import { loadChantConfig } from "../../config";
|
|
|
11
11
|
import { build } from "../../build";
|
|
12
12
|
import { isResourceDeclarable } from "../../declarable";
|
|
13
13
|
import { collectBuildRootContributors, collectChangeSubscribers } from "../plugins";
|
|
14
|
-
import { discoverOps } from "../../op/discover";
|
|
14
|
+
import { discoverOps, discoverStewards } from "../../op/discover";
|
|
15
|
+
import { pickSteward, stewardFormFor, stewardLeaseName, DEFAULT_STEWARD_ENV } from "../../op/steward";
|
|
15
16
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
16
17
|
import { parseDuration } from "../../op/local-executor";
|
|
17
18
|
import {
|
|
@@ -21,13 +22,14 @@ import {
|
|
|
21
22
|
formatRoundLine,
|
|
22
23
|
formatSignalLine,
|
|
23
24
|
DEFAULT_OPERATOR_INTERVAL_MS,
|
|
25
|
+
acquireStewardLease,
|
|
24
26
|
type ChangeSubscriber,
|
|
25
27
|
type OperatorSignalEvent,
|
|
26
28
|
type OperatorTickEvent,
|
|
27
29
|
} from "../../op/operator";
|
|
28
|
-
import { readLease, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease";
|
|
30
|
+
import { readLease, releaseLease, currentHolderId, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease";
|
|
29
31
|
import { readConvergeLedger, type ConvergeTickRecord } from "../../lifecycle/converge-ledger";
|
|
30
|
-
import { readRunLedger } from "../../lifecycle/run-ledger";
|
|
32
|
+
import { readRunLedger, runEnvOf } from "../../lifecycle/run-ledger";
|
|
31
33
|
import type { OpRunRecord } from "../../op/runtime";
|
|
32
34
|
import type { GateResolutionRecord, PendingGateRecord } from "../../lifecycle/gate-ledger";
|
|
33
35
|
import {
|
|
@@ -141,6 +143,7 @@ async function collectOperatorSubscribers(
|
|
|
141
143
|
* leaving the daemon running); omitted, the daemon loops until Ctrl-C.
|
|
142
144
|
*/
|
|
143
145
|
export async function runOperator(ctx: CommandContext): Promise<number> {
|
|
146
|
+
if (ctx.args.steward !== undefined) return runStewardOperator(ctx);
|
|
144
147
|
const { ops, errors } = await discoverConvergeOps({ env: ctx.args.env });
|
|
145
148
|
for (const err of errors) console.error(formatWarning({ message: err }));
|
|
146
149
|
|
|
@@ -205,6 +208,108 @@ export async function runOperator(ctx: CommandContext): Promise<number> {
|
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
// ── chant operator --steward ────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* `chant operator --steward [<name>] [--env <env>] [--interval] [--lease-ttl]
|
|
215
|
+
* [--once]` (#2731) — a steward's local form. The same Ops the steward runs on
|
|
216
|
+
* Fountain, on the same crons, one at a time, each under its operator lease,
|
|
217
|
+
* and the whole process under the steward's own lease.
|
|
218
|
+
*
|
|
219
|
+
* Refuses, before ticking anything:
|
|
220
|
+
* - an environment whose form is `fountain`, because there the composite's
|
|
221
|
+
* Agent and Teammate are the steward and this process would be a second
|
|
222
|
+
* writer beside them;
|
|
223
|
+
* - a steward whose lease another live process holds (a second local
|
|
224
|
+
* operator for it).
|
|
225
|
+
*/
|
|
226
|
+
export async function runStewardOperator(ctx: CommandContext): Promise<number> {
|
|
227
|
+
const { stewards, errors, conflicts } = await discoverStewards();
|
|
228
|
+
for (const err of errors) console.error(formatWarning({ message: err }));
|
|
229
|
+
for (const conflict of conflicts) console.error(formatError({ message: conflict }));
|
|
230
|
+
|
|
231
|
+
const picked = pickSteward(stewards, ctx.args.steward ?? "");
|
|
232
|
+
if (typeof picked === "string") {
|
|
233
|
+
console.error(formatError({ message: picked }));
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
const steward = picked;
|
|
237
|
+
const env = ctx.args.env ?? DEFAULT_STEWARD_ENV;
|
|
238
|
+
const form = stewardFormFor(steward, env);
|
|
239
|
+
if (form !== "local") {
|
|
240
|
+
console.error(formatError({
|
|
241
|
+
message: `steward "${steward.name}" runs on ${form} in environment "${env}", so a local operator for it would be a second writer`,
|
|
242
|
+
hint: `Its turns run on the fountain teammate "${steward.name}". Run this in an environment the declaration's form makes local, or change the form.`,
|
|
243
|
+
}));
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let activities, profiles;
|
|
248
|
+
try {
|
|
249
|
+
[activities, profiles] = await loadOperatorActivities();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const intervalMs = ctx.args.interval ? parseDuration(ctx.args.interval) : DEFAULT_OPERATOR_INTERVAL_MS;
|
|
256
|
+
const leaseTtlMs = ctx.args.leaseTtl ? parseDuration(ctx.args.leaseTtl) : DEFAULT_LEASE_TTL_MS;
|
|
257
|
+
const holder = currentHolderId();
|
|
258
|
+
|
|
259
|
+
// Refuse a second local operator up front rather than letting it loop on
|
|
260
|
+
// `steward-busy` rounds: one steward is one writer.
|
|
261
|
+
const first = await acquireStewardLease(steward.name, holder, { ttlMs: leaseTtlMs });
|
|
262
|
+
if (!first.acquired) {
|
|
263
|
+
console.error(formatError({
|
|
264
|
+
message: `steward "${steward.name}" is already running (lease held by ${first.heldBy?.holder ?? "another process"} until ${first.heldBy?.expiresAt ?? "?"})`,
|
|
265
|
+
hint: "One steward is one writer. Stop the other operator, or wait for its lease to expire.",
|
|
266
|
+
}));
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const onSigint = () => {
|
|
272
|
+
console.error(formatWarning({ message: "interrupted — stopping steward" }));
|
|
273
|
+
controller.abort();
|
|
274
|
+
};
|
|
275
|
+
process.once("SIGINT", onSigint);
|
|
276
|
+
process.once("SIGTERM", onSigint);
|
|
277
|
+
|
|
278
|
+
const printRound = (events: OperatorTickEvent[]) => {
|
|
279
|
+
for (const event of events) console.error(formatInfo(formatRoundLine(event)));
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const scheduled = steward.ops.filter((op) => op.schedule !== undefined);
|
|
283
|
+
try {
|
|
284
|
+
if (ctx.args.once) {
|
|
285
|
+
const events = await runOperatorRound({ steward, holder, leaseTtlMs, activities, profiles, signal: controller.signal });
|
|
286
|
+
printRound(events);
|
|
287
|
+
return events.some((e) => e.kind === "tick-failed" || e.kind === "steward-busy") ? 1 : 0;
|
|
288
|
+
}
|
|
289
|
+
console.error(formatInfo(
|
|
290
|
+
`chant operator: steward ${steward.name} (local) runs ${scheduled.length} scheduled Op(s) of ${steward.ops.length}, ` +
|
|
291
|
+
`checking every ${intervalMs}ms (Ctrl-C to stop)`,
|
|
292
|
+
));
|
|
293
|
+
await runOperatorForever({
|
|
294
|
+
steward,
|
|
295
|
+
holder,
|
|
296
|
+
intervalMs,
|
|
297
|
+
leaseTtlMs,
|
|
298
|
+
activities,
|
|
299
|
+
profiles,
|
|
300
|
+
signal: controller.signal,
|
|
301
|
+
onRound: printRound,
|
|
302
|
+
});
|
|
303
|
+
return 0;
|
|
304
|
+
} finally {
|
|
305
|
+
process.removeListener("SIGINT", onSigint);
|
|
306
|
+
process.removeListener("SIGTERM", onSigint);
|
|
307
|
+
// A courtesy: the lease expires on its own if this never runs.
|
|
308
|
+
const { record } = await readLease(stewardLeaseName(steward.name)).catch(() => ({ record: undefined }));
|
|
309
|
+
if (record?.holder === holder) await releaseLease(stewardLeaseName(steward.name), holder, record.token).catch(() => false);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
208
313
|
// ── chant operator status ───────────────────────────────────────────────────
|
|
209
314
|
|
|
210
315
|
interface OpStatusLine {
|
|
@@ -308,6 +413,12 @@ async function statusFor(opName: string, env: string, cwd?: string): Promise<OpS
|
|
|
308
413
|
export interface StandaloneGateLine {
|
|
309
414
|
op: string;
|
|
310
415
|
gate: string;
|
|
416
|
+
/**
|
|
417
|
+
* The Op whose newest run stopped at this gate, when the gate is a
|
|
418
|
+
* command's own rather than the Op's (#2779): a step running `chant
|
|
419
|
+
* workspace upgrade` stops its run at `workspace-upgrade` / `<scope>`.
|
|
420
|
+
*/
|
|
421
|
+
run?: string;
|
|
311
422
|
description?: string;
|
|
312
423
|
expiresAt: string;
|
|
313
424
|
url?: string;
|
|
@@ -353,6 +464,32 @@ export async function runOperatorStatus(ctx: CommandContext): Promise<number> {
|
|
|
353
464
|
}
|
|
354
465
|
}
|
|
355
466
|
|
|
467
|
+
// A gate a command decided inside an Op's step (#2779) is recorded under
|
|
468
|
+
// the command's op, which is not an `*.op.ts`, so the loop above never
|
|
469
|
+
// reads it. The Op's newest run names it; list it against that Op while it
|
|
470
|
+
// still stands.
|
|
471
|
+
for (const [opName, discovered] of [...allOps.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
472
|
+
let newest: OpRunRecord | undefined;
|
|
473
|
+
try {
|
|
474
|
+
newest = (await readRunLedger(runEnvOf(discovered.config), opName)).records.at(-1);
|
|
475
|
+
} catch {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const gateOp = newest?.status === "gated" ? newest.gate?.op : undefined;
|
|
479
|
+
if (!newest?.gate || !gateOp || gateOp === opName) continue;
|
|
480
|
+
const record = (await pendingGatesFor(gateOp)).find((r) => r.gate === newest.gate!.name);
|
|
481
|
+
if (!record || covered.has(`${gateOp} ${record.gate}`)) continue;
|
|
482
|
+
covered.add(`${gateOp} ${record.gate}`);
|
|
483
|
+
standalone.push({
|
|
484
|
+
op: gateOp,
|
|
485
|
+
gate: record.gate,
|
|
486
|
+
run: opName,
|
|
487
|
+
expiresAt: record.expiresAt,
|
|
488
|
+
...(record.description ? { description: record.description } : {}),
|
|
489
|
+
...(record.url ? { url: record.url } : {}),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
356
493
|
if (ops.length === 0 && standalone.length === 0) {
|
|
357
494
|
console.error(formatWarning({ message: "No ConvergeOp declarations found" }));
|
|
358
495
|
return 0;
|
|
@@ -389,7 +526,7 @@ export async function runOperatorStatus(ctx: CommandContext): Promise<number> {
|
|
|
389
526
|
if (standalone.length > 0) {
|
|
390
527
|
console.log(formatBold("pending gates (no converge tick)"));
|
|
391
528
|
for (const g of standalone) {
|
|
392
|
-
console.log(` - ${g.op} gate "${g.gate}" — resolve: chant approve ${g.op} ${g.gate}`);
|
|
529
|
+
console.log(` - ${g.op} gate "${g.gate}"${g.run ? ` (Op ${g.run}'s run stopped here)` : ""} — resolve: chant approve ${g.op} ${g.gate}`);
|
|
393
530
|
if (g.description) console.log(` ${g.description}`);
|
|
394
531
|
console.log(` expires: ${g.expiresAt}`);
|
|
395
532
|
if (g.url) console.log(` approve at: ${g.url}`);
|