From a838426676ac920c7c53b3a4eb01f0d9a90e3573 Mon Sep 17 00:00:00 2001 From: pbean Date: Sun, 5 Jul 2026 23:43:46 -0700 Subject: [PATCH 1/2] fix(install): guard strip_legacy_hooks against a non-string command A pre-existing hook handler whose `command` is present-but-null crashed strip_legacy_hooks with `TypeError: argument of type 'NoneType' is not iterable` at both dedupe walks (the flat copilot entry and the nested claude/codex/gemini handler). Mirror the isinstance-str guard PR #67 landed in merge_hooks so a malformed hand-edited config is tolerated (a non-string command simply isn't a bmad_auto hook, so it's kept). Regression test covers both walks. Co-Authored-By: Claude Fable 5 --- src/bmad_loop/install.py | 12 +++++++++--- tests/test_install.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) 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/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 From 02013a43d381e11f90d615c956de7df7e5fae4d4 Mon Sep 17 00:00:00 2001 From: pbean Date: Sun, 5 Jul 2026 23:43:46 -0700 Subject: [PATCH 2/2] feat(probe): add antigravity transcript family glob for agy discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give `probe-adapter antigravity` a fallback transcript location so it can auto-discover an agy conversation transcript.jsonl — the step that unblocks writing the deferred token parser (the antigravity profile ships usage_parser = "none"). The path is community-doc-sourced (agy 1.0.x) and degrades safely to the existing "no convention" note if wrong; confirm against a live build with `probe-adapter antigravity --probe`. Companion to #67 — the glob activates once the antigravity profile lands. Co-Authored-By: Claude Fable 5 --- src/bmad_loop/probe.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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(