@crewhaus/spec 0.1.8 → 0.2.1

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 (3) hide show
  1. package/dist/index.d.ts +4759 -206
  2. package/dist/index.js +339 -6
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -26,6 +26,24 @@ const safeName = z
26
26
  * Will grow into the full catalog spec (eval, deploy) — see
27
27
  * docs/MODULE-CATALOG.md PART A Layer F1.
28
28
  */
29
+ /**
30
+ * Section 28 (#43) — OPTIONAL spec-schema version stamp. Every target schema is
31
+ * `.strict()`, so before this field a migration that stamped `version: 1` on a
32
+ * spec's YAML produced a document `parseSpec` REJECTED ("Unrecognized key(s)").
33
+ * This additive optional field gives migrations somewhere to stamp:
34
+ *
35
+ * - ABSENT → the spec is current/unversioned (the pre-#43 world). Fully
36
+ * back-compat: every existing spec keeps parsing unchanged.
37
+ * - PRESENT → a non-negative integer the migration-engine reads as the spec's
38
+ * schema version (`spec.version ?? 0` in migration-engine/migration-runner).
39
+ *
40
+ * It is a spec-schema knob, NOT the IR `version` (which stays `0` — the IR's
41
+ * own contract version). `lower()` does not thread this field into the IR; it
42
+ * exists purely so the versioned-migration chain has a home. Added to EVERY
43
+ * member of the discriminated union because the union is `.strict()` — a field
44
+ * absent from a member would still be rejected on that target.
45
+ */
46
+ const versionField = z.number().int().nonnegative().optional();
29
47
  // Permissions block (Section 7). SECURITY: `mode: "bypass"` is intentionally
30
48
  // absent from the enum — bypass can only enter the system via the CLI flag.
31
49
  // Defense in depth: parse-time and runtime checks both reject it.
@@ -163,6 +181,158 @@ const toolConfigBlock = z
163
181
  }
164
182
  })
165
183
  .optional();
184
+ /**
185
+ * Item 22 — spec-declared provider failover chain, on the agent blocks of
186
+ * the shapes whose emitted runtime calls `runChatLoop` with a single
187
+ * primary model (cli, channel, managed today).
188
+ *
189
+ * `model_fallbacks` is an ordered list of model strings tried when the
190
+ * primary's circuit breaker is open — each entry follows the SAME
191
+ * model-string grammar as `agent.model` (`claude-*`, `openai/*`,
192
+ * `bedrock/*`, `groq/*`, …; validated like `agent.model` at parse time,
193
+ * with the full grammar enforced by the model-router when resolved).
194
+ * Cross-provider fallbacks resolve their own credentials lazily via the
195
+ * normal model-router path — a fallback with a missing key warns at boot
196
+ * and is skipped when tried, never hard-failing the run.
197
+ *
198
+ * `circuit_breaker` tunes the per-candidate breakers. Field names mirror
199
+ * `CircuitBreakerOptions` in `@crewhaus/circuit-breaker` exactly
200
+ * (failureThreshold / windowMs / cooldownMs); package defaults (5 failures
201
+ * / 60s window / 30s cooldown) apply per field when omitted. Declaring
202
+ * `circuit_breaker` WITHOUT `model_fallbacks` is valid: the primary
203
+ * adapter alone gets breaker-wrapped (fail-fast on a degraded provider
204
+ * instead of hammering it).
205
+ */
206
+ const modelFallbacksBlock = z.array(z.string().min(1)).min(1).optional();
207
+ const circuitBreakerBlock = z
208
+ .object({
209
+ /** Consecutive failures inside windowMs that trip the breaker. */
210
+ failureThreshold: z.number().int().positive().optional(),
211
+ /** Window for counting consecutive failures (ms). */
212
+ windowMs: z.number().int().positive().optional(),
213
+ /** How long the breaker stays open before allowing a probe (ms). */
214
+ cooldownMs: z.number().int().positive().optional(),
215
+ })
216
+ .strict()
217
+ .optional();
218
+ /**
219
+ * Item 26 — opt-in two-tier turn-difficulty router. When present, the runtime
220
+ * picks a model tier PER TURN from deterministic signals (estimated context
221
+ * tokens, whether tools are in play, turn index, prior-turn tool_use density):
222
+ * the cheap `fast` model for easy turns, the `default` model for hard ones. A
223
+ * fast-tier turn that FAILS re-runs on `default` (misroute recovery). Both are
224
+ * full model-router grammar strings. `routing` tunes the escalation thresholds
225
+ * (all optional — sensible defaults apply). Omitted entirely → single-model
226
+ * behaviour, byte-identical bundles.
227
+ */
228
+ const modelTiersBlock = z
229
+ .object({
230
+ fast: z.string().min(1),
231
+ default: z.string().min(1),
232
+ routing: z
233
+ .object({
234
+ contextTokenThreshold: z.number().int().positive().optional(),
235
+ toolsToDefault: z.boolean().optional(),
236
+ firstTurnToDefault: z.boolean().optional(),
237
+ priorToolDensityThreshold: z.number().int().positive().optional(),
238
+ })
239
+ .strict()
240
+ .optional(),
241
+ })
242
+ .strict()
243
+ .optional();
244
+ /**
245
+ * Adaptive model routing — an opt-in `model_pool` of user-declared candidate
246
+ * models the runtime selects among PER TURN, with a selection `policy` that can
247
+ * improve the more the harness runs. The N-candidate generalisation of
248
+ * `model_tiers` (which is the two-candidate special case), so the two are
249
+ * mutually exclusive on an agent block (enforced by a refine on each shape).
250
+ *
251
+ * The user always declares the SET; the runtime only ever picks WITHIN it —
252
+ * `agent.model` (and the optimizer's model-path exclusion) is untouched, so
253
+ * learning tunes selection policy, never the candidate roster.
254
+ *
255
+ * - `candidates` — ≥2 model-router grammar strings, each with free-form
256
+ * `tags` (e.g. `cheap`, `strong`) the heuristic routes on. Declare
257
+ * cheapest→strongest; tags override that order.
258
+ * - `policy` — `static` (first candidate), `heuristic` (deterministic
259
+ * difficulty routing, the default), or `learned` (reward-scoreboard arm
260
+ * selection that improves with usage).
261
+ * - `objective` — weights for the learned reward (quality / cost / latency);
262
+ * defaults to quality-dominant (0.7 / 0.2 / 0.1).
263
+ * - `routing` — difficulty thresholds (shared with `model_tiers`) plus the
264
+ * `strongTag`/`cheapTag` the heuristic prefers.
265
+ * - `learning` — read only for `policy: learned`: the per-arm exploration
266
+ * floor and the cost/latency reward references.
267
+ *
268
+ * Omitted entirely → single-model behaviour, byte-identical bundles.
269
+ */
270
+ const modelPoolBlock = z
271
+ .object({
272
+ candidates: z
273
+ .array(z
274
+ .object({
275
+ model: z.string().min(1),
276
+ tags: z.array(z.string().min(1)).default([]),
277
+ })
278
+ .strict())
279
+ .min(2),
280
+ policy: z.enum(["static", "heuristic", "learned"]).default("heuristic"),
281
+ objective: z
282
+ .object({
283
+ quality: z.number().min(0).optional(),
284
+ cost: z.number().min(0).optional(),
285
+ latency: z.number().min(0).optional(),
286
+ })
287
+ .strict()
288
+ .optional(),
289
+ routing: z
290
+ .object({
291
+ contextTokenThreshold: z.number().int().positive().optional(),
292
+ toolsToDefault: z.boolean().optional(),
293
+ firstTurnToDefault: z.boolean().optional(),
294
+ priorToolDensityThreshold: z.number().int().positive().optional(),
295
+ strongTag: z.string().min(1).optional(),
296
+ cheapTag: z.string().min(1).optional(),
297
+ })
298
+ .strict()
299
+ .optional(),
300
+ learning: z
301
+ .object({
302
+ minSamplesPerArm: z.number().int().positive().optional(),
303
+ costRefUsd: z.number().positive().optional(),
304
+ latencyRefMs: z.number().int().positive().optional(),
305
+ })
306
+ .strict()
307
+ .optional(),
308
+ })
309
+ .strict()
310
+ .optional();
311
+ /**
312
+ * Mutual-exclusion refine shared by every agent block that carries model
313
+ * routing: `model_pool` is the superset of both the two-tier router and the
314
+ * ordered failover chain, so declaring it alongside either is an error rather
315
+ * than an ambiguous double-route. (Per-candidate failover chains compose in a
316
+ * later release; this release keeps precedence unambiguous.)
317
+ */
318
+ function refineModelSelection(agent, ctx) {
319
+ if (agent.model_pool === undefined)
320
+ return;
321
+ if (agent.model_tiers !== undefined) {
322
+ ctx.addIssue({
323
+ code: z.ZodIssueCode.custom,
324
+ message: "agent.model_pool and agent.model_tiers are mutually exclusive — model_pool is the N-candidate generalisation of the two-tier router",
325
+ path: ["model_tiers"],
326
+ });
327
+ }
328
+ if (agent.model_fallbacks !== undefined) {
329
+ ctx.addIssue({
330
+ code: z.ZodIssueCode.custom,
331
+ message: "agent.model_pool and agent.model_fallbacks are mutually exclusive in this release — per-candidate failover chains are a future addition",
332
+ path: ["model_fallbacks"],
333
+ });
334
+ }
335
+ }
166
336
  /**
167
337
  * Section 17 — optional override for the model used by
168
338
  * `compaction-autocompact` when summarising long conversations. Defaults
@@ -265,19 +435,51 @@ const failureTaxonomyEntrySchema = z
265
435
  .object({
266
436
  class: z.string().min(1),
267
437
  pattern: z.string().min(1),
268
- recovery: z.enum(["retry", "compact", "continue", "tombstone", "fail"]),
438
+ // Item 23 `switch-model` routes the same turn onto the next provider
439
+ // failover candidate (pairs with `agent.model_fallbacks`; a no-op
440
+ // re-issue when no chain is declared). See recovery-engine +
441
+ // AUTOMATION-OPPORTUNITIES.md item 23.
442
+ recovery: z.enum(["retry", "compact", "continue", "tombstone", "switch-model", "fail"]),
269
443
  hint: z.string().min(1).optional(),
270
444
  })
271
445
  .strict();
272
446
  const failureTaxonomyBlock = z.array(failureTaxonomyEntrySchema).optional();
447
+ /**
448
+ * Item 27 — run-level spend cap with a degradation ladder. Generalizes the
449
+ * optimizer's `--budget-usd` to normal runs. `usd` is the dollar ceiling;
450
+ * when the run's accrued spend reaches it, `on_exceed` decides:
451
+ * - `{ action: "stop" }` — end the run cleanly before the next turn.
452
+ * - `{ action: "degrade", model }` — re-resolve the primary model to the
453
+ * cheaper `model` (one rung) and continue; a later breach on the
454
+ * degraded model stops the run.
455
+ * The check is PRE-TURN (beside compaction), so an in-flight turn always
456
+ * completes. Carried on the same interactive shapes as the failover chain
457
+ * (cli, channel, managed). `on_exceed.model` follows the agent.model
458
+ * grammar. Defaults to `{ action: "stop" }` when `on_exceed` is omitted.
459
+ */
460
+ const budgetBlock = z
461
+ .object({
462
+ usd: z.number().positive(),
463
+ on_exceed: z
464
+ .discriminatedUnion("action", [
465
+ z.object({ action: z.literal("stop") }).strict(),
466
+ z.object({ action: z.literal("degrade"), model: z.string().min(1) }).strict(),
467
+ ])
468
+ .default({ action: "stop" }),
469
+ })
470
+ .strict()
471
+ .optional();
273
472
  /**
274
473
  * Response-feedback block — declares that a harness collects human ratings on
275
474
  * agent responses (thumbs/stars/scale/comment) which `crewhaus distill` turns
276
475
  * into eval datasets + graders. Cross-cutting like security: carried on the
277
476
  * interactive shapes that consume it (cli, channel). `channelReactions` gates
278
477
  * codegen of Slack 👍/👎 → feedback in the channel target; `modality`/`storage`
279
- * configure the capture surfaces; `autoDistill` is a forward-looking flag for a
280
- * continuous flywheel. `.strict()` so a typo'd sub-key fails the build.
478
+ * configure the capture surfaces; `autoDistill` turns accumulated ratings into
479
+ * versioned `<name>-ratings` registry datasets at CLI run teardown (item 1);
480
+ * `exitPrompt` gates the one-keystroke REPL exit rating prompt (default on
481
+ * when the block is present; set `false` to keep capture surfaces without the
482
+ * prompt). `.strict()` so a typo'd sub-key fails the build.
281
483
  */
282
484
  const feedbackBlock = z
283
485
  .object({
@@ -286,10 +488,93 @@ const feedbackBlock = z
286
488
  scale: z.object({ min: z.number().int(), max: z.number().int() }).strict().optional(),
287
489
  storage: z.object({ location: safeName }).strict().optional(),
288
490
  autoDistill: z.boolean().optional(),
491
+ exitPrompt: z.boolean().optional(),
289
492
  channelReactions: z.boolean().optional(),
290
493
  })
291
494
  .strict()
292
495
  .optional();
496
+ /**
497
+ * Feature #53 — cross-session memory block. Its mere presence wires the
498
+ * Remember/Recall tools into the harness (no hand-editing). The auto-*
499
+ * switches layer on top: `autoCapture` summarizes the session's durable
500
+ * outcomes into `.crewhaus/memories/<name>.jsonl` at run teardown;
501
+ * `autoRecall` injects the top-`recallK` relevant memories into the system
502
+ * prompt at session start (mirrors project-memory auto-load). Carried on the
503
+ * interactive shapes that run a chat loop (cli, channel, managed, research).
504
+ * `.strict()` so a typo'd sub-key fails the build.
505
+ */
506
+ const memoryBlock = z
507
+ .object({
508
+ enabled: z.boolean().optional(),
509
+ autoCapture: z.boolean().optional(),
510
+ autoCaptureThreshold: z.number().int().positive().optional(),
511
+ autoRecall: z.boolean().optional(),
512
+ recallK: z.number().int().positive().max(50).optional(),
513
+ })
514
+ .strict()
515
+ .optional();
516
+ /**
517
+ * Ops item 37 — cross-cutting `observability` block. Today it carries one
518
+ * sub-block, `slo`, that declares production Service-Level Objectives + the
519
+ * mitigation ladder the runtime SLO monitor walks on a SUSTAINED breach.
520
+ *
521
+ * The monitor (runtime-core, env/spec-gated) folds bus events into rolling
522
+ * windows (reusing the alert-watchdog's accumulator + the metrics-collector's
523
+ * TTFT histogram) and, when a target is breached for `windowSeconds`, executes
524
+ * the ladder rungs in the declared order: `alert` (webhook/hook), `pause-intake`
525
+ * (gateway/managed 429 `budget_exceeded` path), `rollback` (auto-rollback the
526
+ * env pin via deployment-controller). Every rung is audit-logged.
527
+ *
528
+ * Targets are all OPTIONAL — declare only the SLOs you care about; an omitted
529
+ * target is never evaluated. `mitigation` defaults to `["alert"]` (observe-only
530
+ * is safe) so a spec that lists thresholds without a ladder still warns. Higher
531
+ * rungs are opt-in because they touch traffic/deploys — a spec must ask for them
532
+ * explicitly. `.strict()` so a typo'd sub-key fails the build.
533
+ *
534
+ * NOTE `egress_block_rate` derives from the `permission_decision` egress
535
+ * outcomes (no dedicated egress TraceEvent exists); `ttft_ms`/`p95_latency_ms`
536
+ * derive from the same per-turn/TTFT samples the alert-watchdog accumulates.
537
+ */
538
+ const sloBlock = z
539
+ .object({
540
+ /** Fractional error rate ceiling (unrecovered errors / model calls), e.g. 0.05. */
541
+ error_rate: z.number().min(0).max(1).optional(),
542
+ /** p95 per-turn latency ceiling, milliseconds. */
543
+ p95_latency_ms: z.number().positive().optional(),
544
+ /** p95 time-to-first-token ceiling, milliseconds. */
545
+ ttft_ms: z.number().positive().optional(),
546
+ /** Cost burn ceiling, USD per hour of wall-clock. */
547
+ cost_per_hour_usd: z.number().positive().optional(),
548
+ /** Fractional egress-block rate ceiling (egress-blocked / external calls), e.g. 0.1. */
549
+ egress_block_rate: z.number().min(0).max(1).optional(),
550
+ /**
551
+ * Rolling window (seconds) a breach must persist before the ladder fires.
552
+ * A single blip never mitigates — the monitor only acts on a SUSTAINED
553
+ * breach across this window. Default 300s (5 min).
554
+ */
555
+ window_seconds: z.number().int().positive().optional(),
556
+ /**
557
+ * Mitigation ladder, walked in declared order on a sustained breach. Each
558
+ * rung is executed at most once per session. `alert` is always safe;
559
+ * `pause-intake` / `rollback` touch traffic + deploys so they are opt-in.
560
+ */
561
+ mitigation: z
562
+ .array(z.enum(["alert", "pause-intake", "rollback"]))
563
+ .nonempty()
564
+ .optional(),
565
+ })
566
+ .strict()
567
+ .refine((s) => s.error_rate !== undefined ||
568
+ s.p95_latency_ms !== undefined ||
569
+ s.ttft_ms !== undefined ||
570
+ s.cost_per_hour_usd !== undefined ||
571
+ s.egress_block_rate !== undefined, { message: "observability.slo must declare at least one target threshold" });
572
+ const observabilityBlock = z
573
+ .object({
574
+ slo: sloBlock.optional(),
575
+ })
576
+ .strict()
577
+ .optional();
293
578
  /**
294
579
  * Section 47 — blockchain subsystem blocks (cross-cutting). Any shape may
295
580
  * declare any subset of `chains` / `wallets` / `contracts` /
@@ -422,6 +707,7 @@ const channelGatewayBlock = z
422
707
  const cliSchema = z
423
708
  .object({
424
709
  name: safeName,
710
+ version: versionField,
425
711
  target: z.literal("cli"),
426
712
  agent: z
427
713
  .object({
@@ -431,9 +717,17 @@ const cliSchema = z
431
717
  // runtime default applies. Raise it for turns that emit large
432
718
  // multi-file edits so the model isn't cut off mid-`tool_use`.
433
719
  max_tokens: z.number().int().positive().optional(),
720
+ // Item 22 — provider failover chain (see modelFallbacksBlock docs).
721
+ model_fallbacks: modelFallbacksBlock,
722
+ circuit_breaker: circuitBreakerBlock,
723
+ // Item 26 — opt-in two-tier turn-difficulty router.
724
+ model_tiers: modelTiersBlock,
725
+ // Adaptive model routing — N-candidate pool with a selection policy.
726
+ model_pool: modelPoolBlock,
434
727
  sub_agents: subAgentsBlock,
435
728
  })
436
- .strict(),
729
+ .strict()
730
+ .superRefine(refineModelSelection),
437
731
  tools: z.array(z.string().min(1)).optional(),
438
732
  tool_config: toolConfigBlock,
439
733
  mcp_servers: mcpServersBlock,
@@ -441,7 +735,10 @@ const cliSchema = z
441
735
  compaction: compactionBlock,
442
736
  security: securityBlock,
443
737
  failure_taxonomy: failureTaxonomyBlock,
738
+ budget: budgetBlock,
444
739
  feedback: feedbackBlock,
740
+ memory: memoryBlock,
741
+ observability: observabilityBlock,
445
742
  cli: cliOptionsBlock,
446
743
  chains: chainsBlock,
447
744
  wallets: walletsBlock,
@@ -461,6 +758,7 @@ const workflowStepSchema = z
461
758
  const workflowSchema = z
462
759
  .object({
463
760
  name: safeName,
761
+ version: versionField,
464
762
  target: z.literal("workflow"),
465
763
  model: z.string().min(1),
466
764
  steps: z.array(workflowStepSchema).min(1),
@@ -543,14 +841,23 @@ const channelAgentSchema = z
543
841
  .object({
544
842
  model: z.string().min(1),
545
843
  instructions: z.string().min(1),
844
+ // Item 22 — provider failover chain (see modelFallbacksBlock docs).
845
+ model_fallbacks: modelFallbacksBlock,
846
+ circuit_breaker: circuitBreakerBlock,
847
+ // Item 26 — opt-in two-tier turn-difficulty router.
848
+ model_tiers: modelTiersBlock,
849
+ // Adaptive model routing — N-candidate pool with a selection policy.
850
+ model_pool: modelPoolBlock,
546
851
  tools: z.array(z.string().min(1)).optional(),
547
852
  tool_config: toolConfigBlock,
548
853
  sub_agents: subAgentsBlock,
549
854
  })
550
- .strict();
855
+ .strict()
856
+ .superRefine(refineModelSelection);
551
857
  const channelSchema = z
552
858
  .object({
553
859
  name: safeName,
860
+ version: versionField,
554
861
  target: z.literal("channel"),
555
862
  agent: channelAgentSchema,
556
863
  channels: channelsBlock,
@@ -559,7 +866,10 @@ const channelSchema = z
559
866
  permissions: permissionsBlock,
560
867
  compaction: compactionBlock,
561
868
  failure_taxonomy: failureTaxonomyBlock,
869
+ budget: budgetBlock,
562
870
  feedback: feedbackBlock,
871
+ memory: memoryBlock,
872
+ observability: observabilityBlock,
563
873
  heartbeat: heartbeatBlock,
564
874
  gateway: channelGatewayBlock,
565
875
  chains: chainsBlock,
@@ -599,6 +909,7 @@ const graphEdgeSchema = z
599
909
  const graphSchema = z
600
910
  .object({
601
911
  name: safeName,
912
+ version: versionField,
602
913
  target: z.literal("graph"),
603
914
  model: z.string().min(1),
604
915
  entry: z.string().min(1),
@@ -632,17 +943,29 @@ const managedAgentSchema = z
632
943
  .object({
633
944
  model: z.string().min(1),
634
945
  instructions: z.string().min(1),
946
+ // Item 22 — provider failover chain (see modelFallbacksBlock docs).
947
+ model_fallbacks: modelFallbacksBlock,
948
+ circuit_breaker: circuitBreakerBlock,
949
+ // Item 26 — opt-in two-tier turn-difficulty router.
950
+ model_tiers: modelTiersBlock,
951
+ // Adaptive model routing — N-candidate pool with a selection policy.
952
+ model_pool: modelPoolBlock,
635
953
  })
636
- .strict();
954
+ .strict()
955
+ .superRefine(refineModelSelection);
637
956
  const managedSchema = z
638
957
  .object({
639
958
  name: safeName,
959
+ version: versionField,
640
960
  target: z.literal("managed"),
641
961
  agent: managedAgentSchema,
642
962
  tenants: z.array(managedTenantSchema).min(1),
643
963
  permissions: permissionsBlock,
644
964
  compaction: compactionBlock,
645
965
  failure_taxonomy: failureTaxonomyBlock,
966
+ budget: budgetBlock,
967
+ memory: memoryBlock,
968
+ observability: observabilityBlock,
646
969
  })
647
970
  .strict();
648
971
  // Vector-store backend ids accepted in specs. Mirrors `VectorBackendId`
@@ -666,6 +989,7 @@ const pipelineDocumentSchema = z
666
989
  const pipelineSchema = z
667
990
  .object({
668
991
  name: safeName,
992
+ version: versionField,
669
993
  target: z.literal("pipeline"),
670
994
  agent: z
671
995
  .object({
@@ -731,6 +1055,7 @@ const crewRoutingSchema = z
731
1055
  const crewSchema = z
732
1056
  .object({
733
1057
  name: safeName,
1058
+ version: versionField,
734
1059
  target: z.literal("crew"),
735
1060
  /** Crew-wide model fallback used by any role that omits `role.model`. */
736
1061
  model: z.string().min(1),
@@ -765,6 +1090,7 @@ const researchRetrieveSchema = z
765
1090
  const researchSchema = z
766
1091
  .object({
767
1092
  name: safeName,
1093
+ version: versionField,
768
1094
  target: z.literal("research"),
769
1095
  agent: z
770
1096
  .object({
@@ -782,6 +1108,7 @@ const researchSchema = z
782
1108
  permissions: permissionsBlock,
783
1109
  compaction: compactionBlock,
784
1110
  failure_taxonomy: failureTaxonomyBlock,
1111
+ memory: memoryBlock,
785
1112
  chains: chainsBlock,
786
1113
  wallets: walletsBlock,
787
1114
  contracts: contractsBlock,
@@ -804,6 +1131,7 @@ const batchQueueSchema = z
804
1131
  const batchSchema = z
805
1132
  .object({
806
1133
  name: safeName,
1134
+ version: versionField,
807
1135
  target: z.literal("batch"),
808
1136
  agent: z
809
1137
  .object({
@@ -844,6 +1172,7 @@ const voiceTelephonySchema = z
844
1172
  const voiceSchema = z
845
1173
  .object({
846
1174
  name: safeName,
1175
+ version: versionField,
847
1176
  target: z.literal("voice"),
848
1177
  agent: z
849
1178
  .object({
@@ -878,6 +1207,7 @@ const browserDriverSchema = z
878
1207
  const browserSchema = z
879
1208
  .object({
880
1209
  name: safeName,
1210
+ version: versionField,
881
1211
  target: z.literal("browser"),
882
1212
  agent: z
883
1213
  .object({
@@ -907,6 +1237,7 @@ const browserSchema = z
907
1237
  const evalSchema = z
908
1238
  .object({
909
1239
  name: safeName,
1240
+ version: versionField,
910
1241
  target: z.literal("eval"),
911
1242
  agent: z
912
1243
  .object({
@@ -971,6 +1302,7 @@ const onchainTriggerSchema = z.discriminatedUnion("kind", [
971
1302
  const onchainSchema = z
972
1303
  .object({
973
1304
  name: safeName,
1305
+ version: versionField,
974
1306
  target: z.literal("onchain"),
975
1307
  agent: z
976
1308
  .object({
@@ -1005,6 +1337,7 @@ const onchainSchema = z
1005
1337
  const onchainGameSchema = z
1006
1338
  .object({
1007
1339
  name: safeName,
1340
+ version: versionField,
1008
1341
  target: z.literal("onchain-game"),
1009
1342
  agent: z
1010
1343
  .object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/spec",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "User-facing spec schema (Zod) + YAML parser",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/errors": "0.1.8",
18
+ "@crewhaus/errors": "0.2.1",
19
19
  "yaml": "^2.6.0",
20
20
  "zod": "^3.23.8"
21
21
  },