Files
LlamaCasty/HANDOFF.md
T
gramps 55ca1029a3 UNIT 0: make a dropped webcam frame measurable, not just inferable
The creator is DHH and lipreads their own recordings, so a webcam that
judders is an accessibility defect, not a polish item. Take
ty-20261005-1001-821-0001.mp4 holds 25.6 distinct webcam images/s against
the camera's 30fps with 281 gaps of 50-100ms - roughly one hitch in five -
in a file that muxes flawlessly. Nothing in the app could see that: rawvideo
carries no per-frame timestamps (ffmpeg stamps by ARRIVAL at the declared
60fps), and the stats line divided every average by the number of frames
SUBMITTED, which deadline padding always fills to the full target. So the
pump reported "300/300 frames, avg render 12.4ms" while producing 60
distinct composites.

Helpers/FrameCadence.cs is one pure counter fed from three points:
MediaCaptureFrameSource logs the device's own SystemRelativeTime stamps
against our arrival times (TryAcquireLatestFrame keeps only the LATEST
frame, so falling behind is silent by design), and FramePump logs the
distinct webcam Epoch values that reached a composite. Read together they
name the culprit - device/USB, our reader, or our compositor - instead of
guessing. Render/resolve/wait now divide by the real render count, and
"frames per 5s from R renders" is the duplicate-on-lag ratio stated
honestly.

An out-of-order timestamp counts as a frame but produces no gap and does not
become the next interval's reference, so one bad stamp cannot invent a
multi-second hitch twice.

Also corrects a stale ai.md line (the "webcam renders at ~18fps" figure
came from a different session and counted every newly rendered layer) and
re-records the background-removal ruling as permanently out. Records the
agreed-but-unbuilt Units 1-2, the no-fallback-camera ruling, and the
SetFormatAsync dead end found by probing the 19041 projection.

Suite 402/402, 0 warnings. The one red, LayerReorderPersistenceTests'
RealMouseDrag, fails identically on a stashed clean tree - it needs an
interactive desktop session this shell lacks.
2026-10-05 11:10:22 -07:00

33 KiB
Raw Blame History

HANDOFF — current state

Branch: main (pre-1.0, no feature branches — creator ruling 2026-08-24). Pushed 2026-10-04: origin/main = 048cab7. NOT PUSHED since — local main is ahead and the tree is clean. The stale-push-state failure MyMistakes.md warns about has now bitten twice, so the rule stands: check git rev-list --count origin/main..main before trusting any line here about push state.

Unpushed: 5ad0106 broadcast diagnostic → 74da99c + 0fb2f9c privacy/Test rulings → 8fbc370 diagnostic parse fix → 07a088a the explicit-bind fix → 509ef36 window placement → b2f2197 handoff → UNIT 0 frame-cadence measurement.

🚧 IN FLIGHT 2026-10-05 — UNIT 0 shipped: a dropped webcam frame is finally measurable

The measurement the map never had. Take ty-20261005-1001-821-0001.mp4 (85.6s, 1920×1080, H.264 NVENC, 5124 frames @60) holds 25.6 distinct webcam images/s in its lower-right region against the camera's 30fps: 1,153 normal 33ms intervals, 281 gaps of 50–100ms, 96 bunched 17ms gaps. One hitch in five — the judder a DHH creator lipreads against, invisible in a file that muxes flawlessly. Measured with signalstats/YDIF over crop=480:270:1400:780; the bundled ffmpeg has no mpdecimate. ⛔ The old "webcam renders at ~18fps" claim was wrong (different session, counted all newly rendered layers) — corrected in ai.md.

Shipped: Helpers/FrameCadence.cs (pure counter: frames, gaps, hitches ≥50ms, longest/avg), fed from three points — MediaCaptureFrameSource logs Camera cadence WxH: arrivals … | camera … (our delivery vs the device's own SystemRelativeTime), and FramePump logs distinct webcam Epochs that reached a composite. Full detail in ai.md → "Frame cadence".

Also fixed, and it was a real measurement bug: every pump average was divided by the number of frames submitted, which deadline padding always fills to the full 60fps target — so a pump fed one webcam frame per 200ms logged "300/300 frames, avg render 12.4ms". Render/resolve/wait now divide by statRenders and the line reads 300/300 frames per 5s from 60 renders. That pair IS the duplicate-on-lag ratio. Test: FramePumpTests.Stats_ReportRealRendersAndDistinctWebcamFrames_….

⏭ NEXT STEP — needs the creator, one short recording (60s is plenty): record with the webcam visible and paste nothing — I read %APPDATA%\ytLlive\startup.log for the Camera cadence and FramePump stats lines. Reading them together names the culprit: camera steady + arrivals ragged = we drop it (reader/compositor); ragged in both = device/USB/driver; pump webcam below arrivals = the compositor missed frames the reader already had. Then Unit 1 (exclusive lock + 720p, see below) is judged against those numbers.

Agreed but NOT yet built (decided 2026-10-05, so a later session does not re-derive it):

  • Unit 1 — exclusive camera lock + 720p on the camera the creator picked. Open with MediaCaptureSharingMode.ExclusiveControl; if init is refused, reopen the SAME camera in SharedReadOnly. Target MJPG ≥30fps up to 1280×720, ranked by frame rate then size (this also fixes the existing candidate-ordering bug). The creator REVERSED the ban on taking an exclusive camera lock — that ban was never written into the map, which is why the reversal must be recorded this time. Accepted trade-off: an app needing camera control that opens it after us will fail.
    • ✅ API probe DONE 2026-10-05 against the actual 19041.57 projection (a throwaway compile-probe file, since deleted — do not re-derive, and note the build cost of finding out): MediaCaptureSharingMode.ExclusiveControl is projected, so the old comment in MediaCaptureFrameSource.cs ("Exclusive is not projected by the 19041 SDK") is wrong and must be corrected. Exclusive and SharedReadOnly are the only two MediaCaptureSharingMode values. MediaFrameSource.SupportedFormats and CurrentFormat exist. ⛔ MediaFrameSource.SetFormatAsync(MediaFrameFormat) is a DEAD END here — it takes a MediaFrameFormat, and that type has no public constructor and no static factory in the projection (it is only obtainable from a live frame's VideoMediaFrame.Format). The two-argument (subtype, properties) overload in Microsoft's docs does not exist here. So the format change must go through the VideoDeviceController.SetMediaStreamPropertiesAsync path the code ALREADY has — it is only refused because SharingMode is SharedReadOnly. Under an exclusive lock the existing negotiation should simply start working, which is a much smaller change than planned.
    • Remaining unknown (needs a real device, not research): whether SetMediaStreamPropertiesAsync actually succeeds under ExclusiveControl on this C920. The MediaFrameSource.SupportedFormats route is the fallback probe if it refuses — enumerate, and log what the device offers before assuming anything.
  • Unit 2 — blinking "SD" badge in the preview only, when a better format was available and we did not get it. Top-right corner of the webcam, or the 1:30 point ON THE RIM for a round clip (a round webcam has no corner); outside the mirror transform. ~1Hz opacity blink (under the 3/sec photosensitivity limit), tooltip on hover only, must never appear in the recording or stream (the compositor draws from camera frames, not the WPF tree). Style lives in Themes/Controls.xaml — a DataTrigger storyboard cannot be stopped inside a DataTemplate (PreviewPane.xaml:168-171).
  • ⛔ NO fallback-camera setting, ever — creator ruling: "no fallback - that's bypassing the user's explicit choice." Picking a vcam is a user action in the camera picker. Corollary, also recorded in ai.md: Windows Studio Effects is NOT a vcam — it runs inside the physical camera's driver and never appears as a selectable device. Only Broadcast / XSplit VCam install one.
  • ⛔ Background removal: permanently out. Creator confirmed the existing note: "no - not doing it. the note is correct and accurate." ai.md said "milestone 2", which was stale — now corrected.

🚨 FIXED 2026-10-04 — broadcasts never went live because they were never BOUND

Root cause, proven by the diagnostic rather than inferred: contentDetails.boundStreamId on liveBroadcasts.insert does not bind the broadcast. The log line read lifeCycle=created privacy=private autoStart=true bound=NONE boundMatches=NO-BINDING | streamStatus=active.

created is Google's "incomplete settings" state, so enableAutoStart had no bound stream to fire from, and close-out 403'd invalidTransition because created → complete is not a legal edge. The reason it survived a week: a reusable stream accepts bytes for a broadcast that is not watching it, so a perfectly healthy 99 MB RTMP session is exactly what an unbound broadcast looks like. Google's migration guide states the rule outright — "a liveBroadcast must be bound to exactly one stream before the live broadcast can actually start" — and it was in the docs read during the original design.

Shipped 07a088a: BindBroadcastAsync → POST liveBroadcasts/bind?part=id&id&streamId (no body), called right after CreateBroadcast and before the pump; failure is fatal to the session because an unbound broadcast can never go live. boundStreamId and CreateBroadcast's streamId parameter both removed. The old test asserting insert-time binding laundered a guess into a guarantee and was replaced. Also corrected the TASK 44 claim that liveChatId only appears once live — it is present while lifecycle is created; that claim came from reading a sample's active-status guard as a spec.

⏭ NEXT STEP — needs the creator, one 60-second Test: confirm Broadcast bound: appears and lifecycle walks created → ready → live. If it binds but still won't auto-start, the next suspect is an explicit transition, not the binding — do not re-open the binding question without log proof.

Coverage gap: no automated test asserts PrepareAndStartLiveAsync actually calls bind — the service method is tested in isolation, but the wiring in the ViewModel is not. Acceptable only because the end-to-end symptom was the whole point of the diagnostic; revisit if the wiring is ever touched again.

🚨 FIXED 2026-09-27 — "Change Account" no longer signs you out before the browser opens

Reported as: "I clicked on my signed-in account pic, then clicked sign-in to another account. It opened the browser page to login under a different account and I canceled the request. But when returned to the app, I see logged out — and should not have happened! Cannot sign-in because the callback url was not released." Also reported as hitting both running instances.

Two independent defects, and the second one was masking a race:

  1. The sign-out ran first. ChangeAccountAsync was await SignOutYouTubeAsync(); await SignInAsync();. SignOutYouTubeAsync → TokenStore.Clear() → deletes ytLlive.auth. So the working account's token was destroyed before the browser even opened; cancelling the chooser left nothing. Now ChangeAccountAsync is just await SignInAsync() — success overwrites the token via the sessionChanged hook, cancel/failure keeps the current session. No sign-out is needed at all.
  2. The retry couldn't rebind the callback port. Abandoning the browser gives the app no signal, so the attempt held localhost:8765 for its full 5-minute timeout. A new AuthenticateAsync now cancels and awaits the abandoned attempt before binding, serialised by a SemaphoreSlim (_authGate) — without the gate, the takeover window let a third caller see a free port and race to bind it, i.e. the takeover reproduced the original failure. My own test caught this.

The cross-instance claim was NOT the cause and is still open as a separate question: no code path can sign out a second instance (TokenStore is per-InstanceProfile.DataRoot, Clear() is a local file delete with no server-side revoke, and no second instance had a token at inspection time). Verify both processes carried distinct YTLIVE_INSTANCE values and ran Debug builds before chasing it.

Test: YouTubeAuthServiceTests.RetryingAfterAnAbandonedAttempt_ReleasesTheCallbackPort. Full suite 370/370, 0 warnings (build ytLive.Tests.csproj, not just the app project, or VSTest runs a stale test DLL). Added an openBrowser ctor seam to YouTubeAuthService so tests never launch a real browser. Coverage gap: the ordering fix (defect 1) has no automated test — _youtubeAuth is hardcoded in MainViewModel and ChangeAccountAsync is private, so a real test would shell out to the browser. Wiring an auth seam into MainViewModel is a deliberate follow-up, not a bug-fix extra.

🗄 PARKED — Google OAuth verification: attempted, hit a Google-side bug, tabled by ruling

Creator ruling: unimportant for where we currently sit. Verification is not a live blocker and must not be re-attempted, re-derived, or re-researched without a new ruling. The 2026-09-27 entry that preceded this one wrongly sat at ⏭ NEXT STEP and framed it as the top priority — that was the plan going in, not the outcome, and the outcome was never written down. Corrected 2026-09-28.

What happened: the verification attempt was made and failed on a bug on Google's side — not something we can fix, work around, or retry our way past. ⛔ The exact mechanism was never recorded and is deliberately left blank rather than reconstructed from memory: a confidently wrong note here is worse than none, because the next session would trust it. Do not go looking for the cause.

Owning account (creator-confirmed): llamachile.shop@gmail.com ("gramps"). Cloud project number 198374416003 (from the ClientId) — banked in ai.md:233, do not re-hunt it. The one-hop console link is https://console.cloud.google.com/apis/credentials?project=198374416003 (picker defaults to "Recent" — switch to "All"). ⛔ Not the same account as the YouTube channel signed into the app.

What tabling costs, stated honestly: if the consent screen is still in Testing, the 100-user cap and the 7-day refresh-token expiry both stand — that expiry is why the creator kept having to re-consent. Parking verification does not fix that; it accepts it for the solo-use phase. It becomes real work the moment a second real user needs to sign in, or at Store submission (TASK 48). The sensitive-vs-restricted classification was never read and is moot until then.

One of these two survives the tabling, and the other was a bad idea (corrected 2026-09-28):

  • ✅ DONE — Services/YouTubeAuthService.cs no longer requests auth/youtube.force-ssl. It is redundant: its grant is "see, edit, and permanently delete your YouTube videos, ratings, comments and captions", a strict subset of auth/youtube's "Manage your YouTube account", which is the scope we keep. Google's own liveBroadcasts.insert reference lists auth/youtube as an accepted alternative to force-ssl, so every endpoint this app calls (channels, liveBroadcasts, liveStreams, liveChat, playlistItems) is covered. Two consent lines became one. No forced re-consent — a refresh token keeps the scopes it was originally granted, so existing sessions are untouched; the reduced set applies at the next fresh sign-in. Removing a scope cannot narrow capability when the one kept is broader.
  • ❌ NOT doing this, and the earlier note recommending it was wrong: moving playlistItems.list off the user's session token. It was written during the verification hunt on the theory that "public data should not ride on user credentials". Walking the code kills it: ChannelStatsService is built as new ChannelStatsService(_youtubeAuth) with one HttpClient, and channels?mine=true (same service) genuinely needs the session. To split it you would need a second unauthenticated client plus a Google API key, because every YouTube Data API request needs a credential — without a key the call 403s. So the "fix" means shipping a new extractable credential in the binary, subject to quota abuse, to replace a token the app already holds. It buys zero verification-surface reduction (the data is public; no scope is involved) and zero correctness. Strictly worse. ⛔ Do not "fix" this later without a new ruling.

✅ DECIDED 2026-09-27 — pricing: $10/mo subscription, or $400 lifetime, no annual

**Creator's ruling, verbatim: "Let's just go with $10 monthly recurring subscription for option A and, for option B, a lifetime sub for $400. No annual." ⛔ No .99 prices (earlier ruling: "Let's stop with the x.99 stuff - I find that irritating. Just say: ten bucks a month"). This supersedes the 2026-09-21 one-time $29 → $49 model and the old $99/yr sub. No feature gating: free gets everything, the branding flash is the only paid delta.

✅ Store revenue share VERIFIED — 15%, and subscriptions are NOT penalised. Read from the ADA v8.10 PDF itself (downloaded, text-extracted — not a search excerpt). §6(b)(i) charges 15% on "any Apps (and any In-App Products in such Apps) that are not listed in Section 6(b)(iii)"; 12% is Games-only; 30% covers Xbox console apps/games, Xbox non-subscription IAP, and Windows 8/Phone 8. LlamaCasty is a Windows PC App — not a Game, not Xbox, not Win8 — so 6(b)(i) governs both tiers. The agreement's own changelog (v8.0, Oct 26 2017) says it outright: "implement the 85/15 revenue share for non-Game subscriptions."

⇒ $10/mo nets $8.50/mo (~$102/yr). $400 lifetime nets $340. Also confirmed: the 15% applies after VAT/GST (Net Receipts), payouts are monthly above a $50 threshold, and no better small-indie rate was found in the standard terms.

⛔ Two obligations §6(h) attaches to the recurring tier (why lifetime is the hedge): (1) we must fulfil the subscription for the entire period as marketed, and on breach Microsoft may refund the customer "the full amount, plus taxes... in Microsoft's sole discretion"; (2) raising the price disables auto-renew — so $10 is effectively locked for the product's life absent Microsoft's price-change process.

⏳ Still open: the $50 subscriber→lifetime upgrade-discount SKU was discussed and is not approved — do not declare it. Individual vs Company Partner Center account still unanswered (get it right before enrolling).

✅ DECIDED 2026-09-27 — distribution is the Microsoft Store: MSIX + Store IAP

Creator's criteria, verbatim: "zero headaches, minimal maintenance (for me) while still providing accountability and a reasonably easy upgrade flow." Route A is the only option where all four are solved by handing the work to Microsoft rather than to a certificate vendor: $0/yr, no certificate, no HSM, no annual renewal, no SmartScreen ramp, plus Store auto-update, Store-side payments/entitlements/refunds/support, and Microsoft review as the accountability layer. The rejected options and their reasons are in TASKS/research-store-certification.md §3 — read that before reopening the question, not before re-deriving it.

The big consequence: the whole licensing backend is deleted. PolarLicenseService, PolarLicense, MainViewModel.License.cs (PremiumUrl, customer portal, the OfflineGracePeriod = 14 days subscription-era artifact, renewal/lapse copy) and the wrong "Polar unlocks alerts" string all become dead code. IsPremium comes from the Store entitlement instead of an HTTP call — which also deletes the entire "network flaky → app thinks I'm expired" bug class. Velopack, the update URL and the DO droplet go too.

What does not change: the entitlement. Free gets everything; the branding flash stays the ONLY paid delta. Store IAP changes how IsPremium is obtained, never what it gates.

⛔ The old "still open: the price" note is retired — the price is settled, see the top of this file. The old "~$69 floor" and "don't break the launch-price promise" notes refer to the old Polar storefront and are not current.

✅ DONE 2026-09-28 — TASK 48 item 1: bundle ffmpeg (was the "first code unit")

The app no longer downloads an executable at runtime. scripts/fetch-ffmpeg.sh populates a gitignored ffmpeg/ from a SHA-256-verified pinned month-end BtbN artifact; ytLive.csproj copies it to output and publish and fails the build if it's missing; FfmpegLocator lost its downloader, its zip extraction and the %APPDATA%\ytLlive\tools cache, and now resolves bundle → PATH with a loud, actionable failure. The pin that 404'd is gone, so the 2026-09-01 failure class cannot recur. Suite 379/379 at that point (380 now — see Test state); 16 locator facts including an integration test that executes the bundled binary (which is what proves the sibling DLLs resolve). THIRD-PARTY-NOTICES.txt corrected — it still named the stale tag autobuild-2026-08-09-13-03, i.e. the one that 404'd.

Fresh-clone cost, deliberately accepted: ./scripts/fetch-ffmpeg.sh once (~65 MB). 135 MB of DLLs in git would tax every commit on this 9p tree; the URL + SHA-256 are the audit trail.

Answering "other 3p binaries needing bundling?" — no, verified. ffmpeg was the only download-and-execute. e_sqlite3.dll + WebView2Loader.dll already ship via NuGet (runtimes\win-x64\native\), OpenH264 is compiled into avcodec-63.dll, the rest are managed. Two gaps remain that bundling cannot fix, and they are the next real risks: the WebView2 Evergreen runtime is a machine prerequisite (item 2's manifest dependency + a graceful-failure path), and Velopack's updater downloads and executes, so its teardown in step 3 is a compliance action.

Deferred, not next: the packaging project + Package.appxmanifest (TASK 48 items 2–3)

Creator ruling 2026-10-04: still in active development, so ship-time work waits and queued features come first. The unit is unchanged and ready when it is wanted: Full trust, webcam/microphone, English only; Velopack deleted. Add the DefaultRecordFolder() comment in the same unit (the mediumIL invariant below becomes load-bearing the moment the manifest lands). Full-trust writes under a real package identity are still unverified — confirm on a real packaged build before trusting them with recordings.

Carried over from the research unit, still open:

  • ⛔ Distribution.md:318 recommended a $400+/yr EV certificate for a SmartScreen bypass Microsoft removed in March 2024. Fixed then — had it shipped, it would have cost $400+/yr to buy what $150 buys. Now moot anyway (MSIX is signed by Microsoft), but the lesson in MyMistakes.md stands: a policy citation is a claim about scope, not just text.
  • WebView2 Evergreen runtime is a machine prerequisite the app will hard-fail without. The manifest needs the framework dependency, and WebView2 init needs a graceful error instead of a crash. New, found while bundling ffmpeg.

⛔ Two invariants that break silently if touched

  • Full trust, or recording breaks. DefaultRecordFolder() writes to Downloads/MyVideos; that passes through unvirtualized only at mediumIL. Flip the manifest to appContainer and output vanishes with no error. A comment is owed there when the manifest lands (TASK 48 item 6) — not before.
  • Chat is rendered, never stored. That non-persistence half is the load-bearing part of the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload may be added without re-arguing 11.12 first.

🚨 OPEN 2026-09-28 — the RTMP push never lands: recordings reach YouTube as EMPTY placeholders

This is a live bug, not a queued feature, and it reframes a complaint the map previously treated as closed. It was found on 2026-09-28 while scoping TASK 50, and the handoff had no entry for it at all — corrected here.

What the creator observed: the channel carries 20+ private videos, all content-less placeholders (creator-inspected, 2026-09-28). This is the real explanation of the 2026-09-26 report "I still cannot get private test recordings saved" — it was never true that the app could not find an archived VOD. No video content is ever produced on the channel.

Mechanism (verified against the code AND the channel): liveBroadcasts.insert succeeds and creates the placeholder; everything after it is what fails. Content only exists if all three of these land, and at least one does not:

  1. broadcast reaches LIVE via liveBroadcasts/transition,
  2. ffmpeg actually pushes RTMP to the bound stream key,
  3. the broadcast ends and YouTube transcodes it into a VOD.

enableAutomaticArchiving is never set, so YouTube's true default applies — the archiving route exists but has nothing to archive. Broadcasts are inserted private (Services/YouTubeStreamService.cs:49), so the orphans are not public, and the app has no delete path — the creator clears each one by hand in Studio.

✅ DIAGNOSTIC ANSWERED 2026-10-04 — ffmpeg reaches YouTube; the broadcast never goes LIVE. One test-mode push, read from startup.log. The three steps, resolved:

  1. new MediaFoundationReader/RTMP path — WORKS. ffmpeg opened rtmp://a.rtmp.youtube.com/live2/4pew-…, muxed 7.4 MB video + 46 KB audio, and finalised the FLV with no socket error. Step 2 is fine.
  2. The broadcast stays ready — this is the break. enableAutoStart = true is sent on insert (Services/YouTubeStreamService.cs:73), yet at close-out YouTube refused transition=complete with 403 Invalid transition — which is only legal from live. So YouTube never flipped ready → live.
  3. Never reached (nothing to archive because nothing went live).

Two unknowns, and the honest state of each:

  • The push ran only 8 seconds (08:48:23.9 → 08:48:32.8). YouTube may simply need longer to accept and validate the ingest before it auto-starts. Untested — do not assume.
  • The app cannot see this either way. During a stream it polls stream health (liveStreams) and never the broadcast status; GetLifeCycleStatusAsync is called only from the end path (YouTubeStreamService.cs:146). So the log contains no evidence either way, and EndBroadcastAsync skips only on complete/revoked, meaning it tries complete against a ready broadcast and earns the 403.

⇒ Next step, small and cheap: log the broadcast's lifeCycleStatus while streaming (reuse the existing health poll timer — no new loop), then run one 60-second test. That single run answers it. Do not go hunting beyond it until it has.

Sequencing constraint — do not flip the flag early. enableAutomaticArchiving: false is the correct end state (TASK 50 will upload the local file deliberately), but archiving is currently the only route a recording takes to YouTube — flipping it before TASK 50 ships would leave test streams with nothing at all and re-create the 2026-09-26 complaint in a worse form. Ship the upload, flip the flag in the same task, never before.

Detail and the open 11.12 re-check: TASKS/task-50-publish-recording.md.

Landmines

  • Frame pump jitter — counters now honest, cause NOT yet named (2026-10-05). The same test stream logged 86 stalls per 5s (FramePump stall: iteration 40–50ms against a 17ms budget) while FramePump stats read 301/300 frames, 0 dropped. UNIT 0 removed the reason those two numbers could disagree (the averages were diluted by padded submits) and added the counters that separate reader from compositor — but no one has read a post-UNIT-0 log yet. A creator recording is required first. Do not "fix" the pump before that reading.
  • Stale fact inside TASKS.md (left alone, flagged here per the Scope Lock). The "1.0 gates" section asserts "TASK 36 is now shipped", but the catalog row 36 reads ☐ Queued and task-36-gold-pass.md still shows items 1–6 unchecked. The line most likely means item 2 (branding flash goes live) shipped at 9761b1d. Needs a one-line fix, not a code change — flagging rather than fixing because it is not this unit's job.
  • A stale ytLive.exe (PID 2544) locked bin/.../ytLive.exe and broke dotnet run with MSB3027. Killed. If the build fails to copy ytLive.exe, check for a running instance first.
  • LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder needs an interactive desktop session. As of 2026-10-05 it fails deterministically here (ImgC vs ImgA), verified identical on a clean tree. Run the suite with ytLive CLOSED and expect this one to fail in this shell. Not a regression — don't diagnose it from a WSL-shell failure.
  • RealAppHost.RunAsync exists for a reason — a frame-pump test MUST await inside it. A blocking wait occupies the one shared STA thread and hangs the whole suite with no output. Always background a vstest run and poll the log; a foreground piped vstest returns nothing in this shell even on success.
  • GlobalHotkeys: two instances registering the SAME global hotkey — Windows refuses the second. Left alone deliberately.
  • MSIX writes under a real package identity are unverified. The docs say full trust passes user-profile writes through, but confirm on a real packaged build before trusting it with recordings (TASK 48 item 6).

Test state

402/402, 0 warnings, verified 2026-10-05 (395 + 6 FrameCadenceTests + 1 pump-stats integration test). Build the test project, not just the app project, or VSTest runs a stale DLL. Launch exe for the creator's test run: bin/Debug/net8.0-windows10.0.19041.0/ytLive.exe.

Confirmed environmental flake, NOT a regression (re-checked 2026-10-05, and it now fails deterministically rather than 2-of-3): LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_ PersistsTheReorder fails with Expected "ImgC", Actual "ImgA" at RealAppHost.Run line 68. It fails identically on a stashed clean tree, so it has nothing to do with UNIT 0 — it needs an interactive desktop session this shell does not have (the mouse drag never lands). Do not chase it; it is listed under Landmines as the known interactive-session test.

⚠️ New flake, do not chase it as a regression: on the first full-suite run, AudioPipelineTests.MasterLimiter_CapsTheLiveMix_OnThePipe failed with Expected 0.891, Actual 0 — a zero, not a slightly-off value, which is the signature of the pipe not being fed yet rather than the limiter misbehaving. It passed 27/27 as a class run and 380/380 on an immediate full-suite re-run. It reads the live mix off the named pipe, so it is a whole-suite scheduling artifact of the same family as the RealAppHost STA landmine below — if it ever goes red, re-run before believing it.

Uncommitted / untracked

  • ffmpeg/ — the 9 bundled binaries (~135 MB), gitignored by design. Populated by scripts/fetch-ffmpeg.sh. Do not commit it.
  • MARCOM.md, MONETIZATION.md — both edited (privacy-copy guard; the Polar-obsolete banner and the re-opened-price note) and both gitignored by design (confidential business files, .gitignore:10-11). They stay local, as intended. Same for CREDENTIALS.md — never commit those.

Next

  1. ~PUSH, then the creator's 60-second Test — see the FIXED box above. Packaging is NOT next; the creator's ruling 2026-10-04: still in active development, so ship-time work waits and queued features come first.
  2. Webcam cadence → Unit 1 (exclusive lock + 720p). UNIT 0 is shipped and the counters are in startup.log; the reading needs one 60-second creator recording (details + the exact read-the-log-together table in the IN FLIGHT box above). Then Unit 1: exclusive camera lock + 720p, and Unit 2: the blinking SD badge. Creator to-do, not yet queued: long-run resource/consumption/recycling benchmarking (how long the app stays healthy under full burden) — record only, do not start it.
  3. The packaging project + Package.appxmanifest (TASK 48 items 2–3) — the next code unit. Full trust, webcam/microphone, English only; Velopack deleted. Add the DefaultRecordFolder() comment in the same unit, and the WebView2 framework dependency + graceful-failure path.
  4. TASK 50 — one-button "Publish this video" (TASKS/task-50-publish-recording.md). Queued behind the Store submission by its own precedence note ("MSIX first, publish second — unless the creator reverses that"). Scoping is done; the build is not. ⛔ Do not flip enableAutomaticArchiving until it ships. It also carries an open 11.12 re-check: a publish feature makes the app push creator content to a third party, so the IARC answers must not go to certification describing an app that cannot publish.
  5. Declare the IAP products in Partner Center — the price is settled ($10/mo, $400 lifetime, no annual, 15% verified), so this is unblocked. Blocks: the Store listing and the OverlayHost.xaml copy. ⛔ The Individual-vs-Company account question is still unanswered — get it right before enrolling.
  6. Polar teardown (TASK 48 item 0) — PolarLicenseService, PolarLicense, MainViewModel.License.cs, the OverlayHost.xaml string, the csproj/App.xaml.cs Velopack sites. IsPremium ← Store entitlement. Do this as one unit — a half-torn-down licensing path is worse than either end state.
  7. Vertical recording verification — the compositor tier is proven by Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop; the record path has never been run. FramePump.cs:645 flags off-size tiers as a known follow-up.
  8. WACK + certification notes + the privacy policy (TASK 48 items 4–5) — the policy must state camera/mic is OS-gated, nothing is retained, and nothing is fetched at runtime (now literally true — the ffmpeg bundle is what makes that sentence defensible).
  9. Post-session efficacy report — roll up CurrentHealth (dropped frames, duration, health message) when a stream or recording ends. SessionTeardownTests is the natural home.
  10. Measure whether the Windows camera toggle gates DShow — gates all privacy-forward copy (MARCOM.md guard). Not a certification risk; a trust risk.
  11. TASK 36:212 stale "shipped" line — one-line fix, needs a hand.

Dropped from the list because the ruling made them moot: multi-instance's ffmpeg tools-dir race (item 1 deleted the tools dir entirely), scripts/publish.sh + the LlamaCasty.exe rename (the Store packages and delivers — revisit only if we ever ship outside the Store), and the alert-gating copy (the wrong string dies with the Polar teardown in step 3).

Everything else in the v1 queue: stream resilience (32), bandwidth step-down (33), scheduled streams (34), scene-linked audio (35), the master limiter (12), text source (3.16), reward events (3.20), the media picker (21), webcam identity (9.5), settings units A/C/D (40), and the defaults split (37). Ship-checklist items live in TASKS.md -> "1.0 gates".