Fourth pass: my own fix left the sentences it replaced standing under it
Approved on everything but one paragraph, and that paragraph was mine. The member-tile fix added two correct sentences and left the old render rule beneath them, so §11 said both that tiles are the viewer's own directory rows and that a member's come from the roster join — with the old text adding that reading it as "every acme.* host" is what gate 4 forbids, which is exactly what the join does for a member. A builder could not tell which sentence won. This is the same defect I spent three passes finding in the commit below mine. The two cases now read separately. For a guest, the instances under an app are their own directory rows, and "every acme.* host" would tell a guest in acme.docs that Acme uses Sheets — gate 4's business. For a member, every acme.* host IS their own standing, through the roster rather than a grant, and their call-8 copy tells them the same team anyway. Gate 4's own text now names both cases rather than one. The doctrine's alternative was also mis-described: §5.4 has the HOME announce a new member into each team_instances row's directory entry at roster-write time — no instance, no call 2. The join is still better, because it puts no second writer into directory, but the reason has to be the real one. Call 3 was the same hole one table over: "every team across every app, with kind" is the springboard's data, so a builder implementing it from §9 alone would rebuild the missing tile. It joins now too. Four more ruling 23 falsifies, all found by this pass. Gate 2 drives sign-in, upsert and rekey, and the seed verb is a fourth path that unseals an address and hashes it — it should be driven too. §5.2 said other owners still reach the instance and the instance is the authority; the instance is the authority on items, and a sole owner who loses passkey and codes now leaves a team whose roster nobody can write, with §10's seed verb the only exit — an operator reset in all but name, so §14's trigger fires earlier than it reads. §7's fallback hint should show the requester's roster role, which is the stronger hint the home now owns. And §7's sibling payload names a person §10 says seeds nothing: that person is the welcome-mail recipient and the new instance's own initial admin, not a roster write. §13 gains the roster in step 4 — the table, the seed verb, the owner write behind §14's step-up, call 8 and gate 21 — and the ship gate is no longer "step 4" but step 4 including the roster, since a suite-shape Sheets or Docs needs call 8 for team standing. Reviewed by Fable 5.1; four passes, the fourth approving everything but the paragraph fixed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS
docs/superpowers/specs/2026-08-30-home-design.md+33 −16
CI passed — run details
Intent
<task-notification> <task-id>a4a5a520a7e0368cc</task-id> <tool-use-id>toolu_01CoLEJLmVVKq3aWo22BPVF9</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-oficina-home/d63f5ac7-6b42-48c5-a13d-34e6faf8e44c/tasks/a4a5a520a7e0368cc.output</output-file> <status>completed</status> <summary>Agent "Fable review 4: 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>VERDICT: CHANGES NEEDED — one blocking item, in the §11 paragraph that fix G introduced. Everything else on this branch is right at source; if that paragraph is corrected I would approve without another pass. Verified against `/home/paulca/amadan.net/oficina/home/.claude/worktrees/suite-amendment` at `b0966ea`, the doctrine at `origin/main` (revision 5, `d55fa15`; revision 4 `210312f` checked directly), and rastrillo `v0.22.0` (`passkey/passkey.go`, `sessions/sessions.go`, `auth/handlers.go`). ## The seven fixes **A — ruling 15's ground.** Right now. At v0.22.0 `Gate` is literally `enrolled, err := h.Enrolled(sess.Subject); if !enrolled { return false, nil }`, and `Enrolled` is `SELECT COUNT(*) FROM passkey_credentials WHERE subject = ?`. So an un-enrolled member of a floored team does pass untouched — the spec's sentence is the source. "At assertion minting" is the correct locus, not sign-in: sign-in is team-less, and the team is only known at minting through `aud → team_instances`. The `Method` check is workable: the Gate flow mints `p.method + "+passkey"`, a step-up mints `"passkey"` (lines 336 and 552). "Force enrolment if none" is also right — `StepUpFinish`/`SignInFinish` look up the credential by subject and there is none to assert against. §5.2, §14's row and §7's withdrawal all agree with each other. The Rejected column ("neither spec can build") matches. Done. **B — §2's bound.** True at both tiers. The bullet is explicitly the directory tier and defers the credential tier to §13a. The two tails match doctrine §5.3 (a forged copy lasts at most one TTL "on an instance that admits nobody"; a written row is a row). The reading of §5.5's "what honest code leaves" is correct: the doctrine's point is a compromise need not write a row, not that a written one evaporates. One imprecision, not blocking: the removal tail also carries a TTL (after the owner removes the row, an instance nobody signs in to serves it for up to one more TTL), so strictly it is "until removed, plus one TTL". "Floor, not ceiling" covers it. **C — AGENTS.md rule 3.** Correct against doctrine ruling 2 ("a guest is a scoped member of the granting team; a link share moves nothing") and §6. **D — the three "unchanged" claims.** Scoped. One awkwardness: §0's "The falsifiable form below is unchanged by that amendment — ruling 23 later did change it, and says so — nothing new goes at rest." The trailing clause reads as a standalone claim, which ruling 23 falsifies (`memberships` is new at rest). Nit: "…and says so — that amendment put nothing new at rest." **E — §2.1 and §2's "convenience".** Both match doctrine §5.3 (stale past TTL stops team standing for existing sessions too). Right. **F — calls 2 and 5 carry the roster epoch.** Verified in §9. Right. **G — the member's tile.** The join is the right mechanism, but see blocking below. **Non-blocking taken:** gate 21 is sound and correctly numbered. §13a's citation is accurate — `210312f` is revision 4 and does *not* contain the call-8 scoping (grep confirms), so naming it as a rev-5 rule consumed under gate 9's grace is exactly right. §15's Docs re-review names calls 7 and 8. ## The four questions **Ruling 15's ground — is it right now?** Yes, see A. Nothing I would change. **§2's sentence at both tiers?** Yes, see B. **Is the §11 tile join right? Does it leak?** The join is right and does not leak. A member seeing every `acme.*` instance is what doctrine §5.4 already intends ("their front door shows the whole team on first sign-in"), the member reaches team-visible content in all of them under ruling 1, and every instance's call-8 copy tells them the whole team anyway. A guest still gets directory rows only, so a guest learns no sibling. Gate 4 is unaffected in substance — `team_instances` rows are team rows, not another person's — but the gate's text should now name the two cases it must cover (a guest of `acme.docs` sees no `acme.sheets` tile; a member of Acme sees no Beta instance). **BLOCKING — the paragraph contradicts itself.** `docs/superpowers/specs/2026-08-30-home-design.md` ~lines 1260–1275. After the new first two sentences (members from `memberships ⋈ team_instances`, guests from `directory`), the old text survives unchanged: *"The instances shown under an app are that person's own `(person ref, app, team)` directory rows. Read the other way — 'every `acme.*` host' — … is another person's row and what gate 4 forbids."* The first of those is now false for a member, and the second says gate 4 forbids exactly what the join does for a member. This is the render rule for the front door, and a builder reading it cannot tell which sentence wins. Fix is two sentences: "For a guest, the instances shown under an app are their own directory rows; reading it as 'every `acme.*` host' would tell a guest in `acme.docs` that Acme also uses Sheets, which gate 4 forbids. For a member, every `acme.*` host *is* their own standing, through the roster, and gate 4 asserts both cases." Same paragraph, a citation nit: "asking instances to announce memberships back as directory rows — which is the other way the doctrine allows" mis-describes the doctrine. §5.4 has the *home* announce a new member to each `team_instances` row's directory entry at roster-write time — no instance and no call 2 involved. The join is still the better choice (no second writer into `directory`), but say what the doctrine actually proposes. ## Ruling 23 falsifications not yet caught - **§9 call 3** ("Every team across every app, with `kind`") is the G hole one table over. If call 3 is the springboard's data, it must join `memberships ⋈ team_instances` too, or a builder implementing call 3 from §9 rebuilds the missing tile. Non-blocking if fixed alongside the §11 edit; note it. - **§5.2** "other owners still reach the instance, and the instance is the authority" — the instance is the authority on items; team standing's authority is now the roster. And a *sole* owner who loses everything leaves a team nobody can write the roster for; the only exit is §10's seed verb, which is an operator reset by another name and §5.2 says there is none. Not false enough to block; worth one sentence. - **Gate 2** drives sign-in, upsert and rekey. The seed verb (§10) is a fourth path that unseals an address and hashes it; gate 2 should drive it too. - **§7's fallback hint** renders directory rows beside an unvouched request. The roster is now a stronger hint the home owns (requester's role on the team); render it too. Improvement, not a defect. ## Left non-blocking last time — still? - **§13 sequencing** — still non-blocking, but it is now the largest omission: `memberships`, call 8, the step-up exchange, the seed verb and gate 21 have no step. Doctrine §12 step 4 puts call 8 "on top of home step 4"; the home's own list should say so, and "step 4 must land before either ships" is now incomplete — a suite-shape Sheets or Docs also needs call 8 for team standing. - **§7 sealed payload vs sibling drain** — still non-blocking. The tension (the sibling payload carries a "first owner" that §10 says seeds nothing) resolves as: that address is the welcome-mail recipient and the instance's own initial admin, not a roster write. One sentence in §7 closes it.</result> <usage><subagent_tokens>138563</subagent_tokens><tool_uses>29</tool_uses><duration_ms>416366</duration_ms></usage> </task-notification>
Attributed to this commit: 41.3 g CO₂e (5366901 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on suite-amendment, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md| index 8c69103..a6c0597 100644 |
| --- a/docs/superpowers/specs/2026-08-30-home-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-home-design.md |
| @@ -28,7 +28,7 @@ documents. |
| **"Retains", amended from "learns" on 2026-09-05** (ruling 18, |
| `design-activity-pull`, approved that day). The falsifiable form below is |
| -**unchanged by that amendment** — ruling 23 later did change it, and says so — nothing new goes at rest. |
| +**unchanged by that amendment** — ruling 23 later did change it, and says so — that amendment put nothing new at rest. |
| **"Retains" means holds without a bound.** That definition is doing real |
| work and a first draft of this gloss got it wrong three times over, so it is |
| @@ -536,7 +536,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, and the instance is the |
| +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. |
| ### 5.3 The assertion, fully specified |
| @@ -822,8 +826,7 @@ handed, not whether there is one. |
| document in it, and it never asks. |
| - **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) — with `kind` shown, since a *guest* row in `acme.docs` is emphatically |
| + render the requester's own directory rows **and their roster role, which is the stronger hint the home now owns,** beside it (ruling 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 |
| @@ -918,7 +921,7 @@ every assertion, and one that fails §5.4 is not a preference. |
| |---|---|---|---| |
| | 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, 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**, the pepper epoch **and the roster epoch** (ruling 23 — the doctrine carries it in calls 2, 5 and 8 and in the assertion; a builder reading only §9 would otherwise ship two of the four staleness carriers). Instance-scoped revoke included. Via the outbox. | |
| -| 3 | Directory list | person → home | Every team across every app, with `kind`. | |
| +| 3 | Directory list | person → home | Every team across every app, with `kind` — **joined from `memberships ⋈ team_instances` for members and `directory` for guests** (ruling 23, §11), or a builder implementing this row rebuilds the missing-tile hole. | |
| | 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. Carries the **roster epoch** (ruling 23). | |
| | 6 | Preference write-through | person → home | Redirect POST to the directory origin (§8). | |
| @@ -1054,7 +1057,10 @@ person stub if that owner has never signed in (§6). So the drain calls a |
| **home-side operator verb**, the same surface that marks an instance live; |
| it never holds the pepper itself. That verb mints an owner, so it is |
| security-load-bearing and is named rather than left to be improvised. **A |
| -sibling drain (call 4b) seeds nothing** — the team already has owners. Call 8 reads this table and nothing else. Call 2 cannot write it. |
| +sibling drain (call 4b) seeds nothing** — the team already has owners, and |
| +the person its payload names is the **welcome-mail recipient and that |
| +instance's own initial admin**, not a roster write; §7 and §10 agree once |
| +that is said. Call 8 reads this table and nothing else. Call 2 cannot write it. |
| Guests are **not** on it. A guest grant is minted in the instance and |
| announced to the directory as `kind = guest`, exactly as before; nothing |
| @@ -1262,13 +1268,20 @@ rows, as before. 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 asking instances to announce |
| -memberships back as directory rows — which is the other way the doctrine |
| -allows, and the one that would have made call 2 a second roster writer. The |
| -instances shown under an app are that person's own `(person ref, app, team)` |
| -directory rows. Read the other way — "every `acme.*` host" — a guest in |
| -`acme.docs` would learn that Acme also uses Sheets, which is another |
| -person's row and what gate 4 forbids. A registry entry with no instance |
| +tables, so it joins them rather than taking the doctrine's other route — |
| +§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`. |
| + |
| +**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, |
| +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. |
| +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 |
| team in it, both of which you already knew. |
| @@ -1308,13 +1321,17 @@ No template gallery, no upsell chrome, no waffle. |
| clause has no test behind it. `design-activity-pull`'s gate 11 is the |
| driven version, through the failure paths. |
| 2. **No-address-at-rest** — the schema has no address column, and a full |
| - sign-in plus directory upsert plus rekey leaves no readable address |
| + sign-in plus directory upsert plus rekey **plus the roster seed verb** |
| + (§10 — a fourth path that unseals an address and hashes it) leaves no |
| + readable address |
| anywhere, WAL included. |
| 3. **Assertion abuse suite** — audience confusion, replay, expiry, algorithm |
| 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. |
| + 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. |
| 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 |
| @@ -1359,7 +1376,7 @@ record**, including which model reviewed it. |
| 1. Skeleton, `AGENTS.md`, gates 1, 2 and 9 |
| 2. Hand-rolled sign-in (§5.1), second factor, recovery codes |
| 3. Assertion minting, the redirect protocol, and gate 3 |
| -4. Directory: calls 2 and 3, blind index, person refs |
| +4. Directory: calls 2 and 3, blind index, person refs — **and the roster** (ruling 23): the `memberships` table, the seed verb, the owner write behind §14's step-up, call 8, and gate 21. The doctrine puts call 8 on top of this step, and a suite-shape Sheets or Docs needs it for team standing, so the ship gate below is not just step 4 but step 4 including the roster |
| 5. Signup, queue, rate limits, name reservation |
| 6. The operator drain |
| 7. Preferences, the assertion claim, call 6 |