If work is not on the board, it does not exist.
A body of work is a Feature issue with ordered sub-issues. Its status comes from a merged pull request whose body closes the issue, and nobody ever types a status. If the board is wrong, the fleet works on the wrong thing. We keep no second list to check it against, on purpose, and this component exists to stop anyone from creating one.
The vocabulary (plan, Feature, issue) and the lane routing that decides which agent serves a row are covered on How work flows. This page covers the mechanism underneath. It explains which fields an agent writes, which ones a job computes, and the four ways the board has ended up showing something false.
The fleet owns no file that is the board
The component's list of owned files is empty. That is accurate, not something we forgot to fill in. The board is a hosted product. Everything in the repository that touches it belongs to some other component, and exactly three parts split the work. One script owns the path that writes to the board. One set of workflows owns the checks that keep it accurate. The scheduler owns the only thing that reads the Todo status.
Because we name that boundary explicitly, no fourth component can quietly start keeping its own copy of the board.
An agent writes two fields. Everything else is derived
An agent's whole job on the board is to assign itself and give the issue a title. It can also add a priority label if it wants. A reconciliation workflow on a hosted runner computes every other field, and writes a field only when its value has changed.
| Field | Derived from |
|---|---|
| FieldLane | Derived fromthe login of the primary assignee. The lane names the box, never the portfolio |
| FieldPriority | Derived fromthe priority label, or the middle value if there is no label |
| FieldSprint | Derived fromthe current iteration if the field is empty. The reconciler never overwrites an iteration someone chose on purpose |
| FieldStatus | Derived fromthe board's own built-in workflows. The reconciler only acts as the backstop that moves closed issues to done |
| FieldArchived | Derived froman issue that is closed and done comes off the board. An issue that is open and archived goes back onto it |
| FieldWeek / Cycle | Derived fromno script ever writes this field. See below |
This design has two useful results:
- Agents make zero field calls. However busy the agents get, they cannot cause a rate-limit outage against the tracker from their side.
- Because the reconciler writes only when a value changed, it is idempotent. Running it twice has the same effect as running it once, so it is 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
Someone once designed a plan as five parallel waves and filed it as a chain fourteen issues deep. Nothing reported an error. The board just had one issue ready to dispatch instead of five, and the fleet ran at a fifth of the parallel capacity the plan had, until someone looked at the dependency graph.
If two phases can run at the same time, file them as a wave. The filing tool now refuses a long chain when its tasks share no file, and it prints the same plan rewritten as waves. A serial order needs a stated reason. It is not the 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 creates new iteration ids, and every assignment that pointed at the old ids is left pointing at nothing. On this board, a single call lost 84 field assignments.
The schema shows why, and the documentation does not, so reproduce it yourself instead of trusting this paragraph:
gh api graphql -f query='{ a: __type(name:"ProjectV2IterationFieldIterationInput"){name}
e: __type(name:"ProjectV2Iteration"){kind inputFields{name}} }'
a comes back null, which means the input type a caller would need does not exist. And e.inputFields
has no id. The full input for an iteration is a start date, a duration and a title. So you cannot say
"update this one". You can only say "here is the new list".
The field id is durable, and it is the only id worth hardcoding. An iteration id changes as soon as a human edits the field in the interface. For that reason the filing script looks up the current iteration live every time it files an issue, and treats the result as advisory.
The rule that applies elsewhere is this. Before you write to a hosted API, check through introspection whether the API keeps the identity of the thing you are updating. If its input type has no id field, you are not updating it. You are replacing it.
Read the board, never recall it
Every count on the board changes by the minute, so this page tells you how to read each number instead of stating it.
| You want | Run |
|---|---|
| You wantOpen issues | Rungh api repos/<org>/<repo>/issues --jq 'length' |
| You wantProgress for every Feature, in one call | Runbash _infra/scripts/feature-progress.sh |
| You wantOne issue with its comments | Rungh api repos/<org>/<repo>/issues/<n> |
| You wantOne line for each queued issue, with the reason it is held | Runbash _infra/scripts/sweep-dispatch.sh --explain |
That table has two traps. gh issue view currently fails against this board with a deprecation error from
the older projects API, so use the gh api form, which works. Also, gh api prints its errors to
standard output, not standard error. Check the shape of what you captured. Do not only check that it
is non-empty.
Four ways this board has been made to lie
Saying you are not closing an issue closes it. GitHub matches a closing keyword followed directly by an issue reference anywhere in a pull request body or commit message, and it ignores the words around it. A sentence like "this does NOT close
#1234" counts as a closing reference. This happened twice in one day on one issue. The first time, it came from a pull request whose whole purpose was to keep the issue open. The second time, it came from the follow-up pull request that documented the trap, because that pull request quoted the sentence in its own body.Recovery takes two steps, and only the first one is obvious. A close also sets the status to done, and reopening the issue does not reset it. The issue ends up open and done, and no selector reads that state. So reopen the issue, and set the status back, then read both values back to confirm. To mention an issue without acting on it, use a form with no keyword, and use a placeholder number in any example. That is why the example above uses
#1234.The final state arrives eventually, not instantly. The close event tries to archive the issue, and a scheduled sweep guarantees it. In one measured window, concurrency cancellation killed 25 of 30 event runs in a row. Expect a few rows at any moment that are closed but still listed. This fleet has already raised a false alarm by filing "done is stuck at zero" after reading the board once.
A Feature never closes itself. A parent issue has no pull request, so a closing keyword can never fire on it. Instead, the agent that finishes the last child issue attests the Feature, and a separate sweep closes it. If a Feature is complete but has no attestation, the sweep flags it and does not close it. That is the right way to fail, because the alternative is a parent that closes itself while its own verifier objects.
Nothing gates the default branch. On this plan tier, branch protection and rulesets both return 403 in every repository. A red check is information. A merge never waits on a human. The full argument is on How work flows.