--- name: dogfood description: "The dogfood mailbox protocol: a repeatable loop for two agent sessions in sibling repo checkouts to request, deliver, adopt, and iterate on cross-repo changes (e.g. a pnpm `overrides:` link against a sibling checkout's local prod artifacts) before anything is released. Mail files plus a per-loop JSONL state journal under .claude/dogfood/, a phase/ball machine and an enforced no-push-while-linked rule. /silk:dogfood --init|--send|--status|--watch|--adopt|--exit. Use whenever files under .claude/dogfood/** are read or written, and for \"start a dogfood loop\", \"whose ball is it\", \"adopt the handoff\", \"exit the dogfood loop\", \"unlink the file: override\"." argument-hint: --init | --send | --status | --watch | --adopt | --exit --- # Dogfood mailbox (`/silk:dogfood`) The user invoked `/silk:dogfood` with arguments: `$ARGUMENTS` — or a file under `.claude/dogfood/**` was just read or written, which is itself the trigger to apply this skill (see Inbound processing below). Bare invocation, or no recognized flag, means `--status` plus a recommended next action. A **loop** is one collaboration: an (upstream, downstream, linked-package-set) triple between this repo and one sibling checkout. The **downstream** requests changes and consumes artifacts; the **upstream** implements and provides them. Roles are per-loop, not repo-global — a repo can be downstream in one loop and upstream in another simultaneously. The **repo id** is the counterpart's root `package.json` `name`, used as the mailbox directory key; the journal filename is `.claude/dogfood/[.].jsonl` where `` defaults to `` for the single-loop case. The **ball** says whose move it is; every mail kind deterministically flips or keeps it. Full design rationale, resolved questions, and explicit v1 scope cuts: `docs/superpowers/specs/2026-07-16-dogfood-mailbox-skill-design.md`. ## Mailbox: location and file format Mail lands in the RECEIVING repo at `.claude/dogfood//`. Both repos gitignore `.claude/dogfood` (`--init` adds the entry to both `.gitignore`s if missing). Markdown files, named `YYYY-MM-DD-[-].md`, with light YAML frontmatter (`from`, `to`, `kind`, `round`, optional `loop`, optional `in-reply-to`). Full templates and content contracts for all six kinds: `references/mail-kinds.md` — load when composing a `--send ` mail or reading one you need to verify against its contract. | Kind | Direction | One-line contract | | --- | --- | --- | | `briefing` | either, round 0 or a reopened loop's opening round, `--init` only | Generated protocol boot for the counterpart's session. | | `request` | downstream → upstream | Asks with exact `file:line` cites; long-lived item-status table both sides keep current. | | `handoff` | upstream → downstream | Exact export signatures/error unions from built `.d.ts`, behavior changes, "intentionally not done" section. | | `status` | either | Cheap one-liner that only flips ball/phase; also a relay's downstream-notification vehicle (see Discipline). | | `findings` | downstream → upstream | Adoption results: clean, friction, discrepancies, confirmations. | | `release` | upstream → downstream | The exit trigger — package names, versions, registry. | ## Inbound processing Receiving is not a separate mode. The receiving agent appends the corresponding journal snapshot (updated `phase`/`ball`/`lastMail.in`, `event: "mail-received"`) as the FIRST step of acting on any mail — before drafting a response, before starting the work the mail asks for. This skill auto-loads whenever a file under `.claude/dogfood/**` is read or written, so reading mail is itself the trigger that puts this instruction in front of you. Don't skip the journal append because the mail's content feels like the more urgent thing to act on. ## The state journal One JSONL file per loop, per repo: `.claude/dogfood/[.].jsonl`, gitignored, **appended via `scripts/journal-append.sh`, never edited in place and never hand-rolled with `jq`.** The script inherits the last valid line, patches only the flags you pass, validates the event/phase/ball enums on every append plus `linkType` once at `--init` (it's fixed for the loop's life after that, not a later-append patch), and appends only on success (savvy-web/systems#338) — invocation forms for opening a loop and every later append are in `references/jsonl-journal.md`. Each line is a complete snapshot: current state = the last valid line (no fold/reduce anywhere), history = the file, corrections are appends (`event: "correction"`), corrupt tails self-heal (a malformed last line is skipped, readers walk back). On a **downstream** `--init`, `journal-append.sh` writes `packagesDerived: false` deliberately, even when you already believe you know the closure — deriving it is a step the session owning the downstream repo takes on its own, from the BUILT manifest at the link path (see `--init` step 3), never assumed at init time. Once it genuinely has been derived, append a `correction` snapshot carrying the closure ITSELF — one `--package '='` per entry, plus `--packages-derived true` — or `--clear-packages` with `--packages-derived true` when the derived closure is genuinely empty. `--packages-derived true` on its own would stamp a known-clean marker over the carried-forward `packages: []`, which is the same contradiction `journal-append.sh` now refuses from the other direction (savvy-web/systems#508). An **upstream** `--init` omits the field entirely — the upstream links nothing, so `packagesDerived` (like `packages`/`nativeRebuilds`) never applies to its side of the journal. The field is three-state — `true`, `false`, or absent — and absent is not the same as `false`: a reader that treats them as interchangeable is the exact defect class savvy-web/systems#331 already paid for. A new collaboration can append another `loop-started` line to reopen a closed loop, or run simultaneously as a second loop by using a distinct `` with the same counterpart id. Full snapshot-line shape (every field, upstream-vs-downstream differences, the append recipe): `references/jsonl-journal.md` — load before your first append in a session, or whenever you need the exact field set. ## Phase machine ```text requested → implementing → handoff → adopting → findings ─┐ ▲ │ (iterate: next round) └─────────────────────────────────────────────────────┘ findings (downstream satisfied) → upstream-pr → released → unlinked (terminal) ``` | Phase | Ball | Meaning | | --- | --- | --- | | `requested` | upstream | Request mail sent; upstream to implement. | | `implementing` | upstream | Upstream working; optional `status` mail on milestones. | | `handoff` | downstream | Handoff mail landed; downstream to refresh + adopt. | | `adopting` | downstream | Downstream migrating onto new surfaces and verifying. | | `findings` | upstream | Findings sent. Iterates (→ `implementing`, `round + 1`) or, when downstream is satisfied, advances. | | `upstream-pr` | upstream | Upstream's branch is in GitHub review. Review MAY change APIs — if so, upstream sends `status`/`handoff` and phase legally moves BACKWARD to `adopting`. | | `released` | downstream | Release landed (mail OR probe-verified); downstream runs `--exit`. | | `unlinked` | — | Terminal. Overrides removed, registry install verified. | When the counterpart publishes on merge (per the `--init` release-mechanism probe, below), `released` may arrive at any point from `handoff` onward — it is a side effect of the upstream's PR merging, not a step gated on the downstream's `findings`. Do not force the ball backward to `upstream-pr` to accommodate it: the downstream keeps its current phase and records a separate `pr-recorded` or `mail-received` journal event marking what arrived, then continues adopting. This improvisation was already used successfully in a live loop and is sanctioned here rather than ad hoc (savvy-web/systems#368). ## `--init`: start a loop Interactive-lite. Gather: counterpart path, this repo's role, packages to link. 1. Confirm `.claude/dogfood` is in this repo's `.gitignore`; add it if missing. Same check in the counterpart's `.gitignore` if you have write access to that checkout (you usually do — it's a sibling path). 2. Create the mailbox directories in both repos: `.claude/dogfood//` here, `/.claude/dogfood//` there. 3. **Downstream only:** derive the FULL transitive closure of linked `@scope/*` packages from the **BUILT manifest at the link path** — `/packages//dist/prod/npm/pkg/package.json` — never from the counterpart's SOURCE manifest and never from memory. The bundler rewrites `workspace:` protocols when it emits the publishable manifest, so a source manifest showing `"@scope/dep": "workspace:^"` becomes a plain registry range in the artifact you are actually linking; treating it as a workspace edge over-links a package that needed nothing. Add one `pnpm-workspace.yaml` `overrides:` entry per package genuinely in the closure: `"@scope/name": "file:/packages//dist/prod/npm/pkg"` (confirm the counterpart's actual build-output layout rather than assuming `pkg/`). **An unnecessary override is not merely redundant — pnpm overrides are GLOBAL.** It redirects EVERY reference to that package across the whole tree, including packages belonging to unrelated in-flight work, and substitutes whatever stale build happens to sit in the sibling's `dist/prod`. That has already silently swapped an unrelated dependency for an older build in a tree where another agent was reporting green gates (savvy-web/systems#425). Then run the refresh recipe (`pnpm clean --lockfile && pnpm install --ignore-scripts`), confirm link resolution (`node -e "require.resolve('@scope/name')"` or equivalent), and record `nativeRebuilds` by SCANNING the resolved dependency tree for native deps whose install scripts `--ignore-scripts` skipped. Derive that list; do not copy an example. On the Effect v4 line `@effected/store` compiles nothing (it uses `node:sqlite`), so a store consumer's list is empty — while a vitest repo's real answer is usually `esbuild`, whose platform binary is missing without the rebuild (savvy-web/systems#473). Finally, audit the closure you just linked: `node /scripts/override-audit.mjs /pnpm-workspace.yaml` — the script lives in this skill's `scripts/` directory, and the workspace file to audit is passed EXPLICITLY (with no argument it audits `pnpm-workspace.yaml` relative to the current directory, which is the wrong file whenever you run it from anywhere but the downstream repo's root). It warns for every override whose target the registry would already satisfy at a version matching what consumers declare — which is exactly the shape of an over-derived closure, and exactly the failure the built-manifest rule above exists to prevent. A warning is a prompt to re-check the derivation while a wrong entry is still cheap to remove, not a failure: a deliberate override of a package the upstream has previously published is the normal mid-loop state. 4. **Counterpart launch probe**, before generating anything meant to open a session there: read the counterpart's root `package.json`. Its `devEngines` field says which package manager runs there — do not assume your own. If its `scripts` block has a `claude` script, that script is the launch path (` run claude`), not raw `claude` — repos that bootstrap sessions (selective plugin enablement, env setup) only work through their own script, and raw `claude` silently skips it. Raw `claude` is the fallback only when no such script exists. 5. **Release-mechanism probe.** Read the counterpart's workflows and determine whether merging to its default branch publishes. If it does, release is a *consequence of merge*, not a step the upstream takes at `--exit` — a pre-release hold is not the upstream's to offer, and the phase machine must tolerate `released` arriving before `findings` (see Phase machine, above). Record the answer in the opening journal line's `note` (savvy-web/systems#368). 6. Append the opening `loop-started` journal line in BOTH repos — this is the one deliberate cross-repo write in the whole protocol (every other append is same-repo). `role`/`packages`/`nativeRebuilds`/`linkType` differ per side per the jsonl-journal reference's upstream-side note. **Whose ball opens the loop is a question about the opening mail, not about role.** The role-derived default (`requested` → upstream's ball) assumes the downstream's round-1 `request` has already been sent. A loop opened the other way — from a pre-filed issue, before any request mail exists — starts with the DOWNSTREAM owing the opening mail, and the default is inverted on both sides at once; left standing, the downstream's monitor reads `ball: theirs` and its session sits silent on a turn that is its own. When the side sending round 1's opening mail has not yet sent it, pass `--ball` on BOTH `--init` appends to state that (`--ball ours` on the side that owes the mail, `--ball theirs` on the other), and make the briefing say the same thing in prose per its contract in `references/mail-kinds.md`. 7. Generate the counterpart briefing (template: `references/mail-kinds.md` § `briefing`) and write it to the counterpart's mailbox as round-0 mail. 8. **When it2 is available** (see below): spawn the counterpart session directly, cd'd into its checkout, with the generated briefing as the opening prompt, and badge both panes by role. Otherwise, tell the user the briefing is ready at `` to hand-carry. ## `--send ` 1. Compose the mail from the matching content contract in `references/mail-kinds.md`. `request` and `findings` append to the loop's existing long-lived document when one exists for this round-sequence rather than fragmenting into a new file — only start a fresh file when the kind or the round genuinely warrants it (a `handoff` is always its own file per round; a `request`'s item-status table persists and grows). 2. Write it into the counterpart's mailbox: `/.claude/dogfood//YYYY-MM-DD-[-slug].md`. 3. Append the updated journal snapshot in THIS repo's own journal (`round`/`ball`/`phase`/`lastMail.out`, `event: "mail-sent"`) per the phase machine above — sending a `request` moves `ball` to `theirs` and `phase` to `requested`; sending a `handoff` moves `ball` to `theirs` and `phase` to `handoff`; etc. Get the direction right by checking the phase table's `Ball` column against the phase this mail causes. 4. Ring the it2 doorbell (below) when available. ## `--status` Render the last journal line per loop: role, phase, whose ball, and any unread inbound mail (files in `.claude/dogfood//` newer than that journal's `lastMail.in` — the monitor surfaces this passively; `--status` computes it fresh on demand). Report the recommended next action from the phase table. For a loop in `upstream-pr` or `released`, augment with the action-time GitHub/registry probes (read-only, using the session's existing `gh` auth — never a stored token): `gh pr view` for review state/checks/merged, `gh run list` for silk-release-action progress, and `npm view "@" version` ONCE PER PACKAGE (probe with the repo's own package manager — `pnpm view "@" version` in a pnpm repo — never a bare `npm view` from the repo root: npm 11 enforces `devEngines.packageManager` and fails there with EBADDEVENGINES; `override-audit.mjs` detects the manager itself) in the cut for whether the release actually landed — the exact expected pair, not a bare `npm view version`, which reads whatever the `latest` dist-tag currently points at rather than proving the cut version is published. A multi-package cut publishes staggered (packages have landed minutes apart with every release workflow already green), so "the release landed" means every expected version resolves, and `--watch`'s terminal condition is all of them resolving, not the first. These probes run only when `--status` (or `--watch`/`--exit`) is invoked — never from the background monitor, which stays filesystem-only. ## `--watch` An explicitly human-requested polling loop (the `/loop` pattern: self-paced interval, e.g. 5–15 minutes) over the same probes as `--status`, for "tell me when the effected release lands." This is the one sanctioned exception to no-polling in this skill, because a human asked for it and it has a terminal condition: end the loop the moment the probe goal is reached (PR merged / version visible on the registry), and surface the recommended next action (`--adopt` or `--exit`). Do not poll indefinitely or poll without having been asked. ## `--adopt` The downstream receive flow, run after a `handoff` lands: 1. Re-read the newest handoff mail. 2. Run the refresh recipe: `pnpm clean --lockfile && pnpm install --ignore-scripts`, then `pnpm rebuild ` for every entry in the journal's `nativeRebuilds`. **When the handoff brought a link closure this loop did not have before** — the normal case for a link-lazy loop that opened with `packages: []` — record it structurally as you install it: `--package '='` (repeatable) plus `--packages-derived true` on the next append. Do not pair `--packages-derived true` with an empty array, and do not describe the closure only in `note` prose: the first is the false known-clean signal the field exists to prevent, and the second puts the authoritative list where no reader can consume it (savvy-web/systems#508). See `references/jsonl-journal.md`. Whenever the closure changed, re-run `node /scripts/override-audit.mjs /pnpm-workspace.yaml` (see `--init` step 3 — the workspace path is passed explicitly) — a new entry the registry would already satisfy is the over-derivation signal, cheapest to act on now. 3. Verify the handoff's claims against the INSTALLED `.d.ts` (not the handoff's prose) — drift between what was claimed and what's actually exported/typed is a defect, flag it, don't silently work around it. 4. Migrate consumers onto the new surfaces. 5. **Bump the declared ranges while still linked.** This step reads like bookkeeping until you notice that **the safety net and the check are never present at the same time**: the `file:` override is the net, semver resolution is the check — while linked you have the net and no check, at `--exit` the check and no net. A stale range therefore cannot fail anywhere except the exact moment the net is removed, which is why it passes every gate green and then resolves back to the old versions against code calling the new surfaces (the framing is a downstream's own, from a loop that closed correctly for exactly this reason). The recurrence engine: caret ranges on a `0.x` package pin the minor — `^0.4.1` does not accept `0.5.0` — and on a kit where every meaningful change ships as a `0.x` minor, EVERY release lands outside EVERY range the downstream declares, so this is the default outcome, not a slip. While linked, `file:` overrides replace resolution outright and semver is never consulted; the ranges are consulted for the first time at `--exit`, after the safety net is removed. Bump the ranges now (savvy-web/systems#333), so the registry install at exit is a no-op rather than a downgrade. **Edit the manifest by hand** — `pnpm add @^` resolves the range against the REGISTRY and refuses a version the upstream has not published yet, which is every version worth bumping to mid-loop. The `file:` override supplies the actual build regardless of what the manifest declares, so the hand-written range is correct even though nothing on the registry satisfies it. **This step has no manifest to hand-edit when the range comes from a `configDependencies` catalog plugin** (see `--exit` step 2) — the catalog is fixed for every consumer until the upstream publishes a new plugin version, so there is nothing mid-loop to bump. There the pin bump at `--exit` *is* what does this step's job; there is no separate action to take here for that package. 6. Run the full gates: `types:check`, `build:dev`/`build:prod`, package tests. 7. Draft the `findings` mail (template in `references/mail-kinds.md`) covering what adopted cleanly, friction (with exact asks), handoff discrepancies, and design confirmations. Send it via `--send findings` when ready — `--adopt` drafts, it doesn't auto-send. ## `--exit` Role-aware endgame. Only after this completes does the enforcement hook lift and the branch may finalize (docs → changesets → squash → push → PR). **Upstream:** verify the release actually shipped via an action-time registry check — `npm view "@" version` (with the repo's own package manager, e.g. `pnpm view …` in a pnpm repo — a bare `npm view` from the repo root fails EBADDEVENGINES under a pnpm `devEngines` constraint; from outside the repo npm works, e.g. `(cd "$TMPDIR" && npm view …)`) for EVERY package in the cut (the exact pair; a bare `npm view version` only reads the `latest` dist-tag), terminal on the last one — then send the `release` mail. A partially-live cut is not a release: multi-package cuts publish staggered (two consecutive loops observed packages landing ~2 and ~5.5 minutes apart, with every release workflow already reporting success during the gap), so a green workflow does not mean every package is installable. The registry is the only oracle, and it has to be asked once per package; send the mail only after all of them resolve to their expected versions. **Downstream:** exit requires EITHER a `release` mail OR probe-verified registry presence of every linked package at the expected versions — the fallback for a long-dead upstream session, so the loop never deadlocks on a counterpart that no longer exists. Record whichever verification path was used (mail vs. probe, and the verified versions) in the journal as the audit trail. Then, in order: 1. Remove this loop's entries from `pnpm-workspace.yaml`'s `overrides:` block. (Commenting an entry out instead of deleting it is tolerated — the push guard strips YAML comments quote-awarely before scanning — but removal is still the documented step; don't rely on the tolerance as the normal path.) 2. `pnpm clean --lockfile && pnpm install` against the registry (scripts ON this time — native modules rebuild themselves; do not pass `--ignore-scripts` here). This install is the first time the declared ranges are consulted since the loop linked — the safety net is off and the check is finally present (`--adopt` step 5's whole argument). If that step was skipped, this is where stale ranges silently resolve back to the old versions. **The `--lockfile` drop is load-bearing, not hygiene, when the linked package is consumed through a `configDependencies` catalog plugin** (e.g. `@effected/pnpm-plugin-effect`) rather than a plain `catalog:` dependency — there are then TWO independent stale objects, not one: the `configDependencies` pin in `pnpm-workspace.yaml` (version *and* `+sha512` integrity together) and the lockfile itself, whose first YAML document pins the config dependency and whose body pins the resolved package versions regardless of what the pin now says. Bumping the pin and running a plain `pnpm install` reports "Lockfile is up to date, resolution step is skipped" and resolves the OLD versions — green and wrong (`okf/decisions/kit-effect-peers-via-catalog.md`, savvy-web/systems#536). Verify by RESOLUTION, never by reading a manifest: the installed package actually exports the new symbol, or `node -e` resolving it prints the new version. 3. Re-run the full gates (`types:check`, `build:dev`/`build:prod`, tests). 4. Append the terminal `unlinked` snapshot to the journal, clearing the closure explicitly (`--clear-packages`) so the final line states the tree is unlinked rather than leaving the last live override list standing. Only then is the enforcement hook satisfied for this loop and pushing/opening a PR is unblocked (assuming no OTHER active downstream loop is also linked). The `unlinked` snapshot is the loop's audit trail and is deliberately kept — no delete, no archive step. It is also **quiescent**: the background monitor treats a terminal `unlinked` snapshot as no-turn regardless of its `ball` value, so a finished loop emits zero events across new sessions (its last snapshot still carries `ball: "ours"`, but that is not an actionable turn). Reopening is explicit and deliberate — appending a fresh non-`unlinked` loop-started line makes the phase actionable again; the monitor never reopens a closed loop implicitly. ## Discipline - **Mailbox content is never design documentation.** The repository's design record (an `okf/` bundle, `.claude/design/`, or wherever the repo keeps durable design docs) holds what lasts; mail is history. When a mail thread produces a learning worth keeping past the loop's life, promote it into that record as part of a normal docs pass — don't let `.claude/dogfood/` become a second, informal design-doc tree. - **No push, no PR, from a tree carrying a machine-local link.** This is the whole reason the enforcement hook exists (`hooks/pre-tool-use/dogfood-guard.sh`) — a `file:`/`link:` override escaping the repo, in `pnpm-workspace.yaml`'s `overrides:` block, resolves only on this machine, and publishing it breaks CI and every other clone. The hook decides on TREE STATE, not journal bookkeeping alone (savvy-web/systems#387 fixed a false deny where journal role/phase alone denied a push on a tree with no override at all): a local override in that block denies `git push` / `gh pr create` / `gh pr edit` (Bash), the GitKraken MCP `git_push` / `pull_request_create` equivalents, and the GitHub MCP `create_pull_request` / `update_pull_request` / `push_files`. The `dev` branch is exempt unconditionally — it's a long-lived integration branch consumed as compiled bundles, so an override there is inert; for a Bash `git push`, that exemption applies to the pushed *destination* branch too, not only the currently checked-out one. Also for a Bash `git push` specifically, the hook resolves each refspec's source and, when it names committed content other than the current branch, scans that ref's `pnpm-workspace.yaml` via `git show` instead of the working tree (savvy-web/systems#603 follow-up 2) — otherwise a clean release branch cut from a linked checkout falsely denied, and a linked branch pushed by name from a clean checkout falsely allowed. `gh pr create`/`gh pr edit` and the MCP equivalents are unaffected — a PR targets a branch already on the remote, so there's no local refspec to resolve. With NO override present, a downstream journal in a non-`unlinked` phase only warns (via `additionalContext`), except when its `packagesDerived` is explicitly `false` — closure not yet derived, unknown rather than known-clean — which still denies on any branch but `dev` (or on pushed content the ref-aware scan above has already proven clean, where it stays advisory). Removing the override is `--exit` step 1's job (commenting it out also works, see `--exit`, below). There is no bypass flag — if the hook is genuinely wrong (e.g. the loop is stale and should have been force-exited), append a `correction` snapshot rather than routing around it. The upstream role is not push-guarded — its branch is expected to go to PR mid-loop (`upstream-pr`); its own "no release until the loop exits" discipline is enforced by `--exit`'s role-aware ordering, not by this hook. - **Handoff precision is a contract, not a courtesy.** Read exports and error unions from the built `.d.ts`, not from source or from memory of what was intended — the downstream verifies against installed types and treats drift as a defect. - **A coherent-kit runtime check is the upstream's responsibility before `handoff`.** Per-package gates cannot see a break that is a property of a *pair*. `@effected/walker@0.3.0` and `@effected/glob@0.2.0` were each internally coherent and gate-clean — types 21/21, `build:dev` 12/12, `build:prod` 38/38 with zero warnings — yet could not run together: `glob` renamed `compileSync` to `compileResult`, and `walker` was built four minutes before the rename. The call is internal to walker's implementation, so it appears in no `.d.ts` (savvy-web/systems#335). Run at least one runtime path crossing each internal peer edge before sending a handoff — today the downstream's `--adopt` test run is the de-facto gate, which is late and accidental. - **The coherence probe's fixture must contain the case under dispute.** Running the check is not enough — a differential probe only proves what its fixture exercises, and a fixture that omits the disputed case proves nothing about it (a downstream's own words, after the probe below passed against a broken port). In one loop, a memfs filesystem port was driven through the real consumer's entry points AND differentially compared against the real Node binding on a real tmpdir tree — both agreed, the probe passed, and the port was still broken: it answered symlinks literally where the consumer assumed resolution, so a symlinked package directory silently vanished from enumeration. The fixture had no symlink; the control was complete in every dimension except the one under dispute. Three rules follow. (1) If a divergence between two implementations is known, suspected, or documented, the fixture exercises it — otherwise the probe proves only that the implementations agree where nobody doubted them. (2) A caveat in a handoff ("X will differ") is a FIXTURE REQUIREMENT, not a disclaimer — writing the divergence down without a probe covering it is the documented form of this failure; a fixture containing the case converts the caveat into a failing test. (3) When mutating to prove a guard discriminates, confirm the mutant actually reaches the guarded behavior — a mutant absorbed by an unrelated code path leaves the test green for the wrong reason, and a mutation that fails to kill reads exactly like a passing guard. All three are one error: treating absence of failure as evidence when the setup could not have produced a failure. The controlled-grep rule below is the same instinct applied to artifacts; this is that instinct applied to fixtures. - **`file:` overrides copy, they do not symlink — so there are three clocks.** Upstream commit, upstream `dist/prod`, downstream `node_modules`, each capable of lagging the others. A downstream that verifies without reinstalling first is reading an artifact that may predate half the round. After ANY upstream rebuild, the downstream reinstalls before it verifies anything — not optional. One loop hit this twice (savvy-web/systems#391): once as a stale-artifact adoption report, once mid-verification, where a rebuild nearly passed adoption against a build older than the fix it was checking for. - **Verifying a change is present in a build output has a stated method — don't rely on instinct.** Search recursively (`rg ` or `grep -r`, never a non-recursive `/*.js` glob — `@savvy-web/bundler` emits per-module chunks under subdirectories, and a top-level glob misses all of them). Cite the module path where the symbol lives when reporting presence or absence, not a match count — a path is checkable by the other side, a count isn't. Before reporting a fix as missing from an artifact, grep for a symbol known to be present as a control, confirming the search would have found the real thing. Prefer the published `.d.ts` or `package.json` `exports` map for API-surface claims — chunk layout is a build-tool detail that changes. This exact mistake, a non-recursive glob read as "the fix is missing," happened twice in one round, 2026-07-25, by two different sessions — it costs a false rebuild request and a retraction, not a code fix. - **`request`/`findings` append, they don't fragment.** A long-lived item-status table that both sides keep current across rounds beats a new file per round that loses the running status. - **Sent mail is immutable.** Corrections and additions are new mail, never in-place edits to a file already sent. Amending a sent file failed three times in one loop (savvy-web/systems#391): a withdrawn filing reached an implementing agent as live scope because the amendment landed between the orchestrator's read and its dispatch, and an item appended to an already-read mail went unacknowledged across two rounds. Write a new file plus a `status` ping instead. - **A downstream clearance is necessary but not sufficient for merge — the release window closes at MERGE, not at findings.** A downstream answering "are you blocked?" is answering a question about ITSELF. Whether an API is still cheap to change is a question about the RELEASE BOUNDARY, which only the upstream can see: a surface that has never shipped can be reshaped for free while the PR is open, and the same reshape after publish is a breaking change against a live version. So a downstream may send "satisfied, merge whenever" in good faith and the upstream should still hold if items remain in flight — that is the upstream exercising judgement the downstream does not have the information to make, not ignoring the clearance. In the loop that produced this rule, holding through two further rounds turned seven would-be breaking changes into ordinary pre-release edits (savvy-web/systems#426). Downstreams: state readiness, do not press for merge. Upstreams: treat a clearance as information, and say so when you hold. - **`status` mail is cheap — send it.** Its only job is making a turn change land; err toward sending one on any milestone rather than letting the counterpart's session sit idle guessing. **A relay owes its downstream a status too.** When this repo is simultaneously downstream in one loop and upstream in another over the same change, progress in the upstream loop is a milestone in the downstream loop, not only the upstream one — send the downstream loop's counterpart a `status` when the upstream loop's counterpart ships, when adoption starts, and when this tree is safe to build against again. A downstream holding a `file:` link against this repo's build has no way to know the tree is mid-migration otherwise. ## Optional it2 transport layer When both sessions run in iTerm2 with the `it2` CLI installed (detect at runtime: `command -v it2` plus a session-id probe), the loop's ergonomics upgrade. The file mailbox stays the single source of truth and the full history; it2 is transport and session lifecycle, never payload — every feature below degrades gracefully to the base protocol when it2 is absent (headless, SSH, CI, non-iTerm terminals). Do not treat any it2 signal as authoritative over the mailbox/journal. - **Doorbell on `--send`.** After writing the mail file, ring the counterpart session directly: `it2 session send-text --retry 3 --retry-delay 2s` with a one-line notice (`dogfood mail: round — `). Session ids are ephemeral and are NEVER persisted in the journal — the target is discovered at send time by matching session cwd against `counterpart.path` (`it2 session list`). `--retry 3` is required: against idle sessions the first attempts fail with `Text not delivered` (exit 3, retryable) and delivery landed on the 4th try. Do not add `--require is-at-prompt` — the counterpart is a Claude Code TUI, not a shell prompt, so that check is a shell-integration probe that may never pass. No match found, or delivery still failing after the retries → skip the doorbell silently; the filesystem monitor is the backstop, not a fallback that needs its own error handling. - **Session spawn + role badges on `--init`.** `it2 session split` (or a new tab) cd'd into the counterpart checkout, launch the counterpart's Claude session via the counterpart launch probe's result (its own `claude` script if one exists, else raw `claude`), with the generated `briefing` mail as the opening prompt. Badge both panes by role: `it2 badge set "⬆ effected"` / `"⬇ savvy-web-systems"`. - **Counterpart state-watch during `upstream-pr` and `implementing`.** `it2 session claude-status` / `is-active` / `has-no-queued-claude-messages` answer "is the upstream session idle or mid-iteration"; `it2 session watch` (NDJSON events) gives event-driven wake-up instead of polling for a downstream's blind wait. - **Explicitly NOT adopted: it2's auto-approve/modal plugins** (`it2-session-claude-auto-approve` and similar). Cross-session auto-approval is permission laundering by automation — approvals stay with the human in each session, always. Do not wire this up even if it would make the loop feel smoother; it is out of scope on purpose, not an oversight. ## GitHub release-cycle integration, in one place `upstream-pr` and `released` outlive agent sessions. When the upstream enters `upstream-pr`, it appends a `pr-recorded` journal snapshot carrying `upstream.pr: {repo, number, url}` and sends a `status` mail with the same coordinates; the downstream mirrors those coordinates into its own journal. A `release` mail (or the original request doc) records the expected package set and versions. What GitHub probes do NOT replace: handoff content — pipeline state says *where* upstream is, never *what changed*; API deltas arrive only as `handoff` mail, never inferred from CI state. ## Monitor and enforcement, for context A background monitor (`monitors/dogfood-mail.mjs`, filesystem-only, no network — ever) surfaces new inbound mail and journal turn-flips passively; it never substitutes for running `--status`/`--adopt` yourself. It skips terminal `unlinked` snapshots (a completed loop is quiescent and fires no turn alert, regardless of `ball`), so a finished loop stops nagging across sessions without losing its journal. The enforcement hook (`hooks/pre-tool-use/dogfood-guard.sh`) is the mechanism behind the "no push while linked" discipline above — read that section, not this one, for what it actually does.