Files
ersatztv/docs/handoffs/chicorytv-issue-queue.md
T
timothyandClaude Opus 4.8 8565f731cd
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 8m39s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
fix(process): #317 merge-consent gate auto-grants on satisfied path (no double-prompt)
The H6/H10 merge-consent hook's satisfied path did a bare `exit 0`, which does
not auto-approve a PreToolUse tool call — it only declines to block, so control
fell through to the normal permission system and the raw MCP prompt still fired.
A ready-to-merge PR was therefore confirmed twice (conversationally + a redundant
mechanical prompt). Emit permissionDecision "allow" (new `grant` decision) on the
satisfied and docs-exempt paths so the derived state IS the consent; deny/ask
unchanged (fail-closed); non-merge methods keep the exit-0 passthrough.

Docs: CLAUDE.md, kickoff HARD CONSTRAINTS, docs/decisions.md (append-only, pure insert).
Verified: 7 pipe tests (satisfied->allow, unticked->deny, stale->deny, red-CI->deny,
no-verdict->ask, no-creds->ask, non-merge->passthrough).

fixes #317

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:52:26 +02:00

22 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 last ~6 session comments (newest-first; the full comment payload is large, so stop at ~6). SOURCE OF TRUTH = live Gitea issue state, NEVER the prose. The arc body carries ORDER + goal only; a session comment's "Recommended next" is a forward guess written before the next session acted. Both go stale the instant an item closes (especially under parallel sessions narrating each other's work as "the gate that unblocks X"). So derive the candidate set from LIVE state, not from any inline marker or a prior comment's "next": the current gate = the lowest-numbered OPEN arc item in #237's arc list; its open children are the gate cluster (query them by the review label). Cross-check every arc / "recommended" item's real open/closed state (issue and milestone) before trusting it — do NOT hardcode which issue is the frontier; read it. 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). If the Gitea MCP is down, hit the REST API directly (creds in your global CLAUDE.md): curl -u <user>:<pass> http://192.168.1.95:3000/api/v1/repos/timothy/ersatztv/issues/237. The authoritative pickup + ranking protocol lives in #237's "Session protocol" section — this is a summary; if the two ever disagree, #237 wins.
  2. Pick the highest-ranked OPEN, un-in-progress candidate (or the item the user names). Rank by labels, not just arc position (labels are live state; prose is not): (1) arc order in #237, then (2) gate vs backlog — an item marked gate by the review label OR membership in an open gate milestone outranks anything unmilestoned; then (3) priority: label — high > medium > low within a tier. Pick order across the three pools: the arc frontier (lowest-numbered open arc item) first; an unclaimed Blocker/High priority-pickup (review + priority: high, e.g. #253) beats a non-frontier arc item; reviewer-repo audits are read-only and run in parallel. Confirm the pick is genuinely OPEN in Gitea first. If the prose says "recommended next / now unblocked" but the issue (or its milestone) is already CLOSED, it is done — skip it and fix the stale line in your session comment. Prose lags live state; live state wins; milestone + priority: labels decide gate-vs-backlog, not the prose. (This mirrors #237's Session-protocol ranking — #237 is canonical.)
  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: run the H12 qualification audit (ETV_GITEA_BASICAUTH=user:pass scripts/issue-qualification-audit.sh) and add a priority: label to anything it lists (every issue you filed this session included); then 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 is DERIVED, not asked: the H6/H10 gate (pretooluse-merge-consent.sh) auto-grants a merge when the linked issue's ## Done-when boxes are all ticked, a fresh positive Review-verdict references the current head, and CI is green — no conversational confirmation on that satisfied path (ersatztv#314). Confirm in-conversation only when the gate ASKS (state not derivable: no creds, no linked issue, no Done-when/verdict, Gitea down). Prior pre-approvals still 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 (STANDING workflow/orchestration rules only)

Scope: this section is how we work (orchestration, CI, review, cross-session hygiene) — not engineering decisions. Every "why the code does X" decision lives in docs/decisions.md (+ api-conventions.md §7a/b/c for the concurrency/side-effect mechanics, spa-conventions.md, e2e-local.md). Do NOT re-record a code/API/SPA decision here — it will duplicate and drift. Pruned 2026-07-12: issue-specific engineering bullets now covered in those docs, and dead Blazor-parity process notes, were removed (git history has them).

  • READ docs/README.md → the convention docs FIRST; point recon/implementer agents at specific doc sections. Only recon the task-specific delta.
  • 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.
  • Arm a CI monitor the moment the PR opens (commit-status endpoint), not at the end — in the #216 session three PR runs sat red ~an hour on a CI-only flake while review/E2E ran. (The old MySQL host-port 3306 collision is fixed on main, ef8915f1; a lone MySQL-apply red now = the known infra flake, rerun — see MEMORY / ci-migration-job-mysql-flake.)
  • "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).
  • Live-E2E seeding recipe now lives in docs/e2e-local.md → "Seeding a local TV library for E2E" (tiny ffmpeg testsrc MKVs + LibraryPath SQL rows + scan). Live-E2E is a stated requirement for write-path handler changes — see that doc's "When live-E2E is required" + the decisions.md entry; it's the only net for the lazy-enumeration / reload-through-read-path 500 class (#229).
  • 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. Editing THIS lore: prune covered/stale bullets too (not append-only — git keeps history), git pull --rebase before commit. Two runners (ci-runner VM 127 + bumblebee-runner, 4 slots) → superseded runs drain on their own; no cancel route exists on this Gitea version.
  • 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. Skip rubric (self-exemption is an auditable claim, not a vibe): an independent review pass (Codex, or a fork/subagent when Codex is exhausted) is mandatory for any diff touching locks/concurrency, auth/security, API write-path handlers, or DB migrations, or larger than ~150 changed C# lines. It is skippable only for a pure-SPA/docs leaf with no server-state effect — and when you skip it, say so and why in the PR or close comment (a stated exemption, not silence). This is the correlated-blindspot net: the fork and Codex miss different classes (see the #235 "enumerate every producer" and #250 lessons), so a single reviewer on a risky diff is the gap.
  • Force enumeration of lazy LanguageExt returns in tests (2026-07-11, PR #229): a write-path 500 survived a green fixed-point test because the handler returned a lazy Map the test never enumerated. Enumerate handler return values in tests; live E2E remains the only net for this class. (Write-path reload-through-read-path mechanics: api-conventions §7.)
  • Keep a PR branch current by REBASING on origin/main — do NOT merge main in (ersatztv#311, enforced by the H11 pre-push hook: a push from a branch behind origin/main is blocked → git fetch origin main && git rebase origin/main). A merge commit pulls in every file main changed, incl. files you never touched (e.g. the ~2500 legacy-BOM .cs), which then trip the format hook/CI on code that isn't yours (the #309 session). Rebasing keeps your diff to exactly what you changed. After a rebase that hits conflicts in generated artifacts (v1.json, v1.d.ts, endpoint-index), REGENERATE them (./scripts/update-openapi.sh + npm run generate:api) — never hand-resolve; git text-merges them plausibly-but-wrong and npm run check:api is the guard. Escape hatch for a deliberate non-rebased push: ETV_SKIP_REBASE_CHECK=1 git push.
  • H12 issue-qualification audit (ersatztv#312, scripts/issue-qualification-audit.sh): a session-end check that lists OPEN issues missing a priority: label — the #237 ranking keys off priority:/gate labels, so an unlabeled issue is invisible to it. "Fully qualified" = has a priority: {high,medium,low} label (that signals triage ran; gate-vs-backlog is then derivable from the review label / milestone, and a milestone is NOT required — backlog is unmilestoned). Run it at session end and label anything it flags (esp. issues you filed this session). Fail-open without creds; advisory (exit 1 when any are unqualified). Sibling to H11 (both #311/#312).
  • 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.
  • 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.)
  • A lock/channel-ownership "no cross-release" verdict must enumerate EVERY producer of that message type, not just the enqueuers visible in the PR diff (2026-07-11, #235/#267). The cold fork cleared the new scan-collections per-provider lock as sound — "only the acquirer enqueues, so only its message triggers the scanner unlock" — because it checked the three new controllers but never grepped for ALL Synchronize{X}Collections( senders. Codex did: SchedulerService also enqueues them periodically and unlocked, and ScannerService's finally released the lock whenever held → a scheduled scan cross-releases an API scan's lock (#250 class). Rule: before trusting any "single owner / no double release / no cross-release" claim, grep the whole host project for every writer of that channel message (or acquirer of that lock) — the background scheduler/worker is the usual missing producer. This is why the independent Codex pass earns its keep alongside the fork even when the fork returns MERGEABLE.
  • Fork-vs-Codex disagreement on a gate PR → Fable, and Fable often improves the fix, not just the verdict (2026-07-11, #235): the reconciliation isn't only "who's right" — Fable ratified Codex's High AND caught that the orchestrator's proposed fix (per-source lock-or-skip) would deterministically starve the 2nd+ media source (fixed loop order), prescribing the shipped library-scan §C7 pattern instead (lock once per provider, batch-enqueue, release on the Unlock: isLast message). Escalate the FIX approach too, not just the finding.
  • Harden a soon-to-grow feature with a runtime posture/toggle, not a hardcoded clamp (2026-07-12, #289): the MCP's final design (#58) is read+write, so a GET-clamp would be ripped out — shipped a runtime read-only posture (ERSATZTV_ALLOW_WRITES, default false) instead. When a security fix constrains a capability the roadmap will later want, make the safe state the default of a switch, not a wall; check the feature's issue for its end-state first.
  • Fixing onto an unmerged feature branch: branch off it, land by FF-push, never touch a sibling worktree that has it checked out (2026-07-12, #289 → PR#76). Gotcha: recon by Reading a sibling worktree leaked its paths into my later Edits → they landed there, not the fresh worktree (dotnet built the unchanged tree → "0 new tests" tipped it off). After creating a worktree, do the first Edit/Read against ITS absolute paths and git status it before building.
  • HttpCompletionOption.ResponseHeadersRead moves the body read OUTSIDE HttpClient.Timeout (2026-07-12, #289): with a None read token a slow-drip upstream hangs forever. Fix = per-request linked CancellationTokenSource.CancelAfter(timeout) into BOTH SendAsync and the stream reads + HttpClient.Timeout = InfiniteTimeSpan; catch transport/timeout exceptions and turn them into a response. (Not yet in decisions.md — #289 landed on PR#76, not main.)
  • Queue-drift root cause + standing rule (2026-07-12, user-surfaced): pickups repeatedly re-picked already-done work — #91b was framed "recommended next / now unblocked" for two sessions after it had merged (2026-07-11), and the #251/#252 priority-pickups sat listed "open" after closing — requiring a reactive body correction (tracker comment 16:00). ROOT CAUSE: DONE/OPEN status was read from prose (the arc body's inline "DONE" markers + each session comment's "Recommended next"), which is append-only and hand-edited, so it lags real issue state — worst under parallel sessions that narrate each other's merges as "the gate that unblocks X" (#271 got cast as the last gate for an already-merged #91b). It is a structural bug, not a stale-writer bug: any status embedded in prose will drift. STANDING FIX (don't just re-patch the body next time): live Gitea state is the ONLY source of truth for status. The arc body carries order+goal; a comment's "Recommended next" is a candidate that MUST be re-verified OPEN (issue and milestone) at pickup. When prose disagrees with live state, live state wins — correct the prose in your session comment, never propagate it. STRUCTURAL CURE (2026-07-12 review, Fable): #237's arc no longer carries inline DONE markers — closed items move to a "Done (history)" section, so status lives ONLY in live Gitea state and can't drift; and the gate/frontier is defined structurally (lowest-numbered open arc item), never hardcoded to an issue number. Beware: the first pass at this fix re-planted the very bug by hardcoding "#197 cluster" / "#91b milestone CLOSED" into the kickoff — if you name today's frontier issue in this standing file, you are writing the next drift. Gate cluster = open review-labeled issues serving the current open arc item.