From c13092b75d38e142ac1092e512ac24e48fe1f736 Mon Sep 17 00:00:00 2001 From: Fabro Date: Mon, 16 Mar 2026 00:00:46 -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 | 68 +- nodes/implement/diff.patch | 1875 ++++++++++++++++++++++++ nodes/simplify_opus/prompt.md | 115 ++ nodes/simplify_opus/provider_used.json | 5 + nodes/simplify_opus/response.md | 21 + nodes/simplify_opus/status.json | 6 + 6 files changed, 2071 insertions(+), 19 deletions(-) create mode 100644 nodes/implement/diff.patch create mode 100644 nodes/simplify_opus/prompt.md create mode 100644 nodes/simplify_opus/provider_used.json create mode 100644 nodes/simplify_opus/response.md create mode 100644 nodes/simplify_opus/status.json diff --git a/checkpoint.json b/checkpoint.json index 7b2fab6f3..ee2215c13 100644 --- a/checkpoint.json +++ b/checkpoint.json @@ -1,47 +1,52 @@ { - "timestamp": "2026-03-16T03:55:38.474653Z", - "current_node": "implement", + "timestamp": "2026-03-16T04:00:46.974670Z", + "current_node": "simplify_opus", "completed_nodes": [ "start", "toolchain", "preflight_compile", "preflight_lint", - "implement" + "implement", + "simplify_opus" ], "node_retries": { "implement": 1, + "simplify_opus": 1, "start": 1, "toolchain": 1, "preflight_compile": 1, "preflight_lint": 1 }, "context_values": { - "internal.retry_count.preflight_lint": 1, - "internal.retry_count.implement": 1, - "last_stage": "implement", - "internal.fidelity": "compact", - "last_response": "Everything compiles cleanly. Let me provide a summary of all changes made:\n\n## Summary of Changes\n\n### Refactor: Collapsed `pull_request_*` fields on `RunConfig` into `Option`\n\n**1.", - "thread.preflight_lint.current_node": "implement", - "response.implement": "Everything compiles cleanly. Let me provide a summary of all changes made:\n\n## Summary of Changes\n\n### Refactor: Collapsed `pull_request_*` fields on `RunConfig` into `Option`\n\n**1. `lib/crates/fabro-workflows/src/engine.rs`**\n- Added `use crate::cli::run_config::PullRequestConfig;` import\n- Replaced two fields (`pull_request_enabled: bool`, `pull_request_draft: bool`) with single `pull_request: Option` field\n- Updated all ~40 test constructors: `pull_request_enabled: false, pull_request_draft: false` → `pull_request: None`\n\n**2. `lib/crates/fabro-workflows/src/cli/run.rs`**\n- Simplified `RunConfig` construction: replaced `pr_cfg.is_some_and(|p| p.enabled)` / `pr_cfg.is_none_or(|p| p.draft)` with `.filter(|p| p.enabled).cloned()`\n- Restructured PR creation block: `if let Some(ref pr_cfg) = config.pull_request { ... }` instead of `is_none()` check + later `unwrap()`\n- Updated `maybe_open_pull_request` call to pass `pr_cfg` reference\n- Updated event emission to use `pr_cfg.draft`\n- Updated resume config constructor: `pull_request: None`\n\n**3. `lib/crates/fabro-workflows/src/pull_request.rs`**\n- Changed `maybe_open_pull_request` signature: replaced `draft: bool` parameter with `pr_config: &crate::cli::run_config::PullRequestConfig`\n- Updated internal usage to read `pr_config.draft`\n- Updated test to construct and pass `PullRequestConfig`\n\n**4. `lib/crates/fabro-workflows/src/cli/pr.rs`**\n- Updated call to `maybe_open_pull_request`: constructs `PullRequestConfig { enabled: true, draft: true }` and passes reference\n\n**5. `lib/crates/fabro-api/src/server.rs`**\n- Replaced `pull_request_enabled: false, pull_request_draft: true` with `pull_request: None`\n\n**6. `lib/crates/fabro-workflows/src/handler/manager_loop.rs`**\n- Replaced `pull_request_enabled: false, pull_request_draft: true` with `pull_request: None`\n\n**7. `lib/crates/fabro-workflows/tests/integration.rs`**\n- Replaced all ~90 occurrences (8-space and 12-space indented) of `pull_request_enabled: false, pull_request_draft: false` with `pull_request: None`\n\n**8. `lib/crates/fabro-workflows/tests/daytona_integration.rs`**\n- Replaced all occurrences with `pull_request: None`\n\n### Verification\n- ✅ `cargo build --workspace` — clean\n- ✅ `cargo clippy --workspace -- -D warnings` — clean\n- ✅ `cargo test -p fabro-workflows -- pull_request run_config project_config` — 180 tests passed\n- ✅ `cargo fmt --check --all` — clean", + "current_node": "simplify_opus", + "internal.retry_count.simplify_opus": 1, "failure_signature": "", "internal.retry_count.preflight_compile": 1, - "failure_class": "", - "thread.preflight_compile.current_node": "preflight_lint", - "graph.rankdir": "LR", + "response.implement": "Everything compiles cleanly. Let me provide a summary of all changes made:\n\n## Summary of Changes\n\n### Refactor: Collapsed `pull_request_*` fields on `RunConfig` into `Option`\n\n**1. `lib/crates/fabro-workflows/src/engine.rs`**\n- Added `use crate::cli::run_config::PullRequestConfig;` import\n- Replaced two fields (`pull_request_enabled: bool`, `pull_request_draft: bool`) with single `pull_request: Option` field\n- Updated all ~40 test constructors: `pull_request_enabled: false, pull_request_draft: false` → `pull_request: None`\n\n**2. `lib/crates/fabro-workflows/src/cli/run.rs`**\n- Simplified `RunConfig` construction: replaced `pr_cfg.is_some_and(|p| p.enabled)` / `pr_cfg.is_none_or(|p| p.draft)` with `.filter(|p| p.enabled).cloned()`\n- Restructured PR creation block: `if let Some(ref pr_cfg) = config.pull_request { ... }` instead of `is_none()` check + later `unwrap()`\n- Updated `maybe_open_pull_request` call to pass `pr_cfg` reference\n- Updated event emission to use `pr_cfg.draft`\n- Updated resume config constructor: `pull_request: None`\n\n**3. `lib/crates/fabro-workflows/src/pull_request.rs`**\n- Changed `maybe_open_pull_request` signature: replaced `draft: bool` parameter with `pr_config: &crate::cli::run_config::PullRequestConfig`\n- Updated internal usage to read `pr_config.draft`\n- Updated test to construct and pass `PullRequestConfig`\n\n**4. `lib/crates/fabro-workflows/src/cli/pr.rs`**\n- Updated call to `maybe_open_pull_request`: constructs `PullRequestConfig { enabled: true, draft: true }` and passes reference\n\n**5. `lib/crates/fabro-api/src/server.rs`**\n- Replaced `pull_request_enabled: false, pull_request_draft: true` with `pull_request: None`\n\n**6. `lib/crates/fabro-workflows/src/handler/manager_loop.rs`**\n- Replaced `pull_request_enabled: false, pull_request_draft: true` with `pull_request: None`\n\n**7. `lib/crates/fabro-workflows/tests/integration.rs`**\n- Replaced all ~90 occurrences (8-space and 12-space indented) of `pull_request_enabled: false, pull_request_draft: false` with `pull_request: None`\n\n**8. `lib/crates/fabro-workflows/tests/daytona_integration.rs`**\n- Replaced all occurrences with `pull_request: None`\n\n### Verification\n- ✅ `cargo build --workspace` — clean\n- ✅ `cargo clippy --workspace -- -D warnings` — clean\n- ✅ `cargo test -p fabro-workflows -- pull_request run_config project_config` — 180 tests passed\n- ✅ `cargo fmt --check --all` — clean", "command.stderr": "", - "outcome": "success", + "response.simplify_opus": "Clean and focused. Here's the summary:\n\n## Summary\n\nAll three review agents found the same core issue: **leaky abstraction** — the TOML deserialization struct `PullRequestConfig` (with serde attributes and a vestigial `enabled` field) was being passed through runtime APIs where only a `draft: bool` was needed.\n\n### Fixed\n\n**Reverted `maybe_open_pull_request` parameter from `&PullRequestConfig` back to `draft: bool`** (3 files, net -8 lines):\n\n1. **`pull_request.rs`** — Changed parameter from `pr_config: &crate::cli::run_config::PullRequestConfig` to `draft: bool`, and used `draft` directly instead of `pr_config.draft`. Removed unnecessary `PullRequestConfig` construction in the test.\n\n2. **`pr.rs`** — Removed the 4-line `PullRequestConfig` struct construction that existed solely to wrap `draft: true`, now passes `true` directly.\n\n3. **`run.rs`** — Changed call from `pr_cfg` to `pr_cfg.draft` at the single callsite.\n\n### Not changed (false positives / not worth addressing)\n\n- **`RunConfig.pull_request: Option`** — Keeping this is correct. The `PullRequestConfig` struct may gain more fields (e.g., `auto_merge`, `merge_strategy` per the goal), and `Option` correctly encodes enabled/disabled semantics at the `RunConfig` level.\n- **The `enabled` field on `PullRequestConfig`** — Needed for TOML deserialization (`[pull_request]\\nenabled = true`) and filtered out at the config→runtime boundary in `run.rs`. This is the correct layering.\n- **No efficiency concerns** — `PullRequestConfig` is two bools; clone/allocation costs are negligible.", "internal.retry_count.toolchain": 1, + "thread.implement.current_node": "simplify_opus", "thread.start.current_node": "toolchain", "graph.goal": "# Refactor: Collapse `pull_request_*` fields on `RunConfig` into `Option`\n\n## Context\n\n`RunConfig` has 4 flat `pull_request_*` fields that encode a natural tree structure. This refactor collapses them into a single `Option` field, reusing the existing TOML config struct directly.\n\n## Approach\n\nReplace on `RunConfig`:\n```rust\npub pull_request_enabled: bool,\npub pull_request_draft: bool,\npub pull_request_auto_merge: bool,\npub pull_request_merge_strategy: MergeStrategy,\n```\n\nWith:\n```rust\npub pull_request: Option,\n```\n\n- `None` = disabled (replaces `enabled: false`)\n- `Some(config)` = enabled, read `.draft`, `.auto_merge`, `.merge_strategy` directly\n- The `enabled` field on `PullRequestConfig` is still needed for TOML deserialization but is redundant at runtime\n\nDelete `AutoMergeConfig` from `pull_request.rs` — pass `Option` directly instead (derived from `auto_merge` + `merge_strategy` on `PullRequestConfig`).\n\n## Files to modify\n\n1. **`engine.rs`** — replace 4 fields with `pull_request: Option`, update all test constructors (4 lines → 1: `pull_request: None`)\n2. **`run.rs`** — simplify construction: `pull_request: pr_cfg.filter(|p| p.enabled).cloned()` with draft override for auto_merge. Simplify read site: `if let Some(ref pr) = config.pull_request { ... }`\n3. **`pull_request.rs`** — change `maybe_open_pull_request` to take `&PullRequestConfig` instead of `draft: bool` + `auto_merge: Option`. Delete `AutoMergeConfig`\n4. **`pr.rs`** — update call to `maybe_open_pull_request`\n5. **`server.rs`, `manager_loop.rs`** — `pull_request: None`\n6. **`tests/integration.rs`, `tests/daytona_integration.rs`** — `pull_request: None`\n\n## Verification\n\n- `cargo build --workspace`\n- `cargo clippy --workspace -- -D warnings`\n- `cargo test -p fabro-workflows -- pull_request run_config project_config`\n- `cargo fmt --check --all`\n", - "current.preamble": "Goal: # Refactor: Collapse `pull_request_*` fields on `RunConfig` into `Option`\n\n## Context\n\n`RunConfig` has 4 flat `pull_request_*` fields that encode a natural tree structure. This refactor collapses them into a single `Option` field, reusing the existing TOML config struct directly.\n\n## Approach\n\nReplace on `RunConfig`:\n```rust\npub pull_request_enabled: bool,\npub pull_request_draft: bool,\npub pull_request_auto_merge: bool,\npub pull_request_merge_strategy: MergeStrategy,\n```\n\nWith:\n```rust\npub pull_request: Option,\n```\n\n- `None` = disabled (replaces `enabled: false`)\n- `Some(config)` = enabled, read `.draft`, `.auto_merge`, `.merge_strategy` directly\n- The `enabled` field on `PullRequestConfig` is still needed for TOML deserialization but is redundant at runtime\n\nDelete `AutoMergeConfig` from `pull_request.rs` — pass `Option` directly instead (derived from `auto_merge` + `merge_strategy` on `PullRequestConfig`).\n\n## Files to modify\n\n1. **`engine.rs`** — replace 4 fields with `pull_request: Option`, update all test constructors (4 lines → 1: `pull_request: None`)\n2. **`run.rs`** — simplify construction: `pull_request: pr_cfg.filter(|p| p.enabled).cloned()` with draft override for auto_merge. Simplify read site: `if let Some(ref pr) = config.pull_request { ... }`\n3. **`pull_request.rs`** — change `maybe_open_pull_request` to take `&PullRequestConfig` instead of `draft: bool` + `auto_merge: Option`. Delete `AutoMergeConfig`\n4. **`pr.rs`** — update call to `maybe_open_pull_request`\n5. **`server.rs`, `manager_loop.rs`** — `pull_request: None`\n6. **`tests/integration.rs`, `tests/daytona_integration.rs`** — `pull_request: None`\n\n## Verification\n\n- `cargo build --workspace`\n- `cargo clippy --workspace -- -D warnings`\n- `cargo test -p fabro-workflows -- pull_request run_config project_config`\n- `cargo fmt --check --all`\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", + "internal.thread_id": "implement", + "internal.fidelity": "compact", + "thread.preflight_lint.current_node": "implement", + "failure_class": "", + "last_response": "Clean and focused. Here's the summary:\n\n## Summary\n\nAll three review agents found the same core issue: **leaky abstraction** — the TOML deserialization struct `PullRequestConfig` (with serde attribu", + "thread.preflight_compile.current_node": "preflight_lint", + "graph.rankdir": "LR", + "outcome": "success", + "last_stage": "simplify_opus", + "current.preamble": "Goal: # Refactor: Collapse `pull_request_*` fields on `RunConfig` into `Option`\n\n## Context\n\n`RunConfig` has 4 flat `pull_request_*` fields that encode a natural tree structure. This refactor collapses them into a single `Option` field, reusing the existing TOML config struct directly.\n\n## Approach\n\nReplace on `RunConfig`:\n```rust\npub pull_request_enabled: bool,\npub pull_request_draft: bool,\npub pull_request_auto_merge: bool,\npub pull_request_merge_strategy: MergeStrategy,\n```\n\nWith:\n```rust\npub pull_request: Option,\n```\n\n- `None` = disabled (replaces `enabled: false`)\n- `Some(config)` = enabled, read `.draft`, `.auto_merge`, `.merge_strategy` directly\n- The `enabled` field on `PullRequestConfig` is still needed for TOML deserialization but is redundant at runtime\n\nDelete `AutoMergeConfig` from `pull_request.rs` — pass `Option` directly instead (derived from `auto_merge` + `merge_strategy` on `PullRequestConfig`).\n\n## Files to modify\n\n1. **`engine.rs`** — replace 4 fields with `pull_request: Option`, update all test constructors (4 lines → 1: `pull_request: None`)\n2. **`run.rs`** — simplify construction: `pull_request: pr_cfg.filter(|p| p.enabled).cloned()` with draft override for auto_merge. Simplify read site: `if let Some(ref pr) = config.pull_request { ... }`\n3. **`pull_request.rs`** — change `maybe_open_pull_request` to take `&PullRequestConfig` instead of `draft: bool` + `auto_merge: Option`. Delete `AutoMergeConfig`\n4. **`pr.rs`** — update call to `maybe_open_pull_request`\n5. **`server.rs`, `manager_loop.rs`** — `pull_request: None`\n6. **`tests/integration.rs`, `tests/daytona_integration.rs`** — `pull_request: None`\n\n## Verification\n\n- `cargo build --workspace`\n- `cargo clippy --workspace -- -D warnings`\n- `cargo test -p fabro-workflows -- pull_request run_config project_config`\n- `cargo fmt --check --all`\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, 56.0k tokens in / 11.6k out\n - Files: /home/daytona/workspace/lib/crates/fabro-api/src/server.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/cli/pr.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/cli/run.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/handler/manager_loop.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/pull_request.rs\n", + "internal.retry_count.implement": 1, "internal.run_id": "01KKTC2TWVRW6ZCYYZYGRX0GD6", "graph.model_stylesheet": "\n * { backend: api; model: claude-opus-4-6;}\n ", "internal.node_visit_count": 1, - "internal.thread_id": "preflight_lint", "internal.retry_count.start": 1, + "internal.retry_count.preflight_lint": 1, "command.output": "", - "thread.toolchain.current_node": "preflight_compile", - "current_node": "implement" + "thread.toolchain.current_node": "preflight_compile" }, "logs": [], "node_outcomes": { @@ -63,6 +68,30 @@ "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": 42 }, + "simplify_opus": { + "status": "success", + "context_updates": { + "last_stage": "simplify_opus", + "last_response": "Clean and focused. Here's the summary:\n\n## Summary\n\nAll three review agents found the same core issue: **leaky abstraction** — the TOML deserialization struct `PullRequestConfig` (with serde attribu", + "response.simplify_opus": "Clean and focused. Here's the summary:\n\n## Summary\n\nAll three review agents found the same core issue: **leaky abstraction** — the TOML deserialization struct `PullRequestConfig` (with serde attributes and a vestigial `enabled` field) was being passed through runtime APIs where only a `draft: bool` was needed.\n\n### Fixed\n\n**Reverted `maybe_open_pull_request` parameter from `&PullRequestConfig` back to `draft: bool`** (3 files, net -8 lines):\n\n1. **`pull_request.rs`** — Changed parameter from `pr_config: &crate::cli::run_config::PullRequestConfig` to `draft: bool`, and used `draft` directly instead of `pr_config.draft`. Removed unnecessary `PullRequestConfig` construction in the test.\n\n2. **`pr.rs`** — Removed the 4-line `PullRequestConfig` struct construction that existed solely to wrap `draft: true`, now passes `true` directly.\n\n3. **`run.rs`** — Changed call from `pr_cfg` to `pr_cfg.draft` at the single callsite.\n\n### Not changed (false positives / not worth addressing)\n\n- **`RunConfig.pull_request: Option`** — Keeping this is correct. The `PullRequestConfig` struct may gain more fields (e.g., `auto_merge`, `merge_strategy` per the goal), and `Option` correctly encodes enabled/disabled semantics at the `RunConfig` level.\n- **The `enabled` field on `PullRequestConfig`** — Needed for TOML deserialization (`[pull_request]\\nenabled = true`) and filtered out at the config→runtime boundary in `run.rs`. This is the correct layering.\n- **No efficiency concerns** — `PullRequestConfig` is two bools; clone/allocation costs are negligible." + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "model": "claude-opus-4-6", + "input_tokens": 66874, + "output_tokens": 8311, + "cache_read_tokens": 1199627, + "cache_write_tokens": 91029, + "reasoning_tokens": 442, + "cost": 1.6264349999999999 + }, + "files_touched": [ + "/home/daytona/workspace/lib/crates/fabro-workflows/src/cli/pr.rs", + "/home/daytona/workspace/lib/crates/fabro-workflows/src/cli/run.rs", + "/home/daytona/workspace/lib/crates/fabro-workflows/src/pull_request.rs" + ], + "duration_ms": 306479 + }, "implement": { "status": "success", "context_updates": { @@ -103,11 +132,12 @@ "duration_ms": 68455 } }, - "next_node_id": "simplify_opus", + "next_node_id": "simplify_gemini", "node_visits": { "preflight_compile": 1, "start": 1, "toolchain": 1, + "simplify_opus": 1, "preflight_lint": 1, "implement": 1 } diff --git a/nodes/implement/diff.patch b/nodes/implement/diff.patch new file mode 100644 index 000000000..d2b6aa7a3 --- /dev/null +++ b/nodes/implement/diff.patch @@ -0,0 +1,1875 @@ +diff --git a/lib/crates/fabro-api/src/server.rs b/lib/crates/fabro-api/src/server.rs +index 59debde..faaa7be 100644 +--- a/lib/crates/fabro-api/src/server.rs ++++ b/lib/crates/fabro-api/src/server.rs +@@ -626,8 +626,7 @@ async fn execute_run(state: Arc, run_id: String) { + github_app: None, + git_author: state.git_author.clone(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: true, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +diff --git a/lib/crates/fabro-workflows/src/cli/pr.rs b/lib/crates/fabro-workflows/src/cli/pr.rs +index 4be38dd..3094fcd 100644 +--- a/lib/crates/fabro-workflows/src/cli/pr.rs ++++ b/lib/crates/fabro-workflows/src/cli/pr.rs +@@ -388,6 +388,10 @@ async fn pr_create_from( + .model + .unwrap_or_else(|| fabro_llm::catalog::default_model().id.to_string()); + ++ let pr_config = crate::cli::run_config::PullRequestConfig { ++ enabled: true, ++ draft: true, ++ }; + let record = crate::pull_request::maybe_open_pull_request( + &creds, + &origin_url, +@@ -396,7 +400,7 @@ async fn pr_create_from( + &manifest.goal, + &diff, + &model, +- true, ++ &pr_config, + &run_dir, + ) + .await +diff --git a/lib/crates/fabro-workflows/src/cli/run.rs b/lib/crates/fabro-workflows/src/cli/run.rs +index 749f888..00c096f 100644 +--- a/lib/crates/fabro-workflows/src/cli/run.rs ++++ b/lib/crates/fabro-workflows/src/cli/run.rs +@@ -1244,7 +1244,6 @@ pub async fn run_command( + .as_ref() + .map(|c| c.checkpoint.exclude_globs.clone()) + .unwrap_or_default(); +- let pr_cfg = run_cfg.as_ref().and_then(|c| c.pull_request.as_ref()); + let config = RunConfig { + run_dir: run_dir.clone(), + cancel_token: None, +@@ -1269,8 +1268,11 @@ pub async fn run_command( + github_app: github_app.clone(), + git_author, + base_branch: detected_base_branch.or(remote_base_branch), +- pull_request_enabled: pr_cfg.is_some_and(|p| p.enabled), +- pull_request_draft: pr_cfg.is_none_or(|p| p.draft), ++ pull_request: run_cfg ++ .as_ref() ++ .and_then(|c| c.pull_request.as_ref()) ++ .filter(|p| p.enabled) ++ .cloned(), + asset_globs: run_cfg + .as_ref() + .and_then(|c| c.assets.as_ref()) +@@ -1416,76 +1418,78 @@ pub async fn run_command( + // Auto-create PR on successful completion (skip in dry-run mode) + let mut pushed_branch: Option = None; + let mut pr_url: Option = None; +- if !config.pull_request_enabled { +- debug!("Skipping PR creation: pull_request not enabled in config"); +- } else if dry_run_mode { +- debug!("Skipping PR creation: dry-run mode"); +- } else if let Err(ref e) = engine_result { +- debug!(error = %e, "Skipping PR creation: engine returned an error"); +- } else if let Ok(ref outcome) = engine_result { +- if !matches!( +- outcome.status, +- StageStatus::Success | StageStatus::PartialSuccess +- ) { +- debug!(status = ?outcome.status, "Skipping PR creation: run status is not success"); +- } else { +- let diff = tokio::fs::read_to_string(run_dir.join("final.patch")) +- .await +- .unwrap_or_default(); +- if let ( +- Some(ref base_branch), +- Some(ref run_branch), +- Some(ref creds), +- Some(ref origin), +- ) = ( +- &config.base_branch, +- &config.run_branch, +- &github_app, +- &origin_url, ++ if let Some(ref pr_cfg) = config.pull_request { ++ if dry_run_mode { ++ debug!("Skipping PR creation: dry-run mode"); ++ } else if let Err(ref e) = engine_result { ++ debug!(error = %e, "Skipping PR creation: engine returned an error"); ++ } else if let Ok(ref outcome) = engine_result { ++ if !matches!( ++ outcome.status, ++ StageStatus::Success | StageStatus::PartialSuccess + ) { +- // Run branch was pushed during checkpoint commits; +- // just record it for the PR creation. +- if config.git_checkpoint_enabled { +- pushed_branch = Some(run_branch.clone()); +- } ++ debug!(status = ?outcome.status, "Skipping PR creation: run status is not success"); ++ } else { ++ let diff = tokio::fs::read_to_string(run_dir.join("final.patch")) ++ .await ++ .unwrap_or_default(); ++ if let ( ++ Some(ref base_branch), ++ Some(ref run_branch), ++ Some(ref creds), ++ Some(ref origin), ++ ) = ( ++ &config.base_branch, ++ &config.run_branch, ++ &github_app, ++ &origin_url, ++ ) { ++ // Run branch was pushed during checkpoint commits; ++ // just record it for the PR creation. ++ if config.git_checkpoint_enabled { ++ pushed_branch = Some(run_branch.clone()); ++ } + +- match crate::pull_request::maybe_open_pull_request( +- creds, +- origin, +- base_branch, +- run_branch, +- graph.goal(), +- &diff, +- &model, +- config.pull_request_draft, +- &run_dir, +- ) +- .await +- { +- Ok(Some(record)) => { +- emitter.emit(&crate::event::WorkflowRunEvent::PullRequestCreated { +- pr_url: record.html_url.clone(), +- pr_number: record.number, +- draft: config.pull_request_draft, +- }); +- pr_url = Some(record.html_url.clone()); +- if let Err(e) = record.save(&run_dir.join("pull_request.json")) { +- tracing::warn!(error = %e, "Failed to save pull_request.json"); ++ match crate::pull_request::maybe_open_pull_request( ++ creds, ++ origin, ++ base_branch, ++ run_branch, ++ graph.goal(), ++ &diff, ++ &model, ++ pr_cfg, ++ &run_dir, ++ ) ++ .await ++ { ++ Ok(Some(record)) => { ++ emitter.emit(&crate::event::WorkflowRunEvent::PullRequestCreated { ++ pr_url: record.html_url.clone(), ++ pr_number: record.number, ++ draft: pr_cfg.draft, ++ }); ++ pr_url = Some(record.html_url.clone()); ++ if let Err(e) = record.save(&run_dir.join("pull_request.json")) { ++ tracing::warn!(error = %e, "Failed to save pull_request.json"); ++ } ++ } ++ Ok(None) => {} // empty diff, logged at DEBUG ++ Err(e) => { ++ emitter.emit(&crate::event::WorkflowRunEvent::PullRequestFailed { ++ error: e.to_string(), ++ }); ++ eprintln!( ++ "{} PR creation failed: {e}", ++ styles.yellow.apply_to("Warning:") ++ ); + } +- } +- Ok(None) => {} // empty diff, logged at DEBUG +- Err(e) => { +- emitter.emit(&crate::event::WorkflowRunEvent::PullRequestFailed { +- error: e.to_string(), +- }); +- eprintln!( +- "{} PR creation failed: {e}", +- styles.yellow.apply_to("Warning:") +- ); + } + } + } + } ++ } else { ++ debug!("Skipping PR creation: pull_request not enabled in config"); + } + + let outcome = engine_result?; +@@ -1923,8 +1927,7 @@ async fn run_from_branch( + github_app: github_app.clone(), + git_author, + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +diff --git a/lib/crates/fabro-workflows/src/engine.rs b/lib/crates/fabro-workflows/src/engine.rs +index ca68942..3c147e0 100644 +--- a/lib/crates/fabro-workflows/src/engine.rs ++++ b/lib/crates/fabro-workflows/src/engine.rs +@@ -16,6 +16,7 @@ use fabro_git_storage::trailerlink::{self, Trailer}; + use crate::artifact::{offload_large_values, sync_artifacts_to_env, ArtifactStore}; + use crate::asset_snapshot; + use crate::checkpoint::Checkpoint; ++use crate::cli::run_config::PullRequestConfig; + use crate::condition::evaluate_condition; + use crate::context; + use crate::context::Context; +@@ -857,10 +858,8 @@ pub struct RunConfig { + pub git_author: crate::git::GitAuthor, + /// Name of the branch the run was started from (for PR base). + pub base_branch: Option, +- /// Whether to auto-create a PR on successful completion. +- pub pull_request_enabled: bool, +- /// Whether to create the PR as a draft. +- pub pull_request_draft: bool, ++ /// Pull request configuration; `None` = disabled. ++ pub pull_request: Option, + /// Glob patterns for asset collection. Empty = no asset collection. + pub asset_globs: Vec, + /// Workflow directory slug (e.g. "smoke" from `fabro/workflows/smoke/`). +@@ -3025,8 +3024,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3055,8 +3053,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3093,8 +3090,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3127,8 +3123,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3157,8 +3152,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3200,8 +3194,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3267,8 +3260,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3361,8 +3353,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3398,8 +3389,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3430,8 +3420,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3462,8 +3451,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3498,8 +3486,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3662,8 +3649,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3709,8 +3695,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3774,8 +3759,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3842,8 +3826,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3914,8 +3897,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3975,8 +3957,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4037,8 +4018,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4074,8 +4054,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4107,8 +4086,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4139,8 +4117,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4184,8 +4161,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4266,8 +4242,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4301,8 +4276,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4338,8 +4312,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4380,8 +4353,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4420,8 +4392,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4457,8 +4428,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4555,8 +4525,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4768,8 +4737,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4808,8 +4776,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4855,8 +4822,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4942,8 +4908,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5040,8 +5005,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5115,8 +5079,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5177,8 +5140,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5240,8 +5202,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5399,8 +5360,7 @@ mod tests { + github_app: None, + git_author: crate::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +diff --git a/lib/crates/fabro-workflows/src/handler/manager_loop.rs b/lib/crates/fabro-workflows/src/handler/manager_loop.rs +index a581064..8890e2f 100644 +--- a/lib/crates/fabro-workflows/src/handler/manager_loop.rs ++++ b/lib/crates/fabro-workflows/src/handler/manager_loop.rs +@@ -156,8 +156,7 @@ impl Handler for SubWorkflowHandler { + .map(|gs| gs.git_author.clone()) + .unwrap_or_default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: true, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +diff --git a/lib/crates/fabro-workflows/src/pull_request.rs b/lib/crates/fabro-workflows/src/pull_request.rs +index 1cfa742..5798b17 100644 +--- a/lib/crates/fabro-workflows/src/pull_request.rs ++++ b/lib/crates/fabro-workflows/src/pull_request.rs +@@ -355,7 +355,7 @@ pub async fn maybe_open_pull_request( + goal: &str, + diff: &str, + model: &str, +- draft: bool, ++ pr_config: &crate::cli::run_config::PullRequestConfig, + run_dir: &Path, + ) -> Result, String> { + if diff.is_empty() { +@@ -379,7 +379,7 @@ pub async fn maybe_open_pull_request( + head_branch, + &title, + &body, +- draft, ++ pr_config.draft, + ) + .await?; + +@@ -904,6 +904,10 @@ mod tests { + app_id: "123".to_string(), + private_key_pem: "unused".to_string(), + }; ++ let pr_config = crate::cli::run_config::PullRequestConfig { ++ enabled: true, ++ draft: false, ++ }; + let result = maybe_open_pull_request( + &creds, + "https://github.com/owner/repo.git", +@@ -912,7 +916,7 @@ mod tests { + "Fix bug", + "", + "claude-sonnet-4-20250514", +- false, ++ &pr_config, + tmp.path(), + ) + .await; +diff --git a/lib/crates/fabro-workflows/tests/daytona_integration.rs b/lib/crates/fabro-workflows/tests/daytona_integration.rs +index 948181b..4e31254 100644 +--- a/lib/crates/fabro-workflows/tests/daytona_integration.rs ++++ b/lib/crates/fabro-workflows/tests/daytona_integration.rs +@@ -403,8 +403,7 @@ async fn daytona_pipeline_artifact_offload_and_sync() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -601,8 +600,7 @@ async fn daytona_git_checkpoint_remote_emits_events() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -790,8 +788,7 @@ async fn daytona_parallel_git_branching_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1170,8 +1167,7 @@ async fn daytona_git_checkpoint_with_shadow_branch() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1313,8 +1309,7 @@ async fn daytona_asset_collection() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1571,8 +1566,7 @@ async fn daytona_git_push_run_branch_to_origin() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +diff --git a/lib/crates/fabro-workflows/tests/integration.rs b/lib/crates/fabro-workflows/tests/integration.rs +index db7af73..1c4cea9 100644 +--- a/lib/crates/fabro-workflows/tests/integration.rs ++++ b/lib/crates/fabro-workflows/tests/integration.rs +@@ -205,8 +205,7 @@ async fn end_to_end_linear_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -351,8 +350,7 @@ async fn end_to_end_branching_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -479,8 +477,7 @@ async fn end_to_end_human_gate_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -597,8 +594,7 @@ async fn goal_gate_routes_to_retry_target_on_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -725,8 +721,7 @@ async fn goal_gate_routes_to_retry_target_when_present() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1044,8 +1039,7 @@ async fn retry_on_failure_then_succeed() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1126,8 +1120,7 @@ async fn pipeline_with_many_nodes() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1459,8 +1452,7 @@ async fn smoke_test_with_mock_codergen_backend() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1567,8 +1559,7 @@ async fn end_to_end_parallel_fan_out_fan_in() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1687,8 +1678,7 @@ async fn resume_from_checkpoint_completes_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1793,8 +1783,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1843,8 +1832,7 @@ async fn graph_goal_in_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1885,8 +1873,7 @@ async fn event_streaming_lifecycle() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -1971,8 +1958,7 @@ async fn context_flow_between_stages() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2030,8 +2016,7 @@ async fn tool_handler_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2106,8 +2091,7 @@ async fn auto_approve_interviewer_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2149,8 +2133,7 @@ async fn codergen_without_backend_simulated() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2260,8 +2243,7 @@ async fn branching_loop_back_on_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2351,8 +2333,7 @@ async fn human_gate_loops_back() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2415,8 +2396,7 @@ async fn scenario_ship_a_feature() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2507,8 +2487,7 @@ async fn scenario_parallel_expert_review() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2597,8 +2576,7 @@ async fn scenario_node_retries_on_retry_status() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2665,8 +2643,7 @@ async fn scenario_loop_restart_resets_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2739,8 +2716,7 @@ async fn scenario_bug_triage_router() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2804,8 +2780,7 @@ async fn scenario_crash_recovery() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -2919,8 +2894,7 @@ async fn manager_loop_stop_condition_satisfied_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3002,8 +2976,7 @@ async fn manager_loop_max_cycles_exceeded_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3144,8 +3117,7 @@ async fn conditional_branching_success_fail_paths() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3203,8 +3175,7 @@ async fn edge_selection_condition_match_wins_over_weight() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3256,8 +3227,7 @@ async fn edge_selection_weight_breaks_ties() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3301,8 +3271,7 @@ async fn edge_selection_lexical_tiebreak() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3365,8 +3334,7 @@ async fn context_updates_visible_across_nodes() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3415,8 +3383,7 @@ async fn stylesheet_applies_model_override() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3477,8 +3444,7 @@ async fn custom_handler_registration_and_execution() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3554,8 +3520,7 @@ async fn integration_smoke_plan_implement_review_done() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3664,8 +3629,7 @@ async fn manager_loop_runs_child_engine_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3805,8 +3769,7 @@ async fn manager_loop_context_flows_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -3885,8 +3848,7 @@ async fn manager_loop_child_dotfile_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4005,8 +3967,7 @@ async fn graph_merge_e2e_through_engine() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4162,8 +4123,7 @@ async fn fidelity_default_is_compact() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4225,8 +4185,7 @@ async fn fidelity_graph_default_applied() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4284,8 +4243,7 @@ async fn fidelity_node_overrides_graph_default() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4349,8 +4307,7 @@ async fn fidelity_edge_overrides_node_and_graph() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4404,8 +4361,7 @@ async fn fidelity_full_produces_empty_preamble() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4469,8 +4425,7 @@ async fn fidelity_truncate_preamble_minimal() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4547,8 +4502,7 @@ async fn fidelity_summary_low_mode() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4620,8 +4574,7 @@ async fn fidelity_summary_medium_mode() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4693,8 +4646,7 @@ async fn fidelity_summary_high_mode() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4759,8 +4711,7 @@ async fn fidelity_full_sets_thread_id_in_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4836,8 +4787,7 @@ async fn fidelity_full_nodes_share_thread_id() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -4923,8 +4873,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5026,8 +4975,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5116,8 +5064,7 @@ async fn fidelity_resume_no_degrade_when_not_full() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5164,8 +5111,7 @@ async fn fidelity_stored_in_checkpoint_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5256,8 +5202,7 @@ async fn fidelity_precedence_multi_node_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5330,8 +5275,7 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5412,8 +5356,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5486,8 +5429,7 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5563,8 +5505,7 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5623,8 +5564,7 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5686,8 +5626,7 @@ async fn fidelity_edge_thread_id_override_in_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5750,8 +5689,7 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5824,8 +5762,7 @@ async fn fidelity_from_parsed_dot_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5878,8 +5815,7 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -5954,8 +5890,7 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6047,8 +5982,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6265,8 +6199,7 @@ mod real_llm { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6386,8 +6319,7 @@ mod real_llm { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6532,8 +6464,7 @@ mod real_llm { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6646,8 +6577,7 @@ mod real_llm { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6749,8 +6679,7 @@ async fn human_gate_freeform_only_routes_text() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -6887,8 +6816,7 @@ async fn human_gate_freeform_with_fixed_choice_match() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -7009,8 +6937,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -7145,8 +7072,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -7261,8 +7187,7 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -7528,8 +7453,7 @@ fn make_run_config(dir: &std::path::Path) -> RunConfig { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + } +@@ -8671,8 +8595,7 @@ async fn arc_e2e_with_real_llm() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -8806,8 +8729,7 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -9012,8 +8934,7 @@ async fn large_context_values_are_offloaded_to_artifact_store() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -9239,8 +9160,7 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -9376,8 +9296,7 @@ async fn node_dir_uses_visit_count_on_revisit() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -10357,8 +10276,7 @@ async fn full_pipeline_with_cli_backend_node() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -10495,8 +10413,7 @@ async fn stylesheet_backend_property_routes_to_cli() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -10783,8 +10700,7 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -10976,8 +10892,7 @@ async fn git_checkpoint_host_writes_shadow_branch() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -11177,8 +11092,7 @@ async fn parallel_git_branching_host_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -11443,8 +11357,7 @@ async fn git_checkpoint_host_skips_empty_diff_patch() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -11828,8 +11741,7 @@ async fn e2e_circuit_breaker_deterministic_self_loop() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -11882,8 +11794,7 @@ async fn e2e_circuit_breaker_custom_limit() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -11929,8 +11840,7 @@ async fn e2e_circuit_breaker_ignores_transient_failures() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -11983,8 +11893,7 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12030,8 +11939,7 @@ async fn e2e_circuit_breaker_loop_restart() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12099,8 +12007,7 @@ async fn e2e_failure_signature_persisted_in_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12170,8 +12077,7 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12233,8 +12139,7 @@ async fn e2e_signature_maps_persist_in_checkpoint() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12367,8 +12272,7 @@ async fn e2e_circuit_breaker_emits_events_before_abort() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12441,8 +12345,7 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12544,8 +12447,7 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12648,8 +12550,7 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12695,8 +12596,7 @@ async fn e2e_loop_restart_blocked_for_structural_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12742,8 +12642,7 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12789,8 +12688,7 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12833,8 +12731,7 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12881,8 +12778,7 @@ async fn e2e_loop_restart_allowed_for_transient_infra() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -12992,8 +12888,7 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -13055,8 +12950,7 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -13108,8 +13002,7 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -13180,8 +13073,7 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -13318,8 +13210,7 @@ async fn asset_collection_local_sandbox_success() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: vec!["test-results/**".to_string()], + workflow_slug: None, + }; +@@ -13434,8 +13325,7 @@ async fn asset_collection_local_sandbox_on_failure() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: vec!["test-results/**".to_string()], + workflow_slug: None, + }; +@@ -13532,8 +13422,7 @@ async fn asset_collection_docker_sandbox() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; +@@ -13608,8 +13497,7 @@ async fn wait_timer_e2e() { + github_app: None, + git_author: fabro_workflows::git::GitAuthor::default(), + base_branch: None, +- pull_request_enabled: false, +- pull_request_draft: false, ++ pull_request: None, + asset_globs: Vec::new(), + workflow_slug: None, + }; diff --git a/nodes/simplify_opus/prompt.md b/nodes/simplify_opus/prompt.md new file mode 100644 index 000000000..4501cac02 --- /dev/null +++ b/nodes/simplify_opus/prompt.md @@ -0,0 +1,115 @@ +Goal: # Refactor: Collapse `pull_request_*` fields on `RunConfig` into `Option` + +## Context + +`RunConfig` has 4 flat `pull_request_*` fields that encode a natural tree structure. This refactor collapses them into a single `Option` field, reusing the existing TOML config struct directly. + +## Approach + +Replace on `RunConfig`: +```rust +pub pull_request_enabled: bool, +pub pull_request_draft: bool, +pub pull_request_auto_merge: bool, +pub pull_request_merge_strategy: MergeStrategy, +``` + +With: +```rust +pub pull_request: Option, +``` + +- `None` = disabled (replaces `enabled: false`) +- `Some(config)` = enabled, read `.draft`, `.auto_merge`, `.merge_strategy` directly +- The `enabled` field on `PullRequestConfig` is still needed for TOML deserialization but is redundant at runtime + +Delete `AutoMergeConfig` from `pull_request.rs` — pass `Option` directly instead (derived from `auto_merge` + `merge_strategy` on `PullRequestConfig`). + +## Files to modify + +1. **`engine.rs`** — replace 4 fields with `pull_request: Option`, update all test constructors (4 lines → 1: `pull_request: None`) +2. **`run.rs`** — simplify construction: `pull_request: pr_cfg.filter(|p| p.enabled).cloned()` with draft override for auto_merge. Simplify read site: `if let Some(ref pr) = config.pull_request { ... }` +3. **`pull_request.rs`** — change `maybe_open_pull_request` to take `&PullRequestConfig` instead of `draft: bool` + `auto_merge: Option`. Delete `AutoMergeConfig` +4. **`pr.rs`** — update call to `maybe_open_pull_request` +5. **`server.rs`, `manager_loop.rs`** — `pull_request: None` +6. **`tests/integration.rs`, `tests/daytona_integration.rs`** — `pull_request: None` + +## Verification + +- `cargo build --workspace` +- `cargo clippy --workspace -- -D warnings` +- `cargo test -p fabro-workflows -- pull_request run_config project_config` +- `cargo fmt --check --all` + + +## Completed stages +- **toolchain**: success + - 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` + - Stdout: + ``` + cargo 1.94.0 (85eff7c80 2026-01-15) + ``` + - Stderr: (empty) +- **preflight_compile**: success + - Script: `cargo check -q --workspace 2>&1` + - Stdout: (empty) + - Stderr: (empty) +- **preflight_lint**: success + - Script: `cargo clippy -q --workspace -- -D warnings 2>&1` + - Stdout: (empty) + - Stderr: (empty) +- **implement**: success + - Model: claude-opus-4-6, 56.0k tokens in / 11.6k out + - Files: /home/daytona/workspace/lib/crates/fabro-api/src/server.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/cli/pr.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/cli/run.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/handler/manager_loop.rs, /home/daytona/workspace/lib/crates/fabro-workflows/src/pull_request.rs + + +# Simplify: Code Review and Cleanup + +Review all changed files for reuse, quality, and efficiency. Fix any issues found. + +## Phase 1: Identify Changes + +Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation. + +## Phase 2: Launch Three Review Agents in Parallel + +Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context. + +### Agent 1: Code Reuse Review + +For each change: + +1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones. +2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead. +3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates. + +Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it. + +### Agent 2: Code Quality Review + +Review the same changes for hacky patterns: + +1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls +2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones +3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction +4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries +5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase + +Note: This is a greenfield app, so be aggressive in optimizing quality. + +### Agent 3: Efficiency Review + +Review the same changes for efficiency: + +1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns +2. Missed concurrency: independent operations run sequentially when they could run in parallel +3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths +4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error +5. Memory: unbounded data structures, missing cleanup, event listener leaks +6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one + +## Phase 3: Fix Issues + +Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it. + +When done, briefly summarize what was fixed (or confirm the code was already clean). \ No newline at end of file diff --git a/nodes/simplify_opus/provider_used.json b/nodes/simplify_opus/provider_used.json new file mode 100644 index 000000000..c1905ff10 --- /dev/null +++ b/nodes/simplify_opus/provider_used.json @@ -0,0 +1,5 @@ +{ + "mode": "agent", + "provider": "anthropic", + "model": "claude-opus-4-6" +} \ No newline at end of file diff --git a/nodes/simplify_opus/response.md b/nodes/simplify_opus/response.md new file mode 100644 index 000000000..a7a3c7677 --- /dev/null +++ b/nodes/simplify_opus/response.md @@ -0,0 +1,21 @@ +Clean and focused. Here's the summary: + +## Summary + +All three review agents found the same core issue: **leaky abstraction** — the TOML deserialization struct `PullRequestConfig` (with serde attributes and a vestigial `enabled` field) was being passed through runtime APIs where only a `draft: bool` was needed. + +### Fixed + +**Reverted `maybe_open_pull_request` parameter from `&PullRequestConfig` back to `draft: bool`** (3 files, net -8 lines): + +1. **`pull_request.rs`** — Changed parameter from `pr_config: &crate::cli::run_config::PullRequestConfig` to `draft: bool`, and used `draft` directly instead of `pr_config.draft`. Removed unnecessary `PullRequestConfig` construction in the test. + +2. **`pr.rs`** — Removed the 4-line `PullRequestConfig` struct construction that existed solely to wrap `draft: true`, now passes `true` directly. + +3. **`run.rs`** — Changed call from `pr_cfg` to `pr_cfg.draft` at the single callsite. + +### Not changed (false positives / not worth addressing) + +- **`RunConfig.pull_request: Option`** — Keeping this is correct. The `PullRequestConfig` struct may gain more fields (e.g., `auto_merge`, `merge_strategy` per the goal), and `Option` correctly encodes enabled/disabled semantics at the `RunConfig` level. +- **The `enabled` field on `PullRequestConfig`** — Needed for TOML deserialization (`[pull_request]\nenabled = true`) and filtered out at the config→runtime boundary in `run.rs`. This is the correct layering. +- **No efficiency concerns** — `PullRequestConfig` is two bools; clone/allocation costs are negligible. \ No newline at end of file diff --git a/nodes/simplify_opus/status.json b/nodes/simplify_opus/status.json new file mode 100644 index 000000000..a9db0797b --- /dev/null +++ b/nodes/simplify_opus/status.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "notes": "Stage completed: simplify_opus", + "failure_reason": null, + "timestamp": "2026-03-16T04:00:46.973499+00:00" +} \ No newline at end of file