skills: vendor autonomic-recipe-maintainer + expose all its skills as /cctest-*
One operator interface for both toolkits (operator decision 2026-08-04): - vendor/autonomic-recipe-maintainer: ARM pinned as a submodule at acd5cfb (latest). - scripts/gen-cctest-skills.py: generates a cctest-<name> wrapper pair (.opencode canonical + .claude thin) for every ARM skill — frontmatter carries ARM's own description tagged [recipe-maintainer/cctest]; body points at the canonical submodule SKILL.md, sets cwd/sandbox context, and states the policy overrides (auto-merge-style ARM flows need per-run operator opt-in; never touch cc-ci infra from an ARM skill; submodule is read-only here). Re-run after every submodule bump. - 30 cctest-* wrappers generated. - /help: cctest family section + situation-table rows + the cc-ci-vs-cctest rule of thumb. cctest = the recipe-maintainer test server; these skills run against it + the ARM sandbox, never against the cc-ci CI server/swarm.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
#!/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-<name>`: 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())
|
||||
Reference in New Issue
Block a user