1mod agent;
15mod fs_util;
16mod gate;
17mod hybrid;
18mod resolution;
19mod run_md;
20mod sections;
21mod settings;
22mod split;
23mod support;
24mod types;
25
26pub use resolution::resolve_node_capabilities;
27pub use types::{HybridRealization, RenderFlavor, DEFAULT_RENDER_FLAVOR};
28
29use crate::error::DispatchError;
30use crate::ir::{validate_ir_version, WarbleIr};
31use crate::models::{ModelConfig, ANTHROPIC_PROVIDER};
32use crate::resolve::ResolutionReport;
33use crate::targets::{CapabilityOutcome, TargetId};
34use std::path::Path;
35
36use agent::build_agent_markdown;
37use fs_util::{mkdir_all, write_file, write_json};
38use hybrid::{any_local_provider, emit_hybrid_file_target, emit_hybrid_file_target_mcp};
39use resolution::{print_resolution_summary, resolve_node_with_shared_binding};
40use run_md::build_run_md;
41use settings::{build_settings, wren_config};
42use split::{
43 build_driver_markdown, build_split_run_md, build_split_settings, build_subagent_markdown,
44 should_split_per_step_tier, subagent_name,
45};
46use support::{outcome_supported, realization_supported, trigger_supported, unsupported};
47
48pub fn emit_claude_code(
49 ir: &WarbleIr,
50 out_dir: &Path,
51 target_id: &str,
52 render_flavor: RenderFlavor,
53) -> Result<(), DispatchError> {
54 emit_claude_code_with_models(
55 ir,
56 out_dir,
57 target_id,
58 render_flavor,
59 &ModelConfig::default(),
60 )
61}
62
63fn require_per_step_provider_support(
68 ir: &WarbleIr,
69 target_id: &str,
70 models: &ModelConfig,
71) -> Result<(), DispatchError> {
72 let supported = TargetId::parse(target_id)
73 .map(|t| t.profile())
74 .and_then(|p| p.get("llm:per_step_provider").map(|e| e.outcome))
75 .is_some_and(|outcome| outcome != CapabilityOutcome::Fail);
76 if supported {
77 return Ok(());
78 }
79 for node in &ir.components {
80 for call in &node.llm_calls {
81 let binding = models.binding(&call.tier)?;
82 if binding.provider != ANTHROPIC_PROVIDER {
83 return Err(DispatchError(format!(
84 "llm:per_step_provider: fail on {target_id} — the binding routes step '{}.{}' \
85(tier '{}') to provider '{}', but this target is whole-session single-provider and does not support \
86per-step provider routing (hybrid). Use an all-cloud binding for this target, or dispatch to a target \
87that realizes llm:per_step_provider.",
88 node.verb,
89 call.name,
90 call.tier,
91 binding.provider.as_str()
92 )));
93 }
94 }
95 }
96 Ok(())
97}
98
99pub fn emit_claude_code_with_models(
105 ir: &WarbleIr,
106 out_dir: &Path,
107 target_id: &str,
108 render_flavor: RenderFlavor,
109 models: &ModelConfig,
110) -> Result<(), DispatchError> {
111 emit_claude_code_with_realization(
112 ir,
113 out_dir,
114 target_id,
115 render_flavor,
116 models,
117 HybridRealization::default(),
118 )
119}
120
121pub fn emit_claude_code_with_realization(
124 ir: &WarbleIr,
125 out_dir: &Path,
126 target_id: &str,
127 render_flavor: RenderFlavor,
128 models: &ModelConfig,
129 hybrid: HybridRealization,
130) -> Result<(), DispatchError> {
131 validate_ir_version(ir)?;
132 models.validate(ir)?;
134 if ir.components.iter().any(should_split_per_step_tier) {
137 models.orchestrator()?;
138 }
139 require_per_step_provider_support(ir, target_id, models)?;
145
146 if any_local_provider(ir, models)? {
149 return match hybrid {
150 HybridRealization::BashScript => {
151 emit_hybrid_file_target(ir, out_dir, target_id, models)
152 }
153 HybridRealization::McpServer => {
154 emit_hybrid_file_target_mcp(ir, out_dir, target_id, models)
155 }
156 };
157 }
158
159 let mut reports: Vec<(String, ResolutionReport)> = Vec::with_capacity(ir.components.len());
161 for node in &ir.components {
162 reports.push((
163 node.id.clone(),
164 resolve_node_with_shared_binding(node, &ir.context_binding, target_id)?,
165 ));
166 }
167 let report_for = |id: &str| -> &ResolutionReport {
168 &reports
169 .iter()
170 .find(|(nid, _)| nid == id)
171 .expect("report exists")
172 .1
173 };
174
175 for node in &ir.components {
176 if !realization_supported(node.realization_kind) {
177 return Err(unsupported(
178 "realization_kind",
179 node.realization_kind.as_str(),
180 ));
181 }
182 if !trigger_supported(node.trigger.kind) {
183 return Err(unsupported("trigger.kind", node.trigger.kind.as_str()));
184 }
185 if !outcome_supported(node.effect.outcome.kind) {
186 return Err(unsupported(
187 "outcome.kind",
188 node.effect.outcome.kind.as_str(),
189 ));
190 }
191
192 let claude_dir = out_dir.join(".claude");
193 let agents_dir = claude_dir.join("agents");
194 let wren_dir = out_dir.join(".wren");
195 mkdir_all(&agents_dir)?;
196 mkdir_all(&wren_dir)?;
197
198 let report = report_for(&node.id);
199
200 if should_split_per_step_tier(node) {
201 write_file(
202 &agents_dir.join(format!("{}.md", node.verb)),
203 &build_driver_markdown(node, report, render_flavor, models),
204 )?;
205 for call in &node.llm_calls {
206 write_file(
207 &agents_dir.join(format!("{}.md", subagent_name(&node.verb, call))),
208 &build_subagent_markdown(node, call, models),
209 )?;
210 }
211 write_json(
212 &claude_dir.join("settings.json"),
213 &build_split_settings(node, report, render_flavor),
214 )?;
215 write_json(&wren_dir.join("config.json"), &wren_config())?;
216 write_file(
217 &out_dir.join("RUN.md"),
218 &build_split_run_md(node, report, render_flavor, models),
219 )?;
220 } else {
221 write_file(
222 &agents_dir.join(format!("{}.md", node.verb)),
223 &build_agent_markdown(node, report, render_flavor, models)?,
224 )?;
225 write_json(
229 &claude_dir.join("settings.json"),
230 &build_settings(node, report, render_flavor),
231 )?;
232 write_json(&wren_dir.join("config.json"), &wren_config())?;
233 write_file(
234 &out_dir.join("RUN.md"),
235 &build_run_md(node, report, render_flavor, models)?,
236 )?;
237 }
238 }
239
240 let capability_report = serde_json::json!({
241 "target": target_id,
242 "components": ir.components.iter().map(|node| serde_json::json!({
243 "id": node.id,
244 "capabilities": report_for(&node.id),
245 })).collect::<Vec<_>>(),
246 });
247 write_json(&out_dir.join("capability-report.json"), &capability_report)?;
248
249 for node in &ir.components {
250 print_resolution_summary(
251 &format!("{target_id} (component '{}')", node.id),
252 report_for(&node.id),
253 );
254 }
255
256 Ok(())
257}