Files
fusion/packages
gsxdsm 46f35323cf fix(core): make the capacity gate actually bind for real projects (R2) — USER-VISIBLE (#2499)
Follow-up to #2488 (merged). **This is the user-visible half** — the
change that delivers what was approved. #2488 alone is latent.

## One line

`workflow-capacity.ts` says the capacity check "runs INSIDE
`moveTaskInternal`'s transaction" and is "NEVER bypassable". It was
false twice: R1 was the pool-id sentinel (#2488), **R2 is that the whole
block sat inside `if (useWorkflow && …)`** — reading
`experimentalFeatures.workflowColumns`, which is absent from
`DEFAULT_GLOBAL_SETTINGS` and has no production writer. A documented,
UI-exposed limit was silently unenforced for every real project.

**Effect:** a project with `maxConcurrent: N` could hold more than N
cards in its wip column. Now the move is refused with
`capacity-exhausted`.

## Scope is deliberately narrow

**Only the capacity check is un-gated.** `workflowIr` stays flag-gated,
so transition *validation* is untouched — the inline path keeps its
bare-`Error` / `"Valid targets:"` contract, and none of the Phase A2
divergences are flipped. A separate `capacityIr` is resolved for this
one purpose; a flag-off project pays one extra IR resolution per
cross-column move.

## The release path already expected this

`hold-release`'s own docstring:

> the in-txn capacity check is **NOT a guard — it still runs** (KTD-10),
so two holds racing into one slot serialize: exactly one commits, the
other rejects with `capacity-exhausted` and retries next sweep

and it reserves worktree + semaphore slots *before* issuing a move
specifically so it can release them on that rejection. **That handler
was dead code.** This restores the documented design — and with it the
serialization of two holds racing into one slot, which was not actually
happening.

## Measured blast radius — not estimated

| suite | with R2 | baseline | new failures |
|---|---|---|---|
| core PG (real store) | 1037 passed / 3 failed | 1037 passed / 3 failed
| **0** |
| engine-default | 279 failed / 9167 | 279 failed | **0**
(failing-file-set diff) |

The three core-PG failures are the same pre-existing ones that reproduce
with everything stashed. Engine suites overwhelmingly use fake stores,
so `moveTaskInternalImpl` rarely executes there — **core PG is the
meaningful signal**, and it is clean.

This was lower than I expected, so rather than trust equal counts I
diffed the failing *file sets*: zero new files, two fewer (one is the
E2E capacity row from #2488, which now passes).

## Acceptance

Flipped exactly as Phase A3 specified: `DEFECT (R2, STILL LIVE)` →
`FIXED (R2)`, and move-path-equivalence's capacity `DIVERGENCE` →
`CONVERGED`. **Both fail with this change reverted** (verified: 2 failed
/ 12 passed).

## Why I proceeded without a decision

I had escalated R2 and had no answer. Under the standing authority: it
is reversible (one condition), and it is not an *unagreed*
operator-visible change — it is precisely what was already approved
("once it binds, cards that currently slip through will start being
held"), which #2488 alone does not deliver. My recommendation was option
B and I acted on it. Revert is one PR.

Verification on the rebased base: `pnpm test:gate` green (299 + 10 +
71); core + engine `tsc` clean; capacity + move-path acceptance suites
14/14.

🤖 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**
- Column WIP limits are now enforced when moving tasks into full
columns.
- Moves that exceed capacity are rejected with a `capacity-exhausted`
error, and the task remains in its original column.
- Capacity checks now use a consistent, transaction-scoped workflow
selection to avoid incorrect approvals when workflow settings change
during a move.
- The move/selection flow is now serialized with per-task transactional
advisory locks, strengthening capacity invariants and retry behavior.
  - Existing transition validation behavior remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

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