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:
autonomic-bot
2026-08-04 01:36:11 +00:00
parent b462f1f7f1
commit 49854472b8
64 changed files with 1304 additions and 0 deletions
+115
View File
@@ -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())