diff --git a/src/bmad_loop/install.py b/src/bmad_loop/install.py index c4527be..5699d32 100644 --- a/src/bmad_loop/install.py +++ b/src/bmad_loop/install.py @@ -175,8 +175,10 @@ def strip_legacy_hooks(config: dict) -> tuple[dict, int]: if not isinstance(entry, dict): kept.append(entry) continue - # copilot: the handler dict is the entry itself - if LEGACY_HOOK_MARKER in entry.get("command", ""): + # copilot: the handler dict is the entry itself. Guard the command + # against a non-string (present-but-null) value so a malformed + # hand-edited config can't crash the strip with a TypeError. + if isinstance(cmd := entry.get("command"), str) and LEGACY_HOOK_MARKER in cmd: removed += 1 continue # claude/codex/gemini: handlers nest under "hooks" @@ -185,7 +187,11 @@ def strip_legacy_hooks(config: dict) -> tuple[dict, int]: pruned = [ h for h in nested - if not (isinstance(h, dict) and LEGACY_HOOK_MARKER in h.get("command", "")) + if not ( + isinstance(h, dict) + and isinstance(cmd := h.get("command"), str) + and LEGACY_HOOK_MARKER in cmd + ) ] if len(pruned) != len(nested): removed += len(nested) - len(pruned) diff --git a/src/bmad_loop/probe.py b/src/bmad_loop/probe.py index 2eac010..ec0c9d3 100644 --- a/src/bmad_loop/probe.py +++ b/src/bmad_loop/probe.py @@ -51,12 +51,16 @@ "copilot-events": "~/.copilot/session-state/*/events.jsonl", } # Fallback family glob keyed by the `cli` name, so a CLI whose usage_parser is -# still "none" (e.g. copilot, freshly added) still gets transcript discovery. +# still "none" (e.g. antigravity, freshly added) still gets transcript discovery. FAMILY_GLOBS = { "claude": "~/.claude/projects/*/*.jsonl", "codex": "~/.codex/sessions/*/*/*/rollout-*.jsonl", "gemini": "~/.gemini/tmp/*/chats/session-*.jsonl", "copilot": "~/.copilot/session-state/*/events.jsonl", + # agy (Antigravity CLI) writes a per-conversation transcript.jsonl; this path + # is community-doc-sourced (agy 1.0.x) — confirm against your build with + # `probe-adapter antigravity --probe` before trusting auto-discovery. + "antigravity": "~/.gemini/antigravity-cli/brain/*/.system_generated/logs/transcript.jsonl", } _TOKEN_KEY_RE = re.compile( diff --git a/tests/test_install.py b/tests/test_install.py index 7a8ecc7..bc79fa4 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -168,6 +168,33 @@ def test_strip_legacy_hooks_prunes_within_matcher(): assert [h["command"] for h in handlers] == ["python3 .bmad-loop/bmad_loop_hook.py Stop"] +def test_strip_legacy_hooks_tolerates_non_string_command(): + # a pre-existing handler whose "command" is a non-string (e.g. null) must not + # crash the legacy strip — it just isn't a bmad_auto hook, so it's kept. + # Guarded at both walks: the flat (copilot) entry and the nested handler. + flat = {"hooks": {"agentStop": [{"type": "command", "command": None}]}} + config, removed = strip_legacy_hooks(flat) + assert removed == 0 + assert config["hooks"]["agentStop"] == [{"type": "command", "command": None}] + + nested = { + "hooks": { + "Stop": [ + { + "hooks": [ + {"type": "command", "command": None}, + {"type": "command", "command": LEGACY_CMD}, + ] + } + ] + } + } + config, removed = strip_legacy_hooks(nested) + assert removed == 1 # only the legacy handler is pruned; the null one survives + handlers = config["hooks"]["Stop"][0]["hooks"] + assert handlers == [{"type": "command", "command": None}] + + def test_strip_legacy_hooks_noop_without_hooks(): assert strip_legacy_hooks({}) == ({}, 0) assert strip_legacy_hooks({"hooks": {}})[1] == 0