Reference

How the loop is actually built.

This page is the reference layer — tables, topology and the tool groups. For the short version of each idea, start with concepts. For the argument in the product, the pipeline, then the four mechanisms: spec → done, the fleet, the living graph, memory.

Graphban is a FastAPI + Postgres/pgvector backend, a React SPA, and one 77-tool MCP — all sitting on a single shared service layer. Here's the architecture, the agent-loop mechanics, how authority is enforced and audited, and the data model that makes it work.

System topology

Two entry planes, one code path

Humans enter through REST with a JWT; agents enter through the MCP endpoint with a scoped API key. Both converge on the same service layer, which is the only thing that touches the database.

Web SPA
React 19 · TanStack · JWT
Agents / MCP clients
API key · JSON-RPC 2.0
↓ REST routers        MCP server (77 tools) ↓
Shared service layer
app/services/* — items · memory · prds · assistant · clustering · collision · prioritization · code_graph · events · quotas · orgs · drive_sync
↓ SQLAlchemy models · Alembic migrations
Postgres + pgvector
dialect-aware EmbeddingType · HNSW cosine index

Why the service layer is the keystone

Routers and the MCP server are thin adapters — every tool call and every REST request runs the same function. There is exactly one path to the database, so an agent's create_item appears in the web tracker with no reconciliation, and every capability is available to both people and agents for free.

The differentiator

The agent loop, mechanic by mechanic

prd_coverage
specced but unbuilt
→
decompose_prd
spec → tasks
→
get_backlog
dep-aware, code-local
→
claim_next
atomic, leased
→
update done
→ memory
A · Claiming & leases

Collision-free assignment

claim_next reads the best ready candidate then does an optimistic UPDATE guarded on claimed_by — only one caller wins the row, so two agents never claim the same item. A claim carries a lease (claimed_at); heartbeat extends it, and a lease that goes stale past the window is reclaimable, so a crashed agent's in-progress work is picked up by another. agent_id defaults to the API key's name — one key, one agent.

B · Touchpoints & clusters

Batching by code-locality

Items declare touchpoints — files, globs, or modules they affect. Two items relate when touchpoints overlap (exact, glob, or same directory), and overlap auto-creates a code link. next_cluster claims the best ready item plus its related ready neighbours in one call, so an agent pulls a whole code-neighborhood and works it in one context instead of thrashing.

C · Prioritization

Priority from the dependency graph

Readiness is computed, not flagged: an item is blocked until every item it depends on is done. The backlog sorts ready-first, then by a composite score — status, dependency fan-out (unblocking many ranks higher), request votes rolled onto the linked item, effort, and staleness. claim_next and next_cluster consume the same ranking, so agents always take the highest-leverage ready work.

D · Grill & spec coverage

Grill the spec, then it drives the tracker

PRDs are authored and sharpened by grilling — an interactive interrogation (grill_prd / a streaming grill session) that asks relentless clarifying questions and classifies each open question low- vs high-fidelity (the latter needs a prototype: the grill → prototype → grill handoff). Every decision is preserved as a candidate memory shard. Then decompose_prd parses the spec's sections into a task per gap and prd_coverage returns the per-section rollup — status counts, percent done, and outstanding high-fidelity work.

Institutional memory you can trust

Completing an item auto-extracts its lessons into pgvector memory — but agent-written memory is telemetry, not truth. Lessons (and grill decisions) enter as candidates; a human publishes them before search_memory serves them, and recurring ones cluster into one principle. The loop compounds reviewed knowledge instead of letting a hallucination become ground truth.

Agent interface

The tools, by group

JSON-RPC 2.0 over POST /api/mcp. A key is global by default (project_id null) and each call names the project. A key can still be pinned to one project when it must not see the others. Every tool ships a typed outputSchema, read-only / destructive annotations, and idempotency keys on creates; reads paginate. Arguments are validated against the schema before dispatch, and failures return a typed code — not_found, validation, conflict, unauthorized, internal — with a hint naming the fix.

A key is never shipped schemas it cannot use: a read-only key sees no write tools, and a key enrolled to one role is not shipped the other roles' tools. The manifest is a courtesy; the gate is the enforcement — a manifest can only fail to mention a tool, while the gate refuses the call.

The in-app MCP catalog is the full list. This page names the groups. The live count is 77.

GroupWhat it is for
Orientationget_context first, then list_projects, create_project, setup_project. The last is the resumable first-run checklist.
Work and fleetClaim, heartbeat, release, clusters, review, and sign-off. delegate records a handoff and, with a seat, mints one bound to that item. retire_wave revokes the seats you minted. It does not stop the processes.
Memory and lessonsCandidates, publish, and the learning loop. get_lessons reads the catalog. A published lesson nobody retrieved is a miss.
PRDs and acceptanceGrill, coverage, verdicts, and get_prd. prd_acceptance is eleven views. None of them says complete.
Code graphDescribe, map, neighbors, search, a structural query, and the links between an item and a path.
Scansrecord_scan, scan_status, scans_due. A component that was never scanned reads as never scanned.
Rules and entitlementsget_rule_toggles is read-only, so an agent cannot switch off a rule that reviews it. list_entitlements, set_entitlement, report_entitlement_event, and report_utilization are how capacity routing is observed and recorded.
Documentsput_document, get_document, search_docs. Search covers baselined PRD bodies. Drafts and design rows are opt-in.
Design setsopen_design, put_preamble, put_set_constraints, confirm_tier, compile_prompts, put_canvas, file_finding, resolve_finding, get_design_set. A finding with a canvas check stays open until the bound canvas passes it.

Every accepted write is metered and recorded to an append-only audit ledger, attributed to the calling key.

Trust boundary

Authority is enforced, and audited

Authentication proves identity; authorization decides what an identity may touch — and the two are separate contracts. Capability and permission never get conflated.

Scoped, not just labelled

A key can't out-rank its owner

Every mutation is bounded by the key's declared scopes and its owner's project memberships, checked in one place (security/authz.py) at both the MCP and REST boundaries. A project-scoped key can't reach another project's data — not even by passing a different project_id. Non-members get an existence-hiding 404; a read-only member gets an honest 403.

The ledger

Every change, on the record

One append-only events row per accepted mutation — actor (the agent key and the human principal behind it, or the user), action, target, project, timestamp — written at the boundary so attribution is never lost. The Activity view answers "what did my agents do, and who ran them" directly; auditing an authority model is what makes it a real control rather than a claim.

Human plane

An in-app assistant on the same rails

Beyond the agent loop, a person can open any item or PRD and chat with a model about it over SSE. The assistant is a provider-agnostic tool-calling layer: one internal contract that Anthropic's native tools and the OpenAI function-calling shape (OpenAI, xAI/Grok, Gemini, Ollama) both translate to, so any configured provider drives the same tools.

Propose-then-approve

The assistant can't write unattended

Tool calls that read run immediately; tool calls that write are staged as proposals gated to the caller's own authz — applied only on human approval, prior value captured for revert, and recorded in the same ledger with origin=assistant:<provider>. A prompt-injected item can, at most, propose a rejected action.

Per-thread model · metered

Bring your own key

Each conversation pins its own provider + model (Claude on one thread, Grok on another), keyed per project and encrypted at rest. Text streams token-by-token; input/output tokens are metered on the thread and counted against the org's call quota in hosted mode.

Persistence

Data model

Everything is project-scoped; items is the hub the loop turns on.

TableCarries
itemsstatus, tags, effort, blocker · touchpoints (B) · assignee / claimed_by / claimed_at (A) · prd_id / prd_section (D) · github_url, pr
linkstyped edges — dependency / code / semantic / tag (drives C's ready-set and B's clusters)
memory_shardspgvector embeddings — semantic recall; auto-written on completion
prds · prd_versionsmarkdown specs with version history; sections drive coverage (D)
requests · attachmentspublic feedback triage — votes roll onto linked items (C); image attachments by id
code_nodes · code_edges · code_refsthe agent-described code graph — modules/files/symbols + typed edges, and item/request ↔ path bridges
eventsappend-only audit ledger — actor / action / target per accepted mutation
api_keysread/write scopes and a role ceiling. Global by default — project_id null, and the call names the project. A pin is the exception.
membershipsper-(user, project) role + access — the authorization source of truth
sync_stateper-PRD last-synced hash for Drive conflict detection
platform_configper-project AI provider, integrations, spam settings

The whole Alembic chain applies clean on a fresh database — and CI proves it on Postgres every change.

Fleet substrate

Point a subagent fleet at it

Cursor 3 — and any MCP client — now fans work across a fleet of cheap-model subagents, each running in its own isolated git worktree. Worktrees stop them clobbering each other at the byte level; they can't stop two agents from solving the same problem or grabbing the same work. That coordination gap is what Graphban fills. Connect over the same MCP endpoint — Cursor 3's Team MCP distribution then hands it to every cloud, IDE, and CLI agent on the team.

Curated context

Cheap models start oriented

A sessionStart hook injects the operating loop into an otherwise empty context window — the claimed item, the surrounding code neighborhood, the invariants that define done in this repo. A non-frontier subagent wakes up knowing what it's doing instead of guessing from the prompt.

Collision safety

Past the worktree boundary

Worktrees prevent byte-level clobber; leases prevent wasted work. claim_next and next_cluster hand each subagent a leased item — or a whole code-neighborhood — and an afterFileEdit hook flags any change straying outside it, so a fleet doesn't duplicate effort or drift onto another agent's files.

Portable agent files

One source, every client

A generator emits role-scoped subagent definitions — planner, implementer, scout, verifier — for Cursor, Claude Code, and Codex from a single source, with the repository's invariants baked into each. Regenerate on change; CI fails the build if they drift.

For unattended work, the local supervisor (graphban-fleet) runs seven named harnesses, one git worktree per child, and publishes each branch when its card reaches review. The harness table and the supervisor →

Reach

Integrations

  • GitHub — an inbound issues webhook reads repository.full_name and creates the item in the project that has that repo connected, linking it back to the issue via github_url.
  • Google Drive sync — real two-way sync of PRDs ↔ PRDs/*.md against a SyncBackend interface. The filesystem backend works today; point the sync directory at a Drive Desktop folder and it reaches Drive with no OAuth. A per-PRD hash flags conflicts instead of clobbering; a native Drive-API backend drops into the same interface.
  • CI attestations — GitHub Actions posts suite results to the card as an attestation tied to a commit, which sign-off reads as evidence.
  • Linear in progress — OAuth, a read-heavy mirror with webhooks, and constrained write-back of status, comments and assignee. Built, not yet connectable from the app. Jira follows.
  • Feedback Kit — an embeddable launcher or inline widget with theming, screenshot attachments, and live semantic duplicate detection, plus public boards, votes, a roadmap and an ingest API. Submissions land in the triage queue and their votes feed prioritization.
  • OTLP log export — send logs to the observability stack you already run, configured in Settings, with export counters.
  • Spam protection — always-on honeypot, per-(project, IP) rate limit, and optional Cloudflare Turnstile — all offline-safe by default.
  • AI providers — Embedder / ChatModel / Extractor protocols. The stub is the offline default; Ollama, Claude, or OpenAI swap in by environment variable. A decider provider type adds a fast "System One" model for memory adjudication.

Runtime

Stack & deployment

  • Backend — FastAPI · SQLAlchemy 2.0 · Alembic · pgvector · pydantic-settings. JWT auth + SHA-256-hashed API keys.
  • Frontend — Vite · React 19 · TypeScript · Tailwind v4 (dark-only tokens) · TanStack Query · shadcn/Radix.
  • Orchestration — Docker Compose is the default (pgvector/pgvector:pg16 + api + nginx SPA). A Mac or Linux box can instead run as launchd / systemd via python3 scripts/graphban_host.py install. Migrations run on startup; the app starts empty and you sign up.
  • Quality — over 4,400 backend, 1,100 web and 1,500 fleet tests, run in CI on both SQLite and Postgres + pgvector on every change, with the fleet and CLI suites on Windows too, so the vector-search path and the full migration chain are proven on the engine production actually uses. A green suite is necessary, and it is not evidence.

Native install, on a Mac or a Linux box

A box that already has Postgres + pgvector can skip Docker: python3 scripts/graphban_host.py is one CLI for install, upgrade and uninstall, under launchd or systemd. Postgres is required and verified, never installed. The installer refuses when Postgres or pgvector is missing and will not create, migrate or reconfigure a cluster.

launchd or systemd
# from an unpacked release, with backend/.env already written
python3 scripts/graphban_host.py install --root /opt/graphban --from .

Preflight first (Postgres reachable, vector enabled, the port free), then the venv, then the unit, and it refuses if the job is not actually running afterwards. Upgrades swap the release, keep your .env, and put the old one back if /health does not come up serving the new sha. Uninstall never drops the database. Install from the release tarball, not GitHub's source zip; Settings → This box → Updates does the same fetch-and-upgrade from inside the app. After the stack is up, first run is a checklist and a setup-status card. Finish later with no project and the shell keeps the setup chip, rather than opening an empty create form.

The local tools

gban is the client for a person at a terminal: issue seats, read why an agent is stuck, re-task one. It pulls no dependencies at all. GET /api/playbooks is the skills and loops this deployment publishes, and gban update applies that pack to the checkout. The fleet supervisor is a separate package, and gban fleet offers to install it the first time you need it. The supervisor still holds no authority of its own.

the local tools
uv tool install graphban-cli          # anywhere. No dependencies at all.
brew install asc-me/tap/gban          # macOS tap. Same package, its own pace.
gban update                            # apply the playbook pack
uv tool install graphban-fleet        # gbfleet + gbagent, only if you run waves

What's new

latest 2026-11-10
run

The playbook pack, applied with gban update

A deployment publishes the playbook pack at GET /api/playbooks. gban update applies that pack to the checkout. The fleet supervisor still holds no authority of its own.

fleet

Harness capacity is a routing mode beside the grades

Cheap or frontier still comes from measured sign-offs, and a cell under five samples stays unmeasured. The Harness Capacity tab is the other mode: entitlements, a subscription hierarchy you set, and a forecast of spend. A routing verdict the router never recorded is null, with a note. It is not zero.

run

First run is a checklist, not an empty form

After the stack starts, first run is a checklist, a setup-status card that stays until the first job is proven, and a one-time tour. Finish later with no project and the shell keeps the setup chip. It does not drop you into an empty create form.

run

Specs is a library, and a canvas finding stays open

The Specs list is the PRD, its preamble, and the design-set documents, plus the folder they sync to. search_docs searches baselined PRD bodies; drafts and design rows stay out until the call opts in. A design set compiles a prompt, stores the canvas, and can file a finding with a canvas check. That finding stays open until the bound canvas for the latest compiled prompt passes it.

honesty

An unloaded rule pack is not off

Build → Rules draws each rule at a strength: enforced, checked, external, advisory, unmeasured, or not used. library_loaded: false means no pack is loaded. It does not mean every rule is off, and it does not mean nothing was adopted. Turning a rule on or off is a person's REST action. get_rule_toggles has no MCP write.

run

The tracker board has a Blocked column

The tracker defaults to a board. The columns are Backlog, Next, In progress, Review, Blocked, and Done. Blocked sits beside Review, so a card that cannot move is its own column.

graph

Dead code and test results join the Stack overlay

Two new code-health categories on every component. Dead code reads vulture, knip and ts-prune. Tests reads pytest, vitest, jest or any JUnit report, with passed, failed and skipped counts, and each failing test becomes a finding you can Defrag into a card. A run that executed zero tests is recorded as a finding, not a pass, and a suite that hasn't run at the current commit shows as stale or never run.

graph

The Stack view: an architecture map nobody has to draw

Build → Stack maps the running system from the topology the API reports: your deployables grouped by host, the infrastructure you run, and the external services you depend on, in three nested rings. A code-health overlay records scan findings per component and colours each box by its worst severity. A box that was never scanned reads as never scanned, not as clean.

graph

The code graph groups by area

Nodes now cluster by area and draw their kind as their shape, so the map reads at a glance before you open a single label.

fleet

Gemini and DeepSeek join the fleet

Two new named adapters, agy for Google's Antigravity CLI and dsh for DeepSeek, bring the fleet to seven harnesses alongside Claude Code, Cursor, Grok, Qwen Code and local models through gbagent. Each is version-checked before a child starts. Codex still connects over MCP but has no fleet adapter.

honesty

Hand-merged work can be closed without faking a sign-off

Work merged outside the fleet used to sit open forever, or get a sign-off it never had. A project owner can now close it as merged, and the record says that is what happened.

honesty

A check that did not run is not a pass

Sign-off now refuses evidence that denies an acceptance clause, counts only the reviewer's own tests, and treats a skipped check as missing rather than green. A refusal names the word it matched and the rule behind it.

memory

The decider sorts the memory queue

A project can opt in to a fast System One model that grades each memory candidate: grounded, ready, and whether it contradicts a published lesson. Thresholds are calibrated against human decisions, the uncertain middle stays with a person, and a decider that cannot answer says so on the page.

fleet

Review-only seats, and branches published at review

A review-only seat can review but never build, so a reviewer cannot quietly take a slice. When a card reaches review the supervisor pushes its branch and opens a draft PR, one card per PR. With --merge it enables auto-merge after sign-off; without it, a person merges.

run

Usage, Activity and OTLP log export

A deployment-wide Usage page with model usage, an Activity view rebuilt around lenses and facets, and log export over OTLP to the observability stack you already run.

run

The app, reorganised: Plan, Build, Observe

The redesign groups every surface into three sections: Plan (tracker, requests, PRDs, roadmap), Build (code graph, stack, MCP tools, fleet, outposts) and Observe (memory review, lessons, activity, live, harness). The same in self-host and hosted.

run

Feedback Kit: public boards, votes and a roadmap

Collect feedback through an ingest API or the embeddable widget, publish boards people can vote on, and show a public roadmap. Submissions land in triage and become cards.

fleet

Outposts and the harness mix

The Fleet view is rebuilt around a catalog of harnesses, a mix that sets each vendor's share of launches, and outposts: the macOS, Linux and Windows machines your fleets run on.

fleet

A capability matrix built from real sign-offs

Models are graded per function set from reviewed work and probes, learned across users. Ask for a tier and the resolver picks from what has been measured. A cell with under five samples says unmeasured instead of guessing.

honesty

Done cannot be minted at the door

Two ways around the completion gate existed: create an item already marked done, or release a claimed one into done. Both now refuse. Signing work off still requires an attestation — the status is not a field you get to set because the form accepted it.

honesty

Agents are told the git process, or told it is unmeasured

Nothing used to name a base branch, so agents opened PRs against main because that is what models do. The org — or the local project — now stores a sparse delivery contract: base branch, whether they may push to it, optional naming. get_context returns it. An unset field stays unmeasured, not a silent main. Graphban still does not open PRs, merge, or rebase.

fleet

until is a planner loop, not a thicker supervisor

gbfleet up is still one wave: spawn, wait, reap. until sits around it — keep going while there is work, spawn reviewers when review appears, and on restart adopt the children that are already running rather than launching a second set beside them. The supervisor still holds no authority of its own.

fleet

A wait for a human is a blocked item

When an agent needs a person, that wait is a blocked item on the same tracker rather than a side channel nobody will look at. The work stays visible, and the reason is on the record.

memory

A published lesson that nobody retrieved is a miss

Observe now has a Lessons catalog. Reach and outcomes are computed rather than assumed, and publishing something the next agent never found is recorded as a miss — not as a silent success that looks like the lesson landed.

run

Install without Docker, as a system service

Compose is still the default. A Mac or Linux box can now run Graphban as launchd or systemd, with one CLI for install, upgrade and uninstall. Postgres with pgvector is required and verified, never installed for you. There is no .app.

fleet

The caller picks the model a child runs

Every spawned child used to run whatever its vendor CLI defaulted to, with no way to say otherwise — the cost lever the whole fleet idea rests on, missing. The supervisor now passes a model through, along with the two knobs one vendor each exposes. Pass-through, not selection: the supervisor still does not choose models on your behalf, and a knob the chosen vendor does not have is refused rather than quietly ignored.

graph

The living graph answers back

Hubs, shortest path and two-hop reach reach the UI rather than staying in the API. Paths say which way each edge points, and the whole graph is one tab stop — arrows move real focus, so it is navigable without a mouse.

honesty

The reference docs cannot silently fall behind

Three documents presented themselves as complete inventories and were not: the API reference covered 87 of 160 routes, configuration 19 of 51 settings, the data model 15 of 48 tables. A ratchet now records the known gap and fails on any new one, in both directions — documenting something and leaving it listed as missing fails too.

fleet

The fleet supervisor

gbfleet runs one wave: a child per seat, spawned in its own worktree, waited on, reaped. It is a separate distribution that depends on httpx and nothing else, and it holds no authority of its own — it can only launch a process holding a seat the server issued. Delete it and every invariant still holds; the fleet just needs a human to open terminals again.

fleet

Vendor adapters, named and version-checked

claude, cursor-agent, grok and codex, each resolved by name rather than guessed at, with the supported version asserted before a child is spawned. The credential is written to the child's config file and never to argv, because argv is world-readable on a shared box.

fleet

Mint, list and retire became one capability

A planner can retire the seats it minted and only those, scoped by minted_by. Ending a wave expires seats, never credentials — no client config is touched and nothing has to reconnect.

proof

The supervisor walk, run against a real server

Sixteen steps passed, one is blocked on a decision rather than a budget, and it found nothing in the supervisor. The walk refuses to run unless pointed at a real instance and refuses any project it did not create itself — a walk that quietly passed by not running would be the worst possible version of it.

fix

A closed PRD no longer blanks the PRD page

The status map covered three states and the server sends four. One closed PRD threw during render and took the whole list and editor with it. An unrecognised status now costs one oddly-labelled chip instead of a white screen, and a test compares the two lists across the stack.

fix

Linking a request to an item works on a renamed project

Item ids are frozen at issue time while the key you see is rendered from the project's current tag. The link stored the key you sent instead of the id it resolved to, so on any renamed project it violated a foreign key and returned a 500.

Every entry is work that merged. The full history is on GitHub.