Commit graph

300 commits

Author SHA1 Message Date
Bryan Helmkamp
9c22bb8834 Fix 6 bugs in create/start/attach lifecycle
Bug 1: handle_json_line now reads post-rename field names matching real
JSONL output (node_label, node_id, error instead of name, stage, message).
Existing tests updated to use post-rename names too.

Bug 2: _run_engine restores spec.working_directory via set_current_dir
and uses cached graph.fabro instead of spec.workflow_path.

Bug 3: attach loop deletes interview_request.json immediately after
reading to prevent re-prompting. run_command uses FileInterviewer when
stdin is not a TTY (detached mode).

Bug 4: attach_run loads spec.verbose and passes it to ProgressUI instead
of hardcoding false.

Bug 5: start_run writes Starting status before spawning the engine
process to prevent duplicate engines from concurrent start calls.

Bug 6: handle_json_line dispatches DevcontainerResolved,
DevcontainerLifecycleStarted/Completed/Failed events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:35:59 -04:00
Bryan Helmkamp
1490024823 Add failing regression tests for 6 bugs in create/start/attach lifecycle
Unit tests (run_progress.rs, start.rs):
- Bug 1: handle_json_line reads pre-rename field names (name/stage/branch/message)
  but real JSONL uses post-rename names (node_label/node_id/node_id/error)
- Bug 5: start_run doesn't write Starting status before spawning engine
- Bug 6: handle_json_line missing DevcontainerLifecycleStarted dispatch

Integration tests (cli.rs):
- Bug 2: _run_engine reads spec.workflow_path instead of cached graph.fabro
- Bug 3: attach loop doesn't delete interview_request.json after handling
- Bug 4: attach hardcodes verbose=false, ignoring spec.verbose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:28:19 -04:00
Fabro
350df9315b fabro(01KM4DKAWADPG0HY3PZCGQJ7H2): simplify_opus (success)
Fabro-Run: 01KM4DKAWADPG0HY3PZCGQJ7H2
Fabro-Completed: 6
Fabro-Checkpoint: 140b789412

⚒️ Generated with [Fabro](https://fabro.sh)
2026-03-20 01:56:43 +00:00
Fabro
2cf3330731 fabro(01KM4DKAWADPG0HY3PZCGQJ7H2): implement (success)
Fabro-Run: 01KM4DKAWADPG0HY3PZCGQJ7H2
Fabro-Completed: 5
Fabro-Checkpoint: 92504d679a

⚒️ Generated with [Fabro](https://fabro.sh)
2026-03-20 01:46:09 +00:00
Bryan Helmkamp
ec7d770b0d Deduplicate SSH types and utilities within fabro-sandbox
Extract shared SSH types (SshOutput, SshRunner, GitCloneParams) and
utility functions (wrap_bash_command, resolve_clone_url, clone_repo)
into a new ssh_common module, eliminating ~270 lines of duplication
between the exe and ssh sandbox implementations.

Also extract a shared resolve_path helper used by four sandbox
implementations, and fix an O(n log n) metadata syscall issue in
LocalSandbox::glob by switching to sort_by_cached_key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 20:24:06 -04:00
Bryan Helmkamp
2e5c461149 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
bb2d0a6736 Fix agent sessions ignoring node-level model/provider from stylesheets
AgentApiBackend::create_session() always used the backend's default
model/provider, ignoring attributes set on the node by stylesheet
application. The one_shot path already read node.model() correctly
but the agent session path (used by implement and other agent stages)
did not. Also fixes usage reporting and provider_used.json to reflect
the actual model used rather than the backend default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 19:48:53 -04:00
Bryan Helmkamp
eca32f68f9 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
0937d5ce48 Fix release CI test failures caused by telemetry fork flushing stdout
The double-fork in spawn_detached_unix inherited unflushed stdout/stderr
buffers from the parent process. When the intermediate child called
std::process::exit(0), libc cleanup flushed these buffers again, causing
duplicate output that broke trycmd snapshot comparisons in release builds
(where telemetry defaults to enabled).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:18:03 -04:00
Bryan Helmkamp
a8777bbfe2 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
23588ab1f4 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
Bryan Helmkamp
3f24c2e6ae Change SEGMENT_API_URL to SEGMENT_BASE_URL and append endpoint path
Store only the base URL (e.g. https://api.segment.io) so that
different endpoints (/v1/batch, /v1/track, etc.) can reuse it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:02:29 -04:00
Bryan Helmkamp
bfaa0f8c4b Make SEGMENT_API_URL configurable at compile time
Allow overriding the Segment API endpoint via the SEGMENT_API_URL
environment variable at build time, defaulting to the standard
https://api.segment.io/v1/batch endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:00:34 -04:00
brynary-fabro[bot]
0c1187f3a1 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
8072b271a3 Fix missing skip_clone field in OpenAPI spec and conformance test
The DaytonaConfig struct had a skip_clone field that was missing from
both the OpenAPI spec and the conformance test initializer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:36:49 -04:00
Bryan Helmkamp
e626bcfcd4 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
7b364ab28e 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
Bryan Helmkamp
f24bb10110 Upgrade parity matrix OpenAI model from gpt-5-mini to gpt-5.4-mini
gpt-5-mini is not supported on the ChatGPT/Codex backend, causing all
16 OpenAI parity tests to fail when using browser-auth credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:24:21 -04:00
Bryan Helmkamp
33e5a41cad Fix: Remove std::env::set_var/remove_var from tests (#101)
Introduce an Env trait in fabro-util so tests can inject a HashMap-backed
TestEnv instead of mutating process-global environment variables, which
is unsafe since Rust 1.66+ and causes flakiness in concurrent tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:09:46 -04:00
brynary-fabro[bot]
1349ee14e2 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]
badc3537fb 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]
bc86ba553c 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]
69a16431c4 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
bea69d3d74 Add skip_clone to DaytonaConfig to fix concurrent test failures
Two Daytona integration tests used std::env::set_current_dir to a temp
directory so detect_repo_info() would fail and skip cloning. Since cwd
is process-global, this poisoned concurrent tests. Replace with an
explicit skip_clone config flag that skips repo detection and cloning
during sandbox initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:27:56 -04:00
Bryan Helmkamp
9ee834c198 Remove dead ensure_clean_and_pushed function
This function had zero callers after sync_status was introduced in
ad84f9f9. Remove it along with its four tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:36:05 -04:00
Bryan Helmkamp
d7b4033da4 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
ad84f9f92c 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
77c814ade6 Relax web_fetch parity test assertions for LLM summarization variance
Some providers return summaries of example.com without the exact phrases
"Example Domain" or "example.com", so accept related terms like
"documentation" or "iana" alongside "example".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:20 -04:00
Bryan Helmkamp
92bb8bbf94 Quarantine Zai E2E parity tests behind feature flag
Zai provider tests are unreliable (editing, web_fetch, web_search
failures). Gate them behind cfg(feature = "quarantine") like Inception
tests so they don't block the default test suite.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
aa8b803df9 Fix sprite exec flag parsing and document E2E test instructions
Add `--` separator before `bash -c` in sprite exec args so the CLI
stops parsing flags and doesn't interpret `-c` as its own flag.
Also add E2E live test commands to CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
c50055b36e Fix private repo clone test to use current repo name
The assertion was checking for the old repo name brynary/arc instead of
fabro-sh/fabro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
0fe54b6b61 Fix asset collection tests by setting asset_globs
The Docker and Daytona asset collection tests were failing because
asset_globs was empty, causing the engine to skip collection entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
8bf85fb4ea Fix OpenAI multi-turn failures by only setting store: false in Codex mode
The Codex endpoint commit (459a9c22) added `store: false` unconditionally
to all OpenAI Responses API requests. This broke multi-turn conversations
because OpenAI doesn't persist items when store is false, so referencing
previous reasoning/message IDs on subsequent turns returns a 404. The fix
makes store conditional: true for regular OpenAI (the API default), false
only for the Codex endpoint which requires it.

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:03:19 -04:00
Bryan Helmkamp
e477c19646 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
88b2bf31a6 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
bd8ebcf521 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
fe4896378d 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
0ba54ffdea 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
0c79c368b2 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
e5ec057b79 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
8ef79bb445 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
69a41c7029 Move workflow CLI ownership into fabro-cli 2026-03-18 09:31:11 -04:00
Bryan Helmkamp
bea165192a 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
ea789ba994 Deduplicate config loading and run-config merging
- Extract generic load_config_file<T>(path, filename) helper, reducing
  load_cli_config and load_server_config to one-liners.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:56:51 -04:00