mod claim_facts;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifyCheck {
pub label: String,
pub run_markers: Vec<String>,
pub runners: Vec<String>,
pub non_runs: Vec<String>,
pub flags_only: bool,
}
impl VerifyCheck {
fn new(label: impl Into<String>, markers: &[&str]) -> Self {
let run_markers: Vec<String> = markers.iter().map(|m| m.to_ascii_lowercase()).collect();
Self {
label: label.into(),
runners: run_markers.clone(),
run_markers,
non_runs: Vec::new(),
flags_only: false,
}
}
fn runs_as(mut self, runners: &[&str]) -> Self {
self.runners = runners.iter().map(|r| r.to_ascii_lowercase()).collect();
self
}
fn except(mut self, non_runs: &[&str]) -> Self {
self.non_runs = non_runs.iter().map(|r| r.to_string()).collect();
self
}
fn run_by(&self, command_lc: &str) -> bool {
self.run_markers.iter().any(|m| command_lc.contains(m))
}
fn invocation(&self, command: &str) -> Option<Invocation> {
let segments = split_command(command);
let (index, words, runner) =
segments
.iter()
.enumerate()
.find_map(|(index, (segment, _))| {
let words = strip_transparent_prefix(segment);
let text = words.join(" ").to_ascii_lowercase();
let runner = self.runners.iter().find(|r| {
text == **r
|| text.starts_with(&format!("{r} "))
|| (r.ends_with('_') && text.starts_with(r.as_str()))
})?;
Some((index, words, runner.clone()))
})?;
let taken = runner.split_whitespace().count();
let args = if runner.ends_with('_') {
&words[taken - 1..]
} else {
&words[taken..]
};
if self.flags_only && args.iter().any(|a| !a.starts_with('-')) {
return None;
}
let text = words.join(" ").to_ascii_lowercase();
let non_run = self.non_runs.iter().any(|form| {
if form.starts_with('-') {
args.iter().any(|a| {
a.strip_prefix(form.as_str())
.is_some_and(|rest| rest.is_empty() || rest.starts_with('='))
})
} else {
text == *form || text.starts_with(&format!("{form} "))
}
});
let last = segments[index + 1..]
.iter()
.all(|(next, _)| next.is_empty());
let backgrounded = segments[index].1 == "&";
let skippable = segments[..index].iter().any(|(_, sep)| *sep == "||");
let evidence = last && !backgrounded && !skippable && !non_run;
let plain = evidence
&& segments[..index]
.iter()
.all(|(seg, sep)| *sep == "&&" && seg.split_whitespace().next() == Some("cd"))
&& !segments[index]
.0
.replace("2>&1", "")
.replace("1>&2", "")
.replace(">&2", "")
.contains('>');
Some(Invocation {
evidence,
plain,
normalized: command.split_whitespace().collect::<Vec<_>>().join(" "),
})
}
}
struct Invocation {
evidence: bool,
plain: bool,
normalized: String,
}
fn split_command(command: &str) -> Vec<(String, &'static str)> {
let chars: Vec<char> = command.trim().chars().collect();
let (mut out, mut cur, mut quote, mut i) = (Vec::new(), String::new(), None::<char>, 0);
while i < chars.len() {
let (c, next) = (chars[i], chars.get(i + 1).copied());
if let Some(q) = quote {
cur.push(c);
if q == '"' && c == '\\' {
if let Some(n) = next {
cur.push(n);
i += 2;
continue;
}
}
if c == q {
quote = None;
}
i += 1;
continue;
}
let sep = match (c, next) {
('\'' | '"', _) => {
quote = Some(c);
None
}
('\\', Some(n)) => {
cur.push(c);
cur.push(n);
i += 2;
continue;
}
('&', Some('&')) => Some(("&&", 2)),
('|', Some('|')) => Some(("||", 2)),
('|', Some('&')) => Some(("|", 2)),
('|', _) => Some(("|", 1)),
(';', _) => Some((";", 1)),
('\n', _) => Some(("\n", 1)),
('&', Some('>')) => None,
('&', _) if cur.ends_with('>') => None,
('&', _) => Some(("&", 1)),
_ => None,
};
match sep {
Some((sep, width)) => {
out.push((cur.trim().to_string(), sep));
cur.clear();
i += width;
}
None => {
cur.push(c);
i += 1;
}
}
}
out.push((cur.trim().to_string(), ""));
out
}
fn strip_transparent_prefix(segment: &str) -> Vec<String> {
let mut words: Vec<String> = segment.split_whitespace().map(str::to_string).collect();
loop {
match words.first().map(String::as_str) {
Some(w) if w.contains('=') && !w.starts_with('-') => {
words.remove(0);
}
Some("env" | "time" | "nice") => {
words.remove(0);
}
Some("timeout") if words.len() > 2 => {
words.drain(..2);
}
_ => return words,
}
}
}
const PYTEST_NON_RUNS: &[&str] = &[
"--collect-only",
"--co",
"--collectonly",
"--fixtures",
"--markers",
"--setup-plan",
"--version",
"--help",
"-h",
];
const CARGO_NON_RUNS: &[&str] = &[
"--no-run",
"--list",
"--help",
"-h",
"cargo nextest list",
"cargo nextest archive",
"cargo nextest show-config",
];
const GO_NON_RUNS: &[&str] = &["-c", "-list", "--list", "-n"];
const MAKE_NON_RUNS: &[&str] = &["-n", "--dry-run", "--just-print", "--recon"];
fn non_runs_for(runner: &str) -> &'static [&'static str] {
match runner.split_whitespace().next() {
Some("pytest" | "py.test" | "python" | "python3") => PYTEST_NON_RUNS,
Some("cargo") => CARGO_NON_RUNS,
Some("go") => GO_NON_RUNS,
Some("make") => MAKE_NON_RUNS,
_ => &[],
}
}
pub fn detect_checks(entries: &[String], instruction: &str) -> Vec<VerifyCheck> {
let mut checks = Vec::new();
let mut seen_pytest = false;
for e in entries {
let el = e.to_ascii_lowercase();
let is_py_test = (el.starts_with("test_") && el.ends_with(".py"))
|| el.ends_with("_test.py")
|| el == "conftest.py"
|| el == "pytest.ini";
if is_py_test && !seen_pytest {
seen_pytest = true;
checks.push(
VerifyCheck::new(
"the Python tests (`pytest` / running the test file)",
&[
"pytest",
"unittest",
"py.test",
"python -m test",
"test_",
"_test.py",
],
)
.runs_as(&[
"pytest",
"py.test",
"python -m pytest",
"python3 -m pytest",
"python -m unittest",
"python3 -m unittest",
"python test_",
"python3 test_",
])
.except(PYTEST_NON_RUNS),
);
}
match el.as_str() {
"makefile" => checks.push(
VerifyCheck::new(
"`make test` (the Makefile)",
&["make test", "make check", "make ci"],
)
.except(MAKE_NON_RUNS),
),
"justfile" => checks.push(VerifyCheck::new(
"`just test` (the justfile)",
&["just test", "just check"],
)),
"package.json" => checks.push(VerifyCheck::new(
"`npm test` (package.json)",
&[
"npm test",
"npm run test",
"yarn test",
"pnpm test",
"npm run check",
],
)),
"cargo.toml" => {
checks.push(
VerifyCheck::new("`cargo test`", &["cargo test", "cargo nextest"])
.except(CARGO_NON_RUNS),
);
}
"go.mod" => {
checks.push(VerifyCheck::new("`go test ./...`", &["go test"]).except(GO_NON_RUNS));
}
_ => {}
}
}
if let Some(cmd) = instruction_verify_command(instruction) {
let marker = cmd.to_ascii_lowercase();
let runner = split_command(&cmd)
.iter()
.rev()
.find(|(segment, _)| !segment.is_empty())
.map(|(segment, _)| strip_transparent_prefix(segment).join(" "))
.filter(|runner| !runner.is_empty())
.unwrap_or_else(|| marker.clone());
let mut named = VerifyCheck::new(
format!("the command the task says to run: `{cmd}`"),
&[marker.as_str()],
)
.runs_as(&[runner.as_str()])
.except(non_runs_for(&runner.to_ascii_lowercase()));
named.flags_only = true;
checks.push(named);
}
checks
}
fn instruction_verify_command(instruction: &str) -> Option<String> {
for line in instruction.lines() {
let ll = line.to_ascii_lowercase();
let cues = ll.contains("run ")
|| ll.contains("verify")
|| ll.contains("you can run")
|| ll.contains("to check")
|| ll.contains("to test");
if !cues {
continue;
}
if let Some(start) = line.find('`') {
if let Some(rel) = line[start + 1..].find('`') {
let cmd = line[start + 1..start + 1 + rel].trim();
if cmd.len() >= 3
&& cmd.contains(char::is_alphabetic)
&& cmd.split_whitespace().next().is_some_and(|w| {
w.chars().all(|c| c.is_alphanumeric() || "._-/".contains(c))
})
{
return Some(cmd.to_string());
}
}
}
}
None
}
pub fn unrun<'a>(checks: &'a [VerifyCheck], commands: &[String]) -> Vec<&'a VerifyCheck> {
let lc: Vec<String> = commands.iter().map(|c| c.to_ascii_lowercase()).collect();
checks
.iter()
.filter(|chk| !lc.iter().any(|cmd| chk.run_by(cmd)))
.collect()
}
pub fn verify_gate_nudge(checks: &[VerifyCheck], commands: &[String]) -> Option<String> {
let pending = unrun(checks, commands);
if pending.is_empty() {
return None;
}
let named = pending
.iter()
.map(|c| c.label.as_str())
.collect::<Vec<_>>()
.join("; ");
Some(super::workflow_guidance(format!(
"Before you finish: you have NOT run the verification this task ships — {named}. \
Do not declare the task done on an unverified solution. Run it now with run_command, \
read the output, and if it fails, FIX the code and run it again until it passes. \
Only conclude once you have seen it pass (or have proven there is nothing to run)."
)))
}
pub fn commands_from_messages(messages: &[serde_json::Value]) -> Vec<String> {
let calls = messages.iter().flat_map(|m| {
let chat = m
.get("tool_calls")
.and_then(|c| c.as_array())
.into_iter()
.flatten()
.map(|call| &call["function"]);
let responses = (m["type"] == "function_call").then_some(m);
chat.chain(responses)
});
calls
.filter(|f| f["name"].as_str() == Some("run_command"))
.filter_map(|f| {
let args = &f["arguments"];
match args.as_str() {
Some(s) => serde_json::from_str::<serde_json::Value>(s)
.ok()?
.get("command")?
.as_str()
.map(String::from),
None => args.get("command")?.as_str().map(String::from),
}
})
.collect()
}
pub fn enabled() -> bool {
!std::env::var("NEWT_SELF_VERIFY").is_ok_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "off" | "false"
)
})
}
const MAX_DEPTH: usize = 3;
const MAX_ENTRIES: usize = 10_000;
fn collect_entry_names(
root: &std::path::Path,
max_depth: usize,
budget: usize,
list: &impl Fn(&std::path::Path) -> Vec<(String, bool)>,
) -> Vec<String> {
let mut seen: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
let mut queue: std::collections::VecDeque<(std::path::PathBuf, usize)> =
std::collections::VecDeque::new();
queue.push_back((root.to_path_buf(), 0));
let mut examined = 0usize;
while let Some((dir, depth)) = queue.pop_front() {
for (name, is_dir) in list(&dir) {
examined += 1;
if examined > budget {
return seen.into_values().collect();
}
if is_dir {
let skip = crate::verify_gate::SKIP_DIRS.contains(&name.as_str());
if !skip && depth < max_depth {
queue.push_back((dir.join(&name), depth + 1));
}
}
seen.entry(name.to_ascii_lowercase()).or_insert(name);
}
}
seen.into_values().collect()
}
pub fn workspace_entries(dir: &std::path::Path) -> Vec<String> {
collect_entry_names(dir, MAX_DEPTH, MAX_ENTRIES, &|d| {
std::fs::read_dir(d)
.map(|rd| {
rd.filter_map(Result::ok)
.filter_map(|e| {
let name = e.file_name().into_string().ok()?;
let ft = e.file_type().ok()?;
Some((name, ft.is_dir() && !ft.is_symlink()))
})
.collect()
})
.unwrap_or_default()
})
}
use crate::ExecOutcome;
use content_addressable::{ContentAddressable, ContentId, RawContentId};
pub fn outcomes_enabled() -> bool {
outcomes_switch(std::env::var("NEWT_VERIFY_OUTCOMES").ok().as_deref())
}
fn outcomes_switch(value: Option<&str>) -> bool {
value.is_some_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "on" | "true"))
}
pub fn verification_gate_present(
kind: crate::BackendKind,
api: crate::OpenAiApi,
smart_harness: bool,
) -> bool {
smart_harness
|| kind == crate::BackendKind::Anthropic
|| (kind == crate::BackendKind::Openai && api == crate::OpenAiApi::ChatCompletions)
}
pub fn verification_receipt(
gate_present: bool,
self_verify: bool,
outcomes: bool,
) -> serde_json::Value {
let mode = if !gate_present || !self_verify {
"off"
} else if outcomes {
"result_aware"
} else {
"attempted"
};
let mut receipt = serde_json::json!({ "mode": mode });
if mode == "result_aware" {
receipt["repair_allowance"] = serde_json::json!(VERIFY_REPAIR_ALLOWANCE);
}
receipt
}
pub const VERIFY_REPAIR_ALLOWANCE: usize = 3;
const MAX_TREE_ENTRIES: usize = MAX_ENTRIES;
const MAX_TREE_BYTES: u64 = 64 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
enum Observed {
Exec {
command: String,
outcome: ExecOutcome,
tree: Option<ContentId>,
directory: Option<std::path::PathBuf>,
},
Write,
}
#[derive(Debug, Default, Clone)]
pub struct VerificationLedger {
entries: Vec<Observed>,
claim_directories: Vec<std::path::PathBuf>,
created_prs: Vec<String>,
pushed_branches: Vec<claim_facts::pushes::PushObservation>,
task: String,
result_aware: bool,
checks: Option<Vec<VerifyCheck>>,
}
impl VerificationLedger {
pub(crate) fn for_turn(task: &str, result_aware: bool) -> Self {
Self {
entries: Vec::new(),
task: task.to_string(),
result_aware,
checks: None,
claim_directories: Vec::new(),
created_prs: Vec::new(),
pushed_branches: Vec::new(),
}
}
pub(crate) async fn applicable_checks(
&self,
workspace: &str,
task: &str,
requested: &[String],
) -> Option<Vec<VerifyCheck>> {
let mut checks = detect_off_worker(workspace, task).await?;
let explicit = detect_checks(&[], task);
checks.retain(|check| {
explicit.contains(check)
|| requested
.iter()
.any(|command| check.invocation(command).is_some())
|| (self.result_aware
&& self.entries.iter().any(|entry| {
matches!(entry,
Observed::Exec { command, .. } if check.invocation(command).is_some())
}))
});
Some(checks)
}
async fn checks(&mut self, workspace: &str) -> &[VerifyCheck] {
if self.checks.is_none() {
self.checks = detect_off_worker(workspace, &self.task).await;
}
self.checks.as_deref().unwrap_or_default()
}
pub(crate) fn result_aware(&self) -> bool {
self.result_aware
}
pub fn record_exec(&mut self, command: &str, outcome: ExecOutcome, tree: Option<ContentId>) {
self.entries.push(Observed::Exec {
command: command.to_string(),
outcome,
tree,
directory: None,
});
}
pub fn record_write(&mut self) {
self.checks = None;
self.entries.push(Observed::Write);
}
pub(crate) async fn observe(
&mut self,
name: &str,
args: &serde_json::Value,
ok: bool,
execution: Option<ExecOutcome>,
workspace: &str,
) {
self.observe_directory(name, args, ok, workspace);
self.observe_plain_push(name, args, execution, super::tools::ocap_disabled());
match execution {
Some(outcome) => {
let command = claim_facts::observed_command(name, args);
let directory = claim_facts::command_directory(&command, args, workspace);
let plain = self.checks.as_ref().is_some_and(|checks| {
checks
.iter()
.any(|check| check.invocation(&command).is_some_and(|i| i.plain))
});
if !plain && !super::is_verification_read_command(&command) {
self.checks = None;
}
let evidence = self.result_aware
&& outcome == ExecOutcome::Passed
&& self
.checks(workspace)
.await
.iter()
.any(|check| check.invocation(&command).is_some_and(|i| i.evidence));
let tree = if evidence {
tree_state_off_worker(workspace).await
} else {
None
};
self.record_exec(&command, outcome, tree);
if let Some(Observed::Exec {
directory: observed,
..
}) = self.entries.last_mut()
{
*observed = directory;
}
}
None if super::may_change_workspace(name, args) => self.record_write(),
None => {}
}
}
pub(crate) async fn cap_exit_reason(
&self,
workspace: &str,
gate_on: bool,
round: usize,
solve_obs: Option<&mut super::observability::SolveObservation>,
) -> crate::TurnEndReason {
if !(self.result_aware && gate_on) {
return crate::TurnEndReason::RoundCap;
}
let scanned = self.applicable_checks(workspace, &self.task, &[]).await;
let (decision, report) = conclude(&Conclusion {
checks: scanned.as_deref().unwrap_or_default(),
requested: &[],
ledger: self,
tree_now: self.tree_now(workspace).await,
repairs_used: VERIFY_REPAIR_ALLOWANCE,
rounds_left: false,
});
let exhausted = decision == Decision::Stop(crate::TurnEndReason::RepairExhausted);
if let Some(obs) = solve_obs.filter(|_| exhausted || scanned.is_none()) {
obs.behavior_signals
.push(super::observability::BehaviorSignal::Verification {
round,
decision: if exhausted {
"repair_exhausted"
} else {
SCAN_FAILED
}
.to_string(),
repairs_used: VERIFY_REPAIR_ALLOWANCE,
allowance: VERIFY_REPAIR_ALLOWANCE,
report,
});
}
if exhausted {
crate::TurnEndReason::RepairExhausted
} else {
crate::TurnEndReason::RoundCap
}
}
pub(crate) async fn tree_now(&self, workspace: &str) -> Option<ContentId> {
let needed = self
.entries
.iter()
.any(|e| matches!(e, Observed::Exec { tree: Some(_), .. }));
if needed {
tree_state_off_worker(workspace).await
} else {
None
}
}
}
const SCAN_FAILED: &str = "check_scan_failed";
const NO_CHECKS: &str = "no_checks";
async fn detect_off_worker(workspace: &str, task: &str) -> Option<Vec<VerifyCheck>> {
let (root, task) = (std::path::PathBuf::from(workspace), task.to_string());
tokio::task::spawn_blocking(move || detect_checks(&workspace_entries(&root), &task))
.await
.ok()
}
async fn tree_state_off_worker(workspace: &str) -> Option<ContentId> {
let root = std::path::PathBuf::from(workspace);
tokio::task::spawn_blocking(move || workspace_tree_state(&root))
.await
.ok()
.flatten()
}
pub(crate) struct Concluding<'a> {
pub messages: &'a [serde_json::Value],
pub workspace: &'a str,
pub task: &'a str,
pub rounds_left: bool,
pub round: usize,
pub ledger: &'a VerificationLedger,
pub solve_obs: Option<&'a mut super::observability::SolveObservation>,
}
pub(crate) async fn conclude_turn(turn: Concluding<'_>, repairs_used: usize) -> Decision {
let requested = commands_from_messages(turn.messages);
let scanned = turn
.ledger
.applicable_checks(turn.workspace, turn.task, &requested)
.await;
let checks = scanned.as_deref().unwrap_or_default();
let (decision, report) = conclude(&Conclusion {
checks,
requested: &requested,
ledger: turn.ledger,
tree_now: turn.ledger.tree_now(turn.workspace).await,
repairs_used,
rounds_left: turn.rounds_left,
});
if let Some(obs) = turn.solve_obs {
obs.behavior_signals
.push(super::observability::BehaviorSignal::Verification {
round: turn.round,
decision: match &decision {
_ if scanned.is_none() => SCAN_FAILED.to_string(),
_ if checks.is_empty() => NO_CHECKS.to_string(),
Decision::Accept => "accept".to_string(),
Decision::Nudge(_) => "nudge".to_string(),
Decision::Stop(reason) => serde_json::to_value(reason)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_default(),
},
repairs_used,
allowance: VERIFY_REPAIR_ALLOWANCE,
report,
});
}
decision
}
pub struct Conclusion<'a> {
pub checks: &'a [VerifyCheck],
pub requested: &'a [String],
pub ledger: &'a VerificationLedger,
pub tree_now: Option<ContentId>,
pub repairs_used: usize,
pub rounds_left: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Accept,
Nudge(String),
Stop(crate::TurnEndReason),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StateBasis {
Tree,
MutationChain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CheckStatus {
Passed,
Stale,
Unverified,
Failed,
TimedOut,
Denied,
Unavailable,
Unexecuted,
NeverRun,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CheckReport {
pub label: String,
pub status: CheckStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_at_run: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct VerificationReport {
pub basis: StateBasis,
pub checks: Vec<CheckReport>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_now: Option<String>,
}
#[derive(serde::Serialize)]
struct Mutation<'a> {
command: Option<&'a str>,
outcome: Option<ExecOutcome>,
}
pub fn conclude(c: &Conclusion<'_>) -> (Decision, VerificationReport) {
conclude_with_evidence(c).0
}
fn conclude_with_evidence(
c: &Conclusion<'_>,
) -> ((Decision, VerificationReport), Vec<Option<usize>>) {
let mut journal = crate::event_journal::Journal::new();
let mut chain_ok = true;
let mut heads = Vec::with_capacity(c.ledger.entries.len() + 1);
heads.push(None);
for entry in &c.ledger.entries {
let mutation = match entry {
Observed::Write => Some(Mutation {
command: None,
outcome: None,
}),
Observed::Exec {
command, outcome, ..
} => (!super::is_verification_read_command(command)
&& !c
.checks
.iter()
.any(|check| check.invocation(command).is_some_and(|i| i.plain)))
.then_some(Mutation {
command: Some(command),
outcome: Some(*outcome),
}),
};
if let Some(mutation) = mutation {
chain_ok &= journal.append(mutation).is_ok();
}
heads.push(journal.head().map(ToString::to_string));
}
let chain_now = heads.last().cloned().flatten();
enum Standing<'a> {
None,
Pass(usize, &'a Option<ContentId>),
Unverified(usize),
Fail(ExecOutcome, String, usize),
Blocked(ExecOutcome, usize),
}
let standing = |check: &VerifyCheck| {
let mut state = Standing::None;
for (i, entry) in c.ledger.entries.iter().enumerate() {
let Observed::Exec {
command,
outcome,
tree,
..
} = entry
else {
continue;
};
let Some(run) = check.invocation(command) else {
continue;
};
state = match (run.evidence, *outcome, state) {
(false, _, kept @ (Standing::Pass(..) | Standing::Fail(..))) => kept,
(false, _, _) => Standing::Unverified(i),
(true, ExecOutcome::Passed, Standing::Fail(kind, failed, i))
if failed != run.normalized =>
{
Standing::Fail(kind, failed, i)
}
(true, ExecOutcome::Passed, _) => Standing::Pass(i, tree),
(true, kind @ (ExecOutcome::Failed | ExecOutcome::TimedOut), _) => {
Standing::Fail(kind, run.normalized, i)
}
(true, _, kept @ (Standing::Pass(..) | Standing::Fail(..))) => kept,
(true, kind, _) => Standing::Blocked(kind, i),
};
}
state
};
let standings: Vec<Standing> = c.checks.iter().map(standing).collect();
let basis = if c.tree_now.is_some()
&& standings
.iter()
.all(|s| !matches!(s, Standing::Pass(_, None)))
{
StateBasis::Tree
} else {
StateBasis::MutationChain
};
let state_now = match basis {
StateBasis::Tree => c.tree_now.map(|id| id.to_string()),
StateBasis::MutationChain => chain_now.clone(),
};
let chain_at = |i: usize| heads[i + 1].clone();
let reports: Vec<CheckReport> = c
.checks
.iter()
.zip(&standings)
.map(|(check, standing)| {
let (status, state_at_run) = match standing {
Standing::None if c.requested.iter().any(|r| check.invocation(r).is_some()) => {
(CheckStatus::Unexecuted, None)
}
Standing::None => (CheckStatus::NeverRun, None),
Standing::Unverified(i) => (CheckStatus::Unverified, chain_at(*i)),
Standing::Fail(kind, _, _) => (
if *kind == ExecOutcome::TimedOut {
CheckStatus::TimedOut
} else {
CheckStatus::Failed
},
None,
),
Standing::Blocked(kind, i) => (
if *kind == ExecOutcome::Denied {
CheckStatus::Denied
} else {
CheckStatus::Unavailable
},
chain_at(*i),
),
Standing::Pass(i, tree) => {
let at_run = match basis {
StateBasis::Tree => tree.map(|id| id.to_string()),
StateBasis::MutationChain => chain_at(*i),
};
let fresh = match basis {
StateBasis::Tree => at_run == state_now,
StateBasis::MutationChain => chain_ok && at_run == chain_now,
};
(
if fresh {
CheckStatus::Passed
} else {
CheckStatus::Stale
},
at_run,
)
}
};
CheckReport {
label: check.label.clone(),
status,
state_at_run,
}
})
.collect();
let evidence = standings
.iter()
.map(|standing| match standing {
Standing::None => None,
Standing::Pass(i, _)
| Standing::Unverified(i)
| Standing::Fail(_, _, i)
| Standing::Blocked(_, i) => Some(*i),
})
.collect();
let decision = decide(c, &reports);
(
(
decision,
VerificationReport {
basis,
checks: reports,
state_now,
},
),
evidence,
)
}
fn decide(c: &Conclusion<'_>, reports: &[CheckReport]) -> Decision {
let with = |wanted: &[CheckStatus]| {
reports
.iter()
.filter(|r| wanted.contains(&r.status))
.map(|r| (r.label.as_str(), r.status))
.collect::<Vec<_>>()
};
let broken = with(&[CheckStatus::Failed, CheckStatus::TimedOut]);
let unverified = with(&[
CheckStatus::NeverRun,
CheckStatus::Stale,
CheckStatus::Unverified,
]);
if reports.iter().all(|r| r.status == CheckStatus::Passed) {
return Decision::Accept;
}
let n = c.repairs_used + 1;
let can_nudge = c.repairs_used < VERIFY_REPAIR_ALLOWANCE && c.rounds_left;
if can_nudge && !broken.is_empty() {
let items = broken
.iter()
.map(|(label, status)| match status {
CheckStatus::TimedOut => format!(
"{label} timed out: it did not finish, so find what hangs or makes it slow \
rather than treating this as a build error"
),
_ => format!("{label} ran and failed"),
})
.collect::<Vec<_>>()
.join("; ");
return Decision::Nudge(super::workflow_guidance(format!(
"Before you finish (verification repair {n}/{VERIFY_REPAIR_ALLOWANCE}): {items}. \
Read its output above, fix the cause, and run it again. Conclude only after you \
have seen it pass."
)));
}
if can_nudge && !unverified.is_empty() {
let items = unverified
.iter()
.map(|(label, status)| match status {
CheckStatus::Stale => format!(
"{label} passed, but the workspace changed after it ran, so run it again on \
the current state"
),
CheckStatus::Unverified => format!(
"{label} was run with its exit status hidden by a pipe or a later command, \
so run it again on its own without piping or masking its exit status"
),
_ => format!("{label} has not been run"),
})
.collect::<Vec<_>>()
.join("; ");
return Decision::Nudge(super::workflow_guidance(format!(
"Before you finish (verification {n}/{VERIFY_REPAIR_ALLOWANCE}): {items}. Run it with \
run_command and read the output before you conclude."
)));
}
Decision::Stop(if broken.is_empty() {
crate::TurnEndReason::VerificationIncomplete
} else {
crate::TurnEndReason::RepairExhausted
})
}
pub(crate) fn tree_state(
root: &std::path::Path,
list: &impl Fn(&std::path::Path) -> Vec<(String, bool)>,
size: &impl Fn(&std::path::Path) -> Option<u64>,
read: &impl Fn(&std::path::Path) -> Option<Vec<u8>>,
max_entries: usize,
max_bytes: u64,
) -> Option<ContentId> {
#[derive(serde::Serialize)]
struct TreeState {
files: std::collections::BTreeMap<String, RawContentId>,
}
impl ContentAddressable for TreeState {
fn canonical_form(&self) -> Result<Vec<u8>, content_addressable::ContentError> {
content_addressable::canonical::to_canonical_dagcbor(self)
}
}
let mut files = std::collections::BTreeMap::new();
let (mut entries, mut bytes) = (0usize, 0u64);
let mut queue = std::collections::VecDeque::from([(root.to_path_buf(), String::new())]);
while let Some((dir, prefix)) = queue.pop_front() {
for (name, is_dir) in list(&dir) {
entries += 1;
if entries > max_entries {
return None;
}
let rel = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
if is_dir {
if !crate::verify_gate::SKIP_DIRS.contains(&name.as_str()) {
queue.push_back((dir.join(&name), rel));
}
continue;
}
let path = dir.join(&name);
if bytes.saturating_add(size(&path)?) > max_bytes {
return None;
}
let content = read(&path)?;
bytes = bytes.saturating_add(content.len() as u64);
if bytes > max_bytes {
return None;
}
files.insert(rel, RawContentId::from_content(&content));
}
}
TreeState { files }.content_id().ok()
}
pub fn workspace_tree_state(root: &std::path::Path) -> Option<ContentId> {
let complete = std::cell::Cell::new(true);
let state = tree_state(
root,
&|dir| {
std::fs::read_dir(dir)
.map(|rd| {
rd.filter_map(|entry| {
let value = entry.ok().and_then(|e| {
let name = e.file_name().into_string().ok()?;
let ft = e.file_type().ok()?;
Some((name, ft.is_dir() && !ft.is_symlink()))
});
if value.is_none() {
complete.set(false);
}
value
})
.collect()
})
.unwrap_or_else(|_| {
complete.set(false);
Vec::new()
})
},
&|path| {
let meta = std::fs::symlink_metadata(path).ok()?;
(meta.file_type().is_symlink() || meta.is_file()).then_some(meta.len())
},
&|path| {
let meta = std::fs::symlink_metadata(path).ok()?;
if meta.file_type().is_symlink() {
std::fs::read_link(path)
.ok()
.map(|target| target.to_string_lossy().into_owned().into_bytes())
} else if meta.is_file() {
std::fs::read(path).ok()
} else {
None
}
},
MAX_TREE_ENTRIES,
MAX_TREE_BYTES,
);
complete.get().then_some(state).flatten()
}
pub(crate) fn cargo_check_directory(
command: &str,
args: &serde_json::Value,
workspace: &str,
) -> Option<std::path::PathBuf> {
let parts = split_command(command);
let words: Vec<_> = parts.last()?.0.split_whitespace().take(2).collect();
if words != ["cargo", "check"] {
return None;
}
claim_facts::command_directory(command, args, workspace)
}
#[cfg(test)]
#[path = "self_verify_outcomes_tests.rs"]
mod outcomes_tests;
#[cfg(test)]
mod tests {
use super::*;
fn entries(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn a_tests_directory_does_not_identify_python() {
let checks = detect_checks(&entries(&["Cargo.toml", "tests"]), "");
assert_eq!(checks.len(), 1);
assert_eq!(checks[0].label, "`cargo test`");
}
#[test]
fn detects_a_python_test_file_once() {
let c = detect_checks(
&entries(&["solution.py", "test_outputs.py", "README.md"]),
"",
);
assert_eq!(c.len(), 1);
assert!(c[0].label.contains("Python tests"));
let c2 = detect_checks(&entries(&["test_a.py", "b_test.py", "tests"]), "");
assert_eq!(c2.len(), 1, "pytest detected once, not per file");
}
#[test]
fn detects_build_tool_test_entrypoints() {
let labels: Vec<String> = detect_checks(
&entries(&[
"Makefile",
"package.json",
"Cargo.toml",
"justfile",
"go.mod",
]),
"",
)
.into_iter()
.map(|c| c.label)
.collect();
assert!(labels.iter().any(|l| l.contains("make test")));
assert!(labels.iter().any(|l| l.contains("npm test")));
assert!(labels.iter().any(|l| l.contains("cargo test")));
assert!(labels.iter().any(|l| l.contains("just test")));
assert!(labels.iter().any(|l| l.contains("go test")));
}
#[test]
fn a_cargo_check_alone_does_not_satisfy_the_cargo_test_check() {
let checks = detect_checks(&entries(&["Cargo.toml"]), "");
for only in [
"cargo check",
"cargo check --workspace",
"cargo check --all-targets",
"cargo clippy --workspace -- -D warnings",
] {
assert!(
verify_gate_nudge(&checks, &[only.into()]).is_some(),
"`{only}` runs no test, so the gate must still fire"
);
}
}
#[test]
fn real_test_runs_still_satisfy_the_cargo_check() {
let checks = detect_checks(&entries(&["Cargo.toml"]), "");
for ran in [
"cargo test",
"cargo test -p newt-core",
"cargo test --workspace --all-targets",
"cargo test --lib self_verify",
"cargo nextest run",
"cargo nextest run -p newt-core",
] {
assert_eq!(
verify_gate_nudge(&checks, &[ran.into()]),
None,
"`{ran}` ran the tests, so the gate must be silent"
);
}
}
#[test]
fn detects_an_instruction_verify_command() {
let c = detect_checks(
&entries(&["main.py"]),
"Implement the parser. You can run `python check.py` to verify your work.",
);
assert!(
c.iter().any(|c| c.label.contains("python check.py")),
"{c:?}"
);
}
#[test]
fn no_checks_when_nothing_verifiable() {
assert!(detect_checks(&entries(&["notes.txt", "data.csv"]), "Write a poem.").is_empty());
}
#[test]
fn nudge_fires_when_tests_present_but_never_run() {
let checks = detect_checks(&entries(&["test_outputs.py"]), "");
let commands = vec!["cat solution.py".into(), "ls -la".into()];
let nudge = verify_gate_nudge(&checks, &commands);
assert!(nudge.is_some());
assert!(nudge.unwrap().contains("Python tests"));
}
#[test]
fn nudge_silent_once_the_tests_were_run() {
let checks = detect_checks(&entries(&["test_outputs.py"]), "");
let commands = vec!["python -m pytest test_outputs.py -q".into()];
assert_eq!(verify_gate_nudge(&checks, &commands), None);
}
#[test]
fn nudge_silent_when_the_instruction_command_was_run() {
let checks = detect_checks(&entries(&["m.py"]), "Run `python check.py` to verify.");
assert!(verify_gate_nudge(&checks, &["python check.py".into()]).is_none());
assert!(verify_gate_nudge(&checks, &["python m.py".into()]).is_some());
}
#[test]
fn nudge_silent_when_no_checks_detected() {
assert_eq!(verify_gate_nudge(&[], &["anything".into()]), None);
assert_eq!(
verify_gate_nudge(&detect_checks(&entries(&["a.txt"]), ""), &[]),
None
);
}
#[test]
fn run_marker_match_is_case_insensitive() {
let checks = detect_checks(&entries(&["Makefile"]), "");
assert_eq!(verify_gate_nudge(&checks, &["MAKE TEST".into()]), None);
}
fn tree(spec: &[(&str, &[(&str, bool)])]) -> impl Fn(&std::path::Path) -> Vec<(String, bool)> {
let map: std::collections::BTreeMap<String, Vec<(String, bool)>> = spec
.iter()
.map(|(dir, kids)| {
(
(*dir).to_string(),
kids.iter().map(|(n, d)| ((*n).to_string(), *d)).collect(),
)
})
.collect();
move |p: &std::path::Path| {
map.get(&p.to_string_lossy().replace('\\', "/"))
.cloned()
.unwrap_or_default()
}
}
const MONOREPO: &[(&str, &[(&str, bool)])] = &[
(
".",
&[
("README.md", false),
("backend", true),
("crates", true),
("target", true),
],
),
("./backend", &[("package.json", false), ("app.js", false)]),
("./crates", &[("tuner", true)]),
("./crates/tuner", &[("Cargo.toml", false), ("src", true)]),
("./target", &[("Cargo.toml", false), ("debug", true)]),
];
#[test]
fn a_one_level_scan_cannot_see_the_manifests_and_detects_nothing() {
let names = collect_entry_names(std::path::Path::new("."), 0, MAX_ENTRIES, &tree(MONOREPO));
assert!(
detect_checks(&names, "").is_empty(),
"one level sees only {names:?}"
);
}
#[test]
fn a_manifest_below_the_root_is_detected() {
let names = collect_entry_names(
std::path::Path::new("."),
MAX_DEPTH,
MAX_ENTRIES,
&tree(MONOREPO),
);
let labels: Vec<String> = detect_checks(&names, "")
.into_iter()
.map(|c| c.label)
.collect();
assert!(
labels.iter().any(|l| l.contains("npm test")),
"backend/package.json (one level down): {labels:?}"
);
assert!(
labels.iter().any(|l| l.contains("cargo test")),
"crates/tuner/Cargo.toml (two levels down): {labels:?}"
);
}
#[test]
fn the_walk_does_not_descend_into_ignored_directories() {
let names = collect_entry_names(
std::path::Path::new("."),
MAX_DEPTH,
MAX_ENTRIES,
&tree(&[
(
".",
&[("target", true), ("node_modules", true), (".git", true)],
),
("./target", &[("Cargo.toml", false)]),
("./node_modules", &[("package.json", false)]),
("./.git", &[("Makefile", false)]),
]),
);
assert!(
detect_checks(&names, "").is_empty(),
"nothing inside an ignored dir may register a check: {names:?}"
);
assert!(
names.iter().any(|n| n == "target"),
"the dir NAME is still an entry: {names:?}"
);
}
#[test]
fn a_name_found_repeatedly_registers_exactly_one_check() {
let names = collect_entry_names(
std::path::Path::new("."),
MAX_DEPTH,
MAX_ENTRIES,
&tree(&[
(".", &[("a", true), ("b", true), ("Cargo.toml", false)]),
("./a", &[("Cargo.toml", false)]),
("./b", &[("cargo.toml", false)]),
]),
);
assert_eq!(
names
.iter()
.filter(|n| n.eq_ignore_ascii_case("cargo.toml"))
.count(),
1,
"{names:?}"
);
assert_eq!(detect_checks(&names, "").len(), 1);
}
#[test]
fn a_pathological_tree_stops_at_the_budget_and_keeps_what_it_found() {
let wide: Vec<(String, bool)> = (0..500).map(|i| (format!("d{i}"), true)).collect();
let list = move |p: &std::path::Path| {
if p.to_string_lossy().matches('/').count() > 6 {
Vec::new()
} else {
wide.clone()
}
};
let names = collect_entry_names(std::path::Path::new("."), 8, 1_200, &list);
assert!(
names.len() <= 500,
"the walk stopped rather than enumerating the tree: {}",
names.len()
);
}
struct EnvRestore {
saved: Option<std::ffi::OsString>,
}
impl EnvRestore {
fn take() -> Self {
Self {
saved: std::env::var_os("NEWT_SELF_VERIFY"),
}
}
}
impl Drop for EnvRestore {
fn drop(&mut self) {
match &self.saved {
Some(v) => std::env::set_var("NEWT_SELF_VERIFY", v),
None => std::env::remove_var("NEWT_SELF_VERIFY"),
}
}
}
#[serial_test::serial(newt_self_verify_env)]
#[test]
fn the_gate_is_armed_with_no_environment_variable_set() {
let _restore = EnvRestore::take();
std::env::remove_var("NEWT_SELF_VERIFY");
assert!(
enabled(),
"unset must mean ON — a guard dark by default preserves the \
failure it was written to catch"
);
}
#[serial_test::serial(newt_self_verify_env)]
#[test]
fn an_explicit_off_still_disables_the_gate() {
let _restore = EnvRestore::take();
for off in ["0", "off", "false", "OFF", " 0 ", "False"] {
std::env::set_var("NEWT_SELF_VERIFY", off);
assert!(!enabled(), "NEWT_SELF_VERIFY={off:?} must disable");
}
}
#[serial_test::serial(newt_self_verify_env)]
#[test]
fn the_old_opt_in_still_enables_and_a_typo_does_not_disable() {
let _restore = EnvRestore::take();
for on in ["1", "on", "true", "banana", ""] {
std::env::set_var("NEWT_SELF_VERIFY", on);
assert!(enabled(), "NEWT_SELF_VERIFY={on:?} must leave the gate on");
}
}
#[test]
fn an_attempt_that_fails_still_satisfies_the_check() {
let checks = detect_checks(&entries(&["Cargo.toml"]), "");
let messages = vec![
serde_json::json!({
"role": "assistant",
"tool_calls": [{
"function": {
"name": "run_command",
"arguments": "{\"command\": \"cargo test --workspace\"}"
}
}]
}),
serde_json::json!({
"role": "tool",
"content": "error: no such command: `test`\ncargo: command not found"
}),
];
let cmds = commands_from_messages(&messages);
assert_eq!(
verify_gate_nudge(&checks, &cmds),
None,
"an attempted check satisfies the gate — otherwise a workspace \
where the check CANNOT run pays a nudge every round"
);
}
#[test]
fn commands_from_messages_reads_run_command_args_both_wire_shapes() {
let messages = vec![
serde_json::json!({
"role": "assistant",
"tool_calls": [{
"function": { "name": "run_command", "arguments": "{\"command\": \"pytest -q\"}" }
}]
}),
serde_json::json!({
"role": "assistant",
"tool_calls": [
{ "function": { "name": "run_command", "arguments": { "command": "make test" } } },
{ "function": { "name": "read_file", "arguments": { "path": "x" } } }
]
}),
serde_json::json!({ "role": "user", "content": "hi" }),
];
let cmds = commands_from_messages(&messages);
assert_eq!(cmds, vec!["pytest -q".to_string(), "make test".to_string()]);
}
#[test]
fn commands_from_messages_reads_responses_function_call_items() {
let messages = vec![
serde_json::json!({"role": "user", "content": "fix it"}),
serde_json::json!({"type": "function_call", "call_id": "c1", "name": "run_command",
"arguments": "{\"command\": \"pytest -q\"}"}),
serde_json::json!({"type": "function_call", "call_id": "c2", "name": "read_file",
"arguments": "{\"path\": \"x\"}"}),
serde_json::json!({"type": "function_call_output", "call_id": "c1", "output": "ok"}),
];
assert_eq!(
commands_from_messages(&messages),
vec!["pytest -q".to_string()]
);
}
#[test]
fn end_to_end_from_messages_gate_stays_silent_after_tests_run() {
let checks = detect_checks(&entries(&["test_outputs.py"]), "");
let messages = vec![serde_json::json!({
"role": "assistant",
"tool_calls": [{ "function": { "name": "run_command", "arguments": "{\"command\": \"python -m pytest\"}" }}]
})];
let cmds = commands_from_messages(&messages);
assert_eq!(verify_gate_nudge(&checks, &cmds), None);
}
}