Files
fusion/packages
gsxdsm a56253f426 P0: plan approve/reject rejects EVERY card on a merged planning column — operator-visible stall, cannot approve or reject (#2571)
## P0 — plan approve/reject is dead for cards on a merged planning
column

**This is the "card stuck with nothing to rescue it" case you asked to
hear about immediately.** Found auditing my files after #2515.

### What happens

#2515 removed `triage` from the merged default lineage — one
pre-implementation column, id `todo`, displayed "Planning". Four routes
guard with:

```ts
if (task.column !== "triage") throw badRequest("Task must be in 'triage' column ...")
```

On a workflow with no `triage` column that condition is **true for every
card**, so the routes reject all of them:

| route | effect on a merged-lineage card |
|---|---|
| `POST /tasks/:id/approve-plan` | 400 — **cannot approve** |
| `POST /tasks/:id/reject-plan` | 400 — **cannot reject** |
| `task_refine` route (×2) | 400 — refine blocked |

A card parked `awaiting-approval` can be **neither approved nor
rejected**. It is stuck, the operator is being asked for a decision they
have no way to give, and nothing throws to reveal it.

### Why it is the worst variant of this drift

Everything we have chased so far is a guard that silently **stops**
firing. This is a guard that silently starts firing on **everything** —
same root cause, opposite symptom, and worse, because the failure is
visible to the operator as a task that demands an answer and refuses
every one.

### The fix, and a deliberate choice

The guards resolve the workflow's own intake column through the existing
`resolveIntakeColumnForTask`, and they **widen rather than replace**: a
card is accepted if it is in the resolved intake column **or** in
`triage`.

That is on purpose for a P0. The fix cannot reject anything the route
previously allowed, so it carries no regression risk of its own.
Narrowing to the resolved column alone is a follow-up once the legacy id
is gone everywhere — not something to do under time pressure on a route
that gates operator decisions.

I found the value of that when a strict replacement broke 3 pre-existing
tests in `stranded-refinements-routes.test.ts`. The widened form passes
all of them **and** the new P0 cases.

### The convergence number goes UP, and I am not hiding it

Live-code `column === / !== "todo" | "triage"` in
`register-task-workflow-routes.ts`: **10 → 11**.

Each converted guard keeps the legacy id as an explicit second
condition, so a widened guard has two literals where it had one. The
metric counts id literals; it does not know the guard is now strictly
more correct. Reporting the direction that is true rather than the one
that looks better — and flagging that this file's number will only fall
once the widening can be removed.

### Revert-proof

Restore either bare literal and the matching case fails with **400 where
200 is expected**, on a `todo` card with `awaiting-approval`. A third
case pins that the guard still **narrows** — an `in-progress` card is
still rejected — so this cannot be mistaken for deleting the check.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, dashboard typecheck
green. New suite 3 passed; `stranded-refinements-routes.test.ts` back to
5 passed (it was 3 failed under the strict form).

### Still auditing

`TaskDetailModal.tsx` conversion is in flight on a separate branch.
`TaskCard.tsx` (#2558) and `ListView.tsx` + `taskActivity.ts` (#2566)
are already open — and note #2566 covers `isTaskAgentActive`, whose
planner-lane clause has the *silent* version of this same bug: planning
cards read as idle everywhere at once.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:11:50 -07:00
..
2026-07-26 18:11:47 -07:00