From 7ebcb7804b027afe69c765a137f3978c6176b16c Mon Sep 17 00:00:00 2001 From: Fabro Date: Sat, 21 Mar 2026 12:14:27 -0400 Subject: [PATCH] checkpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚒️ Generated with [Fabro](https://fabro.sh) --- checkpoint.json | 86 +++++---- nodes/simplify_gpt/diff.patch | 266 ++++++++++++++++++++++++++++ nodes/verify/script_invocation.json | 5 + nodes/verify/script_timing.json | 5 + nodes/verify/status.json | 6 + 5 files changed, 332 insertions(+), 36 deletions(-) create mode 100644 nodes/simplify_gpt/diff.patch create mode 100644 nodes/verify/script_invocation.json create mode 100644 nodes/verify/script_timing.json create mode 100644 nodes/verify/status.json diff --git a/checkpoint.json b/checkpoint.json index bfd6b0b67..90fdc6897 100644 --- a/checkpoint.json +++ b/checkpoint.json @@ -1,6 +1,6 @@ { - "timestamp": "2026-03-21T16:14:07.130233Z", - "current_node": "simplify_gpt", + "timestamp": "2026-03-21T16:14:27.785549Z", + "current_node": "verify", "completed_nodes": [ "start", "toolchain", @@ -8,19 +8,22 @@ "preflight_lint", "implement", "simplify_opus", - "simplify_gpt" + "simplify_gpt", + "verify" ], "node_retries": { "simplify_opus": 1, "preflight_compile": 1, - "simplify_gpt": 1, "implement": 1, "toolchain": 1, - "preflight_lint": 1, - "start": 1 + "start": 1, + "simplify_gpt": 1, + "verify": 1, + "preflight_lint": 1 }, "context_values": { - "current.preamble": "Goal: # Fix: Workflow TOML config lost in detach mode\n\n## Context\n\nWhen running `fabro run -d implement-plan`, the `[pull_request]` config from `fabro.toml` / `cli.toml` / `workflow.toml` is silently dropped, so no PR is created despite `enabled = true` at all config levels.\n\n**Root cause chain**:\n1. `create.rs` tries to copy the workflow TOML as `run.toml`, but checks the **raw CLI arg** (`\"implement-plan\"` — no extension) instead of the resolved path. So `run.toml` is never saved.\n2. `RunEngine` always uses cached `graph.fabro` (a DOT file), so `prepare_workflow` returns `run_cfg = None`, losing all TOML-level config.\n3. `pull_request` and `asset_globs` in `RunConfig` only check `run_cfg` without falling back to `run_defaults`.\n\n## Fix 1: Serialize merged `run_cfg` to `run.toml` in create.rs\n\n**File**: `lib/crates/fabro-cli/src/commands/create.rs`\n\nReplace the raw-file-copy block (lines 53-58) with serialization of the already-merged `WorkflowRunConfig`:\n\n- Change `let prep = prepare_workflow(...)` to `let mut prep = ...`\n- Replace the extension check with:\n ```rust\n if let Some(mut cfg) = prep.run_cfg.take() {\n cfg.graph = \"graph.fabro\".to_string();\n let toml_str = toml::to_string_pretty(&cfg)\n .context(\"Failed to serialize run config\")?;\n tokio::fs::write(run_dir.join(\"run.toml\"), toml_str).await?;\n }\n ```\n- Add `use anyhow::Context;` if needed\n\n**Why serialize instead of copy**: The raw TOML's `graph` field (e.g. `\"workflow.fabro\"`) would point to a nonexistent file in the run dir. Serializing lets us rewrite `graph` to `\"graph.fabro\"` (the cached name). The serialized config also has all defaults merged, env vars resolved, and dockerfiles inlined — making the run dir self-contained.\n\n**Why `take()` not `clone()`**: `WorkflowRunConfig` doesn't derive `Clone`, and `prep.run_cfg` is unused after this point in `create.rs`.\n\n## Fix 2: Use `run.toml` in RunEngine path\n\n**File**: `lib/crates/fabro-cli/src/main.rs` (lines 727-733)\n\nReplace the workflow path resolution to use `run.toml`:\n\n```rust\nlet cached_toml = run_dir.join(\"run.toml\");\nlet workflow_path = if cached_toml.exists() {\n cached_toml\n} else {\n run_dir.join(\"graph.fabro\")\n};\n```\n\nWhen `run.toml` exists, `prepare_workflow` → `resolve_workflow` sees `.toml`, calls `load_run_config`, and `resolve_graph_path` resolves `\"graph.fabro\"` relative to the run dir — pointing to the cached graph that already exists there.\n\n## Fix 3: Add `run_defaults` fallbacks (defense-in-depth)\n\n**File**: `lib/crates/fabro-cli/src/commands/run.rs`\n\nEven with Fixes 1+2, bare `.fabro` files passed directly would still hit `run_cfg = None`. Add fallbacks matching the pattern already used elsewhere in the file:\n\n**3a. `pull_request`** (line 1424-1428):\n```rust\npull_request: run_cfg\n .as_ref()\n .and_then(|c| c.pull_request.as_ref())\n .or(run_defaults.pull_request.as_ref())\n .filter(|p| p.enabled)\n .cloned(),\n```\n\n**3b. `asset_globs`** (line 1429-1433):\n```rust\nasset_globs: run_cfg\n .as_ref()\n .and_then(|c| c.assets.as_ref())\n .or(run_defaults.assets.as_ref())\n .map(|a| a.include.clone())\n .unwrap_or_default(),\n```\n\n**3c. `devcontainer`** (line 969-973):\n```rust\nlet devcontainer_config = if run_cfg\n .as_ref()\n .and_then(|c| c.sandbox.as_ref())\n .or(run_defaults.sandbox.as_ref())\n .and_then(|s| s.devcontainer)\n .unwrap_or(false)\n```\n\n**3d. `sandbox.env`** (line 1300-1306):\n```rust\nif let Some(toml_env) = run_cfg\n .as_ref()\n .and_then(|c| c.sandbox.as_ref())\n .or(run_defaults.sandbox.as_ref())\n .and_then(|s| s.env.clone())\n```\n\n## Verification\n\n1. `cargo build --workspace`\n2. `cargo test --workspace`\n3. `cargo clippy --workspace -- -D warnings`\n4. Manual: `fabro run -d implement-plan` with `[pull_request] enabled = true` in `fabro.toml` → verify `run.toml` in run dir has `graph = \"graph.fabro\"` and `[pull_request]` → verify PR created\n\n\n## Completed stages\n- **toolchain**: success\n - 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`\n - Stdout:\n ```\n cargo 1.94.0 (85eff7c80 2026-01-15)\n ```\n - Stderr: (empty)\n- **preflight_compile**: success\n - Script: `cargo check -q --workspace 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **preflight_lint**: success\n - Script: `cargo clippy -q --workspace -- -D warnings 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **implement**: success\n - Model: claude-opus-4-6, 20.7k tokens in / 5.3k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/create.rs, /home/daytona/workspace/lib/crates/fabro-cli/src/commands/run.rs, /home/daytona/workspace/lib/crates/fabro-cli/src/main.rs\n- **simplify_opus**: success\n - Model: claude-opus-4-6, 50.7k tokens in / 13.4k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/run.rs\n", + "internal.retry_count.verify": 1, + "current.preamble": "Goal: # Fix: Workflow TOML config lost in detach mode\n\n## Context\n\nWhen running `fabro run -d implement-plan`, the `[pull_request]` config from `fabro.toml` / `cli.toml` / `workflow.toml` is silently dropped, so no PR is created despite `enabled = true` at all config levels.\n\n**Root cause chain**:\n1. `create.rs` tries to copy the workflow TOML as `run.toml`, but checks the **raw CLI arg** (`\"implement-plan\"` — no extension) instead of the resolved path. So `run.toml` is never saved.\n2. `RunEngine` always uses cached `graph.fabro` (a DOT file), so `prepare_workflow` returns `run_cfg = None`, losing all TOML-level config.\n3. `pull_request` and `asset_globs` in `RunConfig` only check `run_cfg` without falling back to `run_defaults`.\n\n## Fix 1: Serialize merged `run_cfg` to `run.toml` in create.rs\n\n**File**: `lib/crates/fabro-cli/src/commands/create.rs`\n\nReplace the raw-file-copy block (lines 53-58) with serialization of the already-merged `WorkflowRunConfig`:\n\n- Change `let prep = prepare_workflow(...)` to `let mut prep = ...`\n- Replace the extension check with:\n ```rust\n if let Some(mut cfg) = prep.run_cfg.take() {\n cfg.graph = \"graph.fabro\".to_string();\n let toml_str = toml::to_string_pretty(&cfg)\n .context(\"Failed to serialize run config\")?;\n tokio::fs::write(run_dir.join(\"run.toml\"), toml_str).await?;\n }\n ```\n- Add `use anyhow::Context;` if needed\n\n**Why serialize instead of copy**: The raw TOML's `graph` field (e.g. `\"workflow.fabro\"`) would point to a nonexistent file in the run dir. Serializing lets us rewrite `graph` to `\"graph.fabro\"` (the cached name). The serialized config also has all defaults merged, env vars resolved, and dockerfiles inlined — making the run dir self-contained.\n\n**Why `take()` not `clone()`**: `WorkflowRunConfig` doesn't derive `Clone`, and `prep.run_cfg` is unused after this point in `create.rs`.\n\n## Fix 2: Use `run.toml` in RunEngine path\n\n**File**: `lib/crates/fabro-cli/src/main.rs` (lines 727-733)\n\nReplace the workflow path resolution to use `run.toml`:\n\n```rust\nlet cached_toml = run_dir.join(\"run.toml\");\nlet workflow_path = if cached_toml.exists() {\n cached_toml\n} else {\n run_dir.join(\"graph.fabro\")\n};\n```\n\nWhen `run.toml` exists, `prepare_workflow` → `resolve_workflow` sees `.toml`, calls `load_run_config`, and `resolve_graph_path` resolves `\"graph.fabro\"` relative to the run dir — pointing to the cached graph that already exists there.\n\n## Fix 3: Add `run_defaults` fallbacks (defense-in-depth)\n\n**File**: `lib/crates/fabro-cli/src/commands/run.rs`\n\nEven with Fixes 1+2, bare `.fabro` files passed directly would still hit `run_cfg = None`. Add fallbacks matching the pattern already used elsewhere in the file:\n\n**3a. `pull_request`** (line 1424-1428):\n```rust\npull_request: run_cfg\n .as_ref()\n .and_then(|c| c.pull_request.as_ref())\n .or(run_defaults.pull_request.as_ref())\n .filter(|p| p.enabled)\n .cloned(),\n```\n\n**3b. `asset_globs`** (line 1429-1433):\n```rust\nasset_globs: run_cfg\n .as_ref()\n .and_then(|c| c.assets.as_ref())\n .or(run_defaults.assets.as_ref())\n .map(|a| a.include.clone())\n .unwrap_or_default(),\n```\n\n**3c. `devcontainer`** (line 969-973):\n```rust\nlet devcontainer_config = if run_cfg\n .as_ref()\n .and_then(|c| c.sandbox.as_ref())\n .or(run_defaults.sandbox.as_ref())\n .and_then(|s| s.devcontainer)\n .unwrap_or(false)\n```\n\n**3d. `sandbox.env`** (line 1300-1306):\n```rust\nif let Some(toml_env) = run_cfg\n .as_ref()\n .and_then(|c| c.sandbox.as_ref())\n .or(run_defaults.sandbox.as_ref())\n .and_then(|s| s.env.clone())\n```\n\n## Verification\n\n1. `cargo build --workspace`\n2. `cargo test --workspace`\n3. `cargo clippy --workspace -- -D warnings`\n4. Manual: `fabro run -d implement-plan` with `[pull_request] enabled = true` in `fabro.toml` → verify `run.toml` in run dir has `graph = \"graph.fabro\"` and `[pull_request]` → verify PR created\n\n\n## Completed stages\n- **toolchain**: success\n - 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`\n - Stdout:\n ```\n cargo 1.94.0 (85eff7c80 2026-01-15)\n ```\n - Stderr: (empty)\n- **preflight_compile**: success\n - Script: `cargo check -q --workspace 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **preflight_lint**: success\n - Script: `cargo clippy -q --workspace -- -D warnings 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **implement**: success\n - Model: claude-opus-4-6, 20.7k tokens in / 5.3k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/create.rs, /home/daytona/workspace/lib/crates/fabro-cli/src/commands/run.rs, /home/daytona/workspace/lib/crates/fabro-cli/src/main.rs\n- **simplify_opus**: success\n - Model: claude-opus-4-6, 50.7k tokens in / 13.4k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/run.rs\n- **simplify_gpt**: success\n - Model: gpt-5.4, 2.6m tokens in / 20.8k out\n", "graph.goal": "# Fix: Workflow TOML config lost in detach mode\n\n## Context\n\nWhen running `fabro run -d implement-plan`, the `[pull_request]` config from `fabro.toml` / `cli.toml` / `workflow.toml` is silently dropped, so no PR is created despite `enabled = true` at all config levels.\n\n**Root cause chain**:\n1. `create.rs` tries to copy the workflow TOML as `run.toml`, but checks the **raw CLI arg** (`\"implement-plan\"` — no extension) instead of the resolved path. So `run.toml` is never saved.\n2. `RunEngine` always uses cached `graph.fabro` (a DOT file), so `prepare_workflow` returns `run_cfg = None`, losing all TOML-level config.\n3. `pull_request` and `asset_globs` in `RunConfig` only check `run_cfg` without falling back to `run_defaults`.\n\n## Fix 1: Serialize merged `run_cfg` to `run.toml` in create.rs\n\n**File**: `lib/crates/fabro-cli/src/commands/create.rs`\n\nReplace the raw-file-copy block (lines 53-58) with serialization of the already-merged `WorkflowRunConfig`:\n\n- Change `let prep = prepare_workflow(...)` to `let mut prep = ...`\n- Replace the extension check with:\n ```rust\n if let Some(mut cfg) = prep.run_cfg.take() {\n cfg.graph = \"graph.fabro\".to_string();\n let toml_str = toml::to_string_pretty(&cfg)\n .context(\"Failed to serialize run config\")?;\n tokio::fs::write(run_dir.join(\"run.toml\"), toml_str).await?;\n }\n ```\n- Add `use anyhow::Context;` if needed\n\n**Why serialize instead of copy**: The raw TOML's `graph` field (e.g. `\"workflow.fabro\"`) would point to a nonexistent file in the run dir. Serializing lets us rewrite `graph` to `\"graph.fabro\"` (the cached name). The serialized config also has all defaults merged, env vars resolved, and dockerfiles inlined — making the run dir self-contained.\n\n**Why `take()` not `clone()`**: `WorkflowRunConfig` doesn't derive `Clone`, and `prep.run_cfg` is unused after this point in `create.rs`.\n\n## Fix 2: Use `run.toml` in RunEngine path\n\n**File**: `lib/crates/fabro-cli/src/main.rs` (lines 727-733)\n\nReplace the workflow path resolution to use `run.toml`:\n\n```rust\nlet cached_toml = run_dir.join(\"run.toml\");\nlet workflow_path = if cached_toml.exists() {\n cached_toml\n} else {\n run_dir.join(\"graph.fabro\")\n};\n```\n\nWhen `run.toml` exists, `prepare_workflow` → `resolve_workflow` sees `.toml`, calls `load_run_config`, and `resolve_graph_path` resolves `\"graph.fabro\"` relative to the run dir — pointing to the cached graph that already exists there.\n\n## Fix 3: Add `run_defaults` fallbacks (defense-in-depth)\n\n**File**: `lib/crates/fabro-cli/src/commands/run.rs`\n\nEven with Fixes 1+2, bare `.fabro` files passed directly would still hit `run_cfg = None`. Add fallbacks matching the pattern already used elsewhere in the file:\n\n**3a. `pull_request`** (line 1424-1428):\n```rust\npull_request: run_cfg\n .as_ref()\n .and_then(|c| c.pull_request.as_ref())\n .or(run_defaults.pull_request.as_ref())\n .filter(|p| p.enabled)\n .cloned(),\n```\n\n**3b. `asset_globs`** (line 1429-1433):\n```rust\nasset_globs: run_cfg\n .as_ref()\n .and_then(|c| c.assets.as_ref())\n .or(run_defaults.assets.as_ref())\n .map(|a| a.include.clone())\n .unwrap_or_default(),\n```\n\n**3c. `devcontainer`** (line 969-973):\n```rust\nlet devcontainer_config = if run_cfg\n .as_ref()\n .and_then(|c| c.sandbox.as_ref())\n .or(run_defaults.sandbox.as_ref())\n .and_then(|s| s.devcontainer)\n .unwrap_or(false)\n```\n\n**3d. `sandbox.env`** (line 1300-1306):\n```rust\nif let Some(toml_env) = run_cfg\n .as_ref()\n .and_then(|c| c.sandbox.as_ref())\n .or(run_defaults.sandbox.as_ref())\n .and_then(|s| s.env.clone())\n```\n\n## Verification\n\n1. `cargo build --workspace`\n2. `cargo test --workspace`\n3. `cargo clippy --workspace -- -D warnings`\n4. Manual: `fabro run -d implement-plan` with `[pull_request] enabled = true` in `fabro.toml` → verify `run.toml` in run dir has `graph = \"graph.fabro\"` and `[pull_request]` → verify PR created\n", "response.simplify_opus": "## Summary\n\n**Fixed one real issue, accepted pre-existing snapshots, skipped false positives:**\n\n### Fixed\n1. **`run.rs` raw TOML copy → merged serialization** (the main fix): `run.rs` line 701-704 was still copying the raw TOML file from disk (the old buggy behavior), while `create.rs` had been fixed to serialize the merged config. This meant `fabro run` (non-detached) would write a `run.toml` with the wrong `graph` path and without defaults merged. Now both `create.rs` and `run.rs` use the same approach: serialize the merged `WorkflowRunConfig` with `graph` rewritten to `\"graph.fabro\"`.\n\n2. **Accepted pre-existing snapshot updates**: 7 `fabro-model` catalog test snapshots needed updating for the new `effort` field in `ModelFeatures`. Removed the stale `.pending-snap` file.\n\n### Skipped (not worth addressing)\n- **Magic string constants for `\"graph.fabro\"`/`\"run.toml\"`**: Valid observation (18+ occurrences across 7+ files), but a cross-crate refactor beyond the scope of this fix.\n- **Extract `resolve_sandbox_config` helper**: The two inline `.or()` additions match the existing code pattern. Extracting a helper for two call sites is over-abstraction.\n- **No efficiency issues**: All three changes are on cold paths with negligible cost.", "internal.retry_count.toolchain": 1, @@ -28,9 +31,9 @@ "outcome": "success", "failure_class": "", "last_response": "Reviewed and cleaned up the detach-mode fix.\n\nWhat I changed\n- `lib/crates/fabro-cli/src/commands/run.rs`\n - Added shared helpers/constants for cached run files:\n - `RUN_GRAPH_FILE`\n - `RUN_CON", - "internal.thread_id": "simplify_opus", + "internal.thread_id": "simplify_gpt", "command.stderr": "", - "command.output": "", + "command.output": "────────────\n Nextest run ID 56ce7aa0-dd01-423a-8d6c-5f6eca96bfb4 with nextest profile: default\n Starting 3287 tests across 41 binaries (177 tests skipped)\n────────────\n Summary [ 17.059s] 3287 tests run: 3287 passed, 177 skipped\n", "graph.model_stylesheet": "\n * { model: claude-opus-4-6; }\n ", "internal.run_id": "01KM8HA16H7HVBT2P63W2FSWT5", "thread.preflight_lint.current_node": "implement", @@ -38,10 +41,11 @@ "thread.simplify_opus.current_node": "simplify_gpt", "internal.retry_count.simplify_gpt": 1, "last_stage": "simplify_gpt", + "thread.simplify_gpt.current_node": "verify", "graph.rankdir": "LR", "internal.fidelity": "compact", "internal.retry_count.preflight_lint": 1, - "current_node": "simplify_gpt", + "current_node": "verify", "internal.retry_count.preflight_compile": 1, "failure_signature": "", "thread.toolchain.current_node": "preflight_compile", @@ -55,6 +59,10 @@ }, "logs": [], "node_outcomes": { + "start": { + "status": "success", + "duration_ms": 0 + }, "simplify_gpt": { "status": "success", "context_updates": { @@ -73,28 +81,6 @@ }, "duration_ms": 641375 }, - "preflight_compile": { - "status": "success", - "context_updates": { - "command.stderr": "", - "command.output": "" - }, - "notes": "Script completed: cargo check -q --workspace 2>&1", - "duration_ms": 74289 - }, - "start": { - "status": "success", - "duration_ms": 0 - }, - "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": 97 - }, "implement": { "status": "success", "context_updates": { @@ -119,6 +105,24 @@ ], "duration_ms": 294549 }, + "preflight_compile": { + "status": "success", + "context_updates": { + "command.stderr": "", + "command.output": "" + }, + "notes": "Script completed: cargo check -q --workspace 2>&1", + "duration_ms": 74289 + }, + "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": 97 + }, "simplify_opus": { "status": "success", "context_updates": { @@ -141,6 +145,15 @@ ], "duration_ms": 521238 }, + "verify": { + "status": "success", + "context_updates": { + "command.stderr": "", + "command.output": "────────────\n Nextest run ID 56ce7aa0-dd01-423a-8d6c-5f6eca96bfb4 with nextest profile: default\n Starting 3287 tests across 41 binaries (177 tests skipped)\n────────────\n Summary [ 17.059s] 3287 tests run: 3287 passed, 177 skipped\n" + }, + "notes": "Script completed: cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", + "duration_ms": 18368 + }, "preflight_lint": { "status": "success", "context_updates": { @@ -151,14 +164,15 @@ "duration_ms": 13456 } }, - "next_node_id": "verify", + "next_node_id": "fmt", "node_visits": { - "simplify_gpt": 1, + "simplify_opus": 1, + "verify": 1, "start": 1, "preflight_lint": 1, "implement": 1, "toolchain": 1, - "preflight_compile": 1, - "simplify_opus": 1 + "simplify_gpt": 1, + "preflight_compile": 1 } } \ No newline at end of file diff --git a/nodes/simplify_gpt/diff.patch b/nodes/simplify_gpt/diff.patch new file mode 100644 index 000000000..67474c4c5 --- /dev/null +++ b/nodes/simplify_gpt/diff.patch @@ -0,0 +1,266 @@ +diff --git a/lib/crates/fabro-cli/src/commands/create.rs b/lib/crates/fabro-cli/src/commands/create.rs +index 41b2895c..fdd30a86 100644 +--- a/lib/crates/fabro-cli/src/commands/create.rs ++++ b/lib/crates/fabro-cli/src/commands/create.rs +@@ -1,11 +1,10 @@ + use std::path::PathBuf; + +-use anyhow::Context; + use chrono::Local; + use fabro_config::run::RunDefaults; + use fabro_workflows::run_spec::RunSpec; + +-use super::run::{prepare_workflow, RunArgs}; ++use super::run::{cached_graph_path, prepare_workflow, write_run_config_snapshot, RunArgs}; + use fabro_util::terminal::Styles; + + /// Create a workflow run: allocate run directory, persist spec, return (run_id, run_dir). +@@ -22,7 +21,7 @@ pub async fn create_run( + .as_ref() + .ok_or_else(|| anyhow::anyhow!("--workflow is required"))?; + +- let mut prep = prepare_workflow(args, run_defaults, styles, quiet)?; ++ let prep = prepare_workflow(args, run_defaults, styles, quiet)?; + + let goal = prep.graph.goal(); + +@@ -42,7 +41,7 @@ pub async fn create_run( + tokio::fs::create_dir_all(&run_dir).await?; + + // Write essential files +- tokio::fs::write(run_dir.join("graph.fabro"), &prep.source).await?; ++ tokio::fs::write(cached_graph_path(&run_dir), &prep.source).await?; + tokio::fs::write(run_dir.join("id.txt"), &run_id).await?; + std::fs::File::create(run_dir.join("progress.jsonl"))?; + fabro_workflows::run_status::write_run_status( +@@ -51,12 +50,8 @@ pub async fn create_run( + None, + ); + +- // Serialize the merged run config so the run dir is self-contained +- if let Some(mut cfg) = prep.run_cfg.take() { +- cfg.graph = "graph.fabro".to_string(); +- let toml_str = toml::to_string_pretty(&cfg).context("Failed to serialize run config")?; +- tokio::fs::write(run_dir.join("run.toml"), toml_str).await?; +- } ++ // Serialize the merged run config so the run dir is self-contained. ++ write_run_config_snapshot(&run_dir, prep.run_cfg.as_ref()).await?; + + // Build and save RunSpec + let working_directory = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); +diff --git a/lib/crates/fabro-cli/src/commands/run.rs b/lib/crates/fabro-cli/src/commands/run.rs +index 264fbc48..8ff296e4 100644 +--- a/lib/crates/fabro-cli/src/commands/run.rs ++++ b/lib/crates/fabro-cli/src/commands/run.rs +@@ -1,6 +1,6 @@ + use std::collections::HashMap; + use std::io::IsTerminal; +-use std::path::PathBuf; ++use std::path::{Path, PathBuf}; + use std::sync::{Arc, Mutex}; + use std::time::Instant; + +@@ -444,6 +444,70 @@ fn local_sandbox_with_callback(cwd: PathBuf, emitter: Arc) -> Arc< + Arc::new(env) + } + ++pub(crate) const RUN_GRAPH_FILE: &str = "graph.fabro"; ++pub(crate) const RUN_CONFIG_FILE: &str = "run.toml"; ++ ++pub(crate) fn cached_graph_path(run_dir: &Path) -> PathBuf { ++ run_dir.join(RUN_GRAPH_FILE) ++} ++ ++pub(crate) fn cached_run_config_path(run_dir: &Path) -> PathBuf { ++ run_dir.join(RUN_CONFIG_FILE) ++} ++ ++fn serialize_run_config_snapshot(run_cfg: &WorkflowRunConfig) -> anyhow::Result { ++ let mut value = toml::Value::try_from(run_cfg).context("Failed to serialize run config")?; ++ let table = value ++ .as_table_mut() ++ .ok_or_else(|| anyhow::anyhow!("Failed to serialize run config"))?; ++ table.insert( ++ "graph".to_string(), ++ toml::Value::String(RUN_GRAPH_FILE.to_string()), ++ ); ++ toml::to_string_pretty(&value).context("Failed to serialize run config") ++} ++ ++pub(crate) async fn write_run_config_snapshot( ++ run_dir: &Path, ++ run_cfg: Option<&WorkflowRunConfig>, ++) -> anyhow::Result<()> { ++ if let Some(cfg) = run_cfg { ++ let toml_str = serialize_run_config_snapshot(cfg)?; ++ tokio::fs::write(cached_run_config_path(run_dir), toml_str).await?; ++ } ++ Ok(()) ++} ++ ++fn is_missing_cached_run_config(path: &Path, error: &anyhow::Error) -> bool { ++ path.file_name() == Some(std::ffi::OsStr::new(RUN_CONFIG_FILE)) ++ && error.chain().any(|cause| { ++ cause ++ .downcast_ref::() ++ .is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound) ++ }) ++} ++ ++fn resolve_workflow_source( ++ workflow_path: &Path, ++) -> anyhow::Result<(PathBuf, Option)> { ++ let path = project_config::resolve_workflow_arg(workflow_path)?; ++ if path.extension().is_some_and(|ext| ext == "toml") { ++ match run_config::load_run_config(&path) { ++ Ok(cfg) => { ++ let dot = run_config::resolve_graph_path(&path, &cfg.graph); ++ Ok((dot, Some(cfg))) ++ } ++ // Backward compatibility for detached runs created before run.toml existed. ++ Err(err) if is_missing_cached_run_config(&path, &err) => { ++ Ok((path.with_file_name(RUN_GRAPH_FILE), None)) ++ } ++ Err(err) => Err(err), ++ } ++ } else { ++ Ok((path, None)) ++ } ++} ++ + /// Result of workflow preparation (shared between `create` and `run` commands). + pub(crate) struct PreparedWorkflow { + pub source: String, +@@ -480,7 +544,7 @@ pub(crate) fn prepare_workflow( + + // Resolve workflow arg, load run config if TOML, apply defaults + let (dot_path, run_cfg) = { +- let (dot, cfg) = project_config::resolve_workflow(workflow_path)?; ++ let (dot, cfg) = resolve_workflow_source(workflow_path)?; + match cfg { + Some(mut cfg) => { + cfg.apply_defaults(&run_defaults); +@@ -610,7 +674,7 @@ pub async fn run_command( + let PreparedWorkflow { + source, + graph, +- mut run_cfg, ++ run_cfg, + sandbox_provider, + model, + provider, +@@ -680,7 +744,7 @@ pub async fn run_command( + tokio::fs::create_dir_all(&run_dir).await?; + fabro_util::run_log::activate(&run_dir.join("cli.log")) + .context("Failed to activate per-run log")?; +- tokio::fs::write(run_dir.join("graph.fabro"), &source).await?; ++ tokio::fs::write(cached_graph_path(&run_dir), &source).await?; + tokio::fs::write(run_dir.join("run.pid"), std::process::id().to_string()).await?; + fabro_workflows::run_status::write_run_status( + &run_dir, +@@ -698,12 +762,8 @@ pub async fn run_command( + ); + }); + +- // Serialize the merged run config so the run dir is self-contained +- if let Some(ref mut cfg) = run_cfg { +- cfg.graph = "graph.fabro".to_string(); +- let toml_str = toml::to_string_pretty(&*cfg).context("Failed to serialize run config")?; +- tokio::fs::write(run_dir.join("run.toml"), toml_str).await?; +- } ++ // Serialize the merged run config so the run dir is self-contained. ++ write_run_config_snapshot(&run_dir, run_cfg.as_ref()).await?; + + // Create progress UI (used for both normal and verbose modes) + let is_tty = std::io::stderr().is_terminal(); +@@ -1846,7 +1906,10 @@ async fn run_from_branch( + // Read graph DOT from metadata branch + let source = fabro_workflows::git::MetadataStore::read_graph_dot(&original_cwd, &run_id)? + .ok_or_else(|| { +- anyhow::anyhow!("no graph.fabro found on metadata branch for run {run_id}") ++ anyhow::anyhow!( ++ "no {} found on metadata branch for run {run_id}", ++ RUN_GRAPH_FILE ++ ) + })?; + + // If --pipeline was also provided, use it instead (allows overriding) +@@ -1890,7 +1953,7 @@ async fn run_from_branch( + tokio::fs::create_dir_all(&run_dir).await?; + fabro_util::run_log::activate(&run_dir.join("cli.log")) + .context("Failed to activate per-run log")?; +- tokio::fs::write(run_dir.join("graph.fabro"), &source).await?; ++ tokio::fs::write(cached_graph_path(&run_dir), &source).await?; + + let base_sha = fabro_workflows::git::MetadataStore::read_manifest(&original_cwd, &run_id)? + .and_then(|m| m.base_sha); +@@ -2789,6 +2852,47 @@ fn build_event_envelope( + mod tests { + use super::*; + ++ #[test] ++ fn serialize_run_config_snapshot_rewrites_graph_path() { ++ let cfg = run_config::WorkflowRunConfig { ++ version: 1, ++ goal: Some("test".to_string()), ++ graph: "workflow.fabro".to_string(), ++ work_dir: None, ++ llm: None, ++ setup: None, ++ sandbox: None, ++ vars: None, ++ hooks: Vec::new(), ++ checkpoint: Default::default(), ++ pull_request: Some(run_config::PullRequestConfig { ++ enabled: true, ++ ..Default::default() ++ }), ++ assets: None, ++ mcp_servers: Default::default(), ++ github: None, ++ }; ++ ++ let serialized = serialize_run_config_snapshot(&cfg).unwrap(); ++ let reparsed = run_config::parse_run_config(&serialized).unwrap(); ++ ++ assert_eq!(reparsed.graph, RUN_GRAPH_FILE); ++ assert_eq!(reparsed.pull_request, cfg.pull_request); ++ } ++ ++ #[test] ++ fn resolve_workflow_source_falls_back_to_graph_for_missing_cached_run_config() { ++ let dir = tempfile::tempdir().unwrap(); ++ std::fs::write(dir.path().join(RUN_GRAPH_FILE), "digraph test {}").unwrap(); ++ ++ let (dot_path, run_cfg) = ++ resolve_workflow_source(&dir.path().join(RUN_CONFIG_FILE)).unwrap(); ++ ++ assert_eq!(dot_path, dir.path().join(RUN_GRAPH_FILE)); ++ assert!(run_cfg.is_none()); ++ } ++ + #[test] + fn apply_goal_override_cli_wins_over_toml() { + use fabro_graphviz::graph::{AttrValue, Graph}; +diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs +index ad9bb158..da8f5b86 100644 +--- a/lib/crates/fabro-cli/src/main.rs ++++ b/lib/crates/fabro-cli/src/main.rs +@@ -724,14 +724,9 @@ async fn main_inner() -> (String, Result<()>) { + ) + })?; + +- // Prefer cached run.toml (carries full TOML config including +- // pull_request, assets, etc.), fall back to bare graph snapshot. +- let cached_toml = run_dir.join("run.toml"); +- let workflow_path = if cached_toml.exists() { +- cached_toml +- } else { +- run_dir.join("graph.fabro") +- }; ++ // Prefer the cached run.toml. prepare_workflow() falls back to the ++ // sibling graph snapshot for older detached runs that predate run.toml. ++ let workflow_path = commands::run::cached_run_config_path(&run_dir); + + let run_args = commands::run::RunArgs { + workflow: Some(workflow_path), diff --git a/nodes/verify/script_invocation.json b/nodes/verify/script_invocation.json new file mode 100644 index 000000000..c2b2fcf73 --- /dev/null +++ b/nodes/verify/script_invocation.json @@ -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 +} \ No newline at end of file diff --git a/nodes/verify/script_timing.json b/nodes/verify/script_timing.json new file mode 100644 index 000000000..cba6a24ba --- /dev/null +++ b/nodes/verify/script_timing.json @@ -0,0 +1,5 @@ +{ + "duration_ms": 18366, + "exit_code": 0, + "timed_out": false +} \ No newline at end of file diff --git a/nodes/verify/status.json b/nodes/verify/status.json new file mode 100644 index 000000000..6e2ba66f6 --- /dev/null +++ b/nodes/verify/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": "Script completed: cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", + "failure_reason": null, + "timestamp": "2026-03-21T16:14:27.784944+00:00" +} \ No newline at end of file