fabro/AGENTS.md
Bryan Helmkamp 1048534e2c
ci: switch clippy to pinned nightly, clean up workspace lints
- 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>
2026-04-16 18:59:11 -04:00

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 crates
  • cargo nextest run --workspace — run all unit tests
  • cargo nextest run -p fabro-server — test a single crate
  • cargo nextest run -p fabro-workflow -- test_name — run a single test
  • set -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 crate
  • cargo +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-format
  • cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings — lint (CI runs nightly clippy to match; install with rustup 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 manually
  • cd apps/fabro-web && bun test — run tests
  • cd apps/fabro-web && bun run typecheck — type check
  • cd apps/fabro-web && bun run build — production build

Marketing site (apps/marketing)

  • cd apps/marketing && bun run dev — start Astro dev server
  • cd apps/marketing && bun run build — production build
  • cd apps/marketing && bunx vercel --prod — deploy to Vercel (project: website, domain: fabro.sh)

Dev servers

  1. fabro server start — starts the Rust API server (demo mode is per-request via X-Fabro-Demo: 1 header)
  2. cd apps/fabro-web && bun run dev — rebuilds web assets on change; refresh the browser manually
  3. Mintlify docs dev server (requires Docker — mintlify dev needs Node LTS which may not match the host):
    docker 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"
    
    Then open http://localhost:3333. Stop with 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.

  1. Edit docs/api-reference/fabro-api.yaml
  2. cargo build -p fabro-api — build.rs regenerates Rust types and client via progenitor
  3. Write/update handler in lib/crates/fabro-server/src/server.rs, add route to build_router()
  4. cargo nextest run -p fabro-server — conformance test catches spec/router drift
  5. cd 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). Sandbox trait 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.json for 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-firstfabro-api.yaml drives 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 adding tracing calls (info!, debug!, warn!, error!), working on error handling paths, or adding new operations that should be observable
  • docs-internal/events-strategy.md — read when adding or modifying Event variants, touching Emitter/emit(), changing progress.jsonl output, or adding new workflow stage types
  • files-internal/testing-strategy.md — read when adding or reorganizing tests, choosing between unit vs tests/it, deciding whether a test belongs in cmd vs workflow vs scenario, 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 clippy wildcard_imports lint.

Snapshot tests (insta)

Many CLI tests use insta inline snapshots. When a snapshot needs updating:

  1. Run cargo insta pending-snapshots to list what changed
  2. Verify each pending snapshot is expected
  3. Run cargo insta accept to accept all, or cargo 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-retro to 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 by FABRO_TEST_MODE (live, strict; default is twin), and cargo nextest run --profile e2e ... implies strict. Use fabro_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 like fabro_test::test_http_client() or crate-local equivalents instead of reqwest::Client::new(), bare Client::builder().build(), or reqwest::get(...).
  • This is not cosmetic: macOS proxy discovery adds hidden startup overhead to repeated localhost reqwest clients and can surface as misleading nextest timeouts.