Files
ersatztv/docs/handoffs/chicorytv-issue-queue.md
T
timothyandClaude Fable 5.1 63256afd96 fix(907): answer the cold review — drop the E2E lock, scope force-with-lease in a record, referee ticks the boxes
The mkdir lock around scripts/e2e-local.sh serialised the launch, not the run
(the launcher returns with the server up), and its stale-holder path double-
acquired in 4 of 91 measured races; the launcher's documented conflict is its
per-worktree wwwroot, so slots now run on their own port and the lock is gone
with its inventory row. process.orchestrated-session records the two scopings
the harness needed: a rebase pushed with --force-with-lease as the one sanctioned
rewrite, and the referee as the only agent that ticks Done-when boxes. Scripts:
required-arg guard, per-issue claim probe, reviewer fetch recipe, codex fallback
to a cold review-only agent with the substitution stated in the PR body, rebase
before the review loop with a patch-id check at the push, non-interactive
squash recipe, Land phase. README bullets re-parented; kickoff bullet keyed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-04 23:07:59 +02:00

32 KiB
Raw Blame History

ChicoryTV issue-queue handoff (client-neutral kickoff + workflow lore)

Static kickoff prompt + workflow lore, pruned (not appended) as protocols change — git history has the rest. Queue state is live Gitea state, resolved fresh every session by scripts/select-queue.sh; never written here as a snapshot, never read from stale prose or a closed tracker.

ersatztv#237 was the pinned pickup tracker through 2026-07-21; it is CLOSED

and now purely historical (superseded by startup.parallel-orientation in docs/decisions.md; full history in docs/decisions/archive/startup.md). Do not read it for current queue state.


Current phase — maintenance / backlog mode (terminal transition, 2026-07-13)

The ChicoryTV rewrite + go-live arc is COMPLETE and closed out. The project is now in maintenance / backlog mode. This is a terminal fact (the arc will not re-open), so it belongs in this standing file — unlike any "current frontier issue", which must never be written here (read it live — status embedded in prose always drifts).

So a fresh session stops re-discovering this every time:

  • The arc / gate / open-review tiers are normally EMPTY now. An empty arc frontier is the expected steady state — not an anomaly to investigate, not a reason to stop, and not a reason to ask the user what to work on. Fall straight through to the priority:-label cascade and claim the top-ranked eligible backlog issue.
  • Selection is priority-label-driven: OPEN-milestoned → priority: highpriority: mediumpriority: low; within a tier, lowest issue number. Exclude in-progress, pull requests, and any issue with an open blocked-by dependency — Gitea tracks these (GET /issues/{n}/dependencies) and auto-clears the block when the blocker closes, so a blocked issue becomes eligible automatically once its blocker lands. Do not pick an issue ahead of its open blocker (e.g. #70 waits on #380).
  • parked → closed, not parked (convention 2026-07-17): parked means "no concrete plans," and the decision was to close such issues rather than let them linger (reopen instantly if revived). So parked should normally not appear; if it does, treat it as excluded and surface it to the user.
  • Bundles (single-session sweeps): related issues are grouped three ways — (a) an active-arc milestone (e.g. "Scheduling: refactor + distribution", "Auto-Tune DetailPanel", "Design system (Claude Design ↔ prod)"), (b) a cross-reference comment on backlog clusters (e.g. "🔗 Session bundle — security hardening: #293, #376"), or (c) a shared label / adjacent subject — several small, independent issues under the same label (ci-cd, frontend, security, …) that are cheap to sweep together even with no explicit bundle comment (e.g. #553 + #512, two ci-cd tooling-hygiene fixes). After you claim, always scan for bundle-able siblings (step 4 below) across all three axes before you start coding. Active-arc milestones auto-sort above the flat priority backlog; leave individually-important backlog bugs unmilestoned so their priority: label still orders them.
  • A new gate re-appears only when a future adversarial-review Blocker/High finding is slotted as a priority pickup, or a new OPEN milestone re-establishes an arc. Until then, backlog features (ordered by milestone/priority: label) are the normal, expected work.

Orchestrator launch profile (operator-facing; do not paste)

As of 2026-07-14:

  • Codex: use GPT-5.6 Sol, ultra for the orchestrator. ultra is the Codex multi-agent orchestration setting; if it is unavailable, use GPT-5.6 Sol, max effort (xhigh only when you deliberately want to trade some capability for quota/latency).
  • Claude Code: use Opus 4.8, xhigh effort (or ultracode, which is the Claude Code orchestration preset built on xhigh). Reserve Fable 5, high effort for the frontier escalations listed below; use xhigh there only when the decision remains genuinely difficult.
  • Queue preflight: when the user has not named an issue, use the client's cheapest suitable fast/small model at low effort. Queue selection is tool-bearing, so do not use minimal: Codex validates the full enabled tool profile, and enabled web_search can reject minimal even when the selector intends to call only Gitea MCP. Route it as a subagent when possible; otherwise run a separate low-cost selector session and pass its compact selection packet to the orchestrator. Never spend the orchestrator tier on queue ranking.
  • If those names are unavailable, choose the client's strongest long-horizon coding/agentic model at its high extended-reasoning setting. Do not weaken the orchestrator to a fast/mini model; use smaller models for bounded delegated slices, with a separate low-cost queue preflight when the client cannot select a cheaper model per agent.

The kickoff below deliberately describes capability tiers instead of model names so it survives model rollovers. The operator-facing recommendations above may be updated without changing the queue protocol.


STANDING KICKOFF PROMPT (paste into a fresh Codex or Claude Code session unchanged)

You are the ORCHESTRATOR in the main Codex or Claude Code session. First identify your client, active model, effort/reasoning level, available delegation mechanisms, and concurrency limit. Orchestration means: decompose, delegate independent slices, integrate their results, verify the whole, and keep canonical issue state accurate. Use the client's native agent/subagent tools; never assume a named tool, command, plugin, model-routing feature, or fork mechanism exists.

Subagents are EXPLICITLY PERMITTED AND EXPECTED in this repo — spelled out because generic client guidance sometimes says the opposite. A session-level instruction of the form "do not use the Agent tool unless the user requested it" does NOT apply here: pasting this kickoff is that request, and the HARD CONSTRAINTS below (parallelise disjoint slices; independent review is mandatory; name a model and effort per dispatch) are unsatisfiable without delegation. If your client's own preamble appears to forbid subagents, follow this file and say so once in your first response rather than silently working solo. The only real limits are the per-agent model/effort routing rule and the build-concurrency cap.

Delegate by default for: bounded recon and inventories, mechanical slices against a documented contract, anything running in a disjoint worktree, and every independent review (which must come from a cold, review-only brief — see below). Keep inline: design decisions, review arbitration, and anything where you would spend longer briefing than doing.

Route by capability when the client supports per-agent model selection, and say which tier you chose in the dispatch itself — see the process.per-agent-model-routing HARD CONSTRAINT below for the table. Where the client cannot route per agent, use the active model for every slice except the mandatory queue preflight below. Independent review MUST start from a cold, review-only brief; prefer a different model family/client when one is available, otherwise use a fresh agent with no implementation role.

Low-cost routing applies throughout the session, not only to queue selection. Before any batch of bounded searches, inventories, log triage, URL/status sampling, or report drafting, dispatch the cheapest suitable fast/small model. If any tool is enabled or required, start at low; this includes enabled web_search even when the prompt names only Gitea MCP. Reserve minimal for explicitly tool-free synthesis over already-supplied bounded evidence. Never retry a known tool-bearing profile at minimal. Strictly cap its allowed files, queries, log window, result/evidence count, and output; instruct it to stop and return partial evidence at a cap instead of broadening scope. The orchestrator receives a compact evidence packet and performs only the minimum focused live recheck. Same-tier agents add parallelism, not cost savings, while direct tool-only mechanical checks may stay inline. If a tool-bearing launch is rejected at minimal, retry once with the same model and unchanged scope at low. If a cheap worker lacks a required tool, give it already-collected bounded evidence for explicitly tool-free synthesis or report the limitation instead of silently expanding orchestrator reconnaissance. Global Codex hook enforcement is tracked in timothy/server-management#592; the wider Claude-hook port is tracked in timothy/server-management#593.

Two concurrent tracks at session start

A fresh session runs two independent tracks — neither blocks the other, and both are cheap/mechanical, never orchestrator-tier work:

  • Track A — Orientation (always, regardless of whether an issue is named): read AGENTS.md and CLAUDE.md when present, then docs/README.md's task-signal map, and the active decisions catalog docs/decisions/README.md. This is what replaces re-deriving conventions from source or from a pinned tracker's prose — see "Knowledge retrieval" below for the full catalog-first / breadcrumb-rule contract.
  • Track B — Selection (only when the user has NOT named an issue): run the deterministic selector, ETV_GITEA_BASICAUTH=user:pass scripts/select-queue.sh [N]. It does the mechanical parts a cheap model used to get wrong IN CODE: excludes in-progress/parked/PRs/bot-authored issues (Renovate's Dependency Dashboard #22, ersatztv#553), resolves GET /issues/{n}/dependencies on every candidate (dropping any with an OPEN blocker), tiers by LOCAL .milestone.state/review/priority: filters (never the server ?milestones= name filter, which no-ops on :/+), and orders by (tier, priority, issue#). Its DEPS/tiering/ordering are deterministic — trust them, do not re-derive them by hand (the hand-derivation failure modes the script encodes are recorded at ci.gitea-milestone-filter-noop). It also raises two JUDGMENT flags it deliberately does not decide — CLAIM? (a claim can precede the label) and UMBRELLA? (an epic whose children are the real pickups) — resolve ONLY those by reading the flagged issue's comments/body. The script does not rank an arc tier (there is no active arc right now — maintenance/backlog mode, see above); if a new arc/milestone is ever established, add it as a script tier rather than reverting to prose-derived ranking. If the script is unavailable (no creds / Gitea down), it fails open (prints a notice, exits 0) — in that case fall back to a single low-cost model session querying live tiers directly (OPEN-milestone → reviewpriority: high/medium/low), never to inline sorting by the orchestrator. If the user names an issue, skip Track B entirely and go straight to focused retrieval (below) plus a live claimability check on that issue; this explicit user choice is the sole path by which a parked issue may be worked.

Dispatch exactly one selector on the cheapest suitable model at low effort when the client can route a cheaper subagent; otherwise run it in a separate low-cost session before starting or resuming the orchestrator and pass in its packet. Do not fall back to inline sorting or an equally expensive selector. If no cheaper route or session is available, pause and request the selector packet rather than consuming orchestrator tokens on queue ranking.

FIRST read AGENTS.md and CLAUDE.md when present, then docs/README.md and the sections its task-signal map points to for your task, plus the Lessons below. Apply both client instruction files; where they differ, follow the stricter safety/completion requirement unless a higher-priority instruction resolves the conflict.

FRONTIER ESCALATION — these moments go to the strongest available reasoning model. In Claude Code, prefer Fable; in Codex, prefer the strongest frontier model at extended-high reasoning. If you are already running that profile, handle them inline. Otherwise delegate with a SELF-CONTAINED brief (scope, exact question, evidence, file paths, and doc pointers), or ask the user to switch models when the client cannot route the escalation. Do not block merely because one brand-specific model is absent:

  • 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 independent-review findings 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.

Knowledge retrieval (MemPalace + catalog + Gitea — the #642 seam)

MemPalace = candidate discovery only, retrieved at a bounded k; every passage verified against its cited Markdown/Gitea source before use. Default wing = ErsatzTV-Decisions (active docs/decisions.md + docs/decisions/*.md + catalog). History wings ErsatzTV-Decisions-Archive (docs/decisions/archive/**) and Gitea-ErsatzTV (issues/comments incl. closing records) are touched only when the question is explicitly "what did the rule used to be." "What is the current rule for X" never touches the history wings.

The four load-bearing orientation bullets — carry these VERBATIM wherever this contract is referenced (docs/README.md too):

  1. Current conventions/decisions → catalog-first. Start at docs/decisions/README.md; discover via the ErsatzTV-Decisions wing (active) / ErsatzTV-Decisions-Archive (superseded/retired). Resolve by topic/key, never by chasing a file path.
  2. Issue history → evidence, not authority. The Gitea-ErsatzTV wing is historical narrative that may be stale; it never overrides current Markdown.
  3. The breadcrumb rule (the crux behavior change). A file path named inside a historical issue comment (e.g. "grep docs/decisions.md 2026-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 own status: 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.)
  4. Fallback when MemPalace is stale/down: docs/decisions/README.md catalog, then rg '^`key: <dotted.key>`' docs/decisions/. MemPalace is never authority nor sole fallback.

Graceful degradation (calibration, not a correctness cliff): even an under-oriented agent that literally greps docs/decisions.md post-#521 gets valid-but-incomplete results (misses topic files

  • archived records), never wrong ones — that file holds only active records. So this is a completeness/latency risk during rollout, not a correctness cliff.

Altitude / precedence: docs/decisions.md (+ topic files) is normative/current ("the rule is X") — the ErsatzTV-Decisions wing, authority for "what is the current rule." Gitea issues are evidentiary/historical ("we worked X on date D; spec, discussion, outcome") — the Gitea-ErsatzTV wing, authority for "how was this handled / what was the context." A decision record points to its issue; the issue carries the provenance the record compresses. Same fact, two altitudes — canonical Markdown wins for "current."

Issues in MemPalace are for DISCOVERY, not queue state. Encounter a problem → search MemPalace → find relevant docs AND relevant open issue(s): this avoids filing duplicates and surfaces bundles (related issues to pick up together). But a MemPalace issue hit is a candidate: open/closed/claimed/blocked/priority flip constantly and MemPalace lags (seconds1h, up to a week if the webhook is down), so a hit may be a since-closed issue shown open, or miss a just-filed one. Always re-confirm live state in Gitea (scripts/select-queue.sh) before acting — never treat a MemPalace issue hit as current queue state.

What to mine per issue: open issue → the BODY (dedup + bundle-clustering; no closing record yet). Closed issue → body + the ## Closing record (how it was resolved). Ephemeral comments (claim / progress / Review-verdict:) → skip or de-weight both ways — process exhaust, not the knowledge store. The ## Closing record is the one structured per-issue summary meant for retrieval + export.

Staleness bounds (so agents know when to distrust a hit): webhook re-mine in seconds; hourly reconcile; weekly full sweep catches file moves/deletes. Worst case before a supersession takes effect in retrieval: ~1h, or ~1 week if the webhook is down and only the move happened. When in doubt, fall back to exact search (bullet 4 above).

Never derive live queue state from MemPalace, #237, or historical comments.

Then work the queue:

  1. Run the two tracks above (orientation always; selection unless an issue is named).

  2. If Track B ran: apply its ranked shortlist. The orchestrator makes one focused live recheck of the winner (still OPEN, unclaimed, not blocked) before claiming; if it changed, check the next fallback. An empty backlog is not a stopping condition — if the selector returns any eligible candidate, claim its top-ranked winner; do not ask the user to choose merely because candidates belong to different workstreams. Never invent a fix-size, recency, or perceived-relevance tiebreaker.

  3. Claim it — but CHECK FOR AN EXISTING CLAIM FIRST, and the label is not the whole check. The in-progress label prevents duplicate pickup; it does not prevent duplicate work, because another session may already be implementing an issue it has not labelled (or labelled after you read the list). Before writing any code, run all four — they are cheap and they fail differently:

    a. Open PRs referencing the issue. GET /repos/{owner}/{repo}/pulls?state=open and look for fixes #N / refs #N in the body, or the number in the branch name. This is the check that would have caught ersatztv#649 being implemented twice. b. Remote branches naming the issue. git ls-remote --heads origin '*<N>*' — a branch usually exists before the PR does. c. Recent comments on the issue, not just its labels — a "claiming" comment from another session may predate the label, which is exactly what CLAIM? from select-queue.sh flags. d. git fetch origin main, so you are reading current state rather than your session's opening snapshot.

    If any of those hit, do not start: report it to the user and take the next candidate. If none do, claim with the in-progress label and a "claiming" comment (reviewer-repo audits are claimed by comment only). Treat a claim as live until a later comment explicitly releases or abandons it, and exclude audits with a posted deliverable even while the issue remains open for implementer replies.

    Re-fetch origin/main before every push, not only at branch time. A long session can run for hours across several review rounds; main moves underneath it. A branch cut from a stale base whose diff is computed against that stale base will silently show other people's merged work as deletions, and pushing it reverts them. Rebase (never merge main in) and re-run the local gate whenever the fetch shows movement. → process.parallel-session-claim

  4. Scan for bundle-able siblings (always, right after claiming — not optional). Check all three bundle axes from "Bundles" above: the claimed issue's milestone, its cross-references / backlinks, and its shared label(s) (list the other open issues under each of its labels). Surface any small, independent siblings to the user with a size/fit read and a recommendation, and fold the agreed ones into this session (parallel worktrees for disjoint slices). This is how #22's sibling-cluster fixes (#553 + #512) got swept together; skipping it silently narrows a session that could have closed two or three issues for barely more cost.

  5. Read the issue bodies (they carry the task context/evidence) and work the item(s) under the HARD CONSTRAINTS below.

  6. Finish the session: 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); post a ## Closing record (template below) on each issue you closed or substantially progressed; remove your in-progress labels; and complete the per-issue Task Completion Protocol from the applicable AGENTS.md / CLAUDE.md instructions (including the done workflow when required).

  7. Leave the shared checkout fresh (H13): run scripts/refresh-shared-checkout.sh. It fast-forwards /Users/timothy/ersatztv to origin/main and refreshes web/node_modules if the lockfile moved; it refuses and changes nothing unless that tree is on a clean, non-ahead main. A NO-OP is a normal result — report it and carry on.

Closing record (session-end / issue-close artifact)

Post this as the structured closing comment on any issue you close (or substantially progress) — it is both the human-readable summary and the per-issue unit MemPalace mines for retrieval:

## Closing record
**Outcome:** <what shipped / what didn't; PR link>
**Root cause:** <for bug fixes/incidents — why the problem existed, or "unknown, see follow-up #N">
**Decisions/conventions changed:** <keys added/superseded in docs/decisions.md, or "none">
**Reusable knowledge:** <a fact/gotcha worth surfacing to a future session or MemPalace search>
**Verification:** <tests run, live-E2E, CI status>
**Deferred:** <anything explicitly punted, with a follow-up issue link, or "none">
**Docs updated:** <which docs/*.md files changed in this PR, or "none required and why">

HARD CONSTRAINTS:

  • Work in a worktree off origin/main — always, including a one-line docs commit. Never commit in /Users/timothy/ersatztv and never read its git state to learn about main. → process.shared-tree-readonly
  • One committing agent per worktree. Give each parallel slice its own; read-only review agents may share. → process.one-worktree-one-committing-agent
  • Never commit or merge inside a worktree you did not create — land on a sibling-owned branch with a plumbing merge. → process.foreign-worktree-plumbing-merge
  • Parallelize by default on genuinely disjoint slices (normally 23 delegated agents). Cap builds at 34 and gate on FREE RAM, not CPU. Never set ETV_UPDATE_GOLDENS.process.build-concurrency-limits
  • Name the model + effort for every dispatched agent, in the dispatch itself — bounded recon → cheapest fast tier at low; mechanical slice against a documented contract → mid tier; judgment-heavy (design, compiler/parser, security, migrations, review arbitration) → orchestrator tier; independent review → a different model family than the implementer. Omitting it silently inherits the orchestrator tier, so state the choice out loud. → process.per-agent-model-routing
  • Local gate + cold-context review BEFORE the push, never after — and the push is licensed by a CLEAN verdict, not by a review having run. Zero outstanding findings on the current tree, however many rounds that takes; "round 1's findings are fixed" and "the mechanism has been cleared" are both reasons to keep reviewing, not to push. Each extra push auto-cancels the live run, and a cancelled job reads as failure at the commit-status endpoint — so an early push manufactures phantom reds on top of the wasted runner time. → process.local-gate-before-push, ci.cancelled-is-not-a-verdict
  • Independent review is mandatory for locks/concurrency, auth/security, API write-path handlers, DB migrations, or >~150 changed C# lines; a skip must be stated with its reason. → process.independent-review-rubric
  • Batch your pushes — you cannot cancel a CI run. Only the operator can cancel, in the browser. → ci.batch-pushes-no-cancel-route
  • Do not gate your push on host health — trust the build queue. → ci.no-host-health-gating
  • Arm the CI monitor at PR-open, on the head sha. → ci.monitor-armed-at-pr-open
  • Before any push touching .cs, BOM-check the touched set and run the format gate under bash -c. → process.bom-format-detection-recipe
  • Keep a PR branch current by REBASING on origin/main — never merge main in; regenerate (never hand-resolve) generated artifacts after a conflicted rebase. → release.format-as-you-touch-rebase
  • Live-E2E via scripts/e2e-local.sh before the push for write-path/UI changes; curl download endpoints, never browser tabs; use a fresh config dir. → testing.live-e2e-prepush-timing, testing.e2e-local-fresh-config-dir
  • Merge consent is DERIVED, not asked. The H6/H10 gate auto-grants when the ## Done-when boxes are ticked, a fresh Review-verdict references the current head, and CI is green; confirm in conversation only when the gate ASKS. Prior pre-approvals never carry over. → release.merge-consent-autogrant
  • Post every review verdict with scripts/post-review-verdict.sh <pr> <VERDICT> — never hand-write the comment. It also writes the review-verdict/h10 commit status, which is the REQUIRED check that actually gates the merge; a comment alone leaves it absent. Being sha-bound is the point: a commit pushed after an auto-merge is scheduled clears it, so scheduled merges can no longer land unreviewed code. → release.verdict-status-check
  • After the push, fixes are follow-up commits — never amend or force-push a pushed branch. → process.pr-routine-sequence

Lessons / workflow lore (STANDING workflow/orchestration rules only)

Scope: how we work — orchestration, CI triage, review routing, cross-session hygiene. Each rule below is one or two lines plus its decision key:; the evidence, the incident that produced it and the full rationale live in that record (docs/decisions/workflow-process.md for most of them). Keep it that waydocs.no-session-narrative is the general rule (a doc records the end state; the path to it goes in the commit message), and it binds here with one extra cost on top: this file is pasted into every session's kickoff, so a paragraph here is a tax paid by every future session, while a record is retrieved only when it is needed (#542).

Editing: prune covered and stale bullets rather than appending — this is not append-only and git keeps the history. If you add a rule, write the record first and cite it here.

Orientation

  • Read docs/README.md → the convention docs first; point agents at named sections, recon only the task-specific delta. → docs.convention-docs-session-start
  • Run scripts/select-queue.sh for queue selection; trust its deps/tiering/ordering and resolve only its CLAIM?/UMBRELLA? flags. → startup.parallel-orientation
  • A session that runs several issues at once is an orchestrated session: docs/handoffs/orchestration.md owns its mechanics and orchestrator-prompt.md starts it; every slot is still bound by this file. → process.orchestrated-session
  • Claim with in-progress before working — but check for an existing claim first (open PRs referencing the issue, remote branches naming it, comments predating the label, a fresh git fetch), because a label prevents duplicate pickup, not duplicate work: #649 was implemented twice to completion. And re-fetch origin/main before every push — a branch on a stale base reverts whatever merged meanwhile. → process.parallel-session-claim

Building and reviewing

  • The PR routine is a fixed sequence; for API changes build the app project FIRST, then ./scripts/update-openapi.sh, then npm run generate:api. → process.pr-routine-sequence
  • Re-review the FIX COMMIT, not just the initial diff; loop until a clean verdict or an explicit acceptable-defer, and file a tracked issue for anything deferred. → release.review-verdict-gate
  • When independent reviews disagree, escalate to the frontier judge — and escalate the proposed FIX with the finding. → process.review-disagreement-frontier-judge
  • "Make X consistent with Y" findings are new code; for any timer/effect ask when it fires, including on mount. → process.consistency-fix-new-code-scrutiny
  • Before trusting a "single owner / no cross-release" claim, grep the whole host project for every producer — the background scheduler is the usual miss. → process.lock-ownership-enumerate-producers
  • Before deleting a "workaround", enumerate every behavior it provided. → process.enumerate-workaround-behaviors-before-deleting
  • Concurrency tests need a stress loop and a negative control that inverts the real condition — never if (true). → ffmpeg.work-ahead-slot-atomic
  • Enumerate lazy LanguageExt returns in tests; live-E2E is the only net for that class. → release.live-e2e-required
  • Harden a soon-to-grow feature with a runtime posture, not a hardcoded clamp. → process.harden-with-runtime-posture-not-clamp

Reading a red CI run (check these before diagnosing your diff)

  • A killed job reports conclusion: failure — read the log tail for the ❌ Failure - Main marker; log timestamps are UTC, the host is UTC+2. → ci.killed-job-triage
  • cancelled is not a verdict, and the endpoint you poll hides it: commits/{sha}/status has no cancelled state and reports one as failure. Resolve the job conclusion via actions/runs/{id}/jobs before believing a red. → ci.cancelled-is-not-a-verdict
  • A failure inside a setup/cache step, before your code compiles, is environmental. → ci.infra-shaped-red-under-load
  • A lone decisions lifecycle red is a known flake: do nothing, the operator reruns it. A convincing local explanation is always available — check this rule before theorising. → ci.decisions-lifecycle-flake
  • Heavy-render web tests need explicit per-test vitest timeouts. → ci.web-test-per-test-timeouts
  • Verify locally and trust it; CI confirms (~9 min warm, seconds for docs-only). → ci.verify-locally-ci-confirms

Tooling gotchas

  • E2E cleanup kills only PIDs it started — never pkill -f "dotnet ErsatzTV.dll"; a foreign listener is reported, not reaped. Carry this into every delegated E2E brief. → testing.e2e-cleanup-scope-by-pid
  • Playwright-MCP: curl download endpoints, never open a tab; on repeated stalls prefer pgrep -f ms-playwright-mcp and kill only your own PID (that record still says pkill -f; the same shared-host reap is being reconciled in #596). → testing.playwright-mcp-download-and-recovery
  • Channel branding is NOT testable through the troubleshooting-playback API. → testing.troubleshoot-path-cannot-test-branding
  • Codex cheap workers launch via codex exec, not spawn_agent. → process.codex-cheap-worker-launch
  • A dropped subagent connection is transient — resume via SendMessage, don't restart. → process.subagent-drop-resume
  • Fixing onto an unmerged feature branch: branch off it, FF-push, and beware the sibling-worktree path leak. → process.branch-off-feature-branch

Session end

  • Run the H12 audit and label everything it flags. → process.issue-qualification-audit
  • Run scripts/refresh-shared-checkout.sh so the next session's checkout is current. → session.shared-checkout-refresh