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
430 lines
32 KiB
Markdown
430 lines
32 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.
|
||
|
||
**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 → `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 — 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:
|
||
|
||
```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 — 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 way** — `docs.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`
|