Operating

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.

FieldDerived from
Lanethe primary assignee's login — the lane names the box, never the portfolio
Prioritythe priority label, defaulting to the middle value
Sprintthe current iteration if unset; a deliberately-chosen one is never clobbered
Statusthe board's own native workflows; the reconciler is only the closed-to-done backstop
Archivedclosed and done goes off the board; open and archived is pulled back onto it
Week / Cyclenever 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 once

A 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 wantRun
Open issuesgh api repos/<org>/<repo>/issues --jq 'length'
Feature rollups, one callbash _infra/scripts/feature-progress.sh
One issue with its commentsgh api repos/<org>/<repo>/issues/<n>
One line per queued issue, and why each is heldbash _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

Harness

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.

On this page