Stacked on #2486. Base is `feature/u4-safeguard-scoped-to-mutation` — do
not merge before it.
Retires the surfacing family — `surfaceStalePausedTodos`,
`surfaceStalePausedReviews`, `surfaceInReviewStalled` — onto one
policy-driven runner. Migrated **together**, because they were three
copies of one skeleton and a fix applied to one had to be remembered for
the other two.
#2484's characterization suite is the regression floor and **passes
unchanged**.
## Two unconverted sites found in core
Without these the migration would have been **cosmetic for two of the
three**:
`getStalePausedReviewSignal` and `getInReviewStalledSignal` both
hardcoded `task.column !== "in-review"`. `getStalePausedTodoSignal`
gained the equivalent `holdColumn` parameter back in B1 — its two
siblings were missed, so they silently stopped matching for any workflow
that renames its review column. Both now take `reviewColumn`, defaulting
to the legacy id.
## Three bugs this work introduced, and the tests caught
Each was silent in the diff:
1. **I dropped the engine activation floor** from all three signal
calls. Wall-clock the engine wasn't running for is not quiet time, so
every sweep would have reported cards as stale purely because the engine
restarted. Caught by the **pre-existing** suite — not by my own
characterization floor, which is exactly why that floor wasn't
sufficient alone.
2. **I passed `task.column` as the role column**, making the signal's
own column check compare a value against itself — tautologically true,
so the check was silently deleted. The *resolved* role column is now
passed.
3. **The role gate was conditional on the role resolving**, so it was
silently absent for exactly the workflows whose role failed to resolve.
Now unconditional, with the legacy id as fallback.
## The shared test is a table
One row per sweep; every invariant asserted for all three — threshold
inheritance, declared-policy override, renamed role column, the negative
case outside the role column, at-most-once, activation floor, both pause
gates, non-positive-threshold disable, soft-delete. **Adding a fourth
surfacing sweep means adding a row.**
Its log mock **appends** to the card's log, because the at-most-once
dedup reads that log — a call-recording mock cannot observe suppression
at all.
## Measured line delta — this corrects the survey estimate downward
| | lines |
|---|---:|
| `self-healing.ts` | **−192 / +95 = −97** |
| new runner file | **+209** |
| core signal conversions | **+23** |
| **NET** | **+135** |
Three sweeps migrated **increased** total lines by 135 while shrinking
`self-healing.ts` by 97. Per sweep: **−32** in `self-healing.ts`.
**Break-even is 6.5 sweeps.**
Extrapolated to all 34 POLICY sweeps: **−1,099** in `self-healing.ts`,
**−890 net** once the runner is amortized.
My survey estimated **−2,612** for that bucket. The measured figure is
**34% of it**. The reconciler's size was never the issue — the estimate
assumed per-sweep bodies collapse to almost nothing, and they do not:
each keeps a real eligibility predicate, signal call, and operator
message.
## Verification
- 33 shared-family tests green
- 471 passed across five engine suites, with the single known
**pre-existing** `archiveStaleDoneTasks` failure
- 44 core signal tests green
- tsc clean in core and engine; lint clean; merge gate green (299 + 10 +
71)
No changeset: `@fusion/core` and `@fusion/engine` are private.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added support for overriding which workflow column is treated as the
relevant “in review” column for stale-paused and stalled-review
surfacing.
- **Bug Fixes**
- Tightened safety safeguards so only lifecycle-mutating recovery
actions are blocked when a card is user-paused; observational surfacing
remains allowed.
- Improved surfacing consistency across stale paused todos, stale paused
reviews, and in-review stalled cases, including stronger deduping and
cycle-aware behavior.
- **Tests**
- Expanded and reworked safety and surfacing “family” coverage to verify
the new invariants.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->