diff --git a/graph.fabro b/graph.fabro new file mode 100644 index 000000000..e69de29bb diff --git a/manifest.json b/manifest.json new file mode 100644 index 000000000..723245b36 --- /dev/null +++ b/manifest.json @@ -0,0 +1,13 @@ +{ + "run_id": "01KM4CCACXBP0M7KEEWK784ZD9", + "workflow_name": "ImplementAndSimplify", + "goal": "# Unified WorktreeSandbox\n\n## Context\n\nWorktree management is currently split across two locations with duplicated logic:\n\n1. **Parallel branches** (`parallel.rs`): Inline git commands in `ParallelHandler::execute()`, a thin `WorktreeSandbox` decorator, and separate local/remote code paths\n2. **Top-level CLI run** (`run.rs`): A `setup_worktree()` function using synchronous git helpers, with the worktree path fed into a plain `LocalSandbox`\n\nThe goal is a single `WorktreeSandbox` type that wraps any `Arc`, manages the worktree lifecycle in `initialize()`/`cleanup()`, and eliminates the local/remote branching.\n\n## Plan\n\n### Step 1: Create `WorktreeSandbox` in fabro-sandbox\n\n**New file:** `lib/crates/fabro-sandbox/src/worktree.rs`\n\nDefine:\n\n```rust\npub enum WorktreeEvent {\n BranchCreated { branch: String, sha: String },\n WorktreeAdded { path: String, branch: String },\n WorktreeRemoved { path: String },\n Reset { sha: String },\n}\n\npub type WorktreeEventCallback = Arc;\n\npub struct WorktreeConfig {\n pub branch_name: String,\n pub base_sha: String,\n pub worktree_path: String,\n /// Skip branch creation and reset (for resume, where branch already exists).\n pub skip_branch_creation: bool,\n}\n\npub struct WorktreeSandbox {\n inner: Arc,\n config: WorktreeConfig,\n event_callback: Option,\n}\n```\n\n**Constructor + getters:** `new(inner, config)`, `set_event_callback()`, `branch_name()`, `base_sha()`, `worktree_path()`\n\n**`initialize()`:**\n1. If `!skip_branch_creation`: `git branch --force {branch_name} {base_sha}` via `inner.exec_command()`, emit `BranchCreated`\n2. `git worktree remove --force {path}` (best-effort), then `git worktree add {path} {branch}`, emit `WorktreeAdded`\n3. If `!skip_branch_creation`: `git reset --hard {base_sha}` in worktree dir, emit `Reset`\n\nDoes NOT call `inner.initialize()` — the inner sandbox's lifecycle is managed separately.\n\n**`cleanup()`:** `git worktree remove --force {path}`, emit `WorktreeRemoved`. Does NOT call `inner.cleanup()`.\n\n**`working_directory()`:** Returns `config.worktree_path`.\n\n**`exec_command()`:** Defaults `working_dir` to `config.worktree_path` when `None`, delegates to inner.\n\n**All other Sandbox methods:** Delegate to inner. Must be a manual `impl Sandbox` block (can't use `delegate_sandbox!` since it generates `initialize`/`cleanup`/`working_directory`/`exec_command` which we need to override).\n\nAll interpolated values in git commands use `shell_quote()`.\n\n### Step 2: Register module and re-exports\n\n- `lib/crates/fabro-sandbox/src/lib.rs`: Add `pub mod worktree;` and `pub use worktree::WorktreeSandbox;`\n- `lib/crates/fabro-agent/src/sandbox.rs`: Add re-export of `WorktreeSandbox`\n\n### Step 3: Unit tests for WorktreeSandbox\n\nIn `worktree.rs` `#[cfg(test)]` module, using `MockSandbox`:\n\n- `initialize()` issues correct git commands (branch, worktree remove, worktree add, reset) and emits events\n- `skip_branch_creation` skips branch + reset, only does worktree add\n- `cleanup()` issues `worktree remove` and emits `WorktreeRemoved`\n- `working_directory()` returns worktree path\n- `exec_command()` with `None` working_dir defaults to worktree path\n- `exec_command()` with explicit working_dir passes it through\n- `initialize()` propagates errors on non-zero exit\n\n**MockSandbox enhancement:** Add `captured_commands: Mutex>` field to `test_support.rs` to capture the sequence of `exec_command` calls (current `captured_command` only stores the last one). Append to vec in `exec_command()` impl.\n\n### Step 4: Refactor parallel.rs\n\n- **Remove** the private `WorktreeSandbox` struct (lines 28-126) and `use fabro_agent::LocalSandbox`\n- **Replace** the inline git setup loop (lines 361-450) with:\n - Construct `WorktreeConfig` with branch name, base SHA, worktree path\n - Create `WorktreeSandbox::new(Arc::clone(&services.sandbox), config)`\n - Wire event callback to bridge `WorktreeEvent` → `WorkflowRunEvent`\n - Call `initialize().await`\n- This eliminates the `if services.sandbox.is_remote()` branch (lines 442-449) — `WorktreeSandbox` works the same for any inner sandbox\n- **Cleanup loop** (lines 659-668): Keep calling `git_remove_worktree()` on the parent sandbox (the `WorktreeSandbox` Arc is consumed by the spawned task and dropped). Alternatively, could store the sandbox Arc in `BranchResult` and call `.cleanup()`, but the current approach is simpler.\n\n### Step 5: Refactor run.rs — new runs\n\nReplace `setup_worktree()` call (lines 830-845) + separate `LocalSandbox` construction with:\n\n```\nif workdir_strategy == LocalWorktree:\n base_sha = git::head_sha()\n branch_name = \"fabro/run/{run_id}\"\n inner = Arc::new(LocalSandbox::new(original_cwd))\n wt_sandbox = WorktreeSandbox::new(inner, WorktreeConfig { ... })\n wt_sandbox.set_event_callback(bridge to WorkflowRunEvent)\n wt_sandbox.initialize().await\n std::env::set_current_dir(&worktree_path) // stays in CLI, not in sandbox\n sandbox = Arc::new(wt_sandbox)\n // store base_sha, branch_name for RunConfig\n```\n\n**Delete** the `setup_worktree()` function (lines 1696-1714) — its logic is absorbed above.\n\n`std::env::set_current_dir()` stays in `run.rs` — it's a process-global side effect that belongs to the CLI.\n\n### Step 6: Refactor run.rs — resume (run_from_branch)\n\nReplace worktree re-attachment (lines 1810-1822) with:\n\n```\ninner = Arc::new(LocalSandbox::new(original_cwd))\nwt_sandbox = WorktreeSandbox::new(inner, WorktreeConfig {\n branch_name: run_branch,\n base_sha: base_sha.unwrap_or_default(),\n worktree_path: wt_str,\n skip_branch_creation: true, // branch already exists\n})\nwt_sandbox.initialize().await\nstd::env::set_current_dir(&wt)\n```\n\n### Step 7: Verify\n\n- `cargo build --workspace`\n- `cargo test --workspace`\n- `cargo clippy --workspace -- -D warnings`\n- Manual: `fabro run` with worktree mode enabled on a local workflow\n- Manual: `fabro run --run-branch` to test resume path\n\n## Files to modify\n\n| File | Change |\n|---|---|\n| `lib/crates/fabro-sandbox/src/worktree.rs` | **New** — WorktreeSandbox, WorktreeConfig, WorktreeEvent, impl Sandbox, tests |\n| `lib/crates/fabro-sandbox/src/lib.rs` | Add module + re-export |\n| `lib/crates/fabro-sandbox/src/test_support.rs` | Add `captured_commands: Mutex>` to MockSandbox |\n| `lib/crates/fabro-agent/src/sandbox.rs` | Add WorktreeSandbox re-export |\n| `lib/crates/fabro-workflows/src/handler/parallel.rs` | Remove old WorktreeSandbox, use new one |\n| `lib/crates/fabro-cli/src/commands/run.rs` | Replace setup_worktree + run_from_branch worktree logic |\n\n## Functions that become removable\n\n| Function | Location | Reason |\n|---|---|---|\n| `setup_worktree()` | `run.rs:1696` | Logic absorbed into WorktreeSandbox |\n| Old `WorktreeSandbox` struct | `parallel.rs:28-126` | Replaced by shared WorktreeSandbox |\n\nEngine git helpers (`git_add_worktree`, `git_remove_worktree`, etc. in `engine.rs`) stay — still used by parallel cleanup and potentially other callers. Sync git helpers in `git.rs` also stay.\n", + "start_time": "2026-03-20T01:05:04.305274Z", + "node_count": 12, + "edge_count": 15, + "run_branch": "fabro/run/01KM4CCACXBP0M7KEEWK784ZD9", + "base_sha": "1fc3495ba93e711faaa7ea4db9819d1f61d03b78", + "base_branch": "main", + "workflow_slug": "implement", + "host_repo_path": "/Users/bhelmkamp/p/fabro-sh/fabro" +} \ No newline at end of file