Skip to content

fleet-harness

FLT-1 — the universal harness. A LangGraph supervisor that routes ticket-sized coding work across four vendor CLI agents (codex, claude, gemini via agy, grok) plus local Ollama, and keeps working when any single vendor — especially Claude — is down.

Why this exists (outage continuity)

A two-day Claude outage in July 2026 froze a fleet whose orchestration lived inside Claude Code. Lesson: the orchestrator that covers a vendor outage must not depend on that vendor. So:

  • The supervisor brain is a swappable LangChain chat model whose default is local Ollama (qwen3-coder:30b on the gaming PC) — $0, on-LAN, no API key required, no cloud dependency.
  • Routing is deterministic config (routing.yaml), not model vibes.
  • Every seat has an availability probe and an escalation ladder; a dead vendor reroutes work instead of stopping it.
  • Acceptance test: mark Claude down, and a ticket still flows route → implement (codex) → blind review (grok) → validate → human gate.

Language note: this repo is an approved deviation from the fleet's C#-first policy. Reasons: LangGraph/Deep Agents are the ecosystem standard for this exact problem, the skills are career-relevant, and the outage-continuity property argues for maximum-ecosystem tooling.

Architecture

flowchart LR
    subgraph supervisor["fleet-harness (Python · LangGraph)"]
        R["route<br/>deterministic keywords<br/>+ optional local-LLM refine"] --> I["implement<br/>isolated git worktree<br/>escalation ladder"]
        I --> V1["review<br/>BLIND: spec + diff + test claims<br/>DIFFERENT provider · FLT-4<br/>read-only sandbox cwd"]
        V1 --> V2["validate<br/>reruns tests itself<br/>never trusts claims · FLT-3"]
        V2 --> H{{"human gate<br/>LangGraph interrupt"}}
        R --> A["advise<br/>text-only roles<br/>no worktree, no tests"]
        A --> H
        R -. all seats down .-> H
        I -. all implementers failed,<br/>or agent asked for a human .-> H
    end

    subgraph seats["agent seats (FLT-2 contract) · grouped by CAPACITY POOL"]
        C1["codex<br/><i>pool: codex</i>"]
        C2["claude<br/><i>pool: native-claude</i>"]
        C3["gemini-flash · gemini-pro-high<br/><i>pool: agy-gemini (SHARED)</i>"]
        C4["grok<br/><i>pool: grok</i>"]
        C6["agy-gpt-oss<br/><i>pool: agy-gpt-oss (scarce)</i>"]
        C7["agy-claude<br/><i>pool: agy-claude · RESERVE</i>"]
        C5["ollama<br/><i>pool: local · advisory</i>"]
    end

    I --> C1 & C2 & C3 & C4 & C6
    V1 --> C1 & C2 & C3 & C4 & C6
    R --> C5
    H -. "--break-glass only" .-> C7

    Y[("routing.yaml<br/>FLT-6 · one table,<br/>three readers")] --> R
    B[("brain: ChatOllama default<br/>FLEET_BRAIN swaps provider")] --> R

Every seat returns one uniform JSON shape (FLT-2 contract, docs/contract.md): {status, summary, changed_files, tests_attempted, known_risks, needs_human_review}.

The advise branch exists because text-only work (triage, summaries, format checks) has no diff to isolate, review, or test — and because the local Ollama seat cannot edit files. Without that split, the "can this agent edit?" filter would quietly reroute cheap text work to an expensive coding CLI. routing.yaml names those roles in advisory_roles.

Capacity: the ladder's rungs are not free bandwidth

The tempting model — "if this seat is busy, try the next one" — is wrong here, and getting it wrong is expensive:

  • Four seats are agy lanes on one Google account, and gemini-flash and gemini-pro-high draw on the same pool. Throttled Flash does not make Pro-High a fallback; it is the same dead quota wearing a different label.
  • When an agy pool locks out, its load transfers to Codex/Grok and accelerates their limits. Degradation is correlated, not graceful.

So routing.yaml models pools, and the router spends pools, not seats:

Rule Behavior
A rate_limited seat locks its pool every sibling seat on that pool drops out of the run
After max_pool_lockouts (default 1) non-local pools lock the ladder stops and escalates to the human, rather than spending pools whose remaining capacity is unknown
One lockout still escalates — a single 429 is not an outage
Local pool never counts against the budget; it cannot rate-limit us
agy-claude (reserve, ~1–2 calls/window) not on any ladder, unreachable by any classifier; needs --break-glass {native_red,review_gate,council_synth}, which appends it as the last resort and records the reason at the gate
Blind review excludes the implementer's family — agy's Claude lanes are Anthropic seats behind a Google login

This is heuristic until FLT-15 lands. There is no quota API, so nothing here can measure how close Codex is to its cap. capacity.CapacityLedger is the seam a real fleet-global ledger plugs into; the default NullLedger knows only what the current run observed and answers cap_proximity() → Noneunknown, said out loud, rather than a comforting 0.0. Treating None as "fine" is the exact bug this design exists to prevent.

Trust boundaries (the interesting part)

Policy Where enforced
FLT-3: never trust agent claims validation.py reruns tests and records the real exit code; worktree.py measures changed files from git; base.py downgrades a "completed" claim on a nonzero exit
FLT-4: blind, cross-vendor review graph.py review node — the reviewer gets spec + diff + labelled-unverified test claims, never the implementer's narrative, and is always a different provider
Reviewers judge, they don't touch the reviewer runs in a throwaway empty directory, not the implementer's worktree — it is a full editing CLI, and editing the code we just measured would invalidate the diff, the file list, and the validation run. The tree is re-measured afterwards and any drift is reported
Isolation every implement attempt runs in a fresh git worktree; failed attempts are swept so half-edits can't leak into the next agent's run. The freeform Deep Agents mode isolates per tool call the same way
Secrets don't ride the diff the diff is sent to third-party CLIs, so worktree.redact_secrets withholds the contents of secret-shaped files (.env, *.pem, ...) while still showing that they changed; redactions are listed at the human gate
Human authority the graph's only exit is a LangGraph interrupt() approval gate. An agent returning approval_required goes straight there — no further agents or tests run past a control point it raised

Not a security sandbox. Worktree isolation is a blast-radius control, not a boundary against a hostile agent: the vendor CLIs execute tools with your privileges and can use absolute paths. Ticket text is a prompt-injection surface, so treat tasks as trusted input. Running untrusted tickets needs a real boundary (container / AppContainer) — that is deliberately out of scope for this scaffold.

How it rides the C# rails

This harness is the thin Python head on an existing C# body — it replaces no working tool:

  • FLT-2 contract: docs/contract.md is the canonical schema; a C# implementation (fleet-dispatch run-agent) is being built in parallel. The subprocess wrappers here are shaped so swapping codex exec ... for fleet-dispatch run-agent ... changes one command line and nothing else.
  • FLT-6 routing table: routing.yaml at the repo root is read by this harness and (by design) by usage-governor and fleet-dispatch — one table, three readers, zero policy forks.
  • Worktree isolation, review gates, and the never-trust rule mirror the fleet's standing C# policies, so work landed through either rail meets the same bar.

Run it

# install (Python >= 3.11)
uv sync

# what the router sees
uv run python -m fleet_harness probe

# route one ticket
uv run python -m fleet_harness run `
    --task "Add a retry loop to the fetcher and cover it with a test" `
    --repo C:\path\to\target-repo `
    --ticket FLT-42

# FLT-1 acceptance: prove Claude-outage continuity
uv run python -m fleet_harness run `
    --task "Add a retry loop to the fetcher" `
    --repo C:\path\to\target-repo `
    --ticket FLT-42 `
    --simulate-down claude

The run parks at the human gate with a full evidence packet (attempts, diff stats, review verdict, measured validation result) and asks for approval; approved work stays on its wt/<ticket>/harness-* branch for normal landing. --yes auto-approves (CI/demo only), --no-brain skips the LLM classifier for fully-offline deterministic routing.

Configuration

Env var Default Meaning
FLEET_BRAIN (unset) Swap the supervisor brain to any LangChain provider, e.g. anthropic:claude-sonnet-5. Uses that provider's own env auth. Never required.
FLEET_OLLAMA_URL http://10.0.0.7:11434 → localhost fallback Ollama server for the default brain + ollama seat
FLEET_OLLAMA_MODEL qwen3-coder:30b Local model tag

--break-glass <reason> spends the agy-claude reserve for one run. Reason drift toward "convenience" is a policy failure, not a judgement call — the code lands in the human-gate evidence packet so it stays visible.

Board Sync

docs/BOARD.md is the source of truth until Jira passes the board-as-contract checklist. board-sync mirrors one status transition into the local board and any configured remote IDs in docs/board-sync.json.

# dry-run first: prints the file and remote operations, writes nothing
uv run python -m fleet_harness board-sync `
    --ticket FLT-11 `
    --status "In Progress" `
    --mover WT-77c4

# write the board plus configured Vikunja/Jira transitions
uv run python -m fleet_harness board-sync `
    --ticket FLT-11 `
    --status "In Review" `
    --mover WT-77c4 `
    --write

# retry remotes after a transient API failure; leaves BOARD.md unchanged
uv run python -m fleet_harness board-sync `
    --ticket FLT-11 `
    --status "In Review" `
    --mover WT-77c4 `
    --write `
    --remotes-only

Remote writes are config-gated. Vikunja needs FLEET_VIKUNJA_URL and FLEET_VIKUNJA_TOKEN; Jira needs FLEET_JIRA_URL, FLEET_JIRA_EMAIL, and FLEET_JIRA_TOKEN. The mapping file shape is:

{
  "tickets": {
    "FLT-11": {
      "vikunja": {
        "id": "123",
        "status": "Backlog",
        "transitions": {
          "In Progress": { "bucket_id": 456 },
          "Done": { "done": true }
        }
      },
      "jira": {
        "id": "FLT-11",
        "status": "Backlog",
        "transitions": {
          "In Progress": "21",
          "Done": "31"
        }
      }
    }
  }
}

Tests

uv run pytest -q

The suite runs the real compiled graph against the shipped routing.yaml with fake vendor CLIs/git (see tests/conftest.py), and includes the acceptance test test_acceptance_simulate_down_claude.

Layout

routing.yaml                  FLT-6 role table + escalation ladder (shared w/ C#)
docs/contract.md              FLT-2 canonical result schema (shared w/ C#)
docs/BOARD.md                 delivery board (canonical)
src/fleet_harness/
  graph.py                    the supervisor graph (start reading here)
  routing.py                  deterministic router + availability failover
  capacity.py                 pools, correlated-failure budget, FLT-15 ledger seam
  agents/                     CLI wrappers: codex, claude, agy, grok, ollama
  contract.py                 FLT-2 dataclass + defensive parsing
  worktree.py                 git worktree isolation
  validation.py               FLT-3 rerun-the-tests gate
  supervisor.py               freeform Deep Agents mode (secondary)
  cli.py                      python -m fleet_harness
tests/                        real graph, fake edges, offline