Type · code (two arms: one service, one surface)
Parent · none (root sibling) — out-of-band dogfooding finding, no in-flight card, and the epic that owns the surface (MOTIR-326, Epic 6) is done
Discovered in · manual dogfooding, 2026-08-25, reported by Yue
Repo · motir-core (all sites; the ADR lives here too)
docs/decisions/organization-tier.md decides two rules independently and never reconciles them at
count = 1:
At two or more workspaces both are right. At one they combine into a dead end: the tier the user must be placed into is the tier the UI has hidden. Only the header half of §6 shipped.
ShellTierNav.tsx:103 gates the switcher correctly (workspaces.length >= 2). Nothing else is
gated. /settings/workspace — titled "Workspace settings", carrying Name, Members and a
Delete workspace danger zone — is reachable at count 1 from four places:
| site | gate today | should be |
|---|---|---|
app/(authed)/_components/UserMenu.tsx:83 — the "Workspace settings" row | none (UserMenuProps has no count at all) | hidden at ≤ 1 |
app/(authed)/_components/AppCommandPalette.tsx:305 — "Go to settings" | !hasProject only | hidden at ≤ 1 |
app/(authed)/_components/SidebarNav.tsx:386 — the settings door's no-project target | !hasProject only | hidden at ≤ 1 |
app/(authed)/settings/organization/_components/WorkspaceConfigCard.tsx:44 — the "Open workspace settings" button | renders only at orgWorkspaces.length <= 1 (settings/organization/page.tsx:131) | §6d's fold-IN, not a link-OUT |
The last row is the sharp one: the card built for the collapsed state is the thing that advertises the surface the collapsed state is supposed to hide. Its own comment cites §6d while doing the opposite of it — "the config editors are owned by their own design areas and are not redrawn here" is the under-specified half that shipped.
organizationsService.addMemberByEmail → addMember (lib/services/organizationsService.ts:335)
creates an OrganizationMembership and nothing else. Verified, not read off the code: the shipped
assertion is tests/organizations-service.test.ts:191 — "adding a user to the ORG creates NO
workspace membership" — which asserts workspaceMembership is null and
resolveWorkspaceAccess(...) === null.
So in a one-workspace org the invitee signs in and:
getWorkspaceContext() is null → no project, no board, no items;ShellTierNav.tsx:103 hides the switcher at
length < 2, so WorkspaceSwitcher.tsx:78's own zero-workspace "Create workspace" branch is
unreachable dead code in exactly the state it was written for;OrgMembersClient.tsx:496) with no add-to-workspace
action anywhere on the org roster;OrgControl's "New workspace" — which creates a second workspace,
flipping the org out of the collapsed state and splitting the team across two workspaces. The one
available escape is the wrong outcome.addMember, inside the existing withOrgContext transaction: when
the org has exactly one workspace, also create that WorkspaceMembership (role member). At
≥ 2 the asymmetry stands unchanged — the billing-admin-in-zero-workspaces carve-out is only
meaningful once a choice exists, which is the same predicate §6 already uses. Amend
organization-tier.md §5 + §6 to state the arm, and re-point
tests/organizations-service.test.ts:191 at a two-workspace org so it keeps asserting the rule
it was written for.WorkspaceConfigCard's link-out with the folded-in Name / Members / Danger-zone sections on
/settings/organization, and make /settings/workspace itself notFound() at count 1 so it is
not link-reachable either. At ≥ 2 the area returns exactly as it is today./settings/workspace/github and /settings/workspace/jobs are workspace-scoped but not
workspace-named; §6 is about revealing a tier, not relocating every page under it. They stay.This is one root cause and two subtasks — the service arm (~3 pts) and the surface arm (~5 pts). The sizing here is the whole defect; do not run it as one card.
organizationsService.addMember also creates the invitee's
WorkspaceMembership for that sole workspace, in the same transaction, and
resolveWorkspaceAccess(invitee, thatWorkspace) is non-null.addMember still creates no workspace membership
(§5 unchanged) — asserted by the re-pointed tests/organizations-service.test.ts case.docs/decisions/organization-tier.md §5 and §6 both state the count-1 arm, so the two clauses no
longer contradict at the boundary.workspaceCount <= 1, no rendered markup names /settings/workspace: not in UserMenu, not
in AppCommandPalette, not in SidebarNav's settings door, not in WorkspaceConfigCard. (The
row is absent, not disabled — the entry-point rule SidebarNav already states.)workspaceCount <= 1, /settings/workspace answers 404, and the Name / Members / Delete
sections are reachable from /settings/organization instead.workspaceCount >= 2, every entry point and /settings/workspace render exactly as they do
today.docs/decisions/organization-tier.md §5, §6lib/services/organizationsService.ts:335 (addMember), :376 (addMemberByEmail)app/(authed)/_components/ShellTierNav.tsx:103, WorkspaceSwitcher.tsx:78, UserMenu.tsx:83,
AppCommandPalette.tsx:305, SidebarNav.tsx:386app/(authed)/settings/organization/page.tsx:131, _components/WorkspaceConfigCard.tsx:44app/(authed)/settings/workspace/page.tsx, _components/DangerZoneCard.tsxapp/(authed)/settings/organization/members/_components/OrgMembersClient.tsx:496tests/organizations-service.test.ts:191notFound() under app/(authed) — the no-loading.tsx-above-an-existence-deciding-route rule in CLAUDE.md applies.(open)