# mgf-common — Claude Reference

Dense lookup document. No prose. Facts only.

> **See also: [`claude_common.md`](claude_common.md)** in this
> directory — cross-project user behavioral rules, generic
> pre-approvals, and the AI-directions / principal-engineer
> review posture. That file applies to EVERY project on this
> machine; this file is mgf-common-specific.

---

## User pre-approvals — project addenda

The generic pre-approvals are in
[`claude_common.md`](claude_common.md) §"User pre-approvals
(cross-project)". The mgf-common-specific addenda below extend
those:

- **Pinned project root.** Read/write to every file under
  `/home/bassam/PycharmProjects/mgf_common/` is pre-approved
  (the cross-project rule says "every file in the active
  project's directory"; this is the explicit pin).
- **PyPI uploads to `mgf-common`.** Twine upload was pre-approved
  during the release flow and is the standing path for
  every release going forward, following the procedure in
  §"Build and release" below. Each release tag is its own
  implicit go-ahead since the user types `release` /
  `cut vX.Y.Z` to trigger.
- **Default branch is `master`** (not `main`). Cross-project
  rules talk about "default branch"; for this repo that means
  `master` on Codeberg.

The runtime mirror — auto-allow rules at the harness level — is
at `.claude/settings.local.json` (project, gitignored). The
generic global rules live in `~/.claude/settings.json`. Reference
copies of both files live in this directory as
`settings_project.json` and `settings_common.json`; see the
local [`README.md`](README.md) for the layout.

---

## Standing instructions — always follow these

These apply to every conversation, regardless of the specific task.

**After every code or config change:**
- Update the relevant section of this file (CLAUDE.md) if the change adds, removes, or corrects a fact that would be useful in future conversations — gotchas, invariants, non-obvious design decisions, fixed bugs.
- Update the `Last updated` date at the bottom.
- Do NOT add ephemeral details (current branch name, in-progress state, step-by-step task lists). Those belong in tasks or plans, not here.

**Phase critical-facts sections:**
- After fixing a bug in any phase-owned module, add a note to the appropriate Phase N critical facts section below.
- Keep phase critical facts as the canonical "what will burn you" reference — not a change log. Rewrite stale facts rather than appending corrections to corrections.

**Standards compliance (`docs/STANDARDS_COMPLIANCE.md`):**
- The rolling compliance document. Walks every MUST in `docs/standards/` (103 rules across 7 topic docs) with status (✅ / 🟠 / ❌ / ➖), evidence, and pinning test where applicable. Open gaps in §2; closed gaps + audit trail in §3; observations (deliberately deferred findings) in §4; audit method in §5.
- Audit cadence: every release + ad-hoc when a standard bumps a MUST. Pattern from `COMMON_COMPONENTS.md` §"Maintaining compliance over time" (the cornerstone rhythm).
- When you change a public API, the AP-10 pin test (`tests/unit/test_public_api_doc.py`) fails until `PUBLIC_API.md` is updated with a row. The AP-13 pin (`tests/unit/test_version_in_sync.py`) fails when `__version__` and `pyproject.toml::project.version` drift.

**Standards docs (`docs/standards/`):**
- This package IS the reference implementation of `docs/standards/`. When you change a public API here, audit the relevant topic doc (ERROR_HANDLING / LOGGING / CONFIGURATION / DESIGN_PRINCIPLES / SECURITY / TESTING / API_DESIGN / COMMON_COMPONENTS) and update wording in the same commit. The docs MUST stay grounded in code that runs.
- Standards version-bump policy: MAJOR for a MUST change, MINOR for a SHOULD/template change, PATCH for wording. Independent of the package version.
- Current standards version: **v2.0** (introduced `SECURITY.md`, `TESTING.md`, and `API_DESIGN.md`; added DP-11/12/13, EH-11/12/13, LG-13/14, CF-11/12, all SC-*, all TS-*, all AP-*; added §11–§14 to DESIGN_PRINCIPLES, §13–§15 to ERROR_HANDLING, §17–§18 to LOGGING, §16–§18 to CONFIGURATION, §15–§16 to COMMON_COMPONENTS). The MUSTs-index in `docs/standards/README.md` is the canonical sortable list of every MUST across the standards (now 82 rows).
- A repository-root `SECURITY.md` is required by SC-12 (vulnerability-reporting policy, distinct from the engineering standard at `docs/standards/SECURITY_STANDARD.md`).
- A repository-root `PUBLIC_API.md` is required by AP-10 (the contract list — every public name with stability tier).
- A repository-root `CONTRIBUTING.md` is the open-source-hygiene companion to the standards docs (PR process, four green gates, communication channels).

**Consumer feedback (`FEEDBACK.md`):**
- `FEEDBACK.md` at the repo root is the living interface for
  pulling feedback from early consumers BEFORE the API locks at 1.0.
  Treat 🔴 Blocker entries there as release gates — 1.0 MUST NOT ship
  until every 🔴 is either Accepted+shipped, Rejected-with-rationale,
  or explicitly Deferred past 1.0 by maintainer decision.
- **Format v2 (2026-04-26):** every entry uses a uniform frontmatter
  table (Status / Severity / Submitter / Submitted / Decision /
  Decided on / Scheduled for / Shipped in) + structured prose
  sections (Concern / Why it matters / Suggested shape / Cost /
  Decision rationale / Retrospective). The frontmatter is the single
  source of truth for status — grep `Status | OPEN` to find what to
  work on. See FEEDBACK §1.4 for the canonical template.
- The dashboard at the top of FEEDBACK.md gives the at-a-glance
  view: open-blocker count, shipped-this-release table, list of open
  blockers needing decisions.
- When a new 🔴 lands via a consumer PR, triage in the PR thread; the
  decision (Accepted / Rejected / Deferred) and rationale MUST be
  recorded inline in the FEEDBACK.md entry's `Decision` field.
- Shipped entries move to §4 with commit SHA + release tag + a
  one-line retrospective ("did the suggested shape survive contact
  with real implementation?"). That retrospective is the signal
  that tells us which consumer voices to weight more heavily next
  round.

**Memory system (`/home/bassam/.claude/projects/.../memory/`):**
- Save user preferences, project decisions, and feedback to memory files so they persist across sessions.
- Before writing a new memory, check whether an existing one can be updated instead.

**Closed-box interface principle (apply to every API decision):**

This is the load-bearing design principle of the project. Re-read
it before making any public-API decision; quote the failure mode
when reviewing PRs that violate it.

- **The library is a closed box.** Users get full power without
  bypassing to underlying layers (`pydantic`, `opentelemetry`,
  stdlib `logging`, future `paramiko` / `keyring` / etc.). Every
  capability is exposed through `mgf.common.*` — users should
  almost never need `import pydantic` or `import opentelemetry`
  for normal use.

- **Predictable + visible.** No hidden behavior. No hidden
  configuration. Every env var the library reads, every default,
  every flag is visible in one place (the typed `Settings` schema
  + an introspection helper / CLI). The user can see what's set,
  see what would happen, and override anything they need.

- **Simple on the outside, sophisticated on the inside.** Advanced
  internals are fine; complexity that bleeds out to users is what
  kills libraries. A library that is "feature-rich but a maze to
  use" loses to one that is "narrower but the path is paved".

- **Escape hatches exist but are clearly marked "internals".** When
  a consumer truly needs to reach past us (debugging an
  edge-case bug; integrating with a non-mainstream third-party),
  the path exists — but it carries no stability promise and the
  documentation says so explicitly. The default path is the
  closed-box one.

- **The PR review question.** When designing or reviewing any
  public-API change, ask: *"Can the consumer accomplish their
  reasonable use case without reaching past `mgf.common.*`?"* If
  no, the design is wrong — add the missing surface or redirect
  through us. This is the single most important review question
  for this library.

- **The library's competitive advantage IS the closed-box
  interface.** We can ship the most advanced infrastructure
  in the Python ecosystem, but if it costs the user a day of
  integration where the alternative cost an hour, we lose. The
  closed box is what makes ambitious internal sophistication
  worth carrying.

---

## Quick Facts

| Key | Value |
|-----|-------|
| Project name | mgf-common — shared infrastructure for MGF projects (typed exceptions, central config, structured logging + OpenTelemetry tracing) |
| Language / runtime | Python 3.11+ (target `py311` in ruff) |
| Package layout | `src/mgf/common/` (PEP 420 namespace package — **no `__init__.py` at `src/mgf/`**) |
| Package on PyPI | https://pypi.org/project/mgf-common/ (live since 2026-04-22, ) |
| Package on Codeberg | https://codeberg.org/magogi-admin/mgf_common |
| Version | `mgf.common.__version__` = `0.1.0` (mirrored in `pyproject.toml::project.version`) |
| Identity API | `mgf.common.configure(app_name, app_version)` — call once at every consumer entry point BEFORE any observability function |
| `py.typed` marker | `src/mgf/common/py.typed` (required so consumers' `mypy --strict` accepts inline annotations; without it every `import mgf.common` produces "module is installed, but missing library stubs or py.typed marker") |
| Line length | 100 (ruff) |
| Type checking | `mypy --strict` on `src/` (18 source files; uses `[dev,observability]` install for full coverage) |
| Lint | `ruff check src tests` |
| Layering | `import-linter`, contract `mgf-common-is-the-federation-leaf` forbids any import of `vmanager` (and any future consumer top-level package) from inside `mgf.common` |
| Test count | 590 (full suite passes in <3.5s on a warm machine; 19 are `tests/unit/test_examples.py` example-smoke tests, 109 are `tests/unit/test_public_api_doc.py` AP-10 pin tests (now scans 10 modules — added vault + cli in Phase 5), 7 are `tests/unit/observability/test_redaction_property.py` TS-13 Hypothesis tests, 17 from FEEDBACK round 1 closures (/ / / ), 24 from closed-box Phase 1, 30 from closed-box Phase 2, 19 from closed-box Phase 3, 42 from closed-box Phase 4, 82 from closed-box Phase 5 (52 in `tests/unit/test_phase5_vault.py` + 30 in `tests/unit/test_phase5_cli.py`), 31 from closed-box Phase 6a, 33 from closed-box Phase 6b, 20 from closed-box Phase 7 (`tests/unit/test_phase7_migrate.py` — EnvironmentError + make_settings_config renames with import-gate safety / per-rule gating / bootstrap-reshape NOT codemodded / --dry-run / --backup / idempotency / recursive walk with .venv+__pycache__ skip / unsupported-version error paths), 1 each for AP-13 version-sync and LG-12 perf) |
| Coverage gate | `--cov-fail-under=80` on the 3.12 CI job — see `.woodpecker.yml` for the rationale |
| Reference consumer | https://codeberg.org/magogi-admin/VManager — first project built on `mgf-common`; cited throughout the standards docs as the canonical real-world example |
| Test root | `tests/unit/` for unit tests (no integration / property suites today) |
| SSH key for git push | `/home/bassam/PycharmProjects/inthewords/my_stuff/ssh_key/codeberg` (pinned in this repo's `.git/config::core.sshCommand`; relocated from the old VManager path on 2026-04-28) |
| PyPI release token | `/home/bassam/PycharmProjects/VManager/my_stuff/ssh_key/pypi_token.txt` (project-scoped to `mgf-common` — see "Build and release" below; still at the legacy path as of release) |

---

## Dependencies

Core (hatchling):
```
pydantic>=2.6 pydantic-settings>=2.2 pyyaml>=6.0
```

Optional `observability` (Tier C, fully opt-in — every consumer that wants L2 observability installs the extra):
```
opentelemetry-api>=1.27 opentelemetry-sdk>=1.27
opentelemetry-exporter-otlp-proto-http>=1.27 opentelemetry-instrumentation-httpx>=0.48b0
sentry-sdk>=2.0
```

Optional `dev`:
```
pytest>=8 pytest-cov>=5 hypothesis>=6.100
ruff>=0.4 mypy>=1.10 types-PyYAML>=6.0
import-linter>=2.0
```

**No system-package dependencies** (unlike VManager which needs `python3-libvirt`). Plain `pip install mgf-common[observability]` works in any virtualenv.

---

## Cross-cutting patterns

These are the **load-bearing rules** that shaped this package. Read this section before extending any of the modules listed under each rule — silently violating one is the most common way to break consumers (or yourself).

### 1. PEP 420 namespace package — no `__init__.py` at `src/mgf/`

`src/mgf/` MUST NOT have an `__init__.py`. The wheel ships `src/mgf/common/__init__.py` only. This makes `mgf` a PEP 420 implicit namespace package so future siblings (`mgf.cli`, `mgf.gui`, etc.) from other repos can contribute their own subpackages under the same `mgf.*` namespace without colliding with this one.

`pyproject.toml` declares `[tool.hatch.build.targets.wheel] packages = ["src/mgf"]` — Hatchling walks the tree and ships every file (including `py.typed` and the `__init__.py` files at every regular subpackage).

The hallmark of a working PEP 420 namespace package: `import mgf; mgf.__file__` is `None` (no real `__init__.py` to point at). Pinned by `tests/unit/test_identity.py::test_namespace_package_shape`.

### 2. Identity flows through `bootstrap(...)` (or legacy `configure(...)`)

Every observability function (`setup_logging`, `setup_otel`, `report_now`, `install_excepthook`, `install_threading_excepthook`, etc.) reads `mgf.common.app_name()` to derive log paths, env-var prefixes, OTel service name, and the crash-report `app` field. The cache lives in `src/mgf/common/_identity.py`.

**Canonical path:** call `mgf.common.bootstrap(app_name=..., app_version=...)` once per entry point — this populates identity AND wires logging / OTel / excepthooks transactionally. The legacy `configure(app_name, app_version)` still works as a deprecated shim.

`bootstrap()` accepts no args for packaged apps. Identity auto-derives from `sys.modules["__main__"].__spec__.parent` (i.e. the launching package's distribution metadata) when the app is run via `python -m <package>` or a console-script entry that preserves `__spec__`. Helper: `mgf.common._identity._derive_identity_from_metadata(main_module=None)` — conservative, never raises, returns `(None, None)` when no high-confidence signal is available. When derivation fails AND no explicit args were given, `bootstrap()` emits `UnconfiguredWarning` naming the explicit path.

For ad-hoc scripts and test runners, derivation falls through to `(None, None)` — those callers MUST pass `app_name` / `app_version` explicitly. VManager wires explicit identity in `src/vmanager/cli/__main__.py::cli_main` and `src/vmanager/gui/app.py::run`. Without bootstrap (or configure) being called at all, every path lands at the placeholder `"app"` default — pinned by every test in `tests/unit/observability/conftest.py::_configure_test_identity` (autouse fixture sets a known test app name + restores on teardown so the bare identity tests stay clean).

**multi-tenancy (Option C):** `AppContext` has TWO construction modes. Bootstrap-owned (`with bootstrap(...) as ctx:` — sets the process-wide global, shuts down on exit) is the 95% case. Scoped (`with AppContext("plugin", "1.0"):` — direct construction without `_wiring`; pushes onto the per-task contextvar on `__enter__`, pops on `__exit__`) is the 5% library-in-library case. `current_context()` resolves contextvar-first, then global. Closes FEEDBACK — the -era last open 🔴 blocker. Settings inheritance is explicit (consumers pass `settings=parent.settings` when desired); scoped contexts get fresh `MgfSettings()` defaults otherwise. PEP 567 semantics give correct asyncio + threading isolation. Pinned by `tests/unit/test_multitenancy.py` (14 tests).

### 3. `make_settings_config(env_prefix="X_")` is the canonical subclass pattern

Subclasses of `BaseAppSettings` MUST set their own `env_prefix` via `model_config = make_settings_config(env_prefix="X_")`. The naïve `**BaseAppSettings.model_config, env_prefix="X_"` spread DOES NOT WORK — pydantic-settings materialises `model_config` with a default for every one of its ~40 options (`env_prefix=""` among them), so re-supplying `env_prefix` raises `TypeError: got multiple values for keyword argument 'env_prefix'` at import time.

The helper is the safe path: it only passes the keys we explicitly opt into. Tested both pinned-passes and pinned-fail (`tests/unit/config/test_loader.py::TestSubclassPatterns::test_naive_spread_pattern_is_broken`) so a future "simplification" of the base can't reintroduce the bug. Documented in `src/mgf/common/config/_settings.py` docstring.

### 4. Lazy OTel imports — package works without the `[observability]` extra

Every code path that touches `opentelemetry.*` (or `sentry_sdk`, `systemd.journal`) is wrapped in `try: import ... except ImportError: return` at the call site, NOT at module load. This way the default install (no extra) imports cleanly, mypy passes, tests run; consumers who want L2 observability install the extra and the same code paths suddenly do real work.

`pyproject.toml` `[tool.mypy.overrides]` lists `opentelemetry, opentelemetry.*, sentry_sdk, systemd, systemd.*` with `ignore_missing_imports = true` so default installs build under `mypy --strict`.

`getattr(trace, "get_current_span", None)` is the right shape inside `json_formatter._add_otel_context` — direct attribute access trips mypy under some SDK version pins. Documented in the function docstring.

### 5. `_yaml_file_override: ClassVar[Path | None]` per-subclass isolation

`BaseAppSettings._yaml_file_override` is a `ClassVar[Path | None]` declared on the BASE class. When `load_settings` does `cls._yaml_file_override = path` on a subclass, Python class-attribute semantics shadow the base — the subclass gets its own attribute, the base stays `None`. Two simultaneous loads of two different subclasses cannot interfere with each other.

The `ClassVar` annotation is load-bearing — without it, pydantic interprets bare `_x: T = None` as a `ModelPrivateAttr` and the assignment in `load_settings` doesn't reach the class. Documented in `src/mgf/common/config/_settings.py`.

### 6. Per-test save/restore of identity (NOT autouse-globally)

`tests/unit/observability/conftest.py` autouse fixture sets identity for tests in that directory only — `tests/unit/test_identity.py` MUST stay outside that fixture's scope so it can verify the placeholder defaults. This is why the conftest lives at `tests/unit/observability/`, not at `tests/conftest.py`.

The fixture saves prior values, sets `("mgftest", "0.0.0")`, yields, then restores. Without restore, `test_identity` checking placeholder behavior would fail randomly depending on collection order.

### 7. Single TracerProvider per process — session-scoped fixture

OpenTelemetry only allows `set_tracer_provider` to take effect ONCE per process. `tests/unit/observability/conftest.py::_session_tracer_provider` installs it at session scope; the per-test `in_memory_spans` fixture clears the in-memory exporter between tests. **Any new OTel-related test module MUST pull `in_memory_spans` from this conftest, NOT install its own TracerProvider** — the second install silently gets the no-op proxy and every span assertion fails.

Lifted directly from VManager's lesson learned during round-4 follow-up (see VManager's `docs/CLAUDE.md` critical facts).

---

## Package Map

```
mgf_common/
├── pyproject.toml hatchling, package = ["src/mgf"]
├── README.md
├── PUBLIC_API.md contract list per AP-10 (every public name + tier)
├── CHANGELOG.md per-release notes (Keep a Changelog), backfilled to 0.13.0
├── CONTRIBUTING.md dev setup, PR process, communication channels
├── STARTHERE.md two-flow nav hub for adopting mgf-common
├── SECURITY.md vulnerability reporting policy (per SC-12)
├── LICENSE MIT
├── .importlinter single contract: mgf-common-is-the-federation-leaf
├── .woodpecker.yml lint + typecheck + test matrix + build
├── examples/ runnable layered examples (6 topics × 3 layers)
│ ├── README.md index + conventions
│ ├── 01_exceptions/ typed hierarchy + subclassing + translation seam
│ ├── 02_config/ BaseAppSettings + load/save + discriminated unions
│ ├── 03_logging/ setup_logging + Redactor + structured fields
│ ├── 04_observability/ setup_otel + operation_span + log/trace correlation
│ ├── 05_crash_reporting/ report_now + sys/threading excepthooks
│ └── 06_full_app/ production-shaped tiny CLI (the headline integration)
├── docs/
│ ├── CLAUDE.md ← THIS FILE
│ └── standards/ cross-project engineering standards (v2.0)
│ ├── README.md master index, conformance keywords, MUSTs index, glossary
│ ├── DESIGN_PRINCIPLES.md layering, replaceability, testability, GUI threading,
│ │ async (DP-11), time/UTC (DP-12), resource mgmt (DP-13)
│ ├── ERROR_HANDLING.md typed hierarchy, translation seams, top-level handlers,
│ │ async error handling, error-message style guide
│ ├── LOGGING.md stdlib + JSON, OTel correlation, redaction, sinks,
│ │ volume control (LG-13), contextvars correlation (LG-14)
│ ├── CONFIGURATION.md pydantic-settings, precedence, schema versioning,
│ │ discriminated unions (CF-12), env tiers, alias renames
│ ├── SECURITY.md cross-project security standard (SC-01..SC-18) — new in v2.0
│ ├── TESTING.md cross-project testing standard (TS-01..TS-18) — new in v2.0
│ ├── API_DESIGN.md cross-project API-design standard (AP-01..AP-15) — new in v2.0
│ └── COMMON_COMPONENTS.md catalogue of every common component
└── src/
    └── mgf/                             NO __init__.py (PEP 420 namespace)
        └── common/
            ├── __init__.py              re-exports configure / app_name / app_version + __version__
            ├── _identity.py             module-level cache + configure()
            ├── py.typed                 PEP 561 marker (empty file)
            ├── exceptions/
            │   ├── __init__.py          public re-exports
            │   ├── _base.py             AppError
            │   └── _well_known.py       7 categories + 7 generic concretes
            ├── config/
            │   ├── __init__.py          public re-exports
            │   ├── _settings.py         BaseAppSettings + make_settings_config
            │   ├── _paths.py            platform_dir, default_config_path, expand_path
            │   └── _loader.py           load_settings, save_settings (atomic)
            ├── observability/
            │   ├── __init__.py          public re-exports
            │   ├── redaction.py         Redactor + REDACTED_PLACEHOLDER
            │   ├── json_formatter.py    one JSON object per line, OTel-aware
            │   ├── text_formatter.py    TTY-friendly, ANSI colour, NO_COLOR honoured
            │   ├── logging_setup.py     setup_logging — console + rotating file + opt-in sinks
            │   ├── otel_setup.py        setup_otel + operation_span + emit_log_event
            │   ├── crash_reporter.py    report_now (local + sentry/otlp/http sinks)
            │   ├── excepthook.py        install_excepthook (sys)
            │   └── threading_excepthook.py  install_threading_excepthook
            ├── vault/                   credential storage backends (Phase 5; closes FEEDBACK §3.2)
            │   ├── __init__.py          public re-exports + built-in backend pre-registration
            │   ├── _env_vault.py        EnvVault — stdlib-only, env-var backed
            │   ├── _keyring_vault.py    KeyringVault — system credential store via keyring
            │   ├── _file_vault.py       EncryptedFileVault — Fernet AES-128 + PBKDF2 600k
            │   └── _registry.py         register_vault_backend decorator
            └── cli/                     CLI helpers (Phase 5) + diagnostic CLI (Phase 6b)
                ├── __init__.py          public re-exports
                ├── _exit_codes.py       EXIT_* constants (numeric pin per AP-04)
                ├── _handler.py          run_and_translate / translate_exceptions / main_entry
                ├── _provenance.py       field_provenance + env_vars_known_to_mgf shim
                ├── _explain.py          mgf-common explain subcommand
                ├── _validate.py         mgf-common validate subcommand
                ├── _doctor.py           mgf-common doctor subcommand
                ├── _migrate.py          mgf-common migrate subcommand (codemod, Phase 7)
                └── _mgfcli.py           console-script entry (argparse dispatch)
```

---

## Public API (the surface consumers depend on)

### `mgf.common`

```python
mgf.common.configure(app_name: str, app_version: str = "unknown") -> None
mgf.common.app_name() -> str
mgf.common.app_version() -> str
mgf.common.__version__: str
```

### `mgf.common.exceptions`

Hierarchy:
```
AppError
├── ConfigError base for config / spec failures
│ ├── SchemaValidationError carries .errors (list[str]); message: "validation failed: ..."
│ └── AppConfigError config.yaml is missing / malformed / unknown version
├── ProviderError base for third-party-SDK boundary failures
│ ├── ResourceNotFoundError carries .name; message: "no resource named X"
│ └── ResourceAlreadyExistsError carries .name; message: "a resource named X already exists"
├── OperationError base for runtime failures during long-running ops
│ └── CommandError carries .argv / .returncode / .stderr; message: "command failed (exit N): ..."
├── GuestError base for remote-agent communication failures
│ ├── GuestUnreachableError bare subclass; message in args
│ └── GuestCommandError bare subclass; message in args
├── EnvironmentError base for host pre-flight failures (deliberately shadows builtin alias)
├── VaultError base for credential-storage failures
└── SchedulerError base for scheduler / hook subsystem failures
```

Consumer subclassing pattern:
```python
from mgf.common.exceptions import AppError, ProviderError, ResourceNotFoundError

class MyAppError(AppError): ... # nominal alias, optional
class DomainNotFoundError(ResourceNotFoundError): # extend a generic concrete
    def __init__(self, name: str) -> None:
        self.name = name
        super().__init__(f"no VM named {name!r}; run 'myapp vm list'")
```

### `mgf.common.config`

```python
class BaseAppSettings(pydantic_settings.BaseSettings):
    CURRENT_VERSION: ClassVar[int] = 1
    version: int = 1
    # subclasses MUST set model_config via make_settings_config()

def make_settings_config(**overrides: Any) -> SettingsConfigDict
def load_settings(cls: type[T], *, path: Path | None,
                  migrate: Callable | None = None) -> T
def save_settings(cfg: BaseAppSettings, path: Path) -> None
PathKind = Literal["config", "state", "data"] # re-exported alias
def platform_dir(kind: PathKind) -> Path
def default_config_path(app: str | None = None) -> Path
def expand_path(v: object) -> object
```

### `mgf.common.observability`

```python
def setup_logging(*, level: str = "INFO", fmt: LogFormat = "auto",
                  log_file: Path | None = None,
                  redactor: Redactor | None = None) -> None
def setup_otel(*, enabled: bool | None = None) -> None
def operation_span(name: str, **attrs: Any) -> ContextManager[None]
def report_now(exc: BaseException, *, source: str) -> Path
def install_excepthook() -> None
def install_threading_excepthook() -> None

class Redactor:
    @classmethod
    def default(cls) -> Redactor
    def redact(self, payload: dict[str, Any]) -> None
    def redact_string(self, s: str) -> str
```

---

## Phase critical facts

The Round 5 extraction landed in five phases (see `docs/internal/EXTRACTION_HISTORY.md` for the full plan). Each phase below maps to a single git commit on master.

### critical facts (skeleton bootstrap)

**The PEP 420 namespace requires `explicit_package_bases = true` AND `namespace_packages = true` in `[tool.mypy]`.** Without both, mypy reports "Source file found twice under different module names" because it sees `src/mgf/common/X.py` as both `common.X` (via the shorter path) and `mgf.common.X`. Documented in `pyproject.toml`.

**`include_external_packages = True` is required** at the top of `.importlinter`. Without it, the `forbidden_modules = vmanager` contract refuses to start because `vmanager` is an external (non-`mgf`) package. The single contract `mgf-common-is-the-federation-leaf` IS the lift-readiness invariant — without it, a casual `import vmanager.X` inside `mgf.common` would create the cycle that the entire round-5 extraction was designed to prevent.

### critical facts (lift core components)

**`BaseAppSettings` does NOT support the `**model_config` spread for subclasses** — see Cross-cutting pattern §3. Use `make_settings_config(env_prefix="X_")` instead.

**`_yaml_file_override` is a `ClassVar[Path | None]`, not a `ModelPrivateAttr`** — see Cross-cutting pattern §5. The annotation is load-bearing.

**Identity placeholders default to `("app", "unknown")`** — see Cross-cutting pattern §2. Production callers MUST call `configure` first; tests in `tests/unit/test_identity.py` deliberately don't assert the default VALUES (they may have been mutated by an earlier test) but DO assert the placeholder shape (str, present at import time).

**`_isolate_state` fixture in `tests/unit/observability/test_logging_setup.py` MUST `handler.close()` every root-logger handler before removing it** — otherwise `RotatingFileHandler` triggers `PytestUnraisableExceptionWarning` on Python 3.12+ GC, and `filterwarnings = ["error"]` (from pyproject) escalates to a hard test failure.

**Excepthook tests MUST replace `sys.excepthook` with a no-op before installing** the standard hook — pytest's prior hook captures the synthetic exception and marks the test as failed. See `tests/unit/observability/test_excepthooks.py::test_excepthook_writes_crash_report_for_runtime_error`.

**Single TracerProvider per process** — see Cross-cutting pattern §7.

### Phase 2 review critical facts (`make_settings_config` helper)

**`SettingsConfigDict` is a TypedDict — `cast` is the right tool to satisfy mypy** when building it from a dynamic-key `dict`. `make_settings_config` returns `cast("SettingsConfigDict", defaults)` after merging the recommended baseline with caller overrides. Mypy can't validate the keys here (the `**overrides` is typed `Any`); pydantic-settings validates them when it consumes `model_config` on the subclass. Pinned by `tests/unit/config/test_loader.py::TestSubclassPatterns::test_naive_spread_pattern_is_broken` so future contributors can't "simplify" the base back into the bug.

### critical facts (VManager consumer cutover)

**`pyproject.toml` `py.typed` marker is REQUIRED** for downstream `mypy --strict`. Without it, every `import mgf.common.X` in VManager produced `error: Skipping analyzing "mgf.common": module is installed, but missing library stubs or py.typed marker [import-untyped]` AND a cascade of "Class cannot subclass BaseAppSettings (has type Any)" errors. The fix is a single empty file at `src/mgf/common/py.typed`. Hatchling ships it automatically because `[tool.hatch.build.targets.wheel] packages = ["src/mgf"]` walks the tree.

**Every consumer entry point MUST call `mgf.common.configure(app_name=..., app_version=...)`** BEFORE any observability function — see Cross-cutting pattern §2.

**VManager keeps `vmanager.exceptions.VManagerError = AppError` as a one-line alias** for source-compat with every existing `except VManagerError:` call site. The 19 VManager-specific subclasses (`SpecValidationError`, `DomainNotFoundError`, `BackupError`, etc.) stay in VManager. `SpecValidationError` overrides `__init__` to keep the historical `"spec validation failed:"` prefix instead of mgf.common's `"validation failed:"` — VManager tests pin the prefix.

**VManager's `vmanager/observability/` is a re-export shim** — the seven submodule files moved here; `vmanager.observability.__init__` now does `from mgf.common.observability import setup_logging, ..., logging_setup, crash_reporter, ...` so existing import sites keep working unchanged. Consumers SHOULD prefer `from mgf.common.observability import X` for new code.

### critical facts (standards docs migration)

**`docs/standards/` is owned by mgf-common, not by VManager.** VManager has a single `README.md` at `docs/standards/` that's a forwarding pointer. Cross-project engineering standards version-bump policy applies (MAJOR for MUST, MINOR for SHOULD/template, PATCH for wording) — independent of the package version.

Each topic doc carries a "📦 Post-extraction note" header explaining that the implementation moved to `mgf.common` and that gap-closure plans in the body are now an obsolete record of how each gap WAS closed (useful as a worked example for any new project bringing itself to L2).

### critical facts (PyPI release )

**PyPI's trusted-publisher OIDC does not support Codeberg / Forgejo as an issuer.** Confirmed by `pypi.org/manage/account/publishing/` — only GitHub, GitLab, Google, ActiveState. Until that changes, the release flow is **manual `twine upload`** from a developer machine using a project-scoped PyPI API token at `/home/bassam/PycharmProjects/VManager/my_stuff/ssh_key/pypi_token.txt` (mode 0600).

**`ci.codeberg.org` is allow-list gated** — first login as `magogi-admin` returned `org_access_denied`. Apply for access at https://codeberg.org/Codeberg-CI/request-access. The `.woodpecker.yml` lives in the repo as documentation + copy-paste-ready reference even when no runner consumes it. Once granted, every push / PR / tag fires the four steps (lint + typecheck + test matrix + build) with no further config change.

**`python -m build` with `uv` venv requires `--no-isolation`** because `build` creates an isolated venv that needs `python3-venv` (not installed system-wide on this dev host). Workflow:
```bash
uv pip install --python /home/bassam/PycharmProjects/mgf_common/.venv/bin/python build twine hatchling
rm -rf dist/
/home/bassam/PycharmProjects/mgf_common/.venv/bin/python -m build --no-isolation
```

**Coverage gate is `--cov-fail-under=80`, not 85% like VManager.** Lazy-imported sink code (Sentry, OTLP, syslog, journald, HTTP webhook) inflates the unreachable-without-real-backends surface. We sit at ~85% today; 80% catches real regressions without forcing test churn against SDKs we already trust. Documented inline in `.woodpecker.yml`.

---

## Key conventions

- **Identity flows through `mgf.common.configure(...)`.** Every per-app constant in observability code reads `app_name()` instead of being hard-coded. No string literals like `"vmanager"` or `"mgf-common"` in production code paths.
- **Typed exceptions over ad-hoc strings.** Subclass the right `mgf.common.exceptions` category; carry machine-readable fields in `__init__`; let the message be human-readable.
- **`from __future__ import annotations`** at the top of every module. Runtime-unused imports go into `TYPE_CHECKING`.
- **Frozen dataclasses by default** (`Redactor` is frozen so the same instance can be safely shared across threads — every formatter holds one and calls `redact` on every record).
- **Lazy path resolution.** `_default_log_path()`, `_crash_dir()`, etc. resolve under `$HOME` / `XDG_*` at call time. Module-load expansion breaks per-test isolation when tests monkeypatch `HOME`.
- **`save` operations are atomic.** `_write_yaml_atomic` + `crash_reporter._write_local` both use `tempfile.mkstemp` + chmod 0o600 + `Path.replace`. Caller never observes a partially-written file.
- **`report_now` NEVER raises.** Last line of defense; any exception during build / write / ship is caught and logged at WARNING.
- **`hooks/excepthook.install_*` are idempotent.** Re-installation is a no-op; the wrappers carry a sentinel attribute so chained installs don't double-fire crash reports per crash.
- **`operation_span` is no-op-safe.** Without the `[observability]` extra installed OR without `setup_otel` having been called, `operation_span("X")` yields without opening a real span. Callers can use it unconditionally.
- **Public `__all__` lists everything callers should import.** Internal helpers stay un-exported. The exception-module `__all__` lists 15 names (`AppError` + 7 category bases + 7 generic concretes); the config-module `__all__` lists 8 names (`BaseAppSettings`, `make_settings_config`, `load_settings`, `save_settings`, `platform_dir`, `default_config_path`, `expand_path`, `PathKind`); the observability-module `__all__` lists 8 names (`setup_logging`, `setup_otel`, `operation_span`, `report_now`, `install_excepthook`, `install_threading_excepthook`, `Redactor`, `LogFormat`).

---

## Gotchas

1. **`**BaseAppSettings.model_config` spread DOES NOT WORK** — see Cross-cutting pattern §3. Use `make_settings_config(env_prefix="X_")` instead. Pinned by `test_naive_spread_pattern_is_broken`.

2. **`include_external_packages = True` in `.importlinter`** — required because the contract names `vmanager` (an external module) as forbidden. Without it, `lint-imports` refuses to start.

3. **`ci.codeberg.org` access is allow-list gated.** First login returns `org_access_denied`. Apply at https://codeberg.org/Codeberg-CI/request-access.

4. **PyPI trusted-publisher OIDC does NOT support Codeberg.** Manual `twine upload` is the release workflow until PyPI adds Codeberg. Token at `/home/bassam/PycharmProjects/VManager/my_stuff/ssh_key/pypi_token.txt`.

5. **`python -m build` needs `--no-isolation`** on this dev host (no `python3-venv` system package). See critical facts for the workflow.

6. **Single TracerProvider per process** — see Cross-cutting pattern §7. Adding a second OTel test module that installs its own provider silently breaks the first.

7. **`tests/unit/observability/conftest.py` autouse identity fixture is scoped to that directory only.** `tests/unit/test_identity.py` is OUTSIDE the fixture so it can verify placeholder defaults. Don't move the fixture up to `tests/conftest.py` — it would break the identity tests.

8. **`logging_setup._maybe_add_journald` auto-detects systemd via `JOURNAL_STREAM` / `INVOCATION_ID` env vars.** A test running under `systemd-run --user` inherits those env vars and the journald sink will get attached unexpectedly. Tests that count handlers MUST `monkeypatch.delenv("JOURNAL_STREAM", raising=False)` + `monkeypatch.delenv("INVOCATION_ID", raising=False)`. Already done in `test_logging_setup.py::test_journald_skipped_without_systemd_env`.

9. **`json_formatter._add_otel_context` uses `getattr(trace, "get_current_span", None)` not direct attribute access** — see Cross-cutting pattern §4. Don't "simplify" by removing the getattr.

10. **`SettingsConfigDict` cast in `make_settings_config`** — see Phase 2 review critical facts. The `cast("SettingsConfigDict", defaults)` is the type-system shim for the dynamic-key dict; ruff's `TC006` requires the quoted form.

11. **Coverage gate is 80%, not 85%.** See critical facts. The OTel + Sentry + journald + HTTP-webhook sink paths are lazy-imported and only exercised against real backends.

12. **Editable install with uv requires `pip install -e ../mgf_common` style** — `[tool.uv.sources]` path entry is the canonical way for VManager to depend on a local checkout. Production is `mgf-common>=0.1.0,<0.2` from PyPI; the path source stays commented out in VManager's `pyproject.toml` for emergency local testing only (see VManager's CLAUDE.md "mgf-common dependency" section).

---

## Testing

- **All tests** live in `tests/unit/` and run against pure-Python objects. No external services. The `[observability]` extra is required to exercise the OTel-aware code paths (the conftest's session-scoped `_session_tracer_provider` fixture imports `opentelemetry.sdk.*` directly).
- **Test runner:** `pytest` (see `[tool.pytest.ini_options]` in `pyproject.toml`). Markers are strict.
- **Coverage target:** ≥80% on the 3.12 CI job. We sit at ~85% today.
- **Lint:** `ruff check src tests`. Autofix with `--fix`. Ruff is the formatter too.
- **Types:** `mypy src` — `strict = true`. `mypy --strict src/` MUST pass with the `[dev,observability]` install (the sentry / opentelemetry / systemd modules are listed in `[tool.mypy.overrides]` with `ignore_missing_imports = true` so default installs without the extra also pass).
- **Layering:** `lint-imports`. Single contract: `mgf-common-is-the-federation-leaf` forbids any import of `vmanager` (and reserved sibling slots `mgf.cli` / `mgf.qt` / `mgf.providers` / `mgf.gui`) from `mgf.common`. generalized the contract: each new consumer project gets a row in `.importlinter::forbidden_modules`; the federation policy is documented inline in the file's comments.
- **Fast feedback loop:** `uv run pytest -q --tb=no` for a quick pass/fail count without tracebacks.

---

## Build and release

PyPI release is **manual** because PyPI's trusted-publisher OIDC does not support Codeberg / Forgejo (see critical facts). Per-release procedure:

```bash
# 1) Make the change, run local gates.
uv run pytest --tb=short -q --cov --cov-fail-under=80
uv run mypy src
uv run ruff check src tests
uv run lint-imports

# 2) Bump version + add CHANGELOG entry.
$EDITOR pyproject.toml src/mgf/common/__init__.py CHANGELOG.md

# 3) Commit, push, tag (annotated), push tag.
git commit -am "vX.Y.Z release"
git push
git tag -a vX.Y.Z -m "vX.Y.Z release"
git push --tags

# 4) Build the artefacts (--no-isolation works around missing python3-venv on this host).
rm -rf dist/
uv pip install --python .venv/bin/python build twine hatchling # one-time
.venv/bin/python -m build --no-isolation

# 5) Sanity-check before upload.
.venv/bin/python -m twine check dist/*
unzip -l dist/mgf_common-*.whl | grep py.typed # verify py.typed shipped

# 6) Upload to PyPI.
TWINE_USERNAME=__token__ \
  TWINE_PASSWORD="$(cat /home/bassam/PycharmProjects/VManager/my_stuff/ssh_key/pypi_token.txt | tr -d '[:space:]')" \
  .venv/bin/python -m twine upload --non-interactive dist/*

# 7) Verify the install works from a fresh venv.
cd /tmp && uv venv --python 3.12 mgf-smoke
uv pip install --python mgf-smoke/bin/python mgf-common
mgf-smoke/bin/python -c "import mgf.common; print(mgf.common.__version__)"
```

The PyPI token is **project-scoped to `mgf-common` only** — limits blast radius if the file is ever exposed.

`.woodpecker.yml` runs lint + typecheck + test matrix + build on every push (so packaging regressions fail fast) but does NOT publish — see the `# **No PyPI publish step.**` block in the file for the rationale.

When PyPI adds Codeberg as an OIDC issuer (or `ci.codeberg.org` access is granted AND we accept storing a PyPI token in Woodpecker secrets), the publish step gets added per `docs/internal/EXTRACTION_HISTORY.md` §8.4. The release-flow standard lives in `docs/standards/RELEASING.md`.

---

## Codeberg deployment — LIVE

Repo URL: https://codeberg.org/magogi-admin/mgf_common

Local git config:
- `core.sshCommand = ssh -i /home/bassam/PycharmProjects/VManager/my_stuff/ssh_key/codeberg` (pinned in this repo's `.git/config`, no env-var override needed)
- Remote: `ssh://git@codeberg.org/magogi-admin/mgf_common.git`

`.woodpecker.yml` is in place but the runner doesn't fire yet — pending `ci.codeberg.org` access (see critical facts + Gotcha #3).

---

## Roadmap snapshot

Roadmap is intentionally short:

- **Patch / minor cuts** — bug fixes, doc updates, additive primitives. Tests + standards docs may grow; the closed-box public surface stays source-stable.
- **v1.0** — declared stable when the package has ≥2 consumers in production AND survives a full year without a breaking change.

Deferred lifts (catalogued in `docs/internal/EXTRACTION_HISTORY.md` §12):
- `vmanager/common/{shell,ssh,progress,guest_agent}.py` — lift-ready (Protocol decoupling sketched in §12.1) but stay in VManager until a second consumer needs them
- `mgf.common.gui` (CoreWorker, ui_loader, log_viewer_dialog, qt_excepthook) — Qt-specific; lift only when a second GUI consumer exists
- `mgf.common.vault` — VMCredential is generic but storage backends are tied to deployment surface
- `mgf.common.providers.base` — hypervisor-specific; the *pattern* (Provider ABC + MockProvider + FailurePlan) is generic and may extract as `mgf.common.provider_pattern` later

---

## Documentation

| File | Purpose |
|------|---------|
| `README.md` | User-facing overview + install + minimal usage |
|| Keep-a-Changelog; one entry per release |
| `docs/internal/EXTRACTION_HISTORY.md` | Round 5 extraction plan + path-to-PyPI roadmap (history + forward) |
| `docs/standards/` | Cross-project engineering standards (7 topic docs + master README; 103 MUSTs) |
| `docs/STANDARDS_COMPLIANCE.md` | Rolling compliance audit — current state, open gaps, closed-gaps history, observations |
| `FEEDBACK.md` | Living interface for early-consumer feedback — 🔴 Blocker triage gates the 1.0 release |
| `examples/` | Eighteen runnable layered examples (6 topics × 3 layers); the smoke test at `tests/unit/test_examples.py` keeps them from drifting |
| `docs/MIGRATION_0.1_TO_0.3.md` | Consumer-facing → upgrade guide; pairs with the `mgf-common migrate` codemod for the mechanical renames |
| `docs/public/` | **User-facing per-component reference layer** (.x docs reorg). One doc per `mgf.common.*` subpackage + `library_common.md` shared ground. Cross-reference graph in `library_common.md` appendix. Public modules carry `# See: docs/public/XX.md` headers in their docstrings; the deleted `USAGE_PATTERNS.md` and `COMPOSING_SETTINGS.md` content folded in. Files: `library_common.md` (overview + decision tree + cross-ref graph), `01_lifecycle.md` (bootstrap + AppContext + Protocols), `02_exceptions.md`, `03_config.md` (BaseAppSettings), `04_settings.md` (MgfSettings + composition), `05_observability.md` (logging + OTel + redaction), `06_crash_reporting.md`, `07_vault.md`, `08_progress.md`, `09_cli.md` (+ Registry primitive), `10_full_app.md` (integration). |
| `docs/internal/LIFT_CHECKLIST.md` | 8-step recipe for lifting a component from a consumer project into `mgf-common` (or a sibling package). Worked example: the vault lift. . |
| `example_app/` | Standalone consumer package (`mgf-example-app`) — the "second consumer" signal. Independent `pyproject.toml`; depends on `mgf-common` via uv path source. Demonstrates ergonomic shape (composition / CLI registry / `bootstrap_for_test`). Its tests (`example_app/tests/`) are the consumer-side regression smoke. . |
| `PUBLIC_API.md` | Public-API contract per AP-10; pinned in sync with `__all__` by `tests/unit/test_public_api_doc.py` |
| `CONTRIBUTING.md` | Dev setup, four green gates, PR process, communication channels |
| `SECURITY.md` (root) | Vulnerability-reporting policy per SC-12 (engineering standard is at `docs/standards/SECURITY_STANDARD.md`) |
| `docs/claude/CLAUDE.md` | THIS FILE — dense lookup for future sessions. Cross-project rules in sibling [`docs/claude/claude_common.md`](claude_common.md). |

VManager-specific facts live in [`VManager/docs/CLAUDE.md`](https://codeberg.org/magogi-admin/VManager/src/branch/master/docs/CLAUDE.md). Treat the two CLAUDE.md files as a federation: this one covers `mgf.common`'s public API + internals; VManager's covers the VM-management domain + consumer-side observations.

---

*Last updated: 2026-04-28 (— sixth commit of the ergonomics reshape per the plan. Documentation phase + one small public-API addition. NEW DOC `docs/public/library_common.md` (~5 KB) — three integration shapes (app / library / test) with decision tree at top + per-section complete pattern. NEW DOC `docs/public/04_settings.md` (~4 KB) — canonical composition pattern: consumer settings carries `mgf: MgfSettings = Field(default_factory=MgfSettings)` rather than subclassing MgfSettings; the doc walks why (identity confusion + single env-prefix conflict on subclassing) + worked example end-to-end including YAML round-trip + env aliasing + schema versioning. NEW PUBLIC NAME `mgf.common.bootstrap_for_test(app_name="test-app", app_version="0.0.1", *, settings=None) -> AppContext` in NEW PRIVATE MODULE `src/mgf/common/_test_helpers.py` (~110 LOC) — thin wrapper over bootstrap() with MgfSettings.testing() defaults; pair with pytest fixtures. README Quick Start rewritten — was the five-call sequence (configure + setup_logging + setup_otel + install_excepthook + install_threading_excepthook + ad-hoc Settings example), now showcases the no-arg `with mgf.common.bootstrap() as ctx:` shape with a pointer to the production-tuning factory. The Quick Start is now ~15 lines (was ~50). examples/06_full_app/01_simple.py adopted the bootstrap() shape; the practical + advanced layers stay -shaped for now (can lift later if consumer demand surfaces). PUBLIC_API.md totals 120 → 121 (+1 bootstrap_for_test); `mgf.common.__all__` 7 → 8. Test count 694 → 702 (+8: 7 in `tests/unit/test_phase_f.py` covering default-identity / default-settings-match-testing-factory / explicit-identity-override / explicit-settings-override / returns-AppContext-directly / works-as-context-manager / __all__ pin + 1 from AP-10 picking up `bootstrap_for_test`). All four gates green: 702 tests in 3.54s, mypy --strict clean (45 source files), ruff clean, lint-imports clean. Quirks worth recording: (1) Phase F intentionally did NOT extend bootstrap_for_test with capture_logs / capture_spans flags — pytest's caplog covers stdlib log capture and the OTel SDK's InMemorySpanExporter covers spans; bundling these would be premature without consumer demand. The minimal helper signals intent at the call site (`bootstrap_for_test()` is more discoverable than `bootstrap(settings=MgfSettings.testing())`). (2) The composition doc deliberately steers consumers AWAY from subclassing MgfSettings — two reasons documented (identity confusion when MgfSettings grows new sub-models in future minors; env-prefix conflict between MGF_ and the consumer's namespace). (3) The README Quick Start trim is significant: from ~50 LOC of boilerplate to ~15 LOC of idiomatic shape. The example is preserved in git history. (4) examples/06_full_app/01_simple.py update adopts `bootstrap(app_name=..., app_version=..., settings=MgfSettings(...))` rather than no-arg `bootstrap()` because example scripts are run as `python examples/.../01_simple.py` (not `python -m`), so the auto-derivation would fall through to UnconfiguredWarning. Explicit identity is the right shape for ad-hoc examples. (5) Phase F is a great example of the plan's "small phase" model — a documentation-heavy phase with one tiny public-API addition lands in ~3-4 hours (consistent with the plan's estimate). Phase G+ are the federation-scaffolding follow-ons (importlinter generalize, lift checklist, examples → separate consumer, compat matrix, generic registry primitive, PUBLIC_API.json) — independent commits that can land as .x patches. Phase H is sibling packages, deferred until the first lift candidate emerges. Prior: 2026-04-28 (— fifth commit of the ergonomics reshape per the plan. Composable CLI: `register_cli_subcommand` lets consumers extend the `mgf-common` CLI surface AND build their own top-level CLIs (`vmanager doctor` etc.) without re-implementing exit-code translation. NEW PRIVATE MODULE `src/mgf/common/cli/_subcommand_registry.py` (~250 LOC) defines `CliSubcommand` (frozen dataclass record), `_SUBCOMMANDS: dict[str, CliSubcommand]`, `register_cli_subcommand(name, *, handler, help, parser_setup=None)` (plain-call AND decorator forms — when `handler=None`, returns a decorator that registers the wrapped function), `unregister_cli_subcommand(name)` no-op-if-unknown, `_registered_cli_subcommands()` snapshot for tests, plus `build_cli(prog="mgf-common", *, description=None, include_builtins=True)` and `dispatch(args, *, parser=None) -> int`. Pattern matches the registries (crash sinks, log handlers, redaction patterns, span processors, vault backends): `register_*` decorator + `unregister_*` no-op + `_registered_*` snapshot. EAT-YOUR-OWN-DOGFOOD REFACTOR of `_mgfcli.py`: at module import time, the four built-in subcommands (`explain` / `validate` / `doctor` / `migrate`) are registered via `register_cli_subcommand` against the same primitive consumers use. The hard-coded `match args.command` dispatch became one call to `dispatch(args, parser=parser)`. `build_parser()` retained as a thin backward-compat alias for `build_cli()` so existing tests + consumers that pinned the function name keep working. PUBLIC SURFACE: 4 new names on `mgf.common.cli` — `register_cli_subcommand`, `unregister_cli_subcommand`, `CliSubcommand`, `build_cli`. Module count 43 → 44; `mgf.common.cli.__all__` 15 → 19; PUBLIC_API.md totals 116 → 120. The handler/parser_setup type aliases are `Callable[[Any], int]` and `Callable[[Any], None]` matching the existing built-in convention; future minor version may tighten to `argparse.Namespace`/`argparse.ArgumentParser`. CONSUMER USAGE: register a subcommand via `@register_cli_subcommand("greet", help="...", parser_setup=lambda p: p.add_argument("--name"))`; build a consumer top-level CLI via `parser = build_cli(prog="myapp"); rc = dispatch(parser.parse_args(), parser=parser); sys.exit(rc)` — closed-box win, the consumer never imports argparse for routing. Test count 670 → 694 (+24: 20 in `tests/unit/test_phase_e.py` covering 8 registry-shape tests / 7 build_cli tests (default + custom prog + builtin-listing + consumer-subcmd-appears + include_builtins=False filtering + parser_setup-runs-at-build-time) / 5 dispatch-end-to-end tests (consumer-subcmd-via-main + no-subcmd-returns-unknown + dispatch-helper-direct + unknown-command + builtin-still-works regression smoke) / 1 public-surface __all__ pin / 1 CliSubcommand-frozen pin + 4 from AP-10 picking up the new public names). All four gates green: 694 tests in 3.37s, mypy --strict clean (44 source files), ruff clean (1 RUF022 auto-fix in cli/__init__.py for __all__ sort order), lint-imports clean. Quirks worth recording: (1) Test `test_include_builtins_false_filters_them_out` initially used `parser.format_help()` but the parser's *description* itself contains "explain settings, validate config files, ... migration codemods" — so the substring assertion fired false positives. Fixed by using `parser.format_usage()` which only renders the choices line. (2) Test `test_existing_builtins_still_dispatch_via_registry` initially asserted `"mgf-common doctor" in out` but the actual doctor output starts `"✓ mgf-common installed at (namespace)..."`. Fixed by checking for the simpler `"mgf-common"` substring. (3) The `register_cli_subcommand` function uses two return shapes: when `handler` is provided it returns the handler directly (call form); when omitted it returns a decorator that registers + returns the wrapped function. The mypy signature is `SubcommandHandler | Callable[[SubcommandHandler], SubcommandHandler]` — the union covers both shapes. (4) Built-in pre-registration is a side-effect of importing `_mgfcli.py`. Tests that need the registry populated (without a full CLI build) explicitly do `import mgf.common.cli._mgfcli # noqa: F401`. Most tests don't need this since pytest's collection imports test modules that transitively import the CLI surface. (5) `mgf.common.cli.build_parser` is now a one-line alias `def build_parser(): return build_cli()`. The old function isn't deprecated explicitly — it's a useful no-arg shape. (6) The `include_builtins=False` filter uses a hard-coded set `{"explain", "validate", "doctor", "migrate"}`. If a future built-in is added, that set MUST be updated alongside the registration. Phase F (library-mode + composition docs — pure documentation phase) is next per the plan. Prior: 2026-04-28 (— fourth commit of the ergonomics reshape per the plan. The closed-box-principle headliner: vault typed configs replace the `VaultSettings.config: dict[str, Any]` leak. NEW MODULE `src/mgf/common/vault/_configs.py` (~140 LOC) defines three typed configs: `EnvVaultConfig(backend_type: Literal["env"], prefix: str = "MGF_VAULT_")`, `KeyringVaultConfig(backend_type: Literal["keyring"], service: str = "mgf-common")`, `EncryptedFileVaultConfig(backend_type: Literal["file"], path: Path, passphrase: str)`. All inherit from `_BaseVaultConfig(BaseModel)` with `model_config = ConfigDict(extra="forbid", frozen=True)` — typo'd fields fail loudly + mutation raises after construction. Plus `BuiltinVaultBackendConfig = Annotated[Union[3 configs], Field(discriminator="backend_type")]` — the discriminated union alias for typed-only built-in shapes. The three typed configs + the union alias are added to `mgf.common.vault.__all__` (6 → 10). Built-in factories in `mgf/common/vault/__init__.py` updated to dispatch by `isinstance(config, ...VaultConfig)`: typed shape passes through silently; legacy dict shape still works but emits `MgfDeprecationWarning` via `_warn_legacy_dict(backend_name)` helper that uses a `_BUILTIN_TYPED_CONFIG_NAMES` mapping (`"env" → "EnvVaultConfig"`, `"keyring" → "KeyringVaultConfig"`, `"file" → "EncryptedFileVaultConfig"`) for the actionable hint in the warning. `VaultSettings.config` widened from `dict[str, Any]` to `Any = Field(default_factory=dict)` — the field annotation is loose because pydantic's discriminator can't open-extend the consumer-registered backend set; the runtime type is enforced by the factories' isinstance dispatch. Default still `{}` for full backward compat. Custom backends (registered with non-built-in names) DO NOT trigger the deprecation warning — dict is the supported shape for them until grows config-model registration on the registry. PUBLIC_API.md row for VaultSettings refreshed to mention typed configs + the dict fallback for custom backends. PUBLIC_API.md totals 112 → 116 (+4 typed configs + union alias). pyproject.toml: extended ruff per-file-ignores for tests/ with S105 (hardcoded "passwords") and S106 (hardcoded password kwargs) — the vault test suite legitimately uses fake passphrases as fixtures; the existing S105/S106 noqa comments in tests/unit/test_phase5_vault.py were removed (now redundant; ruff RUF100 caught them). Test count 643 → 670 (+27: 23 in `tests/unit/test_phase_d.py` covering 3 config models × 5 typed-shape-pin tests each (default + custom + extra-forbid + frozen + Literal-discriminator-pin) + factory-typed-vs-dict dispatch tests + warning-on-dict tests + VaultSettings end-to-end + custom-backend-no-warning + 4 from AP-10 picking up the new public names). All four gates green: 670 tests in 4.16s, mypy --strict clean (43 source files), ruff clean (after auto-fixing 3 RUF100 unused-noqa hits in test_phase5_vault), lint-imports clean. Quirks worth recording: (1) The factory-level deprecation message originally rendered `f"{backend_name.capitalize()}VaultConfig"` which produced `"FileVaultConfig"` for the file backend — wrong (the actual class is `EncryptedFileVaultConfig`). Fixed by replacing the format with a `_BUILTIN_TYPED_CONFIG_NAMES` dict lookup. (2) `EnvVault.prefix` is private (`_prefix`); the test_phase_d initial draft asserted `v.prefix == "X_"` and got AttributeError — corrected to `v._prefix`. The vault implementations don't expose getters for their config fields; the typed-config classes are the public introspection path. (3) The discriminated-union alias uses `EnvVaultConfig | KeyringVaultConfig | EncryptedFileVaultConfig` (PEP 604 syntax) inside `Annotated[..., Field(discriminator=...)]` — not the older `Union[...]` form. Ruff UP007 enforces. (4) `model_config = ConfigDict(extra="forbid", frozen=True)` on a base class is inherited by subclasses (verified — the EnvVaultConfig.frozen test passes). The base `_BaseVaultConfig` is private (underscore-prefixed); consumers subclass the concrete typed configs if they need extension, not the base. (5) `VaultSettings.config: Any` looks weak for typed-only callers but the closed-box win is the typed CONFIG class, not the field annotation — IDEs see the typed object once it's constructed, and the factory dispatch ensures correctness. The plan's "discriminated union" terminology was aspirational; the actual shape is "typed config classes for built-ins; dict fallback for custom backends; future registry will close the gap". Phase E (composable CLI: `register_cli_subcommand`) is next per the plan. Prior: 2026-04-28 (— third commit of the ergonomics reshape per the plan. Three sub-features in one commit (per the plan's sequencing). C.1 — `MgfSettings.dev()` / `.production()` / `.testing()` intent-driven factory classmethods. Each is a thin wrapper around the constructor with sensible defaults: dev() is `cls(preset="dev", **kwargs)` (matches the dev preset's static overrides); production() forces `preset="explicit"` (deterministic — no auto-detection) plus `LoggingSettings(format="json")` + `OtelSettings(sample_rate=0.05)` if not user-overridden; testing() uses explicit preset with `LoggingSettings(level="WARNING")` + `OtelSettings(enabled=False)` to cut test-suite noise. User-supplied kwargs always win over factory defaults via `kwargs.setdefault("preset", ...)` + `if "logging" not in kwargs:` guards. C.3 — composable presets: `MgfSettings.preset` widened from `PresetName` to `PresetName | list[PresetName]`. List presets merge left-to-right; later entries win on field conflicts. The single-string path is preserved for full backward compat. `_resolve_preset_overrides` was split into `_resolve_single_preset` (the static-or-runtime lookup for one name) + `_resolve_preset_overrides` (handles both single and list, dispatches accordingly). The `cli/_provenance.py::field_provenance` shim renders list presets as `"a+b+c"` for the human-readable provenance detail (was just the bare preset name when single). C.2 — pyproject.toml source: NEW PRIVATE MODULE `mgf.common.config._pyproject_source.py` defines `PyprojectSource(PydanticBaseSettingsSource)` that walks upward from `Path.cwd()` to find the nearest pyproject.toml + reads `[tool.mgf]`. Wired into `MgfSettings` via `settings_customise_sources` classmethod with precedence init > env > pyproject > dotenv > file_secrets > defaults (matches the standard pydantic-settings precedence chain extended at slot 3). `MGF_DISABLE_PYPROJECT=1` is the kill switch — the source returns `{}` immediately without I/O when set. Malformed TOML or missing `[tool.mgf]` section silently contributes nothing (no crash on broken pyproject — the diagnostic CLI's `validate` is the right place to surface TOML errors). Module count 41 → 42; PUBLIC_API.md totals unchanged at 112 (factories are classmethods on MgfSettings — not __all__ entries; pyproject source is private). Test count 618 → 643 (+25 in `tests/unit/test_phase_c.py`: 8 factory tests covering dev/production/testing × default + override + degenerate-preset-arg + 7 composable-preset tests covering single/list/order/helper-direct/explicit-field-still-wins/provenance-rendering + 10 pyproject-source tests covering empty-section/top-level/nested-section/explicit-wins/env-wins/walks-up/kill-switch/malformed-silent/no-pyproject-silent/non-dict-section). All four gates green: 643 tests in 3.32s, mypy --strict clean (42 source files), ruff clean, lint-imports clean. Quirks worth recording: (1) The factories use `kwargs.setdefault("preset", "dev")` so an explicit `preset=` kwarg wins (degenerate but valid: `MgfSettings.dev(preset="systemd")`). For production() and testing(), the `if "logging" not in kwargs:` / `if "otel" not in kwargs:` guard pattern means partial-override scenarios (only one sub-model passed) get the OTHER sub-model's factory default — the natural ergonomic. (2) The provenance shim's mypy fix: `preset_label: str = (settings.preset if isinstance(...) else "+".join(...))` — explicit annotation needed because mypy infers the LHS as `Literal[...]` from the first branch otherwise, and the `"+".join(...)` second-branch returns `str` which doesn't fit. (3) The pyproject source's `MGF_DISABLE_PYPROJECT=1` kill switch is read at construction time of `PyprojectSource`, not lazily — moving it to lazy would let test ordering bleed across boundaries. (4) Test isolation for the pyproject feature: `monkeypatch.chdir(tmp_path)` + write a fake pyproject.toml in tmp_path. The walk-up logic finds tmp_path's pyproject before any project root above it (because tmp_path has no parent with [tool.mgf] under /tmp). (5) The non-test `_clean_env` autouse fixture in test_phase_c wipes MGF_* + JOURNAL_STREAM + OTEL_EXPORTER_OTLP_ENDPOINT so per-test assertions are deterministic regardless of host shell or earlier-test leftover state. (6) Composable presets via env-var: `MGF_PRESET='["docker","auto"]'` works because pydantic-settings auto-parses JSON for complex/union types; pin tests in test_phase_c verify the dict-shape construction path explicitly to keep the contract grounded. Phase D (vault discriminated union) is next per the plan — the closed-box-principle headliner. Prior: 2026-04-28 (— second commit of the ergonomics reshape per the plan. Identity auto-derivation: `bootstrap(app_name, app_version)` are now optional. When omitted, identity derives from `sys.modules["__main__"].__spec__.parent` via `importlib.metadata.distribution(parent)` — works for packaged apps run via `python -m <pkg>` and console-script entries that preserve `__spec__`. Conservative — only fires when there's a high-confidence signal (a `__spec__` whose `parent` resolves to an installed dist); ad-hoc `python script.py` invocations and test-runner contexts return `(None, None)`. When derivation fails AND args were None, `bootstrap()` emits `UnconfiguredWarning` naming the explicit path, then falls back to placeholder identity. NEW HELPER `mgf.common._identity._derive_identity_from_metadata(main_module=None)` — private; the test-only `main_module` parameter lets each branch be exercised deterministically without sys.modules monkeypatching. NEVER raises (PackageNotFoundError + arbitrary exceptions both fall through to `(None, None)`). Cross-cutting rule §2 in this file rewritten to describe the bootstrap-first identity flow + the new derivation. PUBLIC_API.md row for `bootstrap()` signature updated; total public-name count unchanged (no new public names — the helper is private). Test count 604 → 618 (+14 in `tests/unit/test_phase_b.py`: 8 helper-branch tests covering no-main / no-spec / no-parent / missing-parent-attr / unknown-dist / known-dist + 2 swallows-exception cases + 6 bootstrap-integration tests for explicit-args-skip-derivation / no-args-uses-derivation / partial-args-derives-only-missing / warns-when-derivation-fails / no-warning-on-success / current-context-identity-still-holds). All four gates green: 618 tests in 3.16s, mypy --strict clean (41 source files), ruff clean (one F841 fix in test_phase_b for a leftover unused fake_main from initial drafting), lint-imports clean. Quirks worth recording: (1) The dist name from `importlib.metadata.distribution(parent)` is the hyphenated PyPI name (`"mgf-common"`), NOT the importable module path (`"mgf.common"`) — the test for the happy path uses `parent="mgf-common"`. Console-script entries that hyphen-rename the package would not auto-derive correctly without that hyphen showing up in `__spec__.parent`, but `python -m mgf.common` would also fail because mgf.common doesn't define a `__main__.py`; this is consistent with conservative derivation — when in doubt, fall through. (2) The derivation is wired into `bootstrap()` ONLY, NOT into the legacy `configure()` path (which is already deprecated; not worth extending). Consumers using `configure()` MUST keep passing explicit args. (3) `pytest.WarningsRecorder` (`recwarn`) was used for the "no-warning-on-success" test instead of `pytest.warns()`'s negative form (which doesn't have a clean shape) — the test filters `recwarn.list` for `UnconfiguredWarning` instances and asserts the filtered list is empty. (4) The `if app_name is None or app_version is None:` block in bootstrap is structured so the warning emits at most once per call (not once per missing arg) — the warning fires only when at least one final value is still None after derivation attempted. (5) The helper signature uses `main_module: Any | None = None` (not a fancy Protocol) because the only attribute we read is `__spec__`, and stub-injecting via `SimpleNamespace` is the cleanest test shape. Phase C (settings factories + pyproject.toml source + composable presets) is next per the plan. Prior: 2026-04-28 (— first commit of the ergonomics reshape per `docs/V04_PLAN.md` (in-flight). Two changes bundled: (1) bootstrap() return-type cleanup — the `_BootstrapContext` private-class wrapper is gone; AppContext is itself a context manager now (`__enter__` returns self, `__exit__` calls shutdown), so `with bootstrap(...) as ctx:` and `ctx = bootstrap(...)` both yield the same object as `current_context()`, and PUBLIC_API.md no longer references the underscore-prefixed wrapper; the typing wart visible in IDEs and mypy is fixed. (2) Provenance-helper promotion — `field_provenance`, `env_vars_known_to_mgf`, `FieldSource`, `SourceKind` re-exported from `mgf.common.cli.__init__` (implementation stays in private `_provenance.py`), so consumers can build their own settings-debug UIs (admin pages, support tooling) without reaching past us into pydantic-settings. PUBLIC_API.md totals 108 → 112 (+4 from provenance promotion); `mgf.common.cli.__all__` 11 → 15. Test count 590 → 604 (+14: 10 in new `tests/unit/test_phase_a.py` + 4 from AP-10 picking up the new public names). All four gates green: 604 tests in 3.30s, mypy --strict clean (41 source files), ruff clean (one ruff fix in test_phase_a — moved `Path` import under TYPE_CHECKING per TC003), lint-imports clean. Quirks worth recording: (1) Eliminating `_BootstrapContext` is a strict ergonomic upgrade, not a breaking change — the tests that were forced to compare via `.settings is` (instead of `is`) because the wrapper-vs-inner identity differed can now use direct identity (e.g. `current_context() is ctx` works in both `with` and one-shot shapes). The lifecycle test that reached into `ctx1._ctx._shut_down` was simplified to `ctx1._shut_down`. (2) The new `test_no_private_wrapper_class_remains` pin asserts `not hasattr(_lifecycle, '_BootstrapContext')` so a future refactor reintroducing a wrapper fails loudly. (3) The provenance helpers are re-exported as the SAME OBJECTS (no copy / no shim layer); `from mgf.common.cli import field_provenance is from mgf.common.cli._provenance import field_provenance` is True (pinned by `test_field_provenance_works_via_public_path`). (4) Phase A scope was reduced from the original three-item plan: A.2 (env-nesting `__` → `_`) was DROPPED on inspection because the schema has many fields with underscores in their names (`service_namespace`, `sample_rate`, `httpx_instrumented`, `level_overrides`, `extra_keys`, `rotating_max_bytes`, etc.) — switching `env_nested_delimiter` to `_` creates ambiguity (`MGF_OTEL_SERVICE_NAMESPACE` could split as `otel.service.namespace` or `otel.service_namespace`). The friction is real but the alternative is silent breakage; revisit with a different mitigation later (custom EnvSettingsSource subclass that does longest-field-name matching is one option). Prior: 2026-04-28 (closed-box redesign Phase 7 — eighth commit of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Lands the consumer-facing migration guide + the `mgf-common migrate` codemod for safe mechanical → renames. NEW DOCUMENT `docs/MIGRATION_0.1_TO_0.3.md` — canonical "what changed and how do I upgrade?" guide; quick reference table + 5-step recipe + bootstrap-reshape walkthrough (the only substantial change; 7 sub-cases covering explicit settings / preset-driven defaults / non-context-manager use / identity-only flows / migration-period coexistence) + mechanical renames (EnvironmentError → HostEnvironmentError, make_settings_config → settings_config) + every new API (MgfSettings / Protocols / progress / vault / cli / diagnostic CLI / registries) + recommended migration sequence + troubleshooting catalogue + "what is NOT changing" section. Time-to-migrate estimate: ~30 min mechanical + ~1 hr bootstrap reshape per consumer entry point. NEW MODULE `mgf.common.cli._migrate` (private — surfaced as the `mgf-common migrate` subcommand): regex-based codemod with two rules in the →table: `EnvironmentError → HostEnvironmentError` (closes ) and `make_settings_config → settings_config`. Each Rule carries an `import_gate: re.Pattern` that gates the rule per-file — only files mentioning the corresponding `mgf.common.*` subpackage (lexically) are eligible. Word-boundary regex anchoring (`\bIDENTIFIER\b`) so substring matches don't get rewritten — `MyEnvironmentErrorSubclass` stays intact. Walks `<path>` recursively; skips `.venv` / `venv` / `.env` / `env` / `__pycache__` / `.git` / `build` / `dist` / `.tox` / `.mypy_cache` / `.pytest_cache` / `node_modules` directories; symlinks NOT followed; single-file targets supported. `--dry-run` previews; `--backup` writes `.bak` alongside; both default off. Idempotent — running twice is a no-op. Unsupported `<from>`/`<to>` returns `EXIT_CONFIG_ERROR (1)`; missing target returns `EXIT_OPERATION_ERROR (2)`. The bootstrap reshape (5-call → `bootstrap()`) is intentionally NOT in the rule set — it requires AST-level transformation + per-case judgment about which sub-fields the consumer wants in their MgfSettings; the migration guide §1 walks it with copy-pastable examples per case. PUBLIC_API.md row for `mgf-common` updated from "three subcommands" to "four subcommands"; AP-10 pin tests unaffected (CLI command is the public surface, not the underscore-prefixed implementation modules). docs/CLAUDE.md: documentation table extended with a row for `MIGRATION_0.1_TO_0.3.md`. Test count 570 → 590 (+20 in `tests/unit/test_phase7_migrate.py`: 3 EnvironmentError-rename / 2 make_settings_config-rename / 2 per-rule-gating / 1 bootstrap-reshape-NOT-codemodded / 3 dry-run+backup / 1 idempotency / 5 file-walk + skip-dirs + single-file + non-Python / 2 error-paths / 1 dispatch-includes-migrate). All four gates green: 590 tests in 3.44s, mypy --strict clean (41 source files), ruff clean, lint-imports clean. Updated stale Phase 6b dispatch test from `test_build_parser_lists_three_subcommands` to `test_build_parser_lists_phase6b_subcommands` with subset-check semantics so future subcommands don't break the pin. Quirks worth recording: (1) the codemod's per-rule import_gate is critical for safety — an earlier draft used a coarse top-level "does this file mention mgf.common at all?" pre-filter, which over-applied: a file importing from `mgf.common.config` but using Python's builtin `EnvironmentError` (the `OSError` alias) had its builtin renamed. The fix: per-rule gate; the EnvironmentError rule gates on `\bmgf\.common\.exceptions\b`, the make_settings_config rule gates on `\bmgf\.common\.config\b`. Tests `test_only_config_rule_applies_when_only_config_imported` and `test_only_exceptions_rule_applies_when_only_exceptions_imported` are the regression guards. (2) Word-boundary `\bEnvironmentError\b` regex anchoring is non-negotiable — without it, `MyEnvironmentErrorSubclass` would mass-rename to `MyHostEnvironmentErrorSubclass`. Test `test_does_not_touch_substring_of_longer_identifier` is the regression guard. (3) The migration guide's "Recommended migration sequence" §4 closes with the suggestion to add `filterwarnings = ["error::mgf.common._warnings.MgfDeprecationWarning"]` to the consumer's pyproject AFTER the migration is complete — this turns any surface that creeps back in into a hard test failure, the cleanest signal that the migration is locked in. (4) The bootstrap-reshape NON-codemod is a deliberate scope cut, NOT a gap — auto-rewriting `configure() + setup_logging() + setup_otel() + install_excepthook() + install_threading_excepthook()` to `with bootstrap(...) as ctx:` requires (a) AST transformation via libcst (extra dep just for migration tooling), (b) figuring out which arguments to setup_logging/setup_otel land in MgfSettings vs which were defaults, (c) detecting the consumer's preferred bootstrap shape (with-block vs `ctx = bootstrap()`). The risk of subtle wrong transformations is higher than the value of the auto-rewrite; the migration guide §1 walks 7 sub-cases instead. (5) The codemod currently has only one migration registered (0.1→0.3); the table `_MIGRATIONS` is keyed by `(from, to)` pairs so future versions (0.3→0.4 etc.) can register their rules without restructuring the codebase. Phase 7 of the closed-box reshape is now COMPLETE; remaining is Phase 8 — release : bump `__version__` and `pyproject.toml::project.version` to `0.3.0`, move `[Unreleased]` → `[0.3.0]` in CHANGELOG, manual `twine upload` per docs/CLAUDE.md §"Build and release", tag + push to Codeberg, update VManager's pin, open FEEDBACK round 2 against the new shape. Prior: 2026-04-28 (closed-box redesign Phase 6b — seventh commit (final of split 6a/6b) of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Lands the `mgf-common` console-script with three subcommands — `explain` / `validate` / `doctor` — backed by a per-field provenance shim that Phase 6a's `__pydantic_fields_set__` mutation makes possible. NEW MODULE `mgf.common.cli._provenance` (private — surfaced as the `mgf-common` CLI command, not as importable Python): `field_provenance(settings, *, init_overrides, file_overrides) -> dict[str, dict[str, FieldSource]]` walks each field through the precedence ladder env → file → preset → init → default and reports the first hit; FieldSource is a frozen dataclass `(kind: SourceKind, detail: str)` where SourceKind is `"default" | "env" | "preset" | "init" | "file"`; `env_vars_known_to_mgf()` enumerates every MGF_* env var the schema would consult (sorted, with current value or None). Strategy: re-runs `pydantic_settings.sources.EnvSettingsSource(MgfSettings)()` to get the env-only nested dict + re-runs `mgf.common.settings._resolve_preset_overrides(settings.preset)` to get the preset's would-have-been overrides (the same table Phase 6a's validator used). NEW MODULE `mgf.common.cli._explain`: `explain(*, app_name, app_version, preset, out)` prints the active MgfSettings rendered as `[section]` headers + `field value (source)` lines + a footer enumerating every MGF_* env var with its current value. Secret-shaped fields (sentry_dsn / passphrase / http_url / syslog_address / http_sink) are redacted in BOTH the dump (value → "***REDACTED***") AND the env-var footer (env value redacted by name match) so the output is safe to paste into a bug report. Conservative allow-list approach — if a future field carries credential-shaped data without a credential-shaped name, MUST be added to `_REDACT_FIELDS` (no automatic detection). NEW MODULE `mgf.common.cli._validate`: `validate(config_path, ...)` loads YAML via `yaml.safe_load`, runs `MgfSettings.model_validate`, on success prints the `✓ Schema valid` markers + dumps the resolved active config (same shape as explain but with `(file)` provenance for file-supplied fields), on schema failure renders per-field errors on stderr and returns EXIT_CONFIG_ERROR (1), on file-read or YAML-parse failure returns EXIT_OPERATION_ERROR (2), top-level-must-be-mapping enforced. NEW MODULE `mgf.common.cli._doctor`: `doctor(out)` runs install-state + wiring health checks — package install via importlib.metadata + py.typed marker presence + each [extra] (observability/vault) installed-or-missing via importlib.util.find_spec (missing → ⚠ not failure) + state + log dir writability via `os.access(W_OK)` after `mkdir(parents=True, exist_ok=True)` + auto-detection signal warnings (JOURNAL_STREAM/OTEL_EXPORTER_OTLP_ENDPOINT) + per-registry snapshot of crash sinks/log handlers/redaction pattern groups/span processors/vault backends + deprecation-policy reminder. Three marker tiers: ✓ pass / ⚠ heads-up / ✗ failure; each ✗ contributes an actionable recommendation to the footer; exit 0 if no ✗ else EXIT_OPERATION_ERROR. Reads `mgf.common._identity._APP_NAME` directly (bypassing `app_name()`) so it doesn't emit UnconfiguredWarning — the doctor reports environment state, doesn't enforce configure() discipline. NEW MODULE `mgf.common.cli._mgfcli`: `main(argv) -> int` is the console-script entry; argparse-only (no click/typer dep — CLI framework choice is OUT of scope per SP-02 in SCOPE.md); subcommand dispatch via `match` statement on parsed command name; `subparsers.required=False` so `mgf-common` (no subcommand) prints help to stderr + returns EXIT_UNKNOWN. pyproject.toml: added `[project.scripts] mgf-common = "mgf.common.cli._mgfcli:main"`. PUBLIC_API.md: 0 new names in `__all__` — the implementation modules are private; the CLI command itself is the public surface. New row added under the Phase 5 cli section documenting `mgf-common` as a public CLI command with the three subcommands. Test count 537 → 570 (+33 in `tests/unit/test_phase6b_diagnostic_cli.py`: 3 dispatch / 9 explain / 7 validate / 8 doctor / 6 provenance-shim direct unit). All four gates green: 570 tests in 3.33s, mypy --strict clean (40 source files), ruff clean, lint-imports clean. Quirks worth recording: (1) `sub_model.model_fields` raises PydanticDeprecatedSince211 in pydantic v2.11+ — use `type(sub_model).model_fields` (class-level access). pyproject's `filterwarnings=["error"]` policy caught this in 17 tests at first run; provenance shim + explain renderer were updated before commit. (2) `EnvSettingsSource(MgfSettings)()` returns the nested-delim-expanded dict (`{"logging": {"level": "DEBUG"}}` for `MGF_LOGGING__LEVEL=DEBUG`); the shim walks per sub-model. (3) The provenance shim's source ordering reports init/env/file/preset/default — preset appears AFTER env+file even though Phase 6a's validator runs `mode='after'`; this is because the validator only fills in fields that no other source supplied, so in attribution terms preset IS lower priority than env/file even though it runs after them in pydantic's lifecycle. (4) Doctor's `_check_extras` uses `importlib.util.find_spec(name)` which doesn't import the module (avoids triggering side-effects from optional deps); a missing extra is ⚠ not ✗ because the library functions perfectly without optional extras (they only unlock additional surface). (5) The argparse subparser action is found via `isinstance(a, argparse._SubParsersAction)` not by `hasattr(a, "choices")` — the `--help` action ALSO has a `choices` attribute (set to None) which is why the first attempt at `test_build_parser_lists_three_subcommands` failed. (6) `mgf-common` CLI command is registered as a `[project.scripts]` entry; the implementation modules (`_mgfcli.py` etc) are private (underscore-prefixed) — consumers depend on the CLI command, not the import path. Programmatic access to explain/validate/doctor functions can be promoted to the public API on consumer demand per SP-04 in SCOPE.md. Phase 6 of the closed-box reshape is now COMPLETE; remaining phases per CLOSED_BOX_REDESIGN.md §9 are Phase 7 (codemod + migration docs — `mgf-common migrate 0.1 0.2 <path>`) and Phase 8 (release — bump version, CHANGELOG, twine upload, tag, VManager pin update). Prior: 2026-04-28 (closed-box redesign Phase 6a — sixth commit (split 6a/6b) of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Wires the `MgfSettings.preset` semantics that Phase 1 shipped as a typed seam, and lifts §8 of `docs/CLOSED_BOX_REDESIGN.md` into a freestanding `docs/standards/SCOPE.md`. PRESET WIRING: a `model_validator(mode="after")` on MgfSettings walks each preset's `{sub_model: {field: value}}` map and applies values to fields the user did NOT explicitly set. Five presets implemented: `explicit` (no-op — every field stays at its declared default; the new-project default), `dev` (logging.level=DEBUG, format=text, journald=False; otel.enabled=False), `systemd` (logging.format=json, journald=True), `docker` (logging.format=json, journald=False, file=None — container runtime captures stdout), `auto` (legacy detection — TTY → format=text/json, JOURNAL_STREAM → journald, OTEL_EXPORTER_OTLP_ENDPOINT → otel.enabled). Override semantics: explicit kwargs win, env-supplied fields win (because pydantic-settings populates `model_fields_set` BEFORE the validator runs), preset fills in everything not explicitly set. Implementation detail: preset-applied fields are added to the sub-model's `__pydantic_fields_set__` so future introspection (Phase 6b's `mgf-common explain`) can attribute the value to the preset rather than to "default". The `auto` preset's detection happens at MgfSettings construction time, NOT at module import — confirmed by a regression test that mutates env between two consecutive constructions. SCOPE.md: 6 new MUSTs (SP-01 IN-scope list / SP-02 OUT-of-scope with reasoning / SP-03 per-PR scope-justification / SP-04 Adjacent items require demand signal / SP-05 OUT entries cite alternative / SP-06 revisit every MAJOR). Standards version v2.0 → v2.1 (MINOR — SP-* are library-author / process MUSTs, not consumer-code; existing consumer code stays compliant). docs/STANDARDS_COMPLIANCE.md gets the SP-* row block (5 ✅, 1 🟠 SP-06 not yet exercised pre-1.0, 1 ➖ SP-03 process-only). docs/standards/README.md status banner extended with v2.1 entry; folder map gets SCOPE.md row; MUSTs index extended with SP-01..06 (count 103 → 109 across 8 docs); new "What's new in v2.1" section. Test count 506 → 537 (+31 in `tests/unit/test_phase6a_presets.py`: 3 explicit-preset tests / 4 dev / 3 systemd / 3 docker / 5 auto (no-tty/with-tty/journal-stream/otel-endpoint/per-construction-not-cached) / 3 explicit-override / 2 env-override / 2 provenance / 1 idempotency / 4 _resolve_preset_overrides direct unit / 1 bootstrap-default-preset integration). All four gates green: 537 tests in 3.08s, mypy --strict clean (35 source files), ruff clean, lint-imports clean (1 contract). Quirks worth recording: (1) the validator uses `__pydantic_fields_set__.add(field)` to mark preset-applied fields as set — `model_fields_set` is read-only public API; the underlying mutable set is the dunder name. This is the documented pydantic v2 escape hatch for exactly this use case (validators that fill in defaults but want introspection to show the source); (2) `MgfSettings.model_validate(s.model_dump())` round-trips correctly because the dumped dict has every field present (so the preset's overrides are skipped — already in `model_fields_set` after the round-trip) — pinned by `TestIdempotency.test_revalidate_is_noop`; (3) the bootstrap-default-preset test uses `current_context().settings is ctx.settings` instead of `current_context() is ctx` because bootstrap returns a `_BootstrapContext` wrapper (dual-mode `with` / `=` shape from Phase 1) while `current_context()` returns the underlying AppContext — the wrapper delegates via `__getattr__` so `ctx.settings` resolves correctly; (4) the `auto` preset's `_resolve_preset_overrides` is intentionally NOT cached at module import — a regression test (`test_auto_evaluation_is_per_construction_not_module_load`) mutates env between two MgfSettings(preset="auto") constructions and asserts the second sees the new value; this preserves the behaviour of "what env said when bootstrap ran" rather than "what env said when the library was first imported"; (5) the `_clean_env` autouse fixture in test_phase6a_presets.py wipes JOURNAL_STREAM / OTEL_EXPORTER_OTLP_ENDPOINT / MGF_* from the host env so per-test assertions are deterministic — the host shell's prior env state would otherwise contaminate the auto-preset detection. Phase 6b (next commit) lands the diagnostic CLI (`mgf-common explain / validate / doctor`) — the field-source provenance shim that Phase 6a's `model_fields_set` mutation makes possible. Prior: 2026-04-25 (closed-box redesign Phase 5 — fifth commit of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Lands the credential vault module (closes FEEDBACK §3.2) with three built-in backends + a registry for custom stores; adds `mgf.common.cli` with stable exit-code constants + an exception → exit-code translator. NEW MODULE `mgf.common.vault` — three concrete `VaultProtocol` implementations: `EnvVault` (stdlib-only, env-var backed with default prefix `MGF_VAULT_`, key uppercased to `<prefix><KEY>`, audit-logs every op via `mgf.common.observability.get_logger` with `audit=True` per rule SC-17 — KEY logged, VALUE not), `KeyringVault` (system credential store via the `keyring` library — libsecret/Keychain/CredMgr; default service `"mgf-common"`, consumer apps SHOULD override per SC-12; tracks key index in a per-service `__mgf_index__` entry so `list_keys()` is portable across backends — Windows Credential Manager has no native enum), `EncryptedFileVault` (Fernet AES-128 + HMAC-SHA256, KDF PBKDF2-HMAC-SHA256 600 000 iterations OWASP 2023 baseline per SC-05; file header `MGFV` magic + 1-byte version + 16-byte per-vault salt + Fernet token; cached Fernet so PBKDF2 cost paid once per instance; `_kdf_iterations=1000` test override for fast suite; atomic write per CF-04, `0o600` at-rest permissions). Registry pattern matches Phase 3: `register_vault_backend("name")` decorator + `unregister_vault_backend(name)` no-op-if-unknown + `_registered_vault_backends()` snapshot for tests; built-in factories `"env"` / `"keyring"` / `"file"` are pre-registered at module import. NEW MODULE `mgf.common.cli` — eight stable exit-code constants (numeric pin per AP-04: SUCCESS=0, CONFIG=1, OPERATION=2, GUEST=3, ENVIRONMENT=4, PROVIDER=5, VAULT=6, UNKNOWN=10) + three translator entry points: `run_and_translate(main_fn, argv, *, exit_code_map=None, crash_source="cli") -> int` (call-style, NEVER raises, MRO walk for category mapping, `SystemExit(int)` passes through, `SystemExit(str)` becomes UNKNOWN, bare KeyboardInterrupt returns 130 = 128+SIGINT per Unix convention, untyped exceptions ship a crash report through `report_now` before returning UNKNOWN), `translate_exceptions(*, exit_code_map=None, crash_source="cli")` (decorator form, preserves function metadata via @functools.wraps), `main_entry(main_fn, *, exit_code_map=None, crash_source="cli")` (one-liner that reads sys.argv + sys.exits). NEW SETTINGS: `mgf.common.settings.VaultSettings` sub-model on MgfSettings with `backend: str = "env"` + `config: dict[str, Any]` (Phase 5 keeps `config` an untyped dict for forward compat; ships discriminated union once consumer-facing shape settles). pyproject.toml: added `[vault]` extra (`keyring>=24`, `cryptography>=42`); `KeyringVault` and `EncryptedFileVault` lazy-import the optional libraries via `_load_keyring()` / `_load_fernet()` / `_load_pbkdf2()` helpers — constructing either WITHOUT the extra raises `VaultError` chained from the original ImportError with the install hint `pip install mgf-common[vault]` per rule EH-09. tests/unit/test_public_api_doc.py: module discovery extended from 8 to 10 modules (added vault + cli). PUBLIC_API.md: 18 new public names; total 90 → 108 (`mgf.common.settings` 8 → 9 +VaultSettings, NEW `mgf.common.vault` 6, NEW `mgf.common.cli` 11). Test count 406 → 506 (+100: 18 new AP-10 pins + 52 in `test_phase5_vault.py` covering EnvVault round-trip/prefix/audit-log/protocol-conformance, KeyringVault driven through an in-memory fake (substituted into sys.modules['keyring'] + 'keyring.errors' so test runs without touching the real keyring daemon) for service/index-tracking/missing-extra-raises, EncryptedFileVault round-trip-after-reopen/wrong-passphrase-raises/corrupted-magic/truncated-file/unsupported-version/salt-persists-across-reopens/atomic-write-no-temp-leftover/0o600-permissions, registry pre-registration + register-double-raises + factory-arg-validation, VaultSettings extra-forbid + nested-via-MgfSettings; + 30 in `test_phase5_cli.py` covering exit-code numeric pin (each constant individually asserted so any bump fails LOUDLY), parametrised category mapping for 11 typed exceptions, MRO-based override resolution, SystemExit(int)/SystemExit(str)/KeyboardInterrupt special cases, untyped exception → mocked report_now via monkeypatch (captured args list), translate_exceptions decorator metadata preservation). All four gates green: 506 tests in <3s, mypy --strict clean (vault + cli modules added), ruff clean, lint-imports clean. Closed-box wins worth recording: (1) consumers DO NOT import keyring or cryptography directly — they pass a VaultProtocol everywhere; switching backends is a single line at bootstrap (the `[vault]` extra hides the heavy deps from base installs); (2) `KeyringVault` solves the `list_keys()` portability problem with a per-service `__mgf_index__` entry it maintains itself — the only portable shape across the three OS keyring backends; (3) `EncryptedFileVault._build_fernet` returns the cached Fernet on subsequent calls — important because PBKDF2 with 600k iterations is the expensive bit (~120ms per derivation on this dev host) and re-deriving on every read would make the vault unusable; (4) test_keyring tests pass `monkeypatch.setitem(sys.modules, "keyring", fake)` BEFORE constructing KeyringVault — the lazy import inside `__init__` resolves through sys.modules so the fake is what gets bound; (5) the CLI translator's `_default_exit_code_map()` is built lazily on first call so importing `mgf.common.cli` does NOT eagerly import every exception class; (6) `run_and_translate` lazy-imports `report_now` inside the catch-all branch (rather than at module load) so a cli-only consumer doesn't pay for the observability path until it hits an untyped exception. Quirks worth recording: (1) `EncryptedFileVault` rejects empty passphrase at construction (raises VaultError) — the assertion guards against the common bug of forgetting to prompt; (2) the file format has a 1-byte version field at offset 4 — bumping to a new crypto profile is a header-version-bump with both versions handled in `_load_store`; (3) `KeyringVault.delete()` swallows `keyring.errors.PasswordDeleteError` (the no-such-key signal from libsecret) and treats it as a no-op + sets `existed=False` in the audit log; other keyring exceptions propagate as VaultError; (4) the `register_vault_backend` registry will raise ValueError on double-registration of the same name — built-in `env`/`keyring`/`file` are pre-registered at module import, so a consumer that wants to REPLACE one MUST call `unregister_vault_backend("env")` first; (5) test_custom_prefix in test_phase5_vault needs to clear BOTH `MYAPP_*` AND `MGF_VAULT_*` env vars because earlier tests in the same class leak `MGF_VAULT_*` entries; (6) `EXIT_OPERATION_ERROR=2` catches CancellationError + BootstrapError by default (both are OperationError subclasses); CLIs wanting a distinct cancellation exit code MUST pass `exit_code_map={CancellationError: 130}` (or whichever value matches their convention) — the library deliberately doesn't make this default to keep the closed-box surface predictable. Prior: 2026-04-27 (closed-box redesign Phase 4 — fourth commit of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Lifts cancellation + progress primitives from VManager into `mgf.common.progress`, adds the async sibling `mgf.common.progress.asyncio` (sync-first / async-adapter rule §14.1), ships `mgf.common.typing` with the canonical Protocol definitions, and adds `CancellationError(OperationError)` typed exception. NEW MODULE `mgf.common.typing` — 4 `runtime_checkable` Protocols: `CancellationProtocol` (cancel + is_cancelled), `ReporterProtocol` (report + is_cancelled), `LogSinkProtocol` (append_info + append_error — for future GUI helpers), `VaultProtocol` (get / set / delete / list_keys + backend property — concrete impls land in mgf.common.vault Phase 5). NEW MODULE `mgf.common.progress` — `CancellationToken` (sync, threading.Event-backed, cancel/is_cancelled/wait/raise_if_cancelled, ~50ns is_cancelled cost), `NullReporter` (no-op concrete impl), `RecordingReporter` (test double with .calls list + reset()), `ProgressReporter` ergonomic alias for `mgf.common.typing.ReporterProtocol` (same Protocol object, two import paths). NEW MODULE `mgf.common.progress.asyncio` — `AsyncCancellationToken` (asyncio.Event-backed, same surface plus `await wait(timeout)`, bound to a single event loop). NEW EXCEPTION `mgf.common.exceptions.CancellationError(OperationError)` — raised by `raise_if_cancelled()`; default message "operation cancelled by user"; existing `except OperationError:` catches without code change (rule EH-04). pyproject.toml: added `pytest-asyncio>=0.23` to [dev] extra (per rule TS-12); added pytest config `asyncio_mode = "strict"` (per-test marker required) + `asyncio_default_fixture_loop_scope = "function"` (per-test event loop, per TESTING.md §7.1). tests/unit/test_public_api_doc.py: module discovery extended from 4 to 8 modules (added settings / typing / progress / progress.asyncio) — fixed a latent gap where MgfSettings public surface wasn't being pin-tested. PUBLIC_API.md: 10 new public names; total 80 → 90 (`mgf.common.exceptions` 17 → 18, NEW `mgf.common.typing` 4, NEW `mgf.common.progress` 4, NEW `mgf.common.progress.asyncio` 1). Test count 346 → 406 (+60: 18 new AP-10 pins + 42 behavioural in test_phase4_progress.py covering sync CancellationToken, NullReporter, RecordingReporter, async AsyncCancellationToken with pytest.mark.asyncio fixture, CancellationError shape, Protocol runtime_checkable smoke, integration tests for cross-thread sync cancellation + cross-task async cancellation). All four gates green: 406 tests in 2.99s, mypy --strict clean on 27 source files (was 24), ruff clean (with the v2.0 rules), lint-imports clean. Quirks worth recording: (1) `_PROTOCOL_CHECK: type[CancellationProtocol] = AsyncCancellationToken` at the bottom of `progress/asyncio.py` is a static check at import time — if a future refactor accidentally drops a method, this fails at import (cheap CI signal); (2) the cross-thread cancellation integration test needs `time.sleep(0.001)` in the inner loop because otherwise 100 iterations complete in <10ms and outrun the canceller thread's 50ms wakeup; (3) `ProgressReporter` and `ReporterProtocol` are LITERALLY the same Protocol object (`ProgressReporter is ReporterProtocol`) — re-exported through `mgf.common.progress` for ergonomics while the canonical definition stays in `mgf.common.typing` (single source of truth + two ergonomic import paths); (4) ruff `ASYNC109` flags `timeout=` parameter on async functions (preferring `asyncio.timeout`) — `AsyncCancellationToken.wait(timeout=...)` carries a documented `# noqa: ASYNC109 — timeout matches sync sibling` because the sync/async surface parity matters more than the rule's preference. Prior: 2026-04-27 (closed-box redesign Phase 3 — third commit of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Ships four registry patterns for pluggable extensibility: `register_crash_sink`, `register_redaction_pattern_group` (Phase 3 wiring of the `enabled_pattern_groups` field shipped in Phase 1 — fully end-to-end), `register_log_handler` (registry shipped; full wiring in a later phase), `register_span_processor` (same — registry only). Pattern is uniform across all four: `@register_*("name")` decorator, `unregister_*("name")` no-op-if-unknown, `_registered_*()` snapshot for tests + diagnostic CLI, re-registering same name raises ValueError. Type aliases exposed for consumer signatures: `CrashSink` (`Callable[[Path, dict[str, Any]], None]`), `LogHandlerFactory` (`Callable[[MgfSettings], logging.Handler]`), `PatternGroupFactory` (`Callable[[], tuple[re.Pattern[str], ...]]`), `SpanProcessorFactory` (`Callable[[MgfSettings], SpanProcessor]`). CRASH-SINK refactor: `crash_reporter._ship_remote` switched from hardcoded if/elif chain (sentry/otlp/http) to registry lookup; helpers `_ship_sentry` and `_ship_otlp` are PRE-REGISTERED via `@register_crash_sink("sentry")` / `("otlp")` decorators (backward compat preserved). Multi-sink fan-out: when MgfSettings.crash.sinks contains multiple names, each fires independently with try/except wrapping (rule EH-10 best-effort); failure of one doesn't affect others. Legacy `<APP>_CRASH_SINK` env-var dispatch preserved as fallback when no MgfSettings override active (settings.crash.sinks == ["local"] = "use legacy path"). REDACTION wiring end-to-end: `_lifecycle._bootstrap_inner` now resolves `settings.redaction.enabled_pattern_groups` via `_resolve_pattern_groups`, filters out "builtin" (already always-on via `_scrub_string`), passes resolved tuple as `Redactor.extra_value_patterns`. Closes the field-level seam from Phase 1. Pre-registered "builtin" group with the existing 8 patterns (Bearer / JWT / PEM / OpenAI / GitHub / AWS / Slack / Google API keys). PUBLIC_API.md: 12 new public names; total 68 → 80. Test count 315 → 346 (+31: 12 new AP-10 pin assertions + 19 behavioural in `test_phase3_registries.py`). All four gates green: 346 tests in 2.70s, mypy --strict clean on 24 source files, ruff clean, lint-imports clean. Quirks worth recording: (1) the redaction registry's "builtin" group is pre-registered with the existing patterns AND `_scrub_string` still iterates `_BUILTIN_VALUE_PATTERNS` directly — the bootstrap wiring filters "builtin" out of the resolved-extras list to avoid double-firing, but direct `Redactor()` construction bypasses bootstrap and gets builtin via the always-on path; (2) the env-var dispatch (`<APP>_CRASH_SINK`) is preserved by `_resolve_active_sinks()` returning `[]` when settings.crash.sinks is the default `["local"]` (signal: "use legacy path"); explicit override via MgfSettings is the new path; (3) caplog doesn't see records after `setup_logging(force=True)` replaces root handlers — tests checking warning emissions use `capsys` to read stderr directly; (4) the OTel span processor registry has its own TYPE_CHECKING imports for `SpanProcessor` and `MgfSettings` — that's the second TYPE_CHECKING block in `otel_setup.py`; Python supports multiple, no conflict. Prior: 2026-04-26 (closed-box redesign Phase 2 — second commit of the reshape. Wraps the closed-box leaks identified in §3 of the design doc (CB-04 pydantic, CB-08 logging, CB-12/13 OTel), renames + deprecates surfaces with the deprecation cycle starting now, fixes two known limitations (CB-06 to_yaml_dict shallow recursion, the make_settings_config workaround). NEW: `mgf.common._warnings.MgfDeprecationWarning` (subclass of DeprecationWarning) — emitted by every multi-call shim; pyproject.toml filterwarnings exempts it via `"default::mgf.common._warnings.MgfDeprecationWarning"` so consumer test suites with `filterwarnings=["error"]` (rule TS-04) see the warnings without them blocking the build. NEW: `mgf.common.config` re-exports — `BaseModel`, `ConfigDict`, `Field`, `AliasChoices`, `field_validator`, `model_validator`, `ValidationError` (closes CB-04 — consumers no longer `import pydantic` for normal subclass authoring; closed-box smoke pinned by `tests/unit/test_phase2_surface.py::TestClosedBoxSmoke::test_complete_settings_no_pydantic_import_in_consumer_shape`). NEW: `BaseAppSettings.__init_subclass__` accepting every SettingsConfigDict key as subclass kwarg (env_prefix, env_nested_delimiter, env_file, env_file_encoding, extra, case_sensitive, validate_default, arbitrary_types_allowed) — closes ; the `model_config = settings_config(env_prefix="X_")` pattern keeps working through the cycle (the kwarg path respects an explicit body declaration). NEW: `settings_config(...)` (canonical) + `make_settings_config(...)` (deprecated wrapper). NEW: `to_yaml_dict()` now delegates to `model_dump(mode="json")` — recurses into nested sub-models (closes CB-06). NEW: `mgf.common.exceptions.HostEnvironmentError` + `EnvironmentError` deprecated alias via `_DeprecatedEnvironmentErrorMeta` metaclass (closes ); `__call__` emits warning on instantiation; `__subclasscheck__` makes `issubclass(HostEnvironmentError, EnvironmentError)` return True so legacy `except EnvironmentError:` clauses keep catching new instances. NEW: `mgf.common.observability` adds — `get_logger(name=None) -> logging.Logger` (closes CB-08); 5 OTel re-exports (`Span`, `SpanContext`, `SpanKind`, `SpanStatus` (renamed from OTel `Status`), `StatusCode`) with TYPE_CHECKING dual-path so default install (no OTel) gets `Any` sentinels; 4 span helpers (`get_current_span() -> Span | None`, `set_attribute(key, value)`, `add_event(name, attributes, timestamp)`, `set_status(status, description)`) — all no-op-safe when no span active or OTel absent (closes CB-12, CB-13). NEW: each multi-call API refactored to a private `_wire_*` implementation + a public deprecated shim that emits MgfDeprecationWarning and delegates: `_wire_logging` / `setup_logging`, `_wire_otel` / `setup_otel`, `_wire_excepthook` / `install_excepthook`, `_wire_threading_excepthook` / `install_threading_excepthook`. `_lifecycle._bootstrap_inner` calls the private wirings to avoid noise. `make_settings_config` calls `settings_config` directly (no warning recursion). PUBLIC_API.md: 19 new public names; total 49 → 68 (`mgf.common.exceptions` 16 → 17 +HostEnvironmentError; `mgf.common.config` 8 → 16 +settings_config + 7 pydantic re-exports; `mgf.common.observability` 9 → 19 +get_logger +4 span helpers +5 OTel re-exports). 30 new behavioural tests in `tests/unit/test_phase2_surface.py`: 5 pydantic re-export verifications / 4 subclass-kwarg cases (env_prefix / env_nested_delimiter / legacy still-works / explicit-body wins) / 2 to_yaml_dict recursion / 3 HostEnvironmentError (constructable / deprecation warning / isinstance both directions) / 2 settings_config (works + make_settings_config emits) / 2 get_logger / 4 OTel-helpers-no-op / 3 OTel-types-importable / 4 deprecation shims emit / 1 closed-box smoke (a complete Settings using ZERO `import pydantic`). Test count 266 → 315 (+49: 19 new AP-10 pins + 30 behavioural). Files added: `_warnings.py` (39 lines), `config/_reexports.py` (60 lines), `observability/_otel_reexports.py` (74 lines), `observability/_helpers.py` (159 lines), `tests/unit/test_phase2_surface.py` (440 lines, 30 tests). Files modified: `config/_settings.py` (refactored — settings_config canonical, make_settings_config deprecated, BaseAppSettings.__init_subclass__ added, to_yaml_dict now `model_dump(mode="json")`); `config/__init__.py` (re-exports); `observability/__init__.py` (re-exports); `observability/{logging_setup,otel_setup,excepthook,threading_excepthook}.py` (each: rename to `_wire_*` + add public shim); `exceptions/_well_known.py` (HostEnvironmentError + alias metaclass + Any import); `exceptions/__init__.py` (re-exports); `_lifecycle.py` (use `_wire_*` instead of public shims); `settings.py` (use `settings_config` instead of `make_settings_config`); `pyproject.toml` (filterwarnings exemption). All quality gates green: 315 tests in 2.65s, mypy --strict clean on 24 source files (was 20), ruff clean, lint-imports clean. The 29 warnings shown in the test output are MgfDeprecationWarnings from existing -shape tests calling deprecated shims — that's the visible signal that the migration is alive AND the filter exemption is working (neither escalates to failure nor hides them entirely). Compatibility: every API still works; deprecation warnings name the replacement so consumers migrate incrementally. VManager pin guidance unchanged. Quirks worth recording: (1) the `_DeprecatedEnvironmentErrorMeta.__subclasscheck__` makes BOTH `isinstance(host_err, EnvironmentError)` AND `issubclass(HostEnvironmentError, EnvironmentError)` return True during the cycle so legacy try/except clauses in consumer code keep catching new instances; (2) the OTel re-export module uses a TYPE_CHECKING dual-path because falling back to `Any` at runtime requires the assignment form (`Span = Any`), but type-checkers need the real `from opentelemetry.trace import Span as Span` shape — the TYPE_CHECKING branch satisfies both; (3) refactoring APIs to `_wire_*` private + public shim required updating `_lifecycle._bootstrap_inner` to call the private wirings (otherwise every bootstrap would emit 4 deprecation warnings); same for `MgfSettings.model_config = settings_config(...)` and `BaseAppSettings.__init_subclass__` (both internal callers); (4) the test that monkeypatches setup_otel for the bootstrap-atomicity test had to be updated to patch `_wire_otel` since bootstrap calls the private name — same for excepthook. Prior: 2026-04-26 (closed-box redesign Phase 1 — first commit of the reshape per `docs/CLOSED_BOX_REDESIGN.md`. Three new modules + one typed exception join the public surface; existing APIs UNCHANGED so consumer code stays green. NEW: `mgf.common.bootstrap()` — single canonical entry point replacing the five-call sequence (configure + setup_logging + setup_otel + install_excepthook + install_threading_excepthook); transactional with rollback closures (closes design ref §14.2 — on any failure, partial state is unwound LIFO and `BootstrapError(failed_step, cause=...)` is raised with chained cause); `_BootstrapLogBuffer` captures records emitted from inside bootstrap and replays through wired handlers once setup completes (§14.3 — on bootstrap failure, flushes to stderr); idempotent (§14.10 — second call replaces + emits AUDIT log `event="bootstrap.replace"`); auto-preset default (§14.15 — omitted `settings=` arg → `MgfSettings(preset="auto")` preserves auto-detection so legacy consumers don't silently break on upgrade). NEW: `mgf.common.AppContext` — frozen dataclass carrying app_name + app_version + settings + internal _wiring handles; methods `shutdown()` (LIFO rollback) and `reload(new_settings)` (atomic re-wire — closes §14.5 hot-reload). NEW: `mgf.common.current_context()` — readonly accessor; resolution order is per-task contextvar (Phase 2+ AppContext as cm) → process-wide global (set by bootstrap) → None; thread-safe; ~30 ns per call. `mgf.common.app_name() / app_version()` now consult current_context first, then legacy module global, then placeholder. NEW: `mgf.common.settings.MgfSettings` — typed library settings root; BaseAppSettings subclass with `env_prefix="MGF_"` + `env_nested_delimiter="__"`; composes 5 sub-models (LoggingSettings / OtelSettings / CrashSettings / RedactionSettings) + `preset: PresetName` (`Literal["explicit", "dev", "systemd", "docker", "auto"]`). Each sub-model is a frozen-shape pydantic BaseModel with `extra="forbid"` (rule CF-06). Notable per-field additions: `LoggingSettings.level_overrides: dict[str, LogLevel]` (closes §14.6 — eliminates `import logging` leak for per-component levels); `RedactionSettings.strict_mode: bool` (closes §14.12 seam — Phase 6 wires the GDPR/PII patterns). NEW: `mgf.common.exceptions.BootstrapError` — subclass of OperationError; carries `.failed_step` + chains original cause; existing CLI top-level handlers that map OperationError to exit code catch it without code change. INTEGRATION: `_lifecycle._bootstrap_inner` walks 9 wiring steps (identity / logging / level_overrides / buffer_replay / otel / excepthooks / crash_sinks). Each step appends `(name, rollback_fn)` tuple to `_WiringHandles.rollbacks`. Failure path unwinds LIFO with `contextlib.suppress(Exception)` per rollback (no double-fault); raises `BootstrapError(failed_step, cause=e) from e`. PUBLIC_API.md: 12 new public names; total 37 → 49. AP-10 pin tests grew from 36 to 38 (+2 for new mgf.common.bootstrap + current_context + AppContext + 1 BootstrapError = 4 names; some grouping). 24 new behavioural tests in `tests/unit/test_lifecycle.py` covering bootstrap basic / atomicity (failure injection at otel + excepthooks) / idempotency (second bootstrap replaces, AUDIT log fires) / log buffer (capture + replay + level filter) / AppContext data shape / MgfSettings (defaults / env-var aliases / unknown-key reject / level_overrides typed / strict_mode field / sample_rate validation / recursive serialisation) / level-overrides applied at bootstrap + rolled back on shutdown / current_context resolution. Test count 238 → 266 (+28). Files: `src/mgf/common/_lifecycle.py` (NEW, 477 lines); `src/mgf/common/settings.py` (NEW, 273 lines); `src/mgf/common/exceptions/_well_known.py` (BootstrapError class added); `src/mgf/common/__init__.py` (exports); `src/mgf/common/_identity.py` (lazy current_context lookup with cycle break via lazy import); `tests/unit/test_lifecycle.py` (NEW, 24 tests). Compatibility: APIs (configure/setup_logging/setup_otel/install_excepthook/install_threading_excepthook) UNCHANGED — no deprecation warnings yet, no behavioural changes; existing 238-test surface keeps passing without modification. Deprecation cycle starts in a focused later phase (likely Phase 2 with custom warning class + pyproject filter exemption). All four gates green: 266 tests in 2.65s, mypy --strict clean on 20 source files (was 18 — +settings.py +_lifecycle.py), ruff clean (with the new tighter rules + one documented `# noqa: TC003 — pydantic needs runtime resolution` for `from pathlib import Path` in settings.py because pydantic field-type resolution is runtime not type-check), lint-imports clean (1 contract kept). Quirks worth recording: (1) `Path` MUST stay as a runtime import in pydantic-settings modules — moving it under TYPE_CHECKING breaks `MgfSettings.model_rebuild` chain because pydantic resolves field types at runtime not at static-check time (PydanticUserError: "Path is not fully defined"); (2) `make_settings_config()` from existing `_settings.py` does NOT include `env_nested_delimiter` in its baseline, so MgfSettings overrides `env_nested_delimiter="__"` explicitly to enable `MGF_LOGGING__LEVEL=DEBUG` style env aliases; (3) the dual-mode return shape for `bootstrap()` (works as `with bootstrap(...) as ctx:` AND as `ctx = bootstrap(...)`) needed a wrapper class `_BootstrapContext` with `__enter__` + `__exit__` + `__getattr__` delegating to the wrapped AppContext — the obvious `@contextmanager` decorator can't do both modes. Prior: 2026-04-26 (FEEDBACK round 1 — closed 5 entries + restructured FEEDBACK.md to format v2 for AI-actionability. Closures: PUBLIC_API.md (already shipped in earlier commits 61b340c + bd54476 — formally closed in this round), UnconfiguredWarning (new public class `mgf.common.UnconfiguredWarning`, UserWarning subclass, fires once per process when identity read before configure(); `_CONFIGURED` + `_UNCONFIGURED_WARNED` flags, `stacklevel=3` so warning points at caller), extended Redactor patterns (added `sk-...` / `gh{p,o,u,s,r}_...` / `AKIA...` / `xox[baprs]-...` / `AIza...` patterns; `_BUILTIN_VALUE_PATTERNS` tuple unifies the set), Redactor compose via `|` + `DEFAULT_REDACTION_KEYS` public frozenset (closes both sub-suggestions in one go), SchemaValidationError `_MESSAGE_PREFIX` class attribute (subclasses override prefix without bypassing `__init__`). Test count 221 → 238 (+17): 7 new for FEEDBACK closures + 8 parametrised + 2 (compose + frozenset). All new tests follow `test_<feedback-id>_*` naming so the implementation→test→FEEDBACK chain is grep-friendly. PUBLIC_API.md updated with two new entries (`UnconfiguredWarning` and `DEFAULT_REDACTION_KEYS`); pin-test count grew from 36 → 38; total names 35 → 37. CHANGELOG.md `[Unreleased]` block extended with the round 1 closures including code samples for each. FEEDBACK.md fully restructured to format v2: dashboard table at top (status counts + shipped table + open-blockers list), per-entry frontmatter tables (8 fields: Status / Severity / Submitter / Submitted / Decision / Decided on / Scheduled for / Shipped in), structured prose sections (Concern / Why it matters / Suggested shape / Cost / Decision rationale / Retrospective), §1.4 template that future consumer PRs MUST follow, §4 shipped-entries audit trail, §5 rejected-entries audit trail (empty today), §6 multi-tenancy decision (companion to ), §7 reviewer's meta-feedback unchanged. Original prose-organised format preserved in git history at commit bd54476. The 6 remaining open 🔴 Blockers (EnvironmentError rename, multi-tenancy, lift CancellationToken, Protocol interfaces, make_settings_config redesign, pluggable crash sinks) are pre-1.0 DECISION items — not actionable without maintainer input. STANDARDS_COMPLIANCE.md §2 notes that FEEDBACK 🔴s are tracked there, not in the standards-compliance audit, because the standards don't yet require their resolution. All quality gates green: 238 tests in 1.67s, mypy --strict clean, ruff clean, lint-imports clean. Prior: 2026-04-26 (standards compliance audit — first formal pass against the v2.0 standards. Created `docs/STANDARDS_COMPLIANCE.md` (the rolling compliance document per `COMMON_COMPONENTS.md` §"Maintaining compliance over time"): table covering all 103 MUSTs across 7 topic docs with status / evidence / pinning test; open gaps section (zero today); closed-gaps audit trail with 11 closures from this round; observations section with 5 deliberate-defer findings. Mechanical sweeps confirmed clean: DP-12 UTC discipline (no naive datetime), DP-13 resource release (every `open` in `with`), LG-08 lazy formatting (no f-string log calls), SC-02 (no `shell=True`), SC-03 (no `yaml.load`), SC-06 (no `verify=False`), SC-05 (no MD5/SHA1/PRNG random), AP-08 (`from __future__ import annotations` everywhere). Tightened `pyproject.toml` ruff config to enforce DP-14 mechanical checks going forward: added `DTZ` (rule DP-12), `ASYNC` (rule DP-11), `S` (flake8-bandit, covers SC-* surface) to `[tool.ruff.lint]::select`; added per-file ignores `S108`/`S603` for tests, `S101`/`S311`/`S603` for examples (false-positive surface). Closed five concrete gaps with new pin tests/CI steps: AP-13 version-in-sync (`tests/unit/test_version_in_sync.py`), AP-10 PUBLIC_API.md sync (`tests/unit/test_public_api_doc.py` — 36 parameterised assertions), SC-10 vuln-scan in CI (added `vuln-scan` step to `.woodpecker.yml` running `pip-audit --strict --desc`), LG-12 perf benchmark (`tests/unit/test_logging_perf.py` pinning ≤100µs per record at the rotating-file sink path), TS-13 property test for the Redactor (`tests/unit/observability/test_redaction_property.py` — Hypothesis-driven idempotence + key-safety + sensitive-key-scrub). Two high-value standards improvements grounded in bugs we hit while building examples: LOGGING.md §12.1 "Reserved `LogRecord` attribute names" (the `extra={"name": ...}` → `KeyError` trap with the full reserved set listed) + a row in the §12 anti-patterns table; CONFIGURATION.md §8.4 "Serialising nested sub-models — prefer `model_dump(mode='json')`" (the current `to_yaml_dict` shallow-recursion limitation + workaround pattern, with a forward-path note). Crash-reporter S310 ruff hits resolved with documented per-line `noqa` annotations (the URL is operator-controlled). Test count 176 → 221 (+45: 36 AP-10 pins, 7 redactor property, 1 AP-13 sync, 1 LG-12 perf). Full suite passes in 1.61s. mypy --strict clean, ruff clean (with the new tighter rules), lint-imports clean. CLAUDE.md package map + standing-instructions extended with STANDARDS_COMPLIANCE.md, PUBLIC_API.md, CONTRIBUTING.md, root SECURITY.md rows. Prior: 2026-04-26 (added `examples/` directory — 18 runnable layered examples (6 topics × 3 layers) covering exceptions, config, logging, observability, crash reporting, and the full-app integration. Each topic ships `01_simple.py` (~15 lines, elevator pitch), `02_practical.py` (~50 lines, realistic small app, moderate comments), `03_advanced.py` (~150–250 lines, production-shaped with rich inline comments tying back to the standards rules — DP-*, EH-*, LG-*, CF-*, SC-*, AP-*). All 18 scripts self-contain side-effects via `tempfile.TemporaryDirectory` + env-var redirection so they don't touch `~/.local/state`. Added `tests/unit/test_examples.py` smoke test that invokes each example as a subprocess and asserts exit-code-0; the discovery glob `examples/[0-9][0-9]_*/[0-9][0-9]_*.py` picks up new examples automatically. Subprocess shape gives full isolation (no shared global state across examples) at ~50 ms per script. Five quirks worth recording for future contributors: (1) Python's `LogRecord` reserves `name` as an attribute — `extra={"name": ...}` raises KeyError; use a distinct field like `subject` / `widget_name`; (2) the crash-report JSON key is `timestamp`, NOT `ts` (the JSON-formatter log records use `ts`, the crash report uses `timestamp` — different shapes); (3) `BaseAppSettings.to_yaml_dict()` is shallow — it does NOT recurse into nested `BaseModel` sub-fields, so `save_settings(s, path)` fails on Settings classes containing `LoggingSettings` / discriminated unions; the workaround is `model_dump(mode="json")` + manual `yaml.safe_dump`; this is worth filing as FEEDBACK; (4) examples must save+restore `sys.excepthook` around `install_excepthook()` to avoid pytest's prior hook capturing the synthetic exception (same pattern as `tests/unit/observability/test_excepthooks.py`); (5) ruff `RUF001`/`RUF003` flag unicode `×` and `≥` in strings/comments — use ASCII `x` and "at least" instead. Total examples volume: ~2,300 lines (Python) + ~70 lines (smoke test) + ~80 lines (examples/README.md). Test count 157 → 176; full suite passes in 1.36s. mypy --strict clean, ruff clean, lint-imports clean. README + CLAUDE.md updated to surface the new directory. Prior: 2026-04-25 (continuation of the v2.0 standards push, still no code change to `mgf.common`. Three more high-value additions: NEW topic doc `docs/standards/API_DESIGN.md` (AP-01..AP-15; `__all__` discipline, semver rigor, deprecation cycle, type stability + py.typed, stability tiers experimental/stable/deprecated, PUBLIC_API.md contract, CHANGELOG migration recipes, backward-compat shim discipline; addresses FEEDBACK.md 🔴 Blocker "no PUBLIC_API.md"); new ERROR_HANDLING §15 resilience patterns (EH-11/12/13: retry with exponential backoff + full jitter, circuit breaker with three-state machine + audit logs, bulkhead concurrency limiting, timeout cascades, idempotency); root `CONTRIBUTING.md` for open-source hygiene (dev setup, four green gates, PR process, design-discussion channels, conventional commits, security reporting); root `PUBLIC_API.md` (35 public names enumerated with stability tier per AP-10 — `mgf-common` dogfoods its own contract). Master `docs/standards/README.md`: status banner extended with API_DESIGN.md mention; MUSTs index extended from 67 to 85 rows (added EH-11/12/13 + AP-01..15); folder map updated; "What's new in v2.0" extended with API_DESIGN doc + EH-11/12/13. Project README: standards table extended with API_DESIGN.md row; documentation map extended with PUBLIC_API.md, CONTRIBUTING.md, SECURITY.md rows; layout diagram updated. Doc volume: standards folder grew from 8,124 → 9,116 lines; total docs (incl. root files) from 9,328 → 11,038 lines (+18% in this continuation). All quality gates green: 157 tests, mypy --strict clean, ruff clean. Prior: 2026-04-25 (standards v1.2 → v2.0 — major foundation push, no code change to `mgf.common`. Two NEW topic docs in `docs/standards/`: `SECURITY.md` (engineering security standard; SC-01..SC-18; secrets / subprocess / deserialization / crypto / TLS / input validation / logging hygiene / dependency security / vulnerability response / threat-model template; mechanical enforcement matrix mapping every MUST to a check) and `TESTING.md` (TS-01..TS-18; test layout / fixture hierarchy / mock provider / property tests / GUI testing / async testing / performance benchmarks / coverage / isolation / smoke tests / security pins). High-value additions to the five existing docs: DESIGN_PRINCIPLES gained DP-11/12/13 (async ownership, UTC time, resource release) plus §11–§14 (async/concurrency rules, time + dates, resource management, mechanical enforcement matrix); ERROR_HANDLING gained §13 (asyncio loop handler, gather discipline, TaskGroup, ExceptionGroup, contextvars correlation) and §14 (error-message style guide: tone, four-part shape, identifier formatting, hint pinning); LOGGING gained LG-13/14 plus §17 (rate-limit / sample / dedup / metrics-not-logs) and §18 (contextvars-based correlation across `await` points); CONFIGURATION gained CF-11/12 plus §16 (discriminated unions for backend variants), §17 (env-tier layering dev/staging/prod), §18 (field aliases for renames-without-breaks); COMMON_COMPONENTS got §15 (Security infrastructure pointer) and §16 (Test infrastructure pointer) plus refreshed status table and updated bootstrap order. Master `docs/standards/README.md` updated: status banner v1.2 → v2.0, MUSTs index extended with all CF-07..12 + DP-11..13 + LG-13..14 + SC-01..18 + TS-01..18 (was 23 rules; now 67), new "What's new in v2.0" section, expanded "Suggested reading order" with security-audit + new-test-suite paths, expanded folder map showing each doc's coverage. Repo-root `SECURITY.md` (vulnerability-reporting policy) created — dogfoods SC-12; supported versions, contact (email + Codeberg private advisory), 90-day embargo, in/out-of-scope. Project-root `README.md` standards table extended with the two new docs + updated MUST counts; standards version banner v1.2 → v2.0 with link to "What's new". 157 tests pass in 0.22s, mypy --strict clean, ruff clean, no code changes — this is a docs-only push. Standards version bump per the policy (MAJOR for new MUSTs; existing consumer code may be non-compliant against the new MUSTs and must be re-audited; estimated 3–4 audit→close rounds for VManager per the cornerstone-rhythm pattern). Prior: 2026-04-25 (drift-only fact corrections, no code change: exception-module `__all__` actually lists 15 names not 33; config-module `__all__` lists 8 not 7 — `PathKind` is exported as a public type alias and now appears in the Public API listing for consumers writing `def fn(kind: PathKind)`-style signatures; observability-module count of 8 was already correct. Coverage today is ~85% on a clean run, not the stale "84.84%" snapshot from the release notes — the 80% gate in `.woodpecker.yml` is unchanged. 157 tests pass in 0.22s, mypy --strict clean, ruff clean, no new commits since the FEEDBACK.md commit. Prior: 2026-04-23 (added `FEEDBACK.md` at the repo root — living interface for early-consumer feedback pipeline. Intended to pull opinionated pushback from real consumers BEFORE the API locks at 1.0, when stakes are asymmetric: a 🔴 Blocker concern raised now is a minor commit; the same concern after 1.0 ships with N consumers becomes a breaking-change coordination exercise. VManager (the inaugural consumer) submitted its full integration feedback as the first content: 8 🔴 Blockers (EnvironmentError-shadows-builtin, configure-is-module-global, CancellationToken-lives-in-consumer, no-PUBLIC_API.md, no-Protocol-interfaces, make_settings_config-is-a-workaround, crash-sink-not-pluggable, unconfigured-silent-wrong-default), 6 🟡 Papercuts (Redactor-key-only, Redactor-not-composable, shim-layering-undocumented, schema-versioning-not-reusable, SchemaValidationError-prefix-override-ugly, AS-IS-rots-across-consumers), 1 💭 Aspirational (multi-tenancy), plus 16 detailed roadmap entries for upcoming features (lift-progress-module, vault, SSH, service manager, retry/backoff, metrics, testing-fixtures, cli-helpers, qt-gui-helpers, lifecycle-manager, versioned-file-store, container-deployment, i18n, benchmarks, cookiecutter-starter, migration-codemod). Top 3 pre-1.0 blockers needing decisions NOW: rename EnvironmentError, decide multi-tenancy story, +lift CancellationToken+ProgressReporter before shipping SSH or service-manager. `FEEDBACK.md` §2 describes the submission/triage/closure process; §6 recommends API stability tiers; §7 confronts multi-tenancy; §9 is unvarnished meta-feedback (ambitious-mission-vs-small-code, double-edged-reference-consumer, standards-docs-100%-human-not-machine, manual-twine-release-is-a-risk, no-SECURITY.md-yet, cornerstone-rhythm-is-the-highest-leverage-content, extraction-story-should-be-the-headline); §10 documents how future consumers add their own entries. README grew a "Feedback from consumers" section pointing at the new doc; Layout diagram lists FEEDBACK.md + CHANGELOG.md + docs/CLAUDE.md. CHANGELOG entry `[Unreleased] → New: FEEDBACK.md`. 157 tests, mypy --strict clean, ruff clean — no code changes. PyPI pin unaffected. Standing instructions (top of this file) grew a "Consumer feedback" block — treat 🔴 Blocker entries there as release gates before 1.0; Shipped entries move to §11 with commit SHA + release tag + a one-line retrospective. Prior: 2026-04-23 (standards docs bumped v1.1 → v1.2: folded in VManager round 1–3 lessons. No code change — PyPI still the shipping artifact, VManager's `mgf-common>=0.1.0,<0.2` pin unaffected. Five targeted doc additions: `LOGGING.md` §11.1 (pytest stderr-capture distortion in full-stack perf benchmarks; VManager's first LG-12 test timed out at 128.6 µs/record because of pytest capture, dropping the console StreamHandler after setup fixed it to ~50 µs/record), `LOGGING.md` §11.2 (consumer-side guards — thin LG-* tests on the consumer catch shim regressions + consumer-specific extensions + mgf-common pin regressions, even when the implementation lives upstream), `CONFIGURATION.md` §8.3 (accepting pre-versioned files — the WARN-and-default pattern VManager used when adding `version:` to `profiles/*.yaml` in round 1's CF-07-profiles close; carries four scoped MUSTs: WARNING-with-identifier, rewrite-on-next-save, unit-test-pin, reject-bool), `CONFIGURATION.md` §10.1 (consolidated pin-test file pattern for cross-cutting lazy-default helpers — VManager's `tests/unit/test_default_path_resolution.py` is the canonical shape, one test class covering every `_default_X()` helper in one visible place; grounded in round 3's CF-04-module-paths-3), and a new top-level section in `COMMON_COMPONENTS.md` called "Maintaining compliance over time (cornerstone rhythm)" — describes the compliance-doc shape (table + open gaps + closed history + observations), the audit→close→audit rhythm, the compliance-sweep commit pattern, observations-vs-gaps (documenting deferred-but-considered findings), consumer-side guards, and the single-session AI-agent audit pattern. Per the README version policy MINOR bumps cover SHOULD + template + conditional-MUST additions; existing consumer code stays compliant. `docs/standards/README.md` + `LOGGING.md` + `CONFIGURATION.md` + `COMMON_COMPONENTS.md` all carry the v1.2 banner. 157 tests, mypy --strict clean, ruff clean — unchanged from . Prior: 2026-04-22 (released to PyPI; VManager consuming via PyPI pin; Codeberg repo live; Woodpecker CI pending `ci.codeberg.org` access; manual `twine upload` is the release flow until PyPI adds Codeberg as an OIDC issuer).*
