Reference

How the loop is actually built.

This page is the reference layer — tables, topology and the tool surface. For the argument, start with 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 60-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 (60 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 MCP tool surface

JSON-RPC 2.0 over POST /api/mcp, authed by a project-scoped key. 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.

ToolKindPurpose
Orientation
get_contextreadThe key's project, scopes, project/tool counts — call first
list_projectsreadProject ids for the project_id override
create_projectwriteSelf-host only, and only while unlinked to a cloud org — a linked instance would create in the org's tenant
setup_projectreadFirst-run bootstrap — a resumable checklist that takes a fresh project from empty to useful; re-runnable, reports done/pending
Work queue
claim_nextwriteAtomically claim the best ready item (leased)
next_clusterwriteClaim a whole code-neighborhood at once
claim_clusterwriteThe way to take work in a fleet — claims a non-colliding cluster and reserves its touch-areas in the same transaction; a refusal names who holds them and when they free
collision_clustersreadThe current partition — connected components of items that share touch-areas, largest first
heartbeatwriteExtend the lease on a claimed item, and the agent's presence on the roster
release_itemwriteReturn a claim to the queue
get_backlogreadPrioritized backlog — ready-first, with blocked_by / unblocks / votes / score
suggest_nextreadSingle best next item, dependency-aware
Items
create_itemwriteCreate a task (tags, touchpoints, fidelity, PRD link) · idempotent
update_itemwritePatch / advance status · set fidelity
search_itemsreadQuery by text, tags, status (paginated)
get_item_detailsreadItem + linked shards + requests
related_workreadThe code-neighborhood around a task
link_itemswriteTyped link (dependency / code / semantic / tag)
unlink_itemswriteRemove a typed link — the inverse of link_items · idempotent
Memory · Insight
add_memorywriteRecord a memory shard — enters as a candidate pending human publish · idempotent
search_memoryreadSemantic (pgvector cosine) search over published shards
publish_memorywriteSubmits a candidate for independent adjudication — the judge decides, not the caller; kept: false is a normal outcome
reject_memorywriteDiscard your own candidate — needs no judge, since it takes nothing out of the trusted pool
learning_loopreadArtifact recommendations, drafts, usage and retirement candidates
review_recommendationwriteApprove or reject an artifact — the human boundary; approving a shared-file artifact writes nothing
extract_lessonswriteDistill an item's lessons into candidate memory
generate_digestreadProgress digest across the project
PRDs · grill
create_prdwriteAuthor a PRD — the handoff artifact; ## sections drive decompose/coverage
update_prdwritePatch a PRD's title / status / body
grill_prdreadNext clarifying questions to sharpen a PRD before building
decompose_prdwriteSpec sections → tracked tasks (classified by fidelity)
prd_coveragereadPer-section rollup + gaps + open high-fidelity work
answer_grillwriteRelay the author's answer — recorded as agent-relayed, so a reviewer can see it wasn't the author typing
Delivery acceptance
prd_acceptancereadEleven views over one surface — completeness, drift, evidence, lineage, verdicts, baseline, audit brief. None of them ever says "complete."
submit_verdictwriteA per-section verdict with typed citations (code / intent / evidence) that must resolve, or it's malformed
request_rebaselinewriteRe-open the grill for new intent — does not approve, and cannot add sections
close_prdwriteTerminal and irreversible; gated on disposition of undelivered sections, never on delivery
Fleet coordination
register_agentwriteJoin the roster; returns the active role and the tools it will be refused
fleet_statusreadWho is alive, in which role, holding what — and which posture the project is in
propose_allocationreadA mapping of live agents to free clusters, with rationale. Writes nothing
assign_rolewriteCommit the allocation — planner only. Re-tasking releases held work in the same transaction
mint_enrolmentwriteIssue a single-use 30-minute seat carrying one role — planner only, because a planner has no authored work to launder
claim_reviewwriteLease an item you did not build and are independent of; returns the branch
sign_offwriteAccept reviewed work. Above an effort threshold, refuses without a sabotage receipt
bouncewriteReturn work with a required reason that travels with the item; pinned to its author for one lease
Code graph
describe_codewriteThe coding agent upserts the code map — nodes (module/file/symbol + summary) and typed edges; echoes the paths it touched
get_code_mapreadThe project's described nodes + edges
code_neighborsreadEdges around a path + the work items touching it
search_codereadSemantic search over code-node summaries
graph_queryreadThree structural questions — hubs (inbound degree = blast radius), components, path (walked undirected). An unknown edge type is refused, not silently narrowed
link_codewriteBridge an item/request to a code path (affects / implements / fixes / tests)
unlink_codewriteRemove an item/request ↔ code link
Upstream
report_graphban_issuewriteReport a bug/idea about Graphban itself; deduped on arrival

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_keysscoped to a project + read/write scopes — enforced, one key = one agent
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

Runtime

Stack & deployment

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.

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. The fleet supervisor is a separate package, and gban fleet offers to install it the first time you need it.

the local tools
brew install asc-me/tap/gban          # macOS
uv tool install graphban-cli          # anywhere
uv tool install graphban-fleet        # gbfleet + gbagent, only if you run waves

What's new

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