feat: generate repository rename adoption plans
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 25s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
tegwick 2026-08-29 13:08:02 +02:00
parent bdf3a1fdf8
commit ef0b6df3b8
7 changed files with 1294 additions and 5 deletions

View file

@ -16,6 +16,15 @@ import uuid
from pathlib import Path
from typing import Any, Callable
from repository_rename_workplan import (
WorkplanGenerationError,
build_generation_snapshot,
content_checksum,
default_output_path,
render_workplan,
write_workplan_exclusive,
)
CLI_SCHEMA_VERSION = "state-hub.repository-rename-cli.v1"
DEFAULT_API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000")
@ -120,7 +129,9 @@ def _api_request(
method: str,
path: str,
body: dict[str, Any] | None = None,
) -> dict[str, Any]:
*,
expected: type | tuple[type, ...] = dict,
) -> Any:
data = json.dumps(body).encode("utf-8") if body is not None else None
headers = dict(_HEADERS)
if data is not None:
@ -165,9 +176,14 @@ def _api_request(
"State Hub returned an invalid JSON response",
code="invalid_state_hub_response",
) from exc
if not isinstance(payload, dict):
if not isinstance(payload, expected):
expected_name = (
", ".join(item.__name__ for item in expected)
if isinstance(expected, tuple)
else expected.__name__
)
raise RenameCLIError(
"State Hub returned a non-object response",
f"State Hub returned JSON with the wrong shape; expected {expected_name}",
code="invalid_state_hub_response",
)
return payload
@ -361,6 +377,9 @@ def _emit(payload: dict[str, Any], *, as_json: bool) -> None:
print(f" blockers: {len(result.get('blockers') or [])}", file=stream)
if result.get("preflight_file"):
print(f" private preflight: {result['preflight_file']}", file=stream)
elif public["command"] == "generate-workplan" and result.get("output"):
print(f" workplan: {result['output']}", file=stream)
print(f" workplan id: {result.get('workplan_id')}", file=stream)
error = public.get("error")
if error:
print(f" error: {error.get('message')}", file=stream)
@ -490,6 +509,88 @@ def cmd_preflight(args: argparse.Namespace) -> None:
_run(args, action)
def cmd_generate_workplan(args: argparse.Namespace) -> None:
def action() -> dict[str, Any]:
if args.output and Path(args.output).expanduser().exists():
raise RenameCLIError(
f"Refusing to overwrite existing workplan {args.output}",
code="output_exists",
)
repo = _resolve_repo(args, args.old_slug)
workplans = _api_request(
args.api_base,
"GET",
f"/workplans/?repo_id={urllib.parse.quote(str(repo['id']), safe='')}",
expected=list,
)
if args.preflight_file:
wrapper = _load_json(args.preflight_file, expected=dict)
if (
wrapper.get("schema_version") != CLI_SCHEMA_VERSION
or wrapper.get("command") != "preflight"
or str(wrapper.get("repo_id")) != str(repo["id"])
):
raise RenameCLIError(
"Preflight file is not for this registered repository",
code="invalid_preflight_file",
)
report = wrapper.get("result") or {}
else:
report = _api_request(
args.api_base,
"POST",
f"/repos/{repo['id']}/rename/preflight",
{"new_slug": args.new_slug, "queued_edge_writes": []},
)
if (
report.get("old_slug") != args.old_slug
or report.get("new_slug") != args.new_slug
):
raise RenameCLIError(
"Preflight snapshot does not address the requested rename",
code="invalid_preflight_file",
)
try:
snapshot = build_generation_snapshot(
repo=repo,
preflight=report,
workplans=workplans,
repo_path_override=args.repo_path,
)
content = render_workplan(snapshot, owner=args.owner)
output = (
Path(args.output).expanduser().resolve()
if args.output
else default_output_path(snapshot)
)
write_workplan_exclusive(output, content)
except WorkplanGenerationError as exc:
raise RenameCLIError(str(exc), code=exc.code) from exc
return _envelope(
"generate-workplan",
state="achieved",
ok=True,
result={
"output": str(output),
"workplan_id": snapshot.workplan_id,
"workplan_prefix": snapshot.prefix,
"content_checksum": content_checksum(content),
"preflight_report_checksum": report.get("report_checksum"),
"safe_to_apply_at_capture": bool(report.get("safe_to_apply")),
"path_available": snapshot.path_available,
"state_hub_uuid_fields_written": [],
},
repo_id=str(repo["id"]),
next_safe_action={
"action": "review-generated-workplan",
"requires_confirmation": False,
"command": None,
},
)
_run(args, action)
def cmd_start(args: argparse.Namespace) -> None:
def action() -> dict[str, Any]:
operation_id = _validated_uuid(args.operation_id)
@ -763,6 +864,31 @@ def configure_repo_commands(subparsers: argparse._SubParsersAction) -> None:
_common(preflight)
preflight.set_defaults(func=cmd_preflight)
generate = rename_sub.add_parser(
"generate-workplan",
help="Generate a repository-native rename adoption workplan",
)
generate.add_argument("old_slug")
generate.add_argument("new_slug")
generate.add_argument(
"--preflight-file",
default=None,
help="Use a private mode-0600 preflight envelope instead of refreshing",
)
generate.add_argument(
"--repo-path",
default=None,
help="Visible target checkout used for workplan convention discovery",
)
generate.add_argument(
"--output",
default=None,
help="Exclusive output path; defaults to the target workplans directory",
)
generate.add_argument("--owner", default="codex")
_common(generate)
generate.set_defaults(func=cmd_generate_workplan)
start = rename_sub.add_parser(
"start",
help="Create an idempotent operation journal from a private preflight file",