Found by **auditing for the pattern** after hitting it twice, rather
than waiting for a third instance.
## The contract, previously undocumented
`LifecycleColumns` names **one** column per role even when a workflow
declares several. Nothing validates that a trait appears at most once —
`columnsWithFlag` returns an array and `resolveLifecycleColumns` takes
its head. So a workflow may legitimately declare two intake lanes, or
split `mergeBlocker` and `humanReview` across a merge lane and a
separate sign-off lane, and the struct names only one of each.
That makes the fields safe for one question and unsafe for another:
| | question | correct? |
|---|---|---|
| **safe** | *"where should this card **go**?"* | a move target must be
exactly one column |
| **unsafe** | *"is this card **already** there?"* | membership — use
`columnsWithFlag(...).includes(...)` |
## Two shipped bugs, both the unsafe use
Both in #2713, both reading like ordinary conversions:
- a task in a **second terminal column** was rejected with a 409
- a task in a **human-review lane split from the merge lane** was
classified as outside review entirely, suppressing comment re-engagement
I fixed the first, did not generalise, and hit the second one review
round later. That is the actual failure mode this PR addresses — not the
individual bugs, which are already fixed.
## Two call sites left for their owners
Named in the doc comment rather than changed here, since they belong to
other clusters. Both are correct **only while their workflow declares
one column per trait**:
- `packages/engine/src/self-healing.ts` — `columns.intake` /
`columns.hold`
- `packages/core/src/builtin-workflows.ts` — `lifecycle.intake`
## The test asserts the contrast, not just the arity
A reader who sees only *"it returns one id"* learns the wrong lesson.
The test builds a workflow with **two intake and two complete columns**
and shows that exactly one qualifying column **fails an equality check**
against the struct while the **membership form gets it right** — the
shipped-bug shape, reproduced in miniature.
## Verification
`workflow-lifecycle-traits.test.ts` **20 → 22**. `pnpm test:gate` green
(10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. `tsc -p
packages/core/tsconfig.json` clean. `pnpm lint` clean.
Docs + test only — no behaviour change, no census movement, no
changeset.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>