Skip to content

M1.5 First Capture Journey — paused for user verdict - #31

Open
kgarg2468 wants to merge 15 commits into
mainfrom
loop/m15-first-capture
Open

M1.5 First Capture Journey — paused for user verdict#31
kgarg2468 wants to merge 15 commits into
mainfrom
loop/m15-first-capture

Conversation

@kgarg2468

@kgarg2468 kgarg2468 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

M1.5 — First Capture Journey

user_verdict: pending — per ADR-008 this PR pauses for Krish's hands-on pass before merge. Testing setup: double-click Test Current Build.command; guide in .context/TESTING.md.

What this slice ships

The first real visible product: capture → encrypted local store → playback/search.

  • Record with pause/resume, live level meter (honest "Silent" state), title/note while recording; stop with ⌘.
  • Encrypted-at-rest session store (AES-GCM, owner-only dirs); playback decrypts on the fly.
  • Granola-inspired UI: Today view (greeting, strong capture card, recents), Moments list, capture detail with scrubbing playback, capture-named delete confirmations.
  • Menu bar item + popover, ⌥Space global toggle + floating HUD, ⌘K command palette with inline capture results, ⌘F search.
  • Runtime honesty chip (Online/Offline within ~15 s) with calm explainer; capture works offline.
  • Full permission journey (explainer → system prompt → denied/restricted recovery) and save-failure journey (Export / Discard / Try Again; retry lands on the saved capture's detail).
  • Packaged ad-hoc-signed Capture Delegate.app via scripts/package-app.sh.

Review trail (ADR-008 gate)

  1. Opus UX contract + Opus UI implementation.
  2. Real Computer Use run on the packaged bundle — 10 scenarios, evidence in .loop/verification/m1.5/first-capture/ux/ (screenshots, AX dumps, scenario-log.md).
  3. Fresh-eyes Opus visual review: approve-with-nits (F7 major, F8 ship-blocker — both fixed).
  4. Independent Sol (gpt-5.6-sol xhigh) technical review: REQUEST-CHANGES (1 blocker, 6 majors, 7 minors).
  5. Fix pass: 4f47b78 (core lifecycle/single-flight/mutation ordering), 1fea360 (F7 inline palette results, F8 retry routing, F5 AX labels, honest copy, delete dialogs), 2016873 (isSaving holds through retry routing; "encrypted when saved" copy in explainer/Today/usage description), c82ed0d (data-loss blocker: save-failure audio renamed held-, always preserved by launch reconciliation).
  6. Sol delta re-review + confirmation passes on the fix commits (final verdict below).

Known limitations (ADR-009, deliberate checkpoint tradeoffs)

AES-GCM AAD binding + versioned on-disk format (M2); per-root store locking (M2); Developer ID signing/notarization (blocked on user identity — ad-hoc signing re-prompts TCC each rebuild); universal binary; testable AppModel target + XCUITest (M2); /tmp socket path convention (F1); rebindable capture shortcut vs Raycast ⌥Space (F6, M3). Addendum: quit with an unresolved save failure now preserves the audio as a held- file, but there is no in-app recovery UI until M2's pending-capture manifest, and a crash while recording or mid-save still loses the temp. Store mutation errors surface on Today only.

Verification

  • swift build, swift format lint --strict, 19 core tests green (includes new held-recording preservation tests).
  • ./scripts/verify-m0.sh exit 0 — run from a short-path worktree (/tmp/m15-verify) because the repo's long path exceeds macOS's ~104-byte Unix-socket limit (SUN_LEN) for the runtime handshake test; same HEAD, full parity.
  • Computer Use delta retest of the post-review fixes (palette inline rows, retry routing, delete dialogs) was blocked at closeout — host Mac locked; scheduled for morning, and the user's hands-on pass covers exactly these flows. s7/s9 evidence shows pre-fix behavior.

🤖 Generated with Claude Code

Greptile Summary

The PR introduces the first complete native capture journey, including microphone recording, encrypted local persistence, playback, search, and recovery-oriented UI.

  • Adds capture lifecycle management with pause, resume, stop, permission handling, and level metering.
  • Adds Keychain-backed AES-GCM session storage and in-memory decrypted playback.
  • Adds Today, Moments, detail, command-palette, menu-bar, HUD, search, and failure-recovery surfaces.
  • Adds release application packaging with an ad-hoc signature and microphone usage metadata.

Confidence Score: 5/5

The PR appears safe to merge after the required user-verdict gate, with the remaining durability and distribution limitations explicitly acknowledged for later milestones.

The reviewed capture, persistence, playback, lifecycle, and packaging paths did not reveal an unacknowledged concrete failure that satisfies the publication criteria.

Important Files Changed

Filename Overview
Sources/CaptureDelegateApp/AppModel.swift Coordinates permission, recording, persistence, retry, navigation, deletion, runtime status, and application lifecycle behavior.
Sources/CaptureDelegateCore/CaptureEngine.swift Implements microphone recording state transitions, timing, level updates, temporary-file ownership, and stale-file reconciliation.
Sources/CaptureDelegateCore/SecureSessionStore.swift Adds staged AES-GCM persistence, session loading and mutation, plaintext cleanup, and owner-only storage directories.
Sources/CaptureDelegateCore/PlaybackEngine.swift Adds playback from decrypted in-memory audio without writing a plaintext playback copy.
Sources/CaptureDelegateCore/KeychainKeyProvider.swift Provides generation and retrieval of the local encryption key through macOS Keychain.
scripts/package-app.sh Builds, assembles, configures, ad-hoc signs, and verifies the macOS application bundle.
Package.swift Adds the native application executable target and its core and IPC dependencies.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as SwiftUI App
    participant Model as AppModel
    participant Capture as CaptureEngine
    participant Store as SecureSessionStore
    participant Keychain as Keychain
    participant Playback as PlaybackEngine

    User->>UI: Start capture
    UI->>Model: requestStartCapture()
    Model->>Capture: Request permission and start
    Capture-->>UI: Recording state and level updates
    User->>UI: Pause / resume / stop
    UI->>Model: stopAndSave()
    Model->>Capture: stop()
    Capture-->>Model: Temporary audio URL
    Model->>Store: save(session, audio URL)
    Store->>Keychain: Load or create encryption key
    Store->>Store: Encrypt metadata and audio
    Store-->>Model: Saved session
    Model-->>UI: Open session detail
    User->>UI: Play capture
    UI->>Store: loadAudioData(session)
    Store->>Keychain: Load encryption key
    Store-->>Playback: Decrypted audio data
    Playback-->>User: Audio playback
Loading

Reviews (1): Last reviewed commit: "Give session-row buttons an accessible t..." | Re-trigger Greptile

kgarg2468 and others added 11 commits July 19, 2026 00:54
…layback

Sol-implemented per .context/plans/m15-core-services-spec.md: CaptureEngine
(AVAudioRecorder AAC, metering, pause/resume accounting), SecureSessionStore
(CryptoKit AES-GCM combined boxes, Keychain key provider seam, atomic staged
saves that preserve recovery audio on failure), PlaybackEngine
(AVAudioPlayer(data:), never touches disk). 12 unit tests, TDD red/green
evidence under .loop/verification/m1.5/first-capture/core/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus-implemented per .context/plans/m15-ui-spec.md and the ADR-008 UX
contract: NavigationSplitView shell (Today/Moments/Projects/Actions/Agent
Runs/Search), complete 10-state capture machine with permission gate and
secure-save recovery, MenuBarExtra, floating NSPanel HUD, Command-K palette,
full keyboard map incl. Option-Space local monitor, runtime health status
(informational, env-var socket probe), honest explanatory destinations,
Reduce Motion/VoiceOver/grayscale-safe states. All data real from
SecureSessionStore; no mocks or dead controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 10 first-capture scenarios pass on the packaged .app; scenario log
records findings F1-F9. package-app.sh now writes
NSMicrophoneUsageDescription via plutil because PlistBuddy's -c parser
cannot carry an apostrophe and its failure did not trip set -e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single-flight start/save with recovery-sheet reopen; owned plaintext
temp-file reconciliation at launch/termination with PID+start-identity
ownership; ordered non-optimistic title/note/delete pipeline with
failure reconciliation; non-destructive export via staged replace;
corrupt-session-tolerant list() with per-entry diagnostics; playback
timer teardown; surfaced pause/resume errors; stale staging-dir cleanup
at store init. Red/green evidence in worker report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… copy

- F7: ⌘K palette lists matching captures inline (cap 5, overflow row),
  row 0 is the true Enter target, arrow-key wrap, Escape closes
- F8: retry-save routing survives sheet dismissal transition
  (recoveringFromFailure sequencing) so Try Again lands on saved detail
- Save-failure copy now discloses the private plaintext temp file
  honestly instead of promising no unencrypted disk write
- Delete confirmations name the capture in Moments and detail views
- VoiceOver labels on menu bar popover controls and idle toolbar button
- Quieter toolbar New Capture (bordered), tabular digits in moment rows
- ADR-009: recorded M1.5 review-finding deferrals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Honest copy in the three remaining spots that promised at-rest
  encryption without disclosing the private in-recording temp file:
  permission explainer, Today capture card, mic usage description
- Hold isSaving through the 400ms retry-routing window so a capture
  started mid-recovery cannot be navigated away (new minor #15)
- ADR-009 addendum: record termination-preserves-pending-plaintext and
  Today-only mutation-error surfacing as deliberate checkpoint
  tradeoffs picked up by M2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sol's confirmation pass proved the ADR-009 addendum wrong: quitting with
an unresolved save failure left the only copy of the user's audio as a
process-owned temp file, and next-launch reconciliation deleted it as a
dead-owner orphan.

- Save failure now renames the pending temp with a held- prefix
  (CaptureEngine.markRecordingHeld); reconciliation always preserves
  held- files. Retry/export/discard follow the renamed URL through
  pendingCapture.
- New test proves a held file survives reconciliation after the owning
  process dies; ownership test extended with a held dead-owner file.
- ADR-009 addendum rewritten to describe implemented behavior, including
  the remaining crash-window limitation left for M2's manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Traceability: 27 M1.5 rows dispositioned (4 implemented, 23 in_progress
for partially-covered requirements) with tested-commit, packaged-app,
Computer Use, and review evidence; ledger validator passes (287 rows).
Roadmap, progress, and state.json record the ADR-008 pause with
user_verdict: pending on PR #31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kgarg2468 added a commit that referenced this pull request Jul 19, 2026
Traceability: 27 M1.5 rows dispositioned (4 implemented, 23 in_progress
for partially-covered requirements) with tested-commit, packaged-app,
Computer Use, and review evidence; ledger validator passes (287 rows).
Roadmap, progress, and state.json record the ADR-008 pause with
user_verdict: pending on PR #31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kgarg2468

Copy link
Copy Markdown
Owner Author

Closeout addendum: Sol's confirmation pass on the final data-loss fix (c82ed0d) returned CONFIRM — held recordings survive reconciliation, all failure paths hold the audio before showing the sheet, rename is idempotent and failure-safe, and the ADR-009 addendum now matches implemented behavior. Full verify-m0.sh exit 0 at final HEAD 90f75dd (short-path worktree, SUN_LEN parity note in the PR body). Ledger validator passes with 287 original rows; 27 M1.5 traceability rows dispositioned. The Computer Use delta retest of the post-review fixes remains scheduled for this morning (host was locked at closeout, IOConsoleLocked = Yes); its results will be appended here.

🤖 Generated with Claude Code

kgarg2468 and others added 3 commits July 19, 2026 02:58
Live delta retest found typing "untitled" in the palette or search
returned nothing while five "Untitled capture" rows were visible: both
predicates matched the raw stored title, which is empty for untitled
captures. Match SessionDisplay.title so what the user sees is what
search finds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The explicit .id(index) on each row replaced the ForEach element
identity, so LazyVStack kept ghost rows on screen when typing reshaped
the list: with query "untitled" the palette rendered four stale command
rows while the real item array was all sessions, making the highlighted
row diverge from the true Enter target. Scroll-on-selection now targets
the element id instead.

Found by live Computer Use retest: pressing Enter with "Start capture"
highlighted opened a session detail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kgarg2468

Copy link
Copy Markdown
Owner Author

Delta retest update (machine unlocked, live Computer Use, user present): two real palette bugs found and fixed on the spot.

  1. Display-title search mismatch (2daee91): ⌘K palette and ⌘F search matched the raw stored title — empty for untitled captures — so typing "untitled" found nothing while five "Untitled capture" rows were on screen. Both predicates now match SessionDisplay.title(...) + note.
  2. Blocker: palette ghost rows / Enter-target divergence (dac0fe0): .id(index) on each row overrode the ForEach(id: \.element.id) identity, so the LazyVStack kept stale command rows on screen as typing reshaped the list. Empirically proven: with "Start capture" highlighted, Enter opened a session detail (evidence delta-03/delta-04 under .loop/verification/m1.5/first-capture/ux/screens/).

Gates on the fixed tree: verify-m0.sh exit 0 from a fresh /tmp worktree; independent Sol (xhigh) re-review of both commits returned CONFIRM (5/5 checks PASS). All 19 core tests green; both bundles repackaged.

Still open: the post-fix visual re-probe of the palette, the retry→saved-detail routing re-run, and delete-dialog/AX spot checks were interrupted — the user began actively using the machine mid-probe, and injecting further keystrokes risked hitting their foreground apps. These will be covered by an idle-machine re-run or the user's hands-on pass. user_verdict: pending unchanged.

…test evidence

The Today recents, Moments, and Search row buttons exposed only combined
metadata as AXValue with no AXDescription, because the inner MomentRow's
accessibility label does not survive SwiftUI's Button flattening. Put
.accessibilityLabel(SessionDisplay.title(...)) on the Button itself,
matching the MenuBarViews pattern; confirmed via the raw AX API that every
row now reports its display title as AXDescription.

Also lands the resumed Computer Use delta-retest evidence: post-fix palette
render and Enter-target alignment, delete-dialog naming, and the full
retry-save journey (failure sheet, held- protection file, Try Again to
saved detail) all pass on the packaged bundle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kgarg2468

Copy link
Copy Markdown
Owner Author

Delta retest complete — all probes pass on the packaged bundle (b27a676).

Resumed the interrupted Computer Use probes after the machine was free:

  • Palette post-fix render: PASS — query "untitled" now shows the Captures caption + exactly the five real sessions, zero ghost rows (delta-06).
  • Enter target alignment: PASS — Enter opens the highlighted session's detail (delta-07).
  • Delete dialog naming: PASS — “Delete "Untitled capture"?” uses the display title (delta-08).
  • Full retry-save journey: PASS — honest mic explainer → recording → forced save failure → failure sheet → held- protection file on disk → Try Again → saved detail with "Saved" badge, held file consumed (delta-09/11/12). Test capture removed via the real delete flow; the five existing captures are untouched.
  • New minor, fixed in b27a676 — session-row buttons in Today/Moments/Search exposed no accessible title (inner MomentRow label doesn't survive Button flattening). Added .accessibilityLabel on the Button per the MenuBarViews pattern; raw AX API now reports AXDescription = "Untitled capture" on every row.

Gates at b27a676: full verify-m0.sh from a fresh /tmp worktree exits 0 (44 tests). Evidence + scenario log updated under .loop/verification/m1.5/first-capture/ux/.

PR remains paused at user_verdict: pending.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant