Commit graph

191 commits

Author SHA1 Message Date
Bryan Helmkamp
af2a1e4f6d Add server daemon management with Unix socket support
Transform `fabro server` from foreground-only TCP into a proper daemon:
- `server start` launches background daemon with flock-based locking
- `server start --foreground` retains current blocking behavior
- `server stop` sends SIGTERM, waits, escalates to SIGKILL
- `server status` reports running/stopped with PID, bind, uptime (--json)
- `--bind` replaces `--host`/`--port`, supporting Unix sockets and TCP
- Default bind is `{storage_dir}/fabro.sock` (Unix socket)
- Hidden `__serve` subcommand for daemon child process lifecycle
- Graceful shutdown via SIGTERM/SIGINT signal handlers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 10:40:19 -07:00
Bryan Helmkamp
7235414e6a Rename FabroSettings to Settings, eliminate FabroSettingsExt
Move storage_dir() from FabroSettingsExt trait in fabro-config into an
inherent method on Settings in fabro-types. Remove the re-export from
fabro-config so callers import directly from fabro_types. Drop the
redundant Fabro prefix since the type already lives in the fabro_types
crate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 07:36:44 -07:00
Bryan Helmkamp
b56b82d34b Cut over Fabro web app to a server-backed SPA
Replace the old React Router SSR setup with a static SPA build served by
fabro-server, move setup and GitHub auth handling into Rust, and update the
default local web URL and stale Arc-era references to match the Fabro name.
2026-04-01 21:36:01 -07:00
Bryan Helmkamp
d95dedf711 Persist run.created events in the run store
Hydrate the durable run store immediately after create-time event emission so
store-backed readers see the initial run.created event instead of only the
on-disk progress log. Add a regression test covering create-time store
visibility and wire in the object_store dependency needed by that test.
2026-04-01 20:13:14 -04:00
Bryan Helmkamp
12dc5557d8 Consolidate unsafe process code into fabro-proc crate
Rename fabro-proctitle to fabro-proc and add safe wrappers for all
process management primitives (signals, pre-exec hooks). This contains
all unsafe proc code behind a safe API so downstream crates no longer
need #[allow(unsafe_code)] or direct libc dependencies.

New modules: signal (process_alive, sigterm, sigkill, sigterm_process_group),
pre_exec (pre_exec_setsid, pre_exec_setpgid, pre_exec_pdeathsig),
title (existing proctitle code). Eliminates three duplicate process_alive
definitions and removes libc as a direct dep of fabro-cli and fabro-sandbox.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:49:29 -04:00
Bryan Helmkamp
c6cb9c6e7e Expand OpenAI twin coverage across integration tests
Add shared twin scenario helpers and use them to cover OpenAI-backed
CLI, agent parity, workflow, and exec integration paths. This brings the
worktree implementation back into the main checkout as a single commit.
2026-04-01 11:03:34 -04:00
Bryan Helmkamp
c22845e548 Integrate twin-github for fabro-github tests
Add the stripped twin-github test server to the workspace, wire it through
fabro-test, and cover fabro-github's real HTTP auth and pull-request flows
with twin-backed integration tests. This also refactors the GitHub helper
entry points to take explicit base URLs so tests and callers share the same
request path.
2026-04-01 09:48:33 -04:00
Bryan Helmkamp
5b7eabee8f Add twin test mode for OpenAI E2E tests
Integrate twin-openai (fake OpenAI server) into the workspace and wire
it into the e2e_test macro so OpenAI tests can run without real API
credentials. The twin server starts in-process via OnceLock on first use
and provides per-test isolation through bearer-token namespacing.

Changes:
- Add Twin as default TestMode, replacing Off (gating now via #[ignore])
- Extend #[e2e_test] macro with `twin` requirement for twin-only,
  live-only, and dual-mode (twin + live) test gating
- Add e2e_openai!() macro returning (base_url, api_key)
- Convert openai_complete and openai_gpt_5_3_codex_complete to dual-mode
- Add new openai_server_error twin-only test with scripted 500 error
- Standardize axum 0.8 as workspace dependency across all crates
- Relax twin-openai ResponsesRequest to accept unknown fields via flatten

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:36:48 -04:00
Bryan Helmkamp
699691b468 Rename fabro-openai-oauth to fabro-oauth 2026-03-31 13:23:43 -04:00
Bryan Helmkamp
1c6c160e08 Centralize E2E test env var handling 2026-03-31 09:30:38 -04:00
Bryan Helmkamp
cac8f386f9 Rename fabro-git-storage to fabro-checkpoint 2026-03-30 16:17:39 -04:00
Bryan Helmkamp
93aa239056 Add fabro completion subcommand for shell completions
Uses clap_complete to generate tab-completion scripts for zsh, fish,
elvish, and PowerShell. Bash generation is caught gracefully since
clap_complete panics with #[command(flatten)] subcommands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:17:38 -04:00
Bryan Helmkamp
d5976820d5 Rename fabro-workflows crate to fabro-workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 12:27:40 -04:00
Bryan Helmkamp
3936f185cf Remove SSH and Exe sandbox providers
Only three sandbox providers remain: Local, Docker, and Daytona.

- Move detect_clone_params and GitCloneParams from ssh_common into daytona module
- Delete ssh/, exe/, and ssh_common.rs implementation files
- Remove Exe/Ssh variants from SandboxProvider, SandboxSpec, CliSandboxProvider
- Remove data_host from Sandbox trait and SandboxRecord
- Remove ExeSettings, SshSettings, ExeConfig, SshConfig types
- Remove ssh/exe/exedev feature flags from all Cargo.toml files
- Remove openssh workspace dependency
- Remove ExeSettings/SshSettings from OpenAPI spec
- Update docs to remove SSH/Exe references, delete exe-dev.mdx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 12:17:20 -04:00
Bryan Helmkamp
c3634c0740 Replace trycmd tests with fabro-test crate and insta snapshots
Adopt uv's testing pattern: a shared `fabro-test` crate with TestContext
and fabro_snapshot! macro, plus one test file per subcommand under
tests/it/cmd/. This replaces the trycmd-based tests which were hard to
read and didn't compose well with programmatic assertions.

- Create lib/crates/fabro-test with TestContext, run_and_format,
  apply_filters, INSTA_FILTERS, and test_context!/fabro_snapshot! macros
- Add 42 snapshot tests across 16 subcommand files
- Delete trycmd.rs and all tests/cmd/ trycmd files
- Remove trycmd dependency, add fabro-test dev-dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:28:41 -04:00
Bryan Helmkamp
c6313d74bc Use typed RunId across workflows 2026-03-30 09:20:11 -04:00
Bryan Helmkamp
b6947af7af Add per-asset metadata (mime, md5, sha256) to asset capture
Replace the batch AssetsCaptured event with per-file AssetCaptured events
that include content hashes and MIME type. The asset collection manifest
now stores a captured_assets array with full metadata instead of bare
path strings, enabling downstream integrity verification and content
type awareness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:10:09 -04:00
Bryan Helmkamp
0abf7c1b4d Rename fabro-api crate to fabro-server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:09 -04:00
Bryan Helmkamp
b577c4ff95 Rename fabro-types-derive to fabro-macros
Broader name better reflects the crate's role as the workspace's
proc-macro crate, not just derives for fabro-types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:09 -04:00
Bryan Helmkamp
fa26519869 Fix store migration gaps: retro agent, finalize commit, engine failure, and hydration tolerance
- retro_agent::upload_data_files reads from RunStore first with filesystem
  fallback for progress.jsonl, checkpoint, run record, and start record
- write_finalize_commit reads retro.json from store before falling back to disk
- persist_terminal_engine_failure uses build_conclusion_from_store instead of
  disk-only build_conclusion
- open_or_hydrate_run tolerates malformed checkpoint/conclusion/retro/sandbox
  JSON files during hydration (warns and skips instead of failing)
- Box<DbReader> in SlateRunDb fixes clippy large_enum_variant warning
- Fix tests that called open_or_hydrate_run on dirs without run.json
- Nextest test-groups replace global thread cap for better parallelism
- opt-level=1 for dev dependencies shrinks test binary sizes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:09 -04:00
Bryan Helmkamp
96641f4acc Wire workflows and CLI to fabro-store 2026-03-29 13:47:09 -04:00
Bryan Helmkamp
038420d9c8 Replace mockito with httpmock in fabro-hooks, fabro-openai-oauth, and fabro-tracker
mockito's Server::new_async() triggers macOS SCDynamicStoreCreateWithOptions
via hyper-util (~300ms per test), which serializes on configd under workspace
concurrency and causes 4s+ timeouts. httpmock avoids this path entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:09 -04:00
Bryan Helmkamp
6e543814be Replace mockito with HttpClient trait in fabro-github and detect import self-loops
Introduce an HttpClient trait abstraction over reqwest::Client so tests
use a lightweight MockHttpClient instead of spawning a TCP server via
mockito. This removes the mockito dev-dependency entirely and makes
tests faster and more deterministic.

Also add self-loop detection in ImportTransform to poison placeholders
that have edges pointing back to themselves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:09 -04:00
Bryan Helmkamp
d277ecf04a Add detached engine process titles 2026-03-29 13:47:09 -04:00
Bryan Helmkamp
bf9fd2cea2 Create RuntimeState for run runtime and cache paths 2026-03-29 13:47:08 -04:00
Bryan Helmkamp
e417901a3e Add fabro-store with in-memory and SlateDB backends 2026-03-29 13:47:08 -04:00
Bryan Helmkamp
48c21ed521 Create fabro-types and rewire shared types 2026-03-29 13:47:08 -04:00
Bryan Helmkamp
c192c106f6 Rename fabro-types to fabro-api-types
Better reflects that this crate contains auto-generated types scoped
to the API layer. Pure rename with no behavior change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:08 -04:00
Bryan Helmkamp
72acee6c8a Fold fabro-beastie into fabro-cli as internal sleep_inhibitor module
fabro-beastie had no consumers other than fabro-cli behind a feature
flag. Absorbing it as an internal module reduces workspace crate count
without changing any behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:07 -04:00
Bryan Helmkamp
95d8b32919 Refactor config layering into combine plus settings 2026-03-29 13:47:07 -04:00
Bryan Helmkamp
03f73bf83c Add fabro config show command 2026-03-29 13:47:07 -04:00
Bryan Helmkamp
a7774d1735 refactor(sandbox): move SandboxRecord and sandbox_reconnect to fabro-sandbox
These are purely sandbox concerns — they serialize/deserialize sandbox
connection info and reconstruct sandbox instances. Moving them to
fabro-sandbox improves cohesion and removes workflow-layer coupling
from sandbox lifecycle logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:06 -04:00
Bryan Helmkamp
2882ab3640 refactor(graphviz): move graph_render module from fabro-workflows to fabro-graphviz
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:06 -04:00
Bryan Helmkamp
033a5b0675 Consolidate backoff/jitter into fabro-util::BackoffPolicy
Three crates independently implemented the same exponential-backoff-with-jitter
logic. Extract a single BackoffPolicy into fabro-util and have fabro-core,
fabro-workflows, and fabro-llm all use it, eliminating duplication and making
the backoff conversion in core_adapter trivial.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:47:05 -04:00
Bryan Helmkamp
6fa94ee36c Fix test timeouts caused by reqwest system proxy detection on macOS
reqwest::Client::new() queries macOS SCDynamicStore for system proxy
settings, which calls CFBundleGetMainBundle() → readdir() on
target/debug/deps/. With 576K stale build artifacts accumulated in
that directory, each readdir() took 1.3s, causing 15s+ delays when
multiple test processes ran concurrently.

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:36:26 -04:00
Bryan Helmkamp
584c2e11f7 Update tar crate to 0.4.45 (security fix) (#122)
## Summary

- Updates `tar` from 0.4.44 to 0.4.45 via `cargo update -p tar`
- Resolves two open Dependabot security alerts:
- [tar-rs `unpack_in` can chmod arbitrary directories by following
symlinks](https://github.com/fabro-sh/fabro/security/dependabot/3)
- [tar-rs incorrectly ignores PAX size headers if header size is
nonzero](https://github.com/fabro-sh/fabro/security/dependabot/2)

## Test plan

- [x] `cargo build --workspace` succeeds
- [ ] CI passes

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:53:08 -04:00
brynary-fabro[bot]
33d697d93d 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
brynary-fabro[bot]
c81fd1740b 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
brynary-fabro[bot]
60926dfbfd Merge fabro-linear and GitHub tracker into fabro-tracker (#108)
This PR consolidates the tracker ecosystem from three crates
(`fabro-tracker`, `fabro-linear`, `fabro-github`) into two by merging
both tracker implementations into `fabro-tracker` and deleting
`fabro-linear`. The `GitHubTracker` and its supporting functions
(`execute_github_graphql`, `normalize_github_item`,
`fetch_project_items_page`) have been moved from `fabro-github` into a
new `fabro-tracker/src/github.rs` module, while the Linear
implementation from `fabro-linear` moves into
`fabro-tracker/src/linear.rs`. The duplicate `Issue` and `BlockerRef`
type definitions that existed in `fabro-linear` are removed in favor of
the canonical types already defined in `fabro-tracker`.

The dependency direction between `fabro-github` and `fabro-tracker` is
intentionally reversed: `fabro-tracker` now depends on `fabro-github`
for auth primitives (`GitHubAppCredentials`, `sign_app_jwt`,
`create_installation_access_token_for_projects`), while `fabro-github`
drops its dependency on `fabro-tracker` entirely. This eliminates the
circular dependency risk and keeps `fabro-github` focused on its core
responsibility of GitHub App authentication and REST/GraphQL transport.
A shared `execute_graphql_request` helper is introduced in
`fabro-tracker` to reduce duplication between the GitHub and Linear
GraphQL implementations.

All tests that previously lived in `fabro-github` and `fabro-linear` are
relocated to their respective new modules in `fabro-tracker`. The
`test_rsa_key()` helper used in GitHub tracker tests is duplicated in
`fabro-tracker/src/github.rs` since test utilities are not importable
across crate boundaries. The Linear `normalize_issue` function is
updated to set `project_item_id: None` to conform to the shared `Issue`
type, and existing Linear tests are updated accordingly.

### Fabro Details

<details>
<summary>Ran 9 stages in 24m 3s for $6.93</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 15s | – | 0 |
| preflight_lint | 13s | – | 0 |
| implement | 14m 56s | $4.58 | 0 |
| simplify_opus | 6m 50s | $2.35 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 19s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **24m 3s** | **$6.93** | **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:26:32 -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
2f9a1d0482 Bump version to 0.176.2 2026-03-19 19:08:43 -04:00
Bryan Helmkamp
bfdd585298 Bump version to 0.176.1 2026-03-19 15:19:21 -04:00
Bryan Helmkamp
7769345904 Bump version to 0.176.0 2026-03-19 14:31:44 -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
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
Bryan Helmkamp
d81fd8d656 Remove OpenSSL runtime dependency from CLI binary (#94)
## Summary
- Disable git2 default features (`ssh`, `https`) which pulled in
`openssl-sys` and `libssh2-sys`
- These transports are unused — all git2 usage in the codebase is local
repo operations (commits, blobs, revwalks)
- The CLI binary no longer dynamically links against `libssl.3.dylib` /
`libcrypto.3.dylib`

Fixes #92

## Verification
- `otool -L target/debug/fabro | grep ssl` returns nothing (no OpenSSL
linkage)
- `cargo tree -i openssl-sys` returns nothing (fully removed from dep
tree)
- All 179 workspace tests pass

## Test plan
- [ ] Build release binary and verify with `otool -L` (macOS) or `ldd`
(Linux) that no OpenSSL refs remain
- [ ] Run on a machine without OpenSSL v3 installed — should launch
without `dyld` error

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:48:40 -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