diff --git a/README.fr.md b/README.fr.md index 5584e35..5109b3f 100644 --- a/README.fr.md +++ b/README.fr.md @@ -426,10 +426,13 @@ d'une release — tag git, `VERSION`, métadonnées du paquet, wheel/sdist, nom `VERSION` interne du bundle — est fail-closed : un tag qui ne nomme pas l'arbre qu'il pointe ne peut pas publier. -Les releases parlent un protocole de mise à jour versionné (v2) : le bundle est -`ainative-lifecycle-v2-.zip` et un runtime antérieur à v2.2.2 ne peut pas le consommer -même depuis un miroir — le format lui-même le refuse. Chaque artefact publié porte aussi une -attestation de provenance de build GitHub (`gh attestation verify -R Rwanbt/ai-native-dev-stack`) ; +Les releases parlent un protocole de mise à jour versionné (v3) : un manifeste externe +(`ainative-release-v3.json`) est vérifié contre les métadonnées de la source de release avant +tout le reste, puis il ancre le bundle de cycle de vie +(`ainative-lifecycle-v3-.zip`) par nom, version, taille et SHA-256. Une release +pont peut encore parler v2 (`ainative-lifecycle-v2-.zip`), et un runtime antérieur à +v2.2.2 ne peut pas en consommer un, même depuis un miroir — le format lui-même le refuse. +Chaque artefact publié porte aussi une attestation de provenance de build GitHub (`gh attestation verify -R Rwanbt/ai-native-dev-stack`) ; SHA-256 seul signifie intégrité, pas signature humaine (voir docs/RELEASING.md). --- diff --git a/README.md b/README.md index 74cf0e5..390dbc1 100644 --- a/README.md +++ b/README.md @@ -420,10 +420,14 @@ chain - git tag, `VERSION`, package metadata, wheel/sdist, bundle filename and bundle internal `VERSION` - is fail-closed: a tag that does not name the tree it points at cannot publish. -Releases speak a versioned update protocol (v2): the lifecycle bundle is -`ainative-lifecycle-v2-.zip` and a runtime older than v2.2.2 cannot -consume one even when a mirror hands it the file - the format itself refuses it. -Every published artifact also carries a GitHub build-provenance attestation +Releases speak a versioned update protocol (v3): an external manifest +(`ainative-release-v3.json`) is verified against the release source's own +metadata before anything else, and it anchors the lifecycle bundle +(`ainative-lifecycle-v3-.zip`) by name, version, size and SHA-256. A +bridge release can still speak v2 (`ainative-lifecycle-v2-.zip`), and +a runtime older than v2.2.2 cannot consume one even when a mirror hands it the +file - the format itself refuses it. Every published artifact also carries a +GitHub build-provenance attestation (`gh attestation verify -R Rwanbt/ai-native-dev-stack`); SHA-256 alone still means integrity, not a human signature (see docs/RELEASING.md). diff --git a/SUPPORT.md b/SUPPORT.md index f476446..8c4770b 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -11,7 +11,7 @@ | Harnesses | Claude Code, Codex, OpenCode, Cursor, Gemini CLI, MiniMax/Mavis — for the shared method, skills and hooks | | Work management | Generic Git; GitHub (mapping); GitLab (mapping). The Work Authority is resolved locally, purely and fail-closed (ADR-0018) | | Features | Project-scope features independent of the profile (`forge-github` default, `forge-gitlab`), State schema V2 (ADR-0017) | -| Release sources | GitHub.com (built-in, qualified); a local mirror; an anonymous HTTPS release API (`AINATIVE_UPDATE_URL`, never credentialed); GitLab.com (qualified live: Generic Package Registry anchor, managed releases, exact version chain — 2026-09-16); GitLab Self-Managed and GitHub Enterprise Server UNTESTED — **not supported** | +| Release sources | GitHub.com (built-in, qualified live — full V3 chain against the published v2.5.0, 2026-09-17); a local mirror; an anonymous HTTPS release API (`AINATIVE_UPDATE_URL`, never credentialed); GitLab.com (qualified live: Generic Package Registry anchor, managed releases, exact version chain — 2026-09-16); GitLab Self-Managed and GitHub Enterprise Server UNTESTED — **not supported** | Only the latest release is supported; fixes ship as a new patch version. diff --git a/UPDATING.md b/UPDATING.md index 117bab1..dec1ad7 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -62,12 +62,18 @@ The refusal names both versions and prints the exact upgrade command. `ainative status --check-updates` and `ainative doctor --check-updates` still report what is available and say when the runtime must be upgraded first. -Releases from the protocol v2 line cannot be consumed by a runtime older than -v2.2.2 at all: the bundle is named `ainative-lifecycle-v2-.zip` and its -payload sits under `stack/`, which the old selector and the old extractor both -refuse. That containment is deliberate - an old runtime must never apply a -policy it does not know - and the only path forward is upgrading the CLI, then -running `ainative update` in each project. +Releases now speak protocol v3: an external manifest +(`ainative-release-v3.json`) anchors the lifecycle bundle +(`ainative-lifecycle-v3-.zip`), and the bridge release v2.4.4 is the +stop where runtimes that only know protocol v2 are told, in their own +vocabulary, that a CLI upgrade is required first (`CLI_UPDATE_REQUIRED`). +Runtimes older than the bridge refuse a v3 release fail-closed and the only +path forward is the documented manual CLI upgrade, then `ainative update` in +each project. Protocol v2 bridge releases (for example +`ainative-lifecycle-v2-2.4.4.zip`) remain consumable by runtimes from v2.2.2; +a runtime older than v2.2.2 cannot consume one even when a mirror hands it the +file — the format itself refuses it (the payload sits under `stack/`, which +the old selector and the old extractor both refuse). ### What it guarantees @@ -257,14 +263,17 @@ does what it always did, now through the lifecycle manager. 2. Bump `VERSION` (semver) and the `stack-version` header in `AGENTS.md`. 3. Move `CHANGELOG.md` `[Unreleased]` items under `## [x.y.z] - YYYY-MM-DD`. 4. Tag: `git tag vX.Y.Z && git push --tags`. -5. Publish the release assets: wheel, sdist and the lifecycle bundle - `ainative-lifecycle-v2-x.y.z.zip` (built by +5. Publish the release assets: wheel, sdist, the lifecycle bundle + `ainative-lifecycle-v3-x.y.z.zip` and its external manifest + `ainative-release-v3.json` (both built by `scripts/build_lifecycle_bundle.py`; `.github/workflows/release-assets.yml` - builds, gates and attests all three, then publishes the release from a - verified draft). `ainative update check` resolves the latest release from - that source; the bundle's published SHA-256 is verified before any file is - written, and a release without a published digest for the bundle is refused - (`UPDATE_INTEGRITY_METADATA_MISSING`). See `docs/RELEASING.md` for the full + builds, gates and attests all of them, then publishes the release from a + verified draft — a bridge release can still be built with + `--protocol 2`). `ainative update check` resolves the latest release from + that source; the manifest's anchor (SHA-256 + size from the provider's + metadata) and the bundle's published digest are verified before any file is + written, and a release without them is refused (`CHECK_FAILED` / + `UPDATE_INTEGRITY_METADATA_MISSING`). See `docs/RELEASING.md` for the full sequence, the attestations and the PyPI half. Users then see `UPDATE_AVAILABLE` and update with one command — no personal diff --git a/docs/DISTRIBUTION-LIFECYCLE.md b/docs/DISTRIBUTION-LIFECYCLE.md index 72670b7..c17c512 100644 --- a/docs/DISTRIBUTION-LIFECYCLE.md +++ b/docs/DISTRIBUTION-LIFECYCLE.md @@ -429,12 +429,18 @@ that can refuse happens before the first project write: a refused update - wrong runtime, wrong version, bad digest, bad archive - leaves the project byte-identical, cache included. -**The artifact, stated precisely.** The official provider consumes exactly one -asset: the lifecycle bundle `ainative-lifecycle-v2-.zip` published -beside the wheel and the sdist, named for the release's own version. Inside, -a `lifecycle-protocol.json` document names the protocol version, the release +**The artifact, stated precisely.** The official provider consumes the release's +declared pair: the external manifest `ainative-release-v3.json` published beside +the wheel, the sdist and the lifecycle bundle `ainative-lifecycle-v3-.zip`, +each named for the release's own version. The manifest is parsed only after its +anchor — the SHA-256 and size the provider's own metadata reports for it — is +verified; it then declares the bundle's name, version, SHA-256 and size, and the +bundle is verified against that declaration. Inside the bundle, a +`lifecycle-protocol.json` document names the protocol version, the release version and the payload root (`stack/`), and the payload's `VERSION` is compared -with the release again. +with the release again. A bridge release can still be built with `--protocol 2` +(`ainative-lifecycle-v2-.zip`, no manifest) so older runtimes have the +documented upgrade stop. **Containment of pre-2.2.2 runtimes, stated precisely.** Runtimes up to v2.2.2 looked for `ainative-dev-stack-*.zip` and found their payload by taking the @@ -470,16 +476,18 @@ cd your-project && ainative update **A newer lifecycle protocol, stated precisely.** A release that publishes a lifecycle bundle or manifest for a protocol newer than the running runtime's -(`ainative-lifecycle-v3-*`, `ainative-release-v3.json`) is not a broken -release: `update check` reports it as available with a CLI upgrade required, -and `ainative update` refuses it with `CLI_UPDATE_REQUIRED`, before any -download or write, naming the upgrade command. A release that publishes no +(the current line is protocol 3: `ainative-lifecycle-v3-*`, +`ainative-release-v3.json`) is not a broken release *to a runtime that knows +the bridge*: `update check` reports it as available with a CLI upgrade +required, and `ainative update` refuses it with `CLI_UPDATE_REQUIRED`, before +any download or write, naming the upgrade command. A runtime older than the +bridge release has no such vocabulary and refuses it fail-closed (`CHECK_FAILED: +release publishes no ainative-lifecycle-v2-*.zip lifecycle bundle` on +`update check`); its documented escape path is the same upgrade command, +applied manually, then `ainative update`. A release that publishes no lifecycle bundle at all keeps its refusal: -`UPDATE_INTEGRITY_METADATA_MISSING`. A runtime older than the bridge release -sees a V3 release as `UPDATE_INTEGRITY_METADATA_MISSING`; its documented -escape path is the same upgrade command, applied manually, then -`ainative update`. Before the first V3 publication the release workflow -verifies the declared `V3_BRIDGE_RELEASE` with +`UPDATE_INTEGRITY_METADATA_MISSING`. Before the first V3 publication the +release workflow verifies the declared `V3_BRIDGE_RELEASE` with `scripts/check_bridge_release.py`, which resolves it through these same selection rules and blocks the publication (`BLOCK RELEASE`) when the bridge cannot be verified. @@ -629,7 +637,7 @@ Files the stack never wrote are in the "kept" column of every row. | Number | Source of truth | Changes when | |---|---|---| | Stack release | `VERSION` | a release is cut | -| Update protocol | `provider.UPDATE_PROTOCOL_VERSION` | the bundle format changes (currently 2) | +| Update protocol | `release_v3.BUNDLE_PROTOCOL_VERSION` (the current line, 3; the legacy v2 path keeps `provider.UPDATE_PROTOCOL_VERSION = 2`) | the bundle format changes | | Lifecycle state schema | `state.SCHEMA_VERSION` | `state.json`'s shape changes | | Work Plane runtime | `ainative_workplane.__version__` | the verdict engine is released | | Artifact schema | `contracts.SUPPORTED_SCHEMA_VERSIONS` | a Work Plane artifact changes shape | diff --git a/docs/MULTIFORGE-QUALIFICATION.md b/docs/MULTIFORGE-QUALIFICATION.md index 31451c5..d9f8f4c 100644 --- a/docs/MULTIFORGE-QUALIFICATION.md +++ b/docs/MULTIFORGE-QUALIFICATION.md @@ -1,39 +1,74 @@ # Multi-Forge qualification report -**Base commit:** `multiforge` head `6f4946b` plus the documentation commit that -carries this report. **Architecture:** frozen (Multi-Forge v1.3.2-final; -ADR-0017/0018/0019). **Execution:** local, issue-driven; the full CI matrix -runs when the branch is proposed for `dev`. +**Base commit:** `main` `2674c43` (release v2.5.0) plus the documentation +commits that carry this report. **Architecture:** frozen (Multi-Forge +v1.3.2-final; ADR-0017/0018/0019). **Execution:** local, issue-driven, plus the +full CI matrix on the release SHA itself. This report states what was verified, with which evidence, and what was not. Declared support never exceeds qualified support (`SUPPORT.md`, ADR-0019 §13). +## Delivery record + +| Step | Where | Evidence | +|---|---|---| +| Multi-Forge v1 promotion | `main` `8f319ad` (PR #176, #173) | architecture frozen, ADR-0017/0018/0019 | +| FIX-A — protocol-aware bridge gate | `main` `f23b59e` (PR #181, #177) | fail-closed gate derives the protocol from the built artifacts; 5 non-vacuity cases + workflow-entry-point mutation test | +| FIX-B/C/D — provider-neutral cleanups | `main` `1d412fd` (PR #182, #178) | reserved `release-api`; declarative `api_version_header`; provider-aware 403/429 hints | +| **Bridge release 2.4.4** | tag `v2.4.4` at `main` `d7b2626` (PR #183, #179) | Release run 35205639491 green; bundle `sha256:d6ddafbd…` (119 928 B), `SHA256SUMS` matches GitHub's digests; bundle attestation verified (exit 0) | +| **V3 release 2.5.0** | tag `v2.5.0` at `main` `2674c43` (PR #184, #180) | Release run 35210583585 green; assets below | +| Manifest attestation fix | `main` `52c05cc` (PR #186, #185) | `subject-path` now covers `dist/*.json`; v2.5.0's gap recorded below, assets never replaced | + +Published v2.5.0 assets (GitHub Release, 2026-09-17T10:27:50Z): + +| Asset | Size | SHA-256 (== `SHA256SUMS` == GitHub digest) | +|---|---|---| +| `ainative_dev_stack-2.5.0-py3-none-any.whl` | 472 597 | `d2714b532eb226ad24d87e2febb9d61ed2e9c8c6b08c033e03049f145a6aa538` | +| `ainative_dev_stack-2.5.0.tar.gz` | 765 181 | `f7bb94e83c3bd22f3ab9b5d21b50a795674228e21ae7b08db0d13d2b61f66c04` | +| `ainative-lifecycle-v3-2.5.0.zip` | 119 993 | `f801ecdd14b1d1ae03b61de16a5ff8ba1e8c2537e553b79f1825a5831dfb587c` | +| `ainative-release-v3.json` | 502 | `50a4a58f7689fdd137141ac0dbdcdd83f736d9d276149ca9b4129a9cd769a849` | +| `SHA256SUMS` | 395 | `cabf3e24c718d24b282eb39530ae41a47b4c28e9e3abd9672e7c329b3397bf1a` | + +No other assets exist on the release — the published set is exactly the +declared set. + ## What was delivered | Stream | Deliverable | Evidence | |---|---|---| | PR-0A | Release transport credential confinement (`transport.py`) | 23 tests; characterization probe recorded pre-fix | | ADR-MF-01/02/03 | ADR-0017/0018/0019 accepted | merged documents | -| PR-0B | V2 forward bridge (`CLI_UPDATE_REQUIRED`) + publication gate | 5 tests; `scripts/check_bridge_release.py` + 9 gate tests | +| PR-0B | V2 forward bridge (`CLI_UPDATE_REQUIRED`) + publication gate | 5 tests; `scripts/check_bridge_release.py` + gate tests | | PR-1 | Feature model, State V2, atomic switching, GitLab templates | 34 tests + 309-suite regression | | PR-2 | Work Authority resolver, claim grammar, journal, policy docs, distributed template | 27 tests; policy purity tests | | PR-3 | `forge detect/status`, doctor extension, status parity | 7 observation tests | | PR-4 | Source resolver, ReleaseManifest V3, providers, updater wiring | 19 + 32 + 16 + 5 tests | | PR-5 | GitLab provider (V3 contract), purity tests | 10 provider tests + 7 purity tests | +| FIX-A/B/C/D | Protocol-aware gate, provider-neutral transport/config surfaces | #181/#182 merged, all suites green | +| V3 publisher | Protocol-3 builder + external manifest, anchored chain validation, version 2.5.0 | #184 merged; `check_release_versions.py --tag v2.5.0 --dist` verified the 4 published artifacts pre-upload | ## Platform matrix | Platform | Status | |---|---| -| Windows (local) | GREEN — the full local suite (314 lifecycle + CLI/knowledge/machine suites) | -| Linux, macOS | PENDING — CI runs on the `multiforge` → `dev` proposal; not yet executed | +| Windows | GREEN — full CI matrix + local suites (315 lifecycle tests, CLI/knowledge/machine suites) | +| Linux | GREEN — full CI matrix on the release SHA `2674c43` | +| macOS | GREEN — full CI matrix on the release SHA `2674c43` | + +The full matrix ran on the exact release SHA: 52 jobs green, including +Distribution lifecycle (win/macOS/Linux × py3.11/3.13), Upgrade E2E (three +OSes), Clean-install E2E (three OSes), Installers (three OSes), Verified Work +Plane V2 (Linux/Windows), Multi-Vault suites, Vault v4 protocol, Knowledge B1 +and convergence, OpenCode plugin runtime, Python 3.8–3.13, LOC budget gate, +CLI conventions gate and the Production Gate. A separate `sdist round-trip` +workflow was green on the same SHA. ## Forge qualification | Tuple | Status | |---|---| | Generic Git | GREEN — `feature switch none`, zero-feature states valid, observation resolves `UNAVAILABLE` | -| GitHub.com | GREEN for the release source (existing qualified releases + provider contract tests); GREEN for work management (mapping + observation) | +| GitHub.com | **GREEN — qualified live for releases and work management (2026-09-17)**: full V3 chain against the published v2.5.0 (enumeration complete; manifest anchor read from the asset API's `digest`+`size` — `50a4a58f…`, 502 B; anchored manifest parsed; exact version chain held; artifact downloaded, 119 993 B, SHA-256 verified against the manifest; lifecycle protocol 3 accepted; payload `VERSION` 2.5.0). Update flows: bridge 2.4.4 sees 2.5.0 with the exact-runtime gate; applying under 2.4.4 refuses `CLI_UPDATE_REQUIRED` with zero writes; the published 2.5.0 CLI applies 2.4.4→2.5.0 through the V3 flow and `update rollback` restores 2.4.4 | | GitLab.com | **GREEN — qualified live** (2026-09-16) against a public test project (`barat.erwan/ai-native-dev-stack-probe`): Generic Package Registry published `ai-native-dev-stack` 2.4.4 (manifest 514 B `sha256:84a921eb…`, bundle 119 895 B `sha256:67670398…`) plus release `v2.4.4`. Authenticated enumeration (`PRIVATE-TOKEN`), anchor read from the package-file API's `file_sha256` + `size`, anchored manifest parsed, exact version chain held, artifact downloaded with size + SHA-256 verified, lifecycle protocol 3 accepted, and `update check` reported `UPDATE_AVAILABLE 2.4.4` with the exact-runtime gate (`runtime_ready=false` for the 2.4.3 runtime) | | GitLab Self-Managed | UNTESTED — not supported | | GitHub Enterprise Server | UNTESTED — not supported | @@ -60,20 +95,38 @@ Declared support never exceeds qualified support (`SUPPORT.md`, ADR-0019 §13). | P | Selector conflict identical across commands | one resolver; a single E2E asserting update check / update / status / doctor all surface `UPDATE_SOURCE_CONFLICT` | GREEN | | Q | Mandatory secret patterns survive operator configuration | `MANDATORY_SECRET_PATTERNS` (private keys, AWS, GitHub, Slack, GitLab `glpat-`/`gldt-`/`glrt-`/`glsoat-`) with `extra_secret_patterns` as a union; the constructor accepts no replacement parameter; the anti-debt owner and the vault-sync fallback carry the same GitLab prefixes | GREEN | +## Published-release update flows (live evidence) + +Executed on Windows against the real releases and real published wheels, in +throw-away git projects, with `GITHUB_TOKEN` from `gh auth token`. + +| Flow | Observed | +|---|---| +| Pre-bridge 2.4.3 runtime vs the V3-only 2.5.0 release | `CHECK_FAILED: release publishes no ainative-lifecycle-v2-*.zip lifecycle bundle; the official update path refuses what it cannot verify` — fail-closed, zero writes (2.4.3 predates the bridge; documented manual path applies) | +| Documented manual path: 2.4.3 → bridge 2.4.4 CLI | `pip install --upgrade …@v2.4.4` (the exact published command); the bridge then reports `AI Native 2.5.0 is available … must be applied by runtime 2.5.0` with the upgrade command | +| Bridge 2.4.4 runtime `update` (apply) targeting 2.5.0 | `refused: CLI_UPDATE_REQUIRED`, exit 1, project state untouched | +| Published 2.5.0 CLI applies to a 2.4.4 project | `2.4.4 -> 2.5.0: applied` through the V3 flow; state `2.5.0`, schema 2 | +| `update rollback` | `rolled back to 2.4.4 (1 files)` — project assets only, as stated in the output | +| Fresh install from the published 2.5.0 wheel (no checkout, new venv) | `--version` (lifecycle 2.5.0, schema 2, workplane 0.1.0), `init --profile standard`, `status`, `doctor` (release source resolved, `forge-github` active, work authority `UNAVAILABLE`, 0 unresolved claims), `update check` → `Up to date (2.5.0)`, `feature status`, `forge status` | +| Verified profile install | Bootstrap notice present; `trust show` → `anchor:null, governed:false` — no authority is fabricated | + ## Security, migration, claims, integrity - **Transport:** zero provider credentials observed at non-approved origins across the metadata + artifact flow; `https → http` downgrades, userinfo URLs and over-long redirect chains refused; `AINATIVE_UPDATE_URL` anonymous. - **State migration:** V1→V2 inside the next mutation, state-last, idempotent; - absent managed files stay absent; two-work-forge states refuse. + absent managed files stay absent; two-work-forge states refuse; the + published-release E2E above exercised the live transition (schema 2 after + both the 2.4.4 and 2.5.0 updates). - **Claims:** journal written durably before the remote signal; corrupt or unwritable journals fail closed; an uncertain POST is never retried; abandonment is an explicit operator transition that keeps the record. - **Release integrity:** the manifest is parsed only after its external anchor (SHA-256 + size from provider metadata) is verified; the exact version chain holds across six identities; enumeration incompleteness and duplicates - refuse instead of resolving by order. + refuse instead of resolving by order. Verified live on GitHub.com + (v2.5.0) and GitLab.com (2.4.4). ## Known limitations and remaining work @@ -81,19 +134,24 @@ Declared support never exceeds qualified support (`SUPPORT.md`, ADR-0019 §13). release, #185):** the release workflow's `subject-path` did not include `dist/*.json`, so the published `ainative-release-v3.json` of v2.5.0 has no build-provenance attestation (`gh attestation verify` returns 404 for its -digest). The bundle *is* attested. The manifest remains covered by +digest). The bundle *is* attested, and the manifest remains covered by `SHA256SUMS` and by GitHub's asset metadata — which is exactly the anchor the -runtime verifies — and the published v2.5.0 assets are **never replaced -retroactively**. The workflow now attests every published asset; the next -release must show a verifiable manifest attestation. - -**P1 — the declared-support gate:** -- Full CI matrix (Linux/Windows/macOS, py3.11/3.13) — runs on the `dev` - proposals; every run so far has been green (52/52, then 76/76). -- The V2 bridge release (2.4.4) and the V3 release (2.5.0) are now published - and verified; `V3_BRIDGE_RELEASE=2.4.4` is set repository-side, so the - first V3 publication gate is validated for real. The full report refresh - lands separately from this fix (#185). +runtime verifies. The published v2.5.0 assets are **never replaced +retroactively**. The workflow now attests every published asset (`main` +`52c05cc`); the next release must show a verifiable manifest attestation. + +**PyPI:** the **Publish to PyPI** workflow is wired (Trusted Publishing/OIDC) +but every run fails because no trusted publisher is registered on the PyPI +account — including the run dispatched for `v2.5.0`. No API-token workaround +is accepted; until the one-time external setup exists, the pinned GitHub +release (now `v2.5.0`) is the supported distribution channel. + +**Multi-Vault:** GUARDED, not ENFORCED — the policy and surface exist; no +environment enforces it yet. + +**Declared-support gate (P1):** GitLab Self-Managed and GitHub Enterprise +Server remain UNTESTED and are not supported. PyPI publication additionally +depends on the external account configuration above. **Resolved since the first draft of this report:** the GitLab.com **live** qualification (scenario B), scenario Q (mandatory secret patterns with @@ -101,6 +159,8 @@ operator extras), scenario J (GitLab object-storage blob), scenario M (legacy GitLab remote warning), scenario P (one cross-command selector-conflict E2E) and the EN/FR documentation parity automation (`tests/purity/test_docs_parity.py`: heading hierarchy, operational surface, -critical security statements — no raw line-count equality). +critical security statements — no raw line-count equality). The full CI +matrix now runs on `main` itself, and both releases (2.4.4 bridge, 2.5.0 V3) +are published and verified. **P0:** none known.