feat(identifiers): prepare verified cutover batches

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 14:57:56 +02:00
parent bdf3af19e2
commit 5e14d09bdf
10 changed files with 3097 additions and 0 deletions

View file

@ -89,3 +89,4 @@
| task | RMGR-WP-0010-T07 | done | — | workplans/RMGR-WP-0010-authoritative-workload-references.md |
| intake | RMGR-IN-0001 | open | — | intakes/intakes.md |
| intake | RMGR-IN-0002 | open | — | intakes/intakes.md |
| decision | RMGR-DEC-2026-001 | open | — | decisions/RMGR-DEC-2026-001-whynot-identifier-batch.md |

View file

@ -0,0 +1,47 @@
# RMGR-DEC-2026-001 — Whynot Design identifier cutover batch
```yaml
id: RMGR-DEC-2026-001
kind: decision
title: Approve deterministic identifier batch 0002 for whynot-design
status: open
decision_type: pending
owner: Bernd Worsch
repo: repo-manager
workplan: RMGR-WP-0005
workplan_task: RMGR-WP-0005-T04
requested_dispositions:
- approved
- revised
- rejected
source_plan: docs/evidence/RMGR-WP-0005-helixforge-uuid-migration-plan-2026-08-22-v2.json
source_plan_sha256: ac97f6e4f0b35db63a0dc1b4a439f0a39738d13c1407f18419f706d780d4eea0
batch_manifest: docs/evidence/RMGR-WP-0005-batch-0002-whynot-design.json
batch_sha256: 4039224352c6590fdc6b41e739539f1f9c92d098b43ed6e30b3cd9a87b53da65
scope:
repositories: [whynot-design]
replacements: 1
assignments: 0
apply_authorized: false
approval_effect: >-
Approved authorizes one repository-atomic forward cutover of exactly the
pinned batch in both current projections and the authoritative repository
file, subject to repeated no-drift and restore-point checks. Revised or
rejected authorizes no mutation.
rollback: >-
Reverse the file transaction if written, then reverse workstation and
production projection transactions using their durable aliases.
evidence: docs/evidence/RMGR-WP-0005-batch-0002-readiness-2026-08-22.md
created: "2026-08-22"
updated: "2026-08-22"
```
## Decision requested
Approve, revise, or reject the exact batch manifest named above. Approval is
limited to `whynot-design` and the single `WHYNOT-WP-0003` UUID replacement; it
does not authorize any later fleet batch.
The manifest remains fail-closed: any Git, source-fingerprint, projection, or
plan-seal drift before execution voids the approval and requires a new batch
hash.

View file

@ -0,0 +1,77 @@
# RMGR-WP-0005 batch 0002 readiness
**Observed at:** `2026-08-22T12:48:35Z`
**Scope:** `whynot-design` only
**Decision:** `RMGR-DEC-2026-001`
## Outcome
The next deterministic-identifier batch is fully specified and ready for an
explicit approval decision. No database or repository identifier was changed.
The earlier fleet seal correctly rejects 15 repositories whose authoritative
sources changed after it was generated. A refreshed plan now covers 41 eligible
repositories and 230 records with zero collisions:
- 180 replacements;
- 32 assignments;
- 18 already deterministic;
- 0 skipped repositories.
Source plan:
`RMGR-WP-0005-helixforge-uuid-migration-plan-2026-08-22-v2.json`, SHA-256 seal
`ac97f6e4f0b35db63a0dc1b4a439f0a39738d13c1407f18419f706d780d4eea0`.
## Batch 0002
The proposed batch deliberately stays at one repository and one replacement:
| Repository | Record | Current UUID | Derived UUID |
| --- | --- | --- | --- |
| `whynot-design` | `WHYNOT-WP-0003` | `41fed928-f44a-48f4-9870-120310fbf071` | `d3a6ec16-ac40-5ffb-99e7-07f997e59a4a` |
Batch manifest:
`RMGR-WP-0005-batch-0002-whynot-design.json`, SHA-256 seal
`4039224352c6590fdc6b41e739539f1f9c92d098b43ed6e30b3cd9a87b53da65`.
The manifest reports `ready_for_approval: true`, `apply_authorized: false`.
`whynot-design` is clean, exactly synchronized with `origin/main`, and its
origin is the Forgejo lineage. The plan pins HEAD
`4b62cffc86496d587ac8d48a8e199624bc4a5c1f` and source fingerprint
`55ea5e6884be2c5a0a7b8e9e19326f381c2b86afecbc28adc00252ce56793a7f`.
## Projection preflight
Read-only checks against the workstation and production projections agree:
| Check | Workstation | Production |
| --- | ---: | ---: |
| Current workplan UUID lookup | 200 | 200 |
| Derived workplan UUID lookup | 404 | 404 |
| Tasks linked to current workplan | 9 | 9 |
| Progress events linked to current workplan | 11 | 11 |
| Decisions linked to current workplan | 1 | 1 |
This is a useful second pilot: unlike the Repo Manager pilot, the same old row
exists in both projections and has matching dependent-record counts. The
database cascade must preserve those links while the task UUIDs remain
unchanged.
## Approved execution interface
If `RMGR-DEC-2026-001` is approved against the exact batch hash:
1. Re-run batch/source/Git preflight and stop on any drift.
2. Retain fresh per-database restore points and hashes.
3. Apply the repository transaction to workstation and production projections.
4. Apply the sealed authoritative file transaction for `whynot-design`.
5. Commit and push only the mapped workplan file.
6. Run consistency twice and require the derived lookup to return 200, the old
lookup to return 404, and all dependent counts to remain 9/11/1.
7. On any failure after database apply, reverse written files if necessary and
then reverse both database transactions from durable aliases.
The decision does not authorize any other repository or restoration of the
disabled production sweep.

View file

@ -0,0 +1,47 @@
{
"schema": "repo-manager.identifier-migration-batch.v1",
"ok": true,
"ready_for_approval": true,
"apply_authorized": false,
"approval_required": true,
"namespace": "helixforge",
"source_plan_sha256": "ac97f6e4f0b35db63a0dc1b4a439f0a39738d13c1407f18419f706d780d4eea0",
"batch_policy": "repository-atomic, sequential, stop on first failure",
"rollback_order": "reverse files if written, then reverse central projection",
"generated_at": "2026-08-22T12:48:35.222354Z",
"totals": {
"repositories": 1,
"records": 1,
"replace": 1,
"assign": 0,
"unchanged": 0
},
"repositories": [
{
"repo": "whynot-design",
"path": "/home/worsch/whynot-design",
"planned_head_sha": "4b62cffc86496d587ac8d48a8e199624bc4a5c1f",
"source_fingerprint": "55ea5e6884be2c5a0a7b8e9e19326f381c2b86afecbc28adc00252ce56793a7f",
"source_verified": true,
"git_preflight": {
"ok": true,
"head_sha": "4b62cffc86496d587ac8d48a8e199624bc4a5c1f",
"expected_head_sha": "4b62cffc86496d587ac8d48a8e199624bc4a5c1f",
"upstream": "origin/main",
"behind": 0,
"ahead": 0,
"origin": "forgejo-remote:coulomb/whynot-design.git",
"errors": []
},
"mapping_counts": {
"records": 1,
"replace": 1,
"assign": 0,
"unchanged": 0
},
"ready": true
}
],
"errors": [],
"batch_sha256": "4039224352c6590fdc6b41e739539f1f9c92d098b43ed6e30b3cd9a87b53da65"
}

File diff suppressed because it is too large Load diff

View file

@ -44,6 +44,13 @@ rmgr identifier preflight --root /path/to/fleet
rmgr identifier migration-plan --root /path/to/fleet --output uuid-migration.json
rmgr identifier migration-verify --plan uuid-migration.json
rmgr identifier migration-verify --plan uuid-migration.json --repo repo-manager
rmgr identifier migration-batch-plan \
--plan uuid-migration.json \
--repo whynot-design \
--output batch.json
rmgr identifier migration-batch-verify \
--plan uuid-migration.json \
--batch batch.json
```
`migration-plan` is non-mutating. Its versioned JSON output preserves every
@ -61,6 +68,13 @@ repository. `--repo` verifies one atomic apply unit, so unrelated fleet churn
does not invalidate a stable repository. A new plan is required for each unit
whose authoritative source drifts.
`migration-batch-plan` converts selected atomic units into a bounded approval
package. Each selected repository must still match the planned Git HEAD, have a
clean worktree, exactly match its configured upstream, and use a non-retired
origin. The batch records these facts and its own SHA-256 seal, but always emits
`apply_authorized: false`. An explicit decision must cite that batch hash before
any database or file mutation.
Activation and applying a bulk migration remain separate governed steps.
Publishing or planning this function does not retroactively rewrite existing
identifiers. `--namespace` remains an explicit override for a foreign/forked

View file

@ -312,6 +312,20 @@ def main(argv: list[str] | None = None) -> int:
)
p_id_verify.add_argument("--plan", required=True)
p_id_verify.add_argument("--repo", default=None, help="Verify one repository atomic unit")
p_id_batch = identifier_sub.add_parser(
"migration-batch-plan",
help="Pin a clean synchronized repository batch for explicit approval",
)
p_id_batch.add_argument("--plan", required=True)
p_id_batch.add_argument("--repo", action="append", required=True, dest="repos")
p_id_batch.add_argument("--output", default=None)
p_id_batch.add_argument("--force", action="store_true")
p_id_batch_verify = identifier_sub.add_parser(
"migration-batch-verify",
help="Verify a saved batch seal and repeat source/Git preflight",
)
p_id_batch_verify.add_argument("--plan", required=True)
p_id_batch_verify.add_argument("--batch", required=True)
p_id_files = identifier_sub.add_parser(
"migration-files",
help="Validate or execute one repository's sealed UUID file rewrite",
@ -686,7 +700,9 @@ def main(argv: list[str] | None = None) -> int:
load_fleet_namespace,
migrate_repository_identifier_files,
plan_identifier_migration,
plan_identifier_migration_batch,
scan_live_identifier_collisions,
verify_identifier_migration_batch,
verify_identifier_migration_plan,
)
@ -730,6 +746,37 @@ def main(argv: list[str] | None = None) -> int:
except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1
elif args.identifier_command == "migration-batch-plan":
try:
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
if not isinstance(plan, dict):
raise TypeError("migration plan must be a JSON object")
result = plan_identifier_migration_batch(plan, repo_slugs=args.repos)
except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1
if args.output:
output = Path(args.output)
if output.exists() and not args.force:
print(
json.dumps(
{"ok": False, "error": f"output exists: {output}; use --force to replace"},
indent=2,
)
)
return 1
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
elif args.identifier_command == "migration-batch-verify":
try:
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
batch = json.loads(Path(args.batch).read_text(encoding="utf-8"))
if not isinstance(plan, dict) or not isinstance(batch, dict):
raise TypeError("migration plan and batch must be JSON objects")
result = verify_identifier_migration_batch(batch, plan=plan)
except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1
else:
try:
result = plan_identifier_migration(Path(args.root), namespace)

View file

@ -6,6 +6,7 @@ import hashlib
import json
import os
import re
import subprocess
import tempfile
import uuid
from collections import defaultdict
@ -296,6 +297,224 @@ def verify_identifier_migration_plan(
}
def _git_cutover_preflight(repo: Path, *, expected_head_sha: str | None) -> dict[str, Any]:
errors: list[str] = []
def git(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
current_head = head_sha(repo)
if current_head is None:
errors.append("repository has no Git HEAD")
elif current_head != expected_head_sha:
errors.append("Git HEAD changed after migration planning")
status = git("status", "--porcelain")
if status.returncode != 0:
errors.append(status.stderr.strip() or "could not inspect Git worktree")
elif status.stdout.strip():
errors.append("worktree is not clean")
upstream = git("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
upstream_name = upstream.stdout.strip() if upstream.returncode == 0 else None
behind: int | None = None
ahead: int | None = None
if upstream_name is None:
errors.append("current branch has no upstream")
else:
counts = git("rev-list", "--left-right", "--count", "@{u}...HEAD")
try:
behind, ahead = (int(value) for value in counts.stdout.split())
except (TypeError, ValueError):
errors.append(counts.stderr.strip() or "could not compare HEAD with upstream")
else:
if behind or ahead:
errors.append("branch does not exactly match its upstream")
remote = git("remote", "get-url", "origin")
origin = remote.stdout.strip() if remote.returncode == 0 else None
if origin is None:
errors.append("origin remote is missing")
elif any(
marker in origin
for marker in ("gitea-remote", "gitea.coulomb.social", "92.205.130.254")
):
errors.append("origin targets the retired Gitea lineage")
return {
"ok": not errors,
"head_sha": current_head,
"expected_head_sha": expected_head_sha,
"upstream": upstream_name,
"behind": behind,
"ahead": ahead,
"origin": origin,
"errors": errors,
}
def plan_identifier_migration_batch(
plan: dict[str, Any], *, repo_slugs: list[str]
) -> dict[str, Any]:
"""Pin a reviewed repository batch after source and Git preflight.
The result is an approval package, not apply authority. Live database and
file mutation still require an explicit decision citing ``batch_sha256``.
"""
plan_sha256 = _verified_plan_seal(plan)
requested = [slug.strip() for slug in repo_slugs if slug.strip()]
errors: list[dict[str, str]] = []
if not requested:
errors.append({"scope": "batch", "reason": "at least one repository is required"})
if len(set(requested)) != len(requested):
errors.append({"scope": "batch", "reason": "repository list contains duplicates"})
repositories_by_slug: dict[str, list[dict[str, Any]]] = defaultdict(list)
for repository in plan.get("repositories", []):
repositories_by_slug[str(repository.get("repo") or "")].append(repository)
batch_repositories: list[dict[str, Any]] = []
totals = {"repositories": 0, "records": 0, "replace": 0, "assign": 0, "unchanged": 0}
for slug in requested:
matches = repositories_by_slug.get(slug, [])
if len(matches) != 1:
errors.append(
{
"scope": slug,
"reason": "repository must occur exactly once in the source plan",
}
)
continue
repository = matches[0]
source_verification = verify_identifier_migration_plan(plan, repo_slug=slug)
repo = Path(str(repository.get("path") or "")).resolve()
git_preflight = _git_cutover_preflight(
repo,
expected_head_sha=repository.get("planned_head_sha"),
)
mappings = list(repository.get("mappings") or [])
action_counts = {
action: sum(mapping.get("action") == action for mapping in mappings)
for action in ("replace", "assign", "unchanged")
}
actionable = action_counts["replace"] + action_counts["assign"]
if actionable == 0:
errors.append({"scope": slug, "reason": "repository has no migration action"})
for error in source_verification["errors"]:
errors.append({"scope": slug, "reason": error["reason"]})
for reason in git_preflight["errors"]:
errors.append({"scope": slug, "reason": reason})
batch_repositories.append(
{
"repo": slug,
"path": str(repo),
"planned_head_sha": repository.get("planned_head_sha"),
"source_fingerprint": repository.get("source_fingerprint"),
"source_verified": source_verification["ok"],
"git_preflight": git_preflight,
"mapping_counts": {"records": len(mappings), **action_counts},
"ready": source_verification["ok"] and git_preflight["ok"] and actionable > 0,
}
)
totals["repositories"] += 1
totals["records"] += len(mappings)
for action, count in action_counts.items():
totals[action] += count
result = {
"schema": "repo-manager.identifier-migration-batch.v1",
"ok": not errors,
"ready_for_approval": not errors,
"apply_authorized": False,
"approval_required": True,
"namespace": plan.get("namespace"),
"source_plan_sha256": plan_sha256,
"batch_policy": "repository-atomic, sequential, stop on first failure",
"rollback_order": "reverse files if written, then reverse central projection",
"generated_at": utc_now_text(),
"totals": totals,
"repositories": batch_repositories,
"errors": errors,
}
canonical = json.dumps(result, sort_keys=True, separators=(",", ":")).encode("utf-8")
result["batch_sha256"] = hashlib.sha256(canonical).hexdigest()
return result
def verify_identifier_migration_batch(
batch: dict[str, Any], *, plan: dict[str, Any]
) -> dict[str, Any]:
"""Verify a saved batch seal and repeat every selected source/Git check."""
errors: list[dict[str, str]] = []
expected_batch_sha256 = batch.get("batch_sha256")
unsealed = {key: value for key, value in batch.items() if key != "batch_sha256"}
canonical = json.dumps(unsealed, sort_keys=True, separators=(",", ":")).encode("utf-8")
actual_batch_sha256 = hashlib.sha256(canonical).hexdigest()
if batch.get("schema") != "repo-manager.identifier-migration-batch.v1":
errors.append({"scope": "batch", "reason": "unsupported batch schema"})
if expected_batch_sha256 != actual_batch_sha256:
errors.append({"scope": "batch", "reason": "batch SHA-256 mismatch"})
try:
plan_sha256 = _verified_plan_seal(plan)
except (OSError, ValueError) as exc:
errors.append({"scope": "plan", "reason": str(exc)})
plan_sha256 = None
if batch.get("source_plan_sha256") != plan_sha256:
errors.append({"scope": "plan", "reason": "batch source plan SHA-256 mismatch"})
if batch.get("apply_authorized") is not False or batch.get("approval_required") is not True:
errors.append(
{
"scope": "batch",
"reason": "batch must remain unauthorized and approval-required",
}
)
saved_repositories = list(batch.get("repositories") or [])
repo_slugs = [str(item.get("repo") or "") for item in saved_repositories]
if not repo_slugs or any(not slug for slug in repo_slugs):
errors.append({"scope": "batch", "reason": "batch has no valid repository scope"})
fresh = None
else:
try:
fresh = plan_identifier_migration_batch(plan, repo_slugs=repo_slugs)
except (OSError, ValueError) as exc:
errors.append({"scope": "batch", "reason": str(exc)})
fresh = None
if fresh is not None:
errors.extend(fresh["errors"])
fresh_by_slug = {item["repo"]: item for item in fresh["repositories"]}
for saved in saved_repositories:
slug = str(saved.get("repo") or "")
current = fresh_by_slug.get(slug)
if current is None:
errors.append({"scope": slug, "reason": "repository disappeared from batch"})
continue
for field in ("planned_head_sha", "source_fingerprint", "mapping_counts"):
if saved.get(field) != current.get(field):
errors.append({"scope": slug, "reason": f"saved {field} differs from plan"})
return {
"schema": "repo-manager.identifier-migration-batch-verification.v1",
"ok": not errors,
"ready_for_decision": not errors,
"apply_authorized": False,
"approval_required": True,
"batch_sha256": expected_batch_sha256,
"source_plan_sha256": plan_sha256,
"repositories": repo_slugs,
"errors": errors,
}
def _verified_plan_seal(plan: dict[str, Any]) -> str:
if plan.get("schema") != "repo-manager.identifier-migration-plan.v1":
raise ValueError("unsupported identifier migration plan schema")

View file

@ -10,7 +10,9 @@ from repo_manager.identifiers import (
load_fleet_namespace,
migrate_repository_identifier_files,
plan_identifier_migration,
plan_identifier_migration_batch,
scan_live_identifier_collisions,
verify_identifier_migration_batch,
verify_identifier_migration_plan,
)
@ -203,3 +205,84 @@ def test_identifier_file_migration_requires_exact_plan_confirmation(tmp_path: Pa
repo_slug="one",
confirm_plan_sha256="0" * 64,
)
def _push_fixture_to_upstream(repo: Path, remote: Path) -> None:
subprocess.run(["git", "init", "--bare", remote], check=True, capture_output=True)
subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=repo, check=True)
subprocess.run(["git", "push", "-u", "origin", "HEAD:main"], cwd=repo, check=True)
def test_migration_batch_plan_pins_clean_synchronized_repo(tmp_path: Path) -> None:
repo = tmp_path / "one"
path = repo / "workplans" / "one.md"
_workplan(path, "ONE-WP-0001", "active")
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
cwd=repo,
check=True,
capture_output=True,
)
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
plan = plan_identifier_migration(tmp_path, "helixforge")
batch = plan_identifier_migration_batch(plan, repo_slugs=["one"])
assert batch["ok"] is True
assert batch["ready_for_approval"] is True
assert batch["apply_authorized"] is False
assert batch["approval_required"] is True
assert batch["repositories"][0]["git_preflight"]["upstream"] == "origin/main"
assert len(batch["batch_sha256"]) == 64
def test_migration_batch_plan_rejects_dirty_or_duplicate_scope(tmp_path: Path) -> None:
repo = tmp_path / "one"
path = repo / "workplans" / "one.md"
_workplan(path, "ONE-WP-0001", "active")
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
cwd=repo,
check=True,
capture_output=True,
)
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
plan = plan_identifier_migration(tmp_path, "helixforge")
path.write_text(path.read_text(encoding="utf-8") + "\ndrift\n", encoding="utf-8")
batch = plan_identifier_migration_batch(plan, repo_slugs=["one", "one"])
assert batch["ok"] is False
assert batch["ready_for_approval"] is False
assert any(error["reason"] == "repository list contains duplicates" for error in batch["errors"])
assert any(error["reason"] == "worktree is not clean" for error in batch["errors"])
def test_migration_batch_verification_repeats_preflight_and_detects_tampering(
tmp_path: Path,
) -> None:
repo = tmp_path / "one"
path = repo / "workplans" / "one.md"
_workplan(path, "ONE-WP-0001", "active")
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
cwd=repo,
check=True,
capture_output=True,
)
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
plan = plan_identifier_migration(tmp_path, "helixforge")
batch = plan_identifier_migration_batch(plan, repo_slugs=["one"])
assert verify_identifier_migration_batch(batch, plan=plan)["ok"] is True
batch["totals"]["records"] = 999
verification = verify_identifier_migration_batch(batch, plan=plan)
assert verification["ok"] is False
assert any(error["reason"] == "batch SHA-256 mismatch" for error in verification["errors"])

View file

@ -431,6 +431,23 @@ evidence, restore points, hashes, and residuals are in
progress: widening beyond this single repository requires a new explicit fleet
cutover decision and refreshed source fingerprints.
**Batch 0002 prepared (2026-08-22):** the original fleet seal now rejects 15
repositories with legitimate source drift. A refreshed zero-collision plan
covers 41 repositories and 230 records. New `rmgr identifier
migration-batch-plan` pins an exact subset only after source verification plus
clean, Forgejo-synchronized Git preflight, and always emits
`apply_authorized: false` until a decision cites its batch hash.
`RMGR-DEC-2026-001` now offers the directly executable second pilot:
`whynot-design` only, one workplan UUID replacement, no task replacements or
assignments. Workstation and production both resolve the old UUID, reject the
derived UUID, and agree on 9 tasks, 11 progress events, and 1 decision. The
sealed batch hash is
`4039224352c6590fdc6b41e739539f1f9c92d098b43ed6e30b3cd9a87b53da65`.
No mutation has run; approval, revision, or rejection of that exact package is
the remaining gate. Evidence:
`docs/evidence/RMGR-WP-0005-batch-0002-readiness-2026-08-22.md`.
## Retire the interim rule
```task