Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 11s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m40s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Refs #347 Co-Authored-By: OpenAI Codex <codex@openai.com>
339 lines
29 KiB
Markdown
339 lines
29 KiB
Markdown
# ChicoryTV issue-queue handoff (client-neutral kickoff + workflow lore)
|
||
|
||
> **PROTOCOL CHANGED 2026-07-11** (decisions.md entry of same date). Queue state lives in the
|
||
> **pinned Gitea tracker [ersatztv#237](http://192.168.1.95:3000/timothy/ersatztv/issues/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 stable operating guidance: the standing kickoff prompt and the workflow lore.
|
||
> Historical per-session state: `git log` of this file (last state-bearing revision: 8b77d5e7).
|
||
|
||
---
|
||
|
||
## Orchestrator launch profile (operator-facing; do not paste)
|
||
|
||
As of 2026-07-13:
|
||
|
||
- **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 or minimal effort. 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.
|
||
|
||
Route by capability when the client supports per-agent model selection: fast/small for bounded recon,
|
||
balanced for mechanical changes, and the strongest coding/agentic tier for judgment-heavy work.
|
||
Otherwise 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 at low or minimal effort. Strictly cap its files, queries, log window, and output;
|
||
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 cheap worker lacks a required tool, give it already-collected bounded evidence for synthesis or report
|
||
the limitation explicitly 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`.
|
||
|
||
**When the user has not named an issue, queue selection is mechanical fast/small work, never orchestrator
|
||
work.** Dispatch exactly one selector on the cheapest suitable model at low or minimal effort. Give it only
|
||
#237's body, last ~6 comments, every OPEN milestone and its OPEN issues, OPEN `review`-labeled
|
||
issues, and the reviewer-repo candidate list plus each candidate's claim/deliverable comments; it
|
||
returns a ranked shortlist of at most three issue IDs with one-line rationales and live-state evidence. The
|
||
orchestrator receives only that compact packet, then performs a focused live recheck of the winner before
|
||
claiming. Do not load implementation docs or issue bodies into the selector. If the client cannot route a
|
||
cheaper subagent, run the selector 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. If the user names an issue, skip selection and only verify that issue's live claimability.
|
||
|
||
FIRST read `AGENTS.md` and `CLAUDE.md` when present, then `docs/README.md`, the convention docs it
|
||
indexes, and 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.
|
||
|
||
Then work the queue:
|
||
1. Unless the user named an issue, dispatch or obtain the mandatory low-cost selector packet. The
|
||
**selector**, not the orchestrator, reads 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. Candidate
|
||
discovery MUST enumerate every OPEN issue in every OPEN milestone plus OPEN `review`-labeled
|
||
issues; do not limit the pool to tracker prose or recent comments. Treat an umbrella/epic as a
|
||
container rather than a pickup when #237 names its eligible children. 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). Reviewer audits are claimed by comment: a claim remains active until a later comment
|
||
explicitly releases or abandons it, and a posted audit/review deliverable is completed work even
|
||
when its issue stays open for implementer replies. If the Gitea MCP is down, hit the REST API
|
||
directly (use credentials supplied by the environment or your global client instructions; never
|
||
print them):
|
||
`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. The selector picks the highest-ranked OPEN, un-`in-progress` candidate and returns at most two
|
||
fallbacks. **Rank by labels, not just arc position** (labels are live state; prose is not):
|
||
(1) arc order in #237, then
|
||
(2) **milestone/review tier vs backlog** — an item with the `review` label OR membership in an OPEN
|
||
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.
|
||
The orchestrator then makes one focused Gitea read to confirm the proposed winner is still OPEN,
|
||
unclaimed, and not blocked by a closed milestone; if it changed, check the next supplied fallback.
|
||
Do not reread the full tracker or comments for selection. 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. For an equal milestone/review tier and equal priority,
|
||
preserve #237's explicit eligible order, then use lowest issue number as the deterministic fallback;
|
||
never invent a fix-size, recency, or perceived-relevance tiebreaker. (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. Treat that 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.
|
||
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 the
|
||
applicable `AGENTS.md` / `CLAUDE.md` instructions (including the `done` workflow when required).
|
||
|
||
HARD CONSTRAINTS:
|
||
- Work in worktrees off origin/main. Copy web/node_modules from the main checkout.
|
||
- PARALLELIZE BY DEFAULT when there are genuinely disjoint slices: use up to the client's concurrency
|
||
limit, normally 2–3 delegated agents plus the orchestrator. Fan out bounded recon early, but do not
|
||
invent artificial slices or exceed a runtime-imposed agent cap. At most 3–4 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 8–9-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). Run a cold-context adversarial review agent 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 (strongest tier for judgment-heavy /
|
||
balanced tier for mechanical work when model routing is available; doc pointers + exact facts;
|
||
use the available C# language tooling) → API changes: build app project
|
||
FIRST then `./scripts/update-openapi.sh` then `npm run generate:api` → full local test pass
|
||
→ push, PR → cold-context 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 (derived consent gate) → structured close comments per the applicable
|
||
`AGENTS.md` / `CLAUDE.md` protocol.
|
||
- Never run TWO committing agents on one worktree concurrently. Read-only review agents 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-tree` → `commit-tree -p ours -p theirs` → push the
|
||
commit to the branch ref); the owning session then `git pull`s.
|
||
- **Cross-model independent review rotation** (2026-07-11): use the other client/model family when
|
||
it is available — Codex reviews Claude-authored changes; Claude reviews Codex-authored changes.
|
||
Use whatever native integration is actually installed; client-specific helpers such as
|
||
`codex:rescue` are optional accelerators, not protocol dependencies. On quota/tool exhaustion,
|
||
use a cold-context review-only agent in the current client and retry cross-model review next window.
|
||
Independent reviewers do review and stuck-diagnosis work, not implementation on the same change.
|
||
**Skip rubric (self-exemption is an auditable claim, not a vibe)**: an *independent* review pass
|
||
(preferably cross-model; otherwise a cold agent) 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: independent model families 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
|
||
(cross-model if quota, else a cold review agent) 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.
|
||
- **Independent-review disagreement on a gate PR → frontier judge; judge the fix, not just the verdict**
|
||
(2026-07-11, #235): the reconciliation isn't only "who's right" — Fable historically 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 `Read`ing a *sibling* worktree leaked its
|
||
paths into my later `Edit`s → 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.
|