Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/bmad_loop/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion src/bmad_loop/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading