The utility carve-out ruling 20 omitted, and both paths end in the drain
Ruling 20 was over-broad in a way that would have shipped an accessibility bug. Seven utilities — rst-sr-only, rst-mono, rst-m-hide, rst-grow, rst-nm, rst-danger, rst-cell-mut — have NO attribute twin at v0.22.0: five .rst-sr-only selectors and zero [rst-sr-only]. tokens.css says so in the same header this spec already quotes, and rastrillo's §6-v3 says "class is for utilities and the app's own CSS". An agent following ruling 20 literally writes <span rst-sr-only>, nothing matches, and screen-reader-only text renders visible. Kinds, variants and parts are attributes; those seven stay class. This is the same failure class as the two dead reasons: a rule that reads right and is false at the pin. §7 implied the fast path bypasses the operator. It cannot: §7's own load-bearing safety fact is that the home holds no provisioning credential, so nothing but the drain creates an instance, and ruling 7's "a human sees every team before it exists" applies to siblings exactly as to first teams. Both paths enqueue. What differs is what the operator is handed — an instance-vouched request arrives with the label question already settled, an unvouched one does not. Also recorded, because each was a door left open: consumer-level authorization is §9's pre-existing business and not what §2.2 forbids, which is a person-level entitlement; the new instance's first owner and welcome address arrive as sealed queue payload, carried opaquely and never judged; the fallback's hint must show kind, since a guest row in acme.docs is emphatically not "is Acme"; and a compromised acme.docs can vouch for acme.* siblings, bounded by provisionable, by one row per team and app, and by the drain still seeing everything. The §9 rename event could not survive the display-name ruling: if call 2's name never overwrites, a re-announce un-stales nothing. Rename becomes an operator verb at the home — one row, one write, immediately true for every instance and guest, which beats a re-announce that waited on each instance waking. Call 2 now drives two events, not three, and its sealed team name is advisory: a consistency check the home may log a divergence on, never a write. Plus the four the last commit claimed and did not deliver or left inconsistent: §11 still opened by ruling the class vocabulary thirteen lines above ruling 20; §15 still listed the spelling as needing Paul after he had ruled it; §14's auto-drain row still said two decisions when §7 now names a third; and §9's team-deleted event was still team-scoped, because the sentence wrapped and the edit missed it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+70
−28
docs/superpowers/specs/2026-08-30-home-design.md+70 −28
CI passed — run details
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md| index 37b1dff..cbeb7e7 100644 |
| --- a/docs/superpowers/specs/2026-08-30-home-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-home-design.md |
| @@ -1,6 +1,6 @@ |
| # 🤖 The Oficina home — design |
| -**Status:** design, revised **2026-09-05** (rulings 17-19, and four claims |
| +**Status:** design, revised **2026-09-05** (rulings 17-20, and four claims |
| corrected). Originally 2026-08-30. Drafted by the Sheets |
| session with Docs (`oficina/docs`) as a declared consumer. |
| **Review passes:** consumer review by docs-e6 (5 findings, all applied); |
| @@ -60,8 +60,8 @@ blind, replaceable*. |
| | 16 | **`oficina.works` is the primary domain — RP `id.oficina.works`, directory at the apex, instances at `<team>.<app>.oficina.works`** — §4 | Staying on `oncarlos.com` until passkeys forced the move; keeping the `bds` slot | Paul, 2026-09-03 | |
| | 17 | **The springboard is one page: an app grid, with the team switcher as a dropdown in the header. Which apps exist comes from a registry, not from your grants. A team is a suite-wide entity that instances belong to** — §11, §10 | A team-switcher screen as a first level; deriving tiles from grants alone, which hides an app you have no team in and leaves nowhere to press New; grouping by hostname label alone, with no row that exists before the first instance and outlives any one of them | Paul, 2026-09-05 | |
| | 18 | **Activity is approved in principle, pulled at render, and sequenced last** — the front door shows a real feed with titles, the home holds none of it at rest, and its slot is a step 10 after step 4 — `design-activity-pull` | Push into the home; a contentless heat map; refusing the feature | Paul, 2026-09-05 | |
| -| 20 | **Write the attribute spelling — `rst-btn`, not `class="rst-btn"`** — §11 | The class vocabulary, which this section ruled on 2026-08-30 and whose every stated reason has since died; "follow whatever the pin's partials use", which changes this app's markup on a dependency bump | Paul, 2026-09-05 | |
| -| 19 | **A team name is reserved suite-wide. An existing instance may expand its own team; anyone else falls to the drain. The home answers only facts about its own records, never "may this person"** — §7 | Per-app first-come, which lets a stranger squat `acme.sheets` behind your `acme.docs` on a domain that reads as one suite | Paul, 2026-09-05 | |
| +| 19 | **A team name is reserved suite-wide. Both paths end in the drain — an existing instance settles the label question for the operator, it does not bypass them. The home answers only facts about its own records, never "may this person"** — §7 | Per-app first-come, which lets a stranger squat `acme.sheets` behind your `acme.docs` on a domain that reads as one suite | Paul, 2026-09-05 | |
| +| 20 | **Write the attribute spelling — `rst-btn`, not `class="rst-btn"`. The seven unpaired utilities keep `class`** — §11 | The class vocabulary, which this section ruled on 2026-08-30 and whose every stated reason has since died; "follow whatever the pin's partials use", which changes this app's markup on a dependency bump | Paul, 2026-09-05 | |
| --- |
| @@ -564,19 +564,42 @@ ruling 15 rejected a per-team 2FA floor — a policy with no content in it at |
| all — on exactly these grounds. So the distinction has to be sharper. Two |
| paths, and the home answers only facts about its own records in both: |
| -- **Fast path — an existing instance expands its own team.** "Create |
| +**Both paths end in the queue**, and this is not a detail. §7's |
| +load-bearing safety fact is that **the home holds no provisioning |
| +credential** — so nothing the home can do creates `acme.sheets`, and ruling |
| +7's "a human sees every team before it exists" holds for siblings exactly as |
| +for first teams. What the two paths differ in is what the operator is |
| +handed, not whether there is one. |
| + |
| +- **Fast path — an existing instance vouches for its own team.** "Create |
| `acme.sheets`" is initiated from inside `acme.docs`, by whoever |
| - `acme.docs` considers an owner, and reaches the home as an |
| - instance→home call under that instance's consumer credential. The home |
| - checks one thing: **is the calling origin already in `team_instances` for |
| - this team?** That is a row the home itself wrote at the drain, not a |
| - judgement about a person. Who counts as an owner is the instance's |
| - business, decided from its own grants — §2.2's second never-do, working |
| - exactly as intended. |
| -- **Fallback — everyone else goes to the drain.** A request for a reserved |
| - label from anywhere else is enqueued, and a human decides. The home may |
| + `acme.docs` considers an owner, and reaches the home as an instance→home |
| + call under that instance's consumer credential. The home checks one |
| + thing: **is the calling origin already in `team_instances` for this |
| + team?** — joining `consumers.origin` to the instance host the home itself |
| + recorded at the drain. That answers the *label* question and enqueues the |
| + request pre-settled. Who counts as an owner is the instance's business, |
| + decided from its own grants — §2.2's second never-do working exactly as |
| + intended, and the home never learns the answer. |
| +- **Fallback — everyone else arrives unvouched.** The request is enqueued |
| + the same way, and the human decides the label question too. The home may |
| render the requester's own directory rows beside it as a **hint** (ruling |
| - 9), and that is all they are: the operator adjudicates, the home does not. |
| + 9) — with `kind` shown, since a *guest* row in `acme.docs` is emphatically |
| + not "is Acme". They are a hint and nothing else: the operator adjudicates. |
| + |
| +**Two things this does not smuggle in.** Consumer-level authorization is not |
| +new and is not §2.2's concern: §9 already scopes every call to its consumer, |
| +and a label on `oficina.works` is the home's own namespace. What §2.2 |
| +forbids is a *person*-level entitlement, and neither path forms one. And the |
| +new instance's first owner and welcome address arrive as **sealed queue |
| +payload**, exactly as a fallback requester's do (§3.2) — the home carries |
| +that opaquely and never judges it. |
| + |
| +**A compromised `acme.docs` can vouch for `acme.*` siblings.** Bounded by |
| +the registry's `provisionable` set, one row per (team, app), the drain still |
| +seeing every request, and the sibling getting its own consumer token from |
| +that drain rather than inheriting one. Recorded rather than left to be |
| +discovered; §2's blast-radius argument survives it. |
| So the home's whole vocabulary here is two facts: *is this label free*, and |
| *is this calling origin already an instance of this team*. It never forms an |
| @@ -650,7 +673,7 @@ every assertion, and one that fails §5.4 is not a preference. |
| | # | Call | Direction | Notes | |
| |---|---|---|---| |
| | 1 | Sign-in and session minting | person → home | The identity authority. Not a hint. | |
| -| 2 | Directory upsert / revoke | instance → home | Sends address + sealed team name; home hashes; **returns the person ref** and the pepper epoch. Team-scoped revoke included. Via the outbox. | |
| +| 2 | Directory upsert / revoke | instance → home | Sends address **+ sealed team name, which is advisory**: the home owns the display name (§10) and does not overwrite it, so this is a consistency check the home may log a divergence on, never a write. Home hashes; **returns the person ref** and the pepper epoch. Instance-scoped revoke included. Via the outbox. | |
| | 3 | Directory list | person → home | Every team across every app, with `kind`. | |
| | 4 | Provisioning enqueue | person → home | Rate-limited, depth-capped, human-drained. | |
| | 5 | Rekey journal pull | instance → home | **Scoped to this consumer**, keyed on person refs. | |
| @@ -665,11 +688,20 @@ verb exists before launch rather than after the first leak. |
| **Bootstrap:** a fresh instance gets its token as platform config and its |
| allowlist entry from the drain, in the step that creates it. |
| -**Call 2 drives three events**, not one: a grant changes; a team is renamed |
| -(re-announce, so guests stop seeing a stale name); a team is deleted |
| -(team-scoped revoke). Without the third, rows point at a host that 404s — |
| -a convenience failure under §6, which is exactly why it would survive being |
| -missed. |
| +**Call 2 drives two events**, not one: a grant changes; and an **instance** |
| +is deleted (instance-scoped revoke — after §10's split this removes that |
| +`team_instances` row and its directory rows, and leaves `teams` and its |
| +label reservation alone while any sibling instance survives). Without the |
| +second, rows point at a host that 404s — a convenience failure under §6, |
| +which is exactly why it would survive being missed. |
| + |
| +**Renaming is no longer one of them.** An earlier draft had call 2 drive a |
| +re-announce "so guests stop seeing a stale name". That cannot work now the |
| +home owns the display name (§10): if call 2's name never overwrites, a |
| +re-announce un-stales nothing. **Rename is an operator verb at the home** — |
| +one row, one write, immediately true for every instance and every guest, |
| +which is strictly better than a re-announce that depended on each instance |
| +waking up. |
| ### 9.1 Call 5, rewritten |
| @@ -741,8 +773,20 @@ restored is a hope, not a backup* — the restore drill runs before launch. |
| ## 11. Shape and design |
| -rastrillo `server` shape, zero-JS baseline, `day` theme, and the **class** |
| -vocabulary — `class="rst-*"`. |
| +rastrillo `server` shape, zero-JS baseline, `day` theme, and the |
| +**attribute** vocabulary — `rst-btn`, not `class="rst-btn"` (ruling 20). |
| + |
| +**The seven utilities are the exception, and they are not a loophole.** |
| +`rst-sr-only`, `rst-mono`, `rst-m-hide`, `rst-grow`, `rst-nm`, `rst-danger` |
| +and `rst-cell-mut` **keep the class spelling**, because they have **no |
| +attribute twin at all** — verified at v0.22.0: five `.rst-sr-only` |
| +selectors and zero `[rst-sr-only]`. tokens.css says why in its own header: |
| +"Utilities keep the class spelling and are not paired… They are not kinds — |
| +they are cross-cutting styling, which is what class is for, and they stay |
| +class after the move", and rastrillo's §6-v3 agrees ("`class` is for |
| +utilities and the app's own CSS"). Writing `<span rst-sr-only>` matches |
| +nothing, so screen-reader-only text renders **visible**. Kinds, variants and |
| +parts are attributes; these seven are classes. |
| **Corrected 2026-09-05, verified against the pinned `rastrillo v0.22.0`.** |
| An earlier draft recorded "zero bare `rst-*` attribute selectors against |
| @@ -897,7 +941,7 @@ ships**, because call 2's person ref is what their grant rows key on. |
| | **Owner-enforceable per-team 2FA** (ruling 15) | A team asks, *and* either a sixth call carrying team policy is argued past §2.2 or rastrillo grows a per-team notion | |
| | Pegamento's vault facet — team names the home cannot read | The vault ships with a recovery story | |
| | Operator reset for a fully locked-out person | The first person loses passkey and codes together | |
| -| Auto-drain of the queue | The platform ships a machine credential **and** we choose to give up the abuse gate — two decisions | |
| +| Auto-drain of the queue | The platform ships a machine credential, **and** we choose to give up the abuse gate, **and** ruling 19's fallback adjudication is automated — three decisions. A machine credential supplies no judgement about who may take a reserved name (§7) | |
| | **Suite-wide sign-out / session revocation** | Named as an exposure now: a stolen laptop's instance sessions live to their TTL with no revocation channel. Trigger: the first report, or before any team holds regulated data | |
| | **Account deletion / erasure** | Before a person asks, which under most regimes is not optional for long | |
| | **Step-up for instances** (`RequireFresh`-grade) | An instance flow needs it; requires a fresh-assertion round trip with a `max_age`-style parameter | |
| @@ -924,11 +968,9 @@ ships**, because call 2's person ref is what their grant rows key on. |
| it does. Also open there: the per-instance deadline, the fan-out budget, |
| the RAM cache TTL and the body cap are all named as decisions and none is |
| chosen. |
| -- **Class or attribute spelling — §11 needs a re-ruling.** Both reasons §11 |
| - gave for `class="rst-*"` are now false at the pinned version, and |
| - rastrillo's §6-v3 rules one spelling with the class selectors dropped |
| - before 1.0. This app has no `rst-` markup yet, so the switch is free |
| - today and costs a sweep after the first template. Needs Paul. |
| +- ~~Class or attribute spelling — §11 needs a re-ruling~~ — **closed by |
| + ruling 20**, 2026-09-05: the app writes attributes, and the seven |
| + unpaired utilities keep `class`. |
| - ~~A real suite domain — triggers §5.7~~ — closed by ruling 16. §5.7 is |
| titled for the case it does not apply to: the `oficina.works` move needs |
| no RP rename drill. |