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
@@ -120,10 +120,28 @@ pub async fn capture_phase_diff_at(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let base_sha = git(&repo, &["rev-parse", "HEAD"])
|
||||
.await
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
// Diff from where the mission *started*, not from HEAD.
|
||||
//
|
||||
// `HEAD` is the wrong baseline the moment an agent commits, and committing
|
||||
// is the normal path — `rust_sdlc` has a committer role. A mission that did
|
||||
// its job properly leaves a clean tree, so a HEAD-relative diff reports
|
||||
// nothing changed. That is exactly what happened on mission 019fc372: the
|
||||
// agent committed the file it was asked to create and capture recorded
|
||||
// `empty: true` beside a commit that plainly contained the work.
|
||||
//
|
||||
// Diffing against the recorded clone point covers committed, staged and
|
||||
// unstaged changes in one pass. Falling back to HEAD keeps checkouts made
|
||||
// before the base was recorded working, at the cost of missing committed
|
||||
// work — which is why the fallback says so in the metadata.
|
||||
let recorded_base = mission_workspace::base_commit(&repo);
|
||||
let base_is_recorded = recorded_base.is_some();
|
||||
let base_sha = match recorded_base {
|
||||
Some(sha) => sha,
|
||||
None => git(&repo, &["rev-parse", "HEAD"])
|
||||
.await
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|_| "HEAD".to_string()),
|
||||
};
|
||||
|
||||
// `--intent-to-add` registers untracked files with the index without
|
||||
// staging their content, which is what makes them appear in `git diff`.
|
||||
@@ -139,11 +157,11 @@ pub async fn capture_phase_diff_at(
|
||||
// A repo with nothing to add is fine; keep going and let the diff be empty.
|
||||
let _ = git(&repo, &add).await;
|
||||
|
||||
let mut diff_args = vec!["diff", "HEAD", "--"];
|
||||
let mut diff_args = vec!["diff", base_sha.as_str(), "--"];
|
||||
diff_args.extend(excludes.iter().map(String::as_str));
|
||||
let patch = git(&repo, &diff_args).await.unwrap_or_default();
|
||||
|
||||
let mut stat_args = vec!["diff", "HEAD", "--stat", "--"];
|
||||
let mut stat_args = vec!["diff", base_sha.as_str(), "--stat", "--"];
|
||||
stat_args.extend(excludes.iter().map(String::as_str));
|
||||
let diffstat = git(&repo, &stat_args).await.unwrap_or_default();
|
||||
|
||||
@@ -175,6 +193,7 @@ pub async fn capture_phase_diff_at(
|
||||
|
||||
let meta = json!({
|
||||
"base_sha": base_sha,
|
||||
"base_recorded": base_is_recorded,
|
||||
"files_changed": files_changed,
|
||||
"insertions": insertions,
|
||||
"deletions": deletions,
|
||||
|
||||
Reference in New Issue
Block a user