From b1fb172dde06d917cd4747efb968a1c68f2f2e46 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 26 Mar 2026 23:28:08 -0400 Subject: [PATCH] fix: detect early init failures in attach and roll back partial worktree state Two pre-existing issues fixed: 1. attach: while waiting for progress.jsonl, check for terminal status.json and engine child death. A detached run that dies during early init (before any event fires) now surfaces the real failure instead of timing out after 10s. 2. worktree: when `git worktree add` fails after branch creation, roll back the branch with `git branch -D` to avoid leaking partial git state. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/crates/fabro-cli/src/commands/attach.rs | 27 ++++++++++++++++++++- lib/crates/fabro-sandbox/src/worktree.rs | 8 ++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/crates/fabro-cli/src/commands/attach.rs b/lib/crates/fabro-cli/src/commands/attach.rs index 31dfffc4d..f0745111e 100644 --- a/lib/crates/fabro-cli/src/commands/attach.rs +++ b/lib/crates/fabro-cli/src/commands/attach.rs @@ -56,11 +56,36 @@ pub async fn attach_run( }); } - // Wait for progress.jsonl to appear + // Wait for progress.jsonl to appear. + // If the engine dies during early init (before any event is emitted), + // progress.jsonl may never be created. Check for terminal status or + // engine death so we surface the real failure instead of timing out. let mut wait_count = 0; while !progress_path.exists() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; wait_count += 1; + + // Check if engine died before writing any progress + if let Some(record) = read_status_record(&status_path) { + if record.status.is_terminal() { + progress_ui.finish(); + return Ok(determine_exit_code(&conclusion_path, Some(record))); + } + } + + if let Some(guard) = engine_guard.as_mut() { + if let Some(child) = guard.inner() { + if matches!(child.try_wait(), Ok(Some(_))) { + // Engine exited without writing progress.jsonl + progress_ui.finish(); + return Ok(determine_exit_code( + &conclusion_path, + read_status_record(&status_path), + )); + } + } + } + if wait_count > 100 { // Guard's Drop kills+waits on the engine child drop(engine_guard.take()); diff --git a/lib/crates/fabro-sandbox/src/worktree.rs b/lib/crates/fabro-sandbox/src/worktree.rs index 2f53e10a7..399cac110 100644 --- a/lib/crates/fabro-sandbox/src/worktree.rs +++ b/lib/crates/fabro-sandbox/src/worktree.rs @@ -148,6 +148,14 @@ impl Sandbox for WorktreeSandbox { .exec_command(&add_cmd, 30_000, None, None, None) .await?; if result.exit_code != 0 { + // Roll back the branch created above so we don't leak partial state. + if !self.config.skip_branch_creation { + let rollback_cmd = format!("{GIT} branch -D {branch}"); + let _ = self + .inner + .exec_command(&rollback_cmd, 30_000, None, None, None) + .await; + } return Err(format!( "git worktree add failed (exit {}): {}", result.exit_code,