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 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 |