feat: generate repository rename adoption plans
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
parent
bdf3a1fdf8
commit
ef0b6df3b8
7 changed files with 1294 additions and 5 deletions
|
|
@ -326,7 +326,7 @@
|
||||||
| task | STATE-WP-0085-T03 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T03 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
| task | STATE-WP-0085-T04 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T04 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
| task | STATE-WP-0085-T05 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T05 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
| task | STATE-WP-0085-T06 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T06 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
| task | STATE-WP-0085-T07 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T07 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
| task | STATE-WP-0085-T08 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T08 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
| task | STATE-WP-0085-T09 | wait | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
| task | STATE-WP-0085-T09 | wait | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ packages = ["api", "mcp_server", "task_flow_engine"]
|
||||||
artifacts = [
|
artifacts = [
|
||||||
"custodian_cli.py",
|
"custodian_cli.py",
|
||||||
"repository_rename_cli.py",
|
"repository_rename_cli.py",
|
||||||
|
"repository_rename_workplan.py",
|
||||||
"statehub_register.py",
|
"statehub_register.py",
|
||||||
"scripts/consistency_check.py",
|
"scripts/consistency_check.py",
|
||||||
"scripts/repo_sync.py",
|
"scripts/repo_sync.py",
|
||||||
|
|
@ -45,6 +46,7 @@ artifacts = [
|
||||||
[tool.hatch.build.targets.wheel.force-include]
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
"custodian_cli.py" = "custodian_cli.py"
|
"custodian_cli.py" = "custodian_cli.py"
|
||||||
"repository_rename_cli.py" = "repository_rename_cli.py"
|
"repository_rename_cli.py" = "repository_rename_cli.py"
|
||||||
|
"repository_rename_workplan.py" = "repository_rename_workplan.py"
|
||||||
"statehub_register.py" = "statehub_register.py"
|
"statehub_register.py" = "statehub_register.py"
|
||||||
"scripts/consistency_check.py" = "scripts/consistency_check.py"
|
"scripts/consistency_check.py" = "scripts/consistency_check.py"
|
||||||
"scripts/ensure_gitignore_claude_rules.py" = "scripts/ensure_gitignore_claude_rules.py"
|
"scripts/ensure_gitignore_claude_rules.py" = "scripts/ensure_gitignore_claude_rules.py"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,15 @@ import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
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"
|
CLI_SCHEMA_VERSION = "state-hub.repository-rename-cli.v1"
|
||||||
DEFAULT_API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000")
|
DEFAULT_API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000")
|
||||||
|
|
@ -120,7 +129,9 @@ def _api_request(
|
||||||
method: str,
|
method: str,
|
||||||
path: str,
|
path: str,
|
||||||
body: dict[str, Any] | None = None,
|
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
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
headers = dict(_HEADERS)
|
headers = dict(_HEADERS)
|
||||||
if data is not None:
|
if data is not None:
|
||||||
|
|
@ -165,9 +176,14 @@ def _api_request(
|
||||||
"State Hub returned an invalid JSON response",
|
"State Hub returned an invalid JSON response",
|
||||||
code="invalid_state_hub_response",
|
code="invalid_state_hub_response",
|
||||||
) from exc
|
) 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(
|
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",
|
code="invalid_state_hub_response",
|
||||||
)
|
)
|
||||||
return payload
|
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)
|
print(f" blockers: {len(result.get('blockers') or [])}", file=stream)
|
||||||
if result.get("preflight_file"):
|
if result.get("preflight_file"):
|
||||||
print(f" private preflight: {result['preflight_file']}", file=stream)
|
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")
|
error = public.get("error")
|
||||||
if error:
|
if error:
|
||||||
print(f" error: {error.get('message')}", file=stream)
|
print(f" error: {error.get('message')}", file=stream)
|
||||||
|
|
@ -490,6 +509,88 @@ def cmd_preflight(args: argparse.Namespace) -> None:
|
||||||
_run(args, action)
|
_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 cmd_start(args: argparse.Namespace) -> None:
|
||||||
def action() -> dict[str, Any]:
|
def action() -> dict[str, Any]:
|
||||||
operation_id = _validated_uuid(args.operation_id)
|
operation_id = _validated_uuid(args.operation_id)
|
||||||
|
|
@ -763,6 +864,31 @@ def configure_repo_commands(subparsers: argparse._SubParsersAction) -> None:
|
||||||
_common(preflight)
|
_common(preflight)
|
||||||
preflight.set_defaults(func=cmd_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 = rename_sub.add_parser(
|
||||||
"start",
|
"start",
|
||||||
help="Create an idempotent operation journal from a private preflight file",
|
help="Create an idempotent operation journal from a private preflight file",
|
||||||
|
|
|
||||||
724
repository_rename_workplan.py
Normal file
724
repository_rename_workplan.py
Normal file
|
|
@ -0,0 +1,724 @@
|
||||||
|
"""Deterministic repository-native workplan generation for repository renames."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from collections import Counter
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
_WORKPLAN_ID = re.compile(
|
||||||
|
r"^(?P<prefix>[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP)-"
|
||||||
|
r"(?P<number>\d+)$"
|
||||||
|
)
|
||||||
|
_DECLARED_PREFIX = re.compile(
|
||||||
|
r"workplan\s+prefix[^`\n]*`?(?P<prefix>[A-Z][A-Z0-9-]*-WP)-?`?",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_FRONTMATTER = re.compile(r"\A---\s*\n(?P<yaml>.*?)\n---(?:\s*\n|\Z)", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkplanGenerationError(RuntimeError):
|
||||||
|
"""Raised when a safe repository-native plan cannot be generated."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, *, code: str) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WorkplanGenerationSnapshot:
|
||||||
|
repo: dict[str, Any]
|
||||||
|
preflight: dict[str, Any]
|
||||||
|
workplans: tuple[dict[str, Any], ...]
|
||||||
|
prefix: str
|
||||||
|
workplan_id: str
|
||||||
|
task_width: int
|
||||||
|
topic_slug: str
|
||||||
|
repo_path: str | None
|
||||||
|
path_available: bool
|
||||||
|
observed_workplan_ids: tuple[str, ...]
|
||||||
|
archived_workplan_ids: tuple[str, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def old_slug(self) -> str:
|
||||||
|
return str(self.preflight["old_slug"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def new_slug(self) -> str:
|
||||||
|
return str(self.preflight["new_slug"])
|
||||||
|
|
||||||
|
|
||||||
|
def _frontmatter(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
return {}
|
||||||
|
match = _FRONTMATTER.match(text)
|
||||||
|
if match is None:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
value = yaml.safe_load(match.group("yaml")) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
return {}
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _local_metadata(repo_path: Path | None) -> tuple[list[dict[str, Any]], str | None]:
|
||||||
|
if repo_path is None or not repo_path.is_dir():
|
||||||
|
return [], None
|
||||||
|
declared: str | None = None
|
||||||
|
agents = repo_path / "AGENTS.md"
|
||||||
|
try:
|
||||||
|
match = _DECLARED_PREFIX.search(agents.read_text(encoding="utf-8"))
|
||||||
|
except OSError:
|
||||||
|
match = None
|
||||||
|
if match:
|
||||||
|
declared = match.group("prefix").upper().rstrip("-")
|
||||||
|
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
workplans = repo_path / "workplans"
|
||||||
|
if workplans.is_dir():
|
||||||
|
for path in sorted(workplans.rglob("*.md")):
|
||||||
|
meta = _frontmatter(path)
|
||||||
|
record_id = str(meta.get("id") or "").upper()
|
||||||
|
if not _WORKPLAN_ID.match(record_id):
|
||||||
|
filename = path.name
|
||||||
|
if re.match(r"^\d{6}-", filename):
|
||||||
|
filename = filename[7:]
|
||||||
|
filename_match = re.match(
|
||||||
|
r"^(?P<id>[A-Z][A-Z0-9-]*-WP-\d+)", filename
|
||||||
|
)
|
||||||
|
record_id = filename_match.group("id") if filename_match else ""
|
||||||
|
if _WORKPLAN_ID.match(record_id):
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"record_id": record_id,
|
||||||
|
"topic_slug": meta.get("topic_slug"),
|
||||||
|
"status": meta.get("status"),
|
||||||
|
"archived": "archived" in path.relative_to(workplans).parts,
|
||||||
|
"state_hub_workstream_id": meta.get(
|
||||||
|
"state_hub_workstream_id"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows, declared
|
||||||
|
|
||||||
|
|
||||||
|
def _record_id(row: dict[str, Any]) -> str | None:
|
||||||
|
candidates = (
|
||||||
|
row.get("record_id"),
|
||||||
|
row.get("slug"),
|
||||||
|
row.get("backing_filename"),
|
||||||
|
row.get("backing_relative_path"),
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
if not candidate:
|
||||||
|
continue
|
||||||
|
match = re.search(
|
||||||
|
r"([A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-WP-\d+)",
|
||||||
|
str(candidate).upper(),
|
||||||
|
)
|
||||||
|
if match and _WORKPLAN_ID.match(match.group(1)):
|
||||||
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _select_prefix(
|
||||||
|
records: Iterable[dict[str, Any]], declared_prefix: str | None
|
||||||
|
) -> tuple[str, list[str]]:
|
||||||
|
ids = sorted({record_id for row in records if (record_id := _record_id(row))})
|
||||||
|
matches = [_WORKPLAN_ID.match(record_id) for record_id in ids]
|
||||||
|
prefixes = Counter(
|
||||||
|
match.group("prefix") for match in matches if match is not None
|
||||||
|
)
|
||||||
|
if declared_prefix:
|
||||||
|
if prefixes and declared_prefix not in prefixes:
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
f"Declared workplan prefix {declared_prefix!r} conflicts with "
|
||||||
|
f"observed prefixes {sorted(prefixes)!r}",
|
||||||
|
code="workplan_prefix_conflict",
|
||||||
|
)
|
||||||
|
return declared_prefix, ids
|
||||||
|
if not prefixes:
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
"Repository has no established workplan prefix; scaffold or declare one first",
|
||||||
|
code="workplan_prefix_missing",
|
||||||
|
)
|
||||||
|
if len(prefixes) != 1:
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
f"Repository workplan prefix is ambiguous: {sorted(prefixes)!r}",
|
||||||
|
code="workplan_prefix_ambiguous",
|
||||||
|
)
|
||||||
|
return next(iter(prefixes)), ids
|
||||||
|
|
||||||
|
|
||||||
|
def _next_workplan_id(prefix: str, ids: Iterable[str]) -> str:
|
||||||
|
numbers: list[int] = []
|
||||||
|
widths: list[int] = []
|
||||||
|
for record_id in ids:
|
||||||
|
match = _WORKPLAN_ID.match(record_id)
|
||||||
|
if match and match.group("prefix") == prefix:
|
||||||
|
raw = match.group("number")
|
||||||
|
numbers.append(int(raw))
|
||||||
|
widths.append(len(raw))
|
||||||
|
width = max([4, *widths])
|
||||||
|
return f"{prefix}-{max(numbers, default=0) + 1:0{width}d}"
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_path(repo: dict[str, Any], override: str | None) -> Path | None:
|
||||||
|
candidates = [override, repo.get("local_path")]
|
||||||
|
candidates.extend((repo.get("host_paths") or {}).values())
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate:
|
||||||
|
path = Path(str(candidate)).expanduser()
|
||||||
|
if path.is_dir():
|
||||||
|
return path.resolve()
|
||||||
|
if override:
|
||||||
|
return Path(override).expanduser().resolve()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_generation_snapshot(
|
||||||
|
*,
|
||||||
|
repo: dict[str, Any],
|
||||||
|
preflight: dict[str, Any],
|
||||||
|
workplans: Iterable[dict[str, Any]],
|
||||||
|
repo_path_override: str | None = None,
|
||||||
|
) -> WorkplanGenerationSnapshot:
|
||||||
|
"""Normalize volatile API data into a deterministic render snapshot."""
|
||||||
|
try:
|
||||||
|
repo_id = str(uuid.UUID(str(repo.get("id"))))
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
"Registered repository is missing its stable State Hub UUID",
|
||||||
|
code="repository_identity_missing",
|
||||||
|
) from exc
|
||||||
|
if repo_id != str(preflight.get("repo_id")):
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
"Preflight repository UUID does not match the registered repository",
|
||||||
|
code="preflight_repository_mismatch",
|
||||||
|
)
|
||||||
|
if not preflight.get("old_slug") or not preflight.get("new_slug"):
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
"Preflight snapshot is missing old/new repository slugs",
|
||||||
|
code="invalid_preflight_snapshot",
|
||||||
|
)
|
||||||
|
if not re.match(
|
||||||
|
r"^\d{4}-\d{2}-\d{2}", str(preflight.get("preflighted_at") or "")
|
||||||
|
):
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
"Preflight snapshot is missing a stable capture date",
|
||||||
|
code="invalid_preflight_snapshot",
|
||||||
|
)
|
||||||
|
|
||||||
|
path = _repo_path(repo, repo_path_override)
|
||||||
|
local_rows, declared_prefix = _local_metadata(
|
||||||
|
path if path and path.is_dir() else None
|
||||||
|
)
|
||||||
|
api_rows = [dict(row) for row in workplans]
|
||||||
|
all_rows = [*api_rows, *local_rows]
|
||||||
|
prefix, ids = _select_prefix(all_rows, declared_prefix)
|
||||||
|
workplan_id = _next_workplan_id(prefix, ids)
|
||||||
|
|
||||||
|
topic_counts = Counter(
|
||||||
|
str(row.get("topic_slug"))
|
||||||
|
for row in local_rows
|
||||||
|
if row.get("topic_slug")
|
||||||
|
)
|
||||||
|
topic_slug = (
|
||||||
|
topic_counts.most_common(1)[0][0]
|
||||||
|
if topic_counts
|
||||||
|
else str(repo.get("domain_slug") or "repository-rename")
|
||||||
|
)
|
||||||
|
archived = sorted(
|
||||||
|
{
|
||||||
|
record_id
|
||||||
|
for row in all_rows
|
||||||
|
if (record_id := _record_id(row))
|
||||||
|
and (
|
||||||
|
bool(row.get("archived"))
|
||||||
|
or str(row.get("status", "")).lower() == "archived"
|
||||||
|
or bool(row.get("backing_archived"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return WorkplanGenerationSnapshot(
|
||||||
|
repo=dict(repo),
|
||||||
|
preflight=dict(preflight),
|
||||||
|
workplans=tuple(sorted(api_rows, key=lambda row: str(row.get("slug", "")))),
|
||||||
|
prefix=prefix,
|
||||||
|
workplan_id=workplan_id,
|
||||||
|
task_width=2,
|
||||||
|
topic_slug=topic_slug,
|
||||||
|
repo_path=str(path) if path else None,
|
||||||
|
path_available=bool(path and path.is_dir()),
|
||||||
|
observed_workplan_ids=tuple(ids),
|
||||||
|
archived_workplan_ids=tuple(archived),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _json_detail(item: dict[str, Any]) -> str:
|
||||||
|
safe = {
|
||||||
|
key: value
|
||||||
|
for key, value in item.items()
|
||||||
|
if key not in {"preflight_token", "authorization", "credential", "password"}
|
||||||
|
}
|
||||||
|
return json.dumps(safe, sort_keys=True, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def _risk_register(snapshot: WorkplanGenerationSnapshot) -> str:
|
||||||
|
report = snapshot.preflight
|
||||||
|
rows: list[tuple[str, str, str]] = []
|
||||||
|
for severity, key in (("blocker", "blockers"), ("warning", "warnings")):
|
||||||
|
for item in report.get(key) or []:
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
severity,
|
||||||
|
str(item.get("code") or "unnamed"),
|
||||||
|
_json_detail(item),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for handoff in (report.get("affected") or {}).get("external_handoffs") or []:
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
"external-handoff",
|
||||||
|
str(handoff),
|
||||||
|
"Owning repository must be recorded during inventory",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
forge_identity = (report.get("current") or {}).get("forge_identity") or {}
|
||||||
|
if forge_identity.get("verification_state") != "verified":
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
"blocker",
|
||||||
|
"forge-identity-unverified",
|
||||||
|
"Immutable Forge identity must be verified before mutation",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not (report.get("current") or {}).get("forge_available", False):
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
"blocker",
|
||||||
|
"forge-repository-unreadable",
|
||||||
|
"Private/unreadable Forge state cannot be inferred from a local clone",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not snapshot.path_available:
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
"warning",
|
||||||
|
"local-path-unavailable",
|
||||||
|
"Generation used indexed workplans; baseline needs a visible checkout",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
rows.append(("none", "no-preflight-risks", "No blocker or warning reported"))
|
||||||
|
return "\n".join(
|
||||||
|
f"| {severity} | `{code}` | `{detail.replace('|', '\\|')}` |"
|
||||||
|
for severity, code, detail in sorted(rows)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _active_work(snapshot: WorkplanGenerationSnapshot) -> str:
|
||||||
|
active = snapshot.preflight.get("active_work") or {}
|
||||||
|
rows: list[str] = []
|
||||||
|
for workplan in sorted(active.get("workplans") or [], key=lambda row: str(row.get("slug"))):
|
||||||
|
rows.append(
|
||||||
|
f"- workplan `{workplan.get('slug')}` / `{workplan.get('id')}` "
|
||||||
|
f"is `{workplan.get('status')}`"
|
||||||
|
)
|
||||||
|
for task in sorted(active.get("tasks") or [], key=lambda row: str(row.get("id"))):
|
||||||
|
rows.append(
|
||||||
|
f"- task `{task.get('record_id') or task.get('id')}` / `{task.get('id')}` "
|
||||||
|
f"is `{task.get('status')}`"
|
||||||
|
)
|
||||||
|
return "\n".join(rows) if rows else "- No active work was reported by preflight."
|
||||||
|
|
||||||
|
|
||||||
|
def _task(snapshot: WorkplanGenerationSnapshot, number: int, title: str, status: str) -> str:
|
||||||
|
task_id = f"{snapshot.workplan_id}-T{number:0{snapshot.task_width}d}"
|
||||||
|
return (
|
||||||
|
f"## {number}. {title}\n\n"
|
||||||
|
"```task\n"
|
||||||
|
f"id: {task_id}\n"
|
||||||
|
f"status: {status}\n"
|
||||||
|
"priority: high\n"
|
||||||
|
"```\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_workplan(snapshot: WorkplanGenerationSnapshot, *, owner: str) -> str:
|
||||||
|
"""Render the adoption plan. No current clock or random value is consulted."""
|
||||||
|
report = snapshot.preflight
|
||||||
|
repo = snapshot.repo
|
||||||
|
old_slug = snapshot.old_slug
|
||||||
|
new_slug = snapshot.new_slug
|
||||||
|
current = report.get("current") or {}
|
||||||
|
statehub = current.get("statehub") or {}
|
||||||
|
forge = current.get("forge") or {}
|
||||||
|
forge_identity = current.get("forge_identity") or {}
|
||||||
|
preflight_at = str(report.get("preflighted_at") or "")[:10]
|
||||||
|
created = preflight_at
|
||||||
|
source_commit = forge.get("head_commit") or "UNAVAILABLE"
|
||||||
|
forge_id = (
|
||||||
|
forge_identity.get("forge_repository_id")
|
||||||
|
or forge.get("repository_id")
|
||||||
|
or "UNVERIFIED"
|
||||||
|
)
|
||||||
|
aliases = sorted(
|
||||||
|
{
|
||||||
|
str(alias)
|
||||||
|
for alias in [*(repo.get("aliases") or []), old_slug]
|
||||||
|
if alias
|
||||||
|
}
|
||||||
|
)
|
||||||
|
safe = bool(report.get("safe_to_apply"))
|
||||||
|
status = "ready" if safe and source_commit != "UNAVAILABLE" else "proposed"
|
||||||
|
reviewed = (
|
||||||
|
f'reviewed_at: "{created}"\n'
|
||||||
|
"reviewed_by: statehub-repository-rename-generator\n"
|
||||||
|
f'reviewed_against_commit: "{source_commit}"\n'
|
||||||
|
if source_commit != "UNAVAILABLE"
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
confirmation = f"rename:{repo['id']}:{old_slug}:{new_slug}"
|
||||||
|
output = f'''---
|
||||||
|
id: {snapshot.workplan_id}
|
||||||
|
type: workplan
|
||||||
|
title: "Repository identity migration from {old_slug} to {new_slug}"
|
||||||
|
domain: {json.dumps(str(repo.get("domain_slug") or "unknown"))}
|
||||||
|
repo: {json.dumps(old_slug)}
|
||||||
|
status: {status}
|
||||||
|
owner: {json.dumps(owner)}
|
||||||
|
topic_slug: {json.dumps(snapshot.topic_slug)}
|
||||||
|
created: "{created}"
|
||||||
|
updated: "{created}"
|
||||||
|
{reviewed}quality_dor: DoR-Ok
|
||||||
|
quality_dor_at: "{created}"
|
||||||
|
quality_dor_by: statehub-repository-rename-generator
|
||||||
|
---
|
||||||
|
|
||||||
|
# {snapshot.workplan_id} — {old_slug} to {new_slug}
|
||||||
|
|
||||||
|
## Goal and authority boundary
|
||||||
|
|
||||||
|
Adopt the new repository coordinate `{new_slug}` while preserving State Hub
|
||||||
|
repository UUID `{repo['id']}`, Forge repository ID `{forge_id}`, source commit
|
||||||
|
`{source_commit}`, work-record identities, and telemetry relationships.
|
||||||
|
|
||||||
|
This plan changes repository coordinates only. Product/runtime names are a
|
||||||
|
separate explicit decision. No task in this file may close work owned by
|
||||||
|
another repository: it records the external handoff ID and waits for evidence
|
||||||
|
from that owning repository.
|
||||||
|
|
||||||
|
Live Forgejo rename, State Hub rebind, rollback, and old-checkout cleanup are
|
||||||
|
Red-lane actions. They require the exact confirmations below and recorded
|
||||||
|
human approval; redirects are compatibility evidence, not completion.
|
||||||
|
|
||||||
|
## Captured preflight
|
||||||
|
|
||||||
|
- schema: `{report.get('schema_version')}`
|
||||||
|
- report checksum: `{report.get('report_checksum') or 'UNAVAILABLE'}`
|
||||||
|
- safe to apply at capture: `{str(safe).lower()}`
|
||||||
|
- State Hub repository UUID: `{repo['id']}`
|
||||||
|
- Forge repository ID: `{forge_id}`
|
||||||
|
- default branch: `{forge.get('default_branch') or 'UNAVAILABLE'}`
|
||||||
|
- source commit: `{source_commit}`
|
||||||
|
- registered local path: `{statehub.get('local_path') or repo.get('local_path') or 'UNAVAILABLE'}`
|
||||||
|
- host paths: `{json.dumps(statehub.get('host_paths') or repo.get('host_paths') or {}, sort_keys=True)}`
|
||||||
|
- protected/current aliases: `{json.dumps(aliases)}`
|
||||||
|
- existing workplans observed: `{len(snapshot.observed_workplan_ids)}`
|
||||||
|
- archived workplans observed: `{len(snapshot.archived_workplan_ids)}`
|
||||||
|
|
||||||
|
The preflight token is deliberately not stored in this workplan. Generate a
|
||||||
|
fresh private mode-0600 preflight file immediately before execution.
|
||||||
|
|
||||||
|
## Preflight risk register
|
||||||
|
|
||||||
|
Every blocker, warning, and external handoff from the captured snapshot is
|
||||||
|
retained here. T05 must reconcile every row; unknown ownership is a blocker.
|
||||||
|
|
||||||
|
| Severity | Code | Captured detail |
|
||||||
|
| --- | --- | --- |
|
||||||
|
{_risk_register(snapshot)}
|
||||||
|
|
||||||
|
## Active-work coordination snapshot
|
||||||
|
|
||||||
|
{_active_work(snapshot)}
|
||||||
|
|
||||||
|
## External ownership ledger
|
||||||
|
|
||||||
|
| Effect | Owning repository | What this plan may claim |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| State Hub identity and aliases | `state-hub` | Record operation phase/evidence only; State Hub owns the mutation. |
|
||||||
|
| Forge-derived fabric projection | `railiance-fabric` | Record a handoff ID; only that repository closes its source change. |
|
||||||
|
| Credential-route catalog | `ops-warden` | Record route-review handoff; never request or store a secret here. |
|
||||||
|
| SBOM projection | `sbom-nexus` | Record re-ingestion evidence; SBOM Nexus owns its projection. |
|
||||||
|
| CI, package, deployment, policy, and runtime consumers | `<owning-repository>` | T04 must replace this with one exact repository slug and handoff ID per consumer. |
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
output += _task(snapshot, 1, "Capture immutable cleanliness and identity baseline", "todo")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}`.
|
||||||
|
|
||||||
|
- Require a clean target checkout and every branch/tag/change secured to Forgejo.
|
||||||
|
- Record `git status`, branch, remote, source commit, default branch, Forge ID,
|
||||||
|
visibility/readability, protected branches, releases, packages, hooks, deploy
|
||||||
|
keys, Actions variables, and redirects.
|
||||||
|
- Reconcile active work above: quiesce it or record an explicit concurrent-work
|
||||||
|
decision. A moved source commit requires a new preflight.
|
||||||
|
- If no registered path is visible, stop and register/verify a checkout; do not
|
||||||
|
infer private Forge state from local Git.
|
||||||
|
|
||||||
|
Gate: repository UUID, Forge ID, branch, commit, and clean-state evidence are
|
||||||
|
recorded and match a fresh State Hub preflight.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 2, "Prepare repository metadata and work-record frontmatter", "todo")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}`.
|
||||||
|
|
||||||
|
- Prepare repository metadata, README/INTENT/SCOPE/AGENTS references, Forge
|
||||||
|
description/topics, and canonical clone coordinates for `{new_slug}`.
|
||||||
|
- Keep the established `{snapshot.prefix}-` workplan/task prefix and all existing
|
||||||
|
`state_hub_workstream_id` / `state_hub_task_id` values unchanged.
|
||||||
|
- Do not mass-rewrite historical prose or old-slug provenance. New live
|
||||||
|
frontmatter may adopt `repo: {new_slug}` only after State Hub rebind.
|
||||||
|
- Commit preparatory source changes before the final preflight; record the new
|
||||||
|
intended source commit.
|
||||||
|
|
||||||
|
Gate: work-record parsing succeeds and no existing UUID field was removed,
|
||||||
|
replaced, or invented.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 3, "Decide product and runtime naming separately", "todo")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}`.
|
||||||
|
|
||||||
|
Record explicit keep/rename decisions for the binary, Go module/import path,
|
||||||
|
`FLEX_AUTH_*` environment variables, Kubernetes namespace, service/DNS names,
|
||||||
|
Helm release/chart, container/package coordinates, policy vocabulary, API
|
||||||
|
names, dashboards, and telemetry service labels. Repository rename approval
|
||||||
|
does not authorize any of these runtime/product changes.
|
||||||
|
|
||||||
|
Gate: every item has a decision record and independently deployable changes
|
||||||
|
have their own workplan or residual handoff.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 4, "Inventory consumers and create owned handoffs", "todo")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}` for inventory and handoff creation only.
|
||||||
|
|
||||||
|
- Inventory CI includes/actions, package and image publishers, deployments,
|
||||||
|
GitOps/Helm/Kubernetes references, credential routes (using `warden route`),
|
||||||
|
authorization and NetKingdom policy consumers, fabric sources, SBOM scans,
|
||||||
|
docs, badges, webhooks, mirrors, caches, dashboards, alerts, and local clones.
|
||||||
|
- For each external effect, replace `<owning-repository>` in the ledger with the
|
||||||
|
exact repository slug and create a live residual/intake/workplan there.
|
||||||
|
- Record each external work-record ID here. Do not mark that external work done
|
||||||
|
from this repository; completion evidence must come from its owner.
|
||||||
|
- Include every row in the preflight risk register, even when it is only a
|
||||||
|
warning or currently zero-count projection.
|
||||||
|
|
||||||
|
Gate: every discovered external change has one named owning repository and
|
||||||
|
durable handoff ID; unknown ownership blocks the live rename.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 5, "Renew State Hub preflight and record approval", "todo")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
statehub repo rename preflight {old_slug} {new_slug} \\
|
||||||
|
--operation-id <operation-id> \\
|
||||||
|
--output <private-preflight.json> --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify zero blockers, immutable IDs/commit, active-work disposition, target
|
||||||
|
availability, every risk-register row, and no queued edge writes. Record a
|
||||||
|
human decision approving exact operation ID, source commit, target slug, owner,
|
||||||
|
window, rollback limits, and confirmation `{confirmation}`.
|
||||||
|
|
||||||
|
Gate: a current private preflight and explicit human approval exist. Never
|
||||||
|
commit the private preflight file or its token.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 6, "Execute the Forgejo repository rename", "wait")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}`; human approval required.
|
||||||
|
|
||||||
|
Start/retry the durable journal, then apply exactly the Forge phase:
|
||||||
|
|
||||||
|
```text
|
||||||
|
statehub repo rename start {old_slug} {new_slug} \\
|
||||||
|
--operation-id <operation-id> --preflight-file <private-preflight.json> \\
|
||||||
|
--actor <actor> --confirm '{confirmation}' --json
|
||||||
|
statehub repo rename apply <operation-id> --phase forge-renamed \\
|
||||||
|
--confirm '{confirmation}' --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm Forgejo reports the same numeric repository ID and commit under
|
||||||
|
`{new_slug}`. Stop on identity drift, unreadability, target conflict, or moved
|
||||||
|
source; do not create a second State Hub repository.
|
||||||
|
|
||||||
|
Gate: operation journal is `forge-renamed` with immutable identity evidence.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 7, "Record the State Hub identity rebind", "wait")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{old_slug}` for coordination; `state-hub` owns the mutation.
|
||||||
|
|
||||||
|
```text
|
||||||
|
statehub repo rename apply <operation-id> --phase statehub-rebound \\
|
||||||
|
--confirm '{confirmation}' --json
|
||||||
|
statehub repo rename status <operation-id> --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Record State Hub's phase evidence. Verify both `{old_slug}` and `{new_slug}`
|
||||||
|
resolve to repository UUID `{repo['id']}`, `{new_slug}` is canonical, and the old
|
||||||
|
slug is a protected alias. Do not manually update the database.
|
||||||
|
|
||||||
|
Gate: State Hub journal is `statehub-rebound`; IDs and historical relationships
|
||||||
|
are unchanged.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 8, "Establish and register a fresh canonical clone", "wait")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{new_slug}` after rebind.
|
||||||
|
|
||||||
|
- Preserve the old checkout until verification and rollback decisions finish.
|
||||||
|
- Clone `{new_slug}` into a new path; verify origin and Forge numeric ID before
|
||||||
|
trusting redirects.
|
||||||
|
- Register the fresh path against the existing State Hub UUID, update canonical
|
||||||
|
source metadata/frontmatter, commit the source-synchronization revision, and
|
||||||
|
run `statehub fix-consistency` from the new clone.
|
||||||
|
- Apply `source-synced` with mode-0600 evidence naming clone path, remote, head,
|
||||||
|
registration result, and consistency result.
|
||||||
|
|
||||||
|
Gate: no duplicate repository registration exists and the journal is
|
||||||
|
`source-synced`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(
|
||||||
|
snapshot,
|
||||||
|
9,
|
||||||
|
"Verify identity, history, routes, builds, and deployments",
|
||||||
|
"wait",
|
||||||
|
)
|
||||||
|
output += f'''
|
||||||
|
Owner: `{new_slug}` for aggregation; each external owner supplies its evidence.
|
||||||
|
|
||||||
|
Compare relationship checksums—not only counts—for repository UUID, workplans,
|
||||||
|
tasks, progress, decisions, token events/totals, SBOM snapshots, services,
|
||||||
|
capabilities, messages, aliases, bindings, and active dispatch. Verify old/new
|
||||||
|
routes, Forge ID/commit, clean builds/tests, package/image publication, policy
|
||||||
|
and credential routes, fabric/SBOM projections, deployments, health, dashboards,
|
||||||
|
alerts, and telemetry continuity.
|
||||||
|
|
||||||
|
```text
|
||||||
|
statehub repo rename verify <operation-id> --json
|
||||||
|
statehub repo rename apply <operation-id> --phase consumers-verified \\
|
||||||
|
--checks-file <private-checks.json> --evidence-file <private-evidence.json> \\
|
||||||
|
--confirm '{confirmation}' --json
|
||||||
|
statehub repo rename apply <operation-id> --phase completed \\
|
||||||
|
--confirm '{confirmation}' --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Gate: every required local check passes and every external handoff has evidence
|
||||||
|
from its owning repository before completion.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 10, "Exercise rollback decision points", "wait")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{new_slug}` for the decision; State Hub and Forge owners execute their
|
||||||
|
own reversible phases.
|
||||||
|
|
||||||
|
At every phase decide continue, pause safely, or enter rollback preflight.
|
||||||
|
Rollback is allowed only if the old Forge slug remains available and named
|
||||||
|
external effects are reversible. It never deletes aliases or history.
|
||||||
|
|
||||||
|
```text
|
||||||
|
statehub repo rename rollback <operation-id> \\
|
||||||
|
--confirm 'rollback:<operation-id>' --json
|
||||||
|
# Execute only after reviewing safe_to_rollback and irreversible handoffs:
|
||||||
|
statehub repo rename rollback <operation-id> \\
|
||||||
|
--confirm 'rollback:<operation-id>' --execute --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Gate: the forward completion or rolled-back terminal state is explicit; no
|
||||||
|
generic error is treated as proof of rollback.
|
||||||
|
|
||||||
|
'''
|
||||||
|
output += _task(snapshot, 11, "Soak, hand off residuals, and clean up the old checkout", "wait")
|
||||||
|
output += f'''
|
||||||
|
Owner: `{new_slug}`; destructive cleanup requires separate human approval.
|
||||||
|
|
||||||
|
- Define and observe a soak window covering deployments, policy decisions,
|
||||||
|
alerts, telemetry, packages, automation, and old-slug compatibility reads.
|
||||||
|
- Convert every unresolved item into a live residual (`origin: residual`,
|
||||||
|
`origin_ref: {snapshot.workplan_id}`) in its owning repository before finishing.
|
||||||
|
- Retain the protected `{old_slug}` alias. Alias retirement is out of scope.
|
||||||
|
- Only after terminal verification, soak, residual handoff, and explicit cleanup
|
||||||
|
approval may the old local checkout be removed. Record what was removed and
|
||||||
|
whether recovery remains possible from Forgejo.
|
||||||
|
|
||||||
|
Gate: no actionable residual exists only in prose, the new clone is canonical,
|
||||||
|
and old-checkout cleanup evidence is recorded.
|
||||||
|
'''
|
||||||
|
return output.rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def default_output_path(snapshot: WorkplanGenerationSnapshot) -> Path:
|
||||||
|
if not snapshot.path_available or not snapshot.repo_path:
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
"No registered local checkout is visible; pass --output explicitly",
|
||||||
|
code="repository_path_unavailable",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
Path(snapshot.repo_path)
|
||||||
|
/ "workplans"
|
||||||
|
/ f"{snapshot.workplan_id}-repository-rename-to-{snapshot.new_slug}.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_workplan_exclusive(path: Path, content: str) -> None:
|
||||||
|
"""Create a workplan exactly once; overwriting is intentionally unsupported."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
|
||||||
|
except FileExistsError as exc:
|
||||||
|
raise WorkplanGenerationError(
|
||||||
|
f"Refusing to overwrite existing workplan {path}",
|
||||||
|
code="output_exists",
|
||||||
|
) from exc
|
||||||
|
try:
|
||||||
|
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(content)
|
||||||
|
except Exception:
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def content_checksum(content: str) -> str:
|
||||||
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"WorkplanGenerationError",
|
||||||
|
"WorkplanGenerationSnapshot",
|
||||||
|
"build_generation_snapshot",
|
||||||
|
"content_checksum",
|
||||||
|
"default_output_path",
|
||||||
|
"render_workplan",
|
||||||
|
"write_workplan_exclusive",
|
||||||
|
]
|
||||||
|
|
@ -388,3 +388,86 @@ def test_human_status_distinguishes_lifecycle_states(
|
||||||
_run(monkeypatch, "repo", "rename", "status", OPERATION_ID)
|
_run(monkeypatch, "repo", "rename", "status", OPERATION_ID)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert f"{label}: repository rename status" in captured.out + captured.err
|
assert f"{label}: repository rename status" in captured.out + captured.err
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_workplan_cli_writes_once_and_reports_contract(
|
||||||
|
monkeypatch, tmp_path, capsys
|
||||||
|
):
|
||||||
|
repo_root = tmp_path / "flex-auth"
|
||||||
|
repo_root.mkdir()
|
||||||
|
(repo_root / "AGENTS.md").write_text(
|
||||||
|
"**Workplan prefix:** `FLEX-WP-`\n"
|
||||||
|
)
|
||||||
|
output = repo_root / "workplans" / "rename.md"
|
||||||
|
report = {
|
||||||
|
"schema_version": "state-hub.repository-rename-preflight.v1",
|
||||||
|
"repo_id": REPO_ID,
|
||||||
|
"old_slug": "flex-auth",
|
||||||
|
"new_slug": "access-engine",
|
||||||
|
"safe_to_apply": True,
|
||||||
|
"blockers": [],
|
||||||
|
"warnings": [],
|
||||||
|
"report_checksum": "a" * 64,
|
||||||
|
"preflight_token": "private-token",
|
||||||
|
"preflighted_at": "2026-08-29T08:00:00Z",
|
||||||
|
"current": {
|
||||||
|
"statehub": {"local_path": str(repo_root), "host_paths": {}},
|
||||||
|
"forge_identity": {
|
||||||
|
"verification_state": "verified",
|
||||||
|
"forge_repository_id": 42,
|
||||||
|
},
|
||||||
|
"forge_available": True,
|
||||||
|
"forge": {
|
||||||
|
"repository_id": 42,
|
||||||
|
"default_branch": "main",
|
||||||
|
"head_commit": "b" * 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"active_work": {"workplans": [], "tasks": []},
|
||||||
|
"affected": {"external_handoffs": []},
|
||||||
|
}
|
||||||
|
repo = {**_repo(), "local_path": str(repo_root), "host_paths": {}}
|
||||||
|
workplans = [
|
||||||
|
{
|
||||||
|
"slug": "flex-wp-0018",
|
||||||
|
"backing_filename": "FLEX-WP-0018-last.md",
|
||||||
|
"status": "finished",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def request(_api_base, method, path, body=None, *, expected=dict):
|
||||||
|
del body
|
||||||
|
if path.startswith("/repos/flex-auth"):
|
||||||
|
return repo
|
||||||
|
if path.startswith("/workplans/"):
|
||||||
|
assert expected is list
|
||||||
|
return workplans
|
||||||
|
assert method == "POST"
|
||||||
|
return report
|
||||||
|
|
||||||
|
monkeypatch.setattr(rename_cli, "_api_request", request)
|
||||||
|
args = (
|
||||||
|
"repo",
|
||||||
|
"rename",
|
||||||
|
"generate-workplan",
|
||||||
|
"flex-auth",
|
||||||
|
"access-engine",
|
||||||
|
"--repo-path",
|
||||||
|
str(repo_root),
|
||||||
|
"--output",
|
||||||
|
str(output),
|
||||||
|
"--json",
|
||||||
|
)
|
||||||
|
_run(monkeypatch, *args)
|
||||||
|
result = json.loads(capsys.readouterr().out)
|
||||||
|
assert result["state"] == "achieved"
|
||||||
|
assert result["result"]["workplan_id"] == "FLEX-WP-0019"
|
||||||
|
assert result["result"]["workplan_prefix"] == "FLEX-WP"
|
||||||
|
assert result["result"]["state_hub_uuid_fields_written"] == []
|
||||||
|
assert "private-token" not in output.read_text()
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc:
|
||||||
|
_run(monkeypatch, *args)
|
||||||
|
assert exc.value.code == 1
|
||||||
|
refused = json.loads(capsys.readouterr().out)
|
||||||
|
assert refused["error"]["code"] == "output_exists"
|
||||||
|
|
|
||||||
323
tests/test_repository_rename_workplan.py
Normal file
323
tests/test_repository_rename_workplan.py
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from repository_rename_workplan import (
|
||||||
|
WorkplanGenerationError,
|
||||||
|
build_generation_snapshot,
|
||||||
|
content_checksum,
|
||||||
|
default_output_path,
|
||||||
|
render_workplan,
|
||||||
|
write_workplan_exclusive,
|
||||||
|
)
|
||||||
|
from scripts.quality_debt import collect as collect_quality_debt
|
||||||
|
from scripts.validate_repo_adr import validate
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ID = "fda8ad85-a7d7-4055-8f21-902a533e59df"
|
||||||
|
|
||||||
|
|
||||||
|
def _repo(path: str | None = None, **extra) -> dict:
|
||||||
|
return {
|
||||||
|
"id": REPO_ID,
|
||||||
|
"slug": "flex-auth",
|
||||||
|
"canonical_slug": "flex-auth",
|
||||||
|
"domain_slug": "infotech",
|
||||||
|
"local_path": path,
|
||||||
|
"host_paths": {},
|
||||||
|
"aliases": [],
|
||||||
|
**extra,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _preflight(**extra) -> dict:
|
||||||
|
report = {
|
||||||
|
"schema_version": "state-hub.repository-rename-preflight.v1",
|
||||||
|
"repo_id": REPO_ID,
|
||||||
|
"old_slug": "flex-auth",
|
||||||
|
"new_slug": "access-engine",
|
||||||
|
"safe_to_apply": True,
|
||||||
|
"blockers": [],
|
||||||
|
"warnings": [],
|
||||||
|
"report_checksum": "a" * 64,
|
||||||
|
"preflight_token": "must-never-appear-in-workplan",
|
||||||
|
"preflighted_at": "2026-08-29T08:00:00Z",
|
||||||
|
"current": {
|
||||||
|
"statehub": {
|
||||||
|
"repo_id": REPO_ID,
|
||||||
|
"local_path": None,
|
||||||
|
"host_paths": {},
|
||||||
|
},
|
||||||
|
"forge_identity": {
|
||||||
|
"verification_state": "verified",
|
||||||
|
"forge_repository_id": 42,
|
||||||
|
"forge_owner": "coulomb",
|
||||||
|
},
|
||||||
|
"forge_available": True,
|
||||||
|
"forge": {
|
||||||
|
"repository_id": 42,
|
||||||
|
"default_branch": "main",
|
||||||
|
"head_commit": "b" * 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"active_work": {"workplans": [], "tasks": []},
|
||||||
|
"affected": {"external_handoffs": []},
|
||||||
|
}
|
||||||
|
report.update(extra)
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def _workplans() -> list[dict]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": "11111111-1111-4111-8111-111111111111",
|
||||||
|
"slug": "flex-wp-0017",
|
||||||
|
"status": "active",
|
||||||
|
"backing_filename": "FLEX-WP-0017-action-contract.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "22222222-2222-4222-8222-222222222222",
|
||||||
|
"slug": "flex-wp-0018",
|
||||||
|
"status": "archived",
|
||||||
|
"backing_filename": "260828-FLEX-WP-0018-inbound-corrections.md",
|
||||||
|
"backing_archived": True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _repo_root(tmp_path: Path) -> Path:
|
||||||
|
root = tmp_path / "flex-auth"
|
||||||
|
root.mkdir()
|
||||||
|
(root / "AGENTS.md").write_text("**Workplan prefix:** `FLEX-WP-`\n")
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def test_flex_auth_snapshot_is_deterministic_and_complete(tmp_path):
|
||||||
|
root = _repo_root(tmp_path)
|
||||||
|
report = _preflight(
|
||||||
|
warnings=[
|
||||||
|
{
|
||||||
|
"code": "active_work_present",
|
||||||
|
"message": "coordinate NetKingdom security-stack work",
|
||||||
|
"workplan_count": 1,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
affected={
|
||||||
|
"external_handoffs": [
|
||||||
|
"fabric-graph-projections",
|
||||||
|
"interface-change-consumers",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
active_work={
|
||||||
|
"workplans": [
|
||||||
|
{
|
||||||
|
"id": "33333333-3333-4333-8333-333333333333",
|
||||||
|
"slug": "FLEX-WP-0017",
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "44444444-4444-4444-8444-444444444444",
|
||||||
|
"record_id": "FLEX-WP-0017-T05",
|
||||||
|
"status": "progress",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root), aliases=["flexauth-legacy"]),
|
||||||
|
preflight=report,
|
||||||
|
workplans=_workplans(),
|
||||||
|
)
|
||||||
|
first = render_workplan(snapshot, owner="codex")
|
||||||
|
second = render_workplan(snapshot, owner="codex")
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
assert content_checksum(first) == hashlib.sha256(first.encode()).hexdigest()
|
||||||
|
assert snapshot.prefix == "FLEX-WP"
|
||||||
|
assert snapshot.workplan_id == "FLEX-WP-0019"
|
||||||
|
assert first.count("```task\n") == 11
|
||||||
|
assert "id: FLEX-WP-0019-T06\nstatus: wait" in first
|
||||||
|
assert "id: FLEX-WP-0019-T11\nstatus: wait" in first
|
||||||
|
assert "state_hub_workstream_id:" not in first
|
||||||
|
assert "state_hub_task_id:" not in first
|
||||||
|
assert "must-never-appear-in-workplan" not in first
|
||||||
|
assert "`active_work_present`" in first
|
||||||
|
assert "`fabric-graph-projections`" in first
|
||||||
|
assert "`interface-change-consumers`" in first
|
||||||
|
assert "FLEX_AUTH_*" in first
|
||||||
|
assert "NetKingdom policy consumers" in first
|
||||||
|
assert "flexauth-legacy" in first
|
||||||
|
assert "| Credential-route catalog | `ops-warden` |" in first
|
||||||
|
assert "only that repository closes" in first
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_snapshot_passes_parser_and_quality_debt(tmp_path):
|
||||||
|
root = _repo_root(tmp_path)
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root)),
|
||||||
|
preflight=_preflight(),
|
||||||
|
workplans=_workplans(),
|
||||||
|
)
|
||||||
|
content = render_workplan(snapshot, owner="codex")
|
||||||
|
output = default_output_path(snapshot)
|
||||||
|
write_workplan_exclusive(output, content)
|
||||||
|
|
||||||
|
report = validate(root, skip_api=True)
|
||||||
|
assert report.failures == []
|
||||||
|
assert collect_quality_debt(
|
||||||
|
root, api_base=None, include_hub_intakes=False
|
||||||
|
) == []
|
||||||
|
assert output.name.startswith("FLEX-WP-0019-")
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_counts_archived_workplans_when_allocating_id(tmp_path):
|
||||||
|
root = _repo_root(tmp_path)
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root)),
|
||||||
|
preflight=_preflight(),
|
||||||
|
workplans=[
|
||||||
|
{
|
||||||
|
"slug": "flex-wp-0004",
|
||||||
|
"status": "active",
|
||||||
|
"backing_filename": "FLEX-WP-0004-live.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "flex-wp-0099",
|
||||||
|
"status": "archived",
|
||||||
|
"backing_filename": "260101-FLEX-WP-0099-old.md",
|
||||||
|
"backing_archived": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert snapshot.workplan_id == "FLEX-WP-0100"
|
||||||
|
assert snapshot.archived_workplan_ids == ("FLEX-WP-0099",)
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_renders_active_work_and_private_forge_failure(tmp_path):
|
||||||
|
root = _repo_root(tmp_path)
|
||||||
|
report = _preflight(
|
||||||
|
safe_to_apply=False,
|
||||||
|
blockers=[
|
||||||
|
{
|
||||||
|
"code": "forge_unreadable",
|
||||||
|
"message": "private repository could not be inspected",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
current={
|
||||||
|
"statehub": {"repo_id": REPO_ID, "host_paths": {}},
|
||||||
|
"forge_identity": {"verification_state": "unverified"},
|
||||||
|
"forge_available": False,
|
||||||
|
"forge": None,
|
||||||
|
},
|
||||||
|
active_work={
|
||||||
|
"workplans": [
|
||||||
|
{"id": "active-uuid", "slug": "FLEX-WP-0017", "status": "active"}
|
||||||
|
],
|
||||||
|
"tasks": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root)), preflight=report, workplans=_workplans()
|
||||||
|
)
|
||||||
|
rendered = render_workplan(snapshot, owner="codex")
|
||||||
|
assert "status: proposed" in rendered
|
||||||
|
assert "`forge_unreadable`" in rendered
|
||||||
|
assert "`forge-identity-unverified`" in rendered
|
||||||
|
assert "`forge-repository-unreadable`" in rendered
|
||||||
|
assert "workplan `FLEX-WP-0017` / `active-uuid` is `active`" in rendered
|
||||||
|
assert "id: FLEX-WP-0019-T06\nstatus: wait" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_supports_missing_local_path_from_indexed_workplans(tmp_path):
|
||||||
|
missing = tmp_path / "not-mounted"
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(missing)),
|
||||||
|
preflight=_preflight(),
|
||||||
|
workplans=_workplans(),
|
||||||
|
)
|
||||||
|
rendered = render_workplan(snapshot, owner="codex")
|
||||||
|
assert snapshot.prefix == "FLEX-WP"
|
||||||
|
assert snapshot.path_available is False
|
||||||
|
assert "`local-path-unavailable`" in rendered
|
||||||
|
with pytest.raises(WorkplanGenerationError) as exc:
|
||||||
|
default_output_path(snapshot)
|
||||||
|
assert exc.value.code == "repository_path_unavailable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_preserves_old_alias_context(tmp_path):
|
||||||
|
root = _repo_root(tmp_path)
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root), aliases=["old-flex", "flex-auth"]),
|
||||||
|
preflight=_preflight(),
|
||||||
|
workplans=_workplans(),
|
||||||
|
)
|
||||||
|
rendered = render_workplan(snapshot, owner="codex")
|
||||||
|
assert 'protected/current aliases: `["flex-auth", "old-flex"]`' in rendered
|
||||||
|
assert "Retain the protected `flex-auth` alias" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_uuid_fields_are_untouched_and_not_copied_to_new_plan(tmp_path):
|
||||||
|
root = _repo_root(tmp_path)
|
||||||
|
workplans = root / "workplans"
|
||||||
|
workplans.mkdir()
|
||||||
|
existing = workplans / "FLEX-WP-0001-existing.md"
|
||||||
|
original = """---
|
||||||
|
id: FLEX-WP-0001
|
||||||
|
type: workplan
|
||||||
|
title: Existing
|
||||||
|
domain: infotech
|
||||||
|
status: active
|
||||||
|
owner: codex
|
||||||
|
created: "2026-01-01"
|
||||||
|
state_hub_workstream_id: "55555555-5555-4555-8555-555555555555"
|
||||||
|
---
|
||||||
|
"""
|
||||||
|
existing.write_text(original)
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root)), preflight=_preflight(), workplans=[]
|
||||||
|
)
|
||||||
|
rendered = render_workplan(snapshot, owner="codex")
|
||||||
|
assert existing.read_text() == original
|
||||||
|
assert "55555555-5555-4555-8555-555555555555" not in rendered
|
||||||
|
assert "state_hub_workstream_id:" not in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_exclusive_writer_refuses_existing_output(tmp_path):
|
||||||
|
output = tmp_path / "existing.md"
|
||||||
|
output.write_text("operator content\n")
|
||||||
|
with pytest.raises(WorkplanGenerationError) as exc:
|
||||||
|
write_workplan_exclusive(output, "replacement\n")
|
||||||
|
assert exc.value.code == "output_exists"
|
||||||
|
assert output.read_text() == "operator content\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefix_is_never_derived_from_new_slug(tmp_path):
|
||||||
|
root = tmp_path / "repo"
|
||||||
|
root.mkdir()
|
||||||
|
snapshot = build_generation_snapshot(
|
||||||
|
repo=_repo(str(root)),
|
||||||
|
preflight=_preflight(),
|
||||||
|
workplans=_workplans(),
|
||||||
|
)
|
||||||
|
assert snapshot.prefix == "FLEX-WP"
|
||||||
|
assert not snapshot.workplan_id.startswith("ACCESS-ENGINE")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ambiguous_indexed_prefixes_fail_closed(tmp_path):
|
||||||
|
root = tmp_path / "repo"
|
||||||
|
root.mkdir()
|
||||||
|
with pytest.raises(WorkplanGenerationError) as exc:
|
||||||
|
build_generation_snapshot(
|
||||||
|
repo=_repo(str(root)),
|
||||||
|
preflight=_preflight(),
|
||||||
|
workplans=[
|
||||||
|
{"slug": "flex-wp-0001"},
|
||||||
|
{"slug": "other-wp-0001"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert exc.value.code == "workplan_prefix_ambiguous"
|
||||||
|
|
@ -400,7 +400,7 @@ dashboard retains the pre-existing `/docs/intakes` broken-link warning.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: STATE-WP-0085-T06
|
id: STATE-WP-0085-T06
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "154bdbb3-d3bf-5bac-85d7-eecd88545841"
|
state_hub_task_id: "154bdbb3-d3bf-5bac-85d7-eecd88545841"
|
||||||
```
|
```
|
||||||
|
|
@ -441,6 +441,37 @@ Acceptance:
|
||||||
- template snapshot tests cover repositories with active work, archived
|
- template snapshot tests cover repositories with active work, archived
|
||||||
workplans, missing local paths, private forge visibility, and old aliases.
|
workplans, missing local paths, private forge visibility, and old aliases.
|
||||||
|
|
||||||
|
Result (2026-08-29): added `statehub repo rename generate-workplan <old>
|
||||||
|
<new>` with a pure deterministic renderer over the registered repository,
|
||||||
|
captured rename preflight, indexed workplans, and visible repository metadata.
|
||||||
|
Prefix discovery uses the repository's declared convention when present and
|
||||||
|
otherwise requires one unambiguous prefix across active and archived workplan
|
||||||
|
records; it never derives a prefix from the proposed slug. Number allocation
|
||||||
|
includes archived records, so the fixed flex-auth snapshot selects
|
||||||
|
`FLEX-WP-0019` while retaining `FLEX-WP-` task/work-record identity.
|
||||||
|
|
||||||
|
The generated ready/proposed plan carries DoR evidence and eleven ordered
|
||||||
|
tasks for baseline capture, metadata preparation, separate product/runtime
|
||||||
|
naming decisions, consumer/CI/package/deployment/credential/fabric/SBOM/docs
|
||||||
|
inventory, renewed preflight and approval, Forgejo rename, State Hub rebind,
|
||||||
|
fresh-clone registration, relationship-aware verification, rollback, soak,
|
||||||
|
residuals, and old-checkout cleanup. Live rename and destructive cleanup tasks
|
||||||
|
start in `wait`. Every captured blocker, warning, active work item, alias, and
|
||||||
|
external handoff is rendered; an ownership ledger requires exact repository
|
||||||
|
slugs and prohibits the target plan from closing another repository's work.
|
||||||
|
Tokens are omitted. Existing UUID-bearing files remain untouched, and no new
|
||||||
|
`state_hub_workstream_id` or `state_hub_task_id` is invented.
|
||||||
|
|
||||||
|
Output uses exclusive create with no overwrite/force mode; private preflight
|
||||||
|
files retain the mode-0600 input guard. Missing checkouts can render from
|
||||||
|
indexed records only with an explicit output path and remain visibly blocked
|
||||||
|
for baseline capture. Snapshot/parser/quality tests cover deterministic
|
||||||
|
flex-auth output, active work, archived numbering, missing paths, unreadable
|
||||||
|
private Forge state, old aliases, ambiguous prefixes, UUID preservation, and
|
||||||
|
overwrite refusal. Verification: 801 Python tests and the 70-page dashboard
|
||||||
|
build pass; a clean wheel installation exposes the generator command. The
|
||||||
|
dashboard retains the pre-existing `/docs/intakes` broken-link warning.
|
||||||
|
|
||||||
## Prove failure recovery and telemetry continuity
|
## Prove failure recovery and telemetry continuity
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue