fix(delivery): four failures from the #55 trace — auth, prompts, truncation, retry

All four were surfaced while tracing #55 and left open. Each one on its own is
small; together they are why a two-line git rejection took hours to read.

**1. `with_ambient_auth` failed open.** It matched one literal prefix,
`https://git.redclaw.dev/`, and returned the URL unchanged for everything else
with no log line. An `http://` remote, an explicit port, a different case in the
host, an ssh remote, a URL that already carried userinfo — all came back
unauthenticated and looked identical to success. It now returns `Authed`, which
carries the URL AND why no credential reached it, and recognises the forge in
every shape a remote can be written (host parsed with userinfo stripped BEFORE
the port, or `oauth2:token@host` reports its username as the host — the first
version of this function did exactly that and failed its own test).

**2. Nothing set `GIT_TERMINAL_PROMPT=0`.** So a credential-less URL did not
fail — git opened `/dev/tty`, and in a server container that surfaces as
`No such device or address`, several layers from the missing token. Now set on
every git invocation that can reach the network. And `push_url_for` refuses
outright when the URL is on OUR forge and unauthenticated: that push cannot
succeed, and letting it proceed only buys a symptom that looks like something
else.

**3. The truncation fix went to the wrong path.** e31688b clamped the caller,
but a rejected push comes back as `Ok(Publish { error })` — the string was
already cut to 300 head chars inside `git()`, so the reject reason had been
dropped before the both-ends clamp ever saw it. Clamped where the output is
produced, and redacted there too.

**4. #55: a mission that re-clones can never push.** The branch name is
deterministic per (mission, phase, iteration), so a checkout rebuilt after a
retry, a container teardown or disk loss produces divergent history against its
own branch, and git rejects it — leaving the work on a local branch in a
directory the sweeper deletes. Reachable in normal operation, not just by
deleting a checkout by hand.

The escape is a NEW ref, not `--force`: forcing would overwrite whatever the
earlier attempt pushed, which may be the only copy of that work, to make this
attempt look tidy. The retry lands on `<branch>-<sha8>` — deterministic,
self-describing in a branch list, and collision-free since divergent history is
by definition a different sha. "Never force" stays a rule.

NEGATIVE CONTROL, run rather than assumed: with the rescue arm disabled,
`diverged_history_lands_on_a_new_branch_instead_of_being_lost` FAILS with git's
real `! [rejected] ... (fetch first)` — which also demonstrates fix 3, since that
whole message now survives to the assertion. The test asserts the earlier
attempt's ref is byte-identical afterwards.

Also measured, not read off docs: which hooks fire under `claude -p` (2.1.222,
via `--settings`). SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
SubagentStop and Stop fire; TaskCreated, TaskCompleted, TeammateIdle, SessionEnd,
Notification and PreCompact do not. So the agent-teams hooks Slice 3 deferred are
inert on our path by construction, and `Stop` is the seam that could move
`done_when` into the agent's own loop.

507 tests pass, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-06 13:15:15 -07:00
co-authored by Claude Opus 5
parent 12147a1e01
commit 1d554396f4
4 changed files with 472 additions and 56 deletions
+20 -8
View File
@@ -93,9 +93,13 @@ pub async fn clone_vault(clone_url: &str, work_root: &Path) -> Result<PathBuf, S
.map_err(|e| format!("mkdir {}: {e}", work_root.display()))?;
let auth = mission_workspace::with_ambient_auth(clone_url);
let out = tokio::process::Command::new("git")
.args(["clone", "--quiet", "--depth", "1", &auth])
.arg(&path)
if let Some(why) = &auth.unauthenticated {
eprintln!("library: cloning the vault WITHOUT credentials — {why}");
}
let mut cmd = tokio::process::Command::new("git");
cmd.args(["clone", "--quiet", "--depth", "1", &auth.url])
.arg(&path);
let out = mission_workspace::no_terminal_prompt(&mut cmd)
.output()
.await
.map_err(|e| format!("spawn git clone: {e}"))?;
@@ -103,16 +107,15 @@ pub async fn clone_vault(clone_url: &str, work_root: &Path) -> Result<PathBuf, S
return Err(format!(
"clone vault → {}: {}",
out.status,
mission_workspace::redact_token(&String::from_utf8_lossy(&out.stderr))
.chars()
.take(300)
.collect::<String>()
crate::evaluator_tools::clamp_output(&mission_workspace::redact_token(
&String::from_utf8_lossy(&out.stderr)
))
));
}
// The token must not stay in .git/config: the checkout may be handed to a
// container later, and a credential in a file an agent can read is a
// credential an agent has.
mission_workspace::scrub_remote_credentials(&path, &auth);
mission_workspace::scrub_remote_credentials(&path, &auth.url);
Ok(path)
}
@@ -186,6 +189,15 @@ pub async fn run_to_vault(
git(&vault, &["commit", "--no-verify", "-m", &message]).await?;
let auth = mission_workspace::with_ambient_auth(clone_url);
if let Some(why) = &auth.unauthenticated {
if auth.is_forge() {
// Not fatal here — the push below reports its own failure — but the
// reason belongs in the log next to the attempt, not inferred from a
// tty error two layers down.
eprintln!("library: pushing to the forge WITHOUT credentials — {why}");
}
}
let auth = auth.url;
let refspec = format!("HEAD:refs/heads/{branch}");
match git(&vault, &["push", &auth, &refspec]).await {
Ok(_) => {