warble-claude-code 0.18.0

Warble back-end for the Claude Code CLI target: IR → static agent files + reference renderer + capability manifest.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
//! Typed view of the Warble IR (`warble_ir_version: 0.9`) that this back-end consumes.
//!
//! Mirrors [`ir-schema.md`][spec-ir] field-for-field. The IR JSON is the language-neutral seam
//! between the front-end compiler and any back-end: this module depends on the schema doc, not on
//! the front-end's Rust types. Enum arms cover every schema-valid value; arms this target does not
//! yet realize are rejected at emit time (a "wall-hit"), not at deserialization.
//!
//! [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md

use crate::error::DispatchError;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// IR version this back-end understands. Copied (not shared via a `core` dependency — dispatchers
/// never depend on `warble`) from the same source of truth as `docs/spec/ir-schema.md`'s title and
/// the TS back-end's `SUPPORTED_IR_VERSIONS` in `dispatcher/claude-agent-sdk/src/ir.ts`. The sole
/// cross-target lockstep owner is `core/tests/ir_version_lockstep_tests.rs`; this target's
/// `ir_version_tests.rs` covers only its own rejection behavior. An unrecognized
/// `warble_ir_version` is a loud-fail — see [`validate_ir_version`] — never a silent best-effort
/// read.
pub const SUPPORTED_IR_VERSION: &str = "0.9";

/// The one version gate every IR-consuming entry point in this crate (and the `cli` binary, at IR
/// parse time) must call before doing anything else with `ir`: `emit_claude_code_with_realization`
/// calls this as its first statement, and `cli::load_ir` calls it right after deserializing, so
/// every subcommand that reads an `ir.json` — `dispatch` and `manifest` alike — is covered by the
/// same check instead of each reimplementing (and potentially forgetting) it.
pub fn validate_ir_version(ir: &WarbleIr) -> Result<(), DispatchError> {
    if ir.warble_ir_version != SUPPORTED_IR_VERSION {
        return Err(DispatchError::new(format!(
            "unsupported warble_ir_version '{}' (this back-end understands: {SUPPORTED_IR_VERSION})",
            ir.warble_ir_version
        )));
    }
    if ir
        .components
        .iter()
        .any(|node| node.context_binding.absent != ir.context_binding.absent)
    {
        return Err(DispatchError::new(
            "root and component context_binding must agree on context absence",
        ));
    }
    Ok(())
}

/// Reject the composition facets before any executable artifact is emitted. This target parses
/// the complete IR 0.9 shape but does not yet own a trusted component invocation runtime.
pub fn reject_unsupported_component_composition(
    ir: &WarbleIr,
    target: &str,
) -> Result<(), DispatchError> {
    // This emitter starts advertised entries only. An unreachable internal mount is structural
    // inventory, not executable work, so it must not block or become an output artifact.
    for node in ir.components.iter().filter(|node| node.entrypoint) {
        for call in &node.llm_calls {
            if let Some(component_call) = call.component_calls.first() {
                return Err(DispatchError::new(format!(
                    "step '{}' on component '{}' authorizes component call alias '{}' to '{}', \
                     but component_invocation resolves fail on target '{target}' because no trusted \
                     invocation handler is installed (wall-hit)",
                    call.name, node.id, component_call.alias, component_call.component
                )));
            }
        }
    }
    Ok(())
}

/// A step's tier is an **open string**, not a fixed enum. Warble ships a standard core vocabulary
/// (`strong`, `cheap`) that keeps components portable, but a component may name a custom tier; the
/// dispatch-time model config (see `ModelConfig`) maps each tier name to a concrete model, and a
/// tier with no mapping is a loud-fail. Kept as `String` so the seam never constrains the vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RealizationKind {
    Skill,
    Tool,
    GatedTool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComponentType {
    Analytical,
    Assertive,
    Mutating,
    Constitutive,
    Orchestrating,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TriggerKind {
    OneShot,
    Scheduled,
    Event,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutcomeKind {
    None,
    Assertion,
    Mutation,
    Dispatch,
}

impl RealizationKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            RealizationKind::Skill => "skill",
            RealizationKind::Tool => "tool",
            RealizationKind::GatedTool => "gated-tool",
        }
    }
}

impl TriggerKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            TriggerKind::OneShot => "one_shot",
            TriggerKind::Scheduled => "scheduled",
            TriggerKind::Event => "event",
        }
    }
}

impl OutcomeKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            OutcomeKind::None => "none",
            OutcomeKind::Assertion => "assertion",
            OutcomeKind::Mutation => "mutation",
            OutcomeKind::Dispatch => "dispatch",
        }
    }
}

impl ComponentType {
    pub fn as_str(&self) -> &'static str {
        match self {
            ComponentType::Analytical => "analytical",
            ComponentType::Assertive => "assertive",
            ComponentType::Mutating => "mutating",
            ComponentType::Constitutive => "constitutive",
            ComponentType::Orchestrating => "orchestrating",
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ContextBinding {
    /// True only for explicit null in IR 0.9. Empty internal strings are not emitted identities.
    pub absent: bool,
    pub project: String,
    pub binding_mode: String,
    /// Fine-grained resolved binding (IR v0.3): metrics/dimensions/grains/lineage summary the
    /// front-end learned from the bound semantic layer. Carried through and tolerated here; the
    /// Claude Code back-end does not yet consume it (it still drives off the coarse project path).
    pub resolved: Option<serde_json::Value>,
}

impl<'de> Deserialize<'de> for ContextBinding {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        struct Bound {
            project: String,
            binding_mode: String,
            #[serde(default)]
            resolved: Option<serde_json::Value>,
        }
        Ok(match Option::<Bound>::deserialize(deserializer)? {
            Some(value) => Self {
                absent: false,
                project: value.project,
                binding_mode: value.binding_mode,
                resolved: value.resolved,
            },
            None => Self {
                absent: true,
                project: String::new(),
                binding_mode: String::new(),
                resolved: None,
            },
        })
    }
}

/// The IR's profile-level `config` block. Empty since IR `0.6` removed `tier_policy` (an inert
/// field no back-end read); kept as a struct so future profile-level config is an additive change.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct IrConfig {}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct LlmCall {
    pub name: String,
    /// Tier name (conventionally `strong`/`cheap`; custom names allowed — see [`RealizationKind`]
    /// docs and `ModelConfig`). Resolved to a concrete model at dispatch.
    pub tier: String,
    #[serde(default)]
    pub consumes: Vec<String>,
    #[serde(default)]
    pub produces: Option<String>,
    pub prompt: String,
    /// Whether this step only runs under a runtime condition (e.g. a prior step's outcome).
    #[serde(default)]
    pub conditional: bool,
    /// The closed-vocabulary guard deciding whether a `conditional` step runs (IR v0.3+; see
    /// [`ir-schema.md`][spec-ir]). This back-end's actual emission still treats `conditional` as an
    /// opaque flag — the static Claude Code CLI target has no deterministic runtime, so the guard is
    /// judged emergently by the live CLI agent from the emitted prompt text, not evaluated here.
    /// `conditional.rs` carries a deterministic evaluator for the same closed vocabulary, but it is
    /// conformance-only (tested against a fixture shared with the SDK back-end's evaluator, not
    /// wired into emission). This field is tolerated either way so the seam stays forward-compatible.
    ///
    /// [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md
    #[serde(default)]
    pub when: Option<WhenGuard>,
    /// Same-profile component aliases this exact step may invoke. Parsed even though this target
    /// cannot realize them yet, so executable preparation can wall-hit instead of dropping them.
    #[serde(default)]
    pub component_calls: Vec<ComponentCall>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ComponentCall {
    pub alias: String,
    pub component: String,
}

/// A closed-vocabulary guard on a conditional `llm_call`: `guard` is one of `on_failure` /
/// `on_flag` / `on_missing`, `target` is the guard-specific argument. See
/// [`ir-schema.md`][spec-ir].
///
/// [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct WhenGuard {
    pub guard: String,
    pub target: String,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Guardrail {
    pub name: String,
    pub locked: bool,
    #[serde(default)]
    pub scope: Option<String>,
    /// Optional numeric or structured threshold the guardrail enforces (shape is guardrail-specific).
    #[serde(default)]
    pub threshold: Option<serde_json::Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Trigger {
    pub kind: TriggerKind,
}

/// A typed render block: a block type plus its field-name → field-type schema. The field-type
/// strings are echoed into the agent's render contract verbatim; the back-end never interprets
/// them. `BTreeMap` keeps field order stable (deterministic output).
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct RenderBlock {
    #[serde(rename = "type")]
    pub block_type: String,
    #[serde(default)]
    pub fields: BTreeMap<String, String>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Outcome {
    pub kind: OutcomeKind,
    /// Free-form verdict classification for `assertion`-kind outcomes.
    #[serde(default)]
    pub verdict_type: Option<String>,
    /// Event/signal names this outcome emits (e.g. for `dispatch`-kind outcomes).
    #[serde(default)]
    pub emits: Option<Vec<String>>,
    /// The mutation/dispatch target, when applicable.
    #[serde(default)]
    pub target: Option<String>,
    /// The kind of change made, for `mutation`-kind outcomes.
    #[serde(default)]
    pub change_type: Option<String>,
    /// Routing scope for `dispatch`-kind outcomes (shape is dispatch-target-specific).
    #[serde(default)]
    pub routable_scope: Option<serde_json::Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Effect {
    #[serde(default)]
    pub render_blocks: Vec<RenderBlock>,
    pub outcome: Outcome,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct PreconditionResult {
    pub status: String,
    /// Per-predicate evaluation results (IR v0.3): each `{predicate, outcome}`. In v0.2 this was a
    /// free-form string list; v0.3 makes it structured now that predicates are really evaluated.
    #[serde(default)]
    pub checks: Vec<PreconditionCheck>,
}

/// One evaluated `context_precondition` and its outcome (`pass` in emitted IR — a failing
/// predicate loud-fails the compile before any IR is emitted).
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct PreconditionCheck {
    pub predicate: String,
    pub outcome: String,
}

/// A context precondition a component requires to hold before it runs (e.g. `has_metric`).
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Precondition {
    pub predicate: String,
    #[serde(default)]
    pub args: Option<serde_json::Value>,
}

/// A component parameter, either bound at dispatch time (`bind`, with an optional `default`) or
/// sourced from context (`source`). Exactly one of `bind`/`source` is expected to be present per
/// the schema, but both are optional here since this is a Deserialize-only view.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Param {
    pub name: String,
    #[serde(default)]
    pub bind: Option<String>,
    #[serde(default)]
    pub source: Option<String>,
    #[serde(default)]
    pub default: Option<serde_json::Value>,
}

/// An authored evaluation spec: which eval template to run and which metrics it scores.
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct EvalSpec {
    pub template_ref: String,
    #[serde(default)]
    pub metrics: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ComponentNode {
    pub id: String,
    pub entrypoint: bool,
    pub verb: String,
    #[serde(rename = "type")]
    pub component_type: ComponentType,
    pub realization_kind: RealizationKind,
    #[serde(deserialize_with = "ContextBinding::deserialize")]
    pub context_binding: ContextBinding,
    pub precondition_result: PreconditionResult,
    pub prompt_fragment: String,
    pub llm_calls: Vec<LlmCall>,
    pub guardrails: Vec<Guardrail>,
    pub trigger: Trigger,
    #[serde(default)]
    pub required_capabilities: Vec<String>,
    #[serde(default)]
    pub borrowed_actions: Vec<String>,
    pub eval_ref: String,
    pub effect: Effect,
    /// Free-text context capabilities this component requires (e.g. "a wren project ...").
    #[serde(default)]
    pub context_requirements: Vec<String>,
    /// Structured preconditions the bound context must satisfy (e.g. `has_metric`).
    #[serde(default)]
    pub context_precondition: Vec<Precondition>,
    /// Component parameters, either context-sourced or dispatch-bound.
    #[serde(default)]
    pub params: Vec<Param>,
    /// Authored evaluation spec, when the component declares one.
    #[serde(default)]
    pub eval: Option<EvalSpec>,
    /// Optional free-form framing shared by every step of this component (see
    /// `docs/spec/ir-schema.md`).
    #[serde(default)]
    pub brief: Option<String>,
    /// What this component is for, authored for a reader choosing between components rather than for
    /// the agent doing the work. Absent when the author wrote none, in which case the back-end
    /// synthesizes a shape line instead.
    #[serde(default)]
    pub description: Option<String>,
    /// Example requests this component is the right destination for.
    #[serde(default)]
    pub examples: Vec<String>,
    /// Slots belonging to this component's own prompt text. Profile-level slots live on
    /// [`WarbleIr`] instead and are deliberately not copied here.
    #[serde(default)]
    pub slots: Vec<SlotDecl>,
}

/// A named position in prompt text, with the alternative wordings that may fill it (IR 0.7).
///
/// `default` always names one of `variants` — the compiler refuses otherwise. `present_when` is
/// carried but never evaluated here: the spec assigns that to the host, and this back-end only asks
/// the host for the answer.
#[derive(Debug, Clone, PartialEq, Deserialize, serde::Serialize)]
pub struct SlotDecl {
    pub name: String,
    pub default: String,
    pub variants: std::collections::BTreeMap<String, String>,
    /// Present only when the author declared a condition. Carried verbatim, never interpreted.
    #[serde(default)]
    pub present_when: Option<serde_json::Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct WarbleIr {
    pub warble_ir_version: String,
    pub profile: String,
    #[serde(deserialize_with = "ContextBinding::deserialize")]
    pub context_binding: ContextBinding,
    pub config: IrConfig,
    pub components: Vec<ComponentNode>,
    /// Profile-level slots, belonging to the profile's `system_prompt`. Names are unique across the
    /// whole project — the compiler refuses a collision with any component's — so a consumer
    /// resolves `{{ slot.<name> }}` against one flat namespace.
    #[serde(default)]
    pub slots: Vec<SlotDecl>,
}