## What
Pins the **startup stale-planning sweep's** planner-lane read in
`triage.ts`. Test-only — no product change.
A card can hold `status: "planning"` while triage specifies it in place.
A crash or restart before planning completes leaves that status set, and
a startup sweep clears it. If the sweep misses the card, it occupies a
planning admission slot **permanently** and new triage work is never
admitted.
The lane read was converted from `resolvePlannerLanes(store, "")` —
called with an **empty task id**, so it could never resolve a task and
always answered with the default board — to a project-level
`resolveProjectColumnsForRoles(store, ["intake", "hold"])`.
**No test could observe that conversion.** All 25 existing triage test
files stayed green (375/375) with the resolver neutered — under *both*
blinds tried.
## Measured
| | default (control) | legacy ids | renamed | differential |
|---|---|---|---|---|
| converted | pass | pass | pass | pass |
| blinded to empty set | pass | pass | **FAIL** | **FAIL** |
| blinded to legacy pair | pass | pass | **FAIL** | **FAIL** |
```
converted: Tests 4 passed (4)
blinded to empty set: Tests 2 failed | 2 passed (4)
blinded to legacy: Tests 2 failed | 2 passed (4)
existing 25 files: green under BOTH blinds (only this new file fails)
triage suite: 25 files/375 tests -> 26 files/379 tests, all green
lint clean; fnxc-future-dates: none added; census unchanged
```
## A prediction I got wrong, and what it changes
The site is **seed-then-union**:
```ts
const sweepColumns = [...new Set(["triage", "todo", ...projectPlannerColumns])];
```
I expected blinding the resolver to its legacy pair `["triage","todo"]`
to be a **no-op by construction** — the seed already contains both. It
is not. That reasoning holds only for a *default* board; on a renamed
board the resolver is the sole contributor of `drafting`, so the legacy
blind drops it and the renamed cases fail.
The corrected rule, which is narrower than the one I was carrying: **a
seed-then-union site hides a defect only while every lane you assert on
is already in the seed.** Assert on a lane that is not, and the union
stops protecting it. The empty-set blind remains the stricter of the two
because it also models a resolver returning nothing at all. Both are
recorded in the test file so the next person does not re-derive it.
## A tooling failure worth naming
My first triage measurement reported **375/375 green under blinding** —
and was a lie. The blinding script had no mapping for
`["intake","hold"]` and exited `2` **silently**; the `&&`
short-circuited the check while a `;` let vitest run against
**unmodified source**. A no-op blind produces a green run that is
indistinguishable from real coverage.
The script now prints what it substituted and where, fails loudly on an
unmapped role or a missing variable, and I self-tested both directions
(bogus var → rc 2 with a message; real var → rc 0 with the substitution
echoed) before trusting any number above. This is the same standard I
have been applying to other people's guards — *a guard that reports
success without checking anything is worse than no guard* — and my own
instrument failed it.
## Also pinned
The **legacy half** of the union. `triage`/`todo` stay in the sweep even
on a board whose workflow declares neither, because pre-U11 and Coding
(Ideas) rows can rest there. Dropping them in favour of the resolved
lanes alone would strand exactly those rows, so there is now a case
asserting it.
## Flagged, not guessed
- The FNXC stamp at `triage.ts:987` reads `2026-07-31-23:59`, hours
ahead of the `date -u` clock. It is one of the 181 grandfathered stamps
so the gate is green; I left it rather than widen this PR.