diff --git a/checkpoint.json b/checkpoint.json new file mode 100644 index 000000000..2f40e3a8a --- /dev/null +++ b/checkpoint.json @@ -0,0 +1,52 @@ +{ + "timestamp": "2026-03-16T03:47:41.952333Z", + "current_node": "toolchain", + "completed_nodes": [ + "start", + "toolchain" + ], + "node_retries": { + "start": 1, + "toolchain": 1 + }, + "context_values": { + "internal.node_visit_count": 1, + "thread.start.current_node": "toolchain", + "graph.rankdir": "LR", + "graph.model_stylesheet": "\n * { backend: api; model: claude-opus-4-6;}\n ", + "outcome": "success", + "command.output": "cargo 1.94.0 (85eff7c80 2026-01-15)\n", + "current.preamble": "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\n", + "current_node": "toolchain", + "internal.retry_count.start": 1, + "failure_class": "", + "failure_signature": "", + "internal.run_id": "01KKTC38AXE3WSR695SJXKK70Y", + "internal.fidelity": "compact", + "internal.retry_count.toolchain": 1, + "internal.thread_id": "start", + "graph.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", + "command.stderr": "" + }, + "logs": [], + "node_outcomes": { + "toolchain": { + "status": "success", + "context_updates": { + "command.stderr": "", + "command.output": "cargo 1.94.0 (85eff7c80 2026-01-15)\n" + }, + "notes": "Script completed: 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", + "duration_ms": 33 + }, + "start": { + "status": "success", + "duration_ms": 0 + } + }, + "next_node_id": "preflight_compile", + "node_visits": { + "start": 1, + "toolchain": 1 + } +} \ No newline at end of file diff --git a/nodes/start/status.json b/nodes/start/status.json new file mode 100644 index 000000000..9a83d1196 --- /dev/null +++ b/nodes/start/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": null, + "failure_reason": null, + "timestamp": "2026-03-16T03:47:41.909338+00:00" +} \ No newline at end of file diff --git a/nodes/toolchain/script_invocation.json b/nodes/toolchain/script_invocation.json new file mode 100644 index 000000000..d68c414c4 --- /dev/null +++ b/nodes/toolchain/script_invocation.json @@ -0,0 +1,5 @@ +{ + "command": "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", + "language": "shell", + "timeout_ms": null +} \ No newline at end of file diff --git a/nodes/toolchain/script_timing.json b/nodes/toolchain/script_timing.json new file mode 100644 index 000000000..2c78bea0a --- /dev/null +++ b/nodes/toolchain/script_timing.json @@ -0,0 +1,5 @@ +{ + "duration_ms": 32, + "exit_code": 0, + "timed_out": false +} \ No newline at end of file diff --git a/nodes/toolchain/status.json b/nodes/toolchain/status.json new file mode 100644 index 000000000..73d2aae76 --- /dev/null +++ b/nodes/toolchain/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": "Script completed: 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", + "failure_reason": null, + "timestamp": "2026-03-16T03:47:41.952166+00:00" +} \ No newline at end of file