Compare commits
3 commits
85af20b6f1
...
caa655ab9a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa655ab9a | ||
|
|
c85bade62f | ||
|
|
8e0a5f82cb |
6 changed files with 850 additions and 172 deletions
|
|
@ -24,6 +24,30 @@ PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", str(Path(__file__).parent.par
|
|||
BUDDYMON_DIR = Path.home() / ".claude" / "buddymon"
|
||||
CATALOG_FILE = Path(PLUGIN_ROOT) / "lib" / "catalog.json"
|
||||
|
||||
# Each CC session gets its own state file keyed by process group ID.
|
||||
# All hooks within one session share the same PGRP, giving stable per-session state.
|
||||
SESSION_KEY = str(os.getpgrp())
|
||||
SESSION_FILE = BUDDYMON_DIR / "sessions" / f"{SESSION_KEY}.json"
|
||||
|
||||
|
||||
def get_session_state() -> dict:
|
||||
"""Read the current session's state file, falling back to global active.json."""
|
||||
session = load_json(SESSION_FILE)
|
||||
if not session:
|
||||
# No session file yet — inherit from global default
|
||||
global_active = load_json(BUDDYMON_DIR / "active.json")
|
||||
session = {
|
||||
"buddymon_id": global_active.get("buddymon_id"),
|
||||
"challenge": global_active.get("challenge"),
|
||||
"session_xp": 0,
|
||||
}
|
||||
return session
|
||||
|
||||
|
||||
def save_session_state(state: dict) -> None:
|
||||
SESSION_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_json(SESSION_FILE, state)
|
||||
|
||||
KNOWN_EXTENSIONS = {
|
||||
".py": "Python", ".js": "JavaScript", ".ts": "TypeScript",
|
||||
".jsx": "JavaScript/React", ".tsx": "TypeScript/React",
|
||||
|
|
@ -62,15 +86,13 @@ def get_state():
|
|||
|
||||
|
||||
def add_session_xp(amount: int):
|
||||
active_file = BUDDYMON_DIR / "active.json"
|
||||
roster_file = BUDDYMON_DIR / "roster.json"
|
||||
|
||||
active = load_json(active_file)
|
||||
active["session_xp"] = active.get("session_xp", 0) + amount
|
||||
buddy_id = active.get("buddymon_id")
|
||||
save_json(active_file, active)
|
||||
session = get_session_state()
|
||||
session["session_xp"] = session.get("session_xp", 0) + amount
|
||||
buddy_id = session.get("buddymon_id")
|
||||
save_session_state(session)
|
||||
|
||||
if buddy_id:
|
||||
roster_file = BUDDYMON_DIR / "roster.json"
|
||||
roster = load_json(roster_file)
|
||||
if buddy_id in roster.get("owned", {}):
|
||||
roster["owned"][buddy_id]["xp"] = roster["owned"][buddy_id].get("xp", 0) + amount
|
||||
|
|
@ -153,8 +175,7 @@ def is_starter_chosen():
|
|||
|
||||
|
||||
def get_active_buddy_id():
|
||||
active = load_json(BUDDYMON_DIR / "active.json")
|
||||
return active.get("buddymon_id")
|
||||
return get_session_state().get("buddymon_id")
|
||||
|
||||
|
||||
def get_active_encounter():
|
||||
|
|
|
|||
|
|
@ -7,8 +7,31 @@ source "${PLUGIN_ROOT}/lib/state.sh"
|
|||
|
||||
buddymon_init
|
||||
|
||||
ACTIVE_ID=$(buddymon_get_active)
|
||||
SESSION_XP=$(buddymon_get_session_xp)
|
||||
# Per-session state — keyed by process group ID so parallel sessions are isolated.
|
||||
SESSION_KEY=$(python3 -c "import os; print(os.getpgrp())")
|
||||
SESSION_FILE="${BUDDYMON_DIR}/sessions/${SESSION_KEY}.json"
|
||||
mkdir -p "${BUDDYMON_DIR}/sessions"
|
||||
|
||||
# Create session file if missing, inheriting buddy from global active.json
|
||||
if [[ ! -f "${SESSION_FILE}" ]]; then
|
||||
python3 << PYEOF
|
||||
import json, os
|
||||
active = {}
|
||||
try:
|
||||
active = json.load(open('${BUDDYMON_DIR}/active.json'))
|
||||
except Exception:
|
||||
pass
|
||||
session_state = {
|
||||
"buddymon_id": active.get("buddymon_id"),
|
||||
"challenge": active.get("challenge"),
|
||||
"session_xp": 0,
|
||||
}
|
||||
json.dump(session_state, open('${SESSION_FILE}', 'w'), indent=2)
|
||||
PYEOF
|
||||
fi
|
||||
|
||||
ACTIVE_ID=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('buddymon_id',''))" 2>/dev/null)
|
||||
SESSION_XP=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('session_xp',0))" 2>/dev/null)
|
||||
|
||||
# Load catalog for buddy display info
|
||||
CATALOG="${PLUGIN_ROOT}/lib/catalog.json"
|
||||
|
|
@ -16,6 +39,77 @@ CATALOG="${PLUGIN_ROOT}/lib/catalog.json"
|
|||
build_context() {
|
||||
local ctx=""
|
||||
|
||||
# ── Session handoff from previous session ──────────────────────────────
|
||||
local handoff_file="${BUDDYMON_DIR}/handoff.json"
|
||||
if [[ -f "${handoff_file}" ]]; then
|
||||
local handoff_block
|
||||
handoff_block=$(python3 << PYEOF
|
||||
import json, os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
try:
|
||||
h = json.load(open('${handoff_file}'))
|
||||
except Exception:
|
||||
print('')
|
||||
exit()
|
||||
|
||||
buddy_id = h.get('buddy_id')
|
||||
if not buddy_id:
|
||||
print('')
|
||||
exit()
|
||||
|
||||
import json as _json
|
||||
catalog_file = '${CATALOG}'
|
||||
try:
|
||||
catalog = _json.load(open(catalog_file))
|
||||
b = (catalog.get('buddymon', {}).get(buddy_id)
|
||||
or catalog.get('evolutions', {}).get(buddy_id) or {})
|
||||
display = b.get('display', buddy_id)
|
||||
except Exception:
|
||||
display = buddy_id
|
||||
|
||||
lines = [f"### 📬 From your last session ({h.get('date', '?')}) — {display}"]
|
||||
|
||||
xp = h.get('xp_earned', 0)
|
||||
commits = h.get('commits', 0)
|
||||
langs = h.get('languages', [])
|
||||
caught = h.get('caught', [])
|
||||
|
||||
if xp:
|
||||
lines.append(f"- Earned **{xp} XP**")
|
||||
if commits:
|
||||
lines.append(f"- Made **{commits} commit{'s' if commits != 1 else ''}**")
|
||||
if langs:
|
||||
lines.append(f"- Languages touched: {', '.join(langs)}")
|
||||
if caught:
|
||||
lines.append(f"- Caught: {', '.join(caught)}")
|
||||
|
||||
challenge = h.get('challenge')
|
||||
challenge_completed = h.get('challenge_completed', False)
|
||||
if challenge:
|
||||
status = "✅ completed" if challenge_completed else "⏳ still in progress"
|
||||
lines.append(f"- Challenge **{challenge.get('name','?')}** — {status}")
|
||||
|
||||
enc = h.get('active_encounter')
|
||||
if enc:
|
||||
lines.append(f"- ⚠️ **Unresolved encounter carried over:** {enc.get('display', '?')} (strength: {enc.get('current_strength', 100)}%)")
|
||||
|
||||
notes = h.get('notes', [])
|
||||
if notes:
|
||||
lines.append("**Notes:**")
|
||||
for n in notes:
|
||||
lines.append(f" · {n}")
|
||||
|
||||
print('\n'.join(lines))
|
||||
PYEOF
|
||||
)
|
||||
if [[ -n "${handoff_block}" ]]; then
|
||||
ctx+="${handoff_block}\n\n"
|
||||
fi
|
||||
# Archive handoff — consumed for this session
|
||||
rm -f "${handoff_file}"
|
||||
fi
|
||||
|
||||
# ── No starter chosen yet ─────────────────────────────────────────────
|
||||
if [[ "$(buddymon_starter_chosen)" == "false" ]]; then
|
||||
ctx="## 🐾 Buddymon — First Encounter!\n\n"
|
||||
|
|
|
|||
|
|
@ -6,10 +6,29 @@ source "${PLUGIN_ROOT}/lib/state.sh"
|
|||
|
||||
buddymon_init
|
||||
|
||||
ACTIVE_ID=$(buddymon_get_active)
|
||||
SESSION_XP=$(buddymon_get_session_xp)
|
||||
SESSION_KEY=$(python3 -c "import os; print(os.getpgrp())")
|
||||
SESSION_FILE="${BUDDYMON_DIR}/sessions/${SESSION_KEY}.json"
|
||||
|
||||
ACTIVE_ID=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(open('${SESSION_FILE}'))
|
||||
print(d.get('buddymon_id', ''))
|
||||
except Exception:
|
||||
print('')
|
||||
" 2>/dev/null)
|
||||
|
||||
SESSION_XP=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(open('${SESSION_FILE}'))
|
||||
print(d.get('session_xp', 0))
|
||||
except Exception:
|
||||
print(0)
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ -z "${ACTIVE_ID}" ]] || [[ "${SESSION_XP}" -eq 0 ]]; then
|
||||
[[ -f "${SESSION_FILE}" ]] && rm -f "${SESSION_FILE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
@ -20,17 +39,20 @@ SUMMARY=$(python3 << PYEOF
|
|||
import json, os
|
||||
|
||||
catalog_file = '${CATALOG}'
|
||||
active_file = '${BUDDYMON_DIR}/active.json'
|
||||
session_state_file = '${SESSION_FILE}'
|
||||
roster_file = '${BUDDYMON_DIR}/roster.json'
|
||||
session_file = '${BUDDYMON_DIR}/session.json'
|
||||
encounters_file = '${BUDDYMON_DIR}/encounters.json'
|
||||
|
||||
catalog = json.load(open(catalog_file))
|
||||
active = json.load(open(active_file))
|
||||
session_state = json.load(open(session_state_file))
|
||||
roster = json.load(open(roster_file))
|
||||
session = json.load(open(session_file))
|
||||
session = {}
|
||||
try:
|
||||
session = json.load(open(session_file))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
buddy_id = active.get('buddymon_id')
|
||||
buddy_id = session_state.get('buddymon_id')
|
||||
if not buddy_id:
|
||||
print('')
|
||||
exit()
|
||||
|
|
@ -39,7 +61,7 @@ b = (catalog.get('buddymon', {}).get(buddy_id)
|
|||
or catalog.get('evolutions', {}).get(buddy_id) or {})
|
||||
display = b.get('display', buddy_id)
|
||||
|
||||
xp_earned = active.get('session_xp', 0)
|
||||
xp_earned = session_state.get('session_xp', 0)
|
||||
level = roster.get('owned', {}).get(buddy_id, {}).get('level', 1)
|
||||
total_xp = roster.get('owned', {}).get(buddy_id, {}).get('xp', 0)
|
||||
xp_needed = level * 100
|
||||
|
|
@ -61,7 +83,7 @@ commits = session.get('commits_this_session', 0)
|
|||
tools = session.get('tools_used', 0)
|
||||
langs = session.get('languages_seen', [])
|
||||
challenge_completed = session.get('challenge_completed', False)
|
||||
challenge = active.get('challenge')
|
||||
challenge = session_state.get('challenge')
|
||||
|
||||
lines = [f"\n## 🐾 Session complete — {display}"]
|
||||
lines.append(f"**+{xp_earned} XP earned** this session")
|
||||
|
|
@ -72,10 +94,27 @@ if langs:
|
|||
|
||||
if leveled_up:
|
||||
lines.append(f"\n✨ **LEVEL UP!** {display} is now Lv.{new_level}!")
|
||||
# Check if evolution is now available
|
||||
catalog_entry = (catalog.get('buddymon', {}).get(buddy_id)
|
||||
or catalog.get('evolutions', {}).get(buddy_id) or {})
|
||||
evolutions = catalog_entry.get('evolutions', [])
|
||||
evo = next((e for e in evolutions if new_level >= e.get('level', 999)), None)
|
||||
if evo:
|
||||
into = catalog.get('evolutions', {}).get(evo['into'], {})
|
||||
lines.append(f"\n⭐ **EVOLUTION READY!** {display} can evolve into {into.get('display', evo['into'])}!")
|
||||
lines.append(f" Run `/buddymon evolve` to prestige — resets to Lv.1 with upgraded stats.")
|
||||
else:
|
||||
filled = min(20, total_xp * 20 // xp_needed)
|
||||
bar = '█' * filled + '░' * (20 - filled)
|
||||
lines.append(f"XP: [{bar}] {total_xp}/{xp_needed}")
|
||||
# Remind if already at evolution threshold but hasn't evolved yet
|
||||
catalog_entry = (catalog.get('buddymon', {}).get(buddy_id)
|
||||
or catalog.get('evolutions', {}).get(buddy_id) or {})
|
||||
evolutions = catalog_entry.get('evolutions', [])
|
||||
evo = next((e for e in evolutions if level >= e.get('level', 999)), None)
|
||||
if evo:
|
||||
into = catalog.get('evolutions', {}).get(evo['into'], {})
|
||||
lines.append(f"\n⭐ **Evolution available:** `/buddymon evolve` → {into.get('display', evo['into'])}")
|
||||
|
||||
if challenge:
|
||||
if challenge_completed:
|
||||
|
|
@ -87,17 +126,67 @@ print('\n'.join(lines))
|
|||
PYEOF
|
||||
)
|
||||
|
||||
# Reset session XP + clear challenge so next session assigns a fresh one
|
||||
# Write handoff.json for next session to pick up
|
||||
python3 << PYEOF
|
||||
import json
|
||||
active_file = '${BUDDYMON_DIR}/active.json'
|
||||
active = json.load(open(active_file))
|
||||
active['session_xp'] = 0
|
||||
active['challenge'] = None
|
||||
json.dump(active, open(active_file, 'w'), indent=2)
|
||||
import json, os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
session_state_file = '${SESSION_FILE}'
|
||||
session_file = '${BUDDYMON_DIR}/session.json'
|
||||
encounters_file = '${BUDDYMON_DIR}/encounters.json'
|
||||
handoff_file = '${BUDDYMON_DIR}/handoff.json'
|
||||
|
||||
session_state = {}
|
||||
try:
|
||||
session_state = json.load(open(session_state_file))
|
||||
except Exception:
|
||||
pass
|
||||
session_data = {}
|
||||
try:
|
||||
session_data = json.load(open(session_file))
|
||||
except Exception:
|
||||
pass
|
||||
encounters = {}
|
||||
try:
|
||||
encounters = json.load(open(encounters_file))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Collect caught monsters from this session
|
||||
caught_this_session = [
|
||||
e.get('display', e.get('id', '?'))
|
||||
for e in encounters.get('history', [])
|
||||
if e.get('outcome') == 'caught'
|
||||
and e.get('timestamp', '') >= datetime.now(timezone.utc).strftime('%Y-%m-%d')
|
||||
]
|
||||
|
||||
# Carry over any existing handoff notes (user-added via /buddymon note)
|
||||
existing = {}
|
||||
try:
|
||||
existing = json.load(open(handoff_file))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
handoff = {
|
||||
"date": datetime.now(timezone.utc).strftime('%Y-%m-%d'),
|
||||
"buddy_id": session_state.get('buddymon_id'),
|
||||
"xp_earned": session_state.get('session_xp', 0),
|
||||
"commits": session_data.get('commits_this_session', 0),
|
||||
"languages": session_data.get('languages_seen', []),
|
||||
"caught": caught_this_session,
|
||||
"challenge": session_state.get('challenge'),
|
||||
"challenge_completed": session_data.get('challenge_completed', False),
|
||||
"active_encounter": encounters.get('active_encounter'),
|
||||
"notes": existing.get('notes', []), # preserve any manual notes
|
||||
}
|
||||
|
||||
json.dump(handoff, open(handoff_file, 'w'), indent=2)
|
||||
PYEOF
|
||||
|
||||
# Reset session file
|
||||
# Clean up this session's state file — each session is ephemeral
|
||||
rm -f "${SESSION_FILE}"
|
||||
|
||||
# Reset shared session.json for legacy compatibility
|
||||
buddymon_session_reset
|
||||
|
||||
SUMMARY_JSON=$(python3 -c "import json,sys; print(json.dumps(sys.argv[1]))" "${SUMMARY}" 2>/dev/null)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,27 @@ PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", str(Path(__file__).parent.par
|
|||
BUDDYMON_DIR = Path.home() / ".claude" / "buddymon"
|
||||
CATALOG_FILE = Path(PLUGIN_ROOT) / "lib" / "catalog.json"
|
||||
|
||||
SESSION_KEY = str(os.getpgrp())
|
||||
SESSION_FILE = BUDDYMON_DIR / "sessions" / f"{SESSION_KEY}.json"
|
||||
|
||||
|
||||
def get_session_state() -> dict:
|
||||
try:
|
||||
with open(SESSION_FILE) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
global_active = {}
|
||||
try:
|
||||
with open(BUDDYMON_DIR / "active.json") as f:
|
||||
global_active = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"buddymon_id": global_active.get("buddymon_id"),
|
||||
"challenge": global_active.get("challenge"),
|
||||
"session_xp": 0,
|
||||
}
|
||||
|
||||
|
||||
def load_json(path):
|
||||
try:
|
||||
|
|
@ -55,9 +76,8 @@ def main():
|
|||
enc_data["active_encounter"] = enc
|
||||
save_json(enc_file, enc_data)
|
||||
|
||||
# Resolve buddy display name
|
||||
active = load_json(BUDDYMON_DIR / "active.json")
|
||||
buddy_id = active.get("buddymon_id")
|
||||
# Resolve buddy display name from session-specific state
|
||||
buddy_id = get_session_state().get("buddymon_id")
|
||||
buddy_display = "your buddy"
|
||||
if buddy_id:
|
||||
catalog = load_json(CATALOG_FILE)
|
||||
|
|
|
|||
590
lib/catalog.json
590
lib/catalog.json
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"_version": 1,
|
||||
"_note": "Master species catalog. discovered=false entries are hidden until triggered.",
|
||||
|
||||
"bug_monsters": {
|
||||
"NullWraith": {
|
||||
"id": "NullWraith",
|
||||
"display": "👻 NullWraith",
|
||||
"display": "\ud83d\udc7b NullWraith",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 20,
|
||||
|
|
@ -22,15 +21,24 @@
|
|||
"null reference"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 20},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "It was there this whole time. You just never checked."
|
||||
},
|
||||
"FencepostDemon": {
|
||||
"id": "FencepostDemon",
|
||||
"display": "😈 FencepostDemon",
|
||||
"display": "\ud83d\ude08 FencepostDemon",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 25,
|
||||
|
|
@ -46,15 +54,24 @@
|
|||
"off.by.one"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 20},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "Always one step ahead. Or behind. It's hard to tell."
|
||||
},
|
||||
"TypeGreml": {
|
||||
"id": "TypeGreml",
|
||||
"display": "🔧 TypeGreml",
|
||||
"display": "\ud83d\udd27 TypeGreml",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 25,
|
||||
|
|
@ -70,15 +87,24 @@
|
|||
"type.*is not assignable"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 20},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "It only attacks when you're absolutely sure about the type."
|
||||
},
|
||||
"SyntaxSerpent": {
|
||||
"id": "SyntaxSerpent",
|
||||
"display": "🐍 SyntaxSerpent",
|
||||
"display": "\ud83d\udc0d SyntaxSerpent",
|
||||
"type": "bug_monster",
|
||||
"rarity": "very_common",
|
||||
"base_strength": 10,
|
||||
|
|
@ -95,14 +121,20 @@
|
|||
"ParseError"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 30},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 20}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 20
|
||||
}
|
||||
],
|
||||
"flavor": "You'll be embarrassed you let this one survive long enough to catch."
|
||||
},
|
||||
"CORSCurse": {
|
||||
"id": "CORSCurse",
|
||||
"display": "🌐 CORSCurse",
|
||||
"display": "\ud83c\udf10 CORSCurse",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 40,
|
||||
|
|
@ -118,15 +150,24 @@
|
|||
"No 'Access-Control-Allow-Origin'"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 25},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "It's not your fault. Well. It kind of is."
|
||||
},
|
||||
"LoopLich": {
|
||||
"id": "LoopLich",
|
||||
"display": "♾️ LoopLich",
|
||||
"display": "\u267e\ufe0f LoopLich",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 60,
|
||||
|
|
@ -146,15 +187,24 @@
|
|||
"StackOverflow"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 25},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 15}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 15
|
||||
}
|
||||
],
|
||||
"flavor": "The exit condition was always there. You just never believed in it."
|
||||
},
|
||||
"RacePhantom": {
|
||||
"id": "RacePhantom",
|
||||
"display": "👁️ RacePhantom",
|
||||
"display": "\ud83d\udc41\ufe0f RacePhantom",
|
||||
"type": "bug_monster",
|
||||
"rarity": "rare",
|
||||
"base_strength": 80,
|
||||
|
|
@ -172,15 +222,24 @@
|
|||
"async.*conflict"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 15},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 30},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 15}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 15
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 15
|
||||
}
|
||||
],
|
||||
"flavor": "You've proven it exists. That's honestly impressive on its own."
|
||||
},
|
||||
"FossilGolem": {
|
||||
"id": "FossilGolem",
|
||||
"display": "🗿 FossilGolem",
|
||||
"display": "\ud83d\uddff FossilGolem",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 35,
|
||||
|
|
@ -197,23 +256,36 @@
|
|||
"legacy"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 20},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 20}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 20
|
||||
}
|
||||
],
|
||||
"flavor": "It survived every major version. It will outlast you."
|
||||
},
|
||||
"ShadowBit": {
|
||||
"id": "ShadowBit",
|
||||
"display": "🔒 ShadowBit",
|
||||
"display": "\ud83d\udd12 ShadowBit",
|
||||
"type": "bug_monster",
|
||||
"rarity": "rare",
|
||||
"base_strength": 90,
|
||||
"xp_reward": 300,
|
||||
"catchable": true,
|
||||
"defeatable": false,
|
||||
"catch_requires": ["write_failing_test", "isolate_reproduction", "add_documenting_comment"],
|
||||
"description": "Cannot be defeated — only properly contained. Requires full documentation + patching.",
|
||||
"catch_requires": [
|
||||
"write_failing_test",
|
||||
"isolate_reproduction",
|
||||
"add_documenting_comment"
|
||||
],
|
||||
"description": "Cannot be defeated \u2014 only properly contained. Requires full documentation + patching.",
|
||||
"error_patterns": [
|
||||
"vulnerability",
|
||||
"CVE-",
|
||||
|
|
@ -229,15 +301,24 @@
|
|||
"hardcoded.*token"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 25},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 35},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 20}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 35
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 20
|
||||
}
|
||||
],
|
||||
"flavor": "Defeat is not an option. Containment is the only victory."
|
||||
},
|
||||
"VoidSpecter": {
|
||||
"id": "VoidSpecter",
|
||||
"display": "🌫️ VoidSpecter",
|
||||
"display": "\ud83c\udf2b\ufe0f VoidSpecter",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 20,
|
||||
|
|
@ -254,15 +335,24 @@
|
|||
"endpoint.*not found"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 25},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 25},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "It used to exist. Probably."
|
||||
},
|
||||
"MemoryLeech": {
|
||||
"id": "MemoryLeech",
|
||||
"display": "🩸 MemoryLeech",
|
||||
"display": "\ud83e\ude78 MemoryLeech",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 55,
|
||||
|
|
@ -287,15 +377,24 @@
|
|||
"malloc: can't allocate region"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 30},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "It was already there when you opened the task manager."
|
||||
},
|
||||
"CudaCrash": {
|
||||
"id": "CudaCrash",
|
||||
"display": "⚡ CudaCrash",
|
||||
"display": "\u26a1 CudaCrash",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 65,
|
||||
|
|
@ -315,15 +414,24 @@
|
|||
"device-side assert triggered"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "isolate_reproduction", "strength_reduction": 30},
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "Your model fit in VRAM yesterday. You added one layer."
|
||||
},
|
||||
"InfiniteWisp": {
|
||||
"id": "InfiniteWisp",
|
||||
"display": "🌀 InfiniteWisp",
|
||||
"display": "\ud83c\udf00 InfiniteWisp",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 30,
|
||||
|
|
@ -341,15 +449,24 @@
|
|||
"timed out after"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 20},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 30},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "Your fan was always loud. You just never checked why."
|
||||
},
|
||||
"BoundsHound": {
|
||||
"id": "BoundsHound",
|
||||
"display": "🐕 BoundsHound",
|
||||
"display": "\ud83d\udc15 BoundsHound",
|
||||
"type": "bug_monster",
|
||||
"rarity": "common",
|
||||
"base_strength": 25,
|
||||
|
|
@ -369,15 +486,24 @@
|
|||
"RangeError.*index"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 25},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 25},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "It was always length minus one. You just forgot."
|
||||
},
|
||||
"BranchGhost": {
|
||||
"id": "BranchGhost",
|
||||
"display": "🔀 BranchGhost",
|
||||
"display": "\ud83d\udd00 BranchGhost",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 40,
|
||||
|
|
@ -396,15 +522,24 @@
|
|||
"fallthrough"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 30},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 20},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 20
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "You were so sure that case was impossible."
|
||||
},
|
||||
"SwitchTrap": {
|
||||
"id": "SwitchTrap",
|
||||
"display": "🪤 SwitchTrap",
|
||||
"display": "\ud83e\udea4 SwitchTrap",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 35,
|
||||
|
|
@ -425,15 +560,24 @@
|
|||
"Unhandled variant"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 25},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 25},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "You added that new enum value last week. The switch didn't notice."
|
||||
},
|
||||
"RecurseWraith": {
|
||||
"id": "RecurseWraith",
|
||||
"display": "🌪️ RecurseWraith",
|
||||
"display": "\ud83c\udf2a\ufe0f RecurseWraith",
|
||||
"type": "bug_monster",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 45,
|
||||
|
|
@ -452,22 +596,31 @@
|
|||
"stack.*exhausted"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 25},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 25},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 25
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 10
|
||||
}
|
||||
],
|
||||
"flavor": "The base case was there. It just couldn't be reached."
|
||||
},
|
||||
"CatchAll": {
|
||||
"id": "CatchAll",
|
||||
"display": "🕳️ CatchAll",
|
||||
"display": "\ud83d\udd73\ufe0f CatchAll",
|
||||
"type": "bug_monster",
|
||||
"rarity": "rare",
|
||||
"base_strength": 60,
|
||||
"xp_reward": 120,
|
||||
"catchable": true,
|
||||
"defeatable": false,
|
||||
"description": "Born from broad exception handlers. Swallows errors whole. Cannot be defeated — only caught, by narrowing the catch.",
|
||||
"description": "Born from broad exception handlers. Swallows errors whole. Cannot be defeated \u2014 only caught, by narrowing the catch.",
|
||||
"error_patterns": [
|
||||
"except Exception",
|
||||
"except:$",
|
||||
|
|
@ -479,18 +632,26 @@
|
|||
"catch-all.*handler"
|
||||
],
|
||||
"weaken_actions": [
|
||||
{"action": "write_failing_test", "strength_reduction": 30},
|
||||
{"action": "isolate_reproduction", "strength_reduction": 30},
|
||||
{"action": "add_documenting_comment", "strength_reduction": 15}
|
||||
{
|
||||
"action": "write_failing_test",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "isolate_reproduction",
|
||||
"strength_reduction": 30
|
||||
},
|
||||
{
|
||||
"action": "add_documenting_comment",
|
||||
"strength_reduction": 15
|
||||
}
|
||||
],
|
||||
"flavor": "If you catch everything, you learn nothing."
|
||||
}
|
||||
},
|
||||
|
||||
"event_encounters": {
|
||||
"MergeMaw": {
|
||||
"id": "MergeMaw",
|
||||
"display": "🔀 MergeMaw",
|
||||
"display": "\ud83d\udd00 MergeMaw",
|
||||
"type": "event_encounter",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 45,
|
||||
|
|
@ -498,13 +659,16 @@
|
|||
"catchable": true,
|
||||
"defeatable": true,
|
||||
"trigger_type": "command",
|
||||
"command_patterns": ["git merge", "git rebase"],
|
||||
"command_patterns": [
|
||||
"git merge",
|
||||
"git rebase"
|
||||
],
|
||||
"description": "Emerges from the diff between two timelines. Loves conflicts.",
|
||||
"flavor": "It has opinions about your whitespace."
|
||||
},
|
||||
"BranchSprite": {
|
||||
"id": "BranchSprite",
|
||||
"display": "🌿 BranchSprite",
|
||||
"display": "\ud83c\udf3f BranchSprite",
|
||||
"type": "event_encounter",
|
||||
"rarity": "common",
|
||||
"base_strength": 15,
|
||||
|
|
@ -512,13 +676,17 @@
|
|||
"catchable": true,
|
||||
"defeatable": false,
|
||||
"trigger_type": "command",
|
||||
"command_patterns": ["git checkout -b", "git switch -c", "git branch "],
|
||||
"command_patterns": [
|
||||
"git checkout -b",
|
||||
"git switch -c",
|
||||
"git branch "
|
||||
],
|
||||
"description": "Appears when a new branch is born. Harmless. Almost cheerful.",
|
||||
"flavor": "It wanted to come along for the feature."
|
||||
},
|
||||
"DepGolem": {
|
||||
"id": "DepGolem",
|
||||
"display": "📦 DepGolem",
|
||||
"display": "\ud83d\udce6 DepGolem",
|
||||
"type": "event_encounter",
|
||||
"rarity": "common",
|
||||
"base_strength": 30,
|
||||
|
|
@ -526,13 +694,25 @@
|
|||
"catchable": true,
|
||||
"defeatable": true,
|
||||
"trigger_type": "command",
|
||||
"command_patterns": ["pip install", "pip3 install", "npm install", "npm i ", "cargo add", "yarn add", "apt install", "brew install", "poetry add", "uv add", "uv pip install"],
|
||||
"command_patterns": [
|
||||
"pip install",
|
||||
"pip3 install",
|
||||
"npm install",
|
||||
"npm i ",
|
||||
"cargo add",
|
||||
"yarn add",
|
||||
"apt install",
|
||||
"brew install",
|
||||
"poetry add",
|
||||
"uv add",
|
||||
"uv pip install"
|
||||
],
|
||||
"description": "Conjured from the package registry. Brings transitive dependencies.",
|
||||
"flavor": "It brought 847 friends."
|
||||
},
|
||||
"FlakeDemon": {
|
||||
"id": "FlakeDemon",
|
||||
"display": "🎲 FlakeDemon",
|
||||
"display": "\ud83c\udfb2 FlakeDemon",
|
||||
"type": "event_encounter",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 55,
|
||||
|
|
@ -555,7 +735,7 @@
|
|||
},
|
||||
"PhantomPass": {
|
||||
"id": "PhantomPass",
|
||||
"display": "✅ PhantomPass",
|
||||
"display": "\u2705 PhantomPass",
|
||||
"type": "event_encounter",
|
||||
"rarity": "rare",
|
||||
"base_strength": 10,
|
||||
|
|
@ -568,17 +748,17 @@
|
|||
"PASSED",
|
||||
"All tests passed",
|
||||
"tests passed",
|
||||
"✓",
|
||||
"\u2713",
|
||||
"\\d+ passed",
|
||||
"OK$",
|
||||
"SUCCESS"
|
||||
],
|
||||
"description": "Appears only when tests go green after going red. Rare. Fleeting. Cannot be fought — only caught.",
|
||||
"description": "Appears only when tests go green after going red. Rare. Fleeting. Cannot be fought \u2014 only caught.",
|
||||
"flavor": "It was hiding in the red all along."
|
||||
},
|
||||
"TestSpecter": {
|
||||
"id": "TestSpecter",
|
||||
"display": "🧪 TestSpecter",
|
||||
"display": "\ud83e\uddea TestSpecter",
|
||||
"type": "event_encounter",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 25,
|
||||
|
|
@ -586,13 +766,19 @@
|
|||
"catchable": true,
|
||||
"defeatable": true,
|
||||
"trigger_type": "test_file",
|
||||
"test_file_patterns": ["\\.test\\.", "_test\\.", "test_", "_spec\\.", "\\.spec\\."],
|
||||
"test_file_patterns": [
|
||||
"\\.test\\.",
|
||||
"_test\\.",
|
||||
"test_",
|
||||
"_spec\\.",
|
||||
"\\.spec\\."
|
||||
],
|
||||
"description": "Haunts test suites. Drawn to assertions. Debuglin gets excited.",
|
||||
"flavor": "It wanted to make sure the test was named correctly."
|
||||
},
|
||||
"ReviewHawk": {
|
||||
"id": "ReviewHawk",
|
||||
"display": "🦅 ReviewHawk",
|
||||
"display": "\ud83e\udd85 ReviewHawk",
|
||||
"type": "event_encounter",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 40,
|
||||
|
|
@ -613,7 +799,7 @@
|
|||
},
|
||||
"TicketGremlin": {
|
||||
"id": "TicketGremlin",
|
||||
"display": "🎫 TicketGremlin",
|
||||
"display": "\ud83c\udfab TicketGremlin",
|
||||
"type": "event_encounter",
|
||||
"rarity": "common",
|
||||
"base_strength": 30,
|
||||
|
|
@ -636,7 +822,7 @@
|
|||
},
|
||||
"PermWraith": {
|
||||
"id": "PermWraith",
|
||||
"display": "🚫 PermWraith",
|
||||
"display": "\ud83d\udeab PermWraith",
|
||||
"type": "event_encounter",
|
||||
"rarity": "common",
|
||||
"base_strength": 35,
|
||||
|
|
@ -660,7 +846,7 @@
|
|||
},
|
||||
"SudoSprite": {
|
||||
"id": "SudoSprite",
|
||||
"display": "🔑 SudoSprite",
|
||||
"display": "\ud83d\udd11 SudoSprite",
|
||||
"type": "event_encounter",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 20,
|
||||
|
|
@ -677,12 +863,12 @@
|
|||
"sudo chgrp",
|
||||
"setfacl "
|
||||
],
|
||||
"description": "Emerges when permissions are corrected. Doesn't fight — it just watches to make sure you chose the right octal.",
|
||||
"description": "Emerges when permissions are corrected. Doesn't fight \u2014 it just watches to make sure you chose the right octal.",
|
||||
"flavor": "777 was always the answer. Never the right one."
|
||||
},
|
||||
"LayerLurker": {
|
||||
"id": "LayerLurker",
|
||||
"display": "🐋 LayerLurker",
|
||||
"display": "\ud83d\udc0b LayerLurker",
|
||||
"type": "event_encounter",
|
||||
"rarity": "common",
|
||||
"base_strength": 35,
|
||||
|
|
@ -704,7 +890,7 @@
|
|||
},
|
||||
"DiskDemon": {
|
||||
"id": "DiskDemon",
|
||||
"display": "💾 DiskDemon",
|
||||
"display": "\ud83d\udcbe DiskDemon",
|
||||
"type": "event_encounter",
|
||||
"rarity": "uncommon",
|
||||
"base_strength": 50,
|
||||
|
|
@ -726,132 +912,260 @@
|
|||
"flavor": "It's been there since 2019. It's just a log file, you said."
|
||||
}
|
||||
},
|
||||
|
||||
"buddymon": {
|
||||
"Pyrobyte": {
|
||||
"id": "Pyrobyte",
|
||||
"display": "🔥 Pyrobyte",
|
||||
"display": "\ud83d\udd25 Pyrobyte",
|
||||
"type": "buddymon",
|
||||
"affinity": "Speedrunner",
|
||||
"rarity": "starter",
|
||||
"description": "Moves fast, thinks faster. Loves tight deadlines and feature sprints.",
|
||||
"discover_trigger": {"type": "starter", "index": 0},
|
||||
"base_stats": {"power": 40, "catch_rate": 0.45, "xp_multiplier": 1.2},
|
||||
"affinity_bonus_triggers": ["fast_feature", "short_session_win"],
|
||||
"discover_trigger": {
|
||||
"type": "starter",
|
||||
"index": 0
|
||||
},
|
||||
"base_stats": {
|
||||
"power": 40,
|
||||
"catch_rate": 0.45,
|
||||
"xp_multiplier": 1.2
|
||||
},
|
||||
"affinity_bonus_triggers": [
|
||||
"fast_feature",
|
||||
"short_session_win"
|
||||
],
|
||||
"challenges": [
|
||||
{"name": "SPEED RUN", "description": "Implement a feature in under 30 minutes", "xp": 280, "difficulty": 3},
|
||||
{"name": "BLITZ", "description": "Resolve 3 bug monsters in one session", "xp": 350, "difficulty": 4}
|
||||
{
|
||||
"name": "SPEED RUN",
|
||||
"description": "Implement a feature in under 30 minutes",
|
||||
"xp": 280,
|
||||
"difficulty": 3
|
||||
},
|
||||
{
|
||||
"name": "BLITZ",
|
||||
"description": "Resolve 3 bug monsters in one session",
|
||||
"xp": 350,
|
||||
"difficulty": 4
|
||||
}
|
||||
],
|
||||
"evolutions": [
|
||||
{"level": 10, "into": "Infernus", "requires": "affinity_challenge_x3"}
|
||||
{
|
||||
"level": 100,
|
||||
"into": "Infernus"
|
||||
}
|
||||
],
|
||||
"flavor": "It already committed before you finished reading the issue."
|
||||
},
|
||||
"Debuglin": {
|
||||
"id": "Debuglin",
|
||||
"display": "🔍 Debuglin",
|
||||
"display": "\ud83d\udd0d Debuglin",
|
||||
"type": "buddymon",
|
||||
"affinity": "Tester",
|
||||
"rarity": "starter",
|
||||
"description": "Patient, methodical, ruthless. Lives for the reproduction case.",
|
||||
"discover_trigger": {"type": "starter", "index": 1},
|
||||
"base_stats": {"power": 35, "catch_rate": 0.60, "xp_multiplier": 1.0},
|
||||
"affinity_bonus_triggers": ["write_test", "fix_bug_with_test"],
|
||||
"discover_trigger": {
|
||||
"type": "starter",
|
||||
"index": 1
|
||||
},
|
||||
"base_stats": {
|
||||
"power": 35,
|
||||
"catch_rate": 0.6,
|
||||
"xp_multiplier": 1.0
|
||||
},
|
||||
"affinity_bonus_triggers": [
|
||||
"write_test",
|
||||
"fix_bug_with_test"
|
||||
],
|
||||
"challenges": [
|
||||
{"name": "IRON TEST", "description": "Write 5 tests in one session", "xp": 300, "difficulty": 2},
|
||||
{"name": "COVERAGE PUSH", "description": "Increase test coverage in a file", "xp": 250, "difficulty": 2}
|
||||
{
|
||||
"name": "IRON TEST",
|
||||
"description": "Write 5 tests in one session",
|
||||
"xp": 300,
|
||||
"difficulty": 2
|
||||
},
|
||||
{
|
||||
"name": "COVERAGE PUSH",
|
||||
"description": "Increase test coverage in a file",
|
||||
"xp": 250,
|
||||
"difficulty": 2
|
||||
}
|
||||
],
|
||||
"evolutions": [
|
||||
{"level": 10, "into": "Verifex", "requires": "affinity_challenge_x3"}
|
||||
{
|
||||
"level": 100,
|
||||
"into": "Verifex"
|
||||
}
|
||||
],
|
||||
"flavor": "The bug isn't found until the test is written."
|
||||
},
|
||||
"Minimox": {
|
||||
"id": "Minimox",
|
||||
"display": "✂️ Minimox",
|
||||
"display": "\u2702\ufe0f Minimox",
|
||||
"type": "buddymon",
|
||||
"affinity": "Cleaner",
|
||||
"rarity": "starter",
|
||||
"description": "Obsessed with fewer lines. Gets uncomfortable around anything over 300 LOC.",
|
||||
"discover_trigger": {"type": "starter", "index": 2},
|
||||
"base_stats": {"power": 30, "catch_rate": 0.50, "xp_multiplier": 1.1},
|
||||
"affinity_bonus_triggers": ["net_negative_lines", "refactor_session"],
|
||||
"discover_trigger": {
|
||||
"type": "starter",
|
||||
"index": 2
|
||||
},
|
||||
"base_stats": {
|
||||
"power": 30,
|
||||
"catch_rate": 0.5,
|
||||
"xp_multiplier": 1.1
|
||||
},
|
||||
"affinity_bonus_triggers": [
|
||||
"net_negative_lines",
|
||||
"refactor_session"
|
||||
],
|
||||
"challenges": [
|
||||
{"name": "CLEAN RUN", "description": "Complete session with zero linter errors", "xp": 340, "difficulty": 2},
|
||||
{"name": "SHRINK", "description": "Net negative lines of code this session", "xp": 280, "difficulty": 3}
|
||||
{
|
||||
"name": "CLEAN RUN",
|
||||
"description": "Complete session with zero linter errors",
|
||||
"xp": 340,
|
||||
"difficulty": 2
|
||||
},
|
||||
{
|
||||
"name": "SHRINK",
|
||||
"description": "Net negative lines of code this session",
|
||||
"xp": 280,
|
||||
"difficulty": 3
|
||||
}
|
||||
],
|
||||
"evolutions": [
|
||||
{"level": 10, "into": "Nullex", "requires": "affinity_challenge_x3"}
|
||||
{
|
||||
"level": 100,
|
||||
"into": "Nullex"
|
||||
}
|
||||
],
|
||||
"flavor": "It deleted your comment. It was redundant."
|
||||
},
|
||||
"Noctara": {
|
||||
"id": "Noctara",
|
||||
"display": "🌙 Noctara",
|
||||
"display": "\ud83c\udf19 Noctara",
|
||||
"type": "buddymon",
|
||||
"affinity": "Nocturnal",
|
||||
"rarity": "rare",
|
||||
"description": "Only appears after 10pm. Mysterious. Gives bonus XP for late-night focus runs.",
|
||||
"discover_trigger": {"type": "late_night_session", "hours_after": 22, "min_hours": 2},
|
||||
"base_stats": {"power": 55, "catch_rate": 0.35, "xp_multiplier": 1.5},
|
||||
"affinity_bonus_triggers": ["late_night_session", "deep_focus"],
|
||||
"discover_trigger": {
|
||||
"type": "late_night_session",
|
||||
"hours_after": 22,
|
||||
"min_hours": 2
|
||||
},
|
||||
"base_stats": {
|
||||
"power": 55,
|
||||
"catch_rate": 0.35,
|
||||
"xp_multiplier": 1.5
|
||||
},
|
||||
"affinity_bonus_triggers": [
|
||||
"late_night_session",
|
||||
"deep_focus"
|
||||
],
|
||||
"challenges": [
|
||||
{"name": "MIDNIGHT RUN", "description": "3-hour session after 10pm", "xp": 500, "difficulty": 4},
|
||||
{"name": "DAWN COMMIT", "description": "Commit between 2am and 5am", "xp": 400, "difficulty": 3}
|
||||
{
|
||||
"name": "MIDNIGHT RUN",
|
||||
"description": "3-hour session after 10pm",
|
||||
"xp": 500,
|
||||
"difficulty": 4
|
||||
},
|
||||
{
|
||||
"name": "DAWN COMMIT",
|
||||
"description": "Commit between 2am and 5am",
|
||||
"xp": 400,
|
||||
"difficulty": 3
|
||||
}
|
||||
],
|
||||
"evolutions": [
|
||||
{"level": 15, "into": "Umbravex", "requires": "nocturnal_sessions_x5"}
|
||||
{
|
||||
"level": 15,
|
||||
"into": "Umbravex",
|
||||
"requires": "nocturnal_sessions_x5"
|
||||
}
|
||||
],
|
||||
"flavor": "It remembers everything you wrote at 2am. Everything."
|
||||
},
|
||||
"Explorah": {
|
||||
"id": "Explorah",
|
||||
"display": "🗺️ Explorah",
|
||||
"display": "\ud83d\uddfa\ufe0f Explorah",
|
||||
"type": "buddymon",
|
||||
"affinity": "Explorer",
|
||||
"rarity": "uncommon",
|
||||
"description": "Discovered when you touch a new language for the first time. Thrives on novelty.",
|
||||
"discover_trigger": {"type": "new_language"},
|
||||
"base_stats": {"power": 45, "catch_rate": 0.50, "xp_multiplier": 1.2},
|
||||
"affinity_bonus_triggers": ["new_language", "new_library", "touch_new_module"],
|
||||
"discover_trigger": {
|
||||
"type": "new_language"
|
||||
},
|
||||
"base_stats": {
|
||||
"power": 45,
|
||||
"catch_rate": 0.5,
|
||||
"xp_multiplier": 1.2
|
||||
},
|
||||
"affinity_bonus_triggers": [
|
||||
"new_language",
|
||||
"new_library",
|
||||
"touch_new_module"
|
||||
],
|
||||
"challenges": [
|
||||
{"name": "EXPEDITION", "description": "Touch 3 different modules in one session", "xp": 260, "difficulty": 2},
|
||||
{"name": "POLYGLOT", "description": "Write in 2 different languages in one session", "xp": 380, "difficulty": 4}
|
||||
{
|
||||
"name": "EXPEDITION",
|
||||
"description": "Touch 3 different modules in one session",
|
||||
"xp": 260,
|
||||
"difficulty": 2
|
||||
},
|
||||
{
|
||||
"name": "POLYGLOT",
|
||||
"description": "Write in 2 different languages in one session",
|
||||
"xp": 380,
|
||||
"difficulty": 4
|
||||
}
|
||||
],
|
||||
"evolutions": [
|
||||
{"level": 12, "into": "Wandervex", "requires": "new_languages_x5"}
|
||||
{
|
||||
"level": 12,
|
||||
"into": "Wandervex",
|
||||
"requires": "new_languages_x5"
|
||||
}
|
||||
],
|
||||
"flavor": "It's already halfway through the new framework docs."
|
||||
}
|
||||
},
|
||||
|
||||
"evolutions": {
|
||||
"Infernus": {
|
||||
"id": "Infernus",
|
||||
"display": "🌋 Infernus",
|
||||
"display": "\ud83c\udf0b Infernus",
|
||||
"type": "buddymon",
|
||||
"evolves_from": "Pyrobyte",
|
||||
"affinity": "Speedrunner",
|
||||
"description": "Evolved form of Pyrobyte. Moves at dangerous speeds.",
|
||||
"base_stats": {"power": 70, "catch_rate": 0.55, "xp_multiplier": 1.5}
|
||||
"base_stats": {
|
||||
"power": 70,
|
||||
"catch_rate": 0.55,
|
||||
"xp_multiplier": 1.5
|
||||
}
|
||||
},
|
||||
"Verifex": {
|
||||
"id": "Verifex",
|
||||
"display": "🔬 Verifex",
|
||||
"display": "\ud83d\udd2c Verifex",
|
||||
"type": "buddymon",
|
||||
"evolves_from": "Debuglin",
|
||||
"affinity": "Tester",
|
||||
"description": "Evolved form of Debuglin. Sees the bug before the code is even written.",
|
||||
"base_stats": {"power": 60, "catch_rate": 0.75, "xp_multiplier": 1.3}
|
||||
"base_stats": {
|
||||
"power": 60,
|
||||
"catch_rate": 0.75,
|
||||
"xp_multiplier": 1.3
|
||||
}
|
||||
},
|
||||
"Nullex": {
|
||||
"id": "Nullex",
|
||||
"display": "🕳️ Nullex",
|
||||
"display": "\ud83d\udd73\ufe0f Nullex",
|
||||
"type": "buddymon",
|
||||
"evolves_from": "Minimox",
|
||||
"affinity": "Cleaner",
|
||||
"description": "Evolved form of Minimox. Has achieved true minimalism. The file was always one function.",
|
||||
"base_stats": {"power": 55, "catch_rate": 0.65, "xp_multiplier": 1.4}
|
||||
"base_stats": {
|
||||
"power": 55,
|
||||
"catch_rate": 0.65,
|
||||
"xp_multiplier": 1.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ Parse `$ARGUMENTS` (trim whitespace, lowercase the first word) and dispatch:
|
|||
| `fight` | Fight active encounter |
|
||||
| `catch` | Catch active encounter |
|
||||
| `roster` | Full roster view |
|
||||
| `evolve` | Evolve active buddy (available at Lv.100) |
|
||||
| `statusline` | Install Buddymon statusline into settings.json |
|
||||
| `help` | Show command list |
|
||||
|
||||
|
|
@ -117,12 +118,57 @@ Greet them and explain the encounter system.
|
|||
|
||||
## `assign <name>` — Assign Buddy
|
||||
|
||||
Assignment is **per-session** — each Claude Code window can have its own buddy.
|
||||
It writes to the session state file only, not the global default.
|
||||
|
||||
Fuzzy-match `<name>` against owned Buddymon (case-insensitive, partial).
|
||||
If ambiguous, list matches and ask which.
|
||||
If no name given, list roster and ask.
|
||||
|
||||
On match, update `active.json` (buddy_id, reset session_xp, set challenge).
|
||||
Show challenge proposal with Accept / Decline / Reroll.
|
||||
On match, show challenge proposal with Accept / Decline / Reroll, then write:
|
||||
|
||||
```python
|
||||
import json, os, random
|
||||
from pathlib import Path
|
||||
|
||||
BUDDYMON_DIR = Path.home() / ".claude" / "buddymon"
|
||||
PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
||||
catalog = json.load(open(f"{PLUGIN_ROOT}/lib/catalog.json"))
|
||||
|
||||
SESSION_KEY = str(os.getpgrp())
|
||||
SESSION_FILE = BUDDYMON_DIR / "sessions" / f"{SESSION_KEY}.json"
|
||||
SESSION_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
roster = json.load(open(BUDDYMON_DIR / "roster.json"))
|
||||
|
||||
buddy_id = "Debuglin" # replace with matched buddy id
|
||||
buddy_catalog = (catalog.get("buddymon", {}).get(buddy_id)
|
||||
or catalog.get("evolutions", {}).get(buddy_id) or {})
|
||||
challenges = buddy_catalog.get("challenges", [])
|
||||
|
||||
# Load or init session state
|
||||
try:
|
||||
session_state = json.load(open(SESSION_FILE))
|
||||
except Exception:
|
||||
session_state = {}
|
||||
|
||||
session_state["buddymon_id"] = buddy_id
|
||||
session_state["session_xp"] = 0
|
||||
session_state["challenge"] = random.choice(challenges) if challenges else None
|
||||
|
||||
json.dump(session_state, open(SESSION_FILE, "w"), indent=2)
|
||||
|
||||
# Also update global default so new sessions inherit this assignment
|
||||
active = {}
|
||||
try:
|
||||
active = json.load(open(BUDDYMON_DIR / "active.json"))
|
||||
except Exception:
|
||||
pass
|
||||
active["buddymon_id"] = buddy_id
|
||||
json.dump(active, open(BUDDYMON_DIR / "active.json", "w"), indent=2)
|
||||
```
|
||||
|
||||
Show challenge proposal with Accept / Decline / Reroll (updating `session_state["challenge"]` accordingly).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -303,6 +349,100 @@ Read `roster.json` → `language_affinities`. Skip this section if empty.
|
|||
|
||||
---
|
||||
|
||||
## `evolve` — Evolve Buddy (Prestige)
|
||||
|
||||
Evolution is available when the active buddy is **Lv.100** (total XP ≥ 9,900).
|
||||
Evolving resets the buddy to Lv.1 in their new form — but the evolved form has
|
||||
higher base stats and a better XP multiplier, so the second climb is faster.
|
||||
|
||||
Read state and check eligibility:
|
||||
|
||||
```python
|
||||
import json, os
|
||||
|
||||
BUDDYMON_DIR = os.path.expanduser("~/.claude/buddymon")
|
||||
PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
||||
catalog = json.load(open(f"{PLUGIN_ROOT}/lib/catalog.json"))
|
||||
|
||||
active = json.load(open(f"{BUDDYMON_DIR}/active.json"))
|
||||
roster = json.load(open(f"{BUDDYMON_DIR}/roster.json"))
|
||||
|
||||
buddy_id = active.get("buddymon_id")
|
||||
owned = roster.get("owned", {})
|
||||
buddy_data = owned.get(buddy_id, {})
|
||||
level = buddy_data.get("level", 1)
|
||||
total_xp = buddy_data.get("xp", 0)
|
||||
|
||||
# Check evolution entry in catalog
|
||||
catalog_entry = catalog.get("buddymon", {}).get(buddy_id) or catalog.get("evolutions", {}).get(buddy_id)
|
||||
evolutions = catalog_entry.get("evolutions", []) if catalog_entry else []
|
||||
evolution = next((e for e in evolutions if level >= e.get("level", 999)), None)
|
||||
```
|
||||
|
||||
If `evolution` is None or level < 100: show current level and XP toward 100, no evolution available yet.
|
||||
|
||||
If eligible, show evolution preview:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ ✨ Evolution Ready! ║
|
||||
╠══════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ 🔍 Debuglin Lv.100 → 🔬 Verifex ║
|
||||
║ ║
|
||||
║ Verifex: Sees the bug before the code is even written. ║
|
||||
║ catch_rate: 0.60 → 0.75 · xp_multiplier: 1.0 → 1.3 ║
|
||||
║ ║
|
||||
║ ⚠️ Resets to Lv.1. Your caught monsters stay. ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
Evolve? (y/n)
|
||||
```
|
||||
|
||||
On confirm, execute the evolution:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
into_id = evolution["into"]
|
||||
into_data = catalog["evolutions"][into_id]
|
||||
|
||||
# Archive old form with evolution marker
|
||||
owned[buddy_id]["evolved_into"] = into_id
|
||||
owned[buddy_id]["evolved_at"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
# Create new form entry at Lv.1
|
||||
owned[into_id] = {
|
||||
"id": into_id,
|
||||
"display": into_data["display"],
|
||||
"affinity": into_data.get("affinity", catalog_entry.get("affinity", "")),
|
||||
"level": 1,
|
||||
"xp": 0,
|
||||
"evolved_from": buddy_id,
|
||||
"evolved_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
# Carry challenges forward from original form
|
||||
challenges = catalog_entry.get("challenges") or into_data.get("challenges", [])
|
||||
roster["owned"] = owned
|
||||
json.dump(roster, open(f"{BUDDYMON_DIR}/roster.json", "w"), indent=2)
|
||||
|
||||
# Update active to point to evolved form
|
||||
active["buddymon_id"] = into_id
|
||||
active["session_xp"] = 0
|
||||
active["challenge"] = challenges[0] if challenges else None
|
||||
json.dump(active, open(f"{BUDDYMON_DIR}/active.json", "w"), indent=2)
|
||||
```
|
||||
|
||||
Show result:
|
||||
```
|
||||
✨ Debuglin evolved into 🔬 Verifex!
|
||||
Starting fresh at Lv.1 — the second climb is faster.
|
||||
New challenge: IRON TEST
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `statusline` — Install Buddymon Statusline
|
||||
|
||||
Installs the Buddymon statusline into `~/.claude/settings.json`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue