@intentius/chant 0.91.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/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-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/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/boxes.d.ts +34 -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 +3 -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/declaration.d.ts +52 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +114 -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.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.map +1 -1
- package/dist/workspace/points.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts +14 -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 +59 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +16 -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 +19 -0
- 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/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 +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 +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 +313 -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/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 +4 -2
- package/src/workspace/check.schema.json +15 -5
- package/src/workspace/checks/boxes.test.ts +80 -1
- package/src/workspace/checks/boxes.ts +136 -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 +8 -1
- 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/declaration.schema.json +114 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +104 -0
- package/src/workspace/declared-kinds.test.ts +5 -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.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 +14 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +33 -0
- package/src/workspace/points.test.ts +1 -1
- package/src/workspace/points.ts +1 -1
- package/src/workspace/read-contract.test.ts +1 -1
- package/src/workspace/reason-codes.test.ts +11 -1
- package/src/workspace/reason-codes.ts +18 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +13 -3
- package/src/workspace/records-close.schema.json +2 -0
- 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 +36 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +13 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +108 -4
- package/src/workspace/status.schema.json +111 -7
- package/src/workspace/status.ts +36 -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.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
package/src/op/steward.test.ts
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* scheduled Ops under its own lease, and the rule that a steward turn and a
|
|
5
5
|
* coding agent share one checkout without colliding.
|
|
6
6
|
*/
|
|
7
|
-
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { describe, test, expect, vi } from "vitest";
|
|
8
8
|
import { withTestDir } from "@intentius/chant-test-utils";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
13
13
|
import type { OpConfig } from "./types";
|
|
14
|
-
import { declareSteward, isStewardDeclaration, pickSteward, stewardFormFor, stewardLeaseName } from "./steward";
|
|
14
|
+
import { declareSteward, isStewardDeclaration, pickSteward, stewardFormFor, stewardLeaseName, stewardTurnLeaseName } from "./steward";
|
|
15
15
|
import { discoverOps, discoverStewards } from "./discover";
|
|
16
|
-
import { runOperatorRound, acquireStewardLease, formatRoundLine } from "./operator";
|
|
16
|
+
import { runOperatorRound, acquireStewardLease, acquireStewardTurn, formatRoundLine } from "./operator";
|
|
17
17
|
import { readLease } from "../lifecycle/lease";
|
|
18
18
|
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
19
19
|
|
|
@@ -210,3 +210,87 @@ describe("a local steward round", () => {
|
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
describe("a hand run waits its turn (#2750)", () => {
|
|
215
|
+
test("a round holds the turn lease for exactly the length of its tick — a concurrent hand run sees it held, then free again once the tick ends", async () => {
|
|
216
|
+
await withTestDir(async (dir) => {
|
|
217
|
+
await initRepo(dir);
|
|
218
|
+
let releaseTurn: () => void = () => {};
|
|
219
|
+
const gate = new Promise<void>((resolve) => { releaseTurn = resolve; });
|
|
220
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *", "slowTurn")] });
|
|
221
|
+
const activities = new Map<string, ActivityFn>([
|
|
222
|
+
["slowTurn", async () => { await gate; return { ok: true }; }],
|
|
223
|
+
]);
|
|
224
|
+
// Real time on both sides here, deliberately: a fixed `now` on only one
|
|
225
|
+
// of the round's and the hand run's leases would make one see the
|
|
226
|
+
// other's lease as already expired by wall-clock time, which is not
|
|
227
|
+
// the race this test is about.
|
|
228
|
+
const roundPromise = runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities, profiles: PROFILES });
|
|
229
|
+
|
|
230
|
+
// Wait for the round to actually be mid-tick, holding the turn lease.
|
|
231
|
+
await vi.waitFor(async () => {
|
|
232
|
+
const turn = await readLease(stewardTurnLeaseName("box-steward"), { cwd: dir });
|
|
233
|
+
expect(turn.record?.holder).toBe("steward-proc");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// A hand run's own attempt (no `waitMs`) sees it taken and gives up
|
|
237
|
+
// without retrying — this is what `chant run <op>` names in its refusal.
|
|
238
|
+
const midTurn = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
|
|
239
|
+
expect(midTurn.acquired).toBe(false);
|
|
240
|
+
expect(midTurn.heldBy?.holder).toBe("steward-proc");
|
|
241
|
+
|
|
242
|
+
releaseTurn();
|
|
243
|
+
const events = await roundPromise;
|
|
244
|
+
expect(events.map((e) => e.kind)).toEqual(["ticked"]);
|
|
245
|
+
|
|
246
|
+
// The tick released the turn the moment it finished — succeeds again
|
|
247
|
+
// right away, not only once the whole steward process stops.
|
|
248
|
+
const afterTurn = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
|
|
249
|
+
expect(afterTurn.acquired).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a hand run's turn blocks a round: it reports turn-busy and ticks nothing, and releases the op lease it had just taken", async () => {
|
|
254
|
+
await withTestDir(async (dir) => {
|
|
255
|
+
await initRepo(dir);
|
|
256
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
|
|
257
|
+
const held = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
|
|
258
|
+
expect(held.acquired).toBe(true);
|
|
259
|
+
|
|
260
|
+
const log: string[] = [];
|
|
261
|
+
const now = () => new Date(2026, 8, 25, 10, 1, 0);
|
|
262
|
+
const events = await runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities: turns(log), profiles: PROFILES, now });
|
|
263
|
+
|
|
264
|
+
expect(events).toEqual([{ kind: "turn-busy", op: "box-converge", env: "local", steward: "box-steward", heldBy: "hand-run" }]);
|
|
265
|
+
expect(formatRoundLine(events[0])).toContain("turn-held:hand-run");
|
|
266
|
+
expect(log).toEqual([]);
|
|
267
|
+
// The op's own lease, freshly acquired before the turn check, was let go again.
|
|
268
|
+
expect((await readLease("box-converge", { cwd: dir })).record).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("a stale .lock on the turn ref reports lease-error, never a thrown exception out of the round, and still releases the op lease it had just taken (#1959 finding 2, applied to #2750's turn lease)", async () => {
|
|
273
|
+
await withTestDir(async (dir) => {
|
|
274
|
+
await initRepo(dir);
|
|
275
|
+
const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
|
|
276
|
+
|
|
277
|
+
// A previous holder of the turn lease was killed mid-write — its
|
|
278
|
+
// `.lock` file is still sitting there, blocking every future write to
|
|
279
|
+
// `_turns/box-steward`, the same way #1959 finding 2 covers the
|
|
280
|
+
// per-op lease.
|
|
281
|
+
mkdirSync(join(dir, ".git", "refs", "chant", "lease", "_turns"), { recursive: true });
|
|
282
|
+
writeFileSync(join(dir, ".git", "refs", "chant", "lease", "_turns", "box-steward.lock"), "");
|
|
283
|
+
|
|
284
|
+
const log: string[] = [];
|
|
285
|
+
const now = () => new Date(2026, 8, 25, 10, 1, 0);
|
|
286
|
+
const events = await runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities: turns(log), profiles: PROFILES, now });
|
|
287
|
+
|
|
288
|
+
expect(events).toHaveLength(1);
|
|
289
|
+
expect(events[0]).toMatchObject({ kind: "lease-error", op: "_turns/box-steward", env: "local" });
|
|
290
|
+
expect((events[0] as { error: string }).error).toContain("box-steward.lock");
|
|
291
|
+
expect(log).toEqual([]);
|
|
292
|
+
// The op's own lease, freshly acquired before the failed turn attempt, was let go again.
|
|
293
|
+
expect((await readLease("box-converge", { cwd: dir })).record).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
package/src/op/steward.ts
CHANGED
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
* ## What a steward writes
|
|
42
42
|
*
|
|
43
43
|
* A steward's writes are chant writes: run, converge and gate records on the
|
|
44
|
-
* `chant/lifecycle` branch
|
|
44
|
+
* `chant/lifecycle` branch, the answer records of the decision points its
|
|
45
|
+
* Ops ask (`_answers/`, #2786), and lease refs under `refs/chant/lease/`, all made
|
|
45
46
|
* with git plumbing (`../lifecycle/git.ts`: `hash-object`, `mktree`,
|
|
46
47
|
* `commit-tree`, `update-ref`). None of them reads or writes the checkout's
|
|
47
48
|
* index or working tree, so a coding agent editing the app in the same
|
|
@@ -233,6 +234,29 @@ export function stewardLeaseName(steward: string): string {
|
|
|
233
234
|
return `_stewards/${steward}`;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
/**
|
|
238
|
+
* The lease a local steward holds for as long as one of its Ops is actually
|
|
239
|
+
* running: one turn (#2750). Distinct from {@link stewardLeaseName}, which one
|
|
240
|
+
* `chant operator --steward` process holds for its whole life (renewed
|
|
241
|
+
* between ticks, released only when the process stops) — this one is taken
|
|
242
|
+
* right before a run starts and released right after it ends, whether the
|
|
243
|
+
* run is a round's scheduled tick or `chant run <op>` typed by hand. That is
|
|
244
|
+
* what makes an on-request run wait its turn behind a turn already in
|
|
245
|
+
* progress, and succeed again the moment that turn ends, rather than only
|
|
246
|
+
* once the whole steward process stops.
|
|
247
|
+
*
|
|
248
|
+
* `_turns/<name>`, not `_stewards/<name>/turn`: a git ref name is a path
|
|
249
|
+
* component in the loose-refs tree, and `refs/chant/lease/_stewards/<name>`
|
|
250
|
+
* (the lease {@link stewardLeaseName} names) already exists as a *leaf* —
|
|
251
|
+
* git refuses to also create anything *under* it (`fatal: ... exists;
|
|
252
|
+
* cannot create ...`), so nesting the turn lease inside the steward lease's
|
|
253
|
+
* own name is unusable the moment both are live at once. A sibling
|
|
254
|
+
* top-level segment sidesteps that entirely.
|
|
255
|
+
*/
|
|
256
|
+
export function stewardTurnLeaseName(steward: string): string {
|
|
257
|
+
return `_turns/${steward}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
236
260
|
/**
|
|
237
261
|
* Pick the steward `--steward [<name>]` names, or the project's only one.
|
|
238
262
|
* Returns the steward, or the message to refuse with.
|
package/src/op/types.ts
CHANGED
|
@@ -153,6 +153,13 @@ export interface ActivityStep {
|
|
|
153
153
|
* Default: "fastIdempotent"
|
|
154
154
|
*/
|
|
155
155
|
profile?: ActivityProfileName;
|
|
156
|
+
/**
|
|
157
|
+
* How long one attempt may run, such as `"45m"`, in place of the
|
|
158
|
+
* profile's timeout (#2787). The profile's retry policy is kept, so a
|
|
159
|
+
* `shell` step stays one attempt. At most `MAX_STEP_TIMEOUT` (six hours); a longer
|
|
160
|
+
* wait is a gate, not a step.
|
|
161
|
+
*/
|
|
162
|
+
timeout?: string;
|
|
156
163
|
/**
|
|
157
164
|
* Surface this activity's return value as a named run outcome.
|
|
158
165
|
*
|
|
@@ -14,7 +14,8 @@ import type { OpConfig, StepDefinition } from "./types";
|
|
|
14
14
|
import { runOpLocally, OpRunFailure, type OpRunResult } from "./local-executor";
|
|
15
15
|
import { workLeaseOutput, workLeaseProblems, stewardWorkHolder, WORK_LEASE_STEP_ID } from "./work-lease-run";
|
|
16
16
|
import { declareSteward } from "./steward";
|
|
17
|
-
import { Op } from "./builders";
|
|
17
|
+
import { Op, shell } from "./builders";
|
|
18
|
+
import { shellCmd } from "./activities/shell";
|
|
18
19
|
import { WORK_LEASE_ITEM_PATTERN } from "./work-lease-decl";
|
|
19
20
|
import { runOperatorRound } from "./operator";
|
|
20
21
|
import { stepOutput, validateStepOutputRefScope } from "./step-output-ref";
|
|
@@ -164,6 +165,24 @@ describe("a run under a work lease", () => {
|
|
|
164
165
|
});
|
|
165
166
|
});
|
|
166
167
|
|
|
168
|
+
test("a shell pick step that prints JSON hands the lease its candidates, and the next is claimed when the first is held (#2787)", async () => {
|
|
169
|
+
await withTestDir(async (dir) => {
|
|
170
|
+
await initRepo(dir);
|
|
171
|
+
await claimWorkLease("W-4", "other", { cwd: dir, ttlMs: 60_000 });
|
|
172
|
+
const pick = shell(`echo '["W-4","W-5"]'`, { id: "pick", json: true });
|
|
173
|
+
const op = dispatchOp({ workLease: { item: pick.out.json } }, [pick, step("build", { item: workLeaseOutput("item") })]);
|
|
174
|
+
const activities = new Map<string, ActivityFn>([
|
|
175
|
+
["shellCmd", shellCmd as unknown as ActivityFn],
|
|
176
|
+
["build", async () => ({})],
|
|
177
|
+
]);
|
|
178
|
+
const result = await run(op, activities, dir);
|
|
179
|
+
expect(result.status).toBe("ok");
|
|
180
|
+
expect(result.records.map((r) => r.fn)).toEqual(["shellCmd", "workLease:claim", "build"]);
|
|
181
|
+
expect(result.records[2].args).toEqual({ item: "W-5" });
|
|
182
|
+
expect(result.workLease).toMatchObject({ item: "W-5", released: true });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
167
186
|
test("--work takes the place of the Op's item, and a failed run releases not_done", async () => {
|
|
168
187
|
await withTestDir(async (dir) => {
|
|
169
188
|
await initRepo(dir);
|
package/src/op/work-lease-run.ts
CHANGED
|
@@ -354,7 +354,7 @@ export class RunWorkLease {
|
|
|
354
354
|
* keeps what was committed), and give the lease back with the outcome
|
|
355
355
|
* unless it was lost.
|
|
356
356
|
*/
|
|
357
|
-
async finish(status: "ok" | "fail" | "gated", resultsById: ReadonlyMap<string, unknown>): Promise<WorkLeaseRunResult> {
|
|
357
|
+
async finish(status: "ok" | "fail" | "gated" | "waiting", resultsById: ReadonlyMap<string, unknown>): Promise<WorkLeaseRunResult> {
|
|
358
358
|
this.stopHeartbeat();
|
|
359
359
|
await this.beat;
|
|
360
360
|
const lease = this.lease;
|
|
@@ -375,7 +375,7 @@ export class RunWorkLease {
|
|
|
375
375
|
await git(["worktree", "prune"], this.opts.cwd).catch(() => undefined);
|
|
376
376
|
}
|
|
377
377
|
if (this.lost !== undefined) return base;
|
|
378
|
-
let outcome = status === "ok" ? "done" : status === "gated"
|
|
378
|
+
let outcome = status === "ok" ? "done" : status === "gated" || status === "waiting" ? status : "not_done";
|
|
379
379
|
if (status === "ok" && this.spec.outcome && isStepOutputRef(this.spec.outcome)) {
|
|
380
380
|
const declared = resolvePath(resultsById.get(this.spec.outcome.step), this.spec.outcome.path);
|
|
381
381
|
if (typeof declared === "string" && declared.trim() !== "") outcome = declared.trim();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A throwaway workspace for the decide tests: a git repository with an answer
|
|
3
|
+
* kind (the reference workspace's), a task kind the points read through the
|
|
4
|
+
* read contract, three points (a noul, a choice and a score), and a box member
|
|
5
|
+
* that declares one brokered capability and one without a broker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
|
|
13
|
+
/** The chant checkout the tests run in. */
|
|
14
|
+
export const REPO = realpathSync(join(import.meta.dirname, "..", "..", "..", "..", ".."));
|
|
15
|
+
const REF = join(REPO, "reference-workspace");
|
|
16
|
+
|
|
17
|
+
export const MODEL = "jev-1.13.0";
|
|
18
|
+
|
|
19
|
+
const model = (extra: Record<string, unknown> = {}) => ({ kind: "model", backend: "systemone", model: MODEL, threshold: 0.8, ...extra });
|
|
20
|
+
|
|
21
|
+
export const POINTS = {
|
|
22
|
+
points: {
|
|
23
|
+
triage: {
|
|
24
|
+
title: "Does this task need a person now",
|
|
25
|
+
question: { type: "noul", instructions: "Does the task need a person to look at it today?", criteria: { true: "A person looks at it today.", false: "It can wait." } },
|
|
26
|
+
inputs: { "record.size": "the task's size in points", "record.risky": "whether the task touches production" },
|
|
27
|
+
deciders: [{ kind: "table", rows: [{ when: { "record.size": 0 }, answer: false }] }, model(), { kind: "quorum", count: 1 }],
|
|
28
|
+
},
|
|
29
|
+
route: {
|
|
30
|
+
title: "Which team takes this task",
|
|
31
|
+
question: { type: "choice", instructions: "Pick the team that takes the task.", criteria: { platform: "The platform team.", app: "The app team.", docs: "The docs team." } },
|
|
32
|
+
inputs: { "record.title": "the task's title", "record.size": "the task's size in points" },
|
|
33
|
+
deciders: [model({ unreachable: "fail" }), { kind: "quorum", count: 1 }],
|
|
34
|
+
},
|
|
35
|
+
effort: {
|
|
36
|
+
title: "How much effort this task takes",
|
|
37
|
+
question: { type: "score", instructions: "Rate the effort the task takes.", criteria: ["low", "medium", "high"] },
|
|
38
|
+
inputs: { "record.size": "the task's size in points", "member.kind": "the kind of the member the task is in" },
|
|
39
|
+
deciders: [model(), { kind: "quorum", count: 1 }],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const TASK_KIND = `export const recordKind = {
|
|
45
|
+
name: "task",
|
|
46
|
+
location: { dir: ".", match: "^T-[0-9]+\\\\.md$" },
|
|
47
|
+
format: "markdown-front-matter",
|
|
48
|
+
schema: { id: "urn:intentius:chant:test-task:1", path: "task.schema.json" },
|
|
49
|
+
idField: "id",
|
|
50
|
+
stateField: "state",
|
|
51
|
+
states: ["open", "done"],
|
|
52
|
+
closedStates: ["done"],
|
|
53
|
+
};
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const TASK_SCHEMA = {
|
|
57
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
58
|
+
$id: "urn:intentius:chant:test-task:1",
|
|
59
|
+
type: "object",
|
|
60
|
+
required: ["id", "title", "state"],
|
|
61
|
+
properties: {
|
|
62
|
+
id: { type: "string" },
|
|
63
|
+
title: { type: "string" },
|
|
64
|
+
state: { enum: ["open", "done"] },
|
|
65
|
+
size: { type: "integer" },
|
|
66
|
+
risky: { type: "boolean" },
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const task = (id: string, title: string, size: number, risky: boolean) =>
|
|
72
|
+
`---\nid: "${id}"\ntitle: "${title}"\nstate: "open"\nsize: ${size}\nrisky: ${risky}\n---\n\n# ${title}\n`;
|
|
73
|
+
|
|
74
|
+
const scratch: string[] = [];
|
|
75
|
+
|
|
76
|
+
export function cleanScratch(): void {
|
|
77
|
+
for (const d of scratch.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function git(cwd: string, ...args: string[]): string {
|
|
81
|
+
return execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Make the workspace and commit it. Returns its root. */
|
|
85
|
+
export function workspace(points: unknown = POINTS): string {
|
|
86
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-decide-")));
|
|
87
|
+
scratch.push(root);
|
|
88
|
+
const files: Record<string, string> = {
|
|
89
|
+
"chant.workspace.json": JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
name: "decide-test",
|
|
92
|
+
schema: 1,
|
|
93
|
+
members: [
|
|
94
|
+
{ name: "app", dir: "app", kind: "other", because: "a plain Node server" },
|
|
95
|
+
{
|
|
96
|
+
name: "box",
|
|
97
|
+
dir: "box",
|
|
98
|
+
kind: "other",
|
|
99
|
+
because: "the box the steward runs in",
|
|
100
|
+
box: { capabilities: [{ name: "inference", broker: "lobby", scope: ["agent"] }, { name: "raw" }] },
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
records: [{ kind: "answers/answer.kind.mjs" }, { kind: "tasks/task.kind.mjs" }],
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
2,
|
|
107
|
+
),
|
|
108
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
109
|
+
"box/README.md": "The box.\n",
|
|
110
|
+
"decisions/points.json": JSON.stringify(points, null, 2),
|
|
111
|
+
"tasks/task.kind.mjs": TASK_KIND,
|
|
112
|
+
"tasks/task.schema.json": JSON.stringify(TASK_SCHEMA, null, 2),
|
|
113
|
+
"tasks/T-1.md": task("T-1", "Rotate the deploy key", 5, true),
|
|
114
|
+
"tasks/T-2.md": task("T-2", "Fix a typo in the README", 1, false),
|
|
115
|
+
};
|
|
116
|
+
for (const [path, text] of Object.entries(files)) {
|
|
117
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
118
|
+
writeFileSync(join(root, path), text);
|
|
119
|
+
}
|
|
120
|
+
cpSync(join(REF, "answers", "answer.kind.mjs"), join(root, "answers", "answer.kind.mjs"));
|
|
121
|
+
cpSync(join(REF, "answers", "answer.schema.json"), join(root, "answers", "answer.schema.json"));
|
|
122
|
+
git(root, "init", "-q");
|
|
123
|
+
git(root, "add", "-A");
|
|
124
|
+
git(root, "commit", "-q", "-m", "one");
|
|
125
|
+
return root;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The front matter text of a written record. */
|
|
129
|
+
export const recordText = (root: string, path: string): string => readFileSync(join(root, path), "utf-8");
|