Commit graph

363 commits

Author SHA1 Message Date
Bryan Helmkamp
033a5b0675 Consolidate backoff/jitter into fabro-util::BackoffPolicy
Three crates independently implemented the same exponential-backoff-with-jitter
logic. Extract a single BackoffPolicy into fabro-util and have fabro-core,
fabro-workflows, and fabro-llm all use it, eliminating duplication and making
the backoff conversion in core_adapter trivial.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
dbca4d1685 Wire fabro-workflows to use fabro-core executor engine
Fix fabro-core semantics to match fabro-workflows (checkpoint after edge
selection, terminal callback with goal-gate result, loop restart uses edge
target with fresh context, retry-target routing for failed nodes, visit
limit >= semantics, stall token with CancellationToken, backoff jitter).

Add core_adapter module bridging fabro-workflows types to fabro-core traits:
WorkflowGraph/Node/Edge newtypes, bidirectional outcome conversion, context
bridge sharing values/logs via ContextStore, WorkflowNodeHandler with
panic/timeout protection, and full WorkflowLifecycle implementing all 8
RunLifecycle callbacks (events, hooks, fidelity, circuit breaker, checkpoints).

Add run_via_core method behind core-engine feature flag that builds and runs
the fabro-core Executor with the full adapter suite. The existing run_internal
path remains the default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
d0e840841f Add boolean helper methods to FabroConfig and remove unnecessary clones
Add verbose_enabled(), prevent_idle_sleep_enabled(), and
upgrade_check_enabled() helpers to FabroConfig to encapsulate default
values. Update all call sites in fabro-cli to use the new helpers.
Also eliminate an unnecessary clone in SubAgentManager::run_to_completion
and use extend() instead of append()+clone() in config merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
8f2933ac9f Add fabro-core crate: generic workflow execution engine
Standalone crate with no knowledge of git, fidelity, LLMs, hooks,
sandboxes, artifacts, or disk I/O. Provides a ~55-line executor loop
with pluggable NodeHandler, Graph, and RunLifecycle traits.

Key types: CoreError, StageStatus, Outcome, Context (pluggable store),
RetryPolicy, RunState, Executor/ExecutorBuilder, StallWatchdog.

103 tests covering all milestones: foundation types, graph/handler
traits, lifecycle callbacks, executor (linear paths, terminal nodes,
goal gates, visit limits, edge selection, jumps, loop restarts,
cancellation, retry with backoff), and stall detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
22ef5ec916 Fix stale comments referencing deleted config type names
Update references to WorkflowRunConfig, ServerConfig, apply_defaults,
and deny_unknown_fields in comments and docs to reflect the FabroConfig
unification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
25a9bb9117 Unify CliConfig, ServerConfig, ProjectConfig, WorkflowRunConfig, RunDefaults into FabroConfig
Replace five config types with a single FabroConfig superset type. All
loading functions (load_cli_config, load_server_config, load_run_config,
parse_project_config) now return FabroConfig. This eliminates the
run_defaults indirection, into_run_defaults() conversion, and
apply_defaults() bridging method in favor of a single merge_overlay()
that works across all config layers (CLI → project → workflow).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
58471360a7 Merge status + cached_result into SubAgentStatus enum with data
Collapse the coupled `status` and `cached_result` fields into a single
`SubAgentStatus` enum where `Finished` carries the result, eliminating
impossible states (e.g. Completed with no cached result).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:19:03 -04:00
Bryan Helmkamp
67db378bd0 Remove dead selected_options field from Answer struct
The field was populated in constructors but never read by any code.
Selected keys are already carried by AnswerValue::MultiSelected(Vec<String>),
making this field redundant. Also removes the unused options parameter from
Answer::multi_selected().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:07:26 -04:00
Bryan Helmkamp
1b04653f32 Add SubAgentStatus enum for explicit subagent lifecycle tracking
Retain agents in the HashMap after wait/close instead of removing them,
enabling cached result retrieval, status queries, and disambiguated error
messages (never spawned vs completed vs closed vs failed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:50:43 -04:00
Bryan Helmkamp
b461ad8d9a Add #[source] error chaining to SdkError and tool_type field to ToolCall
Preserve original error chains (reqwest, serde_json, etc.) in SdkError
via Arc<dyn Error>-backed #[source] fields on Network, RequestTimeout,
Stream, and Configuration variants. This makes production debugging of
network/TLS/DNS issues easier since error reporters can now walk the
full chain. Serde-compatible via #[serde(skip)] — message string still
carries the text for serialized forms.

Also add a `type` field to ToolCall (defaulting to "function") so
non-function tool types from providers won't be silently mishandled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:38:00 -04:00
Bryan Helmkamp
4a6b6fba08 Support "end" and "End" as exit node identifiers in graph lookup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:17:22 -04:00
Bryan Helmkamp
40ec6919d7 Attractor spec hunks 22-23: add parse_literal and quoted string support to condition parser
Add parse_literal() that strips surrounding double-quotes from condition
literal values, so `outcome="success"` and `outcome=success` behave
identically. Update the tokenizer to handle "..." as single tokens
(including spaces and escaped characters). Add BareLiteral to the
grammar comment per spec Section 10.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 15:16:47 -04:00
Bryan Helmkamp
588515dbf6 Attractor spec hunks 14 & 16: remove error_policy and k_of_n/quorum from parallel handler
Remove ErrorPolicy enum (continue/fail_fast/ignore) and the k_of_n/quorum
join policies from the parallel handler, leaving only wait_all and
first_success. This deletes ~180 lines of conditional logic including
FailFast early termination, the ParallelEarlyTermination event, and all
related tests and documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:59:35 -04:00
Bryan Helmkamp
b9fe1282d3 Attractor spec hunks 9-11: edge selection fallback and default_max_retries rename
- Remove any-edge fallback from select_edge() in deterministic mode; random
  mode retains it as an enhancement over the base spec
- Restrict preferred_label and suggested_next_ids matching to unconditional
  edges only (already applied in prior work, tests added here)
- Rename default_max_retry → default_max_retries across codebase (code, docs,
  fixtures, skills) and change default from 3 to 0
- Update transitions.mdx to document edge selection cascade accurately

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:20:24 -04:00
Bryan Helmkamp
c7a6467640 Replace ContextWindowWarning with generic Warning event
Introduce a reusable Warning { kind, message, details } variant in
AgentEvent so non-fatal warnings (context window usage, deprecation,
etc.) share a single event shape. The context_window warning preserves
all original fields inside the JSON details object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:48:26 -04:00
Bryan Helmkamp
415f14a281 Filter ProcessingEnd from subagent and workflow event forwarding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:32:31 -04:00
Bryan Helmkamp
6070eaa733 Add ProcessingEnd event and model state transitions explicitly
Introduces Session::transition() to validate and emit events on state
changes. Processing→Idle now emits ProcessingEnd (matching the spec's
PROCESSING_END). All bare state assignments go through transition().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:32:26 -04:00
Bryan Helmkamp
538560f852 Move subagent_manager from setter to Session::new() constructor parameter
Replaces set_subagent_manager() with an Option parameter on the
constructor so the dependency is explicit at creation time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:23:23 -04:00
Bryan Helmkamp
d34e8b0291 Wire SubAgentManager into Session at CLI and workflow call sites
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:18:31 -04:00
Bryan Helmkamp
c843599c8e Clean up subagents before emitting SessionEnded in Session.close()
Matches spec shutdown order: cleanup subagents → emit SESSION_END →
transition to CLOSED. Session now holds an optional SubAgentManager
reference and calls close_all() during shutdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:18:28 -04:00
Bryan Helmkamp
2e51ae664a Add SubAgentManager::close_all() to shut down all active subagents
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:18:22 -04:00
Bryan Helmkamp
50ef927398 Map all 5xx status codes to ServerError
Expands the HTTP status code mapping from 500-504 to 500-599 so that
uncommon 5xx codes (505, 507, etc.) are correctly classified as
retryable ServerError instead of falling through to message-based
heuristics. The existing 529 (Overloaded) handling is subsumed by
the broader range.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:10:24 -04:00
Bryan Helmkamp
476f1b9d85 Make RequestTimeout non-retryable by default
Aligns with spec update: 408 timeout errors are now non-retryable
by default. Applications can opt in to timeout retries via custom
retry logic. RequestTimeout remains failover-eligible since a
different provider may not share the same timeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:56:22 -04:00
Bryan Helmkamp
d6fd386d16 Add ReasoningEffort enum to replace raw Option<String>
Introduces a typed ReasoningEffort enum (Low, Medium, High) with
serde, Display, and FromStr support. Updates Request, GenerateParams,
and SessionConfig to use Option<ReasoningEffort> instead of
Option<String>. Aligns with spec change removing "none" as a valid
reasoning_effort value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:50:42 -04:00
Bryan Helmkamp
2fe8e5376d Fix stale AgentProfile trait signature in README
The capabilities() method was removed from the trait but the README
still listed it with an incorrect return type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:42:09 -04:00
Bryan Helmkamp
603f56072d Default max_tool_rounds_per_input and subagent max_turns to unlimited
Align with attractor spec update: both limits now default to 0
(unlimited) instead of 200 and 50 respectively. The
max_tool_rounds_per_input loop check now guards on > 0 so that 0
means no limit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:40:09 -04:00
Bryan Helmkamp
b336e4995e Remove ProfileCapabilities, rename ProviderProfile to AgentProfile, fix layer separation
Move model facts (knowledge_cutoff, context_window) to fabro-model catalog as
source of truth. Move request-shaping (auto-thinking, 1M beta headers, Gemini
safety settings) into fabro-llm adapters. Delete ProfileCapabilities struct and
all dead code (supports_reasoning, supports_streaming, supports_parallel_tool_calls,
OpenAiProfile.reasoning_effort). Fix "powered by OpenAI" mislabeling for
Kimi/ZAI/Minimax/Inception providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 12:38:05 -04:00
Bryan Helmkamp
95fffac267 Flatten LanguageModel trait + ModelInfo into struct Model
Delete the single-implementor LanguageModel trait and merge its methods
into inherent impl on a renamed Model struct. Change provider field from
String to Provider enum, eliminating constant string↔enum conversions
across the codebase. Fix Provider serde attributes so OpenAi serializes
as "openai" (not "open_ai") to match catalog.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:48:55 -04:00
Bryan Helmkamp
4711a9eb48 Redesign fabro-model with LanguageModel trait, Catalog struct, and ModelRef (#121)
Introduce OOP API for the model catalog: LanguageModel trait with blanket
impl on ModelInfo, Catalog struct with typed methods (get, list,
default_for_provider, closest, build_fallback_chain, etc.), ModelRef enum
replacing ModelId, and Provider::OpenAiCompatible variant. Migrate all
callers across the workspace to use Catalog::builtin() and remove the old
free-function API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:15:40 -04:00
Bryan Helmkamp
1e39df55e9 Fix workflow slug lookup for split and resumed runs 2026-03-23 10:23:11 -04:00
Bryan Helmkamp
fea7380c16 Preserve split run metadata across restarts 2026-03-23 10:04:49 -04:00
Bryan Helmkamp
587e5488bd Fix split run lifecycle lookup 2026-03-23 08:44:46 -04:00
Bryan Helmkamp
6fa94ee36c Fix test timeouts caused by reqwest system proxy detection on macOS
reqwest::Client::new() queries macOS SCDynamicStore for system proxy
settings, which calls CFBundleGetMainBundle() → readdir() on
target/debug/deps/. With 576K stale build artifacts accumulated in
that directory, each readdir() took 1.3s, causing 15s+ delays when
multiple test processes ran concurrently.

- Disable jsonschema default features to remove unnecessary reqwest@0.13
  and rustls-platform-verifier dependencies
- Make reqwest::Client lazy in web_search tool (OnceLock) to avoid
  constructing it during profile tests
- Mark validate_api_key_rejects_invalid_key as #[ignore] since it hits
  the live Anthropic API (3.2s per invocation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:13:50 -04:00
Bryan Helmkamp
b59c62b33e Unify fabro run foreground to use create + start + attach (#141)
## Summary

- **Unify foreground and detach code paths**: Both `fabro run` modes now
go through the same `create_run() + start_run()` pipeline, with
foreground adding `attach_run()`. Only `--preflight` remains as a
special case.
- **Fix three bugs in create→start→attach path**: (1) `_run_engine`
crashed for `.fabro` workflows by hardcoding `run.toml` — now falls back
to `graph.fabro`; (2) `attach_run` couldn't detect crashed engines due
to zombie processes — `start_run` now returns the `Child` handle; (3)
`create_run` ignored `--run-id`.
- **Configure nextest slow-timeout profiles**: Tighten unit test timeout
to 2s slow / 4s kill, add `e2e` profile with 10s/30s. Switch CI and docs
to `cargo nextest run`.

## Test plan

- [ ] `cargo nextest run --workspace` passes with new timeout profiles
- [ ] `fabro run <workflow>` works in foreground mode (create + start +
attach)
- [ ] `fabro run --detach <workflow>` prints run ID and exits
- [ ] `fabro attach <run>` works standalone (without child handle)
- [ ] `fabro resume <run>` works for both `.toml` and `.fabro` workflows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:48:09 -04:00
brynary-fabro[bot]
165e2495bf Extract fabro resume subcommand (#137)
This PR extracts the `fabro resume` subcommand from `fabro run`,
replacing the `--resume` and `--run-branch` flags with a dedicated, more
ergonomic interface. Users can now run `fabro resume <RUN_ID>` instead
of constructing `fabro run --run-branch fabro/run/<RUN_ID>` manually,
and the command also accepts run ID prefixes (matching the pattern
established by `fabro rewind` and `fabro fork`). Checkpoint-file-based
resumption is also supported via `fabro resume --checkpoint
path/to/checkpoint.json --workflow workflow.fabro`.

The implementation moves the ~315-line `run_from_branch()` function out
of `run.rs` and into a new `commands/resume.rs` module, splitting it
into two preparation paths (`prepare_from_checkpoint` and
`prepare_from_branch`) that converge on a shared `run_resumed()` tail.
Several previously private helpers in `run.rs` are widened to
`pub(crate)` to allow sharing: `local_sandbox_with_callback`,
`resolve_ssh_config`, `resolve_ssh_clone_params`,
`resolve_preserve_sandbox`, `generate_retro`, `write_finalize_commit`,
`print_final_output`, `print_assets`, and the new `default_run_dir`
helper extracted from duplicated inline logic. The `RunArgs` struct
loses its `resume` and `run_branch` fields along with their
`conflicts_with` annotations, and `RunSpec` drops the corresponding
fields with `#[serde(default)]` for backward compatibility.

Documentation across `docs/reference/cli.mdx`,
`docs/execution/checkpoints.mdx`, and
`docs/core-concepts/how-fabro-works.mdx` is updated to reflect the new
interface, and the `rewind`/`fork` commands now hint `fabro resume
<short-prefix>` instead of the full branch name.

### Fabro Details

<details>
<summary>Ran 9 stages in 30m 25s for $6.70</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 10s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 18m 30s | $3.95 | 0 |
| simplify_opus | 9m 38s | $2.75 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 19s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **30m 25s** | **$6.70** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { 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_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", 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_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-22 13:45:06 -04:00
brynary-fabro[bot]
cd7f0b1415 Support Anthropic fast mode (speed: fast) (#127)
This PR adds support for Anthropic's fast mode beta feature (`speed:
fast`), which enables up to 2.5x faster output for Claude Opus 4.6 at a
6x pricing premium. The implementation follows the same patterns
established by `reasoning_effort`, threading a new `speed:
Option<String>` field through the request/response pipeline from graph
stylesheet properties down through agent configuration, session
management, and the Anthropic provider adapter.

On the provider side, when `speed: "fast"` is set, the `ApiRequest`
struct now includes the `speed` field in the serialized JSON body, and
the `build_beta_header` function injects the required `anthropic-beta:
fast-mode-2026-02-01` header alongside any existing beta headers (cache,
interleaved thinking, etc.) without duplication. The response's
`usage.speed` field is parsed and propagated back through both streaming
and non-streaming paths into `StageUsage` and `Usage` types for
tracking.

Cost accounting applies a 6x multiplier in `compute_stage_cost` when
`speed == "fast"`, reflecting Anthropic's actual pricing differential.
The feature is configurable via stylesheet (`* { speed: fast; }`), which
gets wired through `SessionConfig` and prompt-mode `Request`
construction in the backend layer. New tests cover the API request
serialization, beta header injection, combined cache+fast-mode headers,
and the cost multiplier, while all existing test fixtures have been
updated with `speed: None` to maintain struct exhaustiveness.

### Fabro Details

<details>
<summary>Ran 9 stages in 58m 27s for $6.56</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 20s | – | 0 |
| preflight_lint | 14s | – | 0 |
| implement | 47m 31s | $5.36 | 0 |
| simplify_opus | 7m 59s | $1.20 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 25s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **58m 27s** | **$6.56** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (12 nodes and 15
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_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", 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_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-21 14:37:42 -04:00
Bryan Helmkamp
844e00b72c Fix three bugs in run.toml snapshot handling from PR #138 review
- Fix false positive in backward-compat fallback: use path.exists() instead
  of error chain inspection to distinguish missing run.toml from one with
  a broken internal reference (e.g. missing Dockerfile)
- Skip write_run_config_snapshot in _run_engine path to prevent double
  apply_defaults corrupting the snapshot on each restart
- Resolve ${env.VARNAME} refs in run_defaults.sandbox.env when falling
  back for bare .fabro workflows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:32:09 -04:00
brynary-fabro[bot]
4da1f59695 Fix: Workflow TOML config lost in detach mode (#138)
This PR fixes a bug where workflow TOML configuration (including
`[pull_request]` settings) was silently dropped when running in detached
mode (`fabro run -d`). The root cause was a three-part failure:
`create.rs` checked the raw CLI argument string for a `.toml` extension
instead of the resolved path, so `run.toml` was never written to the run
directory; `RunEngine` always fell back to `graph.fabro` (a DOT file),
causing `prepare_workflow` to return `run_cfg = None` and lose all
TOML-level configuration; and `pull_request`/`asset_globs` fields in
`RunConfig` had no fallback to `run_defaults` when `run_cfg` was absent.

The fix replaces the naive file-copy approach with a proper
serialization pipeline. Rather than copying the raw TOML (which would
contain a `graph` field pointing to a nonexistent file in the run
directory), `create.rs` now calls `write_run_config_snapshot`, which
serializes the already-merged `WorkflowRunConfig` and rewrites the
`graph` field to `"graph.fabro"` — the canonical cached name. This makes
the run directory fully self-contained with all defaults merged,
environment variables resolved, and the graph path correct. `RunEngine`
in `main.rs` now unconditionally points at `run.toml`; a new
`resolve_workflow_source` helper handles the `.toml` path by loading the
config and resolving the graph path, with a backward-compatible fallback
to `graph.fabro` for older detached runs created before this change.

As defense-in-depth, fallbacks to `run_defaults` are added throughout
`run.rs` for `pull_request`, `asset_globs`, `devcontainer`, and
`sandbox.env` — ensuring bare `.fabro` files passed directly still pick
up project-level defaults. Two new unit tests verify the serialization
round-trip (confirming `graph` is rewritten and `pull_request` config is
preserved) and the missing-`run.toml` fallback behavior.

### Fabro Details

<details>
<summary>Ran 9 stages in 26m 29s for $9.17</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 14s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 4m 54s | $0.71 | 0 |
| simplify_opus | 8m 41s | $1.77 | 0 |
| simplify_gpt | 10m 41s | $6.69 | 0 |
| verify | 18s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **26m 29s** | **$9.17** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { 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_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", 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_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-21 14:15:13 -04:00
Bryan Helmkamp
306f5b4b6c Update model catalog snapshots with new effort field
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:11:09 -04:00
Bryan Helmkamp
f307c1fbdf Fix OpenAI reasoning round-trip: use store: false with encrypted_content
The OpenAI Responses API requires store: false for non-Azure endpoints.
Reasoning items round-trip correctly by requesting encrypted_content
via the `include` field, which embeds them in the response payload
rather than relying on server-side storage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:46:01 -04:00
Bryan Helmkamp
025316500d Fix Sonnet 4.5 effort parameter error by converting to thinking API
claude-sonnet-4-5 doesn't support output_config.effort — it needs the
older thinking API with budget_tokens. Add an `effort` feature flag to
ModelFeatures and have the Anthropic adapter auto-convert reasoning_effort
to a thinking config for models that lack effort support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:44:15 -04:00
Bryan Helmkamp
35cac9185b Fix OpenAI provider: always set store: true
store: !codex_mode was sending store: false for non-Codex models,
which prevented reasoning items from being persisted. This broke
multi-turn conversations where reasoning items from turn 1 need to
be sent back in turn 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:37:30 -04:00
Bryan Helmkamp
d29fd66ac9 Run model tests concurrently with progress bar
Tests 6 models at a time in shuffled order to spread load across
providers. Uses indicatif progress bar instead of per-model eprint
lines. Results table is sorted back to original catalog order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:36:26 -04:00
Bryan Helmkamp
1493c848b5 Run only the deep test when --deep is passed, not both
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:28:23 -04:00
Bryan Helmkamp
73a7c28bd2 Add --deep flag to fabro model test for multi-turn tool-use validation
Exercises a 2+ turn tool-call round-trip with reasoning_effort("high")
to catch bugs like store: false that only manifest when reasoning items
from turn 1 are sent back in turn 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:27:25 -04:00
Bryan Helmkamp
a915543fc0 Suppress stderr output in detach and create modes
`prepare_workflow` unconditionally printed Workflow/Graph/Goal info to
stderr, which leaked into `--detach` and `create` output that should
only emit the run ID. Add a `quiet` flag to suppress this output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:26:20 -04:00
Bryan Helmkamp
f4e9503df1 Fix edge thread_id precedence to match fidelity behavior
Edge thread_id should override node thread_id, consistent with how
resolve_fidelity already works. The previous order was reversed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 09:10:52 -04:00
brynary-fabro[bot]
c119bf3c79 Use short hex IDs for subagents instead of UUIDs (#128)
This PR updates subagent ID generation to use short 8-character hex
strings instead of full UUID v4 strings. Previously, subagent IDs were
36-character UUIDs (e.g. `550e8400-e29b-41d4-a716-446655440000`), which
were verbose in CLI output and unwieldy when the LLM needed to reference
them in tools like `send_input`, `wait`, and `close_agent`. The new
format generates IDs like `a3f1b20c` — compact, human-readable, and with
~4 billion possible values, effectively collision-free within a session.

The change is made at the source in `subagent.rs`, where UUID generation
is replaced with `format!("{:08x}",
uuid::Uuid::new_v4().as_fields().0)`. Because IDs are now inherently 8
characters, the display-layer truncations in `cli.rs` (5 occurrences)
and `run_progress.rs` (2 occurrences) are redundant and have been
removed — `agent_id` is used directly in format strings instead of a
`short_id` slice.

### Plan Summary

- **Replace UUID generation** in `subagent.rs`: use the first field of a
UUID v4 formatted as 8-char lowercase hex, yielding IDs like `a3f1b20c`
instead of full 36-char UUIDs
- **Remove `short_id` truncation** in `cli.rs` (5 places) and
`run_progress.rs` (2 places): since IDs are now already 8 chars, the
`let short_id = &agent_id[..8.min(agent_id.len())]` pattern is
eliminated and `{agent_id}` is used directly in all format strings
- No test changes required — existing tests use hardcoded IDs like
`"sa-1"` and don't assert on ID length or format

<details>
<summary>Full plan</summary>

````md
The plan has been written to `/home/daytona/workspace/plan.md`.

It covers:
- **4 files to modify**: `fabro-agent/Cargo.toml` (add `rand` dep), `subagent.rs` (replace UUID with 8-char hex), `cli.rs` (remove 5 `short_id` truncations), `run_progress.rs` (remove 2 `short_id` truncations)
- **Step-by-step implementation** with exact line references and before/after code
- **Verification commands** to confirm correctness
- **Test case analysis** explaining why no test changes are needed
````

</details>

### Fabro Details

<details>
<summary>Ran 3 stages in 18m 46s for $0.57</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| plan | 1m 28s | $0.57 | 0 |
| implement | 17m 6s | – | 0 |
| **Total** | **18m 46s** | **$0.57** | **0** |

</details>

<details>
<summary>Ran <code>GhImplement.fabro</code> (4 nodes and 3
edges)</summary>

```dot
digraph GhImplement {
    graph [
        goal="Implement a GitHub issue",
        model_stylesheet="
            * { model: claude-opus-4-6; }
        "
    ]
    rankdir=LR

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

    plan [label="Plan", prompt="Fetch the GitHub issue from the goal using: gh issue view $goal --json title,body,labels,comments\n\nRead the issue title, description, and any comments carefully. Analyze what code changes are needed to resolve the issue.\n\nWrite a detailed implementation plan to plan.md that includes:\n- Summary of the issue\n- Files to create or modify\n- Step-by-step implementation approach\n- Test cases to add or update\n\nThe plan should be specific enough for another agent to implement without seeing the original issue.\n\nRespond with the location of the plan file (plan.md)."]

    implement [label="Implement", shape=house, stack.child_workflow="fabro/workflows/implement/workflow.fabro", manager.max_cycles=100]

    start -> plan
    plan -> implement [fidelity="summary:high"]
    implement -> exit
}

```

</details>

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

---------

Co-authored-by: Fabro <noreply@fabro.sh>
2026-03-20 21:38:16 -04:00
Bryan Helmkamp
bcba32329a Destructure ModelInfo to avoid unnecessary clone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:58:59 -04:00
Bryan Helmkamp
07c3669d1c Resolve model aliases to canonical IDs in workflow graph nodes
ProviderInferenceTransform only inferred the provider but passed the
raw alias (e.g. "gpt-54") to the LLM API, causing request failures.
Rename to ModelResolutionTransform and resolve aliases via the model
catalog so the canonical ID (e.g. "gpt-5.4") is used in API calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:58:59 -04:00