fix(missions): the planner was planning blind — show it the repository
The first real plan opened with "Identify the crate's hottest code path and run its benchmark harness". This crate has no benchmark harness. The phase ran, found nothing to baseline, delivered zero files, and the plan's second phase was left with nothing to optimise against. The planner saw the mission title, the description, and a boolean for whether a repository was bound. It never saw the repository. A plan about a codebase written without looking at the codebase is a guess that reads like a plan — and the failure surfaces two phases and one VM boot later, as an agent reporting that the thing it was told to run does not exist. The prompt now carries the repository's root listing, read from the FORGE rather than a checkout: at proposal time the mission is still a draft and `ensure_checkout` has not run, so there is nothing on disk to list. It also says outright that a phase needing something absent must CREATE it and say so in its task — the failure was not only ignorance of the tree but the assumption that missing tooling is someone else's problem. A listing that cannot be fetched degrades to "(the repository listing could not be read)" in the prompt rather than to an empty string. A model told the listing is unavailable can hedge; a model told nothing assumes — which is the same distinction as `Option<u32>` for the subagent probe, in a prompt instead of a struct. Found by running the thing end to end rather than by testing it: every unit test here passes with a planner that has never seen a repository. 543 tests pass, clippy clean. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a33dbdcdc3
commit
0aeae07db2
@@ -21,6 +21,68 @@ use crate::{ApiError, AppState, Authed};
|
|||||||
|
|
||||||
const PLANNER_MODEL: &str = "claude-opus-4-8";
|
const PLANNER_MODEL: &str = "claude-opus-4-8";
|
||||||
|
|
||||||
|
/// What the repository actually contains, for the planner's prompt.
|
||||||
|
///
|
||||||
|
/// The planner used to see only the mission's title, description and whether a
|
||||||
|
/// repo was bound — and it planned accordingly. Its first real plan opened with
|
||||||
|
/// "run its benchmark harness" against a crate that has none, so the phase found
|
||||||
|
/// nothing to baseline and delivered zero files. A plan about a repository
|
||||||
|
/// written without seeing the repository is a guess.
|
||||||
|
///
|
||||||
|
/// Read from the FORGE rather than a checkout: at proposal time the mission is
|
||||||
|
/// still a draft and `ensure_checkout` has not run, so there is nothing on disk
|
||||||
|
/// to list. One API call, and a failure degrades to a stated absence rather than
|
||||||
|
/// to silence — a model told "the listing could not be read" can hedge; a model
|
||||||
|
/// told nothing assumes.
|
||||||
|
async fn repo_listing(pool: &sqlx::PgPool, mission_id: uuid::Uuid) -> String {
|
||||||
|
let row: Option<(Option<String>, Option<String>, Option<String>)> = sqlx::query_as(
|
||||||
|
"SELECT r.owner, r.name, r.default_branch
|
||||||
|
FROM missions m JOIN repos r ON r.id = m.repo_id
|
||||||
|
WHERE m.id = $1",
|
||||||
|
)
|
||||||
|
.bind(mission_id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
let Some((Some(owner), Some(name), branch)) = row else {
|
||||||
|
return "(this mission has no repository)".to_string();
|
||||||
|
};
|
||||||
|
let token = std::env::var("GITEA_TOKEN").unwrap_or_default();
|
||||||
|
let url = format!(
|
||||||
|
"https://git.redclaw.dev/api/v1/repos/{owner}/{name}/contents?ref={}",
|
||||||
|
branch.as_deref().unwrap_or("main")
|
||||||
|
);
|
||||||
|
let client = match reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return "(the repository listing could not be read)".to_string(),
|
||||||
|
};
|
||||||
|
let mut req = client.get(&url);
|
||||||
|
if !token.trim().is_empty() {
|
||||||
|
req = req.header("Authorization", format!("token {token}"));
|
||||||
|
}
|
||||||
|
let entries: Vec<serde_json::Value> = match req.send().await {
|
||||||
|
Ok(r) if r.status().is_success() => r.json().await.unwrap_or_default(),
|
||||||
|
_ => return "(the repository listing could not be read)".to_string(),
|
||||||
|
};
|
||||||
|
if entries.is_empty() {
|
||||||
|
return "(the repository root is empty)".to_string();
|
||||||
|
}
|
||||||
|
let mut names: Vec<String> = entries
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| {
|
||||||
|
let n = e.get("name")?.as_str()?;
|
||||||
|
let t = e.get("type").and_then(|v| v.as_str()).unwrap_or("file");
|
||||||
|
Some(if t == "dir" { format!("{n}/") } else { n.to_string() })
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
names.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
const PLAN_SYSTEM: &str = "You decide what ONE software mission actually does — its phases, in order. \
|
const PLAN_SYSTEM: &str = "You decide what ONE software mission actually does — its phases, in order. \
|
||||||
Each phase is a full agent run against the same repository checkout: the next phase sees the tree the \
|
Each phase is a full agent run against the same repository checkout: the next phase sees the tree the \
|
||||||
previous one left. They run SEQUENTIALLY, so phases are expensive and a handoff loses context at every \
|
previous one left. They run SEQUENTIALLY, so phases are expensive and a handoff loses context at every \
|
||||||
@@ -63,11 +125,14 @@ pub async fn suggest(
|
|||||||
.ok_or(ApiError::NotFound)?;
|
.ok_or(ApiError::NotFound)?;
|
||||||
|
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
"MISSION: {}\n\nDESCRIPTION:\n{}\n\nThis mission {} a repository.\n\nPHASE KINDS YOU MAY USE \
|
"MISSION: {}\n\nDESCRIPTION:\n{}\n\nREPOSITORY ROOT: {}\nPlan for the repository as it \
|
||||||
(nothing else runs): {}\nCEILING: {MAX_PHASES} phases.\n\nPropose the plan now (JSON only).",
|
ACTUALLY IS. If the work needs something the repository does not have — a benchmark \
|
||||||
|
harness, a test suite, a config file — the phase that needs it must CREATE it, and its \
|
||||||
|
task must say so.\n\nPHASE KINDS YOU MAY USE (nothing else runs): {}\nCEILING: \
|
||||||
|
{MAX_PHASES} phases.\n\nPropose the plan now (JSON only).",
|
||||||
mission.title,
|
mission.title,
|
||||||
mission.description.as_deref().unwrap_or("(none)"),
|
mission.description.as_deref().unwrap_or("(none)"),
|
||||||
if mission.repo_id.is_some() { "HAS" } else { "has NO" },
|
repo_listing(&state.pool, id).await,
|
||||||
PLANNABLE_KINDS.join(", "),
|
PLANNABLE_KINDS.join(", "),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user