Files
fusion/packages/engine
gsxdsm 17fdf2f0ae U4: scope the user-pause safeguard to lifecycle MUTATION, not observation (re-ratified) (#2486)
Stacked on #2482. Base is
`feature/workflow-vocabulary-u4-override-layer` — do not merge before
it.

Implements the coordinator's **re-ratification** of the user-pause
safeguard with a narrower definition.

## The invariant, written into the code

> The user-pause safeguard means **NEVER MUTATE LIFECYCLE STATE** of a
user-paused card. It does **NOT** mean never observe one.

Respecting a pause exists to stop the engine acting on a card *behind*
the operator who paused it — moving, rebounding, archiving, resuming. A
read-only diagnostic does the opposite: it tells that same operator what
their paused card is doing. Blinding them to their own paused work is
not safety; it is the engine deciding they should not be told.

The sentence is in the source, because the distinction is the whole
point and a future reader will otherwise re-broaden it.

## Why it needed narrowing

Ratified broadly first, that reading was caught suppressing the very
sweeps it was meant to protect. `surfaceStalePausedTodos` exists to
report cards that have sat paused too long — routing it through a
reconciler that suppresses paused cards turns a diagnostic into one that
**silently reports nothing**. Measured, not argued: #2484 proves that
sweep surfaces user-paused cards on current main.

## Scoped by action, never by sweep

`OBSERVATIONAL_ACTIONS` is an **allow-list**, so a newly added mutating
action is suppressed by default — the scoping fails closed. A sweep
cannot opt itself out.

`RecoveryActionKind` deliberately names actions the policy vocabulary
cannot yet author (`rebound`, `archive`, `requeue`, `resume`). The
scoping is only testable if those exist as values, and **a rule that
cannot be tested is a rule that erodes**. `parseWorkflowIr` keeps a
closed action list, so nothing becomes authorable by being named.

## Which field — chosen, not inherited

The gate reads `userPaused`, **not** `paused`. The two diverge
(`branch-group-ops.ts:128` says so outright), and `paused` also covers
engine-authored automation pauses like dispatch-storm, which carry no
operator intent to respect — gating on it would suppress recovery from
the engine's own throttles. The safeguard defers to a **human**
decision, so it keys on the field that records one.

## Both halves kept

The broad case is **narrowed, not deleted**: one test proves mutation is
still suppressed, one proves observation is now permitted. A future
reader must be able to tell the scoping was *deliberate* rather than
eroded by someone who found the broad rule inconvenient.

**Mutation-verified in both directions**, since either error is silent:

| mutation | result |
|---|---|
| re-broaden (suppress observation) | **2 tests fail** |
| over-narrow (`rebound` treated observational) | **3 tests fail** |

## Verification

46 tests green (28 safety + 18 inheritance); tsc clean; lint clean;
merge gate green (299+10+71).

No changeset: `@fusion/engine` is private.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-27 17:37:20 -07:00
..
2026-07-26 18:11:47 -07:00