@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
package/src/op/operator.ts
CHANGED
|
@@ -38,9 +38,12 @@
|
|
|
38
38
|
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
39
39
|
import { discoverOps, type DiscoveredOp } from "./discover";
|
|
40
40
|
import { runOpLocally, OpRunFailure, type OpRunResult } from "./local-executor";
|
|
41
|
-
import { acquireLease, stillHoldsLease, currentHolderId, DEFAULT_LEASE_TTL_MS, type LeaseRecord, type AcquireLeaseResult } from "../lifecycle/lease";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
41
|
+
import { acquireLease, releaseLease, stillHoldsLease, currentHolderId, DEFAULT_LEASE_TTL_MS, type LeaseRecord, type AcquireLeaseResult } from "../lifecycle/lease";
|
|
42
|
+
import { randomUUID } from "node:crypto";
|
|
43
|
+
import { readRunLedger, runEnvOf } from "../lifecycle/run-ledger";
|
|
44
|
+
import { readGateLedger, latestResolutionSince } from "../lifecycle/gate-ledger";
|
|
45
|
+
import { enterStewardTurn } from "./steward-turn";
|
|
46
|
+
import { stewardLeaseName, stewardTurnLeaseName, type StewardDeclaration } from "./steward";
|
|
44
47
|
import { stewardWorkHolder } from "./work-lease-run";
|
|
45
48
|
import { StaleLockError } from "../lifecycle/git";
|
|
46
49
|
import { cronMatches, cronDueBetween } from "./cron";
|
|
@@ -79,7 +82,36 @@ export async function discoverConvergeOps(
|
|
|
79
82
|
|
|
80
83
|
/** One discovered ConvergeOp's outcome for one round — what `chant operator`'s one-line-per-tick log (and its tests) key off of. */
|
|
81
84
|
export type OperatorTickEvent =
|
|
82
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The Op ran. `resumed` names the decision point question it was waiting on
|
|
87
|
+
* (#2749) when a steward ran it because that question is now answered,
|
|
88
|
+
* rather than because its cron fired.
|
|
89
|
+
*/
|
|
90
|
+
| {
|
|
91
|
+
kind: "ticked";
|
|
92
|
+
op: string;
|
|
93
|
+
env: string;
|
|
94
|
+
result: OpRunResult;
|
|
95
|
+
resumed?: string;
|
|
96
|
+
/**
|
|
97
|
+
* The gate the Op's last run stopped at (#2779), when a steward ran it
|
|
98
|
+
* because that gate is now approved rather than because its cron fired.
|
|
99
|
+
*/
|
|
100
|
+
approved?: { op: string; gate: string };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A steward's Op whose last run stopped on an open decision point (#2749),
|
|
104
|
+
* and the question is still open, so the Op is not run this round. A person
|
|
105
|
+
* answers it through hud or `points answer`; the steward never does.
|
|
106
|
+
*/
|
|
107
|
+
| { kind: "waiting-on-point"; op: string; env: string; point: string; question: string; state: string }
|
|
108
|
+
/**
|
|
109
|
+
* A steward's Op whose last run stopped at a gate (#2779), and nobody has
|
|
110
|
+
* approved it since, so the Op is not run this round. `gateOp` is the op
|
|
111
|
+
* the gate is recorded under: the Op's own, or a command's such as
|
|
112
|
+
* `workspace-upgrade`.
|
|
113
|
+
*/
|
|
114
|
+
| { kind: "waiting-on-gate"; op: string; env: string; gateOp: string; gate: string }
|
|
83
115
|
| { kind: "skipped-lease-held"; op: string; env: string; heldBy?: string }
|
|
84
116
|
/** The Op declares its own `schedule.cron` (#2120) and this round did not land on a firing minute — the lease was never touched. An Op without a cron is never reported this way: it ticks every round, on `--interval`. */
|
|
85
117
|
| { kind: "skipped-not-due"; op: string; env: string; cron: string }
|
|
@@ -104,7 +136,17 @@ export type OperatorTickEvent =
|
|
|
104
136
|
* steward. Nothing ticked. `op` is the steward's lease name and `env` is
|
|
105
137
|
* `-`, so a log line keyed on either still reads.
|
|
106
138
|
*/
|
|
107
|
-
| { kind: "steward-busy"; op: string; env: string; steward: string; heldBy?: string }
|
|
139
|
+
| { kind: "steward-busy"; op: string; env: string; steward: string; heldBy?: string }
|
|
140
|
+
/**
|
|
141
|
+
* A local steward's round (#2750) found its turn lease
|
|
142
|
+
* (`stewardTurnLeaseName`) held by someone else already mid-run — a `chant
|
|
143
|
+
* run <op>` typed by hand, or another round somehow still finishing one.
|
|
144
|
+
* Nothing ticked: the round stops here rather than trying its remaining
|
|
145
|
+
* ops, since every one of them would fail the same check (the turn is one
|
|
146
|
+
* lease for the whole steward, not one per op). The next round tries
|
|
147
|
+
* again.
|
|
148
|
+
*/
|
|
149
|
+
| { kind: "turn-busy"; op: string; env: string; steward: string; heldBy?: string };
|
|
108
150
|
|
|
109
151
|
export interface OperatorRoundOptions {
|
|
110
152
|
cwd?: string;
|
|
@@ -137,6 +179,98 @@ export interface OperatorRoundOptions {
|
|
|
137
179
|
* lease, and ticks nothing when another holder has it.
|
|
138
180
|
*/
|
|
139
181
|
steward?: StewardDeclaration;
|
|
182
|
+
/**
|
|
183
|
+
* How a steward's round reads the state of the questions its waiting runs
|
|
184
|
+
* stopped on (#2749): question id to state, or null when they can't be
|
|
185
|
+
* read. Defaults to the workspace's `points` read; a test injects one.
|
|
186
|
+
*/
|
|
187
|
+
readQuestions?: (cwd: string) => Promise<Map<string, string> | null>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A steward's Op whose last run waits on a decision point (#2749). */
|
|
191
|
+
interface WaitState {
|
|
192
|
+
/** The answer record's id. */
|
|
193
|
+
question: string;
|
|
194
|
+
point: string;
|
|
195
|
+
/** The question's state now, or the one the run recorded when it can't be read. */
|
|
196
|
+
state: string;
|
|
197
|
+
/** The question is answered, or its record is gone, so the Op runs again. */
|
|
198
|
+
answered: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The steward's Ops whose newest run is the steward's own and stopped on a
|
|
203
|
+
* decision point, with that question's state now. The questions are read
|
|
204
|
+
* from the workspace the way `points` reads them; a workspace that can't be
|
|
205
|
+
* read leaves every run waiting.
|
|
206
|
+
*/
|
|
207
|
+
async function stewardWaits(steward: StewardDeclaration, opts: OperatorRoundOptions): Promise<Map<string, WaitState>> {
|
|
208
|
+
const out = new Map<string, WaitState>();
|
|
209
|
+
const runs: { op: string; question: string; point: string; state: string }[] = [];
|
|
210
|
+
for (const op of steward.ops) {
|
|
211
|
+
try {
|
|
212
|
+
const newest = (await readRunLedger(runEnvOf(op), op.name, { cwd: opts.cwd })).records.at(-1);
|
|
213
|
+
if (newest?.status === "waiting" && newest.point && newest.steward === steward.name) {
|
|
214
|
+
runs.push({ op: op.name, question: newest.point.id, point: newest.point.point, state: newest.point.state });
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// A ledger that can't be read has no waiting run to resume.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (runs.length === 0) return out;
|
|
221
|
+
const states = await (opts.readQuestions ?? readQuestionStates)(opts.cwd ?? process.cwd());
|
|
222
|
+
for (const r of runs) {
|
|
223
|
+
const now = states?.get(r.question);
|
|
224
|
+
const answered = states !== null && (now === undefined || now === "answered");
|
|
225
|
+
out.set(r.op, { question: r.question, point: r.point, state: now ?? r.state, answered });
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A steward's Op whose last run stopped at a gate (#2779). */
|
|
231
|
+
interface GateWaitState {
|
|
232
|
+
/** The op the gate is recorded under. */
|
|
233
|
+
gateOp: string;
|
|
234
|
+
gate: string;
|
|
235
|
+
/** Someone approved the gate since the run stopped there, so the Op runs again. */
|
|
236
|
+
approved: boolean;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The steward's Ops whose newest run is the steward's own and stopped at a
|
|
241
|
+
* gate, and whether the gate has been approved since. A gate ledger that
|
|
242
|
+
* can't be read leaves the run where it stopped.
|
|
243
|
+
*/
|
|
244
|
+
async function stewardGates(steward: StewardDeclaration, opts: OperatorRoundOptions): Promise<Map<string, GateWaitState>> {
|
|
245
|
+
const out = new Map<string, GateWaitState>();
|
|
246
|
+
for (const op of steward.ops) {
|
|
247
|
+
try {
|
|
248
|
+
const newest = (await readRunLedger(runEnvOf(op), op.name, { cwd: opts.cwd })).records.at(-1);
|
|
249
|
+
if (newest?.status !== "gated" || !newest.gate || newest.steward !== steward.name) continue;
|
|
250
|
+
const gateOp = newest.gate.op ?? op.name;
|
|
251
|
+
const { resolutions } = await readGateLedger(gateOp, { cwd: opts.cwd });
|
|
252
|
+
const approved = latestResolutionSince(resolutions, newest.gate.name, newest.gate.since) !== undefined;
|
|
253
|
+
out.set(op.name, { gateOp, gate: newest.gate.name, approved });
|
|
254
|
+
} catch {
|
|
255
|
+
// A ledger that can't be read has no gated run to resume.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Every question's state in the workspace, by id, or null when the workspace's points can't be read. */
|
|
262
|
+
async function readQuestionStates(cwd: string): Promise<Map<string, string> | null> {
|
|
263
|
+
try {
|
|
264
|
+
const { workspacePoints } = await import("../workspace/points-cli");
|
|
265
|
+
const doc = await workspacePoints({ cwd });
|
|
266
|
+
if (!("questions" in doc)) return null;
|
|
267
|
+
// An answer kind that could not be read might hold the question: say
|
|
268
|
+
// nothing rather than resume a run whose question may still be open.
|
|
269
|
+
if (doc.sources.some((s) => s.reason !== null)) return null;
|
|
270
|
+
return new Map(doc.questions.map((q) => [q.id, q.state]));
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
140
274
|
}
|
|
141
275
|
|
|
142
276
|
/** Take or renew a local steward's own lease (#2731). */
|
|
@@ -148,9 +282,66 @@ export async function acquireStewardLease(
|
|
|
148
282
|
return acquireLease(stewardLeaseName(steward), holder, { ...opts, ttlMs: opts.ttlMs ?? DEFAULT_LEASE_TTL_MS });
|
|
149
283
|
}
|
|
150
284
|
|
|
151
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* How long a caller contending for a steward's turn lease
|
|
287
|
+
* (`acquireStewardTurn`) retries before giving up (#2750). Long enough to
|
|
288
|
+
* ride out a race with a turn that is just ending; short enough that a
|
|
289
|
+
* genuinely busy steward is reported back promptly. A round never waits
|
|
290
|
+
* (`waitMs` omitted there): it already runs on its own timer and simply
|
|
291
|
+
* tries the turn again next round, exactly as it already does for a busy
|
|
292
|
+
* per-op lease. `chant run <op>` (`../cli/handlers/run.ts`) is the caller
|
|
293
|
+
* that waits, since a human is standing at the terminal for the answer.
|
|
294
|
+
*/
|
|
295
|
+
export const STEWARD_TURN_WAIT_MS = 2_000;
|
|
296
|
+
|
|
297
|
+
/** How often {@link acquireStewardTurn} retries while it waits. */
|
|
298
|
+
const STEWARD_TURN_POLL_MS = 150;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Take the steward's turn lease (#2750): the lease every run of one of its
|
|
302
|
+
* Ops — a round's scheduled tick, or `chant run <op>` typed by hand — holds
|
|
303
|
+
* for exactly as long as that one run takes, so the two are never beside each
|
|
304
|
+
* other. Distinct from {@link acquireStewardLease}, which one `chant operator
|
|
305
|
+
* --steward` process holds for its whole life: this one is held per-run, so a
|
|
306
|
+
* hand run succeeds the moment a round's tick ends, not only once the daemon
|
|
307
|
+
* itself stops.
|
|
308
|
+
*
|
|
309
|
+
* Contention never queues past `waitMs` (0 by default): consistent with a
|
|
310
|
+
* round's own leases, which skip and report rather than wait (see this
|
|
311
|
+
* module's doc), and with the fountain form, whose busy teammate is likewise
|
|
312
|
+
* refused rather than retried. A caller that wants to ride out a race with a
|
|
313
|
+
* turn about to end passes `waitMs`, and this polls every
|
|
314
|
+
* {@link STEWARD_TURN_POLL_MS} until it acquires or the wait runs out.
|
|
315
|
+
*/
|
|
316
|
+
export async function acquireStewardTurn(
|
|
317
|
+
steward: string,
|
|
318
|
+
holder: string,
|
|
319
|
+
opts: { cwd?: string; ttlMs?: number; now?: () => Date; waitMs?: number } = {},
|
|
320
|
+
): Promise<AcquireLeaseResult> {
|
|
321
|
+
const deadline = Date.now() + (opts.waitMs ?? 0);
|
|
322
|
+
for (;;) {
|
|
323
|
+
const result = await acquireLease(stewardTurnLeaseName(steward), holder, {
|
|
324
|
+
cwd: opts.cwd,
|
|
325
|
+
ttlMs: opts.ttlMs ?? DEFAULT_LEASE_TTL_MS,
|
|
326
|
+
now: opts.now,
|
|
327
|
+
});
|
|
328
|
+
if (result.acquired || result.reason !== "held" || Date.now() >= deadline) return result;
|
|
329
|
+
await new Promise((r) => setTimeout(r, STEWARD_TURN_POLL_MS));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Release a steward's turn lease, best-effort — a run whose turn was never actually acquired (or already lost) has nothing to release. */
|
|
334
|
+
async function releaseStewardTurn(steward: string, holder: string, token: string, opts: { cwd?: string } = {}): Promise<void> {
|
|
335
|
+
await releaseLease(stewardTurnLeaseName(steward), holder, token, opts).catch(() => false);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The Ops one round considers: a steward's Ops, or every ConvergeOp. A
|
|
340
|
+
* steward's Op with no schedule is considered only to resume a run of the
|
|
341
|
+
* steward's that waits on a decision point.
|
|
342
|
+
*/
|
|
152
343
|
async function roundOps(opts: OperatorRoundOptions): Promise<DiscoveredOp["config"][]> {
|
|
153
|
-
if (opts.steward) return opts.steward.ops
|
|
344
|
+
if (opts.steward) return [...opts.steward.ops];
|
|
154
345
|
const { ops } = await discoverConvergeOps({ cwd: opts.cwd, env: opts.env });
|
|
155
346
|
return ops.map((d) => d.config);
|
|
156
347
|
}
|
|
@@ -197,8 +388,32 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
197
388
|
|
|
198
389
|
if (!(await holdSteward())) return events;
|
|
199
390
|
|
|
391
|
+
// A steward's Ops whose last run waits on a decision point (#2749), read
|
|
392
|
+
// once per round, and the state of those questions now.
|
|
393
|
+
const waiting = steward ? await stewardWaits(steward, opts) : new Map<string, WaitState>();
|
|
394
|
+
// And those whose last run stopped at a gate (#2779), with whether it has
|
|
395
|
+
// been approved since.
|
|
396
|
+
const gated = steward ? await stewardGates(steward, opts) : new Map<string, GateWaitState>();
|
|
397
|
+
|
|
200
398
|
for (const config of await roundOps(opts)) {
|
|
201
399
|
const env = steward ? runEnvOf(config) : (envOf(config) ?? "unknown");
|
|
400
|
+
const wait = waiting.get(config.name);
|
|
401
|
+
const stopped = gated.get(config.name);
|
|
402
|
+
let resumed: string | undefined;
|
|
403
|
+
let approved: { op: string; gate: string } | undefined;
|
|
404
|
+
|
|
405
|
+
// A run waiting on a question that is now answered is resumed on this
|
|
406
|
+
// round, whatever its cron says. One whose question is still open is left
|
|
407
|
+
// until a person answers it, unless its cron fires, which asks again.
|
|
408
|
+
if (wait?.answered) resumed = wait.question;
|
|
409
|
+
// The same for a run stopped at a gate that is now approved (#2779).
|
|
410
|
+
if (stopped?.approved) approved = { op: stopped.gateOp, gate: stopped.gate };
|
|
411
|
+
const standing: OperatorTickEvent | undefined = wait
|
|
412
|
+
? { kind: "waiting-on-point", op: config.name, env, point: wait.point, question: wait.question, state: wait.state }
|
|
413
|
+
: stopped
|
|
414
|
+
? { kind: "waiting-on-gate", op: config.name, env, gateOp: stopped.gateOp, gate: stopped.gate }
|
|
415
|
+
: undefined;
|
|
416
|
+
const resuming = resumed !== undefined || approved !== undefined;
|
|
202
417
|
|
|
203
418
|
// An Op that declares its own cadence (#2120) is ticked on that cron
|
|
204
419
|
// rather than on every round. Level-triggered: the question is whether a
|
|
@@ -212,10 +427,15 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
212
427
|
const lastSeen = opts.scheduleState?.get(config.name);
|
|
213
428
|
const due = lastSeen === undefined ? cronMatches(cron, now) : cronDueBetween(cron, lastSeen, now);
|
|
214
429
|
opts.scheduleState?.set(config.name, now);
|
|
215
|
-
if (!due) {
|
|
216
|
-
events.push({ kind: "skipped-not-due", op: config.name, env, cron });
|
|
430
|
+
if (!due && !resuming) {
|
|
431
|
+
events.push(standing ?? { kind: "skipped-not-due", op: config.name, env, cron });
|
|
217
432
|
continue;
|
|
218
433
|
}
|
|
434
|
+
} else if (steward && !resuming) {
|
|
435
|
+
// An unscheduled Op runs when someone asks for it; the steward runs it
|
|
436
|
+
// only to resume its own waiting or gated run.
|
|
437
|
+
if (standing) events.push(standing);
|
|
438
|
+
continue;
|
|
219
439
|
}
|
|
220
440
|
|
|
221
441
|
let acquired: AcquireLeaseResult;
|
|
@@ -242,8 +462,44 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
242
462
|
}
|
|
243
463
|
|
|
244
464
|
const lease = acquired.lease as LeaseRecord;
|
|
465
|
+
|
|
466
|
+
// A local steward is one turn at a time (#2750): a hand run (`chant run
|
|
467
|
+
// <op>`) holds this same lease for as long as it runs, so a round never
|
|
468
|
+
// ticks beside one. It is one lease for the whole steward, not one per
|
|
469
|
+
// op, so finding it held stops the round outright — every op still ahead
|
|
470
|
+
// of it in `roundOps` would fail the identical check.
|
|
471
|
+
let turn: AcquireLeaseResult | undefined;
|
|
472
|
+
if (steward) {
|
|
473
|
+
try {
|
|
474
|
+
turn = await acquireStewardTurn(steward.name, holder, { cwd: opts.cwd, ttlMs: opts.leaseTtlMs, now: opts.now });
|
|
475
|
+
} catch (err) {
|
|
476
|
+
// Not turn contention — the acquire attempt itself failed (most
|
|
477
|
+
// likely a StaleLockError). Same shape as the per-op acquire's own
|
|
478
|
+
// catch above: report it and stop, rather than let it propagate and
|
|
479
|
+
// leak the per-op lease this round just took.
|
|
480
|
+
await releaseLease(config.name, holder, lease.token, { cwd: opts.cwd }).catch(() => false);
|
|
481
|
+
const message = err instanceof StaleLockError ? err.message : err instanceof Error ? err.message : String(err);
|
|
482
|
+
events.push({ kind: "lease-error", op: stewardTurnLeaseName(steward.name), env, error: message });
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
if (!turn.acquired) {
|
|
486
|
+
await releaseLease(config.name, holder, lease.token, { cwd: opts.cwd }).catch(() => false);
|
|
487
|
+
events.push({ kind: "turn-busy", op: config.name, env, steward: steward.name, heldBy: turn.heldBy?.holder });
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// The run is the steward's turn (#2749): a decision point it asks names the
|
|
493
|
+
// steward and makes its model call through the steward's broker, and an
|
|
494
|
+
// answer from inside the turn is refused.
|
|
495
|
+
const runId = randomUUID();
|
|
496
|
+
const restoreTurn = steward
|
|
497
|
+
? enterStewardTurn({ steward: steward.name, capabilities: steward.capabilities, vault: steward.vault, run: runId })
|
|
498
|
+
: undefined;
|
|
245
499
|
try {
|
|
246
500
|
const result = await runOpLocally(config, opts.activities, opts.profiles, opts.signal, {
|
|
501
|
+
runId,
|
|
502
|
+
...(steward ? { steward: steward.name } : {}),
|
|
247
503
|
ledger: { cwd: opts.cwd },
|
|
248
504
|
// A steward's turn claims work leases as `<steward>/<op>@<holder>`
|
|
249
505
|
// (#2748), which is how `workspace status` finds the lease it holds.
|
|
@@ -263,7 +519,18 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
263
519
|
}),
|
|
264
520
|
});
|
|
265
521
|
const held = await stillHoldsLease(config.name, holder, lease.token, { cwd: opts.cwd });
|
|
266
|
-
events.push(
|
|
522
|
+
events.push(
|
|
523
|
+
held
|
|
524
|
+
? {
|
|
525
|
+
kind: "ticked",
|
|
526
|
+
op: config.name,
|
|
527
|
+
env,
|
|
528
|
+
result,
|
|
529
|
+
...(resumed !== undefined ? { resumed } : {}),
|
|
530
|
+
...(approved !== undefined ? { approved } : {}),
|
|
531
|
+
}
|
|
532
|
+
: { kind: "fenced", op: config.name, env },
|
|
533
|
+
);
|
|
267
534
|
} catch (err) {
|
|
268
535
|
// #2301: "see its ledger record" was the whole message, and it sent the
|
|
269
536
|
// reader to an artifact that a failed ledger append means is missing —
|
|
@@ -277,6 +544,9 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
277
544
|
? err.message
|
|
278
545
|
: String(err);
|
|
279
546
|
events.push({ kind: "tick-failed", op: config.name, env, error: message });
|
|
547
|
+
} finally {
|
|
548
|
+
restoreTurn?.();
|
|
549
|
+
if (steward && turn?.acquired) await releaseStewardTurn(steward.name, holder, turn.lease!.token, { cwd: opts.cwd });
|
|
280
550
|
}
|
|
281
551
|
if (!(await holdSteward())) break;
|
|
282
552
|
}
|
|
@@ -488,7 +758,14 @@ export function formatRoundLine(event: OperatorTickEvent): string {
|
|
|
488
758
|
switch (event.kind) {
|
|
489
759
|
case "ticked":
|
|
490
760
|
return `operator: ${event.op}@${event.env} ticked=1 status=${event.result.status}` +
|
|
491
|
-
(event.result.gate ? ` gate="${event.result.gate.gate}"` : "")
|
|
761
|
+
(event.result.gate ? ` gate="${event.result.gate.gate}"` : "") +
|
|
762
|
+
(event.result.point ? ` point="${event.result.point.id}"` : "") +
|
|
763
|
+
(event.resumed ? ` resumed="${event.resumed}"` : "") +
|
|
764
|
+
(event.approved ? ` approved="${event.approved.op}/${event.approved.gate}"` : "");
|
|
765
|
+
case "waiting-on-point":
|
|
766
|
+
return `operator: ${event.op}@${event.env} waiting=1(point:${event.question}:${event.state})`;
|
|
767
|
+
case "waiting-on-gate":
|
|
768
|
+
return `operator: ${event.op}@${event.env} gated=1(gate:${event.gateOp}/${event.gate})`;
|
|
492
769
|
case "skipped-lease-held":
|
|
493
770
|
return `operator: ${event.op}@${event.env} skipped=1(lease-held${event.heldBy ? `:${event.heldBy}` : ""})`;
|
|
494
771
|
case "skipped-not-due":
|
|
@@ -501,5 +778,7 @@ export function formatRoundLine(event: OperatorTickEvent): string {
|
|
|
501
778
|
return `operator: ${event.op}@${event.env} error=1(lease acquire failed — ${event.error})`;
|
|
502
779
|
case "steward-busy":
|
|
503
780
|
return `operator: steward ${event.steward} skipped=1(steward-lease-held${event.heldBy ? `:${event.heldBy}` : ""})`;
|
|
781
|
+
case "turn-busy":
|
|
782
|
+
return `operator: ${event.op}@${event.env} skipped=1(turn-held${event.heldBy ? `:${event.heldBy}` : ""}, steward "${event.steward}" is already mid-turn)`;
|
|
504
783
|
}
|
|
505
784
|
}
|
package/src/op/runtime.ts
CHANGED
|
@@ -18,23 +18,27 @@
|
|
|
18
18
|
|
|
19
19
|
import type { OpConfig } from "./types";
|
|
20
20
|
import type { StepRecord, OpRunResult } from "./local-executor";
|
|
21
|
+
import type { WaitingPoint } from "./steward-points";
|
|
21
22
|
import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
|
|
22
23
|
import type { RunComponentsOptions, RunComponentsResult } from "../components/cli-support";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* How a run ended, or that it has not. `gated` is the state a run reaches when
|
|
26
|
-
* a gate has no recorded resolution — a fact, not a wait (#2119).
|
|
27
|
+
* a gate has no recorded resolution — a fact, not a wait (#2119). `waiting` is
|
|
28
|
+
* the state it reaches when an activity asked a decision point that is still
|
|
29
|
+
* open (#2749), a fact of the same kind: a person answers the question, and
|
|
30
|
+
* the next run reads the answer.
|
|
27
31
|
*
|
|
28
32
|
* This is the coarse state a *runtime* reports. The ledger records the
|
|
29
33
|
* executor's own three-state outcome instead ({@link OpRunRecord.status}),
|
|
30
34
|
* which has no `running` or `cancelled`: a record is only written once a run
|
|
31
35
|
* has settled.
|
|
32
36
|
*/
|
|
33
|
-
export type OpRunState = "running" | "completed" | "failed" | "gated" | "cancelled";
|
|
37
|
+
export type OpRunState = "running" | "completed" | "failed" | "gated" | "waiting" | "cancelled";
|
|
34
38
|
|
|
35
39
|
/** Map a settled run's ledger status onto the coarser runtime state. */
|
|
36
40
|
export function runStateOf(status: OpRunRecord["status"]): OpRunState {
|
|
37
|
-
return status === "ok" ? "completed" : status === "gated" ? "gated" : "failed";
|
|
41
|
+
return status === "ok" ? "completed" : status === "gated" ? "gated" : status === "waiting" ? "waiting" : "failed";
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** One step's outcome as the run ledger keeps it — `StepRecord` minus the fields only a live renderer needs. */
|
|
@@ -51,6 +55,10 @@ export interface OpRunStepRecord {
|
|
|
51
55
|
error?: string;
|
|
52
56
|
/** Why a gate declined a standing approval that was for another plan (#2300) — see {@link StepRecord.refusal}. */
|
|
53
57
|
refusal?: string;
|
|
58
|
+
/** The open decision point this step asked (#2749), for a step skipped because it is waiting on one. */
|
|
59
|
+
point?: WaitingPoint;
|
|
60
|
+
/** The gate this step's command stopped at (#2779), for a step skipped because of it. */
|
|
61
|
+
gate?: { op: string; gate: string };
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/** One phase's steps and the verdict they add up to. */
|
|
@@ -81,8 +89,10 @@ export interface OpRunStatus {
|
|
|
81
89
|
records?: StepRecord[];
|
|
82
90
|
/** The executor's own result, when the runtime is executing in this process. */
|
|
83
91
|
result?: OpRunResult;
|
|
84
|
-
/** The gate this run is waiting on, when `state` is `gated
|
|
85
|
-
gate?: { name: string; since: string };
|
|
92
|
+
/** The gate this run is waiting on, when `state` is `gated`; `op` names the op it is recorded under when that isn't the run's own (#2779). */
|
|
93
|
+
gate?: { name: string; since: string; op?: string };
|
|
94
|
+
/** The open decision point this run is waiting on, when `state` is `waiting` (#2749). */
|
|
95
|
+
point?: WaitingPoint & { since: string };
|
|
86
96
|
/** Free-text detail for a failed run. */
|
|
87
97
|
error?: string;
|
|
88
98
|
}
|
|
@@ -117,15 +127,28 @@ export interface OpRunRecord {
|
|
|
117
127
|
* failure: the run reached a gate nobody has approved, recorded the fact,
|
|
118
128
|
* and stopped (#2119).
|
|
119
129
|
*/
|
|
120
|
-
status: "ok" | "fail" | "gated";
|
|
130
|
+
status: "ok" | "fail" | "gated" | "waiting";
|
|
121
131
|
/** The Op's own `labels`, copied at run time. */
|
|
122
132
|
labels: Record<string, string>;
|
|
123
133
|
/** Every `outcomeAttribute` the run captured, name → value. */
|
|
124
134
|
outcomes: Record<string, unknown>;
|
|
125
135
|
/** Per-phase, per-step status, in execution order. */
|
|
126
136
|
phases: OpRunPhaseRecord[];
|
|
127
|
-
/**
|
|
128
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The gate the run stopped on, for `status: "gated"`. `op` is set when the
|
|
139
|
+
* gate is recorded under another op than the run's (#2779): a step whose
|
|
140
|
+
* command stopped at its own gate, such as `chant workspace upgrade`'s
|
|
141
|
+
* `workspace-upgrade` / `<scope>`.
|
|
142
|
+
*/
|
|
143
|
+
gate?: { name: string; since: string; op?: string };
|
|
144
|
+
/**
|
|
145
|
+
* The open decision point the run stopped on, for `status: "waiting"`
|
|
146
|
+
* (#2749): the answer record's id, the point, its state then, and when the
|
|
147
|
+
* run stopped. `points --open` and hud read the question itself.
|
|
148
|
+
*/
|
|
149
|
+
point?: WaitingPoint & { since: string };
|
|
150
|
+
/** The steward whose turn the run was, when a steward ran it (#2749). */
|
|
151
|
+
steward?: string;
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
/** An {@link OpRunRecord} before the ledger stamps its version and mints an id. */
|
package/src/op/runtimes/local.ts
CHANGED
|
@@ -52,7 +52,8 @@ function statusFrom(
|
|
|
52
52
|
endedAt: new Date().toISOString(),
|
|
53
53
|
records: result.records,
|
|
54
54
|
result,
|
|
55
|
-
...(result.gate ? { gate: { name: result.gate.gate, since: result.gate.timestamp } } : {}),
|
|
55
|
+
...(result.gate ? { gate: { name: result.gate.gate, since: result.gate.timestamp, ...(result.gate.op !== op ? { op: result.gate.op } : {}) } } : {}),
|
|
56
|
+
...(result.record.point ? { point: result.record.point } : {}),
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -70,6 +71,7 @@ function statusFromRecord(record: OpRunRecord): OpRunStatus {
|
|
|
70
71
|
startedAt: record.started,
|
|
71
72
|
endedAt: record.ended,
|
|
72
73
|
...(record.gate ? { gate: record.gate } : {}),
|
|
74
|
+
...(record.point ? { point: record.point } : {}),
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A source tree as a release artifact (#2782): one directory of a commit,
|
|
3
|
+
* archived with `git archive`, and read back only when its bytes still hash
|
|
4
|
+
* to the digest a gate approved.
|
|
5
|
+
*
|
|
6
|
+
* `git archive <commit> -- <dir>` gives every entry the commit's own
|
|
7
|
+
* timestamp, so the same commit always archives to the same bytes, and the
|
|
8
|
+
* archive's sha256 names the tree exactly. A release plan carries that digest,
|
|
9
|
+
* the ship gate approves the plan, and whatever ships the release reads the
|
|
10
|
+
* archive through {@link readSourceArchive}, which refuses bytes that do not
|
|
11
|
+
* hash to the approved digest.
|
|
12
|
+
*
|
|
13
|
+
* Pure except for the git and file reads each function names.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** One file of an archived tree. */
|
|
22
|
+
export interface SourceFile {
|
|
23
|
+
/** Its path inside the archived directory, with `/` separators. */
|
|
24
|
+
path: string;
|
|
25
|
+
/** Whether git records it executable. */
|
|
26
|
+
executable: boolean;
|
|
27
|
+
data: Buffer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A directory of a commit, archived. */
|
|
31
|
+
export interface SourceArchive {
|
|
32
|
+
/** `sha256:<hex>` of the archive's bytes. */
|
|
33
|
+
digest: string;
|
|
34
|
+
/** Where the archive was written. */
|
|
35
|
+
archive: string;
|
|
36
|
+
/** The commit it was archived from. */
|
|
37
|
+
commit: string;
|
|
38
|
+
/** The archived directory, relative to the repository root (`.` for the root). */
|
|
39
|
+
dir: string;
|
|
40
|
+
/** Files in it. */
|
|
41
|
+
files: number;
|
|
42
|
+
/** Size of the archive in bytes. */
|
|
43
|
+
bytes: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const sha256Digest = (data: Buffer | string): string => `sha256:${createHash("sha256").update(data).digest("hex")}`;
|
|
47
|
+
|
|
48
|
+
function git(cwd: string, args: string[]): Buffer {
|
|
49
|
+
return execFileSync("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Archive `dir` (relative to `cwd`) as committed at `ref` (default HEAD) to
|
|
54
|
+
* `out` (default `<cwd>/dist/releases/<commit>.tar`). Uncommitted changes are
|
|
55
|
+
* not in it: a release ships what is committed. Running it again for the same
|
|
56
|
+
* commit writes the same bytes.
|
|
57
|
+
*/
|
|
58
|
+
export function archiveSourceTree(opts: { dir: string; ref?: string; out?: string; cwd?: string }): SourceArchive {
|
|
59
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
60
|
+
// git names the root by its real path, so the directory is resolved the same way.
|
|
61
|
+
const root = realpathSync(git(cwd, ["rev-parse", "--show-toplevel"]).toString("utf-8").trim());
|
|
62
|
+
const commit = git(cwd, ["rev-parse", "--verify", `${opts.ref ?? "HEAD"}^{commit}`]).toString("utf-8").trim();
|
|
63
|
+
const target = resolve(realpathSync(cwd), opts.dir);
|
|
64
|
+
const dir = relative(root, target).split("\\").join("/") || ".";
|
|
65
|
+
if (dir.startsWith("..") || isAbsolute(dir)) throw new Error(`${opts.dir} is outside the repository at ${root}`);
|
|
66
|
+
const spec = dir === "." ? commit : `${commit}:${dir}`;
|
|
67
|
+
try {
|
|
68
|
+
git(root, ["cat-file", "-e", spec]);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error(`${dir} is not in commit ${commit.slice(0, 12)}: commit it before releasing it`);
|
|
71
|
+
}
|
|
72
|
+
const data = git(root, ["archive", "--format=tar", commit, "--", dir]);
|
|
73
|
+
const archive = resolve(cwd, opts.out ?? join("dist", "releases", `${commit}.tar`));
|
|
74
|
+
mkdirSync(dirname(archive), { recursive: true });
|
|
75
|
+
writeFileSync(archive, data);
|
|
76
|
+
const files = tarEntries(data).filter((e) => e.type === "file").length;
|
|
77
|
+
return { digest: sha256Digest(data), archive, commit, dir, files, bytes: data.length };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface TarEntry {
|
|
81
|
+
type: "file" | "dir" | "symlink" | "other";
|
|
82
|
+
path: string;
|
|
83
|
+
mode: number;
|
|
84
|
+
data: Buffer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const field = (block: Buffer, at: number, len: number): string => {
|
|
88
|
+
const raw = block.subarray(at, at + len);
|
|
89
|
+
const end = raw.indexOf(0);
|
|
90
|
+
return raw.subarray(0, end < 0 ? raw.length : end).toString("utf-8");
|
|
91
|
+
};
|
|
92
|
+
const octal = (block: Buffer, at: number, len: number): number => parseInt(field(block, at, len).trim() || "0", 8);
|
|
93
|
+
|
|
94
|
+
/** The records of a pax header: `<len> key=value\n`. */
|
|
95
|
+
function paxRecords(data: Buffer): Record<string, string> {
|
|
96
|
+
const out: Record<string, string> = {};
|
|
97
|
+
let at = 0;
|
|
98
|
+
while (at < data.length) {
|
|
99
|
+
const space = data.indexOf(0x20, at);
|
|
100
|
+
if (space < 0) break;
|
|
101
|
+
const len = parseInt(data.subarray(at, space).toString("utf-8"), 10);
|
|
102
|
+
if (!Number.isFinite(len) || len <= 0) break;
|
|
103
|
+
const record = data.subarray(space + 1, at + len - 1).toString("utf-8");
|
|
104
|
+
const eq = record.indexOf("=");
|
|
105
|
+
if (eq > 0) out[record.slice(0, eq)] = record.slice(eq + 1);
|
|
106
|
+
at += len;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The entries of a ustar/pax archive, as `git archive` writes one. */
|
|
112
|
+
function tarEntries(tar: Buffer): TarEntry[] {
|
|
113
|
+
const out: TarEntry[] = [];
|
|
114
|
+
let at = 0;
|
|
115
|
+
let pax: Record<string, string> = {};
|
|
116
|
+
while (at + 512 <= tar.length) {
|
|
117
|
+
const header = tar.subarray(at, at + 512);
|
|
118
|
+
if (header.every((b) => b === 0)) break;
|
|
119
|
+
const size = octal(header, 124, 12);
|
|
120
|
+
const flag = String.fromCharCode(header[156] || 0x30);
|
|
121
|
+
const data = tar.subarray(at + 512, at + 512 + size);
|
|
122
|
+
at += 512 + Math.ceil(size / 512) * 512;
|
|
123
|
+
if (flag === "x") {
|
|
124
|
+
pax = paxRecords(data);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (flag === "g") continue;
|
|
128
|
+
const prefix = field(header, 345, 155);
|
|
129
|
+
const name = pax.path ?? (prefix ? `${prefix}/${field(header, 0, 100)}` : field(header, 0, 100));
|
|
130
|
+
pax = {};
|
|
131
|
+
const type = flag === "0" || flag === "\0" || flag === "7" ? "file" : flag === "5" ? "dir" : flag === "2" ? "symlink" : "other";
|
|
132
|
+
out.push({ type, path: name, mode: octal(header, 100, 8), data: Buffer.from(data) });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The files of an archive `archiveSourceTree` wrote, relative to the directory
|
|
139
|
+
* it archived, once its bytes hash to `digest`. Throws, naming both digests,
|
|
140
|
+
* when they do not: whatever would ship them is not what was approved.
|
|
141
|
+
*
|
|
142
|
+
* A symbolic link in the tree is refused rather than followed or dropped.
|
|
143
|
+
*/
|
|
144
|
+
export function readSourceArchive(opts: { archive: string; digest: string; dir?: string }): SourceFile[] {
|
|
145
|
+
const data = readFileSync(opts.archive);
|
|
146
|
+
const actual = sha256Digest(data);
|
|
147
|
+
if (actual !== opts.digest) {
|
|
148
|
+
throw new Error(`${opts.archive} is ${actual}, not the approved ${opts.digest}: it is not the tree the release was planned from`);
|
|
149
|
+
}
|
|
150
|
+
const entries = tarEntries(data);
|
|
151
|
+
const dir = opts.dir && opts.dir !== "." ? `${opts.dir.replace(/\/+$/, "")}/` : "";
|
|
152
|
+
const files: SourceFile[] = [];
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
if (e.type === "dir") continue;
|
|
155
|
+
if (!e.path.startsWith(dir)) throw new Error(`${opts.archive} holds ${e.path}, outside ${dir || "its root"}`);
|
|
156
|
+
const path = e.path.slice(dir.length);
|
|
157
|
+
if (e.type !== "file") throw new Error(`${opts.archive} holds ${path}, which is not a regular file; a source release ships regular files only`);
|
|
158
|
+
files.push({ path, executable: (e.mode & 0o111) !== 0, data: e.data });
|
|
159
|
+
}
|
|
160
|
+
return files;
|
|
161
|
+
}
|