#!/usr/bin/env python3 """Generate /cctest-* wrapper skills from the autonomic-recipe-maintainer submodule. The ARM toolkit (references/recipe-maintainer) carries ~30 recipe-maintenance skills that operate against the recipe-maintainer TEST server ("cctest") + local abra sandbox — a different substrate than the cc-ci skills in this repo. To give the operator ONE interface, every ARM skill is exposed here as `cctest-`: a thin wrapper whose frontmatter carries ARM's own description (so discovery works) and whose body points at the canonical SKILL.md in the submodule, sets the execution context, and states the policy overrides. Run after every submodule bump: python3 scripts/gen-cctest-skills.py It removes cctest-* skills whose ARM source disappeared and (re)writes the rest, in BOTH .opencode/skills/ (canonical) and .claude/skills/ (thin pointer), then prints a summary. Commit the result. """ from __future__ import annotations import re import shutil import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent VENDOR = ROOT / "references/recipe-maintainer" ARM_SKILLS = VENDOR / ".opencode/skills" PREFIX = "cctest-" BODY_TEMPLATE = """# {wrapped} (cctest wrapper) **Canonical procedure:** `references/recipe-maintainer/.opencode/skills/{name}/SKILL.md` — read it and follow it. This wrapper only sets context + policy. **Context:** this is an **autonomic-recipe-maintainer (ARM)** skill. It operates on the recipe-maintainer **cctest** test server / local abra sandbox — NOT on the cc-ci CI server or its shared swarm. Execute with the submodule as your working directory: `cd /srv/cc-ci-orch/references/recipe-maintainer`. If the ARM environment is not yet configured on this host (`settings.toml` from `settings.toml.example`, sandbox/test instances), run `/cctest-intro` / `/cctest-setup-sandbox` first. **Unified policy (same as cc-ci — no differences):** - **Recipe PRs are NEVER merged by an agent.** Every flow ends at an open PR; the operator reviews and merges. This is ARM's own rule too ("PRs are reviewed and merged manually by a human afterwards — never pushes to upstream or merges anything"); ARM's "no human review in the middle" wording refers only to skipping the mid-run plan confirmation, not to merging. - Never touch cc-ci infrastructure (the CI server, its swarm, `/root/*` clones, the weekly timers) from an ARM skill — cc-ci work goes through the cc-ci skills. - The submodule is **pinned**: don't commit into it from here; upstream ARM changes arrive via a deliberate submodule bump + `scripts/gen-cctest-skills.py` regeneration. """ WRAPPER_TEMPLATE = """# {wrapped} (thin wrapper) The canonical definition of this skill lives in the **opencode** position: **`.opencode/skills/{wrapped}/SKILL.md`** Read that file. It in turn wraps the ARM submodule skill `references/recipe-maintainer/.opencode/skills/{name}/SKILL.md`. """ def parse_frontmatter(text: str) -> dict[str, str]: m = re.match(r"\A---\n(.*?)\n---\n", text, re.S) fields: dict[str, str] = {} if m: for line in m.group(1).splitlines(): if ":" in line: k, v = line.split(":", 1) fields[k.strip()] = v.strip() return fields def main() -> int: if not ARM_SKILLS.is_dir(): print(f"ERROR: {ARM_SKILLS} missing — init the submodule first", file=sys.stderr) return 1 arm_names = sorted(p.parent.name for p in ARM_SKILLS.glob("*/SKILL.md")) written, removed = [], [] for pos in (ROOT / ".opencode/skills", ROOT / ".claude/skills"): for stale in pos.glob(f"{PREFIX}*"): if stale.name[len(PREFIX) :] not in arm_names: shutil.rmtree(stale) removed.append(str(stale.relative_to(ROOT))) for name in arm_names: src = ARM_SKILLS / name / "SKILL.md" fm = parse_frontmatter(src.read_text()) desc = fm.get("description", f"ARM skill {name} (no description)") wrapped = PREFIX + name frontmatter = ( f"---\nname: {wrapped}\n" f"description: \"[recipe-maintainer/cctest] {desc} (Wraps the autonomic-recipe-" f"maintainer skill /{name}; runs against the cctest test server + ARM sandbox, " f"not cc-ci. Invoke as /{wrapped}.)\"\n---\n\n" ) canon = ROOT / ".opencode/skills" / wrapped / "SKILL.md" canon.parent.mkdir(parents=True, exist_ok=True) canon.write_text(frontmatter + BODY_TEMPLATE.format(name=name, wrapped=wrapped)) thin = ROOT / ".claude/skills" / wrapped / "SKILL.md" thin.parent.mkdir(parents=True, exist_ok=True) thin.write_text(frontmatter + WRAPPER_TEMPLATE.format(name=name, wrapped=wrapped)) written.append(wrapped) print(f"generated {len(written)} cctest skills: {', '.join(written)}") if removed: print(f"removed stale: {', '.join(removed)}") return 0 if __name__ == "__main__": sys.exit(main())