Found while running MOTIR-1167 (motir run, 2026-08-26, branch subtask/MOTIR-1167-day1-platform-admin, base origin/main @ 2852f19b2). Filed under the planner-bug home because the fix is a change to the plan and to the rule that would have prevented it, not to product code — the code shipped in MOTIR-1167's own PR.
docs/decisions/platform-staff-auth.md §7 is the allocation table every consumer builds to. It carries this row:
| Suspend / reactivate an account | 8.5 MOTIR-1167 |
operator| yes | yes |
and design/platform-admin/design-notes.md Panel 9 states the behaviour precisely:
"They are signed out of every session immediately and cannot sign back in."
Both allocate the ACTION. Neither allocates the MECHANISM, and the mechanism did not exist. Verified on origin/main @ 2852f19b2 before a line was written:
git grep -iE 'suspendedAt|suspended_at|banned|banReason' prisma/schema.prisma → 0 hits. User had no suspension column of any shape.admin plugin — which ships banned / banReason / banExpires — is not in lib/auth/index.ts's plugins array (nextCookies, deviceAuthorization, twoFactor).So the card as written asked for a WRITE and got, unpriced: a schema migration, a session-revocation path, and a change to the authentication boundary — which session-creating path is refused, where that refusal lives, and what the person on the other end is told.
Every signal read green, and each was answering a narrower question than the one that mattered.
validate_work_item("MOTIR-658") returned valid: true with the card ready: true, openBlockers: []. Its likely-over-gate-sizing advisory reads the card's DECLARED storyPoints / estimateMinutes — 5 and 60, comfortably under the gate — so an under-estimate is precisely the defect that channel is structurally unable to report.likely-missing-edge scans a card's own description for not-done work items. The suspension mechanism has no work-item key to name, so there was nothing to detect.PlatformUsageDTO, the console's layout, the audit vocabulary's seed, hash-chained tamper evidence, the production bootstrap of the first staff row. It does not name the suspension mechanism, so the card and the ADR agreed the work was owned while neither owned it.The 2026-08-17 amendment on MOTIR-1167 (from the MOTIR-2933 close-out) came within one sentence of catching it. It correctly widened the criterion from one write to two, and then said:
"Not changed, deliberately: the estimate (60 min / 5 pts). The second write shares the drill-down, the confirm dialog and the
PlatformAuditLogwriter the first one needs, so it is one more action on built rails rather than a second feature."
Half right, and the wrong half is the load-bearing one. The second write does share the drill-down, the dialog and the audit writer — the SURFACE was correctly priced. What it does not share is its own SUBSTRATE, and nothing prompted anybody to ask whether it had any. Send password reset had substrate (the shipped requestPasswordReset flow); Suspend account had none.
Run against a production build, 2026-08-26 — the card is 5 points / 60 minutes, and the delivered diff is 35 files / +3,782 lines, of which the suspension mechanism alone is a migration, two schema columns, a repository with a SELECT … FOR UPDATE guard, lib/auth/accountSuspension.ts, a databaseHooks.session.create.before hook, a copy branch on both credential sign-in surfaces, and its own test file. The two panels priced at 60 minutes were, by themselves, roughly the size the card named.
Two things, and the second is the one that stops it recurring.
The plan. Re-estimate MOTIR-1167 on the record (its numbers are wrong on the row, not merely optimistic), and add a row to platform-staff-auth.md's "deliberately does NOT decide" table naming the account-suspension mechanism and its owner. As of this filing the mechanism ships in MOTIR-1167's PR, so the row records where it landed rather than deferring it — the MOTIR-1916 rule, applied after the fact.
The rule. A planning-rule limb, in the pack the estimation gate lives in: an allocation table that names an ACTION owes a line about the SUBSTRATE that action writes to. "Suspend an account" is a verb over a column, a session set and an auth path; "send a password reset" is a verb over a flow that already ships. The two look identical in a table with an owner column and a role column, and the discriminator — does the thing this verb acts ON exist yet? — is exactly plan-rules/phase-deepen.md's precondition-verification rule, which nobody thought to run on a WRITE because the rule reads as being about surfaces a card renders. The dispatch-time backstop (run.md guard #4) has the same blind spot for the same reason.
The card was not wrong about what to build, and no re-plan was run: run.md's guards #2–#5 are manual/human, the design gate, an absent precondition and a repo straddle, and none of them fired. There is no run-time sizing guard, and inventing one from this incident would be the wrong generalisation — under-estimates are ordinary. What is not ordinary is an allocation table that assigns a write whose substrate no card owns, and that is what this records.