@crewhaus/spec 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +22299 -9100
- package/dist/index.js +1069 -67
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SpecParseError } from "@crewhaus/errors";
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
5
|
// SECURITY (codegen-injection backstop, #147/#148): spec/role/node/step names
|
|
5
6
|
// flow verbatim into generated source across ~14 emitters — `//` and `/* */`
|
|
6
7
|
// comments, template literals, JSON `package.json` manifests, YAML frontmatter,
|
|
@@ -57,6 +58,26 @@ const permissionsBlock = z
|
|
|
57
58
|
.object({
|
|
58
59
|
mode: z.enum(["default", "plan", "auto"]).optional(),
|
|
59
60
|
rules: z.array(permissionRuleSchema).optional(),
|
|
61
|
+
/**
|
|
62
|
+
* Loop contract 0.4 (Batch C, G11) — what a tool permission that resolves
|
|
63
|
+
* to `ask` does on a NON-interactive surface (single-turn / daemon /
|
|
64
|
+
* gateway, anywhere without a synchronous human prompt). `"pause"`
|
|
65
|
+
* (DEFAULT — the safe direction) parks the turn: the runtime persists a
|
|
66
|
+
* `PendingApproval`, publishes an `approval_requested` trace event, and
|
|
67
|
+
* ends the turn with the `approval_pending` failure class + resume token
|
|
68
|
+
* so a later `grant`/`deny` decision re-runs the tool call pre-resolved.
|
|
69
|
+
* `"deny"` is the pre-0.4 collapse behaviour — an ask on a non-interactive
|
|
70
|
+
* surface becomes a denial in place. The REPL always keeps its
|
|
71
|
+
* synchronous prompt regardless of this key.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately OMITTED from `OPTIMIZABLE_PATHS`: this is a safety /
|
|
74
|
+
* human-in-the-loop control, not a quality knob. Letting an optimizer
|
|
75
|
+
* flip a pending-approval `pause` to `deny` (or vice-versa) would let the
|
|
76
|
+
* search loop silently rewrite the approval posture of a deployment — the
|
|
77
|
+
* same reason the intent-gate grader and other safety surfaces stay out
|
|
78
|
+
* of the optimizer's reach.
|
|
79
|
+
*/
|
|
80
|
+
ask_mode: z.enum(["pause", "deny"]).optional(),
|
|
60
81
|
})
|
|
61
82
|
.strict()
|
|
62
83
|
.optional();
|
|
@@ -102,6 +123,17 @@ const subAgentDefinitionSchema = z
|
|
|
102
123
|
])
|
|
103
124
|
.optional(),
|
|
104
125
|
inherit_bypass: z.boolean().optional(),
|
|
126
|
+
/**
|
|
127
|
+
* Item 2 (G31 — A2A federation) — wire this sub-agent to a REMOTE peer
|
|
128
|
+
* instead of spawning it locally. `url` is the peer deployment's base
|
|
129
|
+
* URL; the spawner routes the Task call through `@crewhaus/federation-
|
|
130
|
+
* router` to the peer's inbound A2A handler (whose Agent Card lives at
|
|
131
|
+
* `<url>/.well-known/agent-card.json`), mapping the federation envelope
|
|
132
|
+
* onto A2A message/task semantics. Present ⇒ the entry is a federated
|
|
133
|
+
* peer reference; `description`/`instructions` still describe it to the
|
|
134
|
+
* parent's Task tool (the remote peer owns its own prompt).
|
|
135
|
+
*/
|
|
136
|
+
federation: z.object({ url: z.string().url() }).strict().optional(),
|
|
105
137
|
})
|
|
106
138
|
.strict();
|
|
107
139
|
const subAgentsBlock = z.record(safeName, subAgentDefinitionSchema).optional();
|
|
@@ -361,6 +393,21 @@ function refineModelSelection(agent, ctx) {
|
|
|
361
393
|
const compactionBlock = z
|
|
362
394
|
.object({
|
|
363
395
|
model: z.string().min(1).optional(),
|
|
396
|
+
/** Loop contract 0.4 (Batch A) — context-window fill fraction that
|
|
397
|
+
* triggers autocompaction (e.g. 0.85 = compact at 85% full). Bounded
|
|
398
|
+
* to 0.5–0.99: below half the window a compaction pass costs more
|
|
399
|
+
* than it saves, and 1.0 would only ever fire after an overflow.
|
|
400
|
+
* OPTIMIZABLE (`["compaction","threshold"]` in spec-patch). When
|
|
401
|
+
* omitted the runtime default applies. */
|
|
402
|
+
threshold: z.number().gte(0.5).lte(0.99).optional(),
|
|
403
|
+
/** Loop contract 0.4 (Batch A) — messages preserved verbatim at the
|
|
404
|
+
* HEAD of the transcript by `compaction-snip` before summarising the
|
|
405
|
+
* middle. When omitted the snip package's default applies. */
|
|
406
|
+
snip_keep_head: z.number().int().positive().optional(),
|
|
407
|
+
/** Loop contract 0.4 (Batch A) — messages preserved verbatim at the
|
|
408
|
+
* TAIL of the transcript by `compaction-snip`. When omitted the snip
|
|
409
|
+
* package's default applies. */
|
|
410
|
+
snip_keep_tail: z.number().int().positive().optional(),
|
|
364
411
|
/** Pillar 2 — opt in to the pre-compaction curator pass. Defaults
|
|
365
412
|
* to `false` when omitted; the IR carries the user's choice
|
|
366
413
|
* verbatim so target emitters can wire `@crewhaus/compaction-curator`
|
|
@@ -488,6 +535,297 @@ const budgetBlock = z
|
|
|
488
535
|
})
|
|
489
536
|
.strict()
|
|
490
537
|
.optional();
|
|
538
|
+
/**
|
|
539
|
+
* Loop contract 0.4 (Batch B, G02) — the top-level `evaluation:` block:
|
|
540
|
+
* in-loop output evaluation on the interactive shapes (cli, channel,
|
|
541
|
+
* managed). After each completed assistant turn the runtime scores the
|
|
542
|
+
* final text with `grader`; a score below `threshold` triggers the
|
|
543
|
+
* `on_fail` behaviour:
|
|
544
|
+
*
|
|
545
|
+
* - `retry` (default) — re-prompt the model with the judge's rationale
|
|
546
|
+
* appended as a system nudge, at most `max_retries` times.
|
|
547
|
+
* - `halt` — abort the turn with a classified `evaluation` failure.
|
|
548
|
+
* - `note` — emit an `eval_graded` trace event only.
|
|
549
|
+
*
|
|
550
|
+
* Graders:
|
|
551
|
+
* - `{ type: llm_judge, criteria, model? }` — a model scores the reply
|
|
552
|
+
* in [0,1] against `criteria`; `model` defaults to the shape's primary
|
|
553
|
+
* model (the `cheapest` sentinel resolves like `compaction.model`).
|
|
554
|
+
* Judge calls are METERED into the run budget.
|
|
555
|
+
* - `{ type: contains, value }` / `{ type: regex, value }` —
|
|
556
|
+
* deterministic pass/fail text checks (no threshold; no model spend).
|
|
557
|
+
*
|
|
558
|
+
* `threshold` (0..1, default 0.7) applies to `llm_judge` only — declaring
|
|
559
|
+
* it with a deterministic grader is a parse error. `.strict()` throughout
|
|
560
|
+
* so a typo'd sub-key fails the build.
|
|
561
|
+
*/
|
|
562
|
+
const evaluationGraderSchema = z.discriminatedUnion("type", [
|
|
563
|
+
z
|
|
564
|
+
.object({
|
|
565
|
+
type: z.literal("llm_judge"),
|
|
566
|
+
model: z
|
|
567
|
+
.string()
|
|
568
|
+
.min(1)
|
|
569
|
+
.optional()
|
|
570
|
+
.describe("judge model id; defaults to the shape's primary model (the cheapest sentinel resolves at compile time)"),
|
|
571
|
+
criteria: z
|
|
572
|
+
.string()
|
|
573
|
+
.min(1)
|
|
574
|
+
.describe("what a passing reply must satisfy — the judge scores the final text against this"),
|
|
575
|
+
})
|
|
576
|
+
.strict()
|
|
577
|
+
.describe("model-scored grader: an LLM judges the final text in [0,1] against criteria"),
|
|
578
|
+
z
|
|
579
|
+
.object({
|
|
580
|
+
type: z.literal("contains"),
|
|
581
|
+
value: z.string().min(1).describe("substring the final text must contain (case-sensitive)"),
|
|
582
|
+
})
|
|
583
|
+
.strict()
|
|
584
|
+
.describe("deterministic grader: pass iff the final text contains value"),
|
|
585
|
+
z
|
|
586
|
+
.object({
|
|
587
|
+
type: z.literal("regex"),
|
|
588
|
+
value: z.string().min(1).describe("JavaScript regular expression the final text must match"),
|
|
589
|
+
})
|
|
590
|
+
.strict()
|
|
591
|
+
.describe("deterministic grader: pass iff the final text matches the regex"),
|
|
592
|
+
]);
|
|
593
|
+
const evaluationBlock = z
|
|
594
|
+
.object({
|
|
595
|
+
grader: evaluationGraderSchema,
|
|
596
|
+
threshold: z
|
|
597
|
+
.number()
|
|
598
|
+
.min(0)
|
|
599
|
+
.max(1)
|
|
600
|
+
.optional()
|
|
601
|
+
.describe("passing score in 0..1 (default 0.7); llm_judge grader only"),
|
|
602
|
+
on_fail: z
|
|
603
|
+
.enum(["retry", "halt", "note"])
|
|
604
|
+
.optional()
|
|
605
|
+
.describe("below-threshold behaviour: retry re-prompts with the judge rationale (default), halt aborts the turn classified, note emits a trace event only"),
|
|
606
|
+
max_retries: z
|
|
607
|
+
.number()
|
|
608
|
+
.int()
|
|
609
|
+
.min(1)
|
|
610
|
+
.max(5)
|
|
611
|
+
.optional()
|
|
612
|
+
.describe("hard cap on evaluation-triggered retries per turn (default 1)"),
|
|
613
|
+
})
|
|
614
|
+
.strict()
|
|
615
|
+
.superRefine((e, ctx) => {
|
|
616
|
+
if (e.threshold !== undefined && e.grader.type !== "llm_judge") {
|
|
617
|
+
ctx.addIssue({
|
|
618
|
+
code: z.ZodIssueCode.custom,
|
|
619
|
+
path: ["threshold"],
|
|
620
|
+
message: `evaluation.threshold applies to the llm_judge grader only — the "${e.grader.type}" grader is deterministic pass/fail`,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (e.grader.type === "regex") {
|
|
624
|
+
try {
|
|
625
|
+
new RegExp(e.grader.value);
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
ctx.addIssue({
|
|
629
|
+
code: z.ZodIssueCode.custom,
|
|
630
|
+
path: ["grader", "value"],
|
|
631
|
+
message: `evaluation.grader.value is not a valid regular expression: ${err instanceof Error ? err.message : String(err)}`,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
})
|
|
636
|
+
.describe("in-loop output evaluation: score each completed assistant turn and retry/halt/note below threshold")
|
|
637
|
+
.optional();
|
|
638
|
+
/**
|
|
639
|
+
* Loop contract 0.4 (Batch B, G02) — the `judge:` gate declared by
|
|
640
|
+
* `kind: "judge"` workflow steps and graph nodes. A judge step/node runs no
|
|
641
|
+
* agent turn of its own: it scores the PREVIOUS step's (workflow) /
|
|
642
|
+
* upstream node's (graph) final output in [0,1] against `criteria` and,
|
|
643
|
+
* below `threshold` (default 0.7), applies `on_fail`:
|
|
644
|
+
*
|
|
645
|
+
* - `retry_previous` (default) — re-run the gated step/node with the
|
|
646
|
+
* judge rationale appended as a system nudge, at most `max_retries`
|
|
647
|
+
* times.
|
|
648
|
+
* - `halt` — abort the run with a classified `evaluation` failure.
|
|
649
|
+
* - `continue` — record the `judge_verdict` trace event and proceed.
|
|
650
|
+
*
|
|
651
|
+
* `model` defaults to the shape's top-level `model` (the `cheapest`
|
|
652
|
+
* sentinel resolves like `compaction.model`). Judge calls are METERED into
|
|
653
|
+
* the run budget.
|
|
654
|
+
*/
|
|
655
|
+
const judgeGateBlock = z
|
|
656
|
+
.object({
|
|
657
|
+
criteria: z
|
|
658
|
+
.string()
|
|
659
|
+
.min(1)
|
|
660
|
+
.describe("what a passing upstream output must satisfy — the judge scores against this"),
|
|
661
|
+
model: z
|
|
662
|
+
.string()
|
|
663
|
+
.min(1)
|
|
664
|
+
.optional()
|
|
665
|
+
.describe("judge model id; defaults to the shape's top-level model"),
|
|
666
|
+
threshold: z.number().min(0).max(1).optional().describe("passing score in 0..1 (default 0.7)"),
|
|
667
|
+
on_fail: z
|
|
668
|
+
.enum(["retry_previous", "halt", "continue"])
|
|
669
|
+
.optional()
|
|
670
|
+
.describe("below-threshold behaviour: retry_previous re-runs the gated step/node (default), halt aborts classified, continue records the verdict and proceeds"),
|
|
671
|
+
max_retries: z
|
|
672
|
+
.number()
|
|
673
|
+
.int()
|
|
674
|
+
.min(1)
|
|
675
|
+
.max(5)
|
|
676
|
+
.optional()
|
|
677
|
+
.describe("hard cap on judge-triggered re-runs of the gated step/node (default 1)"),
|
|
678
|
+
})
|
|
679
|
+
.strict()
|
|
680
|
+
.describe("judge gate config for kind: judge workflow steps and graph nodes");
|
|
681
|
+
/**
|
|
682
|
+
* Loop contract 0.4 (Batch A) — extended-thinking selector, carried on the
|
|
683
|
+
* agent blocks of the interactive shapes (cli, channel, managed) and at
|
|
684
|
+
* step/node/role granularity on workflow steps, graph nodes, and crew roles.
|
|
685
|
+
* Exactly ONE of the two forms must be declared (enforced by superRefine):
|
|
686
|
+
*
|
|
687
|
+
* - `{ budget_tokens: n }` — an explicit thinking-token budget (>= 1024,
|
|
688
|
+
* the provider floor), passed through to the provider verbatim.
|
|
689
|
+
* - `{ effort: low|medium|high }` — a portable effort preset the adapter
|
|
690
|
+
* layer converts to a provider-appropriate budget
|
|
691
|
+
* (`EFFORT_THINKING_BUDGET_TOKENS` in `@crewhaus/adapter-anthropic`).
|
|
692
|
+
*
|
|
693
|
+
* `.strict()` so a typo'd sub-key fails the build.
|
|
694
|
+
*/
|
|
695
|
+
const thinkingBlock = z
|
|
696
|
+
.object({
|
|
697
|
+
budget_tokens: z.number().int().min(1024).optional(),
|
|
698
|
+
effort: z.enum(["low", "medium", "high"]).optional(),
|
|
699
|
+
})
|
|
700
|
+
.strict()
|
|
701
|
+
.superRefine((t, ctx) => {
|
|
702
|
+
const forms = (t.budget_tokens !== undefined ? 1 : 0) + (t.effort !== undefined ? 1 : 0);
|
|
703
|
+
if (forms !== 1) {
|
|
704
|
+
ctx.addIssue({
|
|
705
|
+
code: z.ZodIssueCode.custom,
|
|
706
|
+
message: "thinking requires exactly one of budget_tokens (explicit token budget >= 1024) or effort (low|medium|high preset)",
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
})
|
|
710
|
+
.optional();
|
|
711
|
+
/**
|
|
712
|
+
* Loop contract 0.4 (Batch A) — runaway-loop detection tuning inside the
|
|
713
|
+
* `limits:` block. `window` is the trailing tool-call window inspected;
|
|
714
|
+
* `threshold` (>1 — a single repeat is normal) is how many identical calls
|
|
715
|
+
* inside the window count as a loop; `escalation` picks the response:
|
|
716
|
+
* `warn` (trace event only), `justify` (demand a justification string via
|
|
717
|
+
* the intent gate), `abort` (end the run). Runtime owns per-knob defaults.
|
|
718
|
+
*/
|
|
719
|
+
const loopDetectionBlock = z
|
|
720
|
+
.object({
|
|
721
|
+
window: z.number().int().positive().optional(),
|
|
722
|
+
threshold: z.number().int().min(2).optional(),
|
|
723
|
+
escalation: z.enum(["warn", "justify", "abort"]).optional(),
|
|
724
|
+
})
|
|
725
|
+
.strict();
|
|
726
|
+
/**
|
|
727
|
+
* Loop contract 0.4 (Batch A) — the top-level `limits:` block: hard runtime
|
|
728
|
+
* ceilings for one agent loop. Carried on the loop-running shapes (cli,
|
|
729
|
+
* channel, managed, workflow, graph, crew, research, batch, browser); the
|
|
730
|
+
* strict union rejects it loudly elsewhere. Every field optional — declare
|
|
731
|
+
* only the ceilings you want; the runtime owns per-knob defaults.
|
|
732
|
+
*
|
|
733
|
+
* - `max_tool_iterations` — cap on tool-use round-trips per turn
|
|
734
|
+
* (OPTIMIZABLE, `["limits","max_tool_iterations"]` in spec-patch).
|
|
735
|
+
* - `max_concurrent_tools` — parallel tool-execution ceiling per block.
|
|
736
|
+
* - `context_limit` — hard context-token ceiling (overrides the model's).
|
|
737
|
+
* - `deadline_ms` — wall-clock ceiling for the whole run.
|
|
738
|
+
* - `turn_timeout_ms` — wall-clock ceiling for one turn.
|
|
739
|
+
* - `model_call_timeout_ms` — wall-clock ceiling for one model call.
|
|
740
|
+
* - `loop_detection` — see {@link loopDetectionBlock}.
|
|
741
|
+
*
|
|
742
|
+
* The crew shape additionally accepts a `crew:` sub-block (see
|
|
743
|
+
* {@link crewLimitsBlock}) for orchestration-level ceilings.
|
|
744
|
+
*/
|
|
745
|
+
const limitsObject = z
|
|
746
|
+
.object({
|
|
747
|
+
max_tool_iterations: z.number().int().positive().optional(),
|
|
748
|
+
max_concurrent_tools: z.number().int().positive().optional(),
|
|
749
|
+
context_limit: z.number().int().positive().optional(),
|
|
750
|
+
deadline_ms: z.number().int().positive().optional(),
|
|
751
|
+
turn_timeout_ms: z.number().int().positive().optional(),
|
|
752
|
+
model_call_timeout_ms: z.number().int().positive().optional(),
|
|
753
|
+
loop_detection: loopDetectionBlock.optional(),
|
|
754
|
+
})
|
|
755
|
+
.strict();
|
|
756
|
+
const limitsBlock = limitsObject.optional();
|
|
757
|
+
/**
|
|
758
|
+
* Loop contract 0.4 (Batch A) — crew-only orchestration ceilings nested
|
|
759
|
+
* under `limits.crew`. `max_activations` caps total role activations per
|
|
760
|
+
* run; `refusal_depth` (>= 0 — 0 means "never refuse") caps how many times
|
|
761
|
+
* a role may bounce a handoff back; `max_a2a_depth` caps agent-to-agent
|
|
762
|
+
* delegation depth. Accepted ONLY on the crew shape — the base
|
|
763
|
+
* {@link limitsObject} everywhere else rejects the `crew` key.
|
|
764
|
+
*/
|
|
765
|
+
const crewLimitsBlock = limitsObject
|
|
766
|
+
.extend({
|
|
767
|
+
crew: z
|
|
768
|
+
.object({
|
|
769
|
+
max_activations: z.number().int().positive().optional(),
|
|
770
|
+
refusal_depth: z.number().int().nonnegative().optional(),
|
|
771
|
+
max_a2a_depth: z.number().int().positive().optional(),
|
|
772
|
+
})
|
|
773
|
+
.strict()
|
|
774
|
+
.optional(),
|
|
775
|
+
})
|
|
776
|
+
.strict()
|
|
777
|
+
.optional();
|
|
778
|
+
/**
|
|
779
|
+
* Loop contract 0.4 (Batch A) — the hook-event names accepted in a spec's
|
|
780
|
+
* `hooks:` block. DUPLICATED from `HOOK_EVENTS` in `@crewhaus/hooks-engine`
|
|
781
|
+
* (the runtime source of truth) because the spec package stays
|
|
782
|
+
* dependency-light — it must not import runtime packages. A cross-check
|
|
783
|
+
* test in `packages/hooks-engine` imports this const and asserts equality
|
|
784
|
+
* with `HOOK_EVENTS`, so the two lists cannot drift silently. Keep in sync.
|
|
785
|
+
*/
|
|
786
|
+
export const SPEC_HOOK_EVENTS = [
|
|
787
|
+
"session-start",
|
|
788
|
+
"stop",
|
|
789
|
+
"pre-tool",
|
|
790
|
+
"post-tool",
|
|
791
|
+
"pre-model",
|
|
792
|
+
"post-model",
|
|
793
|
+
"pre-compact",
|
|
794
|
+
"post-compact",
|
|
795
|
+
"pre-slash",
|
|
796
|
+
"alert",
|
|
797
|
+
];
|
|
798
|
+
/**
|
|
799
|
+
* Loop contract 0.4 (Batch A) — spec-declared lifecycle hooks, the in-spec
|
|
800
|
+
* equivalent of `.crewhaus/settings.json` `hooks` entries (same shape as
|
|
801
|
+
* hooks-engine's `HookDef`, snake_case per spec convention: `timeout_ms` ↔
|
|
802
|
+
* `timeoutMs`). Carried on the same shapes as `limits:`. Each entry spawns
|
|
803
|
+
* `command` at the named lifecycle `event` (optionally filtered by the
|
|
804
|
+
* `matcher` glob against the payload's `name`).
|
|
805
|
+
*/
|
|
806
|
+
const hookSchema = z
|
|
807
|
+
.object({
|
|
808
|
+
event: z.enum(SPEC_HOOK_EVENTS),
|
|
809
|
+
matcher: z.string().min(1).optional(),
|
|
810
|
+
command: z.string().min(1),
|
|
811
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
812
|
+
})
|
|
813
|
+
.strict();
|
|
814
|
+
const hooksBlock = z.array(hookSchema).optional();
|
|
815
|
+
/**
|
|
816
|
+
* Loop contract 0.4 (Batch A) — per-tool rate limits on the agent blocks of
|
|
817
|
+
* the interactive shapes (cli, channel, managed). Keys are tool names (or
|
|
818
|
+
* `"*"` for the catch-all bucket); `rpm` is the sustained requests-per-
|
|
819
|
+
* minute ceiling, `burst` the optional short-burst allowance on top.
|
|
820
|
+
*/
|
|
821
|
+
const rateLimitsBlock = z
|
|
822
|
+
.record(z.string().min(1), z
|
|
823
|
+
.object({
|
|
824
|
+
rpm: z.number().int().positive(),
|
|
825
|
+
burst: z.number().int().positive().optional(),
|
|
826
|
+
})
|
|
827
|
+
.strict())
|
|
828
|
+
.optional();
|
|
491
829
|
/**
|
|
492
830
|
* Response-feedback block — declares that a harness collects human ratings on
|
|
493
831
|
* agent responses (thumbs/stars/scale/comment) which `crewhaus distill` turns
|
|
@@ -577,6 +915,26 @@ const memoryDreamBlock = z
|
|
|
577
915
|
* prompt at session start (mirrors project-memory auto-load). Carried on the
|
|
578
916
|
* agent-loop shapes (cli, channel, managed, research, crew).
|
|
579
917
|
*
|
|
918
|
+
* Loop contract 0.4 (Batch E, G46) — DEFAULT CHANGE (mildly breaking): when
|
|
919
|
+
* the `memory:` block is PRESENT, `autoRecall` now defaults to `true`
|
|
920
|
+
* (`"session-start"`) and `autoCapture` defaults to `true` (behind the
|
|
921
|
+
* existing `autoCaptureThreshold` gate) — both previously defaulted to
|
|
922
|
+
* `false`. The resolved booleans are stamped into the IR at lower time, so
|
|
923
|
+
* declaring `memory:` at all opts into recall+capture. Opt back out with
|
|
924
|
+
* `autoRecall: false` / `autoCapture: false`.
|
|
925
|
+
*
|
|
926
|
+
* EMBEDDER RESOLUTION ORDER (Batch E, G76) — coherent across the three
|
|
927
|
+
* embedder knobs, applied by the runtime/emitters (the IR carries the raw
|
|
928
|
+
* declared strings):
|
|
929
|
+
* - fact-store recall + the compaction curator: `memory.embedder` →
|
|
930
|
+
* `memory.wiki.embedder` → (none ⇒ BM25-only lexical);
|
|
931
|
+
* - the wiki semantic tier: `memory.wiki.embedder` → `memory.embedder` →
|
|
932
|
+
* (none ⇒ BM25-only);
|
|
933
|
+
* - agent-shape RAG (the `knowledge:` block): `knowledge.embedder` →
|
|
934
|
+
* `memory.embedder` → `memory.wiki.embedder` (a vector store needs
|
|
935
|
+
* embeddings, so with all three absent the target falls back to its
|
|
936
|
+
* default embedder model rather than BM25).
|
|
937
|
+
*
|
|
580
938
|
* v0.3.0 (§9) extensions — all optional so pre-0.3.0 specs parse (and lower)
|
|
581
939
|
* unchanged:
|
|
582
940
|
* - `backend`: `file` (the default when absent) | `thredz` (reserved — the
|
|
@@ -594,19 +952,118 @@ const memoryBlock = z
|
|
|
594
952
|
.object({
|
|
595
953
|
enabled: z.boolean().optional(),
|
|
596
954
|
backend: z.enum(["file", "thredz"]).optional(),
|
|
955
|
+
/** Loop contract 0.4 (Batch A) — top-level embedder for the FACT store
|
|
956
|
+
* (same `@crewhaus/embedder` factory grammar as `wiki.embedder`).
|
|
957
|
+
* Runtime fallback order: `embedder` → `wiki.embedder` — declaring
|
|
958
|
+
* only the wiki one keeps prior behaviour; the top-level knob lets a
|
|
959
|
+
* spec enable hybrid fact recall without enabling the wiki tier. */
|
|
960
|
+
embedder: z.string().min(1).optional(),
|
|
597
961
|
ttl: z
|
|
598
962
|
.string()
|
|
599
963
|
.regex(DURATION_REGEX, 'memory.ttl must be a duration like "90d", "12h", "30m", "60s", or "500ms"')
|
|
600
964
|
.optional(),
|
|
601
965
|
autoCapture: z.boolean().optional(),
|
|
602
966
|
autoCaptureThreshold: z.number().int().positive().optional(),
|
|
603
|
-
|
|
967
|
+
/** Loop contract 0.4 (Batch E, G21) — WHEN auto-recall runs. The boolean
|
|
968
|
+
* form is the pre-0.4 on/off switch (`true` ≡ `"session-start"`); the
|
|
969
|
+
* string form picks the cadence: `"session-start"` injects the recalled
|
|
970
|
+
* block ONCE at boot (the default when memory is present, G46), while
|
|
971
|
+
* `"per-turn"` re-runs the recall closure against the latest user message
|
|
972
|
+
* every turn (or every `refreshEvery` turns) and swaps the volatile
|
|
973
|
+
* recalled tail block WITHOUT re-injecting into the frozen cache prefix. */
|
|
974
|
+
autoRecall: z.union([z.boolean(), z.enum(["session-start", "per-turn"])]).optional(),
|
|
975
|
+
/** Loop contract 0.4 (Batch E, G21) — turns between per-turn recall
|
|
976
|
+
* refreshes (int > 0). Declaring it implies `autoRecall: "per-turn"`
|
|
977
|
+
* (it IS the "every N turns" cadence knob); `"per-turn"` without it
|
|
978
|
+
* refreshes every turn. OPTIMIZABLE (`["memory","refreshEvery"]`). It is
|
|
979
|
+
* a contradiction alongside `autoRecall: false` (rejected at lower time).*/
|
|
980
|
+
refreshEvery: z.number().int().positive().optional(),
|
|
604
981
|
recallK: z.number().int().positive().max(50).optional(),
|
|
982
|
+
/** Loop contract 0.4 (Batch E, G77) — fold session summaries in as a
|
|
983
|
+
* third RRF ranker in the recall fusion (over the existing
|
|
984
|
+
* sessions-index), beside the fact-store and wiki rankers. Default
|
|
985
|
+
* false; opting in surfaces "what we concluded last time" without a
|
|
986
|
+
* dedicated tool call. */
|
|
987
|
+
sessionRecall: z.boolean().optional(),
|
|
605
988
|
wiki: memoryWikiBlock.optional(),
|
|
606
989
|
dream: memoryDreamBlock.optional(),
|
|
607
990
|
})
|
|
608
991
|
.strict()
|
|
609
992
|
.optional();
|
|
993
|
+
// Vector-store backend ids accepted in specs. Mirrors `VectorBackendId`
|
|
994
|
+
// from @crewhaus/vector-store (and `IrVectorBackend`) — the canonical set
|
|
995
|
+
// of implemented backends — kept inline so the spec stays dependency-light.
|
|
996
|
+
// Keep in sync when a backend is added or removed. (Declared here, above the
|
|
997
|
+
// first consumer — the `knowledge:` block — so the cli/channel/managed
|
|
998
|
+
// schemas can reference it; the pipeline/research retrieve blocks below reuse
|
|
999
|
+
// the same const.)
|
|
1000
|
+
const VECTOR_BACKENDS = ["in-memory", "lance", "qdrant", "pinecone", "weaviate"];
|
|
1001
|
+
// The HTTP backends construct only with a `url` + `collection` (the
|
|
1002
|
+
// vector-store factory throws otherwise); parseSpec requires both so a
|
|
1003
|
+
// spec that selects one without them fails at compile, not at runtime.
|
|
1004
|
+
const HTTP_VECTOR_BACKENDS = new Set(["qdrant", "pinecone", "weaviate"]);
|
|
1005
|
+
/**
|
|
1006
|
+
* Loop contract 0.4 (Batch E, G22) — a single knowledge source: exactly ONE
|
|
1007
|
+
* of `path` (a file/dir on disk), `glob` (a shell glob) or `url` (a remote
|
|
1008
|
+
* document) per entry. The exactly-one rule is a self-contained superRefine
|
|
1009
|
+
* so the error is path-bearing at the offending source.
|
|
1010
|
+
*/
|
|
1011
|
+
const knowledgeSourceSchema = z
|
|
1012
|
+
.object({
|
|
1013
|
+
path: z.string().min(1).optional(),
|
|
1014
|
+
glob: z.string().min(1).optional(),
|
|
1015
|
+
url: z.string().min(1).optional(),
|
|
1016
|
+
})
|
|
1017
|
+
.strict()
|
|
1018
|
+
.superRefine((s, ctx) => {
|
|
1019
|
+
const set = [s.path, s.glob, s.url].filter((v) => v !== undefined).length;
|
|
1020
|
+
if (set !== 1) {
|
|
1021
|
+
ctx.addIssue({
|
|
1022
|
+
code: z.ZodIssueCode.custom,
|
|
1023
|
+
message: "each knowledge source needs exactly one of path/glob/url",
|
|
1024
|
+
path: [],
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
/**
|
|
1029
|
+
* Loop contract 0.4 (Batch E, G22) — the agent-shape RAG block. Presence
|
|
1030
|
+
* registers the EXISTING `@crewhaus/tool-retrieve` (chunker → embedder →
|
|
1031
|
+
* vector-store) as a `Retrieve` tool with citations, ingesting `sources` at
|
|
1032
|
+
* build/boot. It REUSES target-pipeline's retrieve lowering + engine (not a
|
|
1033
|
+
* fork), so the sub-keys mirror the pipeline shape:
|
|
1034
|
+
* - `embedder`: the `@crewhaus/embedder` factory grammar for the retrieve
|
|
1035
|
+
* tier. Optional; resolution order `knowledge.embedder → memory.embedder
|
|
1036
|
+
* → memory.wiki.embedder → the target's default embedder model` (a vector
|
|
1037
|
+
* store needs embeddings — see the memory block's EMBEDDER RESOLUTION
|
|
1038
|
+
* ORDER note).
|
|
1039
|
+
* - `vector_backend`: the SAME enum as pipeline `retrieve.vectorBackend`
|
|
1040
|
+
* (default `in-memory`).
|
|
1041
|
+
* - `sources` (required, >= 1): the corpus to ingest (see
|
|
1042
|
+
* {@link knowledgeSourceSchema}).
|
|
1043
|
+
* - `chunk.size` / `chunk.overlap`: chunker tuning (OPTIMIZABLE); default
|
|
1044
|
+
* to pipeline's 400 / 0 at lower time.
|
|
1045
|
+
* - `default_k`: hits returned per Retrieve call (int 1..50, default 5,
|
|
1046
|
+
* OPTIMIZABLE).
|
|
1047
|
+
* `.strict()` so a typo'd sub-key fails the build. Carried on cli/channel/
|
|
1048
|
+
* managed (the interactive agent-loop shapes); the pipeline shape keeps its
|
|
1049
|
+
* dedicated first-class `retrieve:`/`indexing:` blocks.
|
|
1050
|
+
*/
|
|
1051
|
+
const knowledgeBlock = z
|
|
1052
|
+
.object({
|
|
1053
|
+
embedder: z.string().min(1).optional(),
|
|
1054
|
+
vector_backend: z.enum(VECTOR_BACKENDS).optional(),
|
|
1055
|
+
sources: z.array(knowledgeSourceSchema).min(1),
|
|
1056
|
+
chunk: z
|
|
1057
|
+
.object({
|
|
1058
|
+
size: z.number().int().positive().optional(),
|
|
1059
|
+
overlap: z.number().int().nonnegative().optional(),
|
|
1060
|
+
})
|
|
1061
|
+
.strict()
|
|
1062
|
+
.optional(),
|
|
1063
|
+
default_k: z.number().int().positive().max(50).optional(),
|
|
1064
|
+
})
|
|
1065
|
+
.strict()
|
|
1066
|
+
.optional();
|
|
610
1067
|
/**
|
|
611
1068
|
* v0.3.0 Goal 1 (§2.1) — the top-level `continuity:` block: focus, plans,
|
|
612
1069
|
* goals, the proof-of-action ladder, the requirements ledger, and teardown
|
|
@@ -694,6 +1151,14 @@ const thredzObject = z
|
|
|
694
1151
|
.regex(THREDZ_HANDLE_RE, "thredz.agents must be a lowercase handle matching ^[a-z][a-z0-9-]{2,31}$ (or true to derive one from the spec name)"),
|
|
695
1152
|
])
|
|
696
1153
|
.optional(),
|
|
1154
|
+
/**
|
|
1155
|
+
* Item 5 (G44) — enable the nine Thredz messaging tools (`message_send`
|
|
1156
|
+
* / `inbox_poll` / `message_ack` / `thread_get` / `agent_*`). DEFAULT
|
|
1157
|
+
* false: the send-side tools are destructive + justification-gated, so
|
|
1158
|
+
* they stay off unless the author asks. The Thredz server side is already
|
|
1159
|
+
* live (thredz-api) — this flips their registration on.
|
|
1160
|
+
*/
|
|
1161
|
+
messaging: z.boolean().optional(),
|
|
697
1162
|
})
|
|
698
1163
|
.strict();
|
|
699
1164
|
const thredzBlock = z.union([z.boolean(), z.string().min(1), thredzObject]).optional();
|
|
@@ -811,9 +1276,52 @@ const sloBlock = z
|
|
|
811
1276
|
s.ttft_ms !== undefined ||
|
|
812
1277
|
s.cost_per_hour_usd !== undefined ||
|
|
813
1278
|
s.egress_block_rate !== undefined, { message: "observability.slo must declare at least one target threshold" });
|
|
1279
|
+
/**
|
|
1280
|
+
* Loop contract 0.4 (Batch C, G26) — the observability control sub-blocks.
|
|
1281
|
+
* These declare which of the runtime's observability subscribers the emitted
|
|
1282
|
+
* bundle wires and how it stamps their env / subscriber options.
|
|
1283
|
+
*
|
|
1284
|
+
* DEFAULTS SEMANTICS (critical — mirrored in `@crewhaus/ir` + the lowering):
|
|
1285
|
+
* cost tracking and the trace ring buffer are DEFAULT ON even when the whole
|
|
1286
|
+
* `observability:` block is absent — spec ABSENCE is NOT `off`. An EXPLICIT
|
|
1287
|
+
* opt-out (`cost: { enabled: false }` / `trace: { level: off }`) wins. So the
|
|
1288
|
+
* lowering carries only what the spec declares (absent sub-block ⇒ absent IR
|
|
1289
|
+
* key ⇒ the emitter applies the default), and the presence of an explicit
|
|
1290
|
+
* `enabled: false` / `level: off` is what turns a subscriber off.
|
|
1291
|
+
*
|
|
1292
|
+
* - `trace.level`: `off` (no ring buffer, no printer) | `ring` (ring buffer
|
|
1293
|
+
* only, the DEFAULT) | `pretty` (ring + colorised stderr printer) | `json`
|
|
1294
|
+
* (ring + JSON-Lines printer). Absent ⇒ `ring`.
|
|
1295
|
+
* - `metrics.enabled`: attach the metrics-collector subscriber. Opt-IN —
|
|
1296
|
+
* absent ⇒ off.
|
|
1297
|
+
* - `cost.enabled`: attach the cost-tracker subscriber. DEFAULT ON — absent
|
|
1298
|
+
* ⇒ on; set `false` to suppress cost accrual entirely.
|
|
1299
|
+
* - `alerts.enabled`: arm the alert watchdog. Opt-IN — absent ⇒ off.
|
|
1300
|
+
* - `incidents.enabled`: arm incident capture. Opt-IN — absent ⇒ off.
|
|
1301
|
+
* - `otel.endpoint`: OTLP exporter endpoint (e.g. `http://localhost:4318`).
|
|
1302
|
+
* Absent ⇒ no OTel export. Carried verbatim (a `$VAR` value is the
|
|
1303
|
+
* emitter's to resolve).
|
|
1304
|
+
*
|
|
1305
|
+
* Every feature toggle carries a `.default(true)` on `enabled` so a bare
|
|
1306
|
+
* `metrics: {}` reads as "on"; the ABSENT-block default (opt-in features off,
|
|
1307
|
+
* cost/ring on) is applied downstream, not here. `.strict()` so a typo'd
|
|
1308
|
+
* sub-key fails the build.
|
|
1309
|
+
*/
|
|
1310
|
+
const observabilityToggle = z.object({ enabled: z.boolean().default(true) }).strict();
|
|
1311
|
+
const observabilityTraceBlock = z
|
|
1312
|
+
.object({ level: z.enum(["off", "ring", "pretty", "json"]).default("ring") })
|
|
1313
|
+
.strict();
|
|
1314
|
+
const observabilityOtelBlock = z.object({ endpoint: z.string().min(1).optional() }).strict();
|
|
814
1315
|
const observabilityBlock = z
|
|
815
1316
|
.object({
|
|
816
1317
|
slo: sloBlock.optional(),
|
|
1318
|
+
// Loop contract 0.4 (Batch C, G26) — subscriber/exporter controls.
|
|
1319
|
+
trace: observabilityTraceBlock.optional(),
|
|
1320
|
+
metrics: observabilityToggle.optional(),
|
|
1321
|
+
cost: observabilityToggle.optional(),
|
|
1322
|
+
alerts: observabilityToggle.optional(),
|
|
1323
|
+
incidents: observabilityToggle.optional(),
|
|
1324
|
+
otel: observabilityOtelBlock.optional(),
|
|
817
1325
|
})
|
|
818
1326
|
.strict()
|
|
819
1327
|
.optional();
|
|
@@ -904,12 +1412,20 @@ const cliOptionsBlock = z
|
|
|
904
1412
|
.object({
|
|
905
1413
|
banner: cliBannerBlock,
|
|
906
1414
|
/**
|
|
907
|
-
* Phase 2 M2.2 — TUI
|
|
908
|
-
*
|
|
909
|
-
* (
|
|
910
|
-
*
|
|
1415
|
+
* Phase 2 M2.2 — TUI mode. `"basic"` is the readline-driven REPL and the
|
|
1416
|
+
* only mode. Loop contract 0.4 (Batch F, G81) DROPS the never-implemented
|
|
1417
|
+
* `"rich"` (Ink-based) placeholder: it compiled identically to `"basic"`,
|
|
1418
|
+
* so it only ever advertised a capability that did not exist. Declaring
|
|
1419
|
+
* it now fails the compile with a migration note; a future rich TUI would
|
|
1420
|
+
* reintroduce the value when it actually ships.
|
|
911
1421
|
*/
|
|
912
|
-
tui: z
|
|
1422
|
+
tui: z
|
|
1423
|
+
.literal("basic", {
|
|
1424
|
+
errorMap: () => ({
|
|
1425
|
+
message: 'cli.tui "rich" was never implemented and is dropped in loop-contract 0.4 — remove the `tui:` key (the basic readline REPL is the only mode).',
|
|
1426
|
+
}),
|
|
1427
|
+
})
|
|
1428
|
+
.default("basic"),
|
|
913
1429
|
})
|
|
914
1430
|
.strict()
|
|
915
1431
|
.optional();
|
|
@@ -931,6 +1447,52 @@ const heartbeatBlock = z
|
|
|
931
1447
|
})
|
|
932
1448
|
.strict()
|
|
933
1449
|
.optional();
|
|
1450
|
+
/**
|
|
1451
|
+
* Loop contract 0.4 (Batch F, temporal contract / G84 schedule half) — a
|
|
1452
|
+
* `schedule:` block on the daemon-able shapes (channel / managed / batch): a
|
|
1453
|
+
* cron OR interval wake trigger, lowered into the emitted daemon's wake loop
|
|
1454
|
+
* by the temporal downstream. `jitter` (a duration) spreads a random +/- delay
|
|
1455
|
+
* across the trigger so a fleet of identical daemons doesn't stampede on the
|
|
1456
|
+
* boundary; `instructions` is the synthetic prompt each wake runs (the
|
|
1457
|
+
* heartbeat contract, generalised past the fixed interval to a cron). Exactly
|
|
1458
|
+
* one of the two `kind`s — the discriminated union makes the required field
|
|
1459
|
+
* per kind (`cron` vs `every`) a type error to omit.
|
|
1460
|
+
*
|
|
1461
|
+
* Unlike `heartbeat` (channel-only, interval-only), `schedule` is the general
|
|
1462
|
+
* temporal surface: it accepts a cron expression AND rides the `runs resume`
|
|
1463
|
+
* rehydration path, so an interrupted scheduled run resumes exactly-once.
|
|
1464
|
+
*/
|
|
1465
|
+
// A 5- or 6-field cron expression (minute-granularity, optional seconds/year).
|
|
1466
|
+
// Field validity beyond the char class is the daemon's cron parser's job.
|
|
1467
|
+
const CRON_REGEX = /^[0-9*\/,\-?LW#]+(?:\s+[0-9*\/,\-?LW#]+){4,5}$/;
|
|
1468
|
+
const scheduleJitter = z
|
|
1469
|
+
.string()
|
|
1470
|
+
.regex(DURATION_REGEX, 'schedule.jitter must be a duration like "30s", "5m", or "500ms"');
|
|
1471
|
+
const scheduleCronBlock = z
|
|
1472
|
+
.object({
|
|
1473
|
+
kind: z.literal("cron"),
|
|
1474
|
+
cron: z
|
|
1475
|
+
.string()
|
|
1476
|
+
.regex(CRON_REGEX, 'schedule.cron must be a 5- or 6-field cron expression, e.g. "0 */6 * * *"'),
|
|
1477
|
+
/** IANA tz name the cron is evaluated in (e.g. "America/New_York"). */
|
|
1478
|
+
timezone: z.string().min(1).optional(),
|
|
1479
|
+
jitter: scheduleJitter.optional(),
|
|
1480
|
+
instructions: z.string().min(1).optional(),
|
|
1481
|
+
})
|
|
1482
|
+
.strict();
|
|
1483
|
+
const scheduleIntervalBlock = z
|
|
1484
|
+
.object({
|
|
1485
|
+
kind: z.literal("interval"),
|
|
1486
|
+
every: z
|
|
1487
|
+
.string()
|
|
1488
|
+
.regex(DURATION_REGEX, 'schedule.every must be a duration like "6h", "30m", or "60s"'),
|
|
1489
|
+
jitter: scheduleJitter.optional(),
|
|
1490
|
+
instructions: z.string().min(1).optional(),
|
|
1491
|
+
})
|
|
1492
|
+
.strict();
|
|
1493
|
+
const scheduleBlock = z
|
|
1494
|
+
.discriminatedUnion("kind", [scheduleCronBlock, scheduleIntervalBlock])
|
|
1495
|
+
.optional();
|
|
934
1496
|
/**
|
|
935
1497
|
* Phase 3 §3.4 — channel daemon control-UI gateway. When set, the
|
|
936
1498
|
* compiled daemon spawns a second HTTP listener on `port` that serves
|
|
@@ -945,6 +1507,46 @@ const channelGatewayBlock = z
|
|
|
945
1507
|
})
|
|
946
1508
|
.strict()
|
|
947
1509
|
.optional();
|
|
1510
|
+
/**
|
|
1511
|
+
* Item 1 (G30) — the `expose:` block: project THIS compiled bundle's turn
|
|
1512
|
+
* function as an MCP server so Claude Code / IDEs / other CrewHaus runtimes
|
|
1513
|
+
* can call the whole agent as a tool. Carried on the serving shapes
|
|
1514
|
+
* (cli/channel/managed).
|
|
1515
|
+
*
|
|
1516
|
+
* - `mcp.transport`: `stdio` (a spawned stdio MCP server — the
|
|
1517
|
+
* `crewhaus serve --mcp` path) or `sse` (an HTTP+SSE endpoint; SSE-backed
|
|
1518
|
+
* exposure rides the gateway-server tenancy/budgets where the shape has
|
|
1519
|
+
* them).
|
|
1520
|
+
* - `mcp.tools`: `chat` (DEFAULT — one primary invoke tool taking
|
|
1521
|
+
* `{ message }` and returning the final assistant text) or `per-subagent`
|
|
1522
|
+
* (that primary tool PLUS one tool per declared sub-agent). `per-subagent`
|
|
1523
|
+
* needs sub-agents to project — enforced cross-field in `parseSpec`.
|
|
1524
|
+
*
|
|
1525
|
+
* Omitted entirely → the bundle is not exposed as an MCP server (the default).
|
|
1526
|
+
*/
|
|
1527
|
+
const exposeBlock = z
|
|
1528
|
+
.object({
|
|
1529
|
+
mcp: z
|
|
1530
|
+
.object({
|
|
1531
|
+
transport: z.enum(["stdio", "sse"]),
|
|
1532
|
+
tools: z.enum(["chat", "per-subagent"]).optional(),
|
|
1533
|
+
})
|
|
1534
|
+
.strict()
|
|
1535
|
+
.optional(),
|
|
1536
|
+
})
|
|
1537
|
+
.strict()
|
|
1538
|
+
.optional();
|
|
1539
|
+
/**
|
|
1540
|
+
* Item 3 (G32) — the `plugins:` list: names of installed marketplace plugins
|
|
1541
|
+
* whose contributions (tools / channels / models / graders / emitters, plus
|
|
1542
|
+
* skill dirs) this bundle loads at boot. Each entry is a plugin NAME resolved
|
|
1543
|
+
* against the pinned `plugin-registry` (the Ed25519 supply chain guards
|
|
1544
|
+
* install; this wires the previously-missing load path). Order is honoured
|
|
1545
|
+
* (load order). The `crewhaus run --plugins` flag overrides the list. Carried
|
|
1546
|
+
* on the codegen-serving shapes whose boot path reads the registry (cli +
|
|
1547
|
+
* channel).
|
|
1548
|
+
*/
|
|
1549
|
+
const pluginsBlock = z.array(z.string().min(1)).optional();
|
|
948
1550
|
const cliSchema = z
|
|
949
1551
|
.object({
|
|
950
1552
|
name: safeName,
|
|
@@ -958,6 +1560,13 @@ const cliSchema = z
|
|
|
958
1560
|
// runtime default applies. Raise it for turns that emit large
|
|
959
1561
|
// multi-file edits so the model isn't cut off mid-`tool_use`.
|
|
960
1562
|
max_tokens: z.number().int().positive().optional(),
|
|
1563
|
+
// Loop contract 0.4 (Batch A) — extended-thinking selector.
|
|
1564
|
+
thinking: thinkingBlock,
|
|
1565
|
+
// Loop contract 0.4 (Batch A) — stream partial output tokens.
|
|
1566
|
+
// Optional; absent means false (the cli-shape default).
|
|
1567
|
+
streaming: z.boolean().optional(),
|
|
1568
|
+
// Loop contract 0.4 (Batch A) — per-tool rate limits.
|
|
1569
|
+
rate_limits: rateLimitsBlock,
|
|
961
1570
|
// Item 22 — provider failover chain (see modelFallbacksBlock docs).
|
|
962
1571
|
model_fallbacks: modelFallbacksBlock,
|
|
963
1572
|
circuit_breaker: circuitBreakerBlock,
|
|
@@ -977,8 +1586,18 @@ const cliSchema = z
|
|
|
977
1586
|
security: securityBlock,
|
|
978
1587
|
failure_taxonomy: failureTaxonomyBlock,
|
|
979
1588
|
budget: budgetBlock,
|
|
1589
|
+
limits: limitsBlock,
|
|
1590
|
+
hooks: hooksBlock,
|
|
1591
|
+
// Batch G — expose the bundle as an MCP server (G30) + load marketplace
|
|
1592
|
+
// plugins at boot (G32).
|
|
1593
|
+
expose: exposeBlock,
|
|
1594
|
+
plugins: pluginsBlock,
|
|
1595
|
+
// Loop contract 0.4 (Batch B, G02) — in-loop output evaluation.
|
|
1596
|
+
evaluation: evaluationBlock,
|
|
980
1597
|
feedback: feedbackBlock,
|
|
981
1598
|
memory: memoryBlock,
|
|
1599
|
+
// Loop contract 0.4 (Batch E, G22) — agent-shape RAG over doc sources.
|
|
1600
|
+
knowledge: knowledgeBlock,
|
|
982
1601
|
continuity: continuityBlock,
|
|
983
1602
|
thredz: thredzBlock,
|
|
984
1603
|
learning: learningBlock,
|
|
@@ -995,21 +1614,57 @@ const workflowStepSchema = z
|
|
|
995
1614
|
name: safeName,
|
|
996
1615
|
instructions: z.string().min(1),
|
|
997
1616
|
model: z.string().min(1).optional(),
|
|
1617
|
+
// Model max OUTPUT tokens for this step's turn (mirrors cli
|
|
1618
|
+
// `agent.max_tokens`). Optional; runtime default when omitted.
|
|
1619
|
+
max_tokens: z.number().int().positive().optional(),
|
|
1620
|
+
// Loop contract 0.4 (Batch A) — per-step extended-thinking selector.
|
|
1621
|
+
thinking: thinkingBlock,
|
|
998
1622
|
tools: z.array(z.string().min(1)).optional(),
|
|
999
1623
|
tool_config: toolConfigBlock,
|
|
1624
|
+
// Item 9 (G37) — per-step model routing, adopting the cli agent block's
|
|
1625
|
+
// pooled pattern verbatim: ordered failover + breaker tuning + two-tier
|
|
1626
|
+
// router + N-candidate pool, sharing the one mutual-exclusion rule via
|
|
1627
|
+
// `refineModelSelection`. A PolicyRouter decides per step against the
|
|
1628
|
+
// shared routing-store scoreboard. Omitted → the step's single
|
|
1629
|
+
// (`step.model ?? workflow.model`) model, byte-identical bundles.
|
|
1630
|
+
model_fallbacks: modelFallbacksBlock,
|
|
1631
|
+
circuit_breaker: circuitBreakerBlock,
|
|
1632
|
+
model_tiers: modelTiersBlock,
|
|
1633
|
+
model_pool: modelPoolBlock,
|
|
1000
1634
|
})
|
|
1001
|
-
.strict()
|
|
1635
|
+
.strict()
|
|
1636
|
+
.superRefine(refineModelSelection);
|
|
1637
|
+
/**
|
|
1638
|
+
* Loop contract 0.4 (Batch B, G02) — the `kind: "judge"` workflow-step
|
|
1639
|
+
* variant: a gate over the PREVIOUS step's output (see
|
|
1640
|
+
* {@link judgeGateBlock}). Judge steps run no agent turn of their own, so
|
|
1641
|
+
* they carry no instructions/tools — only the gate config. A judge step
|
|
1642
|
+
* cannot be the first step (there is no previous output to gate; enforced
|
|
1643
|
+
* in `parseSpec`). Regular steps stay exactly as before (no `kind` key).
|
|
1644
|
+
*/
|
|
1645
|
+
const workflowJudgeStepSchema = z
|
|
1646
|
+
.object({
|
|
1647
|
+
name: safeName,
|
|
1648
|
+
kind: z.literal("judge"),
|
|
1649
|
+
judge: judgeGateBlock,
|
|
1650
|
+
})
|
|
1651
|
+
.strict()
|
|
1652
|
+
.describe("judge gate step: scores the previous step's output instead of running an agent turn");
|
|
1653
|
+
const workflowAnyStepSchema = z.union([workflowStepSchema, workflowJudgeStepSchema]);
|
|
1002
1654
|
const workflowSchema = z
|
|
1003
1655
|
.object({
|
|
1004
1656
|
name: safeName,
|
|
1005
1657
|
version: versionField,
|
|
1006
1658
|
target: z.literal("workflow"),
|
|
1007
1659
|
model: z.string().min(1),
|
|
1008
|
-
steps: z.array(
|
|
1660
|
+
steps: z.array(workflowAnyStepSchema).min(1),
|
|
1009
1661
|
mcp_servers: mcpServersBlock,
|
|
1010
1662
|
permissions: permissionsBlock,
|
|
1011
1663
|
compaction: compactionBlock,
|
|
1012
1664
|
failure_taxonomy: failureTaxonomyBlock,
|
|
1665
|
+
budget: budgetBlock,
|
|
1666
|
+
limits: limitsBlock,
|
|
1667
|
+
hooks: hooksBlock,
|
|
1013
1668
|
// v0.3.0 — carried but not emit-wired in 0.3.0 (ignored-note comment in
|
|
1014
1669
|
// the generated bundle; NOT default-on here).
|
|
1015
1670
|
continuity: continuityBlock,
|
|
@@ -1088,6 +1743,13 @@ const channelAgentSchema = z
|
|
|
1088
1743
|
.object({
|
|
1089
1744
|
model: z.string().min(1),
|
|
1090
1745
|
instructions: z.string().min(1),
|
|
1746
|
+
// Model max OUTPUT tokens for one turn (mirrors cli `agent.max_tokens`).
|
|
1747
|
+
// Optional; runtime default when omitted.
|
|
1748
|
+
max_tokens: z.number().int().positive().optional(),
|
|
1749
|
+
// Loop contract 0.4 (Batch A) — extended-thinking selector.
|
|
1750
|
+
thinking: thinkingBlock,
|
|
1751
|
+
// Loop contract 0.4 (Batch A) — per-tool rate limits.
|
|
1752
|
+
rate_limits: rateLimitsBlock,
|
|
1091
1753
|
// Item 22 — provider failover chain (see modelFallbacksBlock docs).
|
|
1092
1754
|
model_fallbacks: modelFallbacksBlock,
|
|
1093
1755
|
circuit_breaker: circuitBreakerBlock,
|
|
@@ -1114,13 +1776,26 @@ const channelSchema = z
|
|
|
1114
1776
|
compaction: compactionBlock,
|
|
1115
1777
|
failure_taxonomy: failureTaxonomyBlock,
|
|
1116
1778
|
budget: budgetBlock,
|
|
1779
|
+
limits: limitsBlock,
|
|
1780
|
+
hooks: hooksBlock,
|
|
1781
|
+
// Batch G — expose the daemon's turn as an MCP server (G30) + load
|
|
1782
|
+
// marketplace plugins at boot (G32).
|
|
1783
|
+
expose: exposeBlock,
|
|
1784
|
+
plugins: pluginsBlock,
|
|
1785
|
+
// Loop contract 0.4 (Batch B, G02) — in-loop output evaluation.
|
|
1786
|
+
evaluation: evaluationBlock,
|
|
1117
1787
|
feedback: feedbackBlock,
|
|
1118
1788
|
memory: memoryBlock,
|
|
1789
|
+
// Loop contract 0.4 (Batch E, G22) — agent-shape RAG over doc sources.
|
|
1790
|
+
knowledge: knowledgeBlock,
|
|
1119
1791
|
continuity: continuityBlock,
|
|
1120
1792
|
thredz: thredzBlock,
|
|
1121
1793
|
learning: learningBlock,
|
|
1122
1794
|
observability: observabilityBlock,
|
|
1123
1795
|
heartbeat: heartbeatBlock,
|
|
1796
|
+
// Loop contract 0.4 (Batch F) — cron/interval wake trigger (the general
|
|
1797
|
+
// temporal surface beside the interval-only `heartbeat`).
|
|
1798
|
+
schedule: scheduleBlock,
|
|
1124
1799
|
gateway: channelGatewayBlock,
|
|
1125
1800
|
chains: chainsBlock,
|
|
1126
1801
|
wallets: walletsBlock,
|
|
@@ -1135,6 +1810,11 @@ const graphNodeSchema = z
|
|
|
1135
1810
|
.object({
|
|
1136
1811
|
instructions: z.string().min(1),
|
|
1137
1812
|
model: z.string().min(1).optional(),
|
|
1813
|
+
// Model max OUTPUT tokens for this node's turn (mirrors cli
|
|
1814
|
+
// `agent.max_tokens`). Optional; runtime default when omitted.
|
|
1815
|
+
max_tokens: z.number().int().positive().optional(),
|
|
1816
|
+
// Loop contract 0.4 (Batch A) — per-node extended-thinking selector.
|
|
1817
|
+
thinking: thinkingBlock,
|
|
1138
1818
|
tools: z.array(z.string().min(1)).optional(),
|
|
1139
1819
|
tool_config: toolConfigBlock,
|
|
1140
1820
|
/**
|
|
@@ -1150,10 +1830,62 @@ const graphNodeSchema = z
|
|
|
1150
1830
|
.optional(),
|
|
1151
1831
|
})
|
|
1152
1832
|
.strict();
|
|
1833
|
+
/**
|
|
1834
|
+
* Loop contract 0.4 (Batch B, G02) — the `kind: "judge"` graph-node
|
|
1835
|
+
* variant: a gate over the node's UPSTREAM output (see
|
|
1836
|
+
* {@link judgeGateBlock}). Judge nodes run no agent turn of their own, so
|
|
1837
|
+
* they carry no instructions/tools — only the gate config. The graph entry
|
|
1838
|
+
* cannot be a judge node (there is no upstream output to gate; enforced in
|
|
1839
|
+
* `parseSpec`). Regular nodes stay exactly as before (no `kind` key).
|
|
1840
|
+
*/
|
|
1841
|
+
const graphJudgeNodeSchema = z
|
|
1842
|
+
.object({
|
|
1843
|
+
kind: z.literal("judge"),
|
|
1844
|
+
judge: judgeGateBlock,
|
|
1845
|
+
})
|
|
1846
|
+
.strict()
|
|
1847
|
+
.describe("judge gate node: scores the upstream node's output instead of running an agent turn");
|
|
1848
|
+
const graphAnyNodeSchema = z.union([graphNodeSchema, graphJudgeNodeSchema]);
|
|
1849
|
+
/**
|
|
1850
|
+
* Loop contract 0.4 (Batch A) — declarative edge predicate over the graph's
|
|
1851
|
+
* shared state. The generated graph state is a plain record where each node
|
|
1852
|
+
* writes its reply under its own name (`state["<nodeName>"]`), so `key`
|
|
1853
|
+
* names the upstream NODE whose recorded output the predicate reads
|
|
1854
|
+
* (cross-validated against `nodes` in `parseSpec`). Exactly ONE test form
|
|
1855
|
+
* must be declared (enforced by superRefine):
|
|
1856
|
+
*
|
|
1857
|
+
* - `equals` — take the edge when `state[key] === equals` (string/number/
|
|
1858
|
+
* boolean strict equality).
|
|
1859
|
+
* - `exists: true` — take the edge when `state[key] !== undefined` (the
|
|
1860
|
+
* node has produced output; pairs with hitl `_decision` gating in a
|
|
1861
|
+
* follow-up).
|
|
1862
|
+
*
|
|
1863
|
+
* Lowered to `IrGraphEdge.when` and emitted as a graph-engine
|
|
1864
|
+
* `EdgeCondition` (`(state) => state[key] === equals` / `!== undefined`).
|
|
1865
|
+
* The engine evaluates edges in declaration order and takes the first
|
|
1866
|
+
* match; an edge without `when` matches unconditionally.
|
|
1867
|
+
*/
|
|
1868
|
+
const graphEdgeWhenSchema = z
|
|
1869
|
+
.object({
|
|
1870
|
+
key: z.string().min(1),
|
|
1871
|
+
equals: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
1872
|
+
exists: z.literal(true).optional(),
|
|
1873
|
+
})
|
|
1874
|
+
.strict()
|
|
1875
|
+
.superRefine((w, ctx) => {
|
|
1876
|
+
const forms = (w.equals !== undefined ? 1 : 0) + (w.exists !== undefined ? 1 : 0);
|
|
1877
|
+
if (forms !== 1) {
|
|
1878
|
+
ctx.addIssue({
|
|
1879
|
+
code: z.ZodIssueCode.custom,
|
|
1880
|
+
message: "edge when requires exactly one of equals (value test) or exists: true",
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1153
1884
|
const graphEdgeSchema = z
|
|
1154
1885
|
.object({
|
|
1155
1886
|
from: z.string().min(1),
|
|
1156
1887
|
to: z.string().min(1),
|
|
1888
|
+
when: graphEdgeWhenSchema.optional(),
|
|
1157
1889
|
})
|
|
1158
1890
|
.strict();
|
|
1159
1891
|
const graphSchema = z
|
|
@@ -1163,11 +1895,23 @@ const graphSchema = z
|
|
|
1163
1895
|
target: z.literal("graph"),
|
|
1164
1896
|
model: z.string().min(1),
|
|
1165
1897
|
entry: z.string().min(1),
|
|
1166
|
-
nodes: z.record(safeName,
|
|
1898
|
+
nodes: z.record(safeName, graphAnyNodeSchema),
|
|
1167
1899
|
edges: z.array(graphEdgeSchema).default([]),
|
|
1900
|
+
/**
|
|
1901
|
+
* Loop contract 0.4 (Batch A) — parallel barrier groups, lowered onto
|
|
1902
|
+
* graph-engine's `addParallel`. Each group is >= 2 node names (the
|
|
1903
|
+
* engine rejects smaller groups) that execute concurrently when the
|
|
1904
|
+
* cursor reaches the group's FIRST member; execution continues from the
|
|
1905
|
+
* LAST member's outgoing edge. Node names are cross-validated against
|
|
1906
|
+
* `nodes` in `parseSpec`.
|
|
1907
|
+
*/
|
|
1908
|
+
parallel: z.array(z.array(z.string().min(1)).min(2)).optional(),
|
|
1168
1909
|
permissions: permissionsBlock,
|
|
1169
1910
|
compaction: compactionBlock,
|
|
1170
1911
|
failure_taxonomy: failureTaxonomyBlock,
|
|
1912
|
+
budget: budgetBlock,
|
|
1913
|
+
limits: limitsBlock,
|
|
1914
|
+
hooks: hooksBlock,
|
|
1171
1915
|
chains: chainsBlock,
|
|
1172
1916
|
wallets: walletsBlock,
|
|
1173
1917
|
contracts: contractsBlock,
|
|
@@ -1193,6 +1937,13 @@ const managedAgentSchema = z
|
|
|
1193
1937
|
.object({
|
|
1194
1938
|
model: z.string().min(1),
|
|
1195
1939
|
instructions: z.string().min(1),
|
|
1940
|
+
// Model max OUTPUT tokens for one turn (mirrors cli `agent.max_tokens`).
|
|
1941
|
+
// Optional; runtime default when omitted.
|
|
1942
|
+
max_tokens: z.number().int().positive().optional(),
|
|
1943
|
+
// Loop contract 0.4 (Batch A) — extended-thinking selector.
|
|
1944
|
+
thinking: thinkingBlock,
|
|
1945
|
+
// Loop contract 0.4 (Batch A) — per-tool rate limits.
|
|
1946
|
+
rate_limits: rateLimitsBlock,
|
|
1196
1947
|
// Item 22 — provider failover chain (see modelFallbacksBlock docs).
|
|
1197
1948
|
model_fallbacks: modelFallbacksBlock,
|
|
1198
1949
|
circuit_breaker: circuitBreakerBlock,
|
|
@@ -1200,6 +1951,11 @@ const managedAgentSchema = z
|
|
|
1200
1951
|
model_tiers: modelTiersBlock,
|
|
1201
1952
|
// Adaptive model routing — N-candidate pool with a selection policy.
|
|
1202
1953
|
model_pool: modelPoolBlock,
|
|
1954
|
+
// Loop contract 0.4 (Batch F, G81) — the managed daemon gets a tool
|
|
1955
|
+
// catalog + per-tenant tool_config overlays (applied at runtime through
|
|
1956
|
+
// the policy-engine's tenant context). Mirrors the channel agent block.
|
|
1957
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
1958
|
+
tool_config: toolConfigBlock,
|
|
1203
1959
|
})
|
|
1204
1960
|
.strict()
|
|
1205
1961
|
.superRefine(refineModelSelection);
|
|
@@ -1214,24 +1970,30 @@ const managedSchema = z
|
|
|
1214
1970
|
compaction: compactionBlock,
|
|
1215
1971
|
failure_taxonomy: failureTaxonomyBlock,
|
|
1216
1972
|
budget: budgetBlock,
|
|
1973
|
+
limits: limitsBlock,
|
|
1974
|
+
hooks: hooksBlock,
|
|
1975
|
+
// Batch G — expose the managed daemon as an MCP server (G30). SSE-backed
|
|
1976
|
+
// exposure rides this shape's gateway-server tenancy/budgets. No
|
|
1977
|
+
// `plugins:` here: item 3's boot-path wiring covers cli + channel-bot
|
|
1978
|
+
// codegen, not the managed daemon.
|
|
1979
|
+
expose: exposeBlock,
|
|
1980
|
+
// Loop contract 0.4 (Batch B, G02) — in-loop output evaluation.
|
|
1981
|
+
evaluation: evaluationBlock,
|
|
1217
1982
|
memory: memoryBlock,
|
|
1983
|
+
// Loop contract 0.4 (Batch E, G22) — agent-shape RAG over doc sources.
|
|
1984
|
+
knowledge: knowledgeBlock,
|
|
1218
1985
|
continuity: continuityBlock,
|
|
1219
1986
|
thredz: thredzBlock,
|
|
1220
1987
|
learning: learningBlock,
|
|
1221
1988
|
observability: observabilityBlock,
|
|
1989
|
+
// Loop contract 0.4 (Batch F) — cron/interval wake trigger.
|
|
1990
|
+
schedule: scheduleBlock,
|
|
1222
1991
|
})
|
|
1223
1992
|
.strict();
|
|
1224
|
-
// Vector-store backend ids accepted in specs. Mirrors `VectorBackendId`
|
|
1225
|
-
// from @crewhaus/vector-store (and `IrVectorBackend`) — the canonical set
|
|
1226
|
-
// of implemented backends — kept inline so the spec stays dependency-light.
|
|
1227
|
-
// Keep in sync when a backend is added or removed.
|
|
1228
|
-
const VECTOR_BACKENDS = ["in-memory", "lance", "qdrant", "pinecone", "weaviate"];
|
|
1229
|
-
// The HTTP backends construct only with a `url` + `collection` (the
|
|
1230
|
-
// vector-store factory throws otherwise); parseSpec requires both so a
|
|
1231
|
-
// spec that selects one without them fails at compile, not at runtime.
|
|
1232
|
-
const HTTP_VECTOR_BACKENDS = new Set(["qdrant", "pinecone", "weaviate"]);
|
|
1233
1993
|
// Pipeline / RAG target (Section 21). Carries the embedder + vector-store
|
|
1234
1994
|
// config, an indexing pipeline, and a chat agent that uses Retrieve.
|
|
1995
|
+
// (`VECTOR_BACKENDS` / `HTTP_VECTOR_BACKENDS` are declared above, beside the
|
|
1996
|
+
// `knowledge:` block that first consumes them.)
|
|
1235
1997
|
const pipelineDocumentSchema = z
|
|
1236
1998
|
.object({
|
|
1237
1999
|
id: z.string().min(1),
|
|
@@ -1240,23 +2002,36 @@ const pipelineDocumentSchema = z
|
|
|
1240
2002
|
})
|
|
1241
2003
|
.strict();
|
|
1242
2004
|
/**
|
|
1243
|
-
* Adaptive model routing — the minimal single-agent block
|
|
1244
|
-
*
|
|
1245
|
-
* `
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
2005
|
+
* Adaptive model routing — the minimal single-agent block on the pipeline
|
|
2006
|
+
* shape, carrying the opt-in `model_pool`. Its emitted runtime calls
|
|
2007
|
+
* `runChatLoop` with a single primary (exactly the cli shape's execution
|
|
2008
|
+
* model), so the pool routes there with zero runtime changes. The
|
|
2009
|
+
* superRefine is trivially satisfied today (the shape carries no
|
|
2010
|
+
* `model_tiers`/`model_fallbacks`) but keeps the mutual-exclusion rule
|
|
2011
|
+
* uniform if it ever gains them. NOT used by onchain/onchain-game: their
|
|
2012
|
+
* emitted bundles are callable modules whose agent-loop wiring is still
|
|
2013
|
+
* deferred (see target-onchain slice-2 notes), so a `model_pool` there
|
|
2014
|
+
* would be an inert spec field.
|
|
1253
2015
|
*/
|
|
1254
|
-
const
|
|
2016
|
+
const pooledSingleAgentObject = z
|
|
1255
2017
|
.object({
|
|
1256
2018
|
model: z.string().min(1),
|
|
1257
2019
|
instructions: z.string().min(1),
|
|
1258
2020
|
// Adaptive model routing — N-candidate pool with a selection policy.
|
|
1259
2021
|
model_pool: modelPoolBlock,
|
|
2022
|
+
})
|
|
2023
|
+
.strict();
|
|
2024
|
+
const pooledSingleAgentSchema = pooledSingleAgentObject.superRefine(refineModelSelection);
|
|
2025
|
+
/**
|
|
2026
|
+
* Loop contract 0.4 (Batch A) — the research/batch/browser variant of the
|
|
2027
|
+
* pooled single-agent block: pipeline's shape plus `max_tokens` (model max
|
|
2028
|
+
* OUTPUT tokens for one turn, mirroring the cli docblock — optional; when
|
|
2029
|
+
* omitted the runtime default applies; raise it for turns that emit large
|
|
2030
|
+
* outputs so the model isn't cut off mid-`tool_use`).
|
|
2031
|
+
*/
|
|
2032
|
+
const pooledSingleAgentWithMaxTokensSchema = pooledSingleAgentObject
|
|
2033
|
+
.extend({
|
|
2034
|
+
max_tokens: z.number().int().positive().optional(),
|
|
1260
2035
|
})
|
|
1261
2036
|
.strict()
|
|
1262
2037
|
.superRefine(refineModelSelection);
|
|
@@ -1304,11 +2079,27 @@ const crewRoleSchema = z
|
|
|
1304
2079
|
.object({
|
|
1305
2080
|
instructions: z.string().min(1),
|
|
1306
2081
|
model: z.string().min(1).optional(),
|
|
2082
|
+
// Model max OUTPUT tokens for this role's turns (mirrors cli
|
|
2083
|
+
// `agent.max_tokens`). Optional; runtime default when omitted.
|
|
2084
|
+
max_tokens: z.number().int().positive().optional(),
|
|
2085
|
+
// Loop contract 0.4 (Batch A) — per-role extended-thinking selector.
|
|
2086
|
+
thinking: thinkingBlock,
|
|
1307
2087
|
tools: z.array(z.string().min(1)).optional(),
|
|
1308
2088
|
tool_config: toolConfigBlock,
|
|
1309
2089
|
sub_agents: subAgentsBlock,
|
|
2090
|
+
// Item 9 (G37) — per-role model routing, adopting the cli agent block's
|
|
2091
|
+
// pooled pattern verbatim: ordered failover + breaker tuning + two-tier
|
|
2092
|
+
// router + N-candidate pool, sharing the one mutual-exclusion rule via
|
|
2093
|
+
// `refineModelSelection`. A PolicyRouter decides per role against the
|
|
2094
|
+
// shared routing-store scoreboard. Omitted → the role's single
|
|
2095
|
+
// (`role.model ?? crew.model`) model, byte-identical bundles.
|
|
2096
|
+
model_fallbacks: modelFallbacksBlock,
|
|
2097
|
+
circuit_breaker: circuitBreakerBlock,
|
|
2098
|
+
model_tiers: modelTiersBlock,
|
|
2099
|
+
model_pool: modelPoolBlock,
|
|
1310
2100
|
})
|
|
1311
|
-
.strict()
|
|
2101
|
+
.strict()
|
|
2102
|
+
.superRefine(refineModelSelection);
|
|
1312
2103
|
const crewRoutingMatchEntrySchema = z
|
|
1313
2104
|
.object({
|
|
1314
2105
|
contains: z.string().min(1),
|
|
@@ -1335,6 +2126,11 @@ const crewSchema = z
|
|
|
1335
2126
|
permissions: permissionsBlock,
|
|
1336
2127
|
compaction: compactionBlock,
|
|
1337
2128
|
failure_taxonomy: failureTaxonomyBlock,
|
|
2129
|
+
budget: budgetBlock,
|
|
2130
|
+
// Loop contract 0.4 (Batch A) — crew is the ONE shape whose limits block
|
|
2131
|
+
// additionally accepts the `crew:` orchestration sub-block.
|
|
2132
|
+
limits: crewLimitsBlock,
|
|
2133
|
+
hooks: hooksBlock,
|
|
1338
2134
|
// v0.3.0 — crew joins the memory-carrying shapes (§9: emit-wired; the
|
|
1339
2135
|
// roles share the spec-scoped stores — the plan IS the coordination
|
|
1340
2136
|
// surface, §2.7).
|
|
@@ -1342,6 +2138,11 @@ const crewSchema = z
|
|
|
1342
2138
|
continuity: continuityBlock,
|
|
1343
2139
|
thredz: thredzBlock,
|
|
1344
2140
|
learning: learningBlock,
|
|
2141
|
+
// Loop contract 0.4 (Batch C, G26) — crew joins the observability-carrying
|
|
2142
|
+
// shapes (cli/channel/managed): the orchestrator's cost/trace/metrics/
|
|
2143
|
+
// alert/incident/otel subscribers are spec-controllable per the shared
|
|
2144
|
+
// block's defaults semantics.
|
|
2145
|
+
observability: observabilityBlock,
|
|
1345
2146
|
chains: chainsBlock,
|
|
1346
2147
|
wallets: walletsBlock,
|
|
1347
2148
|
contracts: contractsBlock,
|
|
@@ -1368,7 +2169,7 @@ const researchSchema = z
|
|
|
1368
2169
|
name: safeName,
|
|
1369
2170
|
version: versionField,
|
|
1370
2171
|
target: z.literal("research"),
|
|
1371
|
-
agent:
|
|
2172
|
+
agent: pooledSingleAgentWithMaxTokensSchema,
|
|
1372
2173
|
goal: z.string().min(1),
|
|
1373
2174
|
branchingFactor: z.number().int().min(1).max(8).default(3),
|
|
1374
2175
|
maxDurationMs: z.number().int().positive().default(300_000),
|
|
@@ -1379,6 +2180,9 @@ const researchSchema = z
|
|
|
1379
2180
|
permissions: permissionsBlock,
|
|
1380
2181
|
compaction: compactionBlock,
|
|
1381
2182
|
failure_taxonomy: failureTaxonomyBlock,
|
|
2183
|
+
budget: budgetBlock,
|
|
2184
|
+
limits: limitsBlock,
|
|
2185
|
+
hooks: hooksBlock,
|
|
1382
2186
|
memory: memoryBlock,
|
|
1383
2187
|
continuity: continuityBlock,
|
|
1384
2188
|
thredz: thredzBlock,
|
|
@@ -1407,7 +2211,7 @@ const batchSchema = z
|
|
|
1407
2211
|
name: safeName,
|
|
1408
2212
|
version: versionField,
|
|
1409
2213
|
target: z.literal("batch"),
|
|
1410
|
-
agent:
|
|
2214
|
+
agent: pooledSingleAgentWithMaxTokensSchema,
|
|
1411
2215
|
queue: batchQueueSchema,
|
|
1412
2216
|
concurrency: z.number().int().min(1).max(64).default(4),
|
|
1413
2217
|
idempotencyWindowMs: z.number().int().positive().default(60_000),
|
|
@@ -1417,9 +2221,15 @@ const batchSchema = z
|
|
|
1417
2221
|
permissions: permissionsBlock,
|
|
1418
2222
|
compaction: compactionBlock,
|
|
1419
2223
|
failure_taxonomy: failureTaxonomyBlock,
|
|
2224
|
+
budget: budgetBlock,
|
|
2225
|
+
limits: limitsBlock,
|
|
2226
|
+
hooks: hooksBlock,
|
|
1420
2227
|
// v0.3.0 — carried but not emit-wired in 0.3.0 (ignored-note comment in
|
|
1421
2228
|
// the generated bundle; NOT default-on here).
|
|
1422
2229
|
continuity: continuityBlock,
|
|
2230
|
+
// Loop contract 0.4 (Batch F) — cron/interval wake trigger for the queue
|
|
2231
|
+
// worker daemon.
|
|
2232
|
+
schedule: scheduleBlock,
|
|
1423
2233
|
chains: chainsBlock,
|
|
1424
2234
|
wallets: walletsBlock,
|
|
1425
2235
|
contracts: contractsBlock,
|
|
@@ -1484,7 +2294,7 @@ const browserSchema = z
|
|
|
1484
2294
|
name: safeName,
|
|
1485
2295
|
version: versionField,
|
|
1486
2296
|
target: z.literal("browser"),
|
|
1487
|
-
agent:
|
|
2297
|
+
agent: pooledSingleAgentWithMaxTokensSchema,
|
|
1488
2298
|
driver: browserDriverSchema.default({}),
|
|
1489
2299
|
/** Vision-grounding model. Defaults to the agent's primary model. */
|
|
1490
2300
|
groundingModel: z.string().min(1).optional(),
|
|
@@ -1494,6 +2304,9 @@ const browserSchema = z
|
|
|
1494
2304
|
permissions: permissionsBlock,
|
|
1495
2305
|
compaction: compactionBlock,
|
|
1496
2306
|
failure_taxonomy: failureTaxonomyBlock,
|
|
2307
|
+
budget: budgetBlock,
|
|
2308
|
+
limits: limitsBlock,
|
|
2309
|
+
hooks: hooksBlock,
|
|
1497
2310
|
// v0.3.0 — carried but not emit-wired in 0.3.0 (ignored-note comment in
|
|
1498
2311
|
// the generated bundle; NOT default-on here).
|
|
1499
2312
|
continuity: continuityBlock,
|
|
@@ -1660,70 +2473,259 @@ export const Spec = z.discriminatedUnion("target", [
|
|
|
1660
2473
|
onchainGameSchema,
|
|
1661
2474
|
]);
|
|
1662
2475
|
export { SpecParseError };
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
throw new SpecParseError("invalid YAML", err);
|
|
1670
|
-
}
|
|
1671
|
-
// Friendly early-rejection for `permissions.mode: bypass` so the error
|
|
1672
|
-
// message names the actual security policy rather than a Zod enum mismatch.
|
|
1673
|
-
// The Zod schema also excludes "bypass" from its enum (defense in depth).
|
|
2476
|
+
/**
|
|
2477
|
+
* Friendly early-rejection for `permissions.mode: bypass` so the error
|
|
2478
|
+
* names the actual security policy rather than a Zod enum mismatch. The
|
|
2479
|
+
* Zod schema also excludes "bypass" from its enum (defense in depth).
|
|
2480
|
+
*/
|
|
2481
|
+
function bypassModeIssue(raw) {
|
|
1674
2482
|
if (typeof raw === "object" && raw !== null && "permissions" in raw) {
|
|
1675
2483
|
const perms = raw.permissions;
|
|
1676
2484
|
if (typeof perms === "object" && perms !== null && "mode" in perms) {
|
|
1677
2485
|
const mode = perms.mode;
|
|
1678
2486
|
if (mode === "bypass") {
|
|
1679
|
-
|
|
2487
|
+
return {
|
|
2488
|
+
path: ["permissions", "mode"],
|
|
2489
|
+
code: "custom",
|
|
2490
|
+
message: "permissions.mode: bypass is rejected — bypass mode is only available via the --permission-mode CLI flag, never from a spec file",
|
|
2491
|
+
};
|
|
1680
2492
|
}
|
|
1681
2493
|
}
|
|
1682
2494
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2495
|
+
return undefined;
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* The cross-field invariants `parseSpec` enforces beyond the zod schema,
|
|
2499
|
+
* as a structured issue list (empty = all invariants hold). Kept as data
|
|
2500
|
+
* checks rather than `.refine()`s so every discriminated-union member
|
|
2501
|
+
* stays a plain ZodObject (Zod's discriminatedUnion rejects ZodEffects).
|
|
2502
|
+
* `parseSpec` throws the FIRST issue's message (its historical behaviour);
|
|
2503
|
+
* `parseSpecIssues` returns them all. Check order is load-bearing for
|
|
2504
|
+
* `parseSpec`'s error messages — append, don't reorder.
|
|
2505
|
+
*/
|
|
2506
|
+
function crossFieldIssues(data) {
|
|
2507
|
+
const issues = [];
|
|
2508
|
+
const custom = (path, message) => {
|
|
2509
|
+
issues.push({ path, message, code: "custom" });
|
|
2510
|
+
};
|
|
2511
|
+
// Section 22 — crew cross-field invariants.
|
|
1693
2512
|
if (data.target === "crew") {
|
|
1694
2513
|
const roleNames = Object.keys(data.roles);
|
|
1695
2514
|
if (roleNames.length === 0) {
|
|
1696
|
-
|
|
2515
|
+
custom(["roles"], "crew target requires at least one role");
|
|
1697
2516
|
}
|
|
1698
|
-
if (!roleNames.includes(data.entry)) {
|
|
1699
|
-
|
|
2517
|
+
if (roleNames.length > 0 && !roleNames.includes(data.entry)) {
|
|
2518
|
+
custom(["entry"], `crew.entry "${data.entry}" must name one of crew.roles (got: ${roleNames.join(", ")})`);
|
|
1700
2519
|
}
|
|
1701
2520
|
if (data.routing !== undefined && data.routing.kind === "match" && data.routing.match) {
|
|
1702
2521
|
for (const [from, rules] of Object.entries(data.routing.match)) {
|
|
1703
2522
|
if (!roleNames.includes(from)) {
|
|
1704
|
-
|
|
2523
|
+
custom(["routing", "match", from], `crew.routing.match["${from}"]: source role not in crew.roles`);
|
|
1705
2524
|
}
|
|
1706
|
-
for (const rule of rules) {
|
|
2525
|
+
for (const [ri, rule] of rules.entries()) {
|
|
1707
2526
|
if (!roleNames.includes(rule.to)) {
|
|
1708
|
-
|
|
2527
|
+
custom(["routing", "match", from, ri, "to"], `crew.routing.match["${from}"].to = "${rule.to}" — target role not in crew.roles`);
|
|
1709
2528
|
}
|
|
1710
2529
|
}
|
|
1711
2530
|
}
|
|
1712
2531
|
}
|
|
1713
2532
|
}
|
|
2533
|
+
// Loop contract 0.4 (Batch B, G02) — a judge step gates the PREVIOUS
|
|
2534
|
+
// step's output, so the first step can never be one.
|
|
2535
|
+
if (data.target === "workflow") {
|
|
2536
|
+
const first = data.steps[0];
|
|
2537
|
+
if (first !== undefined && "kind" in first && first.kind === "judge") {
|
|
2538
|
+
custom(["steps", 0], `workflow steps[0] "${first.name}" cannot be a judge step — a judge gates the previous step's output and no step precedes it`);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
// Loop contract 0.4 (Batch A) — graph cross-field invariants:
|
|
2542
|
+
// - every `edges[].when.key` must name a declared node — the generated
|
|
2543
|
+
// graph state records each node's reply under its own name, so a key
|
|
2544
|
+
// that names nothing can never match;
|
|
2545
|
+
// - every `parallel` group member must name a declared node (mirrors
|
|
2546
|
+
// graph-engine's own compile-time check, surfaced at parse time);
|
|
2547
|
+
// - (Batch B) the entry cannot be a judge node — a judge gates its
|
|
2548
|
+
// upstream node's output and the entry has none.
|
|
2549
|
+
if (data.target === "graph") {
|
|
2550
|
+
const nodeNames = Object.keys(data.nodes);
|
|
2551
|
+
for (const [i, edge] of data.edges.entries()) {
|
|
2552
|
+
if (edge.when !== undefined && !nodeNames.includes(edge.when.key)) {
|
|
2553
|
+
custom(["edges", i, "when", "key"], `graph.edges[${i}].when.key "${edge.when.key}" must name a declared node — the shared state records each node's output under its name (nodes: ${nodeNames.join(", ")})`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (data.parallel !== undefined) {
|
|
2557
|
+
for (const [gi, group] of data.parallel.entries()) {
|
|
2558
|
+
for (const nodeName of group) {
|
|
2559
|
+
if (!nodeNames.includes(nodeName)) {
|
|
2560
|
+
custom(["parallel", gi], `graph.parallel[${gi}] references "${nodeName}" which is not a declared node (nodes: ${nodeNames.join(", ")})`);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
const entryNode = data.nodes[data.entry];
|
|
2566
|
+
if (entryNode !== undefined && "kind" in entryNode && entryNode.kind === "judge") {
|
|
2567
|
+
custom(["entry"], `graph entry "${data.entry}" cannot be a judge node — a judge gates its upstream node's output and the entry has none`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// Item 1 (G30) — `expose.mcp.tools: "per-subagent"` projects EACH declared
|
|
2571
|
+
// sub-agent as its own MCP tool, so it needs sub-agents to project. cli and
|
|
2572
|
+
// channel carry `agent.sub_agents`; the managed shape has none at all, so
|
|
2573
|
+
// per-subagent is always a mistake there. Load-bearing: no ir-pass mirrors
|
|
2574
|
+
// this, and the emitter would otherwise ship an MCP server exposing only the
|
|
2575
|
+
// primary tool while the author expected per-sub-agent ones.
|
|
2576
|
+
if (data.target === "cli" || data.target === "channel" || data.target === "managed") {
|
|
2577
|
+
const exposeTools = data.expose?.mcp?.tools;
|
|
2578
|
+
if (exposeTools === "per-subagent") {
|
|
2579
|
+
const subAgents = data.target === "managed"
|
|
2580
|
+
? undefined
|
|
2581
|
+
: data.agent.sub_agents;
|
|
2582
|
+
const count = subAgents === undefined ? 0 : Object.keys(subAgents).length;
|
|
2583
|
+
if (count === 0) {
|
|
2584
|
+
custom(["expose", "mcp", "tools"], `expose.mcp.tools: "per-subagent" projects each sub-agent as its own MCP tool, but the ${data.target} shape declares no sub_agents — use tools: "chat" (the default), or add sub_agents`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
1714
2588
|
// Section 21 — pipeline HTTP-backend invariants. qdrant/pinecone/weaviate
|
|
1715
2589
|
// throw at construction without a url + collection, so selecting one
|
|
1716
|
-
// without both would emit an unrunnable bundle.
|
|
1717
|
-
// a message naming the missing field (kept here, not as a `.refine()`, so
|
|
1718
|
-
// the discriminated-union member stays a plain ZodObject).
|
|
2590
|
+
// without both would emit an unrunnable bundle.
|
|
1719
2591
|
if (data.target === "pipeline" && HTTP_VECTOR_BACKENDS.has(data.retrieve.vectorBackend)) {
|
|
1720
2592
|
const { vectorBackend, url, collection } = data.retrieve;
|
|
1721
2593
|
if (!url) {
|
|
1722
|
-
|
|
2594
|
+
custom(["retrieve", "url"], `pipeline retrieve.vectorBackend "${vectorBackend}" requires retrieve.url (the remote service base URL)`);
|
|
1723
2595
|
}
|
|
1724
2596
|
if (!collection) {
|
|
1725
|
-
|
|
2597
|
+
custom(["retrieve", "collection"], `pipeline retrieve.vectorBackend "${vectorBackend}" requires retrieve.collection`);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return issues;
|
|
2601
|
+
}
|
|
2602
|
+
export function parseSpec(yamlText) {
|
|
2603
|
+
let raw;
|
|
2604
|
+
try {
|
|
2605
|
+
raw = parseYaml(yamlText);
|
|
2606
|
+
}
|
|
2607
|
+
catch (err) {
|
|
2608
|
+
throw new SpecParseError("invalid YAML", err);
|
|
2609
|
+
}
|
|
2610
|
+
const bypass = bypassModeIssue(raw);
|
|
2611
|
+
if (bypass !== undefined) {
|
|
2612
|
+
throw new SpecParseError(bypass.message);
|
|
2613
|
+
}
|
|
2614
|
+
const result = Spec.safeParse(raw);
|
|
2615
|
+
if (!result.success) {
|
|
2616
|
+
throw new SpecParseError(`spec validation failed:\n${result.error.issues
|
|
2617
|
+
.map((i) => ` ${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
2618
|
+
.join("\n")}`, result.error);
|
|
2619
|
+
}
|
|
2620
|
+
const issues = crossFieldIssues(result.data);
|
|
2621
|
+
const firstIssue = issues[0];
|
|
2622
|
+
if (firstIssue !== undefined) {
|
|
2623
|
+
throw new SpecParseError(firstIssue.message);
|
|
2624
|
+
}
|
|
2625
|
+
return result.data;
|
|
2626
|
+
}
|
|
2627
|
+
/** yaml's parse errors carry 1-indexed line/column in `linePos`. */
|
|
2628
|
+
function yamlSyntaxIssue(err) {
|
|
2629
|
+
const linePos = err.linePos;
|
|
2630
|
+
const pos = Array.isArray(linePos) && linePos[0] !== undefined ? linePos[0] : undefined;
|
|
2631
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
2632
|
+
// yaml's message embeds a multi-line code frame; keep the first line and
|
|
2633
|
+
// move its trailing position marker into a uniform suffix.
|
|
2634
|
+
const firstLine = (raw.split("\n")[0] ?? raw).trim();
|
|
2635
|
+
const cleaned = pos !== undefined ? firstLine.replace(/ at line \d+, column \d+:?$/, "") : firstLine;
|
|
2636
|
+
const where = pos !== undefined ? ` (line ${pos.line}, column ${pos.col})` : "";
|
|
2637
|
+
return { path: [], code: "yaml_syntax", message: `invalid YAML: ${cleaned}${where}` };
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Flatten zod issues to `SpecIssue`s. `invalid_union` issues (the workflow
|
|
2641
|
+
* step / graph node / continuity / thredz unions) are replaced by the most
|
|
2642
|
+
* plausible branch's issues — the branch that failed with the FEWEST
|
|
2643
|
+
* problems, ties broken by the fewest unrecognized KEYS (so a step that
|
|
2644
|
+
* declares `kind: judge` with a typo inside `judge:` reports the judge
|
|
2645
|
+
* branch's problem, not the regular branch's "unrecognized 'kind'") — so a
|
|
2646
|
+
* malformed union member reports its actual problem instead of an opaque
|
|
2647
|
+
* "Invalid input". Union sub-issues carry document-absolute paths in zod
|
|
2648
|
+
* v3, so no re-prefixing is needed.
|
|
2649
|
+
*/
|
|
2650
|
+
function unrecognizedKeyCount(err) {
|
|
2651
|
+
let count = 0;
|
|
2652
|
+
for (const issue of err.issues) {
|
|
2653
|
+
if (issue.code === z.ZodIssueCode.unrecognized_keys)
|
|
2654
|
+
count += issue.keys.length;
|
|
2655
|
+
}
|
|
2656
|
+
return count;
|
|
2657
|
+
}
|
|
2658
|
+
function zodIssuesToSpecIssues(zodIssues) {
|
|
2659
|
+
const out = [];
|
|
2660
|
+
for (const issue of zodIssues) {
|
|
2661
|
+
if (issue.code === z.ZodIssueCode.invalid_union && issue.unionErrors.length > 0) {
|
|
2662
|
+
const best = [...issue.unionErrors].sort((a, b) => a.issues.length - b.issues.length || unrecognizedKeyCount(a) - unrecognizedKeyCount(b))[0];
|
|
2663
|
+
if (best !== undefined && best.issues.length > 0) {
|
|
2664
|
+
out.push(...zodIssuesToSpecIssues(best.issues));
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
1726
2667
|
}
|
|
2668
|
+
out.push({ path: [...issue.path], message: issue.message, code: issue.code });
|
|
1727
2669
|
}
|
|
1728
|
-
return
|
|
2670
|
+
return out;
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Loop contract 0.4 (Batch B, G04) — the non-throwing sibling of
|
|
2674
|
+
* {@link parseSpec}: parse `yamlText` and return EVERY diagnostic as a
|
|
2675
|
+
* structured issue list (`[]` when the spec is valid). Built on the same
|
|
2676
|
+
* internals as `parseSpec` — which keeps its throw behaviour — so the two
|
|
2677
|
+
* can never disagree about validity:
|
|
2678
|
+
*
|
|
2679
|
+
* - YAML syntax errors → one issue, `path: []`, `code: "yaml_syntax"`,
|
|
2680
|
+
* line/column in the message.
|
|
2681
|
+
* - schema failures → one issue per zod issue (zod's own `code`s),
|
|
2682
|
+
* with `invalid_union` flattened to the most plausible branch.
|
|
2683
|
+
* - cross-field checks → `code: "custom"` with a best-effort path.
|
|
2684
|
+
*/
|
|
2685
|
+
export function parseSpecIssues(yamlText) {
|
|
2686
|
+
let raw;
|
|
2687
|
+
try {
|
|
2688
|
+
raw = parseYaml(yamlText);
|
|
2689
|
+
}
|
|
2690
|
+
catch (err) {
|
|
2691
|
+
return [yamlSyntaxIssue(err)];
|
|
2692
|
+
}
|
|
2693
|
+
const bypass = bypassModeIssue(raw);
|
|
2694
|
+
if (bypass !== undefined)
|
|
2695
|
+
return [bypass];
|
|
2696
|
+
const result = Spec.safeParse(raw);
|
|
2697
|
+
if (!result.success)
|
|
2698
|
+
return zodIssuesToSpecIssues(result.error.issues);
|
|
2699
|
+
return crossFieldIssues(result.data);
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Loop contract 0.4 (Batch B, G03) — the whole Spec union as a JSON-Schema
|
|
2703
|
+
* document. The document root is a `$ref` to `#/definitions/CrewhausSpec`
|
|
2704
|
+
* (the target-discriminated union); every target shape additionally gets
|
|
2705
|
+
* its own named definition (`#/definitions/cli`, `#/definitions/workflow`,
|
|
2706
|
+
* …) so tooling (editors, the compiler-worker `GET /schema` endpoint, the
|
|
2707
|
+
* studio) can link straight to one shape. Zod `.describe()` annotations
|
|
2708
|
+
* surface as JSON-Schema `description` keys. Pure function of this module
|
|
2709
|
+
* — no I/O, deterministic output.
|
|
2710
|
+
*/
|
|
2711
|
+
export function specJsonSchema() {
|
|
2712
|
+
return zodToJsonSchema(Spec, {
|
|
2713
|
+
name: "CrewhausSpec",
|
|
2714
|
+
definitions: {
|
|
2715
|
+
cli: cliSchema,
|
|
2716
|
+
workflow: workflowSchema,
|
|
2717
|
+
channel: channelSchema,
|
|
2718
|
+
graph: graphSchema,
|
|
2719
|
+
managed: managedSchema,
|
|
2720
|
+
pipeline: pipelineSchema,
|
|
2721
|
+
crew: crewSchema,
|
|
2722
|
+
research: researchSchema,
|
|
2723
|
+
batch: batchSchema,
|
|
2724
|
+
voice: voiceSchema,
|
|
2725
|
+
browser: browserSchema,
|
|
2726
|
+
eval: evalSchema,
|
|
2727
|
+
onchain: onchainSchema,
|
|
2728
|
+
"onchain-game": onchainGameSchema,
|
|
2729
|
+
},
|
|
2730
|
+
});
|
|
1729
2731
|
}
|