14 Progressive disclosure is half-shipped at ONE workspace — the workspace-settings surface is still exposed, and an org invite lands the invitee in ZERO workspaces with no way in
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)
Root cause / fix
docs/decisions/organization-tier.md decides two rules independently and never reconciles them at
count = 1:
- §6 — "the UI reveals a tier only when it offers a choice (its count ≥ 2)", and the settings table: "At one workspace the workspace-settings surface is hidden … the single Settings home folds in the workspace-config sections."
- §5 — "Org add ⟹ NO workspace (no downward propagation) … an 'org-only' member in zero workspaces is a valid state — e.g. a billing admin."
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.
Arm A — the workspace-settings surface is still exposed at one workspace
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.
Arm B — an org invite strands the invitee, and their only exit makes it worse
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:
- has no workspace membership →
getWorkspaceContext()is null → no project, no board, no items; - sees no workspace affordance in the header —
ShellTierNav.tsx:103hides the switcher atlength < 2, soWorkspaceSwitcher.tsx:78's own zero-workspace "Create workspace" branch is unreachable dead code in exactly the state it was written for; - is shown to the admin as "No workspaces" (
OrgMembersClient.tsx:496) with no add-to-workspace action anywhere on the org roster; - has exactly one exit —
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.
Fix direction
- Give §5 a count-1 arm. In
addMember, inside the existingwithOrgContexttransaction: when the org has exactly one workspace, also create thatWorkspaceMembership(rolemember). 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. Amendorganization-tier.md§5 + §6 to state the arm, and re-pointtests/organizations-service.test.ts:191at a two-workspace org so it keeps asserting the rule it was written for. - Hide the surface at ≤ 1 (§6d): drop the three navigation entries, replace
WorkspaceConfigCard's link-out with the folded-in Name / Members / Danger-zone sections on/settings/organization, and make/settings/workspaceitselfnotFound()at count 1 so it is not link-reachable either. At ≥ 2 the area returns exactly as it is today. /settings/workspace/githuband/settings/workspace/jobsare 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.
Acceptance criteria
- In an org with one workspace,
organizationsService.addMemberalso creates the invitee'sWorkspaceMembershipfor that sole workspace, in the same transaction, andresolveWorkspaceAccess(invitee, thatWorkspace)is non-null. - In an org with two or more workspaces,
addMemberstill creates no workspace membership (§5 unchanged) — asserted by the re-pointedtests/organizations-service.test.tscase. docs/decisions/organization-tier.md§5 and §6 both state the count-1 arm, so the two clauses no longer contradict at the boundary.- At
workspaceCount <= 1, no rendered markup names/settings/workspace: not inUserMenu, not inAppCommandPalette, not inSidebarNav's settings door, not inWorkspaceConfigCard. (The row is absent, not disabled — the entry-point ruleSidebarNavalready states.) - At
workspaceCount <= 1,/settings/workspaceanswers 404, and the Name / Members / Delete sections are reachable from/settings/organizationinstead. - At
workspaceCount >= 2, every entry point and/settings/workspacerender exactly as they do today.
Context refs
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:191- ⚠️ AC 5 adds a
notFound()underapp/(authed)— the no-loading.tsx-above-an-existence-deciding-route rule inCLAUDE.mdapplies.
Resolution
(open)