Files
ersatztv/docs/handoffs/chicorytv-issue-queue.md
T
timothyandClaude Opus 4.8 b285e33747
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m19s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m19s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 4m9s
docs(handoff): lore — prose cross-refs go stale after a parallel merge, re-check them (#205/#206)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:22:51 +02:00

18 KiB
Raw Blame History

ChicoryTV issue-queue handoff (static kickoff + workflow lore)

PROTOCOL CHANGED 2026-07-11 (decisions.md entry of same date). Queue state lives in the pinned Gitea tracker ersatztv#237, not in this file. Do NOT write session state, queue order, or next-session prompts here — sessions ending under the old protocol should append their session comment to #237 instead. This file holds only the two stable things: the standing kickoff prompt and the workflow lore. Historical per-session state: git log of this file (last state-bearing revision: 8b77d5e7).


STANDING KICKOFF PROMPT (paste into a fresh session, unchanged every time)

You are the ORCHESTRATOR in the main Claude Code session — Opus 4.8 by default (Fable only when the user picked it; check which model you are). Orchestration = delegation: recon → Explore/haiku; mechanical → sonnet; judgment-heavy implementation → opus; independent review → Codex when quota allows (codex:rescue) + Claude review subagents/forks. FIRST read CLAUDE.md, docs/README.md + the convention docs it indexes, and the Lessons below.

FABLE ESCALATION — these moments go to Fable, no exceptions. If you ARE Fable, handle them inline (forks inherit your context); otherwise spawn an Agent with model: "fable" and a SELF-CONTAINED brief (scope, exact question, file paths, doc pointers — it starts cold), or pause and ask the user to /model-flip for the stretch:

  • Architecture/API design decisions (e.g. #202's media-source API shape) and anything [PLAN-MODE]-tagged.
  • Parity verdicts for blazor-route-parity.md (per-capability diff sign-off) and #91b removal go/no-go.
  • Reconciling contradictory review findings (fork vs Codex disagree) and the final pre-merge judgment on gate PRs.
  • Stuck after two genuinely different approaches, or evidence contradicts the issue/docs. Everything else (claiming, worktrees, dispatching implementers, CI monitoring, protocol bookkeeping, routine merges of green reviewed PRs with user consent) stays at your level.

Then work the queue:

  1. Read the pinned tracker ersatztv#237 — body = goal + ordered arc + session protocol — and its most recent session comments; list open issues in the Blazor removal (#91 phase b) milestone and with the review label; ALSO list open ersatztv-labeled issues in timothy/adversarial-reviewer — unclaimed audits there are pickup candidates too (read-only, parallel-safe; see the tracker's "Pending adversarial reviews" section).
  2. Pick the top arc item that is open and NOT labeled in-progress (or the item the user names).
  3. Claim it: add the in-progress label + a "claiming" comment on the issue(s); reviewer-repo audits are claimed by comment only.
  4. Read the issue bodies (they carry the task context/evidence) and work the item under the HARD CONSTRAINTS below.
  5. Finish by following the session-end protocol in #237: ONE session comment on the tracker (template in the tracker body, incl. triage verdicts for any new issues), remove your in-progress labels, and complete the per-issue Task Completion Protocol from CLAUDE.md.

HARD CONSTRAINTS:

  • Work in worktrees off origin/main. Copy web/node_modules from the main checkout.
  • PARALLELIZE BY DEFAULT: split the task into disjoint slices up front and run 34 implementer agents concurrently (recon agents are free — always fan those out). 34 concurrent dotnet/npm builds are fine on this Mac (M4, 10 cores, 16 GB); the go/no-go signal is FREE RAM, not CPU load (memory_pressure -Q: <20% free → don't launch more build agents; <10% → pause/stagger). CPU load spikes during builds are benign. Never 5+ builds (the historic crash was RAM starvation from an 89-way fan-out). NEVER set ETV_UPDATE_GOLDENS.
  • Never two committing agents on ONE worktree — give each parallel slice its own worktree branched off the feature branch and merge back. Sequence only where a slice genuinely depends on another's output (backend-first narrow, SPA-wide after worked well for #216).
  • Merge consent in-conversation per session (prior pre-approvals do NOT carry over).
  • Arm a CI monitor on the PR head sha AT PR-OPEN (commit-status endpoint), not at the end.
  • Live-E2E via scripts/e2e-local.sh; NEVER exercise download endpoints via browser tabs (curl them). Adversarial review fork per PR diff, SCOPED "review only".

Lessons / workflow lore (append-only; conventions live in docs/, this is workflow lore)

  • READ docs/README.md → the convention docs FIRST; point recon/implementer agents at specific doc sections. Only recon the task-specific delta.
  • blazor-route-parity.md carries mutation-depth verdicts (2026-07-09 sweep table at the top of Section 2). A row is only trustworthy if its cluster verdict is PARITY-OK; the sweep evidence lives in the issues #207#213. Keep the verdict table updated as gates close.
  • "Screen exists" ≠ parity: the root cause of the false SPA-READY rows was same-session verification that never diffed capabilities against the Blazor page. Per-capability diff vs the Blazor page is the standard for any future parity claim.
  • Several gaps are UNWIRED EXISTING endpoints (playout delete/reset, block copy, scan-show, collections add-item kinds) — check the endpoint index before building new API surface.
  • Keep the MAIN checkout's web/node_modules fresh (npm install after pulling a PR that adds a dep) — worktrees copy it, and a stale copy broke typecheck in a #198 worktree.
  • Playwright-MCP E2E: never open tabs/window.open for file-download endpoints — curl them. If browser tools stall repeatedly, pkill -f ms-playwright-mcp and drive a fresh session.
  • The user's main checkout (/Users/timothy/ersatztv) stays ON main and pulled each session. Feature work in worktrees off origin/main; copy web/node_modules from the main checkout.
  • Subagent connection drops = laptop sleep/transient; re-resume via SendMessage (work survives).
  • Build/verify locally, then trust it; CI (VM 127) confirms (~9 min warm). Watch by commit status: /api/v1/repos/timothy/ersatztv/commits/{sha}/status.
  • PR routine (works): worktree off origin/main → implement (opus judgment-heavy / sonnet mechanical; doc pointers + exact facts; csharp-lsp for C#) → API changes: build app project FIRST then ./scripts/update-openapi.sh then npm run generate:api → full local test pass → push, PR → fork adversarial review (SCOPED "review only") + live-E2E for UI changes (scripts/e2e-local.sh) → review fixes as follow-up COMMITS (never amend/force-push) → CI green → merge (consent per session) → structured close comments per CLAUDE.md protocol.
  • Never run TWO committing agents on one worktree concurrently. Read-only review forks are fine.
  • Reusable API facts: DTO records in ErsatzTV.Core/Api need file-scoped #nullable enable; ErsatzTV.Application has NO nullable context (CS8632); Option<T>→nullable = MatchUnsafe(v => (T?)v, () => null); LanguageExtensions.Apply collapses NotFoundError→422, so real 404s need a controller pre-check; id-taking child GETs get an OpenApiErrorResponseContractTests [TestCase].
  • Arm a CI monitor the moment the PR opens — in the #216 session three PR runs sat red for ~an hour (a CI-only debounce-timing flake) while review/E2E ran, because CI was only checked "at the end". Also: concurrent runs can collide on the MySQL service host port 3306 ("port is already allocated") — that's infra flake, rerun; consider unpinning the host port.
  • "Make X consistent with Y" review findings deserve new-code scrutiny — a #216 review fix propagated MediaBrowseScreen's mount-debounce selection-wipe race to SearchScreen instead of catching it. For any timer/effect, ask "when does this fire?" (incl. mount).
  • SPA mutation affordances on media items: USE the shared web/src/media/addTo/ layer (spa-conventions §5c) — don't build screen-local pickers. Schedule adds are valid only for shows/seasons/artists (server validator).
  • Live E2E seeding: the local library isn't API-seedable; the #216 E2E agent generated tiny ffmpeg testsrc MKVs + inserted LibraryPath rows via SQL then scanned. Recipe not yet in docs/e2e-local.md — worth adding next time it's needed.
  • Parallel sessions (2026-07-11 protocol): claim before working (in-progress label — the tiny read→claim race window is accepted; later claimant backs off). Claiming prevents duplicate pickup, NOT overlapping code changes — check the tracker's dependency notes ("#234 after #231", "coordinate with #215") before touching shared surfaces. Lessons edits to THIS file: append bullets only, git pull --rebase before commit.
  • Fan-out health = RAM, not CPU load (2026-07-11, user calibration): the 89-way crash was RAM starvation. uptime load of 1024 during parallel Roslyn/vitest bursts is benign with memory healthy. Watchdog pattern: background monitor emitting only when memory_pressure -Q free % < 20 (silence = healthy); pause/stagger agents below 10%.
  • CI MySQL host-port collision FIXED on main (ef8915f1, issue #236): the migrations service no longer publishes host 3306. Branches created before that commit still collide — merge main in. There are now TWO runners (ci-runner VM 127 + bumblebee-runner), 4 slots: faster drains, and superseded-run results are ignorable. Neither the cancel-run API route nor the web cancel route exists on this Gitea version — stale runs just drain.
  • Two sessions touching one machine: a branch may be checked out in ANOTHER session's worktree — never commit/merge inside a worktree you didn't create. To land a merge on such a branch without touching their checkout: plumbing merge (git read-tree -m base ours theirs into a temp GIT_INDEX_FILE → write-treecommit-tree -p ours -p theirs → push the commit to the branch ref); the owning session then git pulls.
  • Codex is back in the review rotation (2026-07-11): the 2026-07-06 "retired — usage exhausted" was a quota window, not retirement. Codex has 5-hourly + weekly limits (same structure as Claude); rule: if we have usage, use it — for INDEPENDENT review passes (per-PR adversarial review alongside the fork review, adversarial-reviewer audits/re-reviews, stuck-diagnosis second opinions) via codex:rescue, never for implementation. Check readiness/quota with /codex:setup; on exhaustion fall back to fork/subagent review and retry next window.
  • Fixed-point round-trip tests can't see lazy-evaluation crashes (2026-07-11, PR #229): a write-path 500 (unset navs projected after SaveChanges) survived a green fixed-point test because the handler returned a lazy LanguageExt Map the test never enumerated. Rules: force enumeration of handler return values in tests; write-path responses reload through the read path's include chain (api-conventions §7); live E2E remains the only net for this class.
  • Merging main into an open PR branch: REGENERATE generated artifacts (v1.json, v1.d.ts, endpoint-index) after resolving source conflicts — git text-merges them plausibly-but-wrong; npm run check:api is the guard. Also expect other sessions to push to YOUR PR branch (merge-main updates): git pull before push, treat non-FF as signal not error.
  • CI VM test timeouts: heavy-render web tests (100+ item grids) need explicit vitest timeouts (e.g. 15s) — the CI VM hit the 5s default on a test that runs in ~1s locally (run 686). Bump per-test, don't raise the global default.
  • Concurrency tests need a negative control + a stress loop, not a single round (2026-07-11, #231): a one-shot Barrier(N) + Task.WhenAll "exactly one winner" test did NOT catch a deliberately non-atomic check-then-set flag on this Mac — the load→store window is too narrow to collide when threads release once. It green-lit broken code. Fix: hammer the race over many rounds (N worker threads × ~20k rounds, two Barriers per round: acquire → validate one-winner → reset), and ALWAYS prove non-vacuous by temporarily breaking the primitive and watching the test fail. Cheap (sub-second) and deterministic-enough. Same discipline as #12's negative control.
  • Before deleting a "workaround", check it isn't serving a SECOND purpose (2026-07-11, #232): an issue said "remove the SPA PENDING_GRACE_TICKS grace-tick workaround now the API is honest". The grace ticks did double duty — compensating the lying-200 (now genuinely obsolete) AND absorbing the inherent queue→observed-active lag / fast-completion race (still needed). Removing wholesale wedged the scan button disabled-until-reload for short scans; both fork + Codex caught it. Keep a bounded version, re-scoped honestly. When an issue says "delete X", enumerate every behavior X provided before deleting.
  • Re-review the FIX COMMIT, not just the initial PR diff, before closing (2026-07-11, #242, user-surfaced): the pre-fix review is only half the loop — the fix commit is new code no independent reviewer has seen. Here the first fix (gate Edit on saving) only PARTIALLY closed the in-flight-save clobber class; a Codex re-review of the fix commit caught that Delete was the one schedule-switch path (applySwitch) still missing the guard. Fold a scoped re-review (Codex if quota, else a fork) over the fix commit into the close protocol; loop until a clean MERGEABLE verdict or an explicit acceptable-defer (file a tracked issue for the deferral, as #248 here). Cheap relative to shipping a half-fix that reopens the issue.
  • disabled={saving} is the SchedulesScreen convention for "no schedule-switch/mutation mid-save" (2026-07-11, #242): the Select + guardedSwitch already guard saving; Edit and Delete must too — Delete switches schedules via applySwitch, and a stale items PUT resolving after the switch clobbers the next schedule's draft. When adding any control that can switch the active schedule or reopen the editor, gate it on saving (and, for the editor, on the dirty draft).
  • Negative-controlling a lock/flag fix: invert the condition, don't stub if (true) (2026-07-11, #250): to prove a lock-release test is non-vacuous I temporarily broke the fix. First attempt made the guard if (true) — but that left the ownership flag "assigned but never read" → CS0219 under warnings-as-errors → the Application build silently FAILED → dotnet test --no-build ran the STALE (fixed) dll → false PASS that looked like the control had failed to catch the bug. The tell: build output must show Build succeeded / 0 Error(s), not just a truncated tail. Break the primitive by inverting the real condition (if (!lockAcquired)) so the flag stays read and the code compiles; always grep the build for error CS before trusting a --no-build test result. (Codex-found High this session: an outer catch that unlocked unconditionally cross-released another session's lock on a pre-acquisition exception — the fix was a Handle-scoped ownership flag gating the catch.)
  • Post-commit side-effect invalidation must run its QUERIES on CancellationToken.None, not just the enqueue (2026-07-11, #251, Codex-found Medium the fork missed): the deco-invalidation handlers committed the edit on the request token (correct — a pre-commit cancel safely aborts), then queried the affected playouts also on the request token before a WriteAsync(..., None) enqueue. A cancel landing in the window between commit and query throws before the enqueue → edit persisted, no rebuild queued, stale-content bug re-opens. Fix: once SaveChangesAsync has committed, run the ENTIRE side effect (affected-entity queries + enqueue) on CancellationToken.None — a committed change's compensating side effect must not be half-abortable. Generalizes the audit #22 policy from "the enqueue" to "everything after the commit". The fork review OK'd it; only the independent Codex pass caught the query-token gap — evidence for keeping both reviewers when quota allows.
  • Replace-all handler with NO client stable id → positional in-place reconcile via CurrentValues.SetValues (2026-07-11, #252): to stop a delete-and-reinsert handler from cascade-wiping a child that FKs the item (PlayoutScheduleItemFillGroupIndex owns fill-group enumerator state, OnDelete(Cascade)), reconcile by ordered POSITION (the DTO carries no id): same-runtime-type slot → built = BuildItem(...); built.Id = existing.Id; dbContext.Entry(existing).CurrentValues.SetValues(built) (reuse the builder as the SetValues source so NO field is hand-copied/dropped; set the key first so SetValues doesn't zero it), then rebuild child join rows; subtype change/surplus → delete+insert that slot only. Keeps the row id → the cascade never fires. The schedule-item hierarchy is TPT (table-per-type), not TPH — SetValues is still safe (same-type guard; no discriminator). This is the SCOPED placeholder; true content-aware stable child identity is #253 (a moved fill-group item still inherits the state of whatever held its new slot). Anyone doing #253 builds on / replaces this positional reconcile in ReplaceProgramScheduleItemsHandler.
  • A docs PR that lists gates/cross-refs can go STALE mid-flight (2026-07-11, #205/#206 PR #261): while the PR sat in review a listed gate (#204) merged. The PR FF-rejected → rebased; the decisions.md append conflict was trivial, but the real trap was the prose — the new removal runbook still named #204 an open removal gate. After rebasing over a parallel merge, re-read your prose cross-references (gate lists, "blocked on #N", status tables), not just resolve the conflict markers — git fast-forwards stale facts silently. The hand-written-docs sibling of the "regenerate generated artifacts after merging main" lesson. Also: docs-only PRs still trip main's Build & test branch protection, so a rebase = a fresh full-CI cycle before merge.