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:
parent
0c5177ec4c
commit
a6d4a9b615
6 changed files with 294 additions and 3 deletions
|
|
@ -40,6 +40,7 @@ rmgr rapp validate --path ../rapp-user-engine --family-root ..
|
|||
rmgr rapp place --path ../rapp-some-app --reef reef-railiance
|
||||
rmgr conform --path .
|
||||
rmgr prefix-uniqueness --root ..
|
||||
rmgr scaffold --path ../prj-example --flavor project --wp-prefix EX-WP --no-commit
|
||||
```
|
||||
|
||||
Vertical-slice proof: [docs/evidence/t05-vertical-slice.md](docs/evidence/t05-vertical-slice.md).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
190
src/repo_manager/commands/scaffold.py
Normal file
190
src/repo_manager/commands/scaffold.py
Normal 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)
|
||||
65
tests/test_scaffold.py
Normal file
65
tests/test_scaffold.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from pathlib import Path
|
||||
|
||||
from repo_manager.cli import main
|
||||
from repo_manager.commands.scaffold import scaffold_repository
|
||||
from repo_manager.standards import check_repository
|
||||
|
||||
|
||||
def test_prj_scaffold_requires_prefix(tmp_path: Path):
|
||||
dest = tmp_path / "prj-example"
|
||||
result = scaffold_repository(dest, flavor="project", commit=False)
|
||||
assert result.status == "rejected"
|
||||
assert "wp-prefix" in (result.error or {}).get("message", "").lower() or "PRJ" in str(
|
||||
result.error
|
||||
)
|
||||
|
||||
|
||||
def test_prj_scaffold_refuses_flavor_prefix(tmp_path: Path):
|
||||
dest = tmp_path / "prj-example"
|
||||
result = scaffold_repository(dest, flavor="project", wp_prefix="PRJ-WP", commit=False)
|
||||
assert result.status == "rejected"
|
||||
|
||||
|
||||
def test_prj_scaffold_writes_goal_not_intent(tmp_path: Path):
|
||||
dest = tmp_path / "prj-example-cutover"
|
||||
result = scaffold_repository(
|
||||
dest, flavor="project", wp_prefix="EXCO-WP", commit=False
|
||||
)
|
||||
assert result.status == "applied"
|
||||
assert (dest / "GOAL.md").is_file()
|
||||
assert not (dest / "INTENT.md").exists()
|
||||
goal = (dest / "GOAL.md").read_text()
|
||||
for heading in ("Outcome", "Invariants", "Success gates", "Project retirement"):
|
||||
assert heading in goal
|
||||
assert not list((dest / "workplans").glob("*-0001-*.md"))
|
||||
report = check_repository(dest)
|
||||
assert report.ok, report.to_dict()
|
||||
|
||||
|
||||
def test_durable_scaffold_writes_intent(tmp_path: Path):
|
||||
dest = tmp_path / "some-tool"
|
||||
result = scaffold_repository(dest, flavor="tooling", commit=False)
|
||||
assert result.status == "applied"
|
||||
assert (dest / "INTENT.md").is_file()
|
||||
assert not (dest / "GOAL.md").exists()
|
||||
assert (dest / "workplans" / "SOMETOOL-WP-0001-foundation.md").is_file()
|
||||
report = check_repository(dest)
|
||||
assert report.ok, report.to_dict()
|
||||
|
||||
|
||||
def test_cli_scaffold(tmp_path: Path):
|
||||
dest = tmp_path / "cli-tool"
|
||||
assert (
|
||||
main(
|
||||
[
|
||||
"scaffold",
|
||||
"--path",
|
||||
str(dest),
|
||||
"--flavor",
|
||||
"tooling",
|
||||
"--no-commit",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
assert (dest / "INTENT.md").is_file()
|
||||
|
|
@ -115,7 +115,7 @@ consistency lane / STATE-WP-0080 guard. (Do not set `progress` until
|
|||
|
||||
```task
|
||||
id: RMGR-WP-0004-T03
|
||||
status: wait
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8e6fa8d7-f169-44f5-bdc9-29376bb2ef64"
|
||||
```
|
||||
|
|
@ -142,6 +142,11 @@ Behaviour for durable product repos must be preserved exactly; this workplan
|
|||
changes where the capability lives and how project repos are treated, not what
|
||||
ordinary repos receive.
|
||||
|
||||
**Result (2026-08-18):** `rmgr scaffold` writes flavor-correct baselines.
|
||||
`prj-` requires `--wp-prefix` (never `PRJ-WP-`), writes `GOAL.md` with
|
||||
the four required sections, and does not emit a bootstrap workplan.
|
||||
Durable flavors get `INTENT.md` and an optional foundation workplan.
|
||||
|
||||
## Regenerate agent instructions per flavor
|
||||
|
||||
```task
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ status: active
|
|||
owner: codex
|
||||
topic_slug: infotech
|
||||
created: "2026-08-17"
|
||||
updated: "2026-08-17"
|
||||
updated: "2026-08-18"
|
||||
parent_project: prj-state-hub-retirement
|
||||
parent_workplan: SHR-WP-0001
|
||||
related:
|
||||
|
|
@ -253,7 +253,7 @@ Coordinate with the hub-extension architecture in
|
|||
|
||||
```task
|
||||
id: RMGR-WP-0005-T06
|
||||
status: wait
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d440d59c-f78e-4752-84c7-f3d5fdf7d3c3"
|
||||
```
|
||||
|
|
@ -271,3 +271,9 @@ regardless of file content — reproduced three times, via file edit and via
|
|||
`update_task_status`, with the task never holding `todo`. Establish which
|
||||
direction wins for task status and make it consistent with `ADR-001`, where the
|
||||
file originates work.
|
||||
|
||||
**Result (2026-08-18):** In `state-hub` consistency: C-23 does not
|
||||
auto-promote `proposed` → `active` (report only). C-15 no longer
|
||||
writebacks wait over progress/todo; file wins via C-10 (ADR-001).
|
||||
C-15 remains a non-fixable warning when the DB is terminal and the file
|
||||
is not.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue