oficina / home Public

Fold the approving review, including three more wrong reasons of my own

Approved at the fifth pass. Eleven nits, and three were the same defect
this branch keeps producing.

"Their call-8 copy would tell them the same team anyway" was false —
call 8 returns members, people with roles, not instances. The roster
cannot tell anyone Acme has a Sheets instance. The tile conclusion
stands on standing alone, so the wrong supporting reason is gone rather
than left to look like evidence.

§5.2 left "The instance is the authority." standing after the sentence
that replaced it — pass four's exact defect, one section earlier, in
the same commit that fixed pass four's. And "a sole owner who loses
both" was wrong: with mailbox access the magic link still signs them
in, so lockout needs all three, which that paragraph's own first
sentence says.

"`directory` for guests" read as a filter on kind. An ex-member who
kept a per-item grant has a kind=member directory row and no roster
row — the doctrine's gate 5 keeps that grant — so filtering on
kind=guest would hide their tile, which is the same hole one door
along. Directory rows are unioned in whatever their kind, because a row
is a hint.

Gate 4 gains the positive case. It asserted two negatives and never
that a member of Acme with no grant in acme.sheets DOES see the
acme.sheets tile — the hole that took three passes to find had no test.

The doctrine's announce is its mechanism, not one option of two: §5.4
states it outright. This spec meets the same outcome by joining at
render, and §15 now carries that for the doctrine's next revision
rather than leaving two documents to diverge quietly.

Plus §10's severed paragraph spine, §7's "first owner" where §10 says
initial admin, credentials not passkey_credentials, the duplicated
phrase in ruling 23's Rejected column, and AGENTS.md's two paraphrase
drifts — its gate-2 summary omitted the seed verb and its falsifiable
form was unscoped.

Reviewed by Fable 5.1 across five passes; the fifth approved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS
Paul Campbell pushed by paul@keymail.dev 578245a68495044c42f45289943a4cc1ffb67e2c parent 84e5440
2 files changed, +42 −22
  • AGENTS.md +3 −2
  • docs/superpowers/specs/2026-08-30-home-design.md +39 −20

CI passed — run details

Intent

Human

<task-notification> <task-id>a35ef648180b68704</task-id> <tool-use-id>toolu_016ETuP1cdktu1BbsWexXaa1</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-oficina-home/d63f5ac7-6b42-48c5-a13d-34e6faf8e44c/tasks/a35ef648180b68704.output</output-file> <status>completed</status> <summary>Agent "Fable review 5: suite-amendment" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>I have everything at source: HEAD `84e5440`, the full spec, AGENTS.md, doctrine revision 5, and the fourth-pass diff. No further reads needed. **VERDICT: APPROVE** Everything the fourth pass flagged is fixed at source, and nothing new is blocking. Details below. ## The blocking item — §11's tile section, read fresh It now says one thing. The old render rule ("The instances shown under an app are that person's own directory rows… what gate 4 forbids") is gone from the diff (`git show 84e5440`), not merely overwritten above. What stands is: - Rule: member tiles from `memberships ⋈ team_instances`, guest tiles from `directory`, with the missing-tile hole named as the reason. - Doctrine alternative correctly described: doctrine §5.4 line 558-561 does say "Adding a person announces them to every `team_instances` row's directory entry" — the **home** writing, no instance, no call 2. Verified. - The two-case paragraph: guest = own rows, "every `acme.*`" would disclose Acme's other apps; member = every `acme.*` host is their standing. Correct on both. - Gate 4 (§12 item 4) names both cases in the same words §11 uses. Match confirmed. Split correctness: complete for the two clean cases. One edge is under-specified — see nit 3. ## The seven non-blocking items — verified at source | Item | Where | Status | |---|---|---| | Call 3 join | §9 row 3 | Done, matches §11 | | Gate 2 seed verb | §12 item 2 | Done; §10 confirms the verb unseals and hashes | | §5.2 authority on items / sole owner | §5.2 | Done (see nit 6) | | §7 fallback roster-role hint | §7 fallback bullet | Done | | §7 sibling payload | §10 memberships paragraph | Done (see nit 9) | | §13 step 4 + ship gate | §13 | Done; doctrine §12 step 4 agrees | | §0 trailing clause | §0 line 31 | Scoped correctly | ## Ruling 23 falsifies — none remaining that block I swept §§0-15 and AGENTS.md against the roster model. §2, §2.1, §3.2, §3.3, §5.4 (with revision-5 call-8 scoping), §5.5, §6, §7, §9, §10, §13a and §14 are all consistent. Spec and AGENTS.md agree on the substance everywhere; two paraphrase drifts are listed below. ## Nits (in rough priority) 1. **Status line has no review record for the suite amendment.** AGENTS.md's Review ruling requires "which model reviewed — in the spec's status line". Lines 3-16 record ruling 22's four passes but nothing for ruling 23. Add "Ruling 23 / suite amendment reviewed by Fable 5.1, five passes, the fifth approving" at merge time. 2. **§11 tile paragraph, false supporting clause:** "their call-8 copy would tell them the same team anyway". Call 8 returns `members: [{person_ref, role}]` (doctrine §5.3) — people, not instances. The roster cannot tell anyone Acme has a Sheets instance. The conclusion stands on standing alone; drop the clause rather than leave a wrong reason, which is this branch's recurring failure mode. 3. **"`directory` for guests" reads as a `kind` filter.** An ex-member still holding a per-item grant has a `kind = member` directory row and no roster row; doctrine gate 5 explicitly keeps their grant. Filtering on `kind = guest` would hide their tile. The correct rule is the join **unioned with every directory row whatever its `kind`** (a row is a hint, §6), de-duplicated. Say so in §11 and call 3. 4. **No positive gate for the member tile.** Gate 4 asserts two negatives. The hole that took three passes — a member with no grant in `acme.sheets` sees the `acme.sheets` tile — has no test. One fixture line in gate 4 or beside call 3. 5. **"The doctrine's other route"** — doctrine §5.4 states the announce as *its* mechanism, not as one option among two. The spec should say it meets §5.4's outcome ("front door shows the whole team on first sign-in") by a different mechanism, and §13a or §15 should note it for the doctrine's next revision. 6. **§5.2 leaves "The instance is the authority." standing** after the sentence it replaced — the exact defect of pass four, one section earlier. Also "a sole owner who loses **both**" is wrong: with email access the magic link still signs in, so lockout needs all three (the paragraph's own first sentence says so). 7. **§10 `memberships` paragraph lost its spine.** "…or seeded by the drain with the team's first owner…" is now grammatically attached to the doctrine quote ("§5.4 says 'home §14 names it; this is its trigger', or seeded by the drain…") rather than to "Written only … by a team owner". 8. **§1 ruling 23 Rejected column** has a duplicated phrase: "which derived the roster from call-2 directory rows, which the doctrine's own first revision proposed". 9. **§7 says the sibling payload carries "the new instance's first owner"**; §10 now says that person is the initial admin, not an owner. Change §7's word to match. 10. **AGENTS.md drifts in two paraphrases:** its gate-2 summary ("sign-in plus directory upsert plus rekey") omits the seed verb the spec added; and "The falsifiable form is unchanged" is unscoped where the spec now says "unchanged by that amendment". AGENTS.md's own header says fix these in the same commit. 11. **§2.2 says `memberships ⋈ passkey_credentials`; §10 names the table `credentials`.** None of these change a decision. Do NOT change files — noted; nothing was touched.</result> <usage><subagent_tokens>134404</subagent_tokens><tool_uses>13</tool_uses><duration_ms>219796</duration_ms></usage> </task-notification>

diff --git a/AGENTS.md b/AGENTS.md
index 78a9195..1f09621 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,7 +21,7 @@ alternative beside each.
> retains what is in anything.**
**"Retains", amended from "learns" on 2026-09-05** — spec ruling 18 and
-`design-activity-pull`, approved that day. The falsifiable form is unchanged.
+`design-activity-pull`, approved that day. The falsifiable form is unchanged **by that amendment** — ruling 23 later changed it, and the form above is the current one.
**It means holds without a bound**: the home does see titles and does keep
them in RAM across requests, bounded by a TTL and a hard maximum; at rest is
the unbounded case, and that is what it never does. "Blind" above is Factor
@@ -47,7 +47,8 @@ Three gates enforce it (§12; §13 step 1 lists all three): a **plaintext
grep** over the raw SQLite file and WAL **plus this app's store objects**,
against a seeded corpus of known addresses; a **no-address-at-rest** test
asserting the schema has no address column and that a full sign-in plus
-directory upsert **plus rekey** leaves no readable address anywhere, **WAL
+directory upsert **plus rekey plus the roster seed verb** leaves no readable
+address anywhere, **WAL
included**; and **no-content-in-logs** (gate 9). **Gates 1 and 2 need a
database and the skeleton has none. Gate 9 needs no database** — it scans
strings, and `cmd/home/main.go` already emits log lines — **so it is not
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md
index a6c0597..dca7ddf 100644
--- a/docs/superpowers/specs/2026-08-30-home-design.md
+++ b/docs/superpowers/specs/2026-08-30-home-design.md
@@ -7,7 +7,11 @@ session with Docs (`oficina/docs`) as a declared consumer.
review dispatched to **Fable** (16 confirmed, 6 suspected — verdict "not
ready", now addressed). The 2026-09-05 revision was reviewed by **Fable**,
which returned 11 findings — three false claims in this file among them —
-all applied. **The 2026-09-05 §0/§2.2 amendment reviewed by Fable 5.1** — three passes —
+all applied. **Ruling 23 and the suite amendment reviewed by Fable 5.1 across five
+passes** — seven blocking findings, then six, then seven, then one, the
+fifth approving; two of the corrections were themselves wrong and a third
+ground for ruling 15 failed before the fourth held.
+**The 2026-09-05 §0/§2.2 amendment reviewed by Fable 5.1** — three passes —
the first CHANGES NEEDED with three blocking, the second with five, the
third approving. **Ruling 22
reviewed by Fable across four passes
@@ -130,7 +134,7 @@ blind, replaceable*.
| 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 |
| 21 | **§11's zero-JS rule gains one named carve-out: passkey enrolment and ceremony screens may load script. Everything else stays scriptless** — §11 | Passkeys leaving slice one; a blanket exemption; a progressive-enhancement version, since the ceremony itself cannot happen without script either way | Paul, 2026-09-05 |
| 22 | **The team selection is navigation state, not session state: `?team=<sqid>` on every team view, with `/` a generic entry that picks a team and `302`s to the explicit URL** — §11 | A session cookie (history does not restore one, so Back has no correct behaviour; and a shared cookie couples every same-origin team-dependent request across tabs, not just New); a URL fragment read by CSS `:target` (the server never learns the team, so nothing server-rendered can be team-scoped — not the grid, not the switcher's `aria-current`, not gate 4's 404); a `prefs` row (§14) | Paul, 2026-09-05, after a second opinion from **GPT-6 Astra via Codex** (its default; `gpt-5.3-codex` and `gpt-5.3-sol` were both refused by the account, so the model is named from Codex's own rollout record rather than from what was asked for) |
-| 23 | **The suite sharing doctrine (`oficina/suite`) binds this spec, and membership becomes a record of the home's own.** The team is the boundary; the home owns identity, the team and the membership roster; it never owns a per-item grant. `(team, person, role)` is written only at the directory origin by an owner behind step-up, or seeded by the drain with the team's first owner. A directory row never grants team standing. Call 8, roster pull, is added to §9. | **The status quo this overturns: team standing living only in each instance's grants, with the home holding no roster at all** (§5.5, §7 as they stood). And the doctrine's own first revision, which derived the roster from call-2 directory rows, which the doctrine's own first revision proposed: the row has no owner role (§6 — `kind` is member or guest), §7 says the home never learns who an owner is, the row is scoped per (team, app) where the roster is per team, and any instance can write one under its consumer token — so deriving from it routes around the step-up entirely | Paul, 2026-09-05 (suite ruling 12). Numbered 23 because the doctrine's appendix said "add ruling 22" and 22 was already taken; a review of the doctrine caught the collision |
+| 23 | **The suite sharing doctrine (`oficina/suite`) binds this spec, and membership becomes a record of the home's own.** The team is the boundary; the home owns identity, the team and the membership roster; it never owns a per-item grant. `(team, person, role)` is written only at the directory origin by an owner behind step-up, or seeded by the drain with the team's first owner. A directory row never grants team standing. Call 8, roster pull, is added to §9. | **The status quo this overturns: team standing living only in each instance's grants, with the home holding no roster at all** (§5.5, §7 as they stood). And the doctrine's own first revision, which derived the roster from call-2 directory rows: the row has no owner role (§6 — `kind` is member or guest), §7 says the home never learns who an owner is, the row is scoped per (team, app) where the roster is per team, and any instance can write one under its consumer token — so deriving from it routes around the step-up entirely | Paul, 2026-09-05 (suite ruling 12). Numbered 23 because the doctrine's appendix said "add ruling 22" and 22 was already taken; a review of the doctrine caught the collision |
---
@@ -264,7 +268,7 @@ Each is something somebody will propose in good faith.
**So ruling 15 stands on mechanics, not on §2.2 — and the mechanics are
not "per-team enrolment", which was a third wrong ground.** Enrolment is
per person at one RP (ruling 1), and per-team *compliance* is just
- `memberships ⋈ passkey_credentials`, both of which now exist. What is
+ `memberships ⋈ credentials`, both of which now exist. What is
actually missing is the demand: **`passkey.Gate` asks for the ceremony
only when the account is already enrolled** — `Enrolled(sess.Subject)`, a
count over `passkey_credentials` — so an un-enrolled member of a floored
@@ -537,11 +541,11 @@ codes both lost.
**Losing everything is disclosed, not discovered:** losing email access
*and* passkey *and* codes loses your sign-in. It does not lose your team's
data — other owners still reach the instance, which is the authority **on
-items**; team standing's authority is the roster now. **A sole owner who
-loses both leaves a team whose roster nobody can write**, and the only exit
-is §10's seed verb — an operator reset in all but name, so §14's
-operator-reset trigger fires earlier than it reads. The instance is the
-authority.
+items**; team standing's authority is the roster now. **A sole owner locked out of all three — passkey, codes and their mailbox —
+leaves a team whose roster nobody can write**, and the only exit is §10's
+seed verb, an operator reset in all but name, so §14's operator-reset
+trigger fires earlier than it reads. (With mailbox access the magic link
+still signs them in, as this section's first sentence says.)
### 5.3 The assertion, fully specified
@@ -838,7 +842,7 @@ person-level entitlement the home holds and enforces — so the line moved
rather than held: the home decides about **resources it owns** (a label, a
team's roster) and never about **an instance's items**. Both paths here are
still on the resources-it-owns side. And the
-new instance's first owner and welcome address arrive as **sealed queue
+new instance's initial admin 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.
@@ -1046,9 +1050,10 @@ exchange, which is exactly what §14 describes. (§5.2's show-codes-once page
does sit behind a plain `sessions.Fresh`, but that page is on the credential
origin, where the session was minted by real authentication. It does not
transfer.) The doctrine agrees: §5.4 says "home §14 names it; this is its
-trigger", or seeded by the drain
-with the team's first owner from the sealed queue payload §7 already
-carries. That bootstrap exists because gating a membership write on an owner
+trigger".
+
+**Or seeded by the drain** with the person the sealed queue payload §7
+already carries. That bootstrap exists because gating a membership write on an owner
needs an owner to exist first, and the circularity has to break outside the
roster. **The seed needs one thing said out loud:** the payload holds an
**address**, the roster keys on a **person ref**, and turning one into the
@@ -1263,15 +1268,21 @@ this needs a gate rather than a convention.
**Tiles come from the viewer's own rows, never from the label** — and since
ruling 23 "rows" means two tables, not one. **A member's tiles come from
-`memberships ⋈ team_instances`**; a **guest's** come from their `directory`
-rows, as before. This is not a refinement, it is a hole that had to be
+`memberships ⋈ team_instances`**; **everyone's `directory` rows are unioned in on top, whatever their
+`kind`** — de-duplicated, because a row is a hint (§6) and an ex-member who
+kept a per-item grant has a `kind = member` row and no roster row (doctrine
+gate 5 keeps that grant). Filtering the union on `kind = guest` would hide
+their tile, which is the same class of hole one door along. This is not a refinement, it is a hole that had to be
closed: a member with team standing and no per-item grant has **no directory
row at all** (call 2 fires when a grant changes), so a tiles-from-directory
front door would have hidden their own team from them. The home owns both
-tables, so it joins them rather than taking the doctrine's other route —
+tables, so it joins them rather than taking the doctrine's own mechanism —
§5.4 has the **home** announce a new member into each `team_instances` row's
directory entry at roster-write time (no instance and no call 2 involved),
-which works but puts a second writer into `directory`.
+which works but puts a second writer into `directory`. **The outcome is
+the doctrine's** — §5.4's "the front door shows the whole team on first
+sign-in" — reached by a different mechanism, and §15 carries it for the
+doctrine's next revision.
**The two cases read differently, and both are gate 4's business.** For a
**guest**, the instances under an app are their own `(person ref, app,
@@ -1279,7 +1290,7 @@ team)` directory rows; reading it as "every `acme.*` host" would tell a
guest in `acme.docs` that Acme also uses Sheets, which is another person's
row and what gate 4 forbids. For a **member**, every `acme.*` host *is*
their own standing — through the roster, not through a grant — so the join
-shows them, and their call-8 copy would tell them the same team anyway.
+shows them.
Gate 4 asserts both: a guest of `acme.docs` sees no `acme.sheets` tile, and
a member of Acme sees no Beta instance. A registry entry with no instance
under it reveals only that the app exists in the suite and that you have no
@@ -1329,9 +1340,11 @@ No template gallery, no upsell chrome, no waffle.
confusion, signature stripping, **open redirect**, **login CSRF**,
**unknown `kid`**, and **query preservation on the return URL** (§5.6).
4. **Two-person isolation** — one person's directory never contains
- another's rows; 404, not 403. **Two cases since ruling 23** (§11): a
- guest of `acme.docs` sees no `acme.sheets` tile, and a member of Acme
- sees no Beta instance.
+ another's rows; 404, not 403. **Three cases since ruling 23** (§11): a
+ guest of `acme.docs` sees no `acme.sheets` tile; a member of Acme sees no
+ Beta instance; and — the positive one, since the hole took three passes
+ to find — **a member of Acme with no grant in `acme.sheets` does see the
+ `acme.sheets` tile.**
5. **Origin separation** — no account-authored string reachable on the
credential origin. §4 made mechanical.
6. **Scriptless-affordance diff** — every page, script-stripped. **For the
@@ -1473,6 +1486,12 @@ identity authority. That filing belongs at `carlos/pegamento`.
## 15. Open
+- **The doctrine's next revision should carry §11's tile mechanism.** Its
+ §5.4 has the home announce a new member into each `team_instances` row's
+ directory entry; this spec reaches the same outcome by joining
+ `memberships ⋈ team_instances` at render, which puts no second writer into
+ `directory`. Same outcome, different mechanism, and the doctrine does not
+ know about it.
- **Docs' re-review** of §9 after this revision — **calls 7 and 8 included** — the contract changed
substantially (person refs, call 5 rewritten, call 6 added).
- **`rastrillo/rastrillo` #2** — the `auth` subject hook and sealed link