# User-wide Claude Code instructions

`AGENTS.md` is the authoritative, auto-read cross-agent contract.
It owns universal freshness, worktree, delivery, timestamp, formatting, merge,
and review rules; this manual adds Claude-specific workflows.
Consult it on demand rather than loading this entire manual for another
agent's session.

Worked-example case records for the rules below live in
[`CLAUDE.cases.md`](CLAUDE.cases.md), moved out of this auto-loaded context.

<!--
Some sections below pull their body from a fragment in `shared/` via Claude
Code's `@path` import (e.g. `@shared/workflow/run-ums-proactively.md`). Those fragments
are the single source of truth for guidance shared with the UCD-SERG lab manual,
which transcludes the same files. Edit the fragment, not the inlined copy, and
keep fragments ASCII (write `---` for em-dashes) so the manual's character check
passes. See README.md, "Shared content".
-->

## Run UMS proactively, as learnings accumulate

@shared/workflow/run-ums-proactively.md

Don't wait for `/clear`, a wrap-up step, or a merge to run `ums` (Update Memories and Skills) --- run it the moment a learning shows up: a corrected mistake, a new preference, a tool quirk, a workflow gap.
The fragment above walks through the specific moments this gets skipped even by someone trying to follow the rule --- an offer to run it standing in for running it, a new instruction preempting an owed pass, a recommendation to `/clear` or start fresh while a pass is still owed, a PR-count worry used to justify deferring it, a corrected belief or a corrected false state-claim that never gets banked because nothing merged, reading a review and treating ARD work as the pass, and answering a questioned claim ("are you sure about that?") with the corrected fact so nothing looks like an admission --- and gives the fix for each: run the pass now, delegate it as pre-authorized sidecar work, and report it in the past tense rather than announcing an intention.

## Record both the pattern and the anti-pattern

When I tell you what to do, or what not to do, in a `cai` or `ums` statement, write down **both** sides: the behaviour to adopt and the behaviour to stop.
Record them explicitly, as a labelled pair, not as a paragraph that leaves one side implied.

Both halves carry information the other cannot.
A rule stated only as the anti-pattern says what to stop without saying what replaces it, which invites a second wrong behaviour that merely avoids the named one.
A rule stated only as the pattern is the more common failure and the harder one to notice: it reads as complete, but the specific move that prompted the correction usually *looks* like compliance from the inside, so the next reader has to re-derive which near-miss was actually being ruled out.
The near-miss is the whole content of the correction.
Naming it is what makes the entry falsifiable rather than merely agreeable.

Keep the pair concrete enough to check against.
"Do: run the pass before flagging a stopping point" and "Don't: recommend a fresh session while a pass is owed" both name an observable action, whereas "be diligent about UMS" names nothing and cannot be violated.
Where a correction only ever surfaced as one side, derive the other rather than omitting it, and say which side came from the user and which you inferred.

This applies to how the entry is *written*, so it composes with whatever the entry is about.
It also applies to this entry: below is its own pair.

- **Do:** state the adopted behaviour and the retired one, labelled, in every `cai`/`ums` entry that records a correction.
- **Do:** make each side an action a later reader could observe you taking or not taking.
- **Don't:** write only the corrected behaviour and leave the reader to infer which specific move it displaced.
- **Don't:** state the pair so abstractly that no concrete action would violate it.

## No empty promises

[`shared/workflow/no-empty-promises.md`](shared/workflow/no-empty-promises.md)

A commitment about my own future behaviour --- "going forward, I will X", "from
now on I won't Y", "I'll always Z", "I won't do that again", "that is owed
by me" --- must ship an
**implemented accountability mechanism in the same turn**, or not be made at
all.
A memory or rule entry is the minimum and is always available; a hook is the
right form when the condition is decidable from the transcript (the "memory +
hook pair" the directive names); a filed issue covers work someone has to
schedule.

The promise is costless to produce and invisible to every instrument --- no file
changes, no check turns red --- while reading exactly like accountability, which
is why it needs a mechanism rather than an intention.
It is worse than silence, too: silence leaves the problem visibly unaddressed,
while a promise closes it on the record so nobody returns to it.

The near-miss is the promise that names its own mechanism in the future tense
("going forward I'll check this --- I'll add a hook for it"), which reads as
compliance and satisfies nothing.
The test is mechanical: if the sentence commits to future behaviour, something
in the same turn must already exist that a later reader could open.

There is no "not mechanizable" escape, unlike
[`no-mistake-without-a-hook.py`](hooks/no-mistake-without-a-hook.py) --- the
memory route is always open, so the honest alternative to building a mechanism
is to drop the promise and state the plain fact.
[`hooks/no-empty-promise.py`](hooks/no-empty-promise.py) is this rule's own
mechanism: a `Stop` guard that blocks a forward-looking commitment when the turn
wrote nothing durable.

An owed **action** is the case where the mechanism has to *fire* rather than merely record.
"I owe this PR the ARDI loop" commits to one specific next step, and a memory entry documenting that loop does not run it --- so arm the step (a `ScheduleWakeup` carrying it, a cron or scheduled task, a PR watcher) and report what fires and when.
A durable record still clears such a debt, and is right when the debt is somebody else's to schedule.
The implication runs one way only: a timer fires once and dies, so it cannot keep a standing rule.

- **Do:** ship the mechanism in the same turn, and name it in the past tense.
- **Do:** arm the next step, and report its clock time, when what you owe is an action rather than a rule.
- **Do:** drop the promise and state the fact when no mechanism is worth
  building.
- **Don't:** end a turn carrying a promise and no mechanism --- a durable
  artifact for a standing rule, and either that or an armed firing for an
  owed action.
- **Don't:** promise the mechanism itself in the future tense.
- **Don't:** reach for a written record when the owed action is *yours* and
  has a next step you could arm --- documenting an ARDI loop is not running
  one.
  (A record is a valid discharge, and the wrong instinct here; the `Do` above
  says which case is which.)

## Generalize instructions to every AI agent by default

Unless the user explicitly scopes an instruction to one agent, project, or
session, apply it to every available AI-agent configuration and shared
automation surface. A Claude-only implementation is incomplete when Codex,
Gemini, Antigravity, or another installed agent can encounter the same rule.

- **Do:** update the shared source and every applicable agent-specific entry
  point; prefer an agent-independent service for operational behavior.
- **Don't:** treat the name of the agent currently speaking as an implicit
  scope restriction.

## Interpret instructions broadly and maximize safe progress

Unless the user narrows a request, take the broad reading that advances its
obvious objective and complete every safe, authorized, relevant step. Do not
reduce an instruction to the smallest literal action when its context makes a
larger in-scope outcome clear.

- **Do:** inspect for adjacent actionable work, resolve it, verify it, and
  carry it through the normal PR/review/monitoring lifecycle.
- **Don't:** stop at a narrow literal reading that leaves the requested outcome
  only partially achieved.

## Status requests do not make issues report-only

Treat a request for status as a request to inspect live state and finish every
safe, in-scope, concrete action that inspection reveals. A report is the recap
after the work, not a substitute for it. When an issue cannot be fixed
directly, carry it forward with an actual next action. **Every issue noticed,
however small or outside the current task's scope, must at minimum be filed in
the owning GitHub, GitLab, or equivalent tracker.** File it before reporting
it; use the correct private tracker and redact sensitive details when needed.

- **Do:** fix an actionable CI defect, review finding, or configuration gap
  before reporting it as status; revalidate and continue the sweep.
- **Do:** turn an issue outside current authority into a filed/routed blocker,
  not an unowned observation.
- **Do:** file every noticed issue in its owning tracker, even when it is
  trivial, already fixed locally, or outside the active task.
- **Don't:** interpret "status" as report-only after discovering a concrete,
  safe, in-scope repair.
- **Don't:** end with "this failed" or "this needs a fix" when the fix is
  available to perform in the same turn.
- **Don't:** leave a noticed issue as chat prose because it seems too small or
  too far outside the current scope to track.

## Flag good moments to `/clear` in long-running sessions

@shared/workflow/flag-session-boundaries.md

Proactively flag a good stopping point with the `⚠️ **FLAG** ---` tag.
This could be a checkpointed or wrapped multi-step task, a PR merged with no other in-flight work on this conversation, or an open question answered with nothing pending.
Place the tag at the natural end of that turn's recap (or immediately before a `wrap-up` report) rather than mid-task.
A clean stopping point requires that something actually finished, and the fragment's disqualifier list cannot tell you whether anything did --- so name the thing that finished, and read a turn that only explored as having completed nothing however few blockers it trips.
Hold the flag while any PR this session opened or pushed to is still unmerged, per the bright line the fragment states in full; run `wrap-up`'s state sweep first rather than trusting memory, since a bot-opened PR or a leftover branch never entered the conversation.
Default to archive-and-start-new over a bare `/clear` whenever the session might be worth revisiting, and to `/compact` when the next work continues the same loose thread; the fragment covers each option's tradeoff and the same menu applied at the moment of opening a *new* PR, not only at a stopping point.

## Flag good moments to run `compress-session`, too

The mid-task counterpart is covered in the fragment above.
Don't wait for automatic compaction to guess what matters.
Flag it yourself (using the same `⚠️ **FLAG** ---` tag) once a session has grown large with a live task still in flight.
This applies when there are many tool calls, long tool outputs no longer needed, or a session is already through one auto-compaction.
Use `/clear`'s menu when there is nothing left to carry forward; use `compress-session` when there is.

## Actively manage quota usage: models, compaction, and workflow structure

Treat quota as something to manage continuously through a session, not only at a wrap-up or fan-out moment.
Three levers; when any applies, act on it without waiting to be asked.

**Model tier.**
For dispatched work (`Agent` calls, `Workflow` `agent()` calls), route model and effort per [`when-to-orchestrate`](shared/workflow/when-to-orchestrate.md)'s "Route each agent's model/effort" section.
Cheap tier for mechanical, bounded work; inherit or escalate only for judgment-heavy work.
Don't default every dispatched call to the conductor's own tier out of caution.

The conductor's own tier cannot be switched from inside the conversation --- it's client-side only (`memories/preferences.md`).
So the lever there is to **recommend** a change rather than make one.
When the current tier is clearly underpowered for the task ahead, say so and suggest escalating via `/model` or `select-model`.
When a long stretch of ahead-of-time-known mechanical work doesn't need the current tier, say so and prefer delegating it instead.
That means a cheaper-tier subagent, or a separately-billed agent CLI before spending this session's own quota, rather than burning the conductor's tier on it.
Active delegation budgets include `codex` (ChatGPT plan, operationalized by
`delegate-to-codex`), `opencode` (OpenCode Go subscription, free models via
Zen, local Ollama, operationalized by `delegate-to-opencode`), `agy` CLI
(headless dispatch available since the 2026-08-25 clarification), and
OpenRouter (prepaid credit balance for frontier/stealth previews).
`agy` (Google Antigravity)'s **API** route was permanently retired for
dispatched work (user directive, 2026-08-20, ai-config#1776).
Only that route is out --- the `agy --print` CLI and the interactive
subscription/extension are unaffected and not at quota.
`memories/delegation.md` carries the rule, the usage-window semantics
across `opencode`, `codex`, and `agy`, and the prepaid-balance details.
Ground the recommendation in `assess-model-fit`/`select-model` rather than a guess.

**Compaction.**
Already covered by the two sections above --- the `/clear` flag for a clean stopping point, and the `compress-session` flag for mid-task bloat.
Add quota/usage pressure itself as a trigger for both, distinct from context size alone.
The agent has no direct view into it, though --- the usage bar lives in the client's UI, not in the conversation (`memories/preferences.md`).
So key this off what's actually visible: the user naming or showing usage pressure, or --- inside a `Workflow` run with a stated token target --- `budget.spent()`/`budget.remaining()`.
Either is reason enough to compress or recommend a lighter model, on the same terms those sections already set out.

**Workflow structure.**
[`restructure-for-efficiency`](shared/workflow/restructure-for-efficiency.md)

The two levers above spend less on the work **as shaped**, and their saving expires with the session.
This one changes the shape, so it pays every future session --- and it is the one that never announces itself, because following an expensive procedure correctly reads as compliance, and pulling either lever above reads as having managed quota.
So ask separately what a procedure costs *by construction*: always-loaded content only some sessions read, a judgment made twice that wants an instrument, a serial loop the base outruns, an enumerated brief that should have been a query, work at this tier a free CLI could do.
The deliverable is a change to the corpus --- fixed in stride when small, filed with its measurement when not, per `report-mistakes-proactively` --- never a quieter run of the same procedure.
`python3 scripts/check-context-closure.py` is the built instrument for the always-loaded pool.
Its budget is advisory by design, so read an over-budget line as the prompt it is.
Two boundaries.
Efficiency never outranks correctness, so no saving is bought with a skipped check.
And the restructuring goes in its own issue or PR rather than happening inside whatever task noticed it.

Human steps are in scope too --- a merge method, a batching habit, a review-request convention each shape the procedure and each has a price.
Naming one and stopping there is `no-empty-promises` pointed outward, so every suggestion about human behaviour ships a mechanism in the same reply: a written rule at minimum, then a visible marker at the moment of the action, then a guard, then a setting that removes the option.
Pick the rung from the cost of the mistake rather than the strength of the opinion, and leave the decision with the user, per `flag-practice-slippage`.

- **Do:** ask what a procedure costs by construction, separately from what this run costs.
- **Do:** ship a mechanism in the same reply that names a human behaviour change.
- **Don't:** read a pulled lever as having answered the structural question.
- **Don't:** name a behaviour change with nothing behind it.

When several levers genuinely apply at once, do the self-directed ones first.
Compress, compact, or file the structural finding before asking the user to act on a model change.
Only the model change costs them a step.

## Keep a running on-disk session lab notebook

Maintain a "lab notebook" for each session — a dated, append-only file written to *as work happens*, not only when pausing — so that if the session is interrupted with no clean exit (compaction, a forced `/clear`, a crash, a SLURM walltime death), the trail is already on disk and a later session (or I) can pick it up.
The whole point is surviving an interruption that never gives you a clean stop, so the file must live on disk and be updated frequently, not held in context and flushed at the end.

**Where.** In the session's project auto-memory directory, as a `session-YYYY-MM-DD[-slug].md` file, with a one-line pointer added to that directory's `MEMORY.md` like any other memory.
One notebook per session; start it near session start and keep appending.

**Cadence — frequently, and to disk right away.** Append a short, timestamped entry at each state change worth resuming from: a task or subtask started, a decision made or a question I answered, a PR/issue opened, a branch cut, a job launched (SLURM/background/CI, with its id), a blocker hit, a checkpoint reached.
Not every tool call — that's noise — but every step whose loss would cost real reconstruction.

**What each entry carries.** Enough for a cold reader to resume without this conversation: what we're doing and why, what's done versus in flight (branches, open PRs/issues, running jobs and their ids), open questions and decisions, and the next concrete step.

**Relationship to the pause-time and context conventions.** The notebook is the *running recorder*; the others are point-in-time:

- `handoff` writes a single snapshot *when you pause cleanly* — the notebook is its always-current substrate, so a handoff can finalize or point at the notebook instead of rebuilding state from scratch.
- `compress-session` distills the *conversation context* to survive compaction — the notebook is a durable on-disk trail, not a context-window optimization.
- The `/clear` flag above is about *choosing* a clean stop — the notebook is insurance for the stops you don't choose.

Fold a finished session's notebook into durable memory (or prune it) during UMS once its content is captured elsewhere, so the memory directory doesn't accumulate stale logs.

## Keep ai-config and repo checkouts fresh

[`shared/workflow/keep-checkouts-fresh.md`](shared/workflow/keep-checkouts-fresh.md)

Four freshness checks to run each session: the ai-config checkout itself (on `main`, pulled --ff-only, with a safe recovery path for a diverged/orphaned local `main`), the consumer install (Claude Code and Cursor load this repo as a native plugin that auto-updates, so confirm the plugin is enabled and not doubled --- any leftover `~/.claude` copies of `shared/`, `hooks/`, or `memories/` predate the symlink-install removal and want a content diff, with `install-hooks.py` answering only the registration half), the working repo's own `main` checkout, and (where a consumer repo vendors ai-config as a git submodule) the `.ai-config` pin.
The fragment above carries the mechanics, the failure modes each check catches, and the case records.

## Timestamp recaps in local time

When printing a status recap or summary, include a timestamp in the user's local time zone (Pacific Time, `America/Los_Angeles` — get it from `TZ=America/Los_Angeles date "+%Y-%m-%d %H:%M %Z"`; the explicit `TZ` enforces PT on a machine set to any other zone).
This makes "as of when" unambiguous when the user reads the recap later.
Each reading expires immediately: run the command fresh for every recap rather than extrapolating elapsed time from a prior reading.
A single honest measurement earlier in the session is what most easily licenses an invented timestamp later, because the memory of having consulted the clock obscures that the measurement has expired.

**Check the `%Z` in the output.** On Windows Git Bash the `TZ` override silently falls back to GMT (any IANA zone name does), so the command above prints GMT, not PT.
If the suffix isn't PDT/PST, fall back to plain `date` when the machine's system zone is already Pacific.
Otherwise use PowerShell: `[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::UtcNow, 'Pacific Standard Time')`.
Note the output format differs from the bash command — it's a raw `DateTime` with no timezone-abbreviation field, so format it yourself if you need the `PDT`/`PST` suffix or a compact form.

## State the actual time when reporting a scheduled check-in

When telling the user I've scheduled a wakeup or check-in (`ScheduleWakeup`, or an equivalent poll-later mechanism), state the clock time it fires at, not just the relative delay or a bare "I scheduled a check-in."
The tool result already returns a clock time (e.g. "Next wakeup scheduled for 08:22:00") — surface that time in the chat reply instead of dropping it, converting to Pacific local time per the "Timestamp recaps in local time" section above if the returned time is in a different zone.
"Scheduled a check-in to continue monitoring both" leaves the user unable to tell whether that's one minute away or twenty; "I'll check back at 08:22 PT (~4 min)" does not.

## Bare keyword directives

Two families of slash skill read as directives when I write them **without** the leading slash: the **queue commands** that amend the task list, and the **judgment grants** that hand a decision back to you.

### Queue commands

I maintain a family of slash skills for managing the task queue and amending requests: `/also`, `/first`, `/next`, `/before`, `/last`, `/and`, `/remember`, `/always`, and `/cascade`.
When I write one of these keywords **without the leading slash** as a directive — e.g. "also fix the test", "remember that ...", "always link PRs in tables", "and bold it", "next, run the spellcheck", "first, revert that" — interpret it using the corresponding skill's semantics rather than as ordinary prose. (`/remember` and `/always` both route to the `memorize` skill; "cascade" means merge stacked PRs' base branches into the PRs stacked on top of them — including main into unstacked PRs — never the PRs into main; see the `cascade` skill.)
When the word is genuinely just part of a sentence (ambiguous), fall back to the plain reading.

### Judgment grants

The same bare-keyword reading applies to the judgment-grant keywords, which are not queue commands and differ from each other in scope.
`daytb` ("do as you think best", and its longhand `do-as-you-think-best`) hands back **one** decision: choose what you would have recommended, act, and report the choice in the past tense -- it expires with that task.
`away` is the session-scoped version, presuming I am not there to answer at all, and `back` revokes it.
`mwc` is the separate grant covering merge authority, which none of the others extend to.
Read a bare "do as you think best" as `daytb`, not as `away` -- the session-wide reading suspends clarifying questions long after I expected them back.
`dmmhyh` ("don't make me hold your hand") is a correction rather than a proactive grant: it fires when I'm asking for more guidance than the moment calls for.
It resolves the pending item like `daytb`, raises the decide-vs-ask threshold for the rest of the session like `away`'s judgment-call test, and -- unlike either -- writes the correction down as a memory entry so it doesn't have to be re-taught next session.
See [`dmmhyh`](skills/dmmhyh/SKILL.md).

## Link PRs in tables

When listing PRs in a table (or anywhere they could be clickable), make each PR number a markdown link to the PR URL — `[#237](https://github.com/<owner>/<repo>/pull/237)`.
The plain text form forces the user to copy/paste; the linked form lets them open the PR in one click.

## Tag chat output by category so long recaps stay scannable

Recaps get long across many parallel tracks, so tag categories of output with a stable marker and let the eye jump straight to what needs the user's attention.
Terminal markdown can't force text color, so the emoji plus the `===` frame plus the bold label *is* the signal.
Readers skim past a question or a flag buried mid-paragraph; a marked, set-apart block is harder to miss.

Reserve a **`===` box** for the output a user is waiting on — something they must respond to (a question, an offer, a blocker) or the headline answer they asked for — and use a lighter **emoji-prefix** (bold label, no box) for informational categories they can skim.
Boxing everything defeats the purpose, so keep the box meaningful.

Boxed (a `===` line above and below the labeled block):

- ❓ **QUESTION** — need the user's input. For a real either/or, prefer the AskUserQuestion picker over a boxed question. When a question is posed inline in chat prose rather than through a box, still set it apart — its own paragraph (blank line before and after, since a bare newline collapses back into the surrounding paragraph), in bold.
- 💡 **OFFER** — optional work I can do if they want it.
- 🛑 **BLOCKER** — stopped; need their call.
- ✅ **ANSWER** — the headline answer to a question they asked (put nuance below the box).
- 🧭 **RECOMMENDATION** --- the course of action I think they should take,
  when the decision is theirs.
  Distinct from the two categories it is most easily confused with:
  an ✅ **ANSWER** reports what is true,
  and a 💡 **OFFER** proposes work I would do.
  A recommendation is a judgment about what *they* should do,
  including about things I will not be doing ---
  which PR to merge first, which option to decline, whether to stop.
  Lead the box with the action and put the reasoning below it,
  so the box holds the call rather than the argument for it.
  It boxes because it feeds a decision they are waiting to make;
  an opinion nobody was waiting on is a 📊 **UPDATE** with a view in it,
  and stays unboxed.
  - **Do:** box the recommendation, lead with the action,
    keep the reasoning under the box.
  - **Don't:** bury it in a closing paragraph,
    or fold it into an ✅ **ANSWER** box
    so a factual claim and a judgment read as one thing.
- 🔀 **MERGE ORDER** --- several PRs are ready,
  and merging them in the wrong order would produce a wrong result.
  The one category labeled with a markdown **heading** (`### 🔀 MERGE ORDER`) rather than bold text,
  since a heading is the only "large font" lever a terminal has.
  List the PRs in the order to merge, each linked per "Link PRs in tables" above,
  naming what each one's position depends on.
  The PR-side and draft-gating surfaces live in the "Surface merge-order constraints" section.

Prefixed, no box (informational, frequent):

- 📊 **UPDATE** — status or progress.
- ⚠️ **FLAG** --- non-blocking heads-up or risk.
- ✔️ **DONE** — a completed action.
- 🟢 **ALL CLEAR** — nothing needs the user right now; work continues in the background. The recap's standing sign-off.

Keep the markers stable so they become muscle memory.
The set-apart ❓ **QUESTION** format also gives the `prompt-me` / `prompt-me-all` skills a reliable signal to key off when they sweep the transcript for unanswered questions later.
The user may tune the emoji set; the full taxonomy and rationale live in `memories/preferences.md`.

## Never close a reply by offering to do work you are already allowed to do

[shared/workflow/no-cop-out-offers.md](shared/workflow/no-cop-out-offers.md)

"Say the word and I'll push."
"Want me to kick off the re-run?"
Each reads as courtesy and delivers nothing: the work does not happen, you
spend a turn, and the reply reported nothing.

The phrase is not the defect --- asking before a merge, a force-push, or a
deletion is correct, and those sentences look identical.
What decides it is whether the action was **already authorized**, by a
standing instruction, a `daytb`/`away` grant, or your having asked for the
outcome earlier.
When it was, the offer is avoidance wearing courtesy, and the cost is
asymmetric: an unwanted action is cheap to revert, while an unanswered offer
leaves no branch, no PR, no issue --- nothing another session could find.

`hooks/flag-cop-out-offer.py` is the instrument.
It warns rather than blocks, because authorization is not lexically decidable.

- **Do:** perform the authorized action and report it in the past tense.
- **Do:** ask plainly, with no offer wording, when the action is genuinely
  unauthorized and destructive or outward-facing.
- **Don't:** attach a user-conditional to work a standing instruction covers.
- **Don't:** close a status recap with an offer --- that is where this hides.

## Always produce a reply --- never end a turn silently

Every turn ends with user-visible prose.
The section above governs what a reply should contain; this one governs that it
has to exist at all.

The user cannot see tool calls, so a turn carrying work and no prose is
indistinguishable from a turn where nothing happened.
That makes silence worse than a terse reply: a short line reports a result,
while no line reports nothing and reads as a stall.

Four moments produce the empty turn, and none of them feels like withholding:

- **After an interruption.**
  The work completed, the reporting did not, and resuming feels like the
  request is already satisfied.
  Report what finished, in the past tense, rather than assuming the tool
  results were visible.
- **On resuming from a context-window summary.**
  The sharpest of the four, and the one that produced every observed
  recurrence.
  A summary reads like a report --- it is written in the past tense, it
  enumerates what was done, and it is the first thing in the new window ---
  so the work it describes feels already reported to the user.
  It was not.
  The user saw the work itself and never saw the summary, which exists for
  you rather than for them.
- **A no-change background tick.**
  A scheduled check-in that finds nothing still gets one line.
  "Nothing changed" and "the loop died" are the same observation otherwise,
  and only one of them is fine.
- **A run of tool calls with no natural summary.**
  Say what they established, even when the answer is that nothing moved.

The harness sometimes instructs otherwise --- a PR-subscription wake asks for a
check-in to be re-armed "silently without messaging the user".
This preference wins.
Re-arm as instructed and still emit the one line.

- **Do:** end every turn with prose, however short.
- **Do:** report completed work after an interruption, since the user saw none
  of it.
- **Do:** give a no-change tick a single line that says so.
- **Do:** treat a context-window summary as material for you rather than as a
  report already delivered.
- **Don't:** reply `No response requested.`, or any equivalent placeholder that
  occupies the reply without carrying information.
- **Don't:** read a harness instruction to stay silent as overriding this.

(Directive from the user, 2026-08-16: "cai: I always want a response".
A dispatch was issued and verified, the turn was interrupted mid-tool-use, and
the resumed turn emitted `No response requested.` and nothing else.
The user had to ask "did you do it".
Dupe-checked at the time over `CLAUDE.md`, `shared/`, `memories/` and
`skills/`: `empty response`, `no response`, `null reply`, `always respond` and
`end the turn` each returned 0 hits, so the corpus governed a reply's contents
at length and never its existence.
`without messaging the user` also returned 0, which is how the conflicting
instruction was identified as the harness's own wake boilerplate rather than
ours.
Tracked as ai-config#1568.

**Third occurrence, 2026-08-17, recorded here rather than as a sibling entry**,
per "Record both the pattern and the anti-pattern" above and the
recurrence bullet in [`ums`](skills/ums/SKILL.md).
All three fell in one session, each on resuming after a context-window
summary, which is why that moment is now named in the list above --- the
original entry's three moments did not cover it, so the rule was loaded and
matched nothing.

That count meets
[`deterministic-tools`](shared/principles/deterministic-tools.md)'s
third-occurrence bar, and the condition is lexically decidable over one
artifact, so the rule now ships a guard: `hooks/no-placeholder-reply.py`
blocks a reply whose **whole** stripped message is a placeholder.
Whole-message anchoring rather than substring, because this corpus quotes the
banned string constantly --- this very paragraph does --- so a substring
matcher would block every reply that cites the rule it enforces.
The line it draws is between a claim about the **request**, which reports
nothing, and a claim about the **work**: `Nothing to report.` and
`No change.` are deliberately not matched, since a no-change tick is
behaviour this section requires.
Tracked as ai-config#1579.)

## Surface merge-order constraints

When two or more PRs are open and merging them in the wrong order would produce a wrong result,
say so where I'll act on it, not in ordinary prose I'll skim past.
Three surfaces, escalating in strength; use as many as the situation earns.

1. **In chat** --- the boxed `### 🔀 MERGE ORDER` marker above.
2. **On the PRs** --- lead each affected PR's body with a `> [!IMPORTANT]` alert
   naming that PR's position and its prerequisite,
   e.g. "Merge [#N](url) first --- this PR is stacked on its branch."
   Update or drop the alert once the prerequisite merges.
3. **Draft-gating** --- hold the dependent PR as a draft until its prerequisite merges,
   then mark it ready.
   GitHub won't merge a draft,
   so this makes the wrong action unavailable rather than merely discouraged.

Draft-gating is the last resort, not the default, because it costs something real:
converting a ready PR to draft **drops auto-merge and merge-queue membership**,
and a draft doesn't trigger the `@claude` review bot (see `shared/workflow/pr-on-claim.md`),
so drafting an unreviewed PR stalls its own ARDI loop.
Drive the PR to fully clean first, and draft-gate only if the prerequisite still hasn't merged.
Say in chat and on the PR that it's being held and why,
and un-draft promptly once the prerequisite lands.
A silent draft is never a substitute for stating the order.

This fires only when order changes the outcome:
a stacked PR whose base is another open PR,
a PR that would conflict or show a misleading diff if the other landed first,
a migration that must precede its consumer.
Two PRs touching disjoint files usually have no constraint,
and saying so plainly is the right answer, not an occasion for the marker.
But "disjoint" is a claim about their file *sets*, so derive both sets and check the intersection before asserting it, rather than recalling what each PR is "about" --- which is `metacognitive-monitoring.md`'s scope-claim failure (check the population, don't recall it).
`python3 scripts/pr-overlap.py -R <owner>/<repo>` is that derivation, sweeping every pair of open PRs at once and reporting how many pairs it examined alongside how many collided.
Fall back to `gh pr diff <N> --name-only` per PR only where the script cannot run, noting that the hand method misses a rename, whose new path is all the diff reports.
A follow-up PR that extends into a `shared/` (or any) file a prior PR also edited is a common collision, and the two conflict at merge time.
**An empty intersection settles the *collision* cases above and cannot see the *dependency* ones.**
A migration and its consumer, or a PR whose prose cites content another PR adds, are ordering constraints whose file sets never overlap ---
so a derived intersection of zero is evidence about conflicts, not a proof that either order is safe.
Ask separately whether one PR asserts something the other makes true.
For a citation the better fix is to dissolve the dependency rather than sequence it,
by phrasing it as a conditional that is accurate either way ---
see [`challenge-ambiguous-terminology`](shared/workflow/challenge-ambiguous-terminology.md)'s cross-repo citation trap, which applies to a same-repo sibling PR unchanged.
The rationale behind each surface lives in `memories/preferences.md`,
alongside the rest of the taxonomy.

## Present decisions one at a time

When more than one decision needs my input, go through them one at a time:
pose the single most pressing question, wait for my answer, then pose the next.
Don't batch several decisions into one message or one multi-question `AskUserQuestion` call.

Two reasons.
The answer to the first question often changes or moots the later ones, so a batch makes me answer against stale premises.
And a wall of questions invites a partial reply that leaves the rest silently unanswered — the exact failure mode `prompt-me` / `prompt-me-all` exist to recover from.

Mechanics:

- Rank by how blocking each decision is, most pressing first (the same ranking `prompt-me` uses), and pose only the top one — via a single-question `AskUserQuestion` call for a real either/or, or one boxed ❓ **QUESTION** otherwise.
- Say how many more are queued behind it ("2 more decisions after this one"), so the backlog is visible without being posed.
- Fold each answer into the framing of the next question, and silently drop any queued question the answer mooted.
- Keep working on whatever the pending decision doesn't block while waiting.

This changes how decisions are *posed*, not whether to ask at all: `research-before-asking` still gates each question, and an `away` grant still means don't block on questions — resolve them by judgment, or skip-and-note, per that skill's scope.
And it yields to an explicit request for the full backlog — `prompt-me-all` / "ask me everything at once" is the user opting into a batch view.

## Title Claude sessions with the PR/issue number

Name each Claude Code session (the title shown in the web/app session sidebar) `#NNN brief description` — the number of the PR or issue the session is working, then a short description.
Don't prefix it with "PR" or "Issue"; just the bare `#NNN`.
So `#316 session title convention`, not `PR #316 session title convention` or `PR session title convention`.

## Re-check for latest review findings before reporting PR status

**Before** reporting status on a PR (especially "clean" / "ready to merge"), re-read the **most recent** review comment on the PR.
The same fetch applies to any other question about that live PR
("why didn't you wait", "did you fix it", "why haven't you responded").
Don't answer from chat context alone.
Don't trust an earlier "verdict" you've cached — a new review may have been posted since (by the @claude bot, by a human, or by a re-trigger), and that newer review may contain findings the old one missed.

Specifically: when scanning checks (`gh pr checks`) shows green or "no failures", that's about CI state, **not** review verdict.
Always pull the latest review comment and parse it for any "Findings", "Issues", "Remaining" sections before declaring a PR ready.

**Read every round since the one you last processed, not only the newest.**
Several rounds can land during a monitoring gap, and "read the latest" alone fails exactly then.
A test-only push between two substantive rounds gets a fresh verdict that says nothing about the earlier round's unaddressed findings, so the latest comment reads clean while older findings sit open
(measured 2026-08-24 on sparta#1375 --- three rounds landed in one gap, and acting on the newest alone would have reported clean over an open regression finding).
Diff the round list against what you last handled: fetch all `**Claude finished` comments, note each `Reviewed commit:` SHA, and treat any round newer than your last processed one as unread input.

**Filter on the body marker, not on an author login.**
The login a review posts under varies by repo and by run --- `claude`, `claude[bot]`, and `github-actions[bot]` have each been observed carrying a real, complete verdict --- so a login-filtered query silently returns the *previous* round's comment and reads exactly like "no new review yet".
That is a false negative on the one question this section exists to answer, and nothing in the output announces it.
Completed runs start the body with `**Claude finished`, so match that instead:

```bash
gh api repos/<owner>/<repo>/issues/<N>/comments --paginate \
  | jq -s '[.[][] | select(.body | test("\\*\\*Claude finished|### Verdict"))] | last | .body'
```

`memories/gh-cli.md` carries the full statement, including the placeholder-wording trap when polling a run still in flight.

**Also check formal GitHub reviews, not just issue-style comments — a human's `CHANGES_REQUESTED` can be invisible to a comments-only scan.** A review submitted via GitHub's review UI (as opposed to a plain PR comment) shows up in `gh pr view N --json reviews`, and its top-level `body` is frequently **empty** — the actual finding lives entirely in a per-line inline comment, which only appears via `gh api repos/<owner>/<repo>/pulls/N/comments` (a different endpoint from issue comments). Checking `--json comments` alone can miss the review's existence entirely. Before declaring a PR ready, also run:
```
gh pr view N --json reviews --jq '.reviews[] | select(.state == "CHANGES_REQUESTED") | "\(.author.login) \(.submittedAt)"'
gh api repos/<owner>/<repo>/pulls/N/comments --jq '.[] | "\(.path):\(.line // .original_line // "?") \(.user.login) \(.body)"'
```
A `CHANGES_REQUESTED` state is blocking regardless of whether an automated re-review later says "Ready for merge" — that bot verdict doesn't clear a human's own review state, which only the human (or an explicit dismissal) can resolve.

(A specific case of the standing **never assume; always verify** rule in `memories/preferences.md` — confirm the verdict with a fresh query, don't recall it.)

## Post in-chat feedback to the PR

When the user gives feedback, corrections, or guidance in the CLI or chat while working a PR, paraphrase it and post it as a PR comment:

```
gh pr comment <N> --body "<paraphrase>

_Posted by Claude Code (AI agent) --- not written by a human._"
```

One to three sentences is enough.
The trailing marker is required, per the section above: this comment paraphrases the user in the user's own voice under the user's own login, which is the shape most easily read as their own writing.
Don't quote verbatim — paraphrase so it reads naturally in the PR thread.
Skip trivial acknowledgments or conversational exchanges with nothing to act on.

This makes context visible to future @claude sessions, other reviewers, and contributors who only see the PR thread.

## Subscribe to PR updates automatically

When opening or taking over a PR in any repo, subscribe/watch that PR's activity immediately using the available GitHub notification/subscription mechanism. If the current session's tools cannot subscribe, say so explicitly and fall back to active polling for reviews, comments, and checks during the session.

## Monitor every pushed PR head to completion

Whenever ending a turn while waiting for CI completion or AI reviews after pushing to a PR in any repository, launch a `schedule` timer (e.g. 120s) to actively monitor that exact head commit.
If no review has arrived when the timer expires, verify whether review workflow runs are still in progress in CI (`gh run list` / `gh pr view --json statusCheckRollup`). If the reviewer failed, was canceled, skipped with no replacement, or produced a stub review with no stated verdict, invoke self-review fallback per `shared/workflow/self-review-fallback.md`; otherwise fix any dispatch or workflow failures discovered along the way and schedule another timer to maintain continuous monitoring until a review lands, self-review fallback triggers, or CI completes.
Keep polling and address actionable failures or findings until all workflows and check runs are complete and passing (success or skipped), the current-head review is clean, and no review threads remain unresolved.
Once that commit is fully clean and green, stop the **intensive head poll** for it; don't restart that poll for the same commit unless something regresses.
A later push creates a new head commit and starts a new monitoring cycle automatically.

**Ending the head poll does not end the PR watch.**
The two run at different frequencies and answer different questions, and only the first one is finished when a head goes green:

- The **head poll** asks "is this commit done?" and terminates when it is.
- The **PR watch** above ("Subscribe to PR updates automatically") asks "is this PR still mergeable and still clean?" and runs until the PR merges or closes.

That distinction is load-bearing because a clean head can regress with **no push of yours at all**.
The base branch advancing is enough: the PR goes `CONFLICTING`, or `main` catches up to an R package's `DESCRIPTION` version, or a sibling PR merges a colliding append --- each turning a green, review-clean head red while nothing about that commit changed.
`shared/workflow/fully-clean.md` says the same thing about verdicts: a clean CI run and a clean review are a snapshot, not a standing guarantee.

So keep checking mergeability and check state at the lower PR-watch frequency after the head poll ends, and **restart the intensive poll if state regresses** --- a new conflict, a check flipping red, a fresh review comment.
Re-derive it from a live query rather than trusting the earlier verdict.

## Claim a GitHub PR/issue before working on it

[`shared/workflow/claim-pr.md`](shared/workflow/claim-pr.md)

The `claim-pr` skill operationalizes this (the exact claim wording, when it applies, and the closing/unclaim comment).

## Every comment you post to a forge says an agent posted it

[shared/workflow/disclose-agent-authorship.md](shared/workflow/disclose-agent-authorship.md)

A comment posted through `gh`/`glab` under the account holder's credentials carries **their** login and reads as `type: User`, so nothing in the API distinguishes it from a comment they typed --- `memories/gh-cli.md` records auditors mistaking exactly that.
The forge cannot say it, so the body must: end every agent-posted comment with

```
_Posted by Claude Code (AI agent) --- not written by a human._
```

The marker deliberately avoids the robot emoji, which `scripts/check-pr-fully-clean.py` matches as a `REVIEW_BODY_MARKERS` entry --- a disclosed claim comment would otherwise scan as a finding-free **review**.
The fragment carries the rest: the two exemptions, the comment-bodies-only scope, and the queries that verify a marker or a bot identity.

- **Do:** append the marker to every claim, release, status, reply, and self-review comment, including ones whose prose already names the session.
- **Don't:** use the robot emoji in it, or put it in a commit message, a title, an issue body, or a PR body.

## Read a repo's canonical contributor doc before starting work, not just before pushing

[shared/workflow/read-canonical-doc-before-starting.md](shared/workflow/read-canonical-doc-before-starting.md)

When a short `CLAUDE.md` names a fuller document as the actual authority --- `.github/copilot-instructions.md`, `CONTRIBUTING.md`, a linked style guide --- read that document before the first edit, and front-load its pre-PR requirements into the first commit rather than discovering them via a red CI check.

## Open a PR immediately after claiming an issue

[`shared/workflow/pr-on-claim.md`](shared/workflow/pr-on-claim.md)

The strong form of the claim: after claiming an issue you're about to work, open the PR right away — before implementing — from an empty commit, kept as a draft until the implementation lands.
An open PR is the visible in-flight signal other sessions check, so opening it up front stops parallel duplicates.
The `gi`, `gii`, `gip`, and `st` skills operationalize this.

## Every self-review is an adversarial review by a separate subagent

[shared/workflow/adversarial-self-review.md](shared/workflow/adversarial-self-review.md)

Whenever reviewing your own work is called for --- before a push, as the fallback when the external reviewer is down, or the project-conventions pass --- dispatch it to the [`adversarial-reviewer`](.claude/agents/adversarial-reviewer.md) subagent (foreground, read-only) against `git diff origin/<default-branch>...HEAD`, and treat its findings as findings.
The authoring session cannot do it inline: it knows what the change was *meant* to say, so it reads the diff and recovers the intent, which is confirmation rather than review.
Brief the reviewer with the diff and the standards, never with the rationale for the change --- handing over your account of it is what makes the reviewer agree with you.
`hooks/no-push-without-self-review.py` gates the pre-push case on Claude Code.
The fragment covers the rest, including why a same-vendor subagent buys independence of *intent* and not of blind spot.

## Open a PR for every pushed feature branch

After pushing a feature branch, create its PR
unless an existing PR already represents that branch
or the user explicitly says not to.
Don't treat a successful push as the handoff:
the PR is the reviewable unit and the durable visible record of the work.

## Use the existing PR branch, not the harness-specified branch

[`shared/workflow/use-existing-pr-branch.md`](shared/workflow/use-existing-pr-branch.md)

## Skills that call gh/glab: fall back to tool-mappings.md in remote sessions

Many skills under `skills/` name concrete `gh`/`glab` CLI commands (e.g. `gh pr comment`, `gh issue create`).
In a remote/web session where `gh`/`glab` isn't on `PATH`, substitute the equivalent GitHub MCP tool from [`tool-mappings.md`](tool-mappings.md) instead of failing or improvising.
That registry is the single source of truth for the gh/glab-to-MCP mapping in this repo --- don't inline a separate translation table into individual skills; point to `tool-mappings.md` and let it stay the one place to update. (GitLab operations have no MCP equivalent listed there; `glab` stays CLI-only.)

## Install and use MCP servers proactively

[shared/workflow/use-mcp-servers.md](shared/workflow/use-mcp-servers.md)

The section above is about substituting an MCP tool for a CLI command when the CLI is missing.
This one is the other direction: when a server would help, install and register it rather than waiting to be asked --- including locally, where `tool-mappings.md`'s per-model table describes the default rather than a limit.
Covers reading `claude mcp list` for transport rather than name (a plugin's remote server can shadow the local one you meant), 400-versus-401 on an uninterpolated credential, supplying tokens by launch wrapper instead of storing them, opt-in toolsets whose selection *replaces* the default, and verifying by a real call rather than by the tool listing.
Its last section generalizes past MCP: when a standing rule names a mechanism this session doesn't have, look for the local equivalent instead of silently degrading to a worse fallback.

## File an issue before starting a new task

@shared/workflow/issue-first.md

The `st` (Start Task) skill operationalizes this; `gi` (Grab Issue) is the path when the issue already exists.

Its last section is the mirror, and it covers requests that arrive rather than work you go looking for: a request the user makes mid-flight may be deferred on your own judgment when it would grow the change past what it set out to do, provided the deferred item is filed as an issue in the same reply and the reply says what was deferred and why.
The grant is latitude rather than an instruction, and the tracking issue is the whole of what licenses it --- an untracked deferral is a dropped request wearing the vocabulary of scope discipline.
Read the fragment's boundary with `dont-incur-technical-debt` before invoking it, since a defect inside your own diff stays yours to fix now.

## Issue or discussion? Pick the venue by best practice, not by precedent

[shared/workflow/choose-issue-or-discussion.md](shared/workflow/choose-issue-or-discussion.md)

The companion to issue-first above: that rule settles *whether* something is tracked before work starts, this one settles *where* it lands.
Actionable work is an issue.
An open-ended policy question whose deliverable is a decision, and which has a real do-nothing option, is a discussion --- in an answerable category (`Q&A`) so the resolution can be marked as the answer.
Its second half is the general principle: best practice outranks repo precedent when choosing venue or method, and "the board is unused, so nobody would find it there" is circular reasoning that can never permit anyone to start using it.

## If you see something, say something — file an issue for every noticed mistake

[shared/workflow/report-mistakes-proactively.md](shared/workflow/report-mistakes-proactively.md)

The proactive counterpart to issue-first above: when a mistake shows up in any medium (code, prose, AI-config files, `gha` workflows, snapshot and other generated files, or anything else),
even if it is out of scope for the current task, flag it in chat (`⚠️ **FLAG** ---`),
and file a tracking issue immediately, in a repo we administrate.
Never file autonomously in an external repo; the upstream-issues ladder governs that case.
The `defer-issue` skill covers the user-initiated version of this; this rule is self-initiated.

## Say when a practice is slipping, not only when an artifact is wrong

[shared/workflow/flag-practice-slippage.md](shared/workflow/flag-practice-slippage.md)

The counterpart to the rule above, for *practice* rather than for artifacts: that one governs a mistake in a thing and its deliverable is a filed issue, this one governs how the work is being done and its deliverable is one sentence at the moment it is actionable.
The outward direction is already covered by the review fragments and needs no restatement.
The two that need stating are inward, unprompted and outside any review loop, and **upward** --- telling me when *my* practice is slipping, which will not happen by default because deference costs nothing at the moment it is chosen and reads as politeness.
Name the specific practice and gap, cite the rule or label the opinion as an opinion, say it before the action rather than in the retrospective, and say it once --- the decision stays mine, and this is not a licence to relitigate it.

## Learn from every reviewer finding you accept, not only from your own admissions

[shared/workflow/learn-from-review-findings.md](shared/workflow/learn-from-review-findings.md)

The external-correction counterpart to the UMS triggers at the top of this file: those fire on a first-person admission ("I was wrong"), which is why `hooks/remind-ums-after-error.py` deliberately excludes correcting someone else.
Agreeing with a reviewer is the commoner case and the one that machinery misses --- you admit nothing, you accept a finding --- so an accepted finding is a first-push miss to record and, where a decidable condition exists, to algorithmatize, per the goal that every PR gets a clean review on the first push.
`hooks/remind-learn-from-review.py` is that trigger;
like its sibling it only ever adds context and never blocks.
It is registered in `hooks/hooks.json`,
which binds it on the plugin path
and is what `install-hooks.py --fix` binds on the non-plugin path.

## Tracking issues in upstream repos

[shared/workflow/upstream-issues.md](shared/workflow/upstream-issues.md)

The `sup` / `send-upstream` skill operationalizes steps 1--2 (the PR path, including fork-if-needed, and the issue path) and the link-back.
Step 3 (own-repo fallback) is not covered by `sup`; use `gh issue create` in the current repo and ask the user to transfer it.

## Wrap up a merged PR with UMS

When a PR/MR you were working on **merges**, run the `post-merge` skill: verify the merge actually landed, tidy the local branch (checkout `main`, pull, `git branch -d`), confirm any deferred items have follow-up issues, then run **UMS** to capture what the PR's review lifecycle taught — recurring review findings, corrections, and guidance given along the way.
A merge is the natural checkpoint to bank lessons before the context is lost.

This is not the *first* checkpoint, though, and it should rarely be the one carrying the whole backlog.
Per "Run UMS proactively" above, the pass already ran when the review verdict came back clean, so `post-merge`'s UMS covers what the merge itself taught -- a conflict resolved on the way in, a check that only fires on `main`, a squash that reshaped the history.
Run it regardless: a short pass that finds nothing new is the expected outcome when the verdict-time pass did its job, not a reason to skip the step.

"merge it" / "merge this" / "merge the PR" as bare directives (no slash) trigger the `merge-it` skill: when the PR isn't merged yet, it merges the ready PR (squash by default) **then** chains straight into `post-merge` (tidy + UMS); when the PR is already merged it goes directly to `post-merge`.
Either way the post-merge wrap-up — including the UMS follow-up PR — runs **automatically, without asking**.
If the phrase is clearly part of ordinary prose rather than a standalone directive, treat it as such.

## What "fully clean" means

[`shared/workflow/fully-clean.md`](shared/workflow/fully-clean.md)

Escalate a deadlock via the `request-pr-review` skill (human reviewer `the repository owner`, or `gh pr edit <N> --add-reviewer <reviewer>`), and surface the open item to me.

## Always run ARDI on PRs you touch

[`shared/workflow/ardi.md`](shared/workflow/ardi.md)

The `ardi` / `iterate` skill family runs this loop. (See *What "fully clean" means* above; the mechanics for each step are in the sections around here.)

## Do the review yourself when the @claude workflow doesn't produce a verdict

[`shared/workflow/self-review-fallback.md`](shared/workflow/self-review-fallback.md)

When the `@claude` review workflow fails to produce a usable verdict --- quota-skipped, a stub review with no stated `### Verdict`, or no review workflow configured at all --- don't stall ARDI waiting for it: post a self-review at the same standard the bot would apply (including the prose fact-check, not just structural checks), request any other reachable reviewer in parallel, and keep driving to fully-clean.
A fallback self-review is easy to under-scrutinize precisely because it feels like a stopgap; the fragment names the specific gap (structure checked, fact-check skipped) and holds the fallback to the bot's own bar.

## Watch and ARDI every PR you touch --- don't ask first

"Touch" here means driving the branch: you opened it, were asked to iterate or take it to clean, or are pushing fixes.
A request to post a review and leave findings, with no request to edit, is not that kind of touch.

**Driving.**
The persistent-loop standing yes lives in `AGENTS.md` and applies to every agent.
This section is only the Claude-specific half: how this harness wakes, and how it must not double-trigger review.

When you open (or are handed) a PR/MR to drive, in any repo, subscribe to its activity and run the ARDI loop to clean **automatically** --- never ask "should I watch this?" or "should I iterate it?" first.
That answer is a standing yes across all PRs you are driving.
Subscribe with `subscribe_pr_activity` when that tool exists (provided by the GitHub MCP server in remote/web sessions), or babysit locally.
A subscription does not replace the persistent loop: PR-activity webhooks do not deliver CI success, new pushes, or merge / merge-conflict transitions (see [`memories/github-mcp-tools.md`](memories/github-mcp-tools.md)).
Claude's wake is a `/loop`, `send_later`, `CronCreate`, or schedule timer, per `AGENTS.md`.
Re-arm it periodically, since webhooks can't fill that gap.
Drive every review round to fully-clean.

This watch process never formally invokes the `ardi` skill, so read `skills/ardi/SKILL.md` step 6 for the re-request-review mechanics before pushing a fix: after a push, the push itself already triggers the review --- don't also post "@claude review again" in the same round.
On workflows with `concurrency: cancel-in-progress`, the two triggers race and cancel each other, leaving the latest commit's review canceled and `require-review` red for no code reason.
Only post the mention when a round pushed no code (all Rebut/Defer).

Surface to me only when an item is ambiguous, architecturally significant, or deadlocked (the escalation rule above still applies), or when the PR is clean.
Stop watching only when the PR merges or closes, or I tell you to back off.

**Review-only.**
Do not start ARDI, do not push fixes, and do not merge.
Leave the findings and stop unless asked to iterate.
A later request to iterate is a driving request.

(UCD-SERG/shigella#31, 2026-08-25.)

## Babysit PRs efficiently — batch pushes, trust CI's own reports, skip redundant lookups

[shared/workflow/efficient-pr-babysitting.md](shared/workflow/efficient-pr-babysitting.md)

A long babysitting session accumulates avoidable tool calls and CI runs otherwise:
trickled single-item pushes each re-trigger CI and race each other's reviews,
a local re-run can rediscover a gap CI's own comment already named,
and a pure re-post webhook event doesn't need fresh analysis.

## Address every in-scope review comment, even non-blockers

[`shared/workflow/address-every-comment.md`](shared/workflow/address-every-comment.md)

If you and the reviewer reach an impasse on a single item (your rebuttal didn't convince them and their re-raise didn't convince you), escalate that item to a **human reviewer** — request human review via the `request-pr-review` skill (or `gh pr edit <N> --add-reviewer <reviewer>`) and `@`-mention them with the impasse — for the final call rather than looping.

## Request review and drive every started PR to clean

Whenever starting or working on a Pull Request:
1. **Trigger AI review when done pushing**: In repositories where reviews do not auto-trigger, request an AI review (`@claude review` comment, or dispatch `claude-review.yml`) **after completing all code pushes** for the round, not when the PR is first opened and empty.
   In repos that automatically trigger review on PR events (`pull_request` synchronize, opened, ready_for_review), do NOT manually trigger a redundant review if an automated review is already running or queued.
2. **Drive to clean**: Run `ardi` / the review-and-iterate loop to ensure CI passes and all review findings are addressed until the PR reaches a clean verdict.
3. **Request human review only after AI approval or deadlock**: Per [`copilot-review-before-human.md`](shared/vendored/copilot-review-before-human.md), request human review (configured repo reviewers per `skills/request-pr-review/SKILL.md`) **only after** the AI review produces a clean/approved verdict, or if an impasse/deadlock occurs.

- **Do:** Trigger AI review (or let the automated PR review run) after completing code pushes, and request human review only after the AI review is clean/approved (or upon an impasse).
- **Don't:** Manually trigger a redundant `@claude review` comment when an automated review is already running or triggered by the push/ready event.
- **Don't:** Request human review when the PR is first opened empty, before code pushes are complete, or before the AI review has passed / produced a clean verdict.


## Check the remote immediately before every push

[`shared/workflow/check-before-pushing.md`](shared/workflow/check-before-pushing.md)

Take a fresh `git ls-remote` reading of the branch immediately before every `git push` --- not at the start of the round, not when you last synced, not when you opened the PR.
The branch you cut and whose PR you opened is the one you are *least* likely to check, because ownership makes the check read as ceremony rather than as a question with an unknown answer.
`claim-pr` records three ways it gains another agent's commits anyway (the `@claude` agent's `main`-sync, a second CLI session, a human), and every recovery procedure there runs *after* the collision.
An earlier fetch is a measurement of a moment that has passed, and it expires exactly the way a clock reading does.

`--force-with-lease` alone is not the safe form, which no site in this corpus previously said: the lease compares against your remote-tracking ref, so any background fetch silently satisfies it over the very commits it was protecting.
Always pair it with `--force-if-includes` (added in Git 2.30.0), and note that pairing `--force` *with* the lease is not a middle ground --- git documents `-f, --force` as one that "disables that check, the other safety checks in PUSH RULES below, and the checks in `--force-with-lease`".
A `stale info` refusal is not a reason to force either: `memories/git-branches.md` records that it means the remote branch is gone.
Query `gh pr list --state all --head <branch>` before a plain push.
MERGED means auto-delete, not a first publish: do not recreate
(see [`check-before-pushing`](shared/workflow/check-before-pushing.md)).
Otherwise a plain push is the fix.
`ALLOW_FORCE_PUSH=1` is an escape valve for a case the guard did not foresee, and using it means stating why.
`hooks/no-clobbering-push.py` is the mechanism: it refuses a bare force push, whose remedy costs one word, and only warns on a divergence, whose significance it cannot judge.

## Keep PR branches synced with main

[`shared/workflow/sync-with-main.md`](shared/workflow/sync-with-main.md)

(Another instance of **never assume; always verify** — `git fetch` to check main's actual position instead of assuming the branch is current.
The `sync-pr-branch` / `merge-main` skill runs this.)

## Batch merge and resolve, always

The section above is one branch against `main`.
When **several** open PRs need syncing or conflict resolution, do them together in one pass rather than chasing each one's conflict flag as it appears.
The batch pass is the default, not a recovery step for when serial chasing has already failed.

[`shared/workflow/batch-merge-and-resolve.md`](shared/workflow/batch-merge-and-resolve.md)

The key points, restated here because a bare pointer is invisible to a consumer that doesn't load the fragment:

- **Serial chasing cannot converge when the base's merge interval is shorter than a review round.**
  Both are measurable, so compare them rather than judging: `git log origin/main --first-parent -10 --format='%ct'` for the merge rate, and the review check's own `startedAt`/`completedAt` for the round.
  Count **first-parent** commits, not merge commits --- `git log --merges` reports nothing in a squash-merging repo.
- **A `DIRTY` flag means stale or defective, and only the second is a defect.**
  A PR whose content is clean but whose base moved is stale rather than broken.
  Staleness resolves once, at merge time, so re-syncing it eagerly spends a CI cycle and a review round on a state that expires within one merge interval.
- **A conflict your sweep found is not a conflict your merge caused.**
  Attribution is a second axis, and it runs before the claim: intersect the merge's own deleted and renamed paths (`git diff --name-status -M "$merge^1" "$merge" | grep -E '^(D|R)'`) with each conflict, and report conflicts caused alongside conflicts found.
  `git show --name-status <merge>` cannot supply that set for a **true** (two-parent) merge --- it prints no file list at all there, and grepping its header for `^[ADMR]` returns three phantom paths.
  It does diff a squash merge normally, so whether it works depends on how the repo merges rather than on the commit in front of you.
  A conflict you caused on a branch you do not own is an explanatory comment, not a push.
- **Independent per-PR checking cannot see pair collisions.**
  Every PR can be clean against `main` while two of them conflict with each other.
  Only a pairwise `git merge-tree` between PR heads finds that.
- **Any sweep needs a negative control**, run first.
  A zero matrix is indistinguishable from a detector that never ran, and `merge-tree` has two ways of producing one: the legacy three-arg form always exits 0, and its conflict markers are diff-indented, so `grep '^<<<<<<<'` misses them.
  Report how many pairs were examined, not only how many conflicted.
- **`merge=union` raises the stakes rather than lowering them**, since it resolves append collisions with no conflict to review.
- **"No conflict" is not an all-clear.**
  Version parity and Markdown list-item splices both arrive through cleanly-resolved merges with nothing red to point at.
  The transferable lesson: when a defect can be introduced by **deleting** a line, any instrument keyed on added lines is unsound for it --- use a count delta across the merge instead.

## Move referenced assets along with content that migrates or gets removed

<!-- Not yet shared with the lab manual; edit shared/workflow/migrate-referenced-assets.md, not here. -->
[shared/workflow/migrate-referenced-assets.md](shared/workflow/migrate-referenced-assets.md)

## Prioritize internal infrastructure work slightly over feature work

[shared/workflow/pr-prioritization.md](shared/workflow/pr-prioritization.md)

A tie-breaker for `ardia`'s PR-ordering step and `gi`'s (and `gii`/`gip`'s) issue-priority table when candidates are otherwise close in priority.
The fragment also sets the default direction for the age factor: among several open PRs, take the **older** one first unless you have more specific instructions.

## Use subagents when helpful --- and delegate rather than queue

[shared/workflow/use-subagents.md](shared/workflow/use-subagents.md)

Nothing parallelizable should ever sit "queued" --- writing "queued", "next up", "I owe you X", or "still need to" into a status recap is the trigger to launch a subagent on it right then, not a way to describe the plan.
Sidecar delegation (independent investigation, verification, a disjoint slice, an owed UMS pass, a routed `cai`) is pre-authorized and never worth asking about; keep only the blocking critical-path edit local.
Research and reading are dispatchable too, sized by how much comprehension the result needs rather than by how small the fetch looks --- the fragment above covers why that category of work is easy to route wrong without anything in the artifact showing it.

## Derive a set of work items; never hand over an enumeration of it

The section above governs *whether* to dispatch.
This governs how to **scope** what you dispatch.
A brief that lists PR or issue numbers is a snapshot, stale the moment it is written.
Before dispatching work scoped to a list, ask whether that set can grow or change while the work runs.
When it can, hand over the query that derives it rather than the list itself.

The failure is invisible by construction, which is why it needs a rule rather than more care.
Every agent does its job correctly on the list it was given, so the items that appear *between* the lists are covered by nobody, and no artifact reports it --- coverage is a property of the set rather than of any member.
`scripts/pr-sweep.py` is the deterministic half for open PRs, and reports what it examined rather than only what it found.

[`shared/workflow/derive-dont-enumerate.md`](shared/workflow/derive-dont-enumerate.md)

## Subagent worktrees are assigned, and an incident never silently repeals a decision

Two rules, one incident, and the second is the general form of the first.

**Assign the worktree on the `Agent` call.** Set `isolation` yourself rather than leaving each subagent to organize its own working directory, and brief every agent you isolate to stay inside the worktree it was given and to **push early** --- a pushed commit survives anything that happens to a working tree.
Deciding that a particular agent does not need one is fine.
Leaving it unmarked is what is not.
`hooks/flag-unassigned-worktree.py` mechanizes exactly this, and warns rather than blocks.

**Verify a dispatched agent's liveness before touching a worktree you did not just create --- never infer it from a snapshot.**
A clean `git status` and an unlisted agent both describe one instant.
Neither says whether the session working that worktree has actually stopped, and a quiet worktree can mean either "finished" or "between edits".
Ask the agent directly (`SendMessage` to its id, or the equivalent for a peer session) before editing or reclaiming its worktree, including one that has sat quietly for hours --- a long stretch is a reason to ask sooner, not evidence of abandonment.
[`memories/git-worktrees.md`](memories/git-worktrees.md) carries the case where both directions of that misreading --- read as live when quiet, read as dead when live --- happened to the same agent in one session.

**"Stay inside the worktree it was given" holds only while the agent works in the session's own repo.**
`isolation: "worktree"` places that worktree in the **session's primary repository**, never in a repository the brief happens to name --- so a dispatch into a different clone hands the agent a worktree of the wrong repo, and the instruction above is unfollowable as written.
Name the target clone by path instead, and tell the agent to create its own worktree there off `origin/<default-branch>` --- resolved from that repo, never hard-coded, per `memories/preferences.md`'s measured `fatal: invalid reference: origin/main` failure on a repo whose default is named otherwise.
Measured 2026-08-07.
[`memories/git-worktrees.md`](memories/git-worktrees.md) carries the evidence.
[`shared/workflow/challenge-the-assignment.md`](shared/workflow/challenge-the-assignment.md) covers the general form --- a brief must not assert anything about the recipient's environment, which the author cannot query even in principle.

**The general rule is the more valuable half.** When an incident makes you stop doing something you had decided to do, either re-argue the decision explicitly or fix the misuse --- never just change the behaviour.
A repealed decision changes no artifact, so review, tests, and hooks are all blind to it by construction, and the only detector is someone who remembers.
It is more dangerous than ordinary drift because the incident supplies an apparent reason, so from the inside it feels like having learned something rather than like lapsing.
If you cannot point at the message where a decision was reversed, it was not reversed.
It lapsed.

[shared/workflow/incidents-dont-repeal-decisions.md](shared/workflow/incidents-dont-repeal-decisions.md)

## Non-destructive actions

Standing grant, recorded universally in `AGENTS.md` ("Default to action without asking"): proceed with non-destructive steps without asking, and ask only for destructive, ambiguous, high-impact, or genuinely blocking choices.

## Auto-orchestration: always look for Workflow opportunities

The heavy, parallelizable skills (`ardia`, `ardiaei`, `gia`, `gip`, `grade-work`, `opposition-research`, `find-overlap`) decide on their own whether a task warrants multi-agent orchestration via the `Workflow` tool --- so I don't have to type `ultracode` every time.
The `Workflow` tool stays opt-in-gated for bare prompts; an invoked skill is itself the sanctioned opt-in.
Launch a workflow directly when an opt-in signal is already present (`ultracode`, a `+Nk` budget, or "use a workflow"), otherwise propose one with a one-line cost estimate and wait.
The PR/issue-iteration skills stay serial where pushes collide on shared review runners (see the fragment's shared-runner exception).

More generally --- not just inside the named heavy skills --- always look for opportunities to automate work via the `Workflow` tool.
When a task turns out to be workflow-shaped (decomposable, verification-bearing, and at a scale that earns it --- see the fragment's criteria), say so and propose a workflow even if no skill mandated one.
The same opt-in gate still applies: propose with a cost estimate and wait unless an opt-in signal is already present.

@shared/workflow/when-to-orchestrate.md

## Agent teams: a third parallelism primitive, human-gated and advisory

The corpus governs two primitives a session invokes itself --- a single `Agent` call ("Use subagents when helpful", above) and the `Workflow` tool ("Auto-orchestration", just above).
An **agent team** is the third: several separate Claude Code sessions (a lead plus teammates, each its own context window) that coordinate through a shared task list and a mailbox and **message each other directly**, rather than only reporting back.
Unlike the other two, a session cannot form one on its own, so the corpus's role is only to *recommend* it.

The discriminator across all three is one question: **do the workers need to communicate with each other, or does a human want to steer individual workers mid-run?**
No to both --- a subagent or a `Workflow` sweep, per the rules above.
Yes --- an agent team, and only if it is enabled.

**Never assume a team is available, and never author a step that spawns one.**
Agent teams are experimental and off by default (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`), and are spawned by the *user* in natural language and steered through an interactive agent panel --- not invoked by an autonomous or headless session.
So a recommendation to the user is the only correct output; a skill or `Workflow` step that forms a team is a bug.
The one concrete reuse angle: a `.claude/agents/<name>.md` subagent definition doubles as a teammate role (its `tools` and `model` apply, and its body is appended to the teammate's prompt), but its `skills`/`mcpServers` frontmatter is not applied to a teammate.

[shared/workflow/agent-teams.md](shared/workflow/agent-teams.md)

## Algorithmatize checks: instruments over LLM reasoning

Never spend LLM reasoning on a check a deterministic algorithm can decide:
build or run the instrument (a repo script, a CI step, a state dump plus a
threshold) and consume its verdicts, reserving model judgment for the
genuinely semantic remainder.
When you catch yourself (or a reviewer) re-deriving numbers by hand, or
eyeballing an artifact for a property with a numeric definition, that check
wants to be an instrument --- see the fragment for the procedure and tells.

[`shared/workflow/algorithmatize-checks.md`](shared/workflow/algorithmatize-checks.md)

## Automate everything: deterministic tools over work done by hand

Never do by hand any work that can be automated.

The section above governs *checks*.
This is the same instinct over the work itself: prefer deterministic,
inspectable algorithms to model reasoning wherever one will serve, and where
none exists, build it.
Read "work" broadly --- the rule is not about judgment but about doing by hand
what something else already computes,
which includes work carrying no judgment at all.
A hand-typed section number is nobody's decision;
it is a value the renderer already produces, kept in a second place by hand,
and it goes wrong the moment anything enables the real generator.
One principle with two faces, both binding at once --- a **constraint** on the
task in front of you (use the instrument that exists) and a **goal** over time
(build the one that does not, so the constraint gets cheap to obey).
The observable trigger is recurrence: after doing the same judgment task
twice, the third time is a tool.

The argument the checks fragment does not make is **inspectability**.
An algorithm can be read before it runs, reviewed by someone who does not
trust its author, diffed, and re-run to the same answer; model reasoning is
none of those.
That is why a hook beats a rule even when the model would usually follow the
rule.
Applies in every repo, research code included --- a hand-run analysis step or
an eyeballed validation is the same shape as a hand-composed status line.
Design and genuine judgment remain, but as the residue not yet automated
rather than a fixed reserve.

[shared/principles/deterministic-tools.md](shared/principles/deterministic-tools.md)

## Checklists: Do-Confirm, Read-Do, pause points, killer items

Where a check is mechanical but no instrument can decide it -- because it
spans several unrelated observations at one moment, like a pre-push sweep --
the instrument is a **checklist**, and the same discipline applies.

Add one only where a failure is repeatable, expensive, and mechanically
observable, then get four things right:

- **Type.** *Do-Confirm* (work freely, then stop and confirm) is the default.
  Use *Read-Do* (read each item and perform it in order) only when
  reordering the steps changes the answer, or when a step cannot be undone --
  a merge, a release, session-start freshness.
- **Pause point.** State the moment it fires as an observable event ("before
  `git push`", "before reporting the PR ready"), not a topic.
  A checklist with no trigger is read only by whoever was already careful.
- **Killer items.** Mark the one or two steps most often skipped and most
  costly to skip, since a flat list gets triaged under pressure and the
  dropped item is usually the one that looks like bookkeeping.
  The known ones: the UMS pass ending `post-merge`/`ardi`, and `wrap-up`'s
  state sweep.
- **Length.** Five to nine items, action plus evidence.
  Past that it has started teaching; move the explanation into the prose
  above it.

Treat every checklist as a draft until it has been run on real work, and
treat UMS as its revision loop: when a checklist was followed and the failure
happened anyway, the finding is about the checklist, not only the incident.
Don't checklist-ize skills that are mostly design judgment, exploratory
research, or one-off improvisation.

[`shared/workflow/skill-checklists.md`](shared/workflow/skill-checklists.md)

## Never pattern-match blindly: check the purpose transfers

Before reusing a structure --- a template, a working script, a neighbouring
file's shape, a pattern from another tool --- state what the original was
**for** and what the new one is **for**, and confirm those are the same kind
of thing.
Structural fit is necessary and never sufficient.

The tell is that every check you naturally run after adapting a template asks
whether the *mechanism* works, and none asks whether the *purpose* survived
the substitution: same interface, passing tests, and the thing now does the
opposite of what it should.
A template you wrote yourself recently gets the least scrutiny, because
reusing something you just verified feels like consistency rather than like
assuming --- which inverts the scrutiny the situation warrants.
This is not an argument against reuse; it is the check that makes reuse safe.

[`shared/workflow/check-purpose-before-reusing.md`](shared/workflow/check-purpose-before-reusing.md)

## Avoid false dichotomies

When laying out alternatives, test whether they are actually exclusive before
presenting them as such.
The tell is a question posed as either/or and answered with "both" --- which
means the exclusivity was constructed rather than found.

The observable action: before presenting alternatives, state what would be
lost by taking more than one.
If the answer is nothing, they are not alternatives --- enable multi-select,
or present them as composable steps with an order.
Genuinely exclusive options exist (two incompatible designs, a merge strategy,
a name), and presenting those as combinable is its own error; the target is
the unexamined default, not the act of choosing.
Composes with "Present decisions one at a time" above, which governs how many
questions to ask rather than how one question's options relate.

[shared/workflow/avoid-false-dichotomies.md](shared/workflow/avoid-false-dichotomies.md)

## Metacognition: monitor claims by type, and distrust the fluent ones

The two rules above supply instruments and checklists for work that is already
recognized as needing checking.
This one covers the assertion that never raised the question --- and the
regulation step nothing prompts.

Monitor your own claims at **composition time**, as each sentence is written,
rather than in a retrospective afterwards.
Confidence cannot be the trigger, because it runs inversely to accuracy, so key
on claim **type** instead: a claim about **state** gets re-queried, one about
**scope** gets checked against the population, one about **cause** gets asked
what else explains it, and an unexamined **default** gets named and decided.
An answer that arrived with no deliberation owes an alternative you can name
and reject.

[`shared/workflow/metacognitive-monitoring.md`](shared/workflow/metacognitive-monitoring.md)

## Question the assignment, not only the claims

The rule above governs **claims** --- the ones you generate as much as the ones
you are handed.
This one governs what you are asked to **do** --- a brief, an issue body, a
plan, a convention document, or the option set in a posed question.
None of those assert anything, so no claim-checking rule fires on them, and
adopting one feels like compliance rather than like skipping a step.
A wrong claim spoils a sentence; a wrong assignment spoils the whole task,
while every step inside it stays correct and checks green.

Two written lines bound the check: before starting, name the premise the work
rests on and what would show it false; in the report, name one thing in the
assignment you actually checked.
For a posed choice, state what its options presuppose before answering within
them.

It binds the **author** of an assignment too, and that half has no other rule
pointed at it: writing a brief feels like instructing rather than asserting,
so nothing fires on a premise stated inside one.
When a brief you write asserts corpus state --- a file's contents, a rule's
location, a site count --- run the deriving query and paste it beside the
claim, rather than leaving the recipient's discretionary premise check as the
only detector.

[`shared/workflow/challenge-the-assignment.md`](shared/workflow/challenge-the-assignment.md)

## Check for merge conflicts on every merge in an ultracode session

[shared/workflow/ultracode-merge-conflicts.md](shared/workflow/ultracode-merge-conflicts.md)

## Big-picture principles: KISS, DRY, DRW, modularity, and friends

The catalog in `shared/principles/` maps each principle to its purpose,
operational rules, and trade-offs.
When adding a coding or review rule, place it under the principle it serves.

[shared/principles/README.md](shared/principles/README.md)

## Don't reinvent the wheel (DRW) — in dev and in review

Before implementing a new function or feature, check that it hasn't already been done — in one of our own repos, or in a trustworthy external source we could depend on instead (base R, r-lib, tidyverse, a well-maintained CRAN package).
Prefer forking and/or contributing to an existing external source over re-building the functionality from scratch.
Apply this in review too: a hand-rolled equivalent of functionality that already exists is a review finding, the same weight as any other standing review check.

[shared/principles/dont-reinvent-wheel.md](shared/principles/dont-reinvent-wheel.md)

The `prefer-upstream` skill runs the search; the `prefer-packaged-functions` fragment below is the R-function special case; the `scout-peers` skill gates borrowed code by license.

## If a repo isn't using `gha` and would benefit, upgrade it

[shared/workflow/upgrade-to-gha.md](shared/workflow/upgrade-to-gha.md)

The CI-specific, proactive case of DRW above.
`Morrison-Lab/gha` ships the lab's reusable workflows, called from a consumer as `uses: Morrison-Lab/gha/.github/workflows/<name>.yml@vN`.
When a repo you are working in hand-maintains a workflow gha already provides, migrate it --- the upgrade is the deliverable, not the observation.
The corpus's other gha triggers each wait for an event (a bug to patch, a port to close out, new CI to write), so none fires on the commonest case: nothing is broken, and the duplicate has simply sat there absorbing none of gha's fixes.
Candidates are duplication, drift, a named missing fix, or a directory that already calls gha for some workflows and not others.
Not candidates are repo-specific logic gha does not model, a repo deliberately pinned off gha, and a repo we cannot merge a PR to.
The fragment carries the rest: taking the inventory from gha's README table rather than its directory listing, pinning per capability, filing the migration as its own issue and PR, the private-consumer access precondition, the `permissions:` and `concurrency:` traps, and how to confirm the self-edit guard when a migration PR gets no review.

## Don't incur technical debt

[shared/principles/dont-incur-technical-debt.md](shared/principles/dont-incur-technical-debt.md)

## Fail fast — no silent failures

Detect bad state early and stop with a clear error rather than proceeding on it; never swallow an error into a silent fallback (a bare `except:`, a `tryCatch` returning `NULL`, a shell `|| true`), and make any genuinely wanted fallback explicit, bounded, and observable.
Apply this in review too: error handling that hides failure is a review finding, the same weight as any other standing review check.

[`shared/principles/fail-fast.md`](shared/principles/fail-fast.md)

## Coding: KISS is the umbrella principle

Apply KISS to code and prose: use the simplest construct that does the job,
and justify added complexity.
The rules below and `challenge-unnecessary-complexity` are concrete cases,
not an exhaustive list.

## Coding: use the least-flexible construct that does the job

<!-- Not yet shared with the lab manual; edit shared/coding/least-flexible-tool.md, not here. -->
[shared/coding/least-flexible-tool.md](shared/coding/least-flexible-tool.md)

## Coding style: avoid nesting; follow the lab manual

Follow the SERG lab manual (https://ucd-serg.github.io/lab-manual/) for coding and collaboration conventions.

<!-- Shared with the lab manual; edit shared/coding/avoid-nesting.md, not here. -->
[shared/coding/avoid-nesting.md](shared/coding/avoid-nesting.md)

## Coding: single-indent multi-line function signatures

<!-- Not yet shared with the lab manual; edit shared/coding/function-signature-style.md, not here. -->
[shared/coding/function-signature-style.md](shared/coding/function-signature-style.md)

## Coding: prefer existing packaged functions over rolling your own

<!-- Shared with the lab manual; edit shared/coding/prefer-packaged-functions.md, not here. -->
[shared/coding/prefer-packaged-functions.md](shared/coding/prefer-packaged-functions.md)

## Coding: memoise pure, expensive, repeatedly-called functions

<!-- Not yet shared with the lab manual; edit shared/coding/use-memoisation.md, not here. -->
[shared/coding/use-memoisation.md](shared/coding/use-memoisation.md)

## Coding: prefer per-operation grouping over persistent grouping (dplyr)

<!-- Shared with the lab manual; edit shared/coding/per-operation-grouping.md, not here. -->
[shared/coding/per-operation-grouping.md](shared/coding/per-operation-grouping.md)

## Coding: prefer type-stable calls; never `sapply()` outside the console

<!-- Not yet shared with the lab manual; edit shared/coding/type-stable-outputs.md, not here. -->
[shared/coding/type-stable-outputs.md](shared/coding/type-stable-outputs.md)

## Coding: preallocate, `seq_along()`, and `[[i]]` in for loops

<!-- Not yet shared with the lab manual; edit shared/coding/loop-hygiene.md, not here. -->
[shared/coding/loop-hygiene.md](shared/coding/loop-hygiene.md)

## Coding: restore global state your function changes

<!-- Not yet shared with the lab manual; edit shared/coding/restore-global-state.md, not here. -->
[shared/coding/restore-global-state.md](shared/coding/restore-global-state.md)

## Coding: `set -e` is not uniform; tolerate expected non-zero exits explicitly

<!-- Not yet shared with the lab manual; edit shared/coding/errexit-is-not-uniform.md, not here. -->
[`shared/coding/errexit-is-not-uniform.md`](shared/coding/errexit-is-not-uniform.md)

## Coding: an empty bash associative-array subscript is fatal, not a miss

<!-- Not yet shared with the lab manual; edit shared/coding/bash-associative-arrays.md, not here. -->
[`shared/coding/bash-associative-arrays.md`](shared/coding/bash-associative-arrays.md)

`${arr["$k"]:-}` tolerates a missing key and not an empty one, so the `:-`
default cannot guard a validator whose key is derived (`"${stem##*.}"`, a
`cut` field, a regex capture) --- it dies on exactly the malformed input it
exists to reject.
Test the key for emptiness first in an `||` chain, so short-circuiting
prevents the expansion.

## Coding: avoid hard-coding data with an external source of truth

<!-- Shared with the lab manual; edit shared/coding/avoid-hardcoding-external-data.md, not here. -->
[shared/coding/avoid-hardcoding-external-data.md](shared/coding/avoid-hardcoding-external-data.md)

## Coding: make every parameter configurable

<!-- Not yet shared with the lab manual; edit shared/coding/configurable-parameters.md, not here. -->
[shared/coding/configurable-parameters.md](shared/coding/configurable-parameters.md)

## Coding: write tidy code; prefer tidyverse over base R/rlang for it

<!-- Not yet shared with the lab manual; edit shared/coding/tidy-code.md, not here. -->
[shared/coding/tidy-code.md](shared/coding/tidy-code.md)

Apply this both when writing code and when reviewing it — flag base R or
`{rlang}` verbosity in review the same way `per-operation-grouping` flags a
persistent `group_by()` that `.by` would replace.

## Coding: reuse function documentation and argument lists

<!-- Not yet shared with the lab manual; edit shared/coding/reuse-docs-and-args.md, not here. -->
[shared/coding/reuse-docs-and-args.md](shared/coding/reuse-docs-and-args.md)

## Coding: one function per file

<!-- Not yet shared with the lab manual; edit shared/coding/one-function-per-file.md, not here. -->
[shared/coding/one-function-per-file.md](shared/coding/one-function-per-file.md)

Apply this both when writing new code and when reviewing it — a new function
added inline to an existing multi-function file is a review finding, the
same weight as the other modularity checks above.

## Coding: no em-dashes or non-ASCII punctuation in source files

<!-- Not yet shared with the lab manual; edit shared/coding/ascii-punctuation-in-source.md, not here. -->
[shared/coding/ascii-punctuation-in-source.md](shared/coding/ascii-punctuation-in-source.md)

## Coding: decompose complex code into functions, not .qmd chunks

<!-- Not yet shared with the lab manual; edit shared/coding/decompose-to-functions.md, not here. -->
[shared/coding/decompose-to-functions.md](shared/coding/decompose-to-functions.md)

## Writing style: plain, direct prose

<!-- Shared with the lab manual; edit shared/writing/plain-prose.md, not here. -->
[shared/writing/plain-prose.md](shared/writing/plain-prose.md)

The `use-preferred-style` skill (alias `style`) spells out the procedure, the PSW chapter links, and a filler/jargon swap table; the `find-ai-tells` skill (alias `ai-tells`) is the scan-after detector counterpart.

## Writing style: name the referent, so no pronoun is ambiguous

A specific case of the plain-prose rule above, and the one self-review is worst at catching.
The tell is a pronoun or demonstrative --- `it`, `this`, `that`, `which`, `they` --- whose **nearest grammatical antecedent is not its intended referent**.
A pronoun with no clear referent makes a reader pause and re-read, so the cost is a moment.
A pronoun whose *wrong* referent sits closer reads perfectly well, so the reader takes away the wrong fact without ever being unsure.
The remedy is to replace the pronoun with the noun, not to reword around it.

[shared/writing/ambiguous-reference.md](shared/writing/ambiguous-reference.md)

This is distinct from [`challenge-ambiguous-terminology`](shared/workflow/challenge-ambiguous-terminology.md), which governs a word whose **meaning** is unresolved rather than a word whose **antecedent** is.
Apply it wherever `code-review`/`ard`/`ardi` already reviews a prose diff, alongside the other prose-review rules in this file.

## Writing style: semantic line breaks in prose

[`shared/writing/semantic-line-breaks.md`](shared/writing/semantic-line-breaks.md)

## Quarto: div syntax for figure/table labels and captions

[`shared/writing/quarto-figure-captions.md`](shared/writing/quarto-figure-captions.md)

## Challenge ambiguous phrasing and terminology in review

[shared/workflow/challenge-ambiguous-terminology.md](shared/workflow/challenge-ambiguous-terminology.md)

The `ard`/`ardi` skill family and `use-preferred-style`/`find-ai-tells` operationalize this in their respective review contexts.

## Challenge redundant content in review

[shared/workflow/challenge-redundant-content.md](shared/workflow/challenge-redundant-content.md)

The `ard`/`ardi` skill family and `code-review` apply this in PR/MR review; `find-overlap` (and its `consolidate-skills`/`consolidate-memory` actors) is the corpus-wide counterpart when redundancy spans more than the current diff.

## Never assert a corpus gap from a grep

The rule above catches redundant content once it is written.
This one catches the belief that produces it: a phrase grep returning nothing is not evidence the corpus lacks a concept, because grep matches strings while coverage is a claim about ideas.
Report the query and its result, not the conclusion.

[`shared/workflow/grep-is-not-coverage.md`](shared/workflow/grep-is-not-coverage.md)

Fires wherever a search decides whether to author something new --- `skill-builder`'s step 0, `ums`'s step 3, and `find-overlap`, whose own instrument scores this repo's canonical same-idea pair at 0.019 phrase similarity.

## Writing style: scan for AI tells

The detector counterpart to the plain-prose guide above.

<!-- Shared with the lab manual; edit shared/writing/ai-tells.md, not here. -->
[shared/writing/ai-tells.md](shared/writing/ai-tells.md)

The `find-ai-tells` skill (alias `ai-tells`) runs this same catalog on demand against any target text.

## Writing style: an example of a checked pattern is itself checked

[shared/writing/examples-are-scanned.md](shared/writing/examples-are-scanned.md)

When a document explains a mechanically-enforced convention, its illustrative
example sits inside the file the checker scans -- so writing the example the
natural way can trip the rule the passage is describing, and implicate the one
passage meant to prevent it.
Whether it does turns on the checker: backticks and fenced blocks shield
nothing from a line-oriented scanner, and everything from a structure-aware
one, so read it rather than assuming either way.
Teach the checker about code regions when you own it (this repo's
`scripts/lib/fences.py` is that fix), render the example so it cannot match
when you do not, and either way run the detector rather than re-reading --
self-review confirms the claim, which was never the defect.

## Writing style: cite sources thoroughly

[`shared/writing/citations.md`](shared/writing/citations.md)

## Fact-check prose and internal reasoning in review

[`shared/writing/fact-check-prose.md`](shared/writing/fact-check-prose.md)

When running `code-review` or the `ard`/`ardi` loop on a diff that touches prose, apply this policy in addition to the normal review — those skills don't name it internally, but this CLAUDE.md directive governs regardless.

## Writing style: timestamp factual claims about conditions that can change

The complement to the fact-check above: a claim can be *true* yet still decay
into a confident falsehood if it's stated as timeless present-tense fact when
its truth is time-dependent (a package's CRAN status, a "current" version, a
count).
Attach the time the claim was true so a later reader knows to
re-verify it.

[shared/writing/timestamp-volatile-claims.md](shared/writing/timestamp-volatile-claims.md)

## Writing style: math --- include every step; keep each equation simple

[`shared/writing/math-derivation-steps.md`](shared/writing/math-derivation-steps.md)

Two axes.
*Between* displayed lines, write out every step, and flag gaps in review.
*Within* one line, decompose complicated internal structure out into extra
notation, then reapply that until each line carries one operation.
Apply the second thoroughly rather than per equation: name the concept where
it first enters the document, since an unnamed concept is one that gets
silently duplicated across sections.
Stop unfolding at a modeled quantity the reader already accepts at that point
in the argument, which is a test against the exposition rather than a class
of expression.

When running `code-review` or the `ard`/`ardi` loop on a diff that touches
math, apply this in addition to the fact-check above.

## Hyperlink technical terms and results; no forward references

[shared/writing/definition-crossrefs.md](shared/writing/definition-crossrefs.md)

Applies wherever `code-review`/`ard`/`ardi` already reviews a prose diff, alongside the fact-check and ambiguous-terminology checks above.

## Remove forward-pointing phrases from prose, not just crossref divs

The section above covers formal Quarto crossref-div ordering for term/result definitions specifically.
The same problem shows up more broadly as plain-text signposting — "as discussed below", "in the following section", "we'll cover this later" — pointing at content the reader hasn't reached yet, in any prose (not just documents with crossref divs).

[shared/writing/forward-references.md](shared/writing/forward-references.md)

Unlike `definition-crossrefs.md` above, `forward-references.md` has a dedicated actionable skill: the `fix-forward-references` skill (alias `ffr`) detects these with a grep-for-directional-word heuristic and rearranges (or rewords) the prose to fix them.
Run it — or apply its check inline — wherever `ard`/`ardi` reviews a prose diff, alongside the other prose-review rules in this file.

## Rearranging sections, paragraphs, and content across documents is part of editing prose

[shared/writing/reorganize-prose.md](shared/writing/reorganize-prose.md)

Moving a section, subsection, paragraph, or sentence --- within a document, or in a multi-document repo (a website, a book, a manuscript) across documents --- is in scope for a prose edit whenever it improves flow, fixes a forward reference, removes duplicate content, or reunites related content split across distant locations.
A move is authorship, not a no-op: sweep for stale self-references and count-based back-references, bring the relocated lines into compliance with the line-level checks above, migrate any referenced assets, and prove nothing was lost or accidentally added with a bidirectional content comparison.

## Detect concepts defined only in prose, never formalized

`definition-crossrefs.md` above assumes a formal-definition div already exists and checks that mentions link to it in the right order.
A distinct, easy-to-miss gap: a concept stated with full definitional precision --- a bolded name, an equation, an `\eqdef` --- that never became a formal div at all, so it has no stable id and nothing downstream can cite it (or the concept rides along inside a *different* definition's div instead of getting its own).

[shared/writing/informal-definitions.md](shared/writing/informal-definitions.md)

Like `forward-references.md`, this has a dedicated actionable skill: `detect-informal-definitions`.
Run it --- or apply its check inline --- wherever `ard`/`ardi` reviews a diff that introduces new technical content, alongside the other prose-review rules in this file.

## Detect hypothetical examples where real data is already available

A worked example can be a perfectly well-formed `{#exm-...}` div and still reach for invented, round-number quantities --- "suppose 20% of the exposed group..." --- when the document already loads a real dataset it uses elsewhere.
That's a distinct gap from the informal-definitions check above: it isn't a missing div, it's a missed chance to ground the illustration in real data that was already available.

[shared/writing/hypothetical-examples.md](shared/writing/hypothetical-examples.md)

This has a dedicated actionable skill: `detect-hypothetical-examples`.
Run it --- or apply its check inline --- wherever `ard`/`ardi` reviews a diff that introduces or edits a worked example, alongside the other prose-review rules in this file.
Fixing isn't mechanical substitution: a real dataset's effect size is often much less dramatic than an invented one, so weigh whether the real numbers still make the teaching point before publishing them.

## Fact-check code logic and math in review

<!-- Not yet shared with the lab manual; edit shared/coding/fact-check-code-logic.md, not here. -->
[`shared/coding/fact-check-code-logic.md`](shared/coding/fact-check-code-logic.md)

The code counterpart to the prose fact-check above --- catches strategic
mistakes (wrong algorithm or approach), tactical mistakes (wrong
implementation of a right approach), and math/statistics errors (wrong
formula or method, verified against a source), not just prose claims and
derivations.

## A test fixture is not evidence about the system it imitates

The two fact-check rules above assume you can tell a source from a
non-source.
A test fixture defeats that assumption: it lives in the repo, it is named
after real output, and its own comment often vouches for being verbatim ---
so reasoning from its behaviour back to the real system feels like checking
rather than guessing, and the resulting claim arrives dressed as a test
result.

[shared/workflow/fixtures-are-not-evidence.md](shared/workflow/fixtures-are-not-evidence.md)

Distinct from `ardi`'s fixture bullets, which are about coverage (a fixture
too thin to reach a branch) rather than about the inference drawn from one
that works fine.

## Verify the artifact the claim is about, not an adjacent one

Three rules in this corpus each name one adjacent artifact that stands in for
the real one: the fixture rule directly above,
`metacognitive-monitoring`'s neighbouring step read for a failure's cause, and
`fact-check-prose`'s published build read for the branch that produced it.
The substitution is general, and outside those three situations none of the
three loads.

It is not lazy verification but thorough verification of the wrong object, so
the evidence is real, the reasoning from it is sound, and nothing feels like a
guess.
The fragment names four recognizable shapes --- a cached copy for the origin, a
checkout for the run, one half of a mechanism for the whole, a neighbour for
the target --- and one test that works where confirming the claim cannot:
ask what would have to be true for the claim to be **false**, and whether the
artifact in hand could show it.

[shared/workflow/verify-the-right-artifact.md](shared/workflow/verify-the-right-artifact.md)

## Challenge unnecessary complexity in review

[shared/workflow/challenge-unnecessary-complexity.md](shared/workflow/challenge-unnecessary-complexity.md)

When running `code-review`, `ard`/`ardi`, or any prose review (`use-preferred-style`, `find-ai-tells`, `fact-check-prose`), apply this alongside the normal review — those skills don't name it internally, so this CLAUDE.md directive governs regardless. It's distinct from `simplify` (a dead-code-after-refactor sweep) and `tidy` (a separate on-demand audit).

## Useful prompt formats for coding agents

<!-- Vendored from Morrison-Lab/wai; edit there, not here. See README, "Shared content". -->
[shared/vendored/prompt-formats.md](shared/vendored/prompt-formats.md)

## Review with Copilot before requesting human review

This is shared lab guidance on getting an automated review before asking a human reviewer.
When *I* iterate a PR, the ARDI loop above is the mechanism — it already addresses whatever the `@claude` or Copilot reviewer flags — so read this as the lab-member-facing statement of the same principle, not a second loop to run.

<!-- Vendored from Morrison-Lab/wai; edit there, not here. See README, "Shared content". -->
[shared/vendored/copilot-review-before-human.md](shared/vendored/copilot-review-before-human.md)

## Growth mindset: seek resources rather than accept limitations

<!-- Edit shared/workflow/growth-mindset.md, not here. -->
[shared/workflow/growth-mindset.md](shared/workflow/growth-mindset.md)

## Research before asking a human

<!-- Edit shared/workflow/research-before-asking.md, not here. -->
[shared/workflow/research-before-asking.md](shared/workflow/research-before-asking.md)

## Encoding reusable feedback into ai-config

When the user gives feedback, corrections, or guidance that applies beyond the current session (a standing rule, style preference, workflow change, or behavioral note), decide on your own how to encode it --- don't ask.
Choose the right form (memory bullet in CLAUDE.md, update to a shared fragment in `shared/`, new or revised skill, etc.) and commit the change.
Only surface the choice if it's ambiguous or touches something architecturally significant.

**Put the memory in the repo where it belongs, and don't wait for confirmation to do it.**
Session-local auto-memory is a scratchpad, not a home.
A learning parked there is invisible to every other session and to everyone else, so a reusable one has to land in a version-controlled repo --- `ai-config` for a cross-cutting rule, the specific repo for a repo-specific gotcha.
And "decide on your own --- don't ask" above rules out the adjacent move too.
*Offering* to upstream a learning is not upstreaming it, and it spends a round trip to hear an answer already written here.
Open the PR.

- **Do:** commit a reusable learning to the repo that owns it, in the same stride you notice it.
- **Do:** pick the home by scope --- an `ai-config` shared fragment, `CLAUDE.md`, or `memories/` for a cross-repo rule;
  the specific repo's own docs for a repo-specific one.
- **Don't:** leave a reusable learning in session-local auto-memory as a substitute for committing it.
- **Don't:** offer to upstream it, or ask which repo --- decide and do it, surfacing the choice only when it is genuinely ambiguous or architecturally significant.

## PowerShell CLI Command Safety

- **Never pass backtick-containing content in PowerShell double-quoted strings**: PowerShell treats `` ` `` as its escape character — `` `b `` (Backspace, 0x08), `` `n ``, `` `t ``, `` `r ``, etc. — so Markdown code spans and other backtick-containing text will be silently corrupted. Use single-quoted strings (`'...'` / `@'...'@`) for inline content, or write to a file and pass `--body-file` for multi-line PR descriptions.
- **Use body files for GitHub PR descriptions**: Write multi-line PR descriptions to a temp file and pass `--body-file <file>` to `gh pr create`/`gh pr edit`, or `gh api -F body=@<file>` for raw API calls. This avoids terminal string-escaping corruption for any content with backticks or other shell-special characters.
- **The hazard is not PowerShell-specific, and not limited to PR descriptions**: bash and zsh double-quoted strings run backtick spans as command substitution, so `gh pr comment`, `gh issue comment`, `gh api .../comments -f body="..."` / `.../replies -f body="..."`, and `git commit -m "..."` corrupt a backtick-carrying body exactly as `gh pr create --body "..."` does (a `` `ms.` `` code span runs `ms.` as a command and vanishes). Use `--body-file` / `-F body=@<file>` for comment and review-reply bodies too, in any shell, and `git commit -F <file>` for a commit message. See `memories/git.md`'s "`gh pr comment` / `gh api ... -f body=` run backtick spans too" and "`git commit -m "..."` runs backtick spans as shell commands" sections.
- **`git commit -m` is the surface that enumeration hides**, because every other entry posts to GitHub, so a commit message reads as a different kind of thing while the shell treats it identically.
  Measured 2026-08-17: an unescaped span inside a bash double-quoted string runs, so `` `echo SUBSTITUTED` `` became `SUBSTITUTED` in the resulting message.
  The same day a `-m` message quoting a merge command in backticks was refused by `hooks/no-unauthorized-merge.py`; those backticks were backslash-escaped, so what actually matched is unverified, and blocking is the safe direction rather than a defect.
  `git commit -F <file>` succeeded immediately either way, which is why the remedy needs no diagnosis first.
  - **Do:** write a commit message carrying backticks to a file and commit it with `git commit -F <file>`.
  - **Don't:** pass a backtick-carrying message through `git commit -m "..."`, or spend a round diagnosing a guard refusal when the file route costs one command.

## Tool transport collapses doubled backslashes

The sibling of the backtick hazard above, and the same class: content silently
transformed between what I type and what the interpreter receives.

Inside a Bash-tool heredoc with a **quoted** delimiter (`<<'PY'`), which
should be entirely literal, a doubled backslash `\\` arrives as a single `\`.
A single `\` survives intact.
So one level of unescaping is applied somewhere in transport.

**Scope it before relying on it: this is a property of the environment, not of
heredocs.**
Measured 2026-08-22 on Windows 11 / MINGW64 through the Claude Code Bash tool.
A reviewer running the same cases in a GitHub Actions Linux runner could **not**
reproduce any of it, and was right not to --- so a claim stated unconditionally
here is false there, which is how a true observation becomes a wrong rule.
Test your own environment before trusting either answer.

The reproducer is one command and needs no interpreter, which is what rules out
Python's own string parsing as the cause:

```
cat <<'EOF' > out.txt
a\\nb
c\nd
EOF
```

Both lines land in `out.txt` carrying **one** backslash: the doubled form
collapsed, the single form survived.
Nothing but the transport touched it.

It fails silently and plausibly.
A patch script's `assert target in s` fails, which reads as a slightly-wrong
anchor string --- so the natural response is to re-dump the region and retype
the anchor, which fails identically.
The tell only appears on printing `repr()` of the constructed string.

The worse case is not a failed assert.
A heredoc that *writes* `\\d` into a regex emits `\d` --- a corrupted matcher with no
syntax error and a green suite.
Anything writing regexes, escape sequences, or Windows paths through a heredoc
is exposed, including a `jq` filter: `test("\\*\\*Claude finished")` reaches
`jq` as `test("\*\*...")` and dies with `Invalid escape`.

Build the character rather than typing it:

```
B = chr(92)
def bs(t): return t.replace("@@", B)

# A NON-RAW literal is where this bites. Expressing one backslash inside one
# requires typing two, and that doubled form is exactly what collapses -- so
# the placeholder is doing real work here.
target = bs('print("done@@n")')
# -> the 15 characters  print("done\n")  ... with a real backslash,
#    which is what the file being patched actually contains.
```

The same collapse is already described twice.
[`algorithmatize-checks.rationale.md`](shared/workflow/algorithmatize-checks.rationale.md)
records it for **this same transport** --- a shell heredoc feeding Python ---
where `\\b` arrives as `\b` and becomes a **backspace**, worked through as
a mutation that silently corrupts a guard's own regex.
[`address-every-comment.rationale.md`](shared/workflow/address-every-comment.rationale.md)
records a genuinely different one, backslash quoting collapsing across nested
shell layers.
What is new here is the trigger context --- a Bash-tool heredoc whose delimiter
is quoted, so it should be literal --- and the placeholder remedy.
Cross-linked because a dupe-check keyed on this section's vocabulary would
otherwise miss both.

A **raw** string needs none of this: `r"^\d+$"` is single backslashes
throughout, and those survive.
The machinery is for the doubled form --- a non-raw literal, or any target that
must itself contain a backslash escape.

- **Do:** route every literal backslash through `chr(92)` (or a placeholder
  token) when heredoc content must survive verbatim.
- **Do:** print `repr()` of a constructed string when a match inexplicably
  fails, rather than retyping the anchor.
- **Don't:** assume a quoted heredoc delimiter guarantees literal content ---
  on this platform, measured 2026-08-22, it does not.
- **Don't:** carry the claim to another platform without re-measuring; it did
  not reproduce in a Linux CI runner.
- **Don't:** trust a green suite after writing a regex through a heredoc; read
  the emitted line back.

(Measured 2026-08-22; tracked as
[ai-config#1923](https://github.com/Morrison-Lab/ai-config/issues/1923).
Cost three identical failed patch attempts before the cause was visible, then
recurred immediately in a `jq` filter reading a PR review body.)

## Strict Merge Control Policy

- **NEVER merge any Pull Request or Merge Request without explicit user permission.**
  Creating, opening, updating, or driving a PR to clean CI/review does NOT grant permission to merge it.
  Merging a PR is strictly forbidden unless the user explicitly grants session permission (e.g. via `/mwc` or `/maw`) or explicitly issues a merge instruction for that specific PR (e.g. `/merge-it` or "merge this PR").
- **Never merge over open review findings or treat skip notices as approval.**
  Under `mwc`, a PR must be fully clean across CI and all review findings.
  A reviewer skip notice (e.g. for workflow edits or quota limits) never clears or supersedes prior review findings.
  All findings across the PR history must be fully Addressed, Rebutted, or Deferred before merge.
  A disagreement among reviews is not fully clean: any standing not-clean
  --- nits included --- vetoes merge even with `mwc` active
  (ai-config#2274).
- **Another session's PR needs a second condition: clean, and clean for more than twenty minutes --- then warned.**
  Every other rule here settles *when* a PR may be merged;
  this one settles *whose*.
  A peer may have further commits planned, so merging one that just went clean can destroy work it was about to push --- and that is exactly the case where the peer's PR unblocks yours and the temptation is strongest.
  Start the clock at the clean verdict on the current head, which a push resets, rather than at the PR's `updatedAt`, which any comment bumps.
  The threshold is an inference, so confirm it: message the owning session directly when `ListAgents` reaches it, and otherwise post a comment saying you intend to merge and wait a further five minutes for a hold-off.
  [`mwc`](skills/mwc/SKILL.md)'s "Another session's PR" section carries the derivation and the pattern/anti-pattern pair (ai-config#2460).

**One standing exception: PRs targeting `Morrison-Lab/ai-config` carry a standing `mwc` grant**, with no per-session re-issue and no `enable-mwc` step --- `hooks/no-unauthorized-merge.py` reads the merge's target repo off the command.
[`mwc`](skills/mwc/SKILL.md)'s Scope Limit binds in full, so it covers a **fully clean** PR (see [`fully-clean`](shared/workflow/fully-clean.md)) and nothing else.
It is scoped to the **target**, so a merge from an ai-config checkout into another repo is unaffected.

- **Do:** merge a fully-clean ai-config PR without asking, and say in the same reply that you did and why it qualified.
- **Don't:** read it as covering a PR that is not fully clean, or another repo's PR merged from an ai-config checkout.
