Commit graph

83 commits

Author SHA1 Message Date
Bryan Helmkamp
056ce8a073
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
2a7aea5950
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
c7703fc9a0
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
d9b00ec8bc
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
f5d363237a
Fix workflow slug lookup for split and resumed runs 2026-03-23 10:23:11 -04:00
Bryan Helmkamp
b5c014443e
Preserve split run metadata across restarts 2026-03-23 10:04:49 -04:00
Bryan Helmkamp
6f0023698b
Fix split run lifecycle lookup 2026-03-23 08:44:46 -04:00
Bryan Helmkamp
1a2a04b96b
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
3db2c237e2
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]
ac355d58c7
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]
a0fbbcfe3b
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
75a8063482
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]
9e676796ea
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
5ac60c7134
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
brynary-fabro[bot]
0e86505dc9
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
003359b079
Unify goal prefix stripping into shared strip_goal_decoration() in fabro-util
Three places stripped markdown headings and `Plan:` prefixes from goals
with slightly different logic. Extract a shared function so all call sites
behave consistently, and fix `fabro run` which wasn't stripping at all.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:58:59 -04:00
Bryan Helmkamp
f8151c8f89
Show tool details for remaining agent tools in fabro logs and fabro attach
Add detail text for wait, send_input, close_agent (agent_id),
apply_patch (ellipsis), and read_many_files (file count) — these
were falling through to the `_ => None` catch-all in both
`tool_detail()` and `tool_display_name()`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:58:59 -04:00
Bryan Helmkamp
c51c9ebb17
Show tool details for spawn_agent, use_skill, list_dir in fabro logs
Sync `tool_detail()` in logs.rs with `tool_display_name()` in
run_progress.rs — the two had drifted, so `fabro logs -pf` was
missing detail text for these tool types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:58:59 -04:00
brynary-fabro[bot]
956e681e4f
Unified WorktreeSandbox (#117)
This PR introduces a unified `WorktreeSandbox` type in `fabro-sandbox`
that consolidates previously duplicated git worktree management logic
spread across `parallel.rs` and `run.rs`. The new type wraps any
`Arc<dyn Sandbox>`, handles the full worktree lifecycle (branch
creation, `worktree add`, and cleanup) in its `initialize()`/`cleanup()`
methods, overrides `working_directory()` and `exec_command()` to default
to the worktree path, and delegates all other `Sandbox` methods to the
inner sandbox. A `WorktreeConfig` struct controls behavior (branch name,
base SHA, worktree path, and a `skip_branch_creation` flag for resume
flows), and a `WorktreeEventCallback` mechanism bridges lifecycle events
to the workflow event system via a new
`EventEmitter::worktree_callback()` helper.

The old private `WorktreeSandbox` struct in `parallel.rs` (which only
redirected `exec_command` working dirs with no lifecycle awareness) is
removed and replaced with the shared implementation. The
`setup_worktree()` function in `run.rs` is also removed; its logic is
absorbed directly into the `SandboxProvider::Local` branch of sandbox
construction, where `WorktreeSandbox::initialize()` is called and
`std::env::set_current_dir()` follows on success. The resume path
(`run_from_branch`) similarly replaces direct `git::replace_worktree`
calls with `WorktreeSandbox` using `skip_branch_creation: true`. The
`MockSandbox` in `test_support.rs` gains `captured_commands` and
`captured_working_dirs` vectors to support sequenced-command assertions
in the new unit tests.

The `MockSandbox` enhancement is a notable improvement for testability
beyond this specific change—having the full ordered sequence of commands
rather than just the last one makes it straightforward to assert on
multi-step git workflows. One subtle behavior worth noting is that in
`parallel.rs` the `git reset --hard` step previously present after
worktree creation is now absent from `WorktreeSandbox::initialize()`;
the plan mentioned it but the implementation deliberately omits it (the
branch is already force-set to the target SHA, so the reset was
redundant for the parallel case). Cleanup for parallel branches
continues to go through `engine::git_remove_worktree` on the parent
sandbox rather than calling `wt_sandbox.cleanup()`, since the sandbox
`Arc` is consumed by the spawned task—this is a reasonable tradeoff
noted in the plan.

### Fabro Details

<details>
<summary>Ran 11 stages in 62m 32s for $3.67</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 30m 15s | $2.15 | 0 |
| simplify_opus | 15m 5s | $0.71 | 0 |
| simplify_gpt | 11m 8s | $0.54 | 0 |
| verify | 46s | – | 0 |
| fixup | 3m 17s | $0.27 | 0 |
| verify | 46s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **62m 32s** | **$3.67** | **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>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:35:04 -04:00
brynary-fabro[bot]
3680a57a25
Decompose fabro run into create / start / attach (#116)
This PR decomposes `fabro run` into three composable primitives —
`create`, `start`, and `attach` — following the Docker-style lifecycle
model. Previously, `fabro run` performed everything in a single
monolithic function, and `--detach` was implemented by reconstructing
CLI argv to spawn a child process, which was brittle and hard to extend.
The new architecture cleanly separates concerns: `fabro create`
allocates the run directory and persists a `RunSpec` struct to
`spec.json`; `fabro start` spawns a detached `_run_engine` process (a
hidden internal command that reads `spec.json`) via `setsid`; and `fabro
attach` tails `progress.jsonl` with live rendering and handles
file-based interview IPC. `fabro run` is now a composition of these
three primitives, and `fabro run --detach` simply skips the attach step.

The main rendering work lives in a new `handle_json_line()` method on
`ProgressUI` that parses JSONL envelopes and dispatches to the same
internal rendering methods already used by the in-process event handler.
This preserves 100% rendering fidelity without duplicating
spinner/stage/tool-call logic — the attach loop just feeds file lines
into the same code paths. File-based interview IPC is handled in the
attach loop itself: it watches for `interview_request.json`, prompts the
user via `ConsoleInterviewer`, and writes `interview_response.json` back
for the engine to consume. The `hide_bars`/`show_bars` methods
previously private to `ProgressAwareInterviewer` are promoted to public
methods on `ProgressUI` and reused in both the attach loop and the
existing in-process interviewer.

The old `detach_run()` function in `main.rs`, which reconstructed argv
by string-scanning `std::env::args()`, is deleted entirely and replaced
by the `create` + `start` composition. New tests cover the
`handle_json_line` dispatch paths (stage started/completed, tool calls,
retro events, invalid input) and the CLI argument parsing for the new
command variants.

### Fabro Details

<details>
<summary>Ran 9 stages in 30m 55s for $8.55</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 18m 15s | $5.28 | 0 |
| simplify_opus | 10m 31s | $3.27 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 17s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **30m 55s** | **$8.55** | **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>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 14:08:54 -04:00
Bryan Helmkamp
bbfea71bb9
Add Linux ARM64 (aarch64-unknown-linux-gnu) release target
Add aarch64-unknown-linux-gnu as a third release platform using GitHub's
native ARM64 runner. Updates the release workflow matrix, install script
architecture detection, and CLI upgrade platform detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 10:33:46 -04:00
brynary-fabro[bot]
20431799d3
fabro wait subcommand (#112)
This PR adds a `fabro wait` subcommand that blocks until a workflow run
reaches a terminal state and exits with a code reflecting the outcome —
analogous to `docker wait`. The command accepts a run ID prefix or
workflow name, polls `status.json` at a configurable interval
(defaulting to 1 second), and exits 0 on success or 1 on failure/dead.
An optional `--timeout` flag causes the command to bail with an error
message if the deadline is exceeded before the run completes.

The implementation reuses existing infrastructure throughout:
`resolve_run()` for run ID/name resolution, `RunStatusRecord::load()`
and `RunStatus::is_terminal()` for polling, `Conclusion::load()` for
retrieving duration and cost after completion, and `Styles` for colored
terminal output. Human-readable status is written to stderr (preserving
stdout for data), while `--json` mode writes structured conclusion data
to stdout. Missing status files are treated as `Dead` to handle orphaned
runs gracefully. No new dependencies were required.

The change is registered in all three necessary locations:
`commands/mod.rs`, the `Command` enum in `main.rs`, the command name
mapping, and the dispatch match arm. Unit tests cover JSON output across
all terminal states (with and without conclusion data), the
human-readable output path, immediate-terminal poll behavior, and the
missing-file fallback to `Dead`.

### Fabro Details

<details>
<summary>Ran 9 stages in 12m 13s for $2.81</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 12s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 4m 23s | $1.47 | 0 |
| simplify_opus | 4m 22s | $1.34 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 1m 21s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **12m 13s** | **$2.81** | **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-19 22:48:38 -04:00
brynary-fabro[bot]
69ef57cd4a
Add node-level model validation + missing catalog aliases (#110)
This PR addresses two related issues that caused `fabro run` to fail at
runtime with an unhelpful LLM error when using model names like
`gpt-54`: the model catalog was missing hyphenated aliases, and the
validator had no rule to catch unknown model names on workflow nodes.

The catalog changes add `gpt-54`, `gpt-54-pro`, and `gpt-54-mini` as
recognized aliases alongside the existing `gpt54`-style variants for the
`gpt-5.4` model family. This makes the hyphenated form a valid input
that resolves correctly at both validation and runtime, and the updated
`fabro model list` output and insta snapshots reflect the expanded alias
columns. Three new alias-resolution unit tests confirm each mapping.

On the validation side, a new `NodeModelKnownRule` mirrors the existing
`StylesheetModelKnownRule` but iterates over graph nodes instead of
stylesheet declarations, emitting `Severity::Warning` diagnostics with
the relevant `node_id` when a node's `model` or `provider` attribute
doesn't match anything in the catalog. To avoid duplication, the inline
validation logic in `StylesheetModelKnownRule` was refactored into
shared `check_model_known` and `check_provider_known` helpers that both
rules now call. Five unit tests cover the valid-model, unknown-model,
alias, unknown-provider, and no-attribute cases. An unrelated dead-code
helper in `git.rs` was also removed as part of the cleanup.

### Fabro Details

<details>
<summary>Ran 11 stages in 12m 36s for $2.85</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 58s | – | 0 |
| preflight_lint | 10s | – | 0 |
| implement | 2m 56s | $1.13 | 0 |
| simplify_opus | 4m 12s | $1.13 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 10s | – | 0 |
| fixup | 2m 27s | $0.59 | 0 |
| verify | 10s | – | 0 |
| fmt | 0s | – | 0 |
| **Total** | **12m 36s** | **$2.85** | **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>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 22:48:08 -04:00
brynary-fabro[bot]
4a5f9f698f
Strip markdown headings and "Plan:" prefix from GOAL column in fabro ps (#111)
This change cleans up the `GOAL` column in `fabro ps` output by
stripping common markdown noise before displaying goal text.
Specifically, `truncate_goal` now removes leading `#` heading markers
and an optional `Plan:` prefix (applied in order, so `## Plan: migrate
DB` correctly becomes `migrate DB`). Whitespace is trimmed after each
strip step to avoid leading spaces in the result.

As a minor refactor, the truncation logic was extracted into a
standalone `truncate_str` helper. This allows `truncate_goal` to
delegate cleanly after preprocessing, and also lets the workflow name
column (which doesn't need heading/prefix stripping) call `truncate_str`
directly. Five new unit tests cover the stripping behavior, combined
stripping, passthrough of plain text, and that truncation still applies
correctly after stripping.

### Fabro Details

<details>
<summary>Ran 9 stages in 9m 0s for $1.17</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 10s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 2m 28s | $0.42 | 0 |
| simplify_opus | 3m 20s | $0.75 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 1m 20s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **9m 0s** | **$1.17** | **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-19 22:27:03 -04:00
brynary-fabro[bot]
62c310bcec
Extract fabro-model crate from fabro-llm (#109)
This PR extracts a new `fabro-model` crate from `fabro-llm`, isolating
the model catalog, provider identity types, and model metadata into a
focused, lightweight package. The new crate contains `Provider`,
`ModelId`, `ModelInfo` (and related structs), and the full catalog API
(`get_model_info`, `list_models`, `default_model_from_env`,
`build_fallback_chain`, etc.) — with only `serde` and `serde_json` as
runtime dependencies, no async runtime required.

The extraction follows a clean boundary: `fabro-llm` now depends on
`fabro-model` and re-exports `Provider` and `ModelId` at its root so
existing `fabro_llm::Provider` usage remains valid without a migration.
The `ProviderAdapter` trait, `StreamEventStream`, and
`validate_tool_choice` stay in `fabro-llm` since they depend on async
streaming types. All seven consumer crates (`fabro-agent`, `fabro-api`,
`fabro-cli`, `fabro-hooks`, `fabro-workflows`, `fabro-validate`, and
`fabro-llm` itself) are updated to import catalog functions and provider
types directly from `fabro_model` rather than `fabro_llm::catalog` or
`fabro_llm::provider`.

The most impactful dependency reduction is in `fabro-validate`, which
previously pulled in all of `fabro-llm` (tokio, reqwest, the full async
runtime) just to perform catalog lookups and validate provider names. It
now depends solely on `fabro-model`, eliminating that heavyweight
transitive closure. All other consumers retain their `fabro-llm`
dependency for client and streaming functionality, but gain a precise
import path for catalog and identity concerns.

### Fabro Details

<details>
<summary>Ran 9 stages in 21m 16s for $5.47</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 10s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 13m 29s | $3.81 | 1 |
| simplify_opus | 5m 41s | $1.66 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 15s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **21m 16s** | **$5.47** | **1** |

</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-19 22:26:47 -04:00
Bryan Helmkamp
9b1cc75b20
Create fabro-sandbox crate, consolidating Sandbox trait and all implementations
Extract the Sandbox trait, types, and all sandbox implementations from
fabro-agent and four separate crates (fabro-exe, fabro-ssh, fabro-sprites,
fabro-daytona) into a single fabro-sandbox crate. This cleans up the
dependency graph — implementation crates no longer pull in the full
fabro-agent just for the trait.

The new crate uses feature flags (local, docker, ssh, exe, sprites,
daytona, test-support) to gate each implementation. The shell_quote()
helper is unified into a single shared implementation, eliminating four
duplicate copies.

fabro-agent now re-exports all sandbox types from fabro-sandbox for
backward compatibility. The four absorbed crates are removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 20:06:19 -04:00
Bryan Helmkamp
8a3ce34209
Fix upgrade command failing with "No such file or directory"
The release tarball nests the binary in a subdirectory
(fabro-{triple}/fabro), but the upgrade code expected it at the
tarball root. Use the correct nested path matching the tarball structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:58:50 -04:00
Bryan Helmkamp
eaf9c68050
Clean up telemetry: hoist shared computations, tighten visibility
- Compute sanitize_command, repository_identifier, and CI check once
  before the if/else branches to avoid duplicate git I/O
- Make should_track_for_level private (only used by _track_inner)
- Check tracks.is_empty() before credentials in upload_blocking for
  consistency with emit()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:55:25 -04:00
Bryan Helmkamp
94ecc0976b
Add buffered telemetry with track! macro and extract fabro-telemetry crate
Extract telemetry from fabro-util into a dedicated fabro-telemetry crate.
Replace the synchronous Telemetry struct with a global background buffer
that flushes periodically via blocking HTTP (mid-run) and detached
subprocess (final flush at exit). The new API is init_cli()/track!()/shutdown().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:15:33 -04:00
brynary-fabro[bot]
fd5cde2719
Fix: Add OAuth callback URLs to CLI-generated GitHub App manifest (#105)
This PR fixes GitHub issue #97 where `fabro install` creates a GitHub
App that passes `fabro doctor` but fails during OAuth login in
`fabro-web` because the CLI-generated manifest is missing
`callback_urls` and `setup_url` fields. Without these fields, GitHub
rejects the OAuth flow with a "must be configured with a callback URL"
error, even though the web setup flow (`setup.tsx`) already includes
them correctly.

The fix extracts manifest construction into a standalone
`build_github_app_manifest` helper and adds the missing `callback_urls`
and `setup_url` fields, mirroring what the web setup flow already
provides. A new `--web-url` flag (defaulting to `http://localhost:5173`)
is added to the `Install` command so users can specify their web UI base
URL, which is then threaded through `run_install` and `setup_github_app`
to construct the correct OAuth callback endpoints.

A unit test is included to verify that the generated manifest contains
the expected `callback_urls` and `setup_url` values for a given
`web_url`.

### Fabro Details

<details>
<summary>Ran 9 stages in 17m 47s for $1.56</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 14s | – | 0 |
| implement | 6m 32s | $0.64 | 1 |
| simplify_opus | 0s | – | 3 |
| simplify_gpt | 5m 42s | $0.92 | 0 |
| verify | 1m 21s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **17m 47s** | **$1.56** | **4** |

</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>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:39:39 -04:00
Bryan Helmkamp
c5b25c8edc
Change default Anthropic model from claude-opus-4-6 to claude-sonnet-4-6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:33:16 -04:00
Bryan Helmkamp
9c84c8738e
Add probe_model_for_provider to fix false doctor warning on ChatGPT/Codex backend
doctor and provider_auth used cheapest_model (gpt-5-mini) for connectivity
probes, but gpt-5-mini is rejected by the ChatGPT/Codex backend. Adds
probe_model_for_provider() which returns gpt-5.4-mini for OpenAI and falls
back to the default model for other providers.

Fixes #96

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:31:32 -04:00
brynary-fabro[bot]
87403b19a5
Fix: Default provider should respect configured API keys (#103)
This PR fixes a bug where users with only OpenAI or Gemini API keys
configured would hit errors when running commands without an explicit
`--provider` flag, because the system hardcoded `Provider::Anthropic` as
the fallback in multiple places.

The core change introduces `Provider::default_from_env()`, which checks
which providers have API keys configured and selects the best available
one using a precedence order (Anthropic → OpenAI → Gemini), falling back
to Anthropic if none are found. A testable `default_with()` helper
enables thorough unit testing of the selection logic without relying on
environment variables. A companion `catalog::default_model_from_env()`
function pairs the provider selection with the correct default model
lookup.

All six call sites across the CLI, API server, workflow handler, and
agent that previously hardcoded `Provider::Anthropic` or used
`catalog::default_model()` without considering available keys have been
updated to use the new environment-aware defaults. The catalog functions
also now normalize provider aliases (e.g. `"open_ai"` → `"openai"`) to
prevent silent lookup misses.

### Fabro Details

<details>
<summary>Ran 10 stages in 28m 34s for $6.23</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 8s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 4m 40s | $0.79 | 0 |
| simplify_opus | 7m 23s | $1.84 | 0 |
| simplify_gemini | 6m 24s | $1.44 | 0 |
| simplify_gpt | 7m 56s | $2.16 | 0 |
| verify | 14s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **28m 34s** | **$6.23** | **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", 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: Fabro <fabro@fabro.sh>
2026-03-19 12:30:17 -04:00
brynary-fabro[bot]
8a9b89deda
fabro provider login command (#102)
This PR adds a standalone `fabro provider login --provider <name>`
command so users can re-authenticate with LLM providers outside of the
initial `fabro install` wizard—useful when tokens expire or when
switching accounts. For OpenAI, it offers the browser-based OAuth PKCE
flow with an automatic fallback to manual API key entry; all other
providers go straight to an API key prompt with validation. Credentials
are merged non-destructively into `~/.fabro/.env`.

To support this, shared authentication helpers (`provider_key_url`,
`provider_display_name`, `openai_oauth_env_pairs`, `write_env_file`,
`validate_api_key`, `prompt_and_validate_key`, and the underlying prompt
functions) are extracted from `install.rs` into a new `provider_auth.rs`
module. The install flow is updated to import from the shared module and
delegate the OpenAI OAuth-or-fallback logic to a single
`run_openai_oauth_or_api_key` helper, eliminating duplication between
the two code paths.

The new command is wired into the CLI via a `ProviderCommand::Login`
subcommand under `Command::Provider`, with clap-level parsing tests
covering valid providers, missing flags, and unknown provider names.

### Fabro Details

<details>
<summary>Ran 10 stages in 26m 57s for $7.49</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 9m 16s | $2.76 | 0 |
| simplify_opus | 6m 32s | $2.05 | 0 |
| simplify_gemini | 4m 44s | $1.58 | 0 |
| simplify_gpt | 4m 2s | $1.10 | 0 |
| verify | 22s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **26m 57s** | **$7.49** | **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", 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-19 12:30:10 -04:00
brynary-fabro[bot]
333787a417
JSONL analytics event file format (#100)
This PR switches the analytics telemetry file format from
single-JSON-per-file to JSONL (one JSON event per line), enabling future
batching of multiple events into a single file and subprocess. Each file
is now named `fabro-events-{uuid}.jsonl` instead of
`fabro-event-{uuid}.json`, and the Segment API endpoint is updated from
`/v1/track` to `/v1/batch`.

The core changes are in `sender.rs`: `send(Track)` becomes
`emit(&[Track])`, which serializes each track as a compact JSON line;
`send_to_segment()` becomes `upload()`, which reads the JSONL file,
parses each line, injects `"type": "track"`, and POSTs the batch to
Segment. A new pure function `build_segment_batch()` is extracted for
testability, handling empty content, malformed lines (skipped with a
warning), and blank lines gracefully. The panic sender remains unchanged
as single-JSON-per-file.

The call sites in `main.rs` are updated to use the new `emit`/`upload`
signatures, and comprehensive tests are added covering the batch builder
(empty, single, multiple, malformed, blank lines), the emit no-op paths,
and the upload no-op behavior when `SEGMENT_WRITE_KEY` is unset.

### Fabro Details

<details>
<summary>Ran 10 stages in 17m 42s for $3.98</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 11s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 3m 57s | $0.93 | 0 |
| simplify_opus | 3m 5s | $0.92 | 0 |
| simplify_gemini | 3m 56s | $1.17 | 0 |
| simplify_gpt | 3m 26s | $0.97 | 0 |
| verify | 1m 16s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **17m 42s** | **$3.98** | **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", 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-19 12:30:02 -04:00
brynary-fabro[bot]
d5bbc12261
Detect GitHub App visibility mismatch during repo init (#99)
This PR detects when a GitHub App's visibility will prevent installation
on a cross-owner repository during `fabro repo init`. Previously, when
the app wasn't installed, users saw only a generic "install at" URL with
no indication of why the install link might not work—particularly
confusing when the repo belongs to a different owner than the app and
the app is private.

Two new functions are added to `fabro-github`: `get_authenticated_app()`
fetches the app's metadata (slug and owner) via the authenticated `GET
/app` endpoint, and `is_app_public()` probes `GET /apps/{slug}` without
authentication to determine visibility (public apps return 200, private
ones return 404). In `init.rs`, when the app is not installed, we now
compare the app owner against the repo owner and, if they differ and the
app is private, display a targeted warning explaining that the app must
be made public along with a direct link to the settings page. All new
checks are best-effort—failures are silently ignored so the existing
flow is unaffected.

The PR also introduces a `GITHUB_API_BASE_URL` constant to replace
hardcoded URL strings and adds five unit tests covering the new
functions: successful app info retrieval, auth failure handling,
public/private app detection, and verification that the visibility check
sends no `Authorization` header.

### Fabro Details

<details>
<summary>Ran 10 stages in 18m 26s for $4.40</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 13s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 3m 23s | $0.94 | 0 |
| simplify_opus | 5m 17s | $1.44 | 0 |
| simplify_gemini | 2m 52s | $0.92 | 0 |
| simplify_gpt | 3m 24s | $1.10 | 0 |
| verify | 1m 25s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **18m 26s** | **$4.40** | **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", 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-19 12:29:55 -04:00
Bryan Helmkamp
e3b396e001
Clean up GitSyncStatus usage: add use import and unify warning messages
Replace fully-qualified fabro_workflows::git::GitSyncStatus paths with a
use import, and consolidate the near-duplicate dirty-worktree warning arms
into a single block that varies only the environment name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:34:55 -04:00
Bryan Helmkamp
a5273e17a3
Add GitSyncStatus and WorkdirStrategy enums to fix cloud sandbox pre-run checks
The git sync check and auto-push logic was gated on should_create_worktree,
which was always false for remote sandboxes. This meant Daytona/Exe/SSH runs
silently proceeded without verifying commits were pushed or warning about
uncommitted changes. Replace the git_clean boolean and should_create_worktree
boolean with two enums (GitSyncStatus: Synced/Unsynced/Dirty and
WorkdirStrategy: LocalDirectory/LocalWorktree/Cloud) so every combination
is handled explicitly via match arms. Also display the base commit SHA for
cloud runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:17:39 -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
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