Independent cross-family review (Codex, cold brief, read-only worktree) returned BLOCKED with 9 findings. All 9 accepted; #5 partially, keeping one row PINNED with its justification stated. POPULATION (the finding that mattered most). The derivation filtered the scope by an outbound-network token list and argued that was a scope choice rather than a population filter. It omitted `git fetch` — this repo's most common remote read — so prepush-rebase-check.sh, which fetches origin/main and derives a PUSH DECISION, was structurally invisible to a guard claiming to cover "every executable that reads live remote state", along with three others. The defence offered was that over-inclusion is the safe direction; the filter also under-included. The content filter is gone: the population is now all 55 files in the scoped directories, and a file that reads no remote state carries an explicit N/A row. OVERCLAIMS, graded down. Three rows asserted more than the code does: - the scheduled-merge path was PINNED while the hook's own comment concedes the branch-protection read pins nothing → UNSAFE-KNOWN, "preflight, not a pin"; - pr-changed-files.sh was PINNED and claimed "any movement fails", but before-and-after equality is ABA-vulnerable (main → scratch → main) → UNSAFE-KNOWN, pointing at the caller-side event-count fence that does close it; - the CI toolchain image was PINNED on a mutable TAG, against this file's own definition naming a digest → UNSAFE-KNOWN. The release smoke pull stays PINNED: it pulls the tag the same concurrency-serialized job just pushed. The guard-inventory MUTATION regrade is reverted to BEHAVIOUR-ONLY (8/6/20). The review is right on species: the test feeds the real script an input the clause rejects, which this table explicitly defines as behaviour-only and has already regraded three rows for. A manually-executed disarm does not change what the test is. TWO REAL FAIL-OPENS FIXED: - jq `index()` on a STRING is substring search, so a status_check_contexts arriving as "prefix-review-verdict/h10-suffix" answered yes and would auto-grant. Membership is now exact equality over a value first proven to be an array of strings. - post-review-verdict.sh guarded both re-read comparisons with `[ -n "$x" ] &&`, so a 2xx body that merely omitted .head.sha or .base.ref made the check a no-op and the status was posted having confirmed nothing. That second fix carries a lesson worth the line: the obvious mutation (disarm the new `-z` arm) stays GREEN, because the unconditional `!=` also rejects empty — the two overlap, exactly the duplicate-guards-mask-each-other shape. The proof is taken against the REAL predecessor with the `-n` conjunct restored, which goes red showing returncode=0 and a status written. Also: 404 is now separated from 403/transport (an unprotected branch is the strongest form of the finding; `curl -sf` collapses both to an empty string), and the positive control asserts the decision is `allow` and that the endpoint was actually reached, rather than the absence of one phrase. refs #778 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/ — task-signal map
Purpose: route a fresh contributor/agent to the minimal set of docs for the task at hand, instead of a mandatory front-to-back read. Update this doc in the same PR that adds, removes, or retitles a doc below, or that changes which sections a task signal points to.
Start here, always
CLAUDE.md(repo root) — project intro: architecture, layout, dev commands, conventions, Task Completion Protocol.docs/contributing.md— established code patterns (CQRS/MediatR, LanguageExt, the ChicoryTV SPA, EF Core dual-provider migrations, FFmpeg pipeline, analyzers, testing). Read before any non-trivial change.
Task signal → minimal sections
| Signal | Read |
|---|---|
| Session startup / "what's next" (no issue named) | docs/handoffs/chicorytv-issue-queue.md (standing kickoff — two concurrent tracks: orientation ‖ scripts/select-queue.sh 5) |
| Named-issue pickup | Skip queue selection; go straight to focused retrieval — see "Knowledge retrieval" below, then the issue body |
Adding/changing a /api/* endpoint |
docs/api-conventions.md checklist + docs/endpoint-index.md |
| Adding a ChicoryTV SPA screen | docs/spa-conventions.md |
| Scheduling / playout engine work | docs/domain-model.md + decisions catalog rows keyed sched.* (docs/decisions/README.md) |
| Concurrency / optimistic-locking work | docs/api-conventions.md §7a/b/c + docs/decisions/optimistic-concurrency.md |
| Auth / security-surface work | docs/decisions/api-auth-security.md |
| CI / release pipeline work | docs/ci-cd.md + docs/decisions/release-ci-governance.md |
| Proposing a new guard / CI check / regression test convention | docs/defect-shapes-773.md §4 (detector menu + the classes where no detector is plausible), then the two rules every guard must satisfy: docs/decisions/records/testing/guard-derives-population-from-source.md and …/guard-ships-with-mutation-proof.md |
| Adding / changing / deleting a guard file | docs/guard-inventory.md — every guard's row is machine-checked by scripts/tests/test_guard_inventory.py, so a new guard must acquire a row before the suite goes green |
| Writing code that reads live Gitea/remote state and then acts on it | docs/decisions/records/process/check-and-use-pins-a-version.md, then docs/remote-state-inventory.md — a new executable that talks to a remote service must acquire a row there before scripts/tests/test_remote_state_inventory.py goes green |
| Finding every site that references a symbol (multi-site fix/sweep) | docs/local-lsp-tooling.md — which surface answers, and why a delegated agent must be pointed at the csharp-lsp MCP tools rather than the LSP tool |
| Live local run / Playwright-MCP verification | docs/e2e-local.md + scripts/e2e-local.sh |
| Adding/changing a UI-E2E browser flow | docs/e2e-local.md → "UI-E2E harness" + scripts/e2e-ui.sh |
| What does a test suite cover | docs/testing.md |
| Legacy Blazor route lookup | docs/blazor-route-parity.md (historical #91 phase (b) inventory) |
| "Why do we do X this way" / challenging a convention | Catalog-first: docs/decisions/README.md (active rows) → follow the row's link to docs/decisions/records/<area>/<topic>.md for full rationale. docs/decisions/archive/<area>/ only for "what did the rule used to be." |
Knowledge retrieval (MemPalace + catalog + Gitea)
These four rules are the seam agreed with server-management#642 (the Gitea→MemPalace exporter). They apply whether the question comes up via MemPalace, a grep, or a stale comment:
- Current conventions/decisions → catalog-first. Start at
docs/decisions/README.md; discover via theErsatzTV-Decisionswing (active) /ErsatzTV-Decisions-Archive(superseded/retired). Resolve by topic/key, never by chasing a file path. - Issue history → evidence, not authority. The
Gitea-ErsatzTVwing is historical narrative that may be stale; it never overrides current Markdown. - The breadcrumb rule (the crux behavior change). A file path named inside a historical issue
comment (e.g. "grep
docs/decisions.md2026-07-17", "see …") is a breadcrumb, not a live pointer. Find the current rule via the catalog / active wing by concept; do not treat the named path as current. (Why it's safe: still-current → in the active wing, breadcrumb resolves; superseded → the active wing returns the successor and a literal follow lands on a record that announces its ownstatus: superseded; retired → the active wing returns nothing, which is itself the signal. The validator-enforced move-to-archive/is what prevents the catastrophic "superseded rule read as current" case.) - Fallback when MemPalace is stale/down:
docs/decisions/README.mdcatalog, thenrg '^`key: <dotted.key>`' docs/decisions/. MemPalace is never authority nor sole fallback.
MemPalace is candidate discovery only — every passage is verified against its cited Markdown/Gitea
source before use. Never derive live queue state from MemPalace, #237, or historical comments; queue
state is live Gitea state, retrieved via scripts/select-queue.sh (see
docs/handoffs/chicorytv-issue-queue.md). Full retrieval contract (altitude/precedence, staleness
bounds, what's mined per issue): docs/handoffs/chicorytv-issue-queue.md → "Knowledge retrieval".
Also present in docs/
docs/domain-model.md— what the app IS: entity glossary, channel→playout→schedule/block concept map, where each concept is edited in the SPA.docs/api-conventions.md— checklist for adding/changing a/api/*endpoint (controllers, DTOs, error mapping, auth, OpenAPI regen, tests).docs/spa-conventions.md— playbook for adding a screen to the ChicoryTV React SPA.docs/e2e-local.md(+scripts/e2e-local.sh) — how to run a live local instance for manual or Playwright-MCP verification.docs/local-lsp-tooling.md— the code-intelligence surfaces (theLSPtool's three servers and thecsharp-lspMCP server): how each is configured, which one a subagent can actually reach, the traps (a cold server answers the first query with a confidently partial result), andscripts/check-local-lsp.shto verify the preconditions. Read before briefing an agent to find every site referencing a symbol.docs/testing.md— testing map: what each*.Testsproject /websuite covers, golden-file nets, the timezone-independence rule, how to run subsets, the per-PR verification gate.docs/blazor-route-parity.md— historical record of the completed #91 phase (b) cutover: the Blazor Server UI is removed and every legacy route now 302-redirects to its SPA equivalent (or falls through to the catch-all →/app). Read it for the full legacy→SPA route inventory.docs/decisions/records/<area>/<topic>.md— one active decision record per file, YAML frontmatter (key/title/status/since/supersedes/superseded-by, plus optionalstale-after/sources— ersatztv#603), rationale prose in the body. The filename is the key, so one-active-record-per-key is a filesystem property (ersatztv#610).docs/decisions.mdand the topic files remain as the lifecycle-schema narrative plus a "Records formerly in this file" index, which is what keeps older date-based pointers resolvable. Generated active view:docs/decisions/README.md(catalog / task router) — start there. Superseded/retired records live indocs/decisions/archive/and are read only for history, never for "what is the current rule."docs/ci-cd.md— build/test/release pipeline, versioning, dependency management.docs/rest-api.md— REST API design doc for ersatztv#2 (goals, conventions, per-slice plan). Largely superseded day-to-day bydocs/api-conventions.md; read this for the original rationale.docs/mcp.md— theErsatzTV.Mcpstdio JSON-RPC MCP server (#58): how it wraps/api/v1as read + cautious-write tools, its config/env vars, auth, security posture, and the tool catalog.docs/channels.md— Channel entity field reference.docs/m3u-xmltv.md— M3U/XMLTV generation overview (ChannelPlaylist,GetChannelGuideHandler).docs/fork-strategy.md— divergence policy vs upstream ErsatzTV.docs/design-sync.md— Claude Design ↔ repo screen workflow (#92).docs/endpoint-index.md— generated REST endpoint index (method/path/operationId/summary per OpenAPI tag). Do not edit by hand; regenerated byscripts/generate-endpoint-index.py/scripts/update-openapi.sh.docs/handoffs/chicorytv-issue-queue.md— static session kickoff prompt + workflow lore. Queue state is live Gitea state, retrieved each session viascripts/select-queue.sh— see that file's standing kickoff for the two concurrent tracks (orientation ‖ selection). ersatztv#237 is a closed, archival historical tracker (superseded bystartup.parallel-orientationindocs/decisions.md) — not a live pointer.docs/defect-shapes-773.md— root-cause analysis of the recurring defect shapes across the whole closed-issue corpus (#773): the measured class ranking, the four families they consolidate into, the cheapest mechanical detector per class, the classes where no detector is plausible, and an audit of which configured hooks/MCP servers/LSPs are actually invoked. Read it before proposing a new guard or CI check — §4 is the detector menu, and it argues against enumerating cases one incident at a time.docs/remote-state-inventory.md— every executable that reads live remote state and acts on that read, classifiedPINNED/CAS/UNSAFE-KNOWN/N/Awith the window and what bounds it. The population is derived from the filesystem and compared for set equality byscripts/tests/test_remote_state_inventory.py, so a new script that talks to a remote service cannot ship unclassified. Read it withprocess.check-and-use-pins-a-version; it is that record's detector, since the class has no plausible linter (docs/defect-shapes-773.md§4 detector D).docs/guard-inventory.md— every executable guard file, what it blocks, whether it is aGUARDorTOOLING, and whether it ships a mutation proof (MUTATION/BEHAVIOUR-ONLY/NONE) with afile::functionref. The population is derived from the filesystem and the workflow/hook call sites and compared for set equality byscripts/tests/test_guard_inventory.py, so a new guard cannot ship unclassified and a renamed test cannot leave a row claiming coverage it has lost. Guards implemented inline in workflow YAML are deliberately outside that population — the doc states the limit rather than implying coverage.docs/tracker-retrofit-triage-237.md— audit trail for the #524 triage of ersatztv#237's 111 comments (method, per-comment classification, totals). Evidence for thedocs.tracker-comment-retrofitdecision; read it only when triaging another over-cap tracker.docs/handoffs/rest-api.md— original handoff prompt for kicking off the REST API work (#2).