oficina / suite Public

Merge branch 'worktree-doctrine-review' into main

claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev 1bbd2e1be8749f1f0efc7dcef495283bb15353f2 parent 291d957parent 18f943e
7 files changed, +2200 −0
  • AGENTS.md +50 −0
  • CODEX-FINDINGS-R3.md +239 −0
  • CODEX-FINDINGS.md +259 −0
  • CODEX-REVIEW-BRIEF.md +81 −0
  • FABLE-FINDINGS-R3.md +194 −0
  • README.md +36 −0
  • docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md +1341 −0
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..6a7cb1e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,50 @@
+# 🤖 suite
+
+The shared repository of the Oficina suite. See `README.md` for what it
+holds. This file is binding. Where it disagrees with a general skill, this
+file wins.
+
+---
+
+## The one rule
+
+> **Cite, never restate.**
+
+This repository exists because five app specs each restated one contract
+in their own words and the copies drifted. A rule that lives here is
+stated here once. An app spec names the ruling number and the commit it
+read; it does not paraphrase. A paraphrase in an app spec is a bug in that
+spec, fixed by replacing it with a citation.
+
+The same rule binds this repository in the other direction: the home's
+calls 1 to 6 belong to the home spec and are cited here, not restated.
+
+---
+
+## Authorship
+
+- Every ruling carries who made it and when. "This session" is a valid
+ author and is marked as such so a reader can tell a ruling from a
+ proposal.
+- A design here is not approved until a review pass by the
+ highest-capability model available, recorded by name in the status
+ block. The author of a draft cannot be its reviewer.
+- Rejected alternatives are recorded beside the ruling that rejected
+ them, so nobody proposes them again in good faith without seeing why.
+
+## The contract module
+
+- Additive only. A removed field is a new major version.
+- No rastrillo dependency. The module is types, fixtures and a thin
+ client, so the pin chain from an app is one deep.
+- Fixtures are the interface. A fixture is added with the first client
+ that needs it and corrected by the second implementation, never
+ designed from two specs at once.
+- Every constant carries its reason in a comment: a cap, a deadline, a
+ TTL.
+
+## Discussions
+
+Cross-repository requests go through `amadan discuss` on the repository
+whose text must change. A request asks for a boundary to be recorded, not
+for a ruling to be reversed, and says which of the two it is.
diff --git a/CODEX-FINDINGS-R3.md b/CODEX-FINDINGS-R3.md
new file mode 100644
index 0000000..f512e45
--- /dev/null
+++ b/CODEX-FINDINGS-R3.md
@@ -0,0 +1,239 @@
+# Findings on the suite sharing doctrine, revision 3
+
+Reviewed by **GPT-6 (Codex), 2026-09-05**, independently of the draft and revision-3 authors. These are findings and proposed corrections, not rulings. The doctrine has not been edited.
+
+Scope: the entire doctrine at `76bdc26` (doctrine text last changed in `99a8838`), the change from `0c4f098`, commit messages, the original sixteen findings, and the local sibling sources identified below. P1 means a broken or unenforceable trust guarantee; P2 means a contract, adoption, or verification defect that needs correction before implementation. Findings describe specified behavior, not demonstrated vulnerabilities in deployed applications.
+
+## 1. The sixteen original findings
+
+“Closed” means the original failure sequence has a specified answer. It does not mean the implementation or its tests exist. A new problem in the replacement is identified separately.
+
+| Original | Status | Reason |
+|---|---|---|
+| 1. Directory writes bypass roster step-up | **Closed** | Ruling 12 and §§5.3–5.4 explicitly separate membership from call-2 hints; upsert, revoke and delayed replay cannot change the roster. The rejected-alternative argument still overclaims: R3-11. |
+| 2. Compromised-home bound is false | **Partial** | Ruling 13 correctly concedes impersonation, but §5.5 and §9 immediately deny its consequences; §9 now expressly applies the false bound to all three compromises. Worse in that respect: R3-1. |
+| 3. Writes lose source authority | **Partial** | The envelope and refusal of unverifiable sources are real changes, but its S3/link-reader checks cannot be made as specified, and passage removal lacks a safe unit: R3-4 and R3-5. |
+| 4. Unprotected confirmation commit and late destination check | **Closed** | §7.1 now fixes the body in bounded, expiring, single-use browser/person-bound pending state, separates receiver-origin commit, and validates the sending destination before transmission. Gate 7 retains a few fixture decisions. |
+| 5. Expiry and rollback do not bound revocation | **Partial** | Failed refresh and lower epochs now have rules, but existing sessions survive expiry and a restored epoch can catch up without recovering removals: R3-2 and R3-3. |
+| 6. Custody ignores retained copies | **Partial** | Acquisition-time custody and irrecoverable compromised copies are correctly conceded; the promised retirement bound has no target at its citation, and §0 accidentally widens its test to originals: R3-7. |
+| 7. Removal silently revokes guest grants | **Closed** | Gate 5 now separates removal with no independent grants from a guest retaining its granted activity. Its failed-refresh case still needs R3-2/R3-9. |
+| 8. S2 intersection falsely claimed to cost nothing | **Closed** | §4.3 explicitly describes Ana losing the ingested passage despite retaining her source grant. The transition should be an actual gate fixture, not only a parenthetical request. |
+| 9. Browser policy and middleware prevent the hand-off | **Closed** | §7.1 and A.2 name the sender CSP allowance, dedicated receipt guard, pending sign-in flow and extra POST; gate 7 traverses the real stack. |
+| 10. Call 8 lacks a paginated snapshot contract | **Partial** | Epoch-bound cursors and atomic publication answer mixed snapshots; the wire shape, completion marker, bounds and restart behavior remain unspecified: R3-8. |
+| 11. S2 lacks the source-behind case | **Closed** | §4.3 specifies loud `UNKNOWN` for an unrecognized revision. That answers the original revision-7/source-5 case; adopting it against current memoria requires an explicit reconciliation with `AHEAD`: R3-12. |
+| 12. Epoch claim does not pass home's claim test | **Partial** | A.1 now requests the right exception, but “can neither admit nor exclude” is inconsistent with a forced refresh that fails, and failure precedence remains unsettled: R3-9. |
+| 13. Leak list omits roster and activity costs | **Partial** | Roles at rest and the full-roster distribution are disclosed; activity visit/traffic costs are added, but cached activity after revocation still conflicts with the universal next-request promise: R3-10. |
+| 14. Amendments retain contradictions and restatements | **Partial** | Several named leftovers are covered, but Appendix A's general disclaimer conflicts with its literal replacement instructions, and new link/provenance and current-source conflicts are omitted: R3-6/R3-12. |
+| 15. Docs and Sheets positions are misattributed | **Partial** | The Docs qualification is now accurate and A.3 preserves Sheets' local rule, but §§2 and 10 still repeat the rejected reading and replacement: R3-12. |
+| 16. Citation currency misses normative changes | **Closed** | Gate 9 covers every normative change, verifies commit/revision agreement, and explains the one-revision grace; Appendix A requests both fields. |
+
+**Seven closed, nine partial, none wholly unanswered.** Several partial findings now contain useful mechanisms; acknowledgment alone is not the reason they remain partial.
+
+## 2. Findings against revision 3
+
+### R3-1. P1 — The new compromise disclosure contradicts itself at the point of the guarantee
+
+**Where:** doctrine §5.5, lines 486–512; §6.1, lines 573–575; §9 item 4, lines 815–825; ruling 13.
+
+**What is wrong:** after conceding that the credential origin can sign in as anyone, §5.5 says a compromise of the home cannot reach an individually granted item. §9 goes further: none of the three compromises can read private/granted/link-only items, write as a person, or grant a per-item right. These statements directly contradict the new ruling.
+
+**Why:** mint an assertion for Ana, redeem it for an ordinary instance session, then read Ana's private document or exercise her actual edit/share rights. The instance correctly evaluates its own grants and authorizes the impersonated Ana. Home §5.3–§5.5 and activity design §5.1 explicitly support this sequence. A local grants join is not an independent identity check. “No trust in the home's word” in §6.1 makes the same error.
+
+The directory-only case also needs a narrower statement: adding an attacker-controlled account does not impersonate an existing person, but that account can exercise whatever ordinary write or share rights team membership/ownership actually confers. The membership record includes `owner`; compromising its writer is not inherently limited to inserting `member`.
+
+**Confidence:** high; the credential-origin counterexample is explicit in both cited contracts.
+
+**Smallest correction:** remove the universal residual bound, distinguish impersonation from rights acquired by an attacker-controlled member/owner, and make §§5.5, 6.1 and 9 cite ruling 13 consistently. A role-specific directory compromise bound must name the receiving app's role policy. No new security mechanism is required to disclose the existing exposure accurately.
+
+### R3-2. P1 — Failed refresh restores a session-length revocation tail
+
+**Where:** doctrine §5.3, lines 403–412 and 427–449; §5.4, lines 470–473; gate 5.
+
+**What is wrong:** after roster expiry, the instance keeps serving already-admitted people for their session lifetime. It nevertheless promises removal everywhere within the roster TTL. Gate 5 checks new admission and S2, leaving the existing browser session out.
+
+**Why:** admit Ana against epoch 10, remove her at epoch 11, then make call 8 unavailable through expiry. Her existing session keeps reaching team content under the explicit stale policy. Making sibling S2 return `UNKNOWN` does not stop an ordinary Docs read or memoria-owned material. Home §2.1 specifies sliding session renewal; the doctrine neither forbids renewal in this case nor defines an absolute session cutoff. “For the length of their session” is therefore not even necessarily a fixed additional bound.
+
+This is an availability choice the document can make, but it has not priced or consistently stated that choice. “Within one epoch” is still not an elapsed-time condition. Call 7 and S6 also lack an explicit stale-membership treatment.
+
+**Confidence:** high; an unexpired session alone exceeds the claimed bound, without relying on sliding renewal.
+
+**Smallest correction:** either stop using stale team standing on every protected request at a hard maximum age, preserving independently evaluable grants, or specify an absolute, nonrenewable grace and change the revocation promise to include it. Define when age starts for a multipage fetch and whether request deadlines extend the bound. Exercise an already-live session, ordinary reads, memoria-owned reads, S2, S6 and call 7 through failed refresh. Choose the security bound explicitly; cold-wake measurement alone cannot choose it.
+
+### R3-3. P1 — Rejecting a lower epoch does not make restore safe
+
+**Where:** doctrine §5.3, lines 439–445; gate 5; A.1.
+
+**What is wrong:** the receiver rejects lower numbers until the home passes a high-water mark, but no restore-safe record preserves either the mark or the membership changes it represents.
+
+**Why:** a receiver applied epoch 11, which removed Ana. Restore the home to epoch 10, which includes her. One unrelated membership change produces 11 again; two produce 12. The receiver eventually accepts a number that passes the prescribed comparison while Ana's removal remains lost. Requiring strictly greater rather than greater-or-equal merely adds one mutation to the counterexample. Restoring both receiver and home makes refresh-before-admission insufficient too: both can agree on the same obsolete roster.
+
+**Confidence:** high as a missing restore contract. An implementation with an independently durable membership journal could prevent it, but none is specified.
+
+**Smallest correction:** make roster history/restore recovery authoritative independently of the restored database, or quarantine a restored authority until membership has been reconciled through a defined recovery procedure. Preserve epoch identity across history forks; a larger counter alone is not evidence of recovery. Add both numerical catch-up and joint-restore fixtures, not only `11 → 10` rejection. Memoria §7.4's independent tombstone journal is a relevant precedent, not an already inherited roster mechanism.
+
+### R3-4. P1 — The provenance check requires an asker on paths that have none
+
+**Where:** doctrine §7, lines 615–637; §6 S3; §§4.2 and 7.3; gate 6b.
+
+**What is wrong:** every disclosure of a derived item, explicitly including an onward S3 fetch, must ask S2 whether its sources authorize the asker. S3 has no asker. An anonymous holder of a link to the derived item has none either, and §7.3 forbids using a link holder for sibling calls.
+
+**Why:** save a Sheets-derived answer into Docs, then let memoria ingest the new Doc through S3. There is no subject with which Docs can perform its required source S2. Substituting the saver caches a person's authority; inventing a member authorizes the wrong reader; skipping the check permits a newly acquired copy after a source downgrade. Making the Doc `team` does not independently settle the source's current custody or revision.
+
+Likewise, link-sharing the saved Doc does not grant the link holder access to its Sheets sources. The app cannot both promise that anyone with the URL can read this Doc and require a person-bound S2 result for every source. No safe refusal or restricted link-sharing case is stated. Team-sharing plus link-sharing still leaves this problem for the anonymous request, even if member requests work.
+
+**Confidence:** high; the incompatible signatures and requirements are explicit. Memoria §6.1 already records why adding a person to S3 was the wrong earlier fix.
+
+**Smallest correction:** define derived-item custody separately from person authorization, preserving source identities/revisions on onward acquisition and checking every source on person disclosures. Specify how a source's custody change invalidates a derived item's S1/S2/S3 eligibility. Until a no-person authorization mechanism exists, explicitly refuse unsupported derived-item S3/link disclosures or creation of such links. Do not invent an owner asker to make the call fit. Gate 6b needs a successful initial no-person acquisition as well as subsequent refusal.
+
+### R3-5. P1 — A source list cannot justify removing only one passage from a synthesis
+
+**Where:** doctrine §7, lines 615–629; gate 6b, lines 939–943; memoria §5.2 and gate 9.
+
+**What is wrong:** the envelope records only app/id/revision per source, but the outcome says the saved item “loses that passage.” Gate 6b asks for the same result across reads, exports and S3. The cited memoria contract requires withholding the whole derived artefact if any source fails.
+
+**Why:** an answer synthesized from sources A and C can express A's information in its conclusion, title or comparison with C, without quoting an identifiable A passage. A flat source list cannot locate every dependent span. Removing a quote while serving the synthesis leaves the revoked information in the output. Nor does the envelope specify how a newly edited or onward-copied Doc retains dependency coverage.
+
+There is a second output conflict: gate 6b always demands naming the app after revocation, while §4.3 permits silent `NOT_VISIBLE` and S3's custody denial must remain indistinguishable from absence. A source-specific diagnostic in that S3 denial would expose the dependency of an otherwise hidden item.
+
+**Confidence:** high for the insufficient mapping and cited whole-artefact rule; “lose that passage” is ambiguous enough that two implementers could choose opposite behavior.
+
+**Smallest correction:** make whole-artefact withholding the default when any source fails, with unchanged non-disclosure behavior at S3. If independently removable passages are wanted, first define independently authorized units and complete dependencies for each; do not infer them from quotations. Bound and validate the envelope, including transitive expansion, unknown authorities, duplicates and cycles. Gate a synthesized conclusion, title/activity output, changed revision, and an actual second write hop. The present gate performs one write with two sources, not two-hop propagation.
+
+### R3-6. P2 — Anonymous editing changes existing rulings, and actor identity is underspecified
+
+**Where:** ruling 15; doctrine §§7.2–7.3; A.1–A.3. Docs §8.2; Sheets ruling 12 and §8.3; home §5.5.
+
+**What is wrong:** §7.3 presents capability-attributed editing as preserving the existing attribution rule, but those sources explicitly require sign-in to write and mint a guest grant then. Sheets ruling 12 expressly rejects anonymous link editing. Appendix A never identifies these clauses for replacement.
+
+**Why:** a receiver following the amended sibling spec still refuses the new edit link. A receiver following §7.3 writes without the guest grant the sibling's attribution rule requires. Paul's new product requirement can justify changing that decision; the change must be recorded as such, and ruling 15 remains explicitly a proposal.
+
+The new actor also exposes an ambiguity in S8: eligibility is checked against “the item's actor,” while §7.3 attributes each revision to a link. After a link edits an S8-created Doc, does its actor become the link and permanently bar the creating sibling, or stay the sibling and misattribute the revision? The document needs distinct creation authorship and revision attribution. A link identifier in history must also be a non-secret identifier, not the bearer value; Sheets §8.3 already requires hashed token storage.
+
+**Confidence:** high for the sibling contradiction; high for the missing distinction between item creator and revision actor.
+
+**Smallest correction:** record the deliberate supersession of Docs §8.2, Sheets ruling 12/§8.3, and any affected home pending-grant text. Define immutable creator identity separately from each revision's actor and state the S8 policy after person/link edits. Add view-link write refusal, edit-link revision attribution, revocation, and mixed-actor S8 tests. Cite the existing token-storage boundary or replace it explicitly.
+
+### R3-7. P2 — Ruling 14 promises a retirement bound at a section that contains none
+
+**Where:** doctrine §0, lines 65–81; ruling 14; §4.2; §9 item 1.
+
+**What is wrong:** §0 promises retirement within one feed pull plus “its stated retention (§4.2).” Section 4.2 states neither retention nor a restore obligation. Ruling 14's requirement that retirement be “stated and timed” is therefore still unfulfilled.
+
+**Why:** memoria pulls on wake and questions, and §7.4 preserves snapshots while restorable backups reference them. A sleeping consumer or repeated failed enumeration can postpone a successful pull indefinitely. A feed-log TTL is not a cache/snapshot retirement deadline. The text must distinguish time to the next successful observation from time to retirement after that observation, including backups and the circuit breaker. The acquisition-time concession correctly limits malicious consumers, but does not specify honest retirement.
+
+There is also a new scope error: revision 2 tested what an instance held **about the others**. Revision 3 deletes that qualification and tests everything it holds. A Docs instance legitimately holds an original private Doc under ruling 15. Publishing that instance reveals content the team could never read; changing the acquisition timestamp does not cure this counterexample.
+
+**Confidence:** high; the retirement citation has no target, and the private-original counterexample follows from the new ladder.
+
+**Smallest correction:** restore the test's scope to sibling-acquired copies; distinguish originals from those copies. Cite a concrete consumer retirement/snapshot/restore policy, require its stated bounds, and explicitly disclose any unbounded delay before successful synchronization. Ruling 14 may reject push and source-held keys as complete recall mechanisms; it should not imply those alternatives have no possible effect on an honest consumer's retirement window.
+
+### R3-8. P2 — Call 8's snapshot rule is useful, but no interoperable page can yet be encoded
+
+**Where:** doctrine §5.3, lines 379–382 and 414–425; gate 5; A.1.
+
+**What is wrong:** the only wire example still contains just `epoch` and `members`. It has no continuation/completion field or cursor request parameter. No page cap, total staging bound, invalid-cursor response shape or restart action is given. “An empty response is never read as nobody is a member” also fails to distinguish a malformed/absent response from a valid completed empty snapshot.
+
+**Why:** atomic replacement solves mixed epochs only if a client can recognize completion and restart after invalidation. Two implementations can both claim to stage all pages while disagreeing about whether the first response is complete. Repeated epoch changes or a failed middle page must not extend the previous snapshot's usable age or leave unlimited staged data.
+
+The new claim that call 8 is the first response whose size the caller does not control is also inaccurate: S1 enumerates server-owned changes and call 7 has server-selected activity. That does not undermine the need to page the roster.
+
+**Confidence:** high.
+
+**Smallest correction:** provide one bounded request/response schema, explicit terminal marker, invalidation response and discard-and-restart rule. Define valid empty completion separately from fetch failure, and do not reset freshness on incomplete work. Add concurrent mutation, failed-middle-page, empty-completion and repeated-restart fixtures to gate 5. Choose the first client's constants with reasons, as the repository requires.
+
+### R3-9. P2 — Stale authority needs result precedence, and an epoch hint can cost admission
+
+**Where:** doctrine §4.3, lines 292–294; §5.3, lines 389–401 and 427–434; gates 3 and 5; A.1.
+
+**What is wrong:** stale-roster S2 is unconditionally `UNKNOWN`, whereas §4.3 and gate 3 unconditionally demand `NOT_VISIBLE` for non-custodial/deleted items. The document also says a differing epoch hint can neither admit nor exclude anybody, while requiring admission against a fresh roster after that hint.
+
+**Why:** ask about a deleted item while the roster is stale. Its nonexistence is locally determinate; an unreachable membership authority has not made it uncertain. A client needs one specified answer when both clauses apply. Returning `UNKNOWN` for the whole call can be a conservative availability choice, but it must be scoped as an exception; selectively returning it only for existing private items would violate indistinguishability.
+
+For the hint, start with a still-usable cached roster and an assertion carrying a different parseable epoch. Let the triggered call-8 fetch fail. Requiring freshness now refuses admission that the old cache would otherwise allow; accepting the old cache contradicts the fresh-roster sentence. The hint does not directly assert a role, but it can affect availability. “Exactly one request” also needs qualification once call 8 takes several pages.
+
+**Confidence:** high for the incompatible unconditional rules; the hint's availability effect depends on the failure policy that remains to be specified.
+
+**Smallest correction:** define validation and result precedence for unsupported subjects, local non-custody/nonexistence, stale membership, independent grants and revision mismatch. Retain indistinguishability in each case. Specify whether a hint invalidates an otherwise-usable copy and price that availability effect accurately. Test the combined states, including hint-triggered partial/failing refresh before the TTL. `UNKNOWN` remains a useful failure class; the defect is when it overrides a determinate denial and what the consumer is told failed.
+
+### R3-10. P2 — Activity still serves cached authorization despite the strengthened universal rule
+
+**Where:** ruling 3; doctrine §4.3, lines 278–284; §7's activity disclosure check; §9 items 5, 7 and 10; adopted activity design §7.
+
+**What is wrong:** revision 3 says authorization is never cached at any scope and that live authorization bounds the next request on every surface, explicitly including home activity. The adopted activity design permits a last-good response to render on later requests, through failed pulls until a hard maximum age.
+
+**Why:** cache a title, revoke the source or its grant, then render the home again before that maximum expires while the pull fails. The title is newly disclosed from the cache without a current check. This is not merely a browser retaining its previous page. For derived items, checking provenance when the instance first answers call 7 does not recheck the home's later render.
+
+**Confidence:** high; activity §7 expressly describes the removed-person failure case and its cache-age bound.
+
+**Smallest correction:** explicitly resolve the scope conflict: either record activity's bounded cached-disclosure exception by citation and disclose the revocation tail, or change the adopted behavior to require a current check before redisclosure. Do not claim both. Reuse activity gate 20's failed-pull/hard-maximum test, and include a derived item's source revocation. The new cost disclosure should also follow activity §2.2's per-refresh-window emission rather than imply every browser load necessarily produces an instance request.
+
+### R3-11. P2 — Ruling 12's rejected alternative mixes verified facts with an unsupported impossibility claim
+
+**Where:** status lines 21–25; ruling 12's rejected column; doctrine §5.1, lines 341–359.
+
+**What is wrong:** both requested home facts are true, but the conclusion drawn from the second is too strong. Home §6 has `kind = member | guest`, with no owner role, and includes `app` in each directory entry. Home §7 explicitly keeps owner decisions instance-side. Those facts do not prove that projecting a team-wide roster from app-scoped rows is impossible or necessarily produces app-specific standing.
+
+**Why:** projecting distinct `(team, person)` memberships can erase the app dimension and give one answer to every app. It would still suffer the owner-information and untrusted-writer defects, so it is not a defense of revision 1. It is a counterexample to one claimed reason for rejecting it. Ruling 1 alone also does not define every membership consequence; the common roster is established by rulings 4/12, and §3 expressly permits differing lone-instance rosters.
+
+The sentence “Ruling 9 made a directory row a hint” is an unqualified wrong reference: suite ruling 9 concerns writes; **home** ruling 9 concerns directory hints.
+
+**Confidence:** high for the source verification and projection counterexample.
+
+**Smallest correction:** retain the separate owner-controlled table and its two decisive reasons. Describe app scope as an aggregation/lifecycle design problem, not proof that aggregation cannot work; qualify the roster claim by shape. Name home ruling 9. Ruling 13's rejected alternative is substantively honest, although R3-1 shows its correction is not carried through. Ruling 14's alternatives are not complete recall mechanisms, as R3-7 distinguishes. Ruling 15 correctly rejects treating possession of a URL as team-wide discoverability.
+
+### R3-12. P2 — The new ladder and consuming amendments still leave incompatible contracts
+
+**Where:** doctrine §§2, 4.2, 10 and 13; Appendix A; current sibling specs.
+
+**What is wrong and why:** the following are concrete remaining adoption defects, including drift since the previous review.
+
+- **Link lifecycle:** §4.2 correctly permits indexing `team` plus a link, but §13 says a link share that a sibling can index is never built. If Ana adds a link after team ingest, following §13 would retire a still-team item; following §4.2 would retain it. Revoking just the link must not revoke team custody. Downgrading the rung while retaining the link must produce `gone`; deleting/revoking a link alone must not expose link-only item ids or capability values through S1. Gate 3 covers only static states. Also, “a link holder is never on a roster” confuses a request's authority with a person's other rights: Ana can be both a member and a link holder.
+- **Sheets' local consistency rule:** §2, lines 138–141, still attributes the cross-repository problem to its local same-commit rule. §10, lines 871–874, still says to replace that rule. A.3 now correctly says to keep it. The original `AGENTS.md`, lines 5–7, concerns that file and the Sheets design spec in the same repository.
+- **Appendix A remains operationally contradictory:** its introduction says replacements are not pasteable restatements, then A.1–A.3 say to add or replace text with quoted paraphrases. A builder cannot execute both instructions. A.4 leaves memoria §2's and §8.5's unqualified no-sibling-write clauses outside the amendment list. The new provenance requirement also needs receiving-app read/export/activity/S3 adoption, not just A.2's create route.
+- **Home ruling number collision:** A.1 says “Add ruling 22.” Home trunk at `2b68301` already has ruling 22 for URL-based team selection. Its §2.2 activity amendment has landed too; it is no longer a pending change to complete. Use a newly allocated ruling and the actual source baseline.
+- **Memoria citation branches disagree:** decisions 13/14 and the cited gate 26 exist in `tensorx-provider` at `8dfc3c9`; they are not in the supplied primary checkout. Current main at `9464a8b` instead has a five-valued S2 with `AHEAD`, explicit reconciliation, and a different gate 26. §6 cannot say the cited calls move unchanged in substance while A.4 adopts four values without addressing that current contract. This is not a recommendation to adopt `AHEAD`: main itself records unresolved problems with it. It is a requirement to pin and reconcile the actual baseline.
+- **Calendar needs an adoption change, not only a citation:** its substantive model is in `calendar-first-screens`, not the primary checkout's example `Note`. That model confirms there is no commenter role, but team access is represented by explicit per-person grants. A.5's instruction to add consumed rulings alone does not distinguish roster-derived team access from independent grants that survive removal, nor identify how the new sharing ladder applies to calendars/events. Otherwise gate 5 cannot test the intended removal semantics against this model.
+
+**Confidence:** high for the literal contradictions and checked source revisions. The required calendar implementation can take several forms; the missing adoption boundary is the finding.
+
+**Smallest correction:** turn Appendix A into a precise list of owning sections/gates to replace with ruling-and-commit citations, with locally owned integration changes identified separately. Resolve the link lifecycle using the rung predicate, explicitly distinguish anonymous capability requests from signed-in member requests, and add creation/revocation/downgrade/edit-after-ingest fixtures. Pin the sibling baselines, allocate the home ruling without collision, and reconcile memoria's result vocabulary. Preserve Sheets' local rule everywhere the doctrine discusses it.
+
+## 3. Verdict
+
+**Request changes.** The protected membership record, receipt/commit split, explicit acquisition-time custody, and disclosure of S2's product cost are substantive improvements. They do not yet yield a consistent implementable contract. The main blockers are the false compromise bound, session and restore revocation failures, and the provenance rules' missing no-person and whole-artefact behavior.
+
+Ruling 15's distinction between the sharing rung and a link capability is sound for custody of original items. Anonymous editing and link access to derived items require further decisions and consuming amendments. Its proposal status is correctly recorded; this review does not approve it on Paul's behalf.
+
+### Sources and verification limits
+
+All doctrine line references are to [revision 3](docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md) in this worktree. Original finding numbers refer to [CODEX-FINDINGS.md](CODEX-FINDINGS.md), which remains unchanged.
+
+| Source | Checked location and commit |
+|---|---|
+| Home design | [Home trunk design](/home/paulca/amadan.net/oficina/home/.claude/worktrees/trunk/docs/superpowers/specs/2026-08-30-home-design.md), `2b68301`. The primary checkout remains at stale `462df84`. Verified §§0, 2, 5–7 and 9, owner-role absence, app scope, sealed queue bootstrap, call-2 authority, and existing ruling 22. |
+| Activity | [Activity design on home trunk](/home/paulca/amadan.net/oficina/home/.claude/worktrees/trunk/docs/superpowers/specs/2026-09-05-activity-pull-design.md), `2b68301`; also located the originating worktree at `6f7e6a9`. Unlike the previous review's baseline, the design and home amendment have landed on trunk. Checked credentials, guests, response shape, cache failures and gates. |
+| Docs | [Docs design](/home/paulca/amadan.net/oficina/docs/docs/superpowers/specs/2026-08-30-docs-design.md), `f6cf5c2`; sharing, pending sign-in, conversion rejection, module path, router/session middleware and rastrillo v0.22.0. |
+| Sheets | [Sheets design](/home/paulca/amadan.net/oficina/sheets-core/docs/superpowers/specs/2026-08-30-sheets-design.md), `8ed3926`; ruling 12, §§8.2–8.3, `AGENTS.md`, `PRODUCT.md`, module path. |
+| Memoria original baseline | [Primary-checkout design](/home/paulca/amadan.net/oficina/memoria/docs/superpowers/specs/2026-09-03-memoria-design.md), `617eb2a`; authorization, provenance, S1–S7, retirement/restore, gates, sequencing and open defects. |
+| Memoria provider citations | [Provider worktree design](/home/paulca/amadan.net/oficina/memoria/.claude/worktrees/tensorx-provider/docs/superpowers/specs/2026-09-03-memoria-design.md), `8dfc3c9`; located and verified decisions 13/14, §8.4 and passage-authorization gate 26. These citations are real, but branch-specific. |
+| Memoria current main | [Main worktree design](/home/paulca/amadan.net/oficina/memoria/.claude/worktrees/notion-ingest/docs/superpowers/specs/2026-09-03-memoria-design.md), `9464a8b`; compared the changed S2 vocabulary, provenance, admission, reconciliation and acknowledged open defects. |
+| Calendar | [Calendar model](/home/paulca/amadan.net/oficina/calendar/.claude/worktrees/calendar-first-screens/internal/calendar/models.go), `f18898b`; team/personal kinds, explicit grants, owner/editor/viewer roles. Primary `ecb0df9` still has an example `Note` model. |
+
+The Docs middleware and memoria CSP claims were rechecked in local code and the installed dependencies. The previous review's numeric middleware probe was not rerun and is not presented as new test evidence. No browser flow, deployed service, contract conformance suite or persisted home membership schema was executed: the requested review is of a design and this repository has no contract module yet. No sibling files were edited and no cross-repository messages were sent. Private brainstorm attribution and discussion 33's full contents were not independently verified.
+
+## 4. Gate-by-gate assessment
+
+“Writable” below concerns whether the stated test has a definite expected result. It does not report a passing implementation. Gate 6b makes ten entries under the nine numbered gates.
+
+| Gate | Can it be written and run as stated? | Required correction or coverage |
+|---|---|---|
+| 1. Fixture conformance | **Harness yes; complete conformance not yet.** | Declare which calls each app serves; S6 belongs to Sheets and call 8 to home. Call-8 pages and provenance/S3 outcomes still need R3-4/R3-8/R3-9. Add fixtures with their first client as instructed; do not claim every app serves every call. |
+| 2. S2 total decoder | **Yes, as a decoder property.** | Short, duplicate, malformed, non-2xx and unsupported-subject responses have definite outcomes. Add source-forward/source-behind semantic fixtures and mixed stale-roster/non-custodial cases; total decoding does not establish correct authorization. |
+| 3. Custody | **Static checks mostly yes; literal fixture wording still needs correction.** | “All five” includes deleted, so “does hold a grant on them” still demands a live grant to a deleted item; its clarification only repairs the `granted` fixture. Use the same id/asker in private and deleted alternative states, with a former grant where deletion removes it. Add team-plus-link and after-ingest link/rung transitions, including Ana's retained grant. Define stale-roster precedence and derived-item custody. |
+| 4. No cross-team dial | **Yes.** | Correctly distinguishes an initially forbidden destination from a forbidden redirect follow-up; instrument connections per host. Check the browser form destination before rendering, with canonical host parsing and an allowed-host control. |
+| 5. Roster | **Not as a complete revocation gate.** | Successful refresh, retained independent grants, lower-number rejection and lone isolation can be tested. R3-2/R3-3/R3-8/R3-9 block the claimed time/failure guarantee. Add existing-session, S6/call-7 failure, failed middle page, concurrent epoch change, numerical catch-up and joint-restore cases. |
+| 6. Write-as-self | **Yes for the original access checks.** | Authenticated-consumer actor derivation and spoof refusal are now explicit. Add immutable creator versus revision actor behavior after person/link edits under R3-6; the current three denials do not settle it. |
+| 6b. Provenance survives the hop | **No, as stated.** | S3 has no asker; the envelope cannot identify safe partial redaction; universal named denial conflicts with S3 absence-hiding. Resolve R3-4/R3-5. Test successful initial access, whole-artefact dependency failure, activity/title output, changed revisions, anonymous link access, and two sequential writes. Refusal when no source authorization surface exists is independently testable. |
+| 7. Hand-off through middleware | **Yes for the specified receipt/commit security flow; finish the fixture parameters.** | Use the real browser CSP and receiver middleware. Explicitly choose absent/null-origin outcomes and expiry/body limits. §7.1 fixes the server-side body; ignoring an extra changed client body is safe, while the gate demands refusal—choose one wire behavior. Include expired state, concurrent replay, sign-in/session change and a successful scriptless flow. This is substantially repaired from revision 2. |
+| 8. Publish home database | **Yes as a seeded disclosure test against the eventual implementation.** | Owner roles are now correctly part of the expected disclosure. Exercise actual persisted data and failure paths, including bootstrap/roster writes. Preserve home's existing no-content/no-readable-address checks; a schema-column inspection or roster fixture alone does not establish them. |
+| 9. Citation currency | **Yes.** | Commit/revision agreement and normative-change scope are repaired. Keep the deliberate one-revision grace distinct from current conformance. Pin the consuming source baselines too, so the A.1 ruling collision and memoria branch differences are resolved before adoption. |
diff --git a/CODEX-FINDINGS.md b/CODEX-FINDINGS.md
new file mode 100644
index 0000000..d8faba5
--- /dev/null
+++ b/CODEX-FINDINGS.md
@@ -0,0 +1,259 @@
+# Findings on the suite sharing doctrine, revision 2
+
+**Verdict: request changes.** Reviewed by GPT-6 (Codex), 2026-09-05, independently of the draft and revision-2 authors. These are findings and proposed corrections, not new rulings. The doctrine has not been edited.
+
+The review covers the whole doctrine at `0c4f098`, including `git diff 609394e..HEAD`, and the sibling sources identified below. P1 means a trust boundary or promised security property is broken or lacks an enforceable design. P2 means a contract, adoption, or verification defect that needs correction before implementation.
+
+## Verdicts on the three priority changes
+
+| Change | Is the defect real? | Is the correction right? | Consequences |
+|---|---|---|---|
+| S2 intersects authorization with custody, §4.3 | Yes. A previously ingested id lets a consumer distinguish a private item still granted to the asker from a deleted item. The unintersected join in memoria §4.1 permits this. | Yes, for the stated current-visibility privacy guarantee. A determinate non-custodial item must not produce `VISIBLE` or an existence-revealing alternative. | The claim that correct consumers lose nothing is false. They can hold legitimately ingested items when visibility changes. See finding 8. The fix also does not erase those copies: finding 6. |
+| Typed asker on S2 and S6, §3 | Yes. Provisioning intent alone does not ensure that the two endpoints interpret the subject identically. | Yes. `{kind, value}` distinguishes a configuration mismatch from an authorization denial; `UNKNOWN` is the correct failure class. | Clean verdict on this change. Both request types and their fixtures must adopt it. Unknown or unsupported kinds must fail before item-dependent results; an unknown value of a supported kind can still be an ordinary denial. No extra network round trip is inherent in the tag. It does not prove shared authority or shared membership, and should not be claimed to do so. |
+| Hand-off checks `Origin` and asks for confirmation, §7.1 | Yes. Same-site cookies permit a sibling page to submit a credentialed form without a deliberate create action. | The two protections are appropriate, but the commit protocol is incomplete. The home citation supplies a return-host check and login-state binding, not an already specified document-confirmation protocol. | The receiving and committing requests need different rules. Existing middleware and content policy also block the proposed flow, and confirmation adds a round trip. See findings 4 and 9. |
+
+## Findings, ordered by severity
+
+### 1. P1 — Making directory rows authoritative leaves an instance-token route around roster step-up
+
+**Where:** doctrine §5.1, lines 221–225; §5.4, lines 279–283; §6, line 327; A.1. Home §6 and §9; Docs §9.1.
+
+**What is wrong:** §5.1 says the roster is the existing directory rows with `kind = member`, made authoritative. The doctrine also says home calls 1–6 are unchanged. Home call 2 lets an instance upsert and revoke its directory rows under a consumer token. Docs spells out the submitted `kind` at lines 607–608. That was safe only while directory rows were hints.
+
+**Why:** an instance can announce an attacker-controlled address as a member, or replay a queued pre-removal announcement. If call 8 derives the roster from those rows, the attacker becomes a team member without the owner action or fresh assertion required by §5.4. Conversely, instance-scoped deletion can affect membership if directory-row lifetime also defines roster lifetime. A table that serves both purposes needs a write-authority distinction; changing the reader does not create one.
+
+**Confidence:** high; this follows from the specified writers and the proposed source of truth, without assuming a broken token check.
+
+**Smallest correction:** give authoritative `(team, person, role)` membership its own owner-controlled record or equivalent protected provenance. Make call-2 rows projections/hints that cannot grant or revoke team standing. Amend the home-owned contract by citation after recording that boundary there. Test a member upsert, revoke, and delayed replay under an instance token against the authoritative roster.
+
+### 2. P1 — The compromised-home bound is false, including its claim of unavoidable evidence
+
+**Where:** doctrine §5.5, lines 292–301; §6.1, lines 359–361; §9 item 4, lines 502–503. Home §5.3–§5.5; activity design §2.2(b) and §5.1.
+
+**What is wrong:** the doctrine concedes that a compromised home can add members but says it still cannot read documents or act silently. It names owner step-up and the instance's cached roster as bounds on that compromise.
+
+**Why:** an added member can read team-visible content by definition. The attacker can use an account it controls after inserting that account into the roster. If compromise includes the credential origin and signing key, the attacker can also mint an assertion for an existing owner. The receiving grants join then authorizes that owner's actual rights; it cannot distinguish impersonation by the identity authority. The home spec explicitly identifies hostile `sub` as account takeover. This latter risk predates ruling 4.
+
+Step-up performed by the compromised authority does not constrain it. Nor is a directory row or notification unavoidable when the attacker controls the process that writes or sends it. Existing-identity impersonation need not create a membership row at all. The activity design already acknowledges that call 7 removes an observable distinction between normal home traffic and exfiltration (§2.2(b)).
+
+**Confidence:** high. A directory-origin XSS alone has a narrower scope; the doctrine must distinguish it from live directory-server or credential-origin compromise.
+
+**Smallest correction:** separate those threat models and disclose the resulting membership and impersonation capabilities in §5.5 and §9. Describe notifications as evidence produced by honest execution, unless an independent, enforceable audit mechanism is designed. Do not claim that an instance's grants join defeats compromise of the authority that names the grantee.
+
+### 3. P1 — The new writes have no contract for preserving or checking their source authority
+
+**Where:** doctrine §4.3, lines 185–188; §7.1–§7.2; §11; A.4. Memoria §5.2, §9.1, §10 gates 9, 17 and 19, and §12.
+
+**What is wrong:** transitive provenance is required, but the hand-off describes a document body and S8 describes actor and destination constraints. Neither defines how source identities and revisions survive a write into another app, or how that app enforces them on ordinary reads, exports, activity, and onward copies.
+
+**Why:** save an answer derived from a Sheet into Docs, then revoke or edit the Sheet. Docs' own grant to Ana is insufficient to authorize the saved answer under §4.3. Merely assigning the newly created Doc a Docs id must not reset authority. The generic declaration does not tell Docs how to recognize or check this case.
+
+There is also a concrete missing endpoint: an answer can include memoria-owned uploads or Notion material. Docs would need to check that source authority. Memoria decision 7, §9.1, and gate 17 explicitly prohibit sibling-facing endpoints; its §12 requires a new decision and leak entry before adding one. A.4 does not amend those clauses. A browser confirmation establishes the writer's intent, not an exemption from provenance.
+
+**Confidence:** high that the required contract is missing. A particular implementation might enforce the general rule correctly, but the proposed fixtures and amendments do not establish that it can.
+
+**Smallest correction:** define a bounded provenance envelope for both write paths, retain the original source authority, and require source checks on every disclosure path. Add the required memoria authorization surface through an explicit amendment, or defer hand-offs containing sources that the receiver cannot check. Add a two-hop write/read fixture with mixed authorities and a source revocation. Do not implicitly turn “save” into permanent republication.
+
+### 4. P1 — A confirmation page does not specify a protected commit, and a receiver check is too late to prevent cross-team transmission
+
+**Where:** doctrine §4.1, §7.1 lines 395–428, and §11 gate 7. Home §5.5–§5.6.
+
+**What is wrong:** the new rule describes the initial POST's origin check and a confirmation screen, but does not bind the final mutation to the reviewed pending body, the receiver's browser/session, or a single redemption. Applying the sibling-origin rule to both POSTs would also reject the final same-origin Docs form unless Docs unnecessarily had a consumer row for itself.
+
+**Why:** a bare second POST with a `confirm` flag can be submitted by the same allowed sibling that submitted the first. A screen appearing somewhere in the flow is not evidence that this browser reviewed this body. A nonce carried only in a URL is not itself browser binding; home §5.6 separately requires that binding to prevent login CSRF.
+
+The sending side needs a boundary too. If memoria's form action is misconfigured to another team's Docs host, the body has already reached that instance before its `Origin` check rejects it. §4.1's sibling HTTP-client guard does not automatically govern an HTML form action. This contradicts §0's literal “a byte … reaches an instance” test even when the receiver refuses to save it.
+
+**Confidence:** high that both obligations are unstated; this is a design gap, not a claim that a deployed hand-off is exploitable.
+
+**Smallest correction:** distinguish the draft-receipt and commit phases. Validate the exact same-team destination before rendering the sending form. Accept allowed sibling origins only at draft receipt; commit through a receiver-origin form against bounded, expiring, single-use pending state bound to the browser and authenticated person, with the reviewed body fixed server-side. Preserve safe rendering and framing restrictions. Gate forged commits, replay, changed bodies, session changes, absent/null origins, and a foreign-team destination before transmission. OWASP likewise treats same-site subdomains and actual request validation as distinct concerns ([CSRF prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)).
+
+### 5. P1 — Roster expiry does not yet bound revocation, and rollback can restore admission
+
+**Where:** doctrine §5.3 lines 249–275; §5.4 lines 285–286; §5.5 lines 298–299; §11 gate 5.
+
+**What is wrong:** a TTL triggers a refetch, but the doctrine never says what happens when that refetch fails. It still says requests enforce from the local copy and characterizes an outage or rollback as lag rather than admission.
+
+**Why:** cache epoch 10, remove Ana at epoch 11, then make the home unreachable. Retrying at expiry while continuing to enforce epoch 10 admits Ana indefinitely. Refusing stale membership decisions bounds that exposure, but costs availability and produces `UNKNOWN` for S2, not the gate's universal `NOT_VISIBLE`.
+
+A home restored to epoch 10 can also return Ana as a member to a receiver that already applied 11 unless lower epochs are rejected. An instance restored from backup must refresh before using its restored roster. A TTL alone does not settle either direction of rewind. “Within one epoch” is not an elapsed-time bound for an instance that has not learned the new epoch.
+
+**Confidence:** high; these are direct failure sequences for the specified cache.
+
+**Smallest correction:** state the usable-age limit and behavior after an unsuccessful refresh, including existing sessions and sibling reads. Publish a new roster only after a complete fetch and validation. Specify rewind detection/recovery and refresh-before-admission after restore. Express the revocation bound in time, including request deadline or grace if any; use a fake clock in gate 5 and separate successful revocation from unavailable authority. The TTL is an availability/security choice, not merely a sentence tightening or a number derivable from cold-wake latency.
+
+### 6. P1 — The “inside” invariant and leak bound ignore copies retained after visibility changes
+
+**Where:** doctrine §0 lines 38–41; §4.2; §9 item 1. Memoria §5.2, §6.1, §7.4 and §8.1.
+
+**What is wrong:** §0 describes current custody as an invariant, and §9 says a compromised sibling cannot read private items. The pull design necessarily retains formerly team-visible content after a source makes it private or deletes it.
+
+**Why:** memoria ingests revision 7 while team-visible; Docs then makes it private; memoria has not yet pulled `gone`. Publishing memoria's holdings now reveals content the team cannot currently read in Docs. S2 can prevent honest rendering, but cannot remove stored bytes. A compromised consumer can retain previously received content permanently. Memoria's snapshots, delayed pulls, and explicit circuit breaker against wholesale purges make it especially important not to equate denial at render with erasure of custody.
+
+This also limits the existence-hiding claim: `gone` can conceal the distinction between deletion and privatization; it cannot erase knowledge that an item existed before.
+
+**Confidence:** high; no implementation bug or malicious source is required for the initial retention window.
+
+**Smallest correction:** distinguish authorized acquisition, current authorization to render, and eventual retirement of honest cached copies. State the synchronization/retirement bound and restore obligation by citation. In §9 include historical content acquired while eligible and the inability to recall it from a compromised consumer. Rewrite §0's test to match those guarantees rather than promise instantaneous distributed erasure. Also scope the “across” test to suite-mediated flows: existing export/upload features cannot prevent a person deliberately copying material between teams.
+
+### 7. P2 — Gate 5 silently changes what removal means for guests and individual grants
+
+**Where:** doctrine §5.2 lines 227–237; §5.4; §11 gate 5, lines 569–572. Home §5.5; activity design §4–§5.2; Sheets §8.2; Docs §8.2.
+
+**What is wrong:** the gate requires every removed member to receive an empty call-7 response from every instance. The doctrine preserves instance-owned individual/guest grants and says nothing about guests changes.
+
+**Why:** remove Ana's team standing while leaving a valid grant to one document. The instance still owns that grant, and call 7 is specified to evaluate it. Returning its activity is correct under the cited contract. Returning empty solely because Ana is off the team roster revokes a right §5.2 leaves instance-side. S2's custody filter does not change the separate call-7 contract.
+
+**Confidence:** high. If complete offboarding is intended, that is an additional rule, not the current meaning of roster removal.
+
+**Smallest correction:** make gate 5's removed-person fixture explicitly have no remaining independent grants, and add a guest fixture that retains only its granted activity. Alternatively, specify a distinct complete-offboarding operation and its effect on instance grants. Do not implement that policy by asserting the desired empty list in a test.
+
+### 8. P2 — The S2 correction is right, but “costs a correct consumer nothing” is false
+
+**Where:** doctrine §4.3 lines 194–208; A.4. Memoria §4.1 and §6.1.
+
+**What is wrong:** the correction justifies its zero-cost claim using visibility at ingestion, even though its own attack depends on a later visibility change.
+
+**Why:** memoria correctly ingested a document when team-visible. It is now private but still granted to Ana; its `gone` row has not reached memoria, or an answer already being processed still contains it. Before this change, S2 could return `VISIBLE` for Ana. After it, S2 returns `NOT_VISIBLE`. Ana loses the passage and any derived answer requiring it in memoria, while direct access in the source app remains valid. Asking S2 about previously ingested content is exactly how a correct consumer checks current authorization.
+
+**Confidence:** high. This is a necessary product restriction under the chosen indistinguishability guarantee, not an argument to undo the intersection.
+
+**Smallest correction:** replace the zero-cost paragraph with that explicit consequence and a source-app fallback. Add a transition fixture: ingest, downgrade while retaining Ana's grant, query before feed catch-up, receive `NOT_VISIBLE`. Keep the private/deleted response equality. Require custody denial to dominate revision-specific diagnostics in S2 and S6; otherwise `STALE(cell)` or a numeric diagnostic can reopen an existence distinction.
+
+### 9. P2 — The hand-off needs middleware changes and an additional round trip
+
+**Where:** doctrine §7.1 lines 401–428; A.2 lines 710–713. Docs `internal/docs/app.go:406–445`; memoria `cmd/memoria/main.go:24,75–77`; their rastrillo pins.
+
+**What is wrong:** “no new machinery” and “neither costs a round trip” obscure actual consumer work. The proposed form cannot traverse the current sender and receiver unchanged.
+
+**Why:** memoria uses rastrillo v0.23.0's default `Content-Security-Policy`, whose `form-action 'self'` blocks a form targeting Docs. Docs places rastrillo v0.22.0's `csrf.Protect` above every route; its `SameOrigin` rejects `Sec-Fetch-Site: same-site` even with the legitimate memoria `Origin`. Its session middleware also refuses an unauthenticated POST rather than preserving its body through sign-in. Finally, for an already signed-in person, receipt → confirmation → commit requires a second POST where revision 1 committed on the first.
+
+**Confidence:** high. A temporary probe against Docs' actual pinned `csrf.Protect` produced `204` for a same-origin request and `403` for the proposed same-site request. Sender CSP and session behavior were checked in source, not exercised in a browser.
+
+**Smallest correction:** name the narrowly scoped sender `form-action` allowance, dedicated receiving route and its replacement origin guard, pending-body/sign-in handling, and additional confirmation round trip. Preserve the normal same-origin write protections elsewhere. Extend gate 7 through the real middleware and browser policy, including a successful same-team flow. A handler-only fixture would miss both blockers.
+
+### 10. P2 — Call 8 has no coherent paginated snapshot contract
+
+**Where:** doctrine §5.3 lines 244–247 and 269–271; §6 call 8.
+
+**What is wrong:** the review adds pagination but leaves the wire example unpaged, specifies no cap or cursor behavior, and says S2 and S6 already have cursors. They do not: their caps bound caller-supplied batches.
+
+**Why:** fetch page one at epoch 10, change the roster, then fetch page two at epoch 11. Combining the pages can produce a membership set that never existed, skip a removal, or omit an owner. Publishing a partial response has a similar effect. Neither the example nor the citation tells the client which snapshot it received or how to restart.
+
+**Confidence:** high, verified against memoria §6.1's signatures and §4.1's multiple-batch rule.
+
+**Smallest correction:** specify a bounded page shape with epoch-bound cursor, completion indication, and invalidated-cursor response. Require staging and atomic replacement only after all pages of one epoch arrive. Name restart behavior on epoch change and refusal to treat partial/failing results as an empty roster. Add concurrent-change and failed-middle-page fixtures. Replace the inaccurate comparison to S2/S6 pagination.
+
+### 11. P2 — The adopted S2 result vocabulary still lacks the source-behind case
+
+**Where:** doctrine §4.3 and §6's adoption of memoria §4.1; §11 gates 1–2. Memoria §4.1 lines 484–514 and its acknowledged §13 defect.
+
+**What is wrong:** the doctrine adopts four values without correcting the cited definition of `STALE`, which only covers a source that has moved past the requested revision.
+
+**Why:** memoria has revision 7 and a restored source has revision 5. Returning `VISIBLE` would serve an unverified revision; returning `NOT_VISIBLE` silently represents synchronization failure as a denial. The total decoder only checks response completeness and shape. It cannot repair a validly encoded wrong classification. Memoria already records this exact unresolved defect in §13.
+
+**Confidence:** high.
+
+**Smallest correction:** define the nonmatching-revision outcome for both directions, with a recovery distinction if needed. Preserve a loud failure whenever current revision authority cannot be established, while keeping determinate private/deleted denial indistinguishable. Add forward and backward revision fixtures and include the correction in A.4.
+
+### 12. P2 — The roster epoch does not pass the home claim test as cited
+
+**Where:** doctrine §5.3 lines 251–256; A.1 lines 687–691. Home §5.4.
+
+**What is wrong:** the doctrine says the epoch passes the existing test because a failed match is treated as absent. The home test requires non-identity claims to validate against a closed enumerated set and be cosmetically failable. A changing roster epoch is neither a closed cosmetic choice nor merely presentation data.
+
+**Why:** if “failed match” means a different valid epoch, discarding it suppresses the very refresh this feature needs. If it means a malformed claim, the text needs to say so. More fundamentally, suppressing a refresh can leave an old membership decision in force until TTL expiry. Refetching rather than trusting a claimed role is a useful safeguard, but it does not make this the same kind of claim as theme.
+
+**Confidence:** high about the citation mismatch. The bounded-staleness design can still be sound after finding 5 is resolved.
+
+**Smallest correction:** record a narrow invalidation-hint exception in the home-owned test, distinguishing invalid encoding from a valid differing epoch. Make call 8 authoritative and its freshness bound independent of the claim. Cite that exception here instead of saying the unchanged test already passes.
+
+### 13. P2 — The suite leak list omits new roster information and already-disclosed activity costs
+
+**Where:** doctrine §5.1, §5.3, §9, and §11 gate 8. Home §6/§10 and activity design §2.2 and §7.
+
+**What is wrong:** the list omits consequences of the features it adopts, and gate 8 claims the roster adds no column class.
+
+**Why:** home's existing directory `kind` distinguishes member from guest; it does not distinguish owner from member. The new `role` does. If stored openly, publishing the database now identifies team owners. If sealed, the disclosure and test need to say that. Call 8 also distributes the complete team roster and those roles to each instance, rather than just the people known to that instance under the old local models.
+
+The activity design explicitly discloses two suite effects in §2.2: a fine-grained signal of a person's front-door visits sent to their instances, and the loss of outbound-home traffic as a compromise signal. Neither appears in §9. Its §7 also retains last activity in RAM briefly after removal. Those are relevant alongside the existing title-in-RAM entry. Findings 2 and 6 identify two further bounds that need correction, rather than additional reassuring prose.
+
+**Confidence:** high; the old directory schema and the activity disclosures are explicit.
+
+**Smallest correction:** add citations for these costs in §9, choose the roster-role storage/disclosure treatment, and amend gate 8's expected disclosure accordingly. Exercise the actual persisted schema and failure paths; one roster fixture cannot prove that an entire database has no additional information class. An audit of column types alone also misses new information encoded in existing columns.
+
+### 14. P2 — The consuming amendments preserve contradictions and introduce restatements
+
+**Where:** doctrine §10; §12 steps 1–3; Appendix A; repository `AGENTS.md`.
+
+**What is wrong:** the prescribed amendments do not produce the cited, consistent specs that §12 says unblock implementation.
+
+**Why:** several concrete leftovers survive them:
+
+- A.2's proposed Docs sentence names sibling service “in suite shape”; §3 and §12 explicitly require Docs and Sheets to serve lone peers before the home exists. The amendment must name the independent sibling-token switch or cite it without narrowing the scope.
+- A.4 leaves memoria §11's explicit person-ref/home dependency on slice 1, its no-inbound-surface decision and gate, and its stale S7 admission gate. Replacing §4.3 alone does not remove those contradictions. Finding 3 makes the inbound-surface conflict operational rather than editorial.
+- A.1–A.3 prescribe paragraphs paraphrasing boundaries, membership, and egress. That is the practice `AGENTS.md` and §10 say to replace with ruling-and-commit citations. Adding a citation after another copy of a rule still leaves another copy to drift.
+
+**Confidence:** high; these are literal mismatches between the proposed replacements and the surviving source text.
+
+**Smallest correction:** revise Appendix A as a complete list of consuming sections and gates to replace with citations, including the independent shape/token switch, memoria sequencing, admission fixtures, and required new authority surface. Keep locally owned behavior local. Do not declare slice 1 unblocked until the resulting specs agree; the doctrine itself can remain findings-only in this review.
+
+### 15. P2 — Two stated sibling positions are not what their cited text says
+
+**Where:** doctrine §2, §8 lines 474–479, §10 lines 523–527, A.2 §11.2, and A.3's AGENTS amendment.
+
+**What is wrong:** the doctrine supplies a new reason for Docs' conversion-API rejection and treats Sheets' local spec-synchronization rule as a cross-repository rule.
+
+**Why:** Docs §11.2, lines 929–931, rejects conversion APIs because they send customer documents to a third party. It does not say the provider was unnamed or the sending unavoidable. A per-team optional converter with a named provider could satisfy suite §8, so the narrower reason is a policy qualification, not verification that the old rejection already meant it. The ruling may authorize that qualification; the cited passage does not establish its historical premise.
+
+Sheets `AGENTS.md`, lines 5–7, says that where **that file** disagrees with **its design spec**, the spec wins and the file must be fixed in the same commit. Both files are in Sheets. It does not demand one atomic commit spanning five repositories. Replacing that rule would remove a useful local consistency obligation to solve a problem it did not create.
+
+**Confidence:** high for both readings.
+
+**Smallest correction:** identify the Docs change as an intentional qualification under suite ruling 6, with its prior reason accurately cited. Retain Sheets' local same-commit rule and add a separate suite-citation requirement. Do not attribute the new positions to the earlier text.
+
+### 16. P2 — Citation currency can stay green across binding changes
+
+**Where:** doctrine §11 gate 9, lines 584–590; §10; Appendix A's introduction.
+
+**What is wrong:** the new measurable revision only has to increase for changes to §1 or §6. Binding behavior also lives in §3–§5 and §7–§8.
+
+**Why:** change the hand-off commit rule or roster stale-cache policy in its owning section, leave §6's citation untouched, and the prescribed revision can remain unchanged. A consumer of the old behavior passes currency indefinitely. The gate also does not specify checking that a claimed revision actually belongs to the cited commit. The appendix still asks only for a commit, although the new gate requires both.
+
+**Confidence:** high.
+
+**Smallest correction:** bump the revision for every normative contract change, wherever it lives; derive or verify revision from the cited commit; and update the consuming-section format consistently. A one-revision grace period is measurable, but remains a deliberate compatibility allowance, not proof that a consumer follows current security requirements.
+
+## Gate-by-gate assessment
+
+These are assessments of the proposed tests. No contract module exists here, so none of its gates was executed.
+
+| Gate | Can it be written as stated? |
+|---|---|
+| 1. Fixture conformance | The harness can be written. Complete golden requests/responses still need the missing call-8/write shapes and error precedence resolved. Generic conformance must distinguish the calls each app actually serves; S6, for example, belongs to Sheets. |
+| 2. S2 total decoder | Yes as a bounded decoder property, including an unsupported subject kind. It does not prove correct authorization or revision semantics: findings 8 and 11 need semantic fixtures. |
+| 3. Custody | Yes with clarified fixtures. “Grant on a deleted item” should mean a former grantee or retained grant row, not require a dangling live grant contrary to an app's schema. Compare responses for the same requested id in private/deleted alternative states. Add the transition in finding 8. |
+| 4. No cross-team dial | Literal zero connections for a server-issued redirect is impossible: the client must connect to the allowed first host to receive the redirect. Require zero connections for an initially forbidden host and zero **follow-up** connections after an allowed host redirects. Use two fixture servers or an instrumented transport. |
+| 5. Roster | Not yet. “After one epoch” lacks an observation/time condition; failed refresh and retained guest grants have conflicting expected results. See findings 5, 7 and 10. The lone-instance no-home-network assertion is independently testable. |
+| 6. Write-as-self | Yes for the three stated access checks, but derive actor identity from the authenticated consumer and test attempted actor spoofing. Actor/destination checks alone do not exercise provenance; finding 3 needs its own gate. |
+| 7. Hand-off | Not as a complete proof yet. Define receipt/commit states and test through actual middleware plus browser policy. A screen-render fixture cannot establish user-confirmed mutation. See findings 4 and 9. |
+| 8. Publish database | A seeded database disclosure test is feasible. The unchanged-information premise is false or at least undecided for owner roles, and a roster response fixture does not establish at-rest behavior. See finding 13. |
+| 9. Citation currency | Arithmetic is feasible; normative coverage and commit/revision agreement are missing. See finding 16. |
+
+## Sources and verification limits
+
+Paths below identify the exact local documents used. Section and line references in the findings refer to those files as read on 2026-09-05.
+
+| Source | Checkout and commit |
+|---|---|
+| [Doctrine](docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md) | This worktree, `0c4f098`; all sections read, revision-1 diff inspected. |
+| [Home design](/home/paulca/amadan.net/oficina/home/.claude/worktrees/trunk/docs/superpowers/specs/2026-08-30-home-design.md) | `home/.claude/worktrees/trunk`, `5a42f2d`. The supplied primary checkout is on older `domain/oficina-works` at `462df84`, which lacks rulings 17–21; those citations were checked against `main` instead. |
+| [Activity design](/home/paulca/amadan.net/oficina/home/.claude/worktrees/design-activity-pull/docs/superpowers/specs/2026-09-05-activity-pull-design.md) | `design-activity-pull`, `c9274bf`; still explicitly awaiting independent review. Adoption in principle is not approval of all its protocol details. |
+| [Memoria design](/home/paulca/amadan.net/oficina/memoria/docs/superpowers/specs/2026-09-03-memoria-design.md) | `617eb2a`; relevant contracts, grants, ingestion, snapshots, gates and acknowledged open defects inspected. |
+| [Docs design](/home/paulca/amadan.net/oficina/docs/docs/superpowers/specs/2026-08-30-docs-design.md) | `f6cf5c2`; also checked router/session integration and the v0.22.0 rastrillo CSRF implementation. |
+| [Sheets design](/home/paulca/amadan.net/oficina/sheets-core/docs/superpowers/specs/2026-08-30-sheets-design.md) | `bad02cb`; grants join, roles, home contract, PRODUCT.md and AGENTS.md checked. |
+| [Calendar design](/home/paulca/amadan.net/oficina/calendar/.claude/worktrees/calendar-first-screens/docs/superpowers/specs/2026-09-02-calendar-design.md) | `calendar-first-screens`, `f18898b`; no spec exists at the brief's primary-checkout path. The worktree spec and `internal/calendar/models.go` confirm the team-calendar grant model and absence of a commenter role. |
+
+The module-path claim in §10 is correct for the checked `go.mod` files: Docs alone still uses `github.com/carlosframework/docs`. The S4/S5 resolution is supported by memoria §6.1. The home really does specify return-host allowlisting and browser-bound login state; finding 4 concerns the additional document-write protocol, not the existence of those protections.
+
+Browser-request reasoning was checked against the [Fetch Standard's Origin handling](https://fetch.spec.whatwg.org/#origin-header) and [OWASP's CSRF guidance](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html). The middleware probe ran from a temporary file, using the installed dependency without network access; it made no live HTTP request. No end-to-end browser test or deployment was performed. Cross-repository discussions were not sent, and claims about Paul's private brainstorm or discussion 33's full contents were not treated as independently verified evidence.
diff --git a/CODEX-REVIEW-BRIEF.md b/CODEX-REVIEW-BRIEF.md
new file mode 100644
index 0000000..34b531e
--- /dev/null
+++ b/CODEX-REVIEW-BRIEF.md
@@ -0,0 +1,81 @@
+# Review brief — the Oficina suite sharing doctrine, revision 3
+
+You are reviewing this document for the second time. Be adversarial. Your
+job is to find what is wrong, not to confirm that your last review was
+acted on.
+
+## What to review
+
+`docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md` in this
+worktree, at revision 3. Read all of it.
+
+## History
+
+- **Revision 1**, drafted by Fable 5.1 from a brainstorm with Paul, the
+ repository owner.
+- **Revision 2**, revision 1 plus a review pass by Claude Opus 5: six
+ defects, corrected in the text.
+- **Your own review** of revision 2 returned *request changes* with sixteen
+ findings. It is `CODEX-FINDINGS.md` in this directory, unchanged.
+- **Revision 3** claims to answer all sixteen, and adds a product
+ requirement Paul gave afterwards. `git diff 0c4f098..HEAD` is the whole
+ change; `git log` has the reasoning per commit.
+
+## What matters most
+
+**1. Are your sixteen findings actually answered, or only acknowledged?**
+Go through them one at a time. A finding is answered when the document
+would now survive the failure sequence you described, not when it cites
+your finding number beside a paragraph. Say plainly which are closed,
+which are half-closed, and which are worse than before. Four became
+rulings (12, 13, 14 from findings 1, 2 and 6; 15 is new) — check the
+ruling text does the work its finding asked for, and that its **rejected
+alternative** column is honest.
+
+**2. The new material has had no review at all.** Look hardest at:
+
+- **Ruling 12 and §5.1**, the membership record. It asserts three defects
+ in the old derivation, two of them factual claims about the home spec
+ (the directory row has no owner role; it is scoped per app, not per
+ team). Verify both against the home spec. Then ask what the new table
+ breaks: A.1 amends home §7, home §6 and home §0, and the roster is now
+ a thing the home holds that it did not before.
+- **Ruling 15 and §4.2**, the sharing ladder. This is the one row of §1
+ that is a proposal, not Paul's ruling. Four product states: private,
+ granted, team, and "anyone with the link can view or edit". The claim is
+ that a link is a capability beside the ladder, never a rung on it, so
+ custody keys on the rung. Test that against §0, against the change feed,
+ against S2 and S3, and against §7's write paths. Does a link-shared item
+ behave coherently everywhere? Is "link-shared and team-shared" handled?
+ What about a link created *after* ingest, or revoked after ingest?
+- **§7's provenance envelope and §7.3's link-holder writes**, both new.
+- **§7.1's receipt/commit split**, which answers your finding 4.
+- **§5.3's failed-refetch, rewind and paging rules**, which answer your
+ findings 5, 10 and 12. Check the availability trade in the stale case is
+ actually stated coherently and that `UNKNOWN` there does not collide
+ with another meaning of `UNKNOWN`.
+
+**3. The gates, §11**, all nine rewritten. §11's own first line says a gate
+that cannot be written as stated is a spec bug. Can each now be written
+and run? Gate 6b is new.
+
+**4. Verify citations rather than trusting them.** The sibling repos are on
+disk read-only at `/home/paulca/amadan.net/oficina/{home,docs,memoria,
+sheets-core,calendar}`. Your last review found the home checkout on a
+stale branch and used `home/.claude/worktrees/trunk` instead; do that
+again. Revision 3 added many new claims about sibling specs — every one is
+a place a mistake could hide.
+
+## What to produce
+
+Write `CODEX-FINDINGS-R3.md` in this worktree. Structure it as:
+
+1. **A table of your sixteen original findings**, each marked closed,
+ partial or open, with one line saying why.
+2. **New findings** against revision 3's new material, ordered by
+ severity, each with: what is wrong, why, your confidence, and the
+ smallest correction.
+3. **A verdict**: approve, or request changes.
+4. **The gate-by-gate table**, refreshed.
+
+Do not edit the doctrine. Findings only.
diff --git a/FABLE-FINDINGS-R3.md b/FABLE-FINDINGS-R3.md
new file mode 100644
index 0000000..34d5ec5
--- /dev/null
+++ b/FABLE-FINDINGS-R3.md
@@ -0,0 +1,194 @@
+# Findings on the suite sharing doctrine, revision 3
+
+**Verdict: request changes.** Reviewed by Fable 5.1, 2026-09-05, against the doctrine at `HEAD` of this worktree (`76bdc26`), `CODEX-FINDINGS.md`, `CODEX-REVIEW-BRIEF.md`, `git diff 0c4f098..HEAD`, and the sibling specs on disk. The doctrine has not been edited. Findings only.
+
+## 1. My stake, and what it disqualifies me from judging
+
+I am the model that drafted revision 1, and in the consultation after the GPT-6 review I recommended what became rulings 12, 13 and 14. Revision 3 itself is also Fable-authored text, by a different session. So:
+
+- **Not independent:** whether rulings 12, 13 and 14 are the right decisions, and whether their "rejected" columns are honest. I have checked their *factual claims* against the sibling specs, because a citation is either right or wrong regardless of who recommended it, and I report where those claims fail. I have not tried to judge whether the rulings themselves are wise; a reader should discount anything approving I say about them and weight only the defects.
+- **Independent:** ruling 15 and §4.2, which I had never seen; §7's envelope, §7.1's split, §7.3, §5.3's three new rules, all nine gates, Appendix A's new text, and the twelve findings answered in place. None of that text is mine.
+- **A structural caveat** the status block should carry: `AGENTS.md` says the author of a draft cannot be its reviewer. Two of the three reviews of this document, and this one, are by the same model family that drafted it. A same-model reviewer shares the drafter's blind spots. This pass should not be counted as the "third pass" §14 waits on unless Paul decides it is.
+
+Every citation below was read at: home `trunk` worktree `2b68301` (newer than the `5a42f2d` GPT-6 read — the §2.2 amendment has since landed), activity design in the same worktree, memoria main checkout (spec at 1459 lines, 21 gates, decisions to 12) and its `tensorx-provider` worktree where noted, Docs `2026-08-30-docs-design.md` and `internal/docs/app.go`, Sheets `2026-08-30-sheets-design.md`, `PRODUCT.md`, `AGENTS.md`, calendar `calendar-first-screens` worktree, Pegamento `e7442c2`, rastrillo `v0.23.0` in the module cache.
+
+## 2. The sixteen findings
+
+"Closed" means the document now survives the failure sequence the finding described. "Partial" means the mechanism is stated but the sequence, or the fixture the finding asked for, still gets through.
+
+| # | Finding | Status | Why, in one line |
+|---|---|---|---|
+| 1 | Instance-token route around step-up | **Partial** | Own record and "call 2 cannot write membership" close the route; but gate 5 has no upsert/revoke/delayed-replay-under-instance-token fixture, which the finding asked for, and the drain-seeded first owner needs a person ref the drain cannot mint without the pepper (N6). |
+| 2 | Compromised-home bound false | **Partial** | Ruling 13, §5.5 and §9.4 say the right thing. But the false sentence lives in **home §2 itself** — "a compromised directory grants nothing. A compromised home costs the graph and a convenience outage. It does not cost a document." — and A.1 leaves it standing. The doctrine also misattributes: it says revision 1 widened home §2's *directory* sentence to *home*; home §2 says both, so the widening is the home spec's, not revision 1's. |
+| 3 | No provenance contract on writes | **Partial** | Envelope shape and the refuse-unverifiable rule are real. But the check "still authorize the *asker*" cannot run on an S3 fetch (no asker) or a link view (no roster subject), "loses that passage" is undefined once a person has edited a CRDT body, and STALE on a saved answer is unstated. N2, N3. |
+| 4 | Commit protocol and sender-side check | **Closed** | Receipt/commit split, body fixed server-side, single redemption, browser and person binding, destination checked before render, gate 7 through the real middleware. |
+| 5 | Failed refetch and rewind | **Partial** | Failed refetch has a rule, but it keeps serving admitted sessions (30-day framework default, sliding renewal), so the removed member the paragraph cites keeps access anyway (N5). The rewind rule is defeated by epoch-number reuse after a restore (N4). No time bound on the TTL, still "measured later". |
+| 6 | Copies retained after visibility change | **Partial** | Ruling 14 and the rewritten §0 are right in shape. But §0 promises retirement "within one feed pull plus its stated retention (§4.2)" and §4.2 states no retention; and memoria's gate 8 circuit breaker purges nothing on a wholesale purge — the exact case the finding named — so the honest instance does not retire within one pull (N7). |
+| 7 | Gate 5 silently offboards guests | **Closed** | "Holding no other grant" and the guest fixture. |
+| 8 | "Costs a correct consumer nothing" | **Partial** | Cost paragraph corrected. The transition fixture is mentioned in a parenthetical and appears in no gate. "Custody denial must dominate revision diagnostics in S2 and S6 (`STALE(cell)`, `NON_NUMERIC`)" is not addressed anywhere (N10). |
+| 9 | Hand-off needs middleware changes | **Closed** | §7.1's bill and A.2 match what is pinned (`form-action 'self'` at rastrillo v0.23.0 `serve.go:399`; `csrf.Protect` at Docs `app.go:412`). |
+| 10 | Call 8 paging | **Partial** | Epoch-bound cursor, staging, refusal and "empty is not nobody" are stated. The wire example is still `GET /roster?team=&epoch=` → `{epoch, members}` with no cursor, cap or completion field, and gate 5 has no concurrent-change or failed-middle-page fixture. |
+| 11 | Source-behind revision | **Partial** | `UNKNOWN` is the right value and A.4 carries it. No forward/backward revision fixture in §11, which the finding asked for. |
+| 12 | Epoch claim and home §5.4 | **Closed** | Own exception, argued from "the instance decides nothing from it", recorded in A.1. |
+| 13 | Leak list omissions | **Closed** | §9 items 8–10, gate 8 rewritten. |
+| 14 | Amendments leave contradictions | **Partial** | A.2's shape sentence, A.4's §11/fixtures/decision 7 are fixed. A.3 still misses Sheets `PRODUCT.md` **principle 3** (line 170: "Never design a flow that implies data can move to another instance"), which is the sentence memoria §13's blocker names — so A.4's "close the blocker against A.1 to A.3" cannot close it. A.1 misses home §2, §2.1 and §10 (N6). |
+| 15 | Two positions misattributed | **Closed** | Docs §11.2 read correctly as "ships customer documents to a third party"; Sheets `AGENTS.md` kept. |
+| 16 | Citation currency | **Closed** | Any normative change bumps; revision checked against commit. |
+
+Closed 7, partial 9, open 0. The status block's "Revision 3 answers all sixteen" is the same kind of claim GPT-6 flagged in revision 2's "no new machinery": true of the citations, not of the failure sequences.
+
+## 3. New findings, by severity
+
+P1: a trust boundary or promised property is broken, or a normative rule cannot be built as stated. P2: a contract, adoption or verification defect. P3: accuracy.
+
+### N1. P1 — Ruling 15's "edit" link and §7.3 contradict a Paul ruling in Sheets and Docs §8.2, and cite neither
+
+**What is wrong.** Sheets §1 ruling 12: *"Link sharing is viewer-or-commenter, never editor"*, rejected alternative *"Anonymous link editing"*, **Paul, 2026-08-30**. Sheets §8.3: an anonymous editor "would write unattributed rows into version history". Docs §8.2: *"Link shares are a per-document capability, viewer or commenter, never editor. Anything that writes is attributed, and attribution is what mints membership."* Doctrine ruling 15 makes a link "view or edit", and §7.3 builds the actor Sheets rejected by name — *"the honest actor is the link capability itself"* — and calls it a satisfaction of ruling 9. §7.3 cites neither spec; A.2 and A.3 amend neither §8.2 nor §8.3.
+
+**Why it matters.** This is not a drift between restatements; it is a new ruling overturning a settled one without saying so, in the document whose one rule is that a ruling lives once. The moment Docs adopts A.2, Docs §8.2 and suite §7.3 disagree and gate 9 has nothing to say about it. It also weakens ruling 9: "a write names its real actor" becomes "a write names a bearer token", which is what ruling 9's rejected column calls a write capability without an identity.
+
+**The product requirement does not need it.** "Anyone with the link can edit" is already how Docs builds its commenter link: the link is a capability to *obtain* a grant — the holder signs in, attribution mints a guest member, and from then on they are on the `granted` rung with a person ref. That gives Paul all four product states, keeps every write attributed to a person, keeps S2's asker a roster subject with no special case, and deletes §7.3 and leak item 11's "the app cannot name them" — because it can, after the first write.
+
+**Confidence:** high that the contradiction is real and uncited. High that the sign-in-mints-guest shape satisfies the requirement; whether Paul wants anonymous editing anyway is his call, in which case ruling 15 must name Sheets ruling 12 as overturned and A.3 must amend it.
+
+**Smallest correction.** Ruling 15: "view, or edit after sign-in — an edit link is a capability to obtain a guest grant (Docs §8.2, Sheets §8.3), never an anonymous actor." Delete §7.3. Leak item 11 loses "or writes".
+
+### N2. P1 — The provenance envelope has no rule for a viewer who is not a roster subject, which makes a link on a Doc a link on the Sheet it quotes
+
+**What is wrong.** §7: on every disclosure path the receiver "checks that the sources still authorize the asker, through S2 to each app of record". A link holder has no asker subject (§3, §7.3). So for a Doc carrying an envelope, viewed through a link, the check cannot be made — and the doctrine says nothing about what happens then.
+
+**Why.** Ana saves a memoria answer quoting a team-visible Sheet into a Doc, then link-shares the Doc to a contractor. If Docs shows the Doc, the contractor reads Sheets content nobody granted; the link has laundered the cell through one hop, which is the exact thing §4.3's transitivity rule forbids. If Docs refuses, link-sharing a Doc containing any saved answer silently stops working, and nothing says so. Guests are half-covered by accident: S2 to Sheets for a Docs-only guest returns `NOT_VISIBLE`, so the passage is withheld — but the doctrine should say that too, because it is a product surprise ("my guest cannot see the table I pasted").
+
+**Confidence:** high.
+
+**Smallest correction.** One rule in §7: an item carrying an envelope discloses envelope-derived passages only to an asker every source can be asked about; to anyone else — link holders, and under N1 any un-signed-in visitor — those passages are withheld loudly and the rest of the item renders. One fixture in gate 6b: link view of the item from gate 6b's own scenario, expect the passage withheld.
+
+### N3. P1 — §7's check cannot run on an S3 fetch, so gate 6b cannot be written as stated
+
+**What is wrong.** §7 lists "an onward S3 fetch" among the disclosure paths that check "the asker". S3 carries no person (§6, memoria §6.1). Gate 6b asserts "its S3 answer … lose[s] that passage and name[s] the app" — S3 answers a body or a 404 (memoria §6.1) and has no representation for "withheld loudly, app named". And there is no person-less call by which B can ask A whether a source is still team-visible.
+
+**Why.** §11's first line: a gate that cannot be written as stated is a spec bug. This one cannot. Worse, the thing that *would* work is already in the contract and unnamed: S3 itself is the custody probe — B fetches `(id, revision)` from A; 200 means team-visible, 404 means not.
+
+**Confidence:** high.
+
+**Smallest correction.** In §7, split the disclosure paths by what they carry: per-person paths (read, export, activity) check S2 for the asker; the custody path (S3) serves envelope-derived passages only for sources B has probed team-visible at the app of record, and strips the rest silently — S3 is custody-only and must not become loud, or gate 3's "404, never 403" gains a third status. Gate 6b's "revoke the source in A" becomes two fixtures: "take the source below the `team` rung" (S3 loses it) and "revoke the asker's grant while the source stays `team`" (read and export lose it; S3 does not).
+
+Two adjacent gaps in the same rule, both P2 on their own: **what "loses that passage" means once Ana has edited the saved answer** — a CRDT body has no separable passage after the first edit, so either the saved answer is an immutable quoted block that is withheld as a unit, or provenance attaches to the whole item and the whole Doc is withheld, or the rule stops at a person's deliberate act; the doctrine must choose. And **what `STALE` does to a saved answer**: under memoria §4.1 any edit to the Sheet makes the saved revision stale, memoria regenerates but Docs cannot, so a saved answer decays on the source's first edit; say so, or say Docs treats `STALE` as `VISIBLE`-with-a-notice, which is a different rule from memoria's.
+
+### N4. P1 — The rewind rule is defeated by epoch-number reuse, and a home that loses `memberships` has no rebuild path
+
+**What is wrong.** §5.3: an instance refuses an epoch below the one it holds "until the home passes its own high-water mark". The home does not know the high-water mark — it was in the backup it lost. Sequence: instance holds 11; home restored at 10; owner adds someone → home is at 11 with a *different* set; the instance sees an equal epoch and, per §5.3, does not refetch; the next write → 12 → accepted, and everyone removed between old-10 and old-11 is readmitted. Finding 5's sequence, delayed by two writes.
+
+**Second half.** Home §10: *"Losing this database loses credentials, not access. The directory rebuilds as instances re-upsert."* Under ruling 12 the roster does not rebuild — call 2 cannot write membership, no channel runs home→instance, and instances hold the only copies. A home that loses `memberships` loses every team's roster suite-wide, and with no owner in the table nobody can write it. A.1 does not amend §10's sentence, and no recovery is written.
+
+**Confidence:** high on the collision; medium on the fix shape.
+
+**Smallest correction.** Epoch is `(generation, counter)`, the generation minted at each home restore; an instance seeing a generation it has not seen treats the home as unavailable until an operator verb re-seeds `memberships` and marks the generation trusted. Write the re-seed into home gate 8's restore drill (each instance's call-8 copy is a candidate source, which is a reason to keep those copies rather than treat them as disposable). Amend home §10 in A.1.
+
+### N5. P1 — The stale-roster rule pays availability for a property it does not buy
+
+**What is wrong.** §5.3: past the TTL with no successful fetch, the instance "keeps serving people it has already admitted for the length of their session" — then says the alternative it rejected is "an unreachable home lets a removed member keep their access for as long as the outage lasts". Those are the same thing for anyone already signed in. Instance sessions are rastrillo's 30-day default (memoria §13 says so of memoria; nothing in Docs or Sheets overrides it) and home §2.1 asks for *sliding* renewal, so "the length of their session" is unbounded during an outage. §5.4's promise — removed "within one epoch, and on an instance that admits nobody, within the roster TTL" — has an exception the stale rule creates and does not name.
+
+**What the rule does cost:** during any home outage longer than the TTL, every instance answers `UNKNOWN` on S2, so memoria withholds every sibling passage for every member for the outage, and no one new signs in — which the home outage already prevents (home §2.1), so that half buys nothing either.
+
+**Confidence:** high.
+
+**Smallest correction.** Either refuse team-standing-based requests for *all* sessions past the TTL (which is what "costs availability on purpose" already claims), or state that existing sessions are bounded by the instance session TTL, put a number on that TTL in the contract module with its reason, and write the exception into §5.4. Either way the cost paragraph should stop claiming the rejected alternative is different from the chosen one.
+
+### N6. P2 — Ruling 12 makes the home hold and enforce a person-level entitlement, and A.1 does not amend the sentences that forbid it
+
+**What is wrong.** Both factual claims in ruling 12 are true against trunk `2b68301`: home §6 says "`kind` is member or guest", §10's `directory` table is `person ref, app, team sqid, host, kind`, and §7's sentence is quoted accurately (from the fast-path paragraph). But the new table's *writer* rule — "written only by an owner" — is the home deciding "may this person write the roster", and home ruling 19 says the home answers "never 'may this person'", home §7 says "It never forms an opinion about whether a person is entitled to anything", and home §2.2's "what you may *do* is the instance's business" is the stated ground for home ruling 15 rejecting a per-team 2FA floor. §5.2's "is this person on this team is a fact about its records" does not cover "may this person add a member". A.1 amends §7's "never learns" sentence and nothing else in that argument.
+
+Also unamended by A.1, each now false or stale:
+
+- Home §2 bullet 1 (finding 2's sentence; see the table).
+- Home §2.1 "Losing it costs a convenience, not access — for the directory": under §5.3, a home outage past the TTL now costs access at every instance.
+- Home §10 "loses credentials, not access" (N4).
+- "Complete ruling 18's pending change of *never renders* to *never stores*" — landed at `2b68301`; A.1 asks for a change that is done.
+- §5.3 says call-2 and call-5 responses carry the roster epoch; A.1 only adds the assertion claim and call 8.
+- The seeded first owner arrives as a **sealed address** (home §7); `memberships` keys on a person ref; minting a person stub needs the blind index and therefore the pepper (home ruling 4: the home hashes). The drain runs outside the home process (§7). Who hashes is unstated.
+- "Move *step-up for instances* to built-when" — home §14's row is an instance-side `RequireFresh` round trip; the roster write is a directory-origin flow. Defensible since the directory obtains sessions like a consumer (§4), but A.1 should say that is the reading.
+
+**Confidence:** high on every item; they are literal.
+
+**Smallest correction.** A.1 lists ruling 19's second half, §7's "never forms an opinion", §2's bullet 1, §2.1's line, and §10's sentence as amended, states who hashes the seeded owner's address, and drops the already-landed item.
+
+### N7. P2 — §0's retirement promise cites a retention that is stated nowhere and is false for memoria's wholesale case
+
+**What is wrong.** §0: an honest instance retires "within one feed pull plus its stated retention (§4.2)". §4.2 states no retention; S1's per-consumer TTL is the *feed's* retention, not the consumer's purge delay. Ruling 14: "what it must *retire* is stated and timed" — neither. And memoria gate 8: "a wholesale `NOT_VISIBLE` or 404 response states an anomaly and purges nothing"; §5.1's enumeration diff "deletes nothing on a timeout … or partial page". A team folder made private produces a wholesale `gone` in one pull; whether the breaker fires on `gone` rows is unstated, and if it does the honest instance retires nothing — the case finding 6 named explicitly.
+
+**Confidence:** medium-high; it turns on memoria's breaker semantics, which the doctrine adopts by citation.
+
+**Smallest correction.** §4.2 names the bound (one S1 pull plus a purge delay with its reason in the module). A sentence that `gone` rows are applied even when wholesale — the breaker is for S2/S3 anomalies, not for the feed — or the promise in §0 shrinks to what the breaker allows.
+
+### N8. P2 — The ladder's `team` predicate has no rung for "team minus exclusions", which Sheets already has
+
+**What is wrong.** §4.2's predicate is "readable by every roster member by virtue of team standing alone". Sheets §4.5's join is "rows this team can see, plus rows granted to me individually, **minus rows I have been removed from**". A team-visible sheet with one member excluded is readable by every member but one. Under the predicate as written it is not `team`, so custody excludes it and the whole team loses it in memoria because one person was removed; or Sheets' predicate function calls it `team` and the exclusion is enforced by S2 — which is what the two-rule design intends, and is what the doctrine should say. Same question for calendar, whose team calendar is "a grant everyone holds" (§2), which reads as `granted` under the table.
+
+**Confidence:** high that it is unstated; the intended answer is clear.
+
+**Smallest correction.** Predicate: "team standing alone suffices for a member who has not been individually excluded; exclusions are S2's business, not custody's". A.5 says calendar's `Kind = team` maps to the `team` rung.
+
+### N9. P2 — Link changes and the feed are unstated
+
+The brief asked. Under ruling 15 the coherent answer is that a link created or revoked on a `team` item is invisible to S1 — not `gone`, not a revision — and a link on any other rung was never announced. The doctrine does not say it, and the leak is real if an app bumps `touched_at` or `revision` on link creation: S1 carries no visibility flag by design, and a metadata bump on link creation is "this team item was just link-shared" through the side door. One sentence in §4.2; one assertion in gate 3 (create and revoke a link on the `team` fixture, expect no feed row).
+
+### N10. P2 — S6's diagnostic values can reopen the existence distinction (finding 8's second half)
+
+S6 returns `STALE(cell)`, `NON_NUMERIC(cell)`, `MIXED_UNITS` (memoria §6.1). §6's S6 row says "the custody intersection, as S2" but nothing says `NOT_VISIBLE` dominates every other value, and gate 3 covers only S2. A cell in a `private` sheet answering `NON_NUMERIC` tells a sibling the sheet exists. One sentence in §4.3; one S6 fixture in gate 3.
+
+### N11. P2 — Four memoria citations resolve only in an unmerged worktree
+
+"memoria decision 13, Paul, 2026-09-05" (§8), "Decisions 13 and 14 stand" (A.4), "memoria §8.4, gate 26" (§6's S2 row), and leak item 7's "memoria §3.2" rendered-page entry all exist in `memoria/.claude/worktrees/tensorx-provider` and not on memoria's main checkout, which has 21 gates and decisions to 12. The citations are accurate to that worktree — gate 26 is "every passage id in a model prompt is one S2 returned `VISIBLE` for in this same request" — but the doctrine presents them as memoria's spec. Gate 9 checks app→doctrine currency; nothing checks doctrine→app. Smallest: a sources table like `CODEX-FINDINGS.md`'s, naming the commit read per sibling and marking these as pending merge.
+
+### N12. P2 — Gates that still cannot be written as stated
+
+- **Gate 5** "after one epoch" still has no observation condition (GPT-6's words, unchanged). Writable as "after the instance's next call-8, driven by a differing claim on the fake clock". No fixture for finding 1's instance-token replay, finding 10's failed middle page, or finding 8's transition.
+- **Gate 6b**: N3.
+- **Gate 7** "the absent and `null` origin cases decided explicitly" is not an assertion; say refused.
+- **§7.3 has no gate at all** (moot under N1).
+- **§11's preamble** says gates live in the contract module; gates 5, 8 and 9 cannot — 8 is a home-repo database test, 9 is a script over five repos. Say which gates are fixtures and which are app-side obligations.
+
+### N13. P3 — `UNKNOWN` now has four meanings and no reason code
+
+Unsupported subject kind (§3, whole call), sibling past deadline (§6.3), source-behind revision (§4.3, per item), stale roster (§5.3, scope unstated). The consumer's honest move differs — re-ingest, retry, nothing. An additive `reason` on the wire. And say stale-roster `UNKNOWN` is whole-call: per-item, a stale instance would answer `NOT_VISIBLE` for private and deleted items and `UNKNOWN` for team ones, and gate 3's byte-for-byte promise should hold during staleness too.
+
+### N14. P3 — Small mis-citations, mostly carried from revision 2
+
+- §7.1: "Home call 6, whose shape this borrows, carries two protections (home §5.6)". §5.6 is the sign-in redirect protocol; call 6 is §8. The protections are real, the section is wrong.
+- §6.3: "a consumer pre-warms on its own wake (memoria §6.5, activity design §6)". Memoria §6.5 does; activity §6 has a minimum pull interval and no pre-warm.
+- §5.6: "Docs has none yet". Docs §8.2 has viewer and commenter for links, and §7.4 comments.
+- Ruling 13's rejected column, on home §2: see finding 2.
+
+## 4. Verdict
+
+**Request changes.** N1 through N5 are each a trust-boundary or build-as-stated defect in text no reviewer has seen, and N1 contradicts a Paul ruling in a sibling repo without naming it. Nine of the sixteen prior findings are partial, mostly where a mechanism is stated and the fixture the finding asked for is not.
+
+Ruling 15 in particular should not go to Paul as written: the sign-in-mints-guest shape satisfies his four states and needs no new actor, and he ruled against anonymous editing on 2026-08-30. If he wants to reverse that, the row must say so.
+
+## 5. Worse than revision 2
+
+1. **A new contradiction with a settled ruling.** Revision 2 had no link rung and was consistent with Sheets ruling 12 and Docs §8.2. Revision 3's §7.3 builds the alternative that ruling rejected by name, and the document whose one rule is "cite, never restate" does not cite it (N1).
+2. **A wrong rule where there was a gap.** Revision 2 was silent on provenance across a write; revision 3 states a rule that names an asker on a person-less call and lists disclosure paths one of which cannot perform it (N3). Under §10, A.2 will carry that rule into Docs as a citation, so it propagates rather than drifts.
+3. **A lockout mode where there was lag.** Revision 2's stale roster was lag. Revision 3's is refusal below a high-water mark the home cannot observe, exited by a condition number reuse satisfies falsely (N4), plus suite-wide `UNKNOWN` on S2 during any outage past the TTL for a property the same paragraph gives back (N5).
+4. **A promise in §0 with a citation to nothing.** Revision 2's §0 was instantaneous and false. Revision 3's is bounded by "its stated retention (§4.2)", and §4.2 states none (N7). A false absolute is at least legible; a bounded promise pointing at an empty section reads as kept.
+5. **The same overclaim GPT-6 caught, one revision later.** "Revision 3 answers all sixteen" in the status block, §12 step 0, §14 and the README, where nine are partial. Revision 2 said "no new machinery"; the doctrine now records that as a defect and repeats its shape.
+
+Nothing in revision 3 is worse than revision 2 on the twelve findings answered in place except where noted above; the hand-off (§7.1), the epoch claim (§5.3), the leak list (§9) and the Docs §11.2 reading (§8) are clean improvements.
+
+---
+
+### Appendix — gates, refreshed
+
+| Gate | Writable as stated? |
+|---|---|
+| 1 Fixture conformance | Yes; call 8's paged shape and S8's write shape still need golden fixtures before it means anything. |
+| 2 S2 total decoder | Yes. |
+| 3 Custody per rung | Yes for S3 and S2; missing S6 (N10), the link-change no-feed-row assertion (N9), and finding 8's transition. |
+| 4 No cross-team dial | Yes. The hand-off destination check is a different code path (form render, not the client); fine to share a gate, say so. |
+| 5 Roster | Not yet: "after one epoch" needs an observation; no instance-token replay, no failed-middle-page, no existing-session case for the stale rule (N5), no generation-change case (N4). |
+| 6 Write-as-self | Yes. |
+| 6b Provenance | No: the S3 clause (N3); no link-view case (N2); "revoke" ambiguous between rung change and grant change. |
+| 7 Hand-off | Yes, once "decided explicitly" becomes "refused". |
+| 8 Publish the database | Yes, in the home repo, not the contract module. |
+| 9 Citation currency | Yes for the arithmetic and commit check; "any normative change" is a judgment the script cannot make, which is fine if the doctrine says a reviewer makes it. |
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3b5297a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# 🤖 suite
+
+The Oficina suite's shared repository: the sharing doctrine that binds
+every app, and the contract module they implement against.
+
+The suite is a consolidation. Home, Docs, Sheets, memoria and calendar
+each keep their own repository, and mail and files arrive later as
+wrappers around keymail and slopbox. What they share lives here so no app
+owns the rule that binds the others.
+
+## What is here
+
+- `docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`: the
+ doctrine. The team is the boundary. Inside it the apps are one thing;
+ across it, sharing is membership and nothing moves. Read §1 first.
+- The contract module, `amadan.net/oficina/suite`: types, golden
+ fixtures and a thin client for the calls apps make to each other and to
+ the home. Not written yet; the doctrine's §12 says when.
+
+## How an app uses it
+
+An app spec cites the doctrine and lists the rulings it consumes. It never
+restates one. An app imports the module, serves its server half, and runs
+the conformance suite in CI against the version it pins.
+
+## Status
+
+The doctrine is a design, revision 4. Three review passes have run
+against it, by two models, neither of which wrote the draft: Claude Opus 5
+on revision 1, then GPT-6 via Codex on revision 2, then GPT-6 and Fable
+independently on revision 3. The last two agreed closely and both asked
+for changes. All three reports sit beside the doctrine, unedited.
+
+Revision 4 fixes what those reviews found most seriously wrong and says,
+in its status block, what it has deliberately left for later. It has not
+itself been reviewed. §12 step 1 waits.
diff --git a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
new file mode 100644
index 0000000..fda9dc5
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
@@ -0,0 +1,1341 @@
+# 🤖 The Oficina suite — sharing doctrine
+
+How the apps of the suite share identity, membership and content with
+each other, and where the line is that nothing crosses.
+
+**Status:** design, **revision 4**, 2026-09-05. Drafted by the suite
+session (Fable 5.1) from a brainstorm with Paul the same day. Every ruling
+in §1 attributed to Paul was given in that conversation, in an earlier
+spec named in the row, or in the sessions that produced revisions 3 and 4.
+
+**Three review passes, by two models, none of which wrote the draft.**
+Claude Opus 5 on revision 1: six defects. GPT-6 via Codex on revision 2:
+*request changes*, sixteen findings. GPT-6 and Fable 5.1 independently on
+revision 3: *request changes* from both, agreeing closely — 6 and 7 of the
+sixteen closed respectively, none still fully open, plus twelve and five
+new findings. All three reports are beside this file, unedited:
+`CODEX-FINDINGS.md`, `CODEX-FINDINGS-R3.md`, `FABLE-FINDINGS-R3.md`.
+
+**Revision 4 does not claim to answer everything, because revisions 2 and
+3 both made that claim and both were caught.** What it fixes:
+
+- **Ruling 15 is now Paul's and it cites Sheets ruling 12.** Link sharing
+ is read-only; an "edit link" is a capability to obtain a guest grant at
+ sign-in, so every write still has a named actor. Revision 3 proposed an
+ anonymous link editor — the actor Paul had already rejected on
+ 2026-08-30 — without citing the ruling that rejected it. §7.3 is
+ deleted. Recorded plainly because it is the failure §2 diagnoses and
+ §10 exists to prevent, committed by this document.
+- **The provenance envelope splits by path** (§7). Revision 3 required an
+ asker on S3 and on a link read, neither of which has one. Person-less
+ paths check custody; only person paths check S2. Withholding is
+ whole-artefact, since no envelope can map a synthesis back to the source
+ of a sentence.
+- **A stale roster stops serving existing sessions** (§5.3), which is what
+ revision 3 said it was doing and was not.
+- **A restored home mints a new roster generation** (§5.3). Refusing a
+ lower epoch was not enough: a restore that reissues the same numbers
+ catches up in two writes.
+- **§5.5 stops denying what ruling 13 concedes.** A compromised credential
+ origin reads and writes as whoever it signs in as, and no instance-side
+ check distinguishes it. Revision 3 conceded that and then wrote the
+ reassuring clause back two paragraphs later.
+- **Ruling 12's rejected column drops its impossibility claim** and keeps
+ the three facts verified against home §§6 and 7.
+
+**Openly not answered, and deliberately left for iteration** (Paul,
+2026-09-05): call 8's wire encoding and completion marker; result
+precedence when a stale roster and a non-custodial item collide; the
+activity path's cached authorization after revocation; pinning sibling
+baselines, since four memoria citations resolve only on an unmerged
+worktree and A.1's ruling number may collide with an existing home ruling
+22; and most of the fixtures the gates now describe. These are recorded
+here rather than in a passing sentence so the next reader can see the
+edge of the work.
+
+**Revision 4 has not been reviewed.**
+
+**Scope:** the rules that bind every app in the suite when it talks to
+another app or to the home. Consumed by `oficina/home`, `oficina/docs`,
+`oficina/sheets`, `oficina/memoria`, `oficina/calendar`, and any wrapper
+that later joins (mail via keymail, files via slopbox).
+**Reads on top of** `carlos/pegamento:docs/doctrine.md`. Where it
+diverges, §14 says so and names the filing.
+
+---
+
+## 0. The one rule
+
+> **The team is the boundary. Inside it, the apps are one thing. Across
+> it, sharing is membership and nothing moves.**
+
+The falsifiable form, in two halves:
+
+- **Inside.** Take a team's instances of every app. Publish what any one of
+ them holds and the worst anyone learns is what that team could read in
+ the owning app **at the moment the copy was taken**. An honest instance
+ retires a copy that has left team-visibility within one feed pull plus
+ **the retention its own spec states** — memoria §7.4 holds that number
+ today, and a consumer that states none is not conformant. §4.2 carries
+ the rule and deliberately carries no number, because the number is a
+ property of the consumer's storage and not of this document. A
+ compromised consumer never retires anything.
+- **Across.** Take two teams. **There is no path the suite provides** by
+ which a byte of one team's content reaches an instance of the other. A
+ person in both teams reaches both through the home and carries nothing
+ between them. A person who exports a file and uploads it by hand has
+ left the paths the suite provides, and nothing here prevents that.
+
+**"At the moment the copy was taken" is the design, not a hedge.** Custody
+is bounded at acquisition and retired by the feed; it is never enforced by
+erasure, because no app can erase from another app's process — which is
+precisely why ruling 1 put the boundary at the team and not at the
+process. §9 item 7 already concedes this one layer up, for a page a
+browser has rendered. This is the same sentence for bytes at rest.
+
+Revision 1 stated the test instantaneously. That was false for the window
+between a source privatising an item and the consumer pulling its `gone`
+row, and false permanently against a consumer that has been broken into.
+The home made this same correction on its own §0: *"never stores your
+address"* was a claim the schema could not keep, so it became *"never
+stores a **readable** address"* with the window carried in its §3.2. The
+precedent was in the family and this document did not follow it. Ruling
+14.
+
+Pegamento §1 says the app declares its own boundary. The suite is the app
+(memoria decision 1, Paul, 2026-09-03), and it declares the team. The
+process is not the boundary and never was: it was where the boundary
+happened to fall while every CARLOS app was one process.
+
+---
+
+## 1. Settled decisions (don't relitigate casually)
+
+| # | Ruling | Rejected | Who, when |
+|---|---|---|---|
+| 1 | **The team is the boundary. An instance is a process.** A team's instances across every app are one trust domain at `<team>.<app>.oficina.works`. | One boundary per process, which left memoria holding person-initiated snapshots and made the home's activity feed a federation question | Paul, 2026-09-03 (memoria decision 1); reaffirmed suite-wide 2026-09-05 |
+| 2 | **Sharing is membership across teams, unchanged.** No data crosses a team label. A guest is a scoped member of the granting team. A link share moves nothing. | Any federation story; a "copy to my team" verb | Pegamento §3, applied |
+| 3 | **Custody and authorization are two rules.** Custody bounds what a sibling may *hold*: team-visible items only. Authorization is what a *person* may read: asked of the app of record, per person, per query, **never cached**. Team standing is not authorization; it is the roster, a pulled copy refreshed by epoch (§5). A copy is never an authority. | One ACL copied between apps; "is a member" as the only question | Paul, 2026-09-03 (memoria decision 3); generalised 2026-09-05 |
+| 4 | **The home owns identity and the team, including the membership roster. It never owns a per-item grant.** | Per-instance rosters as the record, reconciled by asking siblings; the home holding document-level grants | Paul, 2026-09-05 |
+| 5 | **Two shapes, one switch.** A *lone* instance is its own identity authority and its own roster. A *suite* instance takes both from the home. Membership authority follows identity authority; an instance never has one without the other. | A suite instance half-trusting its local table; a lone instance dialling a home for a roster it does not need | Paul, 2026-08-31 (Docs §8.1, two shapes); extended to the roster 2026-09-05 |
+| 6 | **Egress is one suite sentence.** Egress that is a property of the product is forbidden. Egress that is per-team opt-in, to one named recipient, disclosed, with a working floor when off, is admissible. | Two contradicting texts, Docs §11.2 and memoria §8.3 | Paul, 2026-09-03 (memoria decision 4); made suite-wide 2026-09-05 |
+| 7 | **One contract, one Go module, both halves, fixtures as the interface.** Every app imports the module, serves its server half, and passes the conformance suite in CI. An app spec cites the doctrine and lists consumed rulings. It never restates one. | Each spec restating the contract in its own words, which is what drifted | Paul, 2026-09-05 |
+| 8 | **Two credentials, chosen by what the call proves.** A home-minted assertion proves a person. A consumer bearer token proves a same-team sibling. Custody calls carry the token and no asker. Per-person reads from a sibling carry the token and a named asker. | A token exchange putting the home in the loop of every search; one credential for everything | This session, 2026-09-05 |
+| 9 | **A write names its real actor. No sibling ever acts as a person.** A person writes through their own session, carried by their browser. A sibling writes as itself, into an area the team owner designated for it, touching only what it authored. | On-behalf-of writes, where a sibling token becomes a write capability over every member's identity; browser-carried only, which forbids unattended writes | Paul, 2026-09-05 |
+| 10 | **Separate repos. One shared repo for the doctrine and the contract.** The suite is a consolidation: mail and files arrive as wrappers around keymail and slopbox, so the contract must cross repository boundaries on day one. | A core monorepo for home, docs, sheets, memoria and estilo, which buys atomic spec edits for four apps and loses the property the day mail joins | Paul, 2026-09-05 |
+| 11 | **Activity is pulled at render and held nowhere at rest** — `oficina/home` ruling 18 and the `design-activity-pull` spec, adopted as suite contract call 7. | Push into the home; a contentless heat map | Paul, 2026-09-05 (home ruling 18) |
+| 12 | **Membership is its own record at the home, written only by an owner.** `(team, person, role)`, written at the directory origin behind step-up, or seeded by the drain with the team's first owner. A directory row says where a person has things; it never grants or revokes team standing. Call 2 cannot write membership and call 8 reads only the membership record. | Deriving the roster from call-2 directory rows, which revision 1 chose. The row has no owner role to project (home §6: `kind` is member or guest; home §7: who counts as an owner "is the instance's business… and the home never learns the answer"); it is scoped per (team, app) where the roster is per team, so a projection can express "member of acme.docs but not acme.sheets", which ruling 1 forbids; and any instance writes it under a consumer token, so deriving from it routes around §5.4's step-up. Those three are verified against home §§6 and 7; whether they are together *unbuildable* is a stronger claim than the evidence carries, and revision 3 made it | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 1) |
+| 13 | **A compromised home is three compromises and each is conceded on its own.** A leaked database reveals the graph, the roster, and who owns each team. A compromised directory origin can add a member, and a member reads everything team-visible in every instance of that team. A compromised credential origin can sign in as anyone, owners included, and no instance can tell. | One undifferentiated "compromised home" that "cannot read a document", which took home §2's careful *directory* and widened it to *home*, and counted as evidence what only honest code leaves behind | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 2) |
+| 14 | **Custody is bounded at acquisition and retired by the feed. It is not enforced by erasure.** What an instance may *acquire* is team-visible at the time; what it may *render* is live-authorized (ruling 3); what it must *retire* is stated and timed. Three rules, not one. | §0's inside test stated instantaneously; source-driven tombstone push (home §6: no channel reaches a hibernating instance); sealing pulled content under a source-held key, which buys nothing once a consumer has indexed, since an embedding is content in another costume (memoria §3.1) | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 6) |
+| 15 | **Three rungs and a read-only link.** An item is *private*, *granted* to named people, or *team*. Orthogonally it may carry a **read-only** link capability — view, or comment where the app has that role. **Only the `team` rung is team-visible**, so custody keys on the rung and never on the link. **Every write has a named actor**: a member, or a guest who signed in and took a grant. | Treating a link as team-visible because "anyone can read it", which turns a capability held by whoever has the URL into a listing every team member finds by search. **And anonymous link editing**, which Sheets ruling 12 rejected on 2026-08-30 and which revision 3 rebuilt without citing it. An "edit link" is a capability to *obtain a guest grant at sign-in* — the `granted` rung, and how Docs already builds its commenter link | Paul, 2026-09-05, confirming Sheets ruling 12 |
+
+---
+
+## 2. What the collision was
+
+Nothing inter-app is built. The home is a skeleton. memoria's slice 1 is
+blocked. The activity pull is an unmerged worktree. So this is a spec
+alignment, not a migration, and it is cheap now and expensive in a month.
+
+The collision was never five apps disagreeing about the boundary. It was
+five specs each restating the same rules in their own words:
+
+- Home §2.2, Sheets PRODUCT.md principle 2 and Docs §8.2 each say data
+ never crosses an *instance* boundary. memoria decision 1 says the
+ boundary is the *team*. All four were written in good faith from the
+ same Pegamento sentence, and the word "instance" carried a process
+ assumption nobody had examined.
+- Docs §9 restated the home's five-call contract and was written against
+ a superseded draft. The home spec now has six calls and the activity
+ design a seventh.
+- Docs §11.2 and memoria §8.3 give opposite verdicts on the same egress,
+ and memoria could only ask Docs to change (`oficina/docs` discussion 33)
+ because Sheets AGENTS.md's rule that a contradiction is fixed in the
+ same commit cannot be kept across repositories.
+- Four team models exist: the home's suite-wide `teams` table (ruling 17),
+ Sheets' owners list plus `TeamRole`, Docs' allowed-addresses list, and
+ calendar's `Kind = "team"` calendar everyone holds a grant on. memoria
+ decision 11 works around there being no roster of record by asking
+ every sibling and admitting on any yes.
+
+Restating is the defect. Co-location would not have fixed it; citation
+does. §10 is the mechanism.
+
+---
+
+## 3. Two shapes
+
+An instance is provisioned in exactly one shape and the shape is not a
+runtime mode.
+
+**Lone.** One team, self-hosted, no home. The instance signs people in
+itself with rastrillo's magic link, keeps its own owners list and member
+rows, and never dials a home. This is Docs §8.1's ruling and Factor VII's
+requirement, not a degraded tier. Seapointish is the family precedent:
+"none" is a supported answer to "what glue does this app need".
+
+**Suite.** The instance accepts home assertions (home §5.3) and pulls its
+roster from the home (§5).
+
+The switch is the presence of a home consumer token in platform config.
+With it, the instance is a suite instance in every respect. Without it, a
+lone one in every respect. Ruling 5's reason: an instance that trusted the
+home for identity and its own table for membership would have two rosters
+of record, and the test in §0 could not be written for it.
+
+**Sibling calls are a separate switch**, and it is independent of shape.
+An instance serves and makes the sibling contract (§6) when sibling
+consumer tokens are in its platform config, which the drain mints without
+the home's involvement (memoria §6.2). So a self-hoster running Sheets and
+memoria with no home gets the two talking, and a suite instance always
+may. Two instances that talk were provisioned together and share a shape,
+which is what lets S2's named asker mean one thing: **the subject the
+receiving instance keys its members on**, a person ref in suite shape and
+a lowercased address in lone shape. A pair of lone instances has two
+rosters, one per instance, and that is what lone means; custody and
+authorization work unchanged because neither depends on the roster being
+shared.
+
+**The asker subject is typed on the wire** — `{kind: person_ref | address,
+value}` — because its meaning is shape-dependent and nothing else in an S2
+or S6 call says which shape the pair is in. "Provisioned together" is an
+operational habit, not an enforced property, and a mismatched pair would
+otherwise send a value the receiver keys nothing on and get back
+`NOT_VISIBLE`: the one value §4.3 permits to be silent, returned for a
+mis-provisioning. With the kind on the wire, a receiver that does not key
+on it answers `UNKNOWN` and the gap is named, which is what §4.3 requires
+of everything that is not a denial. *(Review pass, 2026-09-05.)*
+
+---
+
+## 4. Boundaries
+
+### 4.1 The team
+
+The `<team>` DNS label, reserved suite-wide at the home (home ruling 19).
+Every barrier in this document is a barrier at that label. Two of them are
+structural:
+
+- **A sibling's `consumers` row** is both credential and origin allowlist.
+- **Every sibling client refuses a host whose `<team>` label differs from
+ its own**, test-enforced against a fixture of hostile hostnames, and
+ refuses to follow redirects.
+
+Neither is a boundary against code execution on an instance. They stop
+mis-provisioning and accidental cross-team dialling, and that is what
+they are claimed as (memoria §6.3).
+
+### 4.2 Custody
+
+What a sibling may *hold* a copy of. The predicate is **team-visible**:
+readable by every roster member by virtue of team standing alone. Each app
+defines it in one function, and the conformance suite (§11) proves that a
+content fetch for anything else answers 404, never 403.
+
+**The ladder every app implements** (ruling 15). Four product states, three
+rungs and one capability:
+
+| State | Rung | Team-visible? |
+|---|---|---|
+| Private to me | `private` | No |
+| Shared with specific people — members, or guests from outside (§5.2) | `granted` | No |
+| Shared with the whole team | `team` | **Yes** |
+| Anyone with the link can read | *a read-only capability on any rung* | **No, by itself** |
+
+**The product need this serves, in Paul's words: read and write with
+team-mates, read-only in public.** Team-mates read and write by team
+standing or by a grant. The public reads by link and never writes. An
+"anyone with the link can edit" share is the `granted` rung wearing a
+convenient front door: the link carries you to sign-in, sign-in mints a
+guest grant, and from there you are a named actor like everyone else.
+Sheets ruling 12 settled this on 2026-08-30 — *"link sharing is
+viewer-or-commenter, never editor"* — and revision 3 rebuilt the actor it
+rejected without citing it. Ruling 15 cites it now. That failure is
+precisely what §2 diagnoses and §10 exists to stop, committed by the
+document that diagnoses it.
+
+**Custody keys on the rung, never on the link.** The predicate is not "can
+somebody read this", it is "does team standing alone reach it". A link
+share is a capability held by whoever has the URL, and it is deliberately
+not a grant to anybody in particular.
+
+Getting this wrong is a live leak, not a technicality. Ana keeps a
+document private and link-shares it with one contractor. If the link made
+it team-visible, memoria would ingest it, and the first teammate to search
+for its subject would be handed a passage from a document nobody gave
+them. The sharer chose "whoever I send this URL to". Treating that as
+"everyone on the team, findable by search" is a different decision made on
+their behalf. An item that is *both* on the `team` rung and link-shared is
+team-visible because of the rung; the link adds nothing either way.
+
+**A link share still moves nothing** (ruling 2). Whoever holds the URL
+reads the item in the app that owns it, so no byte reaches another team's
+instance and §0's "across" half is untouched. A link holder is never on a
+roster, so no sibling ever names one as an S2 asker — siblings serve
+members, and a link holder is served by the owning app directly.
+
+Individually granted items, guest-only items and link-only items are never
+team-visible. The change feed therefore carries no visibility flag:
+departure from team-visibility is `gone`, indistinguishable from deletion,
+so a sibling cannot learn that a private item exists (memoria §6.1). A
+move down the ladder — `team` to `granted` or `private` — is a departure
+like any other, and reads as `gone`.
+
+### 4.3 Authorization
+
+What a *person* may read. Asked of the app of record with the asker
+named, per query, and answered from that app's grants join. Four values,
+none collapsible into another: `VISIBLE`, `NOT_VISIBLE`, `STALE`,
+`UNKNOWN`. Only `NOT_VISIBLE` may be silent. Everything else that is not
+`VISIBLE` withholds loudly and names the app (memoria §4.1).
+
+**The revision may be behind in either direction, and only one is
+`STALE`.** memoria §4.1 defines `STALE` as the source having moved *past*
+the requested revision. A source restored from backup can be *behind* it —
+the consumer holds revision 7 and the app of record now has 5. `VISIBLE`
+would authorise a revision the app of record cannot vouch for; `NOT_VISIBLE`
+files a synchronisation failure under the one value that is allowed to be
+silent, which is the exact mistake `STALE` was introduced to fix. So a
+revision the app of record does not recognise is **`UNKNOWN`**: withheld
+loudly, the app named, the consumer free to re-ingest. memoria records
+this defect open in its §13; A.4 closes it. *(GPT-6 review, finding 11.)*
+
+**Never cached, at any scope.** Not per session, not per page, not for
+the life of a warm process. Caching authorization is what makes a stale
+answer a leak (memoria §6.5), and the usability pressure to cache within
+a session is exactly the pressure this sentence exists to resist. The one
+copy an instance may hold is the roster (§5.3), and the roster answers
+team standing, not "may this person read this item". An earlier draft of
+ruling 3 said "cached no longer than a session", which conflated the two;
+memoria's session caught it.
+
+**The app of record for an item is the app whose store it was created
+in.** A copy never becomes an authority, and a derived artefact carries the
+provenance of every source it was built from, transitively, so two hops
+cannot launder what one cannot (memoria §13, now a suite rule).
+
+**S2's answer is intersected with custody.** The app of record runs its
+grants join for the named asker, and then answers `NOT_VISIBLE` for any
+item that is not team-visible, whatever the asker's individual grants say.
+
+Without the intersection §4.2's guarantee is false. A sibling holds ids for
+items it ingested, and an item that later leaves team-visibility appears in
+the feed as `gone`, which §4.2 requires to be indistinguishable from
+deletion. An S2 answering from the full join would distinguish them
+perfectly: `VISIBLE` for the one that went private, when a grantee is the
+named asker, and `NOT_VISIBLE` for the one that was deleted. The leak the
+S1 visibility flag was removed to prevent, and the leak S3's "404, never
+403" was designed to prevent, would arrive on the third call.
+
+**The intersection has a cost, and it is a product cost.** A consumer that
+ingested an item while it was team-visible, and asks about it after it went
+private but before `gone` reaches it, now gets `NOT_VISIBLE` where it would
+have got `VISIBLE` for a person who still holds an individual grant. Ana
+loses that passage in memoria, and any answer built on it, while the
+document itself stays open to her in the app of record. That is the price
+of the indistinguishability §4.2 promises; it cannot be avoided while the
+promise stands, and the consumer's honest move is to say which app the
+answer went to rather than to return a shorter one silently.
+
+Revision 2 first wrote that the intersection cost a correct consumer
+nothing, reasoning from visibility at ingestion. That reasoning is wrong:
+a later visibility change is exactly what the defect turns on, so the case
+where the intersection bites is the case the argument assumed away. It
+costs an *incorrect* consumer what decision 3 says it should — custody
+bounds what a sibling may hold, and a sibling has no business asking about
+what it may not hold. *(Review pass, 2026-09-05. memoria §4.1 states the
+join unintersected — "plus rows granted to me individually" — so A.4
+carries the correction. The cost paragraph is corrected from the GPT-6
+review, finding 8, which also wants a transition fixture: ingest, downgrade
+while keeping Ana's grant, query before the feed catches up.)*
+
+---
+
+## 5. Identity and the roster
+
+### 5.1 What the home owns
+
+- **The person**: an opaque person ref, the assertion's `sub` (home §5.3).
+- **The team**: sqid, DNS label, sealed display name (home ruling 17).
+- **The team's instances**: `team_instances`, one row per (team, app).
+- **The roster**: which person refs are on which team, as `owner` or
+ `member`, in a **membership record of its own** — `(team, person, role)`,
+ not a projection of anything else the home holds. This is ruling 4's
+ expansion and ruling 12's shape.
+
+**Revision 1 derived the roster from directory rows, and that was wrong
+three ways.** It is recorded because the economy was tempting and someone
+will propose it again: it adds no table, and it leaves the home's
+publish-the-database claim untouched.
+
+- **The row has no owner role to project.** Home §6's directory row is
+ `(person ref, app, team sqid, host, kind, sealed team name)` and "`kind`
+ is member or guest". Home §7 is explicit that who counts as an owner "is
+ the instance's business… and the home never learns the answer". §5.3's
+ wire shape had no source.
+- **The row is scoped per (team, app); the roster is per team.** A
+ projection can therefore express "a member of acme.docs but not of
+ acme.sheets", which ruling 1 forbids outright.
+- **Any instance can write it** under its consumer token, through call 2.
+ Deriving team standing from it routes around §5.4's step-up completely.
+
+Changing the reader without changing the writer is not a design. Ruling 9
+made a directory row a hint on purpose; revision 1 made it authoritative
+and left the writer alone.
+
+### 5.2 What the home never owns
+
+A per-item grant. Which sheets a member may edit, which document a guest
+may comment on, which calendar a viewer sees: all instance-side, in the
+app's own grants table, decided from there on every request. The home
+answers only facts about its own records, never "may this person" (home
+ruling 19). "Is this person on this team" is a fact about its records.
+"May this person open this sheet" is not.
+
+Guests are not on the roster. A guest grant is minted in the instance and
+announced to the directory as `kind = guest` (home call 2), exactly as
+today. Nothing about guests changes.
+
+### 5.3 How the roster reaches an instance
+
+**Call 8, roster pull**, instance → home, under the instance's consumer
+token, scoped to the instance's own team:
+
+```
+GET /roster?team=<sqid>&epoch=<n>
+→ { epoch, members: [ { person_ref, role: owner|member } ] }
+```
+
+The home cannot push (home §6: no channel reaches a hibernating instance),
+so an instance pulls on wake and whenever it learns its copy is stale. It
+learns that from a **roster epoch** carried in every call-2, call-5 and
+call-8 response, and in the assertion as a claim.
+
+**The epoch claim is an invalidation hint, and it needs its own exception
+in home §5.4.** Revision 2 said the claim passed that test unchanged. It
+does not: home §5.4 admits a non-identity claim when it is drawn from a
+closed enumerated set and fails cosmetically, and a monotonically rising
+epoch is neither of those. What makes it safe is a different argument and
+deserves its own words — **the instance decides nothing from the claim**. A
+claim it cannot parse is ignored. A claim that differs from what it holds
+buys exactly one call-8 refetch, and call 8's answer is what it enforces.
+So a hostile claim costs one request and can neither admit nor exclude
+anybody. A.1 records that exception in the home spec, which owns the test;
+this document cites it rather than asserting the old test already covered
+it. A person whose sign-in reveals a stale roster is admitted against the
+fresh roster, never against the claim. *(GPT-6 review, finding 12.)*
+
+**The copy has a maximum age.** Every carrier of the epoch — call 2, call
+5, call 8, the assertion — needs a person or a wake. Sibling calls carry no
+epoch, deliberately, since a sibling has no business knowing the roster's
+version. So an instance kept warm by sibling traffic alone, admitting
+nobody and never sleeping, has nothing to tell it that its roster moved,
+and §5.4's sentence would not be true of it. The copy therefore carries a
+TTL and is refetched on expiry. The number's reason, in the comment the
+contract module requires: it is the worst-case lag between a removal at the
+home and its effect on an instance nobody signs in to. Measured with §14's
+deadline rather than guessed here. *(Review pass, 2026-09-05.)*
+
+**Call 8 is paged, and a page is only meaningful inside one epoch.** S2
+and S6 cap a batch the *caller* supplies; call 8 is the first call whose
+response size the caller does not control, so it needs a cursor as well as
+a cap. The cursor is bound to the epoch it was issued in. Fetching page one
+at epoch 10 and page two at epoch 11 would otherwise compose a membership
+set that never existed at either — skipping a removal, or dropping an
+owner. So: the home refuses a cursor from a superseded epoch and says which
+epoch is current; the instance **stages every page and replaces its roster
+atomically only when a complete set for one epoch has arrived**; and a
+partial or failed fetch leaves the previous roster in place. A partial
+roster is never published, and an empty response is never read as "nobody
+is a member". *(GPT-6 review, finding 10.)*
+
+**A refetch that fails does not silently extend the copy.** The TTL says
+when to refresh; it has to say what happens when refreshing does not work,
+or "removed within the TTL" is a promise about the happy path only. Past
+the TTL with no successful fetch, the roster is **stale**, and team
+standing stops being established for anybody — **including people already
+in a session**. Revision 3 let existing sessions run on, which on a 30-day
+sliding session is the outage-length revocation tail the paragraph claimed
+to reject, bought at the price of suite-wide `UNKNOWN`. A stale instance
+therefore serves only what an *individual grant* reaches, since a grant is
+instance-side and needs no roster, and answers `UNKNOWN` on S2 rather than
+`NOT_VISIBLE` — the gap is named, per §4.3, because an unreachable home is
+a synchronisation failure and not a denial.
+
+This costs availability on purpose, and the cost is real: a team whose
+home is unreachable loses team-standing access until it returns. The
+alternative is that an unreachable home lets a removed member keep their
+access for as long as the outage lasts, which is the thing ruling 4 was
+adopted to end. *(GPT-6 finding R3-2; Fable finding 5.)*
+
+**A restored home gets a new roster generation, because refusing a lower
+epoch is not enough.** Revision 3 said an instance never accepts an epoch
+below the one it holds. That stops the regression and not the damage: a
+restored home simply issues 10, 11, 12 again over the recovered roster,
+and two writes later the instance is accepting an epoch it considers fresh
+carrying members it had already removed.
+
+So the roster carries **`(generation, epoch)`**. The generation changes
+whenever the home cannot prove continuity with the epochs it last issued —
+any restore from backup. An instance seeing an unfamiliar generation holds
+its roster stale (above), admits nobody on team standing, and refuses to
+publish the new one until an owner confirms it at the directory origin. A
+restore is a human event and it should cost a human action; the
+alternative is that the recovery path is also the readmission path.
+
+**A home that loses the `memberships` table entirely has no rebuild path**
+— call 2 cannot write it (ruling 12) and no channel reaches a hibernating
+instance. Home §10's *"loses credentials, not access"* is therefore false
+for the roster, and A.1 amends it. Recovery is the instances' copies plus
+an owner's confirmation, or it is re-entry by hand. *(GPT-6 finding R3-3;
+Fable finding 4.)*
+
+**The instance enforces from its own copy**, on every request, exactly as
+it does today. What changes is where the copy comes from: the home rather
+than an owners list in an environment variable.
+
+### 5.4 Writing the roster
+
+At the home's directory origin, by a team owner, behind a fresh-assertion
+step-up (home §14 names it; this is its trigger), and **nowhere else**.
+Adding a person announces them to every `team_instances` row's directory
+entry, so their front door shows the whole team on first sign-in. Removing
+a person bumps the epoch, and every instance that wakes or admits anyone
+refetches.
+
+**Call 2 cannot write membership** (ruling 12). It writes directory rows,
+which say where a person has things, and no directory row grants or
+revokes team standing however its `kind` reads. The membership record is
+the only source for call 8.
+
+**The first owner is seeded by the drain**, from the sealed queue payload
+home §7 already carries, because gating a membership write on an owner
+needs an owner to exist first, and that circularity has to break outside
+the roster.
+
+**A member removed at the home is removed everywhere within one epoch**,
+and on an instance that admits nobody, within the roster TTL (§5.3).
+That is the sentence memoria decision 11 could not write and the reason
+its four unreconciled tables existed.
+
+### 5.5 What this costs, said plainly
+
+Ruling 13. "A compromised home" is three different events and they cost
+three different things. Revision 1 ran them together, and it did so by
+taking home §2's careful sentence about a compromised **directory** and
+widening it to the **home**. Home §3.1 keeps the two apart and records
+that its own first draft collapsed them and was wrong to.
+
+**A leaked database** reveals the graph, the roster, and — new with ruling
+12 — who owns each team. It reveals no content.
+
+**A compromised directory origin** can add a member. A member reads
+everything team-visible in every instance of that team, through each app's
+ordinary front door, because that is what ruling 1 means. It cannot read
+an individually granted or guest-only item, write as a person, or grant a
+per-item right. This is the delta ruling 4 and ruling 12 actually
+introduce.
+
+**A compromised credential origin** can sign in as anyone, owners
+included. It holds the signing key (home §5.3), the instance's grants join
+is keyed on the `sub` the home asserted, and home §5.4 already calls a
+hostile `sub` account takeover. No membership row is needed and none is
+left behind. **This is not new and ruling 4 did not cause it**: it has been
+true since home ruling 1 put every credential on one origin, and a passkey
+does not help, because the passkey lives there too. It is cited here, not
+introduced here.
+
+**The step-up is not a bound on any of them**, because the compromised
+authority is the one performing it. Neither is the directory row or the
+notification: those are what honest code leaves, not what a compromise
+must. Home §3.3's idiom applies — said plainly rather than counted as a
+fix.
+
+**What is left as a real bound, and it is narrower than revision 3 said.**
+Against the *directory* tier: an instance's own grants decide what an
+admitted member may read, so adding a member does not reach an
+individually granted item. Against the *credential* tier: **nothing here
+does**. An attacker who can sign in as Ana has Ana's grants, reads what Ana
+reads and writes what Ana writes, and no instance-side check distinguishes
+them — that is what "signs in as anyone" means, and the bound has to be
+read as absent rather than restated in weaker words.
+
+Revision 3 conceded the credential tier in one paragraph and then wrote
+the reassuring clause back in two paragraphs later, which is worse than
+revision 1's honest-but-wrong single bound. *(GPT-6 finding R3-1.)*
+
+**The Pegamento filing changes shape.** It is not that this document
+diverges from §10.3 on membership. It is that **the home was never a
+§10.3 glue**: "a compromised glue grants nothing" is written for a
+registry, and home ruling 1 makes the home an identity authority. §14
+files that, not the narrower claim.
+
+### 5.6 Roles
+
+The roster knows two: `owner` and `member`. Per-item roles stay the app's
+own. Sheets has `commenter`; calendar does not; Docs has none yet. The
+doctrine does not unify them, because a shared role type designed from
+three specs at once would fit none (estilo's one rule). When two apps have
+shipped the same per-item vocabulary, it may move to the contract module.
+Not before.
+
+---
+
+## 6. The suite contract
+
+One Go module, `amadan.net/oficina/suite`, with a client half, a server
+half that mounts beside an app's own router, and golden fixtures. memoria
+§6 specified S1 through S7 and the activity design specified call 7; both
+move here unchanged in substance. The home's calls 1 to 6 stay in the home
+spec, which owns them, and are cited here for completeness of direction.
+
+| Call | Direction | Credential | Proves | Notes |
+|---|---|---|---|---|
+| 1–6 | person → home, instance → home | session; consumer token | — | Home §9. Unchanged. |
+| 7 activity | home → instance | **assertion** for the person, `aud` = instance | who the front door is for | `design-activity-pull` §4–5. The home renders and discards; RAM cache only. |
+| 8 roster | instance → home | consumer token | which instance asks | §5.3. Scoped to the caller's team. Paged; the copy has a TTL. |
+| S1 change feed | sibling → sibling | consumer token | same-team sibling | `(item_id, revision, gone, touched_at)`. No title, no content, no visibility flag. Per-consumer TTL; past it, full re-announce. |
+| S2 authorization | sibling → sibling | consumer token **+ named asker** | same-team sibling, asking for a person | Four values, total decoder, cap 256 pairs. **Never cached**: a passage crosses no request boundary without an S2 `VISIBLE` in the request that carried it (memoria §8.4, gate 26). Asker is a typed subject (§3); the answer is intersected with custody (§4.3). |
+| S3 content | sibling → sibling | consumer token | same-team sibling | `(id, revision)`, **no person**. Team-visible only. 404, never 403. |
+| S6 aggregate | sibling → Sheets | consumer token + named asker | as S2 | Enumerated function over cell refs, never expression text. Cap 64. Typed subject and the custody intersection, as S2. |
+| S8 write-as-self | sibling → sibling | consumer token | same-team sibling | §7.2. Into the caller's designated area only; only items the caller authored. |
+| hand-off | browser → instance | the **person's own session** | the person | §7.1. A redirect POST, the shape of home call 6. Not a server call. |
+
+**S7 membership is retired in suite shape.** It existed because there
+was no roster of record; call 8 is that record. A lone instance never
+needed it. memoria decision 11 is superseded (Appendix A.4).
+
+**S4 and S5 are not calls, and no number is reserved for them.** memoria
+§6.1 resolved S4, the tombstone, into a feed row — `gone` covers deletion
+and departure from team-visibility indistinguishably — and S5, consumer
+expiry, into the per-consumer TTL, since expiry is the decommission verb.
+Both are settled; they are recorded here because a table that runs S1, S2,
+S3, S6, S8 leaves two holes, and a hole in a numbered contract is somewhere
+a later call falls. *(Review pass, 2026-09-05.)*
+
+### 6.1 Why two credentials
+
+**Only the home can mint an assertion**, and putting the home in the loop
+of every memoria search would break memoria §6.4 and its 800 ms deadline
+at once. So a sibling asking "may Ana see these" sends its consumer token
+and names Ana. The sibling is trusted to name the asker because a
+compromised sibling already reads everything team-visible through S3; the
+naming adds nothing to what it can read. It would add something to what it
+can *write*, which is why S8 takes no asker (§7).
+
+**The home does hold the signing key**, so call 7 carries a real
+assertion and the answering instance decides from its grants with no
+trust in the home's word. Ruling 3 of the activity design, adopted.
+
+### 6.2 Authentication and rotation
+
+Per-consumer bearer tokens, minted by the operator drain, delivered as
+platform config, stored hashed by the receiver. Three-phase rotation:
+expand acceptance, switch the sender, contract acceptance (memoria §6.2).
+The home never sees a sibling token. Mutual TLS is unavailable because TLS
+terminates at the platform router.
+
+### 6.3 Availability
+
+Every pull carries a deadline, all siblings are called concurrently, and a
+sibling past its deadline is `UNKNOWN`: withheld loudly, the gap named
+with the app. Partial pages are the normal case. A cold sibling is a wake,
+and a wake is slower than any deadline, so a consumer pre-warms on its own
+wake (memoria §6.5, activity design §6). **A reliably slow sibling is a
+suite health problem the consumer surfaces, never one it routes around by
+caching authorization.**
+
+### 6.4 What the contract does not license
+
+Any hop across a `<team>` label. Any sibling presenting an assertion it
+did not receive from the home for its own `aud`. Any write except §7. Any
+item outside the custody predicate. The home in the loop of a sibling
+call. Content leaving the team's hosts except under ruling 6.
+
+---
+
+## 7. Writes
+
+The question "may memoria write a Doc" has two answers because it is two
+questions.
+
+**Both answers carry the same envelope.** §4.3 requires a derived artefact
+to carry the provenance of every source it was built from, transitively.
+That rule had no mechanism at the one moment it matters — when content
+crosses from one app's store into another's — so it is stated here, for
+both write paths.
+
+A write carrying content derived from items in other apps carries a
+**provenance envelope**: per source, the app of record, the item id and the
+revision read. The receiver stores it as part of the item, not as metadata
+it may drop, and re-checks it on every disclosure path. **Which check
+depends on whether the path has a person**, because two of them do not:
+
+| Path | Has an asker? | The check |
+|---|---|---|
+| An ordinary read, an export, an activity row | Yes | S2 to each app of record, for that person |
+| An onward S3 fetch | **No** — S3 is person-less by definition (§6) | **Custody**: every source still team-visible, or S3 answers 404 |
+| A read through a read-only link | **No** — a link holder is no roster subject | The item carries no link while any source is not itself link-shared |
+
+Revision 3 wrote "the sources still authorize the asker" for all of them,
+naming an asker on two paths that have none and leaving gate 6b
+unwritable. The person-less paths fall back to the predicate that needs no
+person, which is the same split decision 3 has always drawn: custody for
+what may be held and served without an asker, authorization for what a
+person may read. *(GPT-6 finding R3-4; Fable finding 3.)*
+
+**Withholding is whole-artefact, never per-passage.** If any source stops
+authorizing, the entire derived item is withheld and the gap is named. A
+synthesis carries no map from its sentences back to its sources, so an
+envelope that claimed to strike "the passage from the revoked sheet" would
+be asserting an alignment nobody computed. Removing the wrong sentences
+leaks; removing the right ones by luck is not a design. *(GPT-6 finding
+R3-5.)*
+
+Two consequences, both counter-intuitive enough to state:
+
+- **A new id is not a new authority.** Saving a memoria answer into Docs
+ gives the result a Docs id; it does not give it Docs' authority over a
+ Sheets cell it quotes. Ana's grant on the new Doc is necessary and not
+ sufficient. Revoke the sheet and the saved answer loses that passage, in
+ Docs, on the next read.
+- **A source the receiver cannot check is a source it must not accept.**
+ memoria's decision 7 and gate 17 forbid it serving a sibling-facing
+ endpoint at all, so today a hand-off carrying memoria-owned material has
+ no app of record to ask. Until A.4 opens that surface with its own
+ decision and leak entry, a write whose sources include one is refused,
+ rather than accepted with an envelope nobody can verify. **A person's
+ confirmation establishes their intent; it does not stand in for an
+ authorization the receiver cannot make.** *(GPT-6 review, finding 3.)*
+
+### 7.1 A person is present: the hand-off
+
+"Save this answer as a Doc" is Ana's act. memoria renders a form whose
+action is Docs' create endpoint on Docs' own host; Ana's browser POSTs it
+with her own Docs session; Docs sees Ana and Ana owns the document.
+memoria holds no write credential and Docs trusts nothing memoria says.
+
+Same-site cookies make this a plain form: `<team>.memoria.oficina.works`
+and `<team>.docs.oficina.works` share a registrable domain, so a `Lax`
+session cookie rides the POST. Body size is capped by the receiver, with
+the number's reason in a comment. A person with no Docs session is sent
+through sign-in and returns to the pending state by nonce, the flow home
+§5.5 already specifies. This is scriptless and satisfies every app's
+zero-JS floor.
+
+**The receiver checks the origin and asks the person to commit.** What
+makes the cookie ride the POST — same registrable domain — is also what
+makes this a write any same-site page can drive without the person meaning
+it, and a create endpoint that commits on receipt is a drive-by. Home call
+6, whose shape this borrows, carries two protections this borrowed without
+(home §5.6): it validates hosts against the consumers allowlist, and it
+binds the flow to the browser that began it. So:
+
+- **The `Origin` header must match a row in the receiver's `consumers`
+ table**, which §4.1 already makes an origin allowlist. It holds the §4.1
+ barrier on a path that otherwise bypasses it.
+- **The receiver renders what it received and the person commits it.** One
+ screen, one button, the person's own session. This is what makes the
+ document Ana's act rather than memoria's, which is the entire claim of
+ §7.1; without it §7.2's three constraints are reachable by any sibling
+ through §7.1, and the distinction between the two sections collapses.
+
+**Both cost more than revision 2 said they did.** It claimed no new
+machinery and no extra round trip. Neither is true, and the third hand
+established it against the pinned dependencies rather than by reading the
+specs: memoria's rastrillo default CSP sets `form-action 'self'`, which
+stops the form on the way out; Docs mounts `csrf.Protect` above every
+route and its `SameOrigin` check refuses a same-site cross-origin POST,
+measured at 204 same-origin against 403 same-site; and for a person
+already signed in, receipt-then-commit is a second POST where revision 1
+wrote on the first. So the real bill is a narrowly scoped sender
+`form-action` allowance, a dedicated receiving route carrying its own
+origin guard, and one more round trip — and gate 7 has to run through that
+middleware, since a handler-level fixture passes while a browser does not.
+Still scriptless, which was the property worth protecting.
+
+**Receipt and commit are two requests with different rules**, and revision
+2 left the second unspecified. Split:
+
+- **Receipt.** The sibling's POST arrives, its `Origin` is checked against
+ the `consumers` allowlist, and the body is held as **pending state**:
+ bounded, expiring, redeemable once, bound to the browser and — after
+ sign-in — to the authenticated person. Nothing is created.
+- **Commit.** The person's confirmation POST comes from the *receiver's*
+ own origin, against that pending state, and what it commits is the body
+ the server already holds. The sibling-origin rule does not apply to this
+ request and would wrongly reject it. Because the body is fixed
+ server-side a second POST cannot substitute a different one, and because
+ the pending state redeems once, a replay creates nothing.
+
+Without the split, a bare second POST carrying a `confirm` flag can be sent
+by the same allowed sibling, and a screen appearing somewhere in the flow
+is not evidence that this browser saw this body.
+
+**The sending side has a boundary too, and it is earlier than the
+receiver's.** §4.1's rule that a client refuses a host whose `<team>` label
+differs governs the sibling HTTP client; an HTML form action is not that
+client. A form action misconfigured to another team's Docs host puts the
+body on the wire before any `Origin` check at the far end can refuse it,
+which breaks §0's "across" test literally: the byte arrives, and being
+refused on arrival is not the same as not arriving. So **the destination is
+validated against the sender's own `<team>` label before the form is
+rendered**, not after it is submitted. *(GPT-6 review, finding 4.)*
+
+### 7.2 No person is present: write-as-self
+
+A weekly digest. A Notion page ingested and written out as a Doc. That is
+memoria's act, and rastrillo already models a non-human actor. S8 lets a
+sibling create or modify items **as itself**, under its consumer token,
+subject to three constraints the receiver enforces and the conformance
+suite tests:
+
+- **Only into an area the team owner designated for that sibling.** A
+ folder, a tag, a calendar, whatever the app's own grouping is. The
+ designation is an instance-side grant, made in the receiving app's
+ share UI, and it is what makes S8 refusable per team.
+- **Only items the sibling authored.** Every S8 write carries the writer's
+ identity in the item's actor field, and an S8 modify is refused for any
+ item whose actor is not the caller.
+- **Never a person-authored item.** Stated separately because it is the
+ attack the constraints exist to prevent.
+
+The attribution rule holds: anything that writes is attributed, and
+"memoria" is a name a reader can act on.
+
+### 7.3 What was rejected, and why it stays rejected
+
+**On-behalf-of writes**: a sibling writing "as Ana" under its own token.
+It would make every consumer token a write capability over every member's
+identity, and the receiving app could not tell Ana's edit from a
+compromised sibling's. The read-side version of this trust (S2's named
+asker) is acceptable because it adds no reach; the write-side version adds
+all of it.
+
+**Browser-carried only**: forbids unattended writes, which the product
+wants, and would push implementers toward a person-present pretence.
+
+**A link holder as a writer.** Revision 3 built one, and it was wrong
+twice over: Sheets ruling 12 had already rejected it (ruling 15), and it
+would have put an actor on the write path that no app can name — which
+ruling 9 forbids and §7's provenance envelope cannot carry. A link
+reaches sign-in; sign-in mints a grant; the grant names a person. There is
+no fourth actor.
+
+---
+
+## 8. Egress
+
+One sentence, binding on every app:
+
+> **Egress that is a property of the product is forbidden. Egress that is
+> per-team opt-in, to one named recipient, disclosed in the README's first
+> paragraph and on every artefact it produced, with a working floor when
+> it is off, is admissible.**
+
+**Which recipient is the app's decision, never the team's.** A team
+chooses whether, and never whom: a team-configurable endpoint would let an
+owner reinstate a proxy while every answer still printed "one named
+provider" (memoria decision 13, Paul, 2026-09-05).
+
+Docs §11.2's rejection of conversion APIs stands under the first clause.
+**Its own reason is that the egress sends customer documents to a third
+party**, which is what the passage says; "unavoidable and unnamed" is this
+document's *narrowing* of it under ruling 6, not a reading of what it
+already meant. A per-team optional converter with one named provider could
+satisfy §8 and would not have satisfied Docs §11.2 as written. Ruling 6
+authorises the change; the citation must not dress it as a discovery.
+*(GPT-6 review, finding 15.)* memoria §8.3's model provider
+stands under the second, and the comparison is like with like: memoria's
+embedding ingest is whole-corpus egress, comparable in volume to what
+Docs rejected, differing only in the three properties the second clause
+names (discussion 33). Both specs cite this section and delete their own
+wording (Appendix A).
+
+---
+
+## 9. The suite leak list
+
+Each app keeps its own. These are the costs that exist only because the
+apps are a suite, and every one is a consequence of ruling 1 or 4.
+
+1. **A compromised sibling reads everything team-visible in its team**,
+ across every app, through S3, **and everything that was team-visible
+ when it was ingested**. This is what "one trust domain" means and there
+ is no version of ruling 1 without it. Custody buys two things and not a
+ third: it cannot read an item that was never team-visible — private,
+ granted, or link-only (§4.2) — and an honest consumer retires what
+ leaves; a compromised one retires nothing, and no revocation reaches a
+ copy it already holds (ruling 14).
+2. **A compromised sibling can name any asker on S2**, and so learn which
+ team-visible items a given member may see. Bounded by item 1: it can
+ already read them all. **The bound holds only because S2 is intersected
+ with custody** (§4.3); without the intersection it would also learn
+ which private items exist, which item 1 does not cover and §4.2
+ forbids.
+3. **A compromised sibling can write into its own designated area** as
+ itself (S8). It cannot write as a person or touch a person-authored
+ item.
+4. **A compromised home is three events** (§5.5, ruling 13). A leaked
+ database gives up the graph, the roster, and who owns each team. A
+ compromised *directory origin* can add a member, and that member then
+ reads everything team-visible in every instance of the team. A
+ compromised *credential origin* can sign in as anyone, owners included,
+ with no membership row and nothing left behind — not new, and not
+ caused by ruling 4: it is home ruling 1's standing cost of one
+ credential origin. The first two cannot read a private, granted or
+ link-only item, write as a person, or grant a per-item right. **The
+ third can do all of it**, as whoever it signs in as. Neither the
+ step-up nor the notification bounds any of them, because the
+ compromised process is the one performing them.
+5. **The home page holds titles and actor names in RAM** for the length of
+ a response and a short cache (activity design §7). An XSS on the
+ directory origin reaches call 7's output, which is titles, never
+ documents. Never at rest.
+6. **The hand-off puts a document body in a POST** across two origins of
+ one site, in the person's browser. It is the person's own content
+ moving under the person's own session, and the receiver caps it.
+7. **A rendered page is a copy in a browser, and revocation never reaches
+ it.** True of every surface in the suite: a Docs page, a memoria
+ result, the home's activity list. Live authorization bounds what the
+ *next* request shows; nothing bounds what the last one already did.
+ Stated so nobody argues a surface is safe because it re-checks
+ (memoria §3.2).
+8. **Every instance of a team now holds the whole team's roster**, with
+ roles, rather than only the people it had heard of (§5.3). A
+ compromised instance therefore learns the membership and the owners of
+ its team, which under the old per-instance tables it could only partly
+ infer.
+9. **Owner is a new plaintext class at the home** (ruling 12). Publishing
+ the database now identifies each team's owners. Home §0's sentence and
+ gate 8 change to say so; the roster is not the no-op for that claim
+ revision 1 assumed.
+10. **The home learns when a person visits their front door**, at
+ per-visit granularity, and tells that person's instances so they can
+ answer call 7 (activity design §2.2). And once the home makes outbound
+ calls to instances as a matter of course, its outbound traffic stops
+ being a compromise signal (activity design §2.2(b)). Both were
+ disclosed there and belong here too.
+11. **A link capability is bearer-shaped** (ruling 15). Whoever holds the
+ URL reads or writes the item, the app cannot name them, and a
+ forwarded link is a grant nobody recorded. This is what "anyone with
+ the link" means and there is no version of it that is not this.
+
+---
+
+## 10. Where things live
+
+**Repositories stay separate** (ruling 10). `oficina/suite` holds this
+doctrine and the contract module. Estilo stays the design system's home
+and is the precedent for a shared module every app pins.
+
+**Cite, never restate.** Each app spec gains a section, *Consumed suite
+rulings*, listing ruling numbers from §1 and the doctrine commit it was
+read against. Where an app spec today restates a boundary, contract, or
+egress rule, the restatement is replaced by a citation (Appendix A). Sheets
+AGENTS.md's "a contradiction is fixed in the same commit" becomes **"a
+contradiction with the suite doctrine is a bug in the app spec, fixed by
+citing"**, which can be kept across repositories, where the old rule
+could not.
+
+**Conformance is the interface.** The contract module ships golden
+request and response fixtures for every call and a test each app runs in
+CI against the version it pins. Estilo's rule: two specs are guesses, two
+implementations are facts. The fixtures are what memoria's server half and
+Docs' server half both have to satisfy, and they are corrected by the
+second adoption.
+
+**The two-pins trap is the stated cost.** Apps pin the contract module;
+the module pins nothing (it is types, fixtures and a thin client, with no
+rastrillo dependency), so the chain is one deep. Contract changes are
+additive-only, and a removed field is a new major version.
+
+**Docs moves its module path** from `github.com/carlosframework/docs` to
+`amadan.net/oficina/docs` before it imports the contract. Every other app
+is already there.
+
+---
+
+## 11. Testing gates
+
+Gates live in the contract module and each app runs them in CI. A gate
+that cannot be written as stated is a spec bug.
+
+1. **Fixture conformance.** Every server half reproduces the golden
+ response for every golden request, per call. A new fixture is added
+ with the first client that needs it.
+2. **S2 total decoder.** For any 256-pair request, the client yields
+ exactly one result per pair or `UNKNOWN` for the whole call. Tested
+ with short, duplicate, malformed and non-2xx responses, and with an
+ asker subject of a kind the receiver does not key on, which is
+ `UNKNOWN` for the whole call and never `NOT_VISIBLE`.
+3. **Custody, one fixture per rung.** S3 answers 200 for a `team` item and
+ 404 for `private`, `granted`, guest-only, **link-shared-but-not-team**
+ (ruling 15) and deleted. The status and body are identical for "not
+ team-visible" and "does not exist". **S2 answers `NOT_VISIBLE` for the
+ same item id in each state**, byte for byte the answer for the deleted
+ one — the asker holds a live grant in the `private` and `granted`
+ states and a *former* grant in the deleted one, since no schema keeps a
+ live grant against a deleted row. Add **team-plus-link** (team-visible
+ because of the rung) and **a link added or revoked after ingest** (no
+ change to custody either way).
+4. **No cross-team dial.** Zero connections to a host whose `<team>` label
+ differs — `acme-evil`, `acme.docs.oficina.works.evil` — and zero
+ *follow-up* connections when an allowed host answers with a redirect to
+ another label. The literal "without connecting" cannot be written for
+ the redirect case: a client must reach the allowed host to be told to
+ redirect. Two fixture servers, or an instrumented transport that counts
+ connections per host. The hand-off's destination check (§7.1) runs in
+ the same gate, before the form renders.
+5. **Roster**, on a fake clock. A member removed at the home, **holding no
+ other grant**, gets an empty call-7 list and `NOT_VISIBLE` on S2 from
+ every instance after one epoch, and within the roster TTL on an instance
+ that admits nobody. A **guest fixture** keeps exactly the activity its
+ own grant reaches: losing team standing does not revoke an
+ instance-side grant, and asserting an empty list for that person would
+ implement a complete-offboarding policy §5.2 has not agreed to. A
+ **failed refresh** past the TTL yields `UNKNOWN`, not `NOT_VISIBLE`,
+ admits nobody new **and stops serving team standing to an existing
+ session**. A **home rewound** — both to a lower epoch, and to the same
+ epoch numbers under a recovered roster — readmits nobody, and an
+ **unfamiliar generation** holds the roster stale until an owner
+ confirms. A **failed middle page** leaves the previous roster in place.
+ A lone instance, given a home hostname in a fixture, makes no request
+ to it.
+6. **Write-as-self.** S8 is refused outside the caller's designated area,
+ refused for an item whose actor is not the caller, and refused entirely
+ when the team has designated no area. The actor is derived from the
+ authenticated consumer, never from the request body, and a spoofed
+ actor field is refused.
+6b. **Provenance survives the hop** (§7), one case per path because the
+ paths differ. Write an item into app B derived from sources in A and C.
+ Revoke A's source for a person: B's **ordinary read and export** withhold
+ the **whole artefact** and name A — never a partial redaction. Make A's
+ source non-team-visible: B's **S3** answers 404, on custody, with no
+ asker anywhere in the call. Give B's item a read-only link while A's
+ source has none: the link is refused. Assert a write is refused when a
+ source's app of record serves no S2 at all.
+7. **The hand-off, through the real middleware.** A handler-level fixture
+ passes where a browser does not, so this runs against the app's actual
+ CSP and CSRF stack: receipt refused when `Origin` is outside the
+ consumers allowlist; a POST without a session lands in sign-in and
+ returns to the pending state by nonce; a body over the cap refused with
+ no partial write; commit refused when forged, replayed, sent with a
+ changed body, or sent from a different session; the absent and `null`
+ origin cases decided explicitly; a foreign-team destination refused
+ **before transmission**; and one successful same-team flow end to end.
+8. **Publish the home's database.** Counts, team names, memberships —
+ **and, new with ruling 12, who owns each team**. Home §0's sentence
+ changes rather than survives; revision 1's claim that the roster added
+ no column class was wrong. The test runs against the actual persisted
+ schema and its failure paths, not one roster response fixture, because a
+ fixture cannot prove what an entire database holds at rest, and a column
+ audit alone misses new information encoded in existing columns.
+9. **Citation currency.** Each app spec's *Consumed suite rulings* section
+ names a doctrine commit **and the doctrine revision it read**, and the
+ script checks the revision actually belongs to that commit. The revision
+ is in the status block and is bumped by **any normative change,
+ wherever it lives** — §§3 to 8 bind as hard as §1 and §6. Revision 2
+ scoped the bump to §1 and §6, which let a changed hand-off rule or
+ stale-roster policy pass currency indefinitely. The script fails when a
+ spec is more than one revision behind `main`; that grace period is a
+ deliberate compatibility allowance and not evidence the consumer meets
+ current requirements. *("One ruling behind" was not measurable at all —
+ a change to §6 adds no ruling. Review pass 2026-09-05; scope corrected
+ from the GPT-6 review, finding 16.)*
+
+---
+
+## 12. Sequencing
+
+0. ~~**This spec, reviewed.**~~ **Done twice, 2026-09-05: Claude Opus 5,
+ then GPT-6 via Codex.** See the status block. Revision 3 answers all
+ sixteen of the second review's findings; the third pass is §14's first
+ open item.
+1. **The four amendments** (Appendix A) land in home, Docs, Sheets and
+ memoria. This closes `oficina/docs` discussion 33 and unblocks
+ memoria slice 1, which its own §13 says is blocked on agreement, not
+ code. **A.1 is now the largest of the four**, because ruling 12 gives
+ the home a table it did not have and amends home §7's sentence that
+ ownership is the instance's business.
+2. **The contract module**, types and fixtures only: S1, S2, S3, the
+ hostile-hostname fixture, gates 1 to 4. No server half yet. Docs moves
+ its module path.
+3. **memoria slice 1 against the module**, with Sheets and Docs serving
+ S1 to S3. The second adoption corrects the fixtures.
+4. **The home's identity layer** (home §13 step 4) and, on top of it,
+ call 8 and the roster (§5). Gates 5 and 8.
+5. **Call 7**, the activity pull, once the home can mint assertions
+ (home §13 step 10).
+6. **The hand-off** from memoria to Docs. Gate 7.
+7. **S8** and the designation UI in one receiving app. Gate 6.
+
+Steps 2 and 3 do not wait for the home. That is deliberate: the sibling
+contract has no home in the loop and its tokens are a separate switch
+(§3), so memoria can ship against Sheets and Docs in lone shape with
+sibling tokens and an owners list, and the roster arrives later as a
+change to where the list comes from.
+
+---
+
+## 13. Deliberately not built
+
+| Thing | Trigger |
+|---|---|
+| Cross-team anything | Never, under ruling 2 |
+| On-behalf-of writes | Never, under ruling 9 |
+| A unified per-item role vocabulary | Two apps ship the same one (§5.6) |
+| A core monorepo | Never while the suite is a consolidation (ruling 10) |
+| Home-side search or aggregation over pulled content | Never, under home §2.2 as amended |
+| A roster in lone shape that syncs anywhere | Never; lone means lone |
+| Push from an instance to the home | A hibernation model that runs timers, which the platform does not have |
+| A token exchange so siblings can hold assertions | The home's latency budget can afford being in every search, which §6.1 says it cannot |
+| Mail and files as suite apps | A wrapper around keymail or slopbox that serves S1 to S3 and takes a roster |
+| An instance-side check on membership changes — refusing a roster addition, or a call-7 arrival, for a person with no recent session; an owner countersignature on roster writes | A designed mechanism. Ruling 13 concedes a compromised home rather than bounding it, and this is the shape a bound would take. Recorded so it reads as absent by decision, not by oversight |
+| A link share that a sibling can index | Never, under ruling 15. A link is a capability, and a capability that becomes searchable by the team is a different grant than the one made |
+
+---
+
+## 14. Open
+
+- **A third review pass, on revision 3.** Two models have read this and
+ the second returned *request changes*; revision 3 answers all sixteen of
+ its findings and adds four rulings, so what needs reading now is
+ different text. Rulings 12, 13 and 14 are Paul's, taken on Fable's
+ recommendation. **Ruling 15 is this session's proposal from Paul's
+ product requirement and he has not ruled on it** — a reader should treat
+ it as the one rung of §1 that is not yet settled.
+- **What the second review said**, kept for the record. `CODEX-FINDINGS.md`
+ is unedited beside this file; the map from its findings to revision 3 is
+ in the status block. Its six trust-boundary findings were:
+ finding 1, the instance-token write path into the rows §5.1 made
+ authoritative → **ruling 12**; finding 2, the compromised-home bound →
+ **ruling 13**; finding 6, custody as rendering rather than erasure →
+ **ruling 14** and §0; finding 3, provenance missing from both write
+ paths → §7's envelope; finding 5, the failed refetch and the rewound
+ home → §5.3; finding 4, the commit protocol → §7.1. The ten at P2 are
+ answered in place and cited where they landed.
+- **Pegamento filing.** §5.5's divergence from Pegamento §10.3, and
+ ruling 1's application of §1 to a suite, both to `carlos/pegamento`.
+- **The `/auth/begin` dial.** rastrillo's sign-in classifier fetches
+ `.well-known/keymail` on the typed address's domain before the admission
+ gate, so an anonymous visitor makes any instance dial any host
+ (memoria QUESTIONS §5). It affects every app in the suite and belongs
+ upstream in `rastrillo/auth`. Recorded here because no single app owns
+ it.
+- **`rastrillo/rastrillo` #2**, the subject hook and sealed link store,
+ still gates the home's identity layer and therefore steps 4 and 5.
+- **Whether calendar joins the contract in slice 1** or after its first
+ screens merge. It has a spec and content; bodies stay off by default
+ when it does (memoria §12).
+- **The deadline number.** 800 ms per sibling is memoria's starting
+ assumption, unmeasured. It becomes the contract's default once measured
+ against a cold wake on the platform.
+
+---
+
+## Appendix A — the consuming amendments
+
+Each is the text an app spec adopts, replacing its own restatement. Each
+is one commit in its own repository, and each adds a *Consumed suite
+rulings* section naming this document's commit **and revision** (gate 9).
+
+**Where a replacement below still paraphrases a rule, the paraphrase is
+the defect, not the fix.** §10 and AGENTS.md say a rule lives here once
+and an app spec names it. A replacement that puts a second copy of a
+boundary into an app spec and then adds a citation beneath it has left
+the second copy to drift, which is the failure §2 diagnosed. Read each
+amendment as *delete the restatement, name the ruling*; the wording given
+is there to identify what is being deleted, not to be pasted in as a new
+copy. *(GPT-6 review, finding 14.)*
+
+### A.1 `oficina/home`, spec §1 and §2.2
+
+Add ruling 22: *"The suite sharing doctrine (`oficina/suite`) binds this
+spec. The team is the boundary; the home owns identity, the team and the
+membership roster; it never owns a per-item grant. Call 8, roster pull,
+is added to §9."*
+
+In §2.2 replace *"Never federates. Data does not cross an instance
+boundary."* with *"Never crosses a team. Data does not cross a team
+label; a team's instances across apps are one trust domain (suite ruling
+1)."* Complete ruling 18's pending change of *"never renders"* to *"never
+stores"*. Replace *"Never holds an authorization model"* with *"Never
+holds a per-item grant. It holds the membership roster (suite ruling 4),
+and answers only facts about its own records (ruling 19)."*
+
+In §9 add call 8 per suite §5.3, paged with an epoch-bound cursor.
+
+**A.1 is the largest of the four, because ruling 12 gives the home a
+record it does not have.** Add a `memberships` table, `(team, person,
+role)`, written only at the directory origin by an owner behind step-up,
+and seeded by the drain with the first owner from the sealed queue
+payload §7 already carries. Call 8 reads only that table. **Amend §7's
+sentence** that who counts as an owner "is the instance's business… and
+the home never learns the answer": the home now learns it, for team
+standing only, and per-item roles stay the instance's. **Amend §6** so a
+directory row is explicitly a hint that never grants or revokes team
+standing, whatever its `kind` says, and **call 2 cannot write
+membership**. **Amend §0 and its gate**: publishing the database now
+identifies each team's owners, which is a new plaintext class (suite gate
+8).
+
+**Amend §10.** *"Loses credentials, not access"* is false for the roster:
+a home that loses `memberships` cannot rebuild it — call 2 cannot write it
+and no channel reaches a hibernating instance — so recovery is the
+instances' copies plus an owner's confirmation. The roster carries
+`(generation, epoch)` and a restore mints a new generation (suite §5.3).
+
+In §5.3's claims table add `roster_epoch`. In §5.4 record a **narrow
+invalidation-hint exception** rather than claiming the existing test
+covers it: the epoch is not a closed enumerated set and does not fail
+cosmetically, and what makes it admissible is that the instance decides
+nothing from it — an unparseable claim is ignored, a differing claim buys
+exactly one call-8 refetch, and call 8's answer is what is enforced. In
+§14 move *step-up for instances* to built-when: *"roster writes (suite
+§5.4)"*. *(GPT-6 review, findings 1, 12 and 13.)*
+
+### A.2 `oficina/docs`, spec §8.2, §9, §11.2
+
+§8.2: replace *"'Sharing is membership' forbids data crossing the
+instance boundary."* with *"'Sharing is membership' forbids data crossing
+a team boundary (suite ruling 2). Same-team siblings may pull
+team-visible content under the suite contract (suite §6), and Docs serves
+S1 to S3 and S8 **wherever sibling consumer tokens are configured**."*
+The sibling switch is independent of shape (suite §3): §12 steps 2 and 3
+have Docs and Sheets serving lone peers before the home exists, so "in
+suite shape" would forbid the sequencing this document depends on.
+
+§9: delete the restated five-call contract and the *consumed rulings*
+list. Replace with *"Docs consumes the home contract as the home spec
+states it, and the suite contract as `oficina/suite` states it. See
+Consumed suite rulings."*
+
+§11.2: the rejection stands, and the reason **changes** rather than being
+restated. Docs' own words are that a conversion API sends customer
+documents to a third party. Suite §8 is narrower — a per-team optional
+converter with one named provider would satisfy it and would not have
+satisfied Docs §11.2 as written — so the amendment reads *"Rejected under
+suite §8. Ruling 6 governs; this is a qualification of the original
+rejection, not a restatement of it."* This closes discussion 33 by
+agreeing on the rule, not by claiming both specs always meant the same
+thing. *(GPT-6 review, finding 15.)*
+
+Docs' link sharing is **read-only** (suite ruling 15). An "anyone with the
+link can edit" share is a link to sign-in that mints a guest grant, which
+is the shape Docs already uses for its commenter link; the writer is then
+a named guest and §7's envelope can carry them.
+
+Docs is the first receiver of the hand-off, so its create endpoint takes
+suite §7.1 whole: the `Origin` check against the `consumers` allowlist at
+receipt, pending state, and a commit from Docs' own origin. Its pinned
+`csrf.Protect` refuses the receipt POST today and its route needs an
+explicit replacement guard; memoria's CSP needs a narrowly scoped
+`form-action` allowance for the sending side. Gate 7 is Docs' to run
+first, through that middleware rather than around it. *(Review pass
+2026-09-05; extended from the GPT-6 review, findings 4 and 9.)*
+
+### A.3 `oficina/sheets`, PRODUCT.md principle 2, spec §8.2, §9, AGENTS.md
+
+PRODUCT.md principle 2: *"One instance per team. Sharing is membership
+across teams: the sheet never moves, and an outside person becomes a
+scoped member of the team that owns it. Within a team, Sheets is one of
+several apps sharing one trust domain (suite ruling 1), and same-team
+siblings may pull team-visible sheets under the suite contract. There is
+no federation story across teams and there will not be one."*
+
+§8.2: append *"Team standing comes from the roster the home holds (suite
+§5) in suite shape, and from `$SHEETS_OWNERS` in lone shape. Per-sheet
+roles stay here."*
+
+Ruling 12 — *"link sharing is viewer-or-commenter, never editor"* — needs
+no amendment. Suite ruling 15 adopts it suite-wide and cites it; Sheets
+keeps it as the original. This is the direction §10 asks for, and revision
+3 went the other way.
+
+§9: as A.2's §9.
+
+AGENTS.md: **keep** *"a contradiction gets fixed in the same commit"* and
+add a second rule beside it. The existing rule is about that file and the
+Sheets design spec, both of which live in Sheets, so it is a local
+consistency obligation that works and was never the thing that broke.
+Revision 1 read it as a cross-repository claim it does not make. Add:
+*"a contradiction with the suite doctrine is a bug in this spec, fixed by
+citing the ruling."* *(GPT-6 review, finding 15.)*
+
+### A.4 `oficina/memoria`, spec §1, §1.3, §4.3, §6, §8.3, §13
+
+§1: add *"Decisions 1, 3 and 4 are now suite rulings 1, 3 and 6; this
+spec cites them. Decision 11 is superseded: admission is the home's roster
+(suite §5) in suite shape and the owners list in lone shape. S7 is
+retired."*
+
+§1.3: correct the S3 prose to match §6.1: S3 takes no person; the fix
+removed the asker rather than adding one.
+
+§4.1: intersect S2's answer with the custody predicate (suite §4.3), and
+type the asker subject (suite §3). The grants join as §4.1 states it —
+Sheets §4.5's *"plus rows granted to me individually"* — answers `VISIBLE`
+for an item that is not team-visible, which is the leak §6.1 closed on S3
+and the S1 visibility flag, reopened on S2. *(Review pass, 2026-09-05.)*
+
+§4.3: replace the body with a citation of suite §5.3 and delete the
+"at least one sibling says MEMBER" rule, which is what made a guest a
+member.
+
+§6: move the S1 to S6 definitions to the contract module and cite them.
+Replace §6.4's *"Any write"* with *"Any write except suite §7: a
+person's own hand-off, and S8 as memoria itself into its designated
+area."*
+
+§8.3: replace the qualification of Docs §11.2 with a citation of suite
+§8. Decisions 13 and 14 stand unchanged: §8 adopts 13's "the recipient
+is the app's, never the team's", and ruling 3's "never cached" is the
+invariant 14 rests on.
+
+Decision 4 widens from *"named on every answer"* to *"named on every
+answer and every derived artefact"*, because suite §8 says "every
+artefact it produced" and means it. An answer is transient; a derived
+artefact (§5.2) outlives the query and is regenerated when a source
+changes, so it can carry a different provider from the one that first
+made it. §5.2 records the producing model alongside provenance so a
+regenerated artefact never inherits the wrong name. Raised by the memoria
+session, 2026-09-05.
+
+§4.1 also: define the **source-behind** case. `STALE` covers a source
+that has moved past the requested revision; a source restored from backup
+can be behind it, and memoria's own §13 records this open. A revision the
+app of record does not recognise is `UNKNOWN`, withheld loudly — never
+`VISIBLE`, which would authorise a revision nobody can vouch for, and
+never `NOT_VISIBLE`, which is the silent value and the exact mistake
+`STALE` was added to fix (suite §4.3). *(GPT-6 review, finding 11.)*
+
+§9.1 and gate 17: decision 7's "no sibling-facing endpoint" now blocks a
+hand-off whose sources include memoria-owned material, because suite §7
+requires the receiver to check every source with its app of record and
+memoria serves nobody to ask. Either open that surface with its own
+decision and leak entry, as memoria §12 requires, or record that such a
+hand-off is refused until it is. Not an editorial change: it decides
+whether "save this answer as a Doc" works for answers built on memoria's
+own uploads. *(GPT-6 review, finding 3.)*
+
+§11: memoria's person-ref and home dependency on slice 1 survives A.4 as
+written. Slice 1 runs in lone shape against sibling tokens (suite §3, §12
+step 3), so the dependency is on the typed asker subject, not on the
+home. Correct it, or slice 1 stays blocked on something that is not
+actually in its way.
+
+§4.3 and its admission fixtures: retiring S7 leaves the admission gate
+citing a call that no longer exists. Replace the fixtures too, not only
+the prose.
+
+§13: close the blocker *"the source apps must adopt decision 1"* against
+A.1 to A.3 when they land.
+
+### A.5 `oficina/calendar`
+
+No restatement exists to replace. Add *Consumed suite rulings* naming
+rulings 1 to 15, with the doctrine's commit and revision, when calendar's
+first screens merge. Calendar has no commenter role (verified in
+`internal/calendar/models.go`), so §5.6's "not before" applies to it
+unchanged.