# FEEDBACK — mgf-fed

> Consumer-facing feedback queue for `mgf-fed` (DOC-10 / DOC-13). File friction, requests, and
> cross-project asks here (or via `mgf-fed feedback-howto`). Federation-wide concerns go to
> `mgf-common`'s FEEDBACK.md, not here (AG-11).

**Lifecycle (FB-01..04):** `OPEN` → `ACCEPTED` / `SCHEDULED` / `DEFERRED` / `❌ REJECTED` → `✅ SHIPPED`.
Each entry: what hurts · why · evidence (`file:line`) · suggested shape. A release that ships an
entry flips it to `✅ SHIPPED` with a one-paragraph retrospective (REL-07) + a consumer notification (REL-09).

---

### `PAPER-168` — a HALF-LOADED CLI reports its missing verbs as "invalid choice", so a broken install is indistinguishable from a smaller tool

| Field | Value |
|---|---|
| Status | ✅ SHIPPED — the degradation now announces itself in `--help` |
| Severity | 🟡 Sharp edge — it converts "this install is broken" into "that feature does not exist", to the operator and to any script parsing `--help` |
| Submitter | Court/CTO (found while restoring this bench after the 0.25.5 release) |
| Submitted | 2026-08-14 |
| Affects | `mgf-fed` ≤0.25.5 · `__main__._add_registered_subcommands` |
| Decision | ACCEPTED — fix shipped with the paper |
| Shipped in | `19cdc4d` (merged `fd089b5`, PR #192) — `__main__.py` epilog + two pins in `tests/unit/test_main.py`; released in **v0.25.6** |

**Concern.** `_add_registered_subcommands` guards the extended CLI behind

```python
except ImportError:
    return            # docstring: "No-op (silently) when the [cli] extra isn't installed"
```

The guard is **correct and deliberate** — the zero-dep orchestration core must
stay runnable. The *silence* is the defect.

**Measured, on this bench, by accident.** Reinstalling after the 0.25.5 publish
(`uv tool install --reinstall 'mgf-fed==0.25.5'`) dropped `mgf-common`, because
`dependencies = []` — the zero-dep promise from [[PAPER-159]]. The result:

```
$ mgf-fed --help        # 17 verbs listed, not 43
$ mgf-fed app-doctor --standards
mgf-fed: error: argument command: invalid choice: 'app-doctor'
        (choose from status, drift-check, local-index, gates, ...)
```

**`invalid choice` is the same sentence argparse prints for a verb that was never
implemented.** Nothing distinguished *"half this tool failed to load"* from
*"that feature does not exist"*. Twenty-six verbs — `app-doctor`, `standards`,
`catch-up`, `agents`, `explain`, `validate`, `logs`, `migrate` and more — were
gone, and the tool's own `--help` presented the remainder as the complete set.

**Why it matters beyond one confused operator.** This is the house's oldest
defect in the tool's own front door: **"I could not look" rendered as "there was
nothing to see."** Every gate in this repo exists to keep those two apart, and
the CLI that runs them conflated them. A CI step or agent that probes `--help`
for a verb before calling it would conclude the fleet's own orchestrator does not
support `app-doctor` — and would be right about the output and wrong about the
world.

**Resolution (shipped with this paper).** The guard stays; the silence goes.
`_add_registered_subcommands` now **returns the reason** instead of swallowing
it, and `build_parser` renders it as a `--help` epilog naming the symptom the
user actually sees, the reason, the missing verbs and the fix:

```
DEGRADED INSTALL — the extended CLI could not be loaded, so roughly half the
verbs (app-doctor, standards, catch-up, agents, explain, validate, logs,
migrate, …) are MISSING from the list above and will be reported as 'invalid
choice'. Reason: No module named 'mgf.common'. Fix: install the CLI extra …
This is a broken environment, not a smaller tool.
```

Pinned by two tests: one builds the parser with `mgf.common.cli` unimportable and
asserts the notice names symptom, reason and a missing verb; the other asserts a
**healthy** install stays quiet, so the notice is a fault report rather than
decoration. Sabotage-checked — restoring the bare `return` reds the first and
leaves the second green.

**Residual, deliberately not fixed here.** The notice tells the truth *after* the
fact; it does not make the dependency declared. Whether `mgf-common` should be a
real dependency, an extra, or stay undeclared is [[PAPER-159]]'s question —
`dependencies = []` is a promise the `audit` verb already breaks by importing
`yaml`. Two symptoms, one unresolved decision; recorded there rather than
pre-empted here.

---

### `PAPER-167` — `uv.lock` records the package's own version and nothing compares it: it shipped TWO releases stale, under green CI

| Field | Value |
|---|---|
| Status | ✅ SHIPPED — the pin test ships with the paper |
| Severity | 🟡 Sharp edge — a lockfile that misstates the thing it locks, with no gate between it and the index |
| Submitter | Court/CTO (mgf-family cleanup, 2026-08-14) |
| Submitted | 2026-08-14 |
| Affects | `mgf-fed` 0.25.2 → 0.25.4 · `uv.lock` · the release path |
| Decision | ACCEPTED — filed and fixed together |
| Shipped in | `e524020` (PR #191) — released in **v0.25.6** |

**Concern.** `uv.lock` carries the editable package's **own** version:

```toml
name = "mgf-fed"
version = "0.25.2"
source = { editable = "." }
```

Nothing compares that number to `pyproject.toml`. Measured across the tags:

| tag | `pyproject.toml` | `uv.lock` |
|---|---|---|
| `v0.25.2` | 0.25.2 | 0.25.2 |
| `v0.25.3` | 0.25.3 | **0.25.2** |
| `v0.25.4` | 0.25.4 | **0.25.2** |

**Two consecutive releases shipped a lockfile that misnamed the release**, and CI
was green for both.

**Why nothing caught it — and this is the interesting half.** The fleet's whole
`uv.lock` discipline is about *`--frozen`* (WF-14 / CIR-04 / PAPER-125: an
unpinned `uv sync` rewrites the lock mid-pipeline and silently swapped 9 pins to
pypi relics). That discipline is real and it works. But `--frozen` means *"do not
update the lock"* — so it makes a stale lock **quieter**, not louder. The rule
built to stop the lock changing under us also stops anyone noticing it is wrong.

`release_currency()` does not see it either: its model of installable content is
`src/` + `pyproject.toml`, so a lock that disagrees with both is outside the
question it asks. That is **board item 64's class** again — the hazard a check
names is not always the hazard a repo has.

**Why it matters.** A lockfile is the artifact a consumer is told to trust for
reproducibility. One that misstates the version of the very package it locks is a
small lie in a load-bearing place: anyone resolving `mgf-fed` from this lock gets
a graph pinned against a version that is not the one on the tag. Nothing broke
here — the editable self-entry is not what consumers install — but *"nothing broke
this time"* is exactly the argument that lets it run for two more releases.

**Evidence.** `git show v0.25.4:uv.lock | grep -A2 'name = "mgf-fed"'` → `0.25.2`.
Found by hand during the 0.25.5 release, not by any gate; the sibling
`PUBLIC_API.json` drift on the same bump *was* caught, by its own pin test.

**Suggested shape.** A one-line check, in an exit code rather than a habit:
assert `uv.lock`'s own `[[package]]` entry for this project equals
`pyproject.toml`'s `version`. It belongs next to the `PUBLIC_API.json` pin test
(which is the working precedent — same class of defect, same repo, already
caught) and/or in `mgf-fed release-local`'s pre-req list, so a release cannot be
cut over a lock that misnames it. Cheap, deterministic, no network.

**Wider ask, if it generalises:** the version lives in **six** places in this repo
— `pyproject.toml`, `src/mgf/fed/__init__.py`, `CHANGELOG.md`, `PUBLIC_API.json`,
`uv.lock`, and `FEEDBACK.md`'s `Shipped in`. Two of the six are gated. A single
`version-consistency` check that reads all of them and names the disagreeing file
would retire this whole class rather than its newest instance.

---

### `PAPER-166` — `scaffold` will overwrite a pipeline with one whose gate set is a strict SUBSET, silently — the exact harm it already guards `.gitleaks.toml` against

**Severity:** 🔴 Blocker (the write removes running gates from a protected
branch and reports it as `applied 1 byte-exact file(s)`; the loss is invisible
until something that should have been caught is not)
**Origin:** figrecovery · 2026-08-14 · found while fixing mgf-standard PAPER-167

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | figrecovery (clean-baseline sweep) |
| Submitted | 2026-08-14 |
| Affects | `mgf-fed` 0.25.4 · `scaffold` (`src/mgf/fed/_scaffold.py:95-99`) |
| Decision | pending |

**What hurts.** `scaffold`'s `.woodpecker.yml` path is an unconditional
overwrite:

```python
current = wp.read_text(encoding="utf-8") if wp.is_file() else None
if current != want_wp:
    applied.append(".woodpecker.yml (generated)")
    if not dry_run:
        wp.write_text(want_wp, encoding="utf-8")        # _scaffold.py:99
```

Nothing compares what the on-disk pipeline *runs* against what the render
runs. If the repo's pipeline executes gate commands the template has no notion
of, they are deleted, and the only output is `applied 1 byte-exact file(s)`.

**Why this is the sharp version of the complaint.** mgf-fed **already knows
this is harmful and already solved it — for a different file.** Thirty lines
earlier, `.gitleaks.toml` gets a shape-aware, non-destructive merge, with a
comment naming the harm (`_scaffold.py:70-74`):

> *"a consumer file whose additive allowlist extras are the only difference is
> conformant — never clobber it back to the template (that would wipe the
> repo's legit fixture allowlists, the PAPER-03 harm)"*

That reasoning transfers **more** strongly to the pipeline, not less: an
allowlist entry lost from a secret-scan config is a false positive tomorrow; a
gate command lost from CI is a defect class that stops being detected at all,
on the branch protection is supposed to defend.

**Evidence (measured, not hypothetical).** figrecovery's render was applied,
diffed `+432/-263`, read, and reverted. Three gate commands its CI runs today
would have been deleted outright: `bash scripts/check-codename.sh` (a rule
forbidding the repo from naming a vendor — enforced server-side *precisely* so
it cannot be skipped with `--no-verify`), `bash scripts/check-test-mirror.sh`
(TS-01/RA-17), and `uv run --frozen lint-imports` (DP-01, what keeps its CLI
from importing PySide6 at module scope). The `required_commands` gate floor did
not object, and could not: it checks three fixed fleet commands, not the ones
this repo added. Full before/after in mgf-standard `PAPER-167`.

**Consequence.** `scaffold` is the remedy `drift-check` *names* for
`[ci-generated]`, so this is reachable from a routine hygiene prompt, by an
agent, in one word. figrecovery survived only because the render was read
before being trusted.

**Suggested shape.** Give the pipeline the care `.gitleaks.toml` already gets.
Minimum: before writing, extract the command lines from both the on-disk file
and the render; if on-disk runs a command the render does not, **refuse and
name it** (`scaffold: refusing — .woodpecker.yml runs 3 gate command(s) the
render does not: …; pass --force or add an [exempt].woodpecker entry`). That is
a strict improvement even with no merge logic, because the failure mode today
is *silence*. Better: carry the extra commands forward the way
`gitleaks_restore_text` carries additive allowlist entries. Note the check must
compare **commands**, not step names — the same distinction PAPER-120/121
already drew for the gate floor: *"a step NAME is not a gate."*

### `PAPER-165` — the rendered pipeline hardcodes `branch: main` under a comment promising a generator parameter that does not exist

**Severity:** 🟡 Sharp edge (a member whose default branch is not `main` gets a
push pipeline that never triggers — and a pipeline that does not trigger
reports nothing, so it reads as "no failures")
**Origin:** figrecovery · 2026-08-14 · found while fixing mgf-standard PAPER-167

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | figrecovery (clean-baseline sweep) |
| Submitted | 2026-08-14 |
| Affects | `mgf-fed` 0.25.4 · `_woodpecker.py:680-692` (`_substitute` token table) · co-change in `mgf-standard/templates/woodpecker/base.yml:20` + `base-zig.yml:19` |
| Decision | pending |

**What hurts.** The canonical template pins the push trigger to a literal, and
excuses it in the same line:

```yaml
  - event: push
    branch: main  # the fleet default branch (a repo on another default is a
                  # generator param, not a hand-edit — WF-13)
```

**There is no such generator parameter.** `_substitute` (`_woodpecker.py:680`)
replaces a fixed `{{TOKEN}}` table — `{{IMPORT_NAME}}`, `{{DIST_NAME}}`,
`{{EXTRAS_INSTALL}}`, `{{COVERAGE_FLOOR}}`, `{{CONSOLE_SCRIPT}}`, … — and
carries no branch token. Measured: `grep -c branch src/mgf/fed/_woodpecker.py`
returns **0**. The renderer has no concept of a default branch at all.

So the comment is not describing a knob a maintainer can reach; it is
forbidding the only fix that currently exists (the hand-edit) while pointing at
one that does not.

**Why it matters.** This fleet is mixed — `main` for generated repos, `master`
for figrecovery and the four `sooth*` repos. Rendering onto any of them
produces a push pipeline that never fires. That failure is **silent by
construction**: no run, no red, no notification, and a green-looking board.
It is the same class as `PAPER-122` (a member whose every Woodpecker run
errored, unnoticed) and as the counterfeit-default-branch bug the
`fetch-default-branch` step exists to kill.

The irony is local: the very next step the same render emits reads the branch
correctly and *refuses to guess it* —

```yaml
- 'test -n "$CI_REPO_DEFAULT_BRANCH" || (echo "fetch-default-branch: …
   refusing to guess the default branch" >&2; exit 1)'
```

— so the render already treats "assume the branch" as a bug worth failing on,
twenty lines below where it assumes the branch.

**Suggested shape.** Add `{{DEFAULT_BRANCH}}` to the `_substitute` table,
derived from the member's actual default (the roster, or `git symbolic-ref
refs/remotes/origin/HEAD` on the checkout), and land the matching
`branch: {{DEFAULT_BRANCH}}` edit in `mgf-standard`'s `base.yml:20` and
`base-zig.yml:19` in the same wave — the template is that repo's file, so this
one needs both halves. Belt-and-braces: have `drift-check` flag a rendered
`when.branch` that does not match the member's real default, so a future
regression is caught by a gate rather than by a member noticing its CI went
quiet.

*(Both papers above were found while implementing mgf-standard `PAPER-167`,
which enrolled figrecovery as an app-tier member. That fix cures figrecovery.
These two are what would stop the same thing happening to the next member.)*

---

### `PAPER-164` — `status`'s CI column is a root-only file check, so a multi-pipeline repo reads as CI-less while its CI is green on the forge

**Severity:** 🟡 Sharp edge (the fleet dashboard contradicts the forge, and the
tool that tells agents "the filesystem is a cache, the registry is the truth"
is itself concluding absence from a filesystem probe — AG-20's own anti-pattern)
**Origin:** hrb-app · 2026-08-07 · found during the desktop-GUI estate review

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | hrb-app (estate review, 2026-08-07) |
| Submitted | 2026-08-07 |
| Affects | `mgf-fed` ≤0.25.2 · `status` (`src/mgf/fed/_status.py:97`: `has_ci=(d / ".woodpecker.yml").is_file()`) |
| Decision | pending |

**What hurts.** `mgf-fed status` renders hrb-app as the ONLY python member of
39 without a CI tick (`CI ·`), while hrb-app's CI is live and green on the
forge — both required contexts (`ci/woodpecker/pr/ci`, `ci/woodpecker/push/ci`)
reported success on the PRs merged 2026-08-07.

**Why.** hrb-app deliberately moved to the multi-pipeline layout in commit
`80b918d` — tracked files are `.woodpecker/ci.yml` and `.woodpecker/e2e.yml`,
no root `.woodpecker.yml`. `_status.py:97` only probes the root filename. The
rest of mgf-fed already accepts both layouts: `cli/_catch_up.py:3787-3796`
explicitly checks "1. `.woodpecker.yml` at repo root, OR 2. `.woodpecker/*.yml`
(multi-pipeline layout)". `status` was not updated to match, so the two verbs
disagree about the same repo.

**Consequence.** A fleet-status consumer (human or agent) reads `·` as
"missing pipeline" and either files a false defect or — worse — scaffolds a
root `.woodpecker.yml` onto a repo that split its pipelines on purpose.

**Suggested shape.** Mirror catch-up's two-layout acceptance in `_status.py`
(`has_ci = root_file.is_file() or any((d / ".woodpecker").glob("*.yml"))`), or
factor the existing catch-up predicate out and call it from both verbs so the
two can never disagree again.

### `PAPER-163` — the canonical AG-03 `check_commit_trailers.py` template regresses a consumer's base-resolution + subprocess-hardening fixes, and `catch-up` pressures the downgrade

**Severity:** 🟡 High (a gate that silently no-ops on `master`-default repos is
the worst failure mode a gate can have, and `catch-up --apply` actively
reinstalls it over a consumer copy that fixed the hole)
**Origin:** figrecovery · 2026-07-30 · drafted in its `pending-feedback/`
outbox against mgf-fed 0.18.2, relayed 2026-08-07; re-provoked by the 0.1.79
scaffold wave (figrecovery `7bb759b`, 2026-08-06)

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | figrecovery (relayed from its FB-05 outbox) |
| Submitted | 2026-07-30 (relayed 2026-08-07) |
| Affects | the canonical `check_commit_trailers.py` scaffold template · `catch-up --apply` staleness grading |
| Decision | pending |

**What hurts.** `mgf-fed catch-up --apply` (0.18.2) replaced figrecovery's
`scripts/check_commit_trailers.py` with the canonical template — an older,
weaker version of the same script. The canonical template:

1. **Hardcodes `main` as the base-branch fallback.** On a repo whose default
   branch is `master` (figrecovery; Forgejo's default for older repos), an
   unset `CI_COMMIT_TARGET_BRANCH` (every push event) makes the range
   unresolvable — and since the gate deliberately fails OPEN on an
   unresolvable range, the check becomes a **silent no-op**. figrecovery's
   version resolves the base via candidates — `CI_COMMIT_TARGET_BRANCH`, then
   `origin/HEAD`, then `main` AND `master` — precisely to close this hole
   (fixed 2026-07-27 after it was found silently no-opping in that repo).
2. **Drops the resolved-absolute-path subprocess hardening**
   (`GIT = shutil.which("git")`, S607) and the per-call S603 justifications,
   which the federation's own ruff gate (`S` family) flags.

**Consequence.** Any repo that takes the canonical file and has a `master`
default branch ships a commit-trailer gate that never fails. And every
`catch-up` run against a repo carrying the FIXED version reports it as
"stale", pressuring a downgrade — the scaffold class re-landed on figrecovery
in the 0.1.79 wave, which had to re-apply its local proof on top
(`7bb759b`, "inherit the 0.1.79 trailer scaffold — and keep the local proof").

**Suggested shape.** Adopt figrecovery's `scripts/check_commit_trailers.py`
(HEAD of the 2026-07-30 clean-state landing) as the canonical template:
candidate-based base resolution + absolute git path. Until then, `catch-up`
should treat a NEWER-than-template consumer copy as "locally advanced", not
"stale" (same class as the managed-file downgrade recorded in figrecovery's
`LESSON_LEARNED.md`, 2026-07-30).

### `PAPER-161` — the resolver #166 fixed has a twin: `_roster._default_ref` still hardcodes `origin`, and every origin-view consumer inherits it

**Severity:** 🟡 Sharp edge (the fix the fleet just released is silently
incomplete for a whole consumer family — same defect, different import path)
**Origin:** TelePlasma · 2026-08-07 · the 0.1.80 release ceremony's mirror
renders still printed the PAPER-152 degradation banner under mgf-fed 0.25.2

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (waiver-triage round, 2026-08-07) |
| Submitted | 2026-08-07 |
| Affects | `mgf-fed` ≤0.25.2 · `_roster._default_ref` (`src/mgf/fed/_roster.py:181-191`) · `_origin_view.materialize` (`:50`, `:165`, and the hardcoded `fetch origin` at `:172`) · every origin-view consumer: `registry --check`, `registry-consistency`, drift tooling |
| Decision | pending |

**What hurts.** #166 (0.25.2) taught `_audit._default_ref` and the freshness
probe to resolve the configured remote — but `_roster.py:181` carries an
independent copy of the pre-fix resolver (`refs/remotes/origin/HEAD`, then
`origin/{main,master}`), and `_origin_view` imports THAT one (`:50`). Its
opt-in fetch is also literal: `_git(repo_dir, "fetch", "--quiet", "origin")`
(`:172`). So under the very release that shipped the class fix, every
origin-view materialization still degrades a `codeberg`-remote repo to its
working tree:

```
⚠️  ORIGIN-DEFAULT VIEW INCOMPLETE — part of this verdict is BENCH state.
    2 of 66 member(s) could not be read at their origin default ...
      - PlasmaAI — no origin default branch (unfetched clone, or no `origin` remote)
      - soothrede — no origin default branch (unfetched clone, or no `origin` remote)
```

(live under `mgf-fed[cli] 0.25.2`, 2026-08-07 — the 0.1.80 registry render
and the same-day audit regens all carried it).

**Why it matters.** The soothrede waiver died because the *audit path* now
resolves the remote; the *mirror path* answering "no origin default branch"
about the same repo is the identical equivalence ("I could not look" reading
as bench-fallback) one import away. Two resolvers with one name and different
behavior is how the next session re-learns this the hard way.

**Suggested shape.** One resolver: lift #166's configured-remote logic into
the shared home (`_roster`), have `_audit` and `_origin_view` import it, and
fetch the remote the ref names rather than the literal `origin`. The
signatures differ trivially (`str` vs `str | None`); the policy should not.

**Family:** PAPER-152 (bench-vs-fleet), #166 (the half that shipped).

---

### `PAPER-162` — `release-remote` verifies the BENCH's version, so a release that exists only on `origin/main` is invisible to the release tool

**Severity:** 🟡 Sharp edge (the release automation cannot see the release it
is being asked to make whenever the bench is parked on another branch — which
is exactly when a chair runs it from a worktree-based ceremony)
**Origin:** TelePlasma · 2026-08-07 · the mgf-fed 0.25.2 release itself

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (waiver-triage round, 2026-08-07) |
| Submitted | 2026-08-07 |
| Affects | `mgf-fed` ≤0.25.2 · `release-remote` / `_release_remote_one` (`src/mgf/fed/_release.py:225-226`: `version = _version(m.repo_dir)`; `_version` at `:74` reads the working tree's pyproject) |
| Decision | pending |

**What hurts.** Minutes after #166 merged the 0.25.2 bump to `origin/main`,
`release-remote --dry-run mgf-fed` answered:

```
✓ verify — 0.25.1 already published — nothing to do
```

because the bench checkout was parked on another session's branch still
declaring 0.25.1. The tool read `m.repo_dir`'s pyproject — the PAPER-152
shape, in the one verb where "the fleet as it EXISTS" matters most: the
version being released is a property of `origin/<default>` (or the tag), never
of whatever branch the bench happens to have out. 0.25.2 shipped via the raw
tag-push path (MGF_INFRA §2) instead, which works but skips every pre-req
check this verb exists to run.

**Suggested shape.** Resolve the version from `origin/<default>` (or an
explicit `--ref`/tag argument) by default, with the bench read available as a
deliberate preview — the exact `--source=origin|worktree` split `registry`
already grew in 0.25.1, applied to the release family.

**Family:** PAPER-152; the registry verb's own `--source` precedent.

---

### `PAPER-159` — bare-core `audit` crashes mid-verb on `import yaml`: the zero-dep promise and the audit verb disagree

**Severity:** 🟡 Sharp edge (the documented regen command for mgf-standard's audit artifact dies with a traceback on a clean bench — and it is the command the waiver-triage runbook sends a session to on a red board)
**Origin:** TelePlasma · 2026-08-07 · hit by execution during the waiver-triage prep rehearsal (TelePlasma `docs/memory/waiver-triage-2026-08.md`; outbox `pending-feedback/mgf-fed.md`)

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (waiver-triage prep, 2026-08-07) |
| Submitted | 2026-08-07 |
| Affects | `mgf-fed` ≤0.25.1 · `audit --write` / any YAML-touching check (`src/mgf/fed/_audit.py:2384`, `_safe_yaml`) · every bench that follows the documented regen command |
| Decision | pending |

**What hurts.** The regen path documented by the wave finale and the triage
runbook —

```bash
uv run --no-project --with "mgf-fed>=0.25.1,<0.26" mgf-fed audit --write
```

— crashes on a clean bench:

```
File ".../mgf/fed/_audit.py", line 2384, in _safe_yaml
    import yaml
ModuleNotFoundError: No module named 'yaml'
mgf-fed: unexpected error in 'audit': ModuleNotFoundError("No module named 'yaml'")
```

`mgf-fed`'s core declares **zero dependencies** (`dependencies = []`); `pyyaml`
arrives only through `[cli]`'s transitive graph. CI never sees this because
`base.yml` installs `mgf-fed[cli]` — the gap lives precisely on the surface the
command is documented for (a bare federation-root run). The verb is *core*: it
runs, sweeps several checks, and only dies when `ci-workspace-truth` parses
pipeline YAML — so the pyproject comment "the zero-dep orchestration core …
stays runnable without [cli]" is not currently true of `audit`.

**The grace worth keeping.** `--write` did **not** half-write: the artifact
lands only at end-of-run, so the crash left the worktree clean. Whatever the
fix, preserve that property.

**Suggested shape.** One of:
- **(a)** guard the lazy import — catch `ModuleNotFoundError` in `_safe_yaml`
  (or at `audit` verb entry) and fail with an actionable line: "`audit` needs
  PyYAML: install `mgf-fed[cli]` or add `--with pyyaml`"; **plus (b)** document
  the real contract (`audit` requires `[cli]` or explicit `pyyaml`) in `--help`
  and the runbook-facing docs; or
- **(c)** declare `pyyaml` a core dependency and scope the zero-dep claim to
  the verbs it is actually true of.

The defect is the current state: the documented command crashes and the fix is
folklore. Verified fix-in-hand: re-running with
`--with "mgf-fed[cli]>=0.25.1,<0.26"` succeeds (2026-08-07 rehearsal).

---

### `PAPER-153` — `bench rebaseline` resolves `tests/perf` against its OWN install, so the documented re-baseline command runs from nowhere — including mgf-fed's own checkout

**Severity:** 🟡 Sharp edge (a documented command that cannot succeed anywhere, quoted as the remedy in the failure messages of the gate it is supposed to fix — the reader hits it at the worst possible moment)
**Origin:** TelePlasma · 2026-08-06 · found while closing PC-42 / PF-08 (court round L9-F4, TelePlasma PR #83 `3753a00`)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-08-06 — fixed same session; see Decision rationale) |
| Submitter | TelePlasma (PC-42/PF-08 wave) |
| Submitted | 2026-08-06 |
| Affects | `mgf-fed` ≤0.24.0 · `bench rebaseline` (`src/mgf/fed/cli/_bench.py:107`) · every consumer told to run it, and mgf-fed itself |
| Decision | ACCEPTED — option 1 ("operate on the caller's checkout"), fixed same session |
| Shipped in | `81ce990` (v0.25.0) |

**What hurts.** `_run_rebaseline` locates the suite relative to **its own module
file**, not to the caller:

```python
# src/mgf/fed/cli/_bench.py:107
perf_dir = Path(__file__).resolve().parents[4] / "tests" / "perf"
if not perf_dir.is_dir():
    print(
        "bench rebaseline: tests/perf/ not found — this command runs only "
        "from an mgf-common source checkout (it re-records the library's "
        "own baselines).",
        file=sys.stderr,
    )
    return 2
```

The `parents[4]` idiom is correct-by-layout for `src/mgf/<pkg>/cli/_x.py` → repo
root, and `_version.py:40-41` still carries the comment explaining it. But the
error message names **mgf-common** while the code now ships in **mgf-fed** — this
is 0.47 CLI-split residue. The path can no longer reach mgf-common's tree from
anywhere, so the guard is not a helpful restriction; it is the only reachable
branch.

**Why it matters.** The consumer-facing half is what TelePlasma hit: a perf gate
goes red, the failure message says to run `mgf-fed bench rebaseline`, and the
command dead-ends. That is a dead end at precisely the moment a red gate has sent
someone looking for the remedy — the highest-trust, lowest-patience moment a CLI
gets. TelePlasma has de-quoted it in PR #83 and now documents
`MGF_PERF_REBASELINE=1 uv run --frozen pytest -m perf`, which works.

But checking the resolution before filing turned up something broader: **there is
no directory this command succeeds from.** Both modes fail:

- **Installed (the normal case).** On this bench, `_bench.py` lives at
  `~/.local/share/uv/tools/mgf-fed/lib/python3.13/site-packages/mgf/fed/cli/_bench.py`,
  so `parents[4]` is `…/lib/python3.13` and it looks for
  `…/lib/python3.13/tests/perf` — a path that will never exist under any install
  layout.
- **From mgf-fed's own source checkout.** `parents[4]` correctly resolves to the
  repo root — and **mgf-fed has no `tests/perf` directory at all** (`ls` confirms;
  the perf suite belongs to mgf-common). So the source-checkout path fails too.

So the command is not "restricted to one repo". It is unreachable, its error text
points at a repo it can no longer see, and nothing tests it — which is how a
subcommand can ship in a released CLI while succeeding zero times.

**Evidence.** All verified 2026-08-06 against `mgf-fed` 0.24.0 / `origin/main`
`84a8c75`:

- `src/mgf/fed/cli/_bench.py:107` — the resolution, quoted above; `:110-113` the
  message, `:114` the `return 2`.
- `src/mgf/fed/cli/_version.py:40-41` — the same `parents[4]` idiom with the
  comment that documents the assumed layout (`src/mgf/common/cli/_version.py`),
  i.e. the shape this was written against pre-split.
- Installed-path resolution computed directly:
  `parents[4]` → `~/.local/share/uv/tools/mgf-fed/lib/python3.13`; `tests/perf`
  under it → **does not exist**.
- `mgf-fed` repo tree: **no `tests/perf`** anywhere.
- TelePlasma's de-quoting + the working alternative: `tests/perf/hw_tiers.md`,
  `tests/perf/conftest.py`, and the PC-42 closure in
  `docs/inprogress/PERFORMANCE_AND_CAPACITY.md`, all at `3753a00` (PR #83).

**What I'd want** — either resolves it; the first is the smaller change.

1. **Operate on the caller's checkout.** Resolve `tests/perf` from `Path.cwd()`
   (walking up to the repo root), with an explicit `--path` override for the
   unusual case. That makes the command mean *"re-baseline the suite I am
   standing in"*, which is what every reader already assumes it means, and it
   becomes genuinely fleet-wide rather than nominally so.
2. **Or delete it and document the env-var flow.** `MGF_PERF_REBASELINE=1 pytest
   -m perf` is what actually works today, it is what the harness already keys on,
   and a CLI verb that wraps it adds little. Removing it is honest and costs
   consumers nothing — but if it goes, the failure messages and docs that quote
   it need to move to the env-var form in the same wave, or this recurs as a
   dangling reference.

Whichever way it goes, the fix wants a test that **runs the command from a
directory that is not the package's own install** — the absence of one is the
reason this survived a CLI split.

**Relationship to PAPER-152.** The same family, and I think genuinely so rather
than by stretch: a tool resolving a path against **its own location** instead of
**the caller's context**. PAPER-152 is the fleet-state version (gates reading the
bench's checkouts as the fleet); this is the single-command version. They are
independent defects with independent fixes — no shared code path — so this is
offered as a pattern observation, not a merge suggestion. If a principle is worth
writing down anywhere, it is that a fleet tool's *subject* should be the caller's
context unless it explicitly says otherwise.

**Decision rationale.** ACCEPTED and fixed same session (founder-delegated triage,
2026-08-06 — see the commit that flips this row); ships in **0.25.0**. Option 1 as
recommended: `_resolve_perf_dir` searches `Path.cwd()` and each parent for
`tests/perf/`, with `--path` as the explicit override — accepting either a repo root
or the perf directory itself, and nothing else guessed. Option 2 (delete the verb)
was declined: the paper is right that the env-var flow works, but the verb is quoted
in failure messages across the fleet and a removal would trade a dead end for a
dangling reference; making it mean what every reader already assumes is the smaller,
truer change. Two details beyond the ask. The error message now names **what** was
looked for (`tests/perf/`) and **where** (the starting directory, the number of
parents walked, and the way out) — the old text named `mgf-common`, a repo the code
could no longer reach from anywhere, which is the 0.47 CLI-split residue this paper
found. And the re-baseline subprocess now runs **from the resolved tree's root**, so
`--path ../other-repo` cannot re-record one repo's numbers under another repo's
pytest config.

The paper's closing sentence is the part that mattered most, and it is honoured
literally: `tests/unit/cli/test_bench.py::TestRebaselineFromOutsideTheInstall` runs
the real CLI entry (`python -m mgf.fed bench rebaseline`) as a subprocess with its
`cwd` in a seeded tmp tree, and asserts the suite that ran is the one in that tree.
An in-process test could never have caught this — it inherits the runner's cwd, which
is always a checkout. That absence is precisely how a subcommand shipped in a released
CLI while succeeding from zero directories, and the negative fixture (same tmp dir,
no seeded suite → exit 2, message names `tests/perf`, no phantom `mgf-common`) keeps
the dead end from coming back quietly.

The paper's pattern observation is accepted as stated and recorded here rather than
merged into PAPER-152: **a fleet tool's subject is the caller's context unless it
explicitly says otherwise.** PAPER-152's banner is the fleet-state half of the same
principle, fixed in the same PR by a different mechanism — they share the lesson, not
a code path.

### `PAPER-152` — fleet tools read the BENCH's working trees and call it the fleet's state: `registry --check` and `drift-check` grade a state that exists on no remote

**Severity:** 🟡 Sharp edge (the tools run, exit non-zero, and name specific members — the verdict is simply about a different fleet than the one that exists; and it points the operator at a "fix" that is a regression)
**Origin:** TelePlasma · 2026-08-06 · found while enacting the founder-ratified PAPER-151 chain, whose own verification it nearly false-negatived

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-08-06 — offer 1 in 0.25.0; offers 2+3 in **0.25.1**, trigger fired early by the incident below) |
| Submitter | TelePlasma (PAPER-150/151 enactment + the 0.1.78 registry render) |
| Submitted | 2026-08-06 |
| Affects | `mgf-fed` ≤0.24.0 · `registry --check` (via `mgf-standard scripts/gen_mgf_libraries.py`) · `drift-check` (`src/mgf/fed/_roster.py:29-30`) · any bench with parked feature branches |
| Decision | ACCEPTED — all three offers shipped: the banner (0.25.0), then origin/`<default>` resolution + `--source=` (0.25.1) |
| Shipped in | `4c948b6` (v0.25.0) + `c525492` (v0.25.1) |

**What hurts.** Both gates resolve each federation member by reading **whatever
is checked out on the local bench**, and report the result as the fleet's state.

- `registry --check` renders from on-disk trees: `gen_mgf_libraries.py:958-959`
  computes `federation_root = repo_root.parent` and walks
  `sorted(federation_root.iterdir())` (`:431`), reading each sibling's working
  copy.
- `drift-check` reads the registry from the mgf-standard **checkout**, never the
  installed wheel: `_roster.py:29-30` is `_registry_path(root) → root /
  "mgf-standard" / "MGF_LIBRARIES.json"`, with `root` resolved from `$MGF_ROOT` →
  `~/PycharmProjects` → cwd (`:20-26`).

A bench is a workspace, not a mirror. On this one, **51 of the 53 registry
members are sitting on parked feature branches** (62 of 67 git repos under
`MGF_ROOT`). So the gates grade a fleet that exists on no remote — and the
failure is not "slightly stale", it is **wrong in both directions at once**: a
checkout behind its own release under-records, and a checkout carrying an
unmerged walk branch over-records pins that were never shipped.

**Why it matters.** This is not theoretical and it is not a near-miss — I ran it
today, and the gate produced **false REDs naming a specific member**:

```
$ mgf-fed registry --check          # exit 1, 2026-08-06
REGISTRY CURRENCY: STALE
FEDERATION CONSISTENCY GATE: RED
  - TelePlasma pins `mgf-fed [cli]>=0.24.0,<0.25` but mgf-fed is at 0.23.0
      — pin EXCLUDES the current version (walk it forward + verify).
  - TelePlasma pins `mgf-standard >=0.1.78,<0.2` but mgf-standard is at 0.1.77
      — pin EXCLUDES the current version (walk it forward + verify).
```

**Both findings are false, and TelePlasma's pins are exactly right.** On
`origin/main`, `mgf-fed` **is** 0.24.0 (PR #156) and `mgf-standard` **is** 0.1.78
(mgf-standard PR #303). The gate said 0.23.0 / 0.1.77 because it read two
checkouts parked on feature branches — mgf-fed's on
`fix/ci-workspace-truth-invocation`, mgf-standard's 6 commits behind `origin/main`
on `fix/mgf-standard-own-fetch-step`.

The harm is the remediation text. *"Walk it forward + verify"* on a correct pin
means **downgrading `>=0.24.0` to match a version that only exists in a stale
working directory** — the gate's own instruction is the regression. An operator
who trusts the gate makes the fleet worse; the only defence is knowing the tool
well enough to distrust it, which is the opposite of what a gate is for.

Two further consequences, both observed:

1. **The registry itself is graded against the wrong source.** `MGF_LIBRARIES.md`
   was reported STALE against a committed registry that is default-branch-**true**
   — the 0.1.78 release rendered it from each member's `origin/<default>` and got
   two consecutive byte-identical renders. The naive on-disk render disagreed in
   both directions: mgf-fed under-recorded (checkout behind its own release),
   `vm-vmanager-app` / `vm-vmanager-agent` over-recorded (unmerged walk branches'
   pins written into shipped wheel data). That last one is **the 0.1.75 failure
   class recurring** — unshipped state baked into a published artifact.
2. **It nearly false-negatived PAPER-151's own verification.** Measuring the
   PAPER-151 enactment reproduced the OLD findings verbatim, because `drift-check`
   was reading the 10-commits-divergent mgf-standard checkout instead of the
   released policy. The false negative was dodged only by re-measuring against
   `git archive` of the released tag. A fix that had genuinely landed looked
   unlanded — the most expensive shape of wrong, because it invites re-fixing
   something that is already correct.

**Evidence.** All reproduced on this bench, 2026-08-06, against `mgf-fed` 0.24.0:

- `mgf-fed registry --check` → exit **1**, output above.
- Released vs. checked-out, both verified with `git show origin/main:pyproject.toml`:
  `mgf-fed` 0.24.0 vs 0.23.0 on disk; `mgf-standard` 0.1.78 vs 0.1.77 on disk.
- TelePlasma `pyproject.toml:135,152` — `mgf-fed[cli]>=0.24.0,<0.25`,
  `mgf-standard>=0.1.78,<0.2`. Correct against the released versions; graded RED.
- `git rev-list --left-right --count origin/main...HEAD` in mgf-standard → `6 10`
  (divergent both ways), and `MGF_LIBRARIES.json` **differs between the two refs**
  (11 lines) — so `_roster.py:30` demonstrably reads a different registry than the
  default branch carries, right now.
- Bench-wide branch census: 51/53 registry members off their default branch.
- Cross-refs: **PAPER-151** (same chain, SHIPPED same day — consequence 2 is its
  verification); mgf-standard **#303**'s PR body carries the render-source audit;
  TelePlasma court L9 chain, WF-13 row drift-measurement note (TelePlasma **#80**).

**What I'd want** — an offer, not a prescription; listed cheapest first, and the
cheapest alone would have saved today.

1. **A loud banner when a consulted checkout diverges from its default branch.**
   Purely additive, no behaviour change: name the member, the branch, and the
   ahead/behind counts, and say the verdict may not describe any remote. This
   converts a silent false RED into a self-labelling one. If only one lands, this.
2. **Resolve each member at `origin/<default-branch>`.** Fetch, then read through
   `git archive` / `git show <ref>:<path>` rather than the working tree — the same
   discipline the 0.1.78 render used to get two byte-identical passes, and the same
   lesson as the canon's "on a `pull_request` the workspace's default-branch ref is
   counterfeit". Where the artifact is packaged, prefer **installed wheel data**
   over any checkout at all — that is what `drift-check` reading a *checkout* of
   `MGF_LIBRARIES.json` is really missing.
3. **A `--source=` escape hatch** (`origin` default, `worktree` opt-in) so a
   deliberate local preview stays one flag away and the safe path is the default
   rather than the informed choice.

**Verification.** Every number above re-measured at filing time rather than
carried from the session that found it. One figure from the originating session
did **not** reproduce and is deliberately not repeated here: that round recorded
"57 of 63" for the parked-branch census; measuring today against the registry
roster gives **51 of 53** members (62 of 67 git repos under `MGF_ROOT`). The
denominator was never defined in the original note, so the discrepancy is most
likely roster-vs-directory rather than drift — but the reproduced figures are the
ones stated above.

**Decision rationale.** ACCEPTED (founder-delegated triage, 2026-08-06). The paper
listed its offers cheapest-first and said of the first *"if only one lands, this"* —
so that is the one that lands now, in the same commit that flips this row; ships in
**0.25.0**.

**Shipped now — offer 1, the banner.** `_roster.divergent_checkouts` /
`divergence_banner` / `print_divergence_banner` are the shared seam both gates
consult. `drift-check` banners every registry member's checkout plus the mgf-standard
checkout it reads the registry and the policy templates from; `registry` banners every
sibling directory the generator walks, in **both** modes — `--check` because that is
where the false REDs were observed, and a plain regen because baking unshipped state
into a published artifact is consequence 1 of this paper and the 0.1.75 failure class.
The banner names the member, the branch, the compared ref and the ahead/behind counts,
caps the list at 8 with a suppressed-count tail (`MGF_FED_DIVERGENCE_LIMIT` widens it),
and says in as many words that the verdict may describe no remote and that walking a
pin forward to match a stale working directory is the regression. Three properties are
deliberate: **exit codes are untouched** (a fixture pins that a banner-producing
`drift-check` run returns exactly the code it returned before — a banner that moved an
exit code would be the new failure mode the paper explicitly did not ask for); it
**never fetches**, so no gate gains a network dependency and the counts are honestly
described as "against the last-fetched origin refs"; and a checkout it cannot resolve
(not a git repo, no origin default ref, git declines to answer) is **skipped, not
guessed at** — a banner that speculated would be the same defect class it exists to
label. `MGF_FED_NO_DIVERGENCE_BANNER=1` silences it for a bench that knows it is
previewing local state; silence is opt-in and never the default.

Measured on the filing bench the same day: `drift-check` now opens with
`52 of 53 consulted checkout(s) are NOT at their origin default branch`, naming
mgf-fed's own `fix/ci-workspace-truth-invocation` and mgf-standard's
`fix/mgf-standard-own-fetch-step` among them — the two checkouts whose staleness
produced this paper's two false REDs against pins that were exactly right.

**Scheduled — offers 2 and 3.** Resolving each member **at `origin/<default>`** (via
`git show <ref>:<path>` / `git archive`, preferring installed wheel data where the
artifact is packaged) and the `--source={origin,worktree}` escape hatch are the real
fix; the banner only labels the problem. They are a larger change — every read path in
`_roster`, `_drift` and the mgf-standard generator has to move off the working tree,
and the fetch policy has to be decided rather than assumed. **Trigger: the next
mgf-fed feature wave.** This entry stays open until they land; it is not closed by the
banner.

**Note for REL-13 / the release ceremony.** The banner is gate-adjacent output, so a
release that stashes/flips/restores member checkouts should now see it NAME the parked
ones rather than silently mis-grade them. A pinned fixture
(`tests/unit/test_roster.py::TestDivergenceBanner`) builds real git repos with real
`refs/remotes/origin/HEAD` and asserts a seeded divergent member is named, a clean
roster produces nothing at all, and the behind-direction (a checkout behind its own
release — mgf-fed's exact case here) is reported too.

---

**SECOND HALF SHIPPED — 0.25.1 (2026-08-06). The trigger fired early.**

The scheduled half's trigger was recorded above as *"the next mgf-fed feature
wave"*. It arrived as an incident instead, which is the honest thing to record:
**a fleet-red canon pipeline is that wave arriving early.**

**What happened.** 0.25.0 shipped two new audit checks that had been sitting on an
undrained `main` — `registry-consistency` (`eed54f4`) and `lessons-mechanized`
(`76531e8`), both authored 2026-08-05 and contained in **no earlier tag**. The
first of those consumes `run_registry(check=True)`, i.e. the exact working-tree
read this paper is about, on a surface the paper had not seen. mgf-standard's
audit step pip-installs `mgf-fed` **unpinned**, so it picked 0.25.0 up mid-queue;
that repo had deliberately removed its pre-check waivers (`f892393`); and its
pipelines (**515/517**) went red with **9 findings, 8 of them the founder's parked
checkouts** reported as defects of the fleet. Wave C's full analysis is a comment
on mgf-standard **PR #305**, including the detail that most deserves keeping: the
two surfaces gave **opposite answers about the same fleet**, because one read a
bench and the other read the remotes.

**Two defects, and the first was introduced by the fix for this very paper.**

1. **The 8 bench findings WERE the banner.** 0.25.0 printed it to stdout so the
   label would travel beside the verdict. That reasoning was wrong for a concrete
   reason: the stream is machine-parsed. `check_registry_consistency` captures
   `run_registry`'s stdout and turns every line matching `- <text>` into a
   `Finding` — precisely the banner's member-list shape, capped at 8, hence exactly
   8. The lesson is sharper than "use stderr": **a diagnostic must not share a
   channel AND a shape with the data it annotates.** The banner is now on stderr.
2. **The underlying working-tree read** — this paper's offers 2 and 3.

**What shipped.** `_origin_view` materialises every member's committed top-level
files at its `origin/<default>` into a throwaway mirror, and `registry --check`
points the (unmodified) mgf-standard generator at that. The generator is another
repo's file and this one is read-only on it (FB-05) — and a unilateral cross-repo
edit was the last thing a fleet-red incident needed — so the fix is the half
mgf-fed owns: **the root the generator walks.** The mirror copies *every* top-level
blob rather than a curated list, so it never needs to know what the generator
reads; a curated list would have been a second two-map lockstep across
independently-released repos, the defect 0.25.0 had just fixed elsewhere
(mgf-standard PAPER-148).

Offer 3 shipped with it: `--source={origin,worktree}`, defaulting to `origin` for
`--check` and `worktree` for a regen, so the safe path is the default rather than
the informed choice — exactly as this paper asked.

**Fetch policy, decided rather than assumed.** Opt-in (`--fetch`), not default.
`registry-consistency` is registered *offline-capable*, so fetching by default
would quietly give a gate a network dependency it promises not to have, and it
would mutate the operator's clones from inside a read-only audit. In CI the
workspace is a fresh clone whose origin refs are current by construction. A member
that cannot be resolved falls back to its working tree **and is named**, with the
count, in an `ORIGIN-DEFAULT VIEW INCOMPLETE` banner; if mgf-standard itself cannot
be resolved the gate returns 2 (SKIPPED) rather than grade a partial fleet.

**Measured on the filing bench, same day.** The check goes from **9 findings to 4**,
and all four are true at `origin/<default>`: one genuinely stale `MGF_LIBRARIES.md`,
and three pins that genuinely exclude a current release (TelePlasma and
vm-vmanager-app at `mgf-fed[cli]>=0.24.0,<0.25`, inthewords at
`mgf-common>=0.44,<0.47`). Two details worth keeping. First, **the gate does not go
green** — it now reds for reasons that are real, which is the outcome asked for, not
a quieter gate. Second, the remediation text this paper complained about is now
*correct where it appears*: "walk it forward + verify" against TelePlasma's `<0.25`
pin is the right advice, where in the original incident the same sentence would have
downgraded a pin that was already right. The gate has gone from instructing the
operator to make the fleet worse, to telling them the truth.

**One thing deliberately not changed.** `lessons-mechanized`, the other check
0.25.0 carried out, was read for the same defect. Its `ci:` arm — the one most
exposed — already resolves at `origin/<default>` (`_default_ref` +
`_pipeline_sources`), and its docstring states the reason: *"a step that exists
only on a bench gates nothing."* Its `mechanism` / `test:` arms do read the working
tree, but only of **mgf-standard itself**, which in the pipeline that runs the check
IS the commit under test. So it is correct where it matters and drifts only on a
long-lived bench; that is a judgment for the chair, not an incident fix, and it is
recorded here rather than silently widened.

### `PAPER-151` — `[exempt].woodpecker`'s fallback selects on LANGUAGE, not on tier: an exempt Python **app** is held to a library step-list it can never satisfy

**Severity:** 🟡 Sharp edge (the escape hatch grades the exempt member *worse* than the unexempted one — and it blocks the enactment of a founder-ratified ruling)
**Origin:** TelePlasma · 2026-08-06 · found while enacting court round L9 §H3, option A

| Field | Value |
|---|---|
| Status | ✅ SHIPPED |
| Submitter | TelePlasma (court round L9 enactment) |
| Submitted | 2026-08-06 |
| Affects | `mgf-fed` ≤0.23.0 · `drift-check` / `_drift.check` step-name fallback · every Python **consumer app** in `[exempt].woodpecker` |
| Decision | ACCEPTED — option 1 (key the fallback on tier), fixed same session |
| Shipped in | `6765234` (v0.24.0) |

**What hurts.** `[exempt].woodpecker` is the canon's sanctioned home for an
intentionally-bespoke pipeline: seven members live there, each excused
generation-currency and held to a step-name fallback instead. The comment in
`mgf-standard templates/policy.toml:86-91` states the rule as the fleet
understands it —

> mgf-brand (lang=assets) + mgf-website (lang=web): non-Python members with fully-bespoke
> pipelines … held to the step-name fallback — which, for a non-Python member, is the
> universal governance subset (commit-trailers + secret-scan), NOT the Python step-list

— which is accurate, and is exactly the problem. The selector is `lang`, and
nothing else:

```python
# src/mgf/fed/_drift.py:456
is_python = member.lang == "python"
...
# src/mgf/fed/_drift.py:496-498
required_steps: Iterable[str] = (
    policy.get("woodpecker_required_steps", []) if is_python else _UNIVERSAL_REQUIRED_STEPS
)
```

`woodpecker_required_steps` is **library-shaped**: `build`, `sbom`, `publish`,
`verify-devpi`, `tag-coherence`, `smoke`, `test-3.11/3.12/3.13`. A Python
**consumer app** publishes nothing to devpi, builds no wheel, cuts no tag to
verify, and pins `requires-python = ">=3.12"`, so `test-3.11` can never exist.
It is a Python member by *language* and a non-library by *tier* — and the
fallback has no way to say so.

The consequence is that **exempting an app makes its audit worse, not better.**
Measured against `mgf-fed` 0.23.0 on TelePlasma's real tree, adding one name to
`[exempt].woodpecker` trades one finding for twelve:

```
=== TelePlasma, NOT exempt (today) ===
  [ci-generated] .woodpecker.yml differs from generated (run `mgf-fed scaffold`)

=== TelePlasma, IN [exempt].woodpecker ===
  [ci-step] missing steps: lint, typecheck, test-3.11, test-3.12, test-3.13,
            vuln-scan, build, sbom, tag-coherence, smoke, publish, verify-devpi
```

Same tree, same policy, same `_drift.check(member, policy)` call — only the
exempt list differs.

So the escape hatch is closed to the tier that most needs it. TelePlasma's
pipeline **already passes both halves of the governance contract it should be
held to**: the `commit-trailers` and `secret-scan` steps are present, and all
three `required_commands` gate-floor commands (`ruff check`,
`ruff format --check`, `check_commit_trailers.py`) run. It still cannot be
recorded as exempt without manufacturing twelve false findings.

**Why it matters.** This is the direct sequel to **PAPER-128** (OPEN), which
asked that consumer apps stop sitting outside the CI-currency gates. That ask
has since been half-granted — TelePlasma is now a registry member in
`MGF_LIBRARIES.json`, so `drift-check` does see it. What it sees is
`ci-generated`: a demand that the pipeline byte-equal a render which
structurally cannot produce it (**PAPER-150**). The app tier is now *inside* the
gate with no sanctioned way to be right — rendered is impossible, exempt is
worse. Four members are in that position today (TelePlasma, hrb-app,
vm-vmanager-app, ipcam-app).

It also blocks a **founder-ratified ruling**. TelePlasma court round L9 §H3
(ratified 2026-08-06) directs exactly this entry to be added, held to the
universal subset + the `required_commands` floor. The policy line cannot be
written honestly until the fallback can express it — which is why that
enactment stopped at the investigation rather than opening a policy PR that
would have turned one finding into twelve.

**Evidence.**

- `src/mgf/fed/_drift.py:456` — `is_python = member.lang == "python"`.
- `src/mgf/fed/_drift.py:496-498` — the fallback selection; this is the whole mechanism.
- `src/mgf/fed/_drift.py:134` — `_UNIVERSAL_REQUIRED_STEPS = ("commit-trailers", "secret-scan")`.
- `src/mgf/fed/_drift.py:499` — `woodpecker_exempt = member.repo in (policy.get("exempt", {}) or {}).get("woodpecker", [])`: a flat name-membership test. The exempt entry carries **no per-member value**, so there is nowhere for a policy author to record which fallback a given member is held to.
- `src/mgf/fed/_roster.py:34-44` — `Member` carries `repo / lang / registry_version / extras / deps / used_by / wave`. There is **no tier or kind field**, and `MGF_LIBRARIES.json` has no `kind` key to load one from (`_roster.py:73-86`).
- `mgf-standard templates/policy.toml:34-40` (the library-shaped list) and `:80-106` (the exempt block and its rationale comments).

**What I'd want** — any one of these closes it; listed cheapest first.

1. **Key the fallback on tier, not language (recommended).** The registry grows
   an optional `kind` (`"library"` by default, `"app"`), `Member` carries it, and
   `_drift.py:496` selects `_UNIVERSAL_REQUIRED_STEPS` when `kind == "app"`
   regardless of `lang`. One registry field, one `Member` field, one conditional.
   It also gives every *other* app-tier question a name to key on instead of
   re-deriving the tier ad hoc — PAPER-128's roster question, the
   `no-pypi-surface` fragment, `verify-devpi`.
2. **A per-entry fallback override in policy.** `[exempt].woodpecker` becomes a
   table whose value names the fallback: `TelePlasma = "universal"`,
   `mgf-hrb = "default"`. This keeps the decision in `policy.toml`, next to the
   rationale comments that already live there, and the visible source of truth
   stays one file. (The current membership test reads a TOML table's keys
   correctly as-is, so the migration is source-compatible — but `_drift.py:496`
   still has to consult the *value*.)
3. **An app-tier step list in policy.** `woodpecker_required_steps_app = [...]`,
   selected for members in a policy-declared app set. More expressive than (1) —
   an app tier could be held to more than the bare universal two (e.g. `install`,
   `commit-trailers`, `secret-scan`, plus its own test step) — at the cost of a
   second list to keep honest.

(1) is the recommendation: the concept actually missing is **tier**, and naming
it once beats encoding it in three places.

**Interim.** There is none. Option A of the ruling cannot be written today, so
TelePlasma stays un-exempt and carries a standing `ci-generated` finding it
cannot clear — the status quo the ruling existed to end.

**Verification.** Reproduced 2026-08-06 against `mgf-fed` 0.23.0
(`~/.local/share/uv/tools/mgf-fed`): `_drift.check` called directly on the real
TelePlasma checkout with the real loaded fleet policy, the exempt list mutated
**in memory only**. No repository was modified to produce the two outputs above.

**Decision rationale.** ACCEPTED and fixed same session — see the commit that
flips this row; ships in **0.24.0**. Option 1 as recommended and as ratified by
the founder (TelePlasma court round L9 §H3): the registry grows an optional
`kind` (`"library"` default, `"app"`), `Member` carries it, and the step-name
fallback selects `_UNIVERSAL_REQUIRED_STEPS` for `kind == "app"` whatever the
language. The concept that was missing is **tier**, and naming it once beats
encoding it in three places.

Two properties the fix is deliberately narrow about. The **default is
load-bearing**: no row of the installed registry (mgf-standard 0.1.77) declares
`kind`, so all 53 members load as `"library"` and `library_shaped` reduces to the
old `is_python` — behaviour is byte-identical until a registry says otherwise,
and the tests that pin the library step-list are the ones that catch a flipped
default. And the **gate floor is not part of the trade**: `ci-command` keys on
`lang`, never on `kind`, so an app is held to every `required_commands` gate
(PAPER-120/121) exactly as a library is. Exempt from a library's *step list* is
not exempt from the gates that actually run — a standing negative fixture asserts
it. Writing the `[exempt].woodpecker` policy line for TelePlasma, hrb-app,
vm-vmanager-app and ipcam-app now needs only the registry rows; that is
`mgf-standard`'s to land, and this release unblocks it.

PAPER-128's roster question and the `no-pypi-surface` / `verify-devpi` fragments
now have a field to key on, but this change does **not** rewire them: `kind`
exists, `drift-check`'s fallback consults it, nothing else changes semantics.

---

### `PAPER-150` — the renderer has no knobs a consumer app's pipeline needs: no `services:`, no cross-step serialization, no `path:`-gated tier

**Severity:** 🟢 Nice-to-have (the long-term direction — PAPER-151 is the interim mechanism, and it has to work first)
**Origin:** TelePlasma · 2026-08-06 · filed as the REJOIN TRIGGER recorded by court round L9 §H3

| Field | Value |
|---|---|
| Status | OPEN — SPLIT (2026-08-06): (ii) ACCEPTED + fixed same session; (i) ACCEPTED-DIRECTION, DEFERRED. Stays open carrying WF-13's rejoin trigger |
| Submitter | TelePlasma (court round L9 enactment) |
| Submitted | 2026-08-06 |
| Affects | `mgf-fed` ≤0.23.0 · `mgf.fed._woodpecker.render_woodpecker` + `mgf-standard templates/woodpecker/` · the consumer-app tier |
| Decision | SPLIT — (i) renderer knobs: ACCEPTED-DIRECTION, DEFERRED with named triggers. (ii) the successor `present`-list gap: ACCEPTED, fixed same session |

**What hurts.** `woodpecker_generated = true` is fleet policy, and `drift-check`
grades every member on whether its on-disk pipeline byte-equals
`render_woodpecker(member)`. A consumer app cannot pass that check — not because
its pipeline drifted, but because three structural things it requires have no
representation anywhere in the template set.

1. **`services:` containers.** `mgf-standard templates/woodpecker/` contains
   **zero** occurrences of `services:` across `base.yml`, `base-zig.yml` and all
   13 fragments. TelePlasma runs three — a TimescaleDB postgres, a redis, and a
   minio S3-compatible object store (`.woodpecker.yml:28-53`), digest-pinned,
   with environment and, for minio, a `commands:` server invocation. Its
   integration tests, migration round-trip and backup/restore drill all address
   them by service hostname (`postgres:5432`, `redis:6379`, `minio:9000`).
   Nothing the renderer can emit stands them up.

2. **A serialized chain between non-`install` steps.** Every generated step fans
   out from `install`. TelePlasma's three DB-touching steps must run **in
   series** against the single postgres service —
   `pytest-integration → alembic-roundtrip → backup-restore-drill`
   (`.woodpecker.yml:350`, `:407`) — because each runs `alembic upgrade head`
   and, run in parallel, they race creating `alembic_version` (duplicate
   `pg_type` unique violation, observed on the first-ever pipeline run,
   2026-07-10; the constraint is recorded in the comment at `:341-346`). This is
   a correctness requirement, not a preference, and the render has no vocabulary
   for it.

3. **`path:`-gated step tiers.** Five frontend steps (`frontend-lint`,
   `frontend-audit`, `frontend-playwright`, `lighthouse-ci-desktop`,
   `lighthouse-ci-mobile`) carry
   `when: [{event: [push, pull_request], path: "apps/web/**"}]`
   (`.woodpecker.yml:566-570` and four more) so a backend-only PR does not pay
   for a node/pnpm tier. The nearest existing fragment,
   `templates/woodpecker/frag-ui-gate.yml`, is a Python/pytest tier gated on a
   *dependency* (`mgf-uitest-web`), with no `path:` condition and no node
   toolchain — a different shape for a different job.

**Why it matters.** The app tier is now inside the CI-currency gate (PAPER-128,
half-granted: TelePlasma is a registry member) with no way to be graded
correctly — a standing `ci-generated` finding that no `scaffold` run can clear,
because a fresh render would *delete* the services the pipeline needs in order
to work. The sanctioned alternative, `[exempt].woodpecker`, is currently closed
to Python apps for an unrelated reason (**PAPER-151**). And even once PAPER-151
lands, an exempt entry is a *record of a decision not to generate*: it stops the
false finding, it does not deliver template hardening. Every future fleet-wide
pipeline fix reaches generated members and skips exempt ones by construction —
PAPER-109's clone-block hardening is the precedent that already stranded hrb-app
for weeks.

**What I'd want.** Three opt-in knobs, each expressed in the mechanisms that
already exist (`[tool.mgf-fed]` carve-out keys + `# <<INSERT:NAME>>` fragment
splices) — no new machinery:

1. **`services`** — a declarative list of `{name, image, environment, commands}`
   splicing a `services:` block at a new top-level marker. Digest-pinned image
   choices stay the repo's business (they differ per repo); the renderer owns
   placement and shape.
2. **`serialize`** — a declared ordering among named steps, so `depends_on` can
   express a chain and not only a fan-out from `install`.
3. **A `path`-conditioned tier** — either a `path:` field on an existing
   fragment's `when:`, or a node/frontend fragment that carries one, so an app
   can skip its web tier on backend-only changes.

**Cost if deferred.** Bounded and honest — which is why this is 🟢 and not 🟡.
With PAPER-151 shipped, the app tier lives in `[exempt].woodpecker` and is
correctly graded on the universal governance subset plus the `required_commands`
floor. That is a working state. What it costs is that four members (TelePlasma,
hrb-app, vm-vmanager-app, ipcam-app) stay outside template propagation
indefinitely, each re-discovering centrally-known pipeline fixes through its own
red-CI archaeology.

**Cross-reference.** This paper **is** the rejoin trigger recorded by TelePlasma
court round L9 §H3 (founder-ratified 2026-08-06), and by the
`[exempt].woodpecker` comment that ruling directs: *"rejoins
generation-currency when the renderer grows service-container + path-gated-tier
knobs."* Note that mgf-standard's own self-exempt entry already carries the same
shape of promise — it rejoins "when the renderer grows a per-member
coverage-gate knob" (`templates/policy.toml:94-98`). That makes this the second
standing renderer ask of its kind, which is itself an argument for a general
opt-in-block mechanism rather than three more special cases.

**Evidence.**

- `src/mgf/fed/_woodpecker.py:732-747` — `render_woodpecker`: `base.yml` +
  `{{TOKEN}}` substitution + `# <<INSERT:NAME>>` fragment splices. That is the
  entire vocabulary.
- `src/mgf/fed/_woodpecker.py:423-425` —
  `_KNOWN_CI_KEYS = {browser, coverage_floor, vuln_ignore, extra_index, apt_packages, sbom_extras}`:
  no knob addresses services, ordering, or path gates.
- `mgf-standard templates/woodpecker/` — `grep -rn "services:"` returns nothing
  across all 15 files; `base.yml`'s `INSERT` markers are all step-level or
  file-level splices into a fixed skeleton.
- TelePlasma `.woodpecker.yml:28-53` (three services), `:341-346` + `:350` +
  `:407` (the DB serialization chain and the race that forced it), `:566-570`
  (the first of five `path: "apps/web/**"` gates).
- `mgf-standard templates/woodpecker/frag-ui-gate.yml` — the nearest existing
  fragment: dependency-gated, not path-gated, and Python-only.

**Decision rationale — SPLIT** (founder-delegated triage, 2026-08-06). The paper is
two asks wearing one number: a renderer roadmap, and a live mis-grading of the app
tier that the roadmap's cost paragraph quietly documents. They have different sizes
and different urgencies, so they get different answers.

**(i) The renderer knobs — ACCEPTED-DIRECTION, DEFERRED.** `services`, `serialize`,
and a `path`-gated tier are the right shape and the right long-term direction; the
paper's own cost analysis is accepted as written, which is why this is 🟢 and why it
waits. Deferring it is honest because PAPER-151 shipped: the app tier now lives in
`[exempt].woodpecker` and is graded on the universal governance subset plus the
`required_commands` floor — a working state, not a hole. What it costs is exactly what
the paper says: four members stay outside template propagation and re-discover central
pipeline fixes through their own red-CI archaeology (PAPER-109's clone-block hardening
stranding hrb-app is the precedent, and it is a real cost, not a hypothetical one).

**Named triggers** — this is scheduled work, not a "later":

1. **A second app-tier repo asks to rejoin generation-currency.** One standing ask is
   a special case; two is a tier, and a tier earns a mechanism.
2. **The fleet schedules a renderer wave.** Note the paper's own observation that
   mgf-standard's self-exempt entry already carries the same shape of promise
   (`templates/policy.toml:94-98` — rejoins "when the renderer grows a per-member
   coverage-gate knob"). That makes **two** standing renderer asks of different kinds,
   which is the argument for a general opt-in-block mechanism rather than three more
   special cases — and that generalisation is what the wave should build.

This entry **stays OPEN** and remains the rejoin trigger recorded by TelePlasma court
round L9 §H3 and by the `[exempt].woodpecker` comment that ruling directs. Closing it
would delete the trigger.

**(ii) The successor gap — ACCEPTED, fixed same session.** The paper's "Why it
matters" paragraph names a second defect in passing: the app tier is *inside* the gate
"with no way to be graded correctly". PAPER-151 fixed that for the step-name fallback.
The identical library-shaped assumption survived one layer up, in the **presence**
check — `policy.present` demands `PUBLIC_API.json`, `tools/generate_public_api_json.py`
and `SCOPE.md` of every member, so a consumer app carried three standing `missing`
findings for artifacts it does not have and that no `scaffold` run could ever clear.
Fixed by PAPER-151's own pattern and on PAPER-151's own field: `_drift.py` gains
`_LIBRARY_ONLY_PRESENT`, and a `kind == "app"` member is not owed those entries.

**Which artifacts are library-only is the canon's call, and it was checked against the
canon before the set was written** — the answer decides whether this is a two-file or
a three-file fix:

- **`PUBLIC_API.json` + its generator — library-only.** DOC-01 v2.11 §2.0.1 is
  explicit: *"`PUBLIC_API.md` (+ `.json`) | ✅ | ✅ | ❌ | The contract surface for a
  library. Apps don't have one."* (AP-10 / DOC-08; mirrored by `FEDERATION.md` §3.)
- **`SCOPE.md` — library-only, so the fix is three files, not two.** It is absent from
  the DOC-01 v2.11 universal root-doc baseline entirely (neither the §2.0.1 matrix nor
  `FEDERATION.md` §3 lists it), and the rules that mandate it name their subject:
  SP-01/SP-02 read *"**The library** MUST publish an explicit IN-scope / OUT-of-scope
  list"*, and `docs/standards/SCOPE.md` §1 closes with *"Other **libraries** that
  follow this standard ship their own `SCOPE.md`."* Corroborating the reading rather
  than resting on it: none of the six consumer applications on the bench
  (TelePlasma, hrb-app, vm-vmanager-app, ipcam-app, inthewords, figrecovery) ships one.
- **Everything else on the list stays owed by the app tier** — `DEPS.md` especially,
  which DOC-01/DOC-16 make universal in as many words (*"A leaf app fills §1 and leaves
  §2 empty; the file MUST still exist"*). A standing fixture asserts an app is still
  flagged when `DEPS.md` goes missing: this is a correction of a library-shaped policy,
  not an exemption from presence checking.

The language gate and the tier gate stay **independent**, and a fixture pins that too:
a Zig **library** is still excused the Python-packaging pair (they introspect Python
modules) and still owes `SCOPE.md`, which is language-agnostic. The `kind` default
remains load-bearing exactly as in PAPER-151 — a member that declares no `kind` loads
as `"library"` and owes all three, which is every registry row but the app ones.

Measured against `origin/main` (0.24.0) on the filing bench, TelePlasma's drift report
goes from **6 findings to 3**: the three `[missing]` rows clear and nothing else moves.
Those are the WF-13 residuals the walk PR can now honestly strike.

---

### `PAPER-147` — presence is graded, quality is not: a ledger of 306 bare `N/A`s passes DOC-15 identically to an argued one

**Severity:** 🟡 Sharp edge (structural — the gap widens as seeding gets cheaper)
**Origin:** TelePlasma · 2026-08-05 · found while paying the 13-row toll of the 0.23.0 cohort walk (TelePlasma PR #74)

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (cohort walk 2026-08-05) |
| Submitted | 2026-08-05 |
| Affects | `mgf-fed` ≤0.23.0 · `app-doctor --standards` · `audit --check conformance-ledgers` |
| Decision | pending |

**What hurts.** Every gate that reads a conformance ledger grades **presence**:
`app-doctor --standards` counts rule-ID coverage against the installed corpus
(CFM-03), `audit --check conformance-ledgers` compares declared vs accounted.
None of them asks whether a row **says anything**. A ledger holding 306 bare
`N/A` markers scores identically to one where every verdict is argued from
evidence — and `N/A` is the strongest claim in the vocabulary (permanent
inapplicability), which makes it the cheapest to abuse and the costliest to
believe.

**The tool already knows the distinction it doesn't enforce.**
`ledger-seed --status`'s own help text: *"Defaults to ⏳ (not yet assessed) —
the only honest answer a script can give. Pass another only when a human has
actually decided."* (`src/mgf/fed/cli/_ledger_seed.py:71`). Nothing downstream
ever checks whether a human actually decided.

**Why now.** Seeding made bulk rows cheap, and the 2026-08-03 incident (33 new
MUSTs, 32 repos red within minutes — your own 0.21.0 changelog) creates exactly
the pressure that turns into bulk-flipped markers. TelePlasma's walk wrote its
13 rows with per-row rationale by hand; no gate would have noticed 13 bare
markers instead.

**What I'd want.** A `ledger-quality` check (in `app-doctor` or `audit`),
warn-first: an `N/A` or `✅` row whose verdict cell contains nothing beyond the
marker is flagged; N identical justification strings across rows are flagged as
a bulk-flip signature. `⏳` stays exempt — it is the honest empty state.

---

### `PAPER-146` — DOC-15 has four legal markers and no validator: an illegal fifth value lived four days under green gates

**Severity:** 🟡 Sharp edge (a census that cannot parse a row still counts it)
**Origin:** TelePlasma · 2026-08-05 · found while re-verifying the SEAT/CRT rows during the 0.23.0 cohort walk (TelePlasma PR #74)

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (cohort walk 2026-08-05) |
| Submitted | 2026-08-05 |
| Affects | `mgf-fed` ≤0.23.0 · `app-doctor --standards` · every committed ledger in the roster |
| Decision | pending |

**What hurts.** DOC-15's verdict vocabulary is exactly four values — ✅ / 🚫 /
⏳ / N/A — and `mgf-fed` encodes it (`_VALID_STATUS`,
`src/mgf/fed/cli/_ledger_seed.py:59`) but only to validate its **own**
`--status` flag. No gate anywhere validates the status column of an existing
ledger.

**Evidence.** TelePlasma's CRT-08 row carried `⏳→partially met` — a fifth
value no census can count — from 2026-08-01 to 2026-08-05, through green
`app-doctor --standards` runs (12 pass · 0 warn · 0 fail), green `catch-up`,
and green CI on every push in between. It was found by a human re-reading the
ledger during the corpus walk, normalized in TelePlasma PR #74.

**What I'd want.** The cheap version is a regex over the ledger's status
column in `app-doctor --standards`: any row whose marker is not one of the
four legal values is a finding. `_VALID_STATUS` already exists — this is
lifting it from "validates my flag" to "validates the artifact I grade".

---

### `PAPER-145` — `ledger-seed` can only address `<root>/<name>`: no way to seed the checkout you are actually standing in

**Severity:** 🟡 Sharp edge (and a mis-write hazard, second paragraph)
**Origin:** TelePlasma · 2026-08-05 · found by execution during the 0.23.0 cohort walk (TelePlasma PR #74), executor session in an isolated git worktree

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (cohort walk 2026-08-05) |
| Submitted | 2026-08-05 |
| Affects | `mgf-fed` 0.21.0–0.23.0 · `ledger-seed` |
| Decision | pending |

**What hurts.** Repos are addressed exclusively as `<root>/<name>`
(`src/mgf/fed/cli/_ledger_seed.py:88` + the loop's `repo = root / name`).
`--root` exists but only relocates the federation root — the checkout must
still sit at its canonical name under it. There is no `--path DIR` meaning
*"operate on THIS checkout"*, so any non-canonical clone — and every isolated
worktree, e.g. `.claude/worktrees/agent-…`, the shape worktree-isolated
executor sessions use routinely — is structurally out of reach.

**The sharper edge.** An agent whose worktree was skipped (see PAPER-144) and
who re-runs against the canonical root gets `--apply` writing into the
**canonical checkout's working tree** — whichever branch that checkout happens
to have out at that moment, not the branch doing the walk. The write lands;
the wrong tree changes; nothing warns.

**Worked around** in TelePlasma's walk by importing `missing_musts` /
`append_rows` and driving them directly at the worktree's ledger — same bytes,
same idempotence, but the CLI contract is bypassed and the method note lives in
TelePlasma's ledger instead of your interface.

**What I'd want.** `--path DIR` (repeatable), or `repos` entries that contain a
path separator being treated as paths. Write target = that checkout, not
`federation_root()/<name>`.

---

### `PAPER-144` — the audit family is worktree-blind: `(repo/".git").is_dir()` calls every linked worktree "not on disk"

**Severity:** 🟡 Sharp edge (silently false statements about repos that exist)
**Origin:** TelePlasma · 2026-08-05 · found by execution during the 0.23.0 cohort walk (TelePlasma PR #74), executor session in an isolated git worktree

| Field | Value |
|---|---|
| Status | SHIPPED — `is_git_repo()` adopted at all 11 blind sites, proven against a real `git worktree add` |
| Submitter | TelePlasma (cohort walk 2026-08-05) |
| Submitted | 2026-08-05 |
| Affects | `mgf-fed` ≤0.25.4 · `ledger-seed` · the `_audit.py` roster/ref/tree family |
| Decision | ACCEPTED as filed — the ask was already the right shape |
| Shipped in | `04c7cfa` (merged `d8c7e53`, PR #189) — released in **v0.25.5** |

**What hurts.** In a linked worktree, `.git` is a **file** (a `gitdir:`
pointer), not a directory. `run_ledger_seed` gates each repo on
`(repo / ".git").is_dir()` and prints `not on disk — skipped` — a false
statement about a checkout that is the caller's own cwd. Observed verbatim on
2026-08-05: `uv run --frozen mgf-fed ledger-seed TelePlasma --apply` from
TelePlasma's worktree reported the repo absent while standing inside it.

**This is a class, not a site.** The same predicate appears at
`src/mgf/fed/_audit.py:159, 292, 345, 560, 576, 686, 868` — roster discovery,
ref resolution, tree reads. A fleet member checked out as a worktree silently
drops out of rosters and audits; the failure reads as "repo not on disk", which
is the least alarming possible rendering of "I am ignoring a repo that exists".
Worktree-isolated agent sessions are normal fleet practice (SEAT-05 cadence;
TelePlasma's executor seats run in them), so the blind spot sits exactly where
the automation stands.

**What I'd want.** One `_is_git_checkout(path)` helper — `(path / ".git").exists()`
is sufficient, `git -C path rev-parse --is-inside-work-tree` is airtight — used
by every current `is_dir()` site, plus a regression test that runs the family
against a real `git worktree add`. PAPER-145 (addressing) is the same session's
sibling finding; fixing this one without that one still leaves worktrees
unreachable by `ledger-seed`.

**Resolution (2026-08-14) — shipped in 0.25.5.** Implemented as asked: `is_git_repo(path)` in
`mgf.fed._roster`, adopted at **11** sites — `_audit.py` ×8, `_court.py`
`_repos` ×1, `cli/_ledger_seed.py` ×2 (the paper's own reported symptom).

**The paper was half-landed and that is the finding worth keeping.** `_roster.py`
had already been repaired, and its comment said *"(PAPER-144's exact defect)"* in
as many words — while `_audit`, `_court` and `ledger-seed` were never carried
across. The knowledge was captured at one site and the class stayed open at
eleven. A paper is not closed by the first site that reads its title.

**Proven by execution, both directions.** Three tests in `tests/unit/test_audit.py`
plant a **real** `git worktree add` (not a planted marker — a planted marker is the
workaround this defect taught people to write):

    premise            .git is a FILE, rev-parse --is-inside-work-tree = true
    _repos_on_disk     worktree present in the roster
    _court._repos      worktree present in the roster

Sabotage-checked: reverting `_audit._repos_on_disk` reds
`test_repos_on_disk_sees_a_linked_worktree` alone; reverting `_court._repos` reds
`test_court_repos_sees_a_linked_worktree` alone; restoring returns 3 passed. Full
suite 1638 passed; ruff, ruff-format, mypy, lint-imports and
`app-doctor --standards` (13 pass · 0 fail) all green.

**One site deliberately NOT changed, and why.** `cli/_doctor_standards.py:1133`
(`AG-19`, the runtime memory store) also tests `.is_dir()` — but twenty lines
later it reads `mem / ".git" / "config"` directly, and in a linked worktree that
path does not exist (config lives in the main checkout, reachable only via
`commondir`). Left as-is and filed rather than half-fixed: it needs real gitdir
resolution, and the memory store is a plain clone by construction (AG-19), so
nothing is failing today.

> **CORRECTION (2026-08-14) — the decision was right, the reason I gave was not.**
> I wrote that flipping the predicate would *"trade a wrong skip for a wrong
> reason"*. A fresh-context chair measured it and there is **no skip and no
> trade**: the check appends to a `problems[]` list and returns `fail` either
> way. Today it fails with **two** false reasons (*"not under version control"*
> **and** *"no off-machine git remote configured"*); flipping the predicate makes
> it fail with **one**. So the honest statement is that flipping changes *which
> wrong answer* it gives, not *whether* it is wrong — which is still a good
> reason to defer to a real `commondir` fix rather than a one-line edit, but it
> is not the reason I recorded. The exemption is now explicit in code, in
> `_ISDIR_ALLOWED`, with a test that fails if it ever stops being needed.

**Live evidence for a NEIGHBOURING paper, produced by this fix.** The three tests
above are the first in this repo to shell out to real `git` in the
`mgf-common-matrix` step — and that step **failed** on `FileNotFoundError:
'git'` (pipeline 570). Its image is `python:3.12-slim` and, alone among the test
steps, it never runs the `apt-get install … git` line its neighbours do. That is
mgf-standard **PAPER-155** and mgf-common **PAPER-143**, measured rather than
argued: 228 tests skip in that step, so the compat gate really does grade a
smaller suite than the fleet. The tests now carry the house `@requires_git`
guard, which keeps the skip **self-labelling** instead of silent — but the guard
is a costume, not a cure, and the cure belongs in the template.

**Still open, correctly:** PAPER-145. Worktrees are now *seen* by `ledger-seed`
when they sit under the federation root; naming the checkout you are standing in
is the separate addressing gap, exactly as this paper predicted.

---

### `PAPER-140` — the CLAUDE.md auto-block writes an UNTERMINATED HTML comment, silently commenting out every repo's project-specific instructions

**Severity:** 🔴 Blocker (for SEAT-08 adoption) / 🟡 Sharp edge otherwise
**Origin:** TelePlasma · 2026-08-01 · found while adopting SEAT-08

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (SEAT/CRT adoption wave) |
| Submitted | 2026-08-01 |
| Affects | `mgf-fed` ≤0.18.2 · `catch-up --apply`'s `docs/CLAUDE.md` auto-block writer · **21 repos observed** |
| Decision | pending |

**What hurts.** The generator's own template (`mgf/fed/cli/_init_project.py:517`)
emits a **two-line** closing marker:

```
<!-- END mgf-common auto-generated. Project-specific content below
this marker is preserved across `mgf-fed catch-up --apply` runs. -->
```

Every repo on disk carries **only the first line**. The `-->` never appears, so the
HTML comment opened by `<!--` is **never closed** and runs to end-of-file. Everything
in the repo-owned region below the marker — the project-specific lookup, the
conformance override, the "facts that will burn you" section — is inside a comment.

Measured on TelePlasma before the fix: **2,751 characters** swallowed, i.e. the whole
repo-owned region. Verified with `html.parser`, not by eye.

The likely mechanism is the marker constant at `_init_project.py:57`:

```python
CLAUDE_MD_END_MARKER = "<!-- END mgf-common auto-generated."
```

a **single-line** sentinel, while the emitted text is two lines. Any splice that
writes up to and including the marker *line* drops the continuation that carries the
`-->`.

**Scope.** 21 of the repos checked on this workstation carry the truncated marker —
including **`mgf-common`, `mgf-fed` and `mgf-standard` themselves**:

```
figrecovery · mgf-alembic · mgf-common · mgf-common-identity · mgf-common-zig
mgf-django · mgf-fastapi · mgf-fed · mgf-hrb-cap · mgf-hrb-kabi · mgf-hrb-model
mgf-http · mgf-logship · mgf-secrets · mgf-sqlalchemy · mgf-standard
mgf-test-supervisor · mgf-uitest-desktop · mgf-website · mgf-website-template
vm-vmanager-app
```

**Why it hurts.** Two reasons, and the second is the sharp one.

1. **Rendered views hide the content.** On Codeberg/Gitea's markdown render, every
   affected repo's project-specific `CLAUDE.md` section is invisible. The file looks
   like it ends at the auto-block.

2. **It defeats `SEAT-08` at the exact moment the fleet is adopting it.**
   `templates/SEAT_DISPATCH.md` requires the dispatch block to sit **below** the
   `END` marker *and* states it "must load with the rest of the file: a block behind
   a fold or in a linked file does not reach the session." Pasting the block into the
   repo-owned region of an affected repo puts it **inside an unterminated comment** —
   satisfying SEAT-08 on paper while defeating it in fact. A presence check that
   greps for the block would pass. TelePlasma hit this adopting SEAT-08 today and had
   to fix the marker *first*.

**What I'd want.**

1. **Emit the closing line.** Make the writer preserve both lines of the end marker —
   or, more robustly, make the marker a single self-closed line
   (`<!-- END mgf-common auto-generated — project-specific content below is preserved. -->`)
   so no two-line sentinel can be truncated. A one-line marker also matches the
   `BEGIN`-side and every `mgf-common: <section>` marker, all of which are self-closed
   already.
2. **Backfill the 21 repos.** The fix is idempotent and mechanical.
   **Verified:** adding the missing line to TelePlasma survived a subsequent
   `catch-up --apply` unchanged, and `catch-up` still exited 0 — so the repair is
   stable against the very tool that caused it.
3. **Add a check.** `catch-up`'s audit already tests `docs/CLAUDE.md` for the current
   auto-block; an "unterminated comment in CLAUDE.md" finding is cheap
   (`content.count("<!--") != content.count("-->")`) and would have caught this at any
   point in the last several releases. This is the class the WF-13 byte-render check
   cannot see, because the file *is* byte-identical to what the generator produced —
   the generator is what is wrong.

**Verification.** Reproduced 2026-08-01 on `mgf-fed` 0.18.2. Before: `html.parser`
reports 2,751 characters consumed as comment data after the `END` marker, and
`grep -c -- '-->'` finds no terminator anywhere below it. After adding the generator's
own second line: 2,686 characters render, `catch-up` exits 0, `catch-up --apply`
leaves the line intact. Repo census run over 59 `CLAUDE.md` files on this workstation;
21 truncated.

---

### `PAPER-139` — a consumer app cannot clear the outbox gate it is told to run

**Severity:** 🟡 Sharp edge
**Origin:** TelePlasma · 2026-08-01 · discovered by wiring the gate and then filing these very papers

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (mgf-family catch-up round) |
| Submitted | 2026-08-01 |
| Affects | `mgf-fed` ≥0.15.0 · `feedback --in-repo` as a CI gate, for **consumer apps** |
| Decision | pending |

**What hurts.** mgf-fed 0.15.0 added `--in-repo` explicitly *"so an app can gate
its own CI"*, and TelePlasma did exactly that this round (a merge-blocking
`mgf-fed feedback --in-repo . --fresh` step). It works: staging these papers took
the gate from exit 0 to **exit 1**.

But the same release states that **relay stays a supervisor action off the
federation root**, and FB-05 makes a consumer app read-only on every other
federation repo. So the app can *enter* the failing state on its own and
**cannot leave it on its own**.

Under branch protection the two rules compose into a deadlock: the PR that files
a paper is red because the paper is un-relayed, and it cannot be merged while
red; but the relay that would clear it is performed by someone else, against a
repo the app may not write. Filing feedback — the behaviour FB-05 is designed to
encourage — becomes the thing that blocks your own pipeline.

**Why it hurts.** It penalises exactly the discipline the rule wants. The
path of least resistance becomes *don't stage the paper until a supervisor is
standing by*, which is how feedback quietly stops getting filed — the failure
FB-05's "no silent rot" promise exists to prevent, arriving by the opposite
route. TelePlasma hit this on the first paper it filed after adopting the gate.

**What I'd want.** Any one of these would resolve it:

1. **A grace window** — `--in-repo` treats a paper newer than N days (or newer
   than `origin/main`'s merge-base) as *staged*, not as *rot*. Rot is about
   **age**, not about existence; a paper filed in the same PR has not rotted.
2. **A documented intended sequence** for consumer apps, if the deadlock is
   considered acceptable: e.g. *stage → relay → land the receipt and the paper in
   one commit*, which is what TelePlasma's 2026-07-12 round did by accident.
   Right now `--in-repo`'s help text does not say the app cannot clear its own
   gate.
3. **Separate exit codes** for *staged-and-recent* vs *un-relayed-and-stale*, so
   an app can gate on the second without being blocked by the first.

**Verification.** Reproduced 2026-08-01: `mgf-fed feedback --in-repo .` against
this repo returns exit 0 with an empty outbox and exit 1 with the two papers above
staged, on `mgf-fed` 0.18.2. Exit code read from `$?` directly, not through a
pipe.

---

### `PAPER-138` — `migrate` renders "no migration needed" as an error

**Severity:** 🟢 Nice-to-have
**Origin:** TelePlasma · 2026-08-01 · found while planning the same walk

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (mgf-family catch-up round) |
| Submitted | 2026-08-01 |
| Affects | `mgf-fed` ≤0.18.2 · `mgf-fed migrate` |
| Decision | pending |

**What hurts.** Asking whether a cohort walk needs source changes is the natural
first question of any bump. `mgf-fed migrate 0.49 0.51 src --dry-run` answers:

```
✗ unsupported migration: 0.49 → 0.51

Supported migrations:
  0.1 → 0.3
  0.32 → 0.33
  0.38 → 0.39
```

A red `✗` and the word *unsupported* read as **"this tool cannot help you; you are
on your own"**. The actual meaning is the opposite and is very good news: **no
mechanical renames are registered in that span, so no source change is needed.**

**Why it hurts.** The trailing note does explain it — *"A request landing here
means no registered migrations fall within the range"* — but it sits below the
list, after the reader has already been told `✗ unsupported`. On a walk that a
project has been avoiding as risky, the difference between "unsupported" and
"nothing to do" is the difference between deferring again and proceeding. It is
the same *absence-rendered-as-failure* class as PAPER-137, just with the sign
flipped: there, unknown looked like blocked; here, clean looks like broken.

**What I'd want.** Distinguish the two cases the single `✗` currently covers:

- **Span is inside the registered range but contains no renames** → success, e.g.
  `✓ no migrations registered between 0.49 and 0.51 — no source changes needed`,
  exit 0.
- **Span is outside anything the tool knows** (e.g. a typo, or a version older
  than the earliest registration) → keep today's `✗ unsupported`.

If that distinction is not cheaply computable, even reordering — putting the
"means no registered migrations fall within the range" sentence *above* the
supported list, and softening `✗` — would remove most of the misread.

**Verification.** Reproduced 2026-08-01 against `mgf-fed` 0.18.2 for both
`src` and `tests` on TelePlasma at `mgf-common` 0.49.2 → 0.51.0. The walk was
subsequently completed with **zero** source changes and 657 tests green,
confirming the intended reading was correct.

---

### `PAPER-137` — `catch-up` cannot distinguish "blocked" from "could not check"

**Severity:** 🟡 Sharp edge
**Origin:** TelePlasma · 2026-08-01 · cost this project an 18-day stall

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (mgf-family catch-up round) |
| Submitted | 2026-08-01 |
| Affects | `mgf-fed` ≤0.18.2 · `catch-up`'s PAPER-84 cross-sibling pin check |
| Decision | pending |

**What hurts.** When `catch-up` runs outside the project's own environment, the
PAPER-84 sibling-pin compatibility check cannot read the siblings' installed
metadata. It then emits, per sibling:

> Could not read `mgf-fastapi`'s mgf-common pin from its installed metadata (not
> installed in this environment, or unparseable). **Refusing the pin bump** until
> sibling pins can be verified…

and concludes:

> **Not offering `--apply` for this finding — bumping would make `uv sync`
> unresolvable.** Manual workflow: **wait for the conflicting sibling(s) to
> release a version that accepts mgf-common `0.51.0`**, then re-run catch-up.

That final sentence is stated unconditionally, but in this branch **no conflict
was ever observed** — the check did not run. "I could not verify" and "I verified
a conflict" produce the same warning, the same refusal, and the same remediation
advice. The advice is actively wrong in the unverified case: there is no
conflicting sibling to wait for.

**Why it hurts.** This is exactly the shape mgf-fed 0.18.0's own release notes
name as the thing the `audit` verb exists to kill:

> **A check that could not run is a FAILURE, not a pass.** "I could not verify"
> must never share an exit code with "I verified" — that equivalence is what let a
> dead webhook look like a healthy repo for three weeks.

Here it is the mirror image: *could not verify* shares its presentation with
*verified negative*, and the counsel it gives is "wait for upstream." TelePlasma
recorded the resulting "blocked upstream — wait for the cohort" note on
2026-07-13. It was already false: every sibling had moved to the AP-16 wide pin
`>=X,<1.0`, and devpi had published 0.50.0/0.50.1/0.51.0. **The walk was
available the entire time; the project waited 18 days**, and the eventual change
was a pin bump with zero source impact (`mgf-fed migrate 0.49 0.51` reports no
migrations registered in the span).

Run inside the project venv, the same check passes instantly and cleanly — which
is what makes this a presentation defect rather than a logic defect.

**What I'd want.** Three things, in decreasing order of value:

1. **Separate the two outcomes in the message.** An unverifiable check should say
   *"could not check N sibling(s) — this is not a known conflict"*, and must not
   emit "wait for the conflicting sibling(s) to release", because none is known to
   exist. Naming the boundary that could not be crossed — the same discipline
   `audit` already applies — would be enough.
2. **Name the fix that always works**, prominently: *re-run inside the project's
   own environment, where its `mgf-*` deps are installed.* It is currently offered
   as the first of two alternatives, after the refusal has already framed the
   situation as an upstream block.
3. **Consider a distinct exit code or finding kind** for `UNVERIFIABLE` vs
   `CONFLICT`, so a consumer can gate on the difference.

**Verification.** Reproduced 2026-08-01 against `mgf-fed` 0.18.2 in a clean venv
containing only `mgf-fed[cli]`, `mgf-common` 0.51.0 and `mgf-standard` 0.1.69: the
refusal fires for `mgf-fastapi`, `mgf-sqlalchemy` and `mgf-alembic`. The same
command in TelePlasma's own venv, post-walk, reports `✅ pyproject.toml —
mgf-common pin`. Sibling pins were independently confirmed compatible by reading
each sibling's `pyproject.toml` directly: all carry `>=X,<1.0`.

---

### `PAPER-128` — Consumer apps sit outside every CI-currency gate: `drift-check`'s roster excludes them and `catch-up`'s Woodpecker check is presence-only

| Field | Value |
|---|---|
| Status | OPEN |
| Severity | 🟡 High |
| Submitter | hrb-app (commit `5c60339`) |
| Submitted | 2026-07-30 |
| Decision | pending |
| Decided on | — |
| Scheduled for | — |
| Shipped in | — |

**Concern.** A consumer application's committed pipeline can rot indefinitely while every gate it can see stays green. `drift-check`'s generation-currency and gate-floor checks iterate registry members — `src/mgf/fed/_drift.py:588` (`for m in members(root)`), where `members()` enumerates `MGF_LIBRARIES.json` *libraries* — and consumer apps are registry-external by design. The deliberate backstop, `ci-ungoverned` (PAPER-122 resolution), is scoped to cloned `mgf-*` repos that ship **no** pipeline (`_drift.py:537-543`), so an app that HAS a pipeline — however stale — never matches (hrb-app is additionally not `mgf-*`-named). Meanwhile the one check consumer apps DO run, `catch-up`'s Woodpecker check, declares itself "a **presence** check only" (`src/mgf/fed/cli/_catch_up.py:3499`; ✅ whenever `.woodpecker.yml` or `.woodpecker/*.yml` exists, `:3514-3523`). The union of the three is a structural hole: no tool anywhere compares a consumer app's committed pipeline against the currently-generated one.

**Why it matters.** Proven, not hypothesized — hrb-app, 2026-07-29. Its generated-era pipeline predated the clone-hardening re-render (mgf-standard PAPER-109/PAPER-128 family): with no explicit `clone:` block it inherited Woodpecker's default — a partial clone on the repo owner's ~1 h OAuth netrc credential — so pushes red at the clone step (`could not read Username`, exit 128, every real step skipped) for weeks, while `catch-up` reported the CI check ✅ throughout and `drift-check`'s fleet re-render reached only rostered libraries. The fix arrived by a human diffing against a freshly generated sibling pipeline and pinning the canonical hardened SSH clone block by hand; the in-file comment records the hole verbatim: "hrb-app is a consumer app, so it sat outside `mgf-fed drift-check`'s roster and never got the fleet re-render — filed upstream". This paper is that filing. PAPER-123 already established the principle for outboxes: consumer apps are the tier that hits the sharpest edges first and exactly the tier the gate cannot see — and a green check over a rotten state is worse than no check, because it certifies health.

**Evidence.** `src/mgf/fed/_drift.py:19` (`from mgf.fed._roster import … members`), `:588` (member loop), `:599` + `:537-543` (`ungoverned_repos` — registry-external scan deliberately limited to `mgf-*` repos with no pipeline); `src/mgf/fed/cli/_catch_up.py:3490-3523` (`_check_woodpecker_ci` — "This is a **presence** check only"). The incident: hrb-app `.woodpecker.yml` had no `clone:` block until commit `8696438` (2026-07-29, "ci: pin the hardened SSH clone block (PAPER-109/128)") with postmortem in `f6525af`. Cross-refs: PAPER-121/122 (gate floor + `ci-ungoverned`, both member/`mgf-*`-scoped), PAPER-123 (the same blindness, shipped fix, for feedback outboxes), PAPER-118 (`ci-heal` — adjacent but distinct: it restarts a *flaked* clone; a *stale clone block* reds deterministically and no restart helps).

**Suggested shape.**

**Approach A (recommended) — roster consumers for the pipeline-currency gates.** Either the registry (or fleet policy) grows a `consumers` section, or `drift-check` accepts `--also <path>` — or, cheapest and precedented, reuse PAPER-123's shipped discovery: walk federation-root siblings that are real checkouts carrying a `.woodpecker.yml` and hold them to the clone-block and gate-floor currency checks, no registry edit needed (a new consumer app is covered the day it is cloned).

**Approach B — `catch-up`'s Woodpecker check learns the clone-block currency test.** The exact comparison `drift-check` already performs, run against the repo's own committed pipeline, so the check an app runs in its own repo stops certifying presence as health.

Either closes the hole; both is defense-in-depth — A sees the fleet centrally, B tells the app in its own terminal.

**Cost if deferred past v1.0.** Every future template hardening — and PAPER-109's was credential-handling — silently strands the entire app tier, each app rediscovering a centrally-known fix through its own multi-day red-CI archaeology. The failure compounds with fleet size and is invisible by construction: the tools' own reports say ✅ while it happens.

**Decision rationale.** _Filled when triaged._

**Retrospective.** _Filled when shipped._


### `PAPER-127` — `catch-up --apply` promises to migrate the legacy CLAUDE.md marker and never does (21 repos, non-converging)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED |
| Severity | 🟠 High |
| Submitter | Court (2026-07-29 fleet re-render wave) |
| Submitted | 2026-07-29 |
| Shipped in | `52ac1f3` (v0.17.0); made effective by `57e57a0` + `f2d8bc3` (v0.19.0) — the dedupe landed but nothing invoked it |

**Concern.** The `docs/claude/CLAUDE.md` check reports:

> auto-block carries the LEGACY `mgf-common catch-up` BEGIN marker (pre-0.47 adoption — PAPER-04).
> **Run with `--apply` to refresh the block in place and migrate the marker (no duplicate block is written).**

Running `--apply` refreshes the block and **does not migrate the marker**. Re-running reports the
identical warn. It is **non-converging**: `--apply` can be run any number of times and the check
never goes green.

**Mechanism.** The affected files contain **two** auto-blocks. `mgf-alembic/docs/claude/CLAUDE.md`:

- **line 1** — `<!-- BEGIN mgf-common auto-generated — managed by \`mgf-fed catch-up\`.` (canonical)
- **line 298** — `<!-- BEGIN mgf-common auto-generated — managed by \`mgf-common catch-up\`.` (legacy, orphaned)

`_claude_md_block_start()` finds the **first** marker, so `--apply` refreshes the top block
correctly and never touches the orphan below. The detector then still finds
`CLAUDE_MD_LEGACY_BEGIN_MARKER` anywhere in the content (`_catch_up.py:1856,1864`) and re-fires.
So the writer is first-match and the checker is any-match — **the two disagree about what "the
block" is**, and the gap between them is a permanent warn.

The message's parenthetical *"no duplicate block is written"* is true and beside the point: the
duplicate is already there, and nothing removes it.

**Scale.** Exactly **21 of 44** repos. This is the entire remaining `docs/claude/CLAUDE.md`
population in the 2026-07-30 fleet tracker — after a full 41-repo re-render wave took the fleet
from 90 real gaps to 28, these 21 are what is left, and no amount of `--apply` will clear them.

**Why it matters.** A fix-verb that reports success while changing nothing teaches operators that
the warn is noise. It also makes the conformance tracker permanently non-green for a defect that
is one code change away, which is the fastest way to get a gate ignored.

**Evidence.** `_catch_up.py:57-62` (`_claude_md_block_start` — first match wins);
`_catch_up.py:1856` and `:1864` (checker uses `LEGACY_BEGIN_MARKER in content` — any match);
`_catch_up.py:2717-2727` (`_apply_refresh_claude_md` — "a legacy marker is refreshed in place",
emits the current marker for the block it found). Reproduced on `mgf-alembic` (canonical block at
line 1, legacy at line 298) after two consecutive `--apply` runs, both reporting the same warn.

**Suggested shape.** `--apply` should (a) find **all** auto-block markers, canonical and legacy,
(b) refresh the first and **delete the orphans**, and (c) if deletion is too aggressive to do
silently, report the orphan's line number and refuse — either is honest; the current behaviour is
not. Minimum viable: when the checker's any-match finds a marker the writer's first-match did not
handle, say so explicitly instead of claiming `--apply` will fix it.

**Decision rationale.** ACCEPTED and fixed same session — see the commit that flips this row.


### `PAPER-126` — `catch-up --apply` overrides a documented CFM-04 decline, and corrupts a comment doing it

| Field | Value |
|---|---|
| Status | ✅ SHIPPED |
| Severity | 🔴 Blocker-adjacent |
| Submitter | Court (2026-07-29 fleet re-render wave) |
| Submitted | 2026-07-29 |
| Shipped in | `52ac1f3` (v0.17.0) |

**Concern.** Running `mgf-fed catch-up --apply` in **mgf-standard** adds
`mgf-test-supervisor>=0.1.2,<0.2` + `pytest-timeout` to `[project.optional-dependencies].dev`
— a dependency that repo **explicitly and permanently declines**, with the rationale written
in the three comment lines immediately above the insertion point:

> `# mgf-standard -> mgf-test-supervisor, and since mgf-test-supervisor -> mgf-common ->`
> `# mgf-standard it closes a CYCLE that the REL-13 registry generator would record …`
> `# we consciously decline TS-23 here … keeping mgf-standard a PURE LEAF with zero mgf-* deps.`
> `# Documented as a CFM-04 decline-with-rationale …`

`--apply` read past its own explanation and applied the fix anyway. **Committing that output
would close the standards/cornerstone dependency cycle in the repo that defines the corpus.**

**It also corrupts the comment.** The insertion appends a comma to the last *comment* line:

    - # docs/standards/principles/non-adoption-is-valid.md).
    + # docs/standards/principles/non-adoption-is-valid.md).,

TOML still parses (the comma is inside a comment), so **nothing catches it** — the list-append
logic assumes the final line before `]` is an item and never checks whether it is a comment.

**Why it matters.** `--apply` is marketed as *mechanical* and was run across 41 repos in one
wave. A mechanical tool that silently overrides a declared, reasoned decline is not mechanical
— it is opinionated in a direction nobody reviewed. This was caught only because the wave
committed `AGENTS.md` + `CLAUDE.md` by explicit path and the pyproject change was left behind
as unexplained working-tree dirt. **A `git add -A` in that loop would have shipped it.**

**Evidence.** `mgf-standard/pyproject.toml:52-63` (the decline rationale + the injected deps,
reverted). The federation already encodes this decline elsewhere:
`mgf-standard/scripts/fleet_conformance_audit.py:63` lists `"mgf-standard": ("mgf-common pin",
"Federation test supervisor", "Federation marker taxonomy")` as declared N/A — **the audit
knows; `--apply` does not.** `mgf-secrets` showed the same class (TS-19 marker taxonomy
injected, reverted).

**Suggested shape.** (a) `--apply` reads the same decline source the audit does — a repo's
`docs/inprogress/MGF_STANDARDS_CONFORMANCE.md` CFM-04 declines — and **skips-with-a-message**
rather than applying. (b) Failing that, `--apply` must refuse to modify `pyproject.toml`
dependency lists at all and only report. (c) Independently: the list-append helper must not
treat a trailing comment line as an item.

**Decision rationale.** ACCEPTED and fixed same session — see the commit that flips this row.


#### `PAPER-01` — mgf-fed discovery is hardcoded to the mgf family; a second family needs a fork

| Field | Value |
|---|---|
| Status | DEFERRED (2026-07-10 — ACCEPTED with a 5-step plan; needs sooth registry fixtures + design sign-off before it is schedulable) |
| Severity | 🟡 High |
| Submitter | sooth family — cross-family reuse review |
| Submitted | 2026-07-01 |

**Concern.** mgf-fed's federation discovery is bound to mgf-specific paths, an mgf-shaped
registry schema, and a hardcoded cornerstone repo. A second family (e.g. the Zig-language
`sooth` fleet) cannot point mgf-fed at its own fleet without forking `_roster.py` and a few
call sites. The generalization seams are few and visible — but today they are literals, not
config, so a second adopter has no supported entry point.

**Why it matters.** mgf-fed is otherwise a clean, reusable cross-repo orchestrator (roster,
release waves, static gates, CI generation) that a second family would want to adopt whole.
Every hardcode is a place a fork diverges and then rots. The coupling is broad but the
*load-bearing* bindings are concentrated in a handful of spots — so this is a small, high-leverage
change rather than a rewrite. As a coupling indicator, `mgf-common` appears on 276 lines across
24 files and `mgf-standard` on 105 lines across 22 files in `src/`, yet only four seams below
actually block a non-mgf fleet.

**Concrete evidence.**
- **Registry path is a literal.** `src/mgf/fed/_roster.py:30` — `return root / "mgf-standard" / "MGF_LIBRARIES.json"`. The federation root is already env-resolvable (`MGF_ROOT`, `_roster.py:22`), but the standard-repo name and registry filename are not.
- **The roster loader reads mgf-shaped keys only.** `_roster.py:70` — `data.get("release_waves", [])`; `_roster.py:73` — `for lib in data.get("libraries", [])`; `_roster.py:74` — `repo = str(lib["repo"])`. Sooth's registry (`sooth-standard/SOOTH_LIBRARIES.json`) uses `repos[]` with `name`/`slug`/`ring`/`level`/`deps[]` and has **no** `release_waves` and **no** `libraries` — so `members()` returns an empty list against it, silently.
- **Cornerstone identity is a hardcoded comparison.** `src/mgf/fed/_woodpecker.py:425` — `is_cornerstone=member.repo == "mgf-common"` (echoed by `src/mgf/fed/cli/_ping.py:43` — `_CORNERSTONE = "mgf-common"`). Cornerstone-only CI flags (`_woodpecker.py:133-152`) all key off this literal.
- **Gate/release runners assume Python + ruff + wheels.** `src/mgf/fed/_gates.py` composes ambient `ruff` (the `ruff>=0.4,<0.16` pin) as the fleet-wide lint+format gate; `src/mgf/fed/_index.py:19,34` match/glob `*.whl`; `src/mgf/fed/_woodpecker.py:252` emits `dist/{name}-...-py3-none-any.whl`. A Zig family has neither ruff nor wheels.
- **Coupling indicator (grep across `src/`).** `mgf-common` = 276 matching lines / 24 files; `mgf-standard` = 105 matching lines / 22 files.

**Suggested shape.** One change-set makes mgf-fed genuinely multi-family. Keep the mgf values
as defaults so nothing regresses:
1. **`Family` config seam in `_roster.py`** — `(federation_root, standard_repo, registry_file, cornerstone)` resolved from env/dotfile (extend the existing `MGF_ROOT` pattern), replacing the hardcoded `_registry_path` literal.
2. **Normalized registry adapter** — a per-family loader that maps an arbitrary registry JSON onto the existing `Member` dataclass, and **derives release waves topologically from `deps`** when a family declares no explicit `release_waves` (sooth's case).
3. **Per-language runner seam** — gate/build/verify commands keyed by `lang`, so `gates`/`release` stop assuming Python+ruff+wheels (Zig → `zig build` + its own fmt/verify path).
4. **De-hardcode the cornerstone** — `member.repo == family.cornerstone` in place of `== "mgf-common"` (and the `_ping.py` constant).

This is the single seam a second adopter needs; a short **`docs/` note on the `Family`-config
seam** (how to point mgf-fed at a non-mgf registry + register a language runner) would be the
highest-value artifact for that adopter.

*Filed by the sooth family during a cross-family reuse review — feedback, not a fork request.
mgf-fed's orchestrator design is the pattern sooth wants; this paper is about the seams that
would let a second family adopt it without diverging.*

**Resolution (2026-07-10, queue sweep).** Verified fresh: every cited hardcode still stands —
`src/mgf/fed/_roster.py:30` (registry-path literal), `_roster.py:70-74` (mgf-shaped keys),
`src/mgf/fed/_woodpecker.py:509` (`is_cornerstone=member.repo == "mgf-common"`),
`src/mgf/fed/cli/_ping.py:46` (`_CORNERSTONE = "mgf-common"`), and the Python+ruff+wheels
assumptions in `_gates.py`/`_index.py`. **ACCEPTED** — not a sweep-sized change: the seam
touches roster, generator, gates, release and docs together and needs sooth-side registry
fixtures + a design sign-off. Concrete plan (the paper's own shape, ordered): (1) a `Family`
config dataclass in `_roster.py` (`federation_root`/`standard_repo`/`registry_file`/`cornerstone`,
env+dotfile resolved, mgf values as defaults); (2) a registry adapter mapping arbitrary JSON onto
`Member` + topological wave derivation from `deps` when `release_waves` is absent; (3) a
per-`lang` runner table for gate/build/verify commands; (4) `member.repo == family.cornerstone`
replacing both literals; (5) a `docs/` adoption note. Steps 1+4 are independently landable first.

---

#### `PAPER-02` — generated cornerstone CI installs `example_app` un-steered (dependency-confusion)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — fixed via mgf-standard `c6f0523`, re-rendered `d0bb82b`) |
| Severity | 🟡 High |
| Submitter | mgf-fed supervisor (cockpit) |
| Submitted | 2026-07-03 |
| Shipped in | upstream — mgf-standard `c6f0523`, mgf-common `d0bb82b` (verified 2026-07-10) |

**Concern.** The generated cornerstone pipeline installs the in-repo example app with a bare
`pip install --quiet -e "./example_app[dev]"` — **no `--index-url` steering** — immediately
after a properly-steered main install. `example_app` depends on `mgf-common`, so a bare
resolve pulls `mgf-common` (+ any mgf-* deps) from **public PyPI** (dependency-confusion /
stale-version) — the exact hazard `LESSON_LEARNED.md` (2026-06-18) fixed for the main install
via `_woodpecker._steered`, but the `example_app` sub-install was never wrapped.

**Why it matters.** mgf-fed's own `drift-check` flags it (`✗ mgf-common (2)` →
`[ci-unsafe-install] 1 bare pip install … resolves mgf-* deps from public PyPI`). It's on the
**cornerstone's** example-app test — the very step meant to validate a real consumer install —
so a transient public-PyPI stale `mgf-common` would silently test against the wrong version.

**Evidence.** `mgf-common/.woodpecker.yml:223` (`- pip install --quiet -e "./example_app[dev]"`,
bare) vs the steered main install one line up (`:222`, `--index-url …/magogi/prod/+simple/` +
retry-same-command). Generated artifact (provenance marker at top). Steering helper already
exists: `src/mgf/fed/_woodpecker.py` `_steered` (LESSON_LEARNED 2026-06-18).

**Suggested shape.** Route the `example_app` sub-install through the same index-steering the
generator applies to the main install (`_woodpecker._steered` / the `extra_index` knob),
including the `cmd || (sleep 15 && cmd)` retry — never a bare `||` fallback. Re-scaffold
mgf-common.

**Resolution (2026-07-10, queue sweep).** ✅ Shipped upstream and verified live. The canonical
fragment now steers the sub-install with the same-command retry:
`mgf-standard/templates/woodpecker/frag-cornerstone.yml:17` —
`pip install --quiet -e "./example_app[dev]" --index-url "$DEVPI_URL" || (sleep 15 && …same…)`
(mgf-standard commit `c6f0523` "fix(ci-template): steer the cornerstone example_app install
(PKG-06)"). mgf-common re-rendered it: `.woodpecker.yml:243` (commit `d0bb82b` "ci: regenerate
pipeline — steered example_app install (PKG-06)"). No bare fallback remains; the retry re-runs
the steered command.

#### `PAPER-03` — drift-check's `.gitleaks.toml` `[exact]` rule is too strict for a per-repo-customized file

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — `95e14a6` + `bc8dabd`, verified vs the live fleet) |
| Severity | 🟢 Nice-to-have |
| Submitter | mgf-fed supervisor (cockpit) |
| Submitted | 2026-07-03 |
| Shipped in | `95e14a6` + `bc8dabd` (v0.11.2) |

**Concern.** `drift-check` compares each repo's `.gitleaks.toml` **byte-exact** to
`templates/gitleaks.toml`. But `.gitleaks.toml` legitimately carries **repo-specific allowlist
paths** (mgf-fastapi allowlists a test-webhook fixture; mgf-common + mgf-standard allowlist
`.claude/`). Any such justified *addition* reads as `[exact] .gitleaks.toml differs from
template`.

**Why it matters.** False-positive drift on a file that is *designed* to be per-repo
customized. Two central repos show `✗` for correct, security-neutral carve-outs → drift-check
noise (trains maintainers to ignore it), with no clean resolution (removing the entry
re-introduces false-positive leak flags).

**Evidence.** `mgf-fed drift-check` → `✗ mgf-common (2)` (one item) + `✗ mgf-standard (1)`:
`[exact] .gitleaks.toml differs from template/gitleaks.toml`. The differences are additive
entries under `[allowlist].paths`, not rule changes. Companion: mgf-standard PAPER-116
(adding `.claude/` to the template resolves the two current instances regardless).

**Suggested shape.** Make the `.gitleaks.toml` drift rule shape-aware — tolerate *additive*
entries under `[allowlist].paths` (and repo-specific `[[rules.allowlist]]`) while still
flagging removed/changed canonical rules or a missing `--config`. Keeps the real protection
(the config is present + wired) without policing legitimate per-repo allowlists.

**Resolution (2026-07-10, queue sweep).** ✅ Shipped in mgf-fed `95e14a6` ("feat(drift): gitleaks
policy comparison goes shape-aware") + `bc8dabd` (non-destructive restore keeps consumer
carve-outs): `_gitleaks_shape_drift` at `src/mgf/fed/_drift.py:112` tolerates additive
`[allowlist]` list entries while still failing removed/edited canonical entries;
`gitleaks_restore_text` (`_drift.py:198`) re-appends consumer extras on restore. Verified today
against the live template: `_gitleaks_shape_drift(<repo>/.gitleaks.toml,
mgf-standard/templates/gitleaks.toml)` → conformant (`None`) for mgf-fastapi (its extra
test-webhook carve-out is tolerated), mgf-common and mgf-standard. The companion mgf-standard
PAPER-116 also landed the `.claude/` entry in the canonical template (2026-07-04). Unit
coverage: `tests/unit/test_gitleaks_shape.py`.

#### `PAPER-04` — `catch-up --apply` duplicates a legacy `mgf-common catch-up` CLAUDE.md auto-block instead of migrating it

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — legacy-marker alias + in-place migration, this branch) |
| Severity | 🟡 High |
| Submitter | figrecovery |
| Submitted | 2026-07-09 |
| Shipped in | `e9e11b2` (v0.12.0) |

**Concern.** `mgf-fed catch-up --apply` does not recognise a pre-existing
`docs/claude/CLAUDE.md` auto-block written by the OLD `mgf-common catch-up` (its
BEGIN marker reads *"managed by `mgf-common catch-up`"*). Instead of refreshing
the block in place, it **prepends a second**, `mgf-fed`-format managed block —
leaving the file with two full auto-blocks.

**Why it matters.** After `--apply` the repo carried two managed blocks totalling
847 lines, including 24 stale `mgf-common <cmd>` references the new block was
meant to replace. A maintainer must manually dedup — exactly the mechanical toil
catch-up exists to remove — and a less careful run would commit the duplication.

**Evidence.** figrecovery's 2026-07-09 re-sync: its `docs/claude/CLAUDE.md` had a
`mgf-common catch-up`-managed block from an earlier adoption; `mgf-fed catch-up
--apply` prepended a new `mgf-fed` block rather than replacing the legacy one.
Manual dedup (keep the new mgf-fed block + project content, drop the legacy
block) was required before the gate was clean.

**Suggested shape.** On `--apply`, treat the legacy `mgf-common catch-up`
BEGIN/END markers as an alias of the current `mgf-fed` markers — refresh in place
(replace the legacy block) rather than prepending. Alternatively, detect a legacy
marker and emit a one-line migration notice + refuse to double-write.

**Resolution (2026-07-10, queue sweep).** ✅ Shipped (this branch): the legacy
`mgf-common catch-up` BEGIN marker is now an alias — `CLAUDE_MD_LEGACY_BEGIN_MARKER` +
`_find_begin_marker` in `src/mgf/fed/cli/_catch_up.py`; the presence check, `_extract_auto_block`
and `_splice_auto_block` all accept it, and `--apply` refreshes the block IN PLACE while
migrating the marker line to the current `mgf-fed` form (the stale verb never survives a
refresh — no second block is ever prepended). A dedicated audit finding names the legacy-marker
state and routes it to the refresh fix. Tests: `TestLegacyMarkerMigration` in
`tests/unit/cli/test_catch_up.py` (extract/splice/end-to-end `_apply_refresh_claude_md`,
asserting exactly one block + preserved project content).

#### `PAPER-05` — `catch-up`-written AGENTS.md pointer carries a stale "always installed" justification

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — stale justification replaced, this branch) |
| Severity | 🟢 Nice-to-have |
| Submitter | figrecovery |
| Submitted | 2026-07-09 |
| Shipped in | `e9e11b2` (v0.12.0) |

**Concern.** The `AGENTS.md` pointer written by `catch-up --apply` justifies the
agents command with *"`mgf-common` is a dependency of this project, so that
command is always installed."* Post-reorg the command is `mgf-fed agents`, and
installing `mgf-common` does **not** put `mgf-fed` on PATH — they are separately
distributed.

**Why it matters.** The reasoning is now false: a repo can depend on `mgf-common`
and still not have `mgf-fed agents` available. An agent reading the pointer is
told the command is "always installed" when it may not be, and the stale text
reads as un-migrated scaffolding from the mechanical `mgf-common`→`mgf-fed` verb
rename.

**Evidence.** figrecovery's `AGENTS.md` after `mgf-fed catch-up --apply`
(2026-07-09) contained the "`mgf-common` is a dependency … always installed" line
while pointing at `mgf-fed agents`. figrecovery installs `mgf-fed[cli]` as a
separate tool (not a project dep), so the justification did not hold.

**Suggested shape.** Update the generated AGENTS.md justification to reflect the
`mgf-fed[cli]` tool install (e.g. "`mgf-fed` is installed as a federation CLI
tool; run `mgf-fed agents`"), and drop the "dependency of this project ⇒ always
installed" reasoning that assumed the pre-0.47 bundled CLI.

**Resolution (2026-07-10, queue sweep).** ✅ Shipped (this branch): the generated text now says
"`mgf-fed` is installed as a federation CLI tool (`pip install 'mgf-fed[cli]'` or `uv tool
install 'mgf-fed[cli]'` — it is distributed separately from the `mgf-common` library
dependency)" — in `AGENTS_MD_CONTENT` and in `_CLAUDE_MD_TEMPLATE`
(`src/mgf/fed/cli/_init_project.py`); the false "dependency ⇒ always installed" reasoning is
gone. mgf-fed's own `AGENTS.md` + `docs/claude/CLAUDE.md` updated to match (catch-up's AGENTS.md
drift check compares against the constant, so the fleet picks the new pointer up on its next
`catch-up --apply`).

### PAPER-117 — `app-doctor` should warn when the AG-03 hook is present but inert

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — see Resolution) |
| Submitter | mgf-standard (PAPER-117 resolution) on 2026-07-10 |
| Submitted | 2026-07-10 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `mgf-standard/pending-feedback/mgf-fed.md` (FB-05) |
| Shipped in | `e9e11b2` (v0.12.0) |

- **Severity:** 🟡 Sharp edge
- **Filed by:** mgf-standard (PAPER-117 resolution) on 2026-07-10
- **What hurts:** `mgf-fed app-doctor` checks that `.githooks/prepare-commit-msg`
  **exists**, not that it is **active**. A repo that never ran
  `git config core.hooksPath .githooks` — or a pre-commit-framework repo, which
  *cannot* set it (`pre-commit install` hard-errors on `core.hooksPath`; see
  mgf-standard PAPER-117) and hasn't wired the §4b pre-commit path — passes
  doctor while the AG-03 signoff hook silently never fires. figrecovery hit
  exactly this during its 2026-07-09 re-sync.
- **Why it matters:** the local half of AG-18(c) can be silently dead fleet-wide;
  the failure is invisible until the CI `commit-trailers` gate reds a branch
  after the fact (or worse, on repos whose pipeline lacks the fragment).
- **Evidence:** mgf-standard `FEEDBACK.md` PAPER-117 (+ its resolution, this
  wave); `docs/recipes/standardized-git-config.md` §4a/§4b (the two mutually
  exclusive wirings); pre-commit refusal verified on 4.6.0.
- **Suggested shape:** doctor's AG-18 check gains an *active-wiring* probe: PASS
  when `git config --get core.hooksPath` = `.githooks` **or**
  `.git/hooks/prepare-commit-msg` exists (the pre-commit-installed shim); WARN
  (not FAIL — advisory first, per the PAPER-100 staging discipline) when the
  committed hook exists but neither wiring is live.
- **Resolution (2026-07-10, queue sweep):** ✅ shipped (this branch) exactly as suggested —
  `_check_commit_hook` in `src/mgf/fed/cli/_doctor_standards.py` now runs an active-wiring
  probe (`_hook_wiring_active`): PASS when the repo-local `core.hooksPath` resolves to
  `.githooks` (§4a) **or** a `prepare-commit-msg` shim exists in the git *common* hooks dir
  (§4b, worktree-safe via `git rev-parse --git-common-dir`); WARN — never FAIL — when the
  committed hook is INERT, with the two wiring commands in the message. Tests:
  `TestCheckCommitHook` in `tests/unit/cli/test_doctor_standards.py` (hooksPath-wired ok,
  shim-wired ok, inert → warn).
- **Status:** ✅ SHIPPED (2026-07-10)

### PAPER-118 — auto-retrigger a pipeline that reds on the `clone` step alone (PAPER-109(b))

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — see Resolution) |
| Submitter | mgf-standard (PAPER-109 investigation) on 2026-07-10 |
| Submitted | 2026-07-10 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `mgf-standard/pending-feedback/mgf-fed.md` (FB-05) |
| Shipped in | `e9e11b2` (v0.12.0) |

- **Severity:** 🟡 Sharp edge
- **Filed by:** mgf-standard (PAPER-109 investigation) on 2026-07-10
- **What hurts:** the fleet's known clone credential-injection flake
  (mgf-standard PAPER-109) reds pipelines through no fault of the change, and
  the standing mitigation is a **manual** restart (e.g. mgf-fed pipeline #154
  "re-trigger pipeline (clone step flaked)"). In-step retry is proven
  insufficient — plugin-git already defaults to `attempts: 5` / `backoff: 5s`
  and the flake survives it (the netrc is absent for the step's whole
  lifetime; only a fresh pipeline start re-injects it).
- **Why it matters:** every occurrence costs a human a manual re-trigger and
  erodes trust in red (PAPER-109's "people learn to shrug off reds").
- **Suggested shape:** wherever mgf-fed already polls/drives Woodpecker
  pipelines, detect the signature — failed pipeline whose ONLY failed step is
  `clone`, log matching `could not read Username` — and restart it **once**
  (bounded; a second identical failure stays red for a human). Poll Woodpecker
  (self-hosted, unmetered), never Codeberg commit-status (PAPER-110's rate
  budget).
- **Resolution (2026-07-10, queue sweep):** ✅ shipped (this branch) as a new verb —
  `mgf-fed ci-heal` (`src/mgf/fed/cli/_ci_heal.py`, registered in `_mgfcli.py`), hung on the
  Woodpecker API surface mgf-fed already had (`cli/_ping.py`, now shared via `mgf.fed._http`).
  Detection: a `failure` pipeline whose ONLY failed step is `clone` (v2 `workflows[].children[]`,
  `procs` fallback). Once-bound: skipped when ANY later pipeline exists for the same commit
  (covers our restart AND a human's); a second identical red stays red. Restarts via
  `POST /api/repos/{id}/pipelines/{number}`; polls Woodpecker only; `--dry-run` previews;
  `--repo` scoping, else the registry roster. Honest scope note: the log-content match
  (`could not read Username`) is NOT implemented — the Woodpecker log endpoint returns
  base64 entry arrays and the step-shape signature + once-bound already keeps the restart
  safe; tightening on log content is an optional follow-up. Tests:
  `tests/unit/cli/test_ci_heal.py` (signature, once-bound, dry-run, end-to-end).
- **Status:** ✅ SHIPPED (2026-07-10 — `mgf-fed ci-heal`; log-content tightening optional)

### PAPER-119 — bake the Codeberg API rate-budget into mgf-fed's client (PAPER-110, code half)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — see Resolution) |
| Submitter | mgf-standard (PAPER-110 resolution) on 2026-07-10 |
| Submitted | 2026-07-10 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `mgf-standard/pending-feedback/mgf-fed.md` (FB-05) |
| Shipped in | `e9e11b2` (v0.12.0) |

- **Severity:** 🟡 Sharp edge
- **Filed by:** mgf-standard (PAPER-110 resolution) on 2026-07-10
- **What hurts:** the rate-budget discipline lives only as recipe guidance
  (`activate-ci.md` §"Codeberg API rate-budget", mgf-standard PAPER-110) — an
  agent driving `mgf-fed` at automation speed can still hammer Codeberg into
  the hourly 429 wall, because nothing in the client enforces the budget.
- **Why it matters:** the observed failure (mgf-standard PAPER-110 evidence)
  wedged merges for ~an hour after ~9 PR opens + 20s-interval status polls;
  guidance alone doesn't survive an agent that hasn't read the recipe.
- **Suggested shape:** in mgf-fed's Codeberg HTTP layer: (a) a client-side
  steady-rate cap on state-changing calls (a few/min); (b) **exponential
  backoff + jitter on 429** with a hard stop (never retry-hammer); (c) route
  all pipeline-status polling to the Woodpecker API by construction (make the
  Codeberg commit-status call one-shot-only in the API surface); (d) prefer
  SSH git operations where a REST call is avoidable.
- **Resolution (2026-07-10, queue sweep):** fresh-eyes finding first: **mgf-fed has no Codeberg
  REST client at all** — the only HTTP call sites in `src/` are the Woodpecker API
  (`cli/_ping.py`) and the devpi index (`_release.py`); Codeberg writes are printed `tea`/git
  commands a human runs (e.g. `_feedback._pr_commands`), and git rides SSH. Against that:
  **(b) shipped** — new shared helper `mgf.fed._http.request_json` (this branch): exponential
  backoff (5s base, doubling, 120s cap) **+ ±25% jitter** on HTTP 429, honoring a numeric
  `Retry-After`, bounded at 3 retries then a **hard stop** (the last 429 propagates — never a
  retry-hammer). `cli/_ping.py::_api` (and the new `ci-heal`) route through it, so every REST
  call mgf-fed makes — and any future Codeberg call — inherits the budget by construction.
  Tests: `tests/unit/test_http.py`. **(c) holds by construction** — there is no Codeberg
  commit-status client to make one-shot; all pipeline polling/driving is Woodpecker
  (`ping-siblings`, `ci-heal`). **(d) holds** — git operations are SSH; no avoidable REST.
  **(a) ACCEPTED tail** — a steady-rate cap on state-changing Codeberg calls has zero call
  sites to govern today; the standing invariant is that any future Codeberg REST client MUST
  route through `mgf.fed._http`, which is where the cap lands when such a call site appears.
- **Status:** ✅ SHIPPED (2026-07-10 — 429-backoff half + by-construction (c)/(d); (a) steady-rate
  cap is an ACCEPTED tail pending any actual Codeberg call site)

### PAPER-120 — `feedback --relay` crashes on a target with no `FEEDBACK.md` (and half-applies the batch)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-10 — see Resolution) |
| Submitter | mgf-standard (supervisor relay run) on 2026-07-10 |
| Submitted | 2026-07-10 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `mgf-standard/pending-feedback/mgf-fed.md` (FB-05) |
| Shipped in | `e9e11b2` (v0.12.0) |

- **Severity:** 🟡 Sharp edge
- **Filed by:** mgf-standard (supervisor relay run) on 2026-07-10
- **What hurts:** relaying to a target repo that has no `FEEDBACK.md` yet
  raises `FileNotFoundError` from `_relay_one` (`_feedback.py:130` reads the
  target file unconditionally) and aborts the whole `--relay` run mid-batch —
  papers earlier in the plan may already be written/moved while later ones are
  not (non-atomic). Hit live on 2026-07-10 relaying mgf-standard's outbox to
  `mgf-cloud-provision` (a real member with no feedback file yet).
- **Why it matters:** the first relay to any newly-onboarded member fails, and
  a half-applied batch leaves the queues inconsistent — the exact silent-loss
  FB-05 exists to prevent.
- **Evidence:** traceback `mgf/fed/_feedback.py:130 → FileNotFoundError:
  …/mgf-cloud-provision/FEEDBACK.md`; `--dry-run` showed a valid 5-paper plan.
- **Suggested shape:** in `_relay_one`, seed a minimal canonical `FEEDBACK.md`
  header (the feedback-md-template shape) when the target file is absent — or
  skip-with-a-loud-warning; and catch per-paper errors and continue, reporting
  failures at the end (per-paper write+move stays atomic).
- **Resolution (2026-07-10, queue sweep):** ✅ shipped (this branch), both halves, in
  `src/mgf/fed/_feedback.py`: (1) `_relay_one` seeds a minimal canonical `FEEDBACK.md` header
  (`_seed_feedback_md`, the feedback-md-template shape) when the target file is absent and says
  so in its report line — the first relay to a newly-onboarded member now works; (2)
  `run_feedback --relay` captures per-paper `OSError`s, keeps going, invalidates that target's
  PAPER-NN counter (self-healing re-read), prints a failure summary and exits non-zero — a bad
  target can no longer abort the batch mid-way, and each paper's write+move stays atomic (the
  failed paper stays in its outbox untouched). Tests:
  `tests/unit/test_feedback.py::test_relay_seeds_missing_target_feedback_md` and
  `::test_relay_continues_past_a_failing_target` (including the clean re-run after the fix).
- **Status:** ✅ SHIPPED (2026-07-10)

### PAPER-121 — `drift-check`/`app-doctor` should assert required pipeline COMMANDS, not just step names (bespoke pipelines fall behind silently)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-11) |
| Submitter | mgf-standard (WF-13 re-render wave; mgf-standard PAPER-120) on 2026-07-10 |
| Submitted | 2026-07-10 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `mgf-standard/pending-feedback/mgf-fed.md` (FB-05) |
| Shipped in | `f63000c` (v0.12.0) |

- **Severity:** 🟡 Sharp edge
- **Filed by:** mgf-standard (WF-13 re-render wave; mgf-standard PAPER-120) on 2026-07-10
- **What hurts:** repos in `templates/policy.toml [exempt].woodpecker`
  (`mgf-hrb`, `mgf-test-supervisor`, `mgf-website-template`, `mgf-standard`)
  keep hand-maintained pipelines, so `scaffold` never regenerates them and
  `drift-check` skips the generation-currency comparison. A gate added to the
  canonical template therefore reaches every repo EXCEPT them, with no signal.
  `policy.toml`'s `required` list checks step *names*, which stay identical
  while the commands inside them rot.
- **Why it matters:** proven live this wave — after mgf-standard PAPER-115
  added `ruff format --check` to the canonical lint step and the fleet
  re-rendered, all three Python-linting exempt repos still lacked the gate AND
  carried real format drift (including **mgf-standard itself**, the repo that
  authored the gate). Every future gate inherits the same hole.
- **Evidence:** mgf-standard FEEDBACK PAPER-120 (full evidence);
  post-wave `origin/main:.woodpecker.yml` for mgf-hrb / mgf-test-supervisor /
  mgf-standard — `ruff check src tests` present, `ruff format --check` absent;
  `uv run ruff format --check src tests` → 1 drifted file in each.
- **Suggested shape:** add a `required_commands` concept sourced from
  `policy.toml` (e.g. `["ruff check", "ruff format --check",
  "check_commit_trailers.py"]`) and assert it against EVERY repo's
  `.woodpecker.yml` — exempt or not — in `drift-check` (and/or
  `app-doctor --standards`). Exempt means "we don't regenerate your file," not
  "you may silently drop a federation gate." Keep it a substring/step-scoped
  match so bespoke pipelines stay free in shape while staying honest on gates.
- **Resolution (2026-07-11, gate-floor branch):** ✅ shipped, as suggested. New
  `src/mgf/fed/_pipeline.py` defines the **gate floor** — the commands every member's CI
  must actually RUN — and `_drift.gate_floor()` asserts it against EVERY member's
  committed `.woodpecker.yml`, **exempt or not** (a new `ci-command` drift kind).
  `required_commands` is read from the fleet policy when present and DEFAULTS to
  `["ruff check", "ruff format --check", "check_commit_trailers.py"]` when absent, so the
  gate is live before the key lands — **the key still belongs in mgf-standard's policy**
  (`[tool.mgf-fed.policy]` in its `pyproject.toml`, or `templates/policy.toml`), which is a
  different repo and was deliberately not edited from here. Matching is substring, per
  command LINE (`mgf.fed._pipeline.command_lines` — a step *named* `lint` never satisfies a
  gate, and a commented-out command is not a gate), so bespoke pipelines stay free in shape.
  Scoped to `lang == "python"` members: the default list is the Python gate set, and a Zig
  pipeline legitimately has no ruff. `drift-check` reports the floor in its own **GATE
  FLOOR** section. Verified against the real fleet: the four `[exempt].woodpecker` repos'
  `origin/main` pipelines now pass, and a lint step that carries `ruff check` but not
  `ruff format --check` is flagged. Tests: `tests/unit/test_pipeline.py` (20) +
  `tests/unit/test_gate_floor.py` (15) — the latter needs no sibling mgf-standard, so it
  runs in a single-repo CI clone too.
- **Status:** ✅ SHIPPED (2026-07-11)

### PAPER-122 — `app-doctor` should catch the supervisor-coverage ↔ `patch = ["subprocess"]` coupling (green suite, red build)

| Field | Value |
|---|---|
| Status | ✅ SHIPPED (2026-07-11) |
| Submitter | mgf-standard (WF-13 re-render wave; mgf-standard PAPER-121) on 2026-07-10 |
| Submitted | 2026-07-10 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `mgf-standard/pending-feedback/mgf-fed.md` (FB-05) |
| Shipped in | `f63000c` (v0.12.0) |

- **Severity:** 🟡 Sharp edge
- **Filed by:** mgf-standard (WF-13 re-render wave; mgf-standard PAPER-121) on 2026-07-10
- **What hurts:** the canonical coverage step runs the suite through
  `mgf.test_supervisor`, which spawns pytest in CHILD processes. Coverage only
  sees them if the repo's `pyproject.toml` carries `[tool.coverage.run]
  patch = ["subprocess"]` (coverage ≥ 7.10). Without it the build reds with
  **every test passing**: "No data was collected" → `coverage combine` → "No
  data to combine" → exit 1. The pipeline template *documents* the requirement;
  nothing enforces it, and the error names neither the key nor pyproject.
- **Why it matters:** hit three repos in a single wave (`mgf-brand-web`,
  `mgf-cloud-web`, `mgf-fed-web`) the moment they were re-rendered onto that
  step. The signature is maximally confusing (all-green tests, red step), so it
  costs a real debugging session each time; every newly-scaffolded or
  re-rendered repo can walk into it.
- **Evidence:** mgf-brand-web pipeline #31 `test-3.12` log: `[mtest] OK: 39
  passed (39 total)` → `CoverageWarning: No data was collected` → `coverage
  combine` → `No data to combine` → exit 1. Green reference with the key:
  `mgf-http/pyproject.toml`. Fixed per-repo in the wave branches.
- **Suggested shape:** `app-doctor` (or `scaffold`/`drift-check`) FAILS when a
  repo's `.woodpecker.yml` contains `-m mgf.test_supervisor` under a `coverage
  run` while its `pyproject.toml` lacks `[tool.coverage.run] patch =
  ["subprocess"]` — they are a coupled pair (DOC-18-style). Ship the key in the
  cookiecutter by default.
- **Resolution (2026-07-11, gate-floor branch):** ✅ shipped as a **FAIL** (not a warn) in
  three surfaces, all reading the same `mgf.fed._pipeline.coverage_patch_gap`: (1)
  `mgf-fed app-doctor` — a ✗ check with the exact fix as its recommended action; (2)
  `mgf-fed app-doctor --standards` — a `TS-23` **fail** Finding, i.e. it reds the
  `app-doctor` step that the canonical pipeline already runs in EVERY repo, so a
  newly-scaffolded or re-rendered repo hears about the coupling from its own CI; (3)
  `drift-check` — a `ci-coverage` gate-floor item, so the fleet sweep sees it centrally.
  FAIL is justified: both halves of the coupling are read from the repo's own committed
  files (the finding is exact, never a guess), and the consequence is a certain,
  already-observed build failure whose own error message names neither the key nor
  pyproject. Silent when the pipeline does not run the supervisor under coverage, when
  there is no pipeline, or when pyproject cannot be parsed — it never invents a failure.
  Tests: `tests/unit/cli/test_doctor_coverage_coupling.py` (5),
  `tests/unit/cli/test_doctor_standards.py::TestSupervisorCoveragePatch` (5), plus the
  `ci-coverage` cases in `tests/unit/test_gate_floor.py` and `tests/unit/test_pipeline.py`.
  **Not done here:** shipping the key in the cookiecutter — that template lives in
  mgf-standard, a different repo.
- **Resolution (2026-07-11) — the third hole, a repo with NO pipeline at all:** shipped as
  TWO checks, because the reported repo turned out not to be where the report said it was.
  - `ci-missing` — a **registry member** with no `.woodpecker.yml` and no explicit
    `[exempt].no_pipeline` entry is now a gate-floor FAIL in `drift-check`, checked for
    every member regardless of `--shape` (a Zig member with no pipeline runs no gates
    either — the old `--shape python` filter hid it entirely). `[exempt].woodpecker`
    deliberately does NOT excuse it: that list excuses *generation-currency*, and mgf-hrb
    dropping its hand-maintained pipeline would be exactly as bad as never having one.
  - `ci-ungoverned` — **and that check alone would still not have caught mgf-brand.**
    Verified, not assumed: `mgf-brand` is **not a registry member at all**
    (`mgf-standard/MGF_LIBRARIES.json` lists `mgf-brand-qt` and `mgf-brand-web`, never
    `mgf-brand` — it ships no publishable package, so the generator never listed it). Every
    tool we own iterates *registry members*, so nothing was ever looking at it — which is
    the actual root cause of "no gate has ever run on mgf-brand", and a member-scoped check
    reproduces the blind spot instead of closing it. So `drift-check` now also scans the
    federation root for **cloned `mgf-*` repos the registry does not list that ship no
    pipeline**, and fails them (`_drift.ungoverned_repos`). Deliberately narrow — a
    non-registry repo that HAS a pipeline (mgf-website, mgf-cost-watch) is running gates and
    is none of drift-check's business. Verified against the real fleet: exactly **one**
    finding, `mgf-brand`, and no noise. Excusable, like a member, by `[exempt].no_pipeline`.
  Tests: `tests/unit/test_gate_floor.py` (§2 + §4), `tests/unit/test_woodpecker.py`
  (`test_drift_missing_woodpecker_is_a_gate_floor_item` — the old presence-check contract
  deliberately superseded). Neither shells out to `git` (the CI image has no `git` binary),
  so the ungoverned scan detects a checkout by a `.git` path test, not a `git` call.
  **Left to mgf-standard (not editable from here):** registering `mgf-brand`, or recording
  it in `[exempt].no_pipeline` and de-activating it on the forge; and adding the
  `required_commands` key to `templates/policy.toml`.
- **Status:** ✅ SHIPPED (2026-07-11)

### PAPER-123 — `feedback --check` cannot see consumer-app outboxes: the FB-05 no-silent-rot gate is blind to the whole app tier

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | TelePlasma (rescue round S3.2) |
| Submitted | 2026-07-12 |
| Severity | 🟡 Sharp edge |
| Federation paper | relayed by the supervisor from `TelePlasma/pending-feedback/mgf-standard.md` (FB-05); routed here per the FEEDBACK routing table — sibling CLI behaviour |

- **Severity:** 🟡 Sharp edge
- **Filed by:** TelePlasma (2026-07-10 rescue round) on 2026-07-12
- **What hurts:** FB-05 binds *every* project ("a project is read-only on every
  other federation repo") and its gate promises that "a finding can never rot
  silently". But `mgf.fed._feedback` discovers outboxes through
  `_roster.members()`, which enumerates `MGF_LIBRARIES.json` **libraries**.
  Consumer applications (TelePlasma, vm-vmanager-app, hrb-app, figrecovery …)
  are registry-external **by design**, so their `pending-feedback/` is invisible:
  run from the federation root, `mgf-fed feedback --check` printed
  "**No un-relayed feedback in any outbox ✓**" (exit 0) while TelePlasma's outbox
  held **six** papers — one of them a 🔴.
- **Why it matters:** apps are exactly the consumers FB-05 most wants to hear
  from — they hit the sharpest edges first (this batch of six came out of a
  single app's rescue round) — and they are exactly the tier the rot-gate cannot
  see. A silent gate is worse than no gate: it certifies "drained" over a full
  queue, which is the precise failure FB-05 exists to prevent.
- **Evidence:** `src/mgf/fed/_feedback.py:24` (`from mgf.fed._roster import
  federation_root, members`) → `:75` (`for member in members(base)`);
  `_roster.members()` → `load_registry(root)` → `data["libraries"]`. Verified on
  `origin/main` (mgf-fed 0.11.2) at relay time, 2026-07-12. Reproduced:
  `mgf-fed feedback --check` → exit 0 / "no un-relayed feedback" with
  `TelePlasma/pending-feedback/mgf-standard.md` present and un-relayed. Adjacent:
  **PAPER-120** (relay crashes on a target with no `FEEDBACK.md`) — same code
  path, different blind spot.
- **Suggested shape:** either **(a)** walk a declared consumer list in addition to
  the registry (the "consume releases; catch-up automation only" set already named
  in the federation-automation plan) — closes it fleet-wide; or **(b)**, cheaper
  and composable: when `feedback --check` runs **inside a repo**, scan *that*
  repo's outbox regardless of roster membership, so an app can wire the gate into
  its own CI. If instead the fleet's ruling is that apps sit outside FB-05
  entirely, that is a standards CLARIFY (mgf-standard FB-05) — but then the rule's
  "every project" wording should say so, because today it promises a gate the
  tool does not deliver.
- **Status:** ✅ SHIPPED (2026-07-24) — both halves now exist. Shape **(b)** had already shipped as `--in-repo` (while this entry still read OPEN — the queue-hygiene problem the 2026-07-24 MGF Family review was run to find). Shape **(a)**, the fleet-scan hole, is closed here: `_feedback.scan()` now also walks federation-root siblings that are real checkouts carrying a `pending-feedback/` dir — discovered the same way `_drift.ungoverned_repos` finds ungoverned repos, so a new consumer app is covered with no registry edit. Verified against the real fleet (it reports 7 un-relayed papers of genuine FB-05 debt); 2 tests incl. a negative for a non-checkout directory. Note no non-member source appears *today* only because TelePlasma's outbox is currently drained — the hole was structural, not hypothetical.

### PAPER-124 — `app-doctor --standards` should report graceful N/A on non-Python repos (not error)

| Field | Value |
|---|---|
| Status | OPEN |
| Submitter | mgf-common queue sweep (Court/CTO delegated agent) on 2026-07-10 — carries forward mgf-common PAPER-101 (originally filed by mgf-standard's federation conformance sweep, 2026-06-07), MOVED here because the `doctor` CLI left mgf-common at the 0.47.0 pure-library cutover. |
| Submitted | 2026-07-10 |
| Severity | 🟢 Nice-to-have |
| Federation paper | relayed by the supervisor from `mgf-common/pending-feedback/mgf-fed.md` (FB-05) |

- **Severity:** 🟢 Nice-to-have
- **Filed by:** mgf-common queue sweep (Court/CTO delegated agent) on 2026-07-10 — carries forward mgf-common PAPER-101 (originally filed by mgf-standard's federation conformance sweep, 2026-06-07), MOVED here because the `doctor` CLI left mgf-common at the 0.47.0 pure-library cutover.
- **What hurts:** `mgf-fed app-doctor --standards` requires a `pyproject.toml` with `[project]`; on a **non-Python** federation member it prints "cannot detect project shape" and exits non-zero. The Zig libs (`build.zig.zon`: mgf-common-zig, mgf-common-identity, mgf-hrb-{model,kabi,cap,rt}) and the `mgf-website-template` cookiecutter all hit this. It reads as a failure when the honest result is "N/A — not a Python project."
- **Why it matters:** a federation-wide `app-doctor --standards` sweep shows the non-Python members as errors that are actually just out-of-scope for a Python audit — noisy + misleading, exactly the failure shape `catch-up` already fixed for itself (PAPER-97; see `src/mgf/fed/cli/_catch_up.py:214` "N/A — … (non-Python `docs` repo; PAPER-97)").
- **Evidence:** `src/mgf/fed/cli/_doctor_standards.py` — `doctor_standards()` calls `_discover_project`; on `None` it prints "mgf-fed app-doctor --standards: cannot detect project shape (need a pyproject.toml with [project] name + a src/ layout)…" and returns `EXIT_OPERATION_ERROR` (lines 856-864; verified against mgf-fed main checkout 2026-07-10).
- **Suggested shape:** mirror the PAPER-97 catch-up fix — detect a non-Python member (no `pyproject [project]`; presence of `build.zig.zon` / `cookiecutter.json`) and emit a single "N/A — not a Python project (no Python conformance surface)" finding with exit 0 instead of the shape error. The language-agnostic root-doc facts (DOC-15 ledger, DOC-19 `LESSON_LEARNED.md`) could still be checked; the Python code-level checks (AP-09/AP-13/TS-04/DP-12) are N/A.
- **Status:** OPEN

### PAPER-125 — Verb-name reuse across the 0.47 CLI split makes a stale invocation run-but-wrong instead of fail

| Field | Value |
|---|---|
| Status | OPEN |
| Severity | 🟡 High |
| Submitter | hrb-app (commit `96aeb42`) |
| Submitted | 2026-07-29 |
| Decision | pending |
| Decided on | — |
| Scheduled for | — |
| Shipped in | — |

- **Severity:** 🟡 High
- **Filed by:** hrb-app (commit `96aeb42`) on 2026-07-29 — found during the W1 dependency walk to mgf-common 0.50 (the 0.46→0.50 crossing that performs exactly this rename).
- **What hurts:** at the 0.47 pure-library cutover, `mgf-common doctor` became `mgf-fed app-doctor` and `mgf-common feedback` became `mgf-fed feedback-howto`. But **`mgf-fed doctor` and `mgf-fed feedback` both exist as different commands**: `doctor` is mgf-fed's own OP-02 self-diagnostic ("mgf-fed's own operational prerequisites"), and `feedback` is the FB-05 outbox relay ("relay cross-repo feedback outboxes into target inboxes"), whose no-argument default is `--check`. So the obvious migration — mechanically rewrite `mgf-common <verb>` to `mgf-fed <verb>` — silently retargets two verbs. The consumer asked "audit MY app" and got mgf-fed's own health; asked "print the feedback howto" and got an outbox-rot scan. Both run, both print something plausible, both exit 0. hrb-app's doc sweep rewrote ~30 prose call sites across the root docs and `docs/`, and only caught the two collisions by cross-checking every rewritten verb against `--help`.
- **Why it matters:** every consumer crossing 0.46→0.47+ performs this rename, and it is the one migration step a human is most likely to delegate to `sed`. The failure is invisible to every gate: the commands exist, the arguments parse, the exit code is 0, and the output is *shaped* like a diagnostic report. The two flagged forms fail honestly — `mgf-fed doctor --standards` exits 2 on an unrecognized argument, because `doctor` takes only `--root` — so the trap is precisely the **bare** forms, which is also the form that appears in prose docs and agent briefings. Silent-wrong beats loud-broken only when it is actually right.
- **Evidence:** `mgf-fed --help` lists `doctor`, `feedback`, `app-doctor`, and `feedback-howto` as four separate subcommands (verified against mgf-fed 0.16.0 as installed by hrb-app, 2026-07-29): `doctor` → "self-diagnostic — mgf-fed's own operational prerequisites (OP-02)"; `feedback` → "relay cross-repo feedback outboxes into target inboxes (FB-05)"; `mgf-fed feedback --help` → `[--check] [--relay] …` with `--check` documented as "the gate; default"; `mgf-fed doctor --help` → `[--root ROOT]` only. Consumer-side record of the rename sweep: `hrb-app/docs/inprogress/MGF_ADOPTION.md:82` ("All prose across the root + `docs/` docs rewritten from `mgf-common <verb>` to `mgf-fed <verb>`"). Same migration family as PAPER-04 / PAPER-05 (legacy `mgf-common catch-up` artifacts), different mechanism: those are about stale *content*, this is about a stale *invocation* that still works.
- **Suggested shape:** **(A, recommended)** make the legacy intent detectable and say so once. When `mgf-fed doctor` runs with a consumer-app-shaped cwd (a `pyproject.toml` whose `[project]` name is not `mgf-fed`), or `mgf-fed feedback` runs with no outbox in scope and no explicit flag, print one disambiguation line before proceeding — e.g. `note: this is mgf-fed's own self-diagnostic; for a consumer app you probably want 'mgf-fed app-doctor'` — and mirror it for `feedback` → `feedback-howto`. Cheap, additive, no rename, and it lands in the exact terminal where the mistake is being made. **(B)** rename the newer commands (`self-doctor`, `feedback-relay`) and let the legacy names hard-error with a pointer for one minor cycle — cleanest semantically, but it breaks the fleet's own committed pipelines that already call `doctor`/`feedback`, so A first. Either way, a one-line "renamed verbs, and the two names that were reused" table in the 0.47 migration notes would have caught this before the terminal did.
- **Cost if deferred past v1.0:** every future consumer migration pays the same audit tax, and the ones that skip the audit ship docs and agent briefings that confidently tell the next reader to run the wrong command. The cost is not the wrong output once; it is the wrong instruction persisted into a repo's documentation, where nothing will ever fail to reveal it.
- **Status:** OPEN
