@aiwg/cli 2026.9.20 → 2026.9.24

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 (275) 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/cli.js +23 -18
  8. package/dist/src/artifacts/enrichment/cli.js +1 -1
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
  10. package/dist/src/artifacts/query-engine.js +14 -4
  11. package/dist/src/artifacts/views/cli.js +1 -1
  12. package/dist/src/audit/operator-decision.js +10 -0
  13. package/dist/src/auth/credential-store.js +127 -27
  14. package/dist/src/catalog/builtin-models.json +272 -40
  15. package/dist/src/catalog/cli.js +1 -1
  16. package/dist/src/catalog/cli.mjs +1 -1
  17. package/dist/src/cli/handlers/effect.js +872 -0
  18. package/dist/src/cli/handlers/help.js +2 -0
  19. package/dist/src/cli/handlers/index.js +4 -2
  20. package/dist/src/cli/handlers/models.js +41 -10
  21. package/dist/src/cli/handlers/ralph.js +1 -1
  22. package/dist/src/cli/handlers/sessions.js +113 -37
  23. package/dist/src/cli/handlers/subcommands.js +31 -0
  24. package/dist/src/cli/handlers/use.js +46 -9
  25. package/dist/src/cli/handlers/utilities.js +10 -0
  26. package/dist/src/cli/handlers/workspace.js +3 -1
  27. package/dist/src/cli/scope-resolver.js +112 -9
  28. package/dist/src/cli/services/deployment-verification.js +5 -0
  29. package/dist/src/config/aiwg-config.js +3 -0
  30. package/dist/src/decision/acceptance-rollout.js +93 -0
  31. package/dist/src/decision/acceptance.js +175 -0
  32. package/dist/src/decision/adapters/jev.js +234 -13
  33. package/dist/src/decision/adapters/llm-subagent.js +60 -12
  34. package/dist/src/decision/admission.js +550 -0
  35. package/dist/src/decision/batch-benchmark.js +47 -0
  36. package/dist/src/decision/batch-receipts/accounting.js +75 -0
  37. package/dist/src/decision/batch-receipts/export.js +40 -0
  38. package/dist/src/decision/batch-receipts/index.js +9 -0
  39. package/dist/src/decision/batch-receipts/protection.js +40 -0
  40. package/dist/src/decision/batch-receipts/receipt.js +44 -0
  41. package/dist/src/decision/batch-receipts/result-store.js +341 -0
  42. package/dist/src/decision/batch-receipts/store.js +362 -0
  43. package/dist/src/decision/batch-receipts/types.js +2 -0
  44. package/dist/src/decision/batch-receipts/validate.js +127 -0
  45. package/dist/src/decision/batch.js +68 -0
  46. package/dist/src/decision/calibration/index.js +4 -0
  47. package/dist/src/decision/calibration/receipts.js +130 -0
  48. package/dist/src/decision/calibration/registry.js +258 -0
  49. package/dist/src/decision/calibration/types.js +2 -0
  50. package/dist/src/decision/compile-cache/benchmark.js +62 -0
  51. package/dist/src/decision/compile-cache/compatibility.js +65 -0
  52. package/dist/src/decision/compile-cache/file-store.js +280 -0
  53. package/dist/src/decision/compile-cache/identity.js +26 -0
  54. package/dist/src/decision/compile-cache/index.js +9 -0
  55. package/dist/src/decision/compile-cache/prefix.js +21 -0
  56. package/dist/src/decision/compile-cache/records.js +86 -0
  57. package/dist/src/decision/compile-cache/runtime.js +68 -0
  58. package/dist/src/decision/compile-cache/store.js +108 -0
  59. package/dist/src/decision/compile-cache/types.js +2 -0
  60. package/dist/src/decision/context-plan.js +234 -0
  61. package/dist/src/decision/context-qualification.js +60 -0
  62. package/dist/src/decision/ensemble/aggregate.js +215 -0
  63. package/dist/src/decision/ensemble/contract.js +316 -0
  64. package/dist/src/decision/ensemble/drift.js +51 -0
  65. package/dist/src/decision/ensemble/index.js +6 -0
  66. package/dist/src/decision/ensemble/report.js +71 -0
  67. package/dist/src/decision/ensemble/types.js +2 -0
  68. package/dist/src/decision/evaluate.js +1024 -78
  69. package/dist/src/decision/file-lifecycle-store.js +168 -0
  70. package/dist/src/decision/graph-beam.js +40 -0
  71. package/dist/src/decision/graph-budget.js +110 -0
  72. package/dist/src/decision/graph-decision-bridge.js +93 -0
  73. package/dist/src/decision/graph-evidence.js +123 -0
  74. package/dist/src/decision/graph-flow-adapter.js +56 -0
  75. package/dist/src/decision/graph-flow.js +105 -0
  76. package/dist/src/decision/graph-parallel.js +79 -0
  77. package/dist/src/decision/graph-patterns.js +20 -0
  78. package/dist/src/decision/graph-run-store.js +66 -0
  79. package/dist/src/decision/graph-run.js +166 -0
  80. package/dist/src/decision/graph-skill-bridge.js +91 -0
  81. package/dist/src/decision/graph-templates.js +56 -0
  82. package/dist/src/decision/graph.js +115 -0
  83. package/dist/src/decision/index.js +35 -0
  84. package/dist/src/decision/job-accounting.js +93 -0
  85. package/dist/src/decision/job-contract.js +186 -0
  86. package/dist/src/decision/job-effects.js +95 -0
  87. package/dist/src/decision/job-evaluate.js +80 -0
  88. package/dist/src/decision/job-gateway.js +121 -0
  89. package/dist/src/decision/job-http.js +194 -0
  90. package/dist/src/decision/job-payload-store.js +258 -0
  91. package/dist/src/decision/job-poll-limiter.js +92 -0
  92. package/dist/src/decision/job-quota.js +158 -0
  93. package/dist/src/decision/job-runtime.js +240 -0
  94. package/dist/src/decision/job-scheduler.js +26 -0
  95. package/dist/src/decision/job-service.js +117 -0
  96. package/dist/src/decision/job-store.js +264 -0
  97. package/dist/src/decision/job-worker.js +141 -0
  98. package/dist/src/decision/lifecycle.js +148 -0
  99. package/dist/src/decision/patterns/artifacts.js +173 -0
  100. package/dist/src/decision/patterns/catalog.js +108 -0
  101. package/dist/src/decision/patterns/durable-review.js +237 -0
  102. package/dist/src/decision/patterns/index.js +8 -0
  103. package/dist/src/decision/patterns/playground.js +401 -0
  104. package/dist/src/decision/patterns/recorded.js +43 -0
  105. package/dist/src/decision/patterns/specs.js +174 -0
  106. package/dist/src/decision/patterns/types.js +2 -0
  107. package/dist/src/decision/portable-secrets.js +53 -0
  108. package/dist/src/decision/projection.js +210 -0
  109. package/dist/src/decision/qualification/capture.js +67 -0
  110. package/dist/src/decision/qualification/drift.js +68 -0
  111. package/dist/src/decision/qualification/gate-evidence.js +172 -0
  112. package/dist/src/decision/qualification/gates.js +83 -0
  113. package/dist/src/decision/qualification/index.js +12 -0
  114. package/dist/src/decision/qualification/load.js +282 -0
  115. package/dist/src/decision/qualification/manifest.js +37 -0
  116. package/dist/src/decision/qualification/privacy.js +41 -0
  117. package/dist/src/decision/qualification/quality.js +213 -0
  118. package/dist/src/decision/qualification/release.js +116 -0
  119. package/dist/src/decision/qualification/runner.js +408 -0
  120. package/dist/src/decision/qualification/types.js +2 -0
  121. package/dist/src/decision/receipts.js +30 -7
  122. package/dist/src/decision/result-cache/index.js +6 -0
  123. package/dist/src/decision/result-cache/integrity.js +28 -0
  124. package/dist/src/decision/result-cache/key.js +28 -0
  125. package/dist/src/decision/result-cache/service.js +92 -0
  126. package/dist/src/decision/result-cache/store.js +327 -0
  127. package/dist/src/decision/result-cache/types.js +3 -0
  128. package/dist/src/decision/review/authorization.js +110 -0
  129. package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
  130. package/dist/src/decision/review/index.js +12 -0
  131. package/dist/src/decision/review/migrate.js +34 -0
  132. package/dist/src/decision/review/operator-audit.js +59 -0
  133. package/dist/src/decision/review/recovery-adapters.js +252 -0
  134. package/dist/src/decision/review/recovery.js +33 -0
  135. package/dist/src/decision/review/service.js +498 -0
  136. package/dist/src/decision/review/store.js +219 -0
  137. package/dist/src/decision/review/types.js +2 -0
  138. package/dist/src/decision/review/validate.js +205 -0
  139. package/dist/src/decision/scheduler.js +115 -0
  140. package/dist/src/decision/telemetry/context.js +42 -0
  141. package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
  142. package/dist/src/decision/telemetry/exporter.js +77 -0
  143. package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
  144. package/dist/src/decision/telemetry/index.js +13 -0
  145. package/dist/src/decision/telemetry/mapping.js +146 -0
  146. package/dist/src/decision/telemetry/metrics.js +164 -0
  147. package/dist/src/decision/telemetry/otlp-http.js +183 -0
  148. package/dist/src/decision/telemetry/redaction.js +93 -0
  149. package/dist/src/decision/telemetry/retention.js +109 -0
  150. package/dist/src/decision/telemetry/runtime.js +310 -0
  151. package/dist/src/decision/telemetry/trace.js +158 -0
  152. package/dist/src/decision/telemetry/types.js +3 -0
  153. package/dist/src/decision/validate.js +135 -5
  154. package/dist/src/effects/checkpoint-sinks.js +94 -0
  155. package/dist/src/effects/checkpoint.js +76 -0
  156. package/dist/src/effects/decision-links.js +54 -0
  157. package/dist/src/effects/errors.js +48 -0
  158. package/dist/src/effects/identity.js +178 -0
  159. package/dist/src/effects/index.js +25 -0
  160. package/dist/src/effects/keyring.js +160 -0
  161. package/dist/src/effects/keys.js +130 -0
  162. package/dist/src/effects/ledger.js +435 -0
  163. package/dist/src/effects/lifecycle.js +110 -0
  164. package/dist/src/effects/lock-recovery.js +269 -0
  165. package/dist/src/effects/reader.js +103 -0
  166. package/dist/src/effects/records.js +114 -0
  167. package/dist/src/effects/redaction.js +51 -0
  168. package/dist/src/effects/schema.js +73 -0
  169. package/dist/src/effects/store.js +239 -0
  170. package/dist/src/effects/types.js +49 -0
  171. package/dist/src/effects/verifiers/decision.js +168 -0
  172. package/dist/src/effects/verifiers/file.js +106 -0
  173. package/dist/src/effects/verifiers/git.js +206 -0
  174. package/dist/src/effects/verifiers/index.js +90 -0
  175. package/dist/src/effects/verifiers/registry.js +148 -0
  176. package/dist/src/effects/verifiers/review.js +123 -0
  177. package/dist/src/effects/verifiers/tracker.js +299 -0
  178. package/dist/src/effects/verifiers/types.js +21 -0
  179. package/dist/src/effects/verify.js +295 -0
  180. package/dist/src/evidence/bundle.js +2 -0
  181. package/dist/src/extensions/commands/definitions.js +28 -0
  182. package/dist/src/extensions/manifest.js +1 -0
  183. package/dist/src/marketplace/provenance.js +25 -27
  184. package/dist/src/mcp/cli.mjs +11 -1
  185. package/dist/src/mcp/grok-build-config.mjs +189 -0
  186. package/dist/src/mcp/registry.js +21 -1
  187. package/dist/src/mcp/registry.mjs +20 -1
  188. package/dist/src/models/grok-build-models.js +356 -0
  189. package/dist/src/models/index.js +1 -0
  190. package/dist/src/models/model-capabilities.v1.json +34 -0
  191. package/dist/src/models/model-catalog.v1.json +55 -13
  192. package/dist/src/models/model-discovery.js +41 -5
  193. package/dist/src/models/provider-policy.js +3 -2
  194. package/dist/src/plugin/skill-command-translator.js +1 -0
  195. package/dist/src/providers/capability-matrix.yaml +58 -6
  196. package/dist/src/providers/grok-build-acp.js +192 -0
  197. package/dist/src/providers/grok-build-dispatch.js +251 -0
  198. package/dist/src/providers/grok-build-headless.js +170 -0
  199. package/dist/src/providers/muse-paths.js +105 -0
  200. package/dist/src/providers/provider-definitions.js +93 -7
  201. package/dist/src/providers/provider-definitions.mjs +21 -1
  202. package/dist/src/providers/provider-inventory.js +3 -0
  203. package/dist/src/security/artifact-attestation.js +4 -8
  204. package/dist/src/security/signing.js +119 -0
  205. package/dist/src/sessions/adapters/grok-build.js +187 -0
  206. package/dist/src/sessions/adapters/muse.js +412 -0
  207. package/dist/src/sessions/contracts.js +1 -1
  208. package/dist/src/sessions/grok-build-cli.js +196 -0
  209. package/dist/src/sessions/index.js +3 -0
  210. package/dist/src/sessions/workspace-discovery.js +9 -5
  211. package/dist/src/skills/deployer.js +33 -5
  212. package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
  213. package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
  214. package/dist/src/smiths/context-pipeline/finalization.js +1 -32
  215. package/dist/src/smiths/context-pipeline/generator.js +12 -0
  216. package/dist/src/smiths/context-pipeline/index.js +1 -1
  217. package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
  218. package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
  219. package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
  220. package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
  221. package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
  222. package/dist/src/smiths/hook-bridge/index.js +3 -0
  223. package/dist/src/smiths/hook-bridge/shim.js +2 -0
  224. package/dist/src/smiths/hook-bridge/types.js +5 -0
  225. package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
  226. package/dist/src/storage/protected-files.js +102 -0
  227. package/dist/src/tracker/forge-http.js +371 -0
  228. package/dist/src/tracker/project-inputs.js +41 -0
  229. package/package.json +2 -1
  230. package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
  231. package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
  232. package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
  233. package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
  234. package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
  235. package/schemas/decision/DecisionBinding.schema.json +309 -0
  236. package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
  237. package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
  238. package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
  239. package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
  240. package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
  241. package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
  242. package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
  243. package/schemas/decision/DecisionDefinition.schema.json +159 -0
  244. package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
  245. package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
  246. package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
  247. package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
  248. package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
  249. package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
  250. package/schemas/decision/DecisionJob.v1.schema.json +71 -0
  251. package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
  252. package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
  253. package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
  254. package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
  255. package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
  256. package/schemas/decision/DecisionResult.schema.json +524 -0
  257. package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
  258. package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
  259. package/schemas/decision/DecisionReview.schema.json +29 -0
  260. package/schemas/decision/DecisionRuleset.schema.json +313 -0
  261. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
  262. package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
  263. package/schemas/decision/RulesetResult.schema.json +721 -0
  264. package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
  265. package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
  266. package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
  267. package/tools/agents/deploy-agents.mjs +21 -6
  268. package/tools/agents/providers/base.mjs +7 -4
  269. package/tools/agents/providers/codex.mjs +1 -1
  270. package/tools/agents/providers/grok-build.d.mts +7 -0
  271. package/tools/agents/providers/grok-build.mjs +249 -9
  272. package/tools/agents/providers/model-role.mjs +6 -3
  273. package/tools/agents/providers/muse-hooks.mjs +785 -0
  274. package/tools/agents/providers/muse-paths.mjs +112 -0
  275. package/tools/agents/providers/muse.mjs +336 -0
@@ -0,0 +1,264 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { access, link, lstat, mkdir, open, readFile, readdir, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { canonicalJson } from '../security/artifact-trust.js';
5
+ import { assertJobTransition, validateDecisionJob } from './job-contract.js';
6
+ export class JobConflictError extends Error {
7
+ }
8
+ function key(scope, id) { return canonicalJson({ scope, id }); }
9
+ function validateFirst(job) {
10
+ validateDecisionJob(job);
11
+ if (job.state !== 'validating' || job.items.some(item => item.state !== 'queued' || item.attempts.length))
12
+ throw new JobConflictError('Initial job must be undispatched');
13
+ }
14
+ /** Collapse only identical, undispatched caller item IDs; never merge mismatched pins. */
15
+ export function canonicalizeInitialJob(job) {
16
+ if (!Array.isArray(job?.items)) {
17
+ validateFirst(job);
18
+ return job;
19
+ }
20
+ const unique = new Map();
21
+ for (const item of job.items) {
22
+ const previous = unique.get(item?.id);
23
+ if (previous && canonicalJson(previous) !== canonicalJson(item))
24
+ throw new JobConflictError('Duplicate item ID belongs to different request');
25
+ if (!previous)
26
+ unique.set(item?.id, item);
27
+ }
28
+ if (unique.size === job.items.length) {
29
+ validateFirst(job);
30
+ return job;
31
+ }
32
+ if (job.state !== 'validating' || !job.summary || job.summary.queued !== job.items.length ||
33
+ Object.entries(job.summary).some(([state, count]) => state !== 'queued' && count !== 0))
34
+ throw new JobConflictError('Duplicate item summary mismatch');
35
+ const normalized = structuredClone(job);
36
+ normalized.items = [...unique.values()].map(item => structuredClone(item));
37
+ normalized.summary.queued = normalized.items.length;
38
+ validateFirst(normalized);
39
+ return normalized;
40
+ }
41
+ function assertIdentity(initial, existing) {
42
+ if (initial.fingerprint !== existing.fingerprint ||
43
+ canonicalJson(initial.items.map(({ id, fingerprint, subjectDigest, definitionDigest, bindingDigest, rulesetDigest }) => ({ id, fingerprint, subjectDigest, definitionDigest, bindingDigest,
44
+ ...(rulesetDigest ? { rulesetDigest } : {}) }))) !==
45
+ canonicalJson(existing.items.map(({ id, fingerprint, subjectDigest, definitionDigest, bindingDigest, rulesetDigest }) => ({ id, fingerprint, subjectDigest, definitionDigest, bindingDigest,
46
+ ...(rulesetDigest ? { rulesetDigest } : {}) }))) ||
47
+ canonicalJson(initial.budget) !== canonicalJson(existing.budget) ||
48
+ initial.createdAtEpochMs !== existing.createdAtEpochMs || initial.expiresAtEpochMs !== existing.expiresAtEpochMs)
49
+ throw new JobConflictError('Job ID belongs to a different immutable request');
50
+ }
51
+ function validateNext(previous, next) {
52
+ if (previous.deleted || next.revision !== previous.revision + 1 || (next.legalHold !== undefined && typeof next.legalHold !== 'boolean') ||
53
+ (next.deleted && (previous.legalHold || next.legalHold || next.job.state !== previous.job.state)))
54
+ throw new JobConflictError('Invalid job revision or tombstone');
55
+ if (Boolean(previous.legalHold) !== Boolean(next.legalHold)) {
56
+ if (next.deleted || canonicalJson(previous.job) !== canonicalJson(next.job))
57
+ throw new JobConflictError('Legal hold cannot change during job transition');
58
+ }
59
+ else if (next.deleted) {
60
+ if (canonicalJson(previous.job) !== canonicalJson(next.job))
61
+ throw new JobConflictError('Tombstone changed job');
62
+ }
63
+ else
64
+ assertJobTransition(previous.job, next.job);
65
+ }
66
+ const copy = (value) => structuredClone(value);
67
+ export class MemoryJobStore {
68
+ records = new Map();
69
+ async acquire(job) {
70
+ job = canonicalizeInitialJob(job);
71
+ const id = key(job.scope, job.id);
72
+ const existing = this.records.get(id);
73
+ if (existing) {
74
+ assertIdentity(job, existing.job);
75
+ return { owner: false, snapshot: copy(existing) };
76
+ }
77
+ const snapshot = { revision: 1, job: structuredClone(job), deleted: false, legalHold: false };
78
+ this.records.set(id, snapshot);
79
+ return { owner: true, snapshot: copy(snapshot) };
80
+ }
81
+ async read(scope, id) {
82
+ const record = this.records.get(key(scope, id));
83
+ return record ? copy(record) : null;
84
+ }
85
+ async compareAndSwap(previous, next) {
86
+ validateNext(previous, next);
87
+ const id = key(previous.job.scope, previous.job.id);
88
+ const current = this.records.get(id);
89
+ if (!current || canonicalJson(current) !== canonicalJson(previous))
90
+ return false;
91
+ this.records.set(id, copy(next));
92
+ return true;
93
+ }
94
+ }
95
+ /** Append-only, immutable revision journal. Link publication is atomic across processes on one local filesystem. */
96
+ export class FileJobStore {
97
+ directory;
98
+ externallyDeleted;
99
+ constructor(directory, externallyDeleted) {
100
+ this.directory = directory;
101
+ this.externallyDeleted = externallyDeleted;
102
+ }
103
+ async acquire(job) {
104
+ job = canonicalizeInitialJob(job);
105
+ await this.ensureDirectory();
106
+ if (await this.isDeleted(job.scope, job.id))
107
+ throw new JobConflictError('Job tombstoned');
108
+ const snapshot = { revision: 1, job: structuredClone(job), deleted: false, legalHold: false };
109
+ if (await this.publish(snapshot))
110
+ return { owner: true, snapshot: copy(snapshot) };
111
+ const existing = await this.read(job.scope, job.id);
112
+ if (!existing)
113
+ throw new JobConflictError('Job revision unavailable');
114
+ assertIdentity(job, existing.job);
115
+ return { owner: false, snapshot: existing };
116
+ }
117
+ async read(scope, id) {
118
+ await this.ensureDirectory();
119
+ if (await this.isDeleted(scope, id))
120
+ return null;
121
+ const prefix = this.prefix(scope, id);
122
+ const revisions = (await readdir(this.directory)).flatMap(name => {
123
+ const match = name.match(new RegExp(`^${prefix}\\.r([0-9]+)\\.json$`));
124
+ return match ? [Number(match[1])] : [];
125
+ }).sort((a, b) => a - b);
126
+ let previous = null;
127
+ for (const [index, revision] of revisions.entries()) {
128
+ if (revision !== index + 1)
129
+ throw new JobConflictError('Job revision gap');
130
+ const snapshot = JSON.parse(await readFile(join(this.directory, `${prefix}.r${revision}.json`), 'utf8'));
131
+ if (snapshot.revision !== revision || key(snapshot.job.scope, snapshot.job.id) !== key(scope, id))
132
+ throw new JobConflictError('Job scope substitution');
133
+ if (previous)
134
+ validateNext(previous, snapshot);
135
+ else {
136
+ validateFirst(snapshot.job);
137
+ if (snapshot.deleted || snapshot.legalHold)
138
+ throw new JobConflictError('Invalid initial snapshot');
139
+ }
140
+ previous = snapshot;
141
+ }
142
+ return previous ? copy(previous) : null;
143
+ }
144
+ async compareAndSwap(previous, next) {
145
+ validateNext(previous, next);
146
+ const current = await this.read(previous.job.scope, previous.job.id);
147
+ if (!current || canonicalJson(current) !== canonicalJson(previous))
148
+ return false;
149
+ return this.publish(next);
150
+ }
151
+ /** Host-only quota scan. Validate every live journal; corrupted records fail admission closed. */
152
+ async listSnapshots() {
153
+ await this.ensureDirectory();
154
+ const snapshots = [];
155
+ for (const name of await readdir(this.directory)) {
156
+ if (!/^[a-f0-9]{64}\.r1\.json$/.test(name))
157
+ continue;
158
+ const first = JSON.parse(await readFile(join(this.directory, name), 'utf8'));
159
+ if (name !== `${this.prefix(first.job.scope, first.job.id)}.r1.json`)
160
+ throw new JobConflictError('Job scope substitution');
161
+ const current = await this.read(first.job.scope, first.job.id);
162
+ if (current)
163
+ snapshots.push(current);
164
+ }
165
+ return snapshots;
166
+ }
167
+ /** Host-only D10 eraser: call after lifecycle tombstone and authorized job deletion. */
168
+ async purgeDeleted(scope, id) {
169
+ const record = await this.read(scope, id);
170
+ if (record && (!record.deleted || record.legalHold))
171
+ throw new JobConflictError('Job not eligible for erasure');
172
+ if (!record && !await this.hasMarker(scope, id) && !await this.externallyDeleted?.(scope, id))
173
+ throw new JobConflictError('Job tombstone required before erasure');
174
+ await this.ensureDirectory();
175
+ if (!await this.hasMarker(scope, id)) {
176
+ try {
177
+ const handle = await open(this.marker(scope, id), 'wx', 0o600);
178
+ try {
179
+ await handle.sync();
180
+ }
181
+ finally {
182
+ await handle.close();
183
+ }
184
+ }
185
+ catch (error) {
186
+ if (error.code !== 'EEXIST')
187
+ throw error;
188
+ }
189
+ }
190
+ const base = this.prefix(scope, id);
191
+ for (const name of await readdir(this.directory)) {
192
+ if ((name.startsWith(`${base}.r`) && /^r[1-9][0-9]*\.json$/.test(name.slice(base.length + 1))) ||
193
+ (name.startsWith(`.${base}.job-`) && /^job-[0-9a-f-]{36}\.tmp$/.test(name.slice(base.length + 2))))
194
+ await rm(join(this.directory, name));
195
+ }
196
+ const directory = await open(this.directory, 'r');
197
+ try {
198
+ await directory.sync();
199
+ }
200
+ finally {
201
+ await directory.close();
202
+ }
203
+ }
204
+ async ensureDirectory() {
205
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
206
+ const info = await lstat(this.directory);
207
+ if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o077) !== 0)
208
+ throw new JobConflictError('Job storage root must be private');
209
+ }
210
+ marker(scope, id) { return join(this.directory, `${this.prefix(scope, id)}.deleted`); }
211
+ async isDeleted(scope, id) {
212
+ return await this.hasMarker(scope, id) || Boolean(await this.externallyDeleted?.(scope, id));
213
+ }
214
+ async hasMarker(scope, id) {
215
+ try {
216
+ await access(this.marker(scope, id));
217
+ return true;
218
+ }
219
+ catch (error) {
220
+ if (error.code === 'ENOENT')
221
+ return false;
222
+ throw error;
223
+ }
224
+ }
225
+ prefix(scope, id) {
226
+ return createHash('sha256').update(key(scope, id)).digest('hex');
227
+ }
228
+ async publish(snapshot) {
229
+ await this.ensureDirectory();
230
+ const prefix = this.prefix(snapshot.job.scope, snapshot.job.id);
231
+ const destination = join(this.directory, `${prefix}.r${snapshot.revision}.json`);
232
+ const temporary = join(this.directory, `.${prefix}.job-${randomUUID()}.tmp`);
233
+ const handle = await open(temporary, 'wx', 0o600);
234
+ try {
235
+ await handle.writeFile(`${canonicalJson(snapshot)}\n`, 'utf8');
236
+ await handle.sync();
237
+ }
238
+ finally {
239
+ await handle.close();
240
+ }
241
+ try {
242
+ if (await this.isDeleted(snapshot.job.scope, snapshot.job.id))
243
+ throw new JobConflictError('Job tombstoned');
244
+ await link(temporary, destination);
245
+ const directory = await open(this.directory, 'r');
246
+ try {
247
+ await directory.sync();
248
+ }
249
+ finally {
250
+ await directory.close();
251
+ }
252
+ return true;
253
+ }
254
+ catch (error) {
255
+ if (error.code === 'EEXIST')
256
+ return false;
257
+ throw error;
258
+ }
259
+ finally {
260
+ await rm(temporary, { force: true });
261
+ }
262
+ }
263
+ }
264
+ //# sourceMappingURL=job-store.js.map
@@ -0,0 +1,141 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { recount } from './job-runtime.js';
3
+ import { JobConflictError } from './job-store.js';
4
+ export class OfflineJobWorker {
5
+ runtime;
6
+ effects;
7
+ active = new Map();
8
+ constructor(runtime, effects) {
9
+ this.runtime = runtime;
10
+ this.effects = effects;
11
+ }
12
+ /** Persist a dispatch fence before invoking the fixture; ambiguous failures never replay. */
13
+ async run(actor, jobId, itemId, executor, reservation) {
14
+ const previous = await this.runtime.poll(actor, jobId);
15
+ if (!previous)
16
+ throw new JobConflictError('Job unavailable');
17
+ const item = previous.job.items.find(candidate => candidate.id === itemId);
18
+ if (!item || item.state !== 'queued' || !['queued', 'running', 'partially-completed'].includes(previous.job.state) ||
19
+ item.attempts.length >= previous.job.budget.maxAttempts ||
20
+ previous.job.items.filter(candidate => candidate.state === 'running').length >= previous.job.budget.maxConcurrency)
21
+ throw new JobConflictError('Item cannot be dispatched');
22
+ if (executor.requiresReservation && !reservation)
23
+ throw new JobConflictError('Provider work requires budget reservation');
24
+ if (reservation) {
25
+ if (!Number.isSafeInteger(reservation.tokens) || reservation.tokens < 0 ||
26
+ !Number.isSafeInteger(reservation.costMicros) || reservation.costMicros < 0)
27
+ throw new JobConflictError('Invalid job budget reservation');
28
+ const attempts = previous.job.items.flatMap(candidate => candidate.attempts);
29
+ const spentTokens = attempts.reduce((sum, attempt) => sum + (attempt.reservedTokens ?? 0), 0);
30
+ const spentCost = attempts.reduce((sum, attempt) => sum + (attempt.reservedCostMicros ?? 0), 0);
31
+ if (spentTokens + reservation.tokens > previous.job.budget.maxTokens ||
32
+ spentCost + reservation.costMicros > previous.job.budget.maxCostMicros)
33
+ throw new JobConflictError('Job budget reservation exceeded');
34
+ }
35
+ const next = structuredClone(previous.job);
36
+ const scheduled = next.items.find(candidate => candidate.id === itemId);
37
+ scheduled.state = 'running';
38
+ scheduled.attempts.push({ id: randomUUID(), requestDigest: item.fingerprint, outcome: 'dispatched',
39
+ ...(reservation ? { reservedTokens: reservation.tokens, reservedCostMicros: reservation.costMicros } : {}) });
40
+ next.state = previous.job.state === 'queued' ? 'running' : previous.job.state;
41
+ recount(next);
42
+ const key = `${JSON.stringify(actor)}:${jobId}:${itemId}:${scheduled.attempts.at(-1).id}`;
43
+ const controller = new AbortController();
44
+ this.active.set(key, controller);
45
+ // Register cancellation before publishing the fence; CAS losers never call the executor.
46
+ try {
47
+ await this.runtime.advance(actor, jobId, previous, next);
48
+ }
49
+ catch (error) {
50
+ this.active.delete(key);
51
+ throw error;
52
+ }
53
+ let outcome = null;
54
+ let onAbort;
55
+ try {
56
+ if (!controller.signal.aborted) {
57
+ const aborted = new Promise((_resolve, reject) => {
58
+ onAbort = () => reject(new JobConflictError('Execution interrupted'));
59
+ controller.signal.addEventListener('abort', onAbort, { once: true });
60
+ });
61
+ outcome = await Promise.race([executor(structuredClone(scheduled), controller.signal), aborted]);
62
+ }
63
+ }
64
+ catch { /* A thrown/aborted fixture may have executed; do not replay. */ }
65
+ finally {
66
+ if (onAbort)
67
+ controller.signal.removeEventListener('abort', onAbort);
68
+ this.active.delete(key);
69
+ }
70
+ const fenced = scheduled.attempts.at(-1);
71
+ if (this.effects && outcome && !controller.signal.aborted && validOutcome(outcome) && outcome.receiptDigest &&
72
+ ['succeeded', 'abstained', 'review'].includes(outcome.state)) {
73
+ try {
74
+ await this.effects.recordReceipt({ scope: structuredClone(previous.job.scope), jobId, itemId, attemptId: fenced.id,
75
+ requestDigest: fenced.requestDigest, receiptDigest: outcome.receiptDigest });
76
+ }
77
+ catch { /* Recovery evidence only; the job record below stays authoritative. */ }
78
+ }
79
+ // Concurrent items may finish in the same tick. Retry only the local CAS, never the executor.
80
+ for (let retry = 0; retry < 5; retry++) {
81
+ const latest = await this.runtime.poll(actor, jobId);
82
+ if (!latest)
83
+ throw new JobConflictError('Job removed during execution');
84
+ const current = latest.job.items.find(candidate => candidate.id === itemId);
85
+ if (!current || current.state !== 'running' || current.attempts.at(-1)?.id !== scheduled.attempts.at(-1)?.id)
86
+ throw new JobConflictError('In-flight item changed during execution');
87
+ const updated = structuredClone(latest.job);
88
+ const target = updated.items.find(candidate => candidate.id === itemId);
89
+ const attempt = target.attempts.at(-1);
90
+ if (controller.signal.aborted || !outcome || !validOutcome(outcome)) {
91
+ target.state = 'execution-unknown';
92
+ attempt.outcome = 'execution-unknown';
93
+ }
94
+ else {
95
+ target.state = outcome.state;
96
+ attempt.outcome = ['succeeded', 'abstained', 'review'].includes(outcome.state) ? 'succeeded' : 'failed';
97
+ if (outcome.receiptDigest)
98
+ attempt.receiptDigest = outcome.receiptDigest;
99
+ if (outcome.resultDigest)
100
+ target.resultDigest = outcome.resultDigest;
101
+ if (outcome.errorCode)
102
+ target.errorCode = outcome.errorCode;
103
+ }
104
+ recount(updated);
105
+ const outstanding = updated.summary.queued + updated.summary.running + updated.summary['retryable-failed'];
106
+ const unknown = updated.summary['execution-unknown'] > 0;
107
+ if (latest.job.state === 'cancel-requested') {
108
+ updated.state = unknown ? 'failed' : outstanding ? 'partially-completed' : 'canceled';
109
+ }
110
+ else
111
+ updated.state = outstanding ? 'partially-completed' : unknown ? 'failed' : 'completed';
112
+ try {
113
+ return await this.runtime.advance(actor, jobId, latest, updated);
114
+ }
115
+ catch (error) {
116
+ if (!(error instanceof JobConflictError) || error.message !== 'Concurrent job update')
117
+ throw error;
118
+ }
119
+ }
120
+ throw new JobConflictError('Concurrent job updates exhausted');
121
+ }
122
+ async cancel(actor, jobId) {
123
+ const snapshot = await this.runtime.cancel(actor, jobId);
124
+ if (snapshot)
125
+ for (const [key, controller] of this.active) {
126
+ if (key.startsWith(`${JSON.stringify(actor)}:${jobId}:`))
127
+ controller.abort();
128
+ }
129
+ return snapshot;
130
+ }
131
+ }
132
+ function validOutcome(outcome) {
133
+ if (!outcome || !['succeeded', 'abstained', 'review', 'unsupported', 'retryable-failed', 'permanent-failed'].includes(outcome.state))
134
+ return false;
135
+ const digest = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
136
+ if (['succeeded', 'abstained', 'review'].includes(outcome.state))
137
+ return digest(outcome.receiptDigest) && digest(outcome.resultDigest) && outcome.errorCode === undefined;
138
+ return !outcome.resultDigest && !outcome.receiptDigest &&
139
+ (outcome.errorCode === undefined || /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(outcome.errorCode));
140
+ }
141
+ //# sourceMappingURL=job-worker.js.map
@@ -0,0 +1,148 @@
1
+ /** Common host-side lifecycle contract. Never derive these controls from model state. */
2
+ export const DECISION_LIFECYCLE_VERSION = 'decision-lifecycle/v1';
3
+ export const DECISION_LIFECYCLE_SURFACES = [
4
+ 'state', 'receipt', 'trace', 'debug-sidecar', 'cache', 'job', 'review',
5
+ 'calibration', 'evaluation', 'export', 'preprocessing-lineage',
6
+ ];
7
+ export function validateDecisionLifecyclePolicy(policy) {
8
+ if (policy?.version !== DECISION_LIFECYCLE_VERSION || !policy.surfaces
9
+ || Object.keys(policy.surfaces).length !== DECISION_LIFECYCLE_SURFACES.length
10
+ || Object.keys(policy.surfaces).some(surface => !DECISION_LIFECYCLE_SURFACES.includes(surface))) {
11
+ throw new Error('Decision lifecycle policy is incomplete');
12
+ }
13
+ for (const surface of DECISION_LIFECYCLE_SURFACES) {
14
+ const rule = policy.surfaces[surface];
15
+ if (!rule || !['public', 'internal', 'confidential', 'restricted'].includes(rule.classification)
16
+ || !Array.isArray(rule.accessScopes) || !rule.accessScopes.length
17
+ || rule.accessScopes.some(scope => typeof scope !== 'string' || !scope)
18
+ || !Number.isSafeInteger(rule.retentionMs) || rule.retentionMs <= 0
19
+ || !['denied', 'sanitized'].includes(rule.export) || !['erase', 'tombstone'].includes(rule.deletion)
20
+ || !['not-persisted', 'expire-with-primary'].includes(rule.backup)) {
21
+ throw new Error('Decision lifecycle rule is incomplete');
22
+ }
23
+ }
24
+ }
25
+ /** Hold requires an independent authorization decision and finite expiry. */
26
+ export async function placeDecisionLifecycleHold(hold, authorize, store, now) {
27
+ if (!hold.subject || !hold.reason || !hold.authorizedBy || !hold.scope.length
28
+ || hold.scope.some(surface => !DECISION_LIFECYCLE_SURFACES.includes(surface))
29
+ || !Number.isSafeInteger(hold.expiresAt) || hold.expiresAt <= now)
30
+ throw new Error('Decision lifecycle hold denied');
31
+ let approved = false;
32
+ try {
33
+ approved = await authorize(hold);
34
+ }
35
+ catch { /* authorization fails closed */ }
36
+ if (!approved)
37
+ throw new Error('Decision lifecycle hold denied');
38
+ const stored = structuredClone(hold);
39
+ try {
40
+ await store.recordHold(stored);
41
+ }
42
+ catch {
43
+ throw new Error('Decision lifecycle hold recording failed');
44
+ }
45
+ return stored;
46
+ }
47
+ export async function releaseDecisionLifecycleHold(hold, actor, reason, authorize, store, now) {
48
+ if (!actor || !reason || !Number.isSafeInteger(now) || now < 0)
49
+ throw new Error('Decision lifecycle hold release denied');
50
+ let approved = false;
51
+ try {
52
+ approved = await authorize(hold, actor);
53
+ }
54
+ catch { /* authorization fails closed */ }
55
+ if (!approved)
56
+ throw new Error('Decision lifecycle hold release denied');
57
+ try {
58
+ await store.releaseHold(hold, actor, reason, now);
59
+ }
60
+ catch {
61
+ throw new Error('Decision lifecycle hold release failed');
62
+ }
63
+ }
64
+ /** Cascading erasure runs only after the host resolves linked records. No body is returned. */
65
+ export async function eraseDecisionSubject(subject, policy, store, now) {
66
+ validateDecisionLifecyclePolicy(policy);
67
+ if (!subject || !Number.isSafeInteger(now) || now < 0)
68
+ throw new Error('Decision lifecycle deletion denied');
69
+ let links;
70
+ let holds;
71
+ try {
72
+ links = await store.links(subject);
73
+ holds = await store.holds(subject);
74
+ }
75
+ catch {
76
+ throw new Error('Decision lifecycle lookup failed');
77
+ }
78
+ if (holds.some(hold => hold.subject === subject && hold.expiresAt > now
79
+ && links.some(link => hold.scope.includes(link.surface))))
80
+ throw new Error('Decision lifecycle deletion denied by hold');
81
+ const tombstones = [];
82
+ for (const reference of links) {
83
+ if (!DECISION_LIFECYCLE_SURFACES.includes(reference.surface) || !reference.opaqueId) {
84
+ throw new Error('Decision lifecycle reference invalid');
85
+ }
86
+ const tombstone = { subject, reference: { ...reference }, deletedAt: now };
87
+ // Publish the tombstone before erasure so a failed erase cannot resurrect on restore.
88
+ try {
89
+ await store.tombstone(tombstone);
90
+ await store.erase(reference);
91
+ }
92
+ catch {
93
+ throw new Error('Decision lifecycle erasure failed');
94
+ }
95
+ tombstones.push(tombstone);
96
+ }
97
+ return tombstones;
98
+ }
99
+ /** A restored backup cannot make erased content queryable. */
100
+ export function mayRestoreDecisionReference(reference, createdAt, now, policy, tombstones) {
101
+ validateDecisionLifecyclePolicy(policy);
102
+ if (!DECISION_LIFECYCLE_SURFACES.includes(reference.surface) || !Number.isSafeInteger(createdAt)
103
+ || !Number.isSafeInteger(now) || now < createdAt)
104
+ return false;
105
+ return !tombstones.some(value => value.reference.surface === reference.surface && value.reference.opaqueId === reference.opaqueId)
106
+ && now - createdAt < policy.surfaces[reference.surface].retentionMs;
107
+ }
108
+ /**
109
+ * Subject-level restore across lifecycle surfaces. Each entry is checked against tombstones,
110
+ * the surface backup rule and primary retention before its host restore handler runs, so a
111
+ * backup taken before erasure cannot make erased or expired content queryable again.
112
+ */
113
+ export async function restoreDecisionSubjectBackup(backup, policy, tombstones, restore, now) {
114
+ validateDecisionLifecyclePolicy(policy);
115
+ if (!Array.isArray(backup) || !Number.isSafeInteger(now) || now < 0)
116
+ throw new Error('Decision lifecycle restore denied');
117
+ const report = { restored: [], refused: [] };
118
+ for (const entry of backup) {
119
+ const reference = entry?.reference;
120
+ let reason = null;
121
+ if (!entry?.subject || !reference?.opaqueId || !DECISION_LIFECYCLE_SURFACES.includes(reference.surface)
122
+ || !Number.isSafeInteger(entry.createdAt) || entry.createdAt > now)
123
+ reason = 'invalid';
124
+ else if (tombstones.some(value => value.reference.surface === reference.surface
125
+ && value.reference.opaqueId === reference.opaqueId))
126
+ reason = 'tombstoned';
127
+ else if (policy.surfaces[reference.surface].backup === 'not-persisted')
128
+ reason = 'not-persisted';
129
+ else if (!mayRestoreDecisionReference(reference, entry.createdAt, now, policy, tombstones))
130
+ reason = 'expired';
131
+ else if (!restore[reference.surface])
132
+ reason = 'restore-unavailable';
133
+ const copy = structuredClone(entry);
134
+ if (reason) {
135
+ report.refused.push({ entry: copy, reason });
136
+ continue;
137
+ }
138
+ try {
139
+ await restore[reference.surface](reference.opaqueId);
140
+ }
141
+ catch {
142
+ throw new Error('Decision lifecycle restore failed');
143
+ }
144
+ report.restored.push(copy);
145
+ }
146
+ return report;
147
+ }
148
+ //# sourceMappingURL=lifecycle.js.map