mod agent;
mod fs_util;
mod gate;
mod hybrid;
mod resolution;
mod run_md;
mod sections;
mod settings;
mod split;
mod support;
mod types;
pub use resolution::resolve_node_capabilities;
pub use types::{HybridRealization, RenderFlavor, DEFAULT_RENDER_FLAVOR};
use crate::error::DispatchError;
use crate::ir::{validate_ir_version, WarbleIr};
use crate::models::{ModelConfig, ANTHROPIC_PROVIDER};
use crate::resolve::ResolutionReport;
use crate::targets::{CapabilityOutcome, TargetId};
use std::path::Path;
use agent::build_agent_markdown;
use fs_util::{mkdir_all, write_file, write_json};
use hybrid::{any_local_provider, emit_hybrid_file_target, emit_hybrid_file_target_mcp};
use resolution::{print_resolution_summary, resolve_node_with_shared_binding};
use run_md::build_run_md;
use settings::{build_settings, wren_config};
use split::{
build_driver_markdown, build_split_run_md, build_split_settings, build_subagent_markdown,
should_split_per_step_tier, subagent_name,
};
use support::{outcome_supported, realization_supported, trigger_supported, unsupported};
pub fn emit_claude_code(
ir: &WarbleIr,
out_dir: &Path,
target_id: &str,
render_flavor: RenderFlavor,
) -> Result<(), DispatchError> {
emit_claude_code_with_models(
ir,
out_dir,
target_id,
render_flavor,
&ModelConfig::default(),
)
}
fn require_per_step_provider_support(
ir: &WarbleIr,
target_id: &str,
models: &ModelConfig,
) -> Result<(), DispatchError> {
let supported = TargetId::parse(target_id)
.map(|t| t.profile())
.and_then(|p| p.get("llm:per_step_provider").map(|e| e.outcome))
.is_some_and(|outcome| outcome != CapabilityOutcome::Fail);
if supported {
return Ok(());
}
for node in &ir.components {
for call in &node.llm_calls {
let binding = models.binding(&call.tier)?;
if binding.provider != ANTHROPIC_PROVIDER {
return Err(DispatchError(format!(
"llm:per_step_provider: fail on {target_id} — the binding routes step '{}.{}' \
(tier '{}') to provider '{}', but this target is whole-session single-provider and does not support \
per-step provider routing (hybrid). Use an all-cloud binding for this target, or dispatch to a target \
that realizes llm:per_step_provider.",
node.verb,
call.name,
call.tier,
binding.provider.as_str()
)));
}
}
}
Ok(())
}
pub fn emit_claude_code_with_models(
ir: &WarbleIr,
out_dir: &Path,
target_id: &str,
render_flavor: RenderFlavor,
models: &ModelConfig,
) -> Result<(), DispatchError> {
emit_claude_code_with_realization(
ir,
out_dir,
target_id,
render_flavor,
models,
HybridRealization::default(),
)
}
pub fn emit_claude_code_with_realization(
ir: &WarbleIr,
out_dir: &Path,
target_id: &str,
render_flavor: RenderFlavor,
models: &ModelConfig,
hybrid: HybridRealization,
) -> Result<(), DispatchError> {
validate_ir_version(ir)?;
models.validate(ir)?;
if ir.components.iter().any(should_split_per_step_tier) {
models.orchestrator()?;
}
require_per_step_provider_support(ir, target_id, models)?;
if any_local_provider(ir, models)? {
return match hybrid {
HybridRealization::BashScript => {
emit_hybrid_file_target(ir, out_dir, target_id, models)
}
HybridRealization::McpServer => {
emit_hybrid_file_target_mcp(ir, out_dir, target_id, models)
}
};
}
let mut reports: Vec<(String, ResolutionReport)> = Vec::with_capacity(ir.components.len());
for node in &ir.components {
reports.push((
node.id.clone(),
resolve_node_with_shared_binding(node, &ir.context_binding, target_id)?,
));
}
let report_for = |id: &str| -> &ResolutionReport {
&reports
.iter()
.find(|(nid, _)| nid == id)
.expect("report exists")
.1
};
for node in &ir.components {
if !realization_supported(node.realization_kind) {
return Err(unsupported(
"realization_kind",
node.realization_kind.as_str(),
));
}
if !trigger_supported(node.trigger.kind) {
return Err(unsupported("trigger.kind", node.trigger.kind.as_str()));
}
if !outcome_supported(node.effect.outcome.kind) {
return Err(unsupported(
"outcome.kind",
node.effect.outcome.kind.as_str(),
));
}
let claude_dir = out_dir.join(".claude");
let agents_dir = claude_dir.join("agents");
let wren_dir = out_dir.join(".wren");
mkdir_all(&agents_dir)?;
mkdir_all(&wren_dir)?;
let report = report_for(&node.id);
if should_split_per_step_tier(node) {
write_file(
&agents_dir.join(format!("{}.md", node.verb)),
&build_driver_markdown(node, report, render_flavor, models),
)?;
for call in &node.llm_calls {
write_file(
&agents_dir.join(format!("{}.md", subagent_name(&node.verb, call))),
&build_subagent_markdown(node, call, models),
)?;
}
write_json(
&claude_dir.join("settings.json"),
&build_split_settings(node, report, render_flavor),
)?;
write_json(&wren_dir.join("config.json"), &wren_config())?;
write_file(
&out_dir.join("RUN.md"),
&build_split_run_md(node, report, render_flavor, models),
)?;
} else {
write_file(
&agents_dir.join(format!("{}.md", node.verb)),
&build_agent_markdown(node, report, render_flavor, models)?,
)?;
write_json(
&claude_dir.join("settings.json"),
&build_settings(node, report, render_flavor),
)?;
write_json(&wren_dir.join("config.json"), &wren_config())?;
write_file(
&out_dir.join("RUN.md"),
&build_run_md(node, report, render_flavor, models)?,
)?;
}
}
let capability_report = serde_json::json!({
"target": target_id,
"components": ir.components.iter().map(|node| serde_json::json!({
"id": node.id,
"capabilities": report_for(&node.id),
})).collect::<Vec<_>>(),
});
write_json(&out_dir.join("capability-report.json"), &capability_report)?;
for node in &ir.components {
print_resolution_summary(
&format!("{target_id} (component '{}')", node.id),
report_for(&node.id),
);
}
Ok(())
}