Timothy asked why the kickoff handoff doc stores historical narrative when
it should be instructions. It shouldn't — its own lore section is chartered
as "STANDING workflow/orchestration rules only" with the why belonging in
docs/decisions.md. But an inventory of every bullet against the decision
corpus inverted the premise: only ~8 of ~38 were actually covered. 19 had
no record anywhere and 11 more were half-covered, so that single file was
the ONLY copy of the mandatory review rubric, the whole CI-triage
vocabulary, the build-concurrency policy, the H12 session-end audit, and
the plumbing-merge recipe. Pruning first would have destroyed them.
So the records come first. New topic file docs/decisions/workflow-process.md
carries 32 records (ci.*, process.*, testing.*) covering every NONE and
PARTIAL the inventory found, including the Gitea `?milestones=` no-op bug
whose only copy was the archived selector section this prune deletes.
Only then the prune: HARD CONSTRAINTS and the lore section become one- or
two-line rules, each citing the decision key that holds its evidence, and
the 40-line "Archived — do not follow" section is gone. 636 -> 353 lines,
with every cited key verified to resolve against the corpus.
The aggregate corpus budget is re-baselined 4800 -> 5600 with the reason in
the code: the corpus grew because knowledge MOVED into it, which is the
system working, not drift.
refs #542
A session was handed docs/handoffs/chicorytv-issue-queue.md pasted out of
/Users/timothy/ersatztv while that tree was 81 commits behind, so it still
described the queue protocol #520 retired the day before (read tracker
command was ever run against that tree, so every existing "never read its
HEAD" guard was irrelevant: a stale checkout serves stale FILES, and docs
are what a kickoff depends on. Nothing broke only because selection went
through scripts/select-queue.sh.
The lore bullet on that tree already prescribed the shape of the fix for
its earlier failure modes — "a design flaw, not a discipline failure; a
check does not stay true" — so this removes the stale condition instead of
adding another check.
scripts/refresh-shared-checkout.sh fast-forwards the tree to origin/main
and reinstalls web/node_modules when the lockfile moved. It is deliberately
timid: it refuses and changes nothing when the tree is not on main, is
dirty, is ahead, or is mid-rebase/merge, and it never switches branches,
stashes or discards. A NO-OP is a normal outcome.
Uses npm ci rather than npm install — the first version used install,
which rewrote package-lock.json and left the tree dirty, i.e. the exact
state the next run refuses on, so it would have disabled itself after one
use. Asserts the tree is clean at exit.
refs #541
Exhaustive triage of the closed tracker ersatztv#237, whose 111 comments
server-management#642 excludes from MemPalace ingestion (over the per-file
cap) and #520 removes from startup. #524's premise was that facts living only
in those comments would be orphaned and need curating into lifecycle records.
Result: zero decision-shaped orphans. Every durable decision-shaped fact is
already held by the decision corpus or by the individual issue the comment
narrates -- which the exporter does ingest. The tracker was always the lossy
copy, because the session protocol required the fuller closing record on the
worked issue first.
- docs/decisions.md: new active record docs.tracker-comment-retrofit, leading
with the reusable rule (check the worked issue BEFORE the decision corpus)
and the consequence for #642's benchmark row, which has no valid subject.
- docs/tracker-retrofit-triage-237.md: the audit trail -- method, per-comment
classification of all 111, totals, and the one candidate raised and
disproved (#497's Guids/Directors scope, stated more fully on its own issue).
- docs/handoffs/chicorytv-issue-queue.md: sweeps the two genuinely orphaned
LORE facts the triage surfaced (e2e-local.sh readiness probe hanging on a
reused config dir; troubleshooting playback cannot exercise channel branding).
- docs/README.md: index the new doc.
The claim is deliberately narrow: no *decision-shaped* orphans. The lore bucket
was classified but not coverage-checked, and it was not empty -- hence the sweep.
fixes#524
Third review pass: MERGEABLE WITH NITS. Taking the one finding it asked
for before merge, plus a doc nit.
The cancellation filter added last commit had no token check, and it spans
the whole Transcode body -- including every mediator send (ffprobe via
CliWrap, media-server API calls, subtitle extraction, song-video
generation). TaskCanceledException is also what HttpClient throws on its
OWN timeout, so a real timeout in any of those was being downgraded from
an ERROR with a stack trace to a routine "Terminating HLS session"
Information line. Behaviour was unchanged (both arms return false) but the
fault signal was lost, and this repo has been bitten before by
"empty log != the event didn't happen". Now filters on
cancellationToken.IsCancellationRequested, so only genuine caller
cancellation is treated as a graceful teardown.
Doc nit: the <exception> block said cancellation "is thrown"; it is only
thrown when the token trips while the probe is in flight -- cancelling
after it completes returns normally. Now says "may propagate".
Declined the reviewer's optional suggestion to drain until a 0-return
instead of reading exactly one byte: reading exactly one byte is what
makes the guard safe BY CONSTRUCTION, since a server or proxy that
answers 206 with a wider range than requested still cannot be drained
unboundedly. 206-only was confirmed correct rather than extended to short
200s, since deciding "short" from Content-Length would reopen the
unbounded path for a chunked or Content-Length-less response.
Also records the operator's standing rule in the handoff lore: a lone
`decisions.md append-only` red is a known infra flake -- do not
investigate, rebase, amend or push to clear it; the operator reruns that
job from the UI. I violated this earlier in this PR with a
tidy-but-wrong "my entry is no longer at EOF" theory, and the rebase did
not fix it -- the job went red again on a verified pure-append diff, which
is the proof the red was never about the diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The kickoff dispatches a cheap model to rank the backlog, and the lore then made the
orchestrator re-derive its mechanical claims (deps, milestone tiering, priority order,
in-progress state) because a small model kept getting them wrong. This pays that tax down:
the mechanical checks are now CODE — GET /dependencies exclusion, LOCAL
.milestone.state/review/priority tiering (never the no-op ?milestones= filter),
(tier,priority,issue#) ordering, in-progress/parked/PR exclusion — leaving only two
JUDGMENT flags (CLAIM?, UMBRELLA?) for a human/model to resolve.
- scripts/select-queue.sh: fail-open on no-creds/unreachable; ~1.3s; shellcheck clean;
UMBRELLA? computed in the jq pass (no per-candidate body fetch).
- handoff kickoff: run the script FIRST; trust its deps/tiering/ordering, recheck only flags.
- handoff lore: the three 'cheap selector unreliable → re-derive by hand' bullets kept as the
EVIDENCE for why the script exists; the prescription is redirected to 'run the script'.
Operator-requested this session: 'rather than have the lore make us redo the selector's work,
improve the selector.'
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The handoff PR-routine and HARD-CONSTRAINTS placed the cold adversarial review
AFTER push/PR-open. New standing rule (Timothy, 2026-07-17): local gate + cold
review run BEFORE the push; fold pre-push fixes into the branch; CI only
confirms. Runners should see only already-validated work — a Gitea run can't be
cancelled, so a run spent on a lint/review-caught issue is pure waste. Also notes
docs-only PRs are seconds now (ersatztv#416).
Operator-requested after PR #405 pushed 5 times, orphaning live runs the
operator had to cancel by hand. Batches every standing-lore correction this
session produced into one commit (per the batching rule it adds).
New HARD CONSTRAINTS:
- BATCH PUSHES. Cancellation is impossible from the agent side on Gitea 1.25.4
— REST .../runs/{id}/cancel and MCP cancel_run both 404, and the web-UI route
needs a session+CSRF that doesn't script. Only the operator can cancel, so an
orphaned run holds a runner slot until it finishes. If you must supersede a
live run, SAY SO.
- TRUST THE GITEA BUILD QUEUE. Do not gate/throttle a push on host health; the
runners were retuned for stability. Batch because you can't cancel what you
orphan, not to protect the host.
- BOM-CHECK touched .cs before pushing. The #311 gate is fix-as-you-touch, and
it bit two sessions the same day (PR #405 ×6; #70/PR #402 ×19 via Python
utf-8-sig writing BOMs back). Verify your detector — an od-based grep reported
all-clean while 19 files were dirty.
Corrects a claim I nearly published: `dotnet format --include` DOES work here.
The apparent no-op was the SHELL — CI's mapfile is bash-only, zsh has no
mapfile → empty array → zero files → exit 0. Run it under bash -c.
THERE IS NO "main checkout" — the biggest correction here.
/Users/timothy/ersatztv is a shared mutable working tree whose HEAD is whatever
the last session left there. Its name lies, and it bit TWO sessions on
2026-07-17, both doing the obvious thing: one assumed main and committed onto
the #604/CI session's branch 24s after that session's own commit; another read
git log there and concluded main was "4 behind origin" — a phantom. Framed as a
design flaw, not a discipline failure: "check git status first" appears to
confirm the false assumption and then goes stale (it WAS on main at 12:46 and
wasn't by 14:17). Worktree discipline itself is healthy — 10 feature worktrees.
Diagnosing CI reds — three ways to misread one, all hit this session:
- A KILLED job reports conclusion=failure, not cancelled. The tell is a log that
stops mid-step with NO error and NO failure marker. A runner retune killed run
1006's migration + E2E on a BOM-only diff that couldn't break them. Log
timestamps are UTC, host is UTC+2 — convert before correlating.
- cancelled ≠ failure: a cancel is NO verdict, and a run marked failure may hold
a genuine job failure from BEFORE the cancel. Monitors must count FAILED and
CANCELLED separately.
- "Unable to pull refs/heads/v4" is act refreshing its action cache and is
followed by "Cloned …" — noise, not a cause. Grep the failure marker, not
the word "error". An infra-shaped red (setup/cache step, before your code
compiles) is not a code failure; don't file a CI bug off one sample.
Also: the cheap selector's failure modes are wider than deps+priority — it also
misses in-progress claim state and umbrella-vs-child.
Docs-only.
The #73 selector packet reported #385/#386 as deps:clear when both were
blocked, and missed #383 (the only priority:medium in tier 1) by ranking on
issue number. Either error alone produces a bad pickup, and the kickoff's
winner-only recheck cannot catch a wrong winner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `?milestones=<name>` filter returns the whole open-issue list (not a
filtered set) when the milestone title contains special chars, mis-tiering
issues during queue selection. Recipe: fetch open issues once, filter locally
on `.milestone.title`. Also notes the dependencies-API owner/repo/index form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backlog grooming (2026-07-17): the kickoff cascade now excludes any issue with an
open blocked-by dependency (Gitea GET /issues/{n}/dependencies; auto-clears when the
blocker closes) so a blocked issue is never picked ahead of its blocker. Adds the
parked->closed convention and the bundle model: related work is grouped via active-arc
milestones or cross-reference comments and detected on pickup via milestone + backlinks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The go-live arc (#237) is closed; add a 'Current phase' section stating the
terminal transition to backlog mode + structural guidance (empty arc/gate/review
tiers are the expected steady state; fall through to the priority-label cascade).
Drift-proof: terminal fact + structure only, no named frontier issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the outstanding Plex flow in the #202 post-merge integration
checklist against a disposable test Plex Media Server: pin-flow auth,
server/library discovery (the 2026-07-13 gap that returned servers:[]),
sync-preference persistence, and sign-out cleanup all passed. Records
exact API state evidence and the test-admin credential reset.
fixes#333
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the tested GPT-5.4-Mini low-effort worker command, current spawn-agent limitation, and unsupported legacy model trap so future sessions can route bounded work without rediscovery.
Refs #355
Co-Authored-By: OpenAI Codex <codex@openai.com>
Clarify that enabled web_search makes a cheap worker tool-bearing even when the selector intends to use only Gitea, reserve minimal for tool-free supplied-evidence synthesis, and require hard evidence/output caps.
Co-Authored-By: OpenAI Codex <codex@openai.com>
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>
scripts/issue-qualification-audit.sh: 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: label; gate-vs-backlog derives from the
review label / milestone, and a milestone is NOT required (backlog is unmilestoned).
Advisory (exit 1 if any unqualified); fail-open without Gitea creds. Wired into the
kickoff session-end protocol + a lore bullet. Tested live (flagged 2) + no-creds no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- contributing.md §7: document fix-formatting-as-you-touch (normalize a legacy file you
edit, incl. BOM strip; no big-bang), enforced by the pre-commit hook + the new format CI job.
- kickoff lore: flip "merge main into PR branch" -> "rebase on origin/main" (H11 enforces it);
keep the regenerate-generated-artifacts-on-conflict nugget.
- decisions.md: new entry (+ TOC) for both decisions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two of the four non-hook #303 process follow-ups (the two docs items; the
security scan and migration-on-prod-copy smoke are deferred to their own
sessions):
1. Codex-skip rubric (kickoff workflow lore): an independent review pass is
MANDATORY for diffs touching locks/concurrency, auth/security, API
write-path handlers, or migrations, or >~150 changed C# lines; skippable
only for a pure-SPA/docs leaf, and a skip must be stated + justified. Makes
self-exemption an auditable claim (the correlated-blindspot net).
2. Live-E2E is now a STATED REQUIREMENT for API write-path handler changes:
new "When live-E2E is required" section in docs/e2e-local.md + a decisions.md
entry, formalizing the #229 lore bullet. The seeding recipe was already in
e2e-local.md (added for #220), so the stale "recipe not yet in docs" lore
bullet is pruned to a pointer.
Docs-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lore had grown into a changelog (~40 verbose bullets). Removed bullets
whose substance lives in docs/decisions.md or api-conventions §7a/b/c
(#251 post-commit None, #252/#259 reconcile, #253 PR3/PR4 concurrency, addTo
layer, DTO nullable facts, disabled={saving}) and dead Blazor-parity process
notes (parity verdicts, screen!=parity, unwired-endpoint gaps, #205/#206
docs-staleness). Condensed the survivors. Added a scope banner: this section
is standing workflow/orchestration lore only; engineering decisions go in
decisions.md, never here (they duplicate and drift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recent session pickups re-did finished work (thought closed issues were
still open). Root cause: DONE/OPEN status lived in append-only prose (arc
inline markers + each comment's 'Recommended next'), which lags real issue
state — worst across parallel sessions narrating each other's merges.
Kickoff prompt fixes:
- SOURCE OF TRUTH = live Gitea state, never prose; verify OPEN (issue AND
milestone) before claiming.
- Gate/frontier defined structurally (lowest-numbered open arc item), not
hardcoded to an issue number (an earlier pass re-planted the bug by
hardcoding '#197 cluster'/'#91b milestone CLOSED').
- Label-based ranking: arc order -> gate(review label/milestone) -> priority.
- Pick order across arc-frontier / priority-pickup / audit pools.
- Bound the comment read (~6 newest); MCP-down REST fallback; #237 canonical.
- New lore bullet capturing the drift root cause + structural cure.
Companion edits to tracker #237 body (prune arc status -> 'Done (history)'
section) made live; noted in a session comment there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Handoff file reduced to static kickoff prompt + append-only lessons lore;
queue/arc/session log live in pinned tracker ersatztv#237 with in-progress
claim labels and end-of-session triage. Decision recorded in decisions.md;
docs index updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>