@intentius/chant 0.91.0 → 0.94.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/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +6 -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-plugins.d.ts +6 -6
- package/dist/cli/mcp/workspace-tools.d.ts +2 -1
- 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 +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.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/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/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 +201 -0
- 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/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 +11 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +29 -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 +2 -0
- 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 +97 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +37 -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/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 +22 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- 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-intent.d.ts +85 -0
- package/dist/workspace/box-intent.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 +104 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +47 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- 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 +7 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +7 -0
- 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 +49 -4
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +3 -3
- package/dist/workspace/declaration.d.ts +58 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +119 -1
- package/dist/workspace/declared-kinds.d.ts +20 -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-joins.d.ts +5 -5
- 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 +2 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +4 -6
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +16 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +11 -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 +34 -4
- 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 +65 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +24 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +113 -2
- package/dist/workspace/work-cli.d.ts.map +1 -1
- 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/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +34 -2
- package/src/cli/handlers/run.test.ts +154 -2
- package/src/cli/handlers/run.ts +121 -2
- package/src/cli/main.test.ts +10 -0
- package/src/cli/main.ts +23 -3
- package/src/cli/mcp/workspace-plugins.ts +6 -6
- package/src/cli/mcp/workspace-tools.test.ts +35 -19
- package/src/cli/mcp/workspace-tools.ts +62 -4
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +12 -2
- package/src/cli/serve-mcp-workspace.test.ts +6 -6
- 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/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/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -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 +308 -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 +119 -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/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 +18 -3
- package/src/op/local-executor.ts +151 -15
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +20 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +290 -11
- package/src/op/runtime.ts +31 -8
- package/src/op/runtimes/local.ts +3 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/steward-points.test.ts +364 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +87 -3
- package/src/op/steward.ts +25 -1
- package/src/op/types.ts +7 -0
- package/src/op/work-lease-run.test.ts +20 -1
- package/src/op/work-lease-run.ts +2 -2
- 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-intent.test.ts +205 -0
- package/src/workspace/box-intent.ts +159 -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 +169 -0
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +19 -5
- package/src/workspace/checks/boxes.test.ts +82 -2
- package/src/workspace/checks/boxes.ts +202 -1
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +47 -0
- package/src/workspace/checks.ts +20 -2
- 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 +1 -1
- package/src/workspace/decide.ts +156 -23
- package/src/workspace/decision-points.schema.json +3 -3
- package/src/workspace/declaration.schema.json +119 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +112 -0
- package/src/workspace/declared-kinds.test.ts +31 -5
- package/src/workspace/declared-kinds.ts +46 -1
- 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-joins.test.ts +5 -5
- package/src/workspace/intent-joins.ts +5 -5
- package/src/workspace/intent.schema.json +8 -2
- 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/points-cli.ts +17 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +51 -0
- package/src/workspace/points.test.ts +16 -15
- package/src/workspace/points.ts +5 -17
- package/src/workspace/read-contract.test.ts +9 -1
- package/src/workspace/reason-codes.test.ts +18 -8
- package/src/workspace/reason-codes.ts +21 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +27 -4
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-formats.test.ts +15 -15
- 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-since.test.ts +8 -7
- 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 +36 -2
- package/src/workspace/records.test.ts +49 -4
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +14 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +155 -5
- package/src/workspace/status.schema.json +129 -8
- package/src/workspace/status.ts +46 -1
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
- package/src/workspace/work-cli.ts +193 -10
- 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-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
- package/src/workspace/work.test.ts +20 -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
|
@@ -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.
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "../../op/operator";
|
|
30
30
|
import { readLease, releaseLease, currentHolderId, DEFAULT_LEASE_TTL_MS } from "../../lifecycle/lease";
|
|
31
31
|
import { readConvergeLedger, type ConvergeTickRecord } from "../../lifecycle/converge-ledger";
|
|
32
|
-
import { readRunLedger } from "../../lifecycle/run-ledger";
|
|
32
|
+
import { readRunLedger, runEnvOf } from "../../lifecycle/run-ledger";
|
|
33
33
|
import type { OpRunRecord } from "../../op/runtime";
|
|
34
34
|
import type { GateResolutionRecord, PendingGateRecord } from "../../lifecycle/gate-ledger";
|
|
35
35
|
import {
|
|
@@ -413,6 +413,12 @@ async function statusFor(opName: string, env: string, cwd?: string): Promise<OpS
|
|
|
413
413
|
export interface StandaloneGateLine {
|
|
414
414
|
op: string;
|
|
415
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;
|
|
416
422
|
description?: string;
|
|
417
423
|
expiresAt: string;
|
|
418
424
|
url?: string;
|
|
@@ -458,6 +464,32 @@ export async function runOperatorStatus(ctx: CommandContext): Promise<number> {
|
|
|
458
464
|
}
|
|
459
465
|
}
|
|
460
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
|
+
|
|
461
493
|
if (ops.length === 0 && standalone.length === 0) {
|
|
462
494
|
console.error(formatWarning({ message: "No ConvergeOp declarations found" }));
|
|
463
495
|
return 0;
|
|
@@ -494,7 +526,7 @@ export async function runOperatorStatus(ctx: CommandContext): Promise<number> {
|
|
|
494
526
|
if (standalone.length > 0) {
|
|
495
527
|
console.log(formatBold("pending gates (no converge tick)"));
|
|
496
528
|
for (const g of standalone) {
|
|
497
|
-
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}`);
|
|
498
530
|
if (g.description) console.log(` ${g.description}`);
|
|
499
531
|
console.log(` expires: ${g.expiresAt}`);
|
|
500
532
|
if (g.url) console.log(` approve at: ${g.url}`);
|
|
@@ -5,6 +5,13 @@ import { join } from "node:path";
|
|
|
5
5
|
import type { ParsedArgs } from "../registry";
|
|
6
6
|
|
|
7
7
|
const discoverOpsMock = vi.fn();
|
|
8
|
+
// Default: no steward owns anything discovered, so `stewardTurnGate` (#2750)
|
|
9
|
+
// is a no-op for every test that doesn't explicitly set up a steward — a
|
|
10
|
+
// `mockResolvedValueOnce` in those tests overrides it for one call and this
|
|
11
|
+
// default is what every other call sees.
|
|
12
|
+
const discoverStewardsMock = vi.fn().mockResolvedValue({ stewards: new Map(), errors: [], conflicts: [] });
|
|
13
|
+
const acquireStewardTurnMock = vi.fn();
|
|
14
|
+
const releaseLeaseMock = vi.fn();
|
|
8
15
|
const loadChantConfigMock = vi.fn();
|
|
9
16
|
const writeFileSyncMock = vi.fn();
|
|
10
17
|
const mkdirSyncMock = vi.fn();
|
|
@@ -18,7 +25,18 @@ const recordGateApprovalMock = vi.fn();
|
|
|
18
25
|
const { memoryGateLedgerPort } = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
|
|
19
26
|
let gateLedger = memoryGateLedgerPort();
|
|
20
27
|
|
|
21
|
-
vi.mock("../../op/discover", () => ({
|
|
28
|
+
vi.mock("../../op/discover", () => ({
|
|
29
|
+
discoverOps: () => discoverOpsMock(),
|
|
30
|
+
discoverStewards: () => discoverStewardsMock(),
|
|
31
|
+
}));
|
|
32
|
+
vi.mock("../../op/operator", async (importOriginal) => {
|
|
33
|
+
const actual = await importOriginal<typeof import("../../op/operator")>();
|
|
34
|
+
return { ...actual, acquireStewardTurn: (...args: unknown[]) => acquireStewardTurnMock(...args) };
|
|
35
|
+
});
|
|
36
|
+
vi.mock("../../lifecycle/lease", async (importOriginal) => {
|
|
37
|
+
const actual = await importOriginal<typeof import("../../lifecycle/lease")>();
|
|
38
|
+
return { ...actual, releaseLease: (...args: unknown[]) => releaseLeaseMock(...args) };
|
|
39
|
+
});
|
|
22
40
|
vi.mock("../../config", async () => {
|
|
23
41
|
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
24
42
|
return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
|
|
@@ -288,6 +306,138 @@ describe("runOp dispatcher", () => {
|
|
|
288
306
|
});
|
|
289
307
|
});
|
|
290
308
|
|
|
309
|
+
/**
|
|
310
|
+
* chant #2750 — an Op a declared steward lists is one of its turns, not a
|
|
311
|
+
* run beside one: `chant run <op>` for such an Op takes the steward's turn
|
|
312
|
+
* lease (in the local form) or is redirected to the steward's thread (on
|
|
313
|
+
* fountain), rather than running unaware of a turn already in progress.
|
|
314
|
+
*
|
|
315
|
+
* `acquireStewardTurn` and `releaseLease` are stubbed here (mocked above):
|
|
316
|
+
* the point of these tests is the CLI's own decision and its message, not
|
|
317
|
+
* the lease's git plumbing, which `op/steward.test.ts` and
|
|
318
|
+
* `lifecycle/lease.test.ts` already cover against a real repo.
|
|
319
|
+
*/
|
|
320
|
+
describe("runOp: a steward's turn (#2750)", () => {
|
|
321
|
+
function stewardOwning(opName: string, name: string, form: "local" | "fountain" = "local") {
|
|
322
|
+
return {
|
|
323
|
+
stewards: new Map([[name, {
|
|
324
|
+
declaration: {
|
|
325
|
+
kind: "Chant::Steward", name, ops: [{ name: opName, phases: [] }],
|
|
326
|
+
form: { default: form, environments: {} }, capabilities: [], vault: null,
|
|
327
|
+
},
|
|
328
|
+
filePath: "ops/steward.op.ts", exportName: "steward",
|
|
329
|
+
}]]),
|
|
330
|
+
errors: [], conflicts: [],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
beforeEach(() => {
|
|
335
|
+
discoverOpsMock.mockReset();
|
|
336
|
+
loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
|
|
337
|
+
loadPluginsMock.mockReset().mockResolvedValue([]);
|
|
338
|
+
acquireStewardTurnMock.mockReset();
|
|
339
|
+
releaseLeaseMock.mockReset().mockResolvedValue(true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("local form, the turn is free: takes it, runs, and releases it once the run is over", async () => {
|
|
343
|
+
discoverOpsMock.mockResolvedValue({
|
|
344
|
+
ops: new Map([localOp("release", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
345
|
+
errors: [],
|
|
346
|
+
});
|
|
347
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardOwning("release", "box-steward"));
|
|
348
|
+
acquireStewardTurnMock.mockResolvedValue({
|
|
349
|
+
acquired: true,
|
|
350
|
+
lease: { op: "_turns/box-steward", holder: "h", token: "t1", acquiredAt: "x", expiresAt: "y" },
|
|
351
|
+
});
|
|
352
|
+
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
353
|
+
const exit = await runOp({ args: makeArgs({ path: "release" }), plugins: [], serializers: [] });
|
|
354
|
+
stderrWrite.mockRestore();
|
|
355
|
+
|
|
356
|
+
expect(exit).toBe(0);
|
|
357
|
+
expect(acquireStewardTurnMock).toHaveBeenCalledTimes(1);
|
|
358
|
+
expect(acquireStewardTurnMock.mock.calls[0]?.[0]).toBe("box-steward");
|
|
359
|
+
expect(releaseLeaseMock).toHaveBeenCalledTimes(1);
|
|
360
|
+
expect(releaseLeaseMock).toHaveBeenCalledWith("_turns/box-steward", expect.any(String), "t1");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("local form, a turn is already in progress: refused, naming the steward and holder, nothing runs", async () => {
|
|
364
|
+
discoverOpsMock.mockResolvedValue({
|
|
365
|
+
ops: new Map([localOp("release", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
366
|
+
errors: [],
|
|
367
|
+
});
|
|
368
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardOwning("release", "box-steward"));
|
|
369
|
+
acquireStewardTurnMock.mockResolvedValue({
|
|
370
|
+
acquired: false,
|
|
371
|
+
reason: "held",
|
|
372
|
+
heldBy: { op: "_turns/box-steward", holder: "other-host:123:abcd", token: "t0", acquiredAt: "x", expiresAt: "y" },
|
|
373
|
+
});
|
|
374
|
+
const stderr = makeStderrSpy();
|
|
375
|
+
const exit = await runOp({ args: makeArgs({ path: "release" }), plugins: [], serializers: [] });
|
|
376
|
+
|
|
377
|
+
expect(exit).toBe(1);
|
|
378
|
+
const out = stderr.join("\n");
|
|
379
|
+
expect(out).toContain('steward "box-steward"');
|
|
380
|
+
expect(out).toContain("other-host:123:abcd");
|
|
381
|
+
expect(out).toContain("chant operator status --steward box-steward");
|
|
382
|
+
expect(releaseLeaseMock).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("fountain form without --on fountain: refused, naming the redirect, no turn lease touched", async () => {
|
|
386
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("release", [])]), errors: [] });
|
|
387
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardOwning("release", "box-steward", "fountain"));
|
|
388
|
+
const stderr = makeStderrSpy();
|
|
389
|
+
const exit = await runOp({ args: makeArgs({ path: "release" }), plugins: [], serializers: [] });
|
|
390
|
+
|
|
391
|
+
expect(exit).toBe(1);
|
|
392
|
+
const out = stderr.join("\n");
|
|
393
|
+
expect(out).toContain('steward "box-steward"');
|
|
394
|
+
expect(out).toContain("chant run release --on fountain");
|
|
395
|
+
expect(acquireStewardTurnMock).not.toHaveBeenCalled();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("fountain form with --on fountain: the local turn-lease check is skipped and the fountain runtime runs it", async () => {
|
|
399
|
+
const runtime = {
|
|
400
|
+
name: "fountain",
|
|
401
|
+
start: vi.fn(async (op: { name: string }) => ({
|
|
402
|
+
op: op.name,
|
|
403
|
+
runId: "f-1",
|
|
404
|
+
result: async () => ({ op: op.name, runId: "f-1", state: "completed" as const, startedAt: "t", endedAt: "t" }),
|
|
405
|
+
})),
|
|
406
|
+
status: vi.fn(), log: vi.fn(), list: vi.fn(), cancel: vi.fn(),
|
|
407
|
+
};
|
|
408
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("release", [])]), errors: [] });
|
|
409
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardOwning("release", "box-steward", "fountain"));
|
|
410
|
+
loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["fountain"] } });
|
|
411
|
+
makeStdoutSpy();
|
|
412
|
+
const exit = await runOp({
|
|
413
|
+
args: makeArgs({ path: "release", on: "fountain" }),
|
|
414
|
+
plugins: [{ name: "fountain", opRuntime: runtime } as never], serializers: [],
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(exit).toBe(0);
|
|
418
|
+
expect(runtime.start).toHaveBeenCalledTimes(1);
|
|
419
|
+
expect(acquireStewardTurnMock).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("local form, the turn lease's own acquire throws (a stale .lock): refused as a lease error, not an uncaught exception, and nothing runs", async () => {
|
|
423
|
+
discoverOpsMock.mockResolvedValue({
|
|
424
|
+
ops: new Map([localOp("release", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
|
|
425
|
+
errors: [],
|
|
426
|
+
});
|
|
427
|
+
discoverStewardsMock.mockResolvedValueOnce(stewardOwning("release", "box-steward"));
|
|
428
|
+
acquireStewardTurnMock.mockRejectedValue(new Error("cannot lock ref 'refs/chant/lease/_turns/box-steward': File exists"));
|
|
429
|
+
const stderr = makeStderrSpy();
|
|
430
|
+
|
|
431
|
+
const exit = await runOp({ args: makeArgs({ path: "release" }), plugins: [], serializers: [] });
|
|
432
|
+
|
|
433
|
+
expect(exit).toBe(1);
|
|
434
|
+
const out = stderr.join("\n");
|
|
435
|
+
expect(out).toContain('steward "box-steward"');
|
|
436
|
+
expect(out).toContain("cannot lock ref");
|
|
437
|
+
expect(releaseLeaseMock).not.toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
291
441
|
/**
|
|
292
442
|
* chant #2243 — a gated run's exit code is the one thing `--gated-exit`
|
|
293
443
|
* remaps, and the gate is reported where CI can see it without opening a log.
|
|
@@ -373,7 +523,9 @@ describe("runOp: --gated-exit (#2243)", () => {
|
|
|
373
523
|
|
|
374
524
|
test("a run that fails for any other reason is still red under the flag", async () => {
|
|
375
525
|
discoverOpsMock.mockResolvedValue({
|
|
376
|
-
|
|
526
|
+
// atMostOnce: one attempt. The default profile retries twice with 5s and
|
|
527
|
+
// 10s backoff, which put this test at 15s for waits it does not test (#2817).
|
|
528
|
+
ops: new Map([localOp("broken", [{ kind: "activity", fn: "shellCmd", profile: "atMostOnce", args: { cmd: "exit 7" } }])]),
|
|
377
529
|
errors: [],
|
|
378
530
|
});
|
|
379
531
|
const file = summaryFile();
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -11,12 +11,16 @@ import { approveCommand, describeGateMismatch } from "../../op/gate";
|
|
|
11
11
|
import { summaryLedgerPrefix, writeGatedRunSummary, type GatedRunSummary } from "../../op/gate-summary";
|
|
12
12
|
import { createLocalOpRuntime } from "../../op/runtimes/local";
|
|
13
13
|
import type { OpRuntimeProvider, OpRunStatus } from "../../op/runtime";
|
|
14
|
-
import { renderHuman, renderJson } from "../../op/local-output";
|
|
14
|
+
import { pointAnswerCommand, renderHuman, renderJson } from "../../op/local-output";
|
|
15
15
|
import { loadPlugins } from "../plugins";
|
|
16
16
|
import { recordGateApproval } from "./operator";
|
|
17
17
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
18
18
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
19
19
|
import type { CommandContext } from "../registry";
|
|
20
|
+
import { stewardFormFor, stewardTurnLeaseName, DEFAULT_STEWARD_ENV, type StewardDeclaration } from "../../op/steward";
|
|
21
|
+
import { acquireStewardTurn, STEWARD_TURN_WAIT_MS } from "../../op/operator";
|
|
22
|
+
import { releaseLease, currentHolderId, type AcquireLeaseResult } from "../../lifecycle/lease";
|
|
23
|
+
import { StaleLockError } from "../../lifecycle/git";
|
|
20
24
|
import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
|
|
21
25
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
22
26
|
import { maybeRecordAutoRelease } from "../../components/auto-release";
|
|
@@ -527,6 +531,94 @@ function refusesPolicyGateUnderSandbox(ctx: CommandContext, config: OpConfig, op
|
|
|
527
531
|
return true;
|
|
528
532
|
}
|
|
529
533
|
|
|
534
|
+
/** What `stewardTurnGate` decided before `chant run <op>` is allowed to start. */
|
|
535
|
+
type StewardTurnGate =
|
|
536
|
+
| { ok: true; release?: () => Promise<void> }
|
|
537
|
+
| { ok: false; message: string; hint: string };
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* `chant run <op>` for an Op a declared steward lists is one of that
|
|
541
|
+
* steward's turns, not a run beside it (#2750, follow-up to #2731).
|
|
542
|
+
*
|
|
543
|
+
* On fountain, the run belongs on the steward's thread: without `--on
|
|
544
|
+
* fountain` this refuses outright, naming the command that posts it there
|
|
545
|
+
* instead (the fountain runtime's own turn already refuses a busy teammate,
|
|
546
|
+
* unretried — see chant-fountain-ops).
|
|
547
|
+
*
|
|
548
|
+
* In the local form, the run takes the steward's turn lease
|
|
549
|
+
* (`../../op/operator.ts`'s `acquireStewardTurn`) — the same lease a round's
|
|
550
|
+
* scheduled tick holds for the length of its own run. Held already, this
|
|
551
|
+
* waits up to {@link STEWARD_TURN_WAIT_MS} (long enough to ride out a race
|
|
552
|
+
* with a turn that's just ending) and then refuses, naming the steward and
|
|
553
|
+
* its holder. It never queues past that: a round's own leases skip and
|
|
554
|
+
* report rather than wait (`../../op/operator.ts`'s module doc), and a
|
|
555
|
+
* foreground CLI blocked with no visibility for however long a scheduled
|
|
556
|
+
* turn takes would be a worse answer than a clear refusal with a retry in
|
|
557
|
+
* hand. Ctrl-C already means "abort my own run" here; it must not also mean
|
|
558
|
+
* "give up waiting for someone else's turn".
|
|
559
|
+
*
|
|
560
|
+
* `undefined` (via the caller checking `owner`) when no steward lists this
|
|
561
|
+
* op — the ordinary, untouched path.
|
|
562
|
+
*/
|
|
563
|
+
async function stewardTurnGate(opName: string, ctx: CommandContext): Promise<StewardTurnGate | undefined> {
|
|
564
|
+
const { stewards } = await discoverStewards().catch(() => ({ stewards: new Map<string, { declaration: StewardDeclaration }>() }));
|
|
565
|
+
let owner: StewardDeclaration | undefined;
|
|
566
|
+
for (const { declaration } of stewards.values()) {
|
|
567
|
+
if (declaration.ops.some((op) => op.name === opName)) {
|
|
568
|
+
owner = declaration;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (!owner) return undefined;
|
|
573
|
+
|
|
574
|
+
const env = ctx.args.env ?? DEFAULT_STEWARD_ENV;
|
|
575
|
+
const form = stewardFormFor(owner, env);
|
|
576
|
+
|
|
577
|
+
if (form === "fountain") {
|
|
578
|
+
if (ctx.args.on === "fountain") return { ok: true };
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
message: `Op "${opName}" is one of steward "${owner.name}"'s Ops, which runs on fountain in environment "${env}"`,
|
|
582
|
+
hint:
|
|
583
|
+
`Run \`chant run ${opName} --on fountain\` so it posts to "${owner.name}"'s thread. ` +
|
|
584
|
+
`A plain \`chant run ${opName}\` would run it outside that thread, beside the steward instead of one of its turns.`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const holder = currentHolderId();
|
|
589
|
+
let turn: AcquireLeaseResult;
|
|
590
|
+
try {
|
|
591
|
+
turn = await acquireStewardTurn(owner.name, holder, { waitMs: STEWARD_TURN_WAIT_MS });
|
|
592
|
+
} catch (err) {
|
|
593
|
+
// Not turn contention — the acquire attempt itself failed (most likely a
|
|
594
|
+
// StaleLockError, a `.lock` left behind by a killed operator process).
|
|
595
|
+
// Report it as a refusal rather than letting it propagate uncaught out
|
|
596
|
+
// of `runOpOnRuntime`, before that function's own try/catch even starts.
|
|
597
|
+
return {
|
|
598
|
+
ok: false,
|
|
599
|
+
message: `Op "${opName}" is one of steward "${owner.name}"'s turns, and its lease could not be read`,
|
|
600
|
+
hint:
|
|
601
|
+
`${err instanceof StaleLockError ? err.message : err instanceof Error ? err.message : String(err)} ` +
|
|
602
|
+
`This can happen when an operator process was killed mid-write; check refs/chant/lease/_turns/${owner.name} for a stale lock.`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (!turn.acquired) {
|
|
606
|
+
const heldBy = turn.heldBy?.holder;
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
message:
|
|
610
|
+
`Op "${opName}" is one of steward "${owner.name}"'s turns, and a turn is in progress` +
|
|
611
|
+
(heldBy ? ` (held by ${heldBy})` : ""),
|
|
612
|
+
hint:
|
|
613
|
+
`Wait for the turn to finish and re-run \`chant run ${opName}\`, or check \`chant operator status --steward ${owner.name}\`. ` +
|
|
614
|
+
`If it looks stuck, stop the operator process holding it (Ctrl-C, or kill it) — its lease also expires on its own.`,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const lease = turn.lease!;
|
|
618
|
+
const stewardName = owner.name;
|
|
619
|
+
return { ok: true, release: async () => { await releaseLease(stewardTurnLeaseName(stewardName), holder, lease.token).catch(() => false); } };
|
|
620
|
+
}
|
|
621
|
+
|
|
530
622
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
531
623
|
if (ctx.args.generate) {
|
|
532
624
|
if (ctx.args.components) {
|
|
@@ -864,8 +956,20 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
864
956
|
// Pre-flight: --sandbox cannot cover a policyGate step (#2003).
|
|
865
957
|
if (refusesPolicyGateUnderSandbox(ctx, config, opName)) return 1;
|
|
866
958
|
|
|
959
|
+
// A hand run of an Op a declared steward lists is one of that steward's
|
|
960
|
+
// turns, not a run beside it (#2750). `undefined` when no steward lists
|
|
961
|
+
// this op at all — the ordinary path.
|
|
962
|
+
const stewardGate = await stewardTurnGate(opName, ctx);
|
|
963
|
+
if (stewardGate && !stewardGate.ok) {
|
|
964
|
+
console.error(formatError({ message: stewardGate.message, hint: stewardGate.hint }));
|
|
965
|
+
return 1;
|
|
966
|
+
}
|
|
967
|
+
|
|
867
968
|
const runtime = await resolveOpRuntime(ctx);
|
|
868
|
-
if (!runtime)
|
|
969
|
+
if (!runtime) {
|
|
970
|
+
await stewardGate?.release?.();
|
|
971
|
+
return 1;
|
|
972
|
+
}
|
|
869
973
|
|
|
870
974
|
// The work lease (#2748). `--work` names the item an Op with a work lease
|
|
871
975
|
// runs under; it is taken where the run executes, which for a hosted
|
|
@@ -940,6 +1044,18 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
940
1044
|
}
|
|
941
1045
|
return gatedExit;
|
|
942
1046
|
}
|
|
1047
|
+
// A run waiting on an open decision point (#2749) is waiting on a person,
|
|
1048
|
+
// like a gated one, and exits with the same code. Its answer comes through
|
|
1049
|
+
// hud or `points answer`, never a `chant approve`.
|
|
1050
|
+
if (status.state === "waiting") {
|
|
1051
|
+
if (!status.result && status.point) {
|
|
1052
|
+
console.error(formatWarning({
|
|
1053
|
+
message: `Op "${opName}" is waiting on decision point "${status.point.point}" (${status.point.id})`,
|
|
1054
|
+
hint: `A person answers it with: ${pointAnswerCommand(status.point.id)}`,
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
return gatedExit;
|
|
1058
|
+
}
|
|
943
1059
|
return status.state === "completed" ? 0 : 1;
|
|
944
1060
|
} catch (err) {
|
|
945
1061
|
if (err instanceof OpRunFailure) {
|
|
@@ -950,6 +1066,9 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
950
1066
|
return 1;
|
|
951
1067
|
} finally {
|
|
952
1068
|
process.removeListener("SIGINT", onSigint);
|
|
1069
|
+
// The turn is over, win or lose — free it so the steward's next round,
|
|
1070
|
+
// or the next hand run, doesn't wait out this run's own TTL (#2750).
|
|
1071
|
+
await stewardGate?.release?.();
|
|
953
1072
|
}
|
|
954
1073
|
}
|
|
955
1074
|
|
package/src/cli/main.test.ts
CHANGED
|
@@ -727,6 +727,16 @@ describe("workspace graph --intent (#2651)", () => {
|
|
|
727
727
|
});
|
|
728
728
|
});
|
|
729
729
|
|
|
730
|
+
describe("workspace check --changes (#2773)", () => {
|
|
731
|
+
test("parses the range, the work item and the severity", () => {
|
|
732
|
+
const args = parseArgs(["workspace", "check", "--changes", "main..HEAD", "--work", "W-001", "--severity", "fail", "--json"]);
|
|
733
|
+
expect(args).toMatchObject({ command: "workspace", path: "check", changes: "main..HEAD", work: "W-001", severity: "fail", json: true });
|
|
734
|
+
expect(() => parseArgs(["workspace", "check", "--changes"])).toThrow(/--changes needs a range/);
|
|
735
|
+
expect(() => parseArgs(["workspace", "check", "--changes", "main", "--severity"])).toThrow(/--severity needs off, warn or fail/);
|
|
736
|
+
expect(resolveCommand(args, commandRegistry)?.def.name).toBe("workspace check");
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
730
740
|
describe("workspace init and ls (#2534)", () => {
|
|
731
741
|
test("resolve to their commands in the one workspace group, with the directory as extra positional", () => {
|
|
732
742
|
const ls = parseArgs(["workspace", "ls", "examples", "--at", "HEAD", "--json"]);
|
package/src/cli/main.ts
CHANGED
|
@@ -379,6 +379,14 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
379
379
|
// `chant workspace graph --intent <path[:start-end]>` (#2651)
|
|
380
380
|
result.intent = args[++i];
|
|
381
381
|
if (!result.intent || result.intent.startsWith("-")) throw new Error("--intent needs a region: --intent <path[:start-end]>");
|
|
382
|
+
} else if (arg === "--changes") {
|
|
383
|
+
// `chant workspace check --changes <base>..<head>` (#2773)
|
|
384
|
+
result.changes = args[++i];
|
|
385
|
+
if (!result.changes || result.changes.startsWith("-")) throw new Error("--changes needs a range: --changes <base>..<head>");
|
|
386
|
+
} else if (arg === "--severity") {
|
|
387
|
+
// `chant workspace check --changes <range> --severity off|warn|fail` (#2773)
|
|
388
|
+
result.severity = args[++i];
|
|
389
|
+
if (!result.severity || result.severity.startsWith("-")) throw new Error("--severity needs off, warn or fail");
|
|
382
390
|
} else if (arg === "--current") {
|
|
383
391
|
result.current = true;
|
|
384
392
|
} else if (arg === "--set") {
|
|
@@ -858,6 +866,10 @@ Workspace (level 1, #2524):
|
|
|
858
866
|
separate clones coordinate. A claim is refused, exit 2,
|
|
859
867
|
while anyone holds it live. Each change appends to
|
|
860
868
|
_leases/<id>.jsonl on chant/lifecycle
|
|
869
|
+
workspace work history <id> [--kind <kind file>] [--json]
|
|
870
|
+
A work item's lease history: each claim with its token
|
|
871
|
+
and holder, and how it ended, released with an outcome
|
|
872
|
+
or never released (expired or lost). Read-only
|
|
861
873
|
workspace points [--open] [--kind <kind file>] [--at <rev>] [--json]
|
|
862
874
|
List the decision points the declared answer kinds'
|
|
863
875
|
points files declare, and the questions asked of them;
|
|
@@ -896,6 +908,13 @@ Workspace (level 1, #2524):
|
|
|
896
908
|
whose pinned files changed. Needs no workspace file.
|
|
897
909
|
--at reads a commit's git objects; --format json
|
|
898
910
|
prints the read-contract document
|
|
911
|
+
workspace check --changes <base>..<head> [--work <id>] [--severity off|warn|fail] [--kind <kind file>...] [--json]
|
|
912
|
+
Map each path the diff changes to the current records
|
|
913
|
+
whose constrains cover it: change-uncovered when none
|
|
914
|
+
does, change-out-of-scope when a record in hand (the
|
|
915
|
+
--work item and its decisions) lists it in
|
|
916
|
+
out_of_scope. The declaration's changes block sets
|
|
917
|
+
the severity (warn by default) and ignore globs
|
|
899
918
|
workspace build [dir] [--member <name>] [-o <dir>] [--dry-run]
|
|
900
919
|
Build every chant member and example project, each with
|
|
901
920
|
its own chant, one process per toolchain. -o <dir>
|
|
@@ -1265,9 +1284,10 @@ async function loadPluginsOrExit(path: string): Promise<import("../lexicon").Lex
|
|
|
1265
1284
|
* #2700 — whether `path` is the root of a declared workspace and holds no
|
|
1266
1285
|
* lexicon of its own: no `lexicons` in a root config, and no lexicon import in
|
|
1267
1286
|
* the root's source, which leaves the members' directories out (#2527). A
|
|
1268
|
-
* generated
|
|
1269
|
-
* `chant serve mcp` starts there instead of
|
|
1270
|
-
* detected", and serves core with the chant
|
|
1287
|
+
* generated app repo can be this shape: its lexicons are all under a
|
|
1288
|
+
* delivery directory, not the root. `chant serve mcp` starts there instead of
|
|
1289
|
+
* refusing with "No lexicon detected", and serves core with the chant
|
|
1290
|
+
* members' lexicons.
|
|
1271
1291
|
*
|
|
1272
1292
|
* A directory with no workspace declaration costs only `findWorkspaceRoot`'s
|
|
1273
1293
|
* existence checks, so a level-0 project reaches `loadPluginsOrExit` as before.
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* #2700 — the lexicons `chant serve mcp` loads at the root of a declared
|
|
3
3
|
* workspace that has no lexicon of its own.
|
|
4
4
|
*
|
|
5
|
-
* Such a root (a generated
|
|
6
|
-
*
|
|
7
|
-
* there got none of chant's tools. The server now starts with core's
|
|
8
|
-
* resources, and with the lexicons of the workspace's members of
|
|
9
|
-
* each read from that member's own config the way `chant build`
|
|
10
|
-
* member's directory reads it.
|
|
5
|
+
* Such a root (a generated app repo, say, whose lexicons are all under a
|
|
6
|
+
* delivery directory) used to refuse with "No lexicon detected", so an agent
|
|
7
|
+
* started there got none of chant's tools. The server now starts with core's
|
|
8
|
+
* tools and resources, and with the lexicons of the workspace's members of
|
|
9
|
+
* kind chant, each read from that member's own config the way `chant build`
|
|
10
|
+
* in the member's directory reads it.
|
|
11
11
|
*
|
|
12
12
|
* Loading is best effort, one member and one lexicon at a time: a member whose
|
|
13
13
|
* config does not load, or a lexicon this chant cannot import, is left out and
|
|
@@ -85,7 +85,7 @@ describe("which servers have the workspace tools", () => {
|
|
|
85
85
|
const { tools } = (await rpc(server(cwd), "tools/list")) as { tools: { name: string }[] };
|
|
86
86
|
expect(tools.map((t) => t.name)).toEqual(expect.arrayContaining(names));
|
|
87
87
|
}
|
|
88
|
-
expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-records", "workspace-points", "records-new", "records-amend", "records-review", "records-close", "points-answer"]);
|
|
88
|
+
expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-work-history", "records-new", "records-amend", "records-review", "records-close", "points-answer"]);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
test("a server outside any workspace, or given none, does not", async () => {
|
|
@@ -100,8 +100,8 @@ describe("which servers have the workspace tools", () => {
|
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test("the descriptions say records are proposals and by names who decided", () => {
|
|
104
|
-
for (const t of workspaceWriteTools) expect(t.description).toMatch(/proposals until they are reviewed.*by must name the person or agent that actually decided/s);
|
|
103
|
+
test("the descriptions say records are proposals and by names who proposed or decided", () => {
|
|
104
|
+
for (const t of workspaceWriteTools) expect(t.description).toMatch(/proposals until they are reviewed.*by must name the person or agent that actually proposed or decided it/s);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
107
|
|
|
@@ -128,6 +128,28 @@ describe("reads", () => {
|
|
|
128
128
|
expect(bad.isError).toBe(true);
|
|
129
129
|
}, 120_000);
|
|
130
130
|
|
|
131
|
+
test("workspace-work-history returns the document chant workspace work history --json prints (#2785)", async () => {
|
|
132
|
+
const s = server();
|
|
133
|
+
const res = await call(s, "workspace-work-history", { item: "W-1" });
|
|
134
|
+
expect(res.isError).toBeUndefined();
|
|
135
|
+
expect(res.structuredContent).toEqual(cli(["workspace", "work", "history", "W-1", "--json"]));
|
|
136
|
+
expect(res.structuredContent).toMatchObject({ contract: 1, error: { code: expect.any(String) } });
|
|
137
|
+
const bad = await call(s, "workspace-work-history", {});
|
|
138
|
+
expect(bad.isError).toBe(true);
|
|
139
|
+
}, 120_000);
|
|
140
|
+
|
|
141
|
+
test("workspace-changes returns the document chant workspace check --changes --json prints (#2773)", async () => {
|
|
142
|
+
const s = server();
|
|
143
|
+
const res = await call(s, "workspace-changes", { range: "HEAD", severity: "fail" });
|
|
144
|
+
expect(res.isError).toBeUndefined();
|
|
145
|
+
expect(res.structuredContent).toEqual(cli(["workspace", "check", "--changes", "HEAD", "--severity", "fail", "--json"]));
|
|
146
|
+
expect(res.structuredContent).toMatchObject({ severity: "fail", paths: [], findings: [], ok: true });
|
|
147
|
+
const unknown = await call(s, "workspace-changes", { range: "HEAD", work: "W-999" });
|
|
148
|
+
expect(unknown.structuredContent).toMatchObject({ error: { code: "work-item-unknown" } });
|
|
149
|
+
const bad = await call(s, "workspace-changes", { range: "HEAD", severity: "loud" });
|
|
150
|
+
expect(bad.isError).toBe(true);
|
|
151
|
+
}, 120_000);
|
|
152
|
+
|
|
131
153
|
test("an error document is returned as a document, and a flag-shaped value is refused before chant runs", async () => {
|
|
132
154
|
const s = server();
|
|
133
155
|
const missing = await call(s, "workspace-records", { kind: "nowhere/none.kind.mjs" });
|
|
@@ -143,6 +165,7 @@ describe("reads", () => {
|
|
|
143
165
|
expect(mcpToolCall(["workspace", "graph", "--intent", "a.mjs:1", "--kind", KIND, "--json"])).toEqual({ name: "workspace-graph", arguments: { intent: "a.mjs:1", kind: [KIND] } });
|
|
144
166
|
expect(mcpToolCall(["workspace", "graph", "--composites", "--json"])).toEqual({ name: "workspace-graph", arguments: { composites: true } });
|
|
145
167
|
expect(mcpToolCall(["workspace", "check", "--format", "json"])).toBeUndefined();
|
|
168
|
+
expect(mcpToolCall(["workspace", "check", "--changes", "main..HEAD", "--work", "W-001", "--json"])).toEqual({ name: "workspace-changes", arguments: { range: "main..HEAD", work: "W-001" } });
|
|
146
169
|
});
|
|
147
170
|
});
|
|
148
171
|
|
|
@@ -186,25 +209,18 @@ describe("writes", () => {
|
|
|
186
209
|
const reasoning = await call(s, "records-amend", { kind: KIND, id: "fix-001", fields: { question: "Something else?" } });
|
|
187
210
|
expect(reasoning.structuredContent).toMatchObject({ error: { code: "amend-supersede-instead" } });
|
|
188
211
|
|
|
189
|
-
|
|
212
|
+
// A new record opens proposed (MCP forces it), so by names the proposer, in proposedBy's field, not the decider (#2756).
|
|
213
|
+
const both = await call(s, "records-new", { kind: KIND, record: { ...proposal("Two proposers"), proposed_by: "alice" }, by: "bob", dryRun: true });
|
|
190
214
|
expect(both.structuredContent).toMatchObject({ error: { code: "write-input-invalid" } });
|
|
191
|
-
const by = await call(s, "records-new", { kind: KIND, record: proposal("One
|
|
215
|
+
const by = await call(s, "records-new", { kind: KIND, record: proposal("One proposer"), by: "alice", dryRun: true });
|
|
192
216
|
expect(by.structuredContent, JSON.stringify(by.structuredContent)).toHaveProperty("text");
|
|
193
|
-
expect((by.structuredContent as { text: string }).text).toContain('
|
|
217
|
+
expect((by.structuredContent as { text: string }).text).toContain('proposed_by: "alice"');
|
|
218
|
+
expect((by.structuredContent as { text: string }).text).toContain("decided_by: null");
|
|
219
|
+
|
|
220
|
+
// --sign seals the decider, and a record by names as proposed carries no decided_by yet.
|
|
221
|
+
const signed = await call(s, "records-new", { kind: KIND, record: proposal("Signed"), by: "alice", sign: true, dryRun: true });
|
|
222
|
+
expect(signed.structuredContent).toMatchObject({ error: { code: "record-sign-failed", message: expect.stringContaining("names no decided_by") } });
|
|
194
223
|
|
|
195
|
-
// No key configured on this host: the CLI's refusal and remedy.
|
|
196
|
-
const home = { GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1" };
|
|
197
|
-
const saved = { ...process.env };
|
|
198
|
-
Object.assign(process.env, home);
|
|
199
|
-
try {
|
|
200
|
-
const signed = await call(s, "records-new", { kind: KIND, record: proposal("Signed"), by: "alice", sign: true, dryRun: true });
|
|
201
|
-
expect(signed.structuredContent).toMatchObject({ error: { code: "record-sign-failed", message: expect.stringContaining("pass --sign <key file>") } });
|
|
202
|
-
} finally {
|
|
203
|
-
for (const k of Object.keys(home)) {
|
|
204
|
-
if (saved[k] === undefined) delete process.env[k];
|
|
205
|
-
else process.env[k] = saved[k];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
224
|
// Nothing above wrote a file.
|
|
209
225
|
expect(readFileSync(join(ws.dir, "decisions", "fix-001-how-the-app-is-deployed.md"), "utf-8")).toContain('question: "What declares the app\'s deployment?"');
|
|
210
226
|
}, 180_000);
|