"""Parse `claude -p --output-format stream-json` output into tool events. Self-contained equivalent of skill-comply's parser so this repo's harness does not depend on the plugin cache. Inputs are kept as dicts (not JSON strings) because the verdict classifier inspects individual arguments. """ import json def parse_stream_json(text): """Return ordered [{tool, input, output, order}] from stream-json text.""" events = [] pending = {} order = 0 for line in text.strip().splitlines(): try: msg = json.loads(line) except json.JSONDecodeError: continue msg_type = msg.get("type") content = msg.get("message", {}).get("content", []) if not isinstance(content, list): continue if msg_type == "assistant": for block in content: if block.get("type") == "tool_use": pending[block.get("id", "")] = { "tool": block.get("name", "unknown"), "input": block.get("input", {}), "order": order, } order += 1 elif msg_type == "user": for block in content: tool_use_id = block.get("tool_use_id", "") if tool_use_id in pending: info = pending.pop(tool_use_id) output = block.get("content", "") if isinstance(output, list): output = json.dumps(output) events.append({ "tool": info["tool"], "input": info["input"], "output": str(output), "order": info["order"], }) # Calls that never got a result (interrupted runs) still matter for # mutation detection; record them with empty output. for info in pending.values(): events.append({ "tool": info["tool"], "input": info["input"], "output": "", "order": info["order"], }) return sorted(events, key=lambda e: e["order"])