This file is the acceptance criteria. It was transcribed from 38 reference screenshots that no implementer can see. If something is not written here, it is not verifiable — so treat every line as a requirement and, when auditing, mark each DONE / PARTIAL / MISSING against the actual code.
The Status column is the author's belief at time of writing, and is exactly what an audit should distrust.
| # | Requirement | Status |
|---|---|---|
| A1 | Delete every old local build of the app | DONE |
| A2 | Revoke every previously granted permission so they must be re-granted | DONE |
| A3 | Walk the user through onboarding on a freshly built app | DONE |
| # | Requirement | Status |
|---|---|---|
| B1 | Topbar colours must read as Apple-native, not off-brand | DONE — forced .aqua removed |
| B2 | Restore the ORIGINAL logo in the topbar, not the Anthropic-style one | DONE — template mark restored |
| B3 | Menu panel chrome must match a real system menu (rows, separators, metrics) | DONE |
| B4 | Nothing anywhere may use the Anthropic palette or serif display face | DONE — audit for stragglers |
| # | Requirement | Status |
|---|---|---|
| C1 | DMG shows a drag-to-install window with our wordmark | DONE — wordmark only. The painted logo was removed: Finder already draws the real app icon in the left-hand slot, so a second copy of the mark in the corner is the same object twice and reads as a decoy next to the one the user is meant to drag |
| C2 | Instruction line "Drag Context for Claude into Applications to install" | DONE |
| C3 | Dashed drop-target outlines around BOTH icons | DONE |
| C4 | Arrow between the app icon and the Applications folder | DONE |
| C5 | Neutral palette, no warm/clay cast | DONE |
| # | Requirement | Status |
|---|---|---|
| D1 | Big centred logo on a dimmed desktop at first launch | DONE — Cinematic.swift beat 1, scrim 0.96 |
| D2 | The logo BUILDS ITSELF with animation (not a fade-in of a finished image) | DONE — head, eyes, legs stroke on in sequence, then the wordmark. CinematicMarkGeometry scales stroke weight back with size, because ContextMark's 9.5% menu-bar weight renders a torus at 132pt |
| D3 | Background music plays through the sequence | DONE — Sound.music.start() on beat 1, faded out over 2.0s at hand-off |
| D4 | Logo morphs into a horizontal BAR | DONE — beat 3, matched geometry, screenshot caught mid-morph |
| D5 | The bar STRETCHES into a search-bar shape | DONE — beat 4, swoosh on the stretch |
| D6 | A question types itself into the bar | DONE — "what was I working on today?" with a caret, keystroke sound every 4th char |
| D7 | Populated by a large swarm of windows flying in | DONE — REAL captured frames, verified on screen (Xcode with the user's own projects, Chrome with their own bookmarks). Picks one frame per app before a second of any, because an even spread returned five of six identical Cursor frames |
| D8 | Swooshing sounds on the window fly-in, staggered | DONE — one swoosh per card, 0.13s stagger |
| D9 | Click sounds on interaction throughout | DONE — cinematic beats, card advance (go(to:)), chime on every grant |
| D10 | Interactive and immersive overall; skippable | PARTIAL — "Skip intro" and a music mute are on screen and abort is tested from all six beats, but Esc/Skip are NOT verified on hardware: synthetic Escape does not reach a borderless window of an inactive accessory app |
| # | Requirement | Status |
|---|---|---|
| E1 | Welcome card: title, one-line value, "Get started" + "Skip intro" | PARTIAL — card and action DONE; there is no "Skip intro" on the CARD (the cinematic carries it). Skipping the cards would leave a non-functional app |
| E2 | Value card: heading, paragraph, three bulleted claims, "Continue" | DONE |
| E3 | "Enable permissions" card listing each permission with its own explanation | DONE — each row renders its own first-person sentence via Capability.title |
| E4 | Sign in with the Omi account | EXISTS — restyle only |
| E5 | Connectors card: which agent surfaces to register with, checkboxes | EXISTS — restyle only |
| E6 | Progress dots across the flow | DONE — 5 dots when sign-in is needed, 4 when the session restores |
| E7 | An optional "stay in touch" email step exists in the reference | DECIDED OUT — the Omi account already identifies the user; do not add a second email capture |
| # | Requirement | Status |
|---|---|---|
| F1 | An animation showing the app being dragged into the permissions list | DONE — on the real System Settings window, not as a preview inside our card. A floating "Context for Claude" row is carried along the arrow into SettingsSpotlightScene.dropSlot, a row-height strip at the list edge the row arrives from. Only the slot and the source row are dashed — never the whole list, which is what made the earlier version read as "everything here is wrong". The in-card GhostRowReplica rehearsal was deleted along with its "Show me the row" button; the guidance now appears on its own |
| F2 | It highlights exactly where the row needs to go | DONE — AX ring on the REAL System Settings row. 477/477 exact rects live, cross-checked against an independent AX dump; 112 refusals all correct. PermissionGuidance is .pointing or .instruction only, so a mispositioned ring is unrepresentable |
| F3 | Same treatment for Screen Recording | DONE — overlay in openScreenSettingsOnce, relaunch nudge preserved |
| F4 | "Very accurate" to the reference | NOT BUILT — this row previously described the replica-then-ring-the-real-row design as though it existed. It does not; there is no such code. The CONSTRAINT is real and still governs the design: on macOS 26 the dashed "Drag this row up into the list" affordance is System Settings' OWN UI, no app can draw inside that window, and the reference app did not build it either. MenuBarSpotlight rings this app's own status item, not a System Settings row. |
| F5 | Permissions requested one at a time, paced | DONE — preserve |
| F6 | Screen Recording relaunch nudge | DONE — preserve |
| # | Requirement | Status |
|---|---|---|
| G1 | "Learn how to use it" card with Start / Skip | DONE — Tutorial/TutorialCardView.swift, now spoken by the mark ("Let me show you what I do."); walked live |
| G2 | Opens a page in the browser as the first step. NOT Wikipedia — direct user instruction | DONE, on the third instruction, and the chain is the reasoning. (1) Open a page — Wikipedia. (2) "I dont like Ling's website": a third-party commercial page in somebody's browser thirty seconds after asking to watch their screen is wrong on taste and on trust, so the page was cut outright and the beat ran on whatever the user already had open. (3) "Open Anthropic's website and say scroll for a bit." Instruction 2 objected to a stranger's page, and (2) was worse than it looked on the machine that matters: a first run is a fresh Mac with an empty desktop, so a beat that said "go and find something to look at" waited for frames the user had nothing to produce. TutorialModel.readingMaterial is https://www.anthropic.com/research — Anthropic's own site, the one this app can open without choosing a third party's content for somebody, and the page on it there is enough of to scroll: the home page is short and largely static, capture dedupes perceptually, and a user who scrolled it end to end produced very little for the gate. Reported as "open anthropic's research page, maybe, because there's just more content to read and people can scroll through" — opened through TutorialEnvironment.openPage, which answers whether it really opened, so the card claims a page only when NSWorkspace said yes. Not opened at all for a user who declined the screen grant, where nothing they scroll could be captured. testTheCaptureBeatOpensAnthropicsSiteAndAsksForAScroll, testAPageThatWouldNotOpenIsNeverClaimedAsOpened, testNoOtherBeatOpensAnything |
| G3 | Asks the screen-recording permission at the right moment | DONE — screenAccess step, dropped from the plan when the grant already exists, advances on the real TCC answer. The ungranted path is unit-tested, not walked: this machine is granted |
| G4 | Coach mark: "collect your first frames" | DONE, reworded three times and re-placed — "I opened Anthropic's research page." / "Scroll through it for a bit, and I will tell you when I have it." Direct user instruction: do not say see anything. The ask is now a gesture rather than an act of attention, which is also the honest ask — scrolling is what produces distinct screens for the store to hold, and attention is not something this app can observe. The card takes TutorialPlacement.outOfTheWay (bottom trailing, the only step that does) so it is not sitting on the page it just opened. testNoCardAsksTheUserToLookAtSomething walks every state of the beat, including the one where the page would not open |
| G5 | A LIVE frame counter reading our own store — never a timer pretending | GATE DONE, READOUT DECIDED OUT. Direct user instruction: the card must not say it is collecting frames. The gate is untouched and is now the only thing holding the line — framesCollected is still assigned from RewindQueries.frameCount every tick and the model still contains no increment. What the user sees instead is the mark saying "Scroll through it for a bit" and then "Got it.", which is the same fact in a human's words. Mutating the gate into a timer still fails testTheCaptureBeatReflectsTheStoreAndNeverElapsedTime, and testTheCaptureBeatSaysGotItOnlyOnceItReallyHasIt proves the sentence is a consequence of the store too. testNoCardEverSpeaksAProgressCountOrAFrameCount walks the whole flow and fails on any digit |
| G6 | "It's time to open the timeline", showing the chord | DONE, and it is now the beat that teaches the chord. Direct user instruction: "this rewind window should open after asking me to press the command keys and from that action". The window used to open itself the moment the step began, which taught nothing — the tutorial announced a shortcut and then did the shortcut's job. openTimeline now asks and waits: its gate is TutorialGate.realHotkey, satisfied only by GlobalShortcuts genuinely delivering .openTimeline, and the window that appears is opened by the app's own handler in ContextApp.swift, not by the tutorial. GlobalShortcuts.addObserver exists for exactly this — an additive listener, so the tutorial never takes ownership of the shortcut. testTheChordBeatWaitsForTheRealShortcutAndOpensNothingItself asserts timelinePresentations == 0 through five minutes of ticking. The keys type themselves (direct user instruction: "show the keyboard and the key getting pressed"): TutorialChordDemo loops a press over TutorialChordCycle, which draws ⌘⌘ as one cap struck twice and a rebound ⌘⇧K as modifiers held while the last key falls. Two caps was the first drawing and it was reported — "expressing both the command keys one by one. It's supposed to be both the command keys together" — because a Mac has two Command keys and two ⌘ caps lighting in turn draws pressing both of them. Neither reading of that report is the fix: ModifierDoubleTap watches one modifier mask go down, up and down, so both Command keys held together cannot fire it at all. testTheDemonstratedTapIsOneCapAndIsTheGestureTheDetectorFires replays the drawing through that real detector, and replays the "together" reading through it too. The loop stops because the card stops rendering it, which is when the real chord fires |
| G7 | The timeline/rewind window opens | DONE — by the user's keypress (G6), through the shell's own RewindWindow.present wiring; the step only claims the window is up when isVisible agrees. The honest fallback for a machine whose chord cannot fire (Accessibility ungranted) is a labelled waiver that opens it and says so: testAChordThatCannotFireOffersTheWayOutAtOnceAndSaysWhoOpenedIt asserts the card reads "I opened it for you." rather than congratulating a keypress that never happened. The tutorial no longer keeps its own copy of the window's onSearch wiring — Tutorial.searchPillWasPressed() answers the shell, so there is one window with one set of buttons behind it |
| G8 | Coach mark with a left-scrolling animation: "scroll left to see the past" | DONE, and now actually detected. Direct user report: "The two finger dragging didnt work." Two independent causes, both real: (1) nothing listened — the hint was decorative and the step's gate was .userAction, so a perfect gesture changed nothing; (2) the gesture moved nothing — the track's scroll called RewindModel.panTrack, which pans the visible window and is clamped to the loaded day, and resetTrackWindow opens with trackSpan = the whole day, so bounds.end - trackSpan == bounds.start and trackStart could not move by any number of seconds. Every scroll on a freshly opened timeline was arithmetically a no-op. Fixes: TutorialDrag (a pure recogniser over scroll deltas, 12 tests in TutorialDragTests) behind a local NSEvent monitor gates the step as TutorialGate.realGesture; and RewindModel.travel(by:) replaces panTrack, moving the playhead so the picture changes and the window follows via keepPlayheadVisible(). The recogniser has no phase filter (momentum counts), no direction rule (which way is "back" is the user's own natural-scrolling setting, which this app does not get to read) and no trackpad requirement (a notched wheel scales by pointsPerLine). The card's arrow is arrow.left.and.right for the same reason. The gesture is now drawn being made (direct user instruction: show an animation for the left/right scroll): TutorialScrollDemo sweeps a hand across a strip of the timeline with the panels travelling under it, both driven by one phase through TutorialScrollCycle, so "the content moved because your fingers did" is true by construction — testTheDemonstratedPanelsFollowTheHandExactly fails if they ever disagree, which is how an inverted drag would be taught. It sweeps both ways for the same natural-scrolling reason, it collapses to a still under Reduce Motion, and it stops when the real gesture lands |
| G9 | Coach mark: "Find specific moments — click Search All" | DONE — anchored under the pill's real accessibility frame (pill (182,171) 118×25 → card x=31, y=211, arrow up), and the step advances because the real pill was pressed |
| G10 | A popup appears after clicking Search All | PARTIAL — the popup is the tutorial's own query card, not SearchBarWindow. Deliberate: that bar routes the question to Claude, which is the later proof beat, while G12 needs a memory row and a timestamp this app can show. Moving G10/G11 onto the real bar would move G12's gate to QueryStamp |
| G11 | Coach mark: type a query and press Enter | DONE — real field, submits on Return, runs Queries.recall against the real store; an empty result keeps the step and says so |
| G12 | Result appears, then "Found it! Tap the memory to jump back to that exact moment" | DONE, merged into G11's card — typing and finding are one continuous idea, and the mark's line changes from "Type something you just looked at." to "There it is." because there was a real hit. search no longer advances the step itself: the gate is !results.isEmpty and the Continue button is the only thing that reads it, so the check that says "something was really found" is the one the button runs. A second search that finds nothing takes the line back off the card (testAFailedSecondSearchWithdrawsTheFoundItLine) |
| G13 | "You are all set" completion card | DONE — the sentence comes from TutorialOutcome, an enum with exactly one route to .caught (the gate's own condition). A waived frame beat closes on an admission, and a waived screen grant now closes on .cannotSee — the flag the old code set and never read. testTheSuccessSentenceIsReachableOnlyFromARealFrameCount drives every other route and asserts none of them reaches the success sentence |
| G14 | "One more thing — you can always find it up here in the menu bar" | DONE — the menuBar step calls MenuBarSpotlight.show(), and teardown hides it |
| G15 | Shortcut-conflict notification with a one-click remedy | PARTIAL — ShortcutConflicts.scan reads other tools' real config files and the row renders only on evidence. On THIS machine the reference's "Codex also uses ⌘⌘" is FALSE (those Codex commands ship with no default binding and no keymap file exists), so no row appears — correct, but unexercised against a real conflict. The one-click write is deliberately NOT implemented: Codex rewrites keybindings.json wholesale from an in-memory keymap, so writing it under a running Codex leaves the old chord live and the button would report success while the conflict persisted |
| G16 | Progress dots through the tutorial | DECIDED OUT — direct user instruction: the tutorial must not show how many steps it has. Knowing it is fourteen long is a reason to leave it. TutorialProgressDots and TutorialModel.progress are deleted; plan survives because dropping an already-satisfied step still has to work. The flow is also eleven beats now rather than fourteen (G2 and G12 cut or merged; G6 came back as the beat that waits for the chord rather than announcing it), which is the other half of the same instruction |
| G17 | Music throughout, click sounds on every step | DONE, with a last beat — Sound.music.start() at begin(), faded out at the first step that hands the user to another application (TutorialStep.handsOverToAnotherApp, i.e. the browser) and idempotently again by teardown. It used to run until the tutorial ended, and the tutorial does not end on a schedule: claudeProof waits on Claude and cannot be waived, so a run left there looped a 24-second bed indefinitely. Reported from that beat as "you should really stop the noise after a while"; a click on every advance, a swoosh on the page and on the timeline, a chime on each earned gate |
| G18 | The mark does the talking | DONE — every card goes through TalkingMark(lead:leadStyle:aside:), the same component and the same call-site shape onboarding uses, so the character that introduced itself is the one running the tutorial. The words come from TutorialModel.speech, not from the view, which is what lets TutorialTests assert them. Card width went 420 → 470: the mark takes a fixed 56 pt out of every line, and every card was re-rendered in Light and Dark at the new width to confirm nothing clips |
| # | Requirement | Status |
|---|---|---|
| H1 | Ported from the Omi repo rather than reinvented | DONE |
| H2 | Functionally as close to the reference as possible | DONE — verify |
| H3 | Centred window title for the mode | DONE |
| H4 | "Search All" pill with its / hint, top-left |
DONE |
| H5 | Gear button opening Settings | DONE (closure, not yet wired to Settings) |
| H6 | Frame centred with a segment-keyed border | DONE |
| H7 | Chevrons on the frame's left/right edges for segment nav | DONE |
| H8 | Date/time pill bottom-left with a picker | DONE |
| H9 | Four circular controls bottom-right: open externally, live text, zoom out, zoom in | PARTIAL — open-externally is HIDDEN because it cannot be resolved honestly (0.10% of frames); needs capture to record AXURL |
| H10 | Timeline track with per-app coloured segments | DONE |
| H11 | App icon badges pinned along the track | DONE |
| H12 | Two-finger scroll to move forward/backward in time | DONE — horizontal intent wins, pans in seconds. VERIFY it works over the frame area, not only the 32pt track |
| H13 | Live Text highlighting selectable text in the frame | DONE — recomputed on demand, since capture discards Vision boxes |
| H14 | Track zoom distinct from frame-image zoom | DONE |
| H15 | Beautiful | SUBJECTIVE — verify by screenshot |
Sidebar: General, Agents, Capture, Storage, Exclusions. Every row is an icon tile, a title, a grey subtitle, and a right-hand control.
Seven settings rows are deliberately not shipped, each removed on a report on 2026-08-16, and
they account for twelve requirement IDs marked DROPPED below: the Appearance pane entire (I14 and
I17–I22 — one pane, seven IDs), the second shortcut recorder (I2), Airgap Mode's switch (I5), the
agent survey (I13) and Capture Quality with its footnote (I25, I26).
docs/rewind-and-settings.md § Part 2 carries the same list as one table. The pattern in all of them
is the same and is worth stating once: a control that only makes the product worse, or that
duplicates another control, is removed rather than narrowed or hidden — a setting nobody can find
still fires.
| # | Requirement | Status |
|---|---|---|
| I1 | Open Timeline Shortcut — recorder, default double-Command, "Clear it to use ⌘⌘" | PARTIAL — recorder, copy, cleared-state and rejection are real, but bound to SettingsShortcutChord, a stub. GlobalShortcuts now exists and must be adapted onto ShortcutBindingProvider |
| I2 | Open Search Shortcut — recorder, default ⌘⌘⇧ | DROPPED (2026-08-16 report) — it bound a second chord onto the same window I1's opens. ContextApp.shortcutFired answered .openActivity and .openSearch with one window.press(), because the Spotlight panel became the Activity window; so the pane drew two recorders, two defaults and two conflict rows for one behaviour, and a user who rebound one could not tell which had won. Removed from GlobalShortcuts.Action as well as from the pane — hiding the row would have left the chord firing — and ModifierDoubleTap went with it |
| I3 | Conflict row, shown ONLY on a real conflict, with a one-click switch | PARTIAL — UI live-queries and never persists; the real ShortcutConflicts scanner exists but the pane still reads the stub |
| I4 | Launch on Login toggle | MISSING (LoginItem exists) |
| I5 | Airgap Mode toggle — suppresses telemetry, update checks, remote favicon requests; takes effect after relaunch | DROPPED (2026-08-16 report) — the switch, not the promise. ExclusionEngine still carries the flag, NetworkEgress still refuses every client while it is set, and anyone whose exclusions.json already says airgapMode keeps exactly the behaviour they chose. Ripping out the enforcement instead would silently start uploading for those users. Guard: SettingsTests.testAirgapEnforcementSurvivesTheRemovalOfItsSwitch |
| I6 | Updates row showing the real version | DONE — UpdatesSettingsRow reads the bundle version through ContextUpdater. |
| I7 | Automatic Updates toggle | DECIDED — Sparkle now ships (Sources/ContextApp/Update/, the shared micro-app feed, docs/releasing.md). The Updates row has a real Check Now; checks and verified downloads run automatically, while installation always asks before relaunch. A toggle is intentionally omitted because the shipping policy is fixed and locally signed copies are refused by UpdatePolicy. |
| # | Requirement | Status |
|---|---|---|
| I8 | Route to Agent dropdown | DROPPED (J7) — it shipped and did nothing. context.settings.agentRoute had zero readers: no code anywhere branched on it. Its subtitle advertised "With ⌘↵ you can send your query directly to your agents", and there is no ⌘↵ — SearchBarView handles insertNewline: and cancelOperation: only — routing to the path divergence 3 below deliberately removed. Row, AgentRoute, the key, the default and the published property are all gone; a stored value decodes to nothing and is ignored. The real routing decision is I9 |
| I9 | Claude target dropdown: Claude app (prompt pre-filled) or the claude CLI in Terminal |
DONE — ClaudeRouter, chosen in SettingsStore.claudeTarget and read by ClaudeHandoff. This is no longer the search bar's setting: the bar answers in-app from this app's own indexes and never routes to Claude. What the dropdown steers is the tutorial's guided hand-off — Claude App opens claude://claude.ai/new?q= (a new chat, prompt pre-filled and not sent; the code/new variant was verified by watching text land in the Code composer unsent), Terminal runs the claude CLI with the question in whichever app owns .command files. Pre-fill is genuinely supported, not a fallback; the clipboard branch happens only when nothing claims the scheme. The preference has exactly one home and one consumer — no ClaudeRouter.preferredTarget — and the row's subtitle is ClaudeRouter.targetSubtitle, so it describes what this Mac would really do for each option. ClaudeHandoffTests.testTheHandoffRoutesToTheTargetChosenInSettings pins both values of the dropdown |
| I10 | Tip line about mentioning the app inside Claude Code/Codex/Cursor | DONE, narrowed — it now names Claude Code and Claude Desktop only. ClaudeRegistrar writes those two configs (~/.claude.json, claude_desktop_config.json) and nothing in this package writes a Codex or Cursor MCP entry, nor do we ship a CLI (I12), so naming them made two thirds of the sentence work the user would have had to do by hand without being told. Codex and Cursor remain in the I13 survey, which reports what is installed and claims nothing about integration |
| I11 | An illustrative mock of an agent prompt box | MISSING |
| I12 | CLI toggle installing a command to ~/.local/bin |
MISSING — decide whether we ship a CLI at all; our equivalent is the MCP registration |
| I13 | Detected agent list (Claude, Codex, Cursor) with a green "Installed" pill | DROPPED (2026-08-16 report) — it was built, with real detection and a real not-installed state, and it was read-only: nothing on the pane or anywhere else acted on the survey, and the two surfaces this app registers with are named by I10's tip line either way. A list the user cannot do anything with, in the pane where the two controls that do something live. AgentSurface/AgentPresence and the probe are gone; AgentPaths keeps the ~-abbreviation the Storage pane needs |
The pane does not exist. Reported as "remove appearance options altogether … in fact remove
appearance section from settings altogether", and the three things on it came apart cleanly: the
theme tiles let a user disagree with their own machine's Appearance setting inside one app, the four
timeline toggles were four ways to make the timeline worse, and the Dock row was never an appearance
choice. Guard: SettingsTests.testThereIsNoAppearancePane.
| # | Requirement | Status |
|---|---|---|
| I14 | Appearance: System / Light / Dark preview tiles, selected one ringed | DROPPED (2026-08-16 report) — System (the default, and what every install was on) installed no appearance at all, so the control existed only to pin this app against the machine around it. Phase 0 of docs/first-run-experience.md had already deleted a forced NSApp.appearance = .aqua for rendering a light popover inside a dark system menu; the tiles were that defect with a switch on it |
| I15 | Accent Colour dropdown with a colour dot | DROPPED (J7, INV-UI-1) — the requirement itself was wrong, and it took shipping it to see why. Its only consumer was .tint(store.accentColor) on the Settings window, so picking a colour recoloured that window's switches and nothing else: every structural accent in the app reads Ink.accent, a fixed systemBlue chosen precisely so no machine can make it purple. And the requirement's own instruction — "default must mean controlAccentColor" — is the violation: on a Mac whose system accent is Purple, the default value of a control nobody touched painted the Settings window purple, which is exactly what Ink.accent refuses to do. A control that cannot do what it says and can only break the brand rule is deleted, not narrowed. AccentChoice, Key.accent, the published property and accentColor are gone; Ink.accent is the one accent, guarded by InkAccentTests over BrandColourGuard |
| I16 | Show Dock Icon toggle | DONE, moved to General — it decides whether the app has a second home besides the menu bar, which is the same question Launch on Login answers. DockPresence still owns the key, the default and the mapping |
| I17 | Timeline section with a LIVE preview reflecting the toggles below it | DROPPED (2026-08-16 report) with I18–I22 |
| I18 | Open externally toggle + ↵ hint |
DROPPED (2026-08-16 report) — the timeline draws it always |
| I19 | Live Text toggle | DROPPED (2026-08-16 report) — the timeline draws it always |
| I20 | Zoom controls toggle + ⌘⇧+/− hint |
DROPPED (2026-08-16 report) — the timeline draws them always |
| I21 | Segment navigation toggle + ⌥←/→ hint |
DROPPED (2026-08-16 report) — the timeline draws them always |
| I22 | Hidden controls keep their keyboard shortcuts working | DROPPED (2026-08-16 report) — vacuous once no control can be hidden. RewindTests.TimelineControlVisibilityWiringTests now checks the opposite direction: that no preference gate grows back over a control |
| # | Requirement | Status |
|---|---|---|
| I23 | Screen Capture toggle | MISSING |
| I24 | Pause on Inactivity toggle | MISSING |
| I25 | Capture Quality: four tiles (Best / Default / Compact / Smallest), subtitle describes the selection | DROPPED (2026-08-16 report) — it was built and it genuinely worked (FrameImage resized and re-encoded to the selected tile), and the choice it offered was between one good answer and three worse ones: three of the four bought disk back by making the user's own screenshots harder to read, and — since look began handing frames to Claude as images — harder for a model to read too. The shipped Default is now the only answer, in FrameImage.Quality. FrameLoader.maxPixelSize stays at 2400 because frames written on Best Quality are still on disk |
| I26 | Footnote about quality vs disk space | DROPPED (2026-08-16 report) with I25 — there is no selection left to footnote |
| # | Requirement | Status |
|---|---|---|
| I27 | Large header with REAL measured usage and "no storage limits set" | MISSING |
| I28 | Storage management radio group: Off / Compress / Limit | PARTIAL by decision (J7) — Off / Limit only. Compress is dropped. It shipped with a red destructive confirmation warning that "the original detail cannot be recovered", and then did nothing whatsoever: the retention sweep tests strategy == .limit, and no re-encoder exists anywhere in this package. A destructive-role warning in front of a no-op is worse than a missing feature — it teaches the user that this app's warnings are noise, and the next one really does delete. Building the re-encoder is a capture-pipeline change, not a Settings one; if it is ever built the case comes back with it. A persisted "compress" decodes to nil in both readers and falls back to off, so no migration was needed |
| I29 | Off is the default: "Keep all your data. Forever." | DONE — and it is the only strategy that destroys nothing |
| I30 | Limit deletes oldest recordings — needs a threshold control AND an explicit confirmation; never reachable by one stray click | DONE, with two corrections the requirement did not anticipate. (a) Limit is two bounds, not one. ContextStore.enforceRetention prunes by age and by bytes, and Engine.ensureStorage starts the sweep with 30 days, so turning Limit on deletes everything older than a month even at a 200 GB threshold on an 8 GB disk. For a release, no string the user could read said so — undisclosed permanent deletion of their data. The radio row, the header caption, the threshold row and the confirmation now all state it, from one constant (StorageLimit.retentionDays), pinned by testEveryStringDescribingLimitDisclosesTheAgePrune. (b) The confirmation was ordering-dependent. Its isPresented setter called cancelStorageChange() on every dismissal, and SwiftUI may write that binding before running the tapped button's action — which nulled the pending that confirmStorage() promotes, so the deliberate second click silently did nothing. Cancelling is now only the explicit Cancel button; testConfirmingSurvivesEitherOrderOfBindingWriteAndButtonAction drives both orderings through the pane's real binding |
| # | Requirement | Status |
|---|---|---|
| I31 | Search field; Apps / Websites segmented control | MISSING (engine DONE) |
| I32 | Apps: Categories → Password Managers, expandable | MISSING (engine DONE) |
| I33 | Apps: Excluded section, locked rows the user cannot remove | MISSING (engine DONE) |
| I34 | Apps: System section (Notifications, Control Center, Spotlight, Siri, Login Screen) | MISSING (engine DONE) |
| I35 | Apps: Recently Recorded, with real icons | MISSING |
| I36 | Apps: All Applications, alphabetical, with icons | MISSING |
| I37 | Websites: "Search or add domain" | MISSING (engine DONE) |
| I38 | Websites: Banks category | MISSING (engine DONE) |
| I39 | Websites: Exclude Private Tabs toggle | PARTIAL — Chrome/Edge/Brave/Firefox detected from title; Safari and Arc cannot be detected at all, so the copy must not overclaim. Arc was listed as covered and never was: measured across 925 Arc frames in the real database, all 13 distinct window titles are the bare page title (Anthropic, LinkedIn, (9) Home / X) with no browser chrome, so no PrivateBrowsing.titleMarkers entry can ever match. The one row matching "Arc" matched inside "Archit". OpenExternally documents the same titles from the other direction. The sentence is now built from two named lists so a browser cannot be moved between them in prose alone |
| I40 | Websites: Recently Recorded domains with favicons | MISSING — favicons are network requests and must obey Airgap Mode |
| # | Requirement | Status |
|---|---|---|
| J1 | Music playing throughout the experience | DONE — cinematic starts the bed and fades it at hand-off; the tutorial runs its own from begin() to teardown |
| J2 | Sounds on clicks | DONE — click on every card and tutorial advance, swoosh on page open and timeline, chime on each earned gate. Chrome cues honour the system UI-sound setting; the bed has its own mute |
| J3 | Interactive and immersive; better than the reference | JUDGEMENT — verify by screenshot |
| J4 | Everything verified by screenshots and screen observation | ONGOING |
| J5 | All edits land on the upstream PR | ONGOING |
| J6 | Never purple anywhere (INV-UI-1) | ENFORCE |
| J7 | Nothing fabricated: no fake screenshots, no staged payoffs, no controls that cannot work | ENFORCE |
These are decisions, not omissions. An audit should confirm each is still the right call rather than "fixing" them.
- The permission drag affordance is macOS 26's own UI (F4). We guide and highlight; we never redraw it.
- Open-externally is hidden, not faked (H9). Matching URLs in window titles resolves 2.7% of
frames and every one is a link rendered inside a page rather than its address. Needs capture to
record
AXURL. - The search bar answers in-app; only the tutorial hands off to Claude — this reverses an
earlier decision. The bar used to route what was typed into Claude and show nothing itself, which
made every recall a round trip through another app. It now searches this app's own indexes and
renders the hits.
ClaudeRoutersurvives for the one act that is genuinely about Claude — the first-run hand-off (I9) — and for the Settings row that steers it. - No second email capture (E7); the Omi account already identifies the user.
- Update controls exist exactly as far as the updater does (I7). They were omitted while there
was no updater; there is one now, so
Check Nowis real — and it disappears again on any buildUpdatePolicyrefuses, which is every locally signed one. The rule was never "no update controls", it was "no control that cannot do what it says". - Safari and Arc private windows are not excludable from the window title alone (I39); the seam exists for AX to fill. Arc's titles carry no browser chrome at all — measured, not assumed.
- Three reference controls are deleted rather than implemented (I8 Route to Agent, I15 Accent Colour, the Compress case of I28). Each had shipped as a control the user could operate that changed nothing, and Compress said so behind a red destructive warning. J7 rules out controls that cannot work, and it applies to controls that already exist as much as to ones not yet written: a dropdown nobody reads is the same fabrication as a fake screenshot, just harder to notice.
- Exclusions cover screen capture only.
ScreenWatcheris the sole caller ofExclusionEngine.admit/revalidate; no audio source consults the engine. Excluding an app suppresses its screenshots, OCR, window title and accessibility text and leaves microphone and system-audio transcription running. The pane says so above the list — on a privacy surface, an unstated boundary is the failure — and names Pause, which is what does stop audio.