feat(onboard): add rmgr repo-onboard, the missing managed_repos write path

hub-record-authority.yaml assigns managed_repos to repo-manager as
file-derived, but repo-manager exposed no command for it. The only working
path lived in the State Hub repo and defaulted to 127.0.0.1:8000, which is how
seven weeks of onboarding landed in a local cache instead of central.

Order follows ADR-010 decision 5: make the source file correct and reachable
first, then project it. Refuses to onboard when the classification file is
missing or invalid, has uncommitted changes, has no upstream, or has unpushed
commits — a hub record whose backing file is only local cannot be re-derived
by anyone else.

--api-base has no default on purpose. A silent localhost default is the
original defect, not a convenience.

A failed classification PATCH degrades to a warning rather than failing the
run: the authoritative record existing is what stops a repository from being
recoverable only through a discardable cache, and classification is a
projection of a committed file that can be re-derived later.

Refs CUST-WP-0067-T04

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
tegwick 2026-08-24 23:33:22 +02:00
parent 885575802c
commit 1be6d85397
3 changed files with 357 additions and 0 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from repo_manager.commands.rapp import add_rapp_parser
@ -76,6 +77,25 @@ def main(argv: list[str] | None = None) -> int:
help="Rebuild all authoritative UUIDs after proving the repo projection is empty",
)
p_onboard = sub.add_parser(
"repo-onboard",
help="Onboard a repository into its authoritative State Hub from its classification file",
)
p_onboard.add_argument("--path", default=".", help="Repository checkout path")
p_onboard.add_argument("--slug", default=None, help="Override the repo slug (default: directory name)")
p_onboard.add_argument(
"--api-base",
default=os.environ.get("STATE_HUB_API_BASE"),
help="Authoritative State Hub API base URL (or set STATE_HUB_API_BASE). "
"Deliberately has no default: a silent localhost default is how "
"onboarding reached a cache instead of central for seven weeks.",
)
p_onboard.add_argument(
"--dry-run",
action="store_true",
help="Report what would change without writing to the hub",
)
p_cmd = sub.add_parser(
"update-task-status",
help="Command repo.work.update_task_status (file + git commit)",
@ -504,6 +524,29 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status in {"applied", "noop"} else 1
if args.command == "repo-onboard":
from repo_manager.commands.repo_onboard import OnboardError, onboard_repo
if not args.api_base:
print(
"ERROR: --api-base is required (or set STATE_HUB_API_BASE). "
"Point it at the authoritative hub, not a local cache.",
file=sys.stderr,
)
return 2
try:
report = onboard_repo(
Path(args.path),
api_base=args.api_base,
dry_run=args.dry_run,
slug=args.slug,
)
except OnboardError as exc:
print(json.dumps({"status": "blocked", "error": str(exc)}, indent=2))
return 1
print(json.dumps(report, indent=2))
return 0
if args.command == "update-task-status":
from repo_manager.commands.task_status import update_task_status