fix(missions): capture from the clone point, and let agents create files
Two defects found by running a real coding mission (019fc372) rather than a test. Both made a coding phase look like it produced nothing. **Capture measured the wrong baseline.** It diffed the working tree against HEAD, which is correct only while work stays uncommitted. `rust_sdlc` has a *committer* role, so committing is the intended path — meaning a mission that did its job properly leaves a clean tree and captured nothing. That is exactly what happened: the agent created `DELIVERY_PROBE.md`, committed it as `aa3be95`, and the artifact recorded `empty: true` beside a commit that plainly contained the work. `mission_workspace` now records the clone point in `.git/clawmates-base` (in `.git/`, so it travels with the checkout, stays invisible to the repository, and cannot be reached by an agent through its pinned workspace), refreshed whenever `fetch_and_reset` moves HEAD. Capture diffs from there, covering committed, staged and unstaged changes in one pass. Checkouts predating the marker fall back to HEAD and say so via `base_recorded: false`. **Agents could not create files.** `coding_readwrite` granted `file_edit` but not `file_write`. `file_edit` replaces an exact existing string and rejects an empty `old_string`, so creating a new file was impossible. The mission transcript is unambiguous: "the tool rejected empty old_string... the shell is restricted", after which the agent worked around it through `shell`. The comment above that profile has claimed it grants file_write since the day it was written; the list never contained it. Also broadens capture from coding/benchmark/security_scan to every phase kind of a repo-bearing mission: `phase_task_text` tells research phases to "save findings under /mission/repo/research/", so filtering by kind would have discarded every research brief such a mission produced. Regression tests cover committed-only and committed-plus-uncommitted work against a real git repo. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
322c1be89c
commit
409ca65ee7
@@ -121,9 +121,58 @@ async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
||||
}
|
||||
scrub_remote_credentials(path, url);
|
||||
ignore_agent_scaffolding(path);
|
||||
record_base_commit(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remember the commit the mission started from.
|
||||
///
|
||||
/// Delivery needs to answer "what did this mission change", and the obvious
|
||||
/// reading — working tree versus `HEAD` — is wrong the moment an agent
|
||||
/// commits. `rust_sdlc` has a *committer* role, so committing is the normal
|
||||
/// path, not an edge case: a mission that did its job properly would have a
|
||||
/// clean tree and capture nothing at all. Observed exactly that on mission
|
||||
/// 019fc372, where the agent committed `DELIVERY_PROBE.md` and the diff came
|
||||
/// back empty.
|
||||
///
|
||||
/// Written into `.git/` so it travels with the checkout, is invisible to the
|
||||
/// repository, and cannot be edited by an agent through its pinned workspace.
|
||||
pub(crate) fn record_base_commit(path: &std::path::Path) {
|
||||
let out = std::process::Command::new("git")
|
||||
.args([
|
||||
"-C",
|
||||
&path.display().to_string(),
|
||||
"-c",
|
||||
&format!("safe.directory={}", path.display()),
|
||||
"rev-parse",
|
||||
"HEAD",
|
||||
])
|
||||
.output();
|
||||
let Ok(out) = out else { return };
|
||||
if !out.status.success() {
|
||||
return;
|
||||
}
|
||||
let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if sha.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = std::fs::write(path.join(".git/clawmates-base"), format!("{sha}\n")) {
|
||||
eprintln!(
|
||||
"mission_workspace: could not record base commit for {} ({e}) — delivery will \
|
||||
fall back to diffing against HEAD and will miss committed work",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The commit this mission's checkout started from, if it was recorded.
|
||||
pub(crate) fn base_commit(path: &std::path::Path) -> Option<String> {
|
||||
std::fs::read_to_string(path.join(".git/clawmates-base"))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Take the access token back out of `.git/config`.
|
||||
///
|
||||
/// `with_ambient_auth` embeds `GITEA_TOKEN` in the clone URL so the clone can
|
||||
@@ -326,12 +375,15 @@ async fn fetch_and_reset(path: &std::path::Path, branch: &str) -> Result<(), Str
|
||||
return Err(format!(
|
||||
"git reset --hard origin/{branch} → exit {}: {}",
|
||||
reset.status,
|
||||
String::from_utf8_lossy(&reset.stderr)
|
||||
redact_token(&String::from_utf8_lossy(&reset.stderr))
|
||||
.chars()
|
||||
.take(400)
|
||||
.collect::<String>()
|
||||
));
|
||||
}
|
||||
// HEAD just moved to the freshly fetched tip; that is this run's starting
|
||||
// point, so the recorded base moves with it.
|
||||
record_base_commit(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user