The board
One GitHub project board is the only tracker. An agent writes two fields; every other field is derived on a runner, and status changes only when a merged pull request closes an issue.
stable
Sourced from the fleet's own component document for board. Called stable because the one constant every
script uses to address it is bound in five separate files and re-checked on every pull request — which is
the only part of a hosted product a repository can hold still.
Work exists here or it does not exist.
A body of work is a Feature issue with ordered sub-issues. Status derives from a merged pull request whose body closes the issue, and nobody ever types one. If this surface is wrong, the fleet works on the wrong thing — there is no second list to cross-check against, deliberately, and creating one is the fence this whole component exists to hold.
The vocabulary — plan, Feature, issue — and the lane routing that decides which agent serves a row are on How work flows. This page is the mechanism underneath: which fields are written, which are derived, and the four ways this board has been made to lie.
The fleet owns no file that is the board
The component's list of owned files is empty, and that is the fact rather than an omission. The board is a hosted product. Everything in the repository that touches it belongs to a neighbour, and the partition is exactly three ways: one script owns the write path in, one set of workflows owns the mechanisms that keep it honest, and the scheduler owns the single consumer of the Todo status.
Naming that boundary explicitly is what stops a fourth thing quietly growing a copy of the board.
An agent writes two fields. Everything else is derived
An agent's entire board job is: assign itself, and give the issue a title — plus an optional priority label. Every other field is derived on a hosted runner by a reconciliation workflow, and written only on diff.
| Field | Derived from |
|---|---|
| Lane | the primary assignee's login — the lane names the box, never the portfolio |
| Priority | the priority label, defaulting to the middle value |
| Sprint | the current iteration if unset; a deliberately-chosen one is never clobbered |
| Status | the board's own native workflows; the reconciler is only the closed-to-done backstop |
| Archived | closed and done goes off the board; open and archived is pulled back onto it |
| Week / Cycle | never written by any script — see below |
Two properties fall out of that, and both are the point:
- Agents make zero field calls, so no amount of agent activity can cause an agent-side rate-limit outage against the tracker.
- Writing only on diff means the reconciler is idempotent. Running it twice is indistinguishable from running it once, which is what makes it safe to run on a timer and on an event.
The shape of a filing is a real choice
--chain task k is blocked by task k-1
→ exactly ONE issue is ever dispatchable
--wave every task in a group is blocked by the WHOLE of the previous group,
and by no sibling
→ a whole group starts at onceA plan designed as five parallel waves was once filed as a fourteen-deep chain. Nothing errored; the board simply had one dispatchable issue instead of five, and the fleet ran at a fifth of its shape for as long as nobody looked at the dependency graph.
If two phases can run at once, the shape is a wave. The filing tool now refuses a long chain whose tasks name no file in common, and prints the wave rewrite — a serial order needs a stated reason, not a default.
The iteration field is read-only to every script, and this one is worth stealing
Any API write to a board's iteration configuration mints new iteration ids and orphans every assignment that pointed at the old ones. On this board that cost 84 field assignments in a single call.
The reason is visible in the schema rather than in the documentation, so reproduce it rather than trusting this paragraph:
gh api graphql -f query='{ a: __type(name:"ProjectV2IterationFieldIterationInput"){name}
e: __type(name:"ProjectV2Iteration"){kind inputFields{name}} }'a comes back null — the input type a caller would need does not exist. And e.inputFields has no
id: an iteration's complete input set is a start date, a duration and a title, so there is no way to
say "update this one", only "here is the new list".
The field id is durable and is the only id worth hardcoding. An iteration id rotates the moment a human edits the field in the interface, which is why the filing script resolves the current iteration live on every filing and treats the result as advisory.
The transferable rule: before writing to a hosted API, introspect whether the thing you are updating has an identity the API will preserve. If its input type has no id field, you are not updating — you are replacing.
Read the board, never recall it
Every count on it changes by the minute, so this page states the reading rather than the number.
| You want | Run |
|---|---|
| Open issues | gh api repos/<org>/<repo>/issues --jq 'length' |
| Feature rollups, one call | bash _infra/scripts/feature-progress.sh |
| One issue with its comments | gh api repos/<org>/<repo>/issues/<n> |
| One line per queued issue, and why each is held | bash _infra/scripts/sweep-dispatch.sh --explain |
Two traps in that table. gh issue view currently fails against this board with a deprecation error from
the older projects API, so the gh api form is the working one. And gh api prints its errors to
standard output, not standard error — so validate the shape of what you captured, never merely that it
is non-empty.
Four ways this board has been made to lie
-
Saying you are not closing an issue closes it. The platform matches a closing keyword immediately followed by an issue reference anywhere in a pull request body or commit message, and does not read the words around it. A sentence of the form "this does NOT close
#1234" is a closing reference. This fired twice in one day on one issue: first from a pull request whose entire point was that the issue must stay open, and then from the follow-up pull request documenting the trap, which fired it by quoting the sentence into its own body.Recovery is two steps and only the first is obvious. A close also sets the status to done, and reopening the issue does not undo that — leaving it open and done, which is the one state no selector reads. Reopen, and set the status back, then read both back. To refer to an issue without acting on it, use a non-keyword form, and keep any illustration on a placeholder number, which is why the example above is
#1234. -
The terminal state is eventually consistent, not instant. Archiving is attempted on the close event and guaranteed by a scheduled sweep, and concurrency cancellation killed 25 of 30 consecutive event runs in one measured window. Expect a handful of closed-but-still-listed rows at any moment. Filing "done is stuck at zero" off a single read is a false alarm this fleet has already raised.
-
A Feature never closes itself. A closing keyword structurally cannot fire on a parent issue, because a parent has no pull request. So the agent that finishes the last child attests the Feature, and a separate sweep closes it. A complete Feature with no attestation is surfaced, never closed — which is the correct failure, because the alternative is a parent that auto-closes over its own verifier's objection.
-
Nothing gates the default branch. Branch protection and rulesets both return 403 on this plan tier, in every repository. A red check is information. A merge is never a human gate. The full argument is on How work flows.
Take it further
2 · Measure derived-versus-typed on your real tracker
Reads your live issue tracker through its API, counts which fields are actually populated, and finds the ones a human types that a job could compute. Reads only; writes nothing.
You are going to measure a real issue tracker and tell me which of its fields are typed by a human and which could be derived instead.
Read this for the reference model:
curl -s https://docs.utopiamodels.ai/docs/operating/board.md
That fleet writes two fields per issue and derives the rest on a runner. I want to know how far my tracker is from that.
Do not create, edit, close, label or comment on anything. Every call you make must be a read. If a step would require a write to answer, skip it and say so.
## Step 1 — find the credential and the tracker
Work out what tracker this environment can reach. Check, in this order, and stop at the first that authenticates:
gh auth status
git remote -v
env | grep -iE 'JIRA|LINEAR|ASANA|TRELLO|GITHUB_TOKEN|GH_TOKEN' | sed 's/=.*/=<set>/'
Never print a credential value. The sed above is deliberate — report only which variables are SET, never what they contain. If nothing authenticates, tell me and stop; there is no useful version of this audit against a tracker you cannot read.
## Step 2 — pull a real sample
Pull the most recent 100 items, open and closed, with every field the API will give you. For GitHub that is the issues endpoint plus, if the repo is on a project board, a GraphQL query for the project's field values. Two notes from the reference page that will save you a debugging cycle:
- `gh api` writes its ERRORS to standard output, so check the shape of what you captured rather than whether it is non-empty. An error body is a perfectly non-empty string.
- If a project board is involved, read the FIELD ids and never write to an iteration configuration. On the reference fleet that call orphaned 84 assignments, because an iteration has no id in its own input type — there is no update, only a replace.
## Step 3 — count, do not eyeball
For every field present in the sample, report:
- Fill rate: what fraction of items have a non-empty value.
- Distribution: how many distinct values, and whether one value dominates. A field that is 97% one value is a field nobody is really setting.
- Correlation with something already known: does status track whether a linked pull request merged? Does priority track a label? Does assignee track who opened it?
That third one is the whole audit. A field that correlates almost perfectly with something already in the system is a field a job could compute, and every field a job computes is a field that cannot go stale.
## Step 4 — report
Give me three lists.
**Derivable now** — the field, what it correlates with, the rule that would compute it, and the fill rate it would reach.
**Typed and load-bearing** — the fields that genuinely carry a human decision. Say why each one resists derivation; if you cannot say why, it belongs in the first list.
**Dead** — fields with a fill rate low enough that nothing can be depending on them. Recommend deleting these first: they cost nothing to remove and they are the ones making the board look more informative than it is.
Then ask me which list to act on first with the `AskUserQuestion` tool, with a recommended answer and your reason for it.
Finish with the one number I should watch. Not a dashboard — one number, and what it means when it moves.How work flows
The board is the queue, git is the state, and a merged pull request is the only status change. Plans, features, issues, lanes, specs, and why no check can stop a merge.
The planning loop
Five steps from a rough note to filed, dispatchable work — and not one of them is reported. Every step is derived from an event or from the board, so a plan has no field it could lie in.