From 08198fbef8680d5987f99d873d50f1480d7ff09e Mon Sep 17 00:00:00 2001 From: notque Date: Sat, 11 Apr 2026 08:50:22 -0700 Subject: [PATCH 1/2] refactor(scripts): symlink-aware index generation with local override The index generators walk skills/ and agents/ and follow symlinks, so the tracked index files end up containing entries for symlinked directories that are not part of the public tree. Refactor the generator to skip symlinks by default and add an --include-private flag for local workflows that want to see everything. - generate-skill-index.py: skip symlinked directories by default - Add --include-private flag to preserve prior behavior - Add --output flag for writing to an alternate target - generate-agent-index.py: same symlink filtering and CLI flags - routing-manifest.py prefers skills/INDEX.local.json when present - Regenerate skills/INDEX.json, agents/INDEX.json, and routing-tables.md to match the new generator behavior - scripts/README.md: document local workflow with --include-private - scan-negative-framing.py: generalize header comment - toolkit-evolution-cron.sh: instruct cron to use default mode - Tests for symlink exclusion, flag override, output path, and non-symlink preservation --- agents/INDEX.json | 89 ---------- scripts/README.md | 25 +++ scripts/generate-agent-index.py | 63 ++++++-- scripts/generate-skill-index.py | 43 ++++- scripts/routing-manifest.py | 16 +- scripts/scan-negative-framing.py | 8 +- scripts/tests/test_generate_skill_index.py | 180 +++++++++++++++++++++ scripts/toolkit-evolution-cron.sh | 5 +- skills/INDEX.json | 175 +------------------- skills/do/references/routing-tables.md | 17 -- 10 files changed, 318 insertions(+), 303 deletions(-) create mode 100644 scripts/README.md mode change 100644 => 100755 scripts/generate-agent-index.py mode change 100644 => 100755 scripts/routing-manifest.py mode change 100644 => 100755 scripts/scan-negative-framing.py create mode 100644 scripts/tests/test_generate_skill_index.py diff --git a/agents/INDEX.json b/agents/INDEX.json index 9ac265b5..d9d1914e 100644 --- a/agents/INDEX.json +++ b/agents/INDEX.json @@ -931,95 +931,6 @@ ], "complexity": "Medium", "category": "language" - }, - "wrestlejoy-amy-writer-compact": { - "file": "private-agents/wrestlejoy-amy-writer-compact.md", - "short_description": "Use this agent when you need to write WrestleJoy content in Amy Nemmity's voice with context optimization", - "triggers": [ - "WrestleJoy", - "Amy voice", - "Amy compact" - ], - "pairs_with": [ - "voice-writer", - "wrestlejoy-mmr-research" - ], - "complexity": "Comprehensive", - "category": "content" - }, - "wrestlejoy-amy-writer-expanded": { - "file": "private-agents/wrestlejoy-amy-writer-expanded.md", - "short_description": "Use this agent when you need to write WrestleJoy content in Amy Nemmity's voice with maximum depth and extensive examples", - "triggers": [ - "WrestleJoy", - "Amy voice", - "Amy expanded" - ], - "pairs_with": [ - "voice-writer", - "wrestlejoy-mmr-research" - ], - "complexity": "Comprehensive", - "category": "content" - }, - "wrestlejoy-amy-writer": { - "file": "private-agents/wrestlejoy-amy-writer.md", - "short_description": "Use this agent when writing WrestleJoy content in Amy Nemmity's voice", - "triggers": [ - "WrestleJoy", - "Amy voice", - "Amy's voice", - "Amy Nemmity", - "wrestling article", - "wrestling awards", - "Amy style" - ], - "pairs_with": [ - "voice-writer", - "wrestlejoy-mmr-research", - "wrestlejoy-research-transform" - ], - "complexity": "Comprehensive", - "category": "content" - }, - "wrestlejoy-news-editor": { - "file": "private-agents/wrestlejoy-news-editor.md", - "short_description": "Editorial review agent for WrestleJoy automated news pipeline", - "triggers": [ - "wrestlejoy edit", - "review article", - "editorial check", - "news review", - "check article tone", - "wrestlejoy quality check", - "pre-publish review" - ], - "pairs_with": [ - "wrestlejoy-news-producer", - "wrestlejoy-news-pipeline", - "voice-andy-nemmity" - ], - "complexity": "Simple", - "category": "content" - }, - "wrestlejoy-news-producer": { - "file": "private-agents/wrestlejoy-news-producer.md", - "short_description": "Orchestrator agent for the WrestleJoy automated news production pipeline", - "triggers": [ - "wrestlejoy news", - "aew news", - "wrestling news pipeline", - "news automation" - ], - "pairs_with": [ - "wrestlejoy-news-pipeline", - "wrestlejoy-news-editor", - "voice-andy-nemmity", - "wordpress-uploader", - "seo-optimizer" - ], - "complexity": "Complex", - "category": "content" } } } \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..8de53bcb --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,25 @@ +# Scripts + +Utility scripts for the Claude Code Toolkit. + +## Index Generators + +`generate-skill-index.py` and `generate-agent-index.py` walk `skills/` and +`agents/` and produce `INDEX.json` files consumed by the `/do` router. + +By default the generators skip symlinked directories, so the tracked index +files reflect only directly-committed content. + +### Local development workflow + +To index symlinked entries for local workflows, use `--include-private` with +a separate output target: + +```bash +python3 scripts/generate-skill-index.py --include-private --output skills/INDEX.local.json +python3 scripts/generate-agent-index.py --include-private --output agents/INDEX.local.json +``` + +The router (`scripts/routing-manifest.py`) prefers the local file when present, +so local runs see all entries while the tracked index stays public. The +`*.local.json` files are gitignored and never committed. diff --git a/scripts/generate-agent-index.py b/scripts/generate-agent-index.py old mode 100644 new mode 100755 index 2bee4eeb..253761e1 --- a/scripts/generate-agent-index.py +++ b/scripts/generate-agent-index.py @@ -7,11 +7,18 @@ Usage: python scripts/generate-agent-index.py + python scripts/generate-agent-index.py --include-private + python scripts/generate-agent-index.py --include-private --output agents/INDEX.local.json + +Options: + --include-private Include symlinked agent files (default: skip them) + --output PATH Output path (default: agents/INDEX.json) Output: - agents/INDEX.json - Routing index for /do router + agents/INDEX.json - Routing index for /do router (public, tracked) """ +import argparse import json import re from pathlib import Path @@ -108,7 +115,11 @@ def extract_short_description(description: str) -> str: return description[:100] -def generate_index(agents_dir: Path, relative_to: Path | None = None) -> dict: +def generate_index( + agents_dir: Path, + relative_to: Path | None = None, + include_private: bool = False, +) -> dict: """Generate routing index from all agent files. Args: @@ -116,11 +127,16 @@ def generate_index(agents_dir: Path, relative_to: Path | None = None) -> dict: relative_to: If provided, agent file paths in the index will be relative to this directory (e.g. repo root), so private agents get ``private-agents/filename.md`` instead of bare ``filename.md``. + include_private: When True, include symlinked agent files. When False (default), + only directly-tracked files are indexed. """ index = {"version": "1.0", "generated_by": "scripts/generate-agent-index.py", "agents": {}} errors = [] for agent_file in sorted(agents_dir.glob("*.md")): + # Skip symlinked files unless include_private is set. + if agent_file.is_symlink() and not include_private: + continue try: content = agent_file.read_text(encoding="utf-8") except Exception as e: @@ -179,8 +195,26 @@ def generate_index(agents_dir: Path, relative_to: Path | None = None) -> dict: return index -def main(): +def main() -> int: """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate agent routing index from YAML frontmatter.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--include-private", + action="store_true", + default=False, + help="Include symlinked agent files. Use with --output for local-only workflows.", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Output file path (default: agents/INDEX.json relative to repo root).", + ) + args = parser.parse_args() + # Find agents directory relative to script script_dir = Path(__file__).parent repo_root = script_dir.parent @@ -190,22 +224,25 @@ def main(): print(f"Error: agents directory not found at {agents_dir}") return 1 - # Generate index from public agents - index = generate_index(agents_dir, relative_to=repo_root) + # Resolve output path + output_path: Path = args.output if args.output is not None else agents_dir / "INDEX.json" + + # Generate index: symlinked files skipped by default, included with --include-private. + # Private agents directory (gitignored) is only scanned when --include-private is set. + index = generate_index(agents_dir, relative_to=repo_root, include_private=args.include_private) - # Also scan private agents if they exist (gitignored, user-specific) - private_agents_dir = repo_root / "private-agents" - if private_agents_dir.exists() and any(private_agents_dir.iterdir()): - private_index = generate_index(private_agents_dir, relative_to=repo_root) - index["agents"].update(private_index["agents"]) + if args.include_private: + private_agents_dir = repo_root / "private-agents" + if private_agents_dir.exists() and any(private_agents_dir.iterdir()): + private_index = generate_index(private_agents_dir, relative_to=repo_root, include_private=True) + index["agents"].update(private_index["agents"]) # Write index file - index_file = agents_dir / "INDEX.json" - with open(index_file, "w") as f: + with open(output_path, "w") as f: json.dump(index, f, indent=2) # Summary - print(f"Generated {index_file}") + print(f"Generated {output_path}") print(f" Total agents: {len(index['agents'])}") # Show agents with routing metadata diff --git a/scripts/generate-skill-index.py b/scripts/generate-skill-index.py index 1b66e509..c4cd48ed 100755 --- a/scripts/generate-skill-index.py +++ b/scripts/generate-skill-index.py @@ -8,9 +8,15 @@ Usage: python scripts/generate-skill-index.py + python scripts/generate-skill-index.py --include-private + python scripts/generate-skill-index.py --include-private --output skills/INDEX.local.json + +Options: + --include-private Include symlinked directories (default: skip them) + --output PATH Output path (default: skills/INDEX.json) Output: - skills/INDEX.json - Skill routing index for /do router + skills/INDEX.json - Skill routing index for /do router (public, tracked) Exit codes: @@ -19,6 +25,7 @@ 2 - Trigger collisions detected among force-routed entries """ +import argparse import json import re import sys @@ -264,6 +271,7 @@ def generate_index( dir_prefix: str, collection_key: str, is_pipeline: bool = False, + include_private: bool = False, ) -> tuple[dict, list[str]]: """Generate a dict-keyed routing index from all SKILL.md files in a directory. @@ -272,6 +280,8 @@ def generate_index( dir_prefix: Path prefix for file field (e.g., "skills" or "pipelines"). collection_key: Top-level key name in the index (e.g., "skills" or "pipelines"). is_pipeline: Whether entries are pipelines (enables phase extraction). + include_private: When True, include symlinked directories. When False (default), + only directly-tracked directories are indexed. Returns: tuple: (index dict with version/generated/generated_by/collection, @@ -289,6 +299,11 @@ def generate_index( if not skill_dir.is_dir(): continue + # Skip symlinked directories unless --include-private was passed. + # The public index reflects directly-tracked files only. + if skill_dir.is_symlink() and not include_private: + continue + skill_file = skill_dir / "SKILL.md" if not skill_file.exists(): continue @@ -385,6 +400,24 @@ def write_index(index: dict, output_path: Path) -> bool: def main() -> int: """Main entry point.""" + parser = argparse.ArgumentParser( + description="Generate skill routing index from YAML frontmatter.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--include-private", + action="store_true", + default=False, + help="Include symlinked directories. Use with --output for local-only workflows.", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Output file path (default: skills/INDEX.json relative to repo root).", + ) + args = parser.parse_args() + script_dir = Path(__file__).parent repo_root = script_dir.parent skills_dir = repo_root / "skills" @@ -393,12 +426,16 @@ def main() -> int: print(f"Error: skills directory not found at {skills_dir}", file=sys.stderr) return 1 + # Resolve output path + output_path: Path = args.output if args.output is not None else skills_dir / "INDEX.json" + # Generate skills index skills_index, skills_warnings = generate_index( source_dir=skills_dir, dir_prefix="skills", collection_key="skills", is_pipeline=False, + include_private=args.include_private, ) # Report warnings if any @@ -412,8 +449,8 @@ def main() -> int: print("Error: No skills found. Index file not written.", file=sys.stderr) return 1 - # Write skills/INDEX.json - skills_index_path = skills_dir / "INDEX.json" + # Write to output path (default: skills/INDEX.json) + skills_index_path = output_path if not write_index(skills_index, skills_index_path): return 1 diff --git a/scripts/routing-manifest.py b/scripts/routing-manifest.py old mode 100644 new mode 100755 index 25e3c2e8..96cd2c93 --- a/scripts/routing-manifest.py +++ b/scripts/routing-manifest.py @@ -21,9 +21,21 @@ REPO_ROOT = Path(__file__).resolve().parent.parent + +def _resolve_index(tracked: Path, local_name: str) -> Path: + """Return the local override path when it exists, otherwise the tracked path. + + Local override files (INDEX.local.json) are gitignored and may contain + entries for symlinked directories. They are produced by running the + generator with --include-private --output . + """ + local = tracked.parent / local_name + return local if local.exists() else tracked + + INDEX_PATHS = { - "skills": REPO_ROOT / "skills" / "INDEX.json", - "agents": REPO_ROOT / "agents" / "INDEX.json", + "skills": _resolve_index(REPO_ROOT / "skills" / "INDEX.json", "INDEX.local.json"), + "agents": _resolve_index(REPO_ROOT / "agents" / "INDEX.json", "INDEX.local.json"), "pipelines": REPO_ROOT / "skills" / "workflow" / "references" / "pipeline-index.json", } diff --git a/scripts/scan-negative-framing.py b/scripts/scan-negative-framing.py old mode 100644 new mode 100755 index 88c1d867..239190ce --- a/scripts/scan-negative-framing.py +++ b/scripts/scan-negative-framing.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Scan content for negative framing patterns. -VexJoy and WrestleJoy are joy-centered publications. Content should frame -experiences positively or neutrally, not through grievance, accusation, -or victimhood. This script detects negative framing patterns that don't -match the joy-centered editorial voice. +Joy-centered content evaluation. Content should frame experiences positively +or neutrally, not through grievance, accusation, or victimhood. This script +detects negative framing patterns that don't match a joy-centered editorial +voice. Usage: python3 scripts/scan-negative-framing.py diff --git a/scripts/tests/test_generate_skill_index.py b/scripts/tests/test_generate_skill_index.py new file mode 100644 index 00000000..3c5e75d4 --- /dev/null +++ b/scripts/tests/test_generate_skill_index.py @@ -0,0 +1,180 @@ +"""Tests for scripts/generate-skill-index.py symlink-aware behavior.""" + +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parent.parent / "generate-skill-index.py" + + +def _load_gsi(): + """Load generate-skill-index.py as a module (hyphenated name requires importlib).""" + spec = importlib.util.spec_from_file_location("generate_skill_index", SCRIPT) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +gsi = _load_gsi() + +# Minimal valid SKILL.md frontmatter for test fixtures +_SKILL_FRONTMATTER = """\ +--- +name: {name} +description: A test skill for {name}. +version: "1.0" +user-invocable: false +routing: + triggers: + - {name} + category: testing +--- + +## Overview + +Test skill content. +""" + + +def _make_skill_dir(base: Path, name: str) -> Path: + """Create a real (non-symlink) skill directory with a SKILL.md.""" + skill_dir = base / name + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text(_SKILL_FRONTMATTER.format(name=name)) + return skill_dir + + +class TestSymlinkExcludedByDefault: + """Symlinked skill directories are excluded from the index in default mode.""" + + def test_symlink_excluded_by_default(self, tmp_path: Path) -> None: + """A symlinked skill directory should not appear in the index without include_private.""" + # Create the real target outside the skills/ tree + real_skills = tmp_path / "real-skills" + _make_skill_dir(real_skills, "real-target-skill") + + # Build a skills/ dir with one real skill and one symlink + skills_dir = tmp_path / "skills" + skills_dir.mkdir() + _make_skill_dir(skills_dir, "public-skill") + + symlink_dir = skills_dir / "private-skill" + symlink_dir.symlink_to(real_skills / "real-target-skill") + + index, _warnings = gsi.generate_index( + source_dir=skills_dir, + dir_prefix="skills", + collection_key="skills", + include_private=False, + ) + + assert "public-skill" in index["skills"], "Real skill should be included" + # The symlinked dir appears as 'private-skill' in the tree + assert "private-skill" not in index["skills"], "Symlinked directory entry should not appear by default" + + def test_existing_public_skills_preserved(self, tmp_path: Path) -> None: + """Non-symlinked skills are always included regardless of flag state.""" + skills_dir = tmp_path / "skills" + for name in ("alpha-skill", "beta-skill", "gamma-skill"): + _make_skill_dir(skills_dir, name) + + index, _warnings = gsi.generate_index( + source_dir=skills_dir, + dir_prefix="skills", + collection_key="skills", + include_private=False, + ) + + for name in ("alpha-skill", "beta-skill", "gamma-skill"): + assert name in index["skills"], f"Expected '{name}' in index" + + +class TestSymlinkIncludedWithFlag: + """Symlinked skill directories are included when include_private=True.""" + + def test_symlink_included_with_flag(self, tmp_path: Path) -> None: + """include_private=True causes symlinked directories to appear in the index.""" + real_skills = tmp_path / "real-skills" + _make_skill_dir(real_skills, "private-target-skill") + + skills_dir = tmp_path / "skills" + skills_dir.mkdir() + _make_skill_dir(skills_dir, "public-skill") + + symlink_dir = skills_dir / "private-target-skill" + symlink_dir.symlink_to(real_skills / "private-target-skill") + + index, _warnings = gsi.generate_index( + source_dir=skills_dir, + dir_prefix="skills", + collection_key="skills", + include_private=True, + ) + + assert "public-skill" in index["skills"], "Real skill should still be included" + assert "private-target-skill" in index["skills"], "Symlinked skill should be included when include_private=True" + + +class TestCustomOutputPath: + """The --output flag controls where the index file is written.""" + + def test_custom_output_path(self, tmp_path: Path) -> None: + """Generator writes to the path specified by --output.""" + # Build a minimal skills dir next to the output target + skills_dir = tmp_path / "skills" + _make_skill_dir(skills_dir, "some-skill") + + custom_output = tmp_path / "custom" / "output.json" + custom_output.parent.mkdir(parents=True, exist_ok=True) + + index, _warnings = gsi.generate_index( + source_dir=skills_dir, + dir_prefix="skills", + collection_key="skills", + ) + gsi.write_index(index, custom_output) + + assert custom_output.exists(), f"Expected output at {custom_output}" + data = json.loads(custom_output.read_text()) + assert "some-skill" in data["skills"] + + def test_output_is_valid_json(self, tmp_path: Path) -> None: + """The generated file is always valid JSON.""" + skills_dir = tmp_path / "skills" + _make_skill_dir(skills_dir, "json-test-skill") + + output = tmp_path / "INDEX.json" + index, _warnings = gsi.generate_index( + source_dir=skills_dir, + dir_prefix="skills", + collection_key="skills", + ) + gsi.write_index(index, output) + + # json.loads raises if invalid — that is the assertion + data = json.loads(output.read_text()) + assert isinstance(data, dict) + assert "version" in data + assert "skills" in data + + +class TestCLIFlag: + """End-to-end CLI tests for --include-private and --output flags.""" + + def test_cli_output_flag_creates_file(self, tmp_path: Path) -> None: + """--output writes to the specified path.""" + custom_output = tmp_path / "out.json" + result = subprocess.run( + [sys.executable, str(SCRIPT), "--output", str(custom_output)], + capture_output=True, + text=True, + ) + assert result.returncode == 0, f"CLI failed:\n{result.stderr}" + assert custom_output.exists(), "Output file not created" + json.loads(custom_output.read_text()) # must be valid JSON diff --git a/scripts/toolkit-evolution-cron.sh b/scripts/toolkit-evolution-cron.sh index 27890d37..d89dfda0 100755 --- a/scripts/toolkit-evolution-cron.sh +++ b/scripts/toolkit-evolution-cron.sh @@ -92,7 +92,10 @@ Key constraints: - Maximum 3 implementations per cycle (focus over breadth) - Winners that pass critique (STRONG consensus) AND A/B testing (WIN) should be merged via PR - Record all outcomes (wins AND losses) to the learning DB -- Write evolution report to evolution-reports/evolution-report-$(date +%Y-%m-%d).md" +- Write evolution report to evolution-reports/evolution-report-$(date +%Y-%m-%d).md +- Index generation: if the cycle regenerates skills/INDEX.json or agents/INDEX.json, run + the generators in default mode only. Do not pass --include-private. Do not modify + routing-tables.md to add entries for symlinked skill directories." if [ -z "$GH_AUTH_VALID" ]; then PROMPT="$PROMPT diff --git a/skills/INDEX.json b/skills/INDEX.json index 4bc8d184..ef75cd8b 100644 --- a/skills/INDEX.json +++ b/skills/INDEX.json @@ -1,6 +1,6 @@ { "version": "2.0", - "generated": "2026-04-11T10:14:10Z", + "generated": "2026-04-11T15:47:00Z", "generated_by": "scripts/generate-skill-index.py", "skills": { "adr-consultation": { @@ -752,19 +752,6 @@ "workflow" ] }, - "gemini-wrestlejoy-comparison": { - "file": "skills/gemini-wrestlejoy-comparison/SKILL.md", - "description": "A/B test Gemini Pro vs Claude for WrestleJoy voice replication: Prepare inputs, generate with both models, validate outputs, compare results.", - "triggers": [ - "gemini-wrestlejoy-comparison", - "gemini", - "wrestlejoy", - "comparison" - ], - "category": "analysis", - "user_invocable": false, - "version": "2.0.0" - }, "generate-claudemd": { "file": "skills/generate-claudemd/SKILL.md", "description": "Generate project-specific CLAUDE.md from repo analysis.", @@ -976,27 +963,6 @@ "comprehensive-review" ] }, - "joy-check": { - "file": "skills/joy-check/SKILL.md", - "description": "Validate content for joy-centered tonal framing.", - "triggers": [ - "joy check", - "check framing", - "tone check", - "negative framing", - "joy validation", - "too negative", - "reframe positively" - ], - "category": "content", - "user_invocable": true, - "version": "1.0.0", - "pairs_with": [ - "voice-writer", - "anti-ai-editor", - "voice-validator" - ] - }, "kairos-lite": { "file": "skills/kairos-lite/SKILL.md", "description": "Proactive monitoring \u2014 checks GitHub, CI, and toolkit health, produces briefings.", @@ -2187,56 +2153,6 @@ "user_invocable": false, "version": "2.0.0" }, - "voice-amy-nemmity": { - "file": "skills/voice-amy-nemmity/SKILL.md", - "description": "Amy Nemmity's writing voice: warmth, celebration, poetic fragments, community focus, character arc narratives.", - "triggers": [ - "voice-amy-nemmity", - "voice", - "amy", - "nemmity" - ], - "category": "voice", - "user_invocable": false, - "version": "2.0.0" - }, - "voice-andy-disagreement": { - "file": "skills/voice-andy-disagreement/SKILL.md", - "description": "Andy Nemmity's disagreement and forum voice with reasoning ladder, claims-over-people discipline, and anti-escalation harshness control.", - "triggers": [ - "disagree", - "counter-argument", - "critique", - "rebuttal", - "pushback", - "that's wrong", - "I disagree", - "Hacker News", - "HN comment", - "Reddit", - "forum", - "community" - ], - "category": "voice", - "user_invocable": false, - "version": "2.0.0", - "pairs_with": [ - "voice-andy-nemmity" - ] - }, - "voice-andy-nemmity": { - "file": "skills/voice-andy-nemmity/SKILL.md", - "description": "Apply Andy Nemmity's voice profile for content generation: precision-driven improvisation, constraint accumulation, systems framing, calibration qu...", - "triggers": [ - "voice-andy-nemmity", - "voice", - "andy", - "nemmity" - ], - "category": "voice", - "user_invocable": false, - "version": "2.0.0" - }, "voice-validator": { "file": "skills/voice-validator/SKILL.md", "description": "Critique-and-rewrite loop for voice fidelity validation.", @@ -2391,95 +2307,6 @@ "user_invocable": false, "version": "1.0.0" }, - "wrestlejoy-aew-images": { - "file": "skills/wrestlejoy-aew-images/SKILL.md", - "description": "Fetch official promotional images from allelitewrestling.com for WrestleJoy articles.", - "triggers": [ - "aew image", - "aew photo", - "wrestler photo", - "official aew image", - "roster image", - "roster photo", - "aew event poster", - "get aew image" - ], - "category": "content-pipeline", - "user_invocable": true, - "version": "1.0.0", - "pairs_with": [ - "wrestlejoy-news-pipeline", - "voice-andy-nemmity", - "wrestlejoy-external-research" - ], - "agent": "wrestlejoy-news-producer", - "model": "sonnet" - }, - "wrestlejoy-external-research": { - "file": "skills/wrestlejoy-external-research/SKILL.md", - "description": "Evidence-sourced 4-phase external research: Identify, Search, Extract, Validate.", - "triggers": [ - "wrestlejoy-external-research", - "wrestlejoy", - "external", - "research" - ], - "category": "content-pipeline", - "user_invocable": false, - "version": "2.0.0" - }, - "wrestlejoy-mmr-research": { - "file": "skills/wrestlejoy-mmr-research/SKILL.md", - "description": "Evidence-based wrestler research from MMR database: Assess, Query, Synthesize, Output.", - "triggers": [ - "wrestlejoy-mmr-research", - "wrestlejoy", - "mmr", - "research" - ], - "category": "content-pipeline", - "user_invocable": false, - "version": "2.0.0" - }, - "wrestlejoy-news-pipeline": { - "file": "skills/wrestlejoy-news-pipeline/SKILL.md", - "description": "Automated AEW wrestling news production pipeline for WrestleJoy.com.", - "triggers": [ - "run news pipeline", - "wrestlejoy news", - "scan wrestling news", - "generate news articles", - "wrestling news automation", - "aew news pipeline", - "news production pipeline" - ], - "category": "content-pipeline", - "user_invocable": false, - "version": "1.0.0", - "pairs_with": [ - "voice-andy-nemmity", - "voice-writer", - "anti-ai-editor", - "wordpress-uploader", - "seo-optimizer", - "wrestlejoy-external-research" - ], - "agent": "wrestlejoy-news-producer", - "model": "opus" - }, - "wrestlejoy-research-transform": { - "file": "skills/wrestlejoy-research-transform/SKILL.md", - "description": "Transform MMR research data into narrative language: convert ratings to superlatives, trajectories to journey language, peaks to memorable moments.", - "triggers": [ - "wrestlejoy-research-transform", - "wrestlejoy", - "research", - "transform" - ], - "category": "content-pipeline", - "user_invocable": false, - "version": "2.0.0" - }, "x-api": { "file": "skills/x-api/SKILL.md", "description": "Post tweets, build threads, upload media via the X API.", diff --git a/skills/do/references/routing-tables.md b/skills/do/references/routing-tables.md index e0a24fa0..ce741b6b 100644 --- a/skills/do/references/routing-tables.md +++ b/skills/do/references/routing-tables.md @@ -199,7 +199,6 @@ Route to these agents based on the user's task domain. Each entry describes what | **taxonomy-manager** | User wants to manage content categories, tags, or taxonomy systems. | | **wordpress-uploader** | User wants to upload or create draft posts in WordPress programmatically. | | **wordpress-live-validation** | User wants to validate WordPress posts live after upload: check rendering, canonical URLs, or publication status. | -| **joy-check** | User wants to validate that content has positive, joy-centered framing — not negative or fear-based tone. | | **pptx-generator** | User wants to generate a PowerPoint presentation, slide deck, or pitch deck from content or research. | | **frontend-slides** | User wants browser-based HTML presentations: reveal-style slide decks, kiosk presentations, or converting PPTX to web format. | | **gemini-image-generator** | User wants to generate images from text prompts via Google Gemini: sprites, character art, or AI-generated visuals. | @@ -225,9 +224,6 @@ Route to these agents based on the user's task domain. Each entry describes what | **voice-writer** | User wants to generate content in an established voice — multi-step generation with validation. | | **voice-calibrator** | User wants to refine an existing voice profile or improve how well it captures their writing style. | | **voice-validator** | User wants to run a validation loop to confirm generated content matches the voice profile. | -| **voice-andy-nemmity** | User wants to generate content in Andy Nemmity's specific voice: precision-driven, systems framing, improvement-focused, modal writing across 5 registers. The primary voice for VexJoy and WrestleJoy long-form content. NOT: general voice generation (use voice-writer), Amy Nemmity voice (use voice-amy-nemmity). | -| **voice-amy-nemmity** | User wants to generate content in Amy Nemmity's voice: warmth, celebration, poetic fragments, community focus, wabi-sabi authenticity. The WrestleJoy editorial voice for wrestling coverage and awards content. NOT: Andy Nemmity voice (use voice-andy-nemmity). | -| **voice-andy-disagreement** | User wants to write a counter-argument, disagreement post, or forum response in Andy Nemmity's reasoning-first voice: claims over politeness, reasoning ladder, direct rebuttal. NOT: general debate framing (use roast or multi-persona-critique). | **Voice selection:** Use `create-voice` to build voice profiles from writing samples, then `voice-writer` for multi-step generation in that voice. Custom voice profiles are matched via their skill triggers. @@ -301,19 +297,6 @@ Workflows that work together in common sequences: --- -## WrestleJoy Skills - -| Skill | When to Route Here | -|-------|-------------------| -| **wrestlejoy-news-pipeline** | User wants to run the automated WrestleJoy news pipeline: discover positive AEW wrestling news, filter through positivity gate, cluster stories, generate articles in Andy's voice, validate, and upload as WordPress drafts. Triggers: "run news pipeline", "wrestlejoy news", "scan wrestling news", "generate news articles". NOT: long-form wrestler profiles (use voice-andy-nemmity directly). | -| **wrestlejoy-aew-images** | User wants to fetch official promotional images from allelitewrestling.com for WrestleJoy content: event banners, wrestler headshots, promotional art. | -| **wrestlejoy-external-research** | User wants evidence-sourced external research for a WrestleJoy article: identify claims, search sources, extract evidence, validate against multiple sources. | -| **wrestlejoy-mmr-research** | User wants to research wrestler match quality ratings from the MMR database for WrestleJoy content: fetch ratings, assess match history, synthesize data. | -| **wrestlejoy-research-transform** | User wants to convert raw MMR wrestling data into narrative language for WrestleJoy articles: transform ratings into superlatives, statistics into story beats, data into prose. | -| **gemini-wrestlejoy-comparison** | User wants to A/B test Gemini Pro vs Claude for WrestleJoy voice replication quality — comparing outputs against Andy Nemmity's voice profile. | - ---- - ## Validation Skills | Skill | When to Route Here | From b09d20e39425c293f400d6c6cf2540014c137072 Mon Sep 17 00:00:00 2001 From: notque Date: Sat, 11 Apr 2026 08:55:23 -0700 Subject: [PATCH 2/2] chore(gitignore): ignore local index override targets --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index daad5370..a3f8647e 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,10 @@ private-agents/ private-hooks/ private-voices/ +# Local-only index targets, not committed +skills/INDEX.local.json +agents/INDEX.local.json + # Draft articles (work in progress, not committed) drafts/ draft-*.md