Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions packages/claude-code-plugin/hooks/lib/achievement_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,70 @@
"icon": "\U0001f525", # fire
"face": "\u2666\u203f\u2666", # diamond eyes
},
# Micro-achievements: first-5-minute wins (#1436)
{
"id": "first_plan",
"name": "Planner!",
"description": "Enter PLAN mode for the first time",
"metric": "plan_entries",
"threshold": 1,
"icon": "\U0001f4dd", # memo
"face": "\u25c7\u203f\u25c7", # thin diamond eyes
},
{
"id": "first_act",
"name": "Builder!",
"description": "Enter ACT mode for the first time",
"metric": "act_entries",
"threshold": 1,
"icon": "\U0001f528", # hammer
"face": "\u25a0\u203f\u25a0", # square eyes
},
{
"id": "first_eval",
"name": "Critic!",
"description": "Enter EVAL mode for the first time",
"metric": "eval_entries",
"threshold": 1,
"icon": "\U0001f50d", # magnifying glass
"face": "\u25cb\u203f\u25cb", # circle eyes
},
{
"id": "first_auto",
"name": "Autopilot!",
"description": "Enter AUTO mode for the first time",
"metric": "auto_entries",
"threshold": 1,
"icon": "\U0001f680", # rocket
"face": "\u25b7\u203f\u25b7", # triangle eyes
},
{
"id": "council_summon",
"name": "Council Summoner",
"description": "Summon a specialist council for the first time",
"metric": "council_summons",
"threshold": 1,
"icon": "\U0001f451", # crown
"face": "\u2726\u203f\u2726", # four-pointed star eyes
},
{
"id": "tdd_first",
"name": "Test First!",
"description": "Complete your first TDD cycle",
"metric": "tdd_cycles",
"threshold": 1,
"icon": "\u2705", # check mark
"face": "\u2713\u203f\u2713", # checkmark eyes
},
{
"id": "multi_agent",
"name": "Orchestrator!",
"description": "Dispatch parallel specialist agents for the first time",
"metric": "multi_agent_dispatches",
"threshold": 1,
"icon": "\U0001f3af", # bullseye
"face": "\u29bf\u203f\u29bf", # circled bullet eyes
},
]

# Celebration messages by language
Expand Down Expand Up @@ -162,6 +226,13 @@ def _default_progress(self) -> Dict[str, Any]:
"commit_timestamps": [],
"session_dates": [],
"max_streak_days": 0,
# Micro-achievement counters (#1436)
"plan_entries": 0,
"act_entries": 0,
"eval_entries": 0,
"auto_entries": 0,
"council_summons": 0,
"multi_agent_dispatches": 0,
}

def get_progress(self) -> Dict[str, Any]:
Expand All @@ -180,6 +251,29 @@ def get_unlocked(self) -> List[Dict[str, Any]]:
"""
return self._locked_read(self.unlocked_file, [])

def record_mode_entry(self, mode: str) -> None:
"""Record a mode entry for micro-achievement tracking (#1436).

Args:
mode: The mode entered (PLAN, ACT, EVAL, AUTO).
"""
key = f"{mode.lower()}_entries"
progress = self.get_progress()
progress[key] = progress.get(key, 0) + 1
self._locked_write(self.progress_file, progress)

def record_council_summon(self) -> None:
"""Record a council scene render for micro-achievement tracking (#1436)."""
progress = self.get_progress()
progress["council_summons"] = progress.get("council_summons", 0) + 1
self._locked_write(self.progress_file, progress)

def record_multi_agent_dispatch(self) -> None:
"""Record a parallel agent dispatch for micro-achievement tracking (#1436)."""
progress = self.get_progress()
progress["multi_agent_dispatches"] = progress.get("multi_agent_dispatches", 0) + 1
self._locked_write(self.progress_file, progress)

def record_tdd_cycle(self) -> None:
"""Record a completed TDD cycle."""
progress = self.get_progress()
Expand Down
28 changes: 28 additions & 0 deletions packages/claude-code-plugin/hooks/lib/agent_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@ def get_context_prompt(self, agent_name: str) -> str:
parts.append(f"- {json.dumps(p, ensure_ascii=False)}")
return "\n".join(parts)

def get_council_context(self, agent_names: list) -> str:
"""Get combined context prompts for a list of specialists (#1435).

Returns a formatted block suitable for hook output injection.
Only includes agents that have prior findings.

Args:
agent_names: List of specialist agent names.

Returns:
Formatted markdown with memory context, or empty string.
"""
sections = []
returning = []
for name in agent_names:
prompt = self.get_context_prompt(name)
if prompt:
returning.append(name)
sections.append(f"### {name} (returning)\n{prompt}")
if not sections:
return ""
header = (
f"# Agent Council Memory\n"
f"The following {len(returning)} specialist(s) have prior findings "
f"from previous sessions. Include this context when dispatching them.\n"
)
return header + "\n\n".join(sections)

def clear(self, agent_name: str) -> None:
filepath = self._filepath(agent_name)
if os.path.isfile(filepath):
Expand Down
21 changes: 21 additions & 0 deletions packages/claude-code-plugin/hooks/post-tool-use.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ def handle_post_tool_use(data: dict):
except Exception:
pass # Never block tool execution

# Micro-achievement: detect parallel agent dispatch (#1436)
try:
tool_name = data.get("tool_name", "")
if tool_name == "Agent" and data.get("tool_input", {}).get("run_in_background"):
from achievement_tracker import (
AchievementTracker,
render_batch_celebration,
)

tracker = AchievementTracker()
tracker.record_multi_agent_dispatch()
newly_unlocked = tracker.check_achievements()
if newly_unlocked:
import sys as _sys

celebration = render_batch_celebration(newly_unlocked)
if celebration:
print(celebration, file=_sys.stderr)
except Exception:
pass # Never block tool execution

return None


Expand Down
115 changes: 107 additions & 8 deletions packages/claude-code-plugin/hooks/tests/test_achievement_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ class TestAchievementDefinitions:

def test_get_all_definitions(self):
defs = get_achievement_definitions()
assert len(defs) == 5
assert len(defs) == 12 # 5 original + 7 micro-achievements (#1436)
ids = {d["id"] for d in defs}
assert ids == {
assert {
"tdd_champion",
"agent_master",
"quality_guard",
"speed_coder",
"streak",
}
}.issubset(ids)

def test_get_by_id_found(self):
defn = get_achievement_by_id("tdd_champion")
Expand Down Expand Up @@ -199,8 +199,9 @@ def test_tdd_champion_unlock(self, tracker):
tracker._locked_write(tracker.progress_file, progress)

newly = tracker.check_achievements()
assert len(newly) == 1
assert newly[0]["id"] == "tdd_champion"
ids = [a["id"] for a in newly]
assert "tdd_champion" in ids
assert "tdd_first" in ids # threshold=1 also triggers

def test_agent_master_unlock(self, tracker):
progress = tracker.get_progress()
Expand Down Expand Up @@ -240,7 +241,8 @@ def test_already_unlocked_not_repeated(self, tracker):
tracker._locked_write(tracker.progress_file, progress)

first = tracker.check_achievements()
assert len(first) == 1
first_ids = {a["id"] for a in first}
assert "tdd_champion" in first_ids

second = tracker.check_achievements()
assert second == []
Expand All @@ -254,8 +256,9 @@ def test_unlocked_persisted(self, tracker, tmp_data_dir):
# New tracker instance reads persisted data
tracker2 = AchievementTracker(data_dir=tmp_data_dir)
unlocked = tracker2.get_unlocked()
assert len(unlocked) == 1
assert unlocked[0]["id"] == "tdd_champion"
unlocked_ids = {u["id"] for u in unlocked}
assert "tdd_champion" in unlocked_ids
assert "tdd_first" in unlocked_ids


class TestStreakCalculation:
Expand Down Expand Up @@ -402,3 +405,99 @@ def test_two_achievements(self):
result = render_batch_celebration(achievements, "en")
assert "Alpha" in result
assert "+1 more achievements unlocked!" in result


class TestMicroAchievements:
"""Tests for micro-achievement definitions and triggers (#1436)."""

def test_micro_achievement_definitions_exist(self):
"""All 7 micro-achievements should be defined."""
micro_ids = {
"first_plan", "first_act", "first_eval", "first_auto",
"council_summon", "tdd_first", "multi_agent",
}
defined_ids = {d["id"] for d in ACHIEVEMENT_DEFINITIONS}
assert micro_ids.issubset(defined_ids)

def test_micro_achievements_have_threshold_1(self):
"""Micro-achievements should trigger on first occurrence."""
micro_ids = {
"first_plan", "first_act", "first_eval", "first_auto",
"council_summon", "tdd_first", "multi_agent",
}
for defn in ACHIEVEMENT_DEFINITIONS:
if defn["id"] in micro_ids:
assert defn["threshold"] == 1, f"{defn['id']} should have threshold=1"

def test_record_mode_entry_plan(self, tmp_data_dir):
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("PLAN")
progress = tracker.get_progress()
assert progress["plan_entries"] == 1

def test_record_mode_entry_act(self, tmp_data_dir):
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("ACT")
progress = tracker.get_progress()
assert progress["act_entries"] == 1

def test_record_mode_entry_eval(self, tmp_data_dir):
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("EVAL")
progress = tracker.get_progress()
assert progress["eval_entries"] == 1

def test_record_mode_entry_auto(self, tmp_data_dir):
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("AUTO")
progress = tracker.get_progress()
assert progress["auto_entries"] == 1

def test_record_council_summon(self, tmp_data_dir):
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_council_summon()
progress = tracker.get_progress()
assert progress["council_summons"] == 1

def test_record_multi_agent_dispatch(self, tmp_data_dir):
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_multi_agent_dispatch()
progress = tracker.get_progress()
assert progress["multi_agent_dispatches"] == 1

def test_first_plan_unlocks_achievement(self, tmp_data_dir):
"""First PLAN entry should unlock 'Planner!' achievement."""
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("PLAN")
newly = tracker.check_achievements()
ids = [a["id"] for a in newly]
assert "first_plan" in ids

def test_first_tdd_unlocks_both(self, tmp_data_dir):
"""First TDD cycle should unlock both 'tdd_first' (threshold=1)."""
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_tdd_cycle()
newly = tracker.check_achievements()
ids = [a["id"] for a in newly]
assert "tdd_first" in ids

def test_micro_achievement_fires_only_once(self, tmp_data_dir):
"""Achievement should not fire again on second mode entry."""
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("PLAN")
first = tracker.check_achievements()
assert any(a["id"] == "first_plan" for a in first)

tracker.record_mode_entry("PLAN")
second = tracker.check_achievements()
assert not any(a["id"] == "first_plan" for a in second)

def test_multiple_modes_unlock_independently(self, tmp_data_dir):
"""Each mode entry unlocks its own achievement."""
tracker = AchievementTracker(data_dir=tmp_data_dir)
tracker.record_mode_entry("PLAN")
tracker.record_mode_entry("ACT")
newly = tracker.check_achievements()
ids = [a["id"] for a in newly]
assert "first_plan" in ids
assert "first_act" in ids
38 changes: 38 additions & 0 deletions packages/claude-code-plugin/hooks/user-prompt-submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,44 @@ def main():
except Exception:
pass

# Agent Council Memory: inject prior findings for specialists (#1435)
try:
from agent_memory import AgentMemory

_council_agents = []
if council_preset and isinstance(council_preset, dict):
# Extract specialist names from council preset
_council_agents = council_preset.get("specialists", [])
_primary = council_preset.get("primary")
if _primary:
_council_agents = [_primary] + list(_council_agents)
if _council_agents:
mem = AgentMemory()
memory_context = mem.get_council_context(_council_agents)
if memory_context:
print(memory_context)
except Exception:
pass # Never block prompt submission

# Micro-achievement: record mode entry (#1436)
try:
from achievement_tracker import (
AchievementTracker,
render_batch_celebration,
)

tracker = AchievementTracker()
tracker.record_mode_entry(detected_mode)
if council_preset:
tracker.record_council_summon()
newly_unlocked = tracker.check_achievements()
if newly_unlocked:
celebration = render_batch_celebration(newly_unlocked)
if celebration:
print(celebration, file=sys.stderr)
except Exception:
pass # Never block prompt submission

# Exit successfully (exit code 0 = success, output added as context)
sys.exit(0)

Expand Down
Loading
Loading