From a91c3f09570fd85028603ec047724bd99d3f9401 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Sat, 10 Oct 2026 09:09:05 +1100 Subject: [PATCH] DOS+BE: skill updates --- scripts/check_skill_pairs.py | 119 ++++++++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 17 deletions(-) diff --git a/scripts/check_skill_pairs.py b/scripts/check_skill_pairs.py index f71aa974f..ee0999ec7 100644 --- a/scripts/check_skill_pairs.py +++ b/scripts/check_skill_pairs.py @@ -2,17 +2,27 @@ """ Flag PRs that touch some but not all files in a group of mirrored skill files (`.gemini/skills/`, `.github/skills/`, `.claude/skills/`) without -touching the others. `.gemini/skills/skills-index/SKILL.md` documents these -groups and says to "keep body content identical" across them - but nothing -previously enforced that, and the plugin-development pair had already -drifted apart before this check existed. +touching the others, and - for the groups documented in +`.gemini/skills/skills-index/SKILL.md` as covering identical content - +flag when their bodies have actually drifted apart, not just whether a +change touched one side. -This can't verify the files still say the *same thing* (that needs -judgment - some groups are intentionally different in depth, and the three -devcontainer-management targets each cover only part of the Gemini file), -only that a change to one file didn't forget the others exist. Exit -non-zero (but the CI step calling this is non-blocking) when a group looks -one-sided. +Two independent checks: + +1. Presence drift (every group): a PR touched some files in a group but + not the others. Still only a hint ("did you forget the other file?"), + not proof either way - a genuinely tool-specific change can legitimately + touch just one side. +2. Content drift (CONTENT_MATCH_GROUPS only): the groups the skills index + explicitly documents as "All three cover [the same things]" should have + byte-identical bodies (frontmatter excluded - `name`/`description` are + allowed to differ per assistant). `settings`/`settings-management`, + `mcp-activation`, `project-navigation`, and the three devcontainer- + management targets are deliberately NOT in this list - the skills index + documents them as intentionally different in depth/scope, not drifted. + +Exit non-zero (but the CI step calling this is non-blocking) when either +check finds something. python3 scripts/check_skill_pairs.py origin/main """ @@ -44,6 +54,27 @@ GROUPS = [ [".gemini/skills/devcontainer-management/SKILL.md", ".github/skills/devcontainer-configs/SKILL.md"], ] +# Subset of GROUPS (by first-member path) the skills index documents as +# covering identical content across every member - everything else in +# GROUPS is documented there as intentionally different in depth/scope +# (settings, mcp-activation, project-navigation) or intentionally partial +# (the three devcontainer-management targets, each covering only part of +# the Gemini file). +CONTENT_MATCH_GROUP_KEYS = { + ".gemini/skills/plugin-development/plugin-skill.md", + ".gemini/skills/plugin-readme/plugin-readme-skill.md", + ".gemini/skills/testing-workflow/SKILL.md", + ".gemini/skills/pr-analysis/SKILL.md", + ".gemini/skills/scan-pipeline/SKILL.md", + ".gemini/skills/database-patterns/SKILL.md", + ".gemini/skills/prd-writing/SKILL.md", + ".gemini/skills/ux-design-patterns/SKILL.md", + ".gemini/skills/skill-hygiene/SKILL.md", + ".gemini/skills/plugin-review/SKILL.md", + ".gemini/skills/install-scripts/SKILL.md", + ".gemini/skills/logging-standards/SKILL.md", +} + def changed_files(base_ref): result = subprocess.run( @@ -53,12 +84,27 @@ def changed_files(base_ref): return set(result.stdout.splitlines()) -def main(): - if len(sys.argv) != 2: - print("usage: check_skill_pairs.py ", file=sys.stderr) - return 2 +def strip_frontmatter(text): + """Drop a leading YAML frontmatter block (--- ... ---) so name/ + description, which are allowed to differ per assistant, don't count as + content drift. Returns the stripped-and-trimmed body, or the original + text trimmed if there's no frontmatter block.""" + if text.startswith("---"): + parts = text.split("---", 2) + if len(parts) >= 3: + return parts[2].strip() + return text.strip() - changed = changed_files(sys.argv[1]) + +def read_body(path): + try: + with open(path, encoding="utf-8") as f: + return strip_frontmatter(f.read()) + except FileNotFoundError: + return None + + +def check_presence_drift(changed): problems = [] for group in GROUPS: touched = [path for path in group if path in changed] @@ -67,12 +113,51 @@ def main(): problems.append( f"- touched {', '.join(touched)} but not {', '.join(untouched)}." ) + return problems - if problems: + +def check_content_drift(): + problems = [] + for group in GROUPS: + if group[0] not in CONTENT_MATCH_GROUP_KEYS: + continue + bodies = {path: read_body(path) for path in group} + reference_path, reference_body = group[0], bodies[group[0]] + for path, body in bodies.items(): + if path == reference_path: + continue + if body != reference_body: + problems.append( + f"- {path} and {reference_path} are documented as covering " + f"identical content but their bodies differ." + ) + return problems + + +def main(): + if len(sys.argv) != 2: + print("usage: check_skill_pairs.py ", file=sys.stderr) + return 2 + + changed = changed_files(sys.argv[1]) + presence_problems = check_presence_drift(changed) + content_problems = check_content_drift() + + if presence_problems: print("Possible skill-group drift (only some mirrored files were touched):") - print("\n".join(problems)) + print("\n".join(presence_problems)) print("\nIf the change is genuinely tool-specific, ignore this. " "Otherwise update the other file(s) too - see .gemini/skills/skills-index/SKILL.md.") + + if content_problems: + if presence_problems: + print() + print("Skill-pair content drift (bodies documented as identical, but aren't):") + print("\n".join(content_problems)) + print("\nDiff the files directly to see what changed - " + "see .gemini/skills/skills-index/SKILL.md for which groups this applies to.") + + if presence_problems or content_problems: return 1 print("No skill-group drift detected.")