- rust.yml: move clippy to nightly-2026-04-14 (was stable); also pin fmt to the same nightly date for consistency. Both jobs now use the dated nightly and the run-step uses `cargo +nightly-2026-04-14 ...`. - AGENTS.md: update developer commands to match CI. - Duration constructors: replace `Duration::from_secs(N * 60)` / `Duration::from_millis(N * 1000)` with `from_mins` / `from_secs` / `from_hours` across the workspace to satisfy clippy's new `duration_suboptimal_units` lint. std::time::Duration only — custom `settings::duration::Duration` sites kept on `from_secs`. - map/unwrap_or cleanup: `.map(f).unwrap_or(v)` → `.map_or(v, f)`, `.map(f).unwrap_or(false)` on Result → `.is_ok_and(f)`, per `clippy::map_unwrap_or`. - Misc lints: collapse nested `if` into match guard in handler/llm/api.rs and run_state.rs; replace `columns.len() > 0` with `!columns.is_empty()`; switch a pair of `sort_by` calls to `sort_by_key`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build and test commands
Rust
cargo build --workspace— build all cratescargo nextest run --workspace— run all unit testscargo nextest run -p fabro-server— test a single cratecargo nextest run -p fabro-workflow -- test_name— run a single testset -a && source .env && set +a && cargo nextest run --workspace --profile e2e --run-ignored only— run all E2E live tests (requires credentials in.env, see.env.example)set -a && source .env && set +a && cargo nextest run -p fabro-llm --profile e2e --run-ignored only— run E2E tests for a single cratecargo +nightly-2026-04-14 fmt --check --all— check formatting (pinned nightly required for rustfmt config; CI uses the same date)cargo +nightly-2026-04-14 fmt --all— auto-formatcargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings— lint (CI runs nightly clippy to match; install withrustup toolchain install nightly-2026-04-14 --profile minimal --component clippy,rustfmt)
macOS note: if cargo nextest run fails with Too many open files (os error 24) / EMFILE, raise the shell's soft FD limit before running tests, for example ulimit -n 4096 && cargo nextest run --workspace. Some terminals and inherited agent sessions start with ulimit -n 256, which is too low for the shared CLI test daemon under parallel nextest load.
TypeScript (fabro-web)
cd apps/fabro-web && bun run dev— rebuild web assets on change for the Rust server; refresh the browser manuallycd apps/fabro-web && bun test— run testscd apps/fabro-web && bun run typecheck— type checkcd apps/fabro-web && bun run build— production build
Marketing site (apps/marketing)
cd apps/marketing && bun run dev— start Astro dev servercd apps/marketing && bun run build— production buildcd apps/marketing && bunx vercel --prod— deploy to Vercel (project: website, domain: fabro.sh)
Dev servers
fabro server start— starts the Rust API server (demo mode is per-request viaX-Fabro-Demo: 1header)cd apps/fabro-web && bun run dev— rebuilds web assets on change; refresh the browser manually- Mintlify docs dev server (requires Docker —
mintlify devneeds Node LTS which may not match the host):
Then open http://localhost:3333. Stop withdocker run --rm -d -p 3333:3333 -v $(pwd)/docs:/docs -w /docs --name mintlify-dev node:22-slim \ bash -c "npx mintlify dev --host 0.0.0.0 --port 3333"docker stop mintlify-dev.
API workflow
The OpenAPI spec at docs/api-reference/fabro-api.yaml is the source of truth for the fabro-api HTTP interface.
- Edit
docs/api-reference/fabro-api.yaml cargo build -p fabro-api— build.rs regenerates Rust types and client via progenitor- Write/update handler in
lib/crates/fabro-server/src/server.rs, add route tobuild_router() cargo nextest run -p fabro-server— conformance test catches spec/router driftcd lib/packages/fabro-api-client && bun run generate— regenerates TypeScript Axios client
Architecture
Fabro is an AI-powered workflow orchestration platform. Workflows are defined as Graphviz graphs, where each node is a stage (agent, prompt, command, conditional, human, parallel, etc.) executed by the workflow engine.
Rust crates (lib/crates/)
- fabro-cli — CLI entry point. Commands:
run,exec,serve,validate,parse,cp,model,doctor,install,ps,system prune - fabro-workflow — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, retros, and human-in-the-loop interactions
- fabro-agent — AI coding agent with tool use (Bash, Read, Write, Edit, Glob, Grep, WebFetch).
Sandboxtrait abstracts execution environments - fabro-server — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header
- fabro-llm — Unified LLM client with providers: Anthropic, OpenAI, Gemini, OpenAI-compatible, plus retry/middleware/streaming
- fabro-api — Auto-generated Rust types and reqwest HTTP client from OpenAPI spec (build.rs + progenitor)
- fabro-github — GitHub App auth (JWT signing, installation tokens, PR creation)
- fabro-mcp — Model Context Protocol client/server
- fabro-slack — Slack integration (socket mode, blocks API)
- fabro-devcontainer — Parses
.devcontainer/devcontainer.jsonfor container setup - fabro-checkpoint — Git-based checkpoint storage with branch store and metadata branches
- fabro-telemetry — CLI analytics (Segment) and crash reporting (Sentry), with anonymous IDs, command sanitization, and detached subprocess delivery
- fabro-util — Shared utilities (redaction, terminal formatting)
TypeScript (apps/ and lib/packages/)
- apps/fabro-web — React 19 + React Router + Vite + Tailwind CSS frontend
- lib/packages/fabro-api-client — Auto-generated TypeScript Axios client from OpenAPI spec
Key design patterns
- Sandbox trait — Uniform interface for local, Docker, and Daytona execution environments
- Graphviz graph workflows — Stages and transitions defined as Graphviz graph attributes
- OpenAPI-first —
fabro-api.yamldrives Rust type + client generation (progenitor) and TypeScript client generation (openapi-generator) - Checkpoint/resume — Workflows can be paused, checkpointed, and resumed
Strategy docs
When working on Rust crates, read the relevant strategy doc before making changes:
docs-internal/logging-strategy.md— read when addingtracingcalls (info!,debug!,warn!,error!), working on error handling paths, or adding new operations that should be observabledocs-internal/events-strategy.md— read when adding or modifyingEventvariants, touchingEmitter/emit(), changingprogress.jsonloutput, or adding new workflow stage typesfiles-internal/testing-strategy.md— read when adding or reorganizing tests, choosing between unit vstests/it, deciding whether a test belongs incmdvsworkflowvsscenario, or deciding how to structure snapshots and fixtures
Shell quoting in sandbox code
When interpolating values into shell command strings (in fabro-workflow), always use the shell_quote() helper (backed by shlex::try_quote). Never use manual replace('\'', "'\\''") or unquoted interpolation. This applies to file paths, branch names, URLs, env vars, image names, glob patterns, and any other user-controlled input assembled into a shell script.
Rust import style
- Types (structs, enums, traits): import by name —
use crate::outcome::Outcome; - Functions: import the parent module, call as
module::function()—use fabro_workflow::operations; operations::create(...) - No glob imports in production code (
use foo::*). Globs are acceptable in test modules and preludes. Enforced by clippywildcard_importslint.
Snapshot tests (insta)
Many CLI tests use insta inline snapshots. When a snapshot needs updating:
- Run
cargo insta pending-snapshotsto list what changed - Verify each pending snapshot is expected
- Run
cargo insta acceptto accept all, orcargo insta accept --snapshot <path>for a specific one
Never run cargo insta accept without first checking what's pending — it accepts all pending snapshots, which may include unrelated changes.
Testing workflows
fabro run <name>— run a workflow by name (resolves.fabro/workflows/<name>/workflow.toml), e.g.fabro run repl- Use
--no-retroto skip the retro step and finish faster #[e2e_test(twin, live("VAR"))]— dual-mode test that runs against twin-openai or real API.#[e2e_test(twin)]for twin-only tests (e.g., scripted failures).#[e2e_test(live("VAR"))]for live-only tests requiring secrets.#[e2e_test()]for sandbox tests with no API deps. Behavior is controlled byFABRO_TEST_MODE(live,strict; default istwin), andcargo nextest run --profile e2e ...impliesstrict. Usefabro_test::e2e_openai!()in twin/dual-mode tests to get(base_url, api_key).- Local test HTTP clients must use
.no_proxy(). Prefer shared helpers likefabro_test::test_http_client()or crate-local equivalents instead ofreqwest::Client::new(), bareClient::builder().build(), orreqwest::get(...). - This is not cosmetic: macOS proxy discovery adds hidden startup overhead to repeated localhost reqwest clients and can surface as misleading nextest timeouts.