use std::path::Path;
use std::sync::Arc;
use speccoder_core::stage::{Stage, StageState};
use speccoder_core::{CommandData, CommandResult, CommandStatus};
use crate::pipeline::*;
use crate::{
approvals, attempts, external, git, integrity, mcp, mutation, progress, resume, sandbox,
substrate, tools, verify, worktree,
};
#[must_use]
pub fn run(path: &Path, feature: Option<&str>, task: &str, options: &RunOptions) -> CommandResult {
let Some(state) = worktree::state_dir() else {
return CommandResult::new(
"run",
CommandStatus::ConfigError,
"no state directory (set SPECCODER_STATE_DIR or HOME)",
)
.with_code("NO-STATE-DIR");
};
let model = match setup(path, feature, &state, "run") {
Ok(s) => s.model,
Err(r) => return *r,
};
let (driver, key) = match substrate::model_config::open_driver(&model) {
Ok((d, k)) => (Ok::<_, String>(d), k.unwrap_or_default()),
Err(e) => return substrate::model_config::refused("run", &e),
};
match driver {
Ok(provider) => run_task(path, feature, task, &state, provider, Some(&key), options),
Err(e) => CommandResult::new("run", CommandStatus::ConfigError, &redact(&e, Some(&key))),
}
}
pub fn run_with(
path: &Path,
feature: Option<&str>,
task: &str,
state: &Path,
provider: std::sync::Arc<dyn agentplane::model::ModelProvider>,
secret: Option<&str>,
) -> CommandResult {
run_task(
path,
feature,
task,
state,
provider,
secret,
&RunOptions::default(),
)
}
#[must_use]
pub fn run_with_options(
path: &Path,
feature: Option<&str>,
task: &str,
state: &Path,
provider: Arc<dyn agentplane::model::ModelProvider>,
secret: Option<&str>,
options: &RunOptions,
) -> CommandResult {
run_task(path, feature, task, state, provider, secret, options)
}
#[must_use]
pub fn run_with_approver(
path: &Path,
feature: Option<&str>,
task: &str,
state: &Path,
provider: Arc<dyn agentplane::model::ModelProvider>,
secret: Option<&str>,
approver: Arc<dyn approvals::Approver>,
) -> CommandResult {
let options = RunOptions {
approver: Some(approver),
..RunOptions::default()
};
run_task(path, feature, task, state, provider, secret, &options)
}
#[must_use]
pub fn run_native_bug(
root: &Path,
bug: &speccoder_core::bug::BugReport,
state: &Path,
provider: Arc<dyn agentplane::model::ModelProvider>,
secret: Option<&str>,
options: &RunOptions,
) -> CommandResult {
let options = RunOptions {
bug_home: Some(Arc::new(super::bug::NativeBug::of(bug))),
..options.clone()
};
run_task(root, None, &bug.task, state, provider, secret, &options)
}
pub(crate) fn run_task(
path: &Path,
feature: Option<&str>,
task: &str,
state: &Path,
provider: std::sync::Arc<dyn agentplane::model::ModelProvider>,
secret: Option<&str>,
options: &RunOptions,
) -> CommandResult {
let first = run_task_once(
path,
feature,
task,
state,
Arc::clone(&provider),
secret,
options,
);
match first.data() {
Some(CommandData::Run(r)) if r.attempt_status == CHANGED_CODE => {
if let Some(p) = &options.progress {
p.event(&progress::Event::Notice {
text: format!("{task} changed code, so it runs as code"),
});
}
run_task_once(path, feature, task, state, provider, secret, options)
}
_ => first,
}
}
pub const CHANGED_CODE: &str = "planning_changed_code";
#[allow(clippy::too_many_lines)] fn run_task_once(
path: &Path,
feature: Option<&str>,
task: &str,
state: &Path,
provider: std::sync::Arc<dyn agentplane::model::ModelProvider>,
secret: Option<&str>,
options: &RunOptions,
) -> CommandResult {
use speccoder_core::RunReport;
if options.in_place && options.session.is_none() {
return in_session("run", path, feature, state, options, |o| {
run_task(path, feature, task, state, Arc::clone(&provider), secret, o)
});
}
let approver = options.approver.clone();
use speccoder_core::snapshot::TaskState;
let refuse =
|status: CommandStatus, m: String| CommandResult::new("run", status, &redact(&m, secret));
let s = match setup(path, feature, state, "run") {
Ok(s) => s,
Err(r) => return *r,
};
let native = options.bug_home.clone();
let feature_name = match (&native, s.snapshot.feature.as_ref()) {
(Some(n), _) => n.unit.clone(),
(None, Some(f)) => f.feature.name.clone(),
(None, None) => {
return refuse(
CommandStatus::SelectionNeeded,
"no feature selected; pass --feature".to_owned(),
);
}
};
let plane_dir = match &options.lane {
Some(lane) => s.plane_dir.join("lanes").join(lane),
None => s.plane_dir.clone(),
};
let live: Arc<dyn progress::Progress> = Arc::new(progress::Live::new(
&plane_dir,
s.model.prices,
options.progress.clone(),
));
let wt = match &native {
Some(n) => crate::commands::worktree_of(&s.snapshot, &n.unit, &n.dir, state),
None => workspace(path, Some(&feature_name), state, options),
};
let Some(CommandData::Worktree(w)) = wt.data() else {
return CommandResult::carried("run", &wt);
};
if wt.status() != CommandStatus::Ok {
return CommandResult::carried("run", &wt).with_data(CommandData::Worktree(w.clone()));
}
let (Some(wt_path), Some(project_dir)) = (w.path.clone(), w.project_dir.clone()) else {
return refuse(CommandStatus::Error, "worktree without a path".to_owned());
};
let (wt_path, project_dir) = match &options.lane {
None => (wt_path, project_dir),
Some(lane) => match lane_worktree(path, state, &feature_name, lane, &wt_path, &project_dir)
{
Ok(paths) => paths,
Err(e) => return refuse(CommandStatus::GitPrecondition, e),
},
};
let project = std::path::PathBuf::from(&project_dir);
let wt_root = std::path::PathBuf::from(&wt_path);
super::bisect::prune(&wt_root, &s.plane_dir.join("tmp"));
let tmp = plane_dir.join("tmp");
let elsewhere = if options.lane.is_none() {
match crate::approvals::waiting_elsewhere(&project, &plane_dir, &feature_name, task) {
Ok(e) => e,
Err(e) => {
return refuse(
CommandStatus::Error,
format!(
"the calls waiting for a person could not be read, so whether another task's attempt holds the worktree is unknown: {e}"
),
);
}
}
} else {
crate::approvals::Elsewhere::default()
};
if let Some((other, covers)) = elsewhere.waiting {
return refuse(
CommandStatus::NeedsHuman,
format!(
"{other} waits for a person ({covers}) and its attempt holds the worktree: decide it (`speccoder task approve {other}` or `reject`), then `speccoder run --task {other}`"
),
);
}
if options.lane.is_none()
&& let Some(other) = crate::strength::held_elsewhere(&plane_dir, &feature_name, task)
{
return refuse(
CommandStatus::NeedsHuman,
format!(
"{other}'s acceptance tests reject an implementation the spec appears to allow, and its attempt holds the worktree: `speccoder task rule {other} --tests-right --reason …` or `speccoder run --task {other} --fresh-acceptance`"
),
)
.with_code("TESTS-OVER-STRICT");
}
let commits_per_task = verify::find_config(&project)
.and_then(|root| std::fs::read_to_string(root.join(verify::config::FILE)).ok())
.and_then(|t| substrate::run_config::parse(&t).ok())
.is_none_or(|c| c.commit_per_task);
let marker_path = resume::path(&plane_dir, &feature_name, task);
if options.fresh_acceptance && options.lane.is_none() {
let _ = std::fs::remove_file(&marker_path);
crate::strength::release(&crate::strength::dir(&s.plane_dir, &feature_name, task));
}
let head_now = git::Git::at(&wt_root)
.line(&["rev-parse", "HEAD"])
.unwrap_or_default();
let autopilot = options.mode == speccoder_core::oracle::RunMode::Autopilot;
let interrupted = resume::load(&marker_path).filter(|m| {
m.head == head_now && options.lane.is_none() && !head_now.is_empty() && !autopilot
});
if let Some(m) = &interrupted {
if let (Some(id), Ok(mut run)) = (&m.run_id, options.run_id.lock()) {
*run = crate::pipeline::RunId(id.clone());
}
if let Ok(mut asked) = options.asked.lock() {
*asked = (*asked).max(m.asked);
}
if let Ok(mut g) = options.run_grants.lock() {
for o in &m.run_grants {
if !g.contains(o) {
g.push(o.clone());
}
}
}
}
let marker_guard = resume::Guard::new(marker_path.clone());
let resumed_from = interrupted.as_ref().map(|_| {
std::fs::read_to_string(plane_dir.join(progress::FILE))
.ok()
.and_then(|t| serde_json::from_str::<progress::LiveState>(&t).ok())
.filter(|l| l.task == task)
.map_or((None, None), |l| (l.stage, l.last_call))
});
let mut set_aside = None;
if interrupted.is_none()
&& commits_per_task
&& options.session.is_none()
&& worktree::require_clean(&wt_root).is_err()
{
let left = worktree::leftovers(&wt_root);
if !left.is_empty() {
let mut what = Vec::new();
if !left.commit.is_empty() {
what.push(format!("{} is not committed", left.commit.join(", ")));
}
if !left.ignore.is_empty() {
what.push(format!("{} is not git-ignored", left.ignore.join(", ")));
}
let checkout = std::path::Path::new(&s.snapshot.root);
let remedy = crate::code::setup_remedy(checkout, Some(&feature_name));
let command = remedy
.as_ref()
.map_or_else(|| left.fix(checkout, &wt_root, &[]), |r| r.command.clone());
return refuse(
CommandStatus::GitPrecondition,
format!(
"{} — the checks write it on every run, so each run would find it in the worktree and set it aside",
what.join(" and "),
),
)
.with_code("SETUP-INCOMPLETE")
.with_fix(vec!["speccoder fix".to_owned(), command])
.with_remedy(remedy);
}
match set_aside_leftovers(&wt_root, &tmp, &feature_name, task) {
Ok(reference) => set_aside = Some(reference),
Err(e) => return refuse(CommandStatus::GitPrecondition, e),
}
}
let format = crate::format::at(&project);
let Some(snap) = format.inspect(
&project,
if native.is_some() {
None
} else {
Some(&feature_name)
},
) else {
return refuse(
CommandStatus::NotAProject,
format.messages().no_project_in_worktree.to_owned(),
);
};
if native.is_some()
&& crate::bug::state::load(&s.plane_dir, &feature_name, task)
.is_some_and(|b| b.outcome == speccoder_core::bug::Outcome::Fixed)
{
return refuse(
CommandStatus::TaskNotReady,
format!("{task} is already fixed"),
)
.with_code("TASK-DONE");
}
if native.is_none() {
let Some(st) = snap
.feature
.as_ref()
.and_then(|f| f.schedule.as_ref())
.and_then(|sch| sch.tasks.iter().find(|t| t.id == task))
else {
return refuse(
CommandStatus::NotFound,
format!("no task {task} in {feature_name}"),
);
};
match st.state {
TaskState::Done => {
return refuse(
CommandStatus::TaskNotReady,
format!("{task} is already done"),
)
.with_code("TASK-DONE");
}
TaskState::Blocked => {
return refuse(
CommandStatus::TaskNotReady,
format!("{task} is blocked by {}", st.blocked_by.join(", ")),
)
.with_code("TASK-BLOCKED");
}
TaskState::Ready => {}
}
}
let checklists_overridden = if native.is_some() {
false
} else {
match checklist_gate(&project, Some(&feature_name), options.ignore_checklists) {
Ok(overridden) => overridden,
Err(stop) => return *stop,
}
};
let parsed_task = snap
.feature
.as_ref()
.and_then(|f| f.tasks.as_ref())
.and_then(|t| t.tasks.iter().find(|t| t.id == task).cloned());
let tasks_file = snap
.feature
.as_ref()
.and_then(|f| f.tasks.as_ref())
.map(|t| project.join(&t.path));
let bundle = match native
.as_ref()
.map(|n| n.bundle(&project))
.map_or_else(|| format.bundle(&snap, task), Ok)
{
Ok(b) => b,
Err(_) => {
return refuse(
CommandStatus::NotFound,
format!("no task {task} in the worktree's {feature_name}"),
);
}
};
let acceptance_story = native
.is_none()
.then(|| crate::acceptance_at::story_at(&s.plane_dir, &feature_name, task))
.flatten();
let story_task: Option<String> = acceptance_story.as_ref().and_then(|st| {
snap.feature
.as_ref()
.and_then(|f| f.tasks.as_ref())
.and_then(|t| {
t.tasks
.iter()
.find(|t| t.story.as_deref() == Some(st.as_str()))
})
.map(|t| t.id.clone())
});
let bundle = match (&acceptance_story, &story_task) {
(Some(st), Some(other)) => {
let mut b = bundle;
if let Ok(of) = format.bundle(&snap, other) {
b.story = of.story;
}
b.task.story.clone_from(st);
b
}
_ => bundle,
};
let Some(config_root) = verify::find_config(&project) else {
return refuse(
CommandStatus::ConfigError,
"the worktree has no speccoder.toml (is it committed?)".to_owned(),
)
.with_code("CONFIG-MISSING");
};
let config_text =
std::fs::read_to_string(config_root.join(verify::config::FILE)).unwrap_or_default();
let run_config = match substrate::run_config::parse(&config_text) {
Ok(c) => c,
Err(e) => {
return refuse(CommandStatus::ConfigError, format!("speccoder.toml: {e}"))
.with_code("CONFIG-INVALID");
}
};
if run_config.review_required
&& let Some(f) = snap.feature.as_ref()
&& let Some(t) = crate::review::totals_of(&project, format, &feature_name, &f.feature.dir)
&& !t.complete()
{
return refuse(
CommandStatus::NeedsHuman,
format!(
"{feature_name}'s spec is not approved yet ({} approved; {} to review, {} with changes requested, {} changed since, {} open question(s); committed review only) and [review] required is on",
t.approved, t.open, t.changes_requested, t.lapsed, t.questions
),
)
.with_code("REVIEW-PENDING");
}
if run_config.review_required {
let ambiguous = match crate::oracle::open_ambiguities(
&s.plane_dir,
&feature_name,
&format.oracle_requirements(&snap),
) {
Ok(a) => a,
Err(t) => {
return refuse(
CommandStatus::NeedsHuman,
format!(
"{}: whether it is read two ways is unknown and [review] required is on",
t.0
),
)
.with_code("ORACLE-TAMPERED");
}
};
if let Some((req, scenario)) = ambiguous.first() {
return refuse(
CommandStatus::NeedsHuman,
format!(
"{req} is read two ways ({scenario}) and [review] required is on: choose a reading or reword it first"
),
)
.with_code("REVIEW-AMBIGUOUS")
.with_fix(vec![
format!("speccoder review tests {req} --reading A {scenario}"),
format!("speccoder review tests {req} --reading B {scenario}"),
]);
}
}
let executor = match external::parse(&config_text) {
Ok(e) => e,
Err(e) => {
return refuse(CommandStatus::ConfigError, format!("speccoder.toml: {e}"))
.with_code("CONFIG-INVALID");
}
};
let executor = match options.agent.as_deref() {
None => executor,
Some("native") => None,
Some(id) => match external::for_agent(id, executor.as_ref()) {
Ok(e) => Some(e),
Err(e) => return refuse(CommandStatus::UsageError, e),
},
};
let sandbox_config = match sandbox::parse(&config_text) {
Ok(c) => c,
Err(e) => {
return refuse(CommandStatus::ConfigError, format!("speccoder.toml: {e}"))
.with_code("CONFIG-INVALID");
}
};
let (runner, sandbox_notice) = match sandbox::runner(&wt_root, &sandbox_config) {
Ok(r) => r,
Err(e) => return refuse(CommandStatus::ConfigError, e),
};
if options.mode == speccoder_core::oracle::RunMode::Autopilot && !runner.is_sandboxed() {
return refuse(
CommandStatus::ConfigError,
"autopilot needs the OS sandbox, and commands here would run unsandboxed".to_owned(),
)
.with_code("SANDBOX-UNAVAILABLE");
}
let mutation_config = match mutation::parse(&config_text) {
Ok(c) => c,
Err(e) => {
return refuse(CommandStatus::ConfigError, format!("speccoder.toml: {e}"))
.with_code("CONFIG-INVALID");
}
};
let worktree_config = match crate::prepare::parse(&config_text) {
Ok(c) => c,
Err(e) => {
return refuse(CommandStatus::ConfigError, format!("speccoder.toml: {e}"))
.with_code("CONFIG-INVALID");
}
};
let check_argvs = verify::check_argvs(&config_root);
let unit_dir = snap
.feature
.as_ref()
.map(|f| f.feature.dir.clone())
.unwrap_or_default();
let changed_code = crate::attempts::list(&wt_root, &feature_name)
.iter()
.any(|a| a.task == task && a.outcome == CHANGED_CODE);
let planning: Option<String> = parsed_task
.as_ref()
.filter(|t| {
native.is_none()
&& !changed_code
&& speccoder_core::lane::by_paths(&t.title, &unit_dir, &format.rules().judged)
== speccoder_core::lane::Lane::Planning
})
.map(|_| {
let prefix = project
.strip_prefix(&wt_root)
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
if prefix.is_empty() {
unit_dir.trim_end_matches('/').to_owned()
} else {
format!("{prefix}/{}", unit_dir.trim_end_matches('/'))
}
});
let policy = tools::policy::Policy::new(&check_argvs, &run_config.allow)
.with_prefixes(run_config.allow_prefix.clone())
.rules(format.rules());
let policy = match &planning {
Some(dir) => policy.planning(dir),
None => policy,
};
let (policy, runner, carve_notice) = match options.session.as_ref().and_then(|s| s.lock().ok())
{
Some(s) => s.confine(policy, runner),
None => (policy, runner, None),
};
let sandbox_notice = sandbox_notice.or(carve_notice);
if let Some(e) = options.session.as_ref().and_then(|s| {
s.lock().ok().and_then(|s| {
if interrupted.is_some() {
s.check_before_tick(&policy).err()
} else {
s.check(&policy).err()
}
})
}) {
return e.result("run");
}
let wt_root_canonical = std::fs::canonicalize(&wt_root)
.unwrap_or_else(|_| wt_root.clone())
.display()
.to_string();
let this_run = options.run();
let (decisions, task_grants) = match approvals::Decisions::load(&s.plane_dir) {
Ok(mut d) => {
if !autopilot
&& d.claim(&feature_name, task, this_run.as_str())
&& let Err(e) = d.save(&s.plane_dir)
{
return refuse(CommandStatus::Error, e);
}
(
d.for_task(&feature_name, task),
if autopilot {
Vec::new()
} else {
d.for_run(&feature_name, task, this_run.as_str())
},
)
}
Err(e) => return refuse(CommandStatus::ConfigError, e),
};
if let Some((stage, last_call)) = &resumed_from {
live.event(&progress::Event::Resumed {
task: task.to_owned(),
stage: *stage,
last_call: last_call.clone(),
});
}
live.event(&progress::Event::TaskStarted {
feature: feature_name.clone(),
task: task.to_owned(),
title: parsed_task
.as_ref()
.map(|t| t.title.clone())
.unwrap_or_default(),
});
if planning.is_some()
&& let Some(t) = &parsed_task
{
live.event(&progress::Event::Notice {
text: format!(
"📐 planning · writes {}",
speccoder_core::lane::task_paths(&t.title, &unit_dir).join(", ")
),
});
}
let fail = |status: CommandStatus, m: String| {
live.event(&progress::Event::TaskFinished {
task: task.to_owned(),
outcome: "error".to_owned(),
message: redact(&m, secret),
run: None,
input_tokens: 0,
output_tokens: 0,
cache_read_tokens: 0,
cache_write_tokens: 0,
usd: None,
regression: None,
});
refuse(status, m)
};
let mut prepare_notes: Vec<String> = Vec::new();
let mut copied_files: Vec<String> = Vec::new();
if options.session.is_none() {
match crate::include::copy(
&wt_root,
&project,
Path::new(&s.snapshot.root),
&worktree_config.include,
) {
Ok(c) => {
copied_files.clone_from(&c.files);
if !c.now.is_empty() {
prepare_notes.push(format!(
"copied from your checkout ([worktree] include): {}",
c.now.join(", ")
));
}
prepare_notes.extend(
c.skipped
.iter()
.map(|n| format!("not copied ([worktree] include): {n}")),
);
for n in &prepare_notes {
live.event(&progress::Event::Notice { text: n.clone() });
}
}
Err(e) => {
return fail(
CommandStatus::ConfigError,
format!("the files [worktree] include names could not be copied: {e}"),
)
.with_code("PREPARE-FAILED");
}
}
}
let prepares =
options.session.is_none() && worktree_config.prepare && crate::prepare::needed(&project);
if prepares {
let began = std::time::Instant::now();
stage(
&*live,
task,
Stage::Prepare,
StageState::Started,
None,
None,
);
let prepared = crate::prepare::prepare(&crate::prepare::Prepare {
worktree: &wt_root,
project: &project,
config: &worktree_config,
runner: &runner,
state: Some(state),
});
match prepared {
Ok(None) => {}
Ok(Some(p)) => {
stage(
&*live,
task,
Stage::Prepare,
if p.done.is_empty() {
StageState::Skipped
} else {
StageState::Passed
},
Some(if p.done.is_empty() {
"reused: the lockfiles are unchanged".to_owned()
} else {
p.done.join(", ")
}),
Some(began),
);
}
Err(f) => {
stage(
&*live,
task,
Stage::Prepare,
StageState::Failed,
Some(f.message.clone()),
Some(began),
);
return fail(CommandStatus::ConfigError, f.message)
.with_code("PREPARE-FAILED")
.with_fix(f.fix)
.with_commands(f.commands);
}
}
} else if options.session.is_none() {
crate::prepare::forget(&wt_root);
}
let began = std::time::Instant::now();
stage(
&*live,
task,
Stage::Baseline,
StageState::Started,
None,
None,
);
let baseline = match verify::run_checks(&project, Some(verify::config::Tier::Fast), &runner) {
Ok(b) => b,
Err(e) => {
stage(
&*live,
task,
Stage::Baseline,
StageState::Failed,
None,
Some(began),
);
return fail(CommandStatus::ConfigError, format!("baseline: {e}"));
}
};
if !options.allow_failing_baseline
&& let Some((c, why)) = baseline
.checks
.iter()
.find_map(|c| broken(c).map(|w| (c, w)))
{
stage(
&*live,
task,
Stage::Baseline,
StageState::Failed,
Some(format!("`{}` cannot run", c.name)),
Some(began),
);
let checkout = std::path::Path::new(&s.snapshot.root);
if let Some(remedy) = crate::code::setup_remedy(checkout, Some(&feature_name)) {
return fail(
CommandStatus::GitPrecondition,
format!(
"check `{}` cannot run in the worktree: your checkout has not committed its setup — {why}",
c.name
),
)
.with_code("SETUP-INCOMPLETE")
.with_fix(vec!["speccoder fix".to_owned(), remedy.command.clone()])
.with_remedy(Some(remedy));
}
return fail(
CommandStatus::ConfigError,
if why.contains("git add") || why.contains("--allow-failing-baseline") {
format!(
"check `{}` cannot run on the untouched code, so it could never judge {task}: {why}",
c.name
)
} else {
format!(
"check `{}` cannot run on the untouched code, so it could never judge {task}: {why} — fix the check (a file it needs may be uncommitted: `git status`), or run with --allow-failing-baseline",
c.name
)
},
)
.with_code("CHECK-BROKEN");
}
let failing = baseline
.checks
.iter()
.filter(|c| c.status.is_failure())
.count();
stage(
&*live,
task,
Stage::Baseline,
StageState::Passed,
(failing > 0).then(|| format!("{failing} check(s) already failing")),
Some(began),
);
let rt = match tokio_runtime() {
Ok(rt) => rt,
Err(e) => return fail(CommandStatus::RunFailed, e),
};
let bundle_value = match serde_json::to_value(&bundle) {
Ok(v) => v,
Err(e) => return fail(CommandStatus::Error, e.to_string()),
};
let instructions = crate::instructions::collect(
&wt_root,
&project,
&speccoder_core::scope::named_paths(&bundle.task.title),
&run_config.instructions,
);
let attempt_instructions: &[crate::instructions::Instruction] = if executor.is_none() {
&instructions
} else {
&[]
};
let servers = match mcp::parse(&config_text) {
Ok(s) => s,
Err(e) => {
return fail(CommandStatus::ConfigError, format!("speccoder.toml: {e}"))
.with_code("CONFIG-INVALID");
}
};
let mut connected = if servers.is_empty() {
mcp::Connected::default()
} else {
live.event(&progress::Event::Notice {
text: format!("starting {} MCP server(s)", servers.len()),
});
rt.block_on(mcp::connect(&servers, &project, &runner))
};
let approved_tools: Vec<String> = task_grants
.iter()
.filter_map(|g| g.tool_name().map(str::to_owned))
.collect();
let pinned = mcp::pin_reviewed(&s.plane_dir, &mut connected.tools, &approved_tools);
connected.notices.extend(pinned);
let tools_server = match tools::WorkspaceTools::new(&wt_root, policy.clone()) {
Ok(t) => std::sync::Arc::new(t.with_runner(runner.clone())),
Err(e) => return fail(CommandStatus::Error, e.to_string()),
};
let swept =
match crate::approvals::expire_due(&plane_dir, agentplane::core::Timestamp::now_utc()) {
Ok(p) => p,
Err(e) => {
return fail(
CommandStatus::RunFailed,
format!("the calls past their deadline could not be denied: {e}"),
);
}
};
let present = crate::approvals::PresentContext {
root: None,
sandbox: runner.is_sandboxed().then(|| {
if cfg!(target_os = "macos") {
"Seatbelt".to_owned()
} else {
"Landlock+seccomp".to_owned()
}
}),
network: match sandbox_config.network {
sandbox::Network::Off => "none",
sandbox::Network::Loopback => "loopback",
sandbox::Network::On => "unrestricted",
}
.to_owned(),
file_text: None,
};
let plane = match rt.block_on(async {
substrate::Plane::open_with(
&plane_dir,
&s.model,
provider,
Some(tools_server),
substrate::Hooks {
progress: Some(Arc::clone(&live)),
mcp: connected.clients.clone(),
mcp_tools: connected.tools.clone(),
format: Some(format),
present: present.clone(),
},
)
}) {
Ok(p) => p,
Err(e) => return fail(CommandStatus::RunFailed, e.0),
};
let mut notices: Vec<String> = elsewhere
.replaced_journal
.iter()
.chain(swept.iter())
.chain(plane.replaced_journal.iter())
.map(|p| {
format!(
"the journal could not be read by this build and was moved to {}; past runs stay in git",
p.display()
)
})
.chain(connected.notices.iter().cloned())
.chain(sandbox_notice.clone())
.chain(prepare_notes.iter().cloned())
.chain(resumed_from.as_ref().map(|(stage, last_call)| {
format!(
"Resuming — {task} was interrupted during {}{}; finished calls will not repeat.",
stage.map_or("a run", speccoder_core::stage::Stage::name),
last_call
.as_ref()
.map(|c| format!(" ({c})"))
.unwrap_or_default()
)
}))
.collect();
let demand_red =
run_config.acceptance && planning.is_none() && format.is_convergence(&bundle.task.phase);
let start = match &interrupted {
Some(_) => match git::Git::at(&wt_root).line(&["rev-parse", "HEAD^{tree}"]) {
Ok(tree) => worktree::Checkpoint(tree),
Err(e) => return fail(CommandStatus::GitPrecondition, e.0),
},
None => match worktree::checkpoint(&wt_root, &tmp) {
Ok(c) => c,
Err(e) => return fail(CommandStatus::GitPrecondition, e.0),
},
};
let began = std::time::Instant::now();
let bug_ctx = super::bug::Ctx {
rt: &rt,
plane: &plane,
project: &project,
wt_root: &wt_root,
wt_path: &wt_path,
tmp: &tmp,
policy: &policy,
runner: &runner,
state: &s.plane_dir,
feature: &feature_name,
task,
bundle: &bundle_value,
instructions: &instructions,
live: &*live,
approver: approver.as_deref(),
held_out: run_config.held_out,
max_tries: run_config.reproduce_tries,
bug_test: run_config.bug_test.as_deref(),
start: &start,
};
let mut bug_fix = match super::bug::phase(&s.plane_dir, &feature_name, task, &project, format) {
Err(e) => return fail(CommandStatus::NeedsHuman, e),
Ok(None) => None,
Ok(Some(super::bug::Phase::Fix(f))) => Some(*f),
Ok(Some(super::bug::Phase::Reproduce(b))) if interrupted.is_none() => {
match super::bug::reproduce(&bug_ctx, *b) {
Ok(f) => Some(f),
Err(stop) => return *stop,
}
}
Ok(Some(super::bug::Phase::Reproduce(_))) => None,
};
let bug_candidate = bug_fix.as_ref().and_then(|f| {
f.bug.candidate.clone().and_then(|c| {
crate::bug::state::extra(&s.plane_dir, &f.bug.unit, &f.bug.task)
.candidate_patch
.map(|d| (d, c))
})
});
let test_layout = crate::layout::detect(&project, &check_argvs);
let test_layout_text = test_layout.text();
let story_files = super::commit::story_files(&wt_root, &feature_name);
let scenarios = match &bug_fix {
Some(f) => super::bug::scenarios(&f.bug),
None => format.scenarios(&snap, story_task.as_deref().unwrap_or(task)),
};
let mut review_frozen: Option<(speccoder_core::acceptance::PreFrozenAcceptance, String)> = None;
let mut review_reuse = None;
if bug_fix.is_none() && run_config.acceptance && !options.fresh_acceptance && planning.is_none()
{
match crate::oracle::for_task(&s.plane_dir, &project, &wt_root, format, &snap, &scenarios) {
Some(r @ crate::oracle::TaskReuse::Ready { .. }) => review_reuse = Some(r),
Some(crate::oracle::TaskReuse::Reused { req }) => notices.push(format!(
"{req}'s frozen tests were reused by an earlier task of this run: this task's acceptance tests are authored"
)),
Some(crate::oracle::TaskReuse::Elsewhere { req }) => {
notices.push(format!("{req}: frozen oracle not present here — authoring"))
}
Some(crate::oracle::TaskReuse::Lapsed {
req,
frozen,
current,
}) => {
if crate::oracle::note_lapse(&s.plane_dir, &feature_name, &req, &frozen) {
live.event(&progress::Event::Oracle {
step: progress::OracleEvent::Lapsed {
unit: feature_name.clone(),
requirement: req.clone(),
frozen_wording_sha256: frozen,
current_wording_sha256: current,
},
});
}
notices.push(format!(
"{req}'s wording changed since its tests were frozen at review: the frozen oracle lapsed — authoring"
));
}
Some(crate::oracle::TaskReuse::Tampered(why)) => {
return fail(
CommandStatus::NeedsHuman,
format!("{why}: the task stops for a person, nothing is ticked"),
)
.with_code("ORACLE-TAMPERED");
}
Some(crate::oracle::TaskReuse::Unreadable(why)) => {
return fail(
CommandStatus::NeedsHuman,
format!("{why}: whether its tests are frozen is unknown, so the task stops for a person, nothing is ticked"),
)
.with_code("REVIEW-UNREADABLE");
}
None => {}
}
}
let mut acceptance = if let Some(prepared) = interrupted
.as_ref()
.and_then(|m| m.acceptance.clone())
.and_then(|a| serde_json::from_value::<PreparedAcceptance>(a).ok())
{
let mut prepared = prepared;
if let Some(sealed) = bug_fix
.as_ref()
.and_then(|f| f.record.held_out_sealed.as_ref())
{
prepared.held_out = Some(sealed.manifest_sha256.clone());
prepared.report["held_out"] = serde_json::json!({
"state": "on",
"manifest": format!("sha256:{}", sealed.manifest_sha256),
"kept": sealed.count,
});
}
Some(prepared)
} else if let Some(f) = &bug_fix {
stage(&*live, task, Stage::Red, StageState::Started, None, None);
match super::bug::reuse(&bug_ctx, f) {
Ok(a) => Some(a),
Err(e) => {
let _ = worktree::restore(&wt_root, &tmp, &start);
stage(
&*live,
task,
Stage::Red,
StageState::Failed,
Some(e.clone()),
Some(began),
);
return fail(CommandStatus::NeedsHuman, e);
}
}
} else if let Some(reuse) = review_reuse.take() {
stage(&*live, task, Stage::Red, StageState::Started, None, None);
let crate::oracle::TaskReuse::Ready {
record,
files,
tests,
manifest,
} = reuse
else {
return fail(CommandStatus::Error, "no frozen oracle".to_owned());
};
match reuse_pre_frozen(
&project,
&record,
&files,
&tests,
&runner,
RedRule::RunTime,
Some(&SealedCheck {
state: &s.plane_dir,
unit: &feature_name,
wt_root: &wt_root,
tmp: &tmp,
policy: &policy,
}),
) {
Ok(mut a) => {
a.report["pre_frozen"]["manifest"] = serde_json::json!(manifest);
notices.push(format!(
"acceptance tests frozen at review by {} ({}) for {}: no author call",
record.decided.by, record.decided.via, record.requirement
));
review_frozen = Some((*record, manifest));
Some(a)
}
Err(e) => {
let _ = worktree::restore(&wt_root, &tmp, &start);
stage(
&*live,
task,
Stage::Red,
StageState::Failed,
Some(e.clone()),
Some(began),
);
return fail(CommandStatus::NeedsHuman, e).with_code("ORACLE-TAMPERED");
}
}
} else if run_config.acceptance && planning.is_none() && format.acceptance_testable(&bundle) {
stage(&*live, task, Stage::Red, StageState::Started, None, None);
Some(prepare_acceptance(&AcceptanceSetup {
rt: &rt,
plane: &plane,
project: &project,
worktree: &wt_root,
tmp: &tmp,
policy: &policy,
feature: &feature_name,
task,
bundle: bundle_value.clone(),
scenarios: &scenarios,
instructions: &instructions,
runner: &runner,
layout: Some(&test_layout),
story_files: &story_files,
}))
} else {
None
};
if review_frozen.is_none()
&& acceptance
.as_ref()
.is_some_and(|a| a.report["source"] == "review")
&& let Some(crate::oracle::TaskReuse::Ready {
record, manifest, ..
}) = review_reuse.take()
{
review_frozen = Some((*record, manifest));
}
let held_out_store = match (&bug_fix, &review_frozen) {
(Some(f), _) => crate::held_out::sealed_dir(&s.plane_dir, &f.bug.unit, &f.bug.requirement),
(None, Some((r, _))) => {
crate::held_out::sealed_dir(&s.plane_dir, &feature_name, &r.requirement)
}
(None, None) => crate::held_out::dir(&s.plane_dir, &feature_name, task),
};
if let Some(a) = acceptance
.as_mut()
.filter(|a| a.report.get("held_out").is_none())
{
if !run_config.held_out {
a.report["held_out"] = serde_json::json!({ "state": "off" });
} else if a.argv.is_none() {
a.report["held_out"] =
serde_json::json!({ "state": "absent", "reason": "no_visible_tests" });
} else if !a.red {
a.report["held_out"] = serde_json::json!({ "state": "absent", "reason": "not_red" });
} else if scenarios.is_empty() {
a.report["held_out"] =
serde_json::json!({ "state": "absent", "reason": "no_scenarios" });
} else {
let (held, sha) = prepare_held_out(
&AcceptanceSetup {
rt: &rt,
plane: &plane,
project: &project,
worktree: &wt_root,
tmp: &tmp,
policy: &policy,
feature: &feature_name,
task,
bundle: bundle_value.clone(),
scenarios: &scenarios,
instructions: &instructions,
runner: &runner,
layout: Some(&test_layout),
story_files: &story_files,
},
a,
&scenarios,
&held_out_store,
story_checkpoint(
snap.feature.as_ref().and_then(|f| f.tasks.as_ref()),
task,
&scenarios,
),
);
a.report["held_out"] = held;
a.held_out = sha;
}
}
if run_config.held_out && !run_config.acceptance {
notices.push(
"[acceptance] held_out has no effect while generate = false: no held-out tests"
.to_owned(),
);
}
match &acceptance {
None => stage(
&*live,
task,
Stage::Red,
StageState::Skipped,
Some(if planning.is_some() {
"planning work: design documents".to_owned()
} else {
"no acceptance examples".to_owned()
}),
None,
),
Some(a) if a.red => stage(
&*live,
task,
Stage::Red,
StageState::Passed,
a.red_class.map(class_word),
Some(began),
),
Some(a) => stage(
&*live,
task,
Stage::Red,
StageState::Failed,
Some(a.red_class.map_or_else(
|| "not demonstrated".to_owned(),
|c| format!("not demonstrated ({})", class_word(c)),
)),
Some(began),
),
}
if let Some(why) = acceptance
.as_ref()
.and_then(|a| a.generation_failed.clone())
{
let reason = format!(
"the acceptance tests could not be written ({why}); nothing was attempted — run again once the model answers, or set [acceptance] generate = false to rely on the checks alone"
);
let _ = worktree::restore(&wt_root, &tmp, &start);
let report = RunReport {
reason: Some(reason.clone()),
acceptance: acceptance.as_ref().map(|a| a.report.clone()),
set_aside,
notices: notices.clone(),
..bare_report(&feature_name, task, &wt_path, "acceptance_failed")
};
live.event(&finished_event(
task,
&report,
"acceptance_failed",
false,
redact(&reason, secret),
));
return CommandResult::new("run", CommandStatus::NeedsHuman, &redact(&reason, secret))
.with_code("ACCEPTANCE-UNWRITTEN")
.with_data(CommandData::Run(Box::new(report)));
}
if demand_red && !acceptance.as_ref().is_some_and(|a| a.red) {
let why = undemonstrated_why(acceptance.as_ref());
let reason = format!(
"convergence gap not demonstrated: {why}; a person decides whether {task} is real"
);
let saved = attempts::save(
&wt_root,
&tmp,
&feature_name,
task,
&attempts::Record {
run: None,
outcome: "not_demonstrated",
reason: &reason,
pending: None,
usage: None,
},
)
.ok()
.map(|s| s.reference);
let _ = worktree::restore(&wt_root, &tmp, &start);
let report = RunReport {
reason: Some(reason.clone()),
acceptance: acceptance.as_ref().map(|a| a.report.clone()),
saved_attempt: saved,
set_aside,
notices: notices.clone(),
..bare_report(&feature_name, task, &wt_path, "not_demonstrated")
};
live.event(&finished_event(
task,
&report,
"not_demonstrated",
false,
redact(&reason, secret),
));
return CommandResult::new("run", CommandStatus::NeedsHuman, &redact(&reason, secret))
.with_data(CommandData::Run(Box::new(report)));
}
let before = match &interrupted {
Some(m) => worktree::Checkpoint(m.before.clone()),
None => match worktree::checkpoint(&wt_root, &tmp) {
Ok(c) => c,
Err(e) => return fail(CommandStatus::GitPrecondition, e.0),
},
};
let acceptance_saved = acceptance
.as_ref()
.and_then(|a| serde_json::to_value(a).ok());
let frozen: Vec<String> = worktree::files(&wt_root, &before)
.unwrap_or_default()
.into_iter()
.filter(|p| tools::paths::is_test_path(p))
.collect();
let mut usage: Option<serde_json::Value> = None;
let mut samples: Vec<(RunReport, String, worktree::Checkpoint, serde_json::Value)> = Vec::new();
let mut held_out_log: Vec<serde_json::Value> = Vec::new();
let mut bug_weak: Option<speccoder_core::strength::Permitted> = None;
let mut refused_account = false;
let (mut report, attempt_status) = loop {
let mut earlier: Vec<speccoder_core::EarlierAttempt> = Vec::new();
let mut digest: Option<serde_json::Value> = None;
let mut attempt_no = interrupted
.as_ref()
.filter(|m| m.sample == samples.len())
.map_or(1, |m| m.attempt.max(1));
let (mut report, attempt_status) = loop {
let hidden: Vec<String> = options
.session
.as_ref()
.and_then(|s| s.lock().ok().map(|s| s.hidden.clone()))
.unwrap_or_default();
let (task_grants, approved_once) = match approvals::Decisions::load(&s.plane_dir) {
Ok(_) if autopilot => (Vec::new(), Vec::new()),
Ok(mut d) => {
let once = d.take_once(&feature_name, task, this_run.as_str());
if !once.is_empty()
&& let Err(e) = d.save(&s.plane_dir)
{
return fail(CommandStatus::Error, e);
}
(d.for_run(&feature_name, task, this_run.as_str()), once)
}
Err(e) => return fail(CommandStatus::ConfigError, e),
};
let run_approved: Vec<crate::tools::policy::Object> = options
.run_grants
.lock()
.map(|g| g.clone())
.unwrap_or_default();
let input = substrate::attempt::input(
bundle_value.clone(),
&substrate::attempt::Settings {
checks: &check_argvs,
allow: &run_config.allow,
allow_prefix: &run_config.allow_prefix,
max_turns: run_config.max_turns,
max_tool_calls: run_config.max_tool_calls,
reasoning: s.model.reasoning_summary,
frozen: &frozen,
approved: &task_grants,
rejected: &decisions.rejected,
run_approved: &run_approved,
approved_once: &approved_once,
nudges: &decisions.nudges,
mcp: &connected.tools,
root: &wt_root_canonical,
frozen_additions: run_config.frozen_additions,
asks: if options.lane.is_some() {
"stop"
} else {
"tasks"
},
wait_secs: run_config.approval_wait.as_secs(),
network_allow: &sandbox_config.network_allow,
network_allow_prefix: &sandbox_config.network_allow_prefix,
run_id: this_run.as_str(),
asked: options.asked_now(),
budget: run_config.approval_budget,
prompts: approver.is_some() && options.lane.is_none(),
in_place: options.session.is_some(),
hidden: &hidden,
autopilot: options.mode == speccoder_core::oracle::RunMode::Autopilot,
planning: planning.as_deref(),
},
digest.as_ref(),
attempt_instructions,
(!test_layout.runners.is_empty()).then_some(test_layout_text.as_str()),
);
let attempt_began = std::time::Instant::now();
let attempt_detail = if run_config.best_of > 1 {
format!(
"{attempt_no}/{} · sample {}/{}",
run_config.max_attempts,
samples.len() + 1,
run_config.best_of
)
} else {
format!("{attempt_no}/{}", run_config.max_attempts)
};
stage(
&*live,
task,
Stage::Attempt,
StageState::Started,
Some(attempt_detail.clone()),
None,
);
let judged_before = integrity::JudgeSnapshot::take(&wt_root, &policy);
let (run_id, succeeded, mut output) = if let Some((diff, cand)) = &bug_candidate {
let held: Vec<String> = bug_fix
.as_ref()
.and_then(|f| {
f.bug
.held_out_sealed
.clone()
.map(|h| (f.bug.unit.clone(), h))
})
.and_then(|(unit, h)| {
crate::held_out::open_sealed(&s.plane_dir, &unit, &h).ok()
})
.map(|(m, _)| m.files.into_iter().map(|f| f.path).collect())
.unwrap_or_default();
match super::bug::apply_candidate(&bug_ctx, diff, cand, &frozen, &held) {
Ok(x) => x,
Err(stop) => return *stop,
}
} else if let Some(exec) = &executor {
let allow: Vec<Vec<String>> = check_argvs
.iter()
.chain(&run_config.allow)
.cloned()
.collect();
let prompt = external::prompt_in(format.wording(), &bundle_value, digest.as_ref());
let withheld = acceptance
.as_ref()
.and_then(|a| a.held_out.as_ref())
.map(|_| held_out_store.as_path());
if withheld.is_some()
&& !notices
.iter()
.any(|n| n.starts_with("held-out set withheld"))
{
notices.push("held-out set withheld from the external agent, not sealed: it runs with your permissions; a copied held-out literal stops the tick".to_owned());
}
let _ = allow;
let o = match &exec.kind {
external::Kind::Command(argv) => {
external::run(argv, exec.timeout, &wt_root, &prompt, &tmp)
}
external::Kind::Acp(agent) => {
let policy = match substrate::attempt::policy_of(
&input.peek()["settings"],
format.rules(),
) {
Ok(p) => p,
Err(e) => return fail(CommandStatus::RunFailed, e.to_string()),
};
let tools = match tools::WorkspaceTools::new(&wt_root, policy.clone()) {
Ok(t) => t.with_runner(runner.clone()),
Err(e) => {
return fail(
CommandStatus::Error,
format!("{}: {e}", wt_root.display()),
);
}
};
let governor = Arc::new(crate::acp::govern::Governor::new(
crate::acp::govern::Setup {
root: std::fs::canonicalize(&wt_root)
.unwrap_or_else(|_| wt_root.clone()),
tools,
policy,
approver: approver.clone().filter(|_| options.lane.is_none()),
present: present.clone(),
task: format!("{feature_name}/{task}"),
task_title: parsed_task
.as_ref()
.map(|t| t.title.clone())
.unwrap_or_default(),
run: Some(this_run.to_string()),
frozen_additions: run_config.frozen_additions,
run_approved: run_approved.clone(),
approved_once: approved_once.clone(),
progress: Some((task.to_owned(), Arc::clone(&live))),
copied: copied_files.clone(),
agent: agent.id.clone(),
},
));
let a = external::run_acp(
exec,
agent,
&wt_root,
&prompt,
&runner,
governor,
Some((task.to_owned(), Arc::clone(&live))),
);
if let Some(ways) = &a.sign_in {
return login(fail, &a.agent, agent, ways);
}
for n in a.outcome.output["acp"]["notices"]
.as_array()
.into_iter()
.flatten()
.filter_map(serde_json::Value::as_str)
{
notices.push(format!("{}: {n}", a.agent));
}
a.outcome
}
};
(o.run_id, true, o.output)
} else {
let key = match &interrupted {
Some(m) if m.attempt == attempt_no && m.sample == samples.len() => {
m.key.clone()
}
_ => resume::new_key(&feature_name, task),
};
let _ = resume::save(
&marker_path,
&resume::Marker {
key: key.clone(),
before: before.0.clone(),
head: head_now.clone(),
attempt: attempt_no,
sample: samples.len(),
acceptance: acceptance_saved.clone(),
run_id: Some(this_run.to_string()),
asked: options.asked_now(),
run_grants: run_approved.clone(),
},
);
marker_guard.arm();
let outcome = match rt.block_on(plane.attempt_keyed(
&feature_name,
task,
input,
&key,
)) {
Ok((o, resumed)) => {
if resumed {
notices.push(format!(
"resumed run {} of an interrupted process: finished model and tool calls were read back from the journal, not repeated",
o.run_id
));
}
o
}
Err(e) => return fail(CommandStatus::RunFailed, e.0),
};
let context = crate::approvals::PresentContext {
root: Some(wt_root.clone()),
..present.clone()
};
let (outcome, waiting) = match rt.block_on(plane.decide_inline(
outcome,
approver.as_deref(),
&context,
)) {
Ok(x) => {
notices.extend(crate::actions::allow_always_inline(
path,
&feature_name,
&x.always,
state,
));
(x.outcome, x.waiting)
}
Err(e) => return fail(CommandStatus::RunFailed, e.0),
};
if let Some(task_row) = waiting {
marker_guard.keep();
let action = &task_row.justification.proposed_action;
let mut report = bare_report(&feature_name, task, &wt_path, "needs_approval");
report.run_id = outcome.run_id.to_string();
report.notices = notices;
report.tool_calls = action["log"].as_array().cloned().unwrap_or_default();
report.tool_calls.push(serde_json::json!({
"tool": action["tool"],
"arguments": action["arguments"],
"decision": "approval",
"reason": action["request"]["reason"],
"object": action["request"]["object"],
"waiting": true,
"call": task_row.id.to_hex(),
}));
report.pending = Some(serde_json::json!({
"tool": action["tool"],
"arguments": action["arguments"],
"reason": action["request"]["reason"],
"object": action["request"]["object"],
"request": action["request"],
"call": task_row.id.to_hex(),
"deadline": task_row.due_at.map(|t| t.unix_timestamp()),
"waiting": true,
"batched": action["request"]["batched"],
}));
if let (Some(n), Ok(mut asked)) = (
action["request"]["asked"]
.as_u64()
.and_then(|n| u32::try_from(n).ok()),
options.asked.lock(),
) {
*asked = (*asked).max(n);
}
report.reason = Some(format!(
"waiting for a person: {} — `speccoder task approve {task}` or `speccoder task reject {task} --reason …`, then `speccoder run`",
task_row.justification.summary.peek()
));
let (status, message) = status_and_message(&report, "needs_approval", false);
live.event(&finished_event(
task,
&report,
"needs_approval",
false,
redact(&message, secret),
));
return CommandResult::new("run", status, &redact(&message, secret))
.with_data(CommandData::Run(Box::new(report)));
}
let succeeded = matches!(outcome.status, agentplane::prelude::RunStatus::Succeeded);
refused_account = outcome
.failed_effect
.as_ref()
.is_some_and(agentplane::journal::FailedEffect::refuses_the_account);
let mut output = outcome
.output
.as_ref()
.map(|o| o.peek().clone())
.unwrap_or(serde_json::Value::Null);
if !succeeded && output["reason"].as_str().is_none() {
if !output.is_object() {
output = serde_json::json!({});
}
let reason = if rt.block_on(plane.holds_a_pre_054_decision(outcome.run_id)) {
substrate::PRE_054_DECISION.to_owned()
} else {
format!("{:?}", outcome.status)
};
output["reason"] = serde_json::Value::String(redact(&reason, secret));
}
(outcome.run_id.to_string(), succeeded, output)
};
if bug_candidate.is_some()
&& let Some(c) = bug_fix.as_mut().and_then(|f| f.bug.candidate.as_mut())
{
c.applied = true;
}
if let Some(argv) = run_config.format.as_ref().filter(|_| {
succeeded && matches!(output["status"].as_str(), Some("completed" | "incomplete"))
}) {
use verify::process::CommandRunner as _;
let ran = runner.run(&verify::process::Invocation {
argv: argv.clone(),
cwd: project.clone(),
env: verify::process::scrubbed_env(std::env::vars()),
timeout: std::time::Duration::from_secs(300),
output_cap: verify::process::OUTPUT_CAP,
});
if ran.exit_code != Some(0) {
notices.push(format!(
"the formatter (`{}`) did not succeed",
argv.join(" ")
));
}
}
if prepares {
let began = std::time::Instant::now();
match crate::prepare::recheck(&crate::prepare::Prepare {
worktree: &wt_root,
project: &project,
config: &worktree_config,
runner: &runner,
state: Some(state),
}) {
Ok(done) if done.is_empty() => {}
Ok(done) => {
stage(
&*live,
task,
Stage::Prepare,
StageState::Passed,
Some(format!(
"{}: the attempt changed its lockfile",
done.join(", ")
)),
Some(began),
);
}
Err(f) => {
stage(
&*live,
task,
Stage::Prepare,
StageState::Failed,
Some(f.message.clone()),
Some(began),
);
let reason = redact(&f.message, secret);
let saved = attempts::save(
&wt_root,
&tmp,
&feature_name,
task,
&attempts::Record {
run: (!run_id.is_empty()).then_some(run_id.as_str()),
outcome: "prepare_failed",
reason: &reason,
pending: None,
usage: output.get("usage"),
},
)
.and_then(|saved| {
worktree::restore(&wt_root, &tmp, &before).map(|_| saved)
});
let message = match saved {
Ok(saved) => format!(
"{} — the attempt is saved as {} and undone",
f.message, saved.reference
),
Err(e) => format!("{} — saving the attempt failed: {}", f.message, e.0),
};
return fail(CommandStatus::ConfigError, message)
.with_code("PREPARE-FAILED")
.with_fix(f.fix)
.with_commands(f.commands);
}
}
}
let after = match worktree::checkpoint(&wt_root, &tmp) {
Ok(c) => c,
Err(e) => return fail(CommandStatus::GitPrecondition, e.0),
};
let raw_changes = worktree::diff(&wt_root, &before, &after).unwrap_or_default();
if let (Some(n), Ok(mut asked)) = (
output["asked"].as_u64().and_then(|n| u32::try_from(n).ok()),
options.asked.lock(),
) {
*asked = n;
}
let approved_writes = approved_writes(&task_grants, &output["tool_calls"]);
let patch = worktree::patch(&wt_root, &before, &after).unwrap_or_default();
if let Some(name) = crate::include::added_secret(&patch) {
let _ = worktree::restore(&wt_root, &tmp, &before);
return fail(
CommandStatus::NeedsHuman,
format!(
"the attempt's change adds the value of {name}, copied from your checkout ([worktree] include): undone, nothing judged or committed"
),
)
.with_code("SECRET-IN-DIFF");
}
let findings = attempt_findings(&Audited {
wt_root: &wt_root,
policy: &policy,
judged_before: &judged_before,
raw_changes: &raw_changes,
patch: &patch,
frozen: &frozen,
frozen_additions: run_config.frozen_additions,
approved_writes: &approved_writes,
command_drift: &output["integrity"],
});
let changes = changes_of(raw_changes);
let quarantined = !succeeded
&& output["reason"]
.as_str()
.is_some_and(|r| r.starts_with("Quarantined("));
if quarantined {
output["reason"] = serde_json::Value::String(
"the attempt was interrupted while a tool call was running; whether it finished cannot be known, so it was not run again. The attempt is saved and undone; run again to start a fresh attempt".to_owned(),
);
}
let mut attempt_status = if succeeded {
output["status"].as_str().unwrap_or("failed").to_owned()
} else if quarantined {
"interrupted".to_owned()
} else {
"failed".to_owned()
};
if attempt_status == "completed" && changes.is_empty() && output.get("acp").is_some() {
attempt_status = "blocked".to_owned();
output["reason"] = serde_json::json!(format!(
"{} ended its turn without changing anything",
output["acp"]["agent"].as_str().unwrap_or("the agent")
));
}
let mut report = RunReport {
run_id: run_id.clone(),
model_report: output.get("report").cloned(),
reason: output["reason"].as_str().map(str::to_owned),
pending: output.get("pending").cloned(),
tool_calls: output["tool_calls"].as_array().cloned().unwrap_or_default(),
changes,
usage: output.get("usage").cloned(),
integrity: findings.clone(),
acceptance: acceptance.as_ref().map(|a| a.report.clone()),
set_aside: set_aside.clone(),
notices: notices.clone(),
agent: external::agent_attempt(&output),
..bare_report(&feature_name, task, &wt_path, &attempt_status)
};
if let Err(e) = record_inline_approvals(
&s.plane_dir,
&feature_name,
task,
&report.tool_calls,
&options.run_grants,
this_run.as_str(),
) {
report.notices.push(format!(
"the decisions made in this attempt were not recorded ({e}): later attempts ask again"
));
}
for c in report
.tool_calls
.iter()
.filter(|c| c["approved_by"] == "person")
{
if let Some(name) = c["tool"].as_str().filter(|n| n.starts_with(mcp::PREFIX)) {
mcp::mark_reviewed(&s.plane_dir, &connected.tools, name);
}
}
stage(
&*live,
task,
Stage::Attempt,
if attempt_status == "completed" {
StageState::Passed
} else {
StageState::Failed
},
Some(if attempt_status == "completed" {
attempt_detail
} else {
format!("{attempt_detail} {attempt_status}")
}),
Some(attempt_began),
);
if attempt_status == "integrity" {
report.verdict = Some(speccoder_core::verify::Verdict::Failed {
reasons: findings.clone(),
});
}
let judged = matches!(attempt_status.as_str(), "completed" | "incomplete");
if judged {
let began = std::time::Instant::now();
stage(&*live, task, Stage::Verify, StageState::Started, None, None);
if let Err(e) = judge_checks(
&project,
&runner,
&baseline,
acceptance.as_ref(),
&findings,
&mut report,
) {
stage(
&*live,
task,
Stage::Verify,
StageState::Failed,
None,
Some(began),
);
return fail(CommandStatus::ConfigError, format!("verification: {e}"));
}
if let Some(dir) = planning.as_ref().filter(|_| {
matches!(
report.verdict,
Some(
speccoder_core::verify::Verdict::Verified
| speccoder_core::verify::Verdict::RegressionOnly
)
)
}) {
let inside = format!("{dir}/");
let outside: Vec<&str> = report
.changes
.iter()
.filter(|(p, _)| !p.starts_with(&inside))
.map(|(p, _)| p.as_str())
.collect();
let reason = if !outside.is_empty() {
Some(format!(
"a planning task changed code: {}",
outside.join(", ")
))
} else if !report.changes.iter().any(|(_, kind)| kind != "deleted") {
Some(format!("wrote no planning file under {inside}"))
} else {
None
};
if !outside.is_empty() {
attempt_status = CHANGED_CODE.to_owned();
report.attempt_status = CHANGED_CODE.to_owned();
}
if let Some(r) = reason {
report.verdict =
Some(speccoder_core::verify::Verdict::Failed { reasons: vec![r] });
}
}
let otherwise_passing = attempt_status == "completed"
&& findings.is_empty()
&& matches!(
report.verdict,
Some(
speccoder_core::verify::Verdict::Verified
| speccoder_core::verify::Verdict::RegressionOnly
)
);
match judge_collection(
(&project, &wt_root, &tmp),
&runner,
otherwise_passing,
&mut report,
&*live,
task,
) {
Some(Collected::Unknown(why)) => {
attempt_status = "collection_unknown".to_owned();
report.verdict = None;
report.reason = Some(why);
}
Some(Collected::Changed(finding)) => {
report.integrity.push(finding.clone());
report.verdict = Some(speccoder_core::verify::Verdict::Failed {
reasons: vec![finding],
});
attempt_status = "integrity".to_owned();
}
None => {}
}
let checks_held = report.verification.as_ref().is_some_and(|v| v.passed);
stage_with_tail(
&*live,
task,
Stage::Verify,
if checks_held {
StageState::Passed
} else {
StageState::Failed
},
None,
Some(began),
report
.verification
.as_ref()
.map(failing_tail)
.unwrap_or_default(),
);
}
if judged || attempt_status == "integrity" {
stage(
&*live,
task,
Stage::Audit,
if findings.is_empty() {
StageState::Passed
} else {
StageState::Failed
},
findings
.first()
.map(|f| f.trim_start_matches("integrity: ").to_owned()),
None,
);
}
let visibly_passed = judged
&& findings.is_empty()
&& attempt_status == "completed"
&& matches!(
report.verdict,
Some(
speccoder_core::verify::Verdict::Verified
| speccoder_core::verify::Verdict::RegressionOnly
)
);
if let Some(sha) = acceptance
.as_ref()
.and_then(|a| a.held_out.clone())
.filter(|_| visibly_passed)
{
let patch = worktree::patch(&wt_root, &before, &after).unwrap_or_default();
let mut known = format!(
"{}\n{}\n{}",
bundle_value["story"].as_str().unwrap_or_default(),
bundle_value["plan"].as_str().unwrap_or_default(),
bundle_value["constitution"].as_str().unwrap_or_default()
);
for p in report
.acceptance
.as_ref()
.and_then(|a| a["files"].as_array().cloned())
.unwrap_or_default()
{
if let Some(p) = p.as_str() {
known.push_str(
&std::fs::read_to_string(project.join(p)).unwrap_or_default(),
);
}
}
match judge_held_out(
&held_out_store,
&sha,
&scenarios,
&known,
&wt_root,
&project,
&tmp,
&before,
&patch,
&runner,
&policy,
) {
Ok(j) => {
let failed_ids: Vec<String> =
j.failed.iter().map(|s| s.id.clone()).collect();
held_out_log.push(serde_json::json!({
"attempt": held_out_log.len() + 1,
"outcome": if failed_ids.is_empty() { "passed" } else { "failed" },
"scenarios": failed_ids,
}));
if let Some(v) = report.verdict.take() {
let mut v = speccoder_core::verify::with_held_out(v, &j.held_out);
if let speccoder_core::verify::Verdict::Failed { reasons } = &mut v
&& !j.failed.is_empty()
&& let Some(last) = reasons.last_mut()
{
*last = speccoder_core::retry::held_out_reason(&j.failed);
}
report.verdict = Some(v);
}
}
Err((outcome, why)) => {
held_out_log.push(serde_json::json!({
"attempt": held_out_log.len() + 1,
"outcome": outcome,
}));
report.verdict = None;
report.reason = Some(why);
attempt_status = "held_out".to_owned();
}
}
}
if let Some(fix) = bug_fix.as_mut().filter(|_| {
attempt_status == "completed"
&& findings.is_empty()
&& matches!(
report.verdict,
Some(
speccoder_core::verify::Verdict::Verified
| speccoder_core::verify::Verdict::RegressionOnly
)
)
}) {
let keep = crate::strength::dir(&s.plane_dir, &feature_name, task);
let judging = super::bug::Judging {
before: &before,
baseline: &baseline,
verification: report.verification.as_ref(),
base: report
.verdict
.clone()
.unwrap_or(speccoder_core::verify::Verdict::RegressionOnly),
keep: &keep,
};
match super::bug::judge(&bug_ctx, fix, &judging) {
Ok(j) => {
report.verdict = Some(j.verdict);
if let Some(weak) = j.weak {
report.reason = Some(format!(
"reproduction too weak: a fix that special-cases {}'s input passes ({}); `speccoder task rule {task} --tests-right --reason …` if the tests are right",
weak.scenario, weak.rationale
));
bug_weak = Some(weak);
attempt_status = "repro_weak".to_owned();
}
}
Err(e) => {
report.integrity.push(e.clone());
report.reason = Some(e);
report.verdict = None;
attempt_status = "integrity".to_owned();
}
}
}
if let Some(h) = report
.acceptance
.as_mut()
.and_then(|a| a.get_mut("held_out"))
.filter(|h| h["state"] == "on")
{
let failed = held_out_log
.iter()
.filter(|o| o["outcome"] == "failed")
.count();
h["attempts"] = serde_json::json!(held_out_log);
h["gap"] = serde_json::json!(failed > 0);
}
let passing = attempt_status == "completed"
&& matches!(
report.verdict,
Some(
speccoder_core::verify::Verdict::Verified
| speccoder_core::verify::Verdict::RegressionOnly
)
);
if passing && mutation_config.as_ref().is_none_or(|m| !m.suite) {
stage(
&*live,
task,
Stage::Mutation,
StageState::Skipped,
Some(if mutation_config.is_none() {
"not configured".to_owned()
} else {
"suite = false: for acceptance strength only".to_owned()
}),
None,
);
}
if let Some(m) = mutation_config.as_ref().filter(|m| passing && m.suite) {
let began = std::time::Instant::now();
stage(
&*live,
task,
Stage::Mutation,
StageState::Started,
None,
None,
);
let patch = worktree::patch(&wt_root, &before, &after).unwrap_or_default();
let scratch = runner.scratch().unwrap_or_else(|| tmp.clone());
if let Some(result) = mutation::run(m, &project, &patch, &scratch, &runner) {
if m.mode == mutation::Mode::Require
&& let Some(reason) = mutation::failure(&result)
{
report.verdict = Some(speccoder_core::verify::Verdict::Failed {
reasons: vec![reason],
});
}
report.mutation = Some(result);
}
let held = !matches!(
report.verdict,
Some(speccoder_core::verify::Verdict::Failed { .. })
);
stage(
&*live,
task,
Stage::Mutation,
if held {
StageState::Passed
} else {
StageState::Failed
},
report
.mutation
.as_ref()
.and_then(crate::mutation::trailer)
.map(|t| t.trim().trim_start_matches("Mutation: ").to_owned()),
Some(began),
);
}
usage = add_usage(usage.as_ref(), report.usage.as_ref());
let (Some(speccoder_core::verify::Verdict::Failed { reasons }), Some(verification)) =
(report.verdict.clone(), report.verification.as_ref())
else {
break (report, attempt_status);
};
if attempt_status != "completed" {
break (report, attempt_status);
}
let discarded: Vec<String> = report.changes.iter().map(|(p, _)| p.clone()).collect();
let mut d =
speccoder_core::retry::digest(attempt_no, &reasons, verification, &discarded);
d.uncollected = report
.collection
.as_ref()
.map(speccoder_core::collection::Collection::retry_lines)
.unwrap_or_default();
let fingerprint = speccoder_core::retry::fingerprint(&d);
let stop = if bug_candidate.is_some() {
Some("a candidate is judged, never retried".to_owned())
} else {
retry_stop(
&findings,
verification,
earlier.last().map(|e| e.fingerprint.as_str()),
&fingerprint,
attempt_no,
run_config.max_attempts,
)
};
if let Some(stop) = stop {
report.retry_stop = Some(stop);
break (report, attempt_status);
}
if let Err(e) = worktree::restore(&wt_root, &tmp, &before) {
report.retry_stop = Some(format!("cannot undo the attempt: {}", e.0));
break (report, attempt_status);
}
earlier.push(speccoder_core::EarlierAttempt {
run_id: report.run_id.clone(),
attempt_status: attempt_status.clone(),
verdict: report.verdict.clone(),
fingerprint,
digest: d.clone(),
usage: report.usage.clone(),
});
digest = serde_json::to_value(&d).ok();
attempt_no += 1;
};
report.earlier = earlier;
if run_config.best_of <= 1 {
break (report, attempt_status);
}
let end = match worktree::checkpoint(&wt_root, &tmp) {
Ok(c) => c,
Err(e) => return fail(CommandStatus::GitPrecondition, e.0),
};
let patch = worktree::patch(&wt_root, &before, &end).unwrap_or_default();
let summary = sample_summary(&report, &attempt_status, &patch);
samples.push((report, attempt_status, end, summary));
if samples.len() >= run_config.best_of as usize {
let best = best_sample(&samples);
let summaries: Vec<serde_json::Value> = samples
.iter()
.enumerate()
.map(|(i, s)| {
let mut v = s.3.clone();
v["kept"] = serde_json::json!(i == best);
v
})
.collect();
let (mut chosen, status, end, _) = samples.swap_remove(best);
if best != samples.len() {
if let Err(e) = worktree::restore(&wt_root, &tmp, &end) {
return fail(CommandStatus::GitPrecondition, e.0);
}
}
chosen.samples = summaries;
break (chosen, status);
}
if let Err(e) = worktree::restore(&wt_root, &tmp, &before) {
return fail(CommandStatus::GitPrecondition, e.0);
}
};
report.usage = usage;
{
use speccoder_core::context::ContextKind as K;
let part = |k: &str| {
bundle_value[k]
.as_str()
.unwrap_or_default()
.as_bytes()
.to_vec()
};
let mut m = crate::context::Manifest::new();
m.push(
K::Constitution,
"worktree",
"/bundle/constitution",
&part("constitution"),
)
.push(K::Plan, "worktree", "/bundle/plan", &part("plan"))
.push(K::Story, "worktree", "/bundle/story", &part("story"))
.push(
K::Task,
"worktree",
"/bundle/task",
serde_json::to_string(&bundle_value["task"])
.unwrap_or_default()
.as_bytes(),
)
.push(
K::TestLayout,
"worktree",
"/test_layout",
test_layout_text.as_bytes(),
);
report.context = Some(m.finish());
}
report.scope = parsed_task.as_ref().map(|t| scope_of(&report, &t.title));
let strength_dir = crate::strength::dir(&s.plane_dir, &feature_name, task);
let mut attempt_status = attempt_status;
let verified = attempt_status == "completed"
&& matches!(
report.verdict,
Some(speccoder_core::verify::Verdict::Verified)
);
let visible_argv = acceptance
.as_ref()
.filter(|a| a.red)
.and_then(|a| a.argv.clone());
if verified && !run_config.strength.enabled {
stage(
&*live,
task,
Stage::Strength,
StageState::Skipped,
Some("strength = false".to_owned()),
None,
);
}
if let Some(argv) = visible_argv.filter(|_| verified && run_config.strength.enabled) {
use speccoder_core::strength::{Gate, gate, over_strict};
let began = std::time::Instant::now();
stage(
&*live,
task,
Stage::Strength,
StageState::Started,
None,
None,
);
let fin = worktree::checkpoint(&wt_root, &tmp);
let patch = fin
.as_ref()
.ok()
.and_then(|now| worktree::patch(&wt_root, &start, now).ok())
.unwrap_or_default();
let mut notes = Vec::new();
let held: Vec<(crate::held_out::HeldOutFile, String)> =
match acceptance.as_ref().and_then(|a| a.held_out.clone()) {
None => Vec::new(),
Some(sha) => match crate::held_out::load_verified(&held_out_store, &sha) {
Ok((manifest, contents)) => manifest
.files
.into_iter()
.zip(contents.into_iter().map(|(_, c)| c))
.collect(),
Err(t) => {
notes.push(format!("held-out set not overlaid: {}", t.0));
Vec::new()
}
},
};
let rulings = crate::strength::rulings(&strength_dir);
let mut commands = vec![argv];
let measure = |commands: &[Vec<String>]| {
crate::strength::measure(
&crate::strength::Setup {
wt_root: &wt_root,
project: &project,
tmp: &tmp,
runner: &runner,
policy: &policy,
acceptance: commands,
held_out: &held,
mutation: mutation_config.as_ref(),
patch: &patch,
config: &run_config.strength,
keep: &strength_dir,
rulings: &rulings,
},
|changed| {
let input = substrate::near_miss::input(
&bundle_value,
changed,
&crate::mutation::source_patch(&patch),
run_config.strength.near_miss,
run_config.strength.permitted,
);
match rt.block_on(plane.near_miss(&feature_name, task, input)) {
Ok(o) => {
let out = o
.output
.as_ref()
.map(|v| v.peek().clone())
.unwrap_or_default();
let ok = matches!(o.status, agentplane::prelude::RunStatus::Succeeded)
&& !out["answer"].is_null();
crate::strength::Authored {
answer: if ok {
Ok(out["answer"].clone())
} else {
Err(format!(
"the variant author did not answer ({})",
crate::substrate::why_not(&o.status)
))
},
run_id: Some(o.run_id.to_string()),
usage: Some(out["usage"].clone()).filter(|u| !u.is_null()),
}
}
Err(e) => crate::strength::Authored {
answer: Err(redact(&e.0, secret)),
run_id: None,
usage: None,
},
}
},
)
};
let mut measured = measure(&commands);
let min = run_config.strength.min;
if let (Ok(a), Ok(fin)) = (&measured, &fin)
&& gate(min, &a.combined, false) == Gate::Failed
&& over_strict(&a.permitted).is_empty()
{
let visible_tests: Vec<(String, String)> = report
.acceptance
.as_ref()
.and_then(|a| a["files"].as_array().cloned())
.unwrap_or_default()
.iter()
.filter_map(|p| p.as_str())
.map(|p| {
(
p.to_owned(),
std::fs::read_to_string(project.join(p)).unwrap_or_default(),
)
})
.collect();
let setup = AcceptanceSetup {
rt: &rt,
plane: &plane,
project: &project,
worktree: &wt_root,
tmp: &tmp,
policy: &policy,
feature: &feature_name,
task,
bundle: bundle_value.clone(),
scenarios: &scenarios,
instructions: &instructions,
runner: &runner,
layout: Some(&test_layout),
story_files: &story_files,
};
let weak = crate::strength::weak(a);
match strengthen(&setup, &visible_tests, weak, &before, fin) {
Ok(added) => {
if let Some(acc) = report.acceptance.as_mut() {
if let Some(files) = acc["files"].as_array_mut() {
files.extend(added.files.iter().map(|f| serde_json::json!(f)));
}
acc["strengthened"] = serde_json::json!({
"files": added.files,
"argv": added.argv,
"run_id": added.report["run_id"],
"usage": added.report["usage"],
});
}
report.usage = add_usage(report.usage.as_ref(), Some(&added.report["usage"]));
commands.push(added.argv);
let mut again = measure(&commands);
if let Ok(b) = again.as_mut() {
b.gate = gate(min, &b.combined, true);
b.notes.insert(
0,
"strengthened once with added acceptance tests".to_owned(),
);
}
measured = again;
}
Err(why) => {
if let Ok(a) = measured.as_mut() {
a.notes
.insert(0, format!("could not strengthen the set: {why}"));
}
}
}
}
match measured {
Ok(mut a) => {
if a.gate == Gate::Off {
a.gate = gate(min, &a.combined, false);
}
a.notes.extend(notes);
let findings = over_strict(&a.permitted);
if !findings.is_empty() {
attempt_status = "over_strict".to_owned();
report.reason = Some(format!(
"an acceptance test rejects an implementation the spec appears to allow: {}",
findings
.iter()
.map(|p| format!("{} ({})", p.scenario, p.rationale))
.collect::<Vec<_>>()
.join("; ")
));
} else if a.gate == Gate::Failed {
attempt_status = "acceptance_weak".to_owned();
report.reason = Some(format!(
"acceptance set too weak: strength {} is below min_strength {}{}",
speccoder_core::strength::summary(&a.combined, None),
min.unwrap_or_default(),
a.notes
.first()
.map(|n| format!(" ({n})"))
.unwrap_or_default()
));
} else {
crate::strength::release(&strength_dir);
}
report.usage = add_usage(report.usage.as_ref(), a.usage.as_ref());
report.strength = Some(a);
}
Err(e) => {
report.integrity.push(e.clone());
report.reason = Some(e);
report.verdict = None;
attempt_status = "integrity".to_owned();
}
}
report.attempt_status.clone_from(&attempt_status);
let held_up = attempt_status == "completed";
stage(
&*live,
task,
Stage::Strength,
if held_up {
StageState::Passed
} else {
StageState::Failed
},
report.strength.as_ref().map(|a| {
speccoder_core::strength::summary(&a.combined, a.notes.first().map(String::as_str))
}),
Some(began),
);
}
let verdict_passed = attempt_status == "completed"
&& matches!(
report.verdict,
Some(
speccoder_core::verify::Verdict::Verified
| speccoder_core::verify::Verdict::RegressionOnly
)
);
let mut suspected = false;
if !run_config.monitor && verdict_passed {
stage(
&*live,
task,
Stage::Monitor,
StageState::Skipped,
Some("not enabled".to_owned()),
None,
);
}
if run_config.monitor && verdict_passed {
let began = std::time::Instant::now();
stage(
&*live,
task,
Stage::Monitor,
StageState::Started,
None,
None,
);
let patch = worktree::checkpoint(&wt_root, &tmp)
.and_then(|now| worktree::patch(&wt_root, &start, &now))
.unwrap_or_default();
let calls: Vec<serde_json::Value> = report
.tool_calls
.iter()
.map(|c| {
let tool = c["tool"].as_str().unwrap_or_default();
serde_json::json!({ "tool": tool, "target": progress::target(tool, &c["arguments"]), "decision": c["decision"] })
})
.collect();
let input = substrate::monitor::input(&bundle_value, &patch, &calls, &frozen);
let (monitor, suspicion) = monitor_attempt(&rt, &plane, &feature_name, task, input, secret);
report.monitor = Some(monitor);
suspected = suspicion;
stage(
&*live,
task,
Stage::Monitor,
if suspected {
StageState::Failed
} else {
StageState::Passed
},
suspected.then(|| "suspects gaming".to_owned()),
Some(began),
);
}
let in_place_stop = options
.session
.as_ref()
.filter(|_| verdict_passed && !suspected)
.and_then(|s| {
s.lock()
.ok()
.and_then(|s| s.check_before_tick(&policy).err())
});
if let Some(e) = &in_place_stop {
report.reason = Some(e.line());
report.verdict = None;
attempt_status = e.code.to_lowercase().replace('-', "_");
report.attempt_status.clone_from(&attempt_status);
}
let passed = verdict_passed && !suspected && in_place_stop.is_none();
if passed
&& matches!(
report.verdict,
Some(speccoder_core::verify::Verdict::RegressionOnly)
)
{
use speccoder_core::regression::{Regression, RegressionFacts, RegressionReason, decide};
let facts = RegressionFacts {
acceptance_on: run_config.acceptance,
story: acceptance_story
.clone()
.or_else(|| bug_fix.as_ref().map(|f| f.bug.requirement.clone()))
.or_else(|| parsed_task.as_ref().and_then(|t| t.story.clone())),
scenarios: scenarios.len(),
skip: acceptance.as_ref().and_then(|a| a.skip_cause.clone()),
red_class: acceptance.as_ref().and_then(|a| {
if a.red
&& report
.acceptance
.as_ref()
.is_some_and(|r| r["green_flaky"] == true)
{
Some(speccoder_core::verify::RedClass::Flaky)
} else {
a.red_class
}
}),
};
let mut reason = if planning.is_some() {
RegressionReason::PlanningWork
} else {
decide(&facts)
};
if matches!(reason, RegressionReason::AlreadyPassing { .. })
&& bug_fix.is_none()
&& let Some(a) = acceptance.as_ref()
&& let Some(argv) = a.argv.as_ref()
{
let mut files: Vec<(String, String)> = a.report["files"]
.as_array()
.into_iter()
.flatten()
.filter_map(|f| f.as_str())
.filter_map(|f| {
std::fs::read_to_string(project.join(f))
.ok()
.map(|c| (f.to_owned(), c))
})
.collect();
let prefix = project
.strip_prefix(&wt_root)
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
let mut command = Some(argv.clone());
if a.report["appended"] == true
&& let Some(story) = a.report["story_file"].as_str()
{
let at = if prefix.is_empty() {
story.to_owned()
} else {
format!("{prefix}/{story}")
};
let old = crate::git::Git::at(&wt_root)
.run(&["show", &format!("HEAD:{at}")])
.map(|b| String::from_utf8_lossy(&b).into_owned())
.unwrap_or_default();
command = None;
if let Some((_, content)) = files.iter_mut().find(|(f, _)| f == story)
&& let Some(added) = content.strip_prefix(old.as_str()).map(str::to_owned)
{
command = super::bisect::section_command(argv, story, &added);
let imports: String = old
.lines()
.filter(|l| {
let t = l.trim_start();
["use ", "import ", "from ", "package ", "extern crate "]
.iter()
.any(|k| t.starts_with(k))
})
.map(|l| format!("{l}\n"))
.collect();
*content = format!("{imports}{added}");
}
}
let timeout = std::fs::read_to_string(project.join(crate::verify::config::FILE))
.ok()
.and_then(|t| crate::verify::config::parse(&t).ok())
.and_then(|cs| {
cs.into_iter()
.filter(|c| c.tier == crate::verify::config::Tier::Fast)
.map(|c| c.timeout)
.max()
})
.unwrap_or(std::time::Duration::from_secs(600));
let found = match &command {
Some(argv) => super::bisect::search(&super::bisect::Search {
repo: &wt_root,
prefix: &prefix,
feature: &feature_name,
task,
files: &files,
argv,
scratch: &s.plane_dir.join("tmp"),
sandbox: &sandbox_config,
timeout,
worktree: &worktree_config,
checkout: Path::new(&s.snapshot.root),
state: Some(state),
}),
None => super::bisect::Found {
implemented_by: None,
runs: 0,
},
};
live.event(&progress::Event::RegressionBisected {
task: task.to_owned(),
runs: found.runs,
result: found.implemented_by.as_ref().map(|i| match i {
speccoder_core::regression::ImplementedBy::Task { task, .. } => task.clone(),
speccoder_core::regression::ImplementedBy::BeforeFeature { .. } => {
"before_feature".to_owned()
}
}),
});
reason = RegressionReason::AlreadyPassing {
implemented_by: found.implemented_by,
};
}
report.regression = Some(Regression::of(reason));
}
if passed && let (Some(t), Some(file)) = (&parsed_task, &tasks_file) {
let began = std::time::Instant::now();
stage(&*live, task, Stage::Commit, StageState::Started, None, None);
let message = tick_message(&TickFacts {
feature: &feature_name,
task,
title: &t.title,
report: &report,
red: acceptance
.as_ref()
.and_then(|a| a.red_class.filter(|c| a.red && c.is_red())),
checklists_overridden,
extra: bug_fix
.as_ref()
.map(|f| {
super::bug::trailers(
f,
&report
.verdict
.as_ref()
.map(super::outcome::verdict_word)
.unwrap_or_default(),
&crate::strength::rulings(&strength_dir),
)
})
.or_else(|| review_frozen.as_ref().map(|(r, m)| r.trailer(m)))
.unwrap_or_default(),
});
match &options.session {
None => {
match format.tick(file, task, &t.line_hash) {
Ok(()) => report.ticked = true,
Err(e) => report.integrity.push(format!("tick refused: {e}")),
}
if report.ticked && run_config.commit_per_task {
match commit_attempt(&wt_root, &message) {
Ok(id) => report.commit = Some(id),
Err(e) => report.integrity.push(format!("commit failed: {e}")),
}
}
}
Some(session) => {
let digest =
crate::in_place::intent_digest(&project, &policy, format.messages().tasks_file);
if let Ok(mut s) = session.lock() {
let done = s.commit_and_tick(
&plane_dir,
file,
&t.line_hash,
&message,
&digest,
|| format.tick(file, task, &t.line_hash),
);
report.ticked = done.ticked;
report.commit = done.commit;
report.integrity.extend(done.problems);
}
}
}
report.instructions = attempt_instructions
.iter()
.map(|i| speccoder_core::InstructionFile {
path: i.path.clone(),
sha256: i.sha256.clone(),
bytes: i.bytes,
cut: i.cut,
})
.collect();
if let Some(commit) = report
.commit
.clone()
.filter(|_| report.ticked && run_config.attest.enabled)
{
let verdict = report
.verdict
.as_ref()
.map(super::outcome::verdict_word)
.unwrap_or_default();
let sandboxed = sandbox_config.mode != sandbox::Mode::Off && sandbox::backend().is_ok();
let feature_dir = snap
.feature
.as_ref()
.map(|f| f.feature.dir.clone())
.unwrap_or_default();
let requirements = format.requirement_ids(Path::new(&snap.root), &feature_dir);
let tick = crate::attest::Tick {
repo: &wt_root,
config: &config_text,
attest: &run_config.attest,
format: &format!("{:?}", format.kind()).to_lowercase(),
feature: &feature_name,
feature_dir: &feature_dir,
task,
title: &t.title,
line_hash: &t.line_hash,
section: t.story.as_deref(),
section_text: bundle_value["story"].as_str().unwrap_or_default(),
requirements: &requirements,
commit: &commit,
verdict: &verdict,
sandbox: serde_json::json!({
"mode": format!("{:?}", sandbox_config.mode).to_lowercase(),
"backend": sandboxed.then_some(if cfg!(target_os = "macos") { "seatbelt" } else { "landlock" }),
"network": format!("{:?}", sandbox_config.network).to_lowercase(),
}),
executor: serde_json::json!({
"kind": if bug_candidate.is_some() { "candidate".to_owned() } else { executor.as_ref().map_or_else(|| "native".to_owned(), |e| e.kind.name().to_owned()) },
"candidate": bug_candidate.as_ref().map(|(_, c)| serde_json::json!({ "origin": c.origin, "digest": c.digest, "head_sha": c.head_sha })),
}),
model: serde_json::json!({ "provider": s.model.provider, "model": s.model.model }),
bug: bug_fix
.as_ref()
.map(|f| super::bug::predicate_fields(&f.bug, &f.record)),
};
report.attestation = Some(crate::attest::record_tick(&tick, &report));
}
let committed = report.ticked && (report.commit.is_some() || !run_config.commit_per_task);
stage(
&*live,
task,
Stage::Commit,
if committed {
StageState::Passed
} else {
StageState::Failed
},
report.commit.as_ref().map(|c| c.chars().take(7).collect()),
Some(began),
);
}
if passed && let Some(n) = &native {
let began = std::time::Instant::now();
stage(&*live, task, Stage::Commit, StageState::Started, None, None);
let verdict = report
.verdict
.as_ref()
.map(super::outcome::verdict_word)
.unwrap_or_default();
let message = tick_message(&TickFacts {
feature: &feature_name,
task,
title: &n.title,
report: &report,
red: acceptance
.as_ref()
.and_then(|a| a.red_class.filter(|c| a.red && c.is_red())),
checklists_overridden: false,
extra: bug_fix
.as_ref()
.map(|f| {
super::bug::trailers(f, &verdict, &crate::strength::rulings(&strength_dir))
})
.unwrap_or_default(),
});
match commit_attempt(&wt_root, &message) {
Ok(id) => report.commit = Some(id),
Err(e) => report.integrity.push(format!("commit failed: {e}")),
}
if let Some(commit) = report.commit.clone().filter(|_| run_config.attest.enabled) {
let sandboxed = sandbox_config.mode != sandbox::Mode::Off && sandbox::backend().is_ok();
let tick = crate::attest::Tick {
repo: &wt_root,
config: &config_text,
attest: &run_config.attest,
format: &format!("{:?}", format.kind()).to_lowercase(),
feature: &feature_name,
feature_dir: &n.dir,
task,
title: &n.title,
line_hash: "",
section: None,
section_text: &n.story,
requirements: &[],
commit: &commit,
verdict: &verdict,
sandbox: serde_json::json!({
"mode": format!("{:?}", sandbox_config.mode).to_lowercase(),
"backend": sandboxed.then_some(if cfg!(target_os = "macos") { "seatbelt" } else { "landlock" }),
"network": format!("{:?}", sandbox_config.network).to_lowercase(),
}),
executor: serde_json::json!({
"kind": if bug_candidate.is_some() { "candidate" } else if executor.is_some() { "external" } else { "native" },
}),
model: serde_json::json!({ "provider": s.model.provider, "model": s.model.model }),
bug: bug_fix
.as_ref()
.map(|f| super::bug::predicate_fields(&f.bug, &f.record)),
};
report.attestation = Some(crate::attest::record_tick(&tick, &report));
}
stage(
&*live,
task,
Stage::Commit,
if report.commit.is_some() {
StageState::Passed
} else {
StageState::Failed
},
report.commit.as_ref().map(|c| c.chars().take(7).collect()),
Some(began),
);
}
if attempt_status == "repro_weak"
&& let Some(weak) = &bug_weak
&& let Err(e) = crate::strength::hold(&strength_dir, &[weak])
{
report
.integrity
.push(format!("holding the attempt failed: {e}"));
}
if attempt_status == "over_strict" || attempt_status == "repro_weak" {
let findings = report
.strength
.as_ref()
.map(|a| speccoder_core::strength::over_strict(&a.permitted))
.unwrap_or_default();
if attempt_status == "over_strict"
&& let Err(e) = crate::strength::hold(&strength_dir, &findings)
{
report
.integrity
.push(format!("holding the attempt failed: {e}"));
}
marker_guard.keep();
}
if !passed && attempt_status != "over_strict" && attempt_status != "repro_weak" {
let (outcome, reason) = saved_outcome(&report, &attempt_status, suspected);
let run = (!report.run_id.is_empty()).then_some(report.run_id.as_str());
let reason = redact(&reason, secret);
let record = attempts::Record {
run,
outcome: &outcome,
reason: &reason,
pending: report.pending.as_ref(),
usage: report.usage.as_ref(),
};
match attempts::save(&wt_root, &tmp, &feature_name, task, &record)
.and_then(|saved| worktree::restore(&wt_root, &tmp, &start).map(|_| saved))
{
Ok(saved) => report.saved_attempt = Some(saved.reference),
Err(e) => report
.integrity
.push(format!("saving the attempt failed: {}", e.0)),
}
}
let (status, mut message) = status_and_message(&report, &attempt_status, suspected);
if status == CommandStatus::RunFailed
&& let Some(region) = s.model.region.as_deref()
&& report
.reason
.as_deref()
.and_then(substrate::provider_refusal)
.is_some()
{
message.push_str(&format!(
" — Amazon Bedrock, region {region}, model {}",
s.model.model
));
}
let refusal = |result: CommandResult| {
if !result.has_explicit_code()
&& refused_account
&& result.status() == CommandStatus::RunFailed
{
result.with_code("PROVIDER-REFUSED").with_fix(
substrate::model_config::provider_refused_fix(
&s.model,
crate::aws::detect_here().as_ref(),
feature,
),
)
} else {
result
}
};
if let Some(fix) = bug_fix.as_mut() {
return refusal(super::bug::finished(
&bug_ctx,
fix,
report,
&attempt_status,
passed,
status,
redact(&message, secret),
));
}
live.event(&finished_event(
task,
&report,
&attempt_status,
suspected,
redact(&message, secret),
));
let code = collection_code(&report, &attempt_status);
let result = CommandResult::new("run", status, &redact(&message, secret));
match code {
Some(c) => result.with_code(c),
None => refusal(result),
}
.with_data(CommandData::Run(Box::new(report)))
}
fn collection_code(
report: &speccoder_core::RunReport,
attempt_status: &str,
) -> Option<&'static str> {
if attempt_status == "collection_unknown" {
return Some("TESTS-COLLECTION-UNKNOWN");
}
match &report.verdict {
Some(speccoder_core::verify::Verdict::Failed { reasons })
if reasons
.first()
.is_some_and(|r| r.starts_with(speccoder_core::collection::REASON_PREFIX)) =>
{
Some("TESTS-NOT-COLLECTED")
}
_ => None,
}
}
enum Collected {
Unknown(String),
Changed(String),
}
fn judge_collection(
dirs: (&Path, &Path, &Path),
runner: &sandbox::Runner,
otherwise_passing: bool,
report: &mut speccoder_core::RunReport,
live: &dyn progress::Progress,
task: &str,
) -> Option<Collected> {
let (project, wt_root, tmp) = dirs;
let before = worktree::checkpoint(wt_root, tmp).ok();
let checks = std::fs::read_to_string(project.join(crate::verify::config::FILE))
.ok()
.and_then(|t| crate::verify::config::parse(&t).ok())
.unwrap_or_default()
.into_iter()
.filter(|c| c.tier == crate::verify::config::Tier::Fast)
.collect::<Vec<_>>();
let results = report
.verification
.as_ref()
.map(|v| v.checks.clone())
.unwrap_or_default();
let c = crate::collection::judge(&crate::collection::Input {
root: project,
changes: &report.changes,
checks: &checks,
results: &results,
runner,
listings: otherwise_passing,
})?;
if !c.listing.is_empty()
&& let Some(before) = &before
{
let changed: Vec<String> = worktree::checkpoint(wt_root, tmp)
.and_then(|after| worktree::diff(wt_root, before, &after))
.map(|d| {
d.into_iter()
.map(|c| c.path)
.filter(|p| !speccoder_core::collection::is_build_or_dependency(p))
.collect()
})
.unwrap_or_else(|e| vec![format!("(the tree could not be read: {})", e.0)]);
if !changed.is_empty() {
report.collection = Some(c);
return Some(Collected::Changed(format!(
"integrity: a test listing changed the worktree: {}",
changed.join(", ")
)));
}
}
live.event(&progress::Event::CollectionChecked {
task: task.to_owned(),
files: c
.files
.iter()
.map(|f| progress::CollectionFile {
path: f.path.clone(),
state: f.state,
decided_by: f.decided_by,
})
.collect(),
});
let never = c.reasons();
let unknown: Vec<String> = c
.unknown()
.map(|f| {
format!(
"speccoder cannot tell whether the runner ran `{}` ({})",
f.path,
f.why.as_deref().unwrap_or("no report or listing names it")
)
})
.collect();
report.collection = Some(c);
if !never.is_empty() {
let mut reasons = never;
if let Some(speccoder_core::verify::Verdict::Failed { reasons: more }) =
report.verdict.take()
{
reasons.extend(more);
}
report.verdict = Some(speccoder_core::verify::Verdict::Failed { reasons });
return None;
}
(otherwise_passing && !unknown.is_empty()).then(|| Collected::Unknown(unknown.join("; ")))
}
fn with_cause(note: &str, c: &speccoder_core::verify::CheckResult) -> String {
let said = c
.stderr_tail
.lines()
.chain(c.stdout_tail.lines())
.map(str::trim)
.find(|l| l.starts_with("error"))
.map(|l| l.chars().take(200).collect::<String>());
let fix = match said.as_deref() {
Some(l) if l.contains("profile `speccoder` not found") => Some(
"`.config/nextest.toml` is not committed — `git add .config/nextest.toml && git commit -m \"speccoder: nextest profile\"`",
),
Some(l) if l.contains("no tests to run") => Some(
"the project has no tests yet; if the first task writes them, run with --allow-failing-baseline, or add `--no-tests=pass` to the check's argv",
),
Some(l) if l.contains("must be present") || l.contains("no targets") => Some(
"the project has nothing to build yet; if the first task creates it, run with --allow-failing-baseline",
),
_ => None,
};
match (said, fix) {
(Some(s), Some(f)) => format!("{note} ({s}); {f}"),
(Some(s), None) => format!("{note} ({s})"),
_ => note.to_owned(),
}
}
#[must_use]
pub fn broken(c: &speccoder_core::verify::CheckResult) -> Option<String> {
use speccoder_core::verify::CheckStatus;
if !c.status.is_failure() || c.status == CheckStatus::TimedOut {
return None;
}
if let Some(note) = &c.note {
return Some(with_cause(note, c));
}
if c.tests.as_ref().is_some_and(|t| !t.is_empty()) {
return None;
}
let summary = crate::verify::summary::parse(&c.stdout_tail, &c.stderr_tail, c.exit_code);
let runner = c
.argv
.first()
.map(|a| a.rsplit('/').next().unwrap_or(a))
.is_some_and(|a| {
matches!(
a,
"cargo"
| "pytest"
| "python"
| "python3"
| "uv"
| "npm"
| "npx"
| "pnpm"
| "yarn"
| "bun"
| "go"
| "jest"
| "vitest"
| "mvn"
| "gradle"
| "dotnet"
| "mix"
| "swift"
)
});
let cannot_start = matches!(c.exit_code, Some(126 | 127));
let is_broken = match &summary {
_ if cannot_start => true,
Some(s) if s.build_failed => true,
Some(s) => s.passed + s.failed + s.errors == 0,
None => runner,
};
match is_broken {
false => None,
true => {
let first = c
.stderr_tail
.lines()
.chain(c.stdout_tail.lines())
.map(str::trim)
.find(|l| l.to_lowercase().contains("error"))
.or_else(|| c.stderr_tail.lines().map(str::trim).find(|l| !l.is_empty()))
.unwrap_or("it failed without running a test")
.chars()
.take(160)
.collect::<String>();
Some(first)
}
}
}
fn login(
fail: impl FnOnce(CommandStatus, String) -> CommandResult,
shown: &str,
agent: &crate::acp::agent::Agent,
ways: &[crate::acp::session::SignIn],
) -> CommandResult {
let words: Vec<String> = ways
.iter()
.filter(|w| w.command.is_none())
.map(|w| {
format!(
"→ {}",
w.description.clone().unwrap_or_else(|| w.name.clone())
)
})
.collect();
let mut commands: Vec<Vec<String>> = ways
.iter()
.filter_map(|w| w.command.as_deref())
.map(|c| c.split_whitespace().map(str::to_owned).collect())
.collect();
if words.is_empty()
&& commands.is_empty()
&& let Some(p) = agent.preset()
{
commands.push(p.login.split_whitespace().map(str::to_owned).collect());
}
commands.dedup();
let argvs: Vec<speccoder_core::step::Argvs> = commands.into_iter().map(|c| vec![c]).collect();
let mut fix: Vec<String> = argvs
.iter()
.map(|a| speccoder_core::step::render(a))
.collect();
fix.extend(words);
fix.push("speccoder doctor".to_owned());
fail(
CommandStatus::NeedsHuman,
format!("{shown} needs signing in before it can make the attempt; nothing was attempted"),
)
.with_code("EXECUTOR-LOGIN")
.with_fix(fix)
.with_commands(argvs)
}