Compare commits
7 commits
75f3d9e179
...
85af20b6f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85af20b6f1 | ||
|
|
6632d67da4 | ||
|
|
85f53b1e83 | ||
|
|
55747068e1 | ||
|
|
0c311b099b | ||
|
|
a9c5610914 | ||
|
|
d2006727a1 |
6 changed files with 523 additions and 29 deletions
|
|
@ -77,6 +77,54 @@ def add_session_xp(amount: int):
|
||||||
save_json(roster_file, roster)
|
save_json(roster_file, roster)
|
||||||
|
|
||||||
|
|
||||||
|
LANGUAGE_TIERS = [
|
||||||
|
(0, "discovering"),
|
||||||
|
(50, "familiar"),
|
||||||
|
(150, "comfortable"),
|
||||||
|
(350, "proficient"),
|
||||||
|
(700, "expert"),
|
||||||
|
(1200, "master"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _tier_for_xp(xp: int) -> tuple[int, str]:
|
||||||
|
"""Return (level_index, tier_label) for a given XP total."""
|
||||||
|
level = 0
|
||||||
|
label = LANGUAGE_TIERS[0][1]
|
||||||
|
for i, (threshold, name) in enumerate(LANGUAGE_TIERS):
|
||||||
|
if xp >= threshold:
|
||||||
|
level = i
|
||||||
|
label = name
|
||||||
|
return level, label
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_affinity(lang: str) -> dict:
|
||||||
|
"""Return the affinity entry for lang from roster.json, or a fresh one."""
|
||||||
|
roster = load_json(BUDDYMON_DIR / "roster.json")
|
||||||
|
return roster.get("language_affinities", {}).get(lang, {"xp": 0, "level": 0, "tier": "discovering"})
|
||||||
|
|
||||||
|
|
||||||
|
def add_language_affinity(lang: str, xp_amount: int) -> tuple[bool, str, str]:
|
||||||
|
"""Add XP to lang's affinity. Returns (leveled_up, old_tier, new_tier)."""
|
||||||
|
roster_file = BUDDYMON_DIR / "roster.json"
|
||||||
|
roster = load_json(roster_file)
|
||||||
|
affinities = roster.setdefault("language_affinities", {})
|
||||||
|
entry = affinities.get(lang, {"xp": 0, "level": 0, "tier": "discovering"})
|
||||||
|
|
||||||
|
old_level, old_tier = _tier_for_xp(entry["xp"])
|
||||||
|
entry["xp"] = entry.get("xp", 0) + xp_amount
|
||||||
|
new_level, new_tier = _tier_for_xp(entry["xp"])
|
||||||
|
entry["level"] = new_level
|
||||||
|
entry["tier"] = new_tier
|
||||||
|
|
||||||
|
affinities[lang] = entry
|
||||||
|
roster["language_affinities"] = affinities
|
||||||
|
save_json(roster_file, roster)
|
||||||
|
|
||||||
|
leveled_up = new_level > old_level
|
||||||
|
return leveled_up, old_tier, new_tier
|
||||||
|
|
||||||
|
|
||||||
def get_languages_seen():
|
def get_languages_seen():
|
||||||
session = load_json(BUDDYMON_DIR / "session.json")
|
session = load_json(BUDDYMON_DIR / "session.json")
|
||||||
return set(session.get("languages_seen", []))
|
return set(session.get("languages_seen", []))
|
||||||
|
|
@ -121,6 +169,20 @@ def set_active_encounter(encounter: dict):
|
||||||
save_json(enc_file, data)
|
save_json(enc_file, data)
|
||||||
|
|
||||||
|
|
||||||
|
def wound_encounter() -> None:
|
||||||
|
"""Drop active encounter to minimum strength and flag for re-announcement."""
|
||||||
|
enc_file = BUDDYMON_DIR / "encounters.json"
|
||||||
|
data = load_json(enc_file)
|
||||||
|
enc = data.get("active_encounter")
|
||||||
|
if not enc:
|
||||||
|
return
|
||||||
|
enc["current_strength"] = 5
|
||||||
|
enc["wounded"] = True
|
||||||
|
enc["announced"] = False # triggers UserPromptSubmit re-announcement
|
||||||
|
data["active_encounter"] = enc
|
||||||
|
save_json(enc_file, data)
|
||||||
|
|
||||||
|
|
||||||
def match_bug_monster(output_text: str, catalog: dict) -> dict | None:
|
def match_bug_monster(output_text: str, catalog: dict) -> dict | None:
|
||||||
"""Return the first matching bug monster from the catalog, or None."""
|
"""Return the first matching bug monster from the catalog, or None."""
|
||||||
if not output_text:
|
if not output_text:
|
||||||
|
|
@ -285,6 +347,23 @@ def format_new_language_message(lang: str, buddy_display: str) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_language_levelup_message(lang: str, old_tier: str, new_tier: str, total_xp: int, buddy_display: str) -> str:
|
||||||
|
tier_emojis = {
|
||||||
|
"discovering": "🔭",
|
||||||
|
"familiar": "📖",
|
||||||
|
"comfortable": "🛠️",
|
||||||
|
"proficient": "⚡",
|
||||||
|
"expert": "🎯",
|
||||||
|
"master": "👑",
|
||||||
|
}
|
||||||
|
emoji = tier_emojis.get(new_tier, "⬆️")
|
||||||
|
return (
|
||||||
|
f"\n{emoji} **{lang} affinity: {old_tier} → {new_tier}!**\n"
|
||||||
|
f" {buddy_display} has grown more comfortable in {lang}.\n"
|
||||||
|
f" *Total {lang} XP: {total_xp}*\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_commit_message(streak: int, buddy_display: str) -> str:
|
def format_commit_message(streak: int, buddy_display: str) -> str:
|
||||||
if streak < 5:
|
if streak < 5:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -351,14 +430,27 @@ def main():
|
||||||
existing = get_active_encounter()
|
existing = get_active_encounter()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Auto-resolve if the monster's patterns no longer appear in output
|
# On a clean Bash run (monster patterns gone), respect catch_pending,
|
||||||
|
# wound a healthy monster, or auto-resolve a wounded one.
|
||||||
|
# Probability gates prevent back-to-back Bash runs from instantly
|
||||||
|
# resolving encounters before the user can react.
|
||||||
if output and not encounter_still_present(existing, output, catalog):
|
if output and not encounter_still_present(existing, output, catalog):
|
||||||
xp, display = auto_resolve_encounter(existing, buddy_id)
|
if existing.get("catch_pending"):
|
||||||
messages.append(
|
# User invoked /buddymon catch — hold the monster for them
|
||||||
f"\n⚔️ **{buddy_display} defeated {display}!** (auto-resolved)\n"
|
pass
|
||||||
f" +{xp} XP\n"
|
elif existing.get("wounded"):
|
||||||
)
|
# Wounded: 35% chance to flee per clean run (avg ~3 runs to escape)
|
||||||
# else: monster persists, no message — don't spam every tool call
|
if random.random() < 0.35:
|
||||||
|
xp, display = auto_resolve_encounter(existing, buddy_id)
|
||||||
|
messages.append(
|
||||||
|
f"\n💨 **{display} fled!** (escaped while wounded)\n"
|
||||||
|
f" {buddy_display} gets partial XP: +{xp}\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Healthy: 50% chance to wound per clean run (avg ~2 runs to wound)
|
||||||
|
if random.random() < 0.50:
|
||||||
|
wound_encounter()
|
||||||
|
# else: monster still present, no message — don't spam every tool call
|
||||||
elif output or command:
|
elif output or command:
|
||||||
# No active encounter — check for bug monster first, then event encounters
|
# No active encounter — check for bug monster first, then event encounters
|
||||||
session = load_json(BUDDYMON_DIR / "session.json")
|
session = load_json(BUDDYMON_DIR / "session.json")
|
||||||
|
|
@ -394,13 +486,14 @@ def main():
|
||||||
commit_xp = 20
|
commit_xp = 20
|
||||||
add_session_xp(commit_xp)
|
add_session_xp(commit_xp)
|
||||||
|
|
||||||
# ── Write / Edit: new language detection + test file encounters ───────
|
# ── Write / Edit: new language detection + affinity + test file encounters ─
|
||||||
elif tool_name in ("Write", "Edit", "MultiEdit"):
|
elif tool_name in ("Write", "Edit", "MultiEdit"):
|
||||||
file_path = tool_input.get("file_path", "")
|
file_path = tool_input.get("file_path", "")
|
||||||
if file_path:
|
if file_path:
|
||||||
ext = os.path.splitext(file_path)[1].lower()
|
ext = os.path.splitext(file_path)[1].lower()
|
||||||
lang = KNOWN_EXTENSIONS.get(ext)
|
lang = KNOWN_EXTENSIONS.get(ext)
|
||||||
if lang:
|
if lang:
|
||||||
|
# Session-level "first encounter" bonus
|
||||||
seen = get_languages_seen()
|
seen = get_languages_seen()
|
||||||
if lang not in seen:
|
if lang not in seen:
|
||||||
add_language_seen(lang)
|
add_language_seen(lang)
|
||||||
|
|
@ -408,6 +501,13 @@ def main():
|
||||||
msg = format_new_language_message(lang, buddy_display)
|
msg = format_new_language_message(lang, buddy_display)
|
||||||
messages.append(msg)
|
messages.append(msg)
|
||||||
|
|
||||||
|
# Persistent affinity XP — always accumulates
|
||||||
|
leveled_up, old_tier, new_tier = add_language_affinity(lang, 3)
|
||||||
|
if leveled_up:
|
||||||
|
affinity = get_language_affinity(lang)
|
||||||
|
msg = format_language_levelup_message(lang, old_tier, new_tier, affinity["xp"], buddy_display)
|
||||||
|
messages.append(msg)
|
||||||
|
|
||||||
# TestSpecter: editing a test file with no active encounter
|
# TestSpecter: editing a test file with no active encounter
|
||||||
if not get_active_encounter():
|
if not get_active_encounter():
|
||||||
enc = match_test_file_encounter(file_path, catalog)
|
enc = match_test_file_encounter(file_path, catalog)
|
||||||
|
|
|
||||||
|
|
@ -80,24 +80,36 @@ def main():
|
||||||
catchable = enc.get("catchable", True)
|
catchable = enc.get("catchable", True)
|
||||||
flavor = monster.get("flavor", "")
|
flavor = monster.get("flavor", "")
|
||||||
|
|
||||||
catchable_str = "[catchable · catch only]" if not defeatable else f"[{rarity} · {'catchable' if catchable else ''}]"
|
if enc.get("wounded"):
|
||||||
|
# Wounded re-announcement — urgent, catch-or-lose framing
|
||||||
lines = [
|
lines = [
|
||||||
f"\n💀 **{enc['display']} appeared!** {catchable_str}",
|
f"\n🩹 **{enc['display']} is wounded and fleeing!**",
|
||||||
f" Strength: {strength}% · Rarity: {stars}",
|
f" Strength: {strength}% · This is your last chance to catch it.",
|
||||||
]
|
"",
|
||||||
if flavor:
|
f" **{buddy_display}** is ready — move fast!",
|
||||||
lines.append(f" *{flavor}*")
|
"",
|
||||||
if not defeatable:
|
" `[CATCH]` → `/buddymon catch` (near-guaranteed at 5% strength)",
|
||||||
lines.append(" ⚠️ CANNOT BE DEFEATED — catch only")
|
" `[IGNORE]` → it flees on the next clean run",
|
||||||
lines += [
|
]
|
||||||
"",
|
else:
|
||||||
f" **{buddy_display}** is ready to battle!",
|
# Normal first appearance
|
||||||
"",
|
catchable_str = "[catchable · catch only]" if not defeatable else f"[{rarity} · {'catchable' if catchable else ''}]"
|
||||||
" `[FIGHT]` Fix the bug → `/buddymon fight` to claim XP",
|
lines = [
|
||||||
" `[CATCH]` Weaken first (test/repro/comment) → `/buddymon catch`",
|
f"\n💀 **{enc['display']} appeared!** {catchable_str}",
|
||||||
" `[FLEE]` Ignore → monster grows stronger",
|
f" Strength: {strength}% · Rarity: {stars}",
|
||||||
]
|
]
|
||||||
|
if flavor:
|
||||||
|
lines.append(f" *{flavor}*")
|
||||||
|
if not defeatable:
|
||||||
|
lines.append(" ⚠️ CANNOT BE DEFEATED — catch only")
|
||||||
|
lines += [
|
||||||
|
"",
|
||||||
|
f" **{buddy_display}** is ready to battle!",
|
||||||
|
"",
|
||||||
|
" `[FIGHT]` Fix the bug → `/buddymon fight` to claim XP",
|
||||||
|
" `[CATCH]` Weaken first (test/repro/comment) → `/buddymon catch`",
|
||||||
|
" `[FLEE]` Ignore → monster grows stronger",
|
||||||
|
]
|
||||||
|
|
||||||
msg = "\n".join(lines)
|
msg = "\n".join(lines)
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
|
|
|
||||||
18
install.sh
18
install.sh
|
|
@ -278,6 +278,24 @@ if not os.path.exists(log_path):
|
||||||
print(" Created hook_debug.log")
|
print(" Created hook_debug.log")
|
||||||
PYEOF
|
PYEOF
|
||||||
|
|
||||||
|
# Copy statusline script to stable user-local path
|
||||||
|
cp "${REPO_DIR}/lib/statusline.sh" "${BUDDYMON_DIR}/statusline.sh"
|
||||||
|
chmod +x "${BUDDYMON_DIR}/statusline.sh"
|
||||||
|
ok "Installed statusline.sh → ${BUDDYMON_DIR}/statusline.sh"
|
||||||
|
|
||||||
|
# Install statusline into settings.json if not already configured
|
||||||
|
python3 << PYEOF
|
||||||
|
import json
|
||||||
|
f = '${SETTINGS_FILE}'
|
||||||
|
d = json.load(open(f))
|
||||||
|
if 'statusLine' not in d:
|
||||||
|
d['statusLine'] = {"type": "command", "command": "bash ${BUDDYMON_DIR}/statusline.sh"}
|
||||||
|
json.dump(d, open(f, 'w'), indent=2)
|
||||||
|
print(" Installed Buddymon statusline in settings.json")
|
||||||
|
else:
|
||||||
|
print(" statusLine already configured — skipped (run /buddymon statusline to install manually)")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ ${PLUGIN_KEY} installed!"
|
echo "✓ ${PLUGIN_KEY} installed!"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
247
lib/catalog.json
247
lib/catalog.json
|
|
@ -276,7 +276,15 @@
|
||||||
"heap.*exhausted",
|
"heap.*exhausted",
|
||||||
"memory leak",
|
"memory leak",
|
||||||
"Cannot allocate memory",
|
"Cannot allocate memory",
|
||||||
"Killed.*memory"
|
"Killed.*memory",
|
||||||
|
"malloc.*failed",
|
||||||
|
"std::bad_alloc",
|
||||||
|
"java\\.lang\\.OutOfMemoryError",
|
||||||
|
"GC overhead limit exceeded",
|
||||||
|
"JavaScript heap out of memory",
|
||||||
|
"OOMKilled",
|
||||||
|
"oom-killer",
|
||||||
|
"malloc: can't allocate region"
|
||||||
],
|
],
|
||||||
"weaken_actions": [
|
"weaken_actions": [
|
||||||
{"action": "write_failing_test", "strength_reduction": 20},
|
{"action": "write_failing_test", "strength_reduction": 20},
|
||||||
|
|
@ -284,6 +292,198 @@
|
||||||
{"action": "add_documenting_comment", "strength_reduction": 10}
|
{"action": "add_documenting_comment", "strength_reduction": 10}
|
||||||
],
|
],
|
||||||
"flavor": "It was already there when you opened the task manager."
|
"flavor": "It was already there when you opened the task manager."
|
||||||
|
},
|
||||||
|
"CudaCrash": {
|
||||||
|
"id": "CudaCrash",
|
||||||
|
"display": "⚡ CudaCrash",
|
||||||
|
"type": "bug_monster",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"base_strength": 65,
|
||||||
|
"xp_reward": 130,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"description": "Lives in VRAM. Detonates the moment your batch size is one too many. Doesn't negotiate.",
|
||||||
|
"error_patterns": [
|
||||||
|
"CUDA out of memory",
|
||||||
|
"torch\\.cuda\\.OutOfMemoryError",
|
||||||
|
"CUDA error: out of memory",
|
||||||
|
"RuntimeError.*CUDA.*memory",
|
||||||
|
"cuDNN.*insufficient memory",
|
||||||
|
"CUBLAS_STATUS_ALLOC_FAILED",
|
||||||
|
"out of GPU memory",
|
||||||
|
"VRAM.*exhausted",
|
||||||
|
"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}
|
||||||
|
],
|
||||||
|
"flavor": "Your model fit in VRAM yesterday. You added one layer."
|
||||||
|
},
|
||||||
|
"InfiniteWisp": {
|
||||||
|
"id": "InfiniteWisp",
|
||||||
|
"display": "🌀 InfiniteWisp",
|
||||||
|
"type": "bug_monster",
|
||||||
|
"rarity": "common",
|
||||||
|
"base_strength": 30,
|
||||||
|
"xp_reward": 55,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"description": "Spawned from a while loop with no exit. Runs forever. Eats your CPU. Doesn't know it's lost.",
|
||||||
|
"error_patterns": [
|
||||||
|
"KeyboardInterrupt",
|
||||||
|
"Traceback.*KeyboardInterrupt",
|
||||||
|
"infinite loop",
|
||||||
|
"loop.*infinite",
|
||||||
|
"maximum iteration",
|
||||||
|
"process.*hung",
|
||||||
|
"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}
|
||||||
|
],
|
||||||
|
"flavor": "Your fan was always loud. You just never checked why."
|
||||||
|
},
|
||||||
|
"BoundsHound": {
|
||||||
|
"id": "BoundsHound",
|
||||||
|
"display": "🐕 BoundsHound",
|
||||||
|
"type": "bug_monster",
|
||||||
|
"rarity": "common",
|
||||||
|
"base_strength": 25,
|
||||||
|
"xp_reward": 45,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"description": "Lurks at the edge of every array. Patient. Knows you'll be off by one eventually.",
|
||||||
|
"error_patterns": [
|
||||||
|
"IndexError",
|
||||||
|
"index out of range",
|
||||||
|
"list index out of range",
|
||||||
|
"ArrayIndexOutOfBoundsException",
|
||||||
|
"array index.*out of bounds",
|
||||||
|
"index.*out of bounds",
|
||||||
|
"subscript out of range",
|
||||||
|
"out of bounds access",
|
||||||
|
"RangeError.*index"
|
||||||
|
],
|
||||||
|
"weaken_actions": [
|
||||||
|
{"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",
|
||||||
|
"type": "bug_monster",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"base_strength": 40,
|
||||||
|
"xp_reward": 75,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"description": "Lives in the else branch you swore would never execute. It executed.",
|
||||||
|
"error_patterns": [
|
||||||
|
"unreachable code",
|
||||||
|
"dead code",
|
||||||
|
"branch.*never.*taken",
|
||||||
|
"always.*true",
|
||||||
|
"always.*false",
|
||||||
|
"condition.*always",
|
||||||
|
"else.*never",
|
||||||
|
"fallthrough"
|
||||||
|
],
|
||||||
|
"weaken_actions": [
|
||||||
|
{"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",
|
||||||
|
"type": "bug_monster",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"base_strength": 35,
|
||||||
|
"xp_reward": 65,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"description": "Hides in unhandled cases. Switch statements with no default. Match arms that never close. It found the gap.",
|
||||||
|
"error_patterns": [
|
||||||
|
"fall.?through",
|
||||||
|
"missing.*case",
|
||||||
|
"unhandled.*case",
|
||||||
|
"no.*default",
|
||||||
|
"non-exhaustive.*pattern",
|
||||||
|
"MatchError",
|
||||||
|
"match.*non-exhaustive",
|
||||||
|
"switch.*not.*handled",
|
||||||
|
"Missing case for",
|
||||||
|
"Unhandled variant"
|
||||||
|
],
|
||||||
|
"weaken_actions": [
|
||||||
|
{"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",
|
||||||
|
"type": "bug_monster",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"base_strength": 45,
|
||||||
|
"xp_reward": 85,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"description": "The function that forgot to stop. Calls itself into oblivion. The base case was almost right.",
|
||||||
|
"error_patterns": [
|
||||||
|
"RecursionError",
|
||||||
|
"maximum recursion depth",
|
||||||
|
"stack overflow",
|
||||||
|
"StackOverflowError",
|
||||||
|
"too much recursion",
|
||||||
|
"Maximum call stack size exceeded",
|
||||||
|
"infinite recursion",
|
||||||
|
"stack.*exhausted"
|
||||||
|
],
|
||||||
|
"weaken_actions": [
|
||||||
|
{"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",
|
||||||
|
"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.",
|
||||||
|
"error_patterns": [
|
||||||
|
"except Exception",
|
||||||
|
"except:$",
|
||||||
|
"catch.*\\(Exception e\\)",
|
||||||
|
"catch.*\\(e\\).*\\{\\}",
|
||||||
|
"swallowed.*exception",
|
||||||
|
"error.*ignored",
|
||||||
|
"bare except",
|
||||||
|
"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}
|
||||||
|
],
|
||||||
|
"flavor": "If you catch everything, you learn nothing."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -479,6 +679,51 @@
|
||||||
],
|
],
|
||||||
"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 — it just watches to make sure you chose the right octal.",
|
||||||
"flavor": "777 was always the answer. Never the right one."
|
"flavor": "777 was always the answer. Never the right one."
|
||||||
|
},
|
||||||
|
"LayerLurker": {
|
||||||
|
"id": "LayerLurker",
|
||||||
|
"display": "🐋 LayerLurker",
|
||||||
|
"type": "event_encounter",
|
||||||
|
"rarity": "common",
|
||||||
|
"base_strength": 35,
|
||||||
|
"xp_reward": 60,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"trigger_type": "command",
|
||||||
|
"command_patterns": [
|
||||||
|
"docker build",
|
||||||
|
"docker pull",
|
||||||
|
"docker run",
|
||||||
|
"docker compose up",
|
||||||
|
"docker-compose up",
|
||||||
|
"podman build",
|
||||||
|
"podman pull"
|
||||||
|
],
|
||||||
|
"description": "Lives between image layers. Gets comfortable during long builds.",
|
||||||
|
"flavor": "It cached everything except the one layer you changed."
|
||||||
|
},
|
||||||
|
"DiskDemon": {
|
||||||
|
"id": "DiskDemon",
|
||||||
|
"display": "💾 DiskDemon",
|
||||||
|
"type": "event_encounter",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"base_strength": 50,
|
||||||
|
"xp_reward": 95,
|
||||||
|
"catchable": true,
|
||||||
|
"defeatable": true,
|
||||||
|
"trigger_type": "output",
|
||||||
|
"error_patterns": [
|
||||||
|
"No space left on device",
|
||||||
|
"ENOSPC",
|
||||||
|
"disk quota exceeded",
|
||||||
|
"Disk quota exceeded",
|
||||||
|
"not enough space",
|
||||||
|
"insufficient disk space",
|
||||||
|
"no space available",
|
||||||
|
"filesystem is full"
|
||||||
|
],
|
||||||
|
"description": "Manifests when the disk is full. Usually right before a release.",
|
||||||
|
"flavor": "It's been there since 2019. It's just a log file, you said."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
38
lib/statusline.sh
Normal file
38
lib/statusline.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Buddymon statusline — displays active buddy + encounter in the CC status bar.
|
||||||
|
# Install: add to ~/.claude/settings.json → "statusLine" → "command"
|
||||||
|
# Or run: /buddymon statusline (installs automatically)
|
||||||
|
|
||||||
|
B="$HOME/.claude/buddymon"
|
||||||
|
|
||||||
|
# Bail fast if no state directory or no starter chosen
|
||||||
|
[[ -d "$B" ]] || exit 0
|
||||||
|
STARTER=$(jq -r '.starter_chosen // false' "$B/roster.json" 2>/dev/null)
|
||||||
|
[[ "$STARTER" == "true" ]] || exit 0
|
||||||
|
|
||||||
|
# Read state
|
||||||
|
ID=$(jq -r '.buddymon_id // ""' "$B/active.json" 2>/dev/null)
|
||||||
|
[[ -n "$ID" ]] || exit 0
|
||||||
|
|
||||||
|
LVL=$(jq -r ".owned[\"$ID\"].level // 1" "$B/roster.json" 2>/dev/null)
|
||||||
|
XP=$(jq -r '.session_xp // 0' "$B/active.json" 2>/dev/null)
|
||||||
|
|
||||||
|
ENC_JSON=$(jq -c '.active_encounter // null' "$B/encounters.json" 2>/dev/null)
|
||||||
|
ENC_DISPLAY=$(echo "$ENC_JSON" | jq -r '.display // ""' 2>/dev/null)
|
||||||
|
ENC_STRENGTH=$(echo "$ENC_JSON" | jq -r '.current_strength // 0' 2>/dev/null)
|
||||||
|
|
||||||
|
# ANSI colors
|
||||||
|
CY='\033[38;2;23;146;153m' # cyan — buddy
|
||||||
|
GR='\033[38;2;64;160;43m' # green — xp
|
||||||
|
RD='\033[38;2;203;60;51m' # red — encounter
|
||||||
|
DM='\033[38;2;120;120;120m' # dim — separators
|
||||||
|
RS='\033[0m'
|
||||||
|
|
||||||
|
printf "${CY}🐾 ${ID} Lv.${LVL}${RS}"
|
||||||
|
printf " ${DM}·${RS} ${GR}+${XP}xp${RS}"
|
||||||
|
|
||||||
|
if [[ "$ENC_JSON" != "null" ]] && [[ -n "$ENC_DISPLAY" ]]; then
|
||||||
|
printf " ${RD}⚔ ${ENC_DISPLAY} [${ENC_STRENGTH}%%]${RS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
@ -25,6 +25,7 @@ Parse `$ARGUMENTS` (trim whitespace, lowercase the first word) and dispatch:
|
||||||
| `fight` | Fight active encounter |
|
| `fight` | Fight active encounter |
|
||||||
| `catch` | Catch active encounter |
|
| `catch` | Catch active encounter |
|
||||||
| `roster` | Full roster view |
|
| `roster` | Full roster view |
|
||||||
|
| `statusline` | Install Buddymon statusline into settings.json |
|
||||||
| `help` | Show command list |
|
| `help` | Show command list |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -174,14 +175,31 @@ ShadowBit (🔒) cannot be defeated — redirect to catch.
|
||||||
|
|
||||||
Read active encounter. If none: "No active encounter."
|
Read active encounter. If none: "No active encounter."
|
||||||
|
|
||||||
Show strength and weakening status. Explain weaken actions:
|
**Immediately set `catch_pending = True`** on the encounter to suppress auto-resolve
|
||||||
|
while the weakening Q&A is in progress:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json, os
|
||||||
|
BUDDYMON_DIR = os.path.expanduser("~/.claude/buddymon")
|
||||||
|
enc_file = f"{BUDDYMON_DIR}/encounters.json"
|
||||||
|
encounters = json.load(open(enc_file))
|
||||||
|
enc = encounters.get("active_encounter")
|
||||||
|
if enc:
|
||||||
|
enc["catch_pending"] = True
|
||||||
|
encounters["active_encounter"] = enc
|
||||||
|
json.dump(encounters, open(enc_file, "w"), indent=2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Show strength and weakening status. If `enc.get("wounded")` is True, note that
|
||||||
|
it's already at 5% and a catch is near-guaranteed. Explain weaken actions:
|
||||||
- Write a failing test → -20% strength
|
- Write a failing test → -20% strength
|
||||||
- Isolate reproduction case → -20% strength
|
- Isolate reproduction case → -20% strength
|
||||||
- Add documenting comment → -10% strength
|
- Add documenting comment → -10% strength
|
||||||
|
|
||||||
Ask which weakening actions have been done. Apply reductions to `current_strength`.
|
Ask which weakening actions have been done. Apply reductions to `current_strength`.
|
||||||
|
|
||||||
Catch roll:
|
Catch roll (clear `catch_pending` before rolling — success clears encounter, failure
|
||||||
|
leaves it active without the flag so auto-resolve resumes naturally):
|
||||||
```python
|
```python
|
||||||
import json, os, random
|
import json, os, random
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
@ -201,6 +219,9 @@ roster = json.load(open(roster_file))
|
||||||
enc = encounters.get("active_encounter")
|
enc = encounters.get("active_encounter")
|
||||||
buddy_id = active.get("buddymon_id")
|
buddy_id = active.get("buddymon_id")
|
||||||
|
|
||||||
|
# Clear catch_pending before rolling (win or lose)
|
||||||
|
enc["catch_pending"] = False
|
||||||
|
|
||||||
buddy_data = (catalog.get("buddymon", {}).get(buddy_id)
|
buddy_data = (catalog.get("buddymon", {}).get(buddy_id)
|
||||||
or catalog.get("evolutions", {}).get(buddy_id) or {})
|
or catalog.get("evolutions", {}).get(buddy_id) or {})
|
||||||
buddy_level = roster.get("owned", {}).get(buddy_id, {}).get("level", 1)
|
buddy_level = roster.get("owned", {}).get(buddy_id, {}).get("level", 1)
|
||||||
|
|
@ -232,6 +253,9 @@ if success:
|
||||||
json.dump(encounters, open(enc_file, "w"), indent=2)
|
json.dump(encounters, open(enc_file, "w"), indent=2)
|
||||||
print(f"caught:{xp}")
|
print(f"caught:{xp}")
|
||||||
else:
|
else:
|
||||||
|
# Save cleared catch_pending back on failure
|
||||||
|
encounters["active_encounter"] = enc
|
||||||
|
json.dump(encounters, open(enc_file, "w"), indent=2)
|
||||||
print(f"failed:{int(catch_rate * 100)}")
|
print(f"failed:{int(catch_rate * 100)}")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -259,8 +283,64 @@ Read roster and display:
|
||||||
🌐 CORSCurse — caught 2026-03-28
|
🌐 CORSCurse — caught 2026-03-28
|
||||||
|
|
||||||
❓ ??? — [n] more creatures to discover...
|
❓ ??? — [n] more creatures to discover...
|
||||||
|
|
||||||
|
🗺️ Language Affinities
|
||||||
|
──────────────────────────────────────────
|
||||||
|
🛠️ Python comfortable (Lv.2 · 183 XP)
|
||||||
|
📖 TypeScript familiar (Lv.1 · 72 XP)
|
||||||
|
🔭 Rust discovering (Lv.0 · 9 XP)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tier emoji mapping:
|
||||||
|
- 🔭 discovering (0 XP)
|
||||||
|
- 📖 familiar (50 XP)
|
||||||
|
- 🛠️ comfortable (150 XP)
|
||||||
|
- ⚡ proficient (350 XP)
|
||||||
|
- 🎯 expert (700 XP)
|
||||||
|
- 👑 master (1200 XP)
|
||||||
|
|
||||||
|
Read `roster.json` → `language_affinities`. Skip this section if empty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `statusline` — Install Buddymon Statusline
|
||||||
|
|
||||||
|
Installs the Buddymon statusline into `~/.claude/settings.json`.
|
||||||
|
|
||||||
|
The statusline shows active buddy + level + session XP, and highlights any
|
||||||
|
active encounter in red:
|
||||||
|
|
||||||
|
```
|
||||||
|
🐾 Debuglin Lv.90 · +45xp ⚔ 💀 NullWraith [60%]
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this Python to install:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json, os, shutil
|
||||||
|
|
||||||
|
SETTINGS = os.path.expanduser("~/.claude/settings.json")
|
||||||
|
PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
||||||
|
SCRIPT = os.path.join(PLUGIN_ROOT, "lib", "statusline.sh")
|
||||||
|
|
||||||
|
settings = json.load(open(SETTINGS))
|
||||||
|
|
||||||
|
if settings.get("statusLine"):
|
||||||
|
print("⚠️ A statusLine is already configured. Replace it? (y/n)")
|
||||||
|
# ask user — if no, abort
|
||||||
|
# if yes, proceed
|
||||||
|
pass
|
||||||
|
|
||||||
|
settings["statusLine"] = {
|
||||||
|
"type": "command",
|
||||||
|
"command": f"bash {SCRIPT}",
|
||||||
|
}
|
||||||
|
json.dump(settings, open(SETTINGS, "w"), indent=2)
|
||||||
|
print(f"✅ Buddymon statusline installed. Reload Claude Code to activate.")
|
||||||
|
```
|
||||||
|
|
||||||
|
If a `statusLine` is already set, show the existing command and ask before replacing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `help`
|
## `help`
|
||||||
|
|
@ -272,4 +352,5 @@ Read roster and display:
|
||||||
/buddymon fight — fight active encounter
|
/buddymon fight — fight active encounter
|
||||||
/buddymon catch — catch active encounter
|
/buddymon catch — catch active encounter
|
||||||
/buddymon roster — view full roster
|
/buddymon roster — view full roster
|
||||||
|
/buddymon statusline — install statusline widget
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue