review-verdict/h10 Awaiting review verdict for 628ae46
Review verdict / Set review-verdict status (pull_request) Successful in 9s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 11s
PR Gates / Docs update reminder (pull_request) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 26s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 1m28s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 16m59s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 19m5s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
`pretooluse-merge-consent.sh` proves all three consent conditions at the moment the merge tool is called. With `merge_when_checks_succeed=true`, condition (a) is delegated to Gitea, which then merges whatever head is green at ITS merge time — while (b) Done-when and (c) the head-referencing verdict were proven against the head at SCHEDULING time. Every commit pushed in between merges unreviewed. The gate is not bypassed; it is satisfied against a snapshot that stops being true. Demonstrated as a controlled A/B rather than inferred, with a CI check left pending so Gitea waits as it really does: without a required verdict context: unreviewed commit B MERGED with it: same sequence REFUSED after reviewing B and posting it: merges — blocked, not deadlocked NOTE the anecdote in #622 is wrong and is corrected in the docs rather than repeated: PR #619 does carry `Review-verdict: MERGEABLE @ 02c82b35`, posted six seconds before the merge, explicitly re-reviewing the follow-up commits. #622 was filed off a lagging API read. The hole is real regardless, and structural — nothing FORCED that re-review inside the window Gitea would have merged in. This turns a property that held by diligence into one that holds by construction. The fix is the sha, not a smarter check. A Gitea commit status belongs to exactly one commit, so a status written for a parent cannot be inherited by a child that did not exist yet. `review-verdict/h10` becomes a REQUIRED status check on main: push a new head and the context is simply absent, which Gitea reads as not-passing (verified against 1.25.4: missing AND pending both block, and auto-merge re-checks the current head). It also covers merge paths the hook never sees — Gitea UI, raw API, another agent's session. - scripts/post-review-verdict.sh writes the comment and the status together so they cannot drift, and re-reads head after commenting: if a commit landed mid-flight it writes NO status and exits non-zero rather than retargeting the verdict at a commit nobody read. - .gitea/workflows/review-verdict.yml auto-passes the two exempt classes that would otherwise deadlock — Renovate-authored (platformAutomerge) and docs-only — and marks everything else pending. Exemptions are void when the PR touches .claude/, .gitea/, .husky/, scripts/ or docker/ci/. - The hook refuses to SCHEDULE an auto-merge unless that status is green on head, and no longer claims "CI green" on the mwcs path, where it never read CI. Two silent false negatives in the exemption path, both found by verifying rather than reasoning, both fixed at BOTH call sites (workflow and the hook's pre-existing docs-only carve-out): - The files endpoint caps at 50 rows and IGNORES a larger `limit` — PR #619 has 194 changed files and `?limit=100` returns 50. A single page saw ZERO protected paths there where the full enumeration finds ten. Both now page to exhaustion and withhold the exemption if they cannot complete. - A rename is ONE row whose `filename` is the destination, the source only in `previous_filename`. Verified live: `.gitea/workflows/renovate.yml` -> `docs/innocuous-note.md` presented as docs-only with no protected path visible. Both now read BOTH sides. Limits are documented, not papered over: base changes leave the head sha (and so the verdict) unchanged, and a PR editing the workflow is judged by its own edited copy — so PROTECTED is a guardrail against accident, not a tamper-proof control. fixes #622
380 lines
28 KiB
Markdown
380 lines
28 KiB
Markdown
# 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.
|
||
<!-- archival:237 --> 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: high` → `priority: medium`
|
||
→ `priority: 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.
|
||
|
||
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 → `review` → `priority: 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 (seconds–1h, 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**: 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. **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:
|
||
|
||
```markdown
|
||
## 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 2–3 delegated agents). Cap builds at
|
||
3–4 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. → `process.local-gate-before-push`
|
||
- **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 way** — this file is pasted into every session's kickoff, so a paragraph of narrative 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`
|
||
- Claim with `in-progress` before working — but a claim prevents duplicate *pickup*, not overlapping code
|
||
changes. → `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** — report FAILED and CANCELLED separately. → `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`
|