oficina / memoria Public
π€ memoria
The Oficina suite's memory. One instance per team.
This file is binding. Where it disagrees with a general skill, this file wins. Where it disagrees with the design spec, the spec wins and this file is wrong β fix it in the same commit.
The design: docs/superpowers/specs/2026-09-03-memoria-design.md. Its Β§1 is the settled-decisions table, Β§1.2βΒ§1.11 record what ten independent reviews changed. Don't relitigate a settled decision in a drive-by.
---
The one rule
> memoria never speaks a passage without asking whoever owns it whether > this person may see it, right now.
Sibling content: the sibling decides, per person, per query (Β§4.1). Content with no other owner β Notion, Drive, mail, uploads β memoria owns outright and says so (Β§4.2).
The second rule is Sheets': the bucket never holds a readable passage. Chunks, titles and headings are sealed. So is the index, which is why it lives in memory and nowhere else: FTS5 keeps term postings in plaintext.
---
Two rules this app has already broken once each
A guard you have never seen fail is a guard written to pass. Three of the first five gates here could not fail for the thing they named β one grepped main.go for os.Exit near a variable and a neighbouring error path supplied it; one checked for the string "result", which is a substring of "0 results"; one scanned a directory the index was not in. Two more named a test that had been renamed away. After writing any gate, break the thing it guards, watch it go red, revert. Do it per path, not per file, and say in the test comment what you mutated and when.
And do not choose your own mutations. Gate 17 shipped with three, each verified red; a reviewer then tried five ordinary chi idioms and every one passed green, including a route that served a declared path outside the session gate. Three green mutations picked by the author prove the author is consistent, not that the gate works β they are the cases already defended against while writing it. Have someone who did not write the gate pick the mutations. Β§Review's argument, one level down. (Β§1.9.)
Assert the state, not the copy β and if your own mutation cannot reach a guard, the guard is the finding. Both of these were learnt on the same afternoon, from the two mutations out of twenty that survived while building Β§4.1's policy layer (2026-09-06).
Outcome.Record filing the silent NOT_VISIBLE rulings alongside the loud ones rendered an identical page β nothing prints them β while quietly accumulating a per-app count of passages the reader may not see, one switch arm or debug print from being the leak Β§4.1 forbids. The test read Lines() and could not tell the two apart; the test that catches it reads the struct.
And scatter's short-slice fallback could not be reached by any fixture, because the function above it is total. Changing that fallback from UNKNOWN to VISIBLE left the suite green. A defence nothing can reach is the same object as a guard written to pass, whatever the intent β so either make it reachable (extract it, call it directly, as scatter now is) or delete it. Leaving it in place with a comment is the third option and it is the one that ships broken.
A count that fails is not a count of zero. chunkCount returned 0 on error, and the page then told the reader "so this is a real absence rather than an empty index" β the codebase's own worst failure shape, asserted on the line written to prevent it. Anything that cannot be determined says so; it never renders as a confident zero.
---
Mechanical rules
Data
- Never
scope.Ownedorscope.OwnedBy. Both are single-column equality onuser_id, and memoria's documents and chunks are team-shared. Applied to them a list returns only the creator's rows and fails as a plausible empty list, not as an error. (The scaffold's AGENTS.md said the opposite; it was wrong for this app.) - A row that isn't yours 404s, never 403s.
- Scope
tx, never the outer handle, inside transactions. - Never bind a request body onto a GORM model. Explicit
map[string]anyplus a.Selectallowlist. - Migrations are additive-only. New code over an old DB is always safe.
- No document content, title, heading, snippet or query text in any log line, error string or metric label.
carlos logsis platform-held storage we cannot grep. Gate 4 enforces it; it was missing once, and what it found when written was that we were logging sign-in links. - A model provider's response body is content too. An OpenAI-compatible 4xx can echo the input that caused it, so the provider's error path is a content path. Never log one verbatim β status and a fixed string. Gate 31 (spec Β§10); nothing calls a provider before slice 2, so this lands with the first call and not after it.
- An ADDITION to derived state is made after the transaction commits, under
context.WithoutCancel. A client closing the tab must not leave the index disagreeing with the database. - A REMOVAL from derived state is made before it, and
Store.Deletecarries the argument. The two orders fail in opposite directions: indexing early can leave the index holding passages the database does not have, which is a citation to text that is not there; un-indexing early can only leave a stored document unfindable until the next boot, which the app says out loud. Where they disagree, the order that cannot produce a live citation wins. Delete was written the other way and did produce one β found by a code review, 2026-09-04. - A hit is not a citation until the database says the passage is still there.
Store.Searchdrops any hit whose chunk row is gone. Ordering makes the divergence rare; only this makes it harmless, and it is what makesErrIndexLag's sentence true β without it, "stored but not findable" was said about a document whose previous revision was still answering searches.
The suite contract
- Read the route out of the module, never out of memory.
RouteS3is/suite/s3and the item id is a path segment β/suite/s3/{item_id}?revision=β which the constant's own line says and the conformance harness fires. Written as?item_id=it 404s against every conforming sibling, and memoria reads a 404 as "not team-visible, or gone".url.PathEscape, notQueryEscape: a space is%20in a path and+in a query. - A cap on a response has to be detectable, not just applied. S1 and S2 get that free β truncated JSON does not parse. An S3 body is plaintext, so read one byte past the cap and error on it, or a large document is silently indexed as its own first 2 MiB.
- A sibling document's coordinate is
(sealed item id, sibling revision).Document.Revisionis memoria's own counter and is not it, andExternalIDis a one-way fingerprint that cannot be asked about.
Sealing
internal/memoria/sealis the only encryption surface. One HKDF purpose per kind of thing sealed.- A content hash is not a coordinate.
external_idwas a bare SHA-256 of the body, which let anyone with the bucket confirm the team held a document they already had. Identifiers derived from plaintext are keyed (seal.Fingerprint) or they are content.
Frontend
- No build step. Every affordance works scriptless.
- Colours are tokens, never literals, so light and dark resolve through
theme.css. Gate:TestNoColourLiteralsInAppCSS. layout.htmlmust linktheme.css.tokens.cssdefines eleven tokens, none of them colours; without the theme every colour resolves toinitialand whole shorthand declarations go invalid β rules vanish rather than the page going grey. The v0.17.0-era scaffold omitted this link, which is how the rule was learnt;rastrillo newon v0.23.0 emits both, verified 2026-09-05. The rule stays because the failure is silent, not because the scaffold still causes it β andrastrillo doctornow checks the vendored files this app copies.- The attribute grammar (
rst-btn="primary compact"), never the class spelling. Utilities stay classes. - The design system has a vocabulary, and a word outside it renders as nothing.
rst-callout="danger"styles nothing: tokens.css carries the tone indata-tone, so a value on the attribute itself matches the base selector and no tone. Every flash in this app rendered untoned for months and three tests pinned it there, written from the same guess as the markup. Grep the stylesheet for the attribute before writing it. Gate:TestEveryTemplateAttributeIsInTheDesignSystem, which checks names and whether a value is one anything selects on. - Look at the page.
tools/driveserves the app with sign-in off so a browser can see it. It found three bugs in its first run that the whole Go suite passed over β a heading trail rendering as "βΊ Revenue", a snippet repeating its own heading, and a submit button floating over the textarea. A template test proves markup exists, not that it renders. It then found two more in the sources page the day that page was built: a warning-toned callout containing the words "No refusal anomaly", because one flag carried two unrelated states; and a warning on every row of every freshly-woken instance, because "not polled yet" is the ordinary state of a wake. Neither changes a string a test looks for. If you add a section to a page, add it to drive too β the section that is not seeded is the section nobody sees.
rastrillo
- rastrillo is
amadan.net/rastrillo/rastrillo. The old path,github.com/carlosframework/rastrillo, stops at v0.23.0: code importing it gets no fix, and its own checks cannot tell (Memoria zoomed iOS on every tap for a month that way).TestNoPreMoveRastrilloallows one import of it, native.go's second sessions handle for rastrillo/native's server, until that server moves;native_exchange_test.goproves the bridge. The app itself reads sessions througha.sess, the new package. The CLI is a go.modtool:go tool rastrillo β¦, never arastrillofrom PATH. - After
go tool rastrillo doctor --fix, rungo run ./scripts/writeestiloand delete what it added. At 29e4491 --fix overwrites rastrillo.js with the old shim (the old busy rule inside, which fights estilo's busy.js) and adds select.js, datetime.js and calendar.js, which no page links.TestBusyFilesAreEstilosandTestNoUnusedRastrilloScriptsOnDiskfail until both are done. At fb806a2 --fix refuses outright while go.mod still requires the old path (native.go's handle above): copyui/tokens.cssfrom the pinned module by hand, andTestVendoredAssetsMatchTheLibrarysays when. - The CSP is rastrillo's default, which refuses inline style attributes (fb806a2). Nothing Memoria serves standalone carries one;
TestNoPageCarriesAStyleAttributereads every page's markup andmake browser'sTestPagesRenderUnderTheProductionCSPwatches for violations in Chromium. Draw with attributes and memoria.css, neverstyle=;memoria.ServeOptionssays why there is no policy of our own. The one exception is the native kit's approval page, whose inline<style>is allowed by hash on/native/authorizealone;TestNativeApprovalStyleIsTheHashedOnere-hashes it, so a native bump that changes its CSS fails CI rather than shipping it unstyled. - A field styled with
font: inheritunder a selector heavier than a bareinputloses tokens.css's 16px-on-touch rule, and iOS zooms on tap. Restatefont-size: max(1rem, 1em)for it under(pointer: coarse), as memoria.css does for[rst-stack]'s fields.TestNoZoomOnTap(make browser) measures every field.
Auth
requireSessionfails closed. The test seam isAppForTest, named so it cannot be reached by accident, and two gates hold both ends. An earlier version failed open and justified it with a test that did not exist.- The app refuses to start without a DEK, an instance key, owners, and either SMTP or an explicit
MEMORIA_ALLOW_LOGGED_MAIL=1β because without SMTP rastrillo logs the magic link, and that link is a live credential in storage we cannot delete.
Deploys
- One binary,
CGO_ENABLED=0, cgo-free SQLite. carlos deploy, nevercarlos ship. Flags before the binary path.make release, never a barego build, or/api/versionreportsdevforever. Poll the process, notX-Carlos-Version.
---
Review
Every spec and every branch is reviewed by a model that did not write it, before it is approved or merged, and the reviewing model is recorded. Seventeen passes so far, on two strands that ran in parallel and are counted together here because they are one project. Spec drafts 1β3 by GPT Sol and Fable; draft 4 by Fable 5.1; draft 5 by a reviewer whose attribution is disputed β it named itself as the model that wrote the draft, so that pass may not satisfy this rule at all. The slice-0 code by Fable, the drag-and-drop branch by Fable 5.1, and five on the suite strand: the Β§8.3 provider amendment, Β§8.4's shape, gate 17 with the live-key verification, the doctrine adoption, and an adversarial mutation pass on the sign-in dial gate (gate 33 since the merge renumbered it). Slice 1's S1/S3 clients, Β§4.1's policy layer and Β§6.5's fan-out by Fable 5.1 (2026-09-06): twenty-two mutations, eleven survived. The roster, the poll loop and the warm-up by Fable 5.1 (2026-09-07): twelve mutations, seven survived, and five of its ten probes found live defects rather than gate gaps. The breaker and the sources page by Fable 5.1 (2026-09-07): nineteen, thirteen survived, three live defects.
Every one returned REQUEST CHANGES. The most useful have been the ones aimed at tests rather than prose. The provider review found a rule broken in the paragraph that stated it, twice, and a fourth gate that could not have gone red (Β§1.7); the next found a product ruling argued from two misread sections of this same spec (Β§1.8); the next mutation-tested a gate its author had already mutation-tested and broke it five more ways (Β§1.9); the adoption review found a rule invented while citing the section that forbade it, plus eight contradictions in sections the commit never opened (Β§1.10); and the gate-27 pass found two live bypasses in shipped code, not mutations. The drag-and-drop review found two more the same way β by re-running the mutations their author claimed to have run, rather than taking the claim. The slice-1 review found two more, and the first is the worst kind: Content fired S3 at ?item_id=, and the module declares the route as /suite/s3/{item_id}?revision= on the line that names the constant. Against a conforming sibling every fetch would have 404'd, read as "not team-visible, or gone" β the plausible empty list on the one call that fills the corpus β and Β§8.1's S3 breaker would then have reported a permissions anomaly for a routing bug. The test pinned the wrong shape too, which is why it was green: an author's fixture is written from the same wrong model as the code, so it cannot be the thing that catches it. The next review found the same failure again in a different field: suite hosts are <team>.<app>, this repo's own documented example had them reversed, every fixture copied the example, and nothing checked β so the roster accepted an address every call would then be refused for, before dialling, forever, with a page blaming a healthy app.
The pattern is one failure wearing several coats: the writer checks their work against their own model of it rather than against the thing. Open the section you are citing. Open the constant you are calling. Run the mutation you did not think of.
A test written from the same model as the code is part of the code. It cannot catch a wrong assumption; it records one. The S3 route, gate 23's bounds, the sixth disposition born silent, the swapped host labels β each had a test, each test agreed with the bug. What found them was somebody who had read the module and the spec instead.
Grepping for a string is not reading a selector β or a route, or a constant. The callout tone was got wrong three times: rst-callout= "danger", then [rst-callout] data-tone=, and only then the rst-tone the stylesheet actually pairs with the attribute spelling. The second attempt was the commit that described fixing the first, and its tests passed because data-tone="negative" is in the stylesheet β inside the class selector standing next to the one needed. A check that greps will confirm any spelling that appears anywhere in the file. Read the selector, the route declaration, the constant.
The section you did not open is where the fix already was. Β§8.1's breaker clause was amended here on a real observation and a wrong remedy; Β§13 already carried the correct one, three thousand lines down, and it was not opened. The amendment was made in the course of fixing another instance of exactly this. Before amending a section, grep the spec for what it governs β the open-items list is where the last person left the answer.
A gate over source text proves a call is spelled, never that it is reached. The cmd/ wiring gate walks the AST for a call expression, and a reviewer defeated it twice in ordinary Go: app.UseAuth(a) inside a closure nobody invoked, and the sibling wiring behind if len(apps) > 1 so a one-sibling instance ran unwired. Both read as present. The answer is not a cleverer walk β that is a race against however the next person writes it β but to leave nothing in main worth getting wrong: move the logic into a function with tests and let the gate watch for one call disappearing. WireSiblings is that shape.
And an absence gate needs its presence half even when the absence is the point. "PolledAt was not set after a failed poll" passed against a Poll that never set it at all; the test only ever failed on page one, so neither half of the real behaviour was pinned. Assert the thing happens before asserting when it must not. When you retire something, grep for it by name β the sections you did not open are where an adoption commit goes wrong, because a rewritten section has been read twice and an untouched dependant never.
Review a merge against both parents, never against the result. The merge review found more than the four passes before it combined, and it found it by diffing the merge against each side rather than reading what came out. Reading the output cannot tell you what is missing, and a merge is the one change whose defects are all absences (Β§1.11).
And a reviewer's self-reported model id is not evidence. Record what was dispatched and what came back, and record the disagreement when they differ β twice now they have.
---
Authorship
π€ / π¨ markers, Co-Authored-By trailers. AI is a guest with a name tag: opt-in, disclosed, and every path works with it switched off β which in slice 0 means the whole product does.
Work in a worktree, and catch main up first
Take a worktree for the work. Do not work in the shared checkout.
git -C <repo> fetch origin git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main
Both halves matter, for different reasons.
The worktree, because several sessions share this machine and some share this repository, and a checkout has one HEAD, one index and one working tree between all of them. A branch switch under somebody else's uncommitted work destroys it with nothing on screen to say so, and two sessions editing the same paths disagree quietly instead of conflicting loudly. A worktree gives the work its own HEAD and its own files, and costs a directory.
From origin/main, after a fetch β not from whatever the checkout happens to be sitting on. A branch cut from a stale main is a merge conflict scheduled for later, against files somebody else has renamed in the meantime. A branch cut from another task's branch is worse: it carries that task's commits into yours, where they are reviewed as though you wrote them, and the two can only be separated by hand.
Remove it when the work has landed β git worktree remove β so that git worktree list keeps saying something true.
Written down because of 2026-09-17, in meet. The shared checkout was sitting on one task's branch while holding a different task's feature uncommitted on disk β some four hundred lines, a new migration among them β put there by a session nobody else knew was working in that tree. Two other sessions were meanwhile coordinating over the same checkout by asking each other not to touch it, which is the arrangement a worktree makes unnecessary. Nothing was lost. Nothing about the arrangement would have said so if it had been, and by then the same feature existed twice, once on each side of a rename, because the two sessions had branched from different starting points.