Commit graph

272 commits

Author SHA1 Message Date
Bryan Helmkamp
f0eaec9c55
Fix OpenAI multi-turn failures by only setting store: false in Codex mode
The Codex endpoint commit (fd9260c3) added `store: false` unconditionally
to all OpenAI Responses API requests. This broke multi-turn conversations
because OpenAI doesn't persist items when store is false, so referencing
previous reasoning/message IDs on subsequent turns returns a 404. The fix
makes store conditional: true for regular OpenAI (the API default), false
only for the Codex endpoint which requires it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
64bc6d437d
Fix workspace build by removing unnecessary exedev feature from fabro-api
fabro-api doesn't use `SandboxProvider::Exe` but was unconditionally
enabling `exedev` on fabro-workflows. Cargo feature unification made
the `Exe` variant exist while fabro-cli's cfg-gated match arms were
inactive, causing non-exhaustive pattern errors in workspace builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
ae302cb346
Simplify recent CLI additions: dedup, fix TOCTOU, remove wrappers
- Extract git_repo_root() helper in init.rs (was duplicated between
  run_init and run_deinit)
- Fix TOCTOU in run_deinit: remove .exists() check, handle NotFound
  from remove_file directly
- Change dotenv::remove_env_key() to return Option<String> so callers
  don't need to separately parse the file to check key existence
- Remove merge_env wrapper in install.rs, call shared function directly
- Remove duplicate merge_env tests from install.rs (already in dotenv.rs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 18:09:08 -04:00
Bryan Helmkamp
d6fdeb8135
Add fabro secret CLI subcommands for managing ~/.fabro/.env
Provides get/list/rm/set subcommands to manage secrets without manually
editing the .env file. Extracts shared dotenv utilities into
fabro-config::dotenv and refactors install.rs to use them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 18:02:38 -04:00
Bryan Helmkamp
3a4ff8a282
Add hidden --skill flag to fabro repo init
Allows skill installation during project setup via `fabro repo init --skill`,
which installs the fabro-create-workflow skill to .claude/skills/. The flag is
hidden from help output since `fabro skill install` is being deprecated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:54:30 -04:00
Bryan Helmkamp
7c16f3b3c9
Add fabro repo deinit command to reverse project initialization
Removes fabro.toml and the fabro/ directory from the git repo root.
Fails with a clear error when the project is not initialized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:51:45 -04:00
Bryan Helmkamp
e6e92aa749
Remove unreachable wildcard arms in sandbox provider matches
The SandboxProvider::Exe variant is gated behind #[cfg(feature = "exedev")],
so the remaining variants are exhaustively matched without the wildcard.
Removing the dead arms fixes clippy's unreachable-patterns warning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:43:39 -04:00
Bryan Helmkamp
076430cddc
Rename init to repo init with backwards-compat shim
Move `fabro init` under `fabro repo init` subcommand group.
The old `fabro init` still works but is hidden from help and
prints a deprecation warning before executing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:41:49 -04:00
Bryan Helmkamp
f7e07cb827
Expand scenario tests to cover more CLI subcommands
Add coverage for validate, model list, workflow list, doctor, exec,
ps, inspect, logs, rm, system df, asset list, asset cp, and cp.
Uses HOME isolation for run lifecycle tests and synthetic assets
for asset/cp testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 16:05:13 -04:00
Bryan Helmkamp
9122d42de8
Deduplicate CLI command helpers into shared module
Consolidate six duplicated helper functions (tilde_path, color_if,
split_run_path, validate_daytona_provider, format_duration_ms,
format_size) into commands/shared.rs. Also hoist Utc::now() out of a
per-run loop in list_command and avoid an unnecessary Vec<char>
allocation in truncate_goal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:45:25 -04:00
Bryan Helmkamp
c57734244e
Move workflow CLI ownership into fabro-cli 2026-03-18 09:31:11 -04:00
Bryan Helmkamp
bf19a992fa
Update model list snapshots for gpt-5.4-mini
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 15:50:10 -04:00
Bryan Helmkamp
40f53873e4
Deduplicate config loading and run-config merging
- Extract generic load_config_file<T>(path, filename) helper, reducing
  load_cli_config and load_server_config to one-liners.

- Rewrite WorkflowRunConfig::apply_defaults to delegate to
  RunDefaults::merge_overlay, eliminating ~90 lines of duplicate
  deep-merge logic. Both methods now share the same code path.

- Fix bug where RunDefaults::merge_overlay silently dropped the ssh
  sandbox config from overlays (the ssh field was never merged).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:33:04 -04:00
Bryan Helmkamp
24e8e0a74b
Move config type tests into fabro-config
Tests for types defined in fabro-config (HookEvent, HookDefinition,
HookConfig, McpServerConfig, McpTransport, etc.) now live alongside
their definitions rather than in the downstream re-exporting crates.

Tests for types that remain in fabro-hooks (HookContext, HookDecision,
PromptHookResponse) stay in fabro-hooks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:21:16 -04:00
Bryan Helmkamp
165b817250
Fix fabro-config dependency inversion
Move config/data types from upstream crates (fabro-agent, fabro-mcp,
fabro-workflows, fabro-hooks) down into fabro-config so it becomes a
leaf crate depending only on fabro-util + external crates.

New modules in fabro-config:
- mcp.rs: McpServerConfig, McpTransport, McpServerEntry
- sandbox.rs: DaytonaConfig, ExeConfig, SshConfig, SandboxConfig, etc.
- hook.rs: HookEvent, HookDefinition, HookConfig, HookType, TlsMode
- run.rs: RunDefaults, WorkflowRunConfig, LlmConfig, SetupConfig, etc.
- project.rs: ProjectConfig, workflow discovery/resolution functions

Source crates re-export from fabro-config for backward compatibility.
Also removes stale strsim dep and moves toml to dev-deps in
fabro-workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:17:55 -04:00
Bryan Helmkamp
fab6603c40
Clean up fabro-hooks extraction
Extract set_hook_node() helper to deduplicate the 5 call sites that
populate node fields on HookContext. The helper lives in fabro-workflows
(which has the fabro-graphviz dependency) rather than fabro-hooks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 14:12:26 -04:00
Bryan Helmkamp
b5e1d1fa74
Extract fabro-hooks crate from fabro-workflows
Move the self-contained hooks module (~2900 LOC) into its own crate to
clarify the dependency graph and make the hook system independently
reusable. The set_node convenience method is inlined at its two call
sites in parallel.rs since it depends on fabro-graphviz types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 14:08:02 -04:00
Bryan Helmkamp
51cfb55128
Clean up fabro-interview extraction
- Use already-imported names in run_from_branch instead of fully-qualified
  fabro_interview::* paths
- Use std::io::Error::other() for serde error conversion (matches codebase
  convention, more concise)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:56:51 -04:00
Bryan Helmkamp
c18062bc3f
Extract fabro-interview crate from fabro-workflows
The interviewer module (trait + 7 implementations for human-in-the-loop
interactions) had zero dependencies on fabro-workflows internals, making
it a clean extraction. Consumers (fabro-api, fabro-slack) now depend on
fabro-interview directly instead of reaching through fabro-workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:53:36 -04:00
Bryan Helmkamp
4da08d269b
Add GPT-5.4 Mini to model catalog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:53:08 -04:00
Bryan Helmkamp
ef56abb544
Clean up fabro-validate extraction
- Add From<ValidationError> for FabroError to eliminate duplicated
  .map_err(|e| FabroError::Validation(e.0)) at call sites
- Use top-level `use` imports for stylesheet types in rules.rs
  instead of verbose fully-qualified paths
- Remove duplicate parse_condition tests from fabro-workflows
  (already covered by fabro-graphviz)
- Use //! inner doc comments in context/keys.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:42:54 -04:00
Bryan Helmkamp
19e268424f
Extract fabro-validate crate from fabro-workflows
Move validation/lint framework and all 24 rules into a dedicated
fabro-validate crate. As prerequisites, move Fidelity, stylesheet
parser/types, and condition parser into fabro-graphviz (where the
Graph types they operate on already live) so fabro-validate can
depend on fabro-graphviz directly without a circular dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:33:34 -04:00
Bryan Helmkamp
4c901f85d6
Extract fabro-daytona crate from fabro-workflows
Move DaytonaSandbox into its own crate, matching the pattern used by
fabro-exe, fabro-sprites, and fabro-ssh. The new crate internalizes
daytona_sdk::Client creation so callers never touch daytona-sdk directly:

- new() is now async and creates the client internally
- reconnect(name) replaces from_existing() + manual client/get boilerplate
- daytona-sdk and daytona-api-client removed as fabro-workflows dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:10:09 -04:00
Bryan Helmkamp
693f52de62
Extract fabro-retro crate from fabro-workflows
Move retro.rs and retro_agent.rs into a new fabro-retro crate to reduce
the size of fabro-workflows and clarify domain boundaries.

Key design changes:
- Add CompletedStage struct as a flat DTO that decouples retro derivation
  from Checkpoint/Outcome types in the workflow engine
- derive_retro now takes Vec<CompletedStage> (owned) instead of &Checkpoint
- run_retro_agent takes an event_callback closure instead of EventEmitter,
  pushing event filtering to the caller
- Shared build_completed_stages() in fabro-workflows::lib converts
  Checkpoint → Vec<CompletedStage> for both run.rs and server.rs callers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 11:48:40 -04:00
Bryan Helmkamp
49c7e329dd
Normalize fabro_graphviz imports to use shorter re-exported paths
Replace fabro_graphviz::graph::types:: with fabro_graphviz::graph::
everywhere, since graph/mod.rs re-exports types::*. Also simplify
the From<GraphvizError> impl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 11:26:23 -04:00
Bryan Helmkamp
b822a76f79
Extract fabro-graphviz crate from fabro-workflows
Move the self-contained graph/ and parser/ modules into a new
fabro-graphviz crate so the Graphviz DOT parser can be used without
pulling in the full workflow engine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 11:20:55 -04:00
Bryan Helmkamp
870907656c
Use fabro.sh/discord redirect instead of direct Discord invite URL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:22:12 -04:00
Bryan Helmkamp
b0766afc8f
Add fabro discord subcommand to open Discord invite in browser
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:18:02 -04:00
Bryan Helmkamp
693392f3ed
Add fabro docs subcommand to open docs website in browser
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:16:46 -04:00
Bryan Helmkamp
fd9260c3c5
Add Codex endpoint support for OpenAI OAuth flow
Route OpenAI OAuth users through the ChatGPT Codex backend API with
required headers (ChatGPT-Account-Id, originator). The Codex endpoint
requires streaming-only requests, omits unsupported fields (temperature,
max_output_tokens, top_p), and uses a different error format. Also
persists the account ID from OAuth tokens and updates CLI docs for
`fabro ps -q`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 00:47:47 -04:00
Bryan Helmkamp
e09a6de72f
Handle OAuth error responses in OpenAI callback server
When the OAuth provider redirects with an error (e.g. invalid_scope),
the callback server now shows a styled error page and propagates the
error to the CLI instead of failing with a deserialization error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 23:53:57 -04:00
brynary-fabro[bot]
0530605fdb
Add -q/--quiet to fabro ps (#64)
Adds a `-q`/`--quiet` flag to `fabro ps`, mirroring the behavior of
`docker ps -q`. When specified, the command outputs only full run IDs,
one per line, with no headers, footers, or "no runs found"
messages—empty output simply means no matching runs exist.

The quiet flag takes precedence over JSON output, and it composes
naturally with other flags like `-a` (e.g., `fabro ps -qa` lists all run
IDs).

### Fabro Details

<details>
<summary>Ran 10 stages in 10m 13s for $1.09</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.17 | 0 |
| simplify_opus | 0s | $0.30 | 0 |
| simplify_gemini | 0s | $0.30 | 0 |
| simplify_gpt | 0s | $0.32 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **10m 13s** | **$1.09** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 22:00:20 -04:00
brynary-fabro[bot]
9197895f10
Add Sentry panic reporting to fabro CLI (#35)
This PR adds Sentry-based panic reporting to the fabro CLI and improves
the reliability of all background telemetry senders. When the CLI
panics, a Sentry event is serialized to a temp file and uploaded by a
fully detached subprocess, giving visibility into crashes that would
otherwise go unnoticed.

The core infrastructure change is a new `spawn_detached` utility that
uses the double-fork pattern on Unix (fork → setsid → close_fd → fork →
exec) to ensure background subprocesses survive parent exit and terminal
close. This replaces the previous simple `Command::spawn()` approach
used by the analytics sender, which was unreliable since the child could
be killed when the parent exits. Both the analytics sender
(`__send_analytics`) and the new panic sender (`__send_panic`) now share
this `spawn_fabro_subcommand` helper.

The panic module installs a hook early in `main()` that captures panic
info, builds a Sentry event with exception details, stacktrace, and OS
context, then spawns a detached `fabro __send_panic` subprocess to
upload it. It respects the telemetry level setting (no-ops when off),
prevents recursion by setting `FABRO_TELEMETRY=off` in the child,
filters benign "Broken pipe" panics from `| head` usage, and uses a
compile-time `SENTRY_DSN` so dev builds without the DSN set are
unaffected.

### Fabro Details

<details>
<summary>Ran 10 stages in 32m 34s for $8.13</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.35 | 0 |
| simplify_opus | 0s | $2.23 | 0 |
| simplify_gemini | 0s | $1.75 | 0 |
| simplify_gpt | 0s | $1.80 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **32m 34s** | **$8.13** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 22:00:11 -04:00
brynary-fabro[bot]
9d0a2a9a66
Move sandbox lifecycle into the engine (#22)
This PR moves sandbox lifecycle management (initialization, setup
commands, devcontainer phases, and cleanup) from the CLI's `run_command`
god function into the workflow engine. Two new engine methods are
introduced: `run_with_lifecycle()` orchestrates sandbox init, fires the
`SandboxReady` hook (now blocking by default), emits a new
`SandboxInitialized` event, handles remote git setup, runs setup
commands and devcontainer lifecycle phases, then delegates to the
existing `run_internal()` graph execution. `cleanup_sandbox()` fires the
`SandboxCleanup` hook and optionally tears down the sandbox. Both
`SandboxReady` and `SandboxCleanup` hook events were previously defined
but never fired — they now fire naturally within the engine alongside
all other hooks.

The CLI is simplified significantly: sandbox record persistence and
progress UI updates are handled via an event listener for
`SandboxInitialized` rather than inline code. The `run_from_branch`
resume path also benefits, gaining hook support and proper cleanup for
free. A new `LifecycleConfig` struct captures setup commands, timeouts,
and devcontainer phases, keeping the engine's API clean. The existing
`run()` method is unchanged, so API server and integration tests
continue working with pre-initialized sandboxes.

The `setup_remote_git` helper is moved from `cli/run.rs` into
`engine.rs` since it only depends on sandbox exec. Config is now passed
by mutable reference to `run_with_lifecycle` so the engine can fill in
remote git fields (base SHA, run branch) that downstream code needs.
Comprehensive tests verify event emission ordering, setup command
execution/failure, and cleanup behavior.

### Fabro Details

<details>
<summary>Ran 10 stages in 53m 27s for $14.42</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $6.22 | 0 |
| simplify_opus | 0s | $2.63 | 0 |
| simplify_gemini | 0s | $2.73 | 0 |
| simplify_gpt | 0s | $2.84 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **53m 27s** | **$14.42** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 22:00:05 -04:00
brynary-fabro[bot]
74dae0a342
Refactor: Collapse pull_request fields on RunConfig into Option<PullRequestConfig> (#21)
This refactoring collapses four flat `pull_request_*` fields on
`RunConfig` (`pull_request_enabled`, `pull_request_draft`,
`pull_request_auto_merge`, `pull_request_merge_strategy`) into a single
`pull_request: Option<PullRequestConfig>` field. This better represents
the natural tree structure of the configuration: `None` means PR
creation is disabled, while `Some(config)` carries all PR settings
directly.

The PR creation logic in `run.rs` is restructured to use `if let
Some(ref pr_cfg) = config.pull_request` instead of checking a boolean
flag, and construction is simplified to filter out disabled configs at
build time via `.filter(|p| p.enabled).cloned()`. All test constructors
across `engine.rs`, `integration.rs`, `daytona_integration.rs`,
`server.rs`, and `manager_loop.rs` are updated from two lines
(`pull_request_enabled: false, pull_request_draft: false/true`) to a
single `pull_request: None`.

### Fabro Details

<details>
<summary>Ran 10 stages in 20m 32s for $6.62</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.71 | 0 |
| simplify_opus | 0s | $1.63 | 0 |
| simplify_gemini | 0s | $1.74 | 0 |
| simplify_gpt | 0s | $1.55 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **20m 32s** | **$6.62** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:55:41 -04:00
brynary-fabro[bot]
469b5d4670
Fix: Stage durations always show 0s in PR descriptions (#20)
This PR fixes a bug where stage durations in PR descriptions always
displayed `0s`. The root cause was a key mismatch in
`extract_stage_durations()`: the function was building its HashMap using
`node_label` (human-readable names like `"Preflight Compile"`), but all
three lookup sites were querying by `node_id` (DOT graph identifiers
like `"preflight_compile"`). Every lookup missed and fell back to the
default value of `0`.

The fix changes the HashMap key from `node_label` to `node_id` so that
it matches what the callers actually use for lookups. Additionally, the
existing test is updated so that `node_label` and `node_id` values
differ from each other (e.g., `"plan"` vs `"Plan"`), ensuring the test
would have caught this mismatch before the fix rather than silently
passing due to both fields being identical.

### Fabro Details

<details>
<summary>Ran 10 stages in 7m 46s for $1.28</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.21 | 0 |
| simplify_opus | 0s | $0.35 | 0 |
| simplify_gemini | 0s | $0.35 | 0 |
| simplify_gpt | 0s | $0.38 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **7m 46s** | **$1.28** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 21:39:01 -04:00
brynary-fabro[bot]
d29c1d817e
Wire up missing hook invocations (#19)
This PR wires up three `HookEvent` variants—`StageRetrying`,
`ParallelStart`, and `ParallelComplete`—that were defined in the enum
and documented but never actually invoked by the engine. `StageRetrying`
hooks now fire at both retry sites in `execute_with_retry` (error-retry
and explicit-Retry-status paths) immediately before the backoff sleep.
`ParallelStart` and `ParallelComplete` hooks fire in the parallel
handler after their corresponding event emissions, using a new
`EngineServices::run_hooks()` convenience method since the handler
doesn't have direct access to the engine's hook method.

The two remaining unwired events, `SandboxReady` and `SandboxCleanup`,
are marked as reserved with doc comments on the enum variants and
annotated in the docs table, since wiring them requires sandbox
lifecycle changes outside the engine's scope. A small
`HookContext::set_node()` helper is introduced to reduce repeated field
assignment across all hook call sites, and existing
`StageStart`/`StageComplete` hook calls are refactored to use it.

### Fabro Details

<details>
<summary>Ran 10 stages in 25m 6s for $5.60</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.04 | 0 |
| simplify_opus | 0s | $1.51 | 0 |
| simplify_gemini | 0s | $1.44 | 0 |
| simplify_gpt | 0s | $1.62 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **25m 6s** | **$5.60** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-16 21:38:53 -04:00
brynary-fabro[bot]
50c032576d
Style SVG output from fabro graph and API (#18)
This PR styles the SVG output from `fabro graph` and the API graph
endpoints to match the polished look of the documentation SVGs. It
introduces two internal functions in `graph.rs`:
`inject_dot_style_defaults`, which inserts DOT-level defaults
(transparent background, teal `#357f9e` node strokes, gray `#666666`
edges, Helvetica font) after the first `{` in any DOT source, and
`postprocess_svg`, which removes the Graphviz-generated white background
polygon and injects a `<style>` block with `prefers-color-scheme: dark`
media queries for automatic dark mode support.

The `render_dot` function is updated to apply both transformations (DOT
defaults before rendering, SVG post-processing after), and is made `pub`
so the API layer can reuse it. Both `get_graph` in `server.rs` and
`get_run_graph` in `demo/mod.rs` are simplified from ~30 lines of inline
Graphviz process management down to a single call to a shared
`render_dot_svg` helper that delegates to `render_dot` on a blocking
thread.

Six new unit tests validate the styling pipeline: default injection with
and without braces, white background removal, dark mode style insertion,
and the existing SVG integration test is extended to assert styled
output. PNG output is unaffected by the SVG post-processing step.

### Fabro Details

<details>
<summary>Ran 10 stages in 23m 31s for $5.48</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $1.15 | 0 |
| simplify_opus | 0s | $1.55 | 0 |
| simplify_gemini | 0s | $1.65 | 0 |
| simplify_gpt | 0s | $1.13 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **23m 31s** | **$5.48** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 21:38:47 -04:00
Bryan Helmkamp
1af5193a82
Handle assistant output replay on stream retries 2026-03-16 15:59:00 -04:00
Bryan Helmkamp
76dc5f72a4
Add turn-level retry for stream-ended-without-Finish errors
When an LLM stream drops mid-response (e.g. under high concurrency with
OpenAI), retry the same turn up to 3 times instead of failing the entire
agent session. Conversation history is preserved across retries.

Previously this killed the whole stage and restarted from scratch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:16:35 -04:00
Bryan Helmkamp
f39b37da64
Add auto-merge support for pull requests via GitHub GraphQL API
When `auto_merge = true` is set in `[pull_request]` config, Fabro enables
GitHub's auto-merge on created PRs using the `enablePullRequestAutoMerge`
GraphQL mutation. Auto-merge implies `draft = false` since GitHub doesn't
allow auto-merge on draft PRs. A `merge_strategy` field (squash/merge/rebase,
default squash) controls the merge method. Failures to enable auto-merge
(e.g. repo doesn't have the setting enabled) warn but don't fail the run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:37:24 -04:00
brynary-fabro[bot]
470fcfe120
Fix: Sub-agent file writes not tracked in API backend (#17)
This PR fixes a bug where files written by sub-agents were missing from
`outcome.files_touched`, causing downstream pipeline nodes like
`simplify_opus` to receive incomplete file lists. The root cause was
that `spawn_event_forwarder` only matched top-level
`ToolCallStarted`/`ToolCallCompleted` events, while sub-agent tool calls
arrived wrapped in `AgentEvent::SubAgentEvent` and fell through to the
`_ => {}` catch-all.

The fix extracts the file-tracking logic into a standalone
`track_file_event` function that recursively unwraps `SubAgentEvent`
layers before matching on the inner tool call events. This handles
arbitrarily nested sub-agent hierarchies (sub-sub-agents, etc.). The
three separate `Arc<Mutex<...>>` fields for pending calls, touched
files, and last file are consolidated into a single `FileTracking`
struct behind one lock, simplifying the forwarder signature and reducing
lock contention.

Four new unit tests verify the behavior: top-level write tracking,
single-level sub-agent unwrapping, double-nested sub-sub-agent
unwrapping, and proper cleanup of pending entries on tool call errors.

### Fabro Details

<details>
<summary>Ran 10 stages in 22m 1s for $4.18</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.91 | 0 |
| simplify_opus | 0s | $0.89 | 0 |
| simplify_gemini | 0s | $1.44 | 0 |
| simplify_gpt | 0s | $0.94 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **22m 1s** | **$4.18** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 23:18:40 -04:00
brynary-fabro[bot]
cb5b7bc3a7
Limit command stdout/stderr to last N lines in preamble (#16)
This PR limits command stdout/stderr output in the preamble to the last
N lines, reducing token waste from verbose build progress and download
noise. Command nodes like `cargo check` or `cargo clippy` can produce
300+ lines of output, but the useful content (errors, summaries) is
almost always at the tail. Compact and summary:medium fidelity levels
now keep the last 25 lines, while summary:high keeps the last 50 lines.
Truncated output displays an `(N lines omitted)` indicator consistent
with the existing omission pattern used for stages.

The implementation adds a `tail_lines` helper that skips leading lines
beyond the limit, prepends an omission notice, and applies the
appropriate indentation to each retained line. This is applied only to
inline stdout/stderr rendering—artifact pointer branches and other
fidelity levels (summary:low, truncate, full) are intentionally
untouched. The PR includes unit tests for the `tail_lines` helper itself
as well as integration tests verifying truncation behavior at compact
and summary:high fidelity, and confirming artifact pointers remain
untruncated.

### Fabro Details

<details>
<summary>Ran 10 stages in 18m 4s for $3.45</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.84 | 0 |
| simplify_opus | 0s | $0.95 | 0 |
| simplify_gemini | 0s | $0.81 | 0 |
| simplify_gpt | 0s | $0.85 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **18m 4s** | **$3.45** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 23:18:13 -04:00
brynary-fabro[bot]
2972e38bed
Add --direction flag to fabro graph (#15)
This PR adds a `--direction` (`-d`) flag to the `fabro graph` CLI
command, bringing it to parity with the web UI's LR/TB toggle buttons.
Users can now pass `--direction lr` or `--direction tb` to override the
`rankdir` declared in the DOT source before it's handed off to the `dot`
renderer.

The implementation introduces a `GraphDirection` enum (with `ValueEnum`
and `Display` derives for clap integration), an optional `--direction`
field on `GraphArgs`, and an `apply_direction` helper that uses a
lazy-compiled regex to rewrite `rankdir=…` in the DOT source—mirroring
the same approach used by the web UI. The CLI docs are updated with the
new flag, and two unit tests verify that the rewrite works correctly and
that omitting the flag leaves the source unchanged.

### Fabro Details

<details>
<summary>Ran 10 stages in 17m 6s for $3.60</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $0.68 | 0 |
| simplify_opus | 0s | $1.02 | 0 |
| simplify_gemini | 0s | $0.83 | 0 |
| simplify_gpt | 0s | $1.06 | 0 |
| verify | 0s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **17m 6s** | **$3.60** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (13 nodes and 16
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gemini   [label="Simplify (Gemini)", prompt="@prompts/simplify.md", model="gemini-3.1-pro-preview-customtools"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", goal_gate=true, max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gemini -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 23:18:05 -04:00
Bryan Helmkamp
b101ad78a2
Enable pull_request by default in fabro init
The generated fabro.toml now includes an uncommented [pull_request]
section with enabled=true and draft=true, so new projects auto-create
draft PRs on successful workflow runs out of the box.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:53:57 -04:00
Bryan Helmkamp
420c0874a0
Enable auto PR creation and add debug logging for skipped PR cases
The `[pull_request]` config in fabro.toml was missing `enabled = true`,
so workflow runs silently skipped PR creation. Additionally, four skip
paths in the PR creation logic had no logging at all, making it hard to
diagnose why a PR wasn't opened. Added debug-level logs for: config not
enabled, dry-run mode, engine error, and non-success run status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:51:52 -04:00
Bryan Helmkamp
b604bc4b60
Designate retros as experimental, disable by default via [features] flag
Move retro control from [fabro] retro to [features] retros in project
config. Default changes from true to false — retros are now opt-in.
Add retros field to server config Features struct, OpenAPI spec,
TypeScript client, and web app config. Update docs with experimental
warning and new enablement instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:36:32 -04:00
Bryan Helmkamp
bd195175fd
Docs updates: reorder nav, rename DOT Language, simplify dark factory, add fork/upgrade/asset docs
- Move Comparison link below Troubleshooting in nav
- Rename "DOT Language" page to "Fabro Language"
- Remove five-tier table from dark factory page, keep link to Dan Shapiro's post
- Add fork command docs and checkpoints section
- Add upgrade, asset list, asset cp command docs
- Add upgrade_check config reference
- Add retros feature flag to server config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:13:23 -04:00
brynary-fabro[bot]
da3610af59
Unified dry-run via Handler::simulate() (#14)
This PR introduces a unified dry-run mechanism by adding a
`Handler::simulate()` trait method and a `dispatch_handler()` routing
function that selects between `simulate()` and `execute()` based on
`services.dry_run`. Previously, dry-run behavior was scattered
inconsistently across handlers—`CommandHandler` checked
`services.dry_run` inline, `AgentHandler`/`PromptHandler`/`FanInHandler`
relied on the backend being `None`, and `WaitHandler`/`HumanHandler` had
no dry-run support at all (sleeping or blocking on input for real). This
made dry-run behavior fragile and difficult to extend to new handlers.

The new design adds an `Outcome::simulated(node_id)` factory for
standardized dry-run results, a default `simulate()` implementation on
the `Handler` trait that returns a generic simulated success, and
per-handler overrides where custom context updates are needed.
`CommandHandler` populates empty output/stderr, `AgentHandler` and
`PromptHandler` set simulated
`last_stage`/`last_response`/`response.{id}` context keys,
`FanInHandler` calls `heuristic_select()` without an LLM, `HumanHandler`
auto-selects the first choice, and `ParallelHandler` dispatches child
branches through `dispatch_handler()` while skipping git worktree
operations. The inline `dry_run` check in `CommandHandler::execute()` is
removed, and both call sites in the engine (`execute_with_retry` and
parallel branch dispatch) now route through `dispatch_handler()`.

All existing dry-run tests are updated to test `simulate()` directly,
and new tests verify that `dispatch_handler()` correctly routes based on
the `dry_run` flag and that each handler's `simulate()` produces the
expected context updates and outcome structure.

### Fabro Details

<details>
<summary>Ran 7 stages in 24m 57s for $4.72</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $3.29 | 0 |
| simplify | 0s | $1.42 | 0 |
| verify | 0s | – | 0 |
| **Total** | **24m 57s** | **$4.72** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (10 nodes and 13
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan."]
    simplify          [label="Simplify", prompt="@prompts/simplify.md"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify -> verify
    verify -> exit  [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Fabro Assistant <assistant@fabro.dev>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:55:03 -04:00
brynary-fabro[bot]
7ce7a842cc
fabro upgrade command (#13)
This PR adds a `fabro upgrade` command that downloads and installs new
releases from GitHub, along with a passive daily auto-check that
notifies users when a newer version is available. The upgrade flow
supports two download backends: the `gh` CLI (preferred, for auth and
rate-limit benefits) with an automatic fallback to plain HTTPS via
`reqwest` when `gh` is missing or not authenticated. The command
includes SHA256 checksum verification, atomic binary replacement with
rollback on failure, downgrade protection with interactive confirmation,
and `--dry-run`/`--force` flags.

A background upgrade check runs automatically on common commands (`run`,
`exec`, `init`, `install`), caching results in
`~/.fabro/last_upgrade_check.json` to avoid hitting GitHub more than
once per 24 hours. Users can disable this via `upgrade_check = false` in
`~/.fabro/cli.toml` or the `--no-upgrade-check` global flag. The check
is spawned as an async task and its notice prints to stderr after the
main command completes, ensuring it never blocks or breaks normal
operation—all errors are silently swallowed.

The implementation follows a test-first approach with unit tests
covering platform detection, version parsing, SHA256 verification,
upgrade check state serialization/staleness, and the new `upgrade_check`
config field. Dependencies `tempfile` (promoted from dev-dependencies)
and `sha2` are added to `fabro-cli`.

### Fabro Details

<details>
<summary>Ran 7 stages in 18m 39s for $5.61</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.92 | 0 |
| simplify | 0s | $2.68 | 0 |
| verify | 0s | – | 0 |
| **Total** | **18m 39s** | **$5.61** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (10 nodes and 13
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan."]
    simplify          [label="Simplify", prompt="@prompts/simplify.md"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify -> verify
    verify -> exit  [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-15 19:54:57 -04:00