From c12a234652df8675fb7f03b6f1a902cd83165f98 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 5 Apr 2026 23:58:08 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20catch=5Fpending=20race=20condition=20?= =?UTF-8?q?=E2=80=94=20hook=20clears=20flag,=20not=20the=20roll=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the catch roll script wrote catch_pending=False to disk before the Bash process exited. The PostToolUse hook fired after exit, saw False, and could auto-resolve the encounter on the same run as the catch attempt. Fix: the hook now owns the flag lifecycle. - Hook clears catch_pending after consuming it (protects exactly one run) - Roll script never touches catch_pending (no premature clear) - On failure, flag stays True through the roll, cleared by hook afterward - Next /buddymon catch call re-sets it at the top as before Net effect: a failed catch attempt always gets one full clean Bash run of grace before auto-resolve can fire again. --- hooks-handlers/post-tool-use.py | 9 +++++++-- skills/buddymon/SKILL.md | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/hooks-handlers/post-tool-use.py b/hooks-handlers/post-tool-use.py index 4fa77b9..b607c46 100755 --- a/hooks-handlers/post-tool-use.py +++ b/hooks-handlers/post-tool-use.py @@ -457,8 +457,13 @@ def main(): # resolving encounters before the user can react. if output and not encounter_still_present(existing, output, catalog): if existing.get("catch_pending"): - # User invoked /buddymon catch — hold the monster for them - pass + # User invoked /buddymon catch — hold the monster for this run. + # Clear the flag now so the NEXT clean run resumes normal behavior. + # The skill sets it again at the start of each /buddymon catch call. + existing["catch_pending"] = False + enc_data = load_json(BUDDYMON_DIR / "encounters.json") + enc_data["active_encounter"] = existing + save_json(BUDDYMON_DIR / "encounters.json", enc_data) elif existing.get("wounded"): # Wounded: 35% chance to flee per clean run (avg ~3 runs to escape) if random.random() < 0.35: diff --git a/skills/buddymon/SKILL.md b/skills/buddymon/SKILL.md index e65887c..4faacb4 100644 --- a/skills/buddymon/SKILL.md +++ b/skills/buddymon/SKILL.md @@ -298,8 +298,9 @@ if not buddy_id: enc = encounters.get("active_encounter") -# Clear catch_pending before rolling (win or lose) -enc["catch_pending"] = False +# catch_pending is cleared by the PostToolUse hook after it fires on this +# Bash run — do NOT clear it here or the hook will see it already gone and +# may auto-resolve the encounter on the same run as the catch attempt. buddy_data = (catalog.get("buddymon", {}).get(buddy_id) or catalog.get("evolutions", {}).get(buddy_id) or {}) @@ -339,7 +340,8 @@ if success: json.dump(encounters, open(enc_file, "w"), indent=2) print(f"caught:{xp}") else: - # Save cleared catch_pending back on failure + # Leave catch_pending as-is — the PostToolUse hook clears it after this + # Bash run completes, giving one full clean run before auto-resolve resumes. encounters["active_encounter"] = enc json.dump(encounters, open(enc_file, "w"), indent=2) print(f"failed:{int(catch_rate * 100)}")