#!/usr/bin/env python3 """Extract the real `host_exec` command corpus from session transcripts (docs/NASH.md §10.6). Sweeps a directory tree of transcript captures (JSON or JSONL — the Claude stream-json capture files and session transcript stores both work) for `host_exec` tool calls, dedupes the command strings, auto-classifies each as replayable or `excluded:` (network / host-mutating / signing / toolchain — exclusions are explicit, never silent), and appends NEW entries to the host corpus, preserving anything already there (including the self-test entries the harness requires). Usage: extract_host_corpus.py TRANSCRIPTS_DIR [--corpus PATH] [--dry-run] """ import argparse import json import os import re import sys EXCLUSION_PATTERNS = [ # (reason, regex over the whole command) — first hit wins; order: most-specific first. ("network", r"\b(curl|wget|git\s+(push|pull|fetch|clone)|gh\s|ssh\s|scp\s|rsync\s.*:)"), ("mutates-host", r"\b(brew\s+(install|upgrade|uninstall)|sudo\s|defaults\s+write|killall\s" r"|launchctl\s|xcode-select\s+--install)"), ("signing-identity", r"\b(codesign|notarytool|productsign|security\s+import)"), # Toolchain builds replay only against a real tree (--worktree), not the synthetic fixtures. ("toolchain — replay with --worktree against a real package", r"\b(xcodebuild|swift\s+(build|test|run)|xcrun\s|make\b)"), ] def classify(cmd): for reason, pattern in EXCLUSION_PATTERNS: if re.search(pattern, cmd): return f"excluded:{reason}" return "replayable" def walk_json(node, found): """Recursively collect host_exec tool-call commands from any JSON shape.""" if isinstance(node, dict): name = node.get("name", "") if isinstance(name, str) and name.endswith("host_exec"): command = (node.get("input") or {}).get("command") if isinstance(command, str) and command.strip(): found.append(command.strip()) for value in node.values(): walk_json(value, found) elif isinstance(node, list): for value in node: walk_json(value, found) def commands_in_file(path): found = [] try: with open(path, encoding="utf-8", errors="replace") as f: text = f.read() except OSError: return found # Whole-file JSON first; else JSONL line by line. try: walk_json(json.loads(text), found) return found except ValueError: pass for line in text.splitlines(): line = line.strip() if not line or "host_exec" not in line: continue try: walk_json(json.loads(line), found) except ValueError: continue return found def main(): ap = argparse.ArgumentParser() ap.add_argument("transcripts_dir") ap.add_argument( "--corpus", default=os.path.join( os.path.dirname(os.path.abspath(__file__)), "host-corpus.jsonl"), ) ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() commands = [] for root, _dirs, files in os.walk(args.transcripts_dir): for name in files: if name.endswith((".json", ".jsonl")): commands.extend(commands_in_file(os.path.join(root, name))) existing_cmds = set() next_index = 1 if os.path.exists(args.corpus): with open(args.corpus) as f: for line in f: line = line.strip() if line: entry = json.loads(line) existing_cmds.add(entry["cmd"]) match = re.match(r"hx-(\d+)$", entry["id"]) if match: next_index = max(next_index, int(match.group(1)) + 1) new_entries = [] seen = set() for cmd in commands: if cmd in existing_cmds or cmd in seen: continue seen.add(cmd) entry = {"id": f"hx-{next_index}", "cmd": cmd} next_index += 1 klass = classify(cmd) if klass != "replayable": entry["class"] = klass new_entries.append(entry) print(f"transcripts: {args.transcripts_dir}") print(f"host_exec commands found: {len(commands)} unique-new: {len(new_entries)}") for entry in new_entries: tag = entry.get("class", "replayable") print(f" [{tag}] {entry['id']}: {entry['cmd']!r}") if args.dry_run or not new_entries: return with open(args.corpus, "a") as f: for entry in new_entries: f.write(json.dumps(entry) + "\n") print(f"appended {len(new_entries)} entries -> {args.corpus}") if __name__ == "__main__": main()