@intentius/chant 0.90.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +8 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +20 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +7 -4
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +42 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +72 -0
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +40 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +72 -16
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +3 -2
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts +114 -0
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +140 -0
- package/dist/lifecycle/work-lease.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +203 -2
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/discover.d.ts +25 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +18 -5
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/lifecycle-receipt-store.d.ts +34 -0
- package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
- package/dist/op/local-executor.d.ts +48 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +12 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +126 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +46 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +2 -2
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +161 -0
- package/dist/op/steward.d.ts.map +1 -0
- package/dist/op/types.d.ts +58 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts +18 -0
- package/dist/op/work-lease-decl.d.ts.map +1 -0
- package/dist/op/work-lease-run.d.ts +173 -0
- package/dist/op/work-lease-run.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/box-isolation.d.ts +99 -0
- package/dist/workspace/box-isolation.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/box-isolation.d.ts +18 -0
- package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +104 -0
- package/dist/workspace/checks/boxes.d.ts.map +1 -0
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +8 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +13 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +229 -0
- package/dist/workspace/decide.d.ts.map +1 -0
- package/dist/workspace/decision-points.schema.json +137 -0
- package/dist/workspace/declaration.d.ts +103 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +285 -0
- package/dist/workspace/declared-kinds.d.ts +32 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts +113 -0
- package/dist/workspace/points-cli.d.ts.map +1 -0
- package/dist/workspace/points.d.ts +320 -0
- package/dist/workspace/points.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +38 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +23 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +42 -6
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +179 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +68 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +189 -0
- package/dist/workspace/work-cli.d.ts.map +1 -0
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/components.test.ts +93 -0
- package/src/cli/handlers/components.ts +44 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +141 -4
- package/src/cli/handlers/run.test.ts +173 -2
- package/src/cli/handlers/run.ts +174 -3
- package/src/cli/main.test.ts +50 -0
- package/src/cli/main.ts +97 -2
- package/src/cli/mcp/workspace-tools.test.ts +48 -19
- package/src/cli/mcp/workspace-tools.ts +110 -8
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +42 -1
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/components/verbs/vuln-scan.test.ts +124 -1
- package/src/components/verbs/vuln-scan.ts +142 -1
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/git.ts +65 -15
- package/src/lifecycle/lease.test.ts +22 -0
- package/src/lifecycle/lease.ts +133 -29
- package/src/lifecycle/member-ledger.ts +3 -2
- package/src/lifecycle/plan-ledger.test.ts +148 -0
- package/src/lifecycle/plan-ledger.ts +158 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lifecycle/work-lease.test.ts +236 -0
- package/src/lifecycle/work-lease.ts +426 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +124 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/discover.ts +71 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +34 -6
- package/src/op/lifecycle-receipt-store.test.ts +60 -0
- package/src/op/lifecycle-receipt-store.ts +61 -0
- package/src/op/local-executor.ts +367 -33
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +33 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +18 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +360 -10
- package/src/op/runtime.ts +37 -8
- package/src/op/runtimes/local.ts +6 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/step-output-ref.ts +6 -2
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +296 -0
- package/src/op/steward.ts +277 -0
- package/src/op/types.ts +60 -0
- package/src/op/work-lease-decl.ts +80 -0
- package/src/op/work-lease-run.test.ts +345 -0
- package/src/op/work-lease-run.ts +395 -0
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/box-isolation.test.ts +261 -0
- package/src/workspace/box-isolation.ts +205 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +5 -1
- package/src/workspace/check.schema.json +25 -7
- package/src/workspace/checks/box-isolation.ts +68 -0
- package/src/workspace/checks/boxes.test.ts +276 -0
- package/src/workspace/checks/boxes.ts +442 -0
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +72 -0
- package/src/workspace/checks.test.ts +7 -0
- package/src/workspace/checks.ts +27 -3
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +224 -0
- package/src/workspace/decide.ts +709 -0
- package/src/workspace/decision-points.schema.json +137 -0
- package/src/workspace/declaration.schema.json +285 -0
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +241 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +71 -3
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +12 -3
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/point-answer.schema.json +95 -0
- package/src/workspace/points-cli.ts +284 -0
- package/src/workspace/points-write.schema.json +499 -0
- package/src/workspace/points.schema.json +743 -0
- package/src/workspace/points.test.ts +264 -0
- package/src/workspace/points.ts +564 -0
- package/src/workspace/read-contract.test.ts +18 -1
- package/src/workspace/reason-codes.test.ts +24 -1
- package/src/workspace/reason-codes.ts +49 -0
- package/src/workspace/record-assets.test.ts +3 -2
- package/src/workspace/record-assets.ts +4 -1
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +28 -5
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-contract.test.ts +3 -2
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +53 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +142 -6
- package/src/workspace/status-contract.test.ts +189 -6
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +329 -0
- package/src/workspace/status.schema.json +335 -5
- package/src/workspace/status.ts +142 -6
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/work-cli.e2e.test.ts +180 -0
- package/src/workspace/work-cli.ts +429 -0
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work-lease.schema.json +233 -0
- package/src/workspace/work-readiness-chud.test.ts +145 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2732 — `chant workspace work claim|renew|release`, run as the CLI: two
|
|
3
|
+
* processes racing for one work item, the lease in `records --json` and
|
|
4
|
+
* `workspace status --json`, and both over MCP.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { McpServer } from "../cli/mcp/server";
|
|
13
|
+
import { cleanScratch, contract, git, REPO, repo, writeFiles } from "./__fixtures__/contract-repo";
|
|
14
|
+
import recordsSchema from "./records.schema.json";
|
|
15
|
+
import statusSchema from "./status.schema.json";
|
|
16
|
+
import workSchema from "./work-lease.schema.json";
|
|
17
|
+
|
|
18
|
+
const REF = join(REPO, "reference-workspace");
|
|
19
|
+
const MAIN = join(REPO, "packages/core/src/cli/main.ts");
|
|
20
|
+
const LOADER = pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href;
|
|
21
|
+
const CHANT = [process.execPath, "--import", LOADER, MAIN];
|
|
22
|
+
const WORK = "work/work.kind.mjs";
|
|
23
|
+
|
|
24
|
+
function work(id: string, fields: Record<string, unknown> = {}): string {
|
|
25
|
+
const data = { schema: 1, id, title: `Work ${id}`, state: "open", implements: [], needs: [], constrains: ["path:app/server.mjs"], evidence: [], opened_on: "2026-09-25", source: { kind: "workspace", member: "app" }, supersedes: [], ...fields };
|
|
26
|
+
return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Run {
|
|
30
|
+
code: number | null;
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function chant(cwd: string, args: string[]): Promise<Run> {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const child = spawn(CHANT[0], [...CHANT.slice(1), ...args], { cwd, env: { ...process.env, NO_COLOR: "1", TSX_DISABLE_CACHE: "1" } });
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
41
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
42
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const json = (r: Run) => {
|
|
47
|
+
const doc = JSON.parse(r.stdout);
|
|
48
|
+
if ("item" in doc || "error" in doc) contract(workSchema).expectValid(doc);
|
|
49
|
+
return doc;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let root: string;
|
|
53
|
+
beforeAll(() => {
|
|
54
|
+
root = repo({
|
|
55
|
+
"chant.workspace.json": JSON.stringify(
|
|
56
|
+
{
|
|
57
|
+
name: "studio",
|
|
58
|
+
schema: 1,
|
|
59
|
+
members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
|
|
60
|
+
records: [{ kind: "decisions/decision.kind.mjs" }, { kind: WORK }],
|
|
61
|
+
},
|
|
62
|
+
null,
|
|
63
|
+
2,
|
|
64
|
+
),
|
|
65
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
66
|
+
"decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
|
|
67
|
+
"decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
|
|
68
|
+
"work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
|
|
69
|
+
"work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
|
|
70
|
+
"work/W-001-server.md": work("W-001"),
|
|
71
|
+
"work/W-002-next.md": work("W-002"),
|
|
72
|
+
"work/W-003-done.md": work("W-003", { state: "done", closed_on: "2026-09-25", evidence: [{ title: "The review", url: "https://example.com/review" }] }),
|
|
73
|
+
});
|
|
74
|
+
git(root, "add", "-A");
|
|
75
|
+
git(root, "commit", "-q", "-m", "the queue");
|
|
76
|
+
});
|
|
77
|
+
afterAll(cleanScratch);
|
|
78
|
+
|
|
79
|
+
describe("chant workspace work (#2732)", () => {
|
|
80
|
+
test("two processes claiming one work item: one wins, and the other is refused with the holder named", async () => {
|
|
81
|
+
const [a, b] = await Promise.all([
|
|
82
|
+
chant(root, ["workspace", "work", "claim", "W-001", "--holder", "worker-a", "--ttl", "600", "--json"]),
|
|
83
|
+
chant(root, ["workspace", "work", "claim", "W-001", "--holder", "worker-b", "--ttl", "600", "--json"]),
|
|
84
|
+
]);
|
|
85
|
+
const runs = [a, b];
|
|
86
|
+
const won = runs.filter((r) => r.code === 0);
|
|
87
|
+
const lost = runs.filter((r) => r.code === 2);
|
|
88
|
+
expect(won, `${a.stderr}\n${b.stderr}`).toHaveLength(1);
|
|
89
|
+
expect(lost).toHaveLength(1);
|
|
90
|
+
const winner = json(won[0]);
|
|
91
|
+
expect(winner).toMatchObject({ item: "W-001", event: "claim", kind: WORK, ref: "refs/chant/lease/work/W-001", history: { path: "_leases/W-001.jsonl" } });
|
|
92
|
+
const refusal = json(lost[0]);
|
|
93
|
+
expect(refusal.refused.heldBy.holder).toBe(winner.lease.holder);
|
|
94
|
+
expect(refusal.refused.code).toMatch(/^lease-(held|race)$/);
|
|
95
|
+
expect(refusal.refused.message).toContain(winner.lease.holder);
|
|
96
|
+
expect(git(root, "show", "chant/lifecycle:_leases/W-001.jsonl").split("\n")).toHaveLength(1);
|
|
97
|
+
}, 120_000);
|
|
98
|
+
|
|
99
|
+
test("records --json gives each work item its active lease, and status --json lists active and expired leases", async () => {
|
|
100
|
+
const claim = await chant(root, ["workspace", "work", "claim", "W-002", "--holder", "worker-c", "--ttl", "1", "--json"]);
|
|
101
|
+
expect(claim.code, claim.stderr).toBe(0);
|
|
102
|
+
await new Promise((r) => setTimeout(r, 1_100));
|
|
103
|
+
|
|
104
|
+
const records = await chant(root, ["workspace", "records", "--kind", WORK, "--json"]);
|
|
105
|
+
expect(records.code, records.stderr).toBe(0);
|
|
106
|
+
const doc = json(records);
|
|
107
|
+
contract(recordsSchema).expectValid(doc);
|
|
108
|
+
const byId = new Map((doc.records as { id: string; lease?: unknown }[]).map((r) => [r.id, r.lease]));
|
|
109
|
+
expect(byId.get("W-001")).toMatchObject({ holder: expect.stringMatching(/^worker-[ab]$/) });
|
|
110
|
+
expect(byId.get("W-002")).toBeNull(); // expired, so not active
|
|
111
|
+
expect(byId.get("W-003")).toBeNull();
|
|
112
|
+
|
|
113
|
+
const status = await chant(root, ["workspace", "status", "dev", "--json"]);
|
|
114
|
+
expect(status.code, status.stderr).toBe(0);
|
|
115
|
+
const sdoc = json(status);
|
|
116
|
+
contract(statusSchema).expectValid(sdoc);
|
|
117
|
+
expect(sdoc.leases.map((l: { item: string; state: string; member: string | null }) => [l.item, l.state, l.member])).toEqual([
|
|
118
|
+
["W-001", "active", null],
|
|
119
|
+
["W-002", "expired", null],
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// An expired lease can be claimed again, with a new token.
|
|
123
|
+
const again = await chant(root, ["workspace", "work", "claim", "W-002", "--holder", "worker-d", "--json"]);
|
|
124
|
+
expect(again.code, again.stderr).toBe(0);
|
|
125
|
+
expect(json(again).lease.token).not.toBe(json(claim).lease.token);
|
|
126
|
+
}, 120_000);
|
|
127
|
+
|
|
128
|
+
test("renew keeps the token and moves the expiry; release frees the item; the history has a line for each", async () => {
|
|
129
|
+
const claim = json(await chant(root, ["workspace", "work", "claim", "W-001", "--holder", "nobody", "--json"]));
|
|
130
|
+
const holder = claim.refused.heldBy.holder as string;
|
|
131
|
+
const token = claim.refused.heldBy.token as string;
|
|
132
|
+
|
|
133
|
+
const renew = await chant(root, ["workspace", "work", "renew", "W-001", "--holder", holder, "--token", token, "--ttl", "20m", "--json"]);
|
|
134
|
+
expect(renew.code, renew.stderr).toBe(0);
|
|
135
|
+
const renewed = json(renew).lease;
|
|
136
|
+
expect(renewed.token).toBe(token);
|
|
137
|
+
expect(Date.parse(renewed.expiresAt)).toBeGreaterThan(Date.parse(claim.refused.heldBy.expiresAt));
|
|
138
|
+
|
|
139
|
+
const wrong = await chant(root, ["workspace", "work", "release", "W-001", "--holder", "nobody", "--json"]);
|
|
140
|
+
expect(wrong.code).toBe(2);
|
|
141
|
+
expect(json(wrong).refused.code).toBe("lease-held");
|
|
142
|
+
|
|
143
|
+
const release = await chant(root, ["workspace", "work", "release", "W-001", "--holder", holder, "--outcome", "done"]);
|
|
144
|
+
expect(release.code, release.stderr).toBe(0);
|
|
145
|
+
expect(release.stdout).toContain("W-001 released by");
|
|
146
|
+
|
|
147
|
+
const events = git(root, "show", "chant/lifecycle:_leases/W-001.jsonl").split("\n").map((l) => JSON.parse(l));
|
|
148
|
+
expect(events.map((e) => e.event)).toEqual(["claim", "renew", "release"]);
|
|
149
|
+
expect(events[2]).toMatchObject({ holder, by: holder, outcome: "done", token });
|
|
150
|
+
}, 120_000);
|
|
151
|
+
|
|
152
|
+
test("an unknown item, a closed item and a bad verb are errors, not refusals", async () => {
|
|
153
|
+
const unknown = await chant(root, ["workspace", "work", "claim", "W-999", "--holder", "a", "--json"]);
|
|
154
|
+
expect(unknown.code).toBe(1);
|
|
155
|
+
expect(json(unknown).error.code).toBe("work-item-unknown");
|
|
156
|
+
const closed = await chant(root, ["workspace", "work", "claim", "W-003", "--holder", "a", "--json"]);
|
|
157
|
+
expect(closed.code).toBe(1);
|
|
158
|
+
expect(json(closed).error.code).toBe("work-item-closed");
|
|
159
|
+
const verb = await chant(root, ["workspace", "work", "grab", "W-001", "--holder", "a"]);
|
|
160
|
+
expect(verb.code).toBe(1);
|
|
161
|
+
expect(verb.stderr).toContain("claim, renew, release or history");
|
|
162
|
+
const noHolder = await chant(root, ["workspace", "work", "claim", "W-001"]);
|
|
163
|
+
expect(noHolder.code).toBe(1);
|
|
164
|
+
expect(noHolder.stderr).toContain("--holder");
|
|
165
|
+
}, 120_000);
|
|
166
|
+
|
|
167
|
+
test("the lease fields reach an MCP client through workspace-records and workspace-status", async () => {
|
|
168
|
+
const claim = await chant(root, ["workspace", "work", "claim", "W-001", "--holder", "mcp-worker", "--json"]);
|
|
169
|
+
expect(claim.code, claim.stderr).toBe(0);
|
|
170
|
+
const server = new McpServer([], { workspace: { cwd: root, chantCommand: CHANT } });
|
|
171
|
+
const call = async (name: string, args: Record<string, unknown>) => {
|
|
172
|
+
const res = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } });
|
|
173
|
+
return (res.result as { structuredContent: Record<string, unknown> }).structuredContent;
|
|
174
|
+
};
|
|
175
|
+
const records = (await call("workspace-records", { kind: WORK })) as { records: { id: string; lease: { holder: string } | null }[] };
|
|
176
|
+
expect(records.records.find((r) => r.id === "W-001")?.lease?.holder).toBe("mcp-worker");
|
|
177
|
+
const status = (await call("workspace-status", { env: "dev" })) as { leases: { item: string; holder: string; state: string }[] };
|
|
178
|
+
expect(status.leases.find((l) => l.item === "W-001")).toMatchObject({ holder: "mcp-worker", state: "active" });
|
|
179
|
+
}, 120_000);
|
|
180
|
+
});
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace work claim|renew|release <id> --holder <name>` (#2732,
|
|
3
|
+
* ws-055): the work lease, who is working on which work item. And `chant
|
|
4
|
+
* workspace work history <id>` (#2785): the item's lease history, each claim
|
|
5
|
+
* and how it ended, a read-contract output (`work-history.schema.json`).
|
|
6
|
+
*
|
|
7
|
+
* The lease itself is `../lifecycle/work-lease.ts`, the operator lease under
|
|
8
|
+
* the key `work/<id>`. This file finds the work item first: in the work kind
|
|
9
|
+
* `--kind` names, or in the one work kind the declaration names that has a
|
|
10
|
+
* record with the id. A claim on an id no work record has is refused, and so
|
|
11
|
+
* is a claim on a closed item (done or dropped); renew and release are not,
|
|
12
|
+
* so a lease can always be given back.
|
|
13
|
+
*
|
|
14
|
+
* The lease lives in the ledger of the member that owns the work kind file
|
|
15
|
+
* (#2524 D7), whatever directory the command runs in: `records --json`
|
|
16
|
+
* reports it from there too.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes: 0 when the lease was written, 2 when it was refused (someone
|
|
19
|
+
* holds it, it expired, another clone got there first), 1 when the command
|
|
20
|
+
* could not run (usage, an unreadable kind, an unknown item).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { dirname, relative } from "node:path";
|
|
24
|
+
import { formatError } from "../cli/format";
|
|
25
|
+
import type { CommandContext } from "../cli/registry";
|
|
26
|
+
import { parseDuration } from "../op/duration";
|
|
27
|
+
import {
|
|
28
|
+
claimWorkLease,
|
|
29
|
+
leaseHistoryPath,
|
|
30
|
+
listWorkLeases,
|
|
31
|
+
readLeaseHistory,
|
|
32
|
+
releaseWorkLease,
|
|
33
|
+
renewWorkLease,
|
|
34
|
+
workLeaseRef,
|
|
35
|
+
WORK_ITEM_ID_PATTERN,
|
|
36
|
+
type WorkLease,
|
|
37
|
+
type WorkLeaseRefusal,
|
|
38
|
+
type WorkLeaseResult,
|
|
39
|
+
type LeaseHistoryWrite,
|
|
40
|
+
type LeaseHistoryRecord,
|
|
41
|
+
type WorkLeaseState,
|
|
42
|
+
} from "../lifecycle/work-lease";
|
|
43
|
+
import { resolveMemberLedger } from "../lifecycle/member-ledger";
|
|
44
|
+
import { readerVersion, WorkspaceReadError } from "./declaration";
|
|
45
|
+
import { declaredKindFiles, readRecordsFor } from "./records-cli";
|
|
46
|
+
import { loadRecordKind, RecordReadError, type LoadedRecordKind, type RecordEntry } from "./records";
|
|
47
|
+
import { gitRoot } from "./record-source";
|
|
48
|
+
import type { ReasonCode } from "./reason-codes";
|
|
49
|
+
|
|
50
|
+
/** The version of the `work` output this chant writes. */
|
|
51
|
+
export const WORK_LEASE_CONTRACT_VERSION = 1;
|
|
52
|
+
|
|
53
|
+
/** `$id` of the JSON Schema for the `--json` output, shipped beside this file. */
|
|
54
|
+
export const WORK_LEASE_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/work-lease/v1/work-lease.schema.json";
|
|
55
|
+
|
|
56
|
+
export { WORK_LEASE_REFUSALS } from "../lifecycle/work-lease";
|
|
57
|
+
|
|
58
|
+
const USAGE = [
|
|
59
|
+
"chant workspace work claim|renew|release <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--outcome <text>] [--note <text>] [--json]",
|
|
60
|
+
"chant workspace work history <id> [--kind <kind file>] [--json]",
|
|
61
|
+
].join("\n");
|
|
62
|
+
|
|
63
|
+
/** Exit code when the lease was refused rather than the command failing. */
|
|
64
|
+
export const EXIT_LEASE_REFUSED = 2;
|
|
65
|
+
|
|
66
|
+
export type WorkVerb = "claim" | "renew" | "release";
|
|
67
|
+
|
|
68
|
+
/** Why the command could not run. Closed. */
|
|
69
|
+
export const WORK_ERROR_CODES = [
|
|
70
|
+
"write-usage-invalid",
|
|
71
|
+
"kind-unreadable",
|
|
72
|
+
"work-kind-missing",
|
|
73
|
+
"work-kind-ambiguous",
|
|
74
|
+
"work-item-unknown",
|
|
75
|
+
"work-item-closed",
|
|
76
|
+
"not-a-git-repository",
|
|
77
|
+
] as const satisfies readonly ReasonCode[];
|
|
78
|
+
export type WorkErrorCode = (typeof WORK_ERROR_CODES)[number];
|
|
79
|
+
|
|
80
|
+
interface DocHead {
|
|
81
|
+
$schema: string;
|
|
82
|
+
contract: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What `--json` prints. */
|
|
86
|
+
export type WorkLeaseDocument = DocHead &
|
|
87
|
+
(
|
|
88
|
+
| {
|
|
89
|
+
item: string;
|
|
90
|
+
event: WorkVerb;
|
|
91
|
+
/** The work kind file, from the repository root. */
|
|
92
|
+
kind: string;
|
|
93
|
+
ref: string;
|
|
94
|
+
lease: Omit<WorkLease, "item">;
|
|
95
|
+
history: LeaseHistoryWrite;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
item: string;
|
|
99
|
+
event: WorkVerb;
|
|
100
|
+
kind: string;
|
|
101
|
+
ref: string;
|
|
102
|
+
refused: { code: WorkLeaseRefusal; message: string; heldBy: Omit<WorkLease, "item"> | null };
|
|
103
|
+
}
|
|
104
|
+
| { error: { code: WorkErrorCode; message: string } }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
/** Why a work command could not run, with its code. */
|
|
108
|
+
export class WorkError extends Error {
|
|
109
|
+
constructor(
|
|
110
|
+
readonly code: WorkErrorCode,
|
|
111
|
+
message: string,
|
|
112
|
+
) {
|
|
113
|
+
super(message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A work item found by id: its kind, its state, its record, and the repository root its path is from. */
|
|
118
|
+
export interface FoundWorkItem {
|
|
119
|
+
loaded: LoadedRecordKind;
|
|
120
|
+
state: string | null;
|
|
121
|
+
record: RecordEntry;
|
|
122
|
+
root: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The work kind holding `id`: the one `kind` names, or the one declared work
|
|
127
|
+
* kind with a record `id`. Throws a {@link WorkError}.
|
|
128
|
+
*/
|
|
129
|
+
export async function findWorkItem(id: string, cwd: string, kind: string | undefined): Promise<FoundWorkItem> {
|
|
130
|
+
let candidates: string[];
|
|
131
|
+
if (kind !== undefined) {
|
|
132
|
+
candidates = [kind];
|
|
133
|
+
} else {
|
|
134
|
+
try {
|
|
135
|
+
candidates = declaredKindFiles(cwd).map((k) => k.file);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
138
|
+
throw new WorkError("work-kind-missing", `${err.code}: ${err.describe()}; without --kind, the declaration names the work kind`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const found: FoundWorkItem[] = [];
|
|
142
|
+
let workKinds = 0;
|
|
143
|
+
for (const file of candidates) {
|
|
144
|
+
let loaded: LoadedRecordKind;
|
|
145
|
+
try {
|
|
146
|
+
loaded = await loadRecordKind(file, cwd);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof RecordReadError && kind !== undefined) throw new WorkError("kind-unreadable", `${err.code}: ${err.message}`);
|
|
149
|
+
if (err instanceof RecordReadError) continue;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
if (!loaded.kind.work) {
|
|
153
|
+
if (kind !== undefined) throw new WorkError("work-kind-missing", `${kind} is the ${loaded.kind.name} kind, which has no work block`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
workKinds++;
|
|
157
|
+
let read;
|
|
158
|
+
try {
|
|
159
|
+
read = await readRecordsFor({ kind: loaded.file, cwd });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err instanceof RecordReadError) throw new WorkError("kind-unreadable", `${err.code}: ${err.message}`);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
const record = read.result.records.find((r) => r.id === id);
|
|
165
|
+
if (record) found.push({ loaded, state: record.state, record, root: read.root });
|
|
166
|
+
}
|
|
167
|
+
if (workKinds === 0) throw new WorkError("work-kind-missing", "the declaration names no work kind; pass --kind <kind file>");
|
|
168
|
+
if (found.length === 0) throw new WorkError("work-item-unknown", `no work record has the id ${id}`);
|
|
169
|
+
if (found.length > 1) throw new WorkError("work-kind-ambiguous", `${found.length} work kinds have a record ${id}; pass --kind <kind file>`);
|
|
170
|
+
return found[0];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Parse `--ttl`: a whole number of seconds, or a duration such as 10m. */
|
|
174
|
+
export function parseTtl(raw: string): number {
|
|
175
|
+
if (/^\d+$/.test(raw)) return Number(raw) * 1000;
|
|
176
|
+
return parseDuration(raw);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface WorkLeaseRequest {
|
|
180
|
+
verb: WorkVerb;
|
|
181
|
+
id: string;
|
|
182
|
+
holder: string;
|
|
183
|
+
cwd: string;
|
|
184
|
+
kind?: string;
|
|
185
|
+
ttlMs?: number;
|
|
186
|
+
token?: string;
|
|
187
|
+
outcome?: string;
|
|
188
|
+
note?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Run one claim, renew or release and build the document `--json` prints. */
|
|
192
|
+
export async function workLease(req: WorkLeaseRequest): Promise<WorkLeaseDocument> {
|
|
193
|
+
const doc = { $schema: WORK_LEASE_OUTPUT_SCHEMA_ID, contract: WORK_LEASE_CONTRACT_VERSION };
|
|
194
|
+
try {
|
|
195
|
+
if (!WORK_ITEM_ID_PATTERN.test(req.id) || req.id.includes("..")) throw new WorkError("write-usage-invalid", `${JSON.stringify(req.id)} is not a work item id`);
|
|
196
|
+
if (!gitRoot(req.cwd)) throw new WorkError("not-a-git-repository", "a work lease is a git ref, and this directory is not in a git repository");
|
|
197
|
+
const { loaded, state, root } = await findWorkItem(req.id, req.cwd, req.kind);
|
|
198
|
+
if (req.verb === "claim" && state !== null && (loaded.kind.closedStates ?? []).includes(state)) {
|
|
199
|
+
throw new WorkError("work-item-closed", `${req.id} is ${state}, a closed state; there is no work left to claim`);
|
|
200
|
+
}
|
|
201
|
+
const leaseCwd = dirname(loaded.file);
|
|
202
|
+
const { prefix } = await resolveMemberLedger(leaseCwd);
|
|
203
|
+
const opts = { cwd: leaseCwd, ...(req.note ? { note: req.note } : {}) };
|
|
204
|
+
let result: WorkLeaseResult;
|
|
205
|
+
if (req.verb === "claim") result = await claimWorkLease(req.id, req.holder, { ...opts, ttlMs: req.ttlMs });
|
|
206
|
+
else if (req.verb === "renew") result = await renewWorkLease(req.id, req.holder, { ...opts, ttlMs: req.ttlMs, token: req.token });
|
|
207
|
+
else result = await releaseWorkLease(req.id, req.holder, { ...opts, token: req.token, outcome: req.outcome });
|
|
208
|
+
const head = { ...doc, item: req.id, event: req.verb, kind: relative(root, loaded.file).split("\\").join("/"), ref: workLeaseRef(req.id, prefix) };
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
const heldBy = result.heldBy ? strip(result.heldBy) : null;
|
|
211
|
+
return { ...head, refused: { code: result.reason, message: result.message, heldBy } };
|
|
212
|
+
}
|
|
213
|
+
return { ...head, lease: strip(result.lease), history: result.history };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err instanceof WorkError) return { ...doc, error: { code: err.code, message: err.message } };
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function strip(l: WorkLease): Omit<WorkLease, "item"> {
|
|
221
|
+
return { holder: l.holder, token: l.token, acquiredAt: l.acquiredAt, expiresAt: l.expiresAt };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── work history (#2785) ────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/** The version of the `work history` output this chant writes: read contract 1. */
|
|
227
|
+
export const WORK_HISTORY_CONTRACT_VERSION = 1;
|
|
228
|
+
|
|
229
|
+
/** `$id` of the JSON Schema for `work history --json`, shipped beside this file. */
|
|
230
|
+
export const WORK_HISTORY_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json";
|
|
231
|
+
|
|
232
|
+
/** Why `work history` read nothing. Closed. */
|
|
233
|
+
export const WORK_HISTORY_ERROR_CODES = [
|
|
234
|
+
"kind-unreadable",
|
|
235
|
+
"work-kind-missing",
|
|
236
|
+
"work-kind-ambiguous",
|
|
237
|
+
"work-item-unknown",
|
|
238
|
+
"not-a-git-repository",
|
|
239
|
+
] as const satisfies readonly ReasonCode[];
|
|
240
|
+
export type WorkHistoryErrorCode = (typeof WORK_HISTORY_ERROR_CODES)[number];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* How a claim ended. `released`: its holder, or someone closing out an
|
|
244
|
+
* expired lease, gave it back, with an outcome when one was given. `held`: it
|
|
245
|
+
* is the live lease now. `expired`: it ran out and nobody released it or
|
|
246
|
+
* claimed the item since. `lost`: it ran out unreleased and another claim
|
|
247
|
+
* took the item.
|
|
248
|
+
*/
|
|
249
|
+
export type ClaimEnd = "released" | "held" | "expired" | "lost";
|
|
250
|
+
|
|
251
|
+
/** One claim of a work item: one fencing token, from its claim to how it ended. */
|
|
252
|
+
export interface LeaseClaim {
|
|
253
|
+
token: string;
|
|
254
|
+
holder: string;
|
|
255
|
+
acquiredAt: string;
|
|
256
|
+
/** The expiry its last claim or renew set. */
|
|
257
|
+
expiresAt: string;
|
|
258
|
+
renewals: number;
|
|
259
|
+
ended: ClaimEnd;
|
|
260
|
+
/** The release, when it was released. */
|
|
261
|
+
release: { by: string; at: string; outcome: string | null; note: string | null } | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** What `work history --json` prints. */
|
|
265
|
+
export type WorkHistoryDocument =
|
|
266
|
+
| {
|
|
267
|
+
$schema: string;
|
|
268
|
+
contract: number;
|
|
269
|
+
chant: string;
|
|
270
|
+
item: string;
|
|
271
|
+
/** The work kind file, from the repository root. */
|
|
272
|
+
kind: string;
|
|
273
|
+
/** The lease ref. */
|
|
274
|
+
ref: string;
|
|
275
|
+
/** The history file on the branch, under the member's prefix when the kind's member has one. */
|
|
276
|
+
ledger: { branch: string; path: string };
|
|
277
|
+
/** The lease ref's record now, live or expired, or null when there is none. Read from local refs, without fetching. */
|
|
278
|
+
lease: (Omit<WorkLease, "item"> & { state: "active" | "expired" }) | null;
|
|
279
|
+
/** Each claim, oldest first. */
|
|
280
|
+
claims: LeaseClaim[];
|
|
281
|
+
/** Every line of the history, oldest first. */
|
|
282
|
+
events: LeaseHistoryRecord[];
|
|
283
|
+
/** Lines of the history that are not lease events, left out. */
|
|
284
|
+
malformed: number;
|
|
285
|
+
summary: { claims: number; released: number; held: number; expired: number; lost: number; outcomes: Record<string, number> };
|
|
286
|
+
}
|
|
287
|
+
| { $schema: string; contract: number; chant: string; error: { code: WorkHistoryErrorCode; message: string } };
|
|
288
|
+
|
|
289
|
+
/** Fold a history into its claims: one per token, in the order they were first seen. */
|
|
290
|
+
export function claimsOf(events: LeaseHistoryRecord[], live: Pick<WorkLeaseState, "token" | "state"> | null): LeaseClaim[] {
|
|
291
|
+
const byToken = new Map<string, LeaseClaim>();
|
|
292
|
+
const order: string[] = [];
|
|
293
|
+
for (const e of events) {
|
|
294
|
+
let c = byToken.get(e.token);
|
|
295
|
+
if (!c) {
|
|
296
|
+
c = { token: e.token, holder: e.holder, acquiredAt: e.acquiredAt, expiresAt: e.expiresAt, renewals: 0, ended: "expired", release: null };
|
|
297
|
+
byToken.set(e.token, c);
|
|
298
|
+
order.push(e.token);
|
|
299
|
+
}
|
|
300
|
+
if (e.event === "renew") c.renewals++;
|
|
301
|
+
if (e.event !== "release") c.expiresAt = e.expiresAt;
|
|
302
|
+
if (e.event === "release") c.release = { by: e.by, at: e.timestamp, outcome: e.outcome ?? null, note: e.note ?? null };
|
|
303
|
+
}
|
|
304
|
+
return order.map((token, i) => {
|
|
305
|
+
const c = byToken.get(token)!;
|
|
306
|
+
if (c.release) c.ended = "released";
|
|
307
|
+
else if (live && live.token === token) c.ended = live.state === "active" ? "held" : "expired";
|
|
308
|
+
else if (i < order.length - 1) c.ended = "lost";
|
|
309
|
+
else c.ended = "expired";
|
|
310
|
+
return c;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Read work item `id`'s lease history (#2785): every claim, renew and release
|
|
316
|
+
* on `_leases/<id>.jsonl` in the ledger of the member owning the work kind,
|
|
317
|
+
* folded into claims. Read from the local `chant/lifecycle` branch and lease
|
|
318
|
+
* refs; it never fetches, like the other read-contract reads.
|
|
319
|
+
*/
|
|
320
|
+
export async function workHistory(req: { id: string; cwd: string; kind?: string }): Promise<WorkHistoryDocument> {
|
|
321
|
+
const head = { $schema: WORK_HISTORY_OUTPUT_SCHEMA_ID, contract: WORK_HISTORY_CONTRACT_VERSION, chant: readerVersion() };
|
|
322
|
+
try {
|
|
323
|
+
if (!WORK_ITEM_ID_PATTERN.test(req.id) || req.id.includes("..")) throw new WorkError("work-item-unknown", `${JSON.stringify(req.id)} is not a work item id`);
|
|
324
|
+
if (!gitRoot(req.cwd)) throw new WorkError("not-a-git-repository", "a work lease's history is on the chant/lifecycle branch, and this directory is not in a git repository");
|
|
325
|
+
const { loaded, root } = await findWorkItem(req.id, req.cwd, req.kind);
|
|
326
|
+
const leaseCwd = dirname(loaded.file);
|
|
327
|
+
const { prefix } = await resolveMemberLedger(leaseCwd);
|
|
328
|
+
const { records: events, malformed } = await readLeaseHistory(req.id, { cwd: leaseCwd });
|
|
329
|
+
const live = (await listWorkLeases({ cwd: leaseCwd, memberPrefix: prefix })).find((l) => l.item === req.id) ?? null;
|
|
330
|
+
const claims = claimsOf(events, live);
|
|
331
|
+
const count = (end: ClaimEnd) => claims.filter((c) => c.ended === end).length;
|
|
332
|
+
const outcomes: Record<string, number> = {};
|
|
333
|
+
for (const c of claims) if (c.release?.outcome) outcomes[c.release.outcome] = (outcomes[c.release.outcome] ?? 0) + 1;
|
|
334
|
+
return {
|
|
335
|
+
...head,
|
|
336
|
+
item: req.id,
|
|
337
|
+
kind: relative(root, loaded.file).split("\\").join("/"),
|
|
338
|
+
ref: workLeaseRef(req.id, prefix),
|
|
339
|
+
ledger: { branch: "chant/lifecycle", path: leaseHistoryPath(req.id, prefix) },
|
|
340
|
+
lease: live ? { holder: live.holder, token: live.token, acquiredAt: live.acquiredAt, expiresAt: live.expiresAt, state: live.state } : null,
|
|
341
|
+
claims,
|
|
342
|
+
events,
|
|
343
|
+
malformed,
|
|
344
|
+
summary: { claims: claims.length, released: count("released"), held: count("held"), expired: count("expired"), lost: count("lost"), outcomes },
|
|
345
|
+
};
|
|
346
|
+
} catch (err) {
|
|
347
|
+
if (err instanceof WorkError) return { ...head, error: { code: err.code as WorkHistoryErrorCode, message: err.message } };
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The history as lines for a person. */
|
|
353
|
+
export function formatWorkHistory(doc: Extract<WorkHistoryDocument, { claims: unknown }>): string {
|
|
354
|
+
const lines = [`${doc.item}: ${doc.ledger.branch}:${doc.ledger.path}`];
|
|
355
|
+
for (const c of doc.claims) {
|
|
356
|
+
const how = c.release ? `released${c.release.outcome ? ` ${c.release.outcome}` : ""} by ${c.release.by} at ${c.release.at}` : c.ended === "held" ? `held until ${c.expiresAt}` : `${c.ended}, never released (expired ${c.expiresAt})`;
|
|
357
|
+
lines.push(` ${c.acquiredAt} ${c.holder} token ${c.token} ${c.renewals} renewal${c.renewals === 1 ? "" : "s"} ${how}`);
|
|
358
|
+
}
|
|
359
|
+
if (doc.claims.length === 0) lines.push(" never claimed");
|
|
360
|
+
if (doc.malformed > 0) lines.push(` ${doc.malformed} malformed line${doc.malformed === 1 ? "" : "s"} left out`);
|
|
361
|
+
return lines.join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runWorkHistory(ctx: CommandContext): Promise<number> {
|
|
365
|
+
const { args } = ctx;
|
|
366
|
+
const id = args.extraPositional2;
|
|
367
|
+
if (!id) {
|
|
368
|
+
console.error(formatError({ message: "history needs a work item id", hint: USAGE }));
|
|
369
|
+
return 1;
|
|
370
|
+
}
|
|
371
|
+
for (const [flag, v] of [["--holder", args.holder], ["--ttl", args.ttl], ["--token", args.token], ["--outcome", args.outcome], ["--note", args.note]] as const) {
|
|
372
|
+
if (v !== undefined) {
|
|
373
|
+
console.error(formatError({ message: `history is a read and takes no ${flag}`, hint: USAGE }));
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const doc = await workHistory({ id, cwd: process.cwd(), kind: args.kind });
|
|
378
|
+
if (args.json) console.log(JSON.stringify(doc, null, 2));
|
|
379
|
+
else if ("error" in doc) console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
|
|
380
|
+
else console.log(formatWorkHistory(doc));
|
|
381
|
+
return "error" in doc ? 1 : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function runWorkspaceWork(ctx: CommandContext): Promise<number> {
|
|
385
|
+
const { args } = ctx;
|
|
386
|
+
const verb = args.extraPositional;
|
|
387
|
+
if (verb === "history") return runWorkHistory(ctx);
|
|
388
|
+
const fail = (message: string): number => {
|
|
389
|
+
if (args.json) console.log(JSON.stringify({ $schema: WORK_LEASE_OUTPUT_SCHEMA_ID, contract: WORK_LEASE_CONTRACT_VERSION, error: { code: "write-usage-invalid", message } }, null, 2));
|
|
390
|
+
else console.error(formatError({ message, hint: USAGE }));
|
|
391
|
+
return 1;
|
|
392
|
+
};
|
|
393
|
+
if (verb !== "claim" && verb !== "renew" && verb !== "release") {
|
|
394
|
+
return fail(verb ? `chant workspace work takes claim, renew, release or history, not ${verb}` : "chant workspace work needs claim, renew, release or history");
|
|
395
|
+
}
|
|
396
|
+
const id = args.extraPositional2;
|
|
397
|
+
if (!id) return fail(`${verb} needs a work item id`);
|
|
398
|
+
if (!args.holder) return fail(`${verb} needs --holder <name>: who ${verb === "release" ? "gives the lease back" : "holds the lease"}`);
|
|
399
|
+
if (verb === "release" && args.ttl !== undefined) return fail("release takes no --ttl");
|
|
400
|
+
if (verb !== "release" && args.outcome !== undefined) return fail(`${verb} takes no --outcome; it is for release`);
|
|
401
|
+
if (verb === "claim" && args.token !== undefined) return fail("claim takes no --token; a claim mints one");
|
|
402
|
+
let ttlMs: number | undefined;
|
|
403
|
+
if (args.ttl !== undefined) {
|
|
404
|
+
try {
|
|
405
|
+
ttlMs = parseTtl(args.ttl);
|
|
406
|
+
} catch {
|
|
407
|
+
return fail(`--ttl takes seconds or a duration such as 10m, not ${JSON.stringify(args.ttl)}`);
|
|
408
|
+
}
|
|
409
|
+
if (ttlMs <= 0) return fail("--ttl must be more than zero");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const doc = await workLease({ verb, id, holder: args.holder, cwd: process.cwd(), kind: args.kind, ttlMs, token: args.token, outcome: args.outcome, note: args.note });
|
|
413
|
+
if (args.json) console.log(JSON.stringify(doc, null, 2));
|
|
414
|
+
if ("error" in doc) {
|
|
415
|
+
if (!args.json) console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
if ("refused" in doc) {
|
|
419
|
+
if (!args.json) console.error(formatError({ message: `${doc.refused.code}: ${doc.refused.message}` }));
|
|
420
|
+
return EXIT_LEASE_REFUSED;
|
|
421
|
+
}
|
|
422
|
+
if (!args.json) {
|
|
423
|
+
const what = verb === "claim" ? "claimed" : verb === "renew" ? "renewed" : "released";
|
|
424
|
+
const until = verb === "release" ? "" : ` until ${doc.lease.expiresAt}`;
|
|
425
|
+
console.log(`${id} ${what} by ${args.holder}${until} (token ${doc.lease.token})`);
|
|
426
|
+
console.log(` ${doc.ref}; history ${doc.history.path}${doc.history.pushed ? "" : " (not pushed)"}`);
|
|
427
|
+
}
|
|
428
|
+
return 0;
|
|
429
|
+
}
|