From ecd96bb8d215bcfa26226c50b11b9480593670ed Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 11:09:32 +0900 Subject: [PATCH] feat(plugin): define mode-specific Tiny Actor cast presets (#1272) Add deterministic cast presets for PLAN, EVAL, AUTO, and SHIP modes with Buddy moderator copy. Each preset maps a primary agent and specialist list from real .ai-rules agent definitions. --- .../hooks/lib/tiny_actor_presets.py | 74 +++++++ .../tests/test_tiny_actor_presets.py | 206 ++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 packages/claude-code-plugin/hooks/lib/tiny_actor_presets.py create mode 100644 packages/claude-code-plugin/tests/test_tiny_actor_presets.py diff --git a/packages/claude-code-plugin/hooks/lib/tiny_actor_presets.py b/packages/claude-code-plugin/hooks/lib/tiny_actor_presets.py new file mode 100644 index 00000000..3178e15c --- /dev/null +++ b/packages/claude-code-plugin/hooks/lib/tiny_actor_presets.py @@ -0,0 +1,74 @@ +"""Mode-specific Tiny Actor cast presets and Buddy moderator copy. + +Defines deterministic starter content for the Tiny Actor Grid: +- Cast presets per workflow mode (PLAN, EVAL, AUTO, SHIP) +- Buddy moderator identity and greeting copy +- Agent IDs reference real .ai-rules/agents/*.json definitions +""" +from typing import Dict, List, Optional, TypedDict + + +class CastPreset(TypedDict): + primary: str + specialists: List[str] + moderator_copy: str + + +# Buddy moderator identity +BUDDY_FACE: str = "\u25d5\u203f\u25d5" # ◕‿◕ + +DEFAULT_MODERATOR_COPY: str = "Let's get to work." + +CAST_PRESETS: Dict[str, CastPreset] = { + "PLAN": { + "primary": "technical-planner", + "specialists": [ + "security-specialist", + "test-strategy-specialist", + "architecture-specialist", + ], + "moderator_copy": "Let's map it out.", + }, + "EVAL": { + "primary": "code-reviewer", + "specialists": [ + "security-specialist", + "performance-specialist", + "accessibility-specialist", + ], + "moderator_copy": "Time for a checkup.", + }, + "AUTO": { + "primary": "auto-mode", + "specialists": [ + "architecture-specialist", + "security-specialist", + "code-quality-specialist", + "test-strategy-specialist", + ], + "moderator_copy": "Running full cycle.", + }, + "SHIP": { + "primary": "devops-engineer", + "specialists": [ + "security-specialist", + "test-engineer", + "performance-specialist", + "integration-specialist", + ], + "moderator_copy": "Pre-flight checks...", + }, +} + + +def get_cast_preset(mode: str) -> Optional[CastPreset]: + """Return the cast preset for *mode*, or ``None`` for unknown modes.""" + return CAST_PRESETS.get(mode) + + +def get_moderator_copy(mode: str) -> str: + """Return Buddy's moderator copy for *mode*, falling back to the default.""" + preset = CAST_PRESETS.get(mode) + if preset is not None: + return preset["moderator_copy"] + return DEFAULT_MODERATOR_COPY diff --git a/packages/claude-code-plugin/tests/test_tiny_actor_presets.py b/packages/claude-code-plugin/tests/test_tiny_actor_presets.py new file mode 100644 index 00000000..6a1bf1bf --- /dev/null +++ b/packages/claude-code-plugin/tests/test_tiny_actor_presets.py @@ -0,0 +1,206 @@ +"""Tests for hooks/lib/tiny_actor_presets.py — Tiny Actor cast presets.""" +import os +import re +import sys + +import pytest + +# Add hooks/lib to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "hooks", "lib")) + +from tiny_actor_presets import ( + BUDDY_FACE, + CAST_PRESETS, + DEFAULT_MODERATOR_COPY, + get_cast_preset, + get_moderator_copy, +) + +VALID_MODES = ["PLAN", "EVAL", "AUTO", "SHIP"] +KEBAB_CASE_RE = re.compile(r"^[a-z]+(-[a-z]+)*$") +MAX_MODERATOR_COPY_LEN = 40 + + +# --------------------------------------------------------------------------- +# Agents directory for cross-referencing agent IDs +# --------------------------------------------------------------------------- + +def _find_agents_dir(): + """Locate .ai-rules/agents relative to project root.""" + project_root = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..") + ) + candidates = [ + os.path.join(project_root, "packages", "rules", ".ai-rules", "agents"), + os.path.join(project_root, ".ai-rules", "agents"), + ] + for c in candidates: + if os.path.isdir(c): + return c + return None + + +AGENTS_DIR = _find_agents_dir() + + +# --------------------------------------------------------------------------- +# Basic preset tests +# --------------------------------------------------------------------------- + + +class TestGetCastPreset: + @pytest.mark.parametrize("mode", VALID_MODES) + def test_valid_mode_returns_dict(self, mode: str): + result = get_cast_preset(mode) + assert isinstance(result, dict) + + @pytest.mark.parametrize("mode", ["UNKNOWN", "debug", "", "plan"]) + def test_unknown_mode_returns_none(self, mode: str): + assert get_cast_preset(mode) is None + + @pytest.mark.parametrize("mode", VALID_MODES) + def test_preset_has_required_keys(self, mode: str): + preset = get_cast_preset(mode) + assert "primary" in preset + assert "specialists" in preset + assert "moderator_copy" in preset + + @pytest.mark.parametrize("mode", VALID_MODES) + def test_primary_is_string(self, mode: str): + preset = get_cast_preset(mode) + assert isinstance(preset["primary"], str) + assert len(preset["primary"]) > 0 + + @pytest.mark.parametrize("mode", VALID_MODES) + def test_specialists_is_nonempty_list(self, mode: str): + preset = get_cast_preset(mode) + assert isinstance(preset["specialists"], list) + assert len(preset["specialists"]) >= 2 + + @pytest.mark.parametrize("mode", VALID_MODES) + def test_moderator_copy_is_nonempty_string(self, mode: str): + preset = get_cast_preset(mode) + assert isinstance(preset["moderator_copy"], str) + assert len(preset["moderator_copy"]) > 0 + + +# --------------------------------------------------------------------------- +# Determinism +# --------------------------------------------------------------------------- + + +class TestDeterminism: + @pytest.mark.parametrize("mode", VALID_MODES) + def test_same_input_same_output(self, mode: str): + first = get_cast_preset(mode) + second = get_cast_preset(mode) + assert first == second + + +# --------------------------------------------------------------------------- +# Agent ID format (kebab-case) +# --------------------------------------------------------------------------- + + +class TestAgentIdFormat: + @pytest.mark.parametrize("mode", VALID_MODES) + def test_primary_is_kebab_case(self, mode: str): + preset = get_cast_preset(mode) + assert KEBAB_CASE_RE.match(preset["primary"]), ( + f"primary '{preset['primary']}' is not kebab-case" + ) + + @pytest.mark.parametrize("mode", VALID_MODES) + def test_specialists_are_kebab_case(self, mode: str): + preset = get_cast_preset(mode) + for agent_id in preset["specialists"]: + assert KEBAB_CASE_RE.match(agent_id), ( + f"specialist '{agent_id}' is not kebab-case" + ) + + +# --------------------------------------------------------------------------- +# Agent IDs reference existing agent JSON files +# --------------------------------------------------------------------------- + + +@pytest.mark.skipif(AGENTS_DIR is None, reason="agents directory not found") +class TestAgentReferences: + def _all_agent_ids(self): + ids = set() + for mode in VALID_MODES: + preset = get_cast_preset(mode) + ids.add(preset["primary"]) + ids.update(preset["specialists"]) + return ids + + def test_all_agent_ids_have_json_files(self): + for agent_id in self._all_agent_ids(): + path = os.path.join(AGENTS_DIR, f"{agent_id}.json") + assert os.path.isfile(path), ( + f"Agent '{agent_id}' has no JSON at {path}" + ) + + +# --------------------------------------------------------------------------- +# Display budget — moderator copy length +# --------------------------------------------------------------------------- + + +class TestDisplayBudget: + @pytest.mark.parametrize("mode", VALID_MODES) + def test_moderator_copy_within_budget(self, mode: str): + preset = get_cast_preset(mode) + assert len(preset["moderator_copy"]) <= MAX_MODERATOR_COPY_LEN, ( + f"moderator_copy too long ({len(preset['moderator_copy'])} chars)" + ) + + def test_default_moderator_copy_within_budget(self): + assert len(DEFAULT_MODERATOR_COPY) <= MAX_MODERATOR_COPY_LEN + + +# --------------------------------------------------------------------------- +# get_moderator_copy function +# --------------------------------------------------------------------------- + + +class TestGetModeratorCopy: + @pytest.mark.parametrize("mode", VALID_MODES) + def test_returns_mode_specific_copy(self, mode: str): + copy = get_moderator_copy(mode) + preset = get_cast_preset(mode) + assert copy == preset["moderator_copy"] + + def test_unknown_mode_returns_default(self): + assert get_moderator_copy("UNKNOWN") == DEFAULT_MODERATOR_COPY + + def test_returns_string(self): + assert isinstance(get_moderator_copy("PLAN"), str) + + +# --------------------------------------------------------------------------- +# Buddy identity +# --------------------------------------------------------------------------- + + +class TestBuddyIdentity: + def test_buddy_face_is_defined(self): + assert isinstance(BUDDY_FACE, str) + assert len(BUDDY_FACE) > 0 + + def test_buddy_face_value(self): + assert BUDDY_FACE == "\u25d5\u203f\u25d5" + + +# --------------------------------------------------------------------------- +# CAST_PRESETS dict integrity +# --------------------------------------------------------------------------- + + +class TestCastPresetsDict: + def test_has_all_valid_modes(self): + for mode in VALID_MODES: + assert mode in CAST_PRESETS + + def test_no_extra_modes(self): + assert set(CAST_PRESETS.keys()) == set(VALID_MODES)