# Changelog

All notable changes to `mgf-fed` are documented here. Format: [Keep a Changelog](https://keepachangelog.com/);
versioning: [SemVer](https://semver.org/).

## [Unreleased]

## [0.25.7] - 2026-08-14

### Fixed
- **The quiescence guard was watching `HEAD`, not the ref the checks read.**
  0.25.6 shipped `bench_heads` running `rev-parse HEAD` — the working checkout —
  while every check reads `refs/remotes/*`. A fresh-context chair measured both
  halves of the mismatch, and both were live on this bench:

      a commit on a SIDE BRANCH   -> HEAD moves, `ls-tree origin/main` identical
                                     -> the guard REFUSED a run over a change no
                                        check saw
      a fetch advancing origin/main -> what every check reads changes, HEAD still
                                     -> the guard SAID NOTHING

  Four repos here sit on side branches, three of them because the implementer was
  working in them — so the guard was loud where it should be quiet and quiet where
  it should be loud. Not strict; pointed at the wrong thing.

  `bench_heads` now records `_default_ref`'s SHA, and a repo whose ref cannot be
  resolved is recorded as `"unresolvable"` rather than falling back to `HEAD` —
  the fallback would reintroduce the defect, and *"I could not read this"* must
  not render as *"this did not move"*.


## [0.25.6] - 2026-08-14

Patch release. Everything here came from a court chair, and three of the five
findings were against code this repo shipped hours earlier.

### Fixed
- **`_roster.py` is now a gate path (board item 77).** The PAPER-144 fix moved
  eleven roster predicates into one `is_git_repo()` in `_roster.py` — which
  `CANON_PATHS["gate"]` did not watch. Measured: a commit touching only
  `_roster.py` produced *"(NONE — no round is demanded)"*, while the same change
  plus one `_audit.py` line convened a chair. **A review-coverage regression,
  shipped as a refactor.** Whoever decides what counts as a repo decides what
  every gate can see.
- **The tier carve-out is READ from canon, not restated here (board item 71).**
  `templates/policy.toml` now declares `library_only_present` /
  `python_only_present`; `_drift._carveout()` reads them and the built-in
  frozensets are a compatibility fallback, pinned equal to canon by tests.
  Before: two members identical but for `kind`, both missing every `present`
  file, gave **seven** findings as a library and **four** as an app — three files
  vanishing with no finding and no note, while canon said all seven were owed by
  everyone. Behaviour is unchanged; it is now *declared*.
- **`_origin_view._mirror_repo`'s comment was false (board item 78)** and is
  corrected: mgf-standard's `gen_mgf_libraries.py` still requires a `.git`
  DIRECTORY (PAPER-163, open), so the planted marker is load-bearing. A FILE
  marker makes the real generator see one member where a directory shows two.

### Added
- **`ci-branch-reachable` (board item 72)** — a pipeline whose `when.branch`
  matches **no** branch on its own origin never fires, so the board shows *no run
  and no red*. `ci-workspace-truth` reads the clone section; nothing read the
  trigger.

  **The predicate is "none match", not "any missing", and that distinction is the
  check.** Measured across the estate first: seven repos deliberately declare
  `[main, master]` so one pipeline serves both conventions — "any missing" flags
  all seven and finds nothing real; "none match" flags none and still catches the
  `master`-default member whose pipeline one careless word would retire. 55 repos
  examined live, 0 findings. Offline is a **named skip**, never a pass.

- **The audit artifact now expires on CONTENT, not only on the clock (board items
  73 and 75).** `--write` records a sha256 of its inputs (`MGF_LIBRARIES.json`)
  and `--check-artifact` refuses when they have moved: *"it describes a different
  fleet, not merely an older one."*

  `MAX_AUDIT_AGE_DAYS` is 14, and the committed artifact was **six days old** —
  comfortably inside — while describing the registry as it was *before*
  figrecovery was enrolled as an app-tier member. It passed. A member's `kind`
  decides which gates apply to it, so a two-line registry edit changes what
  "clean" *means* for that repo; that edit now invalidates the audit and forces a
  re-run.

- **`--write` refuses to let a moving bench masquerade as a measurement.** It
  records every repo whose HEAD changed between the start and end of the run, and
  `--check-artifact` rejects an artifact naming any. Found by making the mistake:
  a 45-minute run overlapped a commit to `mgf-fed`, and the artifact asserted
  `MGF_LIBRARIES.md is STALE` about a registry that `--check` called CURRENT one
  minute later. **Not stale, and not about an older fleet — a state that existed
  at no single moment**, which nothing in the tool could previously say.

  > **CORRECTION (2026-08-14, added after release — 0.25.6 is published, so this
  > entry is annotated rather than rewritten).** Two of the three claims above are
  > wrong, and a fresh-context chair found both.
  >
  > **"45-minute" is wrong.** Measured from my own logs: 12m19s / 16m06s / 15m31s,
  > each including a fetch loop I had prepended; the chair measured 649s of checks.
  > The figure came from a foreground attempt hitting a 10-minute tool timeout and
  > was then repeated as if measured.
  >
  > **The disproof was invalid.** `gen_mgf_libraries.py --check` reads WORKING
  > TREES; the audit check reads the ORIGIN MIRROR, and `MGF_LIBRARIES.json` was
  > modified in the working tree at the time. Different questions. The finding may
  > have been correct.
  >
  > **And the guard shipped here watches the wrong ref**: `bench_heads` recorded
  > `rev-parse HEAD` while every check reads `refs/remotes/*`, so it refused runs
  > over side-branch commits no check saw, and stayed silent when `origin/main`
  > advanced. Fixed in the next release; see `[Unreleased]`.

- **PAPER-144 is enforced as a CLASS (board item 76).** `test_no_module_asks_is_dir_of_dot_git`
  walks every module under `src/`, parsed with `ast` rather than grepped — the
  regex draft flagged two *comments describing the defect*, including the
  `is_git_repo` docstring written to explain it.

- **`uv.lock` currency pin (PAPER-167)** and **a `DEGRADED INSTALL` notice in
  `--help` (PAPER-168)**, both filed and fixed since 0.25.5.

### Fixed
- **`_roster.py` is now a gate path (board item 77).** The PAPER-144 fix moved
  eleven roster predicates out of `_audit`/`_court`/`ledger_seed` and into one
  `is_git_repo()` in `_roster.py` — which `CANON_PATHS["gate"]` did not watch. A
  fresh-context chair measured the consequence: a commit touching only
  `_roster.py` produced *"(NONE — no round is demanded)"*, while the same change
  plus one `_audit.py` line convened a chair. **A review-coverage regression,
  shipped as a refactor.** Whoever decides what counts as a repo decides what
  every gate can see. Pinned two ways: the path is asserted directly, and a
  second test walks `src/` for any module *defining* `is_git_repo` and fails if
  it is not a gate path — so the check follows the predicate if it moves again.

### Added
- **PAPER-144 is now enforced as a CLASS, not at two sites (board item 76).**
  The 0.25.5 sabotage proof covered 2 of 11 sites; nine — including
  `ledger-seed`, the paper's own symptom — were enforced by nothing. Nine
  bespoke tests would be the wrong answer, because the defect is a class: *this
  module decides whether a path is a repo, and asks the wrong question.*
  `test_no_module_asks_is_dir_of_dot_git` walks every module under `src/` and
  fails on any `(… / ".git").is_dir()` call.

  It parses with **`ast`, not a regex** — the first draft was a regex and it
  flagged two *comments describing the defect*, including the `is_git_repo`
  docstring written to explain it. A checker that cannot tell code from prose
  about code gets silenced by the first person it inconveniences.

  The one deliberate exception (`cli/_doctor_standards.py`, AG-19) sits in an
  explicit `_ISDIR_ALLOWED` with its reason, and a companion test fails if that
  entry stops offending — **an allowlist entry nobody re-checks is a claim, not
  a decision.** Sabotage-verified on the previously-unenforced sites: reverting
  `ledger-seed` now reds, reverting an `_audit.py` site now reds, and repairing
  the allowlisted site reds the staleness test.

### Changed
- **`_origin_view._mirror_repo`'s comment corrected — it was false (board item
  78).** It claimed a directory marker was "no longer forced" because PAPER-144
  moved mgf-fed's predicates onto existence. But this mirror's consumer is
  **mgf-standard's** `gen_mgf_libraries.py`, which still tests `.is_dir()` —
  PAPER-163, still open. Measured with the real generator: a FILE marker makes
  `discover()` see one member where a DIR marker shows two. The comment was an
  invitation to silently shrink the registry.


## [0.25.5] - 2026-08-14

Patch release. One fix, at eleven sites — and the reason it needed eleven is the
part worth reading.

### Fixed
- **The audit family was worktree-blind (PAPER-144).** `.git` is a DIRECTORY in a
  normal clone and a **FILE** (a `gitdir:` pointer) in a linked worktree, so
  `(repo / ".git").is_dir()` answered "no" for a checkout git itself calls
  `--is-inside-work-tree true`. A federation member checked out as a worktree was
  dropped from the roster **with no finding at all** — the audit reported
  "nothing to look at" about a repo it could not look at. Worktree-isolated agent
  seats are normal fleet practice, so the blind spot sat exactly where the
  automation stands.

  Fixed by `is_git_repo()` in `_roster`, now used at all eleven sites:
  `_audit.py` x8 (roster discovery, ref resolution, tree reads), `_court._repos`
  x1, and `cli/_ledger_seed.py` x2 — the last being the paper's own reported
  symptom, `ledger-seed` printing `not on disk — skipped` about the caller's own
  working directory.

  **The paper had been half-landed since 2026-08-05.** `_roster.py` already
  carried the correct predicate *and a comment reading "(PAPER-144's exact
  defect)"*, while `_audit`, `_court` and `ledger-seed` never received it. 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 against a real `git worktree add` rather than a planted `.git` marker —
  a planted marker is the workaround this defect taught people to write, and
  `_origin_view._mirror_repo` still plants one (its comment cited the predicate
  this release changes; corrected in place). Sabotage-checked in both directions:
  each reverted site reds exactly its own test and no other.

  > **CORRECTION (2026-08-14, added after release).** That last sentence is
  > **false as written**, and a fresh-context chair measured it: reverting the
  > eleven sites individually leaves **nine of them green**, including
  > `cli/_ledger_seed.py` — *this paper's own reported symptom*. The proof
  > covered the **two** sites the new tests target, not eleven. Corrected here
  > rather than quietly edited, because 0.25.5 is published and a reader may
  > already have believed it. Class-wide enforcement landed next; see
  > `[Unreleased]`.

  **One site deliberately unchanged:** `cli/_doctor_standards.py:1133` (AG-19,
  the runtime memory store) reads `.git/config` directly twenty lines later, and
  for a linked worktree that path resolves only via `commondir`. Flipping the
  predicate alone would trade a wrong *skip* for a wrong *reason*. Filed rather
  than half-fixed.

## [0.25.4] - 2026-08-12

Patch release, and the shortest possible demonstration of why the check it fixes
exists: 0.25.3 shipped, was installed, and its very first run against the estate
**refused a wave it should have passed** — because the act's date was read on the
forge's calendar rather than this bench's.

### Fixed
- **An act is dated on the reader's calendar, not the committer's.**
  `_last_commit_date` used `git log --date=short`, which renders in the COMMIT's
  own timezone. Codeberg stamps merge commits `+0200`, so four merges made at
  15:44 local (UTC-7) came back dated **2026-08-13** — a day ahead of any date a
  human writing a round on this bench could produce. `court --check` printed
  `FAIL wave … act dated 2026-08-13`, and no round could have satisfied it until
  the calendar caught up.

  A round is dated by a person reading a local calendar; the act must be read on
  the same calendar or the two are not comparable at all. Now
  `--date=format-local:%Y-%m-%d`.

  The fresh-context chair on 0.25.3 had already named this class in its
  `blind_spots` — *"`window_start` is truncated to a LOCAL date while
  `write_round_template` stamps rounds in UTC"* — and the repair for that half
  did not generalise to this one. Both sides are now local.

  The regression test builds its instant FROM the bench's own UTC offset so the
  two calendars are guaranteed to disagree, and skips with a stated reason on a
  bench running at UTC. A fixed literal would have been vacuous on some benches,
  which is exactly the failure this test is about.

## [0.25.3] - 2026-08-12

Patch release: `court --check`'s window was never a date. The gate that exists to
refuse a false pass had disarmed its own worst guard in the only invocation that
actually runs, and the disarming was invisible because its test used a different
input shape than production did.

### Fixed
- **D3 — the window bound was compared as a date it was not.** `run_court` took
  `_normalise_since(since)[:10]` as the window's start and compared each round's
  ISO date to it with `>=`. That is chronological only when BOTH sides are ISO
  dates, and a relative expression passes `_normalise_since` through untouched, so
  the bound was the literal string:

      "2019-01-01" >= "1.day.ago"   -> True   (an ancient round licenses the act)
      "2026-08-12" >= "3.days.ago"  -> False  (a round written today does not)

  The pre-commit hook in the court workspace passes `--since 1.day.ago`. So **D1**
  — *"a round must not license its trigger forever"*, the defect this module's own
  comments call its worst — was switched off in the only place the gate runs, from
  the day that hook shipped, while `test_a_round_predating_the_window_does_not_license_the_trigger`
  stayed green on a bare date. Same shape as F-45 once more: the test exercised a
  path production does not take.

  The window is now resolved through **git's own approxidate**
  (`git rev-parse --since=<expr>` → `--max-age=<epoch>`), so every form git accepts
  resolves identically and there is no second date parser to drift. A fully
  specified ISO date still resolves without git; a relative form that git will not
  answer for is `None`, which is exit 2, never a pass.

- **An unresolvable `--since` is refused rather than silently meaning "now".**
  Approxidate does not reject garbage — measured, `not-a-date` resolves to the
  current second, producing a zero-width window in which nothing can have happened.
  A window under an hour wide, or one starting in the future, now exits 2 and says
  which. The help text also stops claiming `--since` takes "a date or a ref": git
  turns `HEAD~5` into a nonsense timestamp (measured: seven days ago) rather than
  rejecting it.

- **A root with no repos exits 2.** It fell through to *"no fleet-wide act in the
  window"* and exited 0 — a clean bill of health issued over an empty room.

- **D1 is bounded by the ACT, not by `--since`.** The rule *"a round must not
  license its trigger forever"* compared each round's date to the caller's window
  start — so how old a round may be was a function of an argument. A fresh-context
  chair proved the consequence on this very act: widening the court hook from
  `1.day.ago` to `7.days.ago` (for coverage, a sound goal) bought seven days of
  licensing slack as a side effect, and a live run printed **one gate act `OK`
  four times over**, licensed by four rounds that had judged other things. D2
  cannot compensate — canon/gate acts carry `repos=[owner]`, one fixed string that
  every round for that trigger contains by construction.

  The bound is now the date of the newest commit **that the act is made of**
  (path-scoped: a canon act is dated by when canon moved, not by whatever else
  landed in the repo that week — caught live within the hour, when an unrelated
  `mgf-fed` commit dated a gate act six days late and reddened three correct
  rounds). `--since` now decides only *which acts are asked about*, so widening
  the window is purely more coverage. An act whose date cannot be read is refused,
  not abstained on — the same reasoning as the empty-`repos` branch beside it.

- **A round dated in words is not dated.** `date` is a hand-typed TOML field that
  was never validated while the code compared it as a *string*, so anything
  sorting above `"2026-…"` was permanently in-window. The chair measured five —
  `yesterday`, `TBD`, `n/a`, `soon`, `pending` — each licensing its trigger with
  exit 0. It is now parsed with `date.fromisoformat`, and a value that is not a
  date fails loudly.

- **The `--since` argparse help stopped inviting a ref.** The runtime error string
  was corrected and `--help` — the surface a caller reads *before* choosing a
  value — was not. Same act, two surfaces, one fixed.

### Withdrawn before release
- **D4, a `.git/FETCH_HEAD` freshness probe, was cut by the chair that reviewed
  it.** The idea was to let a quiet-but-freshly-fetched fleet pass instead of
  blocking commits, by reading FETCH_HEAD's mtime as "when this machine last
  ASKED the forge". One measurement killed it: **a FAILED `git fetch` writes
  FETCH_HEAD too** — an unreachable remote exits 128 and *creates* the file with a
  current mtime. In a forge outage every repo would have failed to fetch, every
  repo would have looked fetched, and the gate would have printed *"a quiet fleet
  rather than a blind gate"* over exactly the blind bench the probe existed to
  detect. It also passed on **1 of 65** repos fetched, with the count printed but
  not in the exit code — a verifier that is not in an exit code does not exist.

  So "no repo moved anywhere" still exits 2. The message now names *why* it cannot
  tell (`origin/<default>` is a local cache) instead of only that it cannot. The
  wolf-cry that motivated D4 is answered by the **window** instead: the court hook
  moved to `7.days.ago`, which is quiet only when the estate genuinely is.

- **A one-hour floor under the window** was also cut. It caught garbage
  (approxidate returns *now* for what it cannot parse) but refused three
  legitimate windows the chair measured: `--since <today>` in the first hour of
  any day — the form the help text recommends — `--since 30.minutes.ago`, and a
  post-merge CI invocation windowed to the merge that just happened. The garbage
  case is now detected directly: the expression and `now` are resolved in **one**
  `rev-parse` call (no clock tick between them) and refused only when they are the
  same instant.

## [0.25.2] - 2026-08-07

Patch release: one hardcoded name, in both of the functions that had it. The
audit's shared ref resolver asked every repo about `origin` and about nothing
else, so a clone whose remote is named anything else was reported as having
nothing to audit — and the freshness probe one function below it did the same,
which is why the fix is taken as a class rather than a line. Driving record is
the `ci-workspace-truth` / `soothrede` waiver in mgf-standard's
`AUDIT_ACCEPTED.toml` (review_by 2026-08-20), which named the remedy this release
ships: *"have `_default_ref` resolve the configured remote instead of hardcoding
the name"*.

### Fixed
- **`_default_ref` resolves the repo's CONFIGURED remote.** `soothrede`'s only
  remote on the bench is named `codeberg`, so `refs/remotes/origin/HEAD` and both
  named fallbacks missed, the helper returned `""`, and `ci-workspace-truth`
  reported *"no origin/\* default-branch ref — nothing to audit"* about a repo with
  an entirely ordinary default branch. That is this module's founding equivalence —
  *"I could not look"* answering as *"there is nothing there"* — committed in the
  one helper every committed-layer check reads its evidence through, where a single
  wrong assumption is invisible in eight call sites at once. The waiver was filed
  against the consumer precisely because nothing was wrong with `soothrede`.

  The remote is now resolved before any ref is built: `origin` where it exists
  (every existing repo is unchanged, including where `origin` is one remote among
  several), otherwise the sole configured remote. **Several remotes and no
  `origin` still returns `""`** — remotes can disagree about what the default
  branch is, and picking one by position would put a coin toss behind an exit
  code, which is a smaller version of the defect rather than a fix for it.
  Ambiguity stays unaudited and the caller's finding is then true. The return
  contract, the signature and all eight call sites are untouched.

  Fixtures are real git repos with real remotes (`requires_git`, as the
  neighbouring agent-context tests): the question under test is what git answers,
  and a faked `_git` would only prove the parser agrees with the fixture. They
  pin the resolution ORDER too — `<remote>/HEAD` outranks the named fallbacks —
  by giving the remote both a `main` and a `master` and pointing HEAD at the one
  the fallback order would not have guessed.

- **The freshness probe asks the remote the ref names, not `origin`.** Taken as a
  class, because half of it is worse than none: `_origin_ref_unverified` ran
  `ls-remote origin` by name, so a fixed resolver handing over a good
  `refs/remotes/codeberg/main` met a probe that reached nothing — and this module
  turns "reached nothing" into a finding. That trades *unresolvable* for
  *unverifiable* and flips a repo from waived-green to **red on the first online
  audit**, which is not what fixing the resolver was for. The remote name is
  already carried in the ref (`refs/remotes/<remote>/<branch>`), so the probe now
  reads it rather than assuming it — a read, not a signature change. Both the
  unreachable and the STALE messages name the remote that actually holds the ref;
  an operator cannot fetch from a name their clone does not have.

  **Only the online path was ever affected.** `--offline` returns before the
  probe, which is exactly why this half stayed invisible while the resolver's half
  was visible in every audit artifact. The end-to-end pin runs a real check, not
  its parts: a conformant member whose only remote is `codeberg` audits clean —
  and fails against the unfixed resolver ("nothing to audit") *and* against a
  fixed resolver with an unfixed probe ("could not reach the forge") for two
  different reasons, so it can only be satisfied by both.

  Together these close the class the `soothrede` waiver names, for the resolver
  and for the probe below it.

## [0.25.1] - 2026-08-06

Patch release, cut as an incident fix: 0.25.0 carried the new `registry-consistency`
audit check to the fleet, that check consumed the working-tree read PAPER-152 was
filed about, and every mgf-standard pipeline went red with the founder's parked
checkouts reported as defects of the fleet. PAPER-152's scheduled resolution half
ships here — its recorded trigger was "the next mgf-fed feature wave", and a
fleet-red canon pipeline is that wave arriving early.

### Fixed
- **The divergence banner no longer lands in the stream that becomes findings.**
  0.25.0 printed it to stdout so the label would travel beside the verdict. That
  stream is machine-parsed: `_audit.check_registry_consistency` captures
  `run_registry`'s stdout and turns every `- <text>` line into a `Finding`, which
  is exactly the banner's member-list shape — capped at 8, hence **exactly 8
  spurious findings**, on top of one true one. A diagnostic must not share a
  channel *and* a shape with the data it annotates; the banner now goes to
  **stderr**, where it is still loud, still adjacent in any log, and structurally
  incapable of being read as a verdict line.
- **`mgf-fed --version` now sets identity, as its own docstring always claimed.**
  The `--version` fast path (added for the WF-07 release smoke) had been inserted
  *above* the `set_identity` call, so `main(["--version"])` returned without ever
  setting it. The regression was invisible because the test that asserts exactly
  that case — `test_sets_identity_without_filesystem_side_effects` — passed anyway:
  inside the full suite it inherited an identity an earlier test had set, so it was
  green on leaked global state, and alone it fails with `app_name()` returning the
  default `'app'`. Found because the federation test supervisor runs tests in child
  processes and this release's new fixtures re-partitioned them. Identity is set
  first now; `--version` still short-circuits and still exits 0.

### Changed
- **`registry --check` resolves every member at `origin/<default>` (PAPER-152,
  resolution half).** The mgf-standard generator reads whatever a federation root
  contains; mgf-fed owns which root it walks, so `--check` now walks a mirror
  (`_origin_view`) built from each member's committed top-level files at its origin
  default branch. The generator itself is untouched — it is another repo's file
  (FB-05) and this incident is not the moment for a unilateral cross-repo edit.

  The mirror copies **every** top-level blob rather than a known file list, so it
  needs no knowledge of what the generator reads; a curated list would be a second
  two-map lockstep across independently-released repos, the defect 0.25.0 had just
  fixed in `_catch_up._scaffold_dest`.

  **Bench divergence is now strictly informational.** The banner names parked
  checkouts; the check's findings are only true registry↔origin-default
  disagreements. On the founder's bench that turns 9 findings into 4 — and all four
  are real, including two pins that genuinely exclude 0.25.0. The remediation text
  the paper complained about ("walk it forward + verify") is now *correct* advice
  where it appears, which was the whole point.
- **`registry --source={origin,worktree}` and `--fetch`** (PAPER-152's third
  offer). `--check` defaults to `origin`; a regen defaults to `worktree`, because
  it writes a file the operator must be able to reproduce from what they can see.
  `--source=worktree --check` is the deliberate local preview and says outright
  that its verdict describes no remote. **Fetch is opt-in**, and the reasoning is
  recorded rather than assumed: `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 anyway.
- **A member that cannot be resolved at origin degrades LOUDLY.** No remote-tracking
  ref, no git binary, git declining to answer → its working tree is used *and it 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: "I could not grade" must never share an outcome with "it
  is consistent".

## [0.25.0] - 2026-08-06

Minor release: three tools stop answering about themselves and start answering
about their caller. `bench rebaseline` re-baselines the checkout you are standing
in rather than the one it shipped from; `registry` and `drift-check` say out loud
when the checkouts they graded exist on no remote; and the presence policy stops
demanding a library's contract surface from a member that has none. Enacts the
founder-delegated triage of PAPER-148..154 (mgf-fed half).

### Added
- **A loud DIVERGENT CHECKOUTS banner on `registry` and `drift-check`
  (PAPER-152).** Both gates resolve every federation member by reading whatever
  is checked out on the local bench, and report the result as *the fleet's
  state*. A bench is a workspace, not a mirror: on the founder's bench **52 of
  53 registry members sit on parked feature branches**, so the gates graded a
  fleet that exists on no remote — and wrong in both directions at once, since a
  checkout behind its own release under-records while one carrying an unmerged
  walk branch over-records pins that were never shipped. It is not theoretical:
  `registry --check` produced two false REDs naming TelePlasma, whose pins were
  exactly right, because it read mgf-fed's and mgf-standard's parked checkouts.
  The harm was the remediation text — *"walk it forward + verify"* against a
  correct pin means **downgrading it to match a stale working directory**, so
  the gate's own instruction was the regression, and the only defence was
  knowing the tool well enough to distrust it.

  The banner names each diverged member, its branch, the ref compared against
  and the ahead/behind counts (capped at 8 with a suppressed-count tail;
  `MGF_FED_DIVERGENCE_LIMIT` widens it), and says outright that the verdict may
  describe no remote. **Exit codes are unchanged** — this converts a silent
  false RED into a self-labelling one, it does not add a failure. It never
  fetches (no gate gains a network dependency), skips any checkout it cannot
  resolve rather than guessing, and `MGF_FED_NO_DIVERGENCE_BANNER=1` silences it
  for a bench that knows it is previewing local state. Resolving each member at
  `origin/<default>` — the real fix — is scheduled as PAPER-152's second half.

  The real-repo fixtures for it carry the house `requires_git` marker (the banner
  resolves refs *through* git, so a faked `_git` would only prove the parser agrees
  with the fixture), joining the ~94 git-attributable tests this suite already skips
  on `python:*-slim`. Two companion pins never skip — the scan degrades to silence
  when git is absent, and the banner's rendering/cap/tail are asserted over
  synthetic rows — so a git-less runner cannot mute the feature's coverage
  entirely. `frag-mgf-common-matrix.yml` is the one generated step that runs the
  suite without git's two lines; filed upstream at
  `pending-feedback/mgf-standard.md`.

### Changed
- **`bench rebaseline` operates on the CALLER's checkout (PAPER-153).** It
  resolved `tests/perf` against `Path(__file__).parents[4]` — its own install —
  which after the 0.47 CLI split was reachable from **nowhere**: from a wheel
  that path is `…/lib/python3.13`, and mgf-fed's own checkout has no
  `tests/perf` at all (the perf suite belongs to mgf-common, which the path can
  no longer see). A documented remedy, quoted in the failure message of the very
  gate it exists to fix, that succeeded from zero directories — hit at the
  highest-trust, lowest-patience moment a CLI gets. It now searches the current
  directory and each parent for `tests/perf/`, takes an explicit `--path`
  override (a repo root, or the perf directory itself), runs the suite from that
  tree's root, and on failure names **what** it looked for and **where** instead
  of pointing at a repo it cannot reach. The paper's own ask — a test that runs
  the command from a directory that is not the package's install — ships with
  it: an out-of-process `python -m mgf.fed bench rebaseline` against a seeded
  tmp tree. Its absence is how this survived a CLI split.
- **The `present`-list check is tier-scoped, not just language-scoped
  (PAPER-150(ii)).** `policy.present` is library-shaped as well as
  python-shaped: it demanded `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 — the successor of the defect PAPER-151 fixed one layer down,
  and fixed the same way, on the same `kind` field. The classification is the
  canon's: DOC-01 v2.11 §2.0.1 makes `PUBLIC_API.md`/`.json` library-only in as
  many words, and `SCOPE.md` is absent from the universal baseline while SP-01/02
  name *"the library"* as their subject — so it is three files, not two. The
  language gate and the tier gate stay independent (a Zig **library** is still
  excused the Python-packaging pair and still owes `SCOPE.md`), `DEPS.md` and the
  rest of the list stay owed by every tier, and a member that declares no `kind`
  is unchanged. On the filing bench TelePlasma's report goes from 6 findings to
  3, with nothing but the three `[missing]` rows moving.

### Fixed
- **An unknown scaffold artifact warns and skips instead of raising `KeyError`
  (mgf-standard PAPER-148).** `catch-up` writes mgf-standard's scaffold
  artifacts through a two-map lockstep — mgf-standard owns *what* ships
  (`list_scaffold()`), mgf-fed owns *where it goes* (`_SCAFFOLD_DEST`) — and the
  two release independently, so the maps are transiently out of step by
  construction. One direction was inert; the other was a crash: a newer
  mgf-standard naming an artifact this map had never heard of made
  `_SCAFFOLD_DEST[name]` raise, breaking `catch-up` outright for every consumer
  that upgraded in the unlucky order. It now names the artifact, the gap and the
  remedy on stderr and places everything it can, so **both release orders are
  safe**. An unplaceable artifact is also no longer reported as stale — a
  finding whose only honest remedy is "upgrade mgf-fed" is not repo drift, and
  `--apply` could never have cleared it.
- **`_SCAFFOLD_DEST` maps `test_check_commit_trailers.py` (mgf-standard
  PAPER-148(a)).** mgf-standard promotes the trailer-gate's own test into the
  scaffold seam so AG-03's checker ships with the proof that it works; this is
  mgf-fed's half of that wave, landing first so the ordering is not
  load-bearing. Inert until mgf-standard ships the artifact.

## [0.24.0] - 2026-08-06

Minor release: the federation learns the difference between what a member is
written in and what it is for. The consumer-app tier gets a way to be graded
right — and the escape hatch stops grading the members that use it worse than
the ones that do not.

### Added
- **`Member.kind` — the federation gets a TIER, orthogonal to language.** An
  optional per-member `kind` in `MGF_LIBRARIES.json` (`"library"` by default,
  `"app"`), loaded onto `Member`. `lang` answers *what is it written in*; `kind`
  answers *what is it FOR*, and several gates have been quietly re-deriving the
  second question from the first.

### Changed
- **The `[exempt].woodpecker` step-name fallback selects on TIER, not on
  language (PAPER-151).** The full `woodpecker_required_steps` list is
  library-shaped — `build`, `sbom`, `publish`, `verify-devpi`, `tag-coherence`,
  `test-3.11` — and a consumer app publishes nothing to devpi, builds no wheel,
  cuts no tag to verify, and pins `requires-python = ">=3.12"`. Because the
  selector was `lang` alone, a Python **app** was measured against all of it, so
  **exempting an app made its audit worse, not better**: on TelePlasma's real
  tree, adding one name to `[exempt].woodpecker` traded one `ci-generated`
  finding for twelve `ci-step` ones. The escape hatch graded the exempt member
  worse than the unexempted one, and the app tier — four members today
  (TelePlasma, hrb-app, vm-vmanager-app, ipcam-app) — had no sanctioned way to
  be right. App-tier members are now held to the universal governance subset
  (`commit-trailers` + `secret-scan`) whatever their language.

  Two things this deliberately does NOT do. The **gate floor still binds every
  Python member, app or library**: `ci-command` keys on `lang`, never on `kind`,
  so the PAPER-120/121 `required_commands` are enforced on an app exactly as on
  a library — exempt from a library's step list is not exempt from the gates
  that actually run. And **no current member changes grade**: the installed
  registry (mgf-standard 0.1.77) declares `kind` for nobody, so all 53 members
  default to `"library"` and the selection reduces to the previous `is_python`.
  The behaviour is gated on data that does not exist yet; `mgf-standard` lands
  the registry rows that turn it on.

## [0.23.0] - 2026-08-05

Minor release: a new cross-layer check, a gate taught to compare against canon
rather than against a name, and the court-round cadence as a subcommand.

### Added
- **`ci-secret-guards` — SC-23 as a fleet gate.** SC-23 has been a declared MUST
  since 2026-07-22, and until now exactly ONE implementation of it ran anywhere:
  `first-light`'s `tools/verify_ci.py`, wired to that one repo. It was the only
  thing that objected when the unguarded `fetch-default-branch` step shipped to
  43 repos. Pointed at the fleet, the same rule found **13 further violations in
  three repos** — including `mgf-website`'s live deploy pipeline, where an empty
  `PUBLIC_TURNSTILE_SITE_KEY` builds and DEPLOYS a site with a broken contact
  form, green. Not every SC-23 violation is fail-closed, and that shape is the
  one that ships a defect to users.
- **`mgf-fed court`** — the fleet-wide-act cadence (`--check` / `--write`). A
  court round after every act that lands in ≥3 repos, or touches canon, a gate's
  logic, the credential plane, or a release. Blast radius, not effort: a
  count-based trigger fires most during the safest work and passes over the one
  commit that changes canon for 54 repos.

### Changed
- **`ci-workspace-truth` compares the fetch step to CANON, not to its name.**
  Property 1 asked only whether a clone step *named* `fetch-default-branch`
  existed, so a step that pulled both clone secrets into its shell and guarded
  neither passed the gate written in the same session to police that file.
  *Present* and *conformant* are different questions. Canon is extracted from
  `mgf-standard`'s template, never hardcoded — a literal copy here would be a
  second source of truth drifting silently from the first.
- **Both checks read Woodpecker's `.woodpecker/` DIRECTORY layout**, not only a
  root `.woodpecker.yml`. `hrb-app` uses the directory form with two live
  pipelines, and the gate was announcing *"no CI gate at all — add one, or
  declare an exemption"* about it. Acting on that would have permanently
  exempted a repo whose CI is running and unguarded.

### Fixed
- **The SC-23 guard regex accepted `-n`/`-z` as ordinary flags.** `ssh -n
  "$HOST"` and `echo -n "$KEY"` both registered as guards — so the check could
  report a secret guarded by the very line that CONSUMES it. Both shapes appear
  in real deploy steps. The flag must now belong to a test. This deliberately
  DIVERGES from `first-light`'s byte-identical copy, which still carries it.
- **The tolerance-rot detector gave harmful advice.** `seen_exempt` filled after
  the stale-ref `continue`, so on a bench with stale carriers an entry matched
  nothing — not because it was dead but because nothing was read — and the
  finding said *"remove it"*. The zig-token tolerance is live on six repos;
  following that advice would have turned six correct repos into six false reds.
  An unread fleet now reports "could not determine".
- **`ci-workspace-truth` found `figrecovery` hardcoding `master`** in its fetch
  step instead of reading `$CI_REPO_DEFAULT_BRANCH` — green only by coincidence
  of that repo's current default, in the repo the branch-agnostic design was
  derived from. The name check had scored it conformant.


## [0.22.1] - 2026-08-04

Patch release for a bug 0.22.0 shipped and its own first real run exposed.

### Fixed
- **A `(check, target)` pair is a WAIVER KEY, not a finding identity.**
  `--check-artifact` counted distinct pairs, but one check legitimately reports
  several findings about one repo — `ci-workspace-truth` says both "no fetch
  step" AND "fail-open checker" about the same repo. The first real artifact (53
  findings across 38 pairs) therefore tripped the reconciliation with *"53
  reported but 38 enumerable"*: fail-CLOSED, so nothing passed wrongly, but
  simply wrong — and it blocked `mgf-standard` for a bogus reason on the very
  release meant to unblock it. The guard added so that "I cannot enumerate the
  findings" could never read as "there are none" was itself mis-enumerating.

  Findings are counted as findings on both sides; pairs match waivers only. The
  display names each target once while the count stays honest. A waiver covers
  every finding its pair names — deliberate, and documented in the code.

## [0.22.0] - 2026-08-03

ONE-ESTATE **Phase 3**. Ships the gate that keeps the counterfeit default branch
dead, and the mechanism that stops a fleet-wide gate from freezing the repository
that fixes the fleet.

### Added
- **`mgf-fed audit --check ci-workspace-truth`** — the Phase 3 §3.6 gate. Per
  repo, read from the **committed** tree at `origin/<default>`: the pipeline
  carries the `fetch-default-branch` clone step; `scripts/check_commit_trailers.py`
  is byte-identical to the canonical scaffold copy; and that copy carries the
  fail-**closed** marker, so a legacy fail-open script cannot sit there wearing
  the right filename.

  PLAN §3.6(c) proposed grepping member copies for `"skipping"`. That test is
  unusable: the *canonical* hardened script contains the word four times ("this
  check does not pass by skipping"), so it flags every **conformant** repo and
  stays silent about a legacy copy worded differently. The searched-for token
  belonged to the fix, not the defect. Replaced with a positive marker.

- **Accepted findings (`AUDIT_ACCEPTED.toml`)** — `--check-artifact` now splits
  its severity. Artifact **integrity** (present, stamped, fresh, complete) stays
  blocking and **unwaivable**; cross-layer **findings** may be carried by an
  explicit, committed declaration.

  This exists because the gate failed on *any* finding, and this fleet's findings
  are about *other* repos — which froze `mgf-standard` entirely: branch
  protection requires the pipeline, the pipeline runs the gate, and the gate was
  red over eleven unrelated repos' ledgers that **D-12 deliberately chose to
  carry**. A gate that cannot be satisfied by any action available to the person
  it blocks is not a gate; it is a stop, and stops get removed under pressure
  rather than answered.

  Every tooth kept: accepted findings **print on every run**, pass or fail, with
  reason and deciding reference; each **expires**; a waiver matching **nothing
  fails**; anything unlisted is red as before; and a headline count the
  artifact's own detail cannot account for is **unwaivable**, because *"I cannot
  enumerate the findings" must never read as "there are none"*.

### Fixed
- **`ci-workspace-truth`'s roster globbed `mgf-*` — P2-F2, inside the check
  written to end that class.** It could not see `figrecovery` (the repo Phase 3's
  live proof was built on, and the reason the fetch step reads
  `CI_REPO_DEFAULT_BRANCH` rather than a literal), `TelePlasma` (named in PLAN
  §3.4), `vm-vmanager-app`, `magogi-proof` or `first-light` — five live
  pipelines, and the two likeliest to fail, because a `master` default is exactly
  what a `main`-shaped roster overlooks. The check argued about master-default
  repos in its docstring, shipped a master-default fixture, and scoped itself so
  it could never look at one. **44 examined / 25 findings → 54 / 39.**

- **`feedback --fresh` resolved the constant `origin/main`.** On a
  `master`-default repo that ref never resolves, so `--fresh` fell back to the
  working tree on *every run, permanently*, while reporting *"origin/main does
  not resolve"* beside a perfectly good `origin/master`. CIR-16 (b)'s freshness
  guarantee was off for those repos for their whole life, announced as if
  transient. Now resolves `origin/HEAD` first; the note names what it tried.

- **`uv.lock` resolved `mgf-standard` 0.1.66 against a `>=0.1.75` pin** — nine
  releases behind the floor this project declares. A stale lock here *is* the
  stale-corpus trap that produced two confident wrong answers in one session.

## [0.21.1] - 2026-08-03

Patch release for one reason: **0.21.0's tag was cut before the Phase 2 court's
fixes merged**, so the widened `agent-context` roster existed on `main` and
shipped to nobody. That is **P2-F1 exactly** — the finding closed an hour
earlier by releasing 0.21.0 — re-created by the very merge that answered its
siblings. `release-currency` caught it at session close; nothing else would have.

Ships the P2-F2 / P2-F4 / P2-F7 fixes to consumers: **56 examined, 2 findings**
where the released 0.21.0 reports 44 / 0.

### Fixed
- **Three false passes in `agent-context`, found by the Phase 2 court, not by its
  author.** The check was written to certify work its author had done, which is
  exactly the arrangement CRT-01 exists to distrust — and a fresh-context chair
  found what the author did not.

  - **P2-F2 — the roster could not see the failures.** Scoped to
    `MGF_LIBRARIES.json`'s 44 declared members, while the wave's universe was 57.
    The 13 excluded included `mgf` itself — the subject of the acceptance
    criterion four lines further down the same list — and **both** repos already
    known to lack the mechanism. So `exit 0 fleet-wide` was a statement about
    *scope*, not about state. The roster is now every non-sooth git repo on the
    federation root, union the declared members, minus an explicit
    `AGENT_CONTEXT_EXEMPT` table where each entry carries a reason. Fleet result
    moves from *44 examined / 0 findings* to **56 examined / 2 findings** — the
    two being `figrecovery` and `TelePlasma`, which genuinely lack it.
  - **P2-F4 — `AGENTS.md` was a managed surface nothing checked.** PLAN §2.2.3
    listed it beside the root pointer and the docs block; deleting it left every
    gate green, including in `mgf`, criterion 4's own subject.
  - **P2-F7 — duplicate managed blocks in the ROOT file were undetected.** Block
    integrity ran on the deep-context file only, so a root `CLAUDE.md` carrying
    two full blocks passed clean: the `mgf-hrb` duplicate-block defect that
    criterion 3 tests for by hand, one file up, in the file Claude Code actually
    auto-loads.

  Each property sabotage-tested: reverting it fails exactly and only its own test.


## [0.21.0] - 2026-08-03

### Added
- **`mgf-fed audit --check conformance-ledgers` — the watcher for a debt the
  corpus wrote down every time and nobody enforced.** DOC-15 requires every
  repo's ledger to carry a row for each current MUST, and `app-doctor` enforces
  it *per repo, in that repo's CI, against the corpus that repo happens to have
  installed*. So a MUST merged to `mgf-standard`'s main binds nobody until
  someone publishes — and then it is red everywhere at once. On 2026-08-03 a
  release cut to clear an unrelated version drift published five releases' worth
  of corpus in one act: **33 new MUSTs, no ledger carrying any of them, 32 repos
  red within minutes.** The corpus footer had recorded the required follow-up
  wave each time ("every member's `MGF_STANDARDS_CONFORMANCE.md` gains three
  rows … tracked separately") and each time it was deferred. This compares the
  **declared** Index of MUSTs against every member's **committed** ledger, so a
  release that would redden the fleet is visible while it is still a decision.

- **`mgf-fed ledger-seed` — the cohort-agnostic re-baseline.**
  `mgf-standard/scripts/seed_ledger_rebaseline.py` was written for PAPER-100's
  identical situation (40 new MUSTs, every ledger short), hardcodes that cohort,
  and ran once — so this cohort needed a second script and the next would need a
  third. This derives the cohort from the corpus instead of naming it. It seeds
  **⏳ (not yet assessed)** and never guesses: marking the rows `N/A` would go
  green everywhere and be *worse than the gap it closes* — a false conformance
  claim about 33 rules across 43 repos, asserted by a script that assessed
  nothing. `rules.json` carries id/family/doc/kind but no `applies_to`, so
  shape-scoping is genuinely not derivable. Rows are **appended** in their own
  section; the authored ledger is preserved byte-for-byte. `--status` exists for
  the reviewed case. Read-only by default, and a real gap exits non-zero.

### Changed
- **Absorbed the corpus families `mgf-standard` 0.1.75 shipped.** Three
  self-expiring tripwires fired the moment that wheel reached CI, which is
  exactly what they were built to do: `_standards._PENDING_CORPUS` and the
  session-hook kit's `_ENACTING` were staging tables for law enacted on
  mgf-standard's main but not yet in a published wheel, and both now defer to
  `rules.json`. The `[cli]` extra's pin is floored to `>=0.1.75` so the corpus is
  guaranteed rather than hoped for.

  **The hardening cross-check earned its keep.** `_ENACTING` declared
  `CRT-01..16` by hand; the corpus enacted `CRT-01..14`, so this repo had been
  vouching for **two rules that do not exist**. Nothing cited them, so nothing
  else would ever have noticed — a hand-kept mirror of someone else's law,
  drifting silently, which is the precise failure the tripwire was written
  against. Deleting the table removes the drift surface with it.

### Fixed
- **The new ledger check first read the corpus from the INSTALLED wheel** — the
  way `app-doctor` legitimately does. On the bench that wrote it the installed
  corpus was `0.1.66`, **nine releases stale**, so it confidently reported 268
  declared MUSTs while `mgf-standard`'s main declared 306. A check answering
  *"would publishing redden the fleet?"* cannot take its idea of the corpus from
  whatever wheel a laptop happens to have. Both sides now read the committed
  tree. Same wrong-layer class this module exists for, found in this module.


### Added
- **`mgf-fed audit --check agent-context` — the check ONE-ESTATE Phase 2 named as
  its own acceptance criterion, which did not exist.** The program installed the
  root `CLAUDE.md` pointer on 54 repos and declared acceptance to be
  "`audit --check agent-context` exit 0 fleet-wide"; `--list-checks` offered
  eight checks and none of them was that one. So the mechanism reached the fleet
  with nothing in any exit code able to notice it leaving again — this workspace's
  own thesis (*a verifier that isn't in a gate's exit code does not exist*)
  committed by its author. It audits four properties across every repo the
  registry declares, each one a defect the wave actually found: the root file
  exists and carries the managed block; the block is the current canonical
  rendering; **the deep-context path the pointer names exists in the same tree**
  (the five-repo dangling-pointer bug, where writer and checker compared against
  the same constant and neither consulted the repo); and the deep file's managed
  block is well-formed — one BEGIN, one END, balanced HTML comments (the
  eighteen-repo truncated-END defect that left `mgf-common`'s 696 authored lines
  inside a comment). Read from the **committed** tree, never the working tree:
  trees here are routinely parked on other branches, and a filesystem read
  undercounted by three the one time it was tried. It runs clean over all 44
  declared members today.

  **Honest limit on where this is tested.** The negative controls build *real*
  git repos with a real `origin`, because a faked `_git` would only prove the
  parser agrees with its own fixture — the "writer and checker confirm each
  other" trap this check exists to break. `python:*-slim` has no `git`, so those
  11 tests skip in CI (as ~76 already do) and run locally. The image is the
  defect, not the tests; filed rather than papered over, because the pipeline is
  template-generated and widening it is a fleet-wide change, not a line in this
  PR.

### Fixed
- **A stale `origin/*` view could make any committed-layer check answer a
  question nobody asked.** Every such check reads a remote-tracking ref that is
  only as fresh as the last fetch, and none of them asked. `agent-context`
  confirms the local ref matches the forge before judging it, and reports
  staleness — or an unreachable forge — as a finding rather than a silent pass.
- **The committed `AUDIT.json` could not notice a check registered after it was
  written.** `--check-artifact` asked *"what was skipped?"*, and a check added
  later appears in neither `ran` nor `skipped` — so a whole boundary could go
  unexamined while the gate stayed green, which is this module's own failure
  class one layer up. It now fails when any registered check is unaccounted for.
  **Consequence: `AUDIT.json` must be regenerated (`mgf-fed audit --write`) when
  this release reaches CI**, or `mgf-standard`'s `check-artifact` step will
  correctly refuse the artifact that predates `agent-context`.

## [0.20.0] - 2026-08-02

### Fixed
- **The root pointer named a deep-context file that does not exist on repos
  that never migrated to DOC-07.** `ROOT_CLAUDE_MD_BLOCK` hardcoded
  `docs/claude/CLAUDE.md`, but PAPER-54 left `docs/CLAUDE.md` valid and
  `catch-up` deliberately does not move those repos ("existing consumers on the
  legacy path stay valid until they migrate manually"). So on every legacy repo
  the pointer's single job — *the detail is over there* — named nothing, and
  every check reported `ok`. Five repos fleet-wide are in scope — `hrb-app`,
  `inthewords`, `mgf-secrets`, `mgf-website`, `TelePlasma` — which is exactly
  Phase 2.5 batch 3; the 39 canonical repos and the Phase 5 probe targets were
  unaffected. Caught by reading the first applied diff of the batch rather than
  the check's verdict. The block is now rendered per repo from the existing
  `_resolve_claude_md_path()`
  resolver, and the drift check compares against the same rendering so writer
  and checker cannot disagree.

- **The `.gitignore` check tested one line of a block it claims to standardize,
  so lines added to the block could never reach the fleet.** `_check_gitignore`
  returned `ok` as soon as `my_stuff/` was present; the fix is attached only to
  a warn, so on the ~42 repos already carrying that line `_apply_fix_gitignore`
  was unreachable — the source even documented the branch as such. The block was
  therefore append-only in theory and frozen in practice. The check now tests
  **every** line of `_GITIGNORE_STD_BLOCK`, derived from the block itself
  (`_GITIGNORE_REQUIRED_LINES`) so the two cannot drift apart. `my_stuff/` keeps
  its own distinct message: a credential risk must never read like IDE noise.

### Added
- **`.claude/state/` and `.claude/mgf-hooks-disabled` to the standardized ignore
  block (AG-18, SHP-08/SHP-10).** The session-hooks kit writes
  `.claude/state/agent-events.jsonl` and reads the disable sentinel, and it
  installs at **user** level — so the state dir appears in whatever repo a
  session is launched from, not only in repos that ran `hooks install`. It was
  untracked-and-unignored in `mgf-secrets` and `mgf-standard` on discovery, and
  had already blocked a `release-remote` clean-tree pre-req once.

## [0.19.0] - 2026-08-01

### Fixed
- **The auto-block END marker is a whole comment, and the arithmetic was cutting
  inside it.** `CLAUDE_MD_END_MARKER` is only the comment's FIRST SENTENCE; the
  real marker is two lines terminated by `-->`. Every index computed as
  `find(END) + len(END)` therefore landed *inside* the comment. Damage found on
  disk in three repos (`mgf-brand`, `mgf-hrb`, `vm-vmanager-app`): the surviving
  END comment left unterminated — swallowing the rest of the document — and the
  removed block's trailing `this marker is preserved across … -->` stranded as
  visible body text. A fleet-wide `--apply` would have spread it.

  Cuts now land on the comment's true close, with the `-->` search **bounded** to
  the marker's own span: scanning unbounded is worse than the bug, because in an
  already-truncated file the next `-->` belongs to a later block, so the index
  leaps the duplicate and silently disables dedupe (measured — `removed` went
  1 → 0). A refresh re-emits a truncated END whole, so damaged files are
  repairable rather than merely detectable.

  Why PAPER-127 did not catch it: every fixture wrote the bare marker with no
  closing `-->`. The arithmetic was right for the tests and wrong for every real
  file — the fixture hid it.

- **The duplicate block had a repair and no detector.** PAPER-127 added the
  dedupe; nothing invoked it. `_check_claude_md` compares only the FIRST block, so
  a file with two managed blocks — or an unterminated marker comment — reported
  `ok` and `--apply` had no finding to fix. Measured on `mgf-brand`: 2 blocks,
  comments 16/15, verdict ✅. The check now counts blocks and compares `<!--`
  against `-->`.

### Added
- **A managed root `CLAUDE.md` pointer.** DOC-07 puts canonical deep context at
  `docs/claude/CLAUDE.md` and that stays true — but Claude Code auto-loads only
  the ROOT file, so 45 repos ship context no agent reads automatically. catch-up
  now manages a root pointer carrying `mgf-fed agents`, the AG-20 registry table,
  and the counterfeit-default-branch rule.

  **The block is managed; the file is not.** Fourteen repos have a hand-written
  root `CLAUDE.md`; the block is APPENDED and only the span between the markers
  is refreshed. Severity is `warn` deliberately — making it binding amends DOC-07,
  and `docs/standards/**` is a locked surface (founder enactment, LOCK.md §2).

- **The operations plane in the agent briefing** (item 10): the registry table,
  AG-20 quoted with the reason it binds, and the two CI facts a bench cannot
  derive. Two stale claims corrected in item 5 — Woodpecker is self-hosted, not
  Codeberg's; packages resolve from devpi, not the Codeberg registry. The
  STARTHERE.md pin test now resolves via `$MGF_ROOT` instead of skipping on any
  non-editable install, where it had already let `WF-13`/`WF-14` drift through.


### Added
- **`mgf-fed hooks` — the session-hook kit, in observe mode.** mgf repos had **no
  session hooks at all**; this is the mechanism that changes it. `mgf-fed hooks
  install` merges **one line** into a repo's `.claude/settings.json` and copies
  nothing:

  ```sh
  command -v mgf-fed >/dev/null 2>&1 && { mgf-fed hooks session-start || echo '…' >&2; }; exit 0
  ```

  (`…` is a one-line staleness notice — see the F2 fold below.)

  At every session start the hook re-teaches `SEATS.md` **SEAT-02** (name your seat
  before you act, with the command to record it) and appends to an append-only
  `.claude/state/agent-events.jsonl`. **It prints and it records. It never blocks** —
  every path returns `0`, including the paths that only exist because something broke.

  The design note that matters most: **the hook never declares the seat for the
  agent.** It cannot — only the session can read its own system prompt — and a
  fabricated `model=unknown` record would poison the very audit trail the log exists
  to create. *"Automatic"* means the instruction reaches every session mechanically
  and that **non-declaration is visible in the log**, not that the record is invented.

  The event vocabulary is **closed** (`session_start`, `seat_declared`,
  `seat_override`, `escalation`, `journal_step`, `context_ambiguity_filed`) and the
  refusal path is as much of a feature as the append path. A torn write never breaks
  the reader; a persistence failure never changes behaviour.

  The kill-switch works at three scopes — one session (`MGF_HOOKS_DISABLED=1`), the
  whole fleet (a `DISABLED` file next to the package), one repo
  (`.claude/mgf-hooks-disabled`) — is checked before any other logic, and is
  **announced** at session start, because an OFF protocol that looks healthy is the
  invisible-failure class the kit exists to end.

  Also `hooks status` / `uninstall` / `emit <type> k=v` / `log`. `settings.json` is
  **merged, never clobbered**, and unparseable JSON is **refused with nothing
  written** — silently rewriting a file we could not parse is how a tool destroys
  hand-tuned configuration.

- **The stop-time gate and write-set recorder, shipped DISABLED.** Enforce-stage
  material, behind two independent keys: `MGF_HOOKS_GATE=1` arms them at all, and
  `MGF_HOOKS_MODE=enforce` decides whether an armed gate blocks or merely records. The
  step that installs the kit cannot, by itself, produce a session that can be blocked.
  `install` wires `SessionStart` and nothing else.

  Two finding classes, and the distinction is not cosmetic. `seat_undeclared` **may**
  block once armed and enforcing — it has a sanctioned one-command exit, and the gate
  blocks *silence*, never *judgment*. `cross_repo_write` is **advisory in both modes
  by construction**: mgf has ruled no cross-repo handoff channel, so blocking on a
  finding whose discharge does not exist would be a gate with no exit the moment it
  fired. Ruling that channel is the named prerequisite for making it enforceable — a
  corpus decision, not a code change. Boundary doc:
  [`docs/howto/session-hooks.md`](docs/howto/session-hooks.md) §4.

- **Aliases for the three adoption-wave corpus docs** — `seats`/`seat`/`dual-model` →
  `SEATS`, `review`/`court`/`crt` → `REVIEW`, and
  `session_hooks`/`session-hooks`/`hooks`/`shp` → `SESSION_HOOKS`. All three docs are
  enacted on `mgf-standard`'s main but not yet in a published wheel, so their targets
  are listed in a new `_PENDING_CORPUS` set: safe by construction (an alias only
  resolves against the *installed* corpus, so it degrades to the ordinary
  unknown-topic error), and self-expiring per entry —
  `test_pending_corpus_is_still_pending` fails the moment a doc ships, which is when
  the pin gets floored and that entry deleted. Per entry, because they may well ship
  on different days.

### Court fold (S3a, F1–F10)

The round returned **upheld-with-findings**; each is folded here by ID.

- **F1 — the hook path now survives a base install.** `hooks` was registry-merged, and
  the registry lives behind `mgf-common` (the `[cli]` extra) — so on a bare
  `pip install mgf-fed` the verb **did not exist at all**, and every session in every
  adopting repo would have lost its re-teach in silence. The verb group moved to
  `mgf.fed.sessionhooks._cli` (stdlib-only), `mgf-fed` wires it **natively**, and it
  dispatches *before* the command-module imports that pull `mgf-common`. `install` also
  falls back to a stdlib atomic write. Pinned by a blocker simulation that shadows
  `mgf.common` with a module raising `ModuleNotFoundError`; seeded-revert confirms
  four tests go red without the native wiring.
- **F2 — staleness is now detectable, and the ordering is documented.** The shim gains
  a guarded `|| echo … >&2` so an `mgf-fed` too old to serve the verb says so **once**,
  instead of the repo losing its re-teach in silence; a *missing* `mgf-fed` stays
  silent, because absence is the operator's choice. The how-to's §2 leads with the
  ordering rule (upgrade the fleet, *then* install) and the three-case table. Pinned by
  executing the shim line under `/bin/sh` against stub `mgf-fed`s.
- **F3 — the residue check now covers every agent-facing string.** Not "no SHP
  strings" — the SHP ids are mgf law as of the `SESSION_HOOKS` family and the DISABLED
  banner and the gate's `rule` fields keep citing them. The test walks the package AST
  plus the live runtime output and asserts every cited id resolves in the **installed
  corpus's own `rules.json`**, with a self-expiring allowance for the three families
  enacting in this wave. A foreign id (Sooth's `XF-03`) is tolerated only in provenance
  prose that names whose rule it is, and never in runtime output.
- **F4 — "once per session" is now atomic.** `O_CREAT|O_EXCL` replaces a
  read-then-write check that was a race by construction and lost under exactly the load
  that produces duplicates. Barrier repro is the regression test: 16 concurrent starts,
  one record. Restoring loudness on the way through — a claim lost because the state dir
  is *unwritable* now says so, where the first draft went quiet.
- **F5 — hook-path reads are bounded** to a documented 256 KiB tail window
  (`_events.HOOK_READ_BYTES`), so starting a session no longer costs more forever as a
  repo ages. The operator reader stays unbounded. Perf pin measures the **delta between
  a 4 KiB-log run and a 100 MB-log run** (~1 MB today; ~394 MB with the bound seeded
  out), because one run's absolute peak is 40 MB of interpreter and proves nothing.
- **F6 — output failure can no longer take a session down.** Catching the `OSError`
  was not enough: **CPython flushes `sys.stdout` again during interpreter shutdown,
  after `main` returned 0, and a failure there exits 120.** The exhausted stream is now
  swapped out of `sys.stdout` for a sink. `> /dev/full` is the regression test, and it
  failed before the fix.
- **F7 — the kill-switch sentinel can never ship.** The editable-install path is
  git-ignored (verified via `git check-ignore`, not a substring search), no sentinel may
  exist in `src/`, and a wheel built in-test with the repo's own backend must contain
  none.
- **F8** — handled in the S3b fold; the prerequisite is named in the boundary doc.
- **F9 — misattribution corrected.** Sooth's rules *forbid* globbing a shared family
  parent; its registry is the correction, not the flaw. All three call-sites now say
  "from a registry over a shared family parent".
- **F10 — `review`/`court`/`crt` → `REVIEW` and `session_hooks`/`session-hooks`/`hooks`/`shp`
  → `SESSION_HOOKS` aliases**, both added to `_PENDING_CORPUS` alongside `SEATS`. All
  three expire independently, because they may well ship on different days.
- **`MGF_HOOKS_GATE` strict-parses `1`** and nothing else. `true`/`yes`/`on`/a typo read
  as **unarmed** — matching the documented contract, and failing in the direction of not
  blocking, exactly as `MGF_HOOKS_MODE` already did.

### Provenance
- The session-hook kit is **adapted with attribution from the Sooth family's `shp/`**
  (SHP-01..12): the fail-open discipline, the closed vocabulary, the three-scope
  kill-switch, the observe→enforce staging, attribution-before-blame, and the
  never-declare-for-the-agent rule. **A copy, not a dependency** — no import edge, no
  package dependency, no cross-family resolution path. Three deliberate divergences
  (one-line shim instead of four copied scripts; owning-repo resolution instead of a
  registry over a shared family parent — Sooth's rules *forbid* globbing that parent,
  so its registry is the correction, not the flaw; `session_start` added and
  `preflight` dropped) are recorded with their reasons in
  `docs/howto/session-hooks.md` §7.

## [0.18.2] - 2026-07-31

### Fixed
- **`--check-artifact` could not find the artifact in the pipeline it was written
  for.** It resolved `<federation_root>/mgf-standard/AUDIT.json`, but a single-repo
  CI clone IS the working directory and has no federation root — so the gate passed
  locally and reported MISSING in CI. It now checks `./AUDIT.json` first and
  **names which context it read from**.
- **`release-currency` under-reported.** Shipped-ness was judged by a hand-tuned
  exclusion list; `mgf-standard` force-includes `docs/standards` into its wheel
  while the list excludes `docs/`, so a change to the standards corpus would have
  shipped unnoticed. It now reads each repo's `[tool.hatch.build.targets.wheel]`
  `packages` + `force-include` — the config the packager obeys — and falls back to
  the list only when that is unreadable, saying so in the finding.

## [0.18.0] - 2026-07-31

### Added
- **`mgf-fed audit` — the cross-layer gate suite.** Eight checks, each comparing one
  layer of the federation to another: `declared → committed → published → installed
  → live`.

  | check | boundary |
  |---|---|
  | `fleet` | declared ↔ committed |
  | `feedback-status` | declared ↔ declared |
  | `release-currency` | committed ↔ published |
  | `installed-currency` | installed ↔ published |
  | `governance` | live ↔ declared |
  | `ci-freshness` | live ↔ committed |
  | `secret-scopes` | declared ↔ committed |
  | `pypi-namespace` | published ↔ declared |

  These are the **five orphan verifiers from the court workspace, plus two more,
  folded into the gate path**. On 2026-07-30 there were five hand-written verifiers
  in `mgf/tools/` referenced by **zero pipelines** — each written after a real
  incident, run once during the review that produced it, never again. The knowledge
  was captured; the enforcement was not, so the same classes recurred and were
  re-diagnosed from scratch. `verify_release_currency.py` documented the "published
  0.4.10 and main's 0.4.10 are different code" class perfectly, and that class
  recurred the day it was re-read, because nothing ran it.

  Two properties make this more than a pile of checks:

  1. **Every check names the boundary it crossed.** A finding reads *committed ↔
     published*, not just "problem" — including a check that was skipped, which still
     reports what it *would* have compared.
  2. **A check that could not run is a FAILURE, not a pass.** Without `--offline`, a
     missing token or unreachable index exits non-zero. "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.

- **`--write` / `--check-artifact`** — the audit needs the whole federation on disk
  plus credentials, so it cannot run in a single-repo CI clone. The expensive half
  runs where the fleet is and writes `mgf-standard/AUDIT.json`; the cheap half gates
  that artifact on four questions it can answer about itself — stamped, fresh,
  **complete**, clean — and needs no clones, network or credentials. An artifact from
  an `--offline` run records what did not run and therefore **cannot** satisfy the
  gate: zero findings across the checks that ran is not a clean audit.

  Live first run against the real fleet: **8 findings, 0 false positives** — and it
  independently found the unreleased `mgf-secrets` fix that had to be spotted by hand
  hours earlier.

### Fixed
- **`RELEASE_IGNORED` had gone stale**, found by this gate's own first live run: it
  reported 32 of 33 distributions stale, and 25 said only *"shipped files changed:
  AGENTS.md"*. The fleet-wide `AGENTS.md` ripple postdated the tuned exclusion list,
  so a faithful port inherited a list that had decayed — the same over-reporting the
  list was written to prevent, arriving by a different route. **A gate's exclusion
  list is not write-once**, and its decay shows up as noise long before anyone calls
  it a bug. 32 → 6 findings, all real.

### Added
- **`ci-unknown-key` drift finding** — an unrecognised `[tool.mgf-fed]` carve-out key
  is now a *finding*, not a stderr note. `drift-check` reports it, `scaffold` prints
  it as manual work and returns **1**.

  This closes the one CI-config defect the byte-render check structurally cannot
  catch: a key the renderer does not read shapes nothing, so the render is identical
  to itself and `ci-generated` stays silent while the author believes a knob is on.
  `mgf-uitest-desktop` declared `qt = true` (a knob that has never existed, written
  by analogy with `browser = true`); it parsed, printed a note into the middle of a
  46-repo sweep, let `scaffold` report success, and rendered a Qt pipeline with no Qt
  libraries — three red runs before anyone read the note.

  The message now names the consequence ("the rendered pipeline is NOT shaped by
  them"), lists the known keys, and suggests the right one from three sources: a
  **tombstone** table (deleted knobs get the reason — `skip_editable`), a **semantic
  alias** table (`qt` → `apt_packages`; not a typo of anything, so no edit distance
  finds it), then a lexical near-match for genuine typos.

  The render itself still never fails on an unknown key — a crashed renderer would
  strand the fleet. Verified against the real pre-fix `pyproject.toml` recovered from
  `mgf-uitest-desktop` history: the check fires and points at `apt_packages`.

## [0.17.0] - 2026-07-30

The **tooling round** of 2026-07-29/30. MINOR: one new verb and two behaviour fixes
to `catch-up --apply`; no breaking change to any existing verb.

Found by running `catch-up --apply` across **41 repos in one night** — none of these
defects is visible in a single-repo run.

### Added
- **`mgf-fed conformance`** — the authoritative fleet conformance audit is a command,
  not a file path. `mgf-fed status` dropped its `conf%` column under CIR-15 and its
  footer pointed at `mgf-standard/scripts/fleet_conformance_audit.py`; a path cannot
  be listed, installed or discovered, so the fleet's authoritative measure was the
  least reachable thing in it. Its committed tracker had been asserting a clean fleet
  since 2026-07-04. Streams (the audit runs for minutes), bounded at 3600s, warns when
  the tracker would be written UNSTAMPED, and exposes the auditor's repeatable search
  root as `--extra-root` so it cannot collide with mgf-fed's `--root`.

### Fixed
- **`catch-up --apply` no longer overrides a declared CFM-04 decline (PAPER-126).**
  In `mgf-standard` it injected `mgf-test-supervisor` into dev deps — the dependency
  that repo permanently declines because it closes a `mgf-standard →
  mgf-test-supervisor → mgf-common → mgf-standard` **cycle**, with the rationale
  written in the three comment lines directly above the insertion point. `--apply` is
  advertised as *mechanical*; a mechanical tool that silently overrides a reasoned
  decline is not mechanical, it is opinionated in a direction nobody reviewed.
  `_rule_is_declined()` now reads the repo's own `MGF_STANDARDS_CONFORMANCE.md`.
- **`catch-up --apply` no longer corrupts a trailing comment (PAPER-126).** The
  dev-deps insert appended the separating comma to the last *line*, so a list ending
  in an explanation became `# …non-adoption-is-valid.md).,`. TOML parses that — the
  comma is inside a comment — so nothing caught it.
- **`catch-up --apply` converges on the CLAUDE.md auto-block (PAPER-127).** It claimed
  to migrate the legacy `mgf-common catch-up` marker and never did: affected files
  carry **two** auto-blocks, the writer took the first match and the checker tested for
  the legacy marker *anywhere*, so the warn could never clear. **21 of 44 repos** were
  stuck. `_find_begin_marker` now returns the earliest marker and orphan blocks are
  removed before the survivor is refreshed.
- **`fed-tools`: `mgf-secrets` registered; `doctor` sees CLIs and currency; scope
  stated.** The credential plane the workspace rule names as the first thing to ask was
  absent from the front door and uninstalled. `doctor` swept only web consoles, so a
  CLI could never surface as drift; `update --check` was a gate nothing ran and was
  blind to CLIs (it reported 3 stale when 5 were). Every success line now states its own
  scope — `doctor --offline` says outright that currency was **not** checked.

## [0.16.0] - 2026-07-18

The **enforcement half of DP-5** from the 2026-07-17 **CI Support Review**
(`court/reviews/2026-07-17-ci-support-review/`, decision package DP-5 → finding
**CIR-04**, corpus rule **WF-14**). The governance half landed in mgf-standard
0.1.66 (WF-14 went live 2026-07-18); this release makes `drift-check` enforce it.
MINOR: a new gate-floor check is a feature; no breaking change to any existing verb.

### Added
- **`ci-uv-frozen` gate-floor check — enforces WF-14 (CIR-04 / PAPER-125).** A CI
  `uv` verb that re-resolves — `uv run` / `uv export` / `uv sync` / `uv pip install`
  — without `--frozen` re-resolves the dependency graph against the index and
  **rewrites `uv.lock` mid-pipeline** (PAPER-125: an un-frozen CI `uv` verb silently
  swapped 9 pins to pypi relics). `drift-check` now flags every such verb on every
  member's `.woodpecker.yml`, any language, exempt or not — and an audit-shaped
  `uv export` additionally must carry `--no-emit-project` (the project under test is
  not one of its own scanned dependencies). Same architecture as the `ci-clone` /
  `ci-command` gate-floor checks: pure functions `uv_frozen_gap()` + the shared
  `UV_FROZEN_FIX` remediation in `mgf.fed._pipeline`, consumed by `_drift.gate_floor`;
  the new `ci-uv-frozen` kind joins `_GATE_FLOOR_KINDS`. Scanned over
  `command_lines()` (a commented-out `uv run` is not a gate), split per piped/chained
  sub-command so a two-verb line credits `--frozen` to its own invocation. `uv lock`
  (resolve-by-design), `uv tool` (a global install) and `uv --version` are never
  flagged. **A pip-only pipeline runs no such verb → no finding** — the current fleet
  is pip-based, so `drift-check --shape all` stays 40/40 clean (verified).

### Changed
- **`uv.lock` refreshed and committed on the core ring (CIR-04, Part B).** mgf-fed
  carried a stale `uv.lock` (self-version behind, corpus pins behind); it is
  regenerated here so `--frozen` is honoured against a current lock. mgf-fedres and
  mgf-fed-web — which committed no lock at all — gain one via separate lock-only PRs,
  so the WF-14 precondition (a committed lock, WF-11 / §7.1) holds fleet-wide.

## [0.15.0] - 2026-07-17

The **"honest instruments"** round of the 2026-07-17 **CI Support Review**
(`court/reviews/2026-07-17-ci-support-review/CI_SUPPORT_REVIEW.md`, Phase 2 items
2.2 / 2.4 / 2.5 / 2.1 — findings **CIR-16 / CIR-11 / CIR-10 / CIR-15**). MINOR: a
new `ci-stats` verb and new `feedback` check modes; no breaking change to any
existing verb.

### Added
- **`mgf-fed ci-stats` — the first standing CI-health metric (CIR-11).** Before this,
  fleet CI health was an anecdote: the review had to measure its 21.7% red rate with
  throwaway scripts because no standing metric existed. The verb productizes that
  measurement, **reproducibly**, polling **Woodpecker only** (never Codeberg — PAPER-110):
  the last `--limit` pipelines/repo (default 40, the review's method) across the roster,
  reporting **red rate by failed-step class** (a `failure` by its first failed step,
  `clone` when clone-only; an `error` split on the CIR-10 forge config-fetch signature;
  `killed` as its own class), the **restart tax** (commit+event pairs that ran >1 pipeline,
  + total extra runs), **duration p50/p90** (nearest-rank, deterministic), and **per-repo
  outliers** (red rate above the fleet's, ≥2 reds). `--json` for a weekly cron. New module
  `mgf.fed.cli._ci_stats`; every call rides the PAPER-119 429 budget.
- **`mgf-fed feedback --in-repo [PATH]` — the consumer-app rot-gate (CIR-16 (a)).** The
  fleet scan only ever saw registry members, so a **consumer app's** `pending-feedback/`
  outbox was invisible (TelePlasma's 6-paper outbox produced "No un-relayed feedback ✓").
  `--in-repo` scans THIS repo's outbox regardless of roster, so an app can gate its own CI.
  Check-only (relay stays a supervisor action off the federation root).
- **`mgf-fed feedback --fresh` — freshness against a stale clone (CIR-16 (b)).** The gate
  read the dirty **working tree**, so a stale clone missing a just-pushed paper false-greened
  (07-14: 5 papers seen locally vs 7 on origin/main). `--fresh` reads the outbox from
  `origin/main` via a read-only `git show`/`ls-tree` instead. Composes with `--in-repo`.

### Changed
- **`mgf-fed ci-heal` now also restarts transient forge config-fetch ERRORS (CIR-10).**
  Beyond clone-only **failures** (PAPER-109), it restarts **error**-status pipelines whose
  message carries the transient `could not load config from forge` signature — pipelines that
  run **zero steps**, so nothing gates and nothing pages, and which the clone-only signature
  could never see (mgf-alembic #95, mgf-apiprobe #116, sooth-brand #36/#37…). **Same
  once-bound discipline** (skip if any later pipeline exists for the commit; at most one
  restart). Still Woodpecker-only. The error-message extractor + signature are shared with
  `ci-stats` so both read reds identically.
- **`mgf-fed status` drops the `conf%` column (CIR-15).** It regexed emoji from hand-written
  ledgers, ran no checks, and — scoring every repo against rules it never *declared* — ranked
  the fleet **backwards** (`mgf-fed-web`, 0 real gaps at its declared L1, read "32% worst";
  `mgf-fedres`, the one hard fail, read "61%"). A dropped column beats an inverted one: the
  table now prints a pointer to the authoritative scorer,
  `mgf-standard/scripts/fleet_conformance_audit.py` (which honors declared level + N/A +
  documented non-adoption). The honest raw ledger tallies (`conf_ok`/`conf_wip`/`conf_na`)
  remain in `MGF_FED_STATUS.json`; the derived (invertible) `conf_pct` field is removed.

## [0.14.3] - 2026-07-17

Three reviewed fixes from the 2026-07-17 **CI Support Review**
(`court/reviews/2026-07-17-ci-support-review/CI_SUPPORT_REVIEW.md`,
findings CIR-05 / CIR-01 / CIR-03).

### Added
- **`ci-clone` — a new GATE FLOOR check** (`mgf.fed._pipeline.clone_hardening_gap`),
  emitted from `drift-check`'s `gate_floor()` for **every** registry member (any
  language, exempt or not). Every committed `.woodpecker.yml` must carry the hardened
  clone: `partial: false` (the WF-09 full clone that dodges the Codeberg promisor 504)
  **and** `use-ssh: true` (the PAPER-109 SSH clone that retired the flaky short-lived
  OAuth credential). A pipeline with **no** clone block silently inherits Woodpecker's
  default — a partial clone over an OAuth credential — and "an absent block is the most
  invisible kind of drift" (PAPER-128). Evaluated over **non-comment lines only** (a
  commented-out setting is not a setting, and the canonical block's own prose mentions
  `partial: false`); substring philosophy like `missing_commands`, so a bespoke pipeline
  stays free in shape. Excusable via a new `[exempt].clone` policy list (empty default,
  read defensively). Shared remediation string `CLONE_HARDENING_FIX`. Closes CIR-03 /
  PAPER-128's durable ask. Real fleet verified clean: `drift-check --shape all` → 40/40,
  0 items.

### Fixed
- **`_ci_install_safety_drift` read `PIP_INDEX_URL` out of RAW text — a mere COMMENT
  disabled the bare-install check.** A comment merely *mentioning* `PIP_INDEX_URL`
  silently marked the pipeline as index-steered, defeating the dependency-confusion /
  stale-mgf-* bare-`pip install` scan entirely — the exact comment-vs-code class 0.14.2
  fixed for the bare-install scan itself (CIR-05). Env-steering is now judged over
  **non-comment lines**, so a genuine `environment: PIP_INDEX_URL: …` **key line** still
  counts as steered while a comment does not. Regression-tested both directions.
- **The "structurally impossible" dependency-confusion comment in `_woodpecker.py` was
  wrong.** It claimed the devpi index made dependency-confusion structurally impossible;
  true only for names devpi **has a release for**. A name with **no** devpi release (a
  typo'd or pre-declared `mgf-*` dep) falls through `bases: [root/pypi]` to public PyPI
  (proven live 2026-07-17: `+simple/mgf-doc-standard/` → 200; mgf-standard PAPER-123).
  The comment now states the real semantics — published names are mirror-blocked,
  unpublished names fall through until `root/pypi` is severed (CIR-01, decision pending).

## [0.14.0] - 2026-07-16

### Added
- **The WUI-07 `ui-gate` — a console's browser-UI specs, actually run.** A member
  that depends on `mgf-uitest-web` now has a dedicated `ui-gate` step spliced into
  its pipeline at mgf-standard's new `# <<INSERT:ui-gate>>` marker (fragment ships
  in **mgf-standard >= 0.1.64**): install the pinned Chromium, then
  `pytest tests/e2e -m e2e`. Gated on the **dependency**, not a declaration — the
  specs cannot exist without the harness (their conftest imports `mgf.uitest_web`),
  so the tier cannot be added-and-forgotten. Read from the repo's own pyproject
  rather than the registry's `deps`, because a registry regenerated on its own
  cadence would render the gate away while looking fine.

  **Why a dedicated step rather than the specs inside the test jobs — this is the
  load-bearing part.** A console's pytest `addopts` deselect the tier-3 markers by
  default (`-m "not perf and not slow and not stress and not e2e …"`, per TS-18), so
  UI specs left to a normal `pytest -q` run are **silently deselected**: a gate that
  looks present and never runs. The step selects the tier explicitly (a CLI `-m`
  overrides the addopts one), and pytest's exit-5 on an empty selection means a gate
  that ran nothing reds rather than greens.
- **`ci-ui-gate` — a new GATE FLOOR check** (`mgf.fed._pipeline.ui_gate_gap`), wired
  into `drift-check` and the doctors beside `ci-coverage`. Fires when a repo depends
  on the harness but its pipeline never selects the tier — the browser specs exist,
  the harness installs, and **nothing runs them**. Detected on the
  `pytest … -m e2e` invocation, not on a step *named* `ui-gate`: a step name is not
  a gate (PAPER-121), and a commented-out gate is not a gate either.

### Fixed
- **`app-doctor` silently truncated its own checklist under `filterwarnings = ["error"]`.**
  A warning emitted while a check probed an optional surface (e.g. mgf-common's
  observability registries) was raised as an exception under the fleet's own strict
  warning default (TS-05), costing **every check after it** — the operator saw a short
  report and **no error**. It presented as an order-dependent test: once any earlier
  caller had imported the probed module, `sys.modules` served it warning-free and the
  missing checks reappeared, so the doctor's output depended on what had been imported
  before it. Found because a supervisor tier selection happened to probe it first, and
  it reds `mtest --tier 0-2` while a plain `pytest` run stays green (TS-03). The check
  loop now runs under a **scoped** warning suppression: the doctor REPORTS environment
  state, it does not enforce the host's warning policy — and it never changes that
  policy for the caller's own code.
- **The `browser` carve-out shipped in 0.13.0 with no tests.** Covered now, along
  with the new gate: the install renders only under `[tool.mgf-fed] browser = true`,
  lands in all three test jobs, `browser` is a known CI key, the ui-gate does **not**
  imply `browser` (a console's test jobs stay browser-free — only its gate pays the
  download), and a distribution whose name merely *starts with* `mgf-uitest-web`
  does not switch the gate on.

## [0.13.0] - 2026-07-16

### Added
- **`[tool.mgf-fed] browser = true` — an opt-in browser carve-out for the CI
  renderer.** A member that declares it has `playwright install --with-deps
  chromium` spliced into each test job, at the new
  `# <<INSERT:playwright-install>>` markers in mgf-standard's `base.yml`
  (requires `mgf-standard` ≥0.1.63, which ships the companion
  `frag-playwright-install.yml`). `python:*-slim` carries neither the browser
  binary nor its system libraries, so a suite whose product IS driving a real
  browser cannot run in CI at all — let alone reach the TS-06 ≥80% coverage
  floor. Gated exactly like `lint-imports` is gated on an import-linter
  contract existing: `browser` defaults to False, so the marker is dropped for
  the other 40 members and nobody pays the download. `browser` joins
  `_KNOWN_CI_KEYS`, so the key is recognised rather than rejected as unknown
  config. First consumer: `mgf-uitest-web`, whose suite drives each web console
  in a real pinned Chromium.

## [0.12.0] - 2026-07-14

### Added
- **The federation GATE FLOOR** — new `mgf.fed._pipeline`, wired into `drift-check` and
  both `app-doctor` surfaces. Three checks that hold for EVERY registry member, in any
  language, **exempt or not** (closes FEEDBACK PAPER-121 + PAPER-122):
  - `ci-command` — the pipeline must RUN the required gate **commands**, not merely carry
    steps with the right **names**. Sourced from the policy's optional `required_commands`
    key, defaulting to `["ruff check", "ruff format --check", "check_commit_trailers.py"]`
    when it is absent. Applied to `[exempt].woodpecker` repos too: exempt means "we don't
    regenerate your file", not "you may silently drop a federation gate" — the hole that
    let the `ruff format --check` gate miss mgf-hrb / mgf-test-supervisor / **mgf-standard**
    (the repo that authored it) with drift-check staying green.
  - `ci-missing` — a registry member with **no `.woodpecker.yml` at all** (every Woodpecker
    run errors with "could not load config from forge"; no gate has ever run). Checked for
    every member regardless of `--shape`, since a Zig member with no pipeline runs no gates
    either. Excused only by an explicit `[exempt].no_pipeline` entry.
  - `ci-coverage` — a pipeline running `coverage run … -m mgf.test_supervisor` whose
    pyproject lacks `[tool.coverage.run] patch = ["subprocess"]`: the coupling that reds CI
    with **every test passing** ("No data was collected" → "No data to combine" → exit 1).
    Also a FAIL in `app-doctor` (TS-23) and `app-doctor --standards` (the CI-run surface),
    so the fix is named where the confusing failure appears.
  - `ci-ungoverned` — a **cloned `mgf-*` repo the registry does not list** that ships no
    pipeline. Every check we own iterates registry members, so a repo outside the registry
    was outside every gate: mgf-brand (no publishable package, hence never registered) is
    activated on the forge, errors every run, and nothing noticed. Narrow by design — a
    non-registry repo that HAS a pipeline is running gates and is not flagged — which holds
    it to exactly one finding fleet-wide today.
  `drift-check` prints the floor as its own **GATE FLOOR** section — a gate that is not
  running is not the same kind of finding as a file that drifted from the template.
- fed-tools: self-install + liveness + registry-drift checks.

### Fixed
- **`catch-up` no longer demands the deleted `slow` marker.** It kept its own hand-copied
  taxonomy; `mgf-common` 0.50.0 removed `slow` per TS-18 and the checker never followed, so it
  flagged 24 conformant repos as broken — and `--apply` would have written the dead marker back
  into all 24. The taxonomy is now DERIVED from `mgf.common.testing.MARKER_TIER` (canonical per
  TS-19), the check relabelled TS-25→TS-19, and `mgf-common>=0.50` floored. **This release ships
  the fix to the installed CLI** — until now the fix lived only on `main`.

### Note
34 commits had accumulated on `main` since v0.11.2 without a release. A fix that isn't released
isn't shipped. (2026-07-16: the GATE FLOOR detail above was filed under `[Unreleased]` at the
time and is restored to this release, the one that actually shipped it — `_pipeline.py` is
present at the `v0.12.0` tag.)

## [0.11.2] - 2026-07-05

### Fixed
- **`.gitleaks.toml` restore is non-destructive** (closes the PAPER-03 clobber gap
  the shape-aware drift check left open). When a consumer's file merely *lacks* a
  newly-added standard allowlist entry (e.g. template v0.1.62's `\.claude/`), the
  scaffold used to overwrite the whole file — re-adding the standard entry but wiping
  the consumer's additive carve-outs (the mgf-fastapi `test_webhooks.py` case). New
  `_drift.gitleaks_restore_text` merges the consumer's additive allowlist entries into
  the template restore; `_scaffold` calls it in place of the skip-or-overwrite guard,
  so a lagging file is brought to canonical WITHOUT losing legit per-repo carve-outs.
  Pinned in `tests/unit/test_gitleaks_shape.py`.

## [0.11.1] - 2026-07-03

### Changed
- **`release-remote` retargeted from Codeberg's package registry to the self-hosted
  devpi index** (Wave-2 P3 §3.5; founder gate P3: RETARGET — the build-once/promote flow
  and the local-index validation wave are kept verbatim: gate → build → validate →
  local-first smoke against the throwaway local index → promote). The upload endpoint
  derives from `$DEVPI_URL` (index root = the URL minus its `+simple` tail — the same
  `${DEVPI_URL%%+simple*}` derivation the CI publish step uses); the upload account is
  the index's owner segment (`magogi` for magogi/prod); the password comes from
  `$DEVPI_PASSWORD` via env (`UV_PUBLISH_PASSWORD`), never argv (SC-11). Publish
  verification is the anonymous `+simple/` listing (`_devpi_has`, PKG-05 "the index is
  the truth"), matching on the wheel-name shape `-<version>-` so `0.1.1` never
  false-positives on `0.1.10`. `_index.resolve_args(remote=True)` now steers at the
  single devpi index with **no** public-PyPI extra-index (devpi's root/pypi cache
  covers public deps); the local-first smoke (`remote=False`) is unchanged. Runbook:
  mgf-standard `docs/recipes/remote-release.md` (already devpi-shaped). NOTE: pushing
  the release tag also fires the repo's CI tag pipeline at the same index — the direct
  upload promotes the *locally-validated* wheel and either side's "already exists" is
  tolerated as idempotent.
- **`_DEVPI_INDEX` is dead — `$DEVPI_URL` resolution everywhere** (Wave-2 P3 §3.4,
  the PAPER-113 companion): generated pipelines now reference the Woodpecker
  server-level `$DEVPI_URL` variable in every renderer-emitted install (with the same
  fail-fast guard line the template fragments carry — one greppable guard shape
  fleet-wide) instead of embedding the endpoint literal. Host-side tooling resolves
  the same fact through the ONE resolver `mgf.fed._index.devpi_url()` (precedence:
  non-empty `$DEVPI_URL` → the MGF_INFRA §1 default — the only place the literal
  remains). The fleet-lock tooling (`tests/fleet/`) reads the resolver too. A devpi
  host move is now: server variable + the resolver default + the WF-13 re-render wave.
  `.woodpecker.yml` re-rendered accordingly (zero endpoint literals in the render).
- **The fleet policy has ONE namespace** (Wave-2 P3 §3.3): `load_policy` reads the
  canonical `[tool.mgf-fed.policy]` table from **mgf-standard's own** `pyproject.toml`
  first; `templates/policy.toml` remains as a DEPRECATED fallback, read iff the
  pyproject section is absent, with a one-line deprecation note on load. Same keys,
  same semantics, no gate weakened; `mgf-fed doctor` reports whichever source
  resolves. The coverage-floor default stays single-sourced in
  `_woodpecker._DEFAULT_COVERAGE_FLOOR` (85); precedence unchanged
  (`[tool.mgf-fed].coverage_floor` → `[tool.coverage.report].fail_under` → default).
  The stale `[tool.mgf.ci]` comment ghosts are swept (`[tool.mgf-fed]` is and was the
  only namespace read).
- **gitleaks drift goes shape-aware** (Wave-2 P3 §3.6; closes PAPER-03's
  false-positive class): `drift-check` compares `.gitleaks.toml` as parsed TOML shape
  instead of bytes — a consumer's ADDITIVE allowlist entries (its own fixture paths /
  extra allowlist lists) are tolerated; a removed or edited *standard* entry, and any
  non-allowlist difference (title/[extend]/rules/allowlist scalars), still fails.
  `scaffold` mirrors the rule: it never clobbers a shape-conformant consumer copy
  (which would wipe the tolerated additive entries) and still restores a mutated one
  to canonical. Both directions pinned in `tests/unit/test_gitleaks_shape.py`.

### Removed
- **Dead `skip_editable` carve-out knob** (Wave-2 P3 §3.3): parsed and assigned but
  never read — `pip-audit --skip-editable` is intrinsic to the audit command. The
  tombstone check that keeps it dead: `[tool.mgf-fed]` unknown keys (including
  `skip_editable`) now draw a stderr note from the renderer, pinned by
  `tests/unit/test_ci_carveouts.py` (which runs in single-repo CI clones too).
- `CODEBERG_INDEX` / `CODEBERG_PYPI_UPLOAD` / `CODEBERG_PKG_API` constants and the
  `_codeberg_has` REST verify — the orphaned Codeberg release path (superseded by the
  devpi retarget above; `MGF_CODEBERG_TOKEN` is no longer read by `release-remote`).

### Deprecated
- **`provision-ci-host` is now a deprecation shim → `mgf-cloud-provision ci-host`**
  (Wave-2 P0.4; Court: "migrate, one provisioner"). The in-repo implementation rendered
  the pre-Postgres SQLite compose from `deploy/woodpecker/` — a rebuild would have
  reintroduced the known-bad single-writer topology (recon H3). The verb keeps its flags
  for one deprecation cycle: it prints the notice and delegates to
  `mgf.cloud_provision.ci_host.provision_ci_host` when `mgf-cloud-provision` is
  installed, else exits 1 with install/usage instructions (no cloud action either way).
  It never forwards the stale local bundle — only an explicit `--bundle-dir`.
  `deploy/woodpecker/` (README, `docker-compose.yml`, `self-heal.sh`) now carries
  DEPRECATED headers naming the successor: the live runbook, sanitized examples,
  `ops/` self-heal scripts and `backups/` recipes all live in mgf-cloud-provision.
  Also fixed there: the propagated `PCU-`→`PCP-HOESFJV-clouds.yaml` filename typo.

### Fixed
- **AP-16 compat-matrix now covers repos whose runtime `mgf-common` lives in an
  extra, not core `dependencies`.** The floor parser (`_woodpecker._mgf_common_floor`)
  read only `[project].dependencies`, so `mgf-fed` itself — whose core is
  zero-dependency and whose real (24-import) `mgf-common>=0.49` dep sits in the
  `[cli]` extra — silently rendered **no** `mgf-common-matrix` step, excluding the
  cockpit from the AP-16 floor guard it ships to every other consumer. The parser
  now falls back to the repo's *runtime* extras (the same `.[dev,<extras>]` surface
  the matrix installs — everything except `dev`/`standards`), so an extra-only
  runtime pin is guarded. A `standards`-only conformance pin is **not** a runtime
  dep and correctly stays OFF (no floor to bound) — verified for `mgf-brand-web`,
  `mgf-fed-web`, `mgf-fedres`, `mgf-test-web` (0 runtime `mgf.common` imports).
  `mgf-fed`'s own `.woodpecker.yml` regenerates with the matrix at floor
  `mgf-common==0.49.*`. No other repo's render changes.

## [0.11.0] - 2026-07-02

### Added
- **`ping-siblings --stagger` — pace the AP-16 cornerstone fan-out** (default
  4.0s between triggers; `0` restores the old fire-all-at-once). Woodpecker mints
  a per-build clone credential against the forge for each triggered pipeline, and
  a tight ~27-in-a-row burst exhausts Codeberg's rate limit after ~a dozen: the
  tail's `clone` step then fails "could not read Username for
  'https://codeberg.org'" and never runs the compat-matrix. Observed live on the
  mgf-common 0.49.2 fan-out — the first 11 pipelines cloned, 12-27 all failed.
  Each trigger now streams as it fires. The cornerstone-ping CI step calls the
  verb with no args, so it picks up the default — no template change.

### Fixed
- **`tests/fleet/refresh_lock.py` excludes off-devpi libraries from the fleet
  co-install roster.** It filtered to `lang==python` but not to *published* libs,
  so `mgf-cloud-web` (a deliberately off-devpi local GUI tool, registered when
  mgf-cloud-app was renamed) was pulled into the full-fleet `uv` resolve →
  "mgf-cloud-web was not found in the package registry". Now probes devpi's simple
  index and keeps only libs it actually serves (honouring the roster header's
  stated "every *published* library" intent; future off-devpi tools need no
  registry flag), with a guard against writing a near-empty roster when the index
  is unreachable. Regenerated `fleet-coinstall.lock` to the current cornerstone
  (`mgf-common==0.49.2`) — restoring the `test_committed_lock_matches_cornerstone`
  gate the AP-16 ping surfaced.

## [0.10.2] - 2026-07-02

### Fixed
- **app-doctor AP-13 recognizes a dynamic `importlib.metadata` `__version__`.** A
  package whose `__init__` resolves `__version__` at runtime via
  `importlib.metadata.version(...)` with a literal fallback (e.g. `"0.0.0+unknown"`)
  was mis-read by `_read_dunder_version`, which grabbed the *fallback literal* and
  reported false AP-13 drift vs `pyproject` (seen on mgf-cloud: `0.0.0+unknown` vs
  `0.1.3`). AP-13 now detects the dynamic pattern and reports it OK ("resolved at
  runtime via importlib.metadata — no drift possible"). Cherry-picked from #58.

## [0.10.1] - 2026-07-01

### Fixed
- **AP-16 compat-matrix floor clamped to devpi's minimum (0.44).** The matrix re-pins
  `mgf-common==<floor>.*`, but devpi's oldest served mgf-common is 0.44.5 (the Phase-2
  cutover didn't backfill 0.41-0.43). A repo declaring an older floor (e.g. `>=0.41`)
  made the matrix step fail with "No matching distribution". The generator now clamps
  the matrix-tested floor UP to 0.44 (`max(pin_floor, 0.44)`); the repo's pin is left
  untouched. Un-installable older floors resolve to `>=0.44.5` in practice anyway.
- Dropped the matrix's `python -c` version diagnostic (a `': '` inside broke
  Woodpecker's YAML compiler); the non-quiet re-pin logs the resolved version instead.
  (Fragment fix shipped in mgf-standard; assertions updated here.)

## [0.10.0] - 2026-07-01

### Added
- **AP-16 federation pin-window enforcement.** The `.woodpecker.yml` generator now
  renders a `mgf-common-matrix` compat step into every repo that declares a versioned
  `mgf-common` dependency — it re-pins `mgf-common` down to the repo's own floor and
  re-runs the suite, so a real break at the floor reds CI (the upper end of the
  `>=<floor>,<1.0` window is the normal test step, resolved dynamically → no baked
  cornerstone, no drift). Gated on a parseable pin floor AND not-the-cornerstone via a
  new `mgf_common_floor` render param.
- **`mgf-fed ping-siblings`** — the AP-16 B-lite fan-out verb. After a `mgf-common`
  cornerstone release, triggers every mgf-common-consuming sibling's compat-matrix CI
  (federation roster in dev, `mgf.standard` package data in single-repo CI; skips
  cornerstone / non-Python / non-consumers; `--dry-run`; fail-closed on no token). The
  cornerstone's release pipeline runs it post-publish via the companion
  `cornerstone-ping` fragment.
- `fed-tools`: an `update` command + uv-based install; fixed the source-install index.

Companion templates — `frag-mgf-common-matrix.yml`, `frag-cornerstone-ping.yml`, the
base.yml markers, and `scripts/ping_sibling_ci.py` — ship in mgf-standard. See rule
**AP-16** / API_DESIGN §4.4.

## [0.8.0] - 2026-06-29

### Added
- **Scaffold green-on-push-1** — a freshly-scaffolded repo passes CI on its first
  push instead of needing four manual fixes (surfaced shipping the mgf-fed-web /
  mgf-test-web / mgf-fedres web tools). The renderer/scaffold half (templates land in
  mgf-standard):
  - `lint-imports` (DP-01) now renders in the generated `.woodpecker.yml` **only when
    the repo declares an import-linter contract** — a `.importlinter` file OR
    `[tool.importlinter]`. A new `has_importlinter` render param gates the
    `# <<INSERT:lint-imports>>` marker; a repo without a contract omits the line
    instead of fast-failing the lint step on "Could not read any configuration".
  - `vuln-scan` applies a fleet-default `--ignore-vuln CVE-2025-71176` (the dev/CI-only
    pytest <9.0.3 local /tmp DoS, never in any shipped wheel) on every pipeline,
    deduped with any per-repo `[tool.mgf-fed].vuln_ignore`.
  - `mgf-fed scaffold` ships the AG-18 `.githooks/prepare-commit-msg` hook (via policy
    `[exact]`) and the writer `chmod`s `.githooks/*` to 0o755 so the committed hook is
    executable on a fresh clone.

## [0.7.2] - 2026-06-26

### Changed
- **Adopt mgf-common 0.49** (pin `>=0.49,<0.50`) and retire the last three
  closed-box `_DEFERRED` private reaches (AP-14 / DP-13): `_identity._set_identity`
  + `_identity._APP_NAME` -> public `set_identity()` / `app_name()`+`current_context()`;
  `_lifecycle._GLOBAL_CONTEXT` -> `reset_context()`; `_doctor_snapshot.RUNTIME_SNAPSHOT_ENV_VAR`
  -> the public re-export. `tests/test_closed_box.py::_DEFERRED` is now empty —
  mgf.fed reaches mgf.common only through its public API.

## [0.7.1] - 2026-06-26

### Changed
- **Adopt mgf-common 0.48** — widen the `[cli]` pin to `>=0.48,<0.49` (the
  cornerstone's L3 release: the `retry` primitive + a public `io.atomic_write`).
  Part of the fleet cohort pin-walk for the 0.48 minor.
- **AP-14 (closed-box) — route `atomic_write` through the public API.** All 12
  `from mgf.common._io import atomic_write` reaches (in `cli/_catch_up.py` and
  `cli/_migrate.py`) now use the public `mgf.common.io.atomic_write` that
  mgf-common 0.48 promoted. The closed-box guard
  (`tests/test_closed_box.py`) drops its lone `_io` blessing accordingly —
  tightening the contract so no private `mgf.common._*` reach is blessed; the
  three remaining `_DEFERRED` leaks (`_identity`/`_lifecycle`/`_doctor_snapshot`)
  still await their own public mgf.common APIs.

## [0.7.0] - 2026-06-24

### Added
- **Central log shipping (Logging L2, item e).** `mgf-fed` now bootstraps log
  shipping at the top of `main()`: when `MGF_LOGSHIP_URL` and
  `MGF_LOGSHIP_INGEST_TOKEN` are set, it attaches `mgf-logship`'s `LogShipper`
  to the root logger (`app="mgf-fed"`), so every invocation's logs reach the
  federation log host (openobserve-prod) over the NDJSON `/ingest` path. When
  shipping is enabled it also runs `mgf.common.bootstrap()` so records carry
  app/trace context (resolves the `current_context() before bootstrap` warning).
  Fully opt-in and guarded — unset env, or `mgf-logship` not installed, is a
  silent no-op, so the zero-dep orchestration core is unchanged. New private
  `mgf.fed._logging` module; `mgf-logship>=0.1.6` added to the `[cli]` extra.
  Verified end-to-end against the live collector (record + `app_version` land in
  OpenObserve).

## [0.6.0] - 2026-06-19

### Added
- **`mgf-fed start` — the single federation front-door.** One command, one
  consistent agent message for every onboarding case. It is a print-only
  dispatcher over the four existing prompts (it never duplicates their bodies,
  resolved from `mgf-standard`): auto-detects when the repo is already a
  registry member (`[project].name` in `mgf.standard.libraries()`) and prints
  the **sync** prompt; otherwise prints a scenario menu listing every mode with
  a detection hint (empty vs has-code, consumer vs not). `--mode
  {new|onboard|sync|adopt}` forces a mode, `--list` always prints the menu,
  `--path` points detection at a directory other than the cwd. Print-only by
  contract — it emits a prompt or a menu and never touches the filesystem.

### Fixed
- `mgf.fed.__version__` was stuck at `0.4.0` while the distribution shipped
  `0.5.0`; both now track the package version.

## [0.5.0] - 2026-06-16

### Added
- **The end-user / governance CLI, moved from `mgf-common`** (federation reorg
  Wave 3). `pip install 'mgf-fed[cli]'` adds `mgf-fed <verb>` for the former
  `mgf-common` subcommands: `version` / `bundle` / `explain` / `validate` /
  `migrate` / `bench` / `logs` / `standards` / `federation` / `design` /
  `init-project` / `catch-up` / `sync-prompt` / `new-project-prompt` /
  `adopt-prompt` / `onboard-prompt` / `agents` / `federation-sync` / `test`,
  plus **`app-doctor`** (was `mgf-common doctor`) and **`feedback-howto`** (was
  `mgf-common feedback`) — renamed so they don't shadow mgf-fed's native fleet
  `doctor` (self-check) and `feedback` (FB-05 relay). The commands live in the
  new `mgf.fed.cli` package and self-register into mgf-common's subcommand
  registry; `__main__` merges them into the native fleet parser.
- `[cli]` extra (`mgf-common>=0.47`, `mgf-standard`, `mgf-test-supervisor`).
  The zero-dep orchestration core (status / release / drift / …) still runs
  without it — every cli handler is imported lazily and the parser degrades
  gracefully when the extra is absent.

### Changed
- `[standards]` extra is now a thin alias for `[cli]` (back-compat).
- The `[cli]` extra requires `mgf-common>=0.47,<0.48` (the pure-library release
  that shed this CLI).

## [0.4.0] - 2026-06-14

### Added
- **Woodpecker pipeline generator** — `.woodpecker.yml` is generated per-repo from
  `mgf-standard/templates/woodpecker/` (`_woodpecker.render_woodpecker`); `scaffold` writes it,
  `drift-check` enforces generation-currency with a `[exempt].woodpecker` name-list fallback for
  bespoke repos. The CI-unification engine.

## [0.18.1] - 2026-07-31

### Fixed
- `AUDIT.json` excluded from the release-currency source question. The audit's own
  committed artifact is a repo-root governance file no wheel ships; leaving it out
  made the gate report the repo that stores its results — one hour after the same
  gate taught the same lesson about `AGENTS.md`.

## [0.9.0] - 2026-06-29

### Added
- **`fedres check` freshness gate in the woodpecker template (D-GEN).** The
  renderer now splices a standalone `fedres-check` step into a repo's
  `.woodpecker.yml` when (and only when) the repo carries
  `fed-resources/source/project.toml` (new `has_fed_resources` gate, mirroring
  `lint-imports`). The step installs `mgf-fedres` + `mgf-common[standards]` and runs
  `fedres check` — regenerate→byte-identical, nonzero on drift. The Class-P
  projectors read the registry from the mgf.standard package data, so the gate runs
  in a single-repo CI checkout (no sibling mgf-standard needed).

## [0.3.0] - 2026-06-13

### Added
- `mgf-fed feedback` — relay cross-repo feedback outboxes into target inboxes (FB-05). `--check`
  (default) scans every member's `pending-feedback/*.md` and exits non-zero on any un-relayed paper
  (the no-silent-rot gate); `--relay [--target R] [--dry-run]` assigns the target's next `PAPER-NN`,
  renders the FB-02 inbox entry into the target `FEEDBACK.md`, moves the source paper to
  `pending-feedback/relayed/`, and prints the reviewed PR commands. Idempotent; implements the
  mgf-standard `feedback-relay.md` recipe.

## [0.2.0] - 2026-06-11

### Added
- `mgf-fed doctor` — self-diagnostic (OP-02); rich `--version` (OP-01); top-level error handler (EH-04).
- Full ten-doc baseline (AGENTS/SECURITY/CONTRIBUTING/STARTHERE/FEEDBACK/FEDERATION + docs/claude/CLAUDE.md + PUBLIC_API.md) + docs buckets.
- Quality infra: coverage gate (>=80%), import-linter contract (WF-02), 13-marker test taxonomy + smoke layer, pytest-timeout, mgf-test-supervisor adoption ([supervisor] extra), [project.urls].

### Changed
- §Z conformance audit: mgf-fed now 122 ✅ / 0 ⏳ / 96 N/A — 100% of applicable MUSTs (was 80%).

## [0.1.0] - 2026-06-10

### Added
- Initial release — the federation orchestrator skeleton (Phase 0).
- `mgf-fed status` — read-only federation tracking matrix (per-repo version, conformance %,
  CI / import-linter / PUBLIC_API / ledger / `[standards]` presence) → `~/.mgf/MGF_FED_STATUS.json`.
- `mgf-fed drift-check` — verify each repo against the canonical scaffolding policy
  (`mgf-standard/templates/policy.toml`): present-files, byte-exact shared files, required
  `.woodpecker` steps, and pyproject gate-policy (incl. the ruff format-stability pin).
- `mgf-fed local-index` — inspect the local package index (`~/.mgf/local-index/`); the
  `local → Codeberg → PyPI` resolution mechanism for the upcoming local-release flow.
- Roadmap subcommands stubbed (`gates`, `local-ci`, `release-local`, `release-remote`,
  `scaffold`, `registry`) so `--help` documents the plan.
