Files
cc-ci-orchestrator/scripts/gen-cctest-skills.py
T
autonomic-bot be7f8bc850 cctest: unify merge policy wording — recipe PRs are never agent-merged, both sides
Operator decision: no policy difference between cc-ci and recipe-maintainer. On
inspection ARM already agrees (recipe-upgrade-cron-all: 'PRs are reviewed and merged
manually by a human afterwards... never merges anything'; 'no human review in the
middle' = skip the mid-run plan confirmation only). Wrappers previously framed this
as a cc-ci override over ARM auto-merge flows — wrong reading; now stated as ONE
unified rule. /help conventions updated to match.
2026-08-04 01:40:36 +00:00

116 lines
4.9 KiB
Python

#!/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-<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 / "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())