**Claim announced on #2714 before starting.**
`packages/core/src/task-store/update-task-deps.ts` — **7 → 0**.
This one settles an open question and turned up a **live bug on the
shipped default board**, not just on renamed ones.
## 1. What "dependency satisfied" means — settled, not guessed
I flagged this in three files rather than swapping it three times
independently (`executor.ts:12325`,
`register-task-workflow-routes.ts:3995`, here). The answer has to be the
same everywhere or the scheduler and the store disagree about which
cards are blocked. Settled in the store, where `blockedBy` is actually
written:
- **SATISFIED** = the dependency's own board's **complete** or
**archived** column. Archived counts: it is finished work the operator
filed away, and reading it as unsatisfied blocks every dependent forever
with no recourse short of editing the graph.
- **NOT review** — a card in review is not done; its branch has not
landed.
- **Unioned with the legacy ids**, because a row can outlive the column
it is stored in.
On a renamed board the old literals matched nothing, so **every**
dependency read as unresolved and `blockedBy` was pinned to the first
one permanently — dependents never unblocked after the work landed.
## 2. The re-specification move was writing a DELETED column — on the
default board
`hasNewDependencies && column === "todo"` set `column = "triage"`. **U11
(#2515) deleted `triage`**, keeping `todo` as the merged Planning
column. Measured, not assumed:
```
resolveDefaultWorkflowIr() columns:
todo[intake,hold,reset-on-entry] in-progress[wip,…] in-review[merge,…] done[complete] archived[archived]
```
So the store has been writing a column the shipped board does not
declare. And the emitted event hardcoded `from: "todo", to: "triage"` —
**`task:moved` is what the GitHub tracking poster, the auto-merge
handoff and the executor's listeners react to**, so every subscriber was
being told about a column that does not exist.
Now the guard reads the hold lane, the target is the intake lane, the
log line names the real column, and when intake === hold (the default
lineage post-U11) there is **no move and no event** — announcing a move
into the column the card already occupies re-runs reset-on-entry effects
in every listener.
## 3. Two existing suites taught me more than the conversion did
**`refine-duplicate-task.pg.test.ts` proved the union is required, in
one run.** My first version compared only the resolved lanes and refused
a row sitting in `done` on a board declaring `published`: *"Cannot
refine KB-001: task is in 'done', must be in 'published' or
'editorial-review'"*. That row is real, and refusing an operator action
on it is worse than accepting one extra column name. Over-inclusion is
the safe direction for "may I refine this?" — the same reasoning as the
executor's `resolveTerminalColumnsFor`.
**Two assertions expected `"triage"`.** They were not protecting
behaviour; they were protecting a stale literal that outlived its
column. Updated **with the measurement in the file**, because a
silently-changed expectation is indistinguishable from a broken one.
## Revert proof
1 of 3 new PostgreSQL cases reddens when the union is removed. Driven
through the real store on PostgreSQL because `blockedBy` is persisted
and resolution reads the workflow selection from the database — a mocked
store would prove neither.
## Verification
`pnpm test:gate` **487 / 71** · 13/13 in the two pre-existing
dependency/refine suites · 3/3 new · `tsc -p packages/core` clean ·
`pnpm lint` clean · census `--strict` exit 0 (**7 → 0** for this file).
Changeset: none. `@fusion/core` is private, and while item 2 is an
operator-visible fix on the default board, it lands as internal
behaviour with no API change — say the word if you want one anyway for
the release notes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>