From 1ddc7b3c71fb505c1b8befc469c6a4de6c21981f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 20:36:38 +0000 Subject: [PATCH] agent: patch-surface fixes from the comrak-005 transcript - propose_patch allows the /dev/null headers git emits for new/deleted files (previously every new-file diff was refused as an absolute path). - read_target_source serves HEAD (pristine) content instead of the working tree, matching propose_patch's standalone-vs-pristine base so post-proposal reads can't drift a later diff's context lines. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EcKPWQ3MQD4NdEHwar96u2 --- crates/harnessd/src/main.rs | 28 +++++++++++++++++++++++----- results/ledger.sqlite | Bin 65536 -> 69632 bytes 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/harnessd/src/main.rs b/crates/harnessd/src/main.rs index a914edb..178307e 100644 --- a/crates/harnessd/src/main.rs +++ b/crates/harnessd/src/main.rs @@ -189,8 +189,22 @@ fn handle(root: &Path, line: &str) -> Result { let p = field(&req, "path")?; check_target(t)?; check_rel_path(p)?; - let full = root.join(format!("targets/{t}/workspace")).join(p); - let text = std::fs::read_to_string(full)?; + // Serve the PRISTINE (HEAD) content, not the working tree: + // propose_patch applies each proposal against a reset + // workspace, so reads must show the same base the diff needs + // — otherwise reads after a proposal show the patched tree + // and the next diff's context lines drift (phase2-comrak-005 + // burned turns rediscovering this). + let ws = root.join(format!("targets/{t}/workspace")); + let shown = Command::new("git") + .args(["-C", ws.to_str().unwrap(), "show", &format!("HEAD:{p}")]) + .output()?; + ensure!( + shown.status.success(), + "cannot read {p} at HEAD: {}", + String::from_utf8_lossy(&shown.stderr).trim() + ); + let text = String::from_utf8_lossy(&shown.stdout).into_owned(); // Optional line window so large source files (100KB+) can be read in // chunks instead of overflowing one response — otherwise the agent // is tempted to reach for a shell it must not have. @@ -231,7 +245,9 @@ fn handle(root: &Path, line: &str) -> Result { ); // Allowlist: every path named by the diff must be a safe // relative path (the git -C below roots them in the target - // workspace; nothing outside it is reachable). + // workspace; nothing outside it is reachable). `/dev/null` + // is the one absolute path git legitimately emits, for + // new/deleted files. for l in diff.lines() { if let Some(p) = l .strip_prefix("--- a/") @@ -240,8 +256,10 @@ fn handle(root: &Path, line: &str) -> Result { check_rel_path(p.trim())?; } ensure!( - !l.starts_with("--- /") && !l.starts_with("+++ /"), - "absolute diff paths forbidden" + (!l.starts_with("--- /") && !l.starts_with("+++ /")) + || l == "--- /dev/null" + || l == "+++ /dev/null", + "absolute diff paths forbidden (only /dev/null is allowed, for new/deleted files)" ); } let ws = root.join(format!("targets/{t}/workspace")); diff --git a/results/ledger.sqlite b/results/ledger.sqlite index 9594c8fb21b25a8302bc5501fb6f93cb7048015f..35ab862e468a4f1a21bbad9e61d8ee5f05ee51c9 100644 GIT binary patch delta 5580 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 delta 106 zcmV-w0G0oMpag(`1dtmA5&!@I5Ro810TQubq#qy-4#5Bq>kq&W)(@)>mJdS@c@JC< zEe{tD0S?v<><+=RF)*DDv%xMy0SXQeSpX0958V%#vk)*m50Ow0vspi@um%AMLjVaw Mv4J@WvqSm41~uRxl>h($