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.
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
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.
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.
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.
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.
| Group | What it is for |
|---|---|
| Orientation | get_context first, then list_projects, create_project, setup_project. The last is the resumable first-run checklist. |
| Work and fleet | Claim, 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 lessons | Candidates, publish, and the learning loop. get_lessons reads the catalog. A published lesson nobody retrieved is a miss. |
| PRDs and acceptance | Grill, coverage, verdicts, and get_prd. prd_acceptance is eleven views. None of them says complete. |
| Code graph | Describe, map, neighbors, search, a structural query, and the links between an item and a path. |
| Scans | record_scan, scan_status, scans_due. A component that was never scanned reads as never scanned. |
| Rules and entitlements | get_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. |
| Documents | put_document, get_document, search_docs. Search covers baselined PRD bodies. Drafts and design rows are opt-in. |
| Design sets | open_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.
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.
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.
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.
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.
| Table | Carries |
|---|---|
| items | status, tags, effort, blocker · touchpoints (B) · assignee / claimed_by / claimed_at (A) · prd_id / prd_section (D) · github_url, pr |
| links | typed edges — dependency / code / semantic / tag (drives C's ready-set and B's clusters) |
| memory_shards | pgvector embeddings — semantic recall; auto-written on completion |
| prds · prd_versions | markdown specs with version history; sections drive coverage (D) |
| requests · attachments | public feedback triage — votes roll onto linked items (C); image attachments by id |
| code_nodes · code_edges · code_refs | the agent-described code graph — modules/files/symbols + typed edges, and item/request ↔ path bridges |
| events | append-only audit ledger — actor / action / target per accepted mutation |
| api_keys | read/write scopes and a role ceiling. Global by default — project_id null, and the call names the project. A pin is the exception. |
| memberships | per-(user, project) role + access — the authorization source of truth |
| sync_state | per-PRD last-synced hash for Drive conflict detection |
| platform_config | per-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.
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.
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.
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_nameand creates the item in the project that has that repo connected, linking it back to the issue viagithub_url. - Google Drive sync — real two-way sync of PRDs ↔
PRDs/*.mdagainst aSyncBackendinterface. 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/Extractorprotocols. 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 viapython3 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.
# 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.
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