@aiwg/cli 2026.9.20 → 2026.9.21

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.
Files changed (272) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +58 -6
  2. package/agentic/code/providers/model-capabilities.v1.json +34 -0
  3. package/agentic/code/providers/model-catalog.v1.json +55 -13
  4. package/bin/aiwg.mjs +2 -0
  5. package/dist/src/api/index.d.ts +1 -0
  6. package/dist/src/api/index.js +1 -0
  7. package/dist/src/artifacts/enrichment/cli.js +1 -1
  8. package/dist/src/artifacts/views/cli.js +1 -1
  9. package/dist/src/audit/operator-decision.js +10 -0
  10. package/dist/src/auth/credential-store.js +127 -27
  11. package/dist/src/catalog/builtin-models.json +272 -40
  12. package/dist/src/catalog/cli.js +1 -1
  13. package/dist/src/catalog/cli.mjs +1 -1
  14. package/dist/src/cli/handlers/effect.js +872 -0
  15. package/dist/src/cli/handlers/help.js +2 -0
  16. package/dist/src/cli/handlers/index.js +4 -2
  17. package/dist/src/cli/handlers/models.js +41 -10
  18. package/dist/src/cli/handlers/ralph.js +1 -1
  19. package/dist/src/cli/handlers/sessions.js +113 -37
  20. package/dist/src/cli/handlers/subcommands.js +31 -0
  21. package/dist/src/cli/handlers/use.js +46 -9
  22. package/dist/src/cli/handlers/utilities.js +10 -0
  23. package/dist/src/cli/handlers/workspace.js +3 -1
  24. package/dist/src/cli/scope-resolver.js +112 -9
  25. package/dist/src/cli/services/deployment-verification.js +5 -0
  26. package/dist/src/config/aiwg-config.js +3 -0
  27. package/dist/src/decision/acceptance-rollout.js +93 -0
  28. package/dist/src/decision/acceptance.js +175 -0
  29. package/dist/src/decision/adapters/jev.js +234 -13
  30. package/dist/src/decision/adapters/llm-subagent.js +60 -12
  31. package/dist/src/decision/admission.js +550 -0
  32. package/dist/src/decision/batch-benchmark.js +47 -0
  33. package/dist/src/decision/batch-receipts/accounting.js +75 -0
  34. package/dist/src/decision/batch-receipts/export.js +40 -0
  35. package/dist/src/decision/batch-receipts/index.js +9 -0
  36. package/dist/src/decision/batch-receipts/protection.js +40 -0
  37. package/dist/src/decision/batch-receipts/receipt.js +44 -0
  38. package/dist/src/decision/batch-receipts/result-store.js +341 -0
  39. package/dist/src/decision/batch-receipts/store.js +362 -0
  40. package/dist/src/decision/batch-receipts/types.js +2 -0
  41. package/dist/src/decision/batch-receipts/validate.js +127 -0
  42. package/dist/src/decision/batch.js +68 -0
  43. package/dist/src/decision/calibration/index.js +4 -0
  44. package/dist/src/decision/calibration/receipts.js +130 -0
  45. package/dist/src/decision/calibration/registry.js +258 -0
  46. package/dist/src/decision/calibration/types.js +2 -0
  47. package/dist/src/decision/compile-cache/benchmark.js +62 -0
  48. package/dist/src/decision/compile-cache/compatibility.js +65 -0
  49. package/dist/src/decision/compile-cache/file-store.js +280 -0
  50. package/dist/src/decision/compile-cache/identity.js +26 -0
  51. package/dist/src/decision/compile-cache/index.js +9 -0
  52. package/dist/src/decision/compile-cache/prefix.js +21 -0
  53. package/dist/src/decision/compile-cache/records.js +86 -0
  54. package/dist/src/decision/compile-cache/runtime.js +68 -0
  55. package/dist/src/decision/compile-cache/store.js +108 -0
  56. package/dist/src/decision/compile-cache/types.js +2 -0
  57. package/dist/src/decision/context-plan.js +234 -0
  58. package/dist/src/decision/context-qualification.js +60 -0
  59. package/dist/src/decision/ensemble/aggregate.js +215 -0
  60. package/dist/src/decision/ensemble/contract.js +316 -0
  61. package/dist/src/decision/ensemble/drift.js +51 -0
  62. package/dist/src/decision/ensemble/index.js +6 -0
  63. package/dist/src/decision/ensemble/report.js +71 -0
  64. package/dist/src/decision/ensemble/types.js +2 -0
  65. package/dist/src/decision/evaluate.js +1024 -78
  66. package/dist/src/decision/file-lifecycle-store.js +168 -0
  67. package/dist/src/decision/graph-beam.js +40 -0
  68. package/dist/src/decision/graph-budget.js +110 -0
  69. package/dist/src/decision/graph-decision-bridge.js +93 -0
  70. package/dist/src/decision/graph-evidence.js +123 -0
  71. package/dist/src/decision/graph-flow-adapter.js +56 -0
  72. package/dist/src/decision/graph-flow.js +105 -0
  73. package/dist/src/decision/graph-parallel.js +79 -0
  74. package/dist/src/decision/graph-patterns.js +20 -0
  75. package/dist/src/decision/graph-run-store.js +66 -0
  76. package/dist/src/decision/graph-run.js +166 -0
  77. package/dist/src/decision/graph-skill-bridge.js +91 -0
  78. package/dist/src/decision/graph-templates.js +56 -0
  79. package/dist/src/decision/graph.js +115 -0
  80. package/dist/src/decision/index.js +35 -0
  81. package/dist/src/decision/job-accounting.js +93 -0
  82. package/dist/src/decision/job-contract.js +186 -0
  83. package/dist/src/decision/job-effects.js +95 -0
  84. package/dist/src/decision/job-evaluate.js +80 -0
  85. package/dist/src/decision/job-gateway.js +121 -0
  86. package/dist/src/decision/job-http.js +194 -0
  87. package/dist/src/decision/job-payload-store.js +258 -0
  88. package/dist/src/decision/job-poll-limiter.js +92 -0
  89. package/dist/src/decision/job-quota.js +158 -0
  90. package/dist/src/decision/job-runtime.js +240 -0
  91. package/dist/src/decision/job-scheduler.js +26 -0
  92. package/dist/src/decision/job-service.js +117 -0
  93. package/dist/src/decision/job-store.js +264 -0
  94. package/dist/src/decision/job-worker.js +141 -0
  95. package/dist/src/decision/lifecycle.js +148 -0
  96. package/dist/src/decision/patterns/artifacts.js +173 -0
  97. package/dist/src/decision/patterns/catalog.js +108 -0
  98. package/dist/src/decision/patterns/durable-review.js +237 -0
  99. package/dist/src/decision/patterns/index.js +8 -0
  100. package/dist/src/decision/patterns/playground.js +401 -0
  101. package/dist/src/decision/patterns/recorded.js +43 -0
  102. package/dist/src/decision/patterns/specs.js +174 -0
  103. package/dist/src/decision/patterns/types.js +2 -0
  104. package/dist/src/decision/portable-secrets.js +53 -0
  105. package/dist/src/decision/projection.js +210 -0
  106. package/dist/src/decision/qualification/capture.js +67 -0
  107. package/dist/src/decision/qualification/drift.js +68 -0
  108. package/dist/src/decision/qualification/gate-evidence.js +172 -0
  109. package/dist/src/decision/qualification/gates.js +83 -0
  110. package/dist/src/decision/qualification/index.js +12 -0
  111. package/dist/src/decision/qualification/load.js +275 -0
  112. package/dist/src/decision/qualification/manifest.js +37 -0
  113. package/dist/src/decision/qualification/privacy.js +41 -0
  114. package/dist/src/decision/qualification/quality.js +213 -0
  115. package/dist/src/decision/qualification/release.js +116 -0
  116. package/dist/src/decision/qualification/runner.js +408 -0
  117. package/dist/src/decision/qualification/types.js +2 -0
  118. package/dist/src/decision/receipts.js +30 -7
  119. package/dist/src/decision/result-cache/index.js +6 -0
  120. package/dist/src/decision/result-cache/integrity.js +28 -0
  121. package/dist/src/decision/result-cache/key.js +28 -0
  122. package/dist/src/decision/result-cache/service.js +92 -0
  123. package/dist/src/decision/result-cache/store.js +327 -0
  124. package/dist/src/decision/result-cache/types.js +3 -0
  125. package/dist/src/decision/review/authorization.js +110 -0
  126. package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
  127. package/dist/src/decision/review/index.js +12 -0
  128. package/dist/src/decision/review/migrate.js +34 -0
  129. package/dist/src/decision/review/operator-audit.js +59 -0
  130. package/dist/src/decision/review/recovery-adapters.js +252 -0
  131. package/dist/src/decision/review/recovery.js +33 -0
  132. package/dist/src/decision/review/service.js +498 -0
  133. package/dist/src/decision/review/store.js +219 -0
  134. package/dist/src/decision/review/types.js +2 -0
  135. package/dist/src/decision/review/validate.js +205 -0
  136. package/dist/src/decision/scheduler.js +115 -0
  137. package/dist/src/decision/telemetry/context.js +42 -0
  138. package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
  139. package/dist/src/decision/telemetry/exporter.js +77 -0
  140. package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
  141. package/dist/src/decision/telemetry/index.js +13 -0
  142. package/dist/src/decision/telemetry/mapping.js +146 -0
  143. package/dist/src/decision/telemetry/metrics.js +164 -0
  144. package/dist/src/decision/telemetry/otlp-http.js +183 -0
  145. package/dist/src/decision/telemetry/redaction.js +93 -0
  146. package/dist/src/decision/telemetry/retention.js +109 -0
  147. package/dist/src/decision/telemetry/runtime.js +310 -0
  148. package/dist/src/decision/telemetry/trace.js +158 -0
  149. package/dist/src/decision/telemetry/types.js +3 -0
  150. package/dist/src/decision/validate.js +135 -5
  151. package/dist/src/effects/checkpoint-sinks.js +94 -0
  152. package/dist/src/effects/checkpoint.js +76 -0
  153. package/dist/src/effects/decision-links.js +54 -0
  154. package/dist/src/effects/errors.js +48 -0
  155. package/dist/src/effects/identity.js +178 -0
  156. package/dist/src/effects/index.js +25 -0
  157. package/dist/src/effects/keyring.js +160 -0
  158. package/dist/src/effects/keys.js +130 -0
  159. package/dist/src/effects/ledger.js +435 -0
  160. package/dist/src/effects/lifecycle.js +110 -0
  161. package/dist/src/effects/lock-recovery.js +269 -0
  162. package/dist/src/effects/reader.js +103 -0
  163. package/dist/src/effects/records.js +114 -0
  164. package/dist/src/effects/redaction.js +51 -0
  165. package/dist/src/effects/schema.js +73 -0
  166. package/dist/src/effects/store.js +239 -0
  167. package/dist/src/effects/types.js +49 -0
  168. package/dist/src/effects/verifiers/decision.js +168 -0
  169. package/dist/src/effects/verifiers/file.js +106 -0
  170. package/dist/src/effects/verifiers/git.js +206 -0
  171. package/dist/src/effects/verifiers/index.js +90 -0
  172. package/dist/src/effects/verifiers/registry.js +148 -0
  173. package/dist/src/effects/verifiers/review.js +123 -0
  174. package/dist/src/effects/verifiers/tracker.js +299 -0
  175. package/dist/src/effects/verifiers/types.js +21 -0
  176. package/dist/src/effects/verify.js +295 -0
  177. package/dist/src/evidence/bundle.js +2 -0
  178. package/dist/src/extensions/commands/definitions.js +28 -0
  179. package/dist/src/extensions/manifest.js +1 -0
  180. package/dist/src/marketplace/provenance.js +25 -27
  181. package/dist/src/mcp/cli.mjs +11 -1
  182. package/dist/src/mcp/grok-build-config.mjs +189 -0
  183. package/dist/src/mcp/registry.js +21 -1
  184. package/dist/src/mcp/registry.mjs +20 -1
  185. package/dist/src/models/grok-build-models.js +356 -0
  186. package/dist/src/models/index.js +1 -0
  187. package/dist/src/models/model-capabilities.v1.json +34 -0
  188. package/dist/src/models/model-catalog.v1.json +55 -13
  189. package/dist/src/models/model-discovery.js +41 -5
  190. package/dist/src/models/provider-policy.js +3 -2
  191. package/dist/src/plugin/skill-command-translator.js +1 -0
  192. package/dist/src/providers/capability-matrix.yaml +58 -6
  193. package/dist/src/providers/grok-build-acp.js +192 -0
  194. package/dist/src/providers/grok-build-dispatch.js +251 -0
  195. package/dist/src/providers/grok-build-headless.js +170 -0
  196. package/dist/src/providers/muse-paths.js +105 -0
  197. package/dist/src/providers/provider-definitions.js +93 -7
  198. package/dist/src/providers/provider-definitions.mjs +21 -1
  199. package/dist/src/providers/provider-inventory.js +3 -0
  200. package/dist/src/security/artifact-attestation.js +4 -8
  201. package/dist/src/security/signing.js +119 -0
  202. package/dist/src/sessions/adapters/grok-build.js +187 -0
  203. package/dist/src/sessions/adapters/muse.js +412 -0
  204. package/dist/src/sessions/contracts.js +1 -1
  205. package/dist/src/sessions/grok-build-cli.js +196 -0
  206. package/dist/src/sessions/index.js +3 -0
  207. package/dist/src/sessions/workspace-discovery.js +9 -5
  208. package/dist/src/skills/deployer.js +33 -5
  209. package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
  210. package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
  211. package/dist/src/smiths/context-pipeline/finalization.js +1 -32
  212. package/dist/src/smiths/context-pipeline/generator.js +12 -0
  213. package/dist/src/smiths/context-pipeline/index.js +1 -1
  214. package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
  215. package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
  216. package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
  217. package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
  218. package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
  219. package/dist/src/smiths/hook-bridge/index.js +3 -0
  220. package/dist/src/smiths/hook-bridge/shim.js +2 -0
  221. package/dist/src/smiths/hook-bridge/types.js +5 -0
  222. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  223. package/dist/src/storage/protected-files.js +102 -0
  224. package/dist/src/tracker/forge-http.js +371 -0
  225. package/dist/src/tracker/project-inputs.js +41 -0
  226. package/package.json +2 -1
  227. package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
  228. package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
  229. package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
  230. package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
  231. package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
  232. package/schemas/decision/DecisionBinding.schema.json +309 -0
  233. package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
  234. package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
  235. package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
  236. package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
  237. package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
  238. package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
  239. package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
  240. package/schemas/decision/DecisionDefinition.schema.json +159 -0
  241. package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
  242. package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
  243. package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
  244. package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
  245. package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
  246. package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
  247. package/schemas/decision/DecisionJob.v1.schema.json +71 -0
  248. package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
  249. package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
  250. package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
  251. package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
  252. package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
  253. package/schemas/decision/DecisionResult.schema.json +524 -0
  254. package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
  255. package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
  256. package/schemas/decision/DecisionReview.schema.json +29 -0
  257. package/schemas/decision/DecisionRuleset.schema.json +313 -0
  258. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
  259. package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
  260. package/schemas/decision/RulesetResult.schema.json +721 -0
  261. package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
  262. package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
  263. package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
  264. package/tools/agents/deploy-agents.mjs +21 -6
  265. package/tools/agents/providers/base.mjs +7 -4
  266. package/tools/agents/providers/codex.mjs +1 -1
  267. package/tools/agents/providers/grok-build.d.mts +7 -0
  268. package/tools/agents/providers/grok-build.mjs +249 -9
  269. package/tools/agents/providers/model-role.mjs +6 -3
  270. package/tools/agents/providers/muse-hooks.mjs +785 -0
  271. package/tools/agents/providers/muse-paths.mjs +112 -0
  272. package/tools/agents/providers/muse.mjs +336 -0
@@ -0,0 +1,872 @@
1
+ /**
2
+ * `aiwg effect`: record, look up, reconcile and verify side effects in the
3
+ * signed effect ledger (#2720, epic #2714).
4
+ *
5
+ * The ledger proves what AIWG recorded and what a verifier observed. It never
6
+ * authorizes or replays an effect. Output is JSON by default, and every JSON
7
+ * document carries a `schema` member. Exit codes are pinned by
8
+ * docs/contracts/effect-ledger.v1.md ("CLI and exit codes"):
9
+ *
10
+ * 0 present or recorded 3 absent 5 conflict 7 artifact root unavailable
11
+ * 1 internal error 4 unknown 6 integrity failure
12
+ * 2 usage error
13
+ *
14
+ * Scope (tenant and project) and the key provider come from host
15
+ * configuration (`effects` in aiwg.config), never from model input. The ledger
16
+ * signing key lives in the host secret service; only key IDs and public keys
17
+ * are ever printed.
18
+ *
19
+ * @see docs/contracts/effect-ledger.v1.md
20
+ */
21
+ import { readFile } from 'node:fs/promises';
22
+ import { basename, resolve } from 'node:path';
23
+ import { createSecretStore } from '../../auth/credential-store.js';
24
+ import { readAiwgConfig } from '../../config/aiwg-config.js';
25
+ import { CORE_EFFECT_KINDS, DEFAULT_LEDGER_KEY_SERVICE, EFFECT_EXIT_CODES, EFFECT_SUBSYSTEMS, EffectLedgerError, LEDGER_TEST_KEY_ENV, LedgerSigningKey, activeKey, containsRestrictedMaterial, createBuiltinVerifierRegistry, createTrackerVerifiers, credentialStoreKeyProvider, effectId as deriveEffectId, effectOutputJson, environmentTestKeyProvider, generateLedgerKeySecret, initLedgerKeyring, inspectLedgerLocks, isValidEffectId, ledgerLockRecoveryVerifier, lookupEffect, openEffectLedger, payloadDigest, readLedgerKeyring, reconcileEffect, recordIntent, recordOutcome, recordReconciled, recoverStaleLedgerLock, rotateKey, runVerifier, staticKeyProvider, verifyDecisionLinks, verifyLedger, writeCheckpoint, } from '../../effects/index.js';
26
+ import { parseLedgerPrivateKey } from '../../effects/keys.js';
27
+ import { JsonlOperatorDecisionStore, verifyDecisionChain } from '../../audit/operator-decision.js';
28
+ import { readConfig, readGitRemoteUrls } from '../../tracker/project-inputs.js';
29
+ // ---------------------------------------------------------------------------
30
+ // Usage
31
+ // ---------------------------------------------------------------------------
32
+ export const EFFECT_SUBCOMMANDS = ['id', 'intent', 'record', 'lookup', 'reconcile', 'probe', 'verify', 'checkpoint', 'kinds', 'keys', 'recover-lock'];
33
+ export function effectUsage() {
34
+ return [
35
+ 'aiwg effect — Record, look up, reconcile and verify side effects in the signed effect ledger',
36
+ '',
37
+ 'Usage:',
38
+ ' aiwg effect id <identity>',
39
+ ' aiwg effect intent <identity> <payload> [--link k=v]',
40
+ ' aiwg effect record <identity> <payload> [--unverified] [expectations] [--link k=v]',
41
+ ' aiwg effect lookup <effect-id> | <identity>',
42
+ ' aiwg effect reconcile <effect-id> | <identity> [expectations] [--link k=v]',
43
+ ' aiwg effect probe <identity> [--effect-id <id>] [--since <iso>] [expectations]',
44
+ ' aiwg effect verify [--trusted-keyid <keyid>]... [--with-decisions <audit.jsonl>]',
45
+ ' aiwg effect checkpoint',
46
+ ' aiwg effect kinds',
47
+ ' aiwg effect keys list | init | rotate [--reason scheduled|custody-change|compromise]',
48
+ ' aiwg effect recover-lock [--lock <name> --authorize]',
49
+ '',
50
+ 'Identity:',
51
+ ' --kind <kind> --target <scheme:ref> [--context <json>] [--ctx key=value]...',
52
+ ' --issue <n> --action <name> --cycle <n> tracker context members',
53
+ ' --review <id> --continuation <id> --proposal-version <n> D13 review continuation',
54
+ '',
55
+ 'Payload (intent, record):',
56
+ ' --payload-file <path|-> digest the exact bytes the effect sends',
57
+ ' --payload-digest sha256:<hex>',
58
+ '',
59
+ 'Expectations (record, reconcile):',
60
+ ' --expect-digest sha256:<hex> --expect-object <oid> --signed',
61
+ ' --timeout-ms <n> --verifier-version <semver>',
62
+ '',
63
+ 'Common options:',
64
+ ' --subsystem review|job|delivery|custom (default delivery; review for D13 identities)',
65
+ ' --project-dir <dir> --format json|text (default json)',
66
+ '',
67
+ 'record performs intent, verify and completed in one command.',
68
+ 'probe runs the kind verifier once and writes no records (ad-hoc checks such',
69
+ 'as "did this PR merge"); it has the same exit codes 0, 3 and 4.',
70
+ 'verify --with-decisions also checks the #1567 operator-decision chain and that',
71
+ 'every linked operatorDecisionEventId (and record hash) exists in it.',
72
+ 'recover-lock removes a stale ledger lock only with --authorize, refuses live,',
73
+ 'reused or unverifiable owners, and records the recovery in the ledger.',
74
+ '',
75
+ 'Exit codes:',
76
+ ' 0 present or recorded 1 internal error 2 usage error 3 absent',
77
+ ' 4 unknown 5 conflict 6 integrity failure',
78
+ ' 7 artifact root unavailable',
79
+ ].join('\n');
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Argument parsing
83
+ // ---------------------------------------------------------------------------
84
+ const BOOLEAN_FLAGS = new Set(['json', 'unverified', 'verify', 'signed', 'authorize']);
85
+ const REPEATABLE_FLAGS = new Set(['ctx', 'link', 'trusted-keyid']);
86
+ const COMMON = ['project-dir', 'subsystem', 'format', 'json'];
87
+ const IDENTITY = ['kind', 'target', 'context', 'ctx', 'issue', 'action', 'cycle', 'review', 'continuation', 'proposal-version'];
88
+ const PAYLOAD = ['payload-file', 'payload-digest'];
89
+ const EXPECT = ['expect-digest', 'expect-object', 'signed', 'timeout-ms', 'verifier-version'];
90
+ const ALLOWED = {
91
+ id: new Set([...COMMON, ...IDENTITY]),
92
+ intent: new Set([...COMMON, ...IDENTITY, ...PAYLOAD, 'link']),
93
+ record: new Set([...COMMON, ...IDENTITY, ...PAYLOAD, ...EXPECT, 'link', 'unverified', 'verify']),
94
+ lookup: new Set([...COMMON, ...IDENTITY]),
95
+ reconcile: new Set([...COMMON, ...IDENTITY, ...EXPECT, 'link']),
96
+ probe: new Set([...COMMON, ...IDENTITY, ...PAYLOAD, ...EXPECT, 'effect-id', 'since']),
97
+ verify: new Set([...COMMON, 'trusted-keyid', 'with-decisions']),
98
+ checkpoint: new Set(COMMON),
99
+ kinds: new Set(COMMON),
100
+ keys: new Set([...COMMON, 'reason']),
101
+ 'recover-lock': new Set([...COMMON, 'lock', 'authorize', 'link']),
102
+ };
103
+ const LINK_NAMES = new Set(['operatorDecisionEventId', 'operatorDecisionRecordHash', 'traceId', 'spanId', 'toolCallId']);
104
+ /** Link forms from the record schema: #1567 references are sha256 digests (D13 event IDs come from `reviewOperatorEventId`). */
105
+ const LINK_PATTERNS = {
106
+ operatorDecisionEventId: /^sha256:[a-f0-9]{64}$/,
107
+ operatorDecisionRecordHash: /^sha256:[a-f0-9]{64}$/,
108
+ traceId: /^[a-f0-9]{32}$/,
109
+ spanId: /^[a-f0-9]{16}$/,
110
+ };
111
+ const usage = (message, reason) => new EffectLedgerError('usage', message, reason);
112
+ function parseArgs(args) {
113
+ const parsed = { positionals: [], values: new Map(), booleans: new Set() };
114
+ for (let index = 0; index < args.length; index++) {
115
+ const arg = args[index];
116
+ if (!arg.startsWith('--') || arg === '--') {
117
+ parsed.positionals.push(arg);
118
+ continue;
119
+ }
120
+ const eq = arg.indexOf('=');
121
+ const name = arg.slice(2, eq < 0 ? undefined : eq);
122
+ if (BOOLEAN_FLAGS.has(name)) {
123
+ if (eq >= 0)
124
+ throw usage(`--${name} does not take a value`, 'invalid-flag');
125
+ parsed.booleans.add(name);
126
+ continue;
127
+ }
128
+ const value = eq < 0 ? args[++index] : arg.slice(eq + 1);
129
+ if (value === undefined)
130
+ throw usage(`--${name} needs a value`, 'missing-flag-value');
131
+ const existing = parsed.values.get(name) ?? [];
132
+ if (existing.length && !REPEATABLE_FLAGS.has(name))
133
+ throw usage(`--${name} may be given once`, 'duplicate-flag');
134
+ existing.push(value);
135
+ parsed.values.set(name, existing);
136
+ }
137
+ return parsed;
138
+ }
139
+ function checkFlags(sub, parsed) {
140
+ for (const name of [...parsed.values.keys(), ...parsed.booleans]) {
141
+ if (!ALLOWED[sub].has(name))
142
+ throw usage(`Unknown option for aiwg effect ${sub}: --${name}`, 'unknown-flag');
143
+ }
144
+ }
145
+ const one = (parsed, name) => parsed.values.get(name)?.[0];
146
+ function integerFlag(parsed, name, min = 0) {
147
+ const raw = one(parsed, name);
148
+ if (raw === undefined)
149
+ return undefined;
150
+ if (!/^-?[0-9]+$/.test(raw) || !Number.isSafeInteger(Number(raw)) || Number(raw) < min)
151
+ throw usage(`--${name} must be an integer ≥ ${min}`, 'invalid-integer');
152
+ return Number(raw);
153
+ }
154
+ function scalar(raw) {
155
+ if (/^-?(?:0|[1-9][0-9]*)$/.test(raw) && Number.isSafeInteger(Number(raw)))
156
+ return Number(raw);
157
+ if (raw === 'true')
158
+ return true;
159
+ if (raw === 'false')
160
+ return false;
161
+ return raw;
162
+ }
163
+ function keyValue(entry, flag) {
164
+ const eq = entry.indexOf('=');
165
+ if (eq <= 0)
166
+ throw usage(`--${flag} expects key=value`, 'invalid-key-value');
167
+ return [entry.slice(0, eq), entry.slice(eq + 1)];
168
+ }
169
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,255}$/;
170
+ function defaultProject(projectDir) {
171
+ const cleaned = basename(resolve(projectDir)).replace(/[^A-Za-z0-9._:/@-]/g, '-').replace(/^[^A-Za-z0-9]+/, '');
172
+ return cleaned && IDENTIFIER.test(cleaned) ? cleaned : 'project';
173
+ }
174
+ async function hostConfig(projectDir) {
175
+ let config;
176
+ try {
177
+ config = await readAiwgConfig(projectDir);
178
+ }
179
+ catch {
180
+ throw new EffectLedgerError('usage', 'aiwg.config could not be read', 'config-unreadable');
181
+ }
182
+ const effects = config?.effects;
183
+ if (effects === undefined || effects === null)
184
+ return {};
185
+ if (typeof effects !== 'object' || Array.isArray(effects))
186
+ throw usage('aiwg.config effects must be an object', 'config-invalid');
187
+ return effects;
188
+ }
189
+ function testContext(env) {
190
+ return env.VITEST === 'true' || env.NODE_ENV === 'test' || env.CI === 'true' || env.CI === '1';
191
+ }
192
+ function keyStore(session, role) {
193
+ const { deps, config, account } = session;
194
+ if (deps.keyStore)
195
+ return deps.keyStore(role, account);
196
+ if (session.env[LEDGER_TEST_KEY_ENV] && testContext(session.env))
197
+ return null;
198
+ const provider = config.keyProvider ?? {};
199
+ try {
200
+ if (provider.type === 'file') {
201
+ if (!provider.path)
202
+ throw new Error('missing path');
203
+ const pathname = resolve(session.projectDir, provider.path);
204
+ return createSecretStore({ useFile: true, allowFile: true, pathname: role === 'next' ? `${pathname}.next` : pathname, service: DEFAULT_LEDGER_KEY_SERVICE, account });
205
+ }
206
+ if (provider.type !== undefined && provider.type !== 'credential-store')
207
+ throw new Error('unknown provider');
208
+ return createSecretStore({ service: provider.service ?? DEFAULT_LEDGER_KEY_SERVICE, account: role === 'next' ? `${account}.next` : account });
209
+ }
210
+ catch {
211
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key provider is not configured correctly on this host', 'key-provider-invalid');
212
+ }
213
+ }
214
+ function keyProvider(session) {
215
+ const store = keyStore(session, 'current');
216
+ if (!store)
217
+ return environmentTestKeyProvider(session.env);
218
+ return credentialStoreKeyProvider({ account: session.account, store });
219
+ }
220
+ function describeKeyProvider(session) {
221
+ if (session.deps.keyStore)
222
+ return { type: 'injected', account: session.account };
223
+ if (session.env[LEDGER_TEST_KEY_ENV] && testContext(session.env))
224
+ return { type: 'environment-test-key' };
225
+ const provider = session.config.keyProvider ?? {};
226
+ if (provider.type === 'file')
227
+ return { type: 'file', account: session.account };
228
+ return { type: 'credential-store', service: provider.service ?? DEFAULT_LEDGER_KEY_SERVICE, account: session.account };
229
+ }
230
+ function openLedger(session, registry) {
231
+ const writer = session.config.writer ?? 'cli';
232
+ return openEffectLedger({
233
+ projectDir: session.projectDir, scope: session.scope, writer, keyProvider: keyProvider(session),
234
+ ...(session.deps.clock ? { clock: session.deps.clock } : {}),
235
+ ...(session.deps.sink ? { sink: session.deps.sink } : {}),
236
+ ...(registry ? { verifiers: registry } : {}),
237
+ ...(session.deps.lockTimeoutMs ? { lockTimeoutMs: session.deps.lockTimeoutMs } : {}),
238
+ });
239
+ }
240
+ /**
241
+ * The one place the CLI builds its verifier registry: the built-ins, lock
242
+ * recovery, the tracker verifiers (#2719) and any injected extensions.
243
+ * `kinds`, `record`, `reconcile` and `probe` all use it, so `kinds` lists
244
+ * exactly what the registry contains. Tracker authority is resolved lazily at
245
+ * verification time, so a project without tracker configuration still lists
246
+ * the tracker kinds and answers `unknown` / `tracker-blocked`.
247
+ */
248
+ export function buildCliVerifierRegistry(projectRoot, extensions = [], tracker = { config: null, remoteUrls: {} }) {
249
+ const injected = new Set(extensions.map(verifier => verifier.kind));
250
+ const hostVerifiers = [
251
+ ledgerLockRecoveryVerifier({ projectDir: projectRoot }),
252
+ ...createTrackerVerifiers(tracker).filter(verifier => !injected.has(verifier.kind)),
253
+ ];
254
+ return createBuiltinVerifierRegistry({ git: { repoDir: projectRoot }, file: { root: projectRoot } }, [...hostVerifiers, ...extensions]);
255
+ }
256
+ /**
257
+ * Tracker verifier options for the CLI: the project config (`readConfig`) and
258
+ * the git remote URLs (`readGitRemoteUrls`), with `overrides` applied last.
259
+ */
260
+ export async function cliTrackerOptions(projectRoot, env, overrides = {}) {
261
+ const [config, remoteUrls] = await Promise.all([
262
+ 'config' in overrides ? Promise.resolve(overrides.config ?? null) : readConfig(projectRoot),
263
+ overrides.remoteUrls ? Promise.resolve(overrides.remoteUrls) : readGitRemoteUrls(projectRoot),
264
+ ]);
265
+ return { ...overrides, config, remoteUrls, env: overrides.env ?? env };
266
+ }
267
+ async function sessionRegistry(session) {
268
+ const tracker = await cliTrackerOptions(session.projectDir, session.env, session.deps.tracker);
269
+ return buildCliVerifierRegistry(session.projectDir, session.deps.verifiers, tracker);
270
+ }
271
+ function isReviewIdentity(parsed) {
272
+ return ['review', 'continuation', 'proposal-version'].some(name => parsed.values.has(name));
273
+ }
274
+ function resolveSubsystem(parsed) {
275
+ const raw = one(parsed, 'subsystem');
276
+ if (raw !== undefined && !EFFECT_SUBSYSTEMS.includes(raw))
277
+ throw usage('--subsystem must be review, job, delivery or custom', 'invalid-subsystem');
278
+ if (isReviewIdentity(parsed)) {
279
+ if (raw !== undefined && raw !== 'review')
280
+ throw usage('A D13 review identity uses the review subsystem', 'invalid-subsystem');
281
+ return 'review';
282
+ }
283
+ return raw ?? 'delivery';
284
+ }
285
+ function hasIdentityFlags(parsed) {
286
+ return IDENTITY.some(name => parsed.values.has(name));
287
+ }
288
+ function identityFrom(parsed, scope) {
289
+ if (isReviewIdentity(parsed)) {
290
+ const reviewId = one(parsed, 'review');
291
+ const continuationId = one(parsed, 'continuation');
292
+ const proposalVersion = integerFlag(parsed, 'proposal-version', 1);
293
+ if (!reviewId || !continuationId || proposalVersion === undefined) {
294
+ throw usage('A D13 identity needs --review, --continuation and --proposal-version', 'derivation-input-invalid');
295
+ }
296
+ const kind = one(parsed, 'kind') ?? 'decision.review.continuation';
297
+ if (kind !== 'decision.review.continuation')
298
+ throw usage('A D13 identity has kind decision.review.continuation', 'derivation-kind-mismatch');
299
+ for (const name of ['context', 'ctx', 'issue', 'action', 'cycle']) {
300
+ if (parsed.values.has(name))
301
+ throw usage(`--${name} cannot be combined with a D13 review identity`, 'derivation-input-invalid');
302
+ }
303
+ const target = one(parsed, 'target') ?? `review:${scope.tenant}/${scope.project}/${reviewId}`;
304
+ return { kind, target, context: { reviewId, continuationId, proposalVersion }, derivation: 'd13.review/v1' };
305
+ }
306
+ const kind = one(parsed, 'kind');
307
+ const target = one(parsed, 'target');
308
+ if (!kind || !target)
309
+ throw usage('An effect identity needs --kind and --target', 'identity-incomplete');
310
+ const context = {};
311
+ const set = (key, value) => {
312
+ if (Object.prototype.hasOwnProperty.call(context, key))
313
+ throw usage(`Context member ${key} is given twice`, 'invalid-context');
314
+ context[key] = value;
315
+ };
316
+ const json = one(parsed, 'context');
317
+ if (json !== undefined) {
318
+ let value;
319
+ try {
320
+ value = JSON.parse(json);
321
+ }
322
+ catch {
323
+ throw usage('--context must be a JSON object', 'invalid-context');
324
+ }
325
+ if (!value || typeof value !== 'object' || Array.isArray(value))
326
+ throw usage('--context must be a JSON object', 'invalid-context');
327
+ for (const [key, member] of Object.entries(value))
328
+ set(key, member);
329
+ }
330
+ for (const entry of parsed.values.get('ctx') ?? []) {
331
+ const [key, raw] = keyValue(entry, 'ctx');
332
+ set(key, scalar(raw));
333
+ }
334
+ const issue = integerFlag(parsed, 'issue', 1);
335
+ if (issue !== undefined)
336
+ set('issue', issue);
337
+ const action = one(parsed, 'action');
338
+ if (action !== undefined)
339
+ set('action', action);
340
+ const cycle = integerFlag(parsed, 'cycle', 0);
341
+ if (cycle !== undefined)
342
+ set('cycle', cycle);
343
+ if (kind === 'decision.review.continuation')
344
+ throw usage('decision.review.continuation needs --review, --continuation and --proposal-version', 'derivation-input-invalid');
345
+ return { kind, target, context, derivation: 'aiwg.effect/v1' };
346
+ }
347
+ function idFor(identity, scope) {
348
+ return deriveEffectId({ scope, kind: identity.kind, target: identity.target, context: identity.context }, identity.derivation);
349
+ }
350
+ /** The effect ID from a positional argument or from identity flags. */
351
+ function effectIdFrom(parsed, scope) {
352
+ const positional = parsed.positionals[0];
353
+ if (positional !== undefined) {
354
+ if (hasIdentityFlags(parsed))
355
+ throw usage('Give an effect ID or identity flags, not both', 'identity-ambiguous');
356
+ if (parsed.positionals.length > 1)
357
+ throw usage('Only one effect ID may be given', 'unexpected-argument');
358
+ if (!isValidEffectId(positional))
359
+ throw usage('Malformed effect ID', 'malformed-effect-id');
360
+ return positional;
361
+ }
362
+ return idFor(identityFrom(parsed, scope), scope);
363
+ }
364
+ async function payloadFrom(parsed) {
365
+ const digest = one(parsed, 'payload-digest');
366
+ const file = one(parsed, 'payload-file');
367
+ if ((digest === undefined) === (file === undefined))
368
+ throw usage('Give exactly one of --payload-file or --payload-digest', 'payload-required');
369
+ if (digest !== undefined) {
370
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
371
+ throw usage('Payload digest must be sha256:<hex>', 'invalid-payload-digest');
372
+ return digest;
373
+ }
374
+ let bytes;
375
+ try {
376
+ if (file === '-') {
377
+ const chunks = [];
378
+ for await (const chunk of process.stdin)
379
+ chunks.push(Buffer.from(chunk));
380
+ bytes = Buffer.concat(chunks);
381
+ }
382
+ else {
383
+ bytes = await readFile(file);
384
+ }
385
+ }
386
+ catch {
387
+ throw usage('--payload-file could not be read', 'payload-unreadable');
388
+ }
389
+ return payloadDigest(bytes);
390
+ }
391
+ function linksFrom(parsed) {
392
+ const links = {};
393
+ for (const entry of parsed.values.get('link') ?? []) {
394
+ const [key, value] = keyValue(entry, 'link');
395
+ if (!LINK_NAMES.has(key))
396
+ throw usage(`Unknown link name ${key}`, 'invalid-link');
397
+ if (!value || value.length > 256 || /\s/.test(value))
398
+ throw usage('Link values are non-empty references without whitespace', 'invalid-link');
399
+ if (LINK_PATTERNS[key] && !LINK_PATTERNS[key].test(value))
400
+ throw usage(`Link ${key} has the wrong form`, 'invalid-link');
401
+ links[key] = value;
402
+ }
403
+ return links;
404
+ }
405
+ function expectationsFrom(parsed) {
406
+ const expected = {};
407
+ const digest = one(parsed, 'expect-digest');
408
+ if (digest !== undefined) {
409
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
410
+ throw usage('--expect-digest must be sha256:<hex>', 'invalid-expectation');
411
+ expected.digest = digest;
412
+ }
413
+ const object = one(parsed, 'expect-object');
414
+ if (object !== undefined) {
415
+ if (!/^[a-f0-9]{40,64}$/.test(object))
416
+ throw usage('--expect-object must be a full object ID', 'invalid-expectation');
417
+ expected.object = object;
418
+ }
419
+ if (parsed.booleans.has('signed'))
420
+ expected.signed = true;
421
+ const timeoutMs = integerFlag(parsed, 'timeout-ms', 1);
422
+ const verifierVersion = one(parsed, 'verifier-version');
423
+ if (verifierVersion !== undefined && !/^[0-9]+\.[0-9]+\.[0-9]+$/.test(verifierVersion))
424
+ throw usage('--verifier-version must be MAJOR.MINOR.PATCH', 'invalid-expectation');
425
+ return { expected, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(verifierVersion ? { verifierVersion } : {}) };
426
+ }
427
+ function emit(rendered, format) {
428
+ const message = format === 'text' ? rendered.text : effectOutputJson(rendered.output);
429
+ return { exitCode: rendered.output.exitCode, message, rawOutput: true };
430
+ }
431
+ function errorOutput(error) {
432
+ if (error instanceof EffectLedgerError) {
433
+ const safe = containsRestrictedMaterial(error.message) ? 'Effect command failed' : error.message;
434
+ return { schema: 'aiwg.effect.error.v1', exitCode: error.exitCode, error: { code: error.code, ...(error.reason ? { reason: error.reason } : {}), message: safe } };
435
+ }
436
+ return { schema: 'aiwg.effect.error.v1', exitCode: EFFECT_EXIT_CODES.internal, error: { code: 'internal', message: 'Unexpected effect command failure' } };
437
+ }
438
+ function receiptLine(receipt) {
439
+ return `${receipt.phase} ${receipt.effectId} (${receipt.writer}#${receipt.seq}${receipt.idempotent ? ', idempotent' : ''})`;
440
+ }
441
+ function publicKeyring(keyring) {
442
+ if (!keyring)
443
+ return { initialized: false, keys: [], rotations: [] };
444
+ return {
445
+ initialized: true,
446
+ activeKeyid: activeKey(keyring).keyid,
447
+ keys: keyring.keys.map(key => ({
448
+ keyid: key.keyid, algorithm: key.algorithm, publicKey: key.publicKey, validFrom: key.validFrom,
449
+ ...(key.validUntil ? { validUntil: key.validUntil } : {}), status: key.status, ...(key.revokedAt ? { revokedAt: key.revokedAt } : {}),
450
+ })),
451
+ rotations: keyring.rotations.map(rotation => ({
452
+ sequence: rotation.sequence, from: rotation.from, to: rotation.to, effectiveAt: rotation.effectiveAt, reason: rotation.reason,
453
+ })),
454
+ };
455
+ }
456
+ // ---------------------------------------------------------------------------
457
+ // Subcommands
458
+ // ---------------------------------------------------------------------------
459
+ async function runId(session, parsed) {
460
+ if (parsed.positionals.length)
461
+ throw usage('aiwg effect id takes identity flags only', 'unexpected-argument');
462
+ const identity = identityFrom(parsed, session.scope);
463
+ const id = idFor(identity, session.scope);
464
+ return {
465
+ output: { schema: 'aiwg.effect.id.v1', exitCode: 0, effectId: id, idDerivation: identity.derivation, scope: session.scope, kind: identity.kind, target: identity.target, context: identity.context },
466
+ text: id,
467
+ };
468
+ }
469
+ async function runIntent(session, parsed) {
470
+ if (parsed.positionals.length)
471
+ throw usage('aiwg effect intent takes identity flags only', 'unexpected-argument');
472
+ const identity = identityFrom(parsed, session.scope);
473
+ const digest = await payloadFrom(parsed);
474
+ const links = linksFrom(parsed);
475
+ const ledger = openLedger(session);
476
+ const receipt = await recordIntent(ledger, { kind: identity.kind, target: identity.target, context: identity.context, payloadDigest: digest, derivation: identity.derivation, links });
477
+ return { output: { schema: 'aiwg.effect.intent.v1', exitCode: 0, effectId: receipt.effectId, idempotent: receipt.idempotent, receipt }, text: `recorded ${receiptLine(receipt)}` };
478
+ }
479
+ async function runRecord(session, parsed) {
480
+ if (parsed.positionals.length)
481
+ throw usage('aiwg effect record takes identity flags only', 'unexpected-argument');
482
+ if (parsed.booleans.has('unverified') && parsed.booleans.has('verify'))
483
+ throw usage('--unverified and --verify are exclusive', 'flag-conflict');
484
+ const identity = identityFrom(parsed, session.scope);
485
+ const digest = await payloadFrom(parsed);
486
+ const links = linksFrom(parsed);
487
+ const { expected, timeoutMs, verifierVersion } = expectationsFrom(parsed);
488
+ const registry = await sessionRegistry(session);
489
+ const ledger = openLedger(session, registry);
490
+ const intent = await recordIntent(ledger, { kind: identity.kind, target: identity.target, context: identity.context, payloadDigest: digest, derivation: identity.derivation, links });
491
+ const base = { schema: 'aiwg.effect.record.v1', effectId: intent.effectId, intent };
492
+ if (parsed.booleans.has('unverified')) {
493
+ return { output: { ...base, exitCode: 0, status: 'intent', verified: false, idempotent: intent.idempotent, completed: null }, text: `recorded ${receiptLine(intent)} (unverified)` };
494
+ }
495
+ const existing = await lookupEffect(ledger, intent.effectId);
496
+ if (existing.records.some(record => record.phase === 'completed')) {
497
+ const completed = existing.records.find(record => record.phase === 'completed');
498
+ return {
499
+ output: { ...base, exitCode: 0, status: 'completed', verified: true, idempotent: intent.idempotent, completed: { phase: 'completed', writer: completed.writer, seq: completed.seq, recordHash: completed.recordHash }, verification: completed.verification },
500
+ text: `completed ${intent.effectId} (idempotent)`,
501
+ };
502
+ }
503
+ if (existing.records.some(record => record.phase === 'failed')) {
504
+ return { output: { ...base, exitCode: EFFECT_EXIT_CODES.absent, status: 'failed', verified: false, idempotent: intent.idempotent, completed: null }, text: `failed ${intent.effectId}` };
505
+ }
506
+ const intentAt = existing.records.find(record => record.phase === 'intent')?.recordedAt ?? ledger.now();
507
+ const run = await runVerifier(registry.get(identity.kind), {
508
+ effectId: intent.effectId, scope: structuredClone(session.scope), kind: identity.kind, target: identity.target,
509
+ context: structuredClone(identity.context), payloadDigest: digest, intentRecordedAt: intentAt, expected,
510
+ }, { ...(timeoutMs ? { timeoutMs } : {}), ...(verifierVersion ? { verifierVersion } : {}) });
511
+ const verification = { verifier: run.verifier, ...run.observation, checkedAt: ledger.now() };
512
+ const evidence = run.evidence ? { evidence: run.evidence } : {};
513
+ if (verification.result === 'present') {
514
+ const completed = await recordOutcome(ledger, intent.effectId, { phase: 'completed', payloadDigest: digest, verification, links });
515
+ return {
516
+ output: { ...base, exitCode: 0, status: 'completed', verified: true, idempotent: false, completed, verification, ...evidence },
517
+ text: `completed ${intent.effectId} (${verification.reason})`,
518
+ };
519
+ }
520
+ const reconciled = await recordReconciled(ledger, intent.effectId, verification, links);
521
+ const exitCode = verification.result === 'absent' ? EFFECT_EXIT_CODES.absent : EFFECT_EXIT_CODES.unknown;
522
+ return {
523
+ output: { ...base, exitCode, status: 'reconciled', verified: false, idempotent: false, completed: null, reconciled, verification, ...evidence },
524
+ text: `${verification.result} ${intent.effectId} (${verification.reason})`,
525
+ };
526
+ }
527
+ async function runLookup(session, parsed) {
528
+ const id = effectIdFrom(parsed, session.scope);
529
+ const result = await lookupEffect(openLedger(session), id);
530
+ return { output: { schema: 'aiwg.effect.lookup.v1', ...result }, text: `${result.status} ${id}${result.result ? ` (${result.result})` : ''}` };
531
+ }
532
+ async function runReconcile(session, parsed) {
533
+ const id = effectIdFrom(parsed, session.scope);
534
+ const { expected, timeoutMs, verifierVersion } = expectationsFrom(parsed);
535
+ const links = linksFrom(parsed);
536
+ const registry = await sessionRegistry(session);
537
+ const outcome = await reconcileEffect(openLedger(session, registry), id, {
538
+ verifiers: registry, expected, links, ...(timeoutMs ? { timeoutMs } : {}), ...(verifierVersion ? { verifierVersion } : {}),
539
+ });
540
+ const verification = outcome.result.verification;
541
+ return {
542
+ output: {
543
+ schema: 'aiwg.effect.reconcile.v1', exitCode: outcome.result.exitCode, effectId: id, result: outcome.result,
544
+ receipt: outcome.receipt, completed: outcome.completed, ...(outcome.evidence ? { evidence: outcome.evidence } : {}),
545
+ },
546
+ text: `${verification.result} ${id} (${verification.reason})`,
547
+ };
548
+ }
549
+ /** The empty-payload digest a probe passes when no payload is given. */
550
+ const EMPTY_PAYLOAD_DIGEST = payloadDigest(new Uint8Array(0));
551
+ /**
552
+ * `probe`: run the kind's verifier once and write nothing. It never opens the
553
+ * ledger, so it needs no key and no artifact root. The effect ID is
554
+ * `--effect-id` (for example the ID a tracker marker carries) or derived from
555
+ * the identity; `--since` stands in for the intent time (default: the epoch,
556
+ * so no consistency-lag window applies).
557
+ */
558
+ async function runProbe(session, parsed) {
559
+ if (parsed.positionals.length)
560
+ throw usage('aiwg effect probe takes identity flags only', 'unexpected-argument');
561
+ const identity = identityFrom(parsed, session.scope);
562
+ const explicit = one(parsed, 'effect-id');
563
+ if (explicit !== undefined && !isValidEffectId(explicit))
564
+ throw usage('Malformed effect ID', 'malformed-effect-id');
565
+ const id = explicit ?? idFor(identity, session.scope);
566
+ const since = one(parsed, 'since');
567
+ if (since !== undefined && !Number.isFinite(Date.parse(since)))
568
+ throw usage('--since must be an ISO 8601 time', 'invalid-since');
569
+ const hasPayload = parsed.values.has('payload-file') || parsed.values.has('payload-digest');
570
+ const digest = hasPayload ? await payloadFrom(parsed) : EMPTY_PAYLOAD_DIGEST;
571
+ const { expected, timeoutMs, verifierVersion } = expectationsFrom(parsed);
572
+ const registry = await sessionRegistry(session);
573
+ const run = await runVerifier(registry.get(identity.kind), {
574
+ effectId: id, scope: structuredClone(session.scope), kind: identity.kind, target: identity.target,
575
+ context: structuredClone(identity.context), payloadDigest: digest,
576
+ intentRecordedAt: new Date(since === undefined ? 0 : Date.parse(since)).toISOString(), expected,
577
+ }, { ...(timeoutMs ? { timeoutMs } : {}), ...(verifierVersion ? { verifierVersion } : {}) });
578
+ const now = session.deps.clock ? session.deps.clock() : Date.now();
579
+ const verification = { verifier: run.verifier, ...run.observation, checkedAt: new Date(now).toISOString() };
580
+ const exitCode = verification.result === 'present' ? EFFECT_EXIT_CODES.ok
581
+ : verification.result === 'absent' ? EFFECT_EXIT_CODES.absent : EFFECT_EXIT_CODES.unknown;
582
+ return {
583
+ output: {
584
+ schema: 'aiwg.effect.probe.v1', exitCode, effectId: id, kind: identity.kind, target: identity.target, recorded: false,
585
+ verification, ...(run.evidence ? { evidence: run.evidence } : {}),
586
+ },
587
+ text: `${verification.result} ${id} (${verification.reason}; not recorded)`,
588
+ };
589
+ }
590
+ async function readDecisionAudit(path) {
591
+ try {
592
+ return await new JsonlOperatorDecisionStore(path).read();
593
+ }
594
+ catch {
595
+ throw usage('--with-decisions could not be read as an operator-decision JSONL file', 'decisions-unreadable');
596
+ }
597
+ }
598
+ async function runVerify(session, parsed) {
599
+ const trusted = parsed.values.get('trusted-keyid');
600
+ const decisionsPath = one(parsed, 'with-decisions');
601
+ const ledger = openLedger(session);
602
+ const result = await verifyLedger(ledger, trusted ? { trustedKeyids: trusted } : {});
603
+ let decisions;
604
+ let ok = result.ok;
605
+ const failures = [...result.failures];
606
+ if (decisionsPath !== undefined) {
607
+ const records = await readDecisionAudit(resolve(session.projectDir, decisionsPath));
608
+ const chain = verifyDecisionChain(records);
609
+ const links = await verifyDecisionLinks(ledger, records);
610
+ const chainFailure = { reason: 'decision-chain-broken', index: chain.index ?? null };
611
+ if (!chain.ok)
612
+ failures.push(chainFailure);
613
+ failures.push(...links.failures);
614
+ ok = ok && chain.ok && links.ok;
615
+ decisions = {
616
+ records: records.length, chain: chain.ok ? 'intact' : 'broken', ...(chain.ok ? {} : { chainFailureIndex: chain.index }),
617
+ linked: links.linked, events: links.events,
618
+ };
619
+ }
620
+ const text = ok
621
+ ? `ledger intact: ${result.records} records, ${result.writers.length} writers${result.checkpoint ? `, checkpoint ${result.checkpoint.sequence}` : ''}${decisions ? `, ${decisions.linked} decision links` : ''}`
622
+ : `ledger integrity failure: ${failures.map(failure => failure.reason).join(', ')}`;
623
+ return {
624
+ output: {
625
+ schema: 'aiwg.effect.verify.v1', ...result, ok, exitCode: ok ? EFFECT_EXIT_CODES.ok : EFFECT_EXIT_CODES.integrity, failures,
626
+ ...(decisions ? { decisions } : {}),
627
+ },
628
+ text,
629
+ };
630
+ }
631
+ async function runCheckpoint(session) {
632
+ const result = await writeCheckpoint(openLedger(session));
633
+ return {
634
+ output: {
635
+ schema: 'aiwg.effect.checkpoint.v1', exitCode: 0, sequence: result.checkpoint.sequence, digest: result.digest,
636
+ root: result.checkpoint.root, createdAt: result.checkpoint.createdAt, writers: result.checkpoint.writers, sink: result.sink,
637
+ },
638
+ text: `checkpoint ${result.checkpoint.sequence} ${result.digest} -> ${result.sink.reference}`,
639
+ };
640
+ }
641
+ async function runKinds(session) {
642
+ const registry = await sessionRegistry(session);
643
+ const kinds = registry.listKinds();
644
+ const registered = new Set(kinds.map(kind => kind.kind));
645
+ const unverified = CORE_EFFECT_KINDS.filter(kind => !registered.has(kind));
646
+ return {
647
+ output: { schema: 'aiwg.effect.kinds.v1', exitCode: 0, kinds, coreKinds: [...CORE_EFFECT_KINDS], unverifiedCoreKinds: unverified },
648
+ text: kinds.map(kind => `${kind.kind} ${kind.version}${kind.canReportAbsent ? '' : ' (cannot report absent)'}`).join('\n'),
649
+ };
650
+ }
651
+ async function runKeys(session, parsed) {
652
+ const action = parsed.positionals[0] ?? 'list';
653
+ if (parsed.positionals.length > 1)
654
+ throw usage('aiwg effect keys takes one action', 'unexpected-argument');
655
+ if (!['list', 'init', 'rotate'].includes(action))
656
+ throw usage(`Unknown keys action: ${action}`, 'unknown-subcommand');
657
+ if (parsed.values.has('reason') && action !== 'rotate')
658
+ throw usage('--reason applies to keys rotate only', 'unknown-flag');
659
+ const provider = describeKeyProvider(session);
660
+ const base = { schema: 'aiwg.effect.keys.v1', action, scope: session.scope, keyProvider: provider };
661
+ const ledger = openLedger(session);
662
+ ledger.paths();
663
+ if (action === 'list') {
664
+ const keyring = publicKeyring(await readLedgerKeyring(ledger));
665
+ return {
666
+ output: { ...base, exitCode: 0, ...keyring },
667
+ text: keyring.keys.map(key => `${key.keyid} ${key.status}`).join('\n') || 'no ledger keys',
668
+ };
669
+ }
670
+ if (action === 'init') {
671
+ const store = keyStore(session, 'current');
672
+ let provisioned = false;
673
+ if (store) {
674
+ let existing;
675
+ try {
676
+ existing = await store.loadSecret();
677
+ }
678
+ catch {
679
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key could not be read from the host secret service', 'key-unavailable');
680
+ }
681
+ if (!existing) {
682
+ try {
683
+ await store.saveSecret(generateLedgerKeySecret());
684
+ }
685
+ catch {
686
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key could not be stored in the host secret service', 'key-unavailable');
687
+ }
688
+ provisioned = true;
689
+ }
690
+ }
691
+ const initialized = openLedger(session);
692
+ const key = await initialized.signingKey();
693
+ const keyring = publicKeyring(await initLedgerKeyring(initialized));
694
+ return {
695
+ output: { ...base, exitCode: 0, provisioned, keyid: key.keyid, publicKey: key.publicKey, ...keyring },
696
+ text: `${provisioned ? 'provisioned' : 'existing'} ledger key ${key.keyid}`,
697
+ };
698
+ }
699
+ const reason = one(parsed, 'reason') ?? 'scheduled';
700
+ if (!['scheduled', 'custody-change', 'compromise'].includes(reason))
701
+ throw usage('--reason must be scheduled, custody-change or compromise', 'invalid-reason');
702
+ const current = keyStore(session, 'current');
703
+ const next = keyStore(session, 'next');
704
+ if (!current || !next)
705
+ throw usage('Key rotation needs a writable key store; the test key provider cannot rotate', 'key-store-required');
706
+ const loadKey = async (store) => {
707
+ let secret;
708
+ try {
709
+ secret = await store.loadSecret();
710
+ }
711
+ catch {
712
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key could not be read from the host secret service', 'key-unavailable');
713
+ }
714
+ return secret ? new LedgerSigningKey(parseLedgerPrivateKey(secret)) : null;
715
+ };
716
+ const saveSecret = async (store, secret) => {
717
+ try {
718
+ await store.saveSecret(secret);
719
+ }
720
+ catch {
721
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key could not be stored in the host secret service', 'key-unavailable');
722
+ }
723
+ };
724
+ const prior = await loadKey(current);
725
+ if (!prior)
726
+ throw new EffectLedgerError('key-unavailable', 'Effect ledger key is not provisioned; run aiwg effect keys init', 'key-unavailable');
727
+ await initLedgerKeyring(openLedger(session)).catch(error => {
728
+ if (!(error instanceof EffectLedgerError && error.reason === 'key-not-active'))
729
+ throw error;
730
+ });
731
+ const keyring = await readLedgerKeyring(ledger);
732
+ if (!keyring)
733
+ throw new EffectLedgerError('internal', 'Effect ledger keyring could not be created');
734
+ const staged = await loadKey(next);
735
+ if (staged && activeKey(keyring).keyid === staged.keyid) {
736
+ // A previous rotation updated the keyring but did not promote its key.
737
+ const secret = (await next.loadSecret());
738
+ await saveSecret(current, secret);
739
+ await next.deleteSecret().catch(() => undefined);
740
+ return { output: { ...base, exitCode: 0, recovered: true, from: prior.keyid, to: staged.keyid, ...publicKeyring(keyring) }, text: `completed interrupted rotation to ${staged.keyid}` };
741
+ }
742
+ if (activeKey(keyring).keyid !== prior.keyid) {
743
+ throw new EffectLedgerError('key-unavailable', 'The stored ledger key is not the active key; rotation refused', 'key-not-active');
744
+ }
745
+ const successor = generateLedgerKeySecret();
746
+ await saveSecret(next, successor);
747
+ const successorProvider = staticKeyProvider(successor, 'rotation-successor');
748
+ const successorKey = await successorProvider.load();
749
+ const rotated = await rotateKey(ledger, successorProvider, { reason: reason });
750
+ await saveSecret(current, successor);
751
+ await next.deleteSecret().catch(() => undefined);
752
+ const effectiveAt = rotated.rotations.at(-1).effectiveAt;
753
+ return {
754
+ output: { ...base, exitCode: 0, recovered: false, from: prior.keyid, to: successorKey.keyid, effectiveAt, ...publicKeyring(rotated) },
755
+ text: `rotated ledger key ${prior.keyid} -> ${successorKey.keyid}`,
756
+ };
757
+ }
758
+ async function runRecoverLock(session, parsed) {
759
+ if (parsed.positionals.length)
760
+ throw usage('aiwg effect recover-lock takes --lock <name>', 'unexpected-argument');
761
+ const lock = one(parsed, 'lock');
762
+ const ledger = openLedger(session);
763
+ if (lock === undefined) {
764
+ if (parsed.booleans.has('authorize'))
765
+ throw usage('--authorize needs --lock <name>', 'lock-required');
766
+ const locks = await inspectLedgerLocks(ledger);
767
+ return {
768
+ output: { schema: 'aiwg.effect.locks.v1', exitCode: 0, locks },
769
+ text: locks.map(entry => `${entry.lock} ${entry.state ?? 'free'} (${entry.reason})`).join('\n') || 'no ledger locks held',
770
+ };
771
+ }
772
+ const links = linksFrom(parsed);
773
+ const authorized = parsed.booleans.has('authorize');
774
+ const result = await recoverStaleLedgerLock(ledger, lock, { authorize: async () => authorized, links });
775
+ if (result.outcome === 'not-authorized') {
776
+ const error = usage('Stale lock recovery needs explicit operator authorization (--authorize)', 'authorization-required');
777
+ return { output: { ...errorOutput(error), lock, inspection: result.inspection }, text: `${error.message}; ${lock} owner is ${result.inspection.state}` };
778
+ }
779
+ return {
780
+ output: { schema: 'aiwg.effect.lock-recovery.v1', ...result },
781
+ text: `${result.outcome} ${lock} (${result.inspection.reason})${result.effectId ? ` recorded as ${result.effectId}` : ''}`,
782
+ };
783
+ }
784
+ // ---------------------------------------------------------------------------
785
+ // Handler
786
+ // ---------------------------------------------------------------------------
787
+ async function execute(ctx, deps) {
788
+ const args = ctx.args ?? [];
789
+ const wantsText = args.includes('--format') && args[args.indexOf('--format') + 1] === 'text';
790
+ const format = wantsText || args.includes('--format=text') ? 'text' : 'json';
791
+ if (args.length === 0 || args[0] === 'help' || args.includes('--help') || args.includes('-h')) {
792
+ return { exitCode: 0, message: effectUsage(), rawOutput: true };
793
+ }
794
+ try {
795
+ const sub = args[0];
796
+ if (!EFFECT_SUBCOMMANDS.includes(sub))
797
+ throw usage(`Unknown effect subcommand: ${args[0]}`, 'unknown-subcommand');
798
+ const parsed = parseArgs(args.slice(1));
799
+ checkFlags(sub, parsed);
800
+ const formatValue = one(parsed, 'format');
801
+ if (formatValue !== undefined && formatValue !== 'json' && formatValue !== 'text')
802
+ throw usage('--format must be json or text', 'invalid-format');
803
+ const projectDir = resolve(one(parsed, 'project-dir') ?? ctx.cwd ?? process.cwd());
804
+ const subsystem = resolveSubsystem(parsed);
805
+ const config = await hostConfig(projectDir);
806
+ const tenant = config.tenant ?? 'local';
807
+ const project = config.project ?? defaultProject(projectDir);
808
+ if (!IDENTIFIER.test(tenant) || !IDENTIFIER.test(project))
809
+ throw usage('aiwg.config effects tenant and project must be identifiers', 'config-invalid');
810
+ const scope = { tenant, project, subsystem };
811
+ const env = deps.env ?? process.env;
812
+ const account = config.keyProvider?.account ?? `ledger/${tenant}/${project}/${subsystem}`;
813
+ const session = { projectDir, scope, config, account, deps, env };
814
+ let rendered;
815
+ switch (sub) {
816
+ case 'id':
817
+ rendered = await runId(session, parsed);
818
+ break;
819
+ case 'intent':
820
+ rendered = await runIntent(session, parsed);
821
+ break;
822
+ case 'record':
823
+ rendered = await runRecord(session, parsed);
824
+ break;
825
+ case 'lookup':
826
+ rendered = await runLookup(session, parsed);
827
+ break;
828
+ case 'reconcile':
829
+ rendered = await runReconcile(session, parsed);
830
+ break;
831
+ case 'probe':
832
+ rendered = await runProbe(session, parsed);
833
+ break;
834
+ case 'verify':
835
+ rendered = await runVerify(session, parsed);
836
+ break;
837
+ case 'checkpoint':
838
+ rendered = await runCheckpoint(session);
839
+ break;
840
+ case 'kinds':
841
+ rendered = await runKinds(session);
842
+ break;
843
+ case 'keys':
844
+ rendered = await runKeys(session, parsed);
845
+ break;
846
+ case 'recover-lock':
847
+ rendered = await runRecoverLock(session, parsed);
848
+ break;
849
+ default: throw usage(`Unknown effect subcommand: ${String(sub)}`, 'unknown-subcommand');
850
+ }
851
+ return emit(rendered, format);
852
+ }
853
+ catch (error) {
854
+ const output = errorOutput(error);
855
+ const detail = output.error;
856
+ return emit({ output, text: `error (${detail.reason ?? detail.code}): ${detail.message}` }, format);
857
+ }
858
+ }
859
+ export function createEffectHandler(deps = {}) {
860
+ return {
861
+ id: 'effect',
862
+ name: 'Effect Ledger',
863
+ description: 'Record, look up, reconcile and verify side effects in the signed effect ledger',
864
+ category: 'utility',
865
+ aliases: ['effects'],
866
+ async help() { return { exitCode: 0, message: effectUsage(), rawOutput: true }; },
867
+ execute: ctx => execute(ctx, deps),
868
+ };
869
+ }
870
+ export const effectHandler = createEffectHandler();
871
+ export const effectHandlers = [effectHandler];
872
+ //# sourceMappingURL=effect.js.map