#!/usr/bin/env python3 """Generate /cctest-* wrapper skills from the autonomic-recipe-maintainer submodule. The ARM toolkit (vendor/autonomic-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 / "vendor/autonomic-recipe-maintainer" ARM_SKILLS = VENDOR / ".opencode/skills" PREFIX = "cctest-" BODY_TEMPLATE = """# {wrapped} (cctest wrapper) **Canonical procedure:** `vendor/autonomic-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/vendor/autonomic-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. **Policy overrides (cc-ci-orchestrator conventions win):** - Anything that would **merge a recipe PR or push a recipe main without review** requires explicit operator opt-in per run — the cc-ci standing rule is recipe upgrade PRs are operator-merged, and ARM skills that say otherwise (e.g. full-auto upgrade flows) do NOT inherit blanket authorization here. - 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 `vendor/autonomic-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())