checkpoint

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-03-19 22:03:09 -04:00
parent a523a883fb
commit 0fbf4dc46f
5 changed files with 339 additions and 34 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,272 @@
diff --git a/lib/crates/fabro-cli/src/commands/run.rs b/lib/crates/fabro-cli/src/commands/run.rs
index dfe34fa9..d41bef0e 100644
--- a/lib/crates/fabro-cli/src/commands/run.rs
+++ b/lib/crates/fabro-cli/src/commands/run.rs
@@ -8,8 +8,7 @@ use anyhow::{bail, Context};
use chrono::{Local, Utc};
use clap::{Args, ValueEnum};
use fabro_agent::{
- DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox, WorktreeConfig, WorktreeEvent,
- WorktreeEventCallback, WorktreeSandbox,
+ DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox, WorktreeConfig, WorktreeSandbox,
};
use fabro_config::run::{RunDefaults, WorkflowRunConfig};
use fabro_config::{project as project_config, run as run_config, sandbox as sandbox_config};
@@ -423,25 +422,6 @@ struct CostAccumulator {
has_pricing: bool,
}
-/// Build a callback that forwards [`WorktreeEvent`]s as workflow run events on `emitter`.
-fn worktree_event_callback(emitter: Arc<EventEmitter>) -> WorktreeEventCallback {
- Arc::new(move |event| match event {
- WorktreeEvent::BranchCreated { branch, sha } => {
- emitter.emit(&fabro_workflows::event::WorkflowRunEvent::GitBranch { branch, sha });
- }
- WorktreeEvent::WorktreeAdded { path, branch } => {
- emitter
- .emit(&fabro_workflows::event::WorkflowRunEvent::GitWorktreeAdd { path, branch });
- }
- WorktreeEvent::WorktreeRemoved { path } => {
- emitter.emit(&fabro_workflows::event::WorkflowRunEvent::GitWorktreeRemove { path });
- }
- WorktreeEvent::Reset { sha } => {
- emitter.emit(&fabro_workflows::event::WorkflowRunEvent::GitReset { sha });
- }
- })
-}
-
/// Create a [`LocalSandbox`] wired to emit [`WorkflowRunEvent::Sandbox`] events.
fn local_sandbox_with_callback(cwd: PathBuf, emitter: Arc<EventEmitter>) -> Arc<dyn Sandbox> {
let mut env = LocalSandbox::new(cwd);
@@ -1168,7 +1148,7 @@ pub async fn run_command(
skip_branch_creation: false,
};
let mut wt_sandbox = WorktreeSandbox::new(inner, wt_config);
- wt_sandbox.set_event_callback(worktree_event_callback(Arc::clone(&emitter)));
+ wt_sandbox.set_event_callback(Arc::clone(&emitter).worktree_callback());
match wt_sandbox.initialize().await {
Ok(()) => {
@@ -1866,7 +1846,7 @@ async fn run_from_branch(
skip_branch_creation: true, // branch already exists on resume
};
let mut wt_sandbox = WorktreeSandbox::new(inner, wt_config);
- wt_sandbox.set_event_callback(worktree_event_callback(Arc::clone(&emitter)));
+ wt_sandbox.set_event_callback(Arc::clone(&emitter).worktree_callback());
wt_sandbox.initialize().await.map_err(|e| {
anyhow::anyhow!("failed to attach worktree to {run_branch}: {e}")
diff --git a/lib/crates/fabro-sandbox/src/worktree.rs b/lib/crates/fabro-sandbox/src/worktree.rs
index 3283ae14..d9806166 100644
--- a/lib/crates/fabro-sandbox/src/worktree.rs
+++ b/lib/crates/fabro-sandbox/src/worktree.rs
@@ -18,7 +18,6 @@ pub enum WorktreeEvent {
BranchCreated { branch: String, sha: String },
WorktreeAdded { path: String, branch: String },
WorktreeRemoved { path: String },
- Reset { sha: String },
}
/// Callback type for worktree lifecycle events.
@@ -143,30 +142,6 @@ impl Sandbox for WorktreeSandbox {
branch: self.config.branch_name.clone(),
});
- if !self.config.skip_branch_creation {
- let reset_cmd = format!("{GIT} reset --hard {sha}");
- let result = self
- .inner
- .exec_command(
- &reset_cmd,
- 30_000,
- Some(&self.config.worktree_path),
- None,
- None,
- )
- .await?;
- if result.exit_code != 0 {
- return Err(format!(
- "git reset --hard failed (exit {}): {}",
- result.exit_code,
- result.stderr.trim()
- ));
- }
- self.emit(WorktreeEvent::Reset {
- sha: self.config.base_sha.clone(),
- });
- }
-
Ok(())
}
@@ -359,8 +334,8 @@ mod tests {
wt.initialize().await.unwrap();
let cmds = mock.captured_commands.lock().unwrap().clone();
- // branch --force, worktree remove (best-effort), worktree add, reset --hard
- assert_eq!(cmds.len(), 4, "expected 4 git commands, got: {cmds:?}");
+ // branch --force, worktree remove (best-effort), worktree add
+ assert_eq!(cmds.len(), 3, "expected 3 git commands, got: {cmds:?}");
assert!(cmds[0].contains("branch --force"), "cmd[0]: {}", cmds[0]);
assert!(
cmds[1].contains("worktree remove --force"),
@@ -368,11 +343,10 @@ mod tests {
cmds[1]
);
assert!(cmds[2].contains("worktree add"), "cmd[2]: {}", cmds[2]);
- assert!(cmds[3].contains("reset --hard"), "cmd[3]: {}", cmds[3]);
}
#[tokio::test]
- async fn initialize_emits_branch_worktree_reset_events() {
+ async fn initialize_emits_branch_and_worktree_events() {
let (inner, _mock) = make_mock();
let mut wt = WorktreeSandbox::new(inner, make_config("/tmp/wt"));
@@ -383,7 +357,6 @@ mod tests {
WorktreeEvent::BranchCreated { .. } => "BranchCreated",
WorktreeEvent::WorktreeAdded { .. } => "WorktreeAdded",
WorktreeEvent::WorktreeRemoved { .. } => "WorktreeRemoved",
- WorktreeEvent::Reset { .. } => "Reset",
};
events_clone.lock().unwrap().push(label.to_string());
}));
@@ -391,7 +364,7 @@ mod tests {
wt.initialize().await.unwrap();
let captured = events.lock().unwrap();
- assert_eq!(*captured, vec!["BranchCreated", "WorktreeAdded", "Reset"]);
+ assert_eq!(*captured, vec!["BranchCreated", "WorktreeAdded"]);
}
#[tokio::test]
@@ -416,26 +389,6 @@ mod tests {
);
}
- #[tokio::test]
- async fn initialize_reset_uses_worktree_path_as_working_dir() {
- let (inner, mock) = make_mock();
- let wt = WorktreeSandbox::new(inner, make_config("/tmp/wt"));
-
- wt.initialize().await.unwrap();
-
- let wdirs = mock.captured_working_dirs.lock().unwrap().clone();
- // reset command is at index 3, should use worktree path
- assert_eq!(
- wdirs[3],
- Some("/tmp/wt".to_string()),
- "reset --hard should run in worktree dir"
- );
- // branch, remove, add commands use None (inner's default)
- assert_eq!(wdirs[0], None, "branch command should use inner default");
- assert_eq!(wdirs[1], None, "worktree remove should use inner default");
- assert_eq!(wdirs[2], None, "worktree add should use inner default");
- }
-
// -----------------------------------------------------------------------
// initialize() — skip_branch_creation = true
// -----------------------------------------------------------------------
@@ -470,7 +423,6 @@ mod tests {
WorktreeEvent::BranchCreated { .. } => "BranchCreated",
WorktreeEvent::WorktreeAdded { .. } => "WorktreeAdded",
WorktreeEvent::WorktreeRemoved { .. } => "WorktreeRemoved",
- WorktreeEvent::Reset { .. } => "Reset",
};
events_clone.lock().unwrap().push(label.to_string());
}));
diff --git a/lib/crates/fabro-workflows/src/event.rs b/lib/crates/fabro-workflows/src/event.rs
index 227f18fa..03c7d3bd 100644
--- a/lib/crates/fabro-workflows/src/event.rs
+++ b/lib/crates/fabro-workflows/src/event.rs
@@ -1,9 +1,10 @@
use std::sync::atomic::{AtomicI64, Ordering};
+use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::outcome::StageUsage;
-use fabro_agent::{AgentEvent, SandboxEvent};
+use fabro_agent::{AgentEvent, SandboxEvent, WorktreeEvent, WorktreeEventCallback};
/// Events emitted during workflow run execution for observability.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -1070,6 +1071,22 @@ impl EventEmitter {
pub fn touch(&self) {
self.last_event_at.store(epoch_millis(), Ordering::Relaxed);
}
+
+ /// Build a [`WorktreeEventCallback`] that forwards worktree lifecycle events as
+ /// [`WorkflowRunEvent`]s on this emitter.
+ pub fn worktree_callback(self: Arc<Self>) -> WorktreeEventCallback {
+ Arc::new(move |event| match event {
+ WorktreeEvent::BranchCreated { branch, sha } => {
+ self.emit(&WorkflowRunEvent::GitBranch { branch, sha });
+ }
+ WorktreeEvent::WorktreeAdded { path, branch } => {
+ self.emit(&WorkflowRunEvent::GitWorktreeAdd { path, branch });
+ }
+ WorktreeEvent::WorktreeRemoved { path } => {
+ self.emit(&WorkflowRunEvent::GitWorktreeRemove { path });
+ }
+ })
+ }
}
#[cfg(test)]
diff --git a/lib/crates/fabro-workflows/src/handler/parallel.rs b/lib/crates/fabro-workflows/src/handler/parallel.rs
index 5ff62054..63ca994f 100644
--- a/lib/crates/fabro-workflows/src/handler/parallel.rs
+++ b/lib/crates/fabro-workflows/src/handler/parallel.rs
@@ -3,14 +3,14 @@ use std::sync::Arc;
use std::time::Instant;
use async_trait::async_trait;
-use fabro_agent::{Sandbox, WorktreeConfig, WorktreeEvent, WorktreeEventCallback, WorktreeSandbox};
+use fabro_agent::{Sandbox, WorktreeConfig, WorktreeSandbox};
use tokio::sync::Semaphore;
use crate::context::keys;
use crate::context::Context;
use crate::engine::set_hook_node;
use crate::error::FabroError;
-use crate::event::{EventEmitter, WorkflowRunEvent};
+use crate::event::WorkflowRunEvent;
use crate::millis_u64;
use crate::outcome::{Outcome, StageStatus};
use fabro_graphviz::graph::{Graph, Node};
@@ -91,24 +91,6 @@ fn parse_error_policy(raw: &str) -> ErrorPolicy {
}
}
-/// Bridge a [`WorktreeEvent`] to the corresponding workflow run event on `emitter`.
-fn worktree_event_callback(emitter: Arc<EventEmitter>) -> WorktreeEventCallback {
- Arc::new(move |event| match event {
- WorktreeEvent::BranchCreated { branch, sha } => {
- emitter.emit(&WorkflowRunEvent::GitBranch { branch, sha });
- }
- WorktreeEvent::WorktreeAdded { path, branch } => {
- emitter.emit(&WorkflowRunEvent::GitWorktreeAdd { path, branch });
- }
- WorktreeEvent::WorktreeRemoved { path } => {
- emitter.emit(&WorkflowRunEvent::GitWorktreeRemove { path });
- }
- WorktreeEvent::Reset { sha } => {
- emitter.emit(&WorkflowRunEvent::GitReset { sha });
- }
- })
-}
-
struct BranchResult {
id: String,
outcome: Outcome,
@@ -313,8 +295,7 @@ impl Handler for ParallelHandler {
skip_branch_creation: false,
};
let mut wt_sandbox = WorktreeSandbox::new(Arc::clone(&services.sandbox), wt_config);
- wt_sandbox
- .set_event_callback(worktree_event_callback(Arc::clone(&services.emitter)));
+ wt_sandbox.set_event_callback(Arc::clone(&services.emitter).worktree_callback());
wt_sandbox
.initialize()
.await

View file

@ -0,0 +1,5 @@
{
"command": "cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1",
"language": "shell",
"timeout_ms": null
}

View file

@ -0,0 +1,5 @@
{
"duration_ms": 3878,
"exit_code": 100,
"timed_out": false
}

6
nodes/verify/status.json Normal file

File diff suppressed because one or more lines are too long