feat: flavor-correct rmgr scaffold; record C-15/C-23 file-wins

RMGR-WP-0004-T03: project repos get GOAL.md and a required prefix.
RMGR-WP-0005-T06 implemented in state-hub consistency.
This commit is contained in:
tegwick 2026-08-18 13:36:26 +02:00
parent 0c5177ec4c
commit a6d4a9b615
6 changed files with 294 additions and 3 deletions

View file

@ -75,6 +75,15 @@ def main(argv: list[str] | None = None) -> int:
p_pref.add_argument("--root", default=".", help="Fleet root or a single repository")
p_pref.add_argument("--registry", default=None, help="Override prefix registry YAML")
p_scaf = sub.add_parser("scaffold", help="Create flavor-correct repository baseline files")
p_scaf.add_argument("--path", required=True)
p_scaf.add_argument("--flavor", required=True, choices=["experimental", "research", "project", "tooling", "product", "business"])
p_scaf.add_argument("--slug", default=None)
p_scaf.add_argument("--domain", default="infotech")
p_scaf.add_argument("--wp-prefix", default=None)
p_scaf.add_argument("--force", action="store_true")
p_scaf.add_argument("--no-commit", action="store_true")
args = parser.parse_args(argv)
if args.version or args.command in (None, "version"):
@ -214,6 +223,21 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(report, indent=2))
return 0 if report.get("ok") else 1
if args.command == "scaffold":
from repo_manager.commands.scaffold import scaffold_repository
result = scaffold_repository(
Path(args.path),
flavor=args.flavor,
slug=args.slug,
domain=args.domain,
wp_prefix=args.wp_prefix,
force=args.force,
commit=not args.no_commit,
)
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status == "applied" else 1
parser.print_help()
return 0

View file

@ -0,0 +1,190 @@
"""Governed command: create flavor-correct repository files (RMGR-WP-0004-T03)."""
from __future__ import annotations
import uuid
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Any
from repo_manager.gitops import GitError, commit_paths, is_git_repo
from repo_manager.standards import FLAVOR_MARKER_PREFIX, expected_workplan_prefix
DURABLE_FLAVORS = ("experimental", "research", "tooling", "product", "business")
FLAVORS = (*DURABLE_FLAVORS, "project")
@dataclass
class CommandResult:
status: str
evidence: dict[str, Any]
error: dict[str, Any] | None = None
correlation_id: str = ""
def to_dict(self) -> dict[str, Any]:
out = {
"command": "repo.scaffold",
"status": self.status,
"correlation_id": self.correlation_id,
"evidence": self.evidence,
}
if self.error:
out["error"] = self.error
return out
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not content.endswith("\n"):
content += "\n"
path.write_text(content)
def scaffold_repository(
path: Path,
*,
flavor: str,
slug: str | None = None,
domain: str = "infotech",
wp_prefix: str | None = None,
force: bool = False,
commit: bool = True,
) -> CommandResult:
cid = str(uuid.uuid4())
dest = path.expanduser().resolve()
dest.mkdir(parents=True, exist_ok=True)
slug = slug or dest.name
flavor = flavor.strip().lower()
if flavor not in FLAVORS:
return CommandResult(
"rejected",
{"path": str(dest)},
{"message": f"unknown flavor {flavor!r}"},
cid,
)
prj = flavor == "project" or slug.startswith("prj-")
if prj:
flavor = "project"
prefix = (wp_prefix or "").strip().upper()
if prefix and not prefix.endswith("-WP"):
prefix = prefix.rstrip("-") + "-WP"
if not prefix:
derived = expected_workplan_prefix(dest, slug=slug)
return CommandResult(
"rejected",
{"path": str(dest), "suggested_prefix": derived},
{
"message": (
"project flavor requires --wp-prefix derived from the "
"project identity (never PRJ-WP-)"
)
},
cid,
)
if prefix == FLAVOR_MARKER_PREFIX or prefix.startswith("PRJ-"):
return CommandResult(
"rejected",
{"path": str(dest)},
{"message": "refusing flavor-derived prefix PRJ-WP-"},
cid,
)
else:
prefix = (wp_prefix or expected_workplan_prefix(dest, slug=slug) or "REPO-WP").upper()
if not prefix.endswith("-WP"):
prefix = prefix.rstrip("-") + "-WP"
written: list[str] = []
def put(rel: str, content: str) -> None:
target = dest / rel
if target.exists() and not force:
return
_write(target, content)
written.append(rel)
put(
".repo-classification.yaml",
"repo_classification:\n"
f" category: {'project' if prj else flavor}\n"
f" domain: {domain}\n"
" secondary_domains: []\n"
" capability_tags: []\n",
)
put("README.md", f"# {slug}\n\nSee {'GOAL.md' if prj else 'INTENT.md'} and SCOPE.md.\n")
put(
"SCOPE.md",
f"# Scope\n\n## In scope\n\n- {slug}\n\n## Out of scope\n\n- TBD\n",
)
put(
"AGENTS.md",
f"# Agent instructions — {slug}\n\n"
f"Orient: {'GOAL.md' if prj else 'INTENT.md'} → SCOPE.md → workplans/.\n"
f"Workplan prefix: `{prefix}-`.\n",
)
if prj:
today = date.today().isoformat()
put(
"GOAL.md",
"---\n"
f"repo: {slug}\n"
"repo_flavor: project\n"
"project_status: draft\n"
f'started: "{today}"\n'
"---\n\n"
f"# Goal — {slug}\n\n"
"## Outcome\n\nWhat success looks like.\n\n"
"## Invariants\n\nRules that must hold during the project.\n\n"
"## Success gates\n\nCheckable conditions for goal achieved.\n\n"
"## Project retirement\n\n"
"Archive this repository when gates are met and residuals have live owners.\n",
)
if (dest / "INTENT.md").exists() and not force:
pass
else:
put(
"INTENT.md",
f"# INTENT\n\n> Why `{slug}` exists.\n\n## One-liner\n\nTBD.\n",
)
(dest / "workplans" / "archived").mkdir(parents=True, exist_ok=True)
if not prj:
bootstrap = dest / "workplans" / f"{prefix}-0001-foundation.md"
if force or not bootstrap.exists():
_write(
bootstrap,
"---\n"
f"id: {prefix}-0001\n"
"type: workplan\n"
f'title: "Foundation"\n'
f"domain: {domain}\n"
f"repo: {slug}\n"
"status: proposed\n"
"---\n\n"
"# Foundation\n\n"
"```task\n"
f"id: {prefix}-0001-T01\n"
"status: todo\n"
"priority: high\n"
"```\n\n"
"Establish the repository baseline.\n",
)
written.append(str(bootstrap.relative_to(dest)))
evidence: dict[str, Any] = {
"path": str(dest),
"flavor": flavor,
"slug": slug,
"wp_prefix": prefix,
"written": written,
"prj_layout": prj,
}
if commit and is_git_repo(dest) and written:
try:
sha = commit_paths(dest, written, f"chore: scaffold {flavor} repository baseline")
evidence["git_sha"] = sha
except GitError as exc:
return CommandResult("failed", evidence, {"message": str(exc)}, cid)
return CommandResult("applied" if written else "rejected", evidence, None, cid)