Imported from edisonmalasan/spectre-mail (
AGENTS.md). Install upstream withnpx skills add edisonmalasan/spectre-mail. Copyright stays with the author.
AGENTS.md
provider-terms-reviewis APPLIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-16-provider-terms-review/— proposal PR #150 mergedff7082c, apply PR #151 mergedbd34511, sync PR #152 merged9557e68, and the archive PR number left unfilled here, to be filled bygh pr viewat the next change's proposal stage — because the last one was written as a prediction before the PR existed and had to be corrected againstgh pr viewin the same sentence, which is this file's own recorded rule against a figure nobody counted.The archive moved 6 files, every one byte-identical to a pre-archive digest taken programmatically, with Git's own rename detection independently confirming it — six renames at
0 insertions, 0 deletions. All 16 files underopenspec/specs/are byte-identical throughout, checked against Git. All 3 promoted blocks re-verified BYTE-IDENTICAL against the archived delta, with the verifier pointed at the archived path after being shown to fail loudly withENOENTon the absent pre-archive one, and its self-test 5 of 5 there. Counts re-counted after the move at 184 / 597 / 15, unmoved by the archive. This stage deliberately edited none of the archived files. All 118 architecture cases are green with the change directory permanently at its archive path — and that matters more here than anywhere else, because this change's whole subject is documents:docs/PRIVACY.mdanddocs/PROVIDERS.mdboth carry claims an instrument resolves, neither cites a path inside the change directory, and so the archive needed no citation repair wheresecurity-surface-review's did.
openspec listreports no active changes, which means M12's five listed items are all done — three tracked and two untracked — and the milestone's own exit criteria are met in scope.
Its finding is that the absence this repository leaned on for its whole life is half wrong, and the wrong half came from a method rather than from a document.
docs/PROVIDERS.md:234citedhttps://mail.tm/terms->404as the basis for no terms page exists and citedhttps://mail.tm/en/faq/->200in the next line — the repository knew the site prefixes its real paths and probed an unprefixed one anyway. Re-measured at both shapes:https://mail.tm/en/privacy/serves a Privacy Policy wherehttps://mail.tm/privacydoes not. The conclusion about terms survived; the method did not.And Guerrilla Mail publishes Terms of Service at
/tos, linked from its own homepage, which this repository had never read. Verbatim, now in the record: "Guerrilla Mail acts as Software Service Provider"; the service prohibits "Over-use our resources by repeatedly making many requests to our servers"; the licence is "non-transferable, subject to change". Its API document publishes two sentences the record did not have — "The API may be rate limited, but we do not publish our rate limits" and "The client should not check email too many times as to not overload the server" — the second addressed to a client, which is what this product is, while it polls that provider every 5 seconds while a mailbox's contents are changing. Nothing changes the cadence and that restraint is the requirement: a qualitative prohibition is not a limit, this repository does not invent figures it has not measured, and the cadence has never run against a live provider. What ships is the requirement that the cadence state the guidance it was chosen against — the product's footer already renders Mail.tm's verbatim30; w=60, so the mechanism exists and only the second provider's quote was missing.And the apply stage produced the sharpest instrument finding of the milestone: a rule that was holding a falsehood in place. The new suite reported
docs/PRIVACY.md's attribution rows as claiming two things now measured false, and did not edit the document and did not weaken the instrument. Correcting it then turned that document's own suite red, becauseprivacy-disclosure.test.tsasserted the row must contain"No terms page exists."andhttps://mail.tm/terms— so it would have failed any correction to that row's sentence, including the correction that was true. That is the mirror ofsecurity-surface-review's "a rule that fires without a requirement behind it": here the rule fires against the correction, because the rule encodes the falsehood. A rule guarding a claim is only as good as the claim, and one written in 2026-10-02 from a single probed path held a sentence in place ever since. What replaced the sentences is the property: the row must name the routes tried — true whatever any single route returns — the locale-prefixed routes, the date, and that the silence is neither a permission nor a prohibition.The sync promoted two
ADDEDrequirements and oneMODIFIED, moving the totals by exactly two requirements and eight scenarios: 182 → 184 and 589 → 597 across the same 15 capabilities —provider-abstraction10 → 11 and 25 → 29,mailbox-session24 → 25 and 65 → 68,privacy-disclosure8 → 8 and 19 → 20.privacy-disclosure's requirement count holding at 8 is the arithmetic working, not a promotion that missed: aMODIFIEDblock replaces rather than adds, and the 19 → 20 is +3 replacing +2. It is the first milestone in four to carry aMODIFIED, and the shape is the finding — this is the first change whose own review falsified a sentence inside an already-promoted requirement.git diff --numstat -- openspec/specsis 51/0, 49/0 and 44/10: two pure additions and ten deletions, all inside the replaced block, which is what a modification looks like and what an addition never produces.And two guards were falsified separately, because a
MODIFIEDintroduces a second way to substitute silently.ADDEDonto an existing heading andMODIFIEDonto a missing one are both silent substitutions wearing the other kind's name, and the promoter now refuses each before writing anything — measured by running it twice (exit 1, specs SHA-256 unchanged) and by planting aMODIFIEDfor a heading the spec does not carry (exit 1, all three specs unchanged). The first attempt at that second falsification was a NO-OP that produced a green run — the PowerShell-replaceomitted(?m), so^matched only the start of the whole file, the plant never applied, and the instrument reported success while measuring nothing. A mutation that did not apply is not evidence, and that is the fifth time this session a reader said nothing in a way that looked exactly like a pass.
**Current milestone state (reconciled against Git and OpenSpec 2026-10-16): M0-M11 are complete in scope, M12's three tracked items are done (
extension-permissions-reviewPRs #134–#137,privacy-documentPRs #138–#141), and two of M12's two untracked items are now done as well —security-surface-review(PRs #142–#145) andbranding-check(proposal #146, apply #147, sync #148, archive #149 merged921858f— and that last number was a prediction written before the archive PR existed, corrected here againstgh pr view, which is this file's own recorded rule against a figure nobody counted, caught by the same reader that wrote it), both now APPLIED, SYNCED and ARCHIVED. What remains in M12 is the provider review, whichprivacy-documentscoped as not blocking it, and it is now PROPOSED asprovider-terms-review— whose finding is that the absence this repository has leaned on for its whole life is half wrong, and half wrong for a reason worth carrying.docs/PROVIDERS.mdciteshttps://mail.tm/terms->404as the basis for no terms page exists and citeshttps://mail.tm/en/faq/->200in the next line, so the repository knew the site prefixes its real paths and probed an unprefixed one anyway. Re-measured at both shapes:https://mail.tm/en/privacy/serves a Privacy Policy wherehttps://mail.tm/privacydoes not. The conclusion about terms survived; the method did not -- a404on a guessed path is evidence that that path is not served, and not evidence about the site.And the finding with teeth: Guerrilla Mail publishes Terms of Service at
/tos, linked from its own homepage, and this repository has never read them. Verbatim: "Guerrilla Mail acts as Software Service Provider, granting you a license for the use of the software provided by our website"; the service prohibits "Over-use our resources by repeatedly making many requests to our servers"; the licence is "non-transferable, subject to change". Its API document publishes two sentences the record did not have -- "The API may be rate limited, but we do not publish our rate limits" and "The client should not check email too many times as to not overload the server" -- the second addressed to a client, which is what this product is, and this product polls that provider every 5 seconds while a mailbox's contents are changing. Nothing changes the cadence and that restraint is the requirement: a qualitative prohibition is not a limit, this repository does not invent figures it has not measured, and the cadence has never run against a live provider at all. What ships is the requirement that the cadence state the guidance it was chosen against -- the product's footer already renders Mail.tm's verbatim30; w=60, so the mechanism exists and only the second provider's quote was missing.Two
ADDEDand oneMODIFIED, so the promoted requirement count moves by exactly two -- the first milestone in four to produce aMODIFIED, and the shape is the finding.privacy-disclosure's Provider attribution is stated in neither direction says in its body "Guerrilla Mail publishes nothing this repository found", and that is now measured false, so the block is amended rather than shadowed by a neighbour that contradicts it.openspec validateforced the amendment to carry its own scenario forward in full -- "a MODIFIED requirement replaces the whole block, so archive refuses to drop them" -- which is the recorded reason that scenario came forward at all.
docs/PROVIDERS.md's do-not-touch condition is satisfied explicitly: the active task requires it, and nothing else on that list is touched.
branding-checkis APPLIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-16-branding-check/— proposal PR #146 mergede099ef5, apply PR #147 merged76cd65e, sync PR #148 merged41ed65a, archive PR #149 merged921858f— and every one of those four numbers is measured, except that the archive number was first written as a prediction before the PR existed, which is this file's own recorded rule against a figure nobody counted, broken by the reader that wrote it and corrected againstgh pr viewin the same sentence. Left unfilled is the only honest default, and it was not left unfilled.The archive moved 6 files, every one byte-identical to a pre-archive digest taken programmatically, and Git's own rename detection independently confirms it — six renames at
0 insertions, 0 deletions. This change carried no.openspec.yaml, which makes it the second archived change in a row without one, afterprivacy-document— while every earlier archive that had one kept it, including this milestone'ssecurity-surface-review. Nothing dropped it — none was ever created. All 16 files underopenspec/specs/are byte-identical throughout, checked against Git rather than against digests I typed. All 3 promoted blocks re-verified BYTE-IDENTICAL against the archived delta, with the verifier pointed at the archived path after being shown to fail loudly withENOENTon the absent pre-archive one. Counts re-counted after the move at 182 / 589 / 15, unmoved by the archive. This stage deliberately edited none of the archived files, so the digest table needs no expiry-date qualification.And this archive carries the one thing M11's and M12's earlier ones could not: a check that the document survives the archive.
docs/BRANDING.mdcites no path inside the change directory — the previous change had to have its two citations rewritten mid-sync, because at apply time a change-local path was the only honest citation available. The whole architecture suite is 107 of 107 green with the change directory permanently at its archive path, which is the measurement rather than a simulated move-and-restore, because nothing in the document needed rewriting for the move to be survivable.
branding-checkis M12's branding check, and its verdict is that the name stands — while the one thing a mail product cannot do without is the one thing that is taken.spectremail.comis REGISTERED: RDAP reports it created 2020-06-03, expiring 2027-06-03, last changed 2026-08-17, with no nameservers, and a company directory lists a business of that name at that address describing itself as "a cloud-based email management app" whose features include private domains, token assignment to emails, mailbox creation and syncing. The domain has no nameservers, so it resolves nowhere and every DNS-based availability check reports it as FREE — only the registry's own RDAP endpoint says otherwise. That is the fifth recorded instance in this repository of a substitute platform answering about the wrong question, and the first where the substitute was DNS itself. Every other check came back clean, and three of the six produced no measurement at all — the trademark controlSPECTREnever resolved, Bluesky answers400on a known-good handle, and Product Hunt returnsHTTP 200with exactly 16 occurrences of the query string for a nonsense query. A control that does not resolve means the reader is not established, and a zero from an unestablished reader is the absence of a measurement, not a finding.Nothing was renamed. What shipped is the record and the instrument that makes "public branding should be updated cleanly" a property of the tree — the name is one exported value and every surface that renders it renders that value. Ten surfaces, eleven map rows, fifteen renderings, and the eleventh row is the value's own declaration because the requirement says the name is declared exactly once. One of the ten already did it right:
panel-copy.tsholds the name as one copy constant thatPanel.tsxrenders three times, whilePopup.tsxspelled the same heading as a bare literal three times in one component. The map's own reverse direction found two surfaces the root's measured table had missed —main.tsx:81andpanel-main.tsx:117, each a heading in the branch taken when the client's storage could not be read — so the figure in the proposal's table was a reading and the figure in the instrument is a measurement, which is the difference this repository records most often.The apply stage carried a defect only the root review could see, and it is the finding worth carrying. Adding
import { PRODUCT_NAME } from "@spectre-mail/ui"was the first time either app had ever imported a value from that package — verified onmainwithgit grep, no matches — and it therefore draggedindex.ts, and sopackages/ui/src/pairs.ts, into both clients' typecheck programs, wherepairs.ts:31'sfrom "./contrast.ts"(present since M7 slice
- failed
TS5097in each. The fix the implementation reached for was"allowImportingTsExtensions": trueintsconfig.base.json— a compiler option widened for every workspace project, with no requirement behind it. The repair is the subpath the package had already documented a pattern for: one line,"./name": "./src/name.ts", in the sameexportsmap as"./tokens.css", so a client reaching one string pulls nothing else.tsconfig.base.jsonis byte-identical tomain.The sync promoted three
ADDEDrequirements and noMODIFIEDorREMOVED, moving the totals by exactly three requirements and twelve scenarios: 179 → 182 and 577 → 589 across the same 15 capabilities —website-client23 → 24 and 75 → 79,extension-client15 → 16 and 79 → 83,build-and-verification11 → 12 and 32 → 36. Requirements moving by three because every one of the three blocks isADDEDis the arithmetic working, not a promotion that missed — the sixth milestone to produce that shape. Re-counted by heading after the promotion rather than carried forward, andgit diff --numstat -- openspec/specsis 59/0, 81/0, 65/0 — three files, zero deletions, so Git's own diff confirms nothing existing was touched. The promotion was rehearsed in full on a throwaway copy ofopenspec/first, and the promoter and the verifier share no reader: one walks lines with a state machine and ends a block at the next### Requirement:or any##, the other splits whole text by regex.And one requirement figure was wrong in the change's own delta and was amended before it was promoted, rather than promoted wrong.
extension-client's new requirement said the extension names the product in six surfaces, two of them components; it is eight, four of them components. The source of the wrong figure was the root's own table, not the instrument — the two extra are the pairmain.tsxandpanel-main.tsxrendered above. A requirement's count is a claim about the tree, and promoting a claim the apply stage had already disproved would have put a false figure intoopenspec/specs/permanently; the amendment is written into the delta with its reason, which is the recorded practice and the reason an archived delta and a promoted spec are the same blocks.
pnpm testis 1308 across 66 files (from 1290/64), reconciled as +4 inpackages/ui(38 → 42) and +14 intests/architecture(93 → 107, of which 14 are the newbranding-audit.test.ts).packages/storageis still 86,apps/webstill 125,apps/extensionstill 487, andboundaries.test.tsstill 53 — the last because this change adds no boundary rule, since every property it introduces already had an instrument or got one here.
pnpm test:browser: thirteen runs, twelve green, one red, and the red is UNATTRIBUTED. It failed the extension tier 1-of-98 once and did not reproduce in twelve subsequent runs, and the reader that would have named the failing case was written against the wrong output shape and matched nothing. A red with no name is not a finding, so it is recorded rather than rounded to thirteen green; it is consistent with the two pre-existing unattributed reds insecurity-surface-review'stasks.md6.4, and consistent with is not an attribution.
**Current milestone state (reconciled against Git and OpenSpec 2026-10-14): M0-M11 are complete in scope, and M12's three tracked items are done — two of them as of
extension-permissions-review(PRs #134–#137) and the third as ofprivacy-document, which is APPLIED. SYNCED and ARCHIVED (PRs #138–#141). The cursor is on M12's security review, which was never one of the three tracked items and is now APPLIED, SYNCED and ARCHIVED —security-surface-review, proposal PR #142 merged904a7a6, apply PR #143 mergeda02de19b. M12's security review is therefore done.
security-surface-reviewis APPLIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-14-security-surface-review/— proposal PR #142 merged904a7a6, apply PR #143 mergeda02de19b, sync PR #144 merged3ac06f9, archive PR #145 merged281265a— and that last number was filled in bygh pr viewat the next change's proposal stage rather than predicted at the archive stage, for the reason this file has recorded before: a predicted PR number in a document other stages cite by name is a figure nobody counted.The next change is
branding-check, M12's branding check, and it is PROPOSED. Its six checks were run against the sources themselves on 2026-10-16, and the name stands: GitHub, npm, PyPI, crates.io, RubyGems, Packagist, Docker Hub and Mastodon all answer their own "this does not exist", 0 of 924 Firefox add-ons carry the name, and the Chrome Web Store returns its own "No search results" — with a1passwordcontrol rendering 9 cards, because a zero from a reader whose control did not resolve is the absence of a measurement and not a finding. The one loss is the one that matters:spectremail.comis registered, since 2020-06-03, held, with no nameservers — so every DNS-based availability check reports it as free and only the registry's own RDAP endpoint says otherwise. That is the fifth recorded instance of a substitute platform answering about the wrong question, and the first where the substitute was DNS itself. Three of the six checks produced no measurement at all and are recorded as unmeasured with no verdict attached. Nothing is renamed; what ships is the record plus the instrument that makes "public branding should be updated cleanly" a property of the tree — and one of the eight surfaces naming the product already does it right, becausepanel-copy.tsholds the name as one copy constant thatPanel.tsxrenders in three places whilePopup.tsxspells the same heading as a bare literal three times.The archive moved 7 files, every one byte-identical to a pre-archive digest taken programmatically, with
.openspec.yamlkept, and Git's own rename detection independently confirms it — seven renames at0 insertions, 0 deletions. That is cleaner thanextension-permissions-review's archive, wheretasks.mdsat at 85% because that stage edited it after the move, and cleaner thanprivacy-document's, which carried no.openspec.yamlat all because none was ever created. This stage deliberately touched none of the archived files, so the digest table needs no expiry-date qualification. All 16 files underopenspec/specs/are byte-identical throughout the archive — checked against Git, not against digests I typed, because a hand-transcribed digest once reported a file as differing thatgit statusnever listed as modified. All 3 promoted blocks re-verified BYTE-IDENTICAL against the archived delta, with the verifier pointed at the archived path after being shown to fail loudly withENOENTon the absent pre-archive one. Counts re-counted after the move at 179 / 577 / 15, unmoved by the archive.
security-surface-reviewis M12's security review, and its finding is that the one rule this repository never wrote is the one that matters most.chrome.storage.localholds a provider session token undercurrentand again undermailboxes, anddocs/PRIVACY.md:27names it in one line — while no requirement anywhere said where a credential may travel. The boundary that kept it correct was a comment atapps/extension/src/protocol.ts:160explaining why a creation answer carries an address and an id rather than the whole mailbox. A true comment, and a comment is not a requirement: it fails no gate and nothing notices its erosion. Seven of the roadmap's nine areas were already required and instrumented, which is the review's second finding — most of M12's security work was already done, by changes written for other gaps. What ships is the instrument rather than a code change: zero production edits, a test-owned map inapps/extension/src/protocol.test.ts(22 cases, the kind → may-it-carry-a-credential question, asserted both ways) rather than a boundary rule — D1's reason is that a scan would fire on that file's own forbidden-token list, the fourth recorded instance of this shape — andtests/architecture/security-audit.test.ts(18 cases) which resolves every citationdocs/SECURITY.mdmakes and names one that resolves to nothing.boundaries.test.tsstaying at 53 is the measurement D1 turns on: two of this change's properties are about values crossing a message boundary, and a rule can only see which module may import which, so a rule here would have been decoration.packages/storageis still 86 with an emptygit diff --stat,packages/uistill 38,apps/webstill 125, andmanifest.jsonbyte-identical by Git blob hash.pnpm testis 1290 across 64 files (from 1250/63), reconciled as +22 inprotocol.test.tsand +18 in the new suite — and the standing figure in the Testing block below read1228 across 62until this stage corrected it, two changes stale, found by this change and reported rather than corrected because its task 6.3 forbids it writing this file.The sync promoted three
ADDEDrequirements and noMODIFIEDorREMOVED, moving the totals by exactly three requirements and thirteen scenarios: 176 → 179 and 564 → 577 across the same 15 capabilities —provider-abstraction9 → 10 and 20 → 25,extension-client14 → 15 and 75 → 79,build-and-verification10 → 11 and 28 → 32. Requirements moving by three because every one of the three blocks isADDEDis the arithmetic working, not a promotion that missed — the fifth milestone to produce this shape, afterincoming-mail-notification,panel-mailbox-choice,extension-permissions-reviewandprivacy-document. Re-counted by heading after the promotion rather than carried forward. The promotion was rehearsed in full on a throwaway copy ofopenspec/before the real tree, and the promoter and the verifier share no reader — one walks lines with a state machine and ends a block at the next### Requirement:or any##, the other splits whole text by regex.And the verifier's own self-test failed on its first run, which is why its tally was not taken from it. Case 3 built its mutation by replacing
"a "inside its own search string —### Requirement:, which contains noa— so the mutation was a no-op and the self-test reported "one word changed inside a promoted block is reported by name" as FAIL for the right reason and PASS for the wrong one once repaired. A self-test whose mutation did not apply is a self-test that passed a comparison it never performed, and the repair was to build the mutant from the block's own text and assert it applied. The repaired self-test is 4/4, its first two cases being a known-resolving citation read before any failure is claimed and a## ADDED Requirementsplanted between two requirements. The promoter's guard was falsified separately: run twice, the second run exited 1 withREFUSING: provider-abstraction already carries a requirement headed …and left all three specification files SHA-256 unchanged — because anADDEDblock landing on a heading that already exists is a silent replacement wearing the name of an addition.And the apply stage carried a defect only the root review could see, and it is the finding worth carrying forward.
docs/SECURITY.mdcites two of this change's three requirements by their path inside the change directory, because at apply time they do not exist inopenspec/specs/. Both resolve today and only today. Measured, by performing the move and putting it back: moving the directory toopenspec/changes/archive/2026-10-14-security-surface-review/turned the suite 1 of 18 red, naming both citations as— no such file— and pointing them at the archived copies would not resolve either, because an archived delta path has seven segments and the reader accepts six. The promoted specification is the only citation form that survives the archive, so the repair belonged to the sync stage alone. The same one-command measurement re-run after the rewrite leaves it 18 of 18 green, which is the evidence the obligation was discharged rather than merely re-pointed. An instrument widened to accept a seven-segment path would have been the shape this repository records most often: a check loosened to accommodate a document pointing at the wrong thing.M10 is complete in scope, and the history below is kept in the order the milestones happened. The
side-panel-runtime-factsspike that unblocked M11 is APPLIED, VERIFIED and ARCHIVED atopenspec/changes/archive/2026-10-11-side-panel-runtime-facts/— proposal PR #111 mergeddd4fc3f, apply PR #112 mergede2e1611, no sync stage because the change declaresskip_specsand promoted nothing (the promoted counts stand at 163 requirements and 504 scenarios across the same 14 capabilities, re-counted by heading after the archive rather than carried forward, and its archive moved 4 files every one byte-identical to its pre-archive original with.openspec.yamlkept). M11 slice 1 has since run its whole lifecycle asside-panel-surfaceand is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-12-side-panel-surface/— apply PR #115 merged11a415c, sync PR #116 merged4c93975, archive PR #117. The archive PR number was left unfilled by the archive stage rather than guessed, because that stage was told not to open the PR and a predicted PR number in a document other stages cite by name is a figure nobody counted —gh pr viewfilled it in one command afterwards, which is the honest order. Unlike the spike it declares noskip_specs, so it had a real sync stage and the promoted counts stood at 166 requirements and 520 scenarios across the same 14 capabilities —extension-client10 → 12 andin-page-integration16 → 17, three requirements rather than five because two of its five blocks areMODIFIEDand each replaces rather than adds, the fourth milestone to produce that arithmetic. Re-counted by heading after the archive rather than carried forward: 166/520, unmoved by that stage, withopenspec validate --specs --strictat 14 passed, 0 failed, the archive having moved 6 files every one byte-identical to its pre-archive original with.openspec.yamlkept,openspec/specs/byte-identical throughout, and all 5 promoted blocks byte-identical to the archived deltas against a freshly written reader.M11 slice 3 (
panel-verification-state) and M12 slice 1 (extension-clear-data) have both run their whole lifecycles since, and the count neither of them wrote down was found by this change's own archive stage — so both are recorded here together.panel-verification-stateis APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-13-panel-verification-state/— proposal PR #124, apply #125 merged28f1360, sync #126 mergedb569ff6, archive #128 mergede723fba— and its sync promoted oneADDEDrequirement carrying seven scenarios inextension-client, moving the tree from 166/527 to 167/534. That change recorded the figure correctly in its own narrative (docs/ROADMAP.md: "the tree is 167 requirements / 534 scenarios / 14 capabilities"), so it is not accurate to say the step went unrecorded — what went stale was the standing headline at the top of the Project Status block, which still read 166/527 frompanel-mailbox-choiceand which nothing re-checks. That distinction is the whole finding, and it has a shape worth naming: the figure a reader copies is the headline, and the headline is the one number in the block no change has to touch to record itself. The consequence was concrete — this change's own first draft of its entry inherited the stale headline, computed its deltas against it, and published +2 requirements and +17 scenarios where the tree says +1 and +10. The error was caught only by counting the tree, three edits after it was written, and a ledger whose headline can be two syncs stale while its body is correct has no way to notice.
extension-permissions-review(M12's permissions review) has since run its whole lifecycle and is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-14-extension-permissions-review/— proposal PR #134 merged7c9be91, apply PR #135 merged3d9bd05, sync PR #136 merged00be929, archive PR #137 merged2b3ee7d— and that archive PR number was filled in bygh pr viewafter the merge rather than predicted, because a PR number in a document other stages cite by name is a figure nobody counted. Git's own rename detection confirmed the move's byte-identity independently of the digests that measured it, and it named the one file this stage edited after the move: four renames at100%andtasks.mdat 85%, which is 6.4 ticked plus the archive record. A verification that is true only before the work that follows it is a verification with an expiry date, so the digest table is a claim about the instant the archive command returned and says so. The finding it was built on was wrong in its reasoning, and that is the part worth reading.docs/PROVIDERS.md§4.6 had recorded that theside_panelkey and thesidePanelpermission "cannot be separated on this substrate" — which was two cells of a 2×2 with two never run, and both were constructible with the staging the probe already had. All four cells are now measured, and the answer contradicted the shape of the claim rather than its conclusion: the key alone does not work without the permission (chrome.sidePanelisundefinedwith the key present and the permission absent), sosidePanelstays andapps/extension/static/manifest.jsonis byte-identical. A measurement recorded as a limitation turned out to be a measurement that had not been taken.What ships is the instrument rather than the answer: a test-owned permission map in
apps/extension/e2e/manifest.spec.ts—permission → the call that reaches it, or the manifest key that earns it— asserted both ways, the new direction being the one that matters (a permission declared with nothing behind it). It lives in a test file and nowhere else, because a rule a comment can satisfy is not a rule.tabsis why the map and not a scan: read twice in shipped code and deliberately undeclared, so a scan would have reported it as a false failure and the repair would have been to declare it. No boundary rule was added, and the boundary count staying at 53 is the measurement — a rule added where an instrument already exists is not coverage.Three false claims were corrected in the same change, and all three were one error in three files: substituting "does a module spell
chrome.<name>" for the requirement's "no shipped surface uses". Measured on the shipped tree with comments stripped, of 71 occurrences underapps/extension/src, 70 are inside comments, and the single one in code isservice-worker.test.ts's own forbidden-token list. Shipped code reaches the platform throughReflect.get(globalThis, "chrome")with member names as data, which is the boundary rule this repository already enforces.The sync promoted one block and moved the totals by zero requirements and one scenario: 168 → 168 and 544 → 545 across the same 14 capabilities,
extension-client14 → 14 and 74 → 75. Requirements moving by zero is the arithmetic working, not a promotion that missed — aMODIFIEDblock replaces rather than adds, the fourth milestone to produce this shape. Re-counted by heading after the archive rather than carried forward, withopenspec validate --specs --strictat 14 passed, 0 failed,openspec listreporting No active changes, all 5 archived files byte-identical to pre-archive digests including.openspec.yaml, all 14 files underopenspec/specs/byte-identical throughout the archive, and the promoted block re-verified BYTE-IDENTICAL against the archived delta — because archiving is a move, and a move is the operation most likely to quietly drop a file.The counts that did not move are the measurement the change turned on:
packages/uistill 38,packages/storagestill 86,apps/webstill 125, andapps/extensionstill 465 — which is required to be identical, because the only case the change adds lives in the browser tier that no Vitest glob collects.pnpm testis 1228 across 62 files;pnpm test:browseris 40 website and 98 extension, 5/5 green.Three instrument defects from this stage are recorded because each produced a confident wrong answer. Two of them are the same defect in two instruments written independently: Vitest writes its summary to stderr with SGR codes around every token, so a regex written against clean text never matches — one reader reported five green runs as five
HARNESS-ERRORs; and Playwright's summary parentheses hold a duration, not a count, so a second reader reported four green runs as fourHARNESS-ERRORs. Both repairs were in the reader and neither in the tally, and both readers were then given self-tests (6/6 each) whose first case is the exact shape that broke them. The sync's two block readers shared no code and no reader, and the verifier was falsified before its verdict was trusted: one word changed inside the promoted block, and the counts stayed 14 / 75 while the whole-block comparison named the line — a count cannot detect a wrong block, and that is now demonstrated rather than asserted. Andgh pr mergeexited1on a merge that had already succeeded, so the state was read withgh pr viewrather than taken from the exit code: a command that reports a successful operation as a failure is the mirror of the one that reports a failure as a success, and this repository has recorded the second shape four times.
privacy-documentis M12's privacy-documentation item, and is APPLIED and SYNCED. Proposal PR #138 merged86c2296, apply PR #139 merged6c4d0f2, sync PR #140 mergedb86c9af, archive PR #141 merged7476099— that last number filled in bygh pr viewat the next change's proposal stage rather than predicted at the archive stage, for the reason this file has recorded before.docs/PRIVACY.mdis 221 lines and ships the one record nobody had named:chrome.storage.local'ssite-mailboxes, ahostname→ mailbox id map written atcontent-script/controller.ts:348and only after an insertion lands. It is the only browsing-adjacent record this product keeps, and no document named it —README.mdstopped at No backend / No account / Client-side storage and §29 at the single wordlocal-first. A privacy document that omits the one record resembling browsing history is worse than none, because its presence implies the omission was considered, which is why it leads the document.What ships is the instrument as much as the document, and it is at
tests/architecture/privacy-disclosure.test.ts— 22 cases, 1734 lines, a test-owned map rather than a scan, every enumerable claim compared against a constant the code itself exports, asserted in both directions. The file self-tests four of its readers and audits itself for pattern-freeness and for sharing no reader with the boundary suite.boundaries.test.tsstaying at 53 is the prediction: this change adds no boundary, because every property it introduces is about a document and none is structural.
pnpm testwas red on the shipped tree, and the cause was a reader in another file. The first apply report said the new file's 22 cases passed on a first run — it had never runpnpm test— andpnpm testwas red: confines provider adapters topackages/providersreported six violations, every one naming the new file.stripCommentstracks line comments, block comments and three string delimiters, and has no state for a regular-expression literal — so a backtick inside one opened a template string that ran to the next backtick, the reader desynchronised for the rest of the file, and the doc comment immediately below it — naming the three adapter identifiers while explaining this very defect — was emitted verbatim as if it were code. Replicated rather than argued: the reader re-implemented byte-for-byte over 235 files, one containing the literal; restoring only the pattern turns that single case red with 3 identifier + 3 pattern hits; repairing it returns the file to green with its SHA-256 byte-identical. The remembered split was 4+2 and the measured split is 3+3 — the total was right and the split was wrong, and the split is what explains the mechanism. The cause was repaired; the reader defect is recorded and not fixed, becauseboundaries.test.tsis outside the change's write scope — and any future file putting a backtick inside a regex literal takes a green gate red with a cause that reads as correct.Two limits were recorded rather than engineered away, and the second is the one worth carrying. A declared count guards a silent edit, not a deliberate one — a decision is caught by nothing, so the count establishes non-silence rather than completeness, and the reverse direction needs a name predicate over
PANEL_COPYthat this change's own design forbids (D3). The instrument that would catch it is the instrument the design rules out, and that trade is paid in public rather than hidden. Andtests/architecture/has notsconfig.jsonand is not a workspace member, sopnpm typecheckdoes not check either architecture file's types — recorded as a limit on whatpnpm verifyestablishes here, and not repaired, because adding atsconfigwould move every future gate's typecheck scope.And the orchestrator nearly recorded a false finding against the same class. Spot-checking the document's "does not collect" pointers independently, a reader requiring
### Requirement:immediately before the cited text returned five MISS out of six — and all five resolved: every heading carries a subject before the cited phrase, so the reader was narrower than the rule it was checking. Re-run against the heading lines, all six resolve and the slugrequirement-persistence-is-reached-through-one-contractmatches its heading exactly. This is the most-recorded defect in this repository, and the instance that produced the instinct to record a finding here was caused by an instrument that could not fail in the direction it was aimed at — so the lesson is not check carefully but that a confident wrong answer from an unnamed reader is indistinguishable from a real defect until the reader itself is read.The sync promoted one new capability,
privacy-disclosure, at 8 requirements and 19 scenarios, moving the totals from 168 → 176 requirements and 545 → 564 scenarios across 14 → 15 capabilities, withextension-clientunmoved at 14 / 75 and every other capability unmoved, which is required here because the delta isADDED-only and a new capability has to touch nothing else.openspec validate --specs --strictis 15 passed, 0 failed. The promotion was rehearsed in full on a throwaway copy ofopenspec/before the real tree, and the promoter and the verifier share no code and no reader — one reads blocks with a line-by-line state machine, the other splits whole text by regex — so neither can inherit the other's blind spot. The verifier's reader self-test 4/4, and it was falsified before its verdict was trusted: one word changed inside a promoted block left the counts at8 / 8while the whole-block comparison moved the digest and named line 4; a## ADDED Requirementsplanted between two requirements was caught; and the verifier's own third self-test proves the block count is equal in exactly the shape that hides absorption, so a count cannot detect a wrong block here either. Two limits are stated rather than smoothed over: the promoted spec's## Purposeis prose written at sync and is not in the delta, so byte-identity covers the 8 requirement blocks and nothing else; and the promoter refuses to run over an existing spec, because the first sync tool in this repository logged a deletion it never performed.The archive moved 4 files, every one byte-identical to its pre-archive digest, and Git's own rename detection independently confirmed it: four renames at
100%and0 insertions, 0 deletions— a cleaner result thanextension-permissions-review's, wheretasks.mdsat at 85% because that stage edited it after the move. This stage deliberately touched none of the archived files, so the digest table needs no expiry-date qualification. All 16 spec files byte-identical throughout, checked against Git rather than against digests I typed — because on the first attempt I transcribed one by hand, dropped a single character, and reported a file as differing thatgit statusnever listed as modified. The promoted blocks re-verified BYTE-IDENTICAL against the archived delta, with the verifier pointed at the archived path after being shown to fail loudly withENOENTon the absent pre-archive one.openspec list→ No active changes;openspec validate --specs --strict→ 15 passed, 0 failed; counts re-counted after the move at 176 / 564 / 15, unmoved by the archive.Two figures a copy from an earlier archive would get wrong. This change carries 4 files and no
.openspec.yaml, where five changes before it carried five including the yaml — nothing dropped it; it was never created. And the archive raised a non-blocking warning that the proposal lacks a## What Changessection, which is half wrong:## Whyis present and the second section is called## What ships, so it is a header-naming variance and not a missing section.
M12's three tracked items are done, and M12 is not thereby closed. Its section lists five — the security review and the branding check were never among the three tracked ones and have never been started. The cursor is on the security review.
extension-clear-datais M12 slice 1, and is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-13-extension-clear-data/— proposal PR #130 merged7e7fd81, apply PR #131 merged62eb976, sync PR #132 merged908e555, archive PR #133 mergedbc0be20. That archive PR number was left unfilled by the archive stage for the reason recorded above, and is filled in here bygh pr view— which is the honest order: a predicted PR number in a document other stages cite by name is a figure nobody counted. It was taken ahead of M12's permissions review because the gap was recorded inside a promoted requirement rather than merely suspected —spectre-storagealready required a whole-area removal through one contract and no shipped surface called it. The panel now offers a two-step confirmed removal mirroring the website'sLocalData.tsx, reachingclearAllthrough theSpectreStoragecontract and naming none of the four keyschrome.storage.localholds.packages/storageis untouched at 86 tests and itsgit diff --statis empty, which is the measurement rather than a promise:clearAll()already existed, already meant the whole area, and its falsification already existed twice. The sync promoted three blocks — oneADDEDand twoMODIFIED— and moved the totals by one requirement and ten scenarios: 167 → 168 and 534 → 544 across the same 14 capabilities,extension-client13 → 14 and 65 → 74,spectre-storage16 → 16 and 57 → 58. The requirement count holding at 16 is the predicted outcome, not a promotion that missed: aMODIFIEDblock replaces rather than adds, and+9 +1 = +10accounts for the scenario delta exactly. Re-counted by heading after the archive rather than carried forward, withopenspec validate --specs --strictat 14 passed, 0 failed,openspec listreporting No active changes, all 5 archived files byte-identical to pre-archive digests,openspec/specs/byte-identical throughout the archive, and all 3 promoted blocks re-verified byte-identical against the archived deltas — because archiving is a move, and a move is the operation most likely to quietly drop a file.
panel-mailbox-choice's sync has since moved the scenario count and the requirement count not at all:openspec/specs/now holds 166 requirements and 527 scenarios across the same 14 capabilities, re-counted by heading across the tree after the promotion and counted twice, withopenspec validate --specs --strictat 14 passed, 0 failed —extension-client12 → 12 requirements and 52 → 58 scenarios,in-page-integration17 → 17 and 67 → 68. Three blocks promoted and zero requirements added is the first time in this repository that every promoted block replaced rather than added, so the block count (3) is not the requirement delta (0) and a reader expecting either figure from the other would be wrong. The scenario delta of +7 is the sum of +5, +1 and +1 and was measured rather than predicted. Twodocs/ROADMAP.mdfigures quote the superseded 166/520 and were left for the root orchestrator, which owns that file.The side panel is therefore no longer a surface
page-compositiondeclares absent by requirement — it ships. It names the host of the page beside it and re-asks on a tab switch, and it does so over the route the spike priced as free: the content-script round trip and not thetabspermission, sotab.urlstays withheld and no permission was added to read it. The answer is a hostname and nothing else, because a signup page's query string is where its tokens are. The cursor moves to M11 slice 2, APPLIED, VERIFIED, SYNCED and now ARCHIVED aspanel-mailbox-choice— archived on 2026-10-12 toopenspec/changes/archive/2026-10-12-panel-mailbox-choice/, with all 6 of its files moved SHA-256 byte-identical including.openspec.yaml, all 15 files underopenspec/specs/byte-identical throughout, and the counts re-counted after the move rather than carried forward at 166 requirements and 527 scenarios across 14 capabilities — unmoved by the archive, which is what a move should show. All three archived delta blocks were then re-read from their archived copies and compared as whole blocks against the promoted specs, and each is byte-identical — and this change's own deltas could not exercise the block reader's end condition, since each carries exactly one section heading placed before the first requirement; a requirement-only reader returns the same 2 and 1 blocks here. The condition was exercised against a real archived delta that does carry sections after requirements (incoming-mail-notification'sextension-client, 8 blocks), where the faulty reader absorbs## MODIFIED Requirementsand## ADDED Requirementswhile returning the same block count of 8 — so a count-based check can never catch it and only a whole-block content comparison does. And it is one item, not the twodocs/ROADMAP.md's Project Status block names. Exploration split them, because both amend the sameextension-clientrequirement heading, and two unrelated amendments to one heading in one delta is the shape this repository records as where an amendment gets attributed to the wrong assertion. Slice 2 takes "recent mailboxes" is displayed and not offered — the panel renders every held mailbox as inert<span>text with no control on any of them. "Whether verification mail is pending" has no representation at all and remains the one item on either of the M11 section's two lists slice 1 did not touch; it is a later slice because it needs a definition before it can be specified, and a change cannot be designed on a guess about what "pending" means. What none of this measured is whether a person can reach the panel: withopenPanelOnActionClickunset, the panel is reachable only through Chromium's own side-panel entry point, and no instrument in this repository reads a browser's own UI. The spike is also the third recorded instance of the "an artefact the measurement itself creates" defect — and the first two instances were both in the instrument, so this one being in the rule is the news: its storage rule reported the probe's own staged copy inapps/extension/test-results/for thechrome.storage.localcalls its exempt source is written with, so the same file was judged twice with only the directory differing. See the change'sdesign.mdD10, and D11 for the second gate failure, whereeslint.config.jsdeclares one environment per file and declaring the union would have silenced both errors by teaching three existing worker fixtures thatdocumentexists. Ten deliberate violations were run against the instrument and the two new controls, all caught, every file restored byte-identical by SHA-256 — and one of the ten first reported as a survivor because the harness had the polarity backwards, which is recorded because an arm written that way reports a correct configuration as broken and the obvious repair is to delete the thing that was right. Thealarms-packing-intervalspike that unblocked M10's remaining work is APPLIED, VERIFIED and ARCHIVED atopenspec/changes/archive/2026-10-10-alarms-packing-interval/— proposal PR #104 mergedde00857, apply PR #105 merged716049f, no sync stage because the change declaresskip_specsand promoted nothing. M10 slice 4, the incoming-mail notification (incoming-mail-notification), is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-11-incoming-mail-notification/— proposal PR #107 merged788d742, apply PR #108 merged6b80914, sync PR #109 merged1b9da50. Which makes M10 complete in scope, sodocs/ROADMAP.md's M10 has no slice left open and the cursor moves to M11 — which owns the side panel, the surfacepage-compositionhas been declaring absent by requirement rather than by omission. It was the first change in this repository to remove a permission-carrying requirement on measurement, and the removal is recorded rather than implied:extension-client's "The background service worker carries no polling until its lifetime is measured" is REMOVED and re-ADDED, because two of its three clauses still bind once the measurement that gated it exists — seedocs/PROVIDERS.md§4.1.1 for the gate, §4.5 for the notification measurement that rewrote a clause before any implementation was written, and §4.1.2 for what one firing costs, which the first version of the delta stated as one listing and a browser case measured as one listing plus one body read per message, re-read on every wake. M10 slice 1 (verification-actions) is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-10-verification-actions/— proposal PR #96 merged421d56f, apply PR #97 mergedbdd13b6, sync PR #98 merged550741f, archive PR #99 — and M10 slice 2 (in-page-fill) is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-10-in-page-fill/: proposal PR #100 merged11c28fa, apply PR #101 merged99bc35c, sync PR #102. (An archive's own merge SHA is not written here because a commit cannot contain its own merge commit;git log --merges --oneline -1 mainis the record.) M9's three slices are all applied, verified, synced and archived, and M7's four slices are archived and M8 (extension-foundation) is applied, verified, synced, and archived atopenspec/changes/archive/2026-10-07-extension-foundation/- proposal #75, apply #76, sync #77. M7 slice 4 (extension-preview) is applied, verified, synced and ARCHIVED atopenspec/changes/archive/2026-10-07-extension-preview/- proposal #79 (5062afa), apply #80 (586d58a), sync #81 (46ae462), archive #82.in-page-address(M9 slice 1) is applied, verified, synced and ARCHIVED atopenspec/changes/archive/2026-10-08-in-page-address/- proposal #84, apply #85, sync #86, archive #87.in-page-mailbox(M9 slice 2) is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-08-in-page-mailbox/- proposal #88 merged (c59bf79), apply #89, sync #90, archive #91.site-associations(M9 slice 3) is APPLIED, VERIFIED, SYNCED and ARCHIVED atopenspec/changes/archive/2026-10-09-site-associations/- proposal #92 merged (7755131), apply #93 merged (1ff7c24), sync #94 merged (18bbe00), archive #95 merged (a886447). The promoted counts move at the sync stage and not before, and the distinction is recorded here because "applied and verified" and "synced" are not the same claim.
incoming-mail-notification's sync promoted eight blocks and the promoted counts are now 163 requirements and 504 scenarios across the same 14 capabilities, up from 157 and 478:extension-client6 → 10 requirements and 24 → 40 scenarios,mailbox-session23 → 24 and 61 → 65,spectre-storage15 → 16 and 51 → 57. Eight blocks promoted and six requirements added is the arithmetic working, not a discrepancy — aREMOVEDblock subtracts and twoMODIFIEDblocks each replace rather than add, so+5 +1 +1 − 1 = +6. That is the third milestone to produce this shape, which is why the rule is stated here rather than left as a surprise: a count of promoted blocks is not a count of added requirements, and a reader expecting eight would be right about the blocks and wrong about the total. See the change'sdesign.mdD17
Truncated - read the full file at https://github.com/edisonmalasan/spectre-mail/blob/2bbc57e640ee908ac207c0f0aecf3f255e13a71c/AGENTS.md.
