Move the built web bundle into an embedded fabro-spa crate so Cargo and release builds no longer depend on Bun at build time, and preserve the local dev override path for fast UI iteration. At the same time, rename interview and agent-level aborted flows to interrupted, keep cancelled for run-level shutdown, and stop reporting skipped answers as interruptions in the run event stream.
8.2 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 fmt --check --all— check formattingcargo clippy --workspace -- -D warnings— lint
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 (resolvesfabro/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.