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.
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.
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>
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.
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.
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
## 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>
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>
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>
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>
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>
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>
## 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>
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>
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>