Mechanism 1 of 4 · Spec → done

An unanswered question is not a resolved one.

The absence this makes loud: a question nobody answered.

A spec that was never sharpened doesn't produce a warning — it produces work that looks exactly like work against a good spec, right up until you deliver the wrong thing. So Graphban won't let a spec reach "approved" while a dimension of it is still blank.

What a spec is →

The grill

Four dimensions, graded — and the grade is not yours to set.

Grilling is a conversation that sharpens a PRD before anyone builds it: unstated assumptions, scope boundaries, failure modes. The server owns it as state, not the client as a chat log — so the record of what was asked and what was actually answered survives the session.

Every grill is graded against the same four dimensions. Each comes back resolved, deferred, or unanswered. Grading is a separate call from the conversation, run at temperature zero, and when the model is unavailable it falls back to a deterministic stub rather than silently passing.

DimensionThe question it refuses to leave blank
scope_edgesWhat is explicitly out of scope for the first version?
failure_modesWhat happens on the failure path — bad input, missing data, timeout?
contractsWhat is the exact shape of the inputs and outputs at the boundary?
open_decisionsWhich decisions are still open, and which need a prototype to settle?
Four grill dimensions each graded resolved, deferred or unanswered. Three pass the gate; the unanswered one is stopped, and the approval call is refused. resolved deferred unanswered scope_edges resolved failure_modes deferred contracts resolved open_decisions the gate update_prd(status:"approved") ApprovalNotEarned still unanswered: open_decisions answer it — or defer it explicitly — and the status flips by itself.
Approval is a property of the grid, and the exception is the grid speaking. Note that deferred passes. Deferring is a decision on the record; the failure this catches is an implicit non-answer being counted as an answer.

Earned, not set

update_prd(status="approved") is a request, not a switch.

update_prd(prd_id, body?, status?) refuses status="approved" — ApprovalNotEarned: "approved is reached by finishing the grill, not set directly."

The refusal lives in the service layer, not in a route handler, so the REST API, the web UI and the MCP surface are all covered by one check. That placement is the whole point: this call is otherwise exactly how an agent would freeze an intent baseline that nobody had read.

Two more guards sit behind it. A verdict must cite a grill turn that actually resolves the dimension it claims — a citation that doesn't dereference is rejected as malformed. And if the grading standard itself is later found broken, it retroactively invalidates the approvals it granted, because an approval is only as good as the process that granted it.

Zero answers never grades as approved

A grill with no recorded answers is incomplete whatever any model says about it. Without that floor, an empty conversation grades straight through — which is the failure mode this entire mechanism exists to prevent, arriving through the mechanism itself.

The baseline

Approval freezes a version. That version is what the tickets mean.

The moment a PRD earns approval, its body is frozen as a baseline version carrying the per-dimension grill outcomes — so a deferral is visible on the baseline itself, not just in a transcript. Re-approving an unchanged spec is idempotent: it never mints a second "original intent".

decompose_prd then creates one work item per uncovered ## section. The section is the atom of intent, and that choice buys rename detection for free — retitle a section and the work linked under it keeps its identity, so a rename can never manufacture a false absence.

The intent hold

You cannot miss the hold, because it is not a notification.

An agent claims an item, then the spec moves underneath it. Nothing about the item changes. The classic fix is to send a notice — which works exactly as well as everyone remembering to read their notices.

An item claimed against baseline v1 keeps working while the governing baseline moves to v2; the mismatch is derived and returned on every read. + new section v1 v2 supersedes_id GRPH-380 baseline_at_claim = v1 intent_hold: claimed v1 < governing v2 names which sections moved, and whether yours did derived at read time from two stored values — so there is no state to forget to update.
The hold is derived at read time from two stored values, so there is nothing to forget to update and nothing to miss. It rides on every item read — claim, heartbeat, update, search. There is no way to work on an item without reading one.

Rebaselining

A rebaseline is a new link in a chain, not an edit.

When intent genuinely moves, you ask for a new baseline in your own words. The request does not approve anything — it re-opens the grill, and the existing baseline keeps governing until a new one is earned on its own answers.

  • Append-only. The new baseline points back at the old one; the old one is never touched, so what you promised in March is still readable in August.
  • It cannot add sections. New scope is a successor PRD, not a quiet expansion of an approved one — and the refusal fires at the edit that introduces the violation, not at the end of a whole grill you then have to redo.
  • It marks prior work classifications stale, recomputed lazily, so a rebaseline on a large PRD never blocks on a hundred model calls.

Verdicts

A verdict cites, and the citation is dereferenced.

Judging whether a section was actually delivered is a job for something holding the repo — so Graphban doesn't guess. It takes a verdict from an agent that does, and then checks the agent's homework by resolving what it cited.

A verdict with three citations; two resolve to real targets, the third points into nothing and is stopped, invalidating the verdict. submit_verdict code: services/fleet.py intent: § Roles evidence: GRPH-380 code_nodes — resolves baseline § Roles — resolves nothing here citing nothing, or citing something that does not resolve, is rejected as malformed.
A verdict cannot cite something that isn't there — so "I checked" becomes a claim the system can dereference. An absence finding cites the baseline section it says was never delivered, which is why absence is citable rather than silent.

Signing off work you claimed yourself is flagged, not refused — and the separation check has three states, not two: independent, not-independent, and unverifiable. That third state exists because "we have no record of who did the work" must never render as a clean pass.

Completeness

completeness will never tell you it is complete.

Classifying the work that exists can only ever find drift and stowaway scope. It can never surface work that was never done — and "is this PRD finished" is entirely a question about what is missing.

A bar split into delivered, undelivered and absent segments, with a struck-out "100% complete" label above it. 100% complete delivered undelivered absent linked work is done specced, tracked, not built not in the spec at all a single green number is precisely what this must never render.
undelivered is a queue. absent is a hole in the spec. A tool that renders them the same colour is the tool that taught you silence means success.

Four things it deliberately refuses to do:

  • It does not read the live PRD body, only the frozen baseline — measuring delivery against a spec that moves is how drift becomes definitionally zero.
  • It does not judge sufficiency. A linked done item counts as delivered; whether the work actually satisfies the section is the auditing agent's call, because that agent has the repo.
  • It does not emit a percentage.
  • It does not drop framing sections. The section that defines "done" being structurally exempt from every check is one of the original problems this was built to fix — so framing sections are reported and flagged, not filtered away.

Closing

You close a PRD by deciding, not by finishing.

Closing is terminal and irreversible — no edit, no reopen, no rebaseline afterwards. And it is gated on disposition, never on delivery.

close_prd(prd_id, dispositions[]) refuses unless every section with nothing delivered appears exactly once — as promoted to an item or successor PRD, or deferred with a reason.

You are allowed to close a PRD having built four of nine sections. You are not allowed to close one without saying what happened to the other five. The close report then measures against the original intent, not the latest rebaseline — so a spec that quietly shrank to match what got built still reads as a spec that shrank.

The library shipped

A spec is more than the PRD file.

The nav calls this Specs. The list is the PRD, its preamble, and the design-set documents, plus the folder they sync to.

search

Baselined bodies, unless you ask for more

search_docs searches baselined PRD bodies. Drafts and design rows stay out of the result until the call opts in.

design sets

A finding stays open until the canvas passes

A set compiles a prompt from the PRD, the constraints, the preamble, and the confirmed tier. put_canvas stores the screen. file_finding can attach a canvas check, and resolve_finding applied stays refused until the bound canvas for the latest compiled prompt passes it.

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.