//! Execução de subcomandos `sdd` como processos filhos (T-09, T-13).
//! Roda em thread dedicada e entrega o resultado por mpsc, para o event loop
//! continuar animando o spinner. Nunca usa shell (sem injeção — §Segurança).

use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

use serde::Serialize;
use serde_json::{json, Value};

use crate::domain::providers::ProviderModelPricing;
use crate::runtime::redaction::redact_text;

use super::execution::ExecutionTask;
use super::stage::Stage;

#[derive(Clone, Debug, Default, PartialEq)]
pub struct StageRunOptions {
    pub provider: Option<String>,
    pub model: Option<String>,
    pub effort: Option<String>,
    pub offline: bool,
    pub force: bool,
    pub artifact_dir: Option<PathBuf>,
    pub context_window_tokens: Option<u32>,
    pub pricing: Option<ProviderModelPricing>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DirectAdapter {
    Codex,
    Claude,
    Opencode,
    Cursor,
    Devin,
    Trae,
    Antigravity,
}

impl DirectAdapter {
    fn for_provider(provider: &str) -> Option<Self> {
        match provider.trim().to_ascii_lowercase().as_str() {
            "codex" => Some(Self::Codex),
            "claude" => Some(Self::Claude),
            "opencode" => Some(Self::Opencode),
            "cursor" => Some(Self::Cursor),
            "devin" | "devin-cli" => Some(Self::Devin),
            "trae" | "trae-cli" | "trae-agent" => Some(Self::Trae),
            "agy" | "antigravity" | "gemini" | "google-antigravity" => Some(Self::Antigravity),
            _ => None,
        }
    }

    fn provider(self) -> &'static str {
        match self {
            Self::Codex => "codex",
            Self::Claude => "claude",
            Self::Opencode => "opencode",
            Self::Cursor => "cursor",
            Self::Devin => "devin",
            Self::Trae => "trae",
            Self::Antigravity => "antigravity",
        }
    }

    fn command(self) -> &'static str {
        match self {
            Self::Codex => "codex",
            Self::Claude => "claude",
            Self::Opencode => "opencode",
            Self::Cursor => "cursor",
            Self::Devin => "devin",
            Self::Trae => "trae-cli",
            Self::Antigravity => "agy",
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Codex => "codex exec JSONL",
            Self::Claude => "Claude Code print",
            Self::Opencode => "opencode run",
            Self::Cursor => "Cursor Agent print",
            Self::Devin => "Devin CLI print",
            Self::Trae => "Trae Agent isolated run",
            Self::Antigravity => "Antigravity CLI print",
        }
    }

    fn agent_id(self) -> &'static str {
        match self {
            Self::Codex => "codex",
            Self::Claude => "claude-code",
            Self::Opencode => "opencode",
            Self::Cursor => "cursor-agent",
            Self::Devin => "devin",
            Self::Trae => "trae-agent",
            Self::Antigravity => "antigravity",
        }
    }

    fn agent_label(self) -> &'static str {
        match self {
            Self::Codex => "Codex CLI",
            Self::Claude => "Claude Code",
            Self::Opencode => "opencode",
            Self::Cursor => "Cursor Agent",
            Self::Devin => "Devin CLI",
            Self::Trae => "Trae Agent CLI",
            Self::Antigravity => "Antigravity CLI",
        }
    }

    fn login_command(self) -> &'static str {
        match self {
            Self::Codex => "codex login",
            Self::Claude => "claude auth login",
            Self::Opencode => "opencode providers login",
            Self::Cursor => "cursor agent login",
            Self::Devin => "devin auth login",
            Self::Trae => "trae-cli show-config",
            Self::Antigravity => "agy",
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AdapterCapabilities {
    pub provider: String,
    pub artifact_generation: bool,
    pub workspace_execution: bool,
    pub execution_fallback: String,
}

const SDD_CODEX_SKILL_FRONTMATTER_REPAIRS: &[&str] = &[
    ".agents/skills/design-tokens/SKILL.md",
    ".agents/skills/orchestration-plugin/skills/design-tokens/SKILL.md",
];

const SDD_MANAGED_SKILL_NAMES: &[&str] = &[
    "artifact-diagrams",
    "artifact-schemas",
    "bdd-gherkin",
    "checkpoints",
    "code-review",
    "data-contracts",
    "diagram-design",
    "design-flow",
    "design-interview",
    "design-to-tasks",
    "design-tokens",
    "experience-design",
    "frontend-design",
    "information-architecture",
    "integrations",
    "memory",
    "orchestration",
    "output-quality",
    "project-discovery",
    "provider-compatibility",
    "release-readiness",
    "repo-adapter",
    "risk-classifier",
    "security-privacy",
    "source-command-sdd-doctor",
    "test-strategy",
    "traceability",
    "visual-review",
];

/// Resultado da execução de um subcomando de etapa.
#[derive(Clone, Debug)]
pub struct StageResult {
    pub stage: Stage,
    pub exit_code: i32,
    pub stdout: String,
    pub stderr: String,
    pub duration_ms: u128,
    pub trace: Vec<String>,
    pub agent: AgentProvenance,
    pub model: ModelProvenance,
    pub usage: TokenUsageSample,
    pub context: StageContextSample,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct AgentProvenance {
    pub id: String,
    pub label: String,
    pub provider: Option<String>,
    pub command: Option<String>,
    pub source: String,
    pub confidence: String,
}

impl AgentProvenance {
    fn from_adapter(adapter: DirectAdapter, source: impl Into<String>) -> Self {
        Self {
            id: adapter.agent_id().to_string(),
            label: adapter.agent_label().to_string(),
            provider: Some(adapter.provider().to_string()),
            command: Some(adapter.command().to_string()),
            source: source.into(),
            confidence: "invoked".to_string(),
        }
    }

    fn from_harness(options: &StageRunOptions, source: impl Into<String>) -> Self {
        Self {
            id: "sdd-harness".to_string(),
            label: "SDD deterministic harness".to_string(),
            provider: options.provider.clone().and_then(non_empty_string),
            command: Some("sdd".to_string()),
            source: source.into(),
            confidence: "invoked".to_string(),
        }
    }

    fn unsupported(options: &StageRunOptions, source: impl Into<String>) -> Self {
        Self {
            id: "unsupported-workspace-execution".to_string(),
            label: "Unsupported workspace execution adapter".to_string(),
            provider: options.provider.clone().and_then(non_empty_string),
            command: None,
            source: source.into(),
            confidence: "blocked".to_string(),
        }
    }

    fn agent_id_option(&self) -> Option<&str> {
        if self.id.trim().is_empty() {
            None
        } else {
            Some(self.id.as_str())
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct ModelProvenance {
    pub provider: Option<String>,
    pub selected_model: Option<String>,
    pub observed_model: Option<String>,
    pub source: String,
    pub confidence: String,
}

impl ModelProvenance {
    fn from_options(
        options: &StageRunOptions,
        observed_model: Option<String>,
        source: impl Into<String>,
    ) -> Self {
        let observed_model = observed_model.and_then(non_empty_string);
        let confidence = if observed_model.is_some() {
            "observed"
        } else {
            "unreported"
        };
        Self {
            provider: options.provider.clone().and_then(non_empty_string),
            selected_model: options.model.clone().and_then(non_empty_string),
            observed_model,
            source: source.into(),
            confidence: confidence.to_string(),
        }
    }
}

#[derive(Clone, Debug)]
struct ResultProvenance {
    agent: AgentProvenance,
    model: ModelProvenance,
}

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum StageEvent {
    Trace(String),
    Usage(TokenUsageSample),
    Context(StageContextSample),
    Finished(StageResult),
}

/// Amostra local e provider-neutral de consumo de tokens.
/// É uma estimativa simples; adapters podem substituir por valores reais no futuro.
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct TokenUsageSample {
    pub input_tokens_estimate: usize,
    pub cached_input_tokens_estimate: usize,
    pub output_tokens_estimate: usize,
    pub reasoning_output_tokens_estimate: usize,
    pub total_tokens_estimate: usize,
    pub input_bytes: usize,
    pub output_bytes: usize,
    pub provider_reported: bool,
    pub cost: Option<TokenCostEstimate>,
}

#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct TokenCostEstimate {
    pub currency: String,
    pub input_cost: f64,
    pub cached_input_cost: f64,
    pub output_cost: f64,
    pub total_cost: f64,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct StageContextSample {
    pub prompt_tokens_estimate: usize,
    pub prompt_bytes: usize,
    pub artifact_count: usize,
    pub artifact_chars: usize,
    pub truncated: bool,
    pub context_window_tokens: Option<u32>,
}

impl TokenUsageSample {
    pub fn from_text(input: &str, output: &str) -> Self {
        let input_tokens_estimate = estimate_tokens(input);
        let output_tokens_estimate = estimate_tokens(output);
        Self {
            input_tokens_estimate,
            cached_input_tokens_estimate: 0,
            output_tokens_estimate,
            reasoning_output_tokens_estimate: 0,
            total_tokens_estimate: input_tokens_estimate + output_tokens_estimate,
            input_bytes: input.len(),
            output_bytes: output.len(),
            provider_reported: false,
            cost: None,
        }
    }

    pub fn from_provider_usage(
        input_tokens: usize,
        cached_input_tokens: usize,
        output_tokens: usize,
        reasoning_output_tokens: usize,
        total_tokens: usize,
    ) -> Self {
        Self {
            input_tokens_estimate: input_tokens,
            cached_input_tokens_estimate: cached_input_tokens,
            output_tokens_estimate: output_tokens,
            reasoning_output_tokens_estimate: reasoning_output_tokens,
            total_tokens_estimate: total_tokens,
            input_bytes: 0,
            output_bytes: 0,
            provider_reported: true,
            cost: None,
        }
    }

    pub fn with_cost(mut self, pricing: Option<&ProviderModelPricing>) -> Self {
        self.cost = pricing.and_then(|pricing| estimate_cost(&self, pricing));
        self
    }

    pub fn is_empty(&self) -> bool {
        self.total_tokens_estimate == 0 && self.input_bytes == 0 && self.output_bytes == 0
    }
}

fn estimate_cost(
    usage: &TokenUsageSample,
    pricing: &ProviderModelPricing,
) -> Option<TokenCostEstimate> {
    let input_rate = pricing.input_per_million?;
    let output_rate = pricing.output_per_million?;
    let cached_rate = pricing.cached_input_per_million.unwrap_or(input_rate);
    let cached = usage
        .cached_input_tokens_estimate
        .min(usage.input_tokens_estimate);
    let fresh_input = usage.input_tokens_estimate.saturating_sub(cached);
    let input_cost = (fresh_input as f64 / 1_000_000.0) * input_rate;
    let cached_input_cost = (cached as f64 / 1_000_000.0) * cached_rate;
    let output_cost = (usage.output_tokens_estimate as f64 / 1_000_000.0) * output_rate;
    Some(TokenCostEstimate {
        currency: pricing
            .currency
            .clone()
            .unwrap_or_else(|| "USD".to_string()),
        input_cost,
        cached_input_cost,
        output_cost,
        total_cost: input_cost + cached_input_cost + output_cost,
    })
}

/// Resolve o caminho do próprio executável `sdd` (não depende do PATH).
pub fn self_exe() -> PathBuf {
    std::env::current_exe().unwrap_or_else(|_| PathBuf::from("sdd"))
}

/// Monta o `Command` para executar uma etapa, sem disparar ainda.
/// Args sempre vetorizados; a ideia/input vai como argumentos posicionais.
fn build_stage_command(
    exe: &Path,
    stage: Stage,
    name: &str,
    root: &Path,
    input: Option<&str>,
    options: &StageRunOptions,
) -> Command {
    let mut cmd = Command::new(exe);
    cmd.arg(stage.key())
        .arg("--name")
        .arg(name)
        .arg("--root")
        .arg(root);
    if let Some(provider) = options
        .provider
        .as_deref()
        .filter(|value| !value.is_empty())
    {
        cmd.arg("--provider").arg(provider);
    }
    if let Some(model) = options.model.as_deref().filter(|value| !value.is_empty()) {
        cmd.arg("--model").arg(model);
    }
    if let Some(effort) = options.effort.as_deref().filter(|value| !value.is_empty()) {
        cmd.arg("--effort").arg(effort);
    }
    if options.offline {
        cmd.arg("--offline");
    }
    if options.force {
        cmd.arg("--force");
    }
    if let Some(text) = input {
        if !text.is_empty() {
            cmd.arg(text);
        }
    }
    cmd.stdin(Stdio::null());
    cmd
}

/// Dispara a etapa numa thread e retorna o receptor do resultado.
pub fn spawn_stage(
    exe: PathBuf,
    stage: Stage,
    name: String,
    root: PathBuf,
    input: Option<String>,
    options: StageRunOptions,
) -> Receiver<StageEvent> {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let start = Instant::now();
        let result = if let Some(adapter) = direct_generation_adapter(&options) {
            match adapter {
                DirectAdapter::Codex => {
                    run_codex_stage(&exe, stage, &name, &root, &options, start, &tx)
                }
                _ => run_cli_text_stage(adapter, &exe, stage, &name, &root, &options, start, &tx),
            }
        } else {
            run_deterministic_stage(DeterministicStageRun {
                exe: &exe,
                stage,
                name: &name,
                root: &root,
                input: input.as_deref(),
                options: &options,
                start,
                tx: &tx,
            })
        };
        let _ = tx.send(StageEvent::Finished(result));
    });
    rx
}

struct DeterministicStageRun<'a> {
    exe: &'a Path,
    stage: Stage,
    name: &'a str,
    root: &'a Path,
    input: Option<&'a str>,
    options: &'a StageRunOptions,
    start: Instant,
    tx: &'a Sender<StageEvent>,
}

fn run_deterministic_stage(run: DeterministicStageRun<'_>) -> StageResult {
    let DeterministicStageRun {
        exe,
        stage,
        name,
        root,
        input,
        options,
        start,
        tx,
    } = run;
    let mut trace = deterministic_trace(stage, options);
    emit_trace_lines(tx, &trace);
    let mut cmd = build_stage_command(exe, stage, name, root, input, options);
    match cmd.output() {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
            let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
            let output_sample = read_generated_artifact(options, stage)
                .unwrap_or_else(|| format!("{stdout}\n{stderr}"));
            let usage = TokenUsageSample::from_text(input.unwrap_or_default(), &output_sample)
                .with_cost(options.pricing.as_ref());
            push_trace(
                &mut trace,
                tx,
                format!(
                    "harness sdd terminou com exit {} em {}ms",
                    out.status.code().unwrap_or(-1),
                    start.elapsed().as_millis()
                ),
            );
            emit_usage(tx, &usage);
            StageResult {
                stage,
                exit_code: out.status.code().unwrap_or(-1),
                stdout,
                stderr,
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_harness(options, "sdd-harness"),
                model: ModelProvenance::from_options(options, None, "sdd-harness-unreported"),
                usage,
                context: StageContextSample::default(),
            }
        }
        Err(e) => {
            let usage = TokenUsageSample::from_text(input.unwrap_or_default(), "")
                .with_cost(options.pricing.as_ref());
            push_trace(&mut trace, tx, format!("falha ao iniciar harness sdd: {e}"));
            emit_usage(tx, &usage);
            StageResult {
                stage,
                exit_code: -1,
                stdout: String::new(),
                stderr: format!("falha ao iniciar subprocesso: {e}"),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_harness(options, "sdd-harness"),
                model: ModelProvenance::from_options(options, None, "sdd-harness-unreported"),
                usage,
                context: StageContextSample::default(),
            }
        }
    }
}

fn deterministic_trace(stage: Stage, options: &StageRunOptions) -> Vec<String> {
    let mut trace = Vec::new();
    let provider = options.provider.as_deref().unwrap_or("auto");
    trace.push(format!("provider selecionado: {provider}"));
    trace.push(format!("stage: {}", stage.key()));
    if let Some(model) = options.model.as_deref().filter(|value| !value.is_empty()) {
        trace.push(format!("modelo solicitado: {model}"));
    }
    if let Some(effort) = options.effort.as_deref().filter(|value| !value.is_empty()) {
        trace.push(format!("effort solicitado: {effort}"));
    }
    if options.offline {
        trace.push("offline no config: true".to_string());
    }
    if let Some(provider) = options.provider.as_deref() {
        trace.push(format!(
            "adapter direto para {provider} nao configurado; usando harness deterministico"
        ));
    } else {
        trace.push("sem adapter direto selecionado; usando harness deterministico".to_string());
    }
    trace
}

fn read_generated_artifact(options: &StageRunOptions, stage: Stage) -> Option<String> {
    let artifact_dir = options.artifact_dir.as_ref()?;
    std::fs::read_to_string(artifact_dir.join(stage.filename())).ok()
}

fn non_empty_string(value: String) -> Option<String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

pub fn estimate_tokens(text: &str) -> usize {
    let chars = text.chars().count();
    if chars == 0 {
        0
    } else {
        chars.saturating_add(3) / 4
    }
}

fn reported_usage_from_jsonl(stdout: &[u8]) -> Option<TokenUsageSample> {
    let text = String::from_utf8_lossy(stdout);
    text.lines().rev().find_map(reported_usage_from_json_line)
}

fn observed_model_from_json_line(line: &str) -> Option<String> {
    let value = serde_json::from_str::<Value>(line).ok()?;
    observed_model_from_json_value(&value)
}

fn observed_model_from_jsonl(stdout: &[u8]) -> Option<String> {
    let text = String::from_utf8_lossy(stdout);
    text.lines().find_map(observed_model_from_json_line)
}

fn observed_model_from_json_value(value: &Value) -> Option<String> {
    match value {
        Value::Object(object) => {
            for key in ["model", "model_id", "resolved_model", "actual_model"] {
                if let Some(model) = object
                    .get(key)
                    .and_then(Value::as_str)
                    .and_then(|value| non_empty_string(value.to_string()))
                {
                    return Some(model);
                }
            }
            for key in [
                "payload", "response", "session", "turn", "item", "data", "metadata", "event",
                "result", "events", "messages", "choices",
            ] {
                if let Some(model) = object.get(key).and_then(observed_model_from_json_value) {
                    return Some(model);
                }
            }
            None
        }
        Value::Array(items) => items.iter().find_map(observed_model_from_json_value),
        _ => None,
    }
}

fn reported_usage_from_json_line(line: &str) -> Option<TokenUsageSample> {
    let value = serde_json::from_str::<Value>(line).ok()?;
    let usage = value.get("usage").or_else(|| {
        value
            .get("payload")
            .and_then(|payload| payload.get("usage"))
    })?;
    let input = usage.get("input_tokens").and_then(Value::as_u64)? as usize;
    let cached_input = usage
        .get("cached_input_tokens")
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize;
    let output = usage
        .get("output_tokens")
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize;
    let reasoning_output = usage
        .get("reasoning_output_tokens")
        .and_then(Value::as_u64)
        .unwrap_or(0) as usize;
    let total = usage
        .get("total_tokens")
        .and_then(Value::as_u64)
        .map(|value| value as usize)
        .unwrap_or_else(|| input + output);
    Some(TokenUsageSample::from_provider_usage(
        input,
        cached_input,
        output,
        reasoning_output,
        total,
    ))
}

fn codex_event_trace_line(line: &str) -> Option<String> {
    let value = serde_json::from_str::<Value>(line).ok()?;
    let event_type = value
        .get("type")
        .or_else(|| value.get("event"))
        .and_then(Value::as_str)
        .unwrap_or("event");
    if value.get("usage").is_some()
        || value
            .get("payload")
            .and_then(|payload| payload.get("usage"))
            .is_some()
    {
        return Some(format!("codex event: {event_type} · usage"));
    }
    if let Some(item) = value.get("item").or_else(|| value.get("payload")) {
        let item_type = item
            .get("type")
            .or_else(|| item.get("kind"))
            .and_then(Value::as_str);
        let status = item.get("status").and_then(Value::as_str);
        let name = item
            .get("name")
            .or_else(|| item.get("title"))
            .and_then(Value::as_str);
        let detail = [item_type, status, name]
            .into_iter()
            .flatten()
            .collect::<Vec<_>>()
            .join(" · ");
        if !detail.is_empty() {
            return Some(format!(
                "codex event: {event_type} · {}",
                truncate_trace(&detail, 96)
            ));
        }
    }
    if let Some(message) = value
        .get("message")
        .or_else(|| value.get("delta"))
        .and_then(Value::as_str)
        .filter(|message| !message.trim().is_empty())
    {
        return Some(format!(
            "codex event: {event_type} · {}",
            truncate_trace(message, 96)
        ));
    }
    Some(format!("codex event: {event_type}"))
}

fn push_trace(trace: &mut Vec<String>, tx: &Sender<StageEvent>, item: impl Into<String>) {
    let item = item.into();
    trace.push(item.clone());
    emit_trace(tx, item);
}

fn push_trace_once(trace: &mut Vec<String>, item: impl Into<String>) {
    let item = item.into();
    if trace.last() != Some(&item) && !trace.contains(&item) {
        trace.push(item);
    }
}

fn emit_trace_lines(tx: &Sender<StageEvent>, lines: &[String]) {
    for line in lines {
        emit_trace(tx, line.clone());
    }
}

fn emit_trace(tx: &Sender<StageEvent>, item: impl Into<String>) {
    let _ = tx.send(StageEvent::Trace(redact_text(&item.into())));
}

fn emit_usage(tx: &Sender<StageEvent>, usage: &TokenUsageSample) {
    if !usage.is_empty() {
        let _ = tx.send(StageEvent::Usage(usage.clone()));
    }
}

fn emit_context(tx: &Sender<StageEvent>, context: &StageContextSample) {
    if context.prompt_tokens_estimate > 0 || context.artifact_count > 0 {
        let _ = tx.send(StageEvent::Context(context.clone()));
    }
}

fn truncate_trace(input: &str, max: usize) -> String {
    if input.chars().count() <= max {
        input.to_string()
    } else {
        let mut out: String = input.chars().take(max.saturating_sub(1)).collect();
        out.push('…');
        out
    }
}

fn codex_stderr_trace_line(line: &str) -> Option<String> {
    let trimmed = line.trim();
    if trimmed.is_empty() || trimmed == "Reading additional input from stdin..." {
        return None;
    }
    if let Some(warning) = codex_skill_load_warning(trimmed) {
        return Some(warning);
    }
    let redacted = redact_text(trimmed);
    if redacted.contains("ERROR") {
        return Some(format!(
            "erro do Codex CLI: {}",
            truncate_trace(&redacted, 118)
        ));
    }
    Some(format!("codex stderr: {}", truncate_trace(&redacted, 118)))
}

fn codex_skill_load_warning(line: &str) -> Option<String> {
    if !line.contains("failed to load skill") {
        return None;
    }
    let skill = line
        .split("/.agents/skills/")
        .nth(1)
        .and_then(|tail| tail.split("/SKILL.md").next())
        .filter(|value| !value.trim().is_empty())
        .unwrap_or("skill");
    let reason = line
        .split("/SKILL.md:")
        .nth(1)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("frontmatter/YAML inválido");
    let owner = if is_sdd_managed_skill_path(skill) {
        "skill SDD"
    } else {
        "skill local"
    };
    Some(format!(
        "aviso: {owner} {skill} ignorada pelo Codex ({})",
        truncate_trace(&redact_text(reason), 84)
    ))
}

fn is_sdd_managed_skill_path(skill: &str) -> bool {
    let name = skill.rsplit('/').next().unwrap_or(skill);
    SDD_MANAGED_SKILL_NAMES.contains(&name)
}

fn prepare_codex_skill_surface(root: &Path, trace: &mut Vec<String>, tx: &Sender<StageEvent>) {
    let mut sdd_repaired = Vec::new();
    for rel in SDD_CODEX_SKILL_FRONTMATTER_REPAIRS {
        let path = root.join(rel);
        match repair_codex_skill_frontmatter(&path) {
            Ok(Some(skill)) => sdd_repaired.push(skill),
            Ok(None) => {}
            Err(e) => push_trace(
                trace,
                tx,
                format!(
                    "aviso: skill SDD {} nao foi adaptada ({})",
                    skill_label_from_path(&path),
                    truncate_trace(&redact_text(&e.to_string()), 84)
                ),
            ),
        }
    }
    if !sdd_repaired.is_empty() {
        push_trace(
            trace,
            tx,
            format!(
                "skills Codex: {} frontmatter(s) SDD adaptado(s): {}",
                sdd_repaired.len(),
                sdd_repaired.join(", ")
            ),
        );
    }

    let local_repaired = repair_local_codex_skills(root);
    if !local_repaired.is_empty() {
        push_trace(
            trace,
            tx,
            format!(
                "skills Codex: {} frontmatter(s) local(is) adaptado(s): {}",
                local_repaired.len(),
                local_repaired.join(", ")
            ),
        );
    }
}

fn repair_codex_skill_frontmatter(path: &Path) -> std::io::Result<Option<String>> {
    let text = match fs::read_to_string(path) {
        Ok(text) => text,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => return Err(e),
    };
    if skill_frontmatter_is_valid(&text) {
        return Ok(None);
    }
    let skill = skill_label_from_path(path);
    let fixed = if skill_frontmatter_text(&text).is_some() {
        quote_plain_description_in_frontmatter(&text)
    } else {
        Some(prepend_codex_frontmatter(
            &text,
            &skill,
            &format!("Skill SDD {skill} adaptada para Codex."),
        ))
    };
    let Some(fixed) = fixed else {
        return Ok(None);
    };
    if !skill_frontmatter_is_valid(&fixed) {
        return Ok(None);
    }
    fs::write(path, fixed)?;
    Ok(Some(skill))
}

fn repair_local_codex_skills(root: &Path) -> Vec<String> {
    let skills_dir = root.join(".agents/skills");
    let Ok(entries) = fs::read_dir(skills_dir) else {
        return Vec::new();
    };
    let mut repaired = Vec::new();
    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let skill = path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or_default();
        if skill.is_empty() || is_sdd_managed_skill_path(skill) {
            continue;
        }
        let skill_path = path.join("SKILL.md");
        if repair_local_codex_skill_frontmatter(&skill_path)
            .ok()
            .flatten()
            .is_some()
        {
            repaired.push(skill.to_string());
        }
    }
    repaired
}

fn repair_local_codex_skill_frontmatter(path: &Path) -> std::io::Result<Option<String>> {
    let text = match fs::read_to_string(path) {
        Ok(text) => text,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => return Err(e),
    };
    if skill_frontmatter_text(&text).is_some() || !looks_like_markdown_skill(&text) {
        return Ok(None);
    }
    let skill = skill_label_from_path(path);
    let fixed = prepend_codex_frontmatter(
        &text,
        &skill,
        &format!("Skill local {skill} adaptada para Codex."),
    );
    if !skill_frontmatter_is_valid(&fixed) {
        return Ok(None);
    }
    fs::write(path, fixed)?;
    Ok(Some(skill))
}

fn looks_like_markdown_skill(text: &str) -> bool {
    text.lines()
        .find(|line| !line.trim().is_empty())
        .is_some_and(|line| line.trim_start().starts_with("# "))
}

fn skill_frontmatter_is_valid(text: &str) -> bool {
    skill_frontmatter_text(text)
        .as_deref()
        .is_some_and(|frontmatter| serde_yaml::from_str::<serde_yaml::Value>(frontmatter).is_ok())
}

fn skill_frontmatter_text(text: &str) -> Option<String> {
    let mut lines = text.lines();
    if lines.next()?.trim() != "---" {
        return None;
    }
    let mut frontmatter = String::new();
    for line in lines {
        if line.trim() == "---" {
            return Some(frontmatter);
        }
        frontmatter.push_str(line);
        frontmatter.push('\n');
    }
    None
}

fn quote_plain_description_in_frontmatter(text: &str) -> Option<String> {
    let mut lines = text.lines().map(str::to_string).collect::<Vec<_>>();
    if lines.first().map(|line| line.trim()) != Some("---") {
        return None;
    }
    let end = lines
        .iter()
        .enumerate()
        .skip(1)
        .find_map(|(idx, line)| (line.trim() == "---").then_some(idx))?;
    let mut changed = false;
    for line in lines.iter_mut().take(end).skip(1) {
        let Some(value) = line.strip_prefix("description: ") else {
            continue;
        };
        let value = value.trim();
        if value.starts_with('"')
            || value.starts_with('\'')
            || value.starts_with('|')
            || value.starts_with('>')
        {
            continue;
        }
        *line = format!("description: {}", yaml_double_quote(value));
        changed = true;
    }
    if !changed {
        return None;
    }
    let mut out = lines.join("\n");
    if text.ends_with('\n') {
        out.push('\n');
    }
    Some(out)
}

fn prepend_codex_frontmatter(text: &str, skill: &str, description: &str) -> String {
    format!(
        "---\nname: {skill}\ndescription: {}\n---\n\n{}",
        yaml_double_quote(description),
        text.trim_start()
    )
}

fn yaml_double_quote(value: &str) -> String {
    format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
}

fn skill_label_from_path(path: &Path) -> String {
    path.parent()
        .and_then(|parent| parent.file_name())
        .and_then(|name| name.to_str())
        .unwrap_or("skill")
        .to_string()
}

fn direct_generation_adapter(options: &StageRunOptions) -> Option<DirectAdapter> {
    DirectAdapter::for_provider(options.provider.as_deref()?)
}

pub fn has_direct_generation_adapter(provider: &str) -> bool {
    DirectAdapter::for_provider(provider).is_some()
}

pub fn adapter_capabilities(provider: &str) -> AdapterCapabilities {
    let normalized = provider.trim().to_ascii_lowercase();
    let direct = DirectAdapter::for_provider(&normalized);
    let workspace_execution = direct.is_some();
    AdapterCapabilities {
        provider: if normalized.is_empty() {
            "auto".to_string()
        } else {
            normalized
        },
        artifact_generation: direct.is_some(),
        workspace_execution,
        execution_fallback: if let Some(adapter) = direct {
            format!("{} workspace-write", adapter.label())
        } else {
            "harness determinístico sem edição de workspace".to_string()
        },
    }
}

pub fn provider_supports_workspace_execution(provider: &str) -> bool {
    adapter_capabilities(provider).workspace_execution
}

fn run_codex_stage(
    exe: &Path,
    stage: Stage,
    name: &str,
    root: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = Vec::new();
    for item in [
        "provider selecionado: codex".to_string(),
        "adapter: codex exec JSONL".to_string(),
        format!("stage: {}", stage.key()),
        format!("offline no config: {}", options.offline),
        format!("force overwrite: {}", options.force),
    ] {
        push_trace(&mut trace, tx, item);
    }
    if let Some(model) = options.model.as_deref().filter(|value| !value.is_empty()) {
        push_trace(&mut trace, tx, format!("modelo solicitado: {model}"));
    }
    if let Some(effort) = options.effort.as_deref().filter(|value| !value.is_empty()) {
        push_trace(&mut trace, tx, format!("effort solicitado: {effort}"));
    }
    prepare_codex_skill_surface(root, &mut trace, tx);

    let output_path = temp_output_path(stage);
    push_trace(
        &mut trace,
        tx,
        format!("saida temporaria: {}", output_path.display()),
    );
    let (prompt, context_sample) = build_provider_prompt(stage, name, root, options);
    emit_context(tx, &context_sample);
    let prompt_usage = TokenUsageSample::from_text(&prompt, "").with_cost(options.pricing.as_ref());
    emit_usage(tx, &prompt_usage);
    push_trace(
        &mut trace,
        tx,
        format!(
            "prompt montado: ~{} tokens de input local",
            prompt_usage.input_tokens_estimate
        ),
    );
    let mut cmd = build_codex_exec_command(
        root,
        &output_path,
        &prompt,
        options.model.as_deref(),
        options.effort.as_deref(),
        CodexSandbox::ReadOnly,
    );
    push_trace(
        &mut trace,
        tx,
        redact_text(&codex_trace_command(
            root,
            &output_path,
            options.model.as_deref(),
            options.effort.as_deref(),
            CodexSandbox::ReadOnly,
        )),
    );

    let codex_start = Instant::now();
    let mut child = match cmd.spawn() {
        Ok(child) => child,
        Err(e) => {
            push_trace(&mut trace, tx, format!("falha ao iniciar codex exec: {e}"));
            return StageResult {
                stage,
                exit_code: -1,
                stdout: String::new(),
                stderr: format!("falha ao iniciar codex exec: {e}"),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
                model: ModelProvenance::from_options(options, None, "codex-jsonl-unavailable"),
                usage: prompt_usage,
                context: context_sample,
            };
        }
    };
    push_trace(
        &mut trace,
        tx,
        format!("codex exec iniciado: pid {}", child.id()),
    );

    let stderr_handle = child.stderr.take().map(|stderr| {
        let stderr_tx = tx.clone();
        thread::spawn(move || {
            let mut captured = String::new();
            let mut trace_lines = Vec::new();
            for line in BufReader::new(stderr).lines().map_while(Result::ok) {
                if let Some(trace_line) = codex_stderr_trace_line(&line) {
                    if trace_lines.last() != Some(&trace_line) {
                        emit_trace(&stderr_tx, trace_line.clone());
                        trace_lines.push(trace_line);
                    }
                }
                captured.push_str(&line);
                captured.push('\n');
            }
            (captured, trace_lines)
        })
    });

    let mut stdout = String::new();
    let mut reported_usage = None;
    let mut observed_model = None;
    if let Some(child_stdout) = child.stdout.take() {
        for line in BufReader::new(child_stdout).lines().map_while(Result::ok) {
            stdout.push_str(&line);
            stdout.push('\n');
            if observed_model.is_none() {
                if let Some(model) = observed_model_from_json_line(&line) {
                    push_trace(&mut trace, tx, format!("modelo observado: {model}"));
                    observed_model = Some(model);
                }
            }
            if let Some(event_trace) = codex_event_trace_line(&line) {
                push_trace(&mut trace, tx, event_trace);
            }
            if let Some(usage) = reported_usage_from_json_line(&line) {
                let usage = usage.with_cost(options.pricing.as_ref());
                reported_usage = Some(usage.clone());
                emit_usage(tx, &usage);
                push_trace(
                    &mut trace,
                    tx,
                    format!(
                        "tokens reportados: input {} · output {} · total {}",
                        usage.input_tokens_estimate,
                        usage.output_tokens_estimate,
                        usage.total_tokens_estimate
                    ),
                );
            }
        }
    } else {
        push_trace(&mut trace, tx, "codex stdout indisponivel para streaming");
    }

    let status = match child.wait() {
        Ok(status) => status,
        Err(e) => {
            push_trace(&mut trace, tx, format!("falha ao aguardar codex exec: {e}"));
            return StageResult {
                stage,
                exit_code: -1,
                stdout,
                stderr: format!("falha ao aguardar codex exec: {e}"),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
                model: ModelProvenance::from_options(
                    options,
                    observed_model.clone(),
                    "codex-jsonl",
                ),
                usage: reported_usage.unwrap_or(prompt_usage),
                context: context_sample,
            };
        }
    };
    let (stderr, stderr_trace) = stderr_handle
        .and_then(|handle| handle.join().ok())
        .unwrap_or_default();
    for item in stderr_trace {
        push_trace_once(&mut trace, item);
    }
    push_trace(
        &mut trace,
        tx,
        format!(
            "codex exec terminou com exit {} em {}ms",
            status.code().unwrap_or(-1),
            codex_start.elapsed().as_millis()
        ),
    );
    if !status.success() {
        let usage = reported_usage.unwrap_or(prompt_usage);
        let failure = format!(
            "codex exec falhou ao gerar {}:\n{}",
            stage.label(),
            combined_output(stdout.as_bytes(), stderr.as_bytes())
        );
        return StageResult {
            stage,
            exit_code: status.code().unwrap_or(-1),
            stdout,
            stderr: failure,
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
            model: ModelProvenance::from_options(options, observed_model.clone(), "codex-jsonl"),
            usage,
            context: context_sample,
        };
    }

    let generated = match std::fs::read_to_string(&output_path) {
        Ok(content) => strip_wrapping_markdown_fence(&content),
        Err(e) => {
            push_trace(
                &mut trace,
                tx,
                format!(
                    "codex exec nao gravou o arquivo de saida: {}",
                    output_path.display()
                ),
            );
            let usage = reported_usage.unwrap_or(prompt_usage);
            return StageResult {
                stage,
                exit_code: -1,
                stdout,
                stderr: format!(
                    "codex exec terminou, mas nao foi possivel ler {}: {e}",
                    output_path.display()
                ),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
                model: ModelProvenance::from_options(
                    options,
                    observed_model.clone(),
                    "codex-jsonl",
                ),
                usage,
                context: context_sample,
            };
        }
    };
    let usage = reported_usage.unwrap_or_else(|| {
        TokenUsageSample::from_text(&prompt, &generated).with_cost(options.pricing.as_ref())
    });
    if generated.trim().is_empty() {
        push_trace(&mut trace, tx, "codex exec gerou conteudo vazio");
        return StageResult {
            stage,
            exit_code: -1,
            stdout,
            stderr: "codex exec terminou, mas o artefato gerado veio vazio".to_string(),
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
            model: ModelProvenance::from_options(options, observed_model.clone(), "codex-jsonl"),
            usage,
            context: context_sample,
        };
    }
    if let Err(e) = std::fs::write(&output_path, ensure_trailing_newline(&generated)) {
        push_trace(
            &mut trace,
            tx,
            format!("falha ao normalizar markdown gerado: {e}"),
        );
        return StageResult {
            stage,
            exit_code: -1,
            stdout,
            stderr: format!("falha ao preparar artefato gerado: {e}"),
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
            model: ModelProvenance::from_options(options, observed_model.clone(), "codex-jsonl"),
            usage,
            context: context_sample,
        };
    }
    push_trace(
        &mut trace,
        tx,
        format!("markdown gerado: {} bytes", generated.len()),
    );

    let mut save =
        build_artifact_save_command(exe, root, name, stage, &output_path, "draft", options.force);
    let save_start = Instant::now();
    let save_output = match save.output() {
        Ok(output) => output,
        Err(e) => {
            push_trace(
                &mut trace,
                tx,
                format!("falha ao iniciar artifact save: {e}"),
            );
            return StageResult {
                stage,
                exit_code: -1,
                stdout,
                stderr: format!("artifact save falhou ({stage:?}): {e}"),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
                model: ModelProvenance::from_options(
                    options,
                    observed_model.clone(),
                    "codex-jsonl",
                ),
                usage,
                context: context_sample,
            };
        }
    };
    push_trace(
        &mut trace,
        tx,
        format!(
            "artifact save terminou com exit {} em {}ms",
            save_output.status.code().unwrap_or(-1),
            save_start.elapsed().as_millis()
        ),
    );
    for item in trace_output("artifact save stdout", &save_output.stdout) {
        push_trace(&mut trace, tx, item);
    }
    for item in trace_output("artifact save stderr", &save_output.stderr) {
        push_trace(&mut trace, tx, item);
    }

    if save_output.status.success() {
        let _ = std::fs::remove_file(&output_path);
    }

    StageResult {
        stage,
        exit_code: save_output.status.code().unwrap_or(-1),
        stdout: format!("{}{}", stdout, String::from_utf8_lossy(&save_output.stdout)),
        stderr: if save_output.status.success() {
            format!("{}{}", stderr, String::from_utf8_lossy(&save_output.stderr))
        } else {
            format!(
                "artifact save falhou ({stage:?}):\n{}",
                combined_output(&save_output.stdout, &save_output.stderr)
            )
        },
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent: AgentProvenance::from_adapter(DirectAdapter::Codex, "direct-adapter"),
        model: ModelProvenance::from_options(options, observed_model, "codex-jsonl"),
        usage,
        context: context_sample,
    }
}

#[allow(clippy::too_many_arguments)]
fn run_cli_text_stage(
    adapter: DirectAdapter,
    exe: &Path,
    stage: Stage,
    name: &str,
    root: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = Vec::new();
    for item in [
        format!("provider selecionado: {}", adapter.provider()),
        format!("adapter: {}", adapter.label()),
        format!("stage: {}", stage.key()),
        format!("offline no config: {}", options.offline),
        format!("force overwrite: {}", options.force),
    ] {
        push_trace(&mut trace, tx, item);
    }
    if let Some(model) = options.model.as_deref().filter(|value| !value.is_empty()) {
        push_trace(&mut trace, tx, format!("modelo solicitado: {model}"));
    }
    if let Some(effort) = options.effort.as_deref().filter(|value| !value.is_empty()) {
        push_trace(&mut trace, tx, format!("effort solicitado: {effort}"));
    }

    if let Err(message) = ensure_adapter_ready(adapter) {
        push_trace(&mut trace, tx, message.clone());
        return StageResult {
            stage,
            exit_code: -1,
            stdout: String::new(),
            stderr: message,
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
            model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
            usage: TokenUsageSample::default(),
            context: StageContextSample::default(),
        };
    }

    let output_path = temp_output_path(stage);
    push_trace(
        &mut trace,
        tx,
        format!("saida temporaria: {}", output_path.display()),
    );
    let (prompt, context_sample) = build_provider_prompt(stage, name, root, options);
    emit_context(tx, &context_sample);
    let prompt_usage = TokenUsageSample::from_text(&prompt, "").with_cost(options.pricing.as_ref());
    emit_usage(tx, &prompt_usage);
    push_trace(
        &mut trace,
        tx,
        format!(
            "prompt montado: ~{} tokens de input local",
            prompt_usage.input_tokens_estimate
        ),
    );

    let mut cmd = build_cli_text_command(
        adapter,
        root,
        &prompt,
        options.model.as_deref(),
        options.effort.as_deref(),
        CliPermissionMode::ReadOnly,
    );
    push_trace(
        &mut trace,
        tx,
        cli_text_trace_command(
            adapter,
            root,
            options.model.as_deref(),
            options.effort.as_deref(),
        ),
    );

    let started = Instant::now();
    let output = match cmd.output() {
        Ok(output) => output,
        Err(e) => {
            let message = format!(
                "falha ao iniciar {}: {e}. Se estiver deslogado, rode `{}`.",
                adapter.command(),
                adapter.login_command()
            );
            push_trace(&mut trace, tx, message.clone());
            return StageResult {
                stage,
                exit_code: -1,
                stdout: String::new(),
                stderr: message,
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
                model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
                usage: prompt_usage,
                context: context_sample,
            };
        }
    };
    let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
    let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
    push_trace(
        &mut trace,
        tx,
        format!(
            "{} terminou com exit {} em {}ms",
            adapter.command(),
            output.status.code().unwrap_or(-1),
            started.elapsed().as_millis()
        ),
    );
    for item in trace_output(adapter.command(), &output.stderr) {
        push_trace(&mut trace, tx, item);
    }
    if !output.status.success() {
        let usage =
            TokenUsageSample::from_text(&prompt, &stdout).with_cost(options.pricing.as_ref());
        let failure = format!(
            "{} falhou ao gerar {}. Rode `{}` se a sessão estiver expirada:\n{}",
            adapter.command(),
            stage.label(),
            adapter.login_command(),
            combined_output(output.stdout.as_slice(), output.stderr.as_slice())
        );
        return StageResult {
            stage,
            exit_code: output.status.code().unwrap_or(-1),
            stdout,
            stderr: failure,
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
            model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
            usage,
            context: context_sample,
        };
    }

    let generated = strip_wrapping_markdown_fence(&stdout);
    let usage =
        TokenUsageSample::from_text(&prompt, &generated).with_cost(options.pricing.as_ref());
    emit_usage(tx, &usage);
    if generated.trim().is_empty() {
        push_trace(&mut trace, tx, "provider gerou conteudo vazio");
        return StageResult {
            stage,
            exit_code: -1,
            stdout,
            stderr: format!(
                "{} terminou, mas o artefato gerado veio vazio",
                adapter.command()
            ),
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
            model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
            usage,
            context: context_sample,
        };
    }
    if let Err(e) = std::fs::write(&output_path, ensure_trailing_newline(&generated)) {
        push_trace(
            &mut trace,
            tx,
            format!("falha ao escrever markdown gerado: {e}"),
        );
        return StageResult {
            stage,
            exit_code: -1,
            stdout,
            stderr: format!("falha ao preparar artefato gerado: {e}"),
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
            model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
            usage,
            context: context_sample,
        };
    }
    push_trace(
        &mut trace,
        tx,
        format!("markdown gerado: {} bytes", generated.len()),
    );

    let mut save =
        build_artifact_save_command(exe, root, name, stage, &output_path, "draft", options.force);
    let save_start = Instant::now();
    let save_output = match save.output() {
        Ok(output) => output,
        Err(e) => {
            push_trace(
                &mut trace,
                tx,
                format!("falha ao iniciar artifact save: {e}"),
            );
            return StageResult {
                stage,
                exit_code: -1,
                stdout,
                stderr: format!("artifact save falhou ({stage:?}): {e}"),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
                model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
                usage,
                context: context_sample,
            };
        }
    };
    push_trace(
        &mut trace,
        tx,
        format!(
            "artifact save terminou com exit {} em {}ms",
            save_output.status.code().unwrap_or(-1),
            save_start.elapsed().as_millis()
        ),
    );
    for item in trace_output("artifact save stdout", &save_output.stdout) {
        push_trace(&mut trace, tx, item);
    }
    for item in trace_output("artifact save stderr", &save_output.stderr) {
        push_trace(&mut trace, tx, item);
    }
    if save_output.status.success() {
        let _ = std::fs::remove_file(&output_path);
    }

    StageResult {
        stage,
        exit_code: save_output.status.code().unwrap_or(-1),
        stdout: format!("{}{}", stdout, String::from_utf8_lossy(&save_output.stdout)),
        stderr: if save_output.status.success() {
            format!("{}{}", stderr, String::from_utf8_lossy(&save_output.stderr))
        } else {
            format!(
                "artifact save falhou ({stage:?}):\n{}",
                combined_output(&save_output.stdout, &save_output.stderr)
            )
        },
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent: AgentProvenance::from_adapter(adapter, "direct-adapter"),
        model: ModelProvenance::from_options(options, None, "adapter-output-unreported"),
        usage,
        context: context_sample,
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CodexSandbox {
    ReadOnly,
    WorkspaceWrite,
}

impl CodexSandbox {
    fn arg(self) -> &'static str {
        match self {
            CodexSandbox::ReadOnly => "read-only",
            CodexSandbox::WorkspaceWrite => "workspace-write",
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CliPermissionMode {
    ReadOnly,
    WorkspaceWrite,
}

fn build_cli_text_command(
    adapter: DirectAdapter,
    root: &Path,
    prompt: &str,
    model: Option<&str>,
    effort: Option<&str>,
    mode: CliPermissionMode,
) -> Command {
    let command = adapter.command();
    let exe = crate::runtime::platform::find_executable(command)
        .unwrap_or_else(|| PathBuf::from(command));
    let mut cmd = Command::new(exe);
    match adapter {
        DirectAdapter::Claude => {
            cmd.arg("--print")
                .arg("--output-format")
                .arg("text")
                .arg("--permission-mode")
                .arg(match mode {
                    CliPermissionMode::ReadOnly => "plan",
                    CliPermissionMode::WorkspaceWrite => "acceptEdits",
                })
                .arg("--add-dir")
                .arg(root);
            if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--model").arg(model);
            }
            if let Some(effort) = effort.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--effort").arg(effort);
            }
            cmd.arg(prompt);
        }
        DirectAdapter::Opencode => {
            cmd.arg("run")
                .arg("--format")
                .arg("default")
                .arg("--dir")
                .arg(root);
            if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--model").arg(model);
            }
            if let Some(variant) = effort.and_then(opencode_variant) {
                cmd.arg("--variant").arg(variant);
            }
            if mode == CliPermissionMode::WorkspaceWrite {
                cmd.arg("--dangerously-skip-permissions");
            }
            cmd.arg(prompt);
        }
        DirectAdapter::Cursor => {
            cmd.arg("agent")
                .arg("--print")
                .arg("--output-format")
                .arg("text")
                .arg("--trust")
                .arg("--workspace")
                .arg(root);
            if mode == CliPermissionMode::ReadOnly {
                cmd.arg("--mode").arg("plan");
            } else {
                cmd.arg("--force");
            }
            if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--model").arg(model);
            }
            cmd.arg(prompt);
        }
        DirectAdapter::Devin => {
            cmd.arg("--print").arg("--permission-mode").arg(match mode {
                CliPermissionMode::ReadOnly => "normal",
                CliPermissionMode::WorkspaceWrite => "dangerous",
            });
            if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--model").arg(model);
            }
            let prompt = if mode == CliPermissionMode::ReadOnly {
                format!("/plan\n{prompt}")
            } else {
                prompt.to_string()
            };
            cmd.arg("--").arg(prompt);
        }
        DirectAdapter::Trae => {
            let workdir = if mode == CliPermissionMode::ReadOnly {
                temp_headless_workdir(adapter)
            } else {
                root.to_path_buf()
            };
            let _ = fs::create_dir_all(&workdir);
            cmd.arg("run")
                .arg(prompt)
                .arg("--working-dir")
                .arg(&workdir);
            if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--model").arg(model);
            }
            cmd.current_dir(workdir);
            cmd.stdin(Stdio::null());
            cmd.stdout(Stdio::piped());
            cmd.stderr(Stdio::piped());
            return cmd;
        }
        DirectAdapter::Antigravity => {
            cmd.arg("--add-dir").arg(root);
            if mode == CliPermissionMode::ReadOnly {
                cmd.arg("--sandbox");
            } else {
                cmd.arg("--dangerously-skip-permissions");
            }
            if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
                cmd.arg("--model").arg(model);
            }
            cmd.arg("--prompt").arg(prompt);
        }
        DirectAdapter::Codex => {}
    }
    cmd.current_dir(root);
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::piped());
    cmd
}

fn temp_headless_workdir(adapter: DirectAdapter) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "sdd-headless-{}-{}-{nanos}",
        adapter.provider(),
        std::process::id()
    ))
}

fn cli_text_trace_command(
    adapter: DirectAdapter,
    root: &Path,
    model: Option<&str>,
    effort: Option<&str>,
) -> String {
    let mut parts = vec![
        format!("cmd: {}", adapter.command()),
        format!("cwd={}", root.display()),
    ];
    parts.push(format!("adapter={}", adapter.label()));
    if let Some(model) = model.filter(|value| !value.trim().is_empty()) {
        parts.push(format!("model={model}"));
    }
    if let Some(effort) = effort.filter(|value| !value.trim().is_empty()) {
        parts.push(format!("effort={effort}"));
    }
    parts.join(" ")
}

fn opencode_variant(effort: &str) -> Option<&'static str> {
    match effort.trim().to_ascii_lowercase().as_str() {
        "medium" => Some("medium"),
        "high" => Some("high"),
        "xhigh" | "max" => Some("max"),
        _ => None,
    }
}

fn ensure_adapter_ready(adapter: DirectAdapter) -> Result<(), String> {
    if !command_exists(adapter.command()) {
        return Err(format!(
            "{} não está instalado ou não está no PATH. Instale/autentique o client e tente novamente.",
            adapter.command()
        ));
    }
    match adapter {
        DirectAdapter::Codex => ensure_check_contains(adapter, "codex login status", "Logged in"),
        DirectAdapter::Claude => {
            ensure_check_contains(adapter, "claude auth status", "\"loggedIn\": true")
        }
        DirectAdapter::Opencode => ensure_check_regex(
            adapter,
            "opencode providers list",
            r"\b[1-9][0-9]* credentials?\b",
        ),
        DirectAdapter::Cursor => ensure_check_contains(adapter, "cursor agent status", "Logged in"),
        DirectAdapter::Devin => ensure_check_success(adapter, "devin auth status"),
        DirectAdapter::Trae => ensure_check_success(adapter, "trae-cli show-config"),
        DirectAdapter::Antigravity => ensure_check_success(adapter, "agy models"),
    }
}

fn ensure_check_success(adapter: DirectAdapter, check_command: &str) -> Result<(), String> {
    let output = run_check_command(check_command).ok_or_else(|| {
        format!(
            "não foi possível rodar `{check_command}`. Se estiver deslogado, rode `{}`.",
            adapter.login_command()
        )
    })?;
    if output.status.success() {
        return Ok(());
    }
    Err(format!(
        "{} não parece autenticado. Rode `{}` e tente novamente.",
        adapter.command(),
        adapter.login_command()
    ))
}

fn ensure_check_contains(
    adapter: DirectAdapter,
    check_command: &str,
    expected: &str,
) -> Result<(), String> {
    let output = run_check_command(check_command).ok_or_else(|| {
        format!(
            "não foi possível rodar `{check_command}`. Se estiver deslogado, rode `{}`.",
            adapter.login_command()
        )
    })?;
    let combined = String::from_utf8_lossy(&output.stdout).to_string()
        + "\n"
        + &String::from_utf8_lossy(&output.stderr);
    if output.status.success() && combined.contains(expected) {
        return Ok(());
    }
    Err(format!(
        "{} não parece autenticado. Rode `{}` e tente novamente.",
        adapter.command(),
        adapter.login_command()
    ))
}

fn ensure_check_regex(
    adapter: DirectAdapter,
    check_command: &str,
    pattern: &str,
) -> Result<(), String> {
    let output = run_check_command(check_command).ok_or_else(|| {
        format!(
            "não foi possível rodar `{check_command}`. Se estiver deslogado, rode `{}`.",
            adapter.login_command()
        )
    })?;
    let combined = String::from_utf8_lossy(&output.stdout).to_string()
        + "\n"
        + &String::from_utf8_lossy(&output.stderr);
    let matched = regex::Regex::new(pattern)
        .map(|regex| regex.is_match(&combined))
        .unwrap_or(false);
    if output.status.success() && matched {
        return Ok(());
    }
    Err(format!(
        "{} não tem credencial ativa detectada. Rode `{}` e tente novamente.",
        adapter.command(),
        adapter.login_command()
    ))
}

fn run_check_command(command: &str) -> Option<std::process::Output> {
    let mut parts = command.split_whitespace();
    let program = parts.next()?;
    Command::new(program).args(parts).output().ok()
}

fn command_exists(command: &str) -> bool {
    crate::runtime::platform::executable_exists(command)
}

fn build_codex_exec_command(
    root: &Path,
    output_path: &Path,
    prompt: &str,
    model: Option<&str>,
    effort: Option<&str>,
    sandbox: CodexSandbox,
) -> Command {
    let exe = crate::runtime::platform::find_executable("codex")
        .unwrap_or_else(|| PathBuf::from("codex"));
    let mut cmd = Command::new(exe);
    cmd.arg("--ask-for-approval")
        .arg("never")
        .args(codex_effort_config_args(effort))
        .arg("exec")
        .arg("--json")
        .arg("--skip-git-repo-check")
        .arg("--ephemeral")
        .arg("--sandbox")
        .arg(sandbox.arg())
        .arg("--cd")
        .arg(root)
        .arg("--output-last-message")
        .arg(output_path);
    if let Some(model) = model.filter(|value| should_pass_model_to_codex(value)) {
        cmd.arg("--model").arg(model);
    }
    cmd.arg(prompt);
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::piped());
    cmd
}

fn build_artifact_save_command(
    exe: &Path,
    root: &Path,
    name: &str,
    stage: Stage,
    file: &Path,
    state: &str,
    force: bool,
) -> Command {
    let mut cmd = Command::new(exe);
    cmd.arg("artifact")
        .arg("--root")
        .arg(root)
        .arg("save")
        .arg(name)
        .arg(stage.key())
        .arg("--file")
        .arg(file)
        .arg("--state")
        .arg(state);
    if force {
        cmd.arg("--force");
    }
    cmd.stdin(Stdio::null());
    cmd
}

fn should_pass_model_to_codex(model: &str) -> bool {
    let normalized = model.trim();
    !normalized.is_empty()
        && normalized != "default"
        && normalized != "gpt-default"
        && !normalized.ends_with("-default")
}

fn codex_trace_command(
    root: &Path,
    output_path: &Path,
    model: Option<&str>,
    effort: Option<&str>,
    sandbox: CodexSandbox,
) -> String {
    let mut parts = vec!["cmd: codex exec --json".to_string()];
    if let Some(effort) = effort.filter(|value| !value.trim().is_empty()) {
        parts.push(format!("effort={effort}"));
    }
    parts.push(format!("sandbox={}", sandbox.arg()));
    parts.push("--ephemeral".to_string());
    parts.push(format!("cwd={}", root.display()));
    parts.push(format!(
        "output={}",
        output_path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("temp.md")
    ));
    if let Some(model) = model.filter(|value| should_pass_model_to_codex(value)) {
        parts.push(format!("model={model}"));
    }
    parts.join(" ")
}

fn codex_effort_config_args(effort: Option<&str>) -> Vec<String> {
    effort
        .filter(|value| !value.trim().is_empty())
        .map(|effort| {
            vec![
                "--config".to_string(),
                format!("model_reasoning_effort=\"{effort}\""),
            ]
        })
        .unwrap_or_default()
}

fn build_provider_prompt(
    stage: Stage,
    name: &str,
    root: &Path,
    options: &StageRunOptions,
) -> (String, StageContextSample) {
    let sections = required_sections_for(stage).join(", ");
    let previous_context = collect_artifact_context(options.artifact_dir.as_deref(), stage);
    let provider = options.provider.as_deref().unwrap_or("auto");
    let provider_model = options.model.as_deref().unwrap_or("default");
    let provider_effort = options.effort.as_deref().unwrap_or("medium");
    let prompt = format!(
        "Voce e o subagent SDD da etapa {stage_label}.\n\
Gere novamente o artefato Markdown completo para a orquestracao \"{name}\".\n\
\n\
Regras obrigatorias:\n\
- Responda somente com Markdown do artefato final, sem explicacoes fora do artefato.\n\
- Nao use cercas de codigo Markdown no entorno da resposta.\n\
- Nao altere arquivos e nao execute comandos; produza apenas o conteudo do artefato.\n\
- Use PT-BR claro, objetivo e rico em detalhes praticos.\n\
- Substitua placeholders como \"Preencher com o subagent correspondente\" por conteudo real.\n\
- Preserve rastreabilidade, decisoes, evidencias e pendencias.\n\
- Inclua obrigatoriamente estas secoes de segundo nivel: {sections}.\n\
\n\
Metadados para Rastreabilidade:\n\
- Orquestracao: {name}\n\
- Stage: {stage_key}\n\
- Provider: {provider}\n\
- Modelo: {provider_model}\n\
- Effort: {provider_effort}\n\
- Root analisado: {root}\n\
- Modo TUI: geracao direta via provider\n\
\n\
Contexto existente dos artefatos SDD:\n\
{previous_context}\n\
\n\
Produza agora o artefato final completo.",
        stage_label = stage.label(),
        stage_key = stage.key(),
        root = root.display()
    );
    let context = StageContextSample {
        prompt_tokens_estimate: estimate_tokens(&prompt),
        prompt_bytes: prompt.len(),
        artifact_count: previous_context.artifact_count,
        artifact_chars: previous_context.artifact_chars,
        truncated: previous_context.truncated,
        context_window_tokens: options.context_window_tokens,
    };
    (prompt, context)
}

struct ArtifactContext {
    text: String,
    artifact_count: usize,
    artifact_chars: usize,
    truncated: bool,
}

impl ArtifactContext {
    fn empty(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            artifact_count: 0,
            artifact_chars: 0,
            truncated: false,
        }
    }
}

impl std::fmt::Display for ArtifactContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.text)
    }
}

fn collect_artifact_context(artifact_dir: Option<&Path>, stage: Stage) -> ArtifactContext {
    let Some(artifact_dir) = artifact_dir else {
        return ArtifactContext::empty("(sem artefatos anteriores carregados pelo TUI)");
    };
    let current = Stage::ALL
        .iter()
        .position(|item| *item == stage)
        .unwrap_or(0);
    let mut context = String::new();
    let mut used = 0usize;
    let mut artifact_count = 0usize;
    let mut truncated = false;
    for (idx, item) in Stage::ALL.iter().enumerate() {
        if idx > current {
            break;
        }
        let path = artifact_dir.join(item.filename());
        if !path.exists() {
            continue;
        }
        let Ok(content) = std::fs::read_to_string(&path) else {
            continue;
        };
        let remaining = 24_000usize.saturating_sub(used);
        if remaining == 0 {
            truncated = true;
            break;
        }
        let max_chars = remaining.min(6_000);
        let was_clipped = content.chars().count() > max_chars;
        let clipped = clip_chars(&content, max_chars);
        used += clipped.chars().count();
        artifact_count += 1;
        truncated |= was_clipped;
        context.push_str(&format!(
            "\n---\nArquivo: {}\n{}\n",
            item.filename(),
            clipped
        ));
    }
    if context.trim().is_empty() {
        ArtifactContext::empty("(sem artefatos anteriores salvos ainda)")
    } else {
        ArtifactContext {
            text: context,
            artifact_count,
            artifact_chars: used,
            truncated,
        }
    }
}

pub(crate) fn required_sections_for(stage: Stage) -> &'static [&'static str] {
    match stage {
        Stage::Idea => &[
            "Rastreabilidade",
            "Problema",
            "Objetivo",
            "Usuários",
            "Critérios de sucesso",
        ],
        Stage::Prd => &[
            "Rastreabilidade",
            "Resumo",
            "Objetivos",
            "Não objetivos",
            "Usuários",
            "Requisitos",
            "Critérios de aceite",
            "Métricas",
            "Riscos",
        ],
        Stage::Techspec => &[
            "Rastreabilidade",
            "Visão técnica",
            "Arquitetura",
            "Contratos",
            "Dados",
            "Segurança",
            "Performance",
            "Observabilidade",
            "Testes",
            "Riscos",
            "Plano de implementação",
        ],
        Stage::Tasks => &[
            "Rastreabilidade",
            "Backlog",
            "Dependências",
            "Critérios de aceite",
            "Ordem sugerida",
        ],
        Stage::Refinement => &[
            "Rastreabilidade",
            "Resumo",
            "Solução proposta",
            "Pontos de observação",
            "Checklist",
            "Subtasks",
            "Definition of Done",
            "Flags e configurações",
        ],
        Stage::Execution => &[
            "Rastreabilidade",
            "Tarefa",
            "Resumo da implementação",
            "Arquivos alterados",
            "Testes e evidências",
            "Riscos e pendências",
        ],
        Stage::Adr => &[
            "Rastreabilidade",
            "Status",
            "Contexto",
            "Problema",
            "Decisão",
            "Alternativas consideradas",
            "Consequências",
            "Impactos e riscos",
            "Plano de adoção",
            "Plano de reversão",
            "Evidências",
            "Histórico de revisão",
        ],
        Stage::Review => &[
            "Rastreabilidade",
            "Escopo revisado",
            "Evidências",
            "Achados",
            "Testes",
            "Veredito",
        ],
        Stage::Memory => &[
            "Estado final",
            "Rastreabilidade",
            "Decisões",
            "Padrões úteis",
            "Testes e comandos",
            "Pendências",
            "Contexto para o próximo agente",
        ],
    }
}

fn temp_output_path(stage: Stage) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "sdd-tui-{}-{}-{nanos}.md",
        std::process::id(),
        stage.key()
    ))
}

fn strip_wrapping_markdown_fence(input: &str) -> String {
    let trimmed = input.trim();
    let mut lines: Vec<&str> = trimmed.lines().collect();
    if lines.len() >= 2
        && lines.first().is_some_and(|line| {
            let line = line.trim();
            line == "```" || line.eq_ignore_ascii_case("```markdown") || line == "```md"
        })
        && lines.last().is_some_and(|line| line.trim() == "```")
    {
        lines.remove(0);
        lines.pop();
        return ensure_trailing_newline(lines.join("\n").trim());
    }
    ensure_trailing_newline(trimmed)
}

fn ensure_trailing_newline(input: &str) -> String {
    let mut out = input.to_string();
    if !out.ends_with('\n') {
        out.push('\n');
    }
    out
}

fn clip_chars(input: &str, max: usize) -> String {
    if input.chars().count() <= max {
        return input.to_string();
    }
    let mut out: String = input.chars().take(max).collect();
    out.push_str("\n\n[conteudo truncado para caber no prompt]");
    out
}

fn trace_output(label: &str, bytes: &[u8]) -> Vec<String> {
    let text = String::from_utf8_lossy(bytes);
    let mut lines = text
        .lines()
        .filter(|line| !line.trim().is_empty())
        .take(4)
        .map(|line| format!("{label}: {}", redact_text(line)))
        .collect::<Vec<_>>();
    if text.lines().filter(|line| !line.trim().is_empty()).count() > 4 {
        lines.push(format!("{label}: ..."));
    }
    lines
}

fn combined_output(stdout: &[u8], stderr: &[u8]) -> String {
    let stdout = String::from_utf8_lossy(stdout);
    let stderr = String::from_utf8_lossy(stderr);
    let combined = format!("stdout:\n{}\n\nstderr:\n{}", stdout.trim(), stderr.trim());
    redact_text(&combined)
}

/// Dispara UM agente para uma task específica em thread dedicada.
/// Reutiliza o motor existente (codex/deterministic).
/// O fragmento gerado é gravado em `fragment_path`.
pub fn spawn_parallel_agent(
    exe: PathBuf,
    stage: Stage,
    orchestration_name: String,
    task: crate::tui::parallel::TaskRef,
    root: PathBuf,
    fragment_path: PathBuf,
    options: StageRunOptions,
) -> Receiver<StageEvent> {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let start = Instant::now();
        let result = if let Some(adapter) = direct_generation_adapter(&options) {
            match adapter {
                DirectAdapter::Codex => run_codex_parallel_fragment(
                    stage,
                    &orchestration_name,
                    &task,
                    &root,
                    &fragment_path,
                    &options,
                    start,
                    &tx,
                ),
                _ => run_cli_parallel_fragment(
                    adapter,
                    stage,
                    &orchestration_name,
                    &task,
                    &root,
                    &fragment_path,
                    &options,
                    start,
                    &tx,
                ),
            }
        } else {
            run_deterministic_parallel_fragment(
                &exe,
                stage,
                &orchestration_name,
                &task,
                &root,
                &fragment_path,
                &options,
                start,
                &tx,
            )
        };

        let _ = tx.send(StageEvent::Finished(result));
    });
    rx
}

/// Dispara UMA task real de Execution. Diferente da geração de artefato, este
/// caminho autoriza escrita no workspace para o provider que suporta isso.
pub fn spawn_execution_task(
    stage: Stage,
    orchestration_name: String,
    task: ExecutionTask,
    root: PathBuf,
    report_path: PathBuf,
    options: StageRunOptions,
) -> Receiver<StageEvent> {
    let (tx, rx) = mpsc::channel();
    thread::spawn(move || {
        let start = Instant::now();
        let provider = options.provider.as_deref().unwrap_or("auto");
        let result = if provider_supports_workspace_execution(provider) {
            match DirectAdapter::for_provider(provider) {
                Some(DirectAdapter::Codex) => run_codex_execution_task(
                    stage,
                    &orchestration_name,
                    &task,
                    &root,
                    &report_path,
                    &options,
                    start,
                    &tx,
                ),
                Some(adapter) => run_cli_workspace_execution_task(
                    adapter,
                    stage,
                    &orchestration_name,
                    &task,
                    &root,
                    &report_path,
                    &options,
                    start,
                    &tx,
                ),
                None => run_unsupported_execution_task(
                    stage,
                    &orchestration_name,
                    &task,
                    &root,
                    &options,
                    start,
                    &tx,
                ),
            }
        } else {
            run_unsupported_execution_task(
                stage,
                &orchestration_name,
                &task,
                &root,
                &options,
                start,
                &tx,
            )
        };
        let _ = tx.send(StageEvent::Finished(result));
    });
    rx
}

#[allow(clippy::too_many_arguments)]
fn run_codex_execution_task(
    stage: Stage,
    orchestration_name: &str,
    task: &ExecutionTask,
    root: &Path,
    report_path: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = Vec::new();
    let agent = AgentProvenance::from_adapter(DirectAdapter::Codex, "workspace-execution-adapter");
    let requested_model = ModelProvenance::from_options(options, None, "codex-jsonl-unreported");
    record_execution_trace_event(
        root,
        orchestration_name,
        stage,
        task,
        "started",
        options.provider.as_deref().unwrap_or("codex"),
        start.elapsed().as_millis(),
        None,
        None,
        &agent,
        &requested_model,
        None,
    );
    for item in [
        "provider selecionado: codex".to_string(),
        "adapter: codex exec JSONL para execução real de task".to_string(),
        format!("stage: {}", stage.key()),
        format!("task: {} — {}", task.id, task.title),
        "sandbox: workspace-write".to_string(),
    ] {
        push_trace(&mut trace, tx, item);
    }
    prepare_codex_skill_surface(root, &mut trace, tx);

    if let Some(parent) = report_path.parent() {
        if let Err(e) = fs::create_dir_all(parent) {
            record_execution_trace_event(
                root,
                orchestration_name,
                stage,
                task,
                "error",
                "codex",
                start.elapsed().as_millis(),
                None,
                None,
                &agent,
                &requested_model,
                Some(&format!(
                    "falha ao criar diretório do relatório da task: {e}"
                )),
            );
            return execution_error_result(
                stage,
                start,
                trace,
                format!("falha ao criar diretório do relatório da task: {e}"),
                TokenUsageSample::default(),
                StageContextSample::default(),
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
        }
    }

    let (prompt, context) =
        build_execution_task_prompt(stage, orchestration_name, task, root, options);
    emit_context(tx, &context);
    let prompt_usage = TokenUsageSample::from_text(&prompt, "").with_cost(options.pricing.as_ref());
    emit_usage(tx, &prompt_usage);
    push_trace(
        &mut trace,
        tx,
        format!(
            "prompt da task montado: ~{} tokens de input local",
            prompt_usage.input_tokens_estimate
        ),
    );

    let mut cmd = build_codex_exec_command(
        root,
        report_path,
        &prompt,
        options.model.as_deref(),
        options.effort.as_deref(),
        CodexSandbox::WorkspaceWrite,
    );
    push_trace(
        &mut trace,
        tx,
        redact_text(&codex_trace_command(
            root,
            report_path,
            options.model.as_deref(),
            options.effort.as_deref(),
            CodexSandbox::WorkspaceWrite,
        )),
    );

    let started = Instant::now();
    let output = match cmd.output() {
        Ok(output) => output,
        Err(e) => {
            record_execution_trace_event(
                root,
                orchestration_name,
                stage,
                task,
                "error",
                "codex",
                start.elapsed().as_millis(),
                Some(&prompt_usage),
                Some(&context),
                &agent,
                &requested_model,
                Some(&format!(
                    "falha ao iniciar codex exec para {}: {e}",
                    task.id
                )),
            );
            return execution_error_result(
                stage,
                start,
                trace,
                format!("falha ao iniciar codex exec para {}: {e}", task.id),
                prompt_usage,
                context,
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
        }
    };
    push_trace(
        &mut trace,
        tx,
        format!(
            "codex exec da task terminou com exit {} em {}ms",
            output.status.code().unwrap_or(-1),
            started.elapsed().as_millis()
        ),
    );
    let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
    let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
    for line in stdout.lines().filter_map(codex_event_trace_line).take(24) {
        push_trace_once(&mut trace, line);
    }
    for item in trace_output("codex stderr", &output.stderr) {
        push_trace_once(&mut trace, item);
    }
    let usage = reported_usage_from_jsonl(output.stdout.as_slice())
        .map(|usage| usage.with_cost(options.pricing.as_ref()))
        .unwrap_or(prompt_usage);
    let model = ModelProvenance::from_options(
        options,
        observed_model_from_jsonl(output.stdout.as_slice()),
        "codex-jsonl",
    );
    emit_usage(tx, &usage);

    if !output.status.success() {
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "failed",
            "codex",
            start.elapsed().as_millis(),
            Some(&usage),
            Some(&context),
            &agent,
            &model,
            Some(&format!(
                "codex exec falhou na task {} com exit {}",
                task.id,
                output.status.code().unwrap_or(-1)
            )),
        );
        return StageResult {
            stage,
            exit_code: output.status.code().unwrap_or(-1),
            stdout,
            stderr: format!(
                "codex exec falhou na task {}:\n{}",
                task.id,
                combined_output(output.stdout.as_slice(), output.stderr.as_slice())
            ),
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent,
            model: model.clone(),
            usage,
            context,
        };
    }

    let report = match fs::read_to_string(report_path) {
        Ok(content) => strip_wrapping_markdown_fence(&content),
        Err(e) => {
            record_execution_trace_event(
                root,
                orchestration_name,
                stage,
                task,
                "error",
                "codex",
                start.elapsed().as_millis(),
                Some(&usage),
                Some(&context),
                &agent,
                &model,
                Some(&format!(
                    "codex exec terminou, mas não gravou relatório da task em {}: {e}",
                    report_path.display()
                )),
            );
            return execution_error_result(
                stage,
                start,
                trace,
                format!(
                    "codex exec terminou, mas não gravou relatório da task em {}: {e}",
                    report_path.display()
                ),
                usage,
                context,
                ResultProvenance { agent, model },
            );
        }
    };
    if report.trim().is_empty() {
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "error",
            "codex",
            start.elapsed().as_millis(),
            Some(&usage),
            Some(&context),
            &agent,
            &model,
            Some(&format!(
                "codex exec gerou relatório vazio para {}",
                task.id
            )),
        );
        return execution_error_result(
            stage,
            start,
            trace,
            format!("codex exec gerou relatório vazio para {}", task.id),
            usage,
            context,
            ResultProvenance { agent, model },
        );
    }
    if let Err(e) = fs::write(report_path, ensure_trailing_newline(&report)) {
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "error",
            "codex",
            start.elapsed().as_millis(),
            Some(&usage),
            Some(&context),
            &agent,
            &model,
            Some(&format!(
                "falha ao normalizar relatório da task {}: {e}",
                task.id
            )),
        );
        return execution_error_result(
            stage,
            start,
            trace,
            format!("falha ao normalizar relatório da task {}: {e}", task.id),
            usage,
            context,
            ResultProvenance { agent, model },
        );
    }
    push_trace(
        &mut trace,
        tx,
        format!(
            "relatório da task {} gravado: {} bytes",
            task.id,
            report.len()
        ),
    );
    record_execution_trace_event(
        root,
        orchestration_name,
        stage,
        task,
        "succeeded",
        "codex",
        start.elapsed().as_millis(),
        Some(&usage),
        Some(&context),
        &agent,
        &model,
        None,
    );

    StageResult {
        stage,
        exit_code: output.status.code().unwrap_or(0),
        stdout,
        stderr,
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent,
        model,
        usage,
        context,
    }
}

#[allow(clippy::too_many_arguments)]
fn run_cli_workspace_execution_task(
    adapter: DirectAdapter,
    stage: Stage,
    orchestration_name: &str,
    task: &ExecutionTask,
    root: &Path,
    report_path: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = Vec::new();
    let agent = AgentProvenance::from_adapter(adapter, "workspace-execution-adapter");
    let requested_model = ModelProvenance::from_options(options, None, "adapter-output-unreported");
    record_execution_trace_event(
        root,
        orchestration_name,
        stage,
        task,
        "started",
        adapter.provider(),
        start.elapsed().as_millis(),
        None,
        None,
        &agent,
        &requested_model,
        None,
    );
    for item in [
        format!("provider selecionado: {}", adapter.provider()),
        format!("adapter: {} para execução real de task", adapter.label()),
        format!("stage: {}", stage.key()),
        format!("task: {} — {}", task.id, task.title),
        "modo: workspace-write".to_string(),
    ] {
        push_trace(&mut trace, tx, item);
    }

    if let Err(message) = ensure_adapter_ready(adapter) {
        push_trace(&mut trace, tx, message.clone());
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "failed",
            adapter.provider(),
            start.elapsed().as_millis(),
            None,
            None,
            &agent,
            &requested_model,
            Some(&message),
        );
        return execution_error_result(
            stage,
            start,
            trace,
            format!("{} — task {}", message, task.id),
            TokenUsageSample::default(),
            StageContextSample::default(),
            ResultProvenance {
                agent,
                model: requested_model,
            },
        );
    }

    if let Some(parent) = report_path.parent() {
        if let Err(e) = fs::create_dir_all(parent) {
            let message = format!("falha ao criar diretório do relatório da task: {e}");
            record_execution_trace_event(
                root,
                orchestration_name,
                stage,
                task,
                "error",
                adapter.provider(),
                start.elapsed().as_millis(),
                None,
                None,
                &agent,
                &requested_model,
                Some(&message),
            );
            return execution_error_result(
                stage,
                start,
                trace,
                message,
                TokenUsageSample::default(),
                StageContextSample::default(),
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
        }
    }

    let (prompt, context) =
        build_execution_task_prompt(stage, orchestration_name, task, root, options);
    emit_context(tx, &context);
    let prompt_usage = TokenUsageSample::from_text(&prompt, "").with_cost(options.pricing.as_ref());
    emit_usage(tx, &prompt_usage);
    push_trace(
        &mut trace,
        tx,
        format!(
            "prompt da task montado: ~{} tokens de input local",
            prompt_usage.input_tokens_estimate
        ),
    );

    let mut cmd = build_cli_text_command(
        adapter,
        root,
        &prompt,
        options.model.as_deref(),
        options.effort.as_deref(),
        CliPermissionMode::WorkspaceWrite,
    );
    push_trace(
        &mut trace,
        tx,
        cli_text_trace_command(
            adapter,
            root,
            options.model.as_deref(),
            options.effort.as_deref(),
        ),
    );

    let started = Instant::now();
    let output = match cmd.output() {
        Ok(output) => output,
        Err(e) => {
            let message = format!(
                "falha ao iniciar {} para {}: {e}. Se estiver deslogado, rode `{}`.",
                adapter.command(),
                task.id,
                adapter.login_command()
            );
            record_execution_trace_event(
                root,
                orchestration_name,
                stage,
                task,
                "error",
                adapter.provider(),
                start.elapsed().as_millis(),
                Some(&prompt_usage),
                Some(&context),
                &agent,
                &requested_model,
                Some(&message),
            );
            return execution_error_result(
                stage,
                start,
                trace,
                message,
                prompt_usage,
                context,
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
        }
    };
    let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
    let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
    push_trace(
        &mut trace,
        tx,
        format!(
            "{} da task terminou com exit {} em {}ms",
            adapter.command(),
            output.status.code().unwrap_or(-1),
            started.elapsed().as_millis()
        ),
    );
    for item in trace_output(adapter.command(), &output.stderr) {
        push_trace_once(&mut trace, item);
    }
    let usage = TokenUsageSample::from_text(&prompt, &stdout).with_cost(options.pricing.as_ref());
    let model = ModelProvenance::from_options(options, None, "adapter-output-unreported");
    emit_usage(tx, &usage);

    if !output.status.success() {
        let message = format!(
            "{} falhou na task {} com exit {}",
            adapter.command(),
            task.id,
            output.status.code().unwrap_or(-1)
        );
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "failed",
            adapter.provider(),
            start.elapsed().as_millis(),
            Some(&usage),
            Some(&context),
            &agent,
            &model,
            Some(&message),
        );
        return StageResult {
            stage,
            exit_code: output.status.code().unwrap_or(-1),
            stdout,
            stderr: format!(
                "{}:\n{}",
                message,
                combined_output(output.stdout.as_slice(), output.stderr.as_slice())
            ),
            duration_ms: start.elapsed().as_millis(),
            trace,
            agent,
            model,
            usage,
            context,
        };
    }

    let report = strip_wrapping_markdown_fence(&stdout);
    if report.trim().is_empty() {
        let message = format!(
            "{} gerou relatório vazio para {}",
            adapter.command(),
            task.id
        );
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "error",
            adapter.provider(),
            start.elapsed().as_millis(),
            Some(&usage),
            Some(&context),
            &agent,
            &model,
            Some(&message),
        );
        return execution_error_result(
            stage,
            start,
            trace,
            message,
            usage,
            context,
            ResultProvenance { agent, model },
        );
    }
    if let Err(e) = fs::write(report_path, ensure_trailing_newline(&report)) {
        let message = format!("falha ao gravar relatório da task {}: {e}", task.id);
        record_execution_trace_event(
            root,
            orchestration_name,
            stage,
            task,
            "error",
            adapter.provider(),
            start.elapsed().as_millis(),
            Some(&usage),
            Some(&context),
            &agent,
            &model,
            Some(&message),
        );
        return execution_error_result(
            stage,
            start,
            trace,
            message,
            usage,
            context,
            ResultProvenance { agent, model },
        );
    }
    push_trace(
        &mut trace,
        tx,
        format!(
            "relatório da task {} gravado: {} bytes",
            task.id,
            report.len()
        ),
    );
    record_execution_trace_event(
        root,
        orchestration_name,
        stage,
        task,
        "succeeded",
        adapter.provider(),
        start.elapsed().as_millis(),
        Some(&usage),
        Some(&context),
        &agent,
        &model,
        None,
    );

    StageResult {
        stage,
        exit_code: output.status.code().unwrap_or(0),
        stdout,
        stderr,
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent,
        model,
        usage,
        context,
    }
}

fn run_unsupported_execution_task(
    stage: Stage,
    orchestration_name: &str,
    task: &ExecutionTask,
    root: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let provider = options.provider.as_deref().unwrap_or("auto");
    let message = format!(
        "execução real de tasks ainda não é suportada para provider `{provider}`; selecione um provider com adapter de escrita"
    );
    emit_trace(tx, message.clone());
    let agent = AgentProvenance::unsupported(options, "workspace-execution-blocked");
    let model = ModelProvenance::from_options(options, None, "execution-adapter-unreported");
    record_execution_trace_event(
        root,
        orchestration_name,
        stage,
        task,
        "failed",
        provider,
        start.elapsed().as_millis(),
        None,
        None,
        &agent,
        &model,
        Some(&message),
    );
    StageResult {
        stage,
        exit_code: -1,
        stdout: String::new(),
        stderr: format!("{} — task {}", message, task.id),
        duration_ms: start.elapsed().as_millis(),
        trace: vec![message],
        agent,
        model,
        usage: TokenUsageSample::from_text(&execution_task_input(task), "")
            .with_cost(options.pricing.as_ref()),
        context: StageContextSample::default(),
    }
}

#[allow(clippy::too_many_arguments)]
fn record_execution_trace_event(
    root: &Path,
    orchestration_name: &str,
    stage: Stage,
    task: &ExecutionTask,
    status: &str,
    provider: &str,
    duration_ms: u128,
    usage: Option<&TokenUsageSample>,
    context: Option<&StageContextSample>,
    agent_provenance: &AgentProvenance,
    model: &ModelProvenance,
    error: Option<&str>,
) {
    let run_id = crate::runtime::trace::make_run_id(&format!(
        "{}:{}:{}",
        orchestration_name,
        stage.key(),
        task.id
    ));
    let payload = json!({
        "schema_version": crate::runtime::trace::TRACE_SCHEMA_VERSION,
        "event_id": crate::runtime::trace::make_event_id(&format!(
            "{}:{}:{}:{}:{}",
            crate::now_public(),
            orchestration_name,
            stage.key(),
            task.id,
            status
        )),
        "ts": crate::now_public(),
        "run_id": run_id,
        "orchestration": orchestration_name,
        "stage": stage.key(),
        "task_id": task.id,
        "provider": provider,
        "agent": agent_provenance.agent_id_option(),
        "kind": "execution_task",
        "status": status,
        "duration_ms": duration_ms,
        "usage": usage,
        "context": context,
        "model": model,
        "error": error,
        "agent_provenance": agent_provenance,
        "title": task.title,
        "independent": task.independent,
        "dependencies": task.dependencies
    });
    let _ = crate::runtime::trace::record_event(root, "runs.jsonl", payload);
}

#[allow(clippy::too_many_arguments)]
fn run_codex_parallel_fragment(
    stage: Stage,
    orchestration_name: &str,
    task: &crate::tui::parallel::TaskRef,
    root: &Path,
    fragment_path: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = Vec::new();
    let agent = AgentProvenance::from_adapter(DirectAdapter::Codex, "parallel-direct-adapter");
    let requested_model = ModelProvenance::from_options(options, None, "codex-jsonl-unreported");
    for item in [
        "provider selecionado: codex".to_string(),
        "adapter: codex exec JSONL para fragmento paralelo".to_string(),
        format!("stage: {}", stage.key()),
        format!("agent slot: {}", task.id),
    ] {
        push_trace(&mut trace, tx, item);
    }
    prepare_codex_skill_surface(root, &mut trace, tx);

    if let Some(parent) = fragment_path.parent() {
        if let Err(e) = fs::create_dir_all(parent) {
            push_trace(
                &mut trace,
                tx,
                format!("falha ao criar diretório do fragmento paralelo: {e}"),
            );
            return parallel_error_result(
                stage,
                start,
                trace,
                format!("falha ao criar {}: {e}", parent.display()),
                TokenUsageSample::default(),
                StageContextSample::default(),
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
        }
    }

    let (prompt, context) =
        build_parallel_provider_prompt(stage, orchestration_name, task, root, options);
    emit_context(tx, &context);
    let prompt_usage = TokenUsageSample::from_text(&prompt, "").with_cost(options.pricing.as_ref());
    emit_usage(tx, &prompt_usage);
    push_trace(
        &mut trace,
        tx,
        format!(
            "prompt paralelo montado: ~{} tokens de input local",
            prompt_usage.input_tokens_estimate
        ),
    );

    let mut cmd = build_codex_exec_command(
        root,
        fragment_path,
        &prompt,
        options.model.as_deref(),
        options.effort.as_deref(),
        CodexSandbox::ReadOnly,
    );
    push_trace(
        &mut trace,
        tx,
        redact_text(&codex_trace_command(
            root,
            fragment_path,
            options.model.as_deref(),
            options.effort.as_deref(),
            CodexSandbox::ReadOnly,
        )),
    );

    match cmd.output() {
        Ok(out) if out.status.success() => {
            let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
            let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
            let generated = match fs::read_to_string(fragment_path) {
                Ok(content) => strip_wrapping_markdown_fence(&content),
                Err(e) => {
                    return parallel_error_result(
                        stage,
                        start,
                        trace,
                        format!(
                            "codex exec não gravou fragmento {}: {e}",
                            fragment_path.display()
                        ),
                        prompt_usage,
                        context,
                        ResultProvenance {
                            agent,
                            model: requested_model,
                        },
                    );
                }
            };
            if let Err(e) = fs::write(fragment_path, ensure_trailing_newline(&generated)) {
                return parallel_error_result(
                    stage,
                    start,
                    trace,
                    format!(
                        "falha ao normalizar fragmento {}: {e}",
                        fragment_path.display()
                    ),
                    prompt_usage,
                    context,
                    ResultProvenance {
                        agent,
                        model: requested_model,
                    },
                );
            }
            let usage = reported_usage_from_jsonl(out.stdout.as_slice())
                .map(|usage| usage.with_cost(options.pricing.as_ref()))
                .unwrap_or_else(|| {
                    TokenUsageSample::from_text(&prompt, &generated)
                        .with_cost(options.pricing.as_ref())
                });
            let model = ModelProvenance::from_options(
                options,
                observed_model_from_jsonl(out.stdout.as_slice()),
                "codex-jsonl",
            );
            emit_usage(tx, &usage);
            push_trace(
                &mut trace,
                tx,
                format!(
                    "fragmento paralelo {} gravado: {} bytes",
                    task.id,
                    generated.len()
                ),
            );
            StageResult {
                stage,
                exit_code: out.status.code().unwrap_or(0),
                stdout,
                stderr,
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent,
                model,
                usage,
                context,
            }
        }
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
            let mut result = parallel_error_result(
                stage,
                start,
                trace,
                format!(
                    "codex exec falhou no AgentSlot {}:\n{}",
                    task.id,
                    combined_output(out.stdout.as_slice(), out.stderr.as_slice())
                ),
                reported_usage_from_jsonl(out.stdout.as_slice())
                    .map(|usage| usage.with_cost(options.pricing.as_ref()))
                    .unwrap_or(prompt_usage),
                context,
                ResultProvenance {
                    agent,
                    model: ModelProvenance::from_options(options, None, "codex-jsonl-unreported"),
                },
            );
            result.stdout = stdout;
            result.exit_code = out.status.code().unwrap_or(-1);
            result
        }
        Err(e) => parallel_error_result(
            stage,
            start,
            trace,
            format!("falha ao iniciar codex exec no AgentSlot {}: {e}", task.id),
            prompt_usage,
            context,
            ResultProvenance {
                agent,
                model: requested_model,
            },
        ),
    }
}

#[allow(clippy::too_many_arguments)]
fn run_cli_parallel_fragment(
    adapter: DirectAdapter,
    stage: Stage,
    orchestration_name: &str,
    task: &crate::tui::parallel::TaskRef,
    root: &Path,
    fragment_path: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = Vec::new();
    let agent = AgentProvenance::from_adapter(adapter, "parallel-direct-adapter");
    let requested_model = ModelProvenance::from_options(options, None, "adapter-output-unreported");
    for item in [
        format!("provider selecionado: {}", adapter.provider()),
        format!("adapter: {} para fragmento paralelo", adapter.label()),
        format!("stage: {}", stage.key()),
        format!("agent slot: {}", task.id),
    ] {
        push_trace(&mut trace, tx, item);
    }
    if let Err(message) = ensure_adapter_ready(adapter) {
        push_trace(&mut trace, tx, message.clone());
        return parallel_error_result(
            stage,
            start,
            trace,
            message,
            TokenUsageSample::default(),
            StageContextSample::default(),
            ResultProvenance {
                agent,
                model: requested_model,
            },
        );
    }
    if let Some(parent) = fragment_path.parent() {
        if let Err(e) = fs::create_dir_all(parent) {
            return parallel_error_result(
                stage,
                start,
                trace,
                format!("falha ao criar {}: {e}", parent.display()),
                TokenUsageSample::default(),
                StageContextSample::default(),
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
        }
    }

    let (prompt, context) =
        build_parallel_provider_prompt(stage, orchestration_name, task, root, options);
    emit_context(tx, &context);
    let prompt_usage = TokenUsageSample::from_text(&prompt, "").with_cost(options.pricing.as_ref());
    emit_usage(tx, &prompt_usage);
    push_trace(
        &mut trace,
        tx,
        format!(
            "prompt paralelo montado: ~{} tokens de input local",
            prompt_usage.input_tokens_estimate
        ),
    );

    let mut cmd = build_cli_text_command(
        adapter,
        root,
        &prompt,
        options.model.as_deref(),
        options.effort.as_deref(),
        CliPermissionMode::ReadOnly,
    );
    push_trace(
        &mut trace,
        tx,
        cli_text_trace_command(
            adapter,
            root,
            options.model.as_deref(),
            options.effort.as_deref(),
        ),
    );
    match cmd.output() {
        Ok(out) if out.status.success() => {
            let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
            let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
            let generated = strip_wrapping_markdown_fence(&stdout);
            if generated.trim().is_empty() {
                return parallel_error_result(
                    stage,
                    start,
                    trace,
                    format!(
                        "{} gerou fragmento vazio para {}",
                        adapter.command(),
                        task.id
                    ),
                    prompt_usage,
                    context,
                    ResultProvenance {
                        agent,
                        model: requested_model,
                    },
                );
            }
            if let Err(e) = fs::write(fragment_path, ensure_trailing_newline(&generated)) {
                return parallel_error_result(
                    stage,
                    start,
                    trace,
                    format!("falha ao gravar fragmento {}: {e}", fragment_path.display()),
                    prompt_usage,
                    context,
                    ResultProvenance {
                        agent,
                        model: requested_model,
                    },
                );
            }
            let usage = TokenUsageSample::from_text(&prompt, &generated)
                .with_cost(options.pricing.as_ref());
            emit_usage(tx, &usage);
            push_trace(
                &mut trace,
                tx,
                format!(
                    "fragmento paralelo {} gravado: {} bytes",
                    task.id,
                    generated.len()
                ),
            );
            StageResult {
                stage,
                exit_code: out.status.code().unwrap_or(0),
                stdout,
                stderr,
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent,
                model: requested_model,
                usage,
                context,
            }
        }
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
            let mut result = parallel_error_result(
                stage,
                start,
                trace,
                format!(
                    "{} falhou no AgentSlot {}. Rode `{}` se a sessão estiver expirada:\n{}",
                    adapter.command(),
                    task.id,
                    adapter.login_command(),
                    combined_output(out.stdout.as_slice(), out.stderr.as_slice())
                ),
                TokenUsageSample::from_text(&prompt, &stdout).with_cost(options.pricing.as_ref()),
                context,
                ResultProvenance {
                    agent,
                    model: requested_model,
                },
            );
            result.stdout = stdout;
            result.exit_code = out.status.code().unwrap_or(-1);
            result
        }
        Err(e) => parallel_error_result(
            stage,
            start,
            trace,
            format!(
                "falha ao iniciar {} no AgentSlot {}: {e}. Se estiver deslogado, rode `{}`.",
                adapter.command(),
                task.id,
                adapter.login_command()
            ),
            prompt_usage,
            context,
            ResultProvenance {
                agent,
                model: requested_model,
            },
        ),
    }
}

#[allow(clippy::too_many_arguments)]
fn run_deterministic_parallel_fragment(
    exe: &Path,
    stage: Stage,
    orchestration_name: &str,
    task: &crate::tui::parallel::TaskRef,
    root: &Path,
    fragment_path: &Path,
    options: &StageRunOptions,
    start: Instant,
    tx: &Sender<StageEvent>,
) -> StageResult {
    let mut trace = deterministic_trace(stage, options);
    emit_trace_lines(tx, &trace);
    push_trace(
        &mut trace,
        tx,
        format!("agent slot paralelo: {} — {}", task.id, task.title),
    );
    let mut cmd = build_parallel_probe_command(exe, stage, orchestration_name, task, root, options);
    let output = match cmd.output() {
        Ok(output) => output,
        Err(e) => {
            let usage = TokenUsageSample::from_text(&parallel_task_input(task), "")
                .with_cost(options.pricing.as_ref());
            push_trace(
                &mut trace,
                tx,
                format!("falha ao iniciar probe paralelo {}: {e}", task.id),
            );
            return StageResult {
                stage,
                exit_code: -1,
                stdout: String::new(),
                stderr: format!("falha ao iniciar subprocesso para {}: {e}", task.id),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_harness(options, "sdd-parallel-harness"),
                model: ModelProvenance::from_options(options, None, "sdd-harness-unreported"),
                usage,
                context: StageContextSample::default(),
            };
        }
    };

    let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
    let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
    let exit_code = output.status.code().unwrap_or(-1);
    if output.status.success() {
        if let Some(parent) = fragment_path.parent() {
            let _ = fs::create_dir_all(parent);
        }
        let fragment = deterministic_parallel_fragment(stage, task, &stdout, &stderr);
        if let Err(e) = fs::write(fragment_path, fragment.as_bytes()) {
            push_trace(
                &mut trace,
                tx,
                format!("falha ao gravar fragmento paralelo {}: {e}", task.id),
            );
            return StageResult {
                stage,
                exit_code: -1,
                stdout,
                stderr: format!("falha ao gravar fragmento {}: {e}", fragment_path.display()),
                duration_ms: start.elapsed().as_millis(),
                trace,
                agent: AgentProvenance::from_harness(options, "sdd-parallel-harness"),
                model: ModelProvenance::from_options(options, None, "sdd-harness-unreported"),
                usage: TokenUsageSample::from_text(&parallel_task_input(task), "")
                    .with_cost(options.pricing.as_ref()),
                context: StageContextSample::default(),
            };
        }
        push_trace(
            &mut trace,
            tx,
            format!(
                "fragmento paralelo {} gravado em {}",
                task.id,
                fragment_path.display()
            ),
        );
    } else {
        push_trace(
            &mut trace,
            tx,
            format!("probe paralelo {} terminou com exit {exit_code}", task.id),
        );
    }
    let fragment_or_output = if output.status.success() {
        fs::read_to_string(fragment_path).unwrap_or_default()
    } else {
        format!("{stdout}\n{stderr}")
    };
    let usage = TokenUsageSample::from_text(&parallel_task_input(task), &fragment_or_output)
        .with_cost(options.pricing.as_ref());
    emit_usage(tx, &usage);

    StageResult {
        stage,
        exit_code,
        stdout,
        stderr,
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent: AgentProvenance::from_harness(options, "sdd-parallel-harness"),
        model: ModelProvenance::from_options(options, None, "sdd-harness-unreported"),
        usage,
        context: StageContextSample::default(),
    }
}

fn build_parallel_probe_command(
    exe: &Path,
    stage: Stage,
    name: &str,
    task: &crate::tui::parallel::TaskRef,
    root: &Path,
    options: &StageRunOptions,
) -> Command {
    let mut cmd = Command::new(exe);
    cmd.arg(stage.key())
        .arg("--name")
        .arg(name)
        .arg("--root")
        .arg(root);
    if let Some(provider) = options
        .provider
        .as_deref()
        .filter(|value| !value.is_empty())
    {
        cmd.arg("--provider").arg(provider);
    }
    if let Some(model) = options.model.as_deref().filter(|value| !value.is_empty()) {
        cmd.arg("--model").arg(model);
    }
    if let Some(effort) = options.effort.as_deref().filter(|value| !value.is_empty()) {
        cmd.arg("--effort").arg(effort);
    }
    if options.offline {
        cmd.arg("--offline");
    }
    cmd.arg("--dry-run").arg(parallel_task_input(task));
    cmd.stdin(Stdio::null());
    cmd
}

fn parallel_task_input(task: &crate::tui::parallel::TaskRef) -> String {
    format!("{} — {}", task.id, task.title)
}

fn execution_task_input(task: &ExecutionTask) -> String {
    format!("{} — {}\n\n{}", task.id, task.title, task.body)
}

fn deterministic_parallel_fragment(
    stage: Stage,
    task: &crate::tui::parallel::TaskRef,
    stdout: &str,
    stderr: &str,
) -> String {
    let mut out = format!(
        "#### AgentSlot {}\n\n- Etapa: `{}`\n- Task: `{}` — {}\n- Modo: fallback determinístico com probe `--dry-run`.\n",
        task.id,
        stage.key(),
        task.id,
        task.title
    );
    let stdout = stdout.trim();
    if !stdout.is_empty() {
        out.push_str(&format!("- Stdout: `{}`\n", truncate_trace(stdout, 160)));
    }
    let stderr = stderr.trim();
    if !stderr.is_empty() {
        out.push_str(&format!("- Stderr: `{}`\n", truncate_trace(stderr, 160)));
    }
    out.push('\n');
    out
}

fn build_execution_task_prompt(
    stage: Stage,
    name: &str,
    task: &ExecutionTask,
    root: &Path,
    options: &StageRunOptions,
) -> (String, StageContextSample) {
    let previous_context = collect_artifact_context(options.artifact_dir.as_deref(), stage);
    let provider_model = options.model.as_deref().unwrap_or("default");
    let provider_effort = options.effort.as_deref().unwrap_or("medium");
    let dependencies = if task.dependencies.is_empty() {
        "nenhuma dependência declarada".to_string()
    } else {
        task.dependencies.join(", ")
    };
    let prompt = format!(
        "Você é o subagent SDD de Execution executando UMA task real no workspace.\n\
Orquestração: {name}\n\
Task ID: {task_id}\n\
Task: {task_title}\n\
Dependências declaradas: {dependencies}\n\
Root do projeto: {root}\n\
Modelo: {provider_model}\n\
Effort: {provider_effort}\n\
\n\
Contrato operacional obrigatório:\n\
- Inspecione o código real antes de editar.\n\
- Altere os arquivos necessários para concluir somente esta task.\n\
- Rode validações focadas quando fizer sentido para a task e a stack do projeto.\n\
- Preserve alterações locais existentes; não reverta trabalho que você não criou.\n\
- Não gere o artefato final `06-execution.md`; a TUI fará isso após todas as tasks.\n\
- Ao final, responda somente com um relatório Markdown da task, sem cercas no entorno.\n\
- O relatório deve listar: resumo, arquivos alterados, comandos executados, evidências, decisões e pendências.\n\
\n\
Descrição completa da task:\n\
{task_body}\n\
\n\
Contexto SDD disponível:\n\
{previous_context}\n\
\n\
Execute a task agora no workspace e termine com o relatório Markdown.",
        task_id = task.id,
        task_title = task.title,
        task_body = if task.body.trim().is_empty() {
            "(sem corpo detalhado no backlog)"
        } else {
            task.body.trim()
        },
        root = root.display(),
    );
    let context = StageContextSample {
        prompt_tokens_estimate: estimate_tokens(&prompt),
        prompt_bytes: prompt.len(),
        artifact_count: previous_context.artifact_count,
        artifact_chars: previous_context.artifact_chars,
        truncated: previous_context.truncated,
        context_window_tokens: options.context_window_tokens,
    };
    (prompt, context)
}

fn build_parallel_provider_prompt(
    stage: Stage,
    name: &str,
    task: &crate::tui::parallel::TaskRef,
    root: &Path,
    options: &StageRunOptions,
) -> (String, StageContextSample) {
    let sections = required_sections_for(stage).join(", ");
    let previous_context = collect_artifact_context(options.artifact_dir.as_deref(), stage);
    let prompt = format!(
        "Você é um subagent SDD da etapa {stage_label}, executando apenas um AgentSlot.\n\
Orquestração: {name}\n\
Task ID: {task_id}\n\
Task: {task_title}\n\
\n\
Regras obrigatórias:\n\
- Responda somente com Markdown de fragmento, sem cercas no entorno.\n\
- Não altere arquivos e não execute comandos; produza apenas o fragmento deste AgentSlot.\n\
- Use PT-BR claro, com rastreabilidade, evidências, decisões e pendências da task.\n\
- O artefato consolidado final terá estas seções obrigatórias: {sections}.\n\
\n\
Root analisado: {root}\n\
Contexto existente dos artefatos SDD:\n\
{previous_context}\n\
\n\
Produza agora o fragmento do AgentSlot.",
        stage_label = stage.label(),
        task_id = task.id,
        task_title = task.title,
        root = root.display(),
    );
    let context = StageContextSample {
        prompt_tokens_estimate: estimate_tokens(&prompt),
        prompt_bytes: prompt.len(),
        artifact_count: previous_context.artifact_count,
        artifact_chars: previous_context.artifact_chars,
        truncated: previous_context.truncated,
        context_window_tokens: options.context_window_tokens,
    };
    (prompt, context)
}

fn parallel_error_result(
    stage: Stage,
    start: Instant,
    trace: Vec<String>,
    stderr: String,
    usage: TokenUsageSample,
    context: StageContextSample,
    provenance: ResultProvenance,
) -> StageResult {
    StageResult {
        stage,
        exit_code: -1,
        stdout: String::new(),
        stderr,
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent: provenance.agent,
        model: provenance.model,
        usage,
        context,
    }
}

fn execution_error_result(
    stage: Stage,
    start: Instant,
    trace: Vec<String>,
    stderr: String,
    usage: TokenUsageSample,
    context: StageContextSample,
    provenance: ResultProvenance,
) -> StageResult {
    StageResult {
        stage,
        exit_code: -1,
        stdout: String::new(),
        stderr,
        duration_ms: start.elapsed().as_millis(),
        trace,
        agent: provenance.agent,
        model: provenance.model,
        usage,
        context,
    }
}

/// Cria uma nova orquestração via `sdd init "<ideia>"` (T-09). Síncrono.
pub fn run_init(exe: &Path, root: &Path, idea: &str) -> std::io::Result<std::process::Output> {
    Command::new(exe)
        .arg("init")
        .arg(idea)
        .arg("--root")
        .arg(root)
        .stdin(Stdio::null())
        .output()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Verifica a identidade do executável de um `Command` de forma portável.
    ///
    /// Desde que os spawns resolvem o executável via `find_executable` (PATH +
    /// PATHEXT), o programa pode ser o nome cru (`claude`) ou um caminho
    /// absoluto resolvido pelo SO (`C:\...\claude.EXE`), dependendo de quais
    /// CLIs estão instaladas na máquina de teste. Comparamos o file stem,
    /// case-insensitive, para validar o contrato (qual ferramenta é invocada)
    /// sem acoplar o teste ao PATH local.
    fn assert_program_is(cmd: &Command, expected: &str) {
        let program = cmd.get_program();
        let stem = Path::new(program)
            .file_stem()
            .map(|s| s.to_string_lossy().to_lowercase())
            .unwrap_or_default();
        assert_eq!(
            stem,
            expected.to_lowercase(),
            "programa esperado '{expected}', obtido '{}'",
            program.to_string_lossy()
        );
    }

    #[test]
    fn build_command_uses_vectorized_args_no_shell() {
        let exe = PathBuf::from("/usr/bin/sdd");
        let cmd = build_stage_command(
            &exe,
            Stage::Idea,
            "Minha Feature",
            Path::new("."),
            Some("uma ideia; rm -rf /"),
            &StageRunOptions::default(),
        );
        // O programa é o exe, nunca "sh"/"bash".
        assert_eq!(cmd.get_program(), exe.as_os_str());
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_eq!(args[0], "idea");
        assert!(args.contains(&"--name".to_string()));
        assert!(args.contains(&"Minha Feature".to_string()));
        // A string maliciosa permanece como UM argumento isolado (não interpretada).
        assert!(args.contains(&"uma ideia; rm -rf /".to_string()));
        assert!(!args.iter().any(|a| a == "-c"), "não deve haver shell -c");
    }

    #[test]
    fn build_command_passes_provider_model_offline_and_force() {
        let exe = PathBuf::from("/usr/bin/sdd");
        let options = StageRunOptions {
            provider: Some("claude".to_string()),
            model: Some("claude-test".to_string()),
            effort: Some("xhigh".to_string()),
            offline: true,
            force: true,
            artifact_dir: None,
            context_window_tokens: None,
            pricing: None,
        };
        let cmd = build_stage_command(
            &exe,
            Stage::Prd,
            "Minha Feature",
            Path::new("/tmp/proj"),
            None,
            &options,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert!(args.windows(2).any(|w| w == ["--provider", "claude"]));
        assert!(args.windows(2).any(|w| w == ["--model", "claude-test"]));
        assert!(args.windows(2).any(|w| w == ["--effort", "xhigh"]));
        assert!(args.contains(&"--offline".to_string()));
        assert!(args.contains(&"--force".to_string()));
    }

    #[test]
    fn codex_command_omits_placeholder_model() {
        let cmd = build_codex_exec_command(
            Path::new("/tmp/proj"),
            Path::new("/tmp/out.md"),
            "prompt",
            Some("gpt-default"),
            Some("xhigh"),
            CodexSandbox::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "codex");
        assert_eq!(args[0], "--ask-for-approval");
        assert_eq!(args[1], "never");
        assert_eq!(args[2], "--config");
        assert_eq!(args[3], "model_reasoning_effort=\"xhigh\"");
        assert_eq!(args[4], "exec");
        assert!(args.contains(&"--json".to_string()));
        assert!(args.windows(2).any(|w| w == ["--cd", "/tmp/proj"]));
        assert!(args.windows(2).any(|w| w == ["--sandbox", "read-only"]));
        assert!(args
            .windows(2)
            .any(|w| w == ["--output-last-message", "/tmp/out.md"]));
        assert!(!args.contains(&"--model".to_string()));
    }

    #[test]
    fn codex_command_passes_real_model() {
        let cmd = build_codex_exec_command(
            Path::new("/tmp/proj"),
            Path::new("/tmp/out.md"),
            "prompt",
            Some("gpt-5.1-codex"),
            Some("high"),
            CodexSandbox::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert!(args.windows(2).any(|w| w == ["--model", "gpt-5.1-codex"]));
    }

    #[test]
    fn direct_generation_adapter_supports_non_codex_providers() {
        for provider in [
            "codex",
            "claude",
            "opencode",
            "cursor",
            "devin",
            "trae",
            "antigravity",
        ] {
            assert!(
                has_direct_generation_adapter(provider),
                "{provider} should have a direct generation adapter"
            );
        }
        assert!(has_direct_generation_adapter("gemini"));
        assert!(has_direct_generation_adapter("agy"));
        assert!(has_direct_generation_adapter("devin-cli"));
        assert!(has_direct_generation_adapter("trae-cli"));
        assert!(has_direct_generation_adapter("trae-agent"));
        assert!(!has_direct_generation_adapter("custom"));
    }

    #[test]
    fn workspace_execution_capability_is_explicit() {
        for provider in [
            "codex",
            "claude",
            "opencode",
            "cursor",
            "devin",
            "trae",
            "antigravity",
        ] {
            let capabilities = adapter_capabilities(provider);
            assert!(
                capabilities.workspace_execution,
                "{provider} should have an explicit workspace execution adapter"
            );
            assert!(provider_supports_workspace_execution(provider));
        }
        assert!(!provider_supports_workspace_execution("custom"));
        assert!(!adapter_capabilities("custom").workspace_execution);
    }

    #[test]
    fn claude_text_command_uses_print_plan_mode() {
        let cmd = build_cli_text_command(
            DirectAdapter::Claude,
            Path::new("/tmp/proj"),
            "prompt",
            Some("claude-sonnet-4-6"),
            Some("xhigh"),
            CliPermissionMode::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "claude");
        assert!(args.contains(&"--print".to_string()));
        assert!(args.windows(2).any(|w| w == ["--permission-mode", "plan"]));
        assert!(args
            .windows(2)
            .any(|w| w == ["--model", "claude-sonnet-4-6"]));
        assert!(args.windows(2).any(|w| w == ["--effort", "xhigh"]));
    }

    #[test]
    fn cursor_text_command_uses_agent_print_and_trust() {
        let cmd = build_cli_text_command(
            DirectAdapter::Cursor,
            Path::new("/tmp/proj"),
            "prompt",
            Some("composer-2.5"),
            None,
            CliPermissionMode::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "cursor");
        assert_eq!(args[0], "agent");
        assert!(args.contains(&"--print".to_string()));
        assert!(args.contains(&"--trust".to_string()));
        assert!(args.windows(2).any(|w| w == ["--mode", "plan"]));
        assert!(args.windows(2).any(|w| w == ["--model", "composer-2.5"]));
    }

    #[test]
    fn opencode_text_command_uses_run_without_workspace_write_flag() {
        let cmd = build_cli_text_command(
            DirectAdapter::Opencode,
            Path::new("/tmp/proj"),
            "prompt",
            Some("opencode-go/deepseek-v4-pro"),
            Some("xhigh"),
            CliPermissionMode::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "opencode");
        assert_eq!(args[0], "run");
        assert!(args.windows(2).any(|w| w == ["--format", "default"]));
        assert!(args.windows(2).any(|w| w == ["--dir", "/tmp/proj"]));
        assert!(args
            .windows(2)
            .any(|w| w == ["--model", "opencode-go/deepseek-v4-pro"]));
        assert!(args.windows(2).any(|w| w == ["--variant", "max"]));
        assert!(!args.contains(&"--dangerously-skip-permissions".to_string()));
    }

    #[test]
    fn devin_text_command_uses_print_plan_prompt() {
        let cmd = build_cli_text_command(
            DirectAdapter::Devin,
            Path::new("/tmp/proj"),
            "prompt",
            Some("sonnet"),
            Some("xhigh"),
            CliPermissionMode::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "devin");
        assert!(args.contains(&"--print".to_string()));
        assert!(args
            .windows(2)
            .any(|w| w == ["--permission-mode", "normal"]));
        assert!(args.windows(2).any(|w| w == ["--model", "sonnet"]));
        assert!(args.iter().any(|arg| arg.starts_with("/plan\nprompt")));
        assert_eq!(cmd.get_current_dir(), Some(Path::new("/tmp/proj")));
    }

    #[test]
    fn trae_text_command_uses_isolated_workdir() {
        let root = Path::new("/tmp/proj");
        let cmd = build_cli_text_command(
            DirectAdapter::Trae,
            root,
            "prompt",
            Some("trae_agent_model"),
            Some("high"),
            CliPermissionMode::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "trae-cli");
        assert_eq!(args[0], "run");
        assert!(args.contains(&"prompt".to_string()));
        assert!(args
            .windows(2)
            .any(|w| w == ["--model", "trae_agent_model"]));
        let workdir = cmd.get_current_dir().expect("trae workdir");
        assert_ne!(workdir, root);
        assert!(workdir.to_string_lossy().contains("sdd-headless-trae"));
        assert!(args
            .windows(2)
            .any(|w| w[0] == "--working-dir" && w[1] == workdir.to_string_lossy()));
    }

    #[test]
    fn antigravity_text_command_uses_agy_print_contract() {
        let cmd = build_cli_text_command(
            DirectAdapter::Antigravity,
            Path::new("/tmp/proj"),
            "prompt",
            Some("claude-opus-4.6"),
            None,
            CliPermissionMode::ReadOnly,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "agy");
        assert!(args.windows(2).any(|w| w == ["--add-dir", "/tmp/proj"]));
        assert!(args.contains(&"--sandbox".to_string()));
        assert!(args.windows(2).any(|w| w == ["--model", "claude-opus-4.6"]));
        assert!(args.windows(2).any(|w| w == ["--prompt", "prompt"]));
    }

    #[test]
    fn claude_workspace_write_command_uses_accept_edits() {
        let cmd = build_cli_text_command(
            DirectAdapter::Claude,
            Path::new("/tmp/proj"),
            "prompt",
            Some("claude-sonnet-4-6"),
            None,
            CliPermissionMode::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "claude");
        assert!(args
            .windows(2)
            .any(|w| w == ["--permission-mode", "acceptEdits"]));
        assert!(args.windows(2).any(|w| w == ["--add-dir", "/tmp/proj"]));
    }

    #[test]
    fn opencode_workspace_write_command_skips_permissions() {
        let cmd = build_cli_text_command(
            DirectAdapter::Opencode,
            Path::new("/tmp/proj"),
            "prompt",
            Some("opencode-go/deepseek-v4-pro"),
            Some("high"),
            CliPermissionMode::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "opencode");
        assert!(args.windows(2).any(|w| w == ["--dir", "/tmp/proj"]));
        assert!(args.contains(&"--dangerously-skip-permissions".to_string()));
    }

    #[test]
    fn cursor_workspace_write_command_uses_force() {
        let cmd = build_cli_text_command(
            DirectAdapter::Cursor,
            Path::new("/tmp/proj"),
            "prompt",
            Some("composer-2.5"),
            None,
            CliPermissionMode::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "cursor");
        assert_eq!(args[0], "agent");
        assert!(args.contains(&"--force".to_string()));
        assert!(!args.contains(&"--mode".to_string()));
        assert!(args.windows(2).any(|w| w == ["--workspace", "/tmp/proj"]));
    }

    #[test]
    fn devin_workspace_write_command_uses_dangerous_without_plan_prefix() {
        let cmd = build_cli_text_command(
            DirectAdapter::Devin,
            Path::new("/tmp/proj"),
            "prompt",
            Some("sonnet"),
            None,
            CliPermissionMode::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "devin");
        assert!(args
            .windows(2)
            .any(|w| w == ["--permission-mode", "dangerous"]));
        assert!(args.windows(2).any(|w| w == ["--", "prompt"]));
        assert!(!args.iter().any(|arg| arg.starts_with("/plan\n")));
    }

    #[test]
    fn trae_workspace_write_command_uses_project_root() {
        let root = Path::new("/tmp/proj");
        let cmd = build_cli_text_command(
            DirectAdapter::Trae,
            root,
            "prompt",
            Some("trae_agent_model"),
            None,
            CliPermissionMode::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "trae-cli");
        assert_eq!(cmd.get_current_dir(), Some(root));
        assert!(args.windows(2).any(|w| w == ["--working-dir", "/tmp/proj"]));
    }

    #[test]
    fn antigravity_workspace_write_command_skips_permissions() {
        let cmd = build_cli_text_command(
            DirectAdapter::Antigravity,
            Path::new("/tmp/proj"),
            "prompt",
            Some("claude-opus-4.6"),
            None,
            CliPermissionMode::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert_program_is(&cmd, "agy");
        assert!(args.windows(2).any(|w| w == ["--add-dir", "/tmp/proj"]));
        assert!(args.contains(&"--dangerously-skip-permissions".to_string()));
        assert!(!args.contains(&"--sandbox".to_string()));
    }

    #[test]
    fn codex_execution_task_command_uses_workspace_write() {
        let cmd = build_codex_exec_command(
            Path::new("/tmp/proj"),
            Path::new("/tmp/report.md"),
            "prompt",
            Some("gpt-5.1-codex"),
            Some("high"),
            CodexSandbox::WorkspaceWrite,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|a| a.to_string_lossy().into_owned())
            .collect();
        assert!(args
            .windows(2)
            .any(|w| w == ["--sandbox", "workspace-write"]));
    }

    #[test]
    fn artifact_save_command_places_root_before_save() {
        let cmd = build_artifact_save_command(
            Path::new("/usr/bin/sdd"),
            Path::new("/tmp/project"),
            "Minha Feature",
            Stage::Idea,
            Path::new("/tmp/generated.md"),
            "draft",
            true,
        );
        let args: Vec<String> = cmd
            .get_args()
            .map(|arg| arg.to_string_lossy().into_owned())
            .collect();
        assert_eq!(args[0], "artifact");
        assert_eq!(args[1], "--root");
        assert_eq!(args[2], "/tmp/project");
        assert_eq!(args[3], "save");
        assert_eq!(args[4], "Minha Feature");
        assert_eq!(args[5], "idea");
        assert!(args.contains(&"--force".to_string()));
    }

    #[test]
    fn strips_wrapping_markdown_fence() {
        let markdown = strip_wrapping_markdown_fence("```markdown\n# Titulo\n\ntexto\n```\n");
        assert_eq!(markdown, "# Titulo\n\ntexto\n");
    }

    #[test]
    fn token_usage_sample_estimates_input_and_output() {
        let sample = TokenUsageSample::from_text("abcd efgh", "12345678");
        assert_eq!(sample.input_tokens_estimate, 3);
        assert_eq!(sample.output_tokens_estimate, 2);
        assert_eq!(sample.total_tokens_estimate, 5);
        assert_eq!(sample.input_bytes, 9);
        assert_eq!(sample.output_bytes, 8);
        assert!(!sample.provider_reported);
    }

    #[test]
    fn parses_codex_jsonl_usage_when_present() {
        let stdout = br#"{"type":"turn.started"}
{"type":"turn.completed","usage":{"input_tokens":1000,"cached_input_tokens":400,"output_tokens":120,"reasoning_output_tokens":30,"total_tokens":1120}}
"#;
        let sample = reported_usage_from_jsonl(stdout).unwrap();
        assert!(sample.provider_reported);
        assert_eq!(sample.input_tokens_estimate, 1000);
        assert_eq!(sample.cached_input_tokens_estimate, 400);
        assert_eq!(sample.output_tokens_estimate, 120);
        assert_eq!(sample.reasoning_output_tokens_estimate, 30);
        assert_eq!(sample.total_tokens_estimate, 1120);
    }

    #[test]
    fn parses_observed_model_from_codex_jsonl_top_level() {
        let line = r#"{"type":"turn.started","model":"gpt-5.5"}"#;
        assert_eq!(
            observed_model_from_json_line(line).as_deref(),
            Some("gpt-5.5")
        );
    }

    #[test]
    fn parses_observed_model_from_nested_provider_payload() {
        let stdout =
            br#"{"type":"session.started","payload":{"session":{"model":"claude-sonnet-4-6"}}}
{"type":"turn.completed","usage":{"input_tokens":1}}
"#;
        assert_eq!(
            observed_model_from_jsonl(stdout).as_deref(),
            Some("claude-sonnet-4-6")
        );
    }

    #[test]
    fn parses_observed_model_from_array_metadata_payload() {
        let line =
            r#"{"type":"response","data":{"choices":[{"metadata":{"actual_model":"gpt-5.5"}}]}}"#;
        assert_eq!(
            observed_model_from_json_line(line).as_deref(),
            Some("gpt-5.5")
        );
    }

    #[test]
    fn model_provenance_distinguishes_selected_from_observed() {
        let options = StageRunOptions {
            provider: Some("codex".to_string()),
            model: Some("gpt-5.5".to_string()),
            ..StageRunOptions::default()
        };
        let provenance =
            ModelProvenance::from_options(&options, Some("gpt-5.5-actual".to_string()), "jsonl");
        assert_eq!(provenance.selected_model.as_deref(), Some("gpt-5.5"));
        assert_eq!(provenance.observed_model.as_deref(), Some("gpt-5.5-actual"));
        assert_eq!(provenance.confidence, "observed");

        let unreported = ModelProvenance::from_options(&options, None, "adapter-output");
        assert_eq!(unreported.selected_model.as_deref(), Some("gpt-5.5"));
        assert_eq!(unreported.observed_model, None);
        assert_eq!(unreported.confidence, "unreported");
    }

    #[test]
    fn agent_provenance_uses_canonical_adapter_identity() {
        for (adapter, id, label, command) in [
            (DirectAdapter::Codex, "codex", "Codex CLI", "codex"),
            (
                DirectAdapter::Claude,
                "claude-code",
                "Claude Code",
                "claude",
            ),
            (DirectAdapter::Opencode, "opencode", "opencode", "opencode"),
            (
                DirectAdapter::Cursor,
                "cursor-agent",
                "Cursor Agent",
                "cursor",
            ),
            (DirectAdapter::Devin, "devin", "Devin CLI", "devin"),
            (
                DirectAdapter::Trae,
                "trae-agent",
                "Trae Agent CLI",
                "trae-cli",
            ),
            (
                DirectAdapter::Antigravity,
                "antigravity",
                "Antigravity CLI",
                "agy",
            ),
        ] {
            let agent = AgentProvenance::from_adapter(adapter, "direct-adapter");
            assert_eq!(agent.id, id);
            assert_eq!(agent.label, label);
            assert_eq!(agent.command.as_deref(), Some(command));
            assert_eq!(agent.confidence, "invoked");
        }
    }

    #[test]
    fn agent_provenance_marks_deterministic_harness() {
        let options = StageRunOptions {
            provider: Some("custom".to_string()),
            ..StageRunOptions::default()
        };
        let agent = AgentProvenance::from_harness(&options, "sdd-harness");
        assert_eq!(agent.id, "sdd-harness");
        assert_eq!(agent.provider.as_deref(), Some("custom"));
        assert_eq!(agent.command.as_deref(), Some("sdd"));
    }

    #[test]
    fn error_results_preserve_agent_and_model_provenance() {
        let options = StageRunOptions {
            provider: Some("claude".to_string()),
            model: Some("claude-sonnet-4-6".to_string()),
            ..StageRunOptions::default()
        };
        let agent = AgentProvenance::from_adapter(DirectAdapter::Claude, "test-adapter");
        let model = ModelProvenance::from_options(&options, None, "test-unreported");

        let result = parallel_error_result(
            Stage::Tasks,
            Instant::now(),
            vec!["trace".to_string()],
            "boom".to_string(),
            TokenUsageSample::default(),
            StageContextSample::default(),
            ResultProvenance {
                agent: agent.clone(),
                model: model.clone(),
            },
        );
        assert_eq!(result.agent, agent);
        assert_eq!(result.model, model);
    }

    #[test]
    fn execution_trace_event_uses_callsite_provenance() {
        let dir = tempfile::tempdir().unwrap();
        let options = StageRunOptions {
            provider: Some("claude".to_string()),
            model: Some("claude-sonnet-4-6".to_string()),
            ..StageRunOptions::default()
        };
        let agent = AgentProvenance::unsupported(&options, "workspace-execution-blocked");
        let model = ModelProvenance::from_options(&options, None, "execution-adapter-unreported");
        let task = ExecutionTask {
            id: "T-01".to_string(),
            title: "Task".to_string(),
            body: String::new(),
            dependencies: Vec::new(),
            independent: false,
        };

        record_execution_trace_event(
            dir.path(),
            "Trace Event",
            Stage::Execution,
            &task,
            "failed",
            "claude",
            7,
            None,
            None,
            &agent,
            &model,
            Some("blocked"),
        );

        let log = fs::read_to_string(dir.path().join(".sdd/runs.jsonl")).unwrap();
        let event: Value = serde_json::from_str(log.lines().next().unwrap()).unwrap();
        assert_eq!(event["agent"], "unsupported-workspace-execution");
        assert_eq!(event["metadata"]["agent_provenance"]["provider"], "claude");
        assert_eq!(event["model"]["selected_model"], "claude-sonnet-4-6");
    }

    #[test]
    fn codex_stderr_suppresses_stdin_noise() {
        assert_eq!(
            codex_stderr_trace_line("Reading additional input from stdin..."),
            None
        );
    }

    #[test]
    fn codex_stderr_compacts_skill_load_errors() {
        let line = "2026-06-06T16:08:13.215857Z ERROR codex_core::session::session: failed to load skill /Users/allan.oliveira/Documents/code/personal/test-sdd/.agents/skills/design-tokens/SKILL.md: invalid YAML: mapping values are not allowed in this context at line 2 column 71";
        let trace = codex_stderr_trace_line(line).expect("skill error should be visible");
        assert!(trace.starts_with("aviso: skill SDD design-tokens ignorada pelo Codex"));
        assert!(trace.contains("invalid YAML"));
        assert!(!trace.contains("/Users/"));
    }

    #[test]
    fn codex_stderr_reports_missing_skill_frontmatter_as_warning() {
        let line = "2026-06-06T16:08:13.216085Z ERROR codex_core::session::session: failed to load skill /Users/allan.oliveira/Documents/code/personal/test-sdd/.agents/skills/oxc-tooling/SKILL.md: missing YAML frontmatter delimited by ---";
        let trace = codex_stderr_trace_line(line).expect("skill warning should be visible");
        assert!(trace.contains("skill local oxc-tooling"));
        assert!(trace.contains("missing YAML frontmatter"));
        assert!(!trace.contains("/Users/"));
    }

    #[test]
    fn repairs_sdd_design_tokens_frontmatter_for_codex() {
        let dir = tempfile::tempdir().unwrap();
        let skill = dir.path().join(".agents/skills/design-tokens/SKILL.md");
        fs::create_dir_all(skill.parent().unwrap()).unwrap();
        fs::write(
            &skill,
            "---\nname: design-tokens\ndescription: Criar tokens SDD: cores e spacing.\n---\n\n# Design Tokens\n",
        )
        .unwrap();

        let repaired = repair_codex_skill_frontmatter(&skill).unwrap();
        assert_eq!(repaired.as_deref(), Some("design-tokens"));
        let fixed = fs::read_to_string(&skill).unwrap();
        assert!(skill_frontmatter_is_valid(&fixed));
        assert!(fixed.contains("description: \"Criar tokens SDD: cores e spacing.\""));
    }

    #[test]
    fn prepare_codex_skill_surface_repairs_sdd_and_local_missing_frontmatter() {
        let dir = tempfile::tempdir().unwrap();
        let sdd_skill = dir
            .path()
            .join(".agents/skills/orchestration-plugin/skills/design-tokens/SKILL.md");
        let local_skill = dir.path().join(".agents/skills/oxc-tooling/SKILL.md");
        fs::create_dir_all(sdd_skill.parent().unwrap()).unwrap();
        fs::create_dir_all(local_skill.parent().unwrap()).unwrap();
        fs::write(
            &sdd_skill,
            "---\nname: design-tokens\ndescription: Tokens SDD: inválido para YAML.\n---\n\n# SDD\n",
        )
        .unwrap();
        fs::write(&local_skill, "# Skill: oxc-tooling\n").unwrap();

        let (tx, rx) = mpsc::channel();
        let mut trace = Vec::new();
        prepare_codex_skill_surface(dir.path(), &mut trace, &tx);
        drop(tx);

        assert!(trace
            .iter()
            .any(|item| item.contains("1 frontmatter(s) SDD adaptado(s): design-tokens")));
        assert!(trace
            .iter()
            .any(|item| item.contains("1 frontmatter(s) local(is) adaptado(s): oxc-tooling")));
        assert!(rx.try_iter().any(
            |event| matches!(event, StageEvent::Trace(item) if item.contains("skills Codex"))
        ));
        assert!(skill_frontmatter_is_valid(
            &fs::read_to_string(&sdd_skill).unwrap()
        ));
        let fixed_local = fs::read_to_string(&local_skill).unwrap();
        assert!(skill_frontmatter_is_valid(&fixed_local));
        assert!(fixed_local.contains("name: oxc-tooling"));
        assert!(fixed_local.contains("# Skill: oxc-tooling"));
    }

    #[test]
    fn token_usage_cost_uses_cached_input_rate() {
        let pricing = ProviderModelPricing {
            currency: Some("USD".to_string()),
            input_per_million: Some(10.0),
            cached_input_per_million: Some(1.0),
            output_per_million: Some(20.0),
        };
        let sample =
            TokenUsageSample::from_provider_usage(1_000_000, 250_000, 500_000, 0, 1_500_000)
                .with_cost(Some(&pricing));
        let cost = sample.cost.unwrap();
        assert_eq!(cost.currency, "USD");
        assert!((cost.input_cost - 7.5).abs() < f64::EPSILON);
        assert!((cost.cached_input_cost - 0.25).abs() < f64::EPSILON);
        assert!((cost.output_cost - 10.0).abs() < f64::EPSILON);
        assert!((cost.total_cost - 17.75).abs() < f64::EPSILON);
    }

    #[test]
    fn spawn_stage_runs_and_delivers() {
        // Usa o binário `true` (exit 0) como stand-in determinístico.
        let exe = crate::tui::noop_success_exe();
        let rx = spawn_stage(
            exe,
            Stage::Idea,
            "x".to_string(),
            PathBuf::from("."),
            None,
            StageRunOptions::default(),
        );
        let res = loop {
            match rx.recv().expect("deve entregar evento") {
                StageEvent::Finished(result) => break result,
                StageEvent::Trace(_) | StageEvent::Usage(_) | StageEvent::Context(_) => {}
            }
        };
        assert_eq!(res.exit_code, 0);
        assert_eq!(res.stage, Stage::Idea);
    }

    #[test]
    fn spawn_parallel_agent_delivers_finished() {
        use crate::tui::parallel::TaskRef;
        // usa `true` como exe (exit 0)
        let task = TaskRef {
            id: "T-test".into(),
            title: "Test".into(),
            independent: true,
        };
        let fragment_path =
            std::env::temp_dir().join(format!("sdd-test-{}.md", std::process::id()));
        let rx = spawn_parallel_agent(
            crate::tui::noop_success_exe(),
            Stage::Execution,
            "test-orch".into(),
            task,
            std::env::temp_dir(),
            fragment_path,
            StageRunOptions::default(),
        );
        // aguarda Finished em até 5s
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
        loop {
            match rx.try_recv() {
                Ok(StageEvent::Finished(_)) => return, // sucesso
                Ok(_) => {}
                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                    panic!("channel disconnected")
                }
                Err(std::sync::mpsc::TryRecvError::Empty) => {
                    if std::time::Instant::now() > deadline {
                        panic!("timeout — Finished nao chegou em 5s");
                    }
                    std::thread::sleep(std::time::Duration::from_millis(50));
                }
            }
        }
    }

    #[test]
    fn spawn_parallel_agent_reports_non_zero_exit() {
        use crate::tui::parallel::TaskRef;
        let task = TaskRef {
            id: "T-fail".into(),
            title: "Falha".into(),
            independent: true,
        };
        let fragment_path =
            std::env::temp_dir().join(format!("sdd-test-fail-{}.md", std::process::id()));
        let rx = spawn_parallel_agent(
            PathBuf::from("/usr/bin/false"),
            Stage::Review,
            "test-orch".into(),
            task,
            std::env::temp_dir(),
            fragment_path,
            StageRunOptions::default(),
        );
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
        loop {
            match rx.try_recv() {
                Ok(StageEvent::Finished(result)) => {
                    assert_ne!(result.exit_code, 0);
                    return;
                }
                Ok(_) => {}
                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                    panic!("channel disconnected")
                }
                Err(std::sync::mpsc::TryRecvError::Empty) => {
                    if std::time::Instant::now() > deadline {
                        panic!("timeout — Finished nao chegou em 5s");
                    }
                    std::thread::sleep(std::time::Duration::from_millis(50));
                }
            }
        }
    }
}
