`resolveWatchedStage` keyed on the literals `in-progress`/`in-review`,
so on a board that renames either it returned `null` for **every** card.
That is three literals with an outsized blast radius. `observeTask`
returns early on a null stage, so:
- no `OverseerStageObservation` is recorded,
- no `overseer:intervention` entry is emitted,
- and `PlannerRecoveryController`, which consumes those observations,
has nothing to steer, retry or targeted-fix.
**The entire oversight loop was inert and silent about it** — the same
shape as the self-healing sweeps whose queries returned empty arrays.
## I deferred this myself, on a cost argument that was wrong
The audit note I wrote for this site said resolving inside `observeTask`
"buys a workflow read per card per poll". Then I read the caller: the
poll **already awaits `resolveEffectiveSettings` per task**. It is a
per-task async loop regardless, so with an IR cache keyed by workflow
the addition is *(distinct workflows)* resolutions, not *(cards)*.
Pricing the fix before checking the caller cost a deferral. Worth
recording, because "this needs a cost judgement" is the most comfortable
place in this program to leave something.
## The review test is the three-trait union, deliberately
`isReviewColumnRole` checks only `mergeBlocker || humanReview`. A board
whose review lane carries `merge` (**mergeOrchestration**) — the
built-in default's own shape — would classify as *not in review* and be
skipped.
Reaching for the obvious helper would have reintroduced the bug this
change removes, through the helper meant to fix it. There is a case
asserting exactly that.
## Wiring
Both call sites, because either alone leaves a hole:
| site | why it matters |
|---|---|
| the poll (`project-engine.ts`) | per-poll IR cache — a workflow edit
is picked up next tick rather than served stale |
| the manual nudge | otherwise a renamed board answers `no-active-stage`
to an operator pressing the button |
`columnFlags` is in the `unwired-lane-parameter` vocabulary, so the
wiring cannot silently rot — the guard reports it if a future change
drops the argument.
Fail-soft throughout: an unresolvable workflow yields `undefined` and
the callee falls back to the legacy ids, which is exactly today's
behaviour. A v1 IR declares no columns, so it takes the same path.
## Revert proof (measured)
Drop the `columnFlags` branch and **exactly the three renamed-lane cases
fail**:
```
expected null to be "executor"
expected null to be "merger" (mergeOrchestration lane)
expected null to be "merger" (humanReview lane)
```
The legacy-id and neither-role cases stay green — the gate must still
gate, and watching every column would be its own defect.
## Verification
- `pnpm test:gate` — 161 / 487 / 13 / 71 passed
- `pnpm lint` — clean
- `tsc --noEmit` (`@fusion/engine`) — clean
- `planner-overseer.test.ts` +
`planner-recovery-controller-human-control.test.ts` — 64 passed
- unwired-lane guard — 9/9, no new entries
Carries the one-line SQL-baseline re-record (`team-analytics.ts: 6 → 3`)
that #2864 left behind, same as my other open branches — main is red on
it, and identical changes to that line merge without conflict.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>