# 🤖 Activity on the front door — design

**Status:** design, 2026-09-05, revised three times the same day and once
again to coordinate with the suite sharing doctrine (see below).
Drafted by the activity-stream session (`claude-opus-5`).
**The coordination revision was reviewed 2026-09-05 by Fable 5.1**, which
returned CHANGES NEEDED on two blocking findings — both of them §1.1's own
rule turned against it: an "is `main`" assertion that held for fifty-three
seconds, and a second that was true for forty-five seconds and went false
unnoticed. Both are fixed,
and the wording moved to reachability. The four passes below predate that
revision and are recorded separately.

**Review passes: three, all by Fable 5.1, all returning CHANGES NEEDED.**
The first found six blocking (a superseded §11, §7 contradicting itself on
stale rows, §10's 404 rule inverting ruling 5, the shared platform edge cache
upstream of both ends, the RAM cache understated as one person's, and two
"open numbers" that were load-bearing). The second found three (the fan-out
set stated two ways, one of which disclosed a guest's sibling apps; §2's
framing backwards after the rescope; §2.1 answering an alternative nobody
proposed). The third found that §2.1's credential argument was not the whole
story — browser-mediated composition needs no amendment at all — which Paul
then ruled on. **The fourth pass APPROVED**, and the nits it listed are
applied in the commit that carries this line.
**Peer pass, and why it is not the review pass.** The home-spec-compliance
session (`claude-opus-5`) reviewed the first draft and found four things,
all applied: the "no new knowledge" defence was true and beside the point
(§2.2, now a retraction); gate 11 tested only the happy path while §6 makes
failure the normal case (§9); ruling 3's safety rests entirely on `aud`
binding and needed a written "never" against the obvious optimisation (§11);
and one claim in §5 was falsified by §5.3's own last line. That session
wrote enough of the resulting argument to no longer be independent, and it
says so itself. **A fresh reviewer is still required.**

**Coordinated with the suite sharing doctrine, 2026-09-05.** `oficina/suite`,
`docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`, revision 7 at
`2b78680`, adopts this design as **suite contract call 7** (its ruling 11). Five points
were settled with that session and are reflected below: the doctrine's §4.3
now carves out call 7 explicitly, the wire shape stays here rather than moving
into the contract module, the `roster_epoch` invalidation exception is scoped
away from call 7, §1.1 records what this spec consumes, and §3 stops
restating a clause the doctrine's Appendix A.1 owns. What changed here, and
what changed there, is listed at §1.1.

**Scope:** how the home's front door (§11 of the home design) shows a person
what has been happening in the team they are looking at, across that
team's apps that they personally have rows in, without the home
learning or holding any of it.

**Reads on top of** `docs/superpowers/specs/2026-08-30-home-design.md`. Every
`§n` below refers to that spec unless it says otherwise. Nothing here
replaces it; two clauses of it change, and §3 says which.

---

## 0. The short version

The ask was "an activity stream API the other apps post to, so home shows a
feed on sign-in." **The direction inverted during design, and the API is
therefore not the one that was asked for.** Instances do not post. The home
asks each of the viewer's own instances in the selected team "what's theirs, recently?" when it renders
their front door, merges the answers, and holds them nowhere but memory (§7).

**Scoped to the selected team — RULED 2026-09-05 by Paul**, and recorded in
the home spec as ruling 18. The front door is one team's page (ruling 17,
`?team=<sqid>` per ruling 22), so the feed under it is that team's, merged
across the viewer's own rows in that team. An earlier draft specified a
cross-team merged list and argued for it; that argument is retired in §2.1
rather than deleted.

That inversion is the whole design. It is what makes the feature possible
inside the one rule instead of against it:

- **The home stores nothing new.** §0's falsifiable claim — publish the
  database and the worst anyone learns is counts, team names and memberships
  — survives word for word.
- **The app that owns the grants answers the question about the grants.** No
  authorization model at the home (§2.2), no copied ACL to drift, no revoke
  tail.
- **Two clauses change** (§3): §2.2's verb, from *never renders* to *never
  stores*, and §0's prose gloss, which now reads as *never retains*.

What it costs is latency, honestly rendered. §6 and §7.

---

## 1. Settled decisions

| # | Ruling | Rejected | Who, when |
|---|---|---|---|
| 1 | **A real feed with titles, not a contentless heat map** | "4 changes in acme·sheets" with no titles; no feed at all | Paul, 2026-09-05 |
| 2 | **Pull at render. The home holds no activity at rest** | Push into the home (the parked shape); push-a-signal / pull-detail hybrid | Paul, 2026-09-05 |
| 8 | **Server-side pull, and amend §2.2 — not browser-mediated composition** | An `<iframe>` per instance, which satisfies ruling 2 as written and would leave §2.2 unamended, at the cost of the ordered merge, the server deadline, and any rows at all for a viewer without a live session at that sibling — §2.1 | Paul, 2026-09-05 |
| 3 | **Reuse the §5.3 assertion for the pull. No new home→instance credential** | A dedicated service token per instance | This session, 2026-09-05 |
| 4 | **The last good pull is cached in RAM per person, never at rest** | No cache (every load pays cold wake); a cache on disk (stores content) | This session, 2026-09-05 |
| 5 | **A silent instance renders as a named gap, never as an absent row** | Rendering only what arrived | This session, 2026-09-05 |
| 6 | **Scoped to the selected team, following `?team=<sqid>`** — home spec ruling 18 | A cross-team merged list, which this design specified and argued for until 2026-09-05; the argument is retired in §2.1 rather than deleted | Paul, 2026-09-05 |
| 7 | **Participation is a registry flag, never inferred from a `404`** — §10 | Treating a 404 as "does not participate", which makes a broken or hostile instance vanish silently and inverts ruling 5 | This session, 2026-09-05 |

Rulings 3, 4, 5 and 7 are this session's and were flagged as such. Ruling 8
is Paul's, taken after the third review found an alternative that would have
needed no amendment at all (§2.1). Ruling 5 is not a courtesy: §2 explains why it is the
load-bearing test in the document.

---

## 1.1 Consumed suite rulings

The suite sharing doctrine's gate 9 requires a consuming spec to name the
document, its commit **and its revision**, and to list the rulings it takes —
and to *cite* rather than restate them, because a second copy of a boundary is
the drift the doctrine exists to stop. This section is that citation. Nothing
below is restated here in its own words.

**Document:** `oficina/suite`, `docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`,
**revision 7**, commit **`2b78680`**, **reachable from `main`** in that
repository. (Its checkout resolved the file when this was written on
2026-09-05; that is an observation with a date on it, not a standing claim,
for the reason the next paragraph gives.)

**"Reachable from `main`", not "is `main`" — and that wording is the point.**
An earlier draft of this section wrote "that commit is `main`", moved the
citation *because* such an assertion had gone false, and then made the same
assertion again: it was true for **fifty-three seconds** before revision 7
landed. A second such claim, about the suite's own `AGENTS.md` at
`47ae0e8`, was **true when it was written — by forty-five seconds — and went
false twenty-six minutes later, unnoticed.** (An earlier draft of this
paragraph said it was already false when written. It was not: the claim was
made at 18:02:07Z and `47ae0e8` had been `main` since 18:01:22Z. That
correction came from the review, was taken on report, and is only right here
because it was checked at source afterwards — in a section about exactly that
failure.) A present-tense claim about
another repository's tip cannot survive in a document, and chasing it defeats
gate 9's one-revision grace outright. **Reachability is durable** — true
until somebody force-pushes — so that is what this spec asserts. Where a
present-tense observation is genuinely wanted, it is dated and past tense.

**Why the revision moved twice in an evening, when it did not have to.**
Gate 9 lets a consumer sit one revision behind so it is not chased on every
edit, and **every ruling this spec consumes is textually unchanged from
revision 4 through 7** — verified step by step: revision 5 changed the status
block and A.1's note; revision 6 gave S6 its own §12 step and added a §6
paragraph and A.4's cite-as-specification note; revision 7 moved S6 and gave
it a reason. **The doctrine's §1 rulings table is byte-identical between
revision 4 and revision 7**, so none of it reaches §1.1's list. The citation moved
for the wrong reason (chasing an "is `main`" assertion) and now sits at
revision 7 for the right one: it is the current text, and the wording no
longer makes a promise that expires.

| Suite ruling | What this spec takes from it |
|---|---|
| 1 | The team is the boundary; a team's instances across apps are one trust domain. This is why ruling 6's per-team scoping is the correct shape and not a narrowing. |
| 3 | Custody and authorization are two rules, and authorization is never cached. §7 is the one carve-out, and §4.3 of the doctrine now writes it. |
| 4 | The home owns identity, the team and the membership roster, and never a per-item grant. §5.1's argument is re-derived against this. |
| 7 | One contract, one module, fixtures as the interface, and a consuming spec cites rather than restates. This section exists because of it. |
| 8 | Two credentials chosen by what the call proves. Call 7 carries an assertion because it must prove *which person the call is for*, which a consumer token cannot (§5). |
| 11 | Activity is pulled at render and held nowhere at rest — this design, adopted as call 7. |
| 15 | Three rungs and a read-only link. Only the `team` rung is team-visible. Relevant to §5.2's guests, and the reason call 7 never needs a visibility flag. |

**What this spec gave back, recorded so the direction of each borrowing is
legible:** the doctrine's §4.3 call-7 carve-out is this §7's two ages,
reconciled with that session; the epoch scoping in its §5.3 was raised from
here; and its leak-list item 11, a cacheable call-7 response as a
*cross-person* leak at the platform edge, is gate 16, credited there. Its §6.3
sentence — *"a reliably slow sibling is a suite health problem the consumer
surfaces, never one it routes around by caching authorization"* — is ruling 5
generalised, and is cited in §8 as the doctrine's wording of it.

**Why the number was 5 and not 4 — history, kept because it explains the hazard. This spec's first coordination revision
cited 4.** Two commits after revision 4 changed normative text without bumping
the revision: `574cc84` and `bf0eeb8`, the second carrying the §4.3 carve-out
and the §5.3 epoch scoping that this spec consumes. The doctrine's own gate 9
requires a bump on any normative change, so revision 4 was being cited while
its text moved underneath the citation. Raised from here after this spec was
handed one commit for revision 4 while this repository's `suite-amendment`
branch carried another, `210312f`, and neither could be confirmed; the doctrine
session bumped to revision 5 and recorded the reason in its status block.

**`210312f` was not an error where it appeared, and now it is.** Revision 4
was correct when those citations were written and sat inside gate 9's
one-revision grace. That grace has since been spent twice over: the home
spec's own ruling 23 landed on `main` citing revision 4, and the Docs and
Sheets amendments — unmerged, on their `suite-amendment` branches — still do.
**Three revisions behind fails gate 9**, which allows one. This spec and the
home spec move together in this commit; the other two are named here so they
are not forgotten.

**What made this hard to see, recorded because it will recur.** For most of
2026-09-05 the doctrine existed only on that repository's origin and in its
worktrees: every merge went to the remote, and nothing updated the shared
checkout, which sat at `291d957` with an **empty specs directory**. A reader
going to the repository on disk found no document at all, and the only
readable text was a worktree — which is why the first coordination revision
cited a working tree rather than a commit and said so. The checkout is now
fast-forwarded and the file resolved when this was written on 2026-09-05 —
dated, for the reason two paragraphs up.

**The rule this produced now lives in `oficina/suite`'s `AGENTS.md`** (merged
`47ae0e8`, reachable from that repository's `main`): fetch before reading a spec
you did not just write, and fast-forward the shared checkout after you merge.
It is cited here rather than restated, for the same reason §1.1 exists — this
paragraph records why the citation in this spec was hard to get right, and
that file records what everyone must now do about it. What the rule guards
against is not the empty directory, which is loud, but the quiet version: a
session reading a file a few commits behind, citing a hash that resolves, and
being wrong about the text. Gate 9 checks that a citation's revision belongs
to its commit; **nothing checks that the reader read the commit**, and that
gap is open.

---

## 2. Why pull, and what it inherits from a parked design

**This shape was proposed before, in the other direction, and parked.** On
2026-09-02 Paul proposed `escritorio` — a per-team app aggregating sheets,
docs, calendar and mail into one list, apps posting entries opt-in, scoped to
pointers only. Three visibility options were put to a Fable review: **A**,
the app posts the ACL and escritorio filters locally; **B**, escritorio asks
each app at render time; **C**, A plus a priority push on revoke. The verdict
was nine severity-ordered findings, each marked **confirmed** or
**suspected**, and a **conditional** recommendation — not "park it", which
was a later paraphrase: *"Do not build escritorio now; if a merged list must
exist, it is Option B's mechanism without Option B's process."* Paul parked
it and put the home's identity layer first.

**Findings are quoted with their own confidence marking below.** The verdict
distinguishes what it verified against a file from what it reasoned to, and
flattening that distinction is how a citation degrades into a recollection.

**Both sources are in this repo**, because the citation was otherwise
uncheckable — they existed only as local files in a sweepable cache
directory, and the verdict only inside a subagent transcript. No discussion
was opened and no spec was written at the time.

- `docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md`
- `docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md`

**Prefer those files to discussion #6.** #6 is a good-faith reconstruction
written from a session's private memory before the originals were found; its
wording is that session's, not Fable's. These two are the contemporaneous
documents. Quote from them.

The findings that bear on this design, and what happens to each:

| # | The finding | What happens to it here |
|---|---|---|
| 1 | ~~**It answered a different question.**~~ **Overtaken by ruling 6: per team is now the shape.** Per-team aggregation gives a person in three teams three lists, which the verdict called the wrong answer and Paul has since ruled the right one — the verdict's second path, not its first. The §5.4 argument below stands on its own and is what still matters. The cross-team list is forbidden at the only place that could hold it — §5.4, *"'Which documents you can see' passes (a) and fails (b), handing the home a document-level graph it has no business holding."* **Ruling 6 took the verdict's SECOND path** — three lists, per team — so what was reopened is §2.2's verb, not §5.4. The verdict's own resolution: *"either that ruling is reopened deliberately or the want is declined."* | **§2.2's verb was reopened deliberately** (rulings 1 and 2, Paul, 2026-09-05). **§5.4 was not** — the cross-team want was declined instead, which is the verdict's *second* path, and ruling 6 is where that landed. §2.1 argues why a rendered page is not a held graph. |
| 2 | **Titles are first-class sealed content, not cheap pointers.** Sheets §2.1 seals titles under its DEK; a mail "title" is a subject line — *"Re: your termination"*. Any indexer needs Sheets-grade custody. | **Survives, and is ruled through** (ruling 1). The instance decrypts under its own DEK and hands the title over for one render. The home stores nothing, so it never acquires the custody obligation — §3. **Also kills the stale-title leak** the verdict names ("Project Blue" still listed under its old name), which no cached copy can avoid and a pull cannot produce. |
| 3 | **`visible_to` is not a list, it is the grants join** — team rows plus individual grants minus removals. Either the app fans out one membership change to N item pushes, or the indexer re-implements the rule and drifts, failing as *a plausible empty list, not an error*. | **Dissolved by the inversion.** Nobody posts an ACL because nobody posts. The instance evaluates its own join at read time for the person in the assertion. There is no second evaluator and no copy to drift from. |
| 4 | **"Seconds" for a revoke is a median with an unbounded tail.** The carrier is *Sheets'* outbox, and Sheets §9.1.1 says *"for a quiet team a failed upsert may wait weeks for an organic wake, which is acceptable precisely because the directory is a hint"* — an argument that does not transfer to a rendered list. | **Dissolved by the inversion.** Revoke is instant because the answer is computed after it, every time. The only residue is the RAM cache TTL — §7. |
| 5 | **Option B's real costs:** a scriptless page blocking on four cold wakes (*confirmed*), and a visibility endpoint that must not become an enumeration oracle (*suspected*). | **The first is designed for, in the shape the verdict itself pre-approved** — *"not disqualifying if escritorio renders what it has after a deadline and marks the rest 'waking', but that is a design the brief does not describe."* §6 and §8 describe it. **The second is answered in §5.1**, which the first draft of this spec missed. It is treated as real despite being marked suspected, because the cost of the answer is one gate. |
| 8 | **Blocked on §13 step 4**, not started. | **Still true.** §10. |

**Finding 6 does not evaporate — it argues for §3**, and an earlier draft of
this spec wrongly filed it as escritorio-specific. Its principle, quoted
there against escritorio's per-team defence:

> **"Blast radius is bounded by what a service holds, not by how many
> processes it runs in."**

That is the same move §3's amendment makes, pointed the other way. Custody is
what counts, not topology — which is exactly why *"never renders"* was never
the protection and *"never stores"* is. It is the verdict's own principle
doing this design's work, and it is the one sentence to cite if only one is
cited.

Finding 7 (guests) is handled in §5.2 and §8. Finding 9's fourth option is
this design.

**The verdict recommended this mechanism.** Its words: *"a pull-based,
stateless page owned by one existing app… fetched from the sibling with the
viewer's identity, fail-soft on cold wake, nothing stored"* — Option B's
mechanism with the escritorio process deleted.

**The one place this design departs from it**, named because a reviewer who
knows the verdict will reach for it: the verdict put that page inside an
existing app, and this puts it at the home. §2.1.

### 2.1 Why the home, and why a rendered page is not a held graph

The verdict's finding 1 said a per-team feed gives "three lists" and answers
a different question than the one asked. **Ruling 18 accepts three lists**,
so this design now takes the verdict's *second* path — the want is declined
in its original form — rather than its first. An earlier draft of this
section opened by calling finding 1 "the strongest objection available"; it
is no longer an objection at all, it is the shape.

What still needs answering is §5.4, and home spec §13 step 10 asks for that
reopening to be revisited rather than left standing.

**Why the home and not an app — re-justified, because Paul's per-team
ruling retired the argument this section used to make.** That argument was:
only the home knows every team you belong to, so only the home can answer
*"my things"* rather than *"this team's things"*. Per-team scope concedes
exactly that question, so the argument is gone and is recorded here as gone.

What survives is narrower and still uniquely the home's — **and it is the
credential, not the knowledge.**

An earlier draft of this paragraph said "only the home knows which apps a
team has instances in", and answered an alternative nobody proposed (Sheets
holding Docs' grants, which would indeed be federation). Both are wrong. The
knowledge is not exclusive: home spec §9's call 4b lets an instance learn a
sibling exists, and the springboard shows the viewer anyway. And the
verdict's actual recommendation was not grant-holding — it was "a
pull-based, stateless page owned by one existing app… fetched from the
sibling with the viewer's identity", which holds nothing.

**That alternative fails on the credential.** "With the viewer's identity"
means Sheets presenting something at `acme.docs` that speaks for *you* — so
the home would have to mint an `aud=acme.docs` assertion and hand it to
Sheets. §11's first row names that exactly: a cross-instance bearer is full
impersonation, and it is the one thing this design refuses to build. Only
the credential origin mints, so only a page at the home can hold a
viewer-bound assertion per sibling without any instance ever holding one for
another.

**That is not the whole story, and an earlier draft said it was.** There are
three shapes of "fetched from the sibling with the viewer's identity", and
only one of them needs a cross-instance bearer. All three are named here,
because §2.2 is a doctrine clause and amending it without weighing the
alternative that needs no amendment would be the wrong way to get there.

- **Browser-mediated composition.** The front door renders an
  `<iframe src="https://acme.docs.oficina.works/activity">` per row the
  viewer has. `oficina.works` is the registrable domain and the apex is not
  on the public suffix list, so instance and front door are **same-site** and
  the instance's own `SameSite=Lax` session cookie rides along. Zero JS, pull
  at render, nothing at rest — **and the home never touches a title, so
  §2.2 would stand unamended.** With it would go the RAM cache, §2.1's
  cross-person residue, the swap and core-dump questions, the one-process
  question, and gates 11 and 16's parsing surface. **RULED 2026-09-05 by
  Paul: server-side pull, and amend §2.2.** What the frames cost is the
  feature: no ordered merge across apps, only per-app panels; no server-side
  deadline, so a cold instance is an empty frame under a heading rather than
  ruling 5's named gap — the honesty mechanism replaced by a blank; a viewer
  without a live session at that sibling gets a sign-in prompt where rows
  should be, which is most guests and every first visit; a hostile instance
  draws pixels on the suite's most public page; and §6's guest-wake billing
  problem survives with **no home-side throttle that keeps rows on the page**
  — the home authors the markup so it could withhold a frame, but having no
  rows of its own it can only replace it with nothing. **And every instance
  would have to relax its framing policy for the apex**: rastrillo sends
  `frame-ancestors 'none'` and `X-Frame-Options: DENY` by default. The home
  is not in the request path. Recorded as considered and costed, not as
  overlooked.
- **Instance → instance under the caller's own consumer credential**, passing
  `sub`. Fails, but not on the bearer: it would have `acme.docs` trusting
  `acme.sheets` for *who you are*, and home spec §5.4 reserves signed
  identity claims to the credential origin. It also makes `acme.sheets` an
  oracle over every person ref at `acme.docs`.
- **A redirect chain** carrying rows. Not composition at all, and it puts
  titles in URLs — history and referrers — which is exactly why §5.3 keeps
  assertions out of query strings.

**Per-team is also cheaper on every axis this design worries about.** §6's
fan-out drops from every instance a person can reach to the apps in one
team; §7's cache holds less; §5's oracle surface shrinks. The ruling cost
this design its headline and improved everything else.

**Why §5.4 is not breached.** §5.4 governs **what may ride an assertion into
the home's custody**, and its two-sided test is about claims the home comes
to hold. The failing example is a *held* document-level graph. Here:

- **Nothing rides in.** The assertion carries `sub` and theme, exactly as
  §5.3 already specifies. The activity flows the other way, in a response.
- **Nothing is held.** §3's amendment and gate 11 are the mechanism, not the
  intention — a grep proves it.
- **The home learns nothing durable it did not already know.** It knew which
  teams in which apps you belong to; that is the directory, §6. It does not
  learn which documents you can see, because it never asks that question and
  never records an answer.

**The honest residue**, stated rather than argued away and larger than an
earlier draft admitted: for the lifetime of one HTTP response, and for the
RAM cache's TTL, the home's process memory holds a cross-app list of titles
**for every person who rendered inside that window** — not for one person.
That is a cross-person, cross-app set of recent titles, resident in the same
process that mints sessions. §3's amendment lists cross-person aggregation
as never licensed, and home spec §2.2 forbids the console it would build; this builds it as a data structure while forbidding it as a
feature, and the distinction only holds because nothing reads across the
per-person keys. **So the cache is bounded**: a maximum number of person
entries and a maximum bytes per person, both named in §12, evicting oldest
first.

**What "in RAM" leans on, named rather than assumed.** Instances hibernate by
process death (SIGTERM then SIGKILL), so they hold nothing. The home runs
under a memory cap with reclaim before kill. Two platform facts were NOT
established when this was checked on 2026-09-05: whether swap is configured,
and whether core dumps are disabled. **Both must be settled before this
ships** — a swapped or dumped page of titles is at rest, and §3's amendment
is about storage, not intent. That is real, it is what
ruling 1 bought, and it is the thing a reviewer should press on. The claim is
not that this is nothing — it is that *transient and unreplicated* is a
different category from *stored and shipped to a bucket*, and that §0's
falsifiable test is the line that matters because it is the one with a test
behind it.

### 2.2 Two costs that are not knowledge, and are not covered by §2.1

An earlier draft of this spec defended call 7 with "the same knowledge §6
already gives the directory, not new knowledge". **That sentence is true and
beside the point**, and it is retracted here rather than left for a reviewer
to pull on — when it gives way it takes §3 with it. The accurate form:

> **No new knowledge at the home; new emission to instances, and §9's uniform
> call direction spent as a detection signal. Both disclosed.**

**(a) A passive map becomes a broadcast.** §6's knowledge sits still today.
Call 7 makes every front-door render fire an authenticated, timestamped
request at the selected team's hosts that the viewer has rows in (§6). Each instance thereby learns
*"this person opened their front door at 09:14:03"* — including on mornings
they never open that app, and from a host they may have been removed from
whose directory row has not drained.

§3.3 coarsens published timestamps **to the day**, and says why: "timestamps
correlate with events observable at instances." Call 7 hands every instance
in the fleet a fine-grained, signed, unrepudiable version of the signal §3.3
deliberately blunted. Note the shape: ruling 4 keeps the pepper off instances
so one compromised instance cannot learn about the whole suite. This is a
much weaker leak than the pepper, but it is **the same shape** — one instance
learning about a person's life outside it — and it arrives by design rather
than by compromise. **Mitigation is thin and named as thin:** the RAM cache
(§7) collapses repeat renders within its TTL, so the emission is per-TTL
rather than per-load. Nothing removes it.

**(b) It spends a detection property.** home spec §9's calls, call 4b included, are uniformly
person→home or instance→home; **zero run outward**. That uniformity is a
security property rather than an accident of drafting: today a home making
content-bearing requests to instances is anomalous *by construction*, so such
traffic is evidence of compromise, visible to instances — the only parties
positioned to see it. After call 7, an exfiltrating home is indistinguishable
from a working home rendering front doors.

§3.1's second threat model already concedes a great deal when the live host
falls. This removes the one signal that would have told anyone it had. It is
not recoverable by a mitigation in this spec; an instance-side anomaly rule
(call 7 arriving for a person with no recent session) is the only lever, and
it belongs to the instances, not here.

**Neither cost is a reason not to build it** under rulings 1 and 2. Both are
reasons the review pass (§12) should be run by someone who has read this
section.

**This is not escritorio revived.** No new app, no new process, no new store,
no new repo. It is §11's front door — *"one page: a grid of apps, with a team switcher as
a dropdown in the header"* (ruling 17) — growing a recency section.

---

## 3. The two clauses that change

Nothing new goes at rest, so §0's **falsifiable** form is unamended —
publish the database and you still learn only counts, team names,
memberships and §3.2's list. **Its prose form does move**, and saying so is
the honest version: "it never learns what is in anything" now has to be read
as *never retains*. That reading belongs in §0's own gloss rather than left
for the two documents to disagree about.

§2.2's first never-do changes, and only its verb: *never renders* becomes
***never stores***.

**The replacement text is not reproduced here, and that is deliberate.** It
lives in the home spec's §2.2 and in the suite doctrine's Appendix A.1, which
owns the amendment as part of home ruling 23. An earlier revision of this
section quoted the new clause in full; the doctrine's Appendix A preamble
names exactly that as the defect — *"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"* — and it is right. **What this document owns is the
argument for the change, below. What it does not own is the wording.** Read
the clause where it is binding.

**Landed, not proposed.** An earlier revision said "on approval, seven places
change" and listed them. The approval came, and `main` carries the amendment
at `2b68301`: the home spec's §2.2 and §0 gloss, and `AGENTS.md`'s "the one
rule" and its four-things list, all read *never stores* as of 2026-09-05. The
future tense is retired rather than merely updated, because a list of places
still to edit, in a document whose edits have landed, is a reader's trap.
`AGENTS.md` is no longer a binding file this design contradicts; it agrees.

**§2.2's other two never-dos also change, and neither is this design's to
land** — noted so the count in this section's title is not read as the count
of changes to §2.2. §2.2's *never federates*
becomes *never crosses a team*, and *never holds an authorization model*
becomes *never holds a per-item grant* with the roster held explicitly —
suite rulings 1 and 4, arriving as home ruling 23 on branch
`suite-amendment`, not as part of this work. It is named here only because
§5.1 rests on the old wording and had to be re-derived against the new (see
§5.1), and because a reader comparing §2.2 against this document should know
which of its changes came from where.

**Why the amendment is small rather than a hole in the doctrine:**

- **The escritorio verdict's own principle says custody is the thing**, not
  topology: *"blast radius is bounded by what a service holds, not by how
  many processes it runs in"* (finding 6, quoting home §2). It was written to
  deny a per-team process the defence that its boundary made its holdings
  safe. Read forward, it is the argument for this edit: if what a service
  *holds* is what bounds the damage, then a clause about *rendering* was
  never the protection, and one about *storing* is.
- **The exposure is bounded by where it lands.** The front door is on the
  **directory origin** (`oficina.works`), which already renders
  account-authored strings — team names — and already escapes them. A title
  is the same class of string arriving by a different route. §4's split
  holds: **the credential origin renders none of this**, and gate 5 already
  asserts it.
- **"Renders" was doing custody's work.** The original clause was written to
  stop the home *becoming a place documents live* — a support console, a
  preview cache, an index. Storage is what makes it that. A string that
  exists for the length of one HTTP response, in RAM, addressed to the one
  person entitled to it, does not.
- **It is testable.** "Never stores" has a grep behind it (home spec §12, gate 1).
  "Never renders" had no test at all, which is a fair sign it was a
  statement of intent rather than a mechanism.

**What the amendment does not license**, recorded so it is not read wider
than it is: no search over pulled content, no cross-person aggregation, no
analytics, no operator view, no on-disk cache, no retry queue holding a
response body. §11.

---

## 4. Call 7 — the activity pull

The next call in §9's contract, and **the first that runs home →
instance**. Every existing call is instance → home or person → home. That
direction change is the main thing an implementer needs to hold in mind.

```
GET https://<team>.<app>.oficina.works/activity?limit=10&since=<rfc3339>
Authorization: Bearer <assertion>
Accept: application/json
```

**Request.** `limit` is advisory and the instance caps it. `since` is
advisory and lets a warm instance answer cheaply; an instance that ignores it
is still correct.

**Response**, `200`:

```json
{
  "items": [
    {
      "title": "Q3 Budget",
      "url": "https://acme.sheets.oficina.works/s/9fk2",
      "verb": "edited",
      "actor": "Ana",
      "at": "2026-09-04T09:12:00Z"
    }
  ],
  "truncated": false
}
```

| Field | Meaning |
|---|---|
| `title` | The object's display title, decrypted by the instance under its own DEK. The home renders and discards it. |
| `url` | Absolute, and **on the answering instance's own host** — the home rejects any other, or an instance becomes an open redirector through the front door. |
| `verb` | A **closed enumerated set**: `created`, `edited`, `commented`, `shared`. Anything else renders as `changed`, per §5.4's rule that a failed match is treated as *absent*, never as the raw value. |
| `actor` | A display string the instance chooses. **Never an address** — the home does not hold addresses (§3.2) and must not receive one. An instance with no display name for the actor omits the field, and the row renders without an attribution. |
| `at` | RFC 3339, from the instance's clock. Used for ordering only. |
| `truncated` | The instance had more and capped. Rendered as "more in Sheets", never silently dropped. |

**A hard body cap, with its number carrying its reason in a comment** —
§9's contract item 6, which the home spec's first draft failed to adopt. The
home reads at most that many bytes and treats an overrun as a failed pull
(§7), not as a truncated parse. An instance is a consumer-supplied endpoint;
a front door that will read an unbounded body from one has a memory-exhaustion
bug on its most public page.

**Empty is a first-class answer.** `{"items":[]}` with `200` is what a person
with no grant gets, and what a quiet team gets. It is **not** what a broken
team gets — see §7, which is the entire point of ruling 5.

**This spec owns the wire shape, and the suite contract module does not
duplicate it.** Suite ruling 7 puts every inter-app call in one Go module with
golden fixtures as the interface, which raised the question of whether call 7
moves there. Settled with the doctrine session on 2026-09-05: **it does not.**
Call 7 is absent from the module's first step, which builds the
sibling-to-sibling calls only, and the doctrine's contract table records that
this section owns the shape. When call 7 is built, **the module's fixtures
cite gates 11 to 21 rather than restate them** — the reason being the one
suite §2 is about, that a restatement is a second copy free to drift, and a
fixture that has drifted from the gate it was written for is worse than no
fixture because it passes.

Two consequences worth stating. **Gate 16's cacheable-response case is now a
suite-level concern**, recorded as item 11 on the doctrine's leak list,
because the failure lives in the platform edge *between* two apps and neither
app's own tests would ever see it — the home's tests do not run the edge, and
the instance's tests do not know a second person exists. And **an instance
implementing call 7 is conforming to this document**, not to a module, until
that module carries it; a spec is a poorer interface than a fixture, which is
§12's problem and not a reason to move the shape somewhere it would be
restated.

---

## 5. Authentication, and why it needs nothing new

**Who mints, and where the key is.** Home spec §4 puts assertion minting on
the **credential origin**, and the front door is on the **directory origin**,
which obtains assertions like any other consumer. The fan-out therefore mints
N assertions per render from a page that is not the minting origin. Say which
this is: **one process serving two hostnames**, so the signing key's exposure
is unchanged from §5.3 and no cross-origin mint call exists. **Nothing on
main establishes that today** — AGENTS.md says one binary, and §5.3 gives the
directory origin its own `jti` store as a verifier — so §12 carries it as a
thing to settle. It is load-bearing: split the origins and the directory
needs a mint call and a different key story — and note the
consequence, that the directory origin is now also the surface parsing N
consumer-supplied JSON bodies per render (gate 16).

**The pull carries a §5.3 assertion, unchanged.** Ed25519, `aud` bound to the
answering instance's app and host, `sub` the person ref, `iss` the credential
origin, `kid`, 120-second expiry, single-use `jti`.

The only difference from the existing flow is transport: it rides an
`Authorization` header on a server-to-server request instead of an auto-POST
through the browser.

**Why that transport swap is safe, specifically.** An earlier draft claimed
"every property §5.3 relies on is a property of the token, not of how it
travelled." **That is false, and §5.3's own last line is the counter-example:**
"Delivery is an auto-POST, not a query string, so assertions stay out of edge
logs, browser history and referrers" (the pre-2026-09-05 wording; main now
reads "browser history and referrers") — a property of travel, not of the
token. The conclusion survives on the specific argument rather than the
general one: an `Authorization` header achieves the same two things an
auto-POST still claims — its third leg, edge logs, having been removed: §5.3's "edge logs" reason was
retracted on main on 2026-09-05, since no platform request log carrying
paths was found. There is no browser involved so there is no history, and there
is no `Referer` on a server-to-server call. **What must never happen is the
assertion moving into the query string** — the failure §5.3 was guarding
against, which a header avoids for the same reason a POST body does.

**Why not a service token.** A per-instance home→instance bearer would be a
further credential to mint, store, rotate, revoke and leak, and it would
prove only that the caller is the home — not *which person the call is for*.
The assertion proves both in one artifact the suite already verifies. Ruling
3.

**The instance decides capability entirely from its own grants** — §5.5,
verbatim and unmodified. A valid assertion for a person with no grant on that
instance returns an empty list. **This is the answer to §2's objection 2**,
and it is an answer precisely because the home does not participate in it.

**Per-verifier `jti` stores stay sufficient.** Audience binding means an
activity assertion for `acme.sheets` is not valid at `bigco.sheets`, so no
shared store is needed — §5.3's reasoning carries over untouched.

**The assertion will carry `roster_epoch`, and call 7 must decide nothing from
it.** Home ruling 23 adds the epoch to §5.3's claims table as a narrow
exception to §5.4's boundary test — narrow because the epoch is not a closed
enumerated set and does not fail cosmetically, and admissible because 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.

Call 7 rides the same §5.3 assertion, so it **inherits the claim whether it
wants it or not**. The rule for this path is therefore explicit: an instance
answering call 7 must not withhold, include, filter or reorder a single row on
the strength of the epoch. Refetching a roster is not a decision; withholding
an activity row is. An instance that let the epoch change its answer would be
an instance making a trust decision from a home-supplied claim, which is the
exact thing §5.4 forbids and the reason the exception is written narrowly.
The suite doctrine's §5.3 scopes the exception to the call-8 path and nothing
else. **Gate 21.**

**Rate limiting is the instance's.** The home is one caller and a
well-behaved one, but an instance must not assume that; a compromised home is
in scope for every instance's own limits.

### 5.1 It must not become a visibility oracle

The escritorio verdict's finding 5 named this against Option B, whose
endpoint answered *"of these refs, which may person P see?"* — a shape that
turns into an enumeration oracle the moment its caller's token leaks. **The
first draft of this spec did not address it.**

Call 7 is a narrower shape, and the narrowness is deliberate:

- **The caller supplies no refs.** It asks "what is this person's, recently?"
  and the instance chooses the rows. There is no set to probe with.
- **The subject is not caller-chosen.** It rides a signed, audience-bound
  assertion the home mints. Forging one means holding the credential
  origin's Ed25519 private key, at which point the suite has lost anyway.
- **No grant and no such person are the same answer** — `200` with an empty
  list. An instance must not distinguish them, or the endpoint answers
  membership questions about arbitrary person refs. **Gate 18.**
- **Errors carry no detail.** "Not for you" and "no such thing" are one
  response, per §12's gate 4 pattern of 404 rather than 403.

**Re-derived against home ruling 23, because the ground it stood on moved.**
§2.2 used to say the home *never holds an authorization model*, and an earlier
reading of this section leaned on that: the home cannot be an oracle about
who may see what, because it knows nothing about who may see what. Ruling 23
replaces that clause with *never holds a per-item grant*, and gives the home a
membership roster — so the home now does hold a fact about standing, and an
argument from the home's ignorance is no longer available.

The conclusion survives, on the two grounds above and only those: **the caller
supplies no refs, and the subject is not caller-chosen.** Neither mentions
what the home knows. This is a better footing than the one it replaces, not a
weaker one — the old argument depended on a property the home might later
acquire, and then the home acquired it. What the roster does add is a separate
disclosure, priced in the suite leak list's item 9 and in home §0's amended
gate rather than here: publishing the home's database now identifies each
team's owners. That is a fact about *team standing*, which the home is now
the record for; it is still not a fact about any item, and call 7 asks about
items.

**What a leaked home does yield**, said plainly: an attacker holding the
signing key can mint an assertion for any person ref and read that person's
recent titles from any instance they have a grant on. That is strictly less
than what the same key already buys — it can mint a *session* at those
instances — so call 7 widens no boundary. It is not a new hole; it is inside
an existing one, and §2.1 of the home design already prices a compromised
home.

### 5.2 Guests

A guest — `kind: guest` in the directory (§6) — has a person ref and a grant,
so they get a front door and their guest teams appear on it. This needs no
special case, which is itself the argument for the design: the instance
answers from its own grants, and a guest's grants are exactly the rows it
returns.

What a guest sees is already disclosed: §6 rules that a guest grant discloses
the granting team's name, "in the sharing app's share dialog, in words". No
new disclosure is created here.

**A link-share viewer has no person ref** (Sheets §8.3 — "a view link mints
nothing"), so they have no front door and no rows. Correct by construction.

---

## 6. Fan-out, and the cost this design pays

**The viewer's own directory rows in the selected team — not the team's
apps.** The distinction is a leak, not a nicety, and an earlier draft of this
line got it wrong. Home spec §11: "Tiles come from the viewer's own rows,
never from the label… a guest in `acme.docs` would learn that Acme also uses
Sheets, which is another person's row and what gate 4 forbids." Fanning out
to the team's apps would ask `acme.sheets` on that guest's behalf, and
ruling 5 would then render "acme · sheets — waking" on their front door —
disclosing the very thing §11 forbids, through the honesty mechanism. §8.1
already had this right; §6 and §2.1 did not.

So a member of a team in three apps is three hosts; a guest with one row in
that team is one host. The home fans out in
parallel, with a per-request deadline, and renders what arrived.

**Zero JS (§11).** One server-side fan-out, no progressive loading, no
skeleton that fills in. The page is complete when it is sent.

**The client is hardened explicitly, because Go's defaults are wrong for
this.** `http.DefaultClient` has no timeout and follows redirects. Required:
the deadline is a **context covering connect, TLS, headers and the full body
read**, not just the response headers; **redirects are never followed** — a
3xx is a failed pull, because an instance redirecting to a document URL would
otherwise put that URL inside a `net/http` error string, which §3.4 of the
home spec rules is storage; TLS verification on; and **bounded concurrency
across all in-flight renders**, so one instance trickling bytes cannot
exhaust the process that also serves sign-in (home spec §2.1 makes it the
suite's single point of failure).

**The cost, stated rather than buried: instances hibernate and the home does
not.** §2.1 makes the home the one app that never sleeps; every instance is
the opposite. §5.3 sizes assertion expiry at 120 seconds precisely because a
cold wake is "process start plus decrypt plus migrate-check" and "can exceed
a few seconds". So:

- **The first front-door load of the day will mostly miss its deadline.** Not
  an edge case — the normal morning.
- **Every load warms the instances the viewer has rows in**, which changes the
  hibernation economics the platform assumes — and **instance-hours are
  billed to the instance's account, not to the person opening the door**. A
  guest with one row in `acme.docs` would otherwise wake Acme's instance on
  every front-door load, at Acme's expense. So there is a **minimum pull
  interval per (person, instance), enforced even on an explicit refresh**;
  the interval starts on a **completed** pull, not on an attempt, so a timed
  out instance can be retried immediately — otherwise the refresh affordance
  would be dead during exactly the window it exists for.

**Three timers, and which governs.** The **minimum pull interval** decides
whether a pull may be attempted at all. Within that, the **refresh TTL**
decides whether the home bothers. The **hard maximum age** (§7) overrides
both: past it the rows go, whatever the other two say.

Neither the cold-wake latency nor the wake economics is fixed here. Both are **mitigated and disclosed**:

- The RAM cache (§7) makes every load after the first instant.
- The waking state is rendered honestly (§8), so a slow first load is legible
  rather than mysterious or wrong.

**A fan-out budget with no specified behaviour is not a budget.** §12 lists
one as an open number. The number can wait; **what happens at the limit
cannot**, because an un-asked instance must render as its own kind of gap or
ruling 5 is breached silently — the person cannot tell "nothing to say" from
"never asked". Un-asked instances render as a distinct gap with a refresh
affordance, and the budget is per render, not per person.

**Two absences, and they are not the same.** An app the registry does not
mark as participating (§10) is **never asked and renders nothing at all** —
it is not part of this feature. An instance dropped by the budget **would**
have been asked and renders the distinct gap. A builder needs both sentences
or they will collapse into one.

**The named escape hatch**, if the morning experience proves miserable in
use: the rejected hybrid — instances push a *contentless* ping (person ref,
app, team, coarse timestamp, no title, no object ref) so the home knows which
instances have news and wakes only those. It bounds the fan-out to instances
that actually have something to say, at the cost of a small at-rest activity
table the home does not have today. **Trigger: the first real complaint about
front-door latency, or a measured p50 over one second on a warm fleet.** Not
built now, because it adds custody to solve a problem we have not yet
measured.

---

## 7. The cache is in RAM, and that is load-bearing

Ruling 4. The home keeps, per person, the last successful pull per instance,
in process memory, with a short TTL.

- **Never at rest.** Not SQLite, not a file, not a platform cache, not an
  HTTP cache header that would let an intermediary keep it. §3's amendment
  permits rendering, not storing, and a disk cache is storing.
- **The platform edge is a shared cache upstream of both ends, and it must
  be closed on both.** It caches any 2xx GET whose response carries a
  positive `max-age`, keyed on host, path and query, shared across
  every visitor of a host — and it does **not** refuse on a request
  `Cookie`. So: the front door sends `Cache-Control: no-store` (the home
  spec's gate 10), **and call 7's contract requires instances to send
  `no-store` too**. An instance answering `public, max-age=60` would have
  the edge serve person A's rows to the home's next pull for person B with
  the same query string, and the home's own cache key — person ref and
  instance host — cannot see that happen. **A cacheable response is
  therefore a failed pull**: the home discards it and renders a gap. Gate 16
  covers it.
- **Precedent:** §3.2 already handles readable addresses "transient in RAM
  elsewhere". This is the same category, and it is the only category the one
  rule leaves open.
- **A restart empties it**, and that is correct rather than unfortunate.
- **`since` comes only from this cache, and is never persisted.** A future
  "remember the last pull per (person, instance) across restarts" is the
  contentless activity table §11 forbids, arriving as an optimisation rather
  than as a decision.
- **Keyed by person ref and instance host**, so one person's cache can never
  render on another's page — the same isolation gate 4 already asserts for
  the directory.

**Two ages, not one, because an earlier draft contradicted itself.** It said
the TTL bounds staleness "by a number we choose, not by a hibernating fleet's
wake schedule", and then said a failed pull never evicts a good cached
answer. Both cannot hold: if failure preserves rows, a person removed from a
team keeps seeing its titles until that instance next *answers*, which is
exactly the hibernation tail the first sentence disclaims.

- **Refresh TTL** — how long before the home tries again. Short.
- **Hard maximum age** — after which rows are **discarded regardless of pull
  outcome**, failure included. This is the one that bounds the lie, and it is
  the number the removed-from-a-team case turns on.

**A failed pull does not evict a good cached answer that is still inside the
hard maximum.** Past it, the rows go and the gap is rendered (§8), because
"here is what I knew" stops being honest once "when I knew it" is old enough
that the grant may be gone. Gate 20 drives the hard maximum, not the refresh
TTL — a test against the refresh TTL proves the easy half.

**This section is the suite doctrine's one carve-out from "never cached", and
the carve-out is worded the way it is because of what it costs.** Suite ruling
3 holds that authorization is never cached at any scope — not per session, not
per page, not for the life of a warm process. Revision 4 of that document
reached the opposite verdict about this section in two different places, and
the reconciliation, settled with the doctrine session on 2026-09-05, is in its
§4.3: the rule governs an authorization **answer**, and an S2 `VISIBLE` held
past its request is a standing capability that *grows*, because the holder
re-uses it against items and people the app of record never spoke about. This
cache is not that. It holds rendered output for one person, the home holds no
grants, and it can only ever re-serve the same rows to the same person.

**The distinction is real and it is not free, and both documents say so rather
than resting on it.** Inside the cache's life a person removed from a team
still sees titles; calling that "not cached authorization" would be word-play.
That is what the hard maximum is for, and why the doctrine requires the bound
to exist while refusing to carry the number — the number is a property of the
consumer, and **this section is where the consumer states it**, which is why
§12 lists it as one of the four that must be settled before this ships.

**What this closes.** The doctrine listed *"the activity path's cached
authorization after revocation"* among the things it openly had not answered.
It is not open. It is bounded, the two ages are the bound, and the remaining
work is a number rather than a rule.

---

## 8. What the front door renders, including when it cannot

home spec §11 is now one page — a grid of apps for the selected team, with a team switcher in the header (ruling 17). This adds a recency
section above or beside it — **layout is the front-door session's call, not
this spec's.**

Every row: title, app, team, verb, actor if known, relative time, linking to
the instance's own URL. Merged across the viewer's own instances in the selected team,
ordered by `at` descending,
capped.

**And then the part that matters.** Ruling 5:

> **An instance that did not answer must be visually distinguishable from an
> instance with nothing to say.** A quiet team and a broken team must never
> render the same.

This is the direct answer to the failure mode Fable named — *a plausible
empty list, not an error*. A feed that silently drops what it could not fetch
is the worst available shape, because it looks exactly like a slow week and
nobody files a bug against a slow week.

**The suite doctrine generalises this to every inter-app pull**, and its
wording is worth having here because it names the pressure that breaks the
rule rather than just the rule: *"a reliably slow sibling is a suite health
problem the consumer surfaces, never one it routes around by caching
authorization"* (suite §6.3). The temptation ruling 5 exists to resist is not
laziness, it is kindness — an honest gap looks like a defect, a silent
omission looks like a clean page, and the second one ships. That sentence is
this ruling read back from the doctrine, and §7's hard maximum is the same
principle applied to time rather than to a missing instance.

So each instance that failed to answer gets a row of its own, in words:

- `acme · sheets — waking, refresh` (deadline missed; likely cold)
- `acme · calendar — unavailable` (error, refused, or unparseable)
- `acme · sheets — showing what we had at 09:14` (cache served, live pull failed)

Each with a refresh link, which is a plain `GET` and works with scripts off.

**And when nothing at all arrives**, the page says so — *"we couldn't reach
your apps just now"* — rather than rendering an empty feed. The empty state
and the everything-failed state are different pages, and a person can tell
which one they are looking at.

### 8.1 A named gap discloses nothing, including to a guest

Ruling 5 is the most visible thing in this document, so the objection it
attracts is answered here rather than left to be derived. **A named gap
renders a team and app name — does that leak, particularly to a guest?**

The escritorio verdict's finding 7 (*suspected*) flags that guests fit no
option cleanly: a guest must see their one item with no hint the team's other
apps exist. Against ruling 5 that looks alarming. It is not:

- **The fan-out set is the person's own directory rows** (§6), so the home
  only ever names a team and app that person already has a row for.
- **Call 3 already hands them the same list directly** — "every team across
  every app, with `kind`". A named gap discloses strictly nothing the
  directory would not answer if asked.

So the gap is bounded by the directory, and the directory is the thing the
front door is *for*.

**One consequence to expect rather than fix.** The directory is a hint with a
drain lag (§6), so the home will sometimes call an instance the person was
removed from weeks ago, or one whose instance was deleted (home spec §9's call 2, second
event). §5.5 makes that safe — the answer is an empty list, or a 404 — but
ruling 5 then renders a named gap for a team they have left, or an
"unavailable" for a host that no longer exists.

**That is a wrong-looking page with a correct mechanism behind it**, which is
precisely the shape that gets bug-reported and then "fixed" by widening
something. It is recorded here so the fix, when someone reaches for it, is
the right one: **prune the stale directory row**, never suppress the gap.
Suppressing gaps is ruling 5 in reverse.

---

## 9. Testing gates

Numbered from the home spec's **ten**. `main` has gates 1-10, the tenth
being no-store (ruling 22), so these start at 11.

11. **No pulled content at rest — driven through the failure paths, not the
    happy one.** A full front-door render against seeded instances leaves no
    title, URL or actor string in the SQLite file, the WAL, any log line,
    error string, metric label or on-disk artifact. Extends gates 1 and 9;
    gate 1's corpus grows a set of known titles.

    **The happy path is the exception here, so testing it is close to
    testing nothing.** §6 says the first load of the day will mostly miss its
    deadline, which makes failure the normal morning. And the natural Go
    spelling of a failed parse —
    `fmt.Errorf("parsing activity from %s: %w", body, err)` — puts the entire
    response body, every title in it, into an error string, which §3.4 rules
    is storage. A gate that renders only well-formed responses stays green
    while exactly that ships.

    So the gate drives **canary titles through every failure path**:
    malformed JSON, a body over the cap, a bad `at`, a wrong content type, a
    `url` on a foreign host, a connection reset mid-body, and a timeout. The
    canary must appear in none of the places above. This is the class gate
    9's own review turned up three of, and each one was a guard that looked
    complete.
12. **A silent instance is a visible gap** — timeout, empty body, **and a `404` from an app that DECLARED participation** (§10's registry flag), which must render as a gap rather than as nothing. With one instance timing out and
    one returning empty, the render distinguishes them. This gate fails if
    both produce the same output. **The most important test in this
    document.**
13. **The activity assertion is audience-bound.** One minted for
    `acme.sheets.oficina.works`, replayed against `bigco.sheets.oficina.works`,
    fails. Extends gate 3.
14. **Two-person isolation on the pull.** Person A's assertion returns only
    A's rows; a cached answer for A never renders on B's page. Extends
    gate 4.

    **And the guest case, which is the one that bites.** A guest with a row
    in `acme.docs` only: assert the render fans out to `acme.docs` and to
    **nothing else in that team**, and that no gap naming `acme · sheets`
    appears. Fanning out by the team's apps rather than the viewer's rows
    would disclose that Acme uses Sheets through ruling 5's honesty
    mechanism — home spec §11's gate 4 concern, arriving by the back door.

15. **The credential origin renders no pulled content.** Extends gate 5.
16. **A hostile instance response cannot hurt the front door.** Oversized
    body (cap enforced), a `url` on another host (rejected), an unknown
    `verb` (renders `changed`), HTML in `title` and `actor` (escaped), a
    malformed `at` (row dropped, instance marked, page still renders), **a
    cacheable response** (`public, max-age=…` — discarded and rendered as a
    gap, §7), and **a redirect** (never followed, treated as a failed pull,
    §6). An instance is a consumer-supplied endpoint and this page is the
    suite's most public surface.
17. **Scriptless.** The recency section and every refresh link work with
    scripts stripped. Extends gate 6.
18. **Not an oracle** (§5.1). A person ref with no grant and a person ref
    that does not exist produce byte-identical responses from an instance.

19. **One assertion, one host.** The fan-out mints a separate assertion per
    instance, and a test asserts that no two requests in one render carry the
    same `aud`. §11's first row is the reason this is a gate and not a note.

20. **The hard maximum age is enforced through failure.** Seed a cached
    answer, make the instance fail, advance past the hard maximum, and
    assert the rows are gone and a gap is rendered — not that they survive
    because the pull failed. A test against the refresh TTL proves the easy
    half; this is the half that bounds the lie (§7).

21. **`roster_epoch` changes no call-7 answer** (§5). Run the same pull twice
    against the same instance with the same grants, once with a matching
    epoch claim, once with an epoch far ahead of what the instance holds, and
    once with an unparseable one. All three responses must be byte-identical.
    An instance that filters, withholds or reorders on the epoch fails —
    that is a trust decision from a home-supplied claim, and §5.4 forbids it.
    The exception the epoch rides on is scoped to call 8; this gate is what
    keeps it there.

---

## 10. Sequencing, and what blocks it

**Blocked, and not marginally.** Call 7 needs:

- **§13 step 3** — assertion minting. The pull has no credential without it.
- **§13 step 4** — the directory and person refs. The home cannot know which
  instances to ask, and `sub` has nothing to carry.

Step 2 (sign-in) has a reviewed plan on main at `docs/superpowers/plans/2026-09-03-home-signin.md` (`b92c9b8`) and cannot begin
until `HOME_DEK` is minted, which only Paul can do.

**This is spec-now, build-later, and the slot is after step 4** — a step 10 in
§13's list. It has no claim on anything earlier, and it must not acquire one:
the identity layer is the suite's critical path and this is a front-door
nicety.

**Consumers.** Call 7 is implemented by every instance that wants to appear.
Sheets is the near-term one — §10.2 there is already a recency list, so the
endpoint is a projection of a query it has. Docs likewise.

**The projection is not free, and the endpoint should say so.** Sheets §2.1
seals titles under its DEK, so answering call 7 decrypts once per row rather
than reading a column. That is a real cost on a list query and it is the
argument for the instance capping `limit` hard and low — the front door wants
a handful of rows per app, not a page of them.

**Participation is declared, not inferred.** An earlier draft said a `404`
from an instance means "this app does not participate", rendered as nothing.
That is ruling 5 exactly backwards: a participating app that loses its route
in a deploy, or a hostile one, would vanish silently from every front door —
"a plausible empty list, not an error", which is the failure ruling 5 exists
to prevent. `404` is already overloaded too: §5.1 uses it for "no such
thing" and §8.1 for a dead host rendered *unavailable*.

So participation is **a flag in the home's compiled-in app registry** (home
spec §11), beside `provisionable` and `integrated`. An app that does not
declare it is never asked. An app that declares it and then answers `404`
is **a gap**, rendered as one, and gate 12 covers that case.

---

## 11. Deliberately not built

| Thing | Trigger |
|---|---|
| **One assertion reused across the fan-out** | **Never.** Written here because someone will propose it in good faith, citing §6 — six signatures on a latency-critical path is exactly the optimisation §6 creates the incentive for. Ruling 3 is safe *only* because `aud` is app-plus-instance-host and §5.5 makes each instance decide from its own grants. An assertion valid at every host in a person's fleet is a cross-instance bearer, which is full impersonation everywhere they have a grant. Ed25519 signing is microseconds; the latency is the network and the cold wake. Gate 19. |
| The contentless-ping hybrid (§6) | A real latency complaint, or measured p50 over one second warm |
| Any at-rest store of pulled **content** — titles, URLs, actors | Never, under §3's amendment as written. Changing this reopens §0. |
| Any at-rest **contentless** activity table | Only as §6's escape hatch, on §6's trigger. It is new custody — a per-person, per-team timestamp series, which is §3.3's de-anonymisation vector at fine resolution — so it is a decision, not a performance tweak. |
| Search across the feed | Never at the home. `memoria` is where cross-corpus search lives, and it has its own custody story. |
| Filtering by app, team, person, or date | Someone asks. It is rendering, not custody, so it is cheap — but the front door is a front door. |
| Notifications, unread counts, marking read | Any of these needs per-person read state at the home, which is new custody. Argue it separately. |
| Calendar and mail as sources | `oficina/calendar` has a repo and no spec; `oficina/mail` has neither. And the structural reason, not just the missing spec: an `integrated: false` app verifies no §5.3 assertion (home §11, §14), so it cannot answer call 7 at all. |
| An operator view of anyone's feed | Never (§2.2, §0). |

---

## 12. Open

- **The review pass.** AGENTS.md requires the highest-capability model
  available, recorded by name. **Run — four passes by Fable 5.1, the fourth
  approving.** The three that returned CHANGES NEEDED are summarised in the
  status block. What remains open below is design, not review. **Reopened in
  one respect:** all four passes predate the coordination revision, so §1.1,
  §3's retirement of the future tense, §4's module paragraph, §5's epoch rule,
  §5.1's re-derivation, §7's carve-out and gate 21 carry no review at all.
- ~~`AGENTS.md` is stale against the home spec on three counts.~~
  **Closed 2026-09-05:** `main` carries all three retractions at `5a42f2d`,
  and `AGENTS.md` there is current.
- **Numbers.** The per-instance deadline and the body cap are tunables with
  specified behaviour at the limit, and can be chosen from measurement. Three
  are **not** merely tunables and must be settled before this ships:
  - the **hard maximum cache age** (§7), which is what bounds how long a
    person removed from a team keeps seeing its titles — the refresh TTL
    does not;
  - the **cache bounds** (§2.1): maximum person entries and maximum bytes per
    person, since the cache is cross-person;
  - the **minimum pull interval** per (person, instance) (§6), which is what
    stops a guest's front door billing someone else's account;
  - the **refresh TTL** (§7), which home spec §15 lists as open here and
    which the earlier draft of this bullet dropped from both lists.

  The fan-out budget's *number* can wait; its behaviour at the limit is
  specified in §6 and is not open.
- **An upstream discrepancy to file on `oficina/suite`.** Its call-7 row
  says fixtures cite this spec's gates **10–20**; this spec's gates run
  **11–21** — there is no gate 10 here (the home spec owns it) and gate 21
  is new. §4's "gates 11 to 21" is right; the doctrine's row is not.
- **Two upstream stalenesses, also to file.** The doctrine's §14 first
  bullet still calls ruling 15 unruled while its own §1 attributes it to
  Paul; and §1.1 consumes 15 as settled.
- **Whether the credential and directory origins are one process.** §5
  assumes they are, and the signing-key story depends on it. Nothing on
  main says either way.
- **Two platform facts** the "in RAM" claim leans on and which were not
  established: whether swap is configured, and whether core dumps are
  disabled. Both must be answered before this ships (§2.1). Both are
  questions for the platform, not a guess in a spec. *(An orphaned fragment
  of this bullet, left by an earlier edit, is repaired here rather than
  deleted, since deleting it would have silently dropped the clause.)*
- ~~**Which commit revision 4 of the suite doctrine is.**~~ **Closed
  2026-09-05.** It was neither of the two named. Revision 4 was `210312f`;
  **`574cc84` then changed a §14 open item and A.1's ruling number 22→23,
  and `bf0eeb8` changed §4.3's call-7 carve-out and §5.3's epoch scoping**
  — the second of those is squarely normative and this spec consumes it —
  **all without bumping the revision, which that document's own gate 9
  forbids.** It became revision 5 at `f627359`, and is revision 7 at
  `2b78680` as this lands; **§1.1 carries the current citation** rather
  than this bullet, which is why this bullet no longer names one. Verified against that
  repository's `main` and its checkout rather than taken on report. The
  cause is recorded in §1.1 because it is a repository hazard rather than a
  spec one. Checked and clear on this spec's side: its only memoria reference
  is a general one in §11 and does not touch memoria decisions 13 or 14, its
  gate 26 or its §8.4 — the four the doctrine found resolving only on an
  unmerged worktree, and which remain open at `oficina/memoria` discussion 1.
- **When call 7 joins the suite contract module** (§4). It is deliberately
  absent from the module's first step, so until it arrives an instance
  implementing call 7 conforms to a document rather than to a fixture. That is
  a worse interface, accepted for now because the alternative was restating
  the shape in two places.
- **Front-door layout** is the other session's. This spec deliberately does
  not draw it.
- **Whether `actor` is worth having at all.** It is the field most likely to
  be absent in practice (an instance needs a display name it may not hold),
  and a feed of "Q3 Budget — edited" reads fine without it.
