diff --git a/graph.fabro b/graph.fabro new file mode 100644 index 000000000..4bc3ad5ee --- /dev/null +++ b/graph.fabro @@ -0,0 +1,38 @@ +digraph ImplementAndSimplify { + graph [ + goal="Implement and simplify", + model_stylesheet=" + * { backend: api; model: claude-opus-4-6;} + " + ] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0] + preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0] + preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0] + fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3] + implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."] + simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"] + simplify_gemini [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"] + simplify_gpt [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"] + verify [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"] + fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3] + fmt [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0] + + start -> toolchain + toolchain -> preflight_compile [condition="outcome=success"] + toolchain -> exit + preflight_compile -> preflight_lint [condition="outcome=success"] + preflight_compile -> exit + preflight_lint -> implement [condition="outcome=success"] + preflight_lint -> fix_lints + fix_lints -> preflight_lint + implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify + verify -> fmt [condition="outcome=success"] + verify -> fixup + fixup -> verify + fmt -> exit +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 000000000..833395e9f --- /dev/null +++ b/manifest.json @@ -0,0 +1,13 @@ +{ + "run_id": "01KKTC38AXE3WSR695SJXKK70Y", + "workflow_name": "ImplementAndSimplify", + "goal": "# Plan: Move sandbox lifecycle into the engine\n\n## Context\n\n`SandboxReady` and `SandboxCleanup` hook events are defined in `HookEvent` but never fired. They can't fire naturally because sandbox init/cleanup happens in `cli/run.rs` — outside the engine where all other hooks fire. The root cause is that `cli/run.rs` is a ~2000-line god function orchestrating everything: sandbox creation, init, setup commands, devcontainer lifecycle, engine construction, engine run, retro, PR creation, and cleanup. The engine receives an already-initialized sandbox and has no role in lifecycle management.\n\nThis refactor moves sandbox initialization and setup into the engine so `SandboxReady` fires naturally alongside `RunStart`. Cleanup stays callable from the CLI (the retro agent needs the sandbox alive after the graph completes) but fires through an engine method so `SandboxCleanup` also uses the hook infrastructure.\n\n## Design\n\n### Two new engine methods\n\n**`run_with_lifecycle(graph, config, lifecycle, checkpoint?) -> Result`**\n1. `sandbox.initialize()`\n2. Fire `SandboxReady` hook (blocking — can abort run)\n3. Emit `SandboxInitialized` event (CLI listener writes `sandbox.json`, updates progress UI)\n4. Remote git setup if `sandbox.is_remote()` — produces `base_sha`, `run_branch`, `base_branch`, merged into `config`\n5. Run setup commands inside sandbox\n6. Run devcontainer lifecycle phases inside sandbox\n7. Call existing `run_internal()` (unchanged — fires RunStart → graph → RunComplete)\n8. Return outcome (sandbox still alive)\n\n**`cleanup_sandbox(run_id, workflow_name, preserve) -> Result<()>`**\n1. Fire `SandboxCleanup` hook (non-blocking)\n2. If `!preserve`: call `sandbox.cleanup()`\n\nExisting `run()` is unchanged — API server and integration tests keep using it with pre-initialized sandboxes.\n\n### New config struct\n\n```rust\n// engine.rs\npub struct LifecycleConfig {\n pub setup_commands: Vec,\n pub setup_command_timeout_ms: u64,\n pub devcontainer_phases: Vec<(String, Vec)>,\n}\n```\n\n`run_with_lifecycle` takes `config: RunConfig` by value (currently `&RunConfig`) so it can fill in remote git values. `run_internal` continues to take `&RunConfig`.\n\n### CLI flow after refactor\n\n```\nsandbox = create_sandbox() // unchanged\nsandbox = ReadBeforeWriteSandbox::new(sandbox) // moved before init (delegate_sandbox! delegates initialize)\nengine = build_engine(sandbox, hook_runner, ...)\noutcome = engine.run_with_lifecycle(graph, config, lifecycle)\n// retro, conclusion, PR creation — sandbox still alive\nengine.cleanup_sandbox(run_id, workflow_name, preserve)\n```\n\nSingle scopeguard around the entire block that calls `engine.cleanup_sandbox()` on panic.\n\n## Steps\n\n### 1. `hook/types.rs` — Make `SandboxReady` blocking by default\n- Add `Self::SandboxReady` to `is_blocking_by_default()` match arm (line 29-32)\n- `SandboxCleanup` stays non-blocking (correct default)\n\n### 2. `engine.rs` — Add `LifecycleConfig` struct and `run_with_lifecycle` method\n- Define `LifecycleConfig` (setup_commands, setup_command_timeout_ms, devcontainer_phases)\n- Add `pub async fn run_with_lifecycle(self, graph, config, lifecycle, checkpoint) -> Result` that:\n - Calls `self.services.sandbox.initialize()`\n - Fires `SandboxReady` hook via `self.run_hooks()`\n - Emits `WorkflowRunEvent::SandboxInitialized { working_directory }` via emitter\n - Calls remote git setup if `sandbox.is_remote()`, fills config.base_sha/run_branch/base_branch/git_checkpoint_enabled\n - Runs setup commands via `sandbox.exec_command()`, emitting Setup* events\n - Runs devcontainer lifecycle via `devcontainer_bridge::run_devcontainer_lifecycle()`\n - Calls `self.run_internal()` (or `run_from_checkpoint` path)\n - Returns outcome\n\n### 3. `engine.rs` — Add `cleanup_sandbox` method\n- `pub async fn cleanup_sandbox(&self, run_id, workflow_name, preserve) -> Result<(), String>`\n- Fires `SandboxCleanup` hook\n- If `!preserve`: calls `self.services.sandbox.cleanup()`\n\n### 4. `engine.rs` — Move `setup_remote_git` from `cli/run.rs`\n- Move the `setup_remote_git()` function (cli/run.rs line 1636-1679) into engine.rs\n- It only uses `sandbox.exec_command()` and `run_id` — no CLI dependencies\n\n### 5. `event.rs` — Add `SandboxInitialized` event variant\n- Add `WorkflowRunEvent::SandboxInitialized { working_directory: String }` variant\n- This replaces the inline sandbox.json writing in cli/run.rs\n\n### 6. `cli/run.rs` — Register event listener for sandbox.json\n- Before calling `run_with_lifecycle`, register a listener on the emitter for `SandboxInitialized`\n- Listener captures the pre-built `SandboxRecord` template (all provider-specific fields filled, `working_directory` empty)\n- On event: fill `working_directory` from event, call `record.save()`\n- Also update progress UI `set_working_directory` in the same listener\n\n### 7. `cli/run.rs` — Refactor `run_command` to use new engine methods\n- Move `ReadBeforeWriteSandbox` wrapping to before engine construction (currently at line 966, after init — delegate_sandbox! macro delegates initialize so wrapping before init works)\n- Move `HookRunner` creation earlier (before engine construction) — currently line 1222, move to ~line 800\n- Remove: `sandbox.initialize()` (line 886), remote git setup (lines 982-996), setup commands (lines 1031-1072), devcontainer lifecycle (lines 1074-1091)\n- Build `LifecycleConfig` from `setup_commands` and `devcontainer_config`\n- Build `RunConfig` without remote git fields (leave base_sha/run_branch/base_branch as None for remote — engine fills them)\n- Call `engine.run_with_lifecycle()` instead of `engine.run()`\n- Replace cleanup section (lines 1587-1604) with `engine.cleanup_sandbox()`\n- Replace two scopeguards with one that calls `engine.cleanup_sandbox()` on panic\n- Remove `status_guard` for SandboxInitFailed — engine handles init errors\n\n### 8. `cli/run.rs` — Refactor `run_from_branch` to use new engine methods\n- Use `run_with_lifecycle` with empty `LifecycleConfig` (no setup commands, no devcontainer)\n- Add `cleanup_sandbox()` call (currently no scopeguard — this is an improvement)\n- This gives the resume path hooks for free (currently has zero hooks)\n\n### 9. `docs/agents/hooks.mdx` — Update docs\n- Remove any \"reserved\" annotations for `sandbox_ready` / `sandbox_cleanup`\n- Note that `sandbox_ready` is blocking by default\n\n## Files to modify\n- `lib/crates/fabro-workflows/src/hook/types.rs` — SandboxReady blocking default\n- `lib/crates/fabro-workflows/src/engine.rs` — LifecycleConfig, run_with_lifecycle, cleanup_sandbox, setup_remote_git\n- `lib/crates/fabro-workflows/src/event.rs` — SandboxInitialized event variant\n- `lib/crates/fabro-workflows/src/cli/run.rs` — major simplification of run_command and run_from_branch\n- `docs/agents/hooks.mdx` — remove \"reserved\" annotations\n\n## Files unchanged\n- `lib/crates/fabro-workflows/src/handler/mod.rs` — EngineServices unchanged\n- `lib/crates/fabro-workflows/src/hook/runner.rs` — handles any HookEvent generically\n- `lib/crates/fabro-agent/src/sandbox.rs` — Sandbox trait unchanged\n- `lib/crates/fabro-agent/src/read_before_write_sandbox.rs` — delegate_sandbox! already delegates initialize/cleanup\n- All sandbox implementations — unchanged\n\n## Verification\n1. `cargo build --workspace` — compile check\n2. `cargo test --workspace` — all existing tests pass (existing `run()` path unchanged)\n3. `cargo clippy --workspace -- -D warnings` — no new warnings\n4. Manual test: `fabro run` with a workflow that has `sandbox_ready` and `sandbox_cleanup` hooks configured — verify hooks fire\n5. Manual test: `fabro run --sandbox daytona` — verify remote git setup still works through the engine\n6. Manual test: `fabro run` with `--preserve-sandbox` — verify cleanup is skipped but SandboxCleanup hook still fires\n", + "start_time": "2026-03-16T03:47:41.904480Z", + "node_count": 13, + "edge_count": 16, + "run_branch": "fabro/run/01KKTC38AXE3WSR695SJXKK70Y", + "base_sha": "470fcfe1200b2102c0cdf91c73b0ed8d925f258a", + "base_branch": "main", + "workflow_slug": "implement", + "host_repo_path": "/Users/bhelmkamp/p/fabro-sh/fabro" +} \ No newline at end of file diff --git a/sandbox.json b/sandbox.json new file mode 100644 index 000000000..d50ca1f66 --- /dev/null +++ b/sandbox.json @@ -0,0 +1,5 @@ +{ + "provider": "daytona", + "working_directory": "/home/daytona/workspace", + "identifier": "fabro-01KKTC38AXE3WSR695SJXKK70Y" +} \ No newline at end of file