Stacked on #2478. Base is
`feature/workflow-vocabulary-u4-reconciler-slice` — do not merge before
it.
Implements the ratified precedence: **declared explicitly → policy wins;
left unset → defer to the project/global setting, exactly as today.**
## The design
`resolveEffectiveRecovery(declared, inherited)` composes the two **per
field**, so a workflow may declare a threshold while inheriting the
action. It mirrors the two-tier merge `effective-settings.ts` already
implements for workflow settings (a stored value overrides the base; a
declaration default only fills an absent key) rather than inventing a
fourth precedence system beside model selection, project settings, and
workflow settings.
**Absence stays absent.** `??` treats an explicitly-`undefined` field as
unset, so a policy is never normalized into a built-in default. The
distinction a naive implementation gets wrong:
> **equal-to-default is not the same as unset**
A declaration whose value happens to equal the legacy literal is a
*deliberate choice* and must still override a customized operator
setting. Only true absence defers.
An effective policy requires **both** halves — a threshold with no
action never fires, an action with no threshold has nothing to fire on —
so a half-resolved policy yields `undefined` rather than something
present but inert.
## The test that matters was written first, and failed
> a project with a CUSTOMIZED threshold and the policy key UNSET must
observe the customized value
This is where a green suite lies. "Read the policy, else use the
built-in default" passes every obvious test while silently resetting an
operator who tuned `stalePausedTodoThresholdMs` — no error, nothing in
any diff, the sweep just starts firing on a schedule nobody chose.
**Mutation-verified in both directions:**
| mutation | result |
|---|---|
| substitute a built-in default for the inherited setting | **5 tests
fail** |
| invert precedence (inherited beats declared) | **3 tests fail** |
## Upgrade guarantee
Asserted as a property over several operator values: an undeclared
workflow observes *exactly* the operator's value. That is what makes
landing the policy table a zero-behavior-change upgrade that touches no
project.
## What is NOT here
**`surfaceStalePausedTodos` is not retired.** Migrating it surfaced a
safeguard-semantics collision I escalated rather than resolved
unilaterally: the sweep exists to surface cards that have been
**paused** too long, but the reconciler's ratified user-pause safeguard
suppresses `surface` on user-paused cards — so migrating it as-is would
suppress a large part of what the sweep is for. `paused` and
`userPaused` are distinct fields that can diverge (see
`branch-group-ops.ts:128`). The sweep is untouched pending that
decision.
## Verification
- 34 tests green (10 new inheritance + 24 safety)
- `tsc --noEmit` clean, `pnpm lint` clean
No changeset: `@fusion/engine` is private.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Recovery decisions now correctly inherit operator settings when a
workflow does not specify a recovery policy.
- Workflow-specific recovery settings override inherited values,
including when matching built-in defaults.
- Recovery settings can now be applied independently by field, allowing
thresholds and stale-item actions to inherit separately.
- Recovery is suppressed safely when no complete policy is available,
preventing unintended recovery actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->