Mechanism 3 of 4 · The living graph

The map is drawn by the agent that reads the code.

The absence this makes loud: a file the map has never seen.

A code map that goes quietly out of date is worse than no map, because confidently-wrong structure is exactly what an agent will act on. So this one records freshness as a first-class state, and will tell you the difference between a file that went stale and a file it has never heard of.

What the code graph is →

Who builds it

Your coding agent already read the file. Graphban never has to.

The graph is not built by a parser Graphban runs over a checkout it doesn't have. Your external coding agent is the producer — it has the real repository in context, so it is the source of truth, and it calls describe_code as a byproduct of work it was doing anyway.

Graphban's own connected model is the consumer: semantic search over node summaries, neighbourhood reads, structural queries, and natural-language questions about the codebase answered from the graph without a checkout. Nodes carry a path, a kind (module · file · symbol · doc · config), a one-paragraph summary that gets embedded, a content hash, and a freshness flag. Edges are directed and typed — imports · calls · owns · tested_by · references — and an edge may point at a file nobody has described yet.

Docs and config are graph kinds too. Work that touches AGENTS.md or a deploy template has a blast radius, and a map that only knows about source files can't show it.

The key

Three channels that never borrow each other's meaning.

Everything you'll see on the map encodes exactly one of three things. Keeping them separate is what makes a glowing node mean one unambiguous thing.

node fill
what kind of thing
module
file · symbol
doc
config
edge stroke
what kind of relation
imports
calls
tested_by
references
cloud
whose agent holds it
you
predicted area
another person
contention

Hue is spent entirely on kind. Presence gets shape and opacity instead — which is why a held node still shows its own colour and freshness underneath, and the cloud never tints the node itself.

Staying true

The health check retires nothing. It is a read.

Re-describing a directory with prune=true does not delete the nodes it didn't see. It marks them stale. A partial describe should never destroy history — and a node that vanishes takes the meaning of its edges with it.

Describing code with prune marks unseen nodes stale rather than deleting them; the health endpoint reports fresh, stale, and never-described as three distinct states. describe_code(prune=true) the external agent is the producer unseen → stale, kept delete unseen GET /code/health fresh stale ever_described: false never described is not stale — and it is never printed as "clean" no sweeper, no periodic job. It retires nothing. It is a read.
The health check has no side effects, and it separates "went stale" from "never existed to the graph" — the second is the absence that reads clean everywhere else. A periodic job producing a report nobody reads is worse than a number you can ask for.

It is also honest about its own limits. With no checkout, the server cannot distinguish a deleted file from one nobody has described — so it only flags what is decidable from the path string, and refuses to guess further. And when nothing has ever been described, it says so explicitly rather than reporting zero stale nodes and looking healthy.

Queries

Inbound edges answer "what breaks if I change this".

hubs

Ranks by inbound degree. A file that imports forty things must not outrank one that forty things import — the first is cheap to change, the second is your blast radius.

components

Connected groups, largest first, each with an anchor to name it. This is what the galaxy view collapses to when a map exceeds the detail budget — when it collapses at all.

path

Shortest route between two files, walked undirected, with each hop reporting which way the edge actually points.

All three are deterministic and read-only. An unknown edge type is refused, not silently narrowed — quietly dropping a filter you asked for returns a smaller answer that looks like a complete one.

Scale

The galaxy view refuses to collapse what it cannot explain.

Past a certain size a force-directed graph stops being a picture and starts being a hairball. The usual answer is to draw the same thing slower. Above its detail budget Graphban instead changes what it draws: connected components collapse into super-nodes, each labelled by its highest-degree member, and clicking one enters it with a breadcrumb back.

Search still sees through it. Pressing / matches across collapsed components and enters the one holding the hit — a find that can't see what the view is hiding is a find that reports zero results for a node sitting right there. And zero genuine hits dims the graph and shows a literal 0, rather than leaving the map looking like nobody searched.

Collapsing on component count produces one huge super-node beside five specks; collapsing only when the distribution justifies it leaves the flat graph visible instead. collapse when count > 1 146 nodes 3 · 1 · 1 · 1 · 1 six components, so it collapses — and explains nothing collapse when the shape justifies it largest holds 95% → stay flat the honest mess is the more useful answer
A summary that hides the structure while claiming to summarise it is worse than no summary. The guard checks the distribution, not the count: it refuses to collapse when the largest component holds more than 60% of the graph.

The numbers above are real. Running components() against this repo's own live graph during the acceptance walk returned [146, 3, 1, 1, 1, 1] — 95% of the graph in a single component, because almost everything is reachable from the models module. Six components clears a "more than one component" test easily, so the original guard would have rendered one enormous dot beside five specks and called it a summary.

The gap was in the test data, not the reasoning

That guard had tests, and they passed. They used ten equal components of ninety nodes each — the friendly case, invented rather than measured. The check was green while being wrong about the only shape of graph anyone would ever point it at. It took deploying it and reading the live map to find, and the regression test now uses the measured shape instead of a convenient one.

The rest of the view is ordinary care taken seriously: layout runs in a worker so the main thread stays responsive, positions are computed from the unfiltered edge set so toggling a filter never reshuffles the map underneath you, labels fade in by level of detail, and the whole thing is keyboard-navigable with one tab stop into the canvas and arrow keys between nodes.

Presence

A node glows because an agent holds a live lease — never because an item declared an intention.

The map shows you where your fleet actually is right now. The distinction between a lease and a declaration is the entire design, because only one of them can expire.

A declared intention keeps a node glowing after its agent dies, whereas a lease-backed cloud fades to nothing exactly at its expiry. an item declares an intention intent agent last seen 2,333s ago — still glowing a declaration has no expiry an agent holds a lease expires_at expires_at the glow is a function of expires_at and the clock — nothing has to notice the agent died.
Presence decays by construction. The 2,333-second example is real, from our own instance — it's the one lease that doesn't expire yet, and it's on the known-gaps list.

Presence is a surveillance surface with a person's name attached, so it is reachable only with a signed-in session — never with an agent key. And it uses a deliberately stricter path matcher than the collision divvy does: over-matching is safe when you're deciding what to block (you over-block, you never collide) and a lie when you're drawing a picture of where people are.

Contention

Overlap is normal. Overlap across two people is an alarm.

one person normal — no ring
two people, apart normal — no ring
two people, same file contention ring

The divvy exists so the third picture cannot happen. So when it does, one of exactly three things is wrong: a lease lapsed in a race, the path matcher has a gap, or somebody claimed work outside the divvy. When the glow is wrong, the fleet is wrong — which is why this is drawn as an alarm and not as a busy-looking file.

Off the map

15 of 100 touchpoints on our own backlog resolve to no node.

They are listed, not dropped. An area that matches nothing on the map goes into a tray with its holder and a reason — undescribed or stale — because a presence payload that silently omits what it couldn't resolve is an absence reading as a clean result.

Areas that resolve to no graph node are collected in an off-map tray with their reason and holder, rather than being discarded. the map not dropped off the map docs/deploy.md — undescribed AGENTS.md — undescribed .claude/settings.json — stale 15 of 100 touchpoints the count doubles as a visible measure of how much of the repo nobody has described yet.
What the map cannot place is listed, not dropped. Our own coverage is about 30% — 123 described nodes against 401 source files — and the tray is how you find that out without going looking for it.

The stack view

The architecture diagram nobody has to keep up to date.

The code graph shows files and how they connect. The Stack view, one level up, shows the running system: every box, frame and edge is computed from the topology the API reports, so the map is a function of the system rather than a drawing of it.

① Code we write

Your deployables, grouped by the host they run on, with protocol-labelled edges between them.

② Infrastructure we run

Databases and the fleet's outposts: what you operate yourself, nested around your code.

③ External services

What you depend on but don't run, in its own column so the boundary of ownership is visible.

A code-health overlay that never defaults to green

Scan skills record findings per component, and each box takes the colour of its worst severity. A box is clean at a commit, has findings, or never scanned or stale, and the third is not drawn as the first. A full scan report lists every finding, filterable by component and category.

The code graph itself now groups nodes by area and draws each node's kind as its shape, so different kinds of node can be told apart before you read a label. The stack view, in full →

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.