Skip to main content

warble_claude_code/
targets.rs

1//! Target capability profiles — the declarative side of the capability model (see
2//! [`capability-model.md`][spec-cap]).
3//!
4//! A runtime target is `engine × mode`, never just "claude-code": the same engine in a different
5//! mode is a genuinely different capability set (headless *loses* `human_approval` but *gains*
6//! `structured_output_capture`). This module only declares profiles; `resolve.rs` links them.
7//!
8//! [spec-cap]: https://github.com/Canner/Warble/blob/v0.1.0/docs/spec/capability-model.md
9
10use serde::Serialize;
11use std::collections::HashMap;
12
13/// One of the four resolution outcomes a capability can take on a target.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum CapabilityOutcome {
17    Native,
18    RealizeVia,
19    Degrade,
20    Fail,
21}
22
23/// Who supplies a resolved capability.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "lowercase")]
26pub enum ProvidedBy {
27    Runtime,
28    Warble,
29    None,
30}
31
32/// safety-critical capabilities must never silently degrade — unsupported means the resolution
33/// pass aborts. required/best-effort may degrade with a warning recorded in the report.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "kebab-case")]
36pub enum Criticality {
37    SafetyCritical,
38    Required,
39    BestEffort,
40}
41
42#[derive(Debug, Clone)]
43pub struct CapabilityEntry {
44    pub outcome: CapabilityOutcome,
45    pub via: Option<&'static str>,
46    pub provided_by: ProvidedBy,
47    pub criticality: Criticality,
48    pub note: Option<&'static str>,
49}
50
51pub type CapabilityProfile = HashMap<&'static str, CapabilityEntry>;
52
53/// The two claude-code targets: engine × mode.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum TargetId {
56    Headless,
57    Interactive,
58}
59
60impl TargetId {
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            TargetId::Headless => "claude-code:headless",
64            TargetId::Interactive => "claude-code:interactive",
65        }
66    }
67
68    pub fn parse(value: &str) -> Option<TargetId> {
69        match value {
70            "claude-code:headless" => Some(TargetId::Headless),
71            "claude-code:interactive" => Some(TargetId::Interactive),
72            _ => None,
73        }
74    }
75
76    pub fn profile(&self) -> CapabilityProfile {
77        match self {
78            TargetId::Headless => headless_profile(),
79            TargetId::Interactive => interactive_profile(),
80        }
81    }
82}
83
84/// claude-code target's default mode when the caller doesn't specify one.
85pub const DEFAULT_TARGET: TargetId = TargetId::Headless;
86
87pub fn is_known_target(value: &str) -> bool {
88    TargetId::parse(value).is_some()
89}
90
91pub fn known_target_names() -> [&'static str; 2] {
92    [TargetId::Headless.as_str(), TargetId::Interactive.as_str()]
93}
94
95fn entry(
96    outcome: CapabilityOutcome,
97    via: Option<&'static str>,
98    provided_by: ProvidedBy,
99    criticality: Criticality,
100    note: Option<&'static str>,
101) -> CapabilityEntry {
102    CapabilityEntry {
103        outcome,
104        via,
105        provided_by,
106        criticality,
107        note,
108    }
109}
110
111fn headless_profile() -> CapabilityProfile {
112    use CapabilityOutcome::*;
113    use Criticality::*;
114    use ProvidedBy::{Runtime, Warble};
115    HashMap::from([
116        (
117            "sql_execution:read_only",
118            entry(Native, Some("bash-wren"), Runtime, Required, None),
119        ),
120        (
121            "genbi_build",
122            entry(Native, Some("bash-wren"), Runtime, Required, None),
123        ),
124        (
125            "semantic_introspection",
126            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
127        ),
128        // +Constitutive: reading the semantic model's structure to propose a context edit (models/
129        // metrics/knowledge) — realized the same way as semantic_introspection, via the `wren` CLI.
130        (
131            "schema_introspection",
132            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
133        ),
134        ("llm:strong", entry(Native, None, Runtime, Required, None)),
135        ("llm:cheap", entry(Native, None, Runtime, Required, None)),
136        (
137            "llm:per_step_tier",
138            entry(RealizeVia, Some("subagents"), Runtime, Required, None),
139        ),
140        // Per-step PROVIDER routing (cloud+local in one run) — distinct from per_step_tier, which is
141        // same-provider model selection. The whole-session `claude` process can't switch provider
142        // mid-run, so Warble realizes the LOCAL step two ways (--hybrid-realization): `bash-script`
143        // (a Bash-run local-inference script) or `mcp-server` (a `.mcp.json` registering `warble
144        // mcp-serve`, so the local call is an MCP tool — no bash widening). Cloud steps stay the
145        // driver's own `wren` work either way.
146        (
147            "llm:per_step_provider",
148            entry(
149                RealizeVia,
150                Some("bash-script|mcp-server"),
151                Warble,
152                Required,
153                Some("local step via a bash-script script or an mcp-server (warble mcp-serve); cloud steps are the driver's own wren work"),
154            ),
155        ),
156        (
157            "render_contract",
158            entry(RealizeVia, Some("html-file"), Runtime, BestEffort, None),
159        ),
160        (
161            "structured_output_capture",
162            entry(Native, Some("stream-json"), Runtime, Required, None),
163        ),
164        (
165            "human_approval",
166            entry(
167                Fail,
168                None,
169                ProvidedBy::None,
170                SafetyCritical,
171                Some("no human in the loop in headless mode"),
172            ),
173        ),
174        (
175            "write_authz",
176            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
177        ),
178        // +Constitutive: the path-scoped authorization gate for a `context`-target mutation (models/
179        // metrics/knowledge), distinct from `write_authz` (data writes) and `artifact_write` (render
180        // writes) — scopes must never cross. Realized via the same filesystem, scoped to a path.
181        (
182            "context_write_authz",
183            entry(RealizeVia, Some("scoped-fs"), Runtime, SafetyCritical, None),
184        ),
185        (
186            "artifact_write",
187            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
188        ),
189        // +Assertive borrows the scheduling / event / notify transports from the runtime (OS cron,
190        // pub/sub, MCP) — the IR names the capability + criticality only; the mechanism (cron / slack)
191        // is legalized here, never in the IR (capability-model §6/§7). A target with no such mechanism
192        // wired keeps these `fail` (loud, never silent).
193        (
194            "scheduler",
195            entry(RealizeVia, Some("os-cron"), Runtime, Required, None),
196        ),
197        (
198            "event_bus",
199            entry(RealizeVia, Some("pub-sub"), Runtime, Required, None),
200        ),
201        (
202            "notify_channel",
203            entry(RealizeVia, Some("mcp-notify"), Runtime, Required, None),
204        ),
205        (
206            "blast_radius",
207            entry(
208                Fail,
209                None,
210                Warble,
211                SafetyCritical,
212                Some("requires fine_grained_binding"),
213            ),
214        ),
215        // +Mutating borrows version control (git) from the runtime as the apply-time checkpoint a
216        // rollback restores from. Warble declares the requirement; git itself is never owned here.
217        (
218            "version_control",
219            entry(RealizeVia, Some("git"), Runtime, Required, None),
220        ),
221    ])
222}
223
224fn interactive_profile() -> CapabilityProfile {
225    use CapabilityOutcome::*;
226    use Criticality::*;
227    use ProvidedBy::{Runtime, Warble};
228    HashMap::from([
229        (
230            "sql_execution:read_only",
231            entry(Native, Some("bash-wren"), Runtime, Required, None),
232        ),
233        (
234            "genbi_build",
235            entry(Native, Some("bash-wren"), Runtime, Required, None),
236        ),
237        (
238            "semantic_introspection",
239            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
240        ),
241        // +Constitutive: see headless_profile — same mechanism, not a differentiator across modes.
242        (
243            "schema_introspection",
244            entry(RealizeVia, Some("bash-wren"), Runtime, Required, None),
245        ),
246        ("llm:strong", entry(Native, None, Runtime, Required, None)),
247        ("llm:cheap", entry(Native, None, Runtime, Required, None)),
248        (
249            "llm:per_step_tier",
250            entry(RealizeVia, Some("subagents"), Runtime, Required, None),
251        ),
252        // See headless_profile: per-step provider routing (hybrid) is realized via an emitted
253        // local-inference script (bash-script); same in interactive mode.
254        (
255            "llm:per_step_provider",
256            entry(
257                RealizeVia,
258                Some("bash-script|mcp-server"),
259                Warble,
260                Required,
261                Some("local step via an emitted local-inference script (Bash); cloud steps stay native subagents"),
262            ),
263        ),
264        (
265            "render_contract",
266            entry(
267                Degrade,
268                Some("terminal-markdown"),
269                Runtime,
270                BestEffort,
271                None,
272            ),
273        ),
274        (
275            "structured_output_capture",
276            entry(
277                Degrade,
278                None,
279                ProvidedBy::None,
280                BestEffort,
281                Some("not captured in the interactive TUI"),
282            ),
283        ),
284        (
285            "human_approval",
286            entry(Native, None, Runtime, SafetyCritical, None),
287        ),
288        (
289            "write_authz",
290            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
291        ),
292        // +Constitutive: see headless_profile — same path-scoped gate, not a differentiator across
293        // modes.
294        (
295            "context_write_authz",
296            entry(RealizeVia, Some("scoped-fs"), Runtime, SafetyCritical, None),
297        ),
298        (
299            "artifact_write",
300            entry(RealizeVia, Some("fs"), Runtime, SafetyCritical, None),
301        ),
302        // Same borrowed transports as headless (+Assertive): scheduler/event/notify are runtime-
303        // supplied on both modes; only render_contract + structured_output_capture + human_approval
304        // differ between the two.
305        (
306            "scheduler",
307            entry(RealizeVia, Some("os-cron"), Runtime, Required, None),
308        ),
309        (
310            "event_bus",
311            entry(RealizeVia, Some("pub-sub"), Runtime, Required, None),
312        ),
313        (
314            "notify_channel",
315            entry(RealizeVia, Some("mcp-notify"), Runtime, Required, None),
316        ),
317        (
318            "blast_radius",
319            entry(
320                Fail,
321                None,
322                Warble,
323                SafetyCritical,
324                Some("requires fine_grained_binding"),
325            ),
326        ),
327        // +Mutating borrows version control (git) from the runtime as the apply-time checkpoint a
328        // rollback restores from. Warble declares the requirement; git itself is never owned here.
329        (
330            "version_control",
331            entry(RealizeVia, Some("git"), Runtime, Required, None),
332        ),
333    ])
334}