@andrewhopper/fd
v0.1.6
Published
Flight Director — visualize and monitor AI agent state machines
Maintainers
Readme
Flight Director
A sidecar supervisor for coding agents — it doesn't run the agent, it watches and constrains one that's already running.
Define DAG workflows in YAML that constrain AI agent behavior with tool gates, budgets, and transition rules. Unlike agent frameworks that orchestrate execution, Flight Director sits alongside an autonomous agent and enforces what it's allowed to do at each phase.
Prerequisites
- Rust 1.70+ — install via rustup.rs
- Node.js 18+
- jq
- git
◉ diagnose ──[found_it]──→ ○ fix ──[fix_applied]──→ ○ verify ──[tests_pass]──→ ◎ done
Read,Grep,Glob only no WebSearch/WebFetch Read,Bash,Grep only
500 tokens / 20 calls 500 tokens / 20 calls 300 tokens / 10 callsHow it works
Every tool call flows through the gate engine. On allow, a hint is injected back into the agent's context with the current state, instructions, available transitions, budget status, and criteria progress. On block, the agent gets a rich error explaining why and what to do instead.
What it does
- Define workflows as YAML state machines — states, transitions, tool gates, budgets, exit criteria
- Enforce constraints — each state restricts which tools the agent can use, what files it can touch, and how many tokens/calls it can spend
- Track progress — session state persists to disk, transitions are recorded, budgets are monitored
Two ways to use it:
| | Rust CLI (recommended) | Viewer (full-stack) |
|---|---|---|
| What | Compiled binary with Claude Code hook integration | React + Express app with interactive graph visualization |
| For | Claude Code — enforces constraints via PreToolUse/PostToolUse hooks | Local dev — watch agents traverse the DAG in real-time |
| Run | fd-cli init --machine machine.yaml | cd viewer && npm run dev |
Quick Start
./bin/fd-cliThat's it. On first run, Flight Director walks you through setup — checks prerequisites, builds the Rust CLI, configures hooks for your AI tools, and lets you pick a workflow machine. On subsequent runs it prints session status and starts the viewer.
./bin/fd-cli --setup # Force re-run the config flow
./bin/fd-cli --no-viewer # Print status only
./bin/fd-cli --machine tdd # Skip the picker, init with a specific machineTo build from source directly:
cargo build --releaseWhich path?
| Goal | Section | |------|---------| | Just trying it? | Quick Start | | Building from source? | Rust CLI | | Web viewer? | Viewer Setup | | Embedding in a project? | Subtree |
Rust CLI (manual setup)
# Build and configure hooks for your AI tools
bash scripts/setup-hooks.sh
# Initialize a session
./target/release/fd-cli init --machine docs/scenarios/simple-bugfix.yaml
# Check status
./target/release/fd-cli status
# Transition
./target/release/fd-cli transition found_it
# View log
./target/release/fd-cli logsetup-hooks.sh builds the binary and auto-detects which AI tools you have installed (Claude Code, Gemini CLI, Kiro). See Hook Setup for details.
Viewer (full-stack)
# First-time setup
bash scripts/bootstrap.sh
# Start dev server
cd viewer
npm run devOpens at localhost:5199 (client) with API at localhost:3199 (server).
Hook Setup
scripts/setup-hooks.sh auto-detects installed AI tools and configures each to call fd-cli via hooks. It builds the Rust binary, runs bootstrap if needed, and writes the appropriate settings file for each tool.
# Configure all detected tools
bash scripts/setup-hooks.sh
# Configure a specific tool only
bash scripts/setup-hooks.sh --tool claude-code
bash scripts/setup-hooks.sh --tool gemini-cli
# Remove Flight Director hooks from all tools
bash scripts/setup-hooks.sh --uninstall
# Skip the cargo build (binary must already exist)
bash scripts/setup-hooks.sh --skip-buildSupported tools
| Tool | Status | Settings file | Hook events |
|------|--------|---------------|-------------|
| Claude Code | Native | .claude/settings.json | PreToolUse, PostToolUse |
| Gemini CLI | Adapter | .gemini/settings.json | BeforeTool, AfterTool |
| Kiro | Template | .kiro/settings.json | PreToolUse, PostToolUse |
| Codex | Stub | — | Hooks not yet upstream |
Claude Code calls fd-cli directly (native protocol). Gemini CLI and Kiro go through a universal shim (scripts/hooks/fd-shim.sh) that translates between each tool's stdin/stdout format and fd-cli's HookInput/HookOutput protocol.
The script is idempotent — re-running it replaces existing Flight Director hooks without duplicating them, and preserves any non-Flight Director hooks in the settings file.
Adding a new tool
Create a single file in scripts/hooks/adapters/ implementing the adapter interface:
adapter_name() # "my-tool"
adapter_label() # "My Tool" (human-readable)
adapter_detect() # exit 0 if tool is installed
adapter_ready() # exit 0 if hooks are supported
adapter_settings_path() # path to settings file
adapter_hooks_json() # JSON fragment with hook entries
adapter_uninstall_filter() # jq filter to remove Flight Director hooksFor tools that need stdin/stdout translation (not native), also implement:
adapter_event_to_fd() # map tool event → fd-cli subcommand
adapter_translate_input() # jq filter: tool stdin → HookInput
adapter_translate_output_allow() # jq filter: fd-cli allow → tool allow
adapter_translate_output_block() # jq filter: fd-cli block → tool blockNo changes to setup-hooks.sh or fd-shim.sh are needed. See docs/hooks.md for the fd-cli hook protocol.
Writing Machines
Machines are YAML files. Here's the simplest possible one:
id: yolo
name: "YOLO"
description: "No guardrails."
override_policy:
allow_override: true
allow_waiver: true
require_reason: false
hard_criteria_bypass: true
states:
- id: code
description: "Write code."
initial: true
transitions:
- name: ship_it
target:
state: ship
- id: ship
description: "Deploy."
transitions:
- name: done
target:
state: done
- id: done
description: "In production."
terminal: trueTool Gates
Constrain which tools the agent can use in a state:
# Only allow these tools
tool_gates:
- gate_type: tool_allowlist
params:
tools: [Read, Grep, Glob]
message: "Read-only — no edits allowed"
hard: true
# Block specific tools
- gate_type: tool_denylist
params:
tools: [WebSearch, WebFetch]
message: "No web access"
hard: false
# Restrict bash commands
- gate_type: bash_policy
params:
allow_commands: ["npm test", "npm run lint"]
deny_commands: ["rm -rf", "git push --force"]
message: "Safe commands only"
hard: true
# Restrict file paths for Write/Edit
- gate_type: path_pattern
params:
tools: [Write, Edit]
allow_patterns: ["tests/**", "**/*.test.*"]
deny_patterns: ["src/**"]
message: "Only test files may be edited"
hard: trueGate types: tool_allowlist, tool_denylist, bash_policy, path_pattern, input_pattern
When hard: true, the constraint is absolute. When hard: false, it's a soft warning.
Criteria
Conditions evaluated during transitions. Eight built-in types: min_tool_calls, max_tool_calls, min_tokens, max_tokens, min_duration, max_duration, command, file_exists. See docs/criteria.md for full reference.
Budgets
Limit resource consumption per state:
budget:
max_tokens: 5000
max_tool_calls: 30
max_wall_time_ms: 300000
thresholds:
- percent: 70
action: Notify
- percent: 90
action: BlockExit Criteria
Conditions that must be met before leaving a state:
exit_criteria:
- criterion_type: command
params:
command: "npm test"
expect_exit_code: 0
message: "Tests must pass"
hard: trueAuto-transitions
Transitions that fire automatically when a condition is met:
transitions:
- name: tests_pass
target:
state: refactor
auto:
criterion_type: command
params:
command: "npm test"
expect_exit_code: 0
message: "Auto-advance when tests pass"Fork/Join (Concurrency)
transitions:
- name: parallelize
target:
type: Fork
targets: [lint, test, typecheck]
join: merge-resultsData Flow
Pass named data between states — upstream states declare outputs, downstream states declare inputs:
states:
- id: triage
data_flow:
outputs:
- key: bug_classification
description: "Severity, component, symptoms"
- id: reproduce
data_flow:
inputs:
- key: bug_classification
from_state: triage
outputs:
- key: error_trace
description: "Stack trace and repro steps"Partitions
Split work across keys (e.g., test files). On re-entry, only failed partitions re-run:
partitions:
keys: ["tests/**/*.test.ts"]
redo_failed_only: trueRetry Policy
Auto-retry a state on failure with configurable backoff (None, Linear, Exponential):
retry:
max_retries: 3
backoff: Exponential
base_delay_ms: 2000
max_delay_ms: 30000
retryable: [CriterionNotMet, Timeout]Timeout
Time-limit a state with heartbeat monitoring:
timeout:
state_timeout_ms: 600000
on_timeout: ForceTransition # Notify | ForceTransition | Abort | Retry
timeout_transition: next-state
heartbeat_interval_ms: 30000
on_heartbeat_missed: NotifySpawn (Multi-Agent)
Launch concurrent agents with convergence strategies:
# State-level spawn
spawn:
strategy: Race # Explore | Race | Vote
max_agents: 2
convergence:
min_agree: 1
timeout_ms: 600000
on_timeout: AbortSlowest
on_limit: NotifyCoordinator
# Machine-level spawn with parameter matrix
spawn:
spawn_point: explore
count: 3
strategy: Explore
params_matrix:
- { approach: "top-down" }
- { approach: "bottom-up" }
- { approach: "lateral" }
convergence:
max_wall_time_ms: 600000
on_limit: AbortSee docs/advanced-features.md for full reference on all advanced features.
Example Machines
| Machine | States | Description |
|---------|--------|-------------|
| yolo | 3 | No constraints, manual transitions |
| simple-bugfix | 4 | diagnose → fix → verify → done |
| tdd | 7 | Red-green-refactor cycle with loops |
| complex-bugfix | 8 | Retry, heartbeat, bash policy, data flow |
| bdd | — | Behavior-driven development |
| greenfield-landing-page | — | Landing page from scratch |
| greenfield-mobile-app | — | Mobile app from scratch |
All live in docs/scenarios/.
Documentation
| Doc | What it covers | |-----|----------------| | docs/rust-cli.md | Rust CLI command reference | | docs/criteria.md | Criteria system — all 8 types, hard/soft, waivers | | docs/advanced-features.md | Data flow, partitions, retry, timeout, spawn, join | | docs/hooks.md | Hook protocol (PreToolUse/PostToolUse) and fd-cli integration | | docs/override-policy.md | Override policy, waivers, rollback, trigger types | | docs/architecture.md | Rust crate architecture, data flow, dependencies | | docs/diagrams/ | PlantUML sequence and component diagrams | | docs/types/ | HTML type reference pages |
Architecture (Viewer)
viewer/src/
├── shared/ # Types + YAML parser (used by both)
│ ├── types/ # Branded IDs, MachineDef, gates, budget, runtime
│ └── yaml-parser # YAML → MachineDef
├── server/ # Express + WebSocket
│ ├── routes/ # /api/machines, /api/sessions, /api/health
│ ├── data-source/ # FileReader + ZMQ + HybridSource
│ └── ws/ # WebSocket bridge
└── client/ # React 19 + Vite
├── components/ # atoms/ molecules/ organisms/ graph/
├── stores/ # Zustand (machine, session, ui, graph, ws)
├── hooks/ # useWebSocket, useSessionData, useBreadcrumbNav
└── lib/ # graph-builder, layout (Dagre), colors, formatData flow: files on disk → Express reads → ZMQ triggers re-reads → WebSocket pushes to React → Zustand stores → graph re-renders.
Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| ASM_PORT | 3199 | Express server port |
| ASM_HOST | 127.0.0.1 | Express server host |
| ASM_DIR | $CWD/.claude/asm/ | Data directory |
| ZMQ_ENDPOINT | ipc:///tmp/asm.sock | ZMQ socket for live updates |
Project Structure
flight-director/
├── Cargo.toml # Rust workspace root
├── crates/
│ ├── fd-core/ # Library — types, engine, storage, display
│ └── fd-cli/ # Binary — CLI commands
├── docs/
│ ├── scenarios/ # Example YAML machines
│ ├── diagrams/ # PlantUML architecture diagrams
│ ├── types/ # HTML type reference pages
│ ├── rust-cli.md # CLI command reference
│ ├── criteria.md # Criteria system reference
│ ├── hooks.md # Claude Code hook integration
│ ├── override-policy.md # Override & waiver system
│ ├── architecture.md # Rust crate architecture
│ └── advanced-features.md # Data flow, partitions, retry, timeout, spawn
├── scripts/
│ ├── bootstrap.sh # First-time setup
│ ├── setup-hooks.sh # Configure AI tool hooks
│ └── hooks/
│ ├── fd-shim.sh # Universal shim for non-native tools
│ └── adapters/ # One file per tool (claude-code, gemini-cli, kiro, codex)
├── viewer/ # Full-stack viewer app (React + Express)
└── machines/ # Runtime machine instancesLicense
MIT
