use std::path::{Path, PathBuf};
use std::process::Command;
use newt_core::scope_grounding::{definition_grep_pattern, grep_terms, ground_scope};
use newt_scheduler::{Edit, Workspace};
pub fn infer_test_command(dir: &Path) -> Option<String> {
let has = |name: &str| dir.join(name).exists();
if has("justfile") || has("Justfile") {
Some("just check".to_string())
} else if has("Cargo.toml") {
Some("cargo test".to_string())
} else if has("pyproject.toml") || has("pytest.ini") || has("tox.ini") {
Some("pytest -x".to_string())
} else {
None
}
}
pub fn crew_normalize_commands(dir: &Path) -> Vec<String> {
match std::env::var("NEWT_CREW_NORMALIZE") {
Ok(v) if v.trim().is_empty() || v.trim().eq_ignore_ascii_case("none") => Vec::new(),
Ok(v) => vec![v],
Err(_) => {
newt_core::tooling::resolved_phase_commands(dir, newt_core::tooling::Phase::Format)
}
}
}
fn is_safe_worktree_path(path: &str) -> bool {
use std::path::Component;
Path::new(path)
.components()
.all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
}
pub(crate) fn git(dir: &Path, args: &[&str]) -> anyhow::Result<String> {
let out = Command::new("git").args(args).current_dir(dir).output()?;
if !out.status.success() {
anyhow::bail!(
"git {}: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub(crate) fn def_sites_grep(repo_dir: &Path) -> impl Fn(&str) -> Vec<String> + '_ {
move |sym: &str| -> Vec<String> {
git(
repo_dir,
&[
"-c",
"core.quotepath=false",
"grep",
"--full-name",
"-nE",
"-e",
&definition_grep_pattern(sym),
"--",
"*.rs",
],
)
.map(|s| s.lines().map(str::to_string).collect())
.unwrap_or_default()
}
}
pub struct WorktreeWorkspace {
base: PathBuf,
worktree: PathBuf,
test_cmd: String,
}
impl WorktreeWorkspace {
pub fn create(base: &Path, id: &str, base_ref: &str, test_cmd: String) -> anyhow::Result<Self> {
let scratch = newt_core::scratch::ensure_scratch(base)?;
let worktree = scratch.join("worktrees").join(id);
if let Some(parent) = worktree.parent() {
std::fs::create_dir_all(parent)?;
}
git(
base,
&[
"worktree",
"add",
"--detach",
worktree.to_str().unwrap_or_default(),
base_ref,
],
)?;
Ok(Self {
base: base.to_path_buf(),
worktree,
test_cmd,
})
}
pub fn path(&self) -> &Path {
&self.worktree
}
pub fn diff(&self) -> String {
let _ = git(&self.worktree, &["add", "-A"]);
git(&self.worktree, &["diff", "--cached", "HEAD"]).unwrap_or_default()
}
fn normalize(&self) {
for cmd in crew_normalize_commands(&self.worktree) {
#[cfg(unix)]
let mut c = {
let mut c = Command::new("sh");
c.arg("-c").arg(&cmd);
c
};
#[cfg(windows)]
let mut c = {
let mut c = Command::new("cmd");
c.arg("/C").arg(&cmd);
c
};
c.env("CARGO_TARGET_DIR", crew_shared_target_dir(&self.base));
match c.current_dir(&self.worktree).output() {
Ok(o) if !o.status.success() => {
eprintln!(" crew normalize (`{cmd}`) failed — committing unformatted; the gate may flag it");
}
Err(e) => {
eprintln!(
" crew normalize (`{cmd}`) could not run ({e}) — committing unformatted"
);
}
_ => {}
}
}
}
pub fn commit_to_branch(
&self,
branch: &str,
author_name: &str,
author_email: &str,
message: &str,
) -> anyhow::Result<(String, String)> {
self.normalize();
git(&self.worktree, &["checkout", "-q", "-b", branch])?;
git(&self.worktree, &["add", "-A"])?;
if git(&self.worktree, &["diff", "--cached", "--quiet"]).is_ok() {
anyhow::bail!("no changes to land");
}
git(
&self.worktree,
&[
"-c",
&format!("user.name={author_name}"),
"-c",
&format!("user.email={author_email}"),
"commit",
"-q",
"-m",
message,
],
)?;
let sha = git(&self.worktree, &["rev-parse", "--short", "HEAD"])?;
Ok((branch.to_string(), sha))
}
pub fn cleanup(&self) {
let _ = git(
&self.base,
&[
"worktree",
"remove",
"--force",
self.worktree.to_str().unwrap_or_default(),
],
);
}
}
impl Drop for WorktreeWorkspace {
fn drop(&mut self) {
self.cleanup();
}
}
impl Workspace for WorktreeWorkspace {
fn files(&self) -> Vec<String> {
git(&self.worktree, &["ls-files"])
.map(|o| {
o.lines()
.map(str::to_string)
.filter(|l| !l.is_empty())
.collect()
})
.unwrap_or_default()
}
fn read(&self, path: &str) -> Option<String> {
if !is_safe_worktree_path(path) {
return None;
}
std::fs::read_to_string(self.worktree.join(path)).ok()
}
fn apply(&mut self, edits: &[Edit]) -> Vec<String> {
let mut written = Vec::new();
for e in edits {
if !is_safe_worktree_path(&e.path) {
continue;
}
let full = self.worktree.join(&e.path);
if let Some(parent) = full.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::write(&full, &e.new_content).is_ok() {
written.push(e.path.clone());
}
}
written
}
fn run_test(&self) -> (bool, String) {
#[cfg(unix)]
let mut cmd = {
let mut c = Command::new("sh");
c.arg("-c").arg(&self.test_cmd);
c
};
#[cfg(windows)]
let mut cmd = {
let mut c = Command::new("cmd");
c.arg("/C").arg(&self.test_cmd);
c
};
cmd.env("CARGO_TARGET_DIR", crew_shared_target_dir(&self.base));
match cmd.current_dir(&self.worktree).output() {
Ok(o) => {
let out = format!(
"{}{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
);
(o.status.success(), out)
}
Err(e) => (false, format!("failed to run `{}`: {e}", self.test_cmd)),
}
}
}
fn crew_shared_target_dir(base: &Path) -> PathBuf {
newt_core::scratch::scratch_root(base).join("crew-target")
}
use newt_core::Config;
use newt_scheduler::{
run_crew, BackendPool, CrewConfig, CrewOutcome, CrewStatus, Dispatcher, LocalDispatcher,
StaticSource,
};
pub struct CrewArgs {
pub task: String,
pub crew: Option<String>,
pub dir: Option<PathBuf>,
pub test: Option<String>,
pub max_attempts: Option<u32>,
pub dry_run: bool,
}
pub async fn run_cli(args: CrewArgs) -> anyhow::Result<i32> {
let cfg = Config::resolve().map_err(|e| anyhow::anyhow!("config: {e}"))?;
run_with(&cfg, args, &LocalDispatcher).await
}
pub struct PlanArgs {
pub file: PathBuf,
pub dir: Option<PathBuf>,
pub execute: bool,
pub one_shot: bool,
pub max_leaves: usize,
}
pub fn render_plan_preview(plan: &newt_core::plan::Plan) -> String {
use std::collections::HashSet;
let leaves = plan.leaves();
let leaf_ids: HashSet<&str> = leaves.iter().map(|s| s.id.as_str()).collect();
let mut out = String::new();
if let Some(g) = &plan.goal {
out.push_str(&format!("goal: {g}\n"));
}
out.push_str(&format!(
"{} subtask(s); {} leaf/leaves to dispatch:\n",
plan.subtasks.len(),
leaves.len()
));
for leaf in &leaves {
let after = if leaf.deps.is_empty() {
String::new()
} else {
let deps: Vec<String> = leaf
.deps
.iter()
.map(|d| {
if leaf_ids.contains(d.as_str()) {
d.clone()
} else {
format!("{d} [will stall]")
}
})
.collect();
format!(" (after {})", deps.join(", "))
};
out.push_str(&format!(" • {} — {}{after}\n", leaf.id, leaf.instruction));
}
out
}
fn plan_sanity(plan: &newt_core::plan::Plan) -> Vec<String> {
use std::collections::HashSet;
let ids: HashSet<&str> = plan.subtasks.iter().map(|s| s.id.as_str()).collect();
let mut problems = Vec::new();
for s in &plan.subtasks {
for d in &s.deps {
if !ids.contains(d.as_str()) {
problems.push(format!(
"subtask `{}` depends on `{}`, which no subtask defines — it can never run",
s.id, d
));
}
}
if s.instruction.trim().is_empty() {
problems.push(format!("subtask `{}` has an empty instruction", s.id));
}
}
if plan.leaves().is_empty() && !plan.subtasks.is_empty() {
problems.push(
"the plan has no dispatchable leaves (every subtask is a parent) — nothing would run"
.to_string(),
);
}
problems
}
fn path_tokens(text: &str) -> Vec<String> {
text.split(|c: char| c.is_whitespace() || "()[]{}<>,;:\"'`".contains(c))
.filter(|t| {
t.contains('/')
&& t.rsplit_once('.').is_some_and(|(_, ext)| {
(1..=4).contains(&ext.len()) && ext.chars().all(|c| c.is_ascii_alphanumeric())
})
})
.map(str::to_string)
.collect()
}
fn plan_grounding_contradictions(
plan: &newt_core::plan::Plan,
def_sites: impl Fn(&str) -> Vec<String>,
) -> Vec<String> {
let mut out = Vec::new();
for s in &plan.subtasks {
let paths = path_tokens(&s.instruction);
if paths.is_empty() {
continue;
}
for sym in grep_terms(&s.instruction) {
let sites = def_sites(&sym);
if sites.is_empty() {
continue; }
let def_files: Vec<String> = sites
.iter()
.filter_map(|l| l.split(':').next().map(str::to_string))
.collect();
for claimed in &paths {
let agrees = def_files
.iter()
.any(|f| f == claimed || f.ends_with(claimed) || claimed.ends_with(f.as_str()));
if !agrees {
out.push(format!(
"subtask `{}`: `{sym}` is defined at {}, not `{claimed}` — make the edit there.",
s.id,
def_files.join(", "),
));
}
}
}
}
out.sort();
out.dedup();
out
}
fn ground_subtask_scopes(
plan: &mut newt_core::plan::Plan,
def_sites: &impl Fn(&str) -> Vec<String>,
) {
for s in &mut plan.subtasks {
s.context = ground_scope(&s.instruction, &s.context, def_sites);
}
}
pub async fn run_plan_cli(args: PlanArgs) -> anyhow::Result<i32> {
let toml = std::fs::read_to_string(&args.file)
.map_err(|e| anyhow::anyhow!("read {}: {e}", args.file.display()))?;
let mut plan = newt_core::plan::Plan::from_toml_str(&toml)
.map_err(|e| anyhow::anyhow!("parse plan {}: {e}", args.file.display()))?;
println!("plan: {}", args.file.display());
print!("{}", render_plan_preview(&plan));
if !args.execute {
println!("\n(preview only — re-run with --execute to dispatch one crew per leaf)");
return Ok(0);
}
if args.one_shot {
grant_one_shot_authority(&mut plan);
}
let log_path = args.file.with_extension("run.toml");
execute_plan(&mut plan, args.dir, args.max_leaves, Some(log_path), None).await
}
fn grant_one_shot_authority(plan: &mut newt_core::plan::Plan) {
use newt_core::role_profile::ScopeSpec;
for s in &mut plan.subtasks {
s.caveat_policy.fs_read = ScopeSpec::default(); s.caveat_policy.fs_write = ScopeSpec::default(); }
println!(
"--one-shot: granted each leaf fs_read + fs_write (worktree-bounded); \
exec + net stay denied (verify runs via the runner's trusted command)."
);
}
struct DefGroundReground {
dir: std::path::PathBuf,
}
impl newt_core::agentic::Reground for DefGroundReground {
fn reground(&self, error: &str, instruction: &str) -> Option<String> {
let sym = unresolved_symbol(error)?;
let sites = git(
&self.dir,
&[
"grep",
"-nE",
"-e",
&definition_grep_pattern(&sym),
"--",
"*.rs",
],
)
.ok()?;
let file = sites.lines().next()?.split(':').next()?.to_string();
if file.is_empty() || instruction.contains(&file) {
return None;
}
Some(format!(
"{instruction}\n\nGROUNDING: `{sym}` is defined at {file} — make the edit there, do not invent paths."
))
}
}
fn unresolved_symbol(error: &str) -> Option<String> {
const MARKERS: &[&str] = &[
"cannot find function `",
"cannot find value `",
"cannot find type `",
"cannot find macro `",
"cannot find struct, variant or union type `",
"no method named `",
"no function or associated item named `",
"use of undeclared `",
];
for m in MARKERS {
if let Some(i) = error.find(m) {
let rest = &error[i + m.len()..];
if let Some(j) = rest.find('`') {
let sym = &rest[..j];
if !sym.is_empty() {
return Some(sym.to_string());
}
}
}
}
None
}
pub async fn execute_plan(
plan: &mut newt_core::plan::Plan,
dir: Option<PathBuf>,
max_leaves: usize,
log_path: Option<PathBuf>,
locked_verify: Option<String>,
) -> anyhow::Result<i32> {
let problems = plan_sanity(plan);
if !problems.is_empty() {
eprintln!("✗ plan is not structurally runnable — refusing to dispatch:");
for p in &problems {
eprintln!(" - {p}");
}
return Ok(2);
}
let leaf_count = plan.leaves().len();
if leaf_count > max_leaves {
return Err(anyhow::anyhow!(
"plan has {leaf_count} leaves (> --max-leaves {max_leaves}); each is an autonomous \
crew with no per-leaf review. Re-run with `--max-leaves {leaf_count}` to confirm you \
intend to run them all."
));
}
let cfg = Config::resolve().map_err(|e| anyhow::anyhow!("config: {e}"))?;
let dir = dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
println!(
"executing {leaf_count} leaf/leaves autonomously (one crew each; per-leaf verify gates each)…"
);
let caveats = newt_acp_worker::worker_session_caveats(None);
let reground = DefGroundReground { dir: dir.clone() };
let runner =
crate::crew_runner::LocalCrewRunner::new(cfg, dir, newt_core::agentic::Presence::Prompt)
.with_locked_verify(locked_verify);
let run = newt_core::agentic::run_plan_with_reground(plan, &caveats, &runner, ®round).await;
for id in &run.dispatched {
if let Some(s) = plan.subtask(id) {
println!(" [{:?}] {}", s.status, id);
}
}
if let Some(e) = &run.failed {
println!("✗ stopped at a failed leaf: {e}");
}
if !run.remaining.is_empty() {
println!("remaining (blocked/stalled): {}", run.remaining.join(", "));
}
println!(
"{}",
if run.complete {
"✓ plan complete"
} else {
"plan incomplete"
}
);
if !run.dispatched.is_empty() {
if let Some(log) = log_path {
std::fs::write(&log, plan.to_toml_string()?)
.map_err(|e| anyhow::anyhow!("write run-log {}: {e}", log.display()))?;
println!("run-log → {}", log.display());
}
}
Ok(i32::from(!run.complete))
}
const PLAN_AUTHOR_SYSTEM: &str = "You are a planning lead. Decompose the GOAL into a \
dependency-ordered plan of the FEWEST engineering subtasks that accomplish it — each \
subtask MUST change code (produce a file edit). Reply with ONLY JSON: \
{\"goal\":\"<the goal>\",\"subtasks\":[{\"id\":\"<short-kebab-id>\",\
\"instruction\":\"<imperative code-changing step>\",\
\"deps\":[\"<id of a step that must finish first>\"],\
\"files\":[\"<relative path of a REAL existing file this step edits; omit if unknown>\"],\
\"verify\":\"<shell command that exits 0 once THIS step is done; omit if none>\"}]}. \
A small, single-file change is ONE subtask. Do NOT create separate \
inspect/understand/explore/locate/verify/test/run-tests subtasks — the harness reads \
the repo for you and automatically verifies EVERY subtask after it runs (put a step's \
own check in its `verify` FIELD, never as a standalone subtask). That prohibition covers \
REPHRASINGS of the same non-actionable pattern too — \"add edge-case tests\", \"clean up \
the code\", \"update the comments\", \"validate the fix\" are inspect/verify/test steps \
wearing different words; fold any such follow-up into the ONE subtask that changes the \
behavior, or drop it. Example: the goal \"Fix `humanize_duration` so 90 seconds returns \
'1m 30s' and the test passes\" is ONE subtask, not six — \
{\"goal\":\"Fix humanize_duration so 90 seconds returns '1m 30s'\",\
\"subtasks\":[{\"id\":\"fix-duration-format\",\"instruction\":\"Fix the minutes/seconds \
formatting in humanize_duration so it is correct for every input: zero, under a minute, \
and a mixed minutes+seconds value\",\"deps\":[],\
\"verify\":\"cargo test humanize_duration\"}]} — NOT \
fix-then-adjust-then-format-then-test-edge-cases-then-clean-up-then-update-comments; a \
failed later subtask strands the earlier ones half-fixed. Use `deps` for ordering (a \
step lists the ids it waits on). Ids: short, stable, unique. Do NOT grant permissions or \
describe authority — only the work.";
fn extract_json_object(s: &str) -> Option<&str> {
let start = s.find('{')?;
let bytes = s.as_bytes();
let (mut depth, mut in_str, mut esc) = (0i32, false, false);
for i in start..bytes.len() {
let c = bytes[i];
if in_str {
match c {
b'\\' if !esc => esc = true,
b'"' if !esc => in_str = false,
_ => esc = false,
}
} else if c == b'"' {
in_str = true;
} else if c == b'{' {
depth += 1;
} else if c == b'}' {
depth -= 1;
if depth == 0 {
return Some(&s[start..=i]);
}
}
}
None
}
fn parse_authored_plan(raw: &str) -> Option<newt_core::plan::Plan> {
use newt_core::plan::{Aggregation, CaveatPolicy, Plan, Subtask, SubtaskStatus};
use std::collections::HashSet;
let v: serde_json::Value = serde_json::from_str(extract_json_object(raw)?).ok()?;
let arr = v.get("subtasks")?.as_array()?;
if arr.is_empty() {
return None;
}
let mut seen = HashSet::new();
let mut subtasks = Vec::with_capacity(arr.len());
for s in arr {
let id = s.get("id")?.as_str()?.trim().to_string();
let instruction = s.get("instruction")?.as_str()?.trim().to_string();
if id.is_empty() || instruction.is_empty() || !seen.insert(id.clone()) {
return None;
}
let deps = s
.get("deps")
.and_then(|d| d.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
let context = s
.get("files")
.and_then(|f| f.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(str::trim).map(str::to_string))
.filter(|p| !p.is_empty())
.collect()
})
.unwrap_or_default();
subtasks.push(Subtask {
id,
instruction,
deps,
parallel_ok: false,
context,
verify: s.get("verify").and_then(|x| x.as_str()).map(str::to_string),
status: SubtaskStatus::Pending,
result: None,
parent: None,
caveat_policy: CaveatPolicy::default(), });
}
Some(Plan {
goal: v.get("goal").and_then(|x| x.as_str()).map(str::to_string),
aggregation: Aggregation::default(),
subtasks,
})
}
#[derive(Clone, Copy)]
enum MarkerKind {
Inspect,
Gate,
}
const ACTION_MARKERS: &[(&str, MarkerKind)] = &[
("inspect", MarkerKind::Inspect),
("examine", MarkerKind::Inspect),
("explore", MarkerKind::Inspect),
("investigate", MarkerKind::Inspect),
("understand", MarkerKind::Inspect),
("locate", MarkerKind::Inspect),
("identify", MarkerKind::Inspect),
("review", MarkerKind::Inspect),
("analyze", MarkerKind::Inspect),
("verify", MarkerKind::Gate),
("validate", MarkerKind::Gate),
("test", MarkerKind::Gate),
("confirm", MarkerKind::Gate),
("ensure", MarkerKind::Gate),
];
fn effective_markers(cfg: Option<&newt_core::PlanPruneConfig>) -> Vec<(String, MarkerKind)> {
let mut out: Vec<(String, MarkerKind)> = ACTION_MARKERS
.iter()
.map(|(v, k)| ((*v).to_string(), *k))
.collect();
if let Some(c) = cfg {
let removed: Vec<String> = c.remove.iter().map(|w| w.to_ascii_lowercase()).collect();
out.retain(|(v, _)| !removed.contains(v));
let mut add = |words: &[String], kind: MarkerKind| {
for w in words {
let w = w.trim().to_ascii_lowercase();
if !w.is_empty() && !removed.contains(&w) && !out.iter().any(|(v, _)| *v == w) {
out.push((w, kind));
}
}
};
add(&c.add_inspect, MarkerKind::Inspect);
add(&c.add_gate, MarkerKind::Gate);
}
out
}
fn marker_kind_in(markers: &[(String, MarkerKind)], instruction: &str) -> Option<MarkerKind> {
let head = instruction
.split(|c: char| !c.is_alphanumeric())
.find(|w| !w.is_empty())
.unwrap_or("")
.to_ascii_lowercase();
markers
.iter()
.find(|(verb, _)| *verb == head)
.map(|(_, kind)| *kind)
}
fn prune_non_actionable_subtasks_in(
plan: &mut newt_core::plan::Plan,
markers: &[(String, MarkerKind)],
) {
use std::collections::HashSet;
if !plan
.subtasks
.iter()
.any(|s| marker_kind_in(markers, &s.instruction).is_none())
{
return;
}
loop {
let depended: HashSet<String> = plan
.subtasks
.iter()
.flat_map(|s| s.deps.iter().cloned())
.collect();
let victim =
plan.subtasks
.iter()
.position(|s| match marker_kind_in(markers, &s.instruction) {
Some(MarkerKind::Inspect) => true,
Some(MarkerKind::Gate) => !depended.contains(&s.id),
None => false,
});
let Some(idx) = victim else { return };
let removed = plan.subtasks.remove(idx);
for s in &mut plan.subtasks {
if let Some(pos) = s.deps.iter().position(|d| *d == removed.id) {
s.deps.remove(pos);
for vd in &removed.deps {
if !s.deps.contains(vd) {
s.deps.push(vd.clone());
}
}
}
}
}
}
pub async fn author_plan(
pool: &newt_scheduler::BackendPool,
dispatcher: &dyn Dispatcher,
model: &str,
goal: &str,
max_subtasks: usize,
prune: Option<&newt_core::PlanPruneConfig>,
) -> anyhow::Result<newt_core::plan::Plan> {
let req = newt_scheduler::ChatRequest::new()
.system(PLAN_AUTHOR_SYSTEM)
.user(format!("GOAL:\n{goal}\n\nAt most {max_subtasks} subtasks."));
let reply = pool
.run_role(dispatcher, newt_core::Tier::Complex, model, req)
.await
.ok_or_else(|| {
anyhow::anyhow!("no live model reachable to author the plan (model {model})")
})?;
parse_authored_plan(&reply.result.content)
.map(|mut plan| {
if !prune.map(|c| c.disabled).unwrap_or(false) {
prune_non_actionable_subtasks_in(&mut plan, &effective_markers(prune));
}
plan
})
.ok_or_else(|| {
anyhow::anyhow!(
"the model did not return a parseable JSON plan. Raw reply:\n{}",
reply.result.content
)
})
}
pub async fn author_plan_to_plan(
goal: &str,
max_subtasks: usize,
repo_dir: &Path,
) -> anyhow::Result<newt_core::plan::Plan> {
let cfg = Config::resolve().map_err(|e| anyhow::anyhow!("config: {e}"))?;
let model = cfg
.backends
.first()
.map(|b| b.model.clone())
.ok_or_else(|| {
anyhow::anyhow!("no backend configured to author a plan (add a [[backends]])")
})?;
let pool = BackendPool::from_source(&StaticSource::from_configs(cfg.backends.iter()));
let mut effective_goal = goal.to_string();
let docs = fetch_referenced_docs(goal);
if !docs.is_empty() {
println!(
"read referenced GitHub doc(s) via gh ({} chars)",
docs.len()
);
effective_goal.push_str(&format!(
"\n\nThe referenced document(s) below are the TASK to implement — \
decompose THESE into concrete engineering subtasks:{docs}"
));
}
let repo = fetch_repo_context(repo_dir);
if !repo.is_empty() {
println!("read target-repo context ({} chars)", repo.len());
effective_goal.push_str(&repo);
}
let hits = fetch_code_grep_hits(&format!("{goal}{docs}"), repo_dir);
if !hits.is_empty() {
println!(
"found relevant code location(s) via grep ({} chars)",
hits.len()
);
effective_goal.push_str(&hits);
}
println!("authoring a plan for: {goal} (model {model})…");
let prune_cfg = cfg.plan.as_ref().map(|p| p.prune.clone());
let mut plan = author_plan(
&pool,
&LocalDispatcher,
&model,
&effective_goal,
max_subtasks,
prune_cfg.as_ref(),
)
.await?;
let def_sites = def_sites_grep(repo_dir);
let contradictions = plan_grounding_contradictions(&plan, &def_sites);
if contradictions.is_empty() {
ground_subtask_scopes(&mut plan, &def_sites);
return Ok(plan);
}
println!(
"plan grounding contradictions — re-authoring with corrections:\n {}",
contradictions.join("\n ")
);
effective_goal.push_str(&format!(
"\n\nGROUNDING CORRECTIONS — the prior plan targeted the wrong file(s); \
fix these and do NOT invent paths:\n{}",
contradictions.join("\n")
));
let mut plan = author_plan(
&pool,
&LocalDispatcher,
&model,
&effective_goal,
max_subtasks,
prune_cfg.as_ref(),
)
.await?;
ground_subtask_scopes(&mut plan, &def_sites);
Ok(plan)
}
fn fetch_repo_context(dir: &Path) -> String {
let mut facts: Vec<String> = Vec::new();
if dir.join("Cargo.toml").exists() {
facts.push("Language/build: Rust (a cargo workspace).".into());
} else if dir.join("package.json").exists() {
facts.push("Language/build: JavaScript/TypeScript (npm).".into());
} else if dir.join("pyproject.toml").exists() || dir.join("setup.py").exists() {
facts.push("Language/build: Python.".into());
} else if dir.join("go.mod").exists() {
facts.push("Language/build: Go.".into());
}
if let Some(cmd) = infer_test_command(dir) {
facts.push(format!("Verify/build command: `{cmd}`."));
}
if let Ok(top) = git(dir, &["ls-tree", "--name-only", "HEAD"]) {
let entries: Vec<&str> = top.lines().filter(|l| !l.is_empty()).take(40).collect();
if !entries.is_empty() {
facts.push(format!("Top-level entries: {}.", entries.join(", ")));
}
}
if facts.is_empty() {
return String::new();
}
format!(
"\n\nTarget repository context — author subtasks that fit THIS codebase \
(use its real language, build command, and directories; do NOT invent a \
different stack):\n- {}",
facts.join("\n- ")
)
}
fn github_refs(goal: &str) -> Vec<(String, String, String, String)> {
let mut refs = Vec::new();
for token in goal.split_whitespace() {
let Some(idx) = token.find("github.com/") else {
continue;
};
let parts: Vec<&str> = token[idx + "github.com/".len()..].split('/').collect();
if parts.len() < 4 {
continue;
}
let num: String = parts[3]
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if !num.is_empty() && (parts[2] == "issues" || parts[2] == "pull") {
refs.push((
parts[0].to_string(),
parts[1].to_string(),
parts[2].to_string(),
num,
));
}
}
refs
}
struct GroundingBlock {
term: String,
defs: Vec<String>,
mentions: Vec<String>,
}
fn truncate_grep_line(line: &str) -> String {
line.chars().take(160).collect()
}
fn format_grounding_hits(blocks: &[GroundingBlock]) -> String {
let mut out = String::new();
let mut total = 0usize;
for b in blocks {
if total >= 25 {
break;
}
for d in b.defs.iter().take(6) {
if total >= 25 {
break;
}
out.push_str(&format!(" {} [def]: {}\n", b.term, truncate_grep_line(d)));
total += 1;
}
let mut shown_mentions = 0usize;
for m in &b.mentions {
if total >= 25 || shown_mentions >= 2 {
break;
}
if b.defs.contains(m) {
continue;
}
out.push_str(&format!(" {}: {}\n", b.term, truncate_grep_line(m)));
total += 1;
shown_mentions += 1;
}
}
if out.is_empty() {
return String::new();
}
format!(
"\n\nRelevant existing code — task terms already appear here. A `[def]` \
line is the DEFINITION site (the real seam to edit); implement AT these \
sites, do NOT invent file paths:\n{out}"
)
}
fn fetch_code_grep_hits(task: &str, dir: &Path) -> String {
let lines = |res: anyhow::Result<String>| -> Vec<String> {
res.map(|s| s.lines().map(str::to_string).collect())
.unwrap_or_default()
};
let mut blocks = Vec::new();
for term in grep_terms(task) {
let defs = lines(git(
dir,
&[
"grep",
"-nE",
"-e",
&definition_grep_pattern(&term),
"--",
"*.rs",
],
));
let mentions = lines(git(dir, &["grep", "-n", "-F", "-e", &term, "--", "*.rs"]));
if !defs.is_empty() || !mentions.is_empty() {
blocks.push(GroundingBlock {
term,
defs,
mentions,
});
}
}
format_grounding_hits(&blocks)
}
fn fetch_referenced_docs(goal: &str) -> String {
let mut out = String::new();
for (owner, repo, kind, num) in github_refs(goal) {
let sub = if kind == "pull" { "pr" } else { "issue" };
let slug = format!("{owner}/{repo}");
match std::process::Command::new("gh")
.args([sub, "view", &num, "--repo", &slug, "--json", "title,body"])
.output()
{
Ok(o) if o.status.success() => {
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&o.stdout) {
let title = v["title"].as_str().unwrap_or("");
let body = v["body"].as_str().unwrap_or("");
out.push_str(&format!(
"\n\n--- {sub} {slug}#{num}: {title} ---\n{body}\n"
));
}
}
Ok(o) => eprintln!(
"note: `gh {sub} view {num}` failed ({}) — planning from the goal text alone",
String::from_utf8_lossy(&o.stderr).trim()
),
Err(e) => {
eprintln!("note: could not run `gh` ({e}) — planning from the goal text alone");
}
}
}
out
}
pub async fn one_shot_goal_cli(
goal: &str,
dir: Option<PathBuf>,
max_leaves: usize,
locked_verify: Option<String>,
) -> anyhow::Result<i32> {
let repo_dir = dir
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let mut plan = author_plan_to_plan(goal, max_leaves, &repo_dir).await?;
print!("{}", render_plan_preview(&plan));
grant_one_shot_authority(&mut plan);
println!("\n--one-shot: executing the authored plan autonomously…");
execute_plan(
&mut plan,
dir,
max_leaves,
Some(PathBuf::from("plan.run.toml")),
locked_verify,
)
.await
}
pub async fn author_plan_cli(
goal: String,
output: Option<PathBuf>,
max_subtasks: usize,
dir: Option<PathBuf>,
) -> anyhow::Result<i32> {
let repo_dir = dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let plan = author_plan_to_plan(&goal, max_subtasks, &repo_dir).await?;
let toml = plan
.to_toml_string()
.map_err(|e| anyhow::anyhow!("serialize plan: {e}"))?;
print!("{}", render_plan_preview(&plan));
match &output {
Some(path) => {
if path.exists() {
anyhow::bail!(
"{} already exists — choose another -o path or remove it first \
(authoring won't overwrite a plan you may have edited)",
path.display()
);
}
std::fs::write(path, &toml)
.map_err(|e| anyhow::anyhow!("write {}: {e}", path.display()))?;
println!(
"\nwrote plan → {} — review/edit (grant caveats where needed), then \
`newt plan {} --execute`",
path.display(),
path.display()
);
}
None => println!(
"\n--- plan.toml (write to a file, then `newt plan <file> --execute`) ---\n{toml}"
),
}
Ok(0)
}
async fn run_with(
cfg: &Config,
args: CrewArgs,
dispatcher: &dyn Dispatcher,
) -> anyhow::Result<i32> {
let crew_name = resolve_crew_name(cfg, args.crew.as_deref())?;
let crew = &cfg.crews[&crew_name];
crew.validate(cfg).map_err(|e| anyhow::anyhow!("{e}"))?;
let planner_model = model_for_role(cfg, &crew.planner)?;
let navigator_model = match &crew.navigator {
Some(n) => model_for_role(cfg, n)?,
None => planner_model.clone(),
};
let triage_model = match &crew.triage {
Some(t) => model_for_role(cfg, t)?,
None => planner_model.clone(),
};
let dir = args
.dir
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let test_cmd = args
.test
.clone()
.or_else(|| crew.test.clone())
.or_else(|| infer_test_command(&dir))
.ok_or_else(|| {
anyhow::anyhow!(
"no verification command — pass --test, set the crew's `test`, or add a \
justfile / Cargo.toml / pyproject.toml to {}",
dir.display()
)
})?;
let max_attempts = args
.max_attempts
.or_else(|| crew.budgets.as_ref().and_then(|b| b.max_attempts))
.unwrap_or(3);
println!(
"crew '{crew_name}': planner▸{planner_model} navigator▸{navigator_model} \
triage▸{triage_model} (max {max_attempts}, test `{test_cmd}`)"
);
if args.dry_run {
return Ok(0);
}
let pool = BackendPool::from_source(&StaticSource::from_configs(cfg.backends.iter()));
let mut ws = WorktreeWorkspace::create(&dir, &worktree_id(), "HEAD", test_cmd)?;
let crew_cfg = CrewConfig {
navigator_model,
planner_model,
triage_model,
max_attempts,
role_timeout: None,
calibrate_baseline: true,
};
let caveats = newt_acp_worker::worker_session_caveats(None);
let outcome = run_crew(
&pool,
dispatcher,
&mut ws,
&crew_cfg,
&caveats,
&args.task,
&[],
)
.await;
let touched_in = ws.path().to_path_buf();
drop(ws);
Ok(render(&outcome, &touched_in))
}
fn resolve_crew_name(cfg: &Config, explicit: Option<&str>) -> anyhow::Result<String> {
if let Some(n) = explicit {
if cfg.crews.contains_key(n) {
return Ok(n.to_string());
}
anyhow::bail!("no crew named '{n}' (known: {})", names(cfg.crews.keys()));
}
match cfg.crews.len() {
1 => Ok(cfg.crews.keys().next().unwrap().clone()),
0 => anyhow::bail!("no crews defined — add a [crews.<name>] or ~/.newt/crews/<name>.toml"),
_ => anyhow::bail!(
"multiple crews defined ({}) — pick one with --crew",
names(cfg.crews.keys())
),
}
}
pub(crate) fn model_for_role(cfg: &Config, loadout_name: &str) -> anyhow::Result<String> {
let l = cfg
.loadouts
.get(loadout_name)
.ok_or_else(|| anyhow::anyhow!("role loadout '{loadout_name}' not found"))?;
if let Some(m) = &l.model {
return Ok(m.split('@').next().unwrap_or(m).to_string());
}
if let Some(p) = &l.provider {
if let Some(b) = cfg.backends.iter().find(|b| &b.name == p) {
return Ok(b.model.clone());
}
}
anyhow::bail!(
"role loadout '{loadout_name}' has no model (set `model` or a `provider` with a backend)"
)
}
fn names<'a>(keys: impl Iterator<Item = &'a String>) -> String {
let v: Vec<&str> = keys.map(String::as_str).collect();
if v.is_empty() {
"none".to_string()
} else {
v.join(", ")
}
}
pub(crate) fn worktree_id() -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("crew-{}-{nanos}", std::process::id())
}
fn render(o: &CrewOutcome, worktree: &Path) -> i32 {
match o.status {
CrewStatus::Passed => {
let touched = if o.touched.is_empty() {
"(none)".to_string()
} else {
o.touched.join(", ")
};
println!(
"✓ crew passed in {} attempt(s) — touched: {touched}\n worktree: {}",
o.attempts,
worktree.display()
);
0
}
CrewStatus::NeedsHumanReview => {
println!(
"⚠ crew needs human review — {} attempt(s) without a green check",
o.attempts
);
2
}
CrewStatus::VacuousVerify => {
println!(
"⚠ vacuous verify — the check passed on the unedited baseline, so a green \
cannot prove the change ({} attempt(s)); not landed",
o.attempts
);
2
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_author_prompt_steers_to_minimal_action_only_subtasks() {
assert!(PLAN_AUTHOR_SYSTEM.contains("FEWEST"));
assert!(PLAN_AUTHOR_SYSTEM.contains("MUST change code"));
assert!(PLAN_AUTHOR_SYSTEM.contains("Do NOT create separate"));
assert!(PLAN_AUTHOR_SYSTEM.contains("verifies EVERY subtask"));
}
#[test]
fn plan_author_prompt_forbids_rephrased_verify_subtasks_and_gives_a_worked_example() {
assert!(PLAN_AUTHOR_SYSTEM.contains("REPHRASINGS"));
assert!(PLAN_AUTHOR_SYSTEM.contains("is ONE subtask, not six"));
assert!(PLAN_AUTHOR_SYSTEM.contains("fix-duration-format"));
}
#[test]
fn github_refs_parses_issue_and_pr_urls_in_prose() {
let refs = github_refs(
"https://github.com/Gilamonster-Foundation/newt-agent/issues/548 <- take a look",
);
assert_eq!(
refs,
vec![(
"Gilamonster-Foundation".to_string(),
"newt-agent".to_string(),
"issues".to_string(),
"548".to_string(),
)]
);
let refs = github_refs("see https://github.com/o/r/pull/12).");
assert_eq!(refs[0].2, "pull");
assert_eq!(refs[0].3, "12");
assert!(github_refs("implement the thing").is_empty());
}
#[test]
fn one_shot_authority_grants_fs_keeps_exec_and_net_denied() {
use newt_core::role_profile::{ScopeKeyword, ScopeSpec};
let denied = ScopeSpec::Keyword(ScopeKeyword::None);
let all = ScopeSpec::Keyword(ScopeKeyword::All);
let mut plan = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\ninstruction = \"do a thing\"\n",
)
.unwrap();
assert_eq!(plan.subtasks[0].caveat_policy.fs_write, denied);
grant_one_shot_authority(&mut plan);
let pol = &plan.subtasks[0].caveat_policy;
assert_eq!(pol.fs_read, all, "fs_read granted");
assert_eq!(pol.fs_write, all, "fs_write granted");
assert_eq!(pol.exec, denied, "exec stays denied");
assert_eq!(pol.net, denied, "net stays denied");
}
#[test]
fn plan_preview_lists_leaves_and_their_deps() {
let plan = newt_core::plan::Plan::from_toml_str(
"goal = \"ship it\"\n\
[[subtask]]\nid=\"epic\"\ninstruction=\"big\"\n\
[[subtask]]\nid=\"a\"\ninstruction=\"do a\"\nparent=\"epic\"\n\
[[subtask]]\nid=\"b\"\ninstruction=\"do b\"\nparent=\"epic\"\ndeps=[\"a\"]\n",
)
.unwrap();
let preview = render_plan_preview(&plan);
assert!(preview.contains("goal: ship it"), "{preview}");
assert!(preview.contains("3 subtask(s); 2 leaf"), "{preview}");
assert!(preview.contains("• a — do a"), "{preview}");
assert!(preview.contains("• b — do b (after a)"), "{preview}");
assert!(
!preview.contains("• epic"),
"a branch is not a dispatch unit"
);
assert!(!preview.contains("will stall"), "a-leaf dep is satisfiable");
}
#[test]
fn plan_preview_flags_deps_that_will_stall() {
let plan = newt_core::plan::Plan::from_toml_str(
"[[subtask]]\nid=\"epic\"\ninstruction=\"branch\"\n\
[[subtask]]\nid=\"a\"\ninstruction=\"do a\"\nparent=\"epic\"\ndeps=[\"epic\"]\n\
[[subtask]]\nid=\"b\"\ninstruction=\"do b\"\nparent=\"epic\"\ndeps=[\"ghost\"]\n",
)
.unwrap();
let preview = render_plan_preview(&plan);
assert!(
preview.contains("epic [will stall]"),
"branch dep: {preview}"
);
assert!(
preview.contains("ghost [will stall]"),
"absent dep: {preview}"
);
}
#[test]
fn worktree_path_guard_refuses_absolute_and_dotdot() {
assert!(is_safe_worktree_path("src/lib.rs"));
assert!(is_safe_worktree_path("a/b/c.txt"));
assert!(!is_safe_worktree_path("/etc/passwd"), "absolute escapes");
assert!(
!is_safe_worktree_path("../../../etc/cron.d/x"),
".. escapes"
);
assert!(!is_safe_worktree_path("a/../../b"), "embedded .. escapes");
}
#[test]
fn worktree_read_is_confined_to_the_worktree() {
let repo = git_repo();
let ws =
WorktreeWorkspace::create(repo.path(), &worktree_id(), "HEAD", "true".into()).unwrap();
assert_eq!(ws.read("hello.txt").as_deref(), Some("world\n"));
assert!(ws.read("/etc/hostname").is_none(), "absolute read refused");
assert!(
ws.read("../../../../etc/hostname").is_none(),
".. read refused"
);
}
#[test]
fn parse_authored_plan_maps_json_to_a_default_deny_plan() {
let raw = "Sure! ```json\n{\"goal\":\"g\",\"subtasks\":[\
{\"id\":\"a\",\"instruction\":\"do a\",\"verify\":\"just check\"},\
{\"id\":\"b\",\"instruction\":\"do b\",\"deps\":[\"a\"]}]}\n``` (done)";
let plan = parse_authored_plan(raw).expect("parsed a plan");
assert_eq!(plan.goal.as_deref(), Some("g"));
assert_eq!(plan.subtasks.len(), 2);
assert_eq!(plan.subtasks[1].deps, vec!["a"]);
assert_eq!(plan.subtasks[0].verify.as_deref(), Some("just check"));
assert!(plan.subtasks[0].parent.is_none());
assert_eq!(
plan.subtasks[0].caveat_policy,
newt_core::plan::CaveatPolicy::default()
);
}
#[test]
fn parse_authored_plan_rejects_empty_or_unparseable() {
assert!(parse_authored_plan("no json at all").is_none());
assert!(
parse_authored_plan("{\"goal\":\"g\",\"subtasks\":[]}").is_none(),
"empty subtasks → not a usable plan"
);
assert!(parse_authored_plan("{not json}").is_none());
assert!(
parse_authored_plan(
"{\"subtasks\":[{\"id\":\"a\",\"instruction\":\"x\"},{\"id\":\"a\",\"instruction\":\"y\"}]}"
)
.is_none(),
"duplicate ids"
);
assert!(
parse_authored_plan("{\"subtasks\":[{\"id\":\" \",\"instruction\":\"x\"}]}").is_none(),
"blank id"
);
assert!(
parse_authored_plan("{\"subtasks\":[{\"id\":\"a\",\"instruction\":\"\"}]}").is_none(),
"empty instruction"
);
}
#[tokio::test]
async fn author_plan_decomposes_a_goal_via_the_model() {
struct PlanMock;
#[async_trait::async_trait]
impl Dispatcher for PlanMock {
async fn dispatch(
&self,
_b: &newt_scheduler::PoolBackend,
_m: &str,
_r: newt_scheduler::ChatRequest,
) -> anyhow::Result<newt_scheduler::ChatReply> {
Ok(newt_scheduler::ChatReply {
content: "```json\n{\"goal\":\"ship\",\"subtasks\":[\
{\"id\":\"a\",\"instruction\":\"do a\",\"verify\":\"cargo test\"},\
{\"id\":\"b\",\"instruction\":\"do b\",\"deps\":[\"a\"]}]}\n```"
.to_string(),
model_id: "m".into(),
usage: None,
})
}
}
let cfg: Config = toml::from_str(
"[[backends]]\nname=\"x\"\nendpoint=\"http://x:11434\"\nmodel=\"m\"\ntiers=[]\n",
)
.unwrap();
let pool = BackendPool::from_source(&StaticSource::from_configs(cfg.backends.iter()));
let plan = author_plan(&pool, &PlanMock, "m", "ship the thing", 8, None)
.await
.expect("authored a plan");
assert_eq!(plan.goal.as_deref(), Some("ship"));
assert_eq!(plan.subtasks.len(), 2);
assert_eq!(plan.subtasks[1].deps, vec!["a"]);
assert_eq!(
plan.subtasks[0].caveat_policy,
newt_core::plan::CaveatPolicy::default()
);
}
#[test]
fn effective_markers_composes_remove_then_add() {
let cfg = newt_core::PlanPruneConfig {
disabled: false,
add_inspect: vec!["Scrutinize".into(), " ".into(), "verify".into()],
add_gate: vec!["smoke".into()],
remove: vec!["REVIEW".into(), "verify".into()],
};
let m = effective_markers(Some(&cfg));
assert!(
marker_kind_in(&m, "Review the code for style").is_none(),
"removed builtin no longer marks"
);
assert!(
marker_kind_in(&m, "verify the output").is_none(),
"a removed verb cannot be re-added"
);
assert!(matches!(
marker_kind_in(&m, "Scrutinize the module"),
Some(MarkerKind::Inspect)
));
assert!(matches!(
marker_kind_in(&m, "smoke the build"),
Some(MarkerKind::Gate)
));
assert!(matches!(
marker_kind_in(&m, "inspect the parser"),
Some(MarkerKind::Inspect)
));
let d = effective_markers(None);
assert_eq!(d.len(), ACTION_MARKERS.len());
}
#[test]
fn prune_respects_the_config_lexicon() {
let cfg = newt_core::PlanPruneConfig {
add_inspect: vec!["scrutinize".into()],
..Default::default()
};
let mut plan = marker_plan(vec![
marker_sub("pad", "Scrutinize the module layout", &[]),
marker_sub("work", "Add the parser", &["pad"]),
]);
prune_non_actionable_subtasks_in(&mut plan, &effective_markers(Some(&cfg)));
let ids: Vec<&str> = plan.subtasks.iter().map(|s| s.id.as_str()).collect();
assert_eq!(ids, vec!["work"], "custom inspect verb pruned, dep rewired");
assert!(plan.subtasks[0].deps.is_empty());
let mut plan2 = marker_plan(vec![
marker_sub("pad", "Scrutinize the module layout", &[]),
marker_sub("work", "Add the parser", &["pad"]),
]);
prune_non_actionable_subtasks_in(&mut plan2, &effective_markers(None));
assert_eq!(plan2.subtasks.len(), 2, "defaults do not know the verb");
}
#[tokio::test]
async fn author_plan_with_disabled_prune_keeps_padding_leaves() {
struct PaddedPlanMock;
#[async_trait::async_trait]
impl Dispatcher for PaddedPlanMock {
async fn dispatch(
&self,
_b: &newt_scheduler::PoolBackend,
_m: &str,
_r: newt_scheduler::ChatRequest,
) -> anyhow::Result<newt_scheduler::ChatReply> {
Ok(newt_scheduler::ChatReply {
content: "{\"goal\":\"g\",\"subtasks\":[\
{\"id\":\"look\",\"instruction\":\"Inspect the module\"},\
{\"id\":\"work\",\"instruction\":\"Add the fix\",\"deps\":[\"look\"]}]}"
.to_string(),
model_id: "m".into(),
usage: None,
})
}
}
let cfg: Config = toml::from_str(
"[[backends]]\nname=\"x\"\nendpoint=\"http://x:11434\"\nmodel=\"m\"\ntiers=[]\n",
)
.unwrap();
let pool = BackendPool::from_source(&StaticSource::from_configs(cfg.backends.iter()));
let disabled = newt_core::PlanPruneConfig {
disabled: true,
..Default::default()
};
let kept = author_plan(&pool, &PaddedPlanMock, "m", "g", 8, Some(&disabled))
.await
.expect("authored");
assert_eq!(kept.subtasks.len(), 2, "disabled ⇒ padding survives");
let pruned = author_plan(&pool, &PaddedPlanMock, "m", "g", 8, None)
.await
.expect("authored");
assert_eq!(pruned.subtasks.len(), 1, "default ⇒ padding pruned");
assert_eq!(pruned.subtasks[0].id, "work");
assert!(pruned.subtasks[0].deps.is_empty(), "dep rewired");
}
fn marker_sub(id: &str, instruction: &str, deps: &[&str]) -> newt_core::plan::Subtask {
newt_core::plan::Subtask {
id: id.into(),
instruction: instruction.into(),
deps: deps.iter().map(|s| (*s).to_string()).collect(),
parallel_ok: false,
context: vec![],
verify: None,
status: newt_core::plan::SubtaskStatus::Pending,
result: None,
parent: None,
caveat_policy: newt_core::plan::CaveatPolicy::default(),
}
}
fn marker_plan(subs: Vec<newt_core::plan::Subtask>) -> newt_core::plan::Plan {
newt_core::plan::Plan {
goal: None,
aggregation: newt_core::plan::Aggregation::default(),
subtasks: subs,
}
}
#[test]
fn prune_drops_inspect_and_terminal_gate_rewiring_deps() {
let mut plan = marker_plan(vec![
marker_sub(
"inspect-test",
"Inspect the existing test to understand the failure",
&[],
),
marker_sub(
"fix",
"Modify humanize_duration to return \"1m 30s\"",
&["inspect-test"],
),
marker_sub(
"validate",
"Verify the tests pass with cargo test",
&["fix"],
),
]);
prune_non_actionable_subtasks_in(&mut plan, &effective_markers(None));
let ids: Vec<&str> = plan.subtasks.iter().map(|s| s.id.as_str()).collect();
assert_eq!(
ids,
vec!["fix"],
"inspect + terminal gate pruned, fix survives"
);
assert!(plan.subtasks[0].deps.is_empty(), "dangling dep removed");
}
#[test]
fn prune_keeps_a_gate_a_real_leaf_depends_on() {
let mut plan = marker_plan(vec![
marker_sub("extract", "Extract the sum into a helper", &[]),
marker_sub("check", "Validate the helper compiles", &["extract"]),
marker_sub("use", "Rewrite summarize to call the helper", &["check"]),
]);
prune_non_actionable_subtasks_in(&mut plan, &effective_markers(None));
let ids: Vec<&str> = plan.subtasks.iter().map(|s| s.id.as_str()).collect();
assert_eq!(
ids,
vec!["extract", "check", "use"],
"mid-plan gate retained"
);
}
#[test]
fn prune_leaves_an_all_marker_plan_untouched() {
let mut plan = marker_plan(vec![
marker_sub("a", "Inspect the module", &[]),
marker_sub("b", "Verify the build", &["a"]),
]);
prune_non_actionable_subtasks_in(&mut plan, &effective_markers(None));
assert_eq!(plan.subtasks.len(), 2, "all-marker plan untouched");
}
#[test]
fn prune_is_a_noop_on_a_plan_of_real_work() {
let mut plan = marker_plan(vec![
marker_sub("a", "Add error handling to parse_port", &[]),
marker_sub("b", "Rename the helper", &["a"]),
]);
prune_non_actionable_subtasks_in(&mut plan, &effective_markers(None));
let ids: Vec<&str> = plan.subtasks.iter().map(|s| s.id.as_str()).collect();
assert_eq!(ids, vec!["a", "b"]);
assert_eq!(plan.subtasks[1].deps, vec!["a"]);
}
#[test]
fn ground_subtask_scopes_unions_def_sites_with_declared_files() {
let mut plan = marker_plan(vec![
marker_sub("fix", "Fix `humanize_duration` in the util module", &[]),
marker_sub("new", "Create the brand-new reporting module", &[]),
]);
plan.subtasks[0].context =
vec!["tests/util_test.rs".to_string(), "src/util.rs".to_string()];
plan.subtasks[1].context = vec!["src/report.rs".to_string()];
let def_sites = |sym: &str| -> Vec<String> {
if sym == "humanize_duration" {
vec!["src/util.rs:2:pub fn humanize_duration".to_string()]
} else {
Vec::new()
}
};
ground_subtask_scopes(&mut plan, &def_sites);
assert_eq!(
plan.subtasks[0].context,
vec!["src/util.rs".to_string(), "tests/util_test.rs".to_string()],
"derived seam leads; declared companion appended; dup deduped"
);
assert_eq!(
plan.subtasks[1].context,
vec!["src/report.rs".to_string()],
"no def-site found → the declared file survives alone"
);
}
#[test]
fn parse_authored_plan_reads_declared_files_into_context() {
let raw = r#"{"goal":"g","subtasks":[
{"id":"a","instruction":"Fix util","files":["src/util.rs"," "],"deps":[]},
{"id":"b","instruction":"Add docs","deps":["a"]}
]}"#;
let plan = parse_authored_plan(raw).expect("parses");
assert_eq!(plan.subtasks[0].context, vec!["src/util.rs".to_string()]);
assert!(plan.subtasks[1].context.is_empty());
}
#[test]
fn crew_shared_target_is_a_single_absolute_per_root_dir() {
#[cfg(windows)]
let root = Path::new(r"C:\tmp\throw");
#[cfg(not(windows))]
let root = Path::new("/tmp/throw");
let a = crew_shared_target_dir(root);
assert!(a.ends_with(".scratch/crew-target"), "{a:?}");
assert!(a.is_absolute(), "must be absolute: {a:?}");
assert_eq!(crew_shared_target_dir(root), a);
}
#[test]
fn plan_sanity_flags_dangling_deps_and_passes_clean_plans() {
let ok = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\ninstruction = \"do a\"\n\
[[subtask]]\nid = \"b\"\ninstruction = \"do b\"\ndeps = [\"a\"]\n",
)
.unwrap();
assert!(plan_sanity(&ok).is_empty(), "{:?}", plan_sanity(&ok));
let bad = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"b\"\ninstruction = \"do b\"\ndeps = [\"ghost\"]\n",
)
.unwrap();
let probs = plan_sanity(&bad);
assert!(probs.iter().any(|p| p.contains("ghost")), "{probs:?}");
}
#[test]
fn claim_check_refutes_a_subtask_targeting_the_wrong_file() {
let plan = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\n\
instruction = \"In newt-cli/src/crew.rs, modify the `help_lines` function\"\n",
)
.unwrap();
let def_sites = |sym: &str| -> Vec<String> {
if sym == "help_lines" {
vec!["newt-tui/src/lib.rs:8273:fn help_lines() {".to_string()]
} else {
vec![]
}
};
let c = plan_grounding_contradictions(&plan, def_sites);
assert!(
c.iter()
.any(|p| p.contains("help_lines") && p.contains("newt-tui/src/lib.rs")),
"must cite the real def site: {c:?}"
);
}
#[test]
fn claim_check_refutes_an_unquoted_symbol_in_the_wrong_file() {
let plan = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\n\
instruction = \"Refactor the help_lines function in newt-cli/src/crew.rs\"\n",
)
.unwrap();
let def_sites = |sym: &str| -> Vec<String> {
if sym == "help_lines" {
vec!["newt-tui/src/lib.rs:8273:fn help_lines() {".to_string()]
} else {
vec![]
}
};
let c = plan_grounding_contradictions(&plan, def_sites);
assert!(
c.iter()
.any(|p| p.contains("help_lines") && p.contains("newt-tui")),
"unquoted symbol must now be refuted: {c:?}"
);
}
#[test]
fn claim_check_passes_when_the_claimed_file_matches_the_def() {
let plan = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\n\
instruction = \"In newt-tui/src/lib.rs, modify `help_lines`\"\n",
)
.unwrap();
let def_sites = |_: &str| vec!["newt-tui/src/lib.rs:8273:fn help_lines() {".to_string()];
assert!(plan_grounding_contradictions(&plan, def_sites).is_empty());
}
#[test]
fn claim_check_never_refutes_a_new_symbol_or_pathless_step() {
let p1 = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\ninstruction = \"create `new_thing` in src/new.rs\"\n",
)
.unwrap();
assert!(plan_grounding_contradictions(&p1, |_| vec![]).is_empty());
let p2 = newt_core::plan::Plan::from_toml_str(
"goal = \"g\"\n[[subtask]]\nid = \"a\"\ninstruction = \"refactor `help_lines`\"\n",
)
.unwrap();
let def = |_: &str| vec!["newt-tui/src/lib.rs:8273:fn help_lines() {".to_string()];
assert!(plan_grounding_contradictions(&p2, def).is_empty());
}
#[test]
fn path_tokens_extracts_file_paths_only() {
assert_eq!(
path_tokens("edit newt-cli/src/crew.rs and call `help_lines`, not foo"),
vec!["newt-cli/src/crew.rs".to_string()]
);
assert!(path_tokens("just a sentence with no paths").is_empty());
}
#[test]
fn repo_context_detects_language_layout_and_skips_non_repo() {
let repo = git_repo();
std::fs::write(repo.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
std::fs::write(repo.path().join("main.rs"), "fn main() {}\n").unwrap();
git(repo.path(), &["add", "-A"]).unwrap();
git(repo.path(), &["commit", "-qm", "add cargo"]).unwrap();
let ctx = fetch_repo_context(repo.path());
assert!(ctx.contains("Rust"), "detects Rust: {ctx}");
assert!(
ctx.contains("cargo test"),
"infers the build command: {ctx}"
);
assert!(ctx.contains("Cargo.toml"), "lists top-level entries: {ctx}");
let empty = tempfile::tempdir().unwrap();
assert!(fetch_repo_context(empty.path()).is_empty());
}
fn git_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let p = dir.path();
for args in [
vec!["init", "-q"],
vec!["config", "user.email", "t@t"],
vec!["config", "user.name", "t"],
vec!["config", "core.autocrlf", "false"],
] {
git(p, &args).unwrap();
}
std::fs::write(p.join("hello.txt"), "world\n").unwrap();
git(p, &["add", "-A"]).unwrap();
git(p, &["commit", "-qm", "init"]).unwrap();
dir
}
#[test]
fn infer_test_command_priority() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(infer_test_command(dir.path()), None, "no markers → None");
std::fs::write(dir.path().join("pyproject.toml"), "").unwrap();
assert_eq!(infer_test_command(dir.path()).as_deref(), Some("pytest -x"));
std::fs::write(dir.path().join("Cargo.toml"), "").unwrap();
assert_eq!(
infer_test_command(dir.path()).as_deref(),
Some("cargo test")
);
std::fs::write(dir.path().join("justfile"), "").unwrap();
assert_eq!(
infer_test_command(dir.path()).as_deref(),
Some("just check")
);
}
#[test]
fn crew_normalize_commands_come_from_tooling_packs() {
let dir = tempfile::tempdir().unwrap();
assert!(crew_normalize_commands(dir.path()).is_empty());
std::fs::write(dir.path().join("Cargo.toml"), "").unwrap();
assert!(crew_normalize_commands(dir.path()).contains(&"cargo fmt".to_string()));
}
#[test]
fn commit_to_branch_lands_work_visible_to_base() {
let repo = git_repo();
let mut ws =
WorktreeWorkspace::create(repo.path(), "land1", "HEAD", "true".into()).unwrap();
ws.apply(&[Edit {
path: "added.rs".into(),
new_content: "pub fn f() {}\n".into(),
}]);
let (branch, sha) = ws
.commit_to_branch("crew/land1", "newt", "newt@bot", "land it")
.unwrap();
assert_eq!(branch, "crew/land1");
assert!(!sha.is_empty());
drop(ws);
let files = git(repo.path(), &["ls-tree", "-r", "--name-only", "crew/land1"]).unwrap();
assert!(
files.lines().any(|l| l == "added.rs"),
"branch carries the work: {files}"
);
assert!(
!repo.path().join("added.rs").exists(),
"base tree untouched until merge"
);
}
#[test]
fn leaf_chains_off_prior_landed_tip() {
let repo = git_repo();
let mut wsa = WorktreeWorkspace::create(repo.path(), "a", "HEAD", "true".into()).unwrap();
wsa.apply(&[Edit {
path: "a.txt".into(),
new_content: "A\n".into(),
}]);
let (_a, sha_a) = wsa.commit_to_branch("crew/a", "n", "n@b", "a").unwrap();
drop(wsa);
let mut wsb = WorktreeWorkspace::create(repo.path(), "b", &sha_a, "true".into()).unwrap();
assert!(
wsb.read("a.txt").is_some(),
"leaf B forked off A's tip sees A's file"
);
wsb.apply(&[Edit {
path: "b.txt".into(),
new_content: "B\n".into(),
}]);
wsb.commit_to_branch("crew/b", "n", "n@b", "b").unwrap();
drop(wsb);
let files = git(repo.path(), &["ls-tree", "-r", "--name-only", "crew/b"]).unwrap();
assert!(
files.lines().any(|l| l == "a.txt"),
"consolidated tip has a.txt: {files}"
);
assert!(
files.lines().any(|l| l == "b.txt"),
"consolidated tip has b.txt: {files}"
);
}
#[test]
fn commit_to_branch_errs_with_no_changes() {
let repo = git_repo();
let ws = WorktreeWorkspace::create(repo.path(), "land2", "HEAD", "true".into()).unwrap();
assert!(
ws.commit_to_branch("crew/land2", "n", "n@b", "noop")
.is_err(),
"no changes → nothing to land"
);
}
#[test]
fn worktree_isolates_reads_and_writes() {
let repo = git_repo();
let mut ws = WorktreeWorkspace::create(repo.path(), "t1", "HEAD", "true".into()).unwrap();
assert!(ws.files().iter().any(|f| f == "hello.txt"));
assert_eq!(
ws.read("hello.txt").as_deref().map(str::trim_end),
Some("world")
);
assert_eq!(ws.read("nope.txt"), None);
let written = ws.apply(&[Edit {
path: "src/new.rs".into(),
new_content: "fn main() {}\n".into(),
}]);
assert_eq!(written, vec!["src/new.rs".to_string()]);
assert_eq!(ws.read("src/new.rs").as_deref(), Some("fn main() {}\n"));
assert!(
!repo.path().join("src/new.rs").exists(),
"edit must NOT touch the live tree"
);
}
#[cfg(unix)]
#[test]
fn run_test_passes_and_reports_failure() {
let repo = git_repo();
let ok = WorktreeWorkspace::create(repo.path(), "t2a", "HEAD", "test -f hello.txt".into())
.unwrap();
assert!(ok.run_test().0, "committed file present → pass");
let bad = WorktreeWorkspace::create(repo.path(), "t2b", "HEAD", "exit 3".into()).unwrap();
assert!(!bad.run_test().0, "non-zero exit → fail");
}
#[test]
fn cleanup_removes_the_worktree() {
let repo = git_repo();
let path = {
let ws = WorktreeWorkspace::create(repo.path(), "t3", "HEAD", "true".into()).unwrap();
let p = ws.path().to_path_buf();
assert!(p.exists());
p
};
assert!(!path.exists(), "Drop removed the worktree");
}
fn crew_cfg() -> Config {
toml::from_str(
r#"
[[backends]]
name = "p"
endpoint = "http://p:11434"
model = "planner-m"
tiers = []
[[backends]]
name = "n"
endpoint = "http://n:11434"
model = "nav-m"
tiers = []
[[backends]]
name = "t"
endpoint = "http://t:11434"
model = "triage-m"
tiers = []
[loadouts.planner]
provider = "p"
[loadouts.navigator]
provider = "n"
[loadouts.triage]
provider = "t"
[crews.coder]
planner = "planner"
navigator = "navigator"
triage = "triage"
"#,
)
.unwrap()
}
#[test]
fn resolve_crew_name_explicit_single_none_multiple() {
let cfg = crew_cfg();
assert_eq!(resolve_crew_name(&cfg, Some("coder")).unwrap(), "coder");
assert_eq!(resolve_crew_name(&cfg, None).unwrap(), "coder"); assert!(resolve_crew_name(&cfg, Some("ghost"))
.unwrap_err()
.to_string()
.contains("no crew named 'ghost'"));
let empty = Config::default();
assert!(resolve_crew_name(&empty, None)
.unwrap_err()
.to_string()
.contains("no crews defined"));
}
#[test]
fn model_for_role_from_provider_backend_and_missing() {
let cfg = crew_cfg();
assert_eq!(model_for_role(&cfg, "planner").unwrap(), "planner-m");
assert!(model_for_role(&cfg, "ghost").is_err());
}
struct RoleMock;
#[async_trait::async_trait]
impl Dispatcher for RoleMock {
async fn dispatch(
&self,
_backend: &newt_scheduler::PoolBackend,
model: &str,
_req: newt_scheduler::ChatRequest,
) -> anyhow::Result<newt_scheduler::ChatReply> {
let content = match model {
"nav-m" => r#"{"relevant_files": ["marker.txt"]}"#,
"planner-m" => r#"{"edits": [{"path": "FIXED.txt", "new_content": "ok\n"}]}"#,
"triage-m" => r#"{"summary": "missing file", "next_action": "create it"}"#,
_ => "{}",
};
Ok(newt_scheduler::ChatReply {
content: content.to_string(),
model_id: model.to_string(),
usage: None,
})
}
}
#[cfg(unix)] #[tokio::test]
async fn crew_converges_with_a_fixing_planner() {
let repo = git_repo();
let cfg = crew_cfg();
let args = CrewArgs {
task: "make the check pass".into(),
crew: Some("coder".into()),
dir: Some(repo.path().to_path_buf()),
test: Some("test -f FIXED.txt".into()),
max_attempts: Some(2),
dry_run: false,
};
let code = run_with(&cfg, args, &RoleMock).await.unwrap();
assert_eq!(code, 0, "planner's edit creates FIXED.txt → verify passes");
}
#[tokio::test]
async fn crew_dry_run_resolves_without_touching_the_repo() {
let repo = git_repo();
let cfg = crew_cfg();
let args = CrewArgs {
task: "noop".into(),
crew: Some("coder".into()),
dir: Some(repo.path().to_path_buf()),
test: Some("true".into()),
max_attempts: None,
dry_run: true,
};
let code = run_with(&cfg, args, &RoleMock).await.unwrap();
assert_eq!(code, 0);
assert!(!repo.path().join(".scratch/worktrees").exists());
}
#[test]
fn grounding_surfaces_the_definition_even_when_decoys_sort_first() {
let blocks = vec![GroundingBlock {
term: "help_lines".to_string(),
defs: vec![
"newt-tui/src/lib.rs:8273:fn help_lines() -> &'static [&'static str] {".to_string(),
],
mentions: vec![
"newt-cli/src/crew.rs:100: // help_lines rolls up the dgx block".to_string(),
"newt-cli/src/crew.rs:200: assert!(out.contains(\"help_lines\"));".to_string(),
"newt-cli/src/crew.rs:300: let _ = help_lines_marker;".to_string(),
],
}];
let out = format_grounding_hits(&blocks);
assert!(
out.contains("newt-tui/src/lib.rs:8273") && out.contains("[def]"),
"the real definition must be surfaced and marked: {out}"
);
}
#[test]
fn grounding_never_drops_a_definition_under_the_budget() {
let blocks: Vec<GroundingBlock> = (0..30)
.map(|i| GroundingBlock {
term: format!("sym{i}"),
defs: vec![format!("src/a.rs:{i}:fn sym{i}() {{")],
mentions: (0..8)
.map(|j| format!("src/b.rs:{j}:// sym{i} mention {j}"))
.collect(),
})
.collect();
let out = format_grounding_hits(&blocks);
assert!(
out.contains("[def]"),
"definitions surface under the budget: {out}"
);
let first = out
.lines()
.find(|l| l.trim_start().starts_with("sym"))
.unwrap();
assert!(
first.contains("[def]"),
"first hit must be a definition: {first}"
);
}
#[test]
fn empty_blocks_yield_empty_grounding() {
assert!(format_grounding_hits(&[]).is_empty());
}
#[test]
fn unresolved_symbol_parses_rustc_errors() {
assert_eq!(
unresolved_symbol("error[E0425]: cannot find function `help_lines` in this scope"),
Some("help_lines".to_string())
);
assert_eq!(
unresolved_symbol("error[E0599]: no method named `roll_up` found for struct"),
Some("roll_up".to_string())
);
assert_eq!(unresolved_symbol("error: mismatched types"), None);
}
}