docktor — Hardening Tracker

live · updated 2026-07-19 17:39:22 MDT · auto-refreshes every 20s

Harden Marathon — live

base d83595e3 · 3 agents on disjoint surfaces, own branch + rolling PR each · 23 fixes shipped to main

23
fixes shipped
3
agents running
1
critical/security
3
privacy/consent
+54
tests added

Agent lanes (live)

each agent owns a disjoint surface cluster and works highest-value-first; refill via a scoped bug-hunt when a lane empties

Agent 1 — Music & Weather

harden/agent-1 · no open PR yet

surfaces: docktor/Services/SpotifyAPI*.swift, docktor/Services/NowPlayingManager.swift, docktor/Services/LyricsService.swift, docktor/Services/WeatherManager.swift, docktor/Widgets/Music/**, docktor/Widgets/Weather/**

  • #1 Weather isDaylight sunrise/sunset misalignment on asymmetric polar-null days
    WeatherManager.swift:665-669 + WeatherHelpers.isDaylight
    two independent compactMaps drop a nil from only one array on a polar transition day, shifting every subsequent index -> wrong day/night glyphs. Pair sunrise/sunset by index instead. (PR #147 review nit 1)
  • #4 Lyrics updateCurrentIndex freezes on any non-monotonic timestamp
    LyricsService.swift:~435-442
    the early `break` assumes ascending timestamps; one out-of-order line makes the highlight freeze for the rest of the song. Verify not already covered by 97c1a16b; if open, fix + table-test.
  • #7 FormatHelpers.formatTime negative-input guard
    Helpers/FormatHelpers.swift:6-15
    negative seconds render as "0:-5"; latent today (callers >=0) but a cheap guard + test. Low.

Agent 2 — Feedback, Telemetry & Worker

harden/agent-2 · PR #150 ↗ Harden marathon — Agent 2 (Feedback/Telemetry/Worker)

surfaces: docktor/Services/Feedback/**, docktor/Core/Telemetry/**, feedback-worker/**

  • #2 Crash-report redaction: broaden beyond /Users home paths
    docktor/Services/Feedback/CrashReportParser.swift redact()
    review found residual leaks: case-sensitive (/USERS/ not caught), URL-encoded paths, and non-path PII (emails/IPs embedded in symbol/termination strings) pass through unredacted. Add case-insensitivity + an email/IP scrub. Fully unit-testable.
  • #5 Worker: verify Cf-Access JWT (or force-disable workers.dev) when Access is enabled
    feedback-worker/src/index.js isDashboardAuthorized
    when CF_ACCESS_ENABLED=true the email header is trusted WITHOUT verifying Cf-Access-Jwt-Assertion; the bare *.workers.dev route stays live and spoofable. Verify the JWT against the team keys, and/or document+guard disabling workers.dev. (PR #147 review HIGH residual). node-testable.
  • #8 Worker session-length median bias (LIMIT 5000)
    feedback-worker/src/index.js:~537-544
    median computed over the 5000 LONGEST sessions overstates the typical session. Needs a corrected query; confirm with a richer D1 fixture.

Agent 3 — Dock Previews, Panels & Calendar

harden/agent-3 · no open PR yet

surfaces: docktor/DockPreviews/**, docktor/Panel/**, docktor/Services/AppleCalendarAPI.swift, docktor/Services/UnifiedCalendarService.swift, docktor/Services/CalendarEventParsing.swift, docktor/Widgets/Calendar/**

  • #3 Dock follower mid-swipe nil-frame abort
    docktor/Panel/WidgetManager.swift:1706-1717
    a single transient nil AX frame mid-swipe aborts the follower for the rest of the gesture -> visible mid-swipe snap. Needs a witness-VM visual gate; if VM unavailable, extract the decision into a pure kernel and unit-test that.
  • #6 Follower slide ignores Reduce Motion
    docktor/Panel/WidgetManager.swift:1582-1591
    setPanelSlideOffsetSmoothed always animates; the settle path honors AnimationPreset.animationsEnabled but the 30Hz follow path does not. a11y.
  • #9 Fresh deep-coverage bug-hunt across DockPreviews + Panel
    docktor/DockPreviews/**, docktor/Panel/**
    highest fix-density area in the repo; run a scoped bug-hunt to refill this lane with concrete, testable defects.

User & developer impact — shipped

What you’ll notice

Visible fixes a beta user experiences directly

  • Weather day/night stays correct near the polesmedium
    Hourly sun/moon icons stay correct in far-north/south locations. Before, a polar day where Open-Meteo omits only sunrise (or only sunset) shifted every following day’s sun times, mislabeling day as night for the rest of the forecast.
    Weather
  • Music scrubber can’t show a time past the endlow
    The elapsed-time label on the music scrubber no longer shows an impossible time like “4:00 / 3:30”. The position ticks locally between player updates; if one was missed it kept counting past the end until the next poll (~30s).
    Music · player scrubber
  • Synced lyrics honor the LRC offset taglow
    Synced lyrics that ship with an [offset] correction now line up with the music instead of running consistently early or late for the whole song.
    Music · lyrics
  • Weather widget survives null hourly stats toohigh
    Extends the earlier polar-null fix: the Weather widget no longer blanks when Open-Meteo omits an hourly humidity, wind, or UV value (which it can past the forecast horizon).
    Weather
  • Widgets no longer snap mid-swipe when the Dock blinks outmedium
    When you swipe between Spaces, the widget bar that follows the Dock no longer jerks to its end position if the Dock's accessibility frame momentarily disappears mid-swipe (a Dock relaunch or permission blip). The slide stays smooth and only settles if the Dock stays unreadable.
    Dock widgets · space swipe
  • Reduce Motion now also stills the widget swipe-follow slidelow
    With General > Animations turned off, the widget bar that follows the Dock during a Space swipe or auto-hide no longer animates its slide and then jumps at the end — it now snaps in step with the Dock, matching the rest of the reduced-motion experience.
    Dock widgets · space swipe / auto-hide
  • Gray/white custom calendar colors save correctly againlow
    Choosing a gray, white, or black custom calendar color no longer stores a wrong bright hue. Color.toHex() previously turned white into #FFFF00 (yellow) and 50% gray into #80FF00 (green) for any grayscale-backed color.
    Calendar · custom colors
  • Weather widget survives partial Open-Meteo datahigh
    The Weather widget no longer goes blank where Open-Meteo omits a value — e.g. polar sunrise/sunset, or precipitation past the forecast horizon. Before, one missing value wiped current + hourly + daily.
    Weather
  • Recently Played shows “X minutes ago” againhigh
    The “X minutes ago” line is back on every Recently Played row. Real Spotify timestamps include milliseconds the app couldn’t parse, so the relative time silently disappeared for everyone.
    Music · Recently Played
  • Join links found on company Zoom/Webex domainslow
    Events whose notes carry a company meeting link like my-team.webex.com or eu01-x.zoom.us now get their Join button — hyphenated company subdomains (and uppercase pastes) were silently dropped before.
    Calendar · meeting join
  • Lyrics stay in sync through repeated choruseslow-medium
    Songs whose lyrics compress repeated choruses no longer show raw [01:45.00] tags in the middle of a line, the chorus highlights every time it comes around, and one odd timestamp can no longer freeze the karaoke highlight for the rest of the song.
    Music · Lyrics
  • Up Next survives a podcast in the queuemedium
    Queueing a podcast episode (or listening to one) no longer wipes the whole Up Next list. Episodes now show up in the queue with the show's name where the artist goes.
    Music · Up Next

Quiet guarantees

Invisible robustness / privacy / security wins

  • Now-playing position stops at the end of the tracklow
    Fixes the root cause behind the scrubber overrun: the playback clock that ticks between player updates now stops at the track end instead of counting past it, so anything reading the position (scrubber, lyrics sync) stays truthful even if an update is missed.
    Music · playback position
  • Calendar countdown logic is now single-sourced and testedlow
    No behavior change for users; the calendar's "in N min" label and the pulsing final-countdown share one implementation now, so a future tweak to those thresholds stays consistent between the widget and the rest of the app.
    Calendar · countdown
  • Calendar countdown thresholds now single-sourced everywherelow
    No behavior change; internal cleanup so the calendar's urgent/final-countdown thresholds live in exactly one place.
    Calendar · countdown
  • Crash reports stop leaking your usernamehigh
    Crash reports sent from the beta no longer expose your Mac username. The old redaction missed bare home-folder paths and could even leak a second username by “bridging” across text.
    Feedback · crash reports
  • Nothing is sent before you’ve seen the disclosurehigh
    A crash that happened before you finished onboarding can no longer be transmitted early. Nothing about a crash leaves your Mac until you’ve actually seen the telemetry disclosure and had the chance to opt out.
    Feedback · telemetry consent
  • One report, one email — even when the network hiccupslow-medium
    If your feedback went through but the reply got lost, the app's automatic retry no longer files the same report twice (and the maintainer no longer gets two alert emails about it).
    Feedback backend (Cloudflare Worker)
  • Feedback backend shrugs off malformed URLs and cookiesmedium
    No visible change — the ops dashboard's edges got sturdier.
    Feedback backend (Cloudflare Worker)
  • Heartbeat metrics can't be flooded or poisonedmedium
    No visible change — this hardens the anonymous liveness ping's backend so beta metrics stay trustworthy.
    Feedback backend (Cloudflare Worker)
  • Only real meeting links auto-open at meeting startmedium
    An event whose URL merely mentions a meeting service (a tracking redirect, a lookalike domain like zoom.us.evil.example) is no longer treated as a video call — which matters because docktor auto-opens that link when the meeting starts.
    Calendar · meeting join
  • Queued feedback can no longer double-send or vanishmedium
    A queued feedback report (or harvested crash summary) is now sent exactly once. Before, the launch drain racing the crash-harvester drain could send the same report twice — or silently drop a report that arrived while another was sending.
    Feedback · retry queue
  • Feedback dashboard auth bypass closedcritical
    Your submitted feedback, screenshots, and install ID can no longer be read by a stranger. The ops dashboard trusted a header that anyone could fake on the default deployment.
    Feedback backend (Cloudflare Worker)

Ranked impact log

#FixSeverityUser valueDev valueWhat it means for youWhat it means for the codebaseTestsCommit
1Weather day/night stays correct near the poles
Weather
mediumMediumMediumHourly sun/moon icons stay correct in far-north/south locations. Before, a polar day where Open-Meteo omits only sunrise (or only sunset) shifted every following day’s sun times, mislabeling day as night for the rest of the forecast.parseWeatherResponse builds sunrise/sunset from the SAME kept indices so isDaylight’s by-index pairing can’t misalign after an asymmetric null. +2 tests (asymmetric-null alignment).+2500b96af
CI (macOS)
1Weather widget survives null hourly stats too
Weather
highHighMediumExtends the earlier polar-null fix: the Weather widget no longer blanks when Open-Meteo omits an hourly humidity, wind, or UV value (which it can past the forecast horizon).Made the hourly relativeHumidity2m/windSpeed10m/uvIndex arrays optional-ELEMENT ([Int?]?/[Double?]?) and nil-safe at the access sites; a single null used to throw the whole decode. +1 decode test.+1151b9654
CI (macOS)
1Widgets no longer snap mid-swipe when the Dock blinks out
Dock widgets · space swipe
mediumMediumMediumWhen you swipe between Spaces, the widget bar that follows the Dock no longer jerks to its end position if the Dock's accessibility frame momentarily disappears mid-swipe (a Dock relaunch or permission blip). The slide stays smooth and only settles if the Dock stays unreadable.WidgetManager.followDockTick aborted the whole gesture on the FIRST nil AX frame, snapping the still-mid-slide panels. Extracted a pure DockFollowerNilFrameGate that tolerates up to 4 consecutive nil frames (~100 ms at 30 Hz) and resets on any readable frame, so only a sustained blackout aborts. Abort behavior unchanged; 3 s safety deadline still bounds the gesture.docktorTests/DockFollowerNilFrameGateTests.swift: +6 tests (single-nil tolerate, tolerance boundary abort, readable-frame reset, fresh-start reset, degenerate tolerance, default-tolerance blackout window).fa01de9e
scripts/build.sh debug + build-for-testing green in isolated worktree; DockFollowerNilFrameGateTests 6/6 pass locally; CI xctest authoritative.
1Weather widget survives partial Open-Meteo data
Weather
highHighHighThe Weather widget no longer goes blank where Open-Meteo omits a value — e.g. polar sunrise/sunset, or precipitation past the forecast horizon. Before, one missing value wiped current + hourly + daily.The Open-Meteo Codable arrays (precip / sunrise / sunset / UV) now accept null elements; a null sun event is skipped and a null precip/UV reads as 0. +2 tests including a null-laden decode that threw before the fix.+268a90028
CI (macOS)
2Music scrubber can’t show a time past the end
Music · player scrubber
lowLow-MedLowThe elapsed-time label on the music scrubber no longer shows an impossible time like “4:00 / 3:30”. The position ticks locally between player updates; if one was missed it kept counting past the end until the next poll (~30s).Added FormatHelpers.clampedPosition(_:duration:) (floors at 0, caps at duration, passes through when duration unknown) and used it for the displayed position; the progress fill was already clamped. +3 unit tests.+38d1d1a1a
CI (macOS)
2Reduce Motion now also stills the widget swipe-follow slide
Dock widgets · space swipe / auto-hide
lowMediumMediumWith General > Animations turned off, the widget bar that follows the Dock during a Space swipe or auto-hide no longer animates its slide and then jumps at the end — it now snaps in step with the Dock, matching the rest of the reduced-motion experience.setPanelSlideOffsetSmoothed animated the content-layer transform unconditionally, while the settle leg (finishSwipeSlide) already zeroed its animation duration when animations were disabled — so follow and settle disagreed. Extracted a pure SmoothedSlideMotion policy (animate vs snap) gated on AnimationPreset.animationsEnabled and wired it into both continuous-follow call sites.docktorTests/SmoothedSlideMotionTests.swift: +4 tests (animate when enabled, snap when disabled, snap on non-positive duration, real follower-smoothing constant animates).4bd8d933
scripts/build.sh debug + build-for-testing green in isolated worktree; SmoothedSlideMotionTests 4/4 pass locally; CI xctest authoritative.
2Recently Played shows “X minutes ago” again
Music · Recently Played
highHighMediumThe “X minutes ago” line is back on every Recently Played row. Real Spotify timestamps include milliseconds the app couldn’t parse, so the relative time silently disappeared for everyone.played_at now parses both fractional- and whole-second ISO8601 (with fallback); the new regression test uses a real millisecond payload that would have failed before. Also capped a previously-unbounded 401 retry loop in search().+3 (incl. fractional fixture)0315f65e
CI (macOS)
3Synced lyrics honor the LRC offset tag
Music · lyrics
lowLow-MedLowSynced lyrics that ship with an [offset] correction now line up with the music instead of running consistently early or late for the whole song.parseLRC now parses the LRC [offset:±ms] metadata tag and shifts every timestamp (positive = earlier), clamped at 0; previously the tag was silently dropped. +2 pure parser tests.+2f120765d
CI (macOS)
3Gray/white custom calendar colors save correctly again
Calendar · custom colors
lowLowMediumChoosing a gray, white, or black custom calendar color no longer stores a wrong bright hue. Color.toHex() previously turned white into #FFFF00 (yellow) and 50% gray into #80FF00 (green) for any grayscale-backed color.Color.toHex() read NSColor(self).cgColor.components positionally as [r,g,b], but a grayscale CGColor has only [white, alpha] — so alpha was read as green and blue defaulted to 0 (confirmed with a live NSColor probe). Now converts via usingColorSpace(.sRGB) before reading channels, matching the already-correct hexString(from:) path. RGB inputs unchanged.docktorTests/CalendarColorTests.swift: +3 regression tests (grayscale-backed white/black/mid-gray). Existing toHex tests only used sRGB Color(red:green:blue:), so never hit the buggy branch.adea931d
scripts/build.sh debug + build-for-testing green; CalendarColorTests 19/19 + CalendarColorManagerTests 5/5 pass locally; live NSColor probe confirmed repro + fix; CI xctest authoritative.
3Crash reports stop leaking your username
Feedback · crash reports
highHigh (privacy)HighCrash reports sent from the beta no longer expose your Mac username. The old redaction missed bare home-folder paths and could even leak a second username by “bridging” across text.redact() rewritten to match a single username segment (no whitespace/newline bridging), handle no-trailing-slash paths, and preserve the real /Users/Shared folder.+3abd2a5e7
CI (macOS)
4Now-playing position stops at the end of the track
Music · playback position
lowLow-MedMediumFixes the root cause behind the scrubber overrun: the playback clock that ticks between player updates now stops at the track end instead of counting past it, so anything reading the position (scrubber, lyrics sync) stays truthful even if an update is missed.Extracted a pure NowPlayingArbitration.tickedPosition(referenceValue:elapsed:duration:) that clamps to [0, duration] (floors at 0 when duration unknown) and routed tickPosition through it. +3 pure unit tests.+36e7ac1b2
CI (macOS)
4Calendar countdown logic is now single-sourced and tested
Calendar · countdown
lowLowHighNo behavior change for users; the calendar's "in N min" label and the pulsing final-countdown share one implementation now, so a future tweak to those thresholds stays consistent between the widget and the rest of the app.UnifiedCalendarEvent duplicated CalendarEventParsing.timeUntil inline and kept isUrgent/isFinalCountdown/finalCountdownSeconds as untestable Date()-reading properties. Moved the countdown classification into CalendarEventParsing as pure (start:now:) functions and delegated the properties to them. finalCountdownSeconds now reads `now` once (was two Date() reads that could disagree across a tick). Verbatim move; existing property-level suites stay green.docktorTests/CalendarEventParsingTests.swift: +3 deterministic kernel tests (urgent/final-countdown windows, ceil rounding + nil-outside-window). CalendarEventParsing/UnifiedCalendarEvent/CalendarCountdownState suites 64/64 pass locally.181202da
scripts/build.sh debug + build-for-testing green in isolated worktree; 64/64 across the 3 affected suites pass locally; CI xctest authoritative.
4Nothing is sent before you’ve seen the disclosure
Feedback · telemetry consent
highHigh (consent)HighA crash that happened before you finished onboarding can no longer be transmitted early. Nothing about a crash leaves your Mac until you’ve actually seen the telemetry disclosure and had the chance to opt out.CrashReportHarvester now requires the TelemetryConsent watermark AND the opt-out toggle, matching the heartbeat gate. The old code checked only the toggle, so a pre-onboarding report could flush on the next launch.+1 new, 8 adaptedd77e275d
CI (macOS)
5Calendar countdown thresholds now single-sourced everywhere
Calendar · countdown
lowLowMediumNo behavior change; internal cleanup so the calendar's urgent/final-countdown thresholds live in exactly one place.AppleCalendarEvent held a third inline copy of the 300 s urgent / 5 s final-countdown thresholds + ceil rounding (its timeUntil already delegated to CalendarEventParsing; the classifiers did not). Delegated isUrgent/isFinalCountdown/finalCountdownSeconds to the kernel so CalendarEventParsing is the single source of truth across AppleCalendarEvent and UnifiedCalendarEvent. Behavior identical.Covered by the CalendarEventParsing kernel tests added in the previous commit; calendar suites 64/64 pass locally.210f6e1c
scripts/build.sh debug + build-for-testing green in isolated worktree; calendar suites pass locally; CI xctest authoritative.
5Feedback dashboard auth bypass closed
Feedback backend (Cloudflare Worker)
criticalHigh (PII)CriticalYour submitted feedback, screenshots, and install ID can no longer be read by a stranger. The ops dashboard trusted a header that anyone could fake on the default deployment.isDashboardAuthorized no longer trusts the spoofable Cf-Access-Authenticated-User-Email header unless CF_ACCESS_ENABLED is explicitly set; otherwise it requires the signed session cookie. Fully verified locally.+2 (npm 74/74 green)3cb2c61e
npm 74/74 (local, full)
6Queued feedback can no longer double-send or vanish
Feedback · retry queue
mediumMed-HighMediumA queued feedback report (or harvested crash summary) is now sent exactly once. Before, the launch drain racing the crash-harvester drain could send the same report twice — or silently drop a report that arrived while another was sending.flushPending collapses overlapping drains behind an in-flight flag, re-reads the live queue every iteration, and removes sent items by value instead of position. New FeedbackSending protocol seam + a gated test sender make the interleavings deterministically testable.+2 (deterministic interleaving)803a9da6
CI (macOS)
7Up Next survives a podcast in the queue
Music · Up Next
mediumMediumMediumQueueing a podcast episode (or listening to one) no longer wipes the whole Up Next list. Episodes now show up in the queue with the show's name where the artist goes.SpotifyQueueItem discriminates on the payload's `type`: episodes decode into a minimal SpotifyEpisode (show + images) instead of failing SpotifyTrack's artists/album requirements; items without a discriminator keep the strict track shape so malformed tracks still fail loudly.+2 (mixed track/episode fixture)11a74967
CI (macOS)
8Only real meeting links auto-open at meeting start
Calendar · meeting join
mediumMedium (safety)MediumAn event whose URL merely mentions a meeting service (a tracking redirect, a lookalike domain like zoom.us.evil.example) is no longer treated as a video call — which matters because docktor auto-opens that link when the meeting starts.isVideoConferenceURL now parses the URL and matches the real host — exact for meet.google.com / teams.microsoft.com / facetime.apple.com, domain-suffix for zoom.us / webex.com — http(s) only, case-insensitive. Substring tricks via path, query, userinfo, or host suffix all fail closed.+20c774471
CI (macOS)
9Lyrics stay in sync through repeated choruses
Music · Lyrics
low-mediumMediumMediumSongs whose lyrics compress repeated choruses no longer show raw [01:45.00] tags in the middle of a line, the chorus highlights every time it comes around, and one odd timestamp can no longer freeze the karaoke highlight for the rest of the song.parseLRC consumes every stacked [mm:ss.xx] tag (one entry per timestamp, time-sorted, unique post-sort ids); the position tracker is a full-scan currentLineID kernel shared with the test seam instead of an early-break walk plus a hand-drifted copy.+397c1a16b
CI (macOS)
10Heartbeat metrics can't be flooded or poisoned
Feedback backend (Cloudflare Worker)
mediumLowMedium-HighNo visible change — this hardens the anonymous liveness ping's backend so beta metrics stay trustworthy.uptimeSeconds clamps to [0, one year] and non-finite input stores 0 (Infinity used to reach the D1 bind); the heartbeat_events append self-guards in SQL (NOT EXISTS a same-install row newer than 60s, same single batch), so a token-holding spammer can't grow the table or the derived metrics without bound. App pings every 120s — honest events are never dropped.+2 (npm 76/76 green)eef33c74
npm 76/76 (local, full)
11Feedback backend shrugs off malformed URLs and cookies
Feedback backend (Cloudflare Worker)
mediumLowMediumNo visible change — the ops dashboard's edges got sturdier.A stray %zz in a dashboard/blob path or a garbage session cookie threw URIError out of decodeURIComponent into the top-level catch: an unauthenticated 500 plus log noise. safeDecode falls back to the raw string, which then fails closed through each path's own strict lookup (302 to login / 404).+1 (npm 77/77 green)92c10241
npm 77/77 (local, full)
12Join links found on company Zoom/Webex domains
Calendar · meeting join
lowLow-MediumLow-MediumEvents whose notes carry a company meeting link like my-team.webex.com or eu01-x.zoom.us now get their Join button — hyphenated company subdomains (and uppercase pastes) were silently dropped before.Free-text extraction patterns allow hyphenated multi-level subdomain segments for Zoom/Webex and match case-insensitively via (?i), while the service domain stays anchored immediately before the first path slash so lookalike hosts still never match.+2f8e0acc6
CI (macOS)
13One report, one email — even when the network hiccups
Feedback backend (Cloudflare Worker)
low-mediumLow-MediumMediumIf your feedback went through but the reply got lost, the app's automatic retry no longer files the same report twice (and the maintainer no longer gets two alert emails about it).Every /submit runs a dedupe probe (15-min window on category + message + meta_json) before writing; a verbatim retry returns the original id with deduplicated: true and skips row, blobs, and email. meta.submittedAt is stamped at payload creation, so distinct reports never collide. No schema change.+2 (npm 79/79 green)862c3bbf
npm 79/79 (local, full)

What’s next — ranked backlog

#Target (surface)User valueSeverityConfCostScoreStatus
1Spotify Recently-Played timestamp parse + bounded search retry (Music) — bare ISO8601DateFormatter drops fractional played_at → "Xm ago" vanishes every rowHighHighHighS78LANDED — 0315f65e
2Worker dashboard auth bypass via spoofable Cf-Access-Authenticated-User-Email (feedback-worker src/index.js:909-917) — any request with that header reads all beta-tester PII; default deploy has no Access app in frontHigh (PII)Critical (security)HighS96LANDED — 3cb2c61e (npm test 74/74, fully verified)
3Crash-report redaction leaks usernames (CrashReportParser.swift:155-159) — regex misses /Users/x w/o trailing slash and greedy-bridges across text/newlines, leaking a second usernameHigh (PII)High (security)HighS90LANDED — abd2a5e7 (build + test-target compile green; full run on CI)
4Crash harvester ignores consent watermark (CrashReportHarvester.swift:68-71) — can transmit a crash summary before onboarding/opt-out (heartbeat gates on both; harvester doesn't)High (consent)High (privacy)HighS88LANDED — d77e275d (build + test-target compile green; full run on CI)
5Weather: non-optional Open-Meteo arrays (WeatherManager.swift:176,200-203) — one null (polar sunrise/sunset, null precip) fails decode → entire forecast blanksHigh (widget dead)HighHighS84LANDED — 68a90028 (build + test-target compile green; full run on CI)
6FeedbackService flushPending re-entrancy (FeedbackService.swift:191-205) — boot + harvest drains overlap → duplicate sends / lost queue itemsMed-HighMedHighM72LANDED — 803a9da6 (seamed via FeedbackSending; full run on CI)
7Spotify queue decode fails on podcast episode (SpotifyAPI.swift:44-59) — a podcast in the queue throws → whole "Up Next" blanksMedMedMed-HighM70LANDED — 11a74967 (typed SpotifyQueueItem; full run on CI)
8Calendar isVideoConferenceURL substring match (CalendarEventParsing.swift:48-57) — lookalike/redirect URLs classified as meetings and auto-opened at startMedMed (auto-open)HighS68LANDED — 0c774471 (host-anchored; full run on CI)
9Lyrics multi-timestamp LRC lines (LyricsService.swift:392,399-406) — compressed chorus tags render raw [mm:ss] text and only highlight onceMedLow-MedMedS60LANDED — 97c1a16b (with #10, same-file pair)
10Lyrics updateCurrentIndex non-monotonic freeze (LyricsService.swift:435-442) — early break freezes highlight after any out-of-order timestampMedLow-MedMedS58LANDED — 97c1a16b (with #9; kernel-extracted, seam delegates)
11Worker /beta/heartbeat unbounded (src/index.js:222-281) — no rate limit, unbounded uptime → D1 growth / metric pollution (install_id cap already landed on main; backlog note part-stale)Med (dev)MedHighS56LANDED — eef33c74 (npm test 76/76, fully verified locally)
12Worker malformed cookie/path → unauth 500 (src/index.js:982-991,66) — decodeURIComponent throws → 500 instead of fail-closed 302; add safeDecodeLow-Med (dev)MedHighS50LANDED — 92c10241 (npm test 77/77, fully verified locally)
13Calendar meet regex misses hyphenated vanity subdomains + case (CalendarEventParsing.swift:72-79)Low-MedLowHighS48LANDED — f8e0acc6 (full run on CI)
14Worker /submit no idempotency → duplicate rows/blobs/emails on client retry (src/index.js:151-209)Low-MedLow-MedHighM44LANDED — 862c3bbf (npm test 79/79, fully verified locally)

Round log

RoundTargetPlan fileCommitVerifyCyclesFilesNotes
0Assess + prioritize + planthis ledger + spotify-recently-played-timestamp-2026-07-19.mdSeeded from latent-bugs-2026-07-19.md; 4 explore agents verified against current tree; most of snapshot already landed; new backlog built
1Spotify played_at parse + bounded search retryspotify-recently-played-timestamp-2026-07-19.md0315f65eHost build gate + test-target compile GREEN; full test+coverage deferred to CI/VM1SpotifyAPI+Library.swift, SpotifyLibraryFetchTests.swift3 tests added incl. fractional fixture that would have failed pre-fix
2Fresh bug-hunt (4 parallel explore agents)this ledgerBeta-visible seed exhausted; surfaced 14 new targets (security/privacy on top): worker auth bypass, crash-report PII leak, consent gate, Weather null-decode, etc.
2Worker Cf-Access auth-bypass fix(from bug-hunt)3cb2c61enpm test 74/74 GREEN (full gate, local)1feedback-worker/{src/index.js,test/dashboard-auth.test.js,README.md,.dev.vars.example}Bypass closed by default; Access honored only when CF_ACCESS_ENABLED="true"
3Crash-report redaction PII leak fix(from bug-hunt)abd2a5e7Build + test-target compile GREEN; full run on CI1CrashReportParser.swift, CrashReportParserTests.swiftUsername no longer leaks (bare path + whitespace-bridge); /Users/Shared preserved; +3 tests
4Crash harvester consent-watermark gate(from bug-hunt)d77e275dBuild + test-target compile GREEN; full run on CI1CrashReportHarvester.swift, CrashReportHarvesterTests.swiftRequires consent watermark AND opt-out toggle (mirrors heartbeat); +1 new test, 8 adapted
5Weather null-element decode tolerance(from bug-hunt)68a90028Build + test-target compile GREEN; full run on CI1WeatherManager.swift, WeatherModelTests.swift, WeatherManagerTests.swift5 Open-Meteo arrays accept null elements; +2 tests
PRCheckpoint: push + full critical review of PR #1470cdf382eCI GREEN at 0cdf382e (macOS build-and-test 1997 tests + coverage, dockwitness, worker 74/74, Pages)OptOutTransmissionTests.swiftReviewed the full 20-file diff end-to-end. 1 blocker found + fixed: round 4's consent-watermark gate missed the release-checklist proofs in OptOutTransmissionTests — the consent-on control never established the watermark, producing exactly the 2 CI issues at fe370122. Fixed by establishing the watermark in both proofs (mirrors the round-4 adaptation). The green run also test-validates the three CI-deferred fixes abd2a5e7, d77e275d, 68a90028 at head. Non-blockers: redaction space-in-home-dir residual logged under Parked. Review was performed inline (single reviewer) rather than via parallel subagents — full diff read, every hunk.
6FeedbackService flushPending re-entrancy(from bug-hunt)803a9da6CI GREEN at 3ac586f1 (macOS build-and-test + coverage, dockwitness, worker, Pages)1FeedbackService.swift, FeedbackSender.swift, FeedbackServiceTests.swiftOverlapping drains collapse via in-flight flag; live-queue re-read each iteration; remove-by-value (cap-drop safe). New FeedbackSending seam + GatedFeedbackSender test double; +2 deterministic interleaving regression tests that fail pre-fix
7Spotify queue: podcast episode blanks Up Next(from bug-hunt)11a74967Logic-reviewed; full test run on CI at head1SpotifyAPI.swift, SpotifyAPI+Library.swift, SpotifyModelTests.swift, SpotifyLibraryTests.swiftSpotifyQueueItem discriminates on type; episodes render with show-as-artist; absent type stays strict-track. File overlap with round 1 (SpotifyAPI+Library.swift, disjoint hunks) — same Spotify theme, splits into the same theme PR
8Calendar lookalike-URL meeting classification(from bug-hunt)0c774471Logic-reviewed; full test run on CI at head1CalendarEventParsing.swift, CalendarEventParsingTests.swiftHost-anchored match (exact for meet/teams/facetime, suffix for zoom.us/webex.com), http(s)-only, case-insensitive; rejects path/query/userinfo/host-suffix tricks. Auto-open surface hardened
9Lyrics LRC pair: stacked chorus tags + non-monotonic highlight freeze (#9+#10)(from bug-hunt)97c1a16bLogic-reviewed; full test run on CI at head1LyricsService.swift, LyricsParserTests.swiftOne round for both (same 50-line kernel; round-1 precedent). Stacked tags expand + time-sort with post-sort unique ids (id contract: source-index → time-order, ids only ever opaque); currentLineID kernel extracted, _testCurrentIndex delegates instead of drifting. +3 tests
PRCheckpoint #2 (after fix 10): full critical review of rounds 6–9 delta(review)CI GREEN at 3b89d61e (build-and-test 21:40Z; note the runs at 5b2e891 and 441a361 were CANCELLED by concurrency superseding, so 3b89d61e is the single authoritative macOS run for rounds 7–9)git diff 0cdf382..3b89d61No blockers. Two deliberate contract adaptations, both logged (OptOut watermark at checkpoint 1; lyrics id order in round 9 — test renamed + strengthened with uniqueness assertion). Cross-file consumer sweep: lyric ids only ever opaque (fixture/integration tests construct their own); calendar consumers unchanged interface; no new silent fallbacks. PR body refreshed with full landed list. Review performed inline (single reviewer), full delta read
10Worker heartbeat: uptime clamp + per-install event floor (#11)(from bug-hunt)eef33c74npm test 76/76 GREEN (full gate, local; was 74)1feedback-worker/{src/index.js,test/handlers.test.js}Finite clamp to [0, 1y] (Infinity stored 0, was a failing bind); heartbeat_events append self-guards in SQL (NOT EXISTS newer-than-60s row per install, same batch — no extra round-trip); live upsert unconditional; app pings 120s so honest events never dropped
11Worker safeDecode: malformed %-encoding fails closed (#12)(from bug-hunt)92c10241npm test 77/77 GREEN (full gate, local; was 76)1feedback-worker/{src/index.js,test/dashboard-auth.test.js}safeDecode (raw-string fallback) at all four decodeURIComponent sites; undecodable input flows to each path's own 302/404 instead of a pre-auth 500. File overlap with rounds 2/10 (src/index.js, disjoint hunks) — same worker theme
12Calendar vanity-subdomain extraction (#13)(from bug-hunt)f8e0acc6Logic-reviewed; full test run on CI at head1CalendarEventParsing.swift, CalendarEventParsingTests.swiftZoom/Webex subdomains allow hyphens + multiple levels; (?i) on all patterns; domain still anchored before the first path slash (lookalikes rejected, parity with round 8). File overlap with round 8 (same files, disjoint hunks) — same calendar theme
13Worker /submit retry idempotency (#14)(from bug-hunt)862c3bbfnpm test 79/79 GREEN (full gate, local; was 77)1feedback-worker/{src/index.js,test/handlers.test.js}Dedupe probe (window+category+message+meta_json, 15 min) before writes; verbatim retry returns original id, writes no row/blob/email; fresh submittedAt in meta means new reports never collide; no schema change. File overlap with rounds 2/10/11 (worker theme)

All commits on the branch (newest first)

Generated by the harden-marathon tracker · impact data: .cursor/skills/harden-marathon/landed.json · ledger: .cursor/plans/harden-marathon-2026-07-19.md