Skip to content

Commit b9df988

Browse files
committed
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.
1 parent 9afa3db commit b9df988

2 files changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Mode-specific Tiny Actor cast presets and Buddy moderator copy.
2+
3+
Defines deterministic starter content for the Tiny Actor Grid:
4+
- Cast presets per workflow mode (PLAN, EVAL, AUTO, SHIP)
5+
- Buddy moderator identity and greeting copy
6+
- Agent IDs reference real .ai-rules/agents/*.json definitions
7+
"""
8+
from typing import Dict, List, Optional, TypedDict
9+
10+
11+
class CastPreset(TypedDict):
12+
primary: str
13+
specialists: List[str]
14+
moderator_copy: str
15+
16+
17+
# Buddy moderator identity
18+
BUDDY_FACE: str = "\u25d5\u203f\u25d5" # ◕‿◕
19+
20+
DEFAULT_MODERATOR_COPY: str = "Let's get to work."
21+
22+
CAST_PRESETS: Dict[str, CastPreset] = {
23+
"PLAN": {
24+
"primary": "technical-planner",
25+
"specialists": [
26+
"security-specialist",
27+
"test-strategy-specialist",
28+
"architecture-specialist",
29+
],
30+
"moderator_copy": "Let's map it out.",
31+
},
32+
"EVAL": {
33+
"primary": "code-reviewer",
34+
"specialists": [
35+
"security-specialist",
36+
"performance-specialist",
37+
"accessibility-specialist",
38+
],
39+
"moderator_copy": "Time for a checkup.",
40+
},
41+
"AUTO": {
42+
"primary": "auto-mode",
43+
"specialists": [
44+
"architecture-specialist",
45+
"security-specialist",
46+
"code-quality-specialist",
47+
"test-strategy-specialist",
48+
],
49+
"moderator_copy": "Running full cycle.",
50+
},
51+
"SHIP": {
52+
"primary": "devops-engineer",
53+
"specialists": [
54+
"security-specialist",
55+
"test-engineer",
56+
"performance-specialist",
57+
"integration-specialist",
58+
],
59+
"moderator_copy": "Pre-flight checks...",
60+
},
61+
}
62+
63+
64+
def get_cast_preset(mode: str) -> Optional[CastPreset]:
65+
"""Return the cast preset for *mode*, or ``None`` for unknown modes."""
66+
return CAST_PRESETS.get(mode)
67+
68+
69+
def get_moderator_copy(mode: str) -> str:
70+
"""Return Buddy's moderator copy for *mode*, falling back to the default."""
71+
preset = CAST_PRESETS.get(mode)
72+
if preset is not None:
73+
return preset["moderator_copy"]
74+
return DEFAULT_MODERATOR_COPY
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""Tests for hooks/lib/tiny_actor_presets.py — Tiny Actor cast presets."""
2+
import os
3+
import re
4+
import sys
5+
6+
import pytest
7+
8+
# Add hooks/lib to path
9+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "hooks", "lib"))
10+
11+
from tiny_actor_presets import (
12+
BUDDY_FACE,
13+
CAST_PRESETS,
14+
DEFAULT_MODERATOR_COPY,
15+
get_cast_preset,
16+
get_moderator_copy,
17+
)
18+
19+
VALID_MODES = ["PLAN", "EVAL", "AUTO", "SHIP"]
20+
KEBAB_CASE_RE = re.compile(r"^[a-z]+(-[a-z]+)*$")
21+
MAX_MODERATOR_COPY_LEN = 40
22+
23+
24+
# ---------------------------------------------------------------------------
25+
# Agents directory for cross-referencing agent IDs
26+
# ---------------------------------------------------------------------------
27+
28+
def _find_agents_dir():
29+
"""Locate .ai-rules/agents relative to project root."""
30+
project_root = os.path.abspath(
31+
os.path.join(os.path.dirname(__file__), "..", "..", "..")
32+
)
33+
candidates = [
34+
os.path.join(project_root, "packages", "rules", ".ai-rules", "agents"),
35+
os.path.join(project_root, ".ai-rules", "agents"),
36+
]
37+
for c in candidates:
38+
if os.path.isdir(c):
39+
return c
40+
return None
41+
42+
43+
AGENTS_DIR = _find_agents_dir()
44+
45+
46+
# ---------------------------------------------------------------------------
47+
# Basic preset tests
48+
# ---------------------------------------------------------------------------
49+
50+
51+
class TestGetCastPreset:
52+
@pytest.mark.parametrize("mode", VALID_MODES)
53+
def test_valid_mode_returns_dict(self, mode: str):
54+
result = get_cast_preset(mode)
55+
assert isinstance(result, dict)
56+
57+
@pytest.mark.parametrize("mode", ["UNKNOWN", "debug", "", "plan"])
58+
def test_unknown_mode_returns_none(self, mode: str):
59+
assert get_cast_preset(mode) is None
60+
61+
@pytest.mark.parametrize("mode", VALID_MODES)
62+
def test_preset_has_required_keys(self, mode: str):
63+
preset = get_cast_preset(mode)
64+
assert "primary" in preset
65+
assert "specialists" in preset
66+
assert "moderator_copy" in preset
67+
68+
@pytest.mark.parametrize("mode", VALID_MODES)
69+
def test_primary_is_string(self, mode: str):
70+
preset = get_cast_preset(mode)
71+
assert isinstance(preset["primary"], str)
72+
assert len(preset["primary"]) > 0
73+
74+
@pytest.mark.parametrize("mode", VALID_MODES)
75+
def test_specialists_is_nonempty_list(self, mode: str):
76+
preset = get_cast_preset(mode)
77+
assert isinstance(preset["specialists"], list)
78+
assert len(preset["specialists"]) >= 2
79+
80+
@pytest.mark.parametrize("mode", VALID_MODES)
81+
def test_moderator_copy_is_nonempty_string(self, mode: str):
82+
preset = get_cast_preset(mode)
83+
assert isinstance(preset["moderator_copy"], str)
84+
assert len(preset["moderator_copy"]) > 0
85+
86+
87+
# ---------------------------------------------------------------------------
88+
# Determinism
89+
# ---------------------------------------------------------------------------
90+
91+
92+
class TestDeterminism:
93+
@pytest.mark.parametrize("mode", VALID_MODES)
94+
def test_same_input_same_output(self, mode: str):
95+
first = get_cast_preset(mode)
96+
second = get_cast_preset(mode)
97+
assert first == second
98+
99+
100+
# ---------------------------------------------------------------------------
101+
# Agent ID format (kebab-case)
102+
# ---------------------------------------------------------------------------
103+
104+
105+
class TestAgentIdFormat:
106+
@pytest.mark.parametrize("mode", VALID_MODES)
107+
def test_primary_is_kebab_case(self, mode: str):
108+
preset = get_cast_preset(mode)
109+
assert KEBAB_CASE_RE.match(preset["primary"]), (
110+
f"primary '{preset['primary']}' is not kebab-case"
111+
)
112+
113+
@pytest.mark.parametrize("mode", VALID_MODES)
114+
def test_specialists_are_kebab_case(self, mode: str):
115+
preset = get_cast_preset(mode)
116+
for agent_id in preset["specialists"]:
117+
assert KEBAB_CASE_RE.match(agent_id), (
118+
f"specialist '{agent_id}' is not kebab-case"
119+
)
120+
121+
122+
# ---------------------------------------------------------------------------
123+
# Agent IDs reference existing agent JSON files
124+
# ---------------------------------------------------------------------------
125+
126+
127+
@pytest.mark.skipif(AGENTS_DIR is None, reason="agents directory not found")
128+
class TestAgentReferences:
129+
def _all_agent_ids(self):
130+
ids = set()
131+
for mode in VALID_MODES:
132+
preset = get_cast_preset(mode)
133+
ids.add(preset["primary"])
134+
ids.update(preset["specialists"])
135+
return ids
136+
137+
def test_all_agent_ids_have_json_files(self):
138+
for agent_id in self._all_agent_ids():
139+
path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
140+
assert os.path.isfile(path), (
141+
f"Agent '{agent_id}' has no JSON at {path}"
142+
)
143+
144+
145+
# ---------------------------------------------------------------------------
146+
# Display budget — moderator copy length
147+
# ---------------------------------------------------------------------------
148+
149+
150+
class TestDisplayBudget:
151+
@pytest.mark.parametrize("mode", VALID_MODES)
152+
def test_moderator_copy_within_budget(self, mode: str):
153+
preset = get_cast_preset(mode)
154+
assert len(preset["moderator_copy"]) <= MAX_MODERATOR_COPY_LEN, (
155+
f"moderator_copy too long ({len(preset['moderator_copy'])} chars)"
156+
)
157+
158+
def test_default_moderator_copy_within_budget(self):
159+
assert len(DEFAULT_MODERATOR_COPY) <= MAX_MODERATOR_COPY_LEN
160+
161+
162+
# ---------------------------------------------------------------------------
163+
# get_moderator_copy function
164+
# ---------------------------------------------------------------------------
165+
166+
167+
class TestGetModeratorCopy:
168+
@pytest.mark.parametrize("mode", VALID_MODES)
169+
def test_returns_mode_specific_copy(self, mode: str):
170+
copy = get_moderator_copy(mode)
171+
preset = get_cast_preset(mode)
172+
assert copy == preset["moderator_copy"]
173+
174+
def test_unknown_mode_returns_default(self):
175+
assert get_moderator_copy("UNKNOWN") == DEFAULT_MODERATOR_COPY
176+
177+
def test_returns_string(self):
178+
assert isinstance(get_moderator_copy("PLAN"), str)
179+
180+
181+
# ---------------------------------------------------------------------------
182+
# Buddy identity
183+
# ---------------------------------------------------------------------------
184+
185+
186+
class TestBuddyIdentity:
187+
def test_buddy_face_is_defined(self):
188+
assert isinstance(BUDDY_FACE, str)
189+
assert len(BUDDY_FACE) > 0
190+
191+
def test_buddy_face_value(self):
192+
assert BUDDY_FACE == "\u25d5\u203f\u25d5"
193+
194+
195+
# ---------------------------------------------------------------------------
196+
# CAST_PRESETS dict integrity
197+
# ---------------------------------------------------------------------------
198+
199+
200+
class TestCastPresetsDict:
201+
def test_has_all_valid_modes(self):
202+
for mode in VALID_MODES:
203+
assert mode in CAST_PRESETS
204+
205+
def test_no_extra_modes(self):
206+
assert set(CAST_PRESETS.keys()) == set(VALID_MODES)

0 commit comments

Comments
 (0)