import logging
import argparse
import json
from pathlib import Path
from typing import Dict, Any

from agi_dw.core.verifier.service import VerifierServiceConfig, verify as verifier_run  # type: ignore

from agi_dw.core.actuator.service import compute_router_features  # type: ignore

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument(
		"--verified",
		nargs="+",
		default=[
			str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"),
		],
		help="One or more verified JSONL files (CLI and/or DOM)",
	)
	ap.add_argument("--out", default=str(root / "data" / "skills" / "router_ds.jsonl"))
	ap.add_argument("--add-hard-negatives", action="store_true", help="Augment dataset with templated hard/negative CLI examples")
	args = ap.parse_args()

	in_paths = [Path(p) for p in args.verified]
	out_path = Path(args.out)
	out_path.parent.mkdir(parents=True, exist_ok=True)

	written = 0
	with out_path.open("w", encoding="utf-8") as f_out:
		for in_path in in_paths:
			if not in_path.exists():
				continue
			with in_path.open("r", encoding="utf-8") as f_in:
				for line in f_in:
					line = line.strip()
					if not line:
						continue
					try:
						tr: Dict[str, Any] = json.loads(line)
						obs = tr.get("obs", {})
						plan = tr.get("plan", {})
					# Prefer verifier service risk if not already present
					vr = (tr.get("critique") or {}).get("risk")
					if vr is None:
						try:
							v_cfg = VerifierServiceConfig(model="meta-llama/Llama-3.2-3B", timeout_sec=5)
							vres = verifier_run({"obs": obs, "plan": plan, "action": tr.get("action", {}), "result": tr.get("result", {})}, v_cfg)
							vr = float(vres.get("risk", 0.5))
						except Exception:
							vr = None
					extras = {
						"verifier_risk": vr,
						"wm_prior": tr.get("wm_prior"),
					}
						features = compute_router_features(obs, plan, extras=extras)
						label = 1 if (tr.get("result", {}).get("status") == "ok") else 0
						example = {"features": features, "label": label}
						# Include task name if present to enable per-task threshold tuning
						try:
							task = tr.get("task")
							if isinstance(task, str) and task:
								example["task"] = task
						except Exception:
							pass
						f_out.write(json.dumps(example) + "\n")
						written += 1
					except Exception:
						continue
		# Optional: add hard/negative CLI seeds to balance routing
		if args.add_hard_negatives:
			# Simulate plans that mention CLI tools but likely fail
			hard_plans = [
				{"subgoals": ["count lines wrong"], "tools": ["cli"], "constraints": {}, "note": "wc without file"},
				{"subgoals": ["grep typo"], "tools": ["cli"], "constraints": {}, "note": "grep missing pattern"},
			]
			for hp in hard_plans:
				try:
					features = compute_router_features({"kind": "cli"}, hp, extras={})
					example = {"features": features, "label": 0}
					f_out.write(json.dumps(example) + "\n")
					written += 1
				except Exception:
					continue
	print(f"Wrote {written} router examples -> {out_path}")
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import hashlib
import json
from pathlib import Path
from typing import Dict, Iterable, Tuple

def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def hash_key(obj: Dict) -> int:
	# Stable hash from task_id + plan json when present
	tid = str(obj.get("task_id", ""))
	pl = obj.get("plan", {})
	key = (tid + "|" + json.dumps(pl, ensure_ascii=False, sort_keys=True))
	return int(hashlib.sha256(key.encode("utf-8")).hexdigest(), 16)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = hash_key(obj)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--src", default=str(root / "data" / "traces" / "all.jsonl"))
	ap.add_argument("--train", default=str(root / "data" / "splits" / "train.jsonl"))
	ap.add_argument("--eval", default=str(root / "data" / "splits" / "eval.jsonl"))
	ap.add_argument("--ratio", type=float, default=0.8)
	args = ap.parse_args()

	src = Path(args.src)
	n_tr, n_ev = write_split(src, Path(args.train), Path(args.eval), max(0.0, min(1.0, float(args.ratio))))
	print(json.dumps({"train": n_tr, "eval": n_ev, "src": str(src)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import hashlib
import json
from pathlib import Path
from typing import Dict, Iterable, Tuple

def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def hash_key(obj: Dict) -> int:
	# Stable hash from task_id + plan json when present
	tid = str(obj.get("task_id", ""))
	pl = obj.get("plan", {})
	key = (tid + "|" + json.dumps(pl, ensure_ascii=False, sort_keys=True))
	return int(hashlib.sha256(key.encode("utf-8")).hexdigest(), 16)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = hash_key(obj)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"


import hashlib
import json
from pathlib import Path
from typing import Dict, Iterable, Tuple

def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def hash_key(obj: Dict) -> int:
	# Stable hash from task_id + plan json when present
	tid = str(obj.get("task_id", ""))
	pl = obj.get("plan", {})
	key = (tid + "|" + json.dumps(pl, ensure_ascii=False, sort_keys=True))
	return int(hashlib.sha256(key.encode("utf-8")).hexdigest(), 16)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = hash_key(obj)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev


		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def hash_key(obj: Dict) -> int:
	# Stable hash from task_id + plan json when present
	tid = str(obj.get("task_id", ""))
	pl = obj.get("plan", {})
	key = (tid + "|" + json.dumps(pl, ensure_ascii=False, sort_keys=True))
	return int(hashlib.sha256(key.encode("utf-8")).hexdigest(), 16)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = hash_key(obj)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--src", default=str(root / "data" / "traces" / "all.jsonl"))
	ap.add_argument("--train", default=str(root / "data" / "splits" / "train.jsonl"))
	ap.add_argument("--eval", default=str(root / "data" / "splits" / "eval.jsonl"))
	ap.add_argument("--ratio", type=float, default=0.8)
	args = ap.parse_args()

	src = Path(args.src)
	n_tr, n_ev = write_split(src, Path(args.train), Path(args.eval), max(0.0, min(1.0, float(args.ratio))))
	print(json.dumps({"train": n_tr, "eval": n_ev, "src": str(src)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


main

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = hash_key(obj)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--src", default=str(root / "data" / "traces" / "all.jsonl"))
	ap.add_argument("--train", default=str(root / "data" / "splits" / "train.jsonl"))
	ap.add_argument("--eval", default=str(root / "data" / "splits" / "eval.jsonl"))
	ap.add_argument("--ratio", type=float, default=0.8)
	args = ap.parse_args()

	src = Path(args.src)
	n_tr, n_ev = write_split(src, Path(args.train), Path(args.eval), max(0.0, min(1.0, float(args.ratio))))
	print(json.dumps({"train": n_tr, "eval": n_ev, "src": str(src)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import json
from pathlib import Path
from typing import Dict

def build_example(trace: Dict) -> Dict[str, str]:
	# Compact input: obs+plan+result only
	inp = {
		"obs": trace.get("obs", {}),
		"plan": trace.get("plan", {}),
		"result": trace.get("result", {}),
	}
	# Heuristic target YAML (bootstrap)
	status = (trace.get("result") or {}).get("status", "error")
	if status == "ok":
		success_prob, risk = 0.8, 0.2
	else:
		success_prob, risk = 0.3, 0.6
	tgt = (
		f"success_prob: {success_prob}\n"
		f"risk: {risk}\n"
		f"critique: minimal bootstrap label\n"
	)
	return {"input": json.dumps(inp, ensure_ascii=False), "output": tgt}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("src", nargs="?", default=str(root / "data" / "traces" / "web_dom.verified.jsonl"))
	ap.add_argument("dst", nargs="?", default=str(root / "data" / "skills" / "verifier_ds.jsonl"))
	args = ap.parse_args()

	src = Path(args.src)
	dst = Path(args.dst)
	dst.parent.mkdir(parents=True, exist_ok=True)

	n = 0
	with src.open("r", encoding="utf-8") as fin, dst.open("w", encoding="utf-8") as fout:
		for line in fin:
			line = line.strip()
			if not line:
				continue
			try:
				tr: Dict = json.loads(line)
			except Exception:
				continue
			ex = build_example(tr)
			fout.write(json.dumps(ex, ensure_ascii=False) + "\n")
			n += 1
	print(f"Wrote {n} examples -> {dst}")
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import json
from pathlib import Path
from typing import Dict

def build_example(trace: Dict) -> Dict[str, str]:
	# Compact input: obs+plan+result only
	inp = {
		"obs": trace.get("obs", {}),
		"plan": trace.get("plan", {}),
		"result": trace.get("result", {}),
	}
	# Heuristic target YAML (bootstrap)
	status = (trace.get("result") or {}).get("status", "error")
	if status == "ok":
		success_prob, risk = 0.8, 0.2
	else:
		success_prob, risk = 0.3, 0.6
	tgt = (
		f"success_prob: {success_prob}\n"
		f"risk: {risk}\n"
		f"critique: minimal bootstrap label\n"
	)
	return {"input": json.dumps(inp, ensure_ascii=False), "output": tgt}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("src", nargs="?", default=str(root / "data" / "traces" / "web_dom.verified.jsonl"))
	ap.add_argument("dst", nargs="?", default=str(root / "data" / "skills" / "verifier_ds.jsonl"))
	args = ap.parse_args()

	src = Path(args.src)
	dst = Path(args.dst)
	dst.parent.mkdir(parents=True, exist_ok=True)

	n = 0
	with src.open("r", encoding="utf-8") as fin, dst.open("w", encoding="utf-8") as fout:
		for line in fin:
			line = line.strip()
			if not line:
				continue
			try:


	# Compact input: obs+plan+result only
	inp = {
		"obs": trace.get("obs", {}),
		"plan": trace.get("plan", {}),
		"result": trace.get("result", {}),
	}
	# Heuristic target YAML (bootstrap)
	status = (trace.get("result") or {}).get("status", "error")
	if status == "ok":
		success_prob, risk = 0.8, 0.2
	else:
		success_prob, risk = 0.3, 0.6
	tgt = (
		f"success_prob: {success_prob}\n"
		f"risk: {risk}\n"
		f"critique: minimal bootstrap label\n"
	)
	return {"input": json.dumps(inp, ensure_ascii=False), "output": tgt}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("src", nargs="?", default=str(root / "data" / "traces" / "web_dom.verified.jsonl"))
	ap.add_argument("dst", nargs="?", default=str(root / "data" / "skills" / "verifier_ds.jsonl"))
	args = ap.parse_args()

	src = Path(args.src)
	dst = Path(args.dst)
	dst.parent.mkdir(parents=True, exist_ok=True)

	n = 0
	with src.open("r", encoding="utf-8") as fin, dst.open("w", encoding="utf-8") as fout:
		for line in fin:
			line = line.strip()
			if not line:
				continue
			try:
				tr: Dict = json.loads(line)
			except Exception:
				continue
			ex = build_example(tr)
			fout.write(json.dumps(ex, ensure_ascii=False) + "\n")
			n += 1
	print(f"Wrote {n} examples -> {dst}")
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import hashlib
import json
from pathlib import Path
from typing import Dict, Tuple

def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def key_for_verifier(obj: Dict) -> str:
	obs = obj.get("obs", {}) if isinstance(obj.get("obs"), dict) else {}
	plan = obj.get("plan", {}) if isinstance(obj.get("plan"), dict) else {}
	return json.dumps({"obs": obs, "plan": plan}, ensure_ascii=False, sort_keys=True)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = int(hashlib.sha256(key_for_verifier(obj).encode("utf-8")).hexdigest(), 16)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--src", default=str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"))
	ap.add_argument("--train", default=str(root / "data" / "splits" / "verifier_train.jsonl"))
	ap.add_argument("--eval", default=str(root / "data" / "splits" / "verifier_eval.jsonl"))
	ap.add_argument("--ratio", type=float, default=0.8)
	args = ap.parse_args()

	n_tr, n_ev = write_split(Path(args.src), Path(args.train), Path(args.eval), max(0.0, min(1.0, float(args.ratio))))
	print(json.dumps({"train": n_tr, "eval": n_ev}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import hashlib
import json
from pathlib import Path
from typing import Dict, Tuple

def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def key_for_verifier(obj: Dict) -> str:
	obs = obj.get("obs", {}) if isinstance(obj.get("obs"), dict) else {}
	plan = obj.get("plan", {}) if isinstance(obj.get("plan"), dict) else {}
	return json.dumps({"obs": obs, "plan": plan}, ensure_ascii=False, sort_keys=True)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = int(hashlib.sha256(key_for_verifier(obj).encode("utf-8")).hexdigest(), 16)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)


import hashlib
import json
from pathlib import Path
from typing import Dict, Tuple

def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def key_for_verifier(obj: Dict) -> str:
	obs = obj.get("obs", {}) if isinstance(obj.get("obs"), dict) else {}
	plan = obj.get("plan", {}) if isinstance(obj.get("plan"), dict) else {}
	return json.dumps({"obs": obs, "plan": plan}, ensure_ascii=False, sort_keys=True)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = int(hashlib.sha256(key_for_verifier(obj).encode("utf-8")).hexdigest(), 16)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev


def iter_jsonl(path: Path):
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def key_for_verifier(obj: Dict) -> str:
	obs = obj.get("obs", {}) if isinstance(obj.get("obs"), dict) else {}
	plan = obj.get("plan", {}) if isinstance(obj.get("plan"), dict) else {}
	return json.dumps({"obs": obs, "plan": plan}, ensure_ascii=False, sort_keys=True)

def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = int(hashlib.sha256(key_for_verifier(obj).encode("utf-8")).hexdigest(), 16)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--src", default=str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"))
	ap.add_argument("--train", default=str(root / "data" / "splits" / "verifier_train.jsonl"))
	ap.add_argument("--eval", default=str(root / "data" / "splits" / "verifier_eval.jsonl"))
	ap.add_argument("--ratio", type=float, default=0.8)
	args = ap.parse_args()

	n_tr, n_ev = write_split(Path(args.src), Path(args.train), Path(args.eval), max(0.0, min(1.0, float(args.ratio))))
	print(json.dumps({"train": n_tr, "eval": n_ev}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


def write_split(src: Path, out_train: Path, out_eval: Path, ratio: float) -> Tuple[int, int]:
	out_train.parent.mkdir(parents=True, exist_ok=True)
	out_eval.parent.mkdir(parents=True, exist_ok=True)
	n_tr = 0
	n_ev = 0
	with out_train.open("w", encoding="utf-8") as ftr, out_eval.open("w", encoding="utf-8") as fev:
		for obj in iter_jsonl(src):
			h = int(hashlib.sha256(key_for_verifier(obj).encode("utf-8")).hexdigest(), 16)
			p = (h % 1000) / 1000.0
			line = json.dumps(obj, ensure_ascii=False) + "\n"
			if p < ratio:
				ftr.write(line)
				n_tr += 1
			else:
				fev.write(line)
				n_ev += 1
	return n_tr, n_ev

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--src", default=str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"))
	ap.add_argument("--train", default=str(root / "data" / "splits" / "verifier_train.jsonl"))
	ap.add_argument("--eval", default=str(root / "data" / "splits" / "verifier_eval.jsonl"))
	ap.add_argument("--ratio", type=float, default=0.8)
	args = ap.parse_args()

	n_tr, n_ev = write_split(Path(args.src), Path(args.train), Path(args.eval), max(0.0, min(1.0, float(args.ratio))))
	print(json.dumps({"train": n_tr, "eval": n_ev}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


from __future__ import annotations
import logging

import argparse
import json
from pathlib import Path
from typing import Dict, Any

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--out", default=str(root / "data" / "traces" / "practice_ds.jsonl"))
	ap.add_argument("--n", type=int, default=5, help="Number of synthetic practice items")
	ap.add_argument("--seeds", default="1,2,3,4,5", help="Comma-separated integer seeds to vary sampling")
	args = ap.parse_args()

	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)

	# Minimal synthetic practice items: trivial failing tests with gold diff
	# This is a placeholder to scaffold the practice dataset structure.
	written = 0
	seeds = []
	try:
		seeds = [int(s) for s in str(getattr(args, "seeds", "1,2,3,4,5") or "").split(",") if s.strip()]
	except Exception:
		seeds = [1, 2, 3, 4, 5]

	with outp.open("w", encoding="utf-8") as f:
		for i in range(max(1, int(args.n))):
			item: Dict[str, Any] = {
				"obs": {"kind": "code", "tests": ["tests/test_add.py::test_add_basic"], "failure_excerpt": "FAILED tests/test_add.py::test_add_basic - assert add(1,2)==3"},
				"plan": {"intent_summary": "Fix add to return a+b", "primary_path": "app.py"},
				"action": {"tool": "code.patch.apply", "args": {"size": 2, "files": 1}},
				"result": {"status": "ok"},
				"diff_text": (
					"diff --git a/app.py b/app.py\n"
					"index 0000000..0000000 100644\n"
					"--- a/app.py\n"
					"+++ b/app.py\n"
					"@@ -1,1 +1,1 @@\n"
					"-def add(a, b): return a-b\n"
					"+def add(a, b): return a+b\n"
				),
			}
			f.write(json.dumps(item, ensure_ascii=False) + "\n")
			written += 1

	print(json.dumps({"ok": True, "items": int(written), "out": str(outp)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())

import argparse
import json
import difflib
import re
from pathlib import Path
from typing import Any, Dict, List, Tuple

def _find_minus_returns(text: str) -> List[Tuple[int, str, str]]:
	"""Find lines with a simple subtraction in return statements and propose replacement with addition.

	Returns list of tuples (line_index, old_line, new_line).
	"""
	out: List[Tuple[int, str, str]] = []
	for i, line in enumerate(text.splitlines()):
		# match 'return <expr> - <expr>' with a minimal heuristic
		if re.search(r"\breturn\b.+-.+", line):
			new_line = re.sub(r"-", "+", line, count=1)
			if new_line != line:
				out.append((i, line, new_line))
	return out

def _make_unified_diff(path: str, before: str, after: str) -> str:
	a_lines = before.splitlines(keepends=True)
	b_lines = after.splitlines(keepends=True)
	# difflib.unified_diff emits ---/+++ headers automatically; wrap with git-style header for apply convenience
	diff_body = list(difflib.unified_diff(a_lines, b_lines, fromfile=f"a/{path}", tofile=f"b/{path}", lineterm=""))
	header = [
		f"diff --git a/{path} b/{path}",
		"index 0000000..0000000 100644",
	]
	return "\n".join(header + diff_body) + "\n"

def _build_row(repo: Path, rel_path: Path, before: str, after: str) -> Dict[str, Any]:
	rel = rel_path.as_posix()
	diff_text = _make_unified_diff(rel, before, after)
	ctx_files = [{"path": rel, "text": before[:8000]}]
	intent = {
		"intent_summary": f"Fix arithmetic bug in {rel}",
		"primary_path": rel,
		"allow_paths": [rel, "tests/**/*.py"],
		"block_paths": ["data/**", "models/**", "dist/**", "build/**", "node_modules/**"],
		"budgets": {"max_files": 1, "max_added": 200, "max_deleted": 200},
		"target_symbols": [],
	}
	# Minimal failure summary template for practice; downstream consumers can ignore or replace
	failure_summary = f"- tests/test_{rel_path.stem}.py::test_behavior"
	return {
		"failure_summary": failure_summary,
		"ctx_files": ctx_files,
		"src_candidates": [rel],
		"src_files": ctx_files,
		"code_index": {},
		"lint_summary": "",
		"intent": intent,
		"diff_text": diff_text,
		"churn": {"added": after.count("\n") - before.count("\n"), "deleted": 0},
		"labels": {
			"candidate_size": int(sum(1 for l in diff_text.splitlines() if l.startswith("+") or l.startswith("-"))),
			"candidate_files": 1,
			"plausible_fix": True,
			"touches_primary": True,
			"applied_ok": False,
		},
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--repos", nargs="*", default=[str(root)])
	ap.add_argument("--out", default=str(root / "data" / "traces" / "practice.jsonl"))
	ap.add_argument("--limit", type=int, default=100)
	args = ap.parse_args()

	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)

	n = 0
	with outp.open("w", encoding="utf-8") as w:
		for repo_str in (args.repos or []):
			repo = Path(repo_str).resolve()
			if not repo.exists():
				continue
			for fp in repo.rglob("*.py"):
				try:
					# Skip typical non-source dirs
					if any(part in {".git", "venv", ".venv", "node_modules", "dist", "build", "models", "data"} for part in fp.parts):
						continue
					before = fp.read_text(encoding="utf-8")
				except Exception:
					continue
				candidates = _find_minus_returns(before)
				if not candidates:
					continue
				# Apply the first suggested line edit to build an after state
				idx, old_line, new_line = candidates[0]
				lines = before.splitlines()
				lines[idx] = new_line.rstrip("\n")
				after = "\n".join(lines) + ("\n" if before.endswith("\n") else "")
				row = _build_row(repo, fp.relative_to(repo), before, after)
				try:
					w.write(json.dumps(row, ensure_ascii=False) + "\n")
					n += 1
				except Exception:
					continue
				if int(args.limit or 0) > 0 and n >= int(args.limit):
					break
			if int(args.limit or 0) > 0 and n >= int(args.limit):
				break

	print(json.dumps({"ok": True, "items": int(n), "out": str(outp)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


	return {
		"failure_summary": failure_summary,
		"ctx_files": ctx_files,
		"src_candidates": [rel],
		"src_files": ctx_files,
		"code_index": {},
		"lint_summary": "",
		"intent": intent,
		"diff_text": diff_text,
		"churn": {"added": after.count("\n") - before.count("\n"), "deleted": 0},
		"labels": {
			"candidate_size": int(sum(1 for l in diff_text.splitlines() if l.startswith("+") or l.startswith("-"))),
			"candidate_files": 1,
			"plausible_fix": True,
			"touches_primary": True,
			"applied_ok": False,
		},
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--repos", nargs="*", default=[str(root)])
	ap.add_argument("--out", default=str(root / "data" / "traces" / "practice.jsonl"))
	ap.add_argument("--limit", type=int, default=100)
	args = ap.parse_args()

	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)

	n = 0
	with outp.open("w", encoding="utf-8") as w:
		for repo_str in (args.repos or []):
			repo = Path(repo_str).resolve()
			if not repo.exists():
				continue
			for fp in repo.rglob("*.py"):
				try:
					# Skip typical non-source dirs
					if any(part in {".git", "venv", ".venv", "node_modules", "dist", "build", "models", "data"} for part in fp.parts):
						continue
					before = fp.read_text(encoding="utf-8")
				except Exception:
					continue
				candidates = _find_minus_returns(before)
				if not candidates:
					continue
				# Apply the first suggested line edit to build an after state
				idx, old_line, new_line = candidates[0]
				lines = before.splitlines()
				lines[idx] = new_line.rstrip("\n")
				after = "\n".join(lines) + ("\n" if before.endswith("\n") else "")
				row = _build_row(repo, fp.relative_to(repo), before, after)
				try:
					w.write(json.dumps(row, ensure_ascii=False) + "\n")
					n += 1
				except Exception:
					continue
				if int(args.limit or 0) > 0 and n >= int(args.limit):
					break
			if int(args.limit or 0) > 0 and n >= int(args.limit):
				break

	print(json.dumps({"ok": True, "items": int(n), "out": str(outp)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


			}
			f.write(json.dumps(item, ensure_ascii=False) + "\n")
			written += 1

	print(json.dumps({"ok": True, "items": int(written), "out": str(outp)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())

import argparse
import json
import difflib
import re
from pathlib import Path
from typing import Any, Dict, List, Tuple

def _find_minus_returns(text: str) -> List[Tuple[int, str, str]]:
	"""Find lines with a simple subtraction in return statements and propose replacement with addition.

	Returns list of tuples (line_index, old_line, new_line).
	"""
	out: List[Tuple[int, str, str]] = []
	for i, line in enumerate(text.splitlines()):
		# match 'return <expr> - <expr>' with a minimal heuristic
		if re.search(r"\breturn\b.+-.+", line):
			new_line = re.sub(r"-", "+", line, count=1)
			if new_line != line:
				out.append((i, line, new_line))
	return out

def _make_unified_diff(path: str, before: str, after: str) -> str:
	a_lines = before.splitlines(keepends=True)
	b_lines = after.splitlines(keepends=True)
	# difflib.unified_diff emits ---/+++ headers automatically; wrap with git-style header for apply convenience
	diff_body = list(difflib.unified_diff(a_lines, b_lines, fromfile=f"a/{path}", tofile=f"b/{path}", lineterm=""))
	header = [
		f"diff --git a/{path} b/{path}",
		"index 0000000..0000000 100644",
	]
	return "\n".join(header + diff_body) + "\n"

def _build_row(repo: Path, rel_path: Path, before: str, after: str) -> Dict[str, Any]:
	rel = rel_path.as_posix()
	diff_text = _make_unified_diff(rel, before, after)
	ctx_files = [{"path": rel, "text": before[:8000]}]
	intent = {
		"intent_summary": f"Fix arithmetic bug in {rel}",


import re
from pathlib import Path
from typing import Any, Dict, List, Tuple

def _find_minus_returns(text: str) -> List[Tuple[int, str, str]]:
	"""Find lines with a simple subtraction in return statements and propose replacement with addition.

	Returns list of tuples (line_index, old_line, new_line).
	"""
	out: List[Tuple[int, str, str]] = []
	for i, line in enumerate(text.splitlines()):
		# match 'return <expr> - <expr>' with a minimal heuristic
		if re.search(r"\breturn\b.+-.+", line):
			new_line = re.sub(r"-", "+", line, count=1)
			if new_line != line:
				out.append((i, line, new_line))
	return out

def _make_unified_diff(path: str, before: str, after: str) -> str:
	a_lines = before.splitlines(keepends=True)
	b_lines = after.splitlines(keepends=True)
	# difflib.unified_diff emits ---/+++ headers automatically; wrap with git-style header for apply convenience
	diff_body = list(difflib.unified_diff(a_lines, b_lines, fromfile=f"a/{path}", tofile=f"b/{path}", lineterm=""))
	header = [
		f"diff --git a/{path} b/{path}",
		"index 0000000..0000000 100644",
	]
	return "\n".join(header + diff_body) + "\n"

def _build_row(repo: Path, rel_path: Path, before: str, after: str) -> Dict[str, Any]:
	rel = rel_path.as_posix()
	diff_text = _make_unified_diff(rel, before, after)
	ctx_files = [{"path": rel, "text": before[:8000]}]
	intent = {
		"intent_summary": f"Fix arithmetic bug in {rel}",
		"primary_path": rel,
		"allow_paths": [rel, "tests/**/*.py"],
		"block_paths": ["data/**", "models/**", "dist/**", "build/**", "node_modules/**"],
		"budgets": {"max_files": 1, "max_added": 200, "max_deleted": 200},
		"target_symbols": [],
	}
	# Minimal failure summary template for practice; downstream consumers can ignore or replace
	failure_summary = f"- tests/test_{rel_path.stem}.py::test_behavior"
	return {
		"failure_summary": failure_summary,
		"ctx_files": ctx_files,
		"src_candidates": [rel],


		# match 'return <expr> - <expr>' with a minimal heuristic
		if re.search(r"\breturn\b.+-.+", line):
			new_line = re.sub(r"-", "+", line, count=1)
			if new_line != line:
				out.append((i, line, new_line))
	return out

def _make_unified_diff(path: str, before: str, after: str) -> str:
	a_lines = before.splitlines(keepends=True)
	b_lines = after.splitlines(keepends=True)
	# difflib.unified_diff emits ---/+++ headers automatically; wrap with git-style header for apply convenience
	diff_body = list(difflib.unified_diff(a_lines, b_lines, fromfile=f"a/{path}", tofile=f"b/{path}", lineterm=""))
	header = [
		f"diff --git a/{path} b/{path}",
		"index 0000000..0000000 100644",
	]
	return "\n".join(header + diff_body) + "\n"

def _build_row(repo: Path, rel_path: Path, before: str, after: str) -> Dict[str, Any]:
	rel = rel_path.as_posix()
	diff_text = _make_unified_diff(rel, before, after)
	ctx_files = [{"path": rel, "text": before[:8000]}]
	intent = {
		"intent_summary": f"Fix arithmetic bug in {rel}",
		"primary_path": rel,
		"allow_paths": [rel, "tests/**/*.py"],
		"block_paths": ["data/**", "models/**", "dist/**", "build/**", "node_modules/**"],
		"budgets": {"max_files": 1, "max_added": 200, "max_deleted": 200},
		"target_symbols": [],
	}
	# Minimal failure summary template for practice; downstream consumers can ignore or replace
	failure_summary = f"- tests/test_{rel_path.stem}.py::test_behavior"
	return {
		"failure_summary": failure_summary,
		"ctx_files": ctx_files,
		"src_candidates": [rel],
		"src_files": ctx_files,
		"code_index": {},
		"lint_summary": "",
		"intent": intent,
		"diff_text": diff_text,
		"churn": {"added": after.count("\n") - before.count("\n"), "deleted": 0},
		"labels": {
			"candidate_size": int(sum(1 for l in diff_text.splitlines() if l.startswith("+") or l.startswith("-"))),
			"candidate_files": 1,
			"plausible_fix": True,
			"touches_primary": True,
			"applied_ok": False,
		},
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--repos", nargs="*", default=[str(root)])
	ap.add_argument("--out", default=str(root / "data" / "traces" / "practice.jsonl"))
	ap.add_argument("--limit", type=int, default=100)
	args = ap.parse_args()

	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)

	n = 0
	with outp.open("w", encoding="utf-8") as w:
		for repo_str in (args.repos or []):
			repo = Path(repo_str).resolve()
			if not repo.exists():
				continue
			for fp in repo.rglob("*.py"):


import logging
import argparse
import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

def iter_jsonl(path: Path) -> Iterable[Dict[str, Any]]:
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def is_near_miss(trace: Dict[str, Any]) -> Tuple[bool, str]:
	# Domain and status
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	domain = str(obs.get("kind", "")).lower()
	result = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	status = str(result.get("status", "")).lower()
	# Basic success flag
	ok = status == "ok"
	# Verifier risk if present
	risk = None
	try:
		crit = trace.get("critique", {}) or {}
		risk = float(crit.get("risk", 0.5))
	except Exception:
		risk = None
	# WM prior signals if present
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	succ_prob = None
	try:
		succ_prob = float(wm_prior.get("success_prob", 0.5)) if wm_prior else None
	except Exception:
		succ_prob = None
	# Planner candidates self-eval
	pc = trace.get("planner_candidates") if isinstance(trace.get("planner_candidates"), dict) else {}
	self_evals: List[float] = []
	try:
		self_evals = [float(x) for x in (pc.get("self_eval") or [])]
	except Exception:
		self_evals = []
	se_best = max(self_evals) if self_evals else None

	# Heuristics for near-miss classification
	# 1) Not successful, but verifier risk indicates ambiguous (0.4..0.7)
	if not ok and risk is not None and 0.4 <= risk <= 0.7:
		return True, "verifier_ambiguous"
	# 2) Blocked by WM budgets but success_prob close to threshold (0.4..0.6)
	if status == "blocked" and succ_prob is not None and 0.4 <= succ_prob <= 0.6:
		return True, "wm_borderline"
	# 3) High self-eval from planner candidates but final status not ok
	if not ok and se_best is not None and se_best >= 0.6:
		return True, "planner_self_eval_high"
	# 4) Router uncertainty (if present)
	router = trace.get("router") if isinstance(trace.get("router"), dict) else {}
	prob = router.get("prob") if isinstance(router.get("prob"), (int, float)) else None
	if not ok and prob is not None:
		try:
			p = float(prob)
			if 0.4 <= p <= 0.6:
				return True, "router_uncertain"
		except Exception:
			pass
	# 5) DOM: net blocked counts indicate policy blocks; consider for replay
	if domain == "dom" and not ok and int(trace.get("net_blocked", 0) or 0) > 0:
		return True, "network_blocked"
	return False, ""

def to_replay_record(trace: Dict[str, Any], reason: str) -> Dict[str, Any]:
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	plan = trace.get("plan", {}) if isinstance(trace.get("plan"), dict) else {}
	action = trace.get("action", {}) if isinstance(trace.get("action"), dict) else {}
	crit = trace.get("critique", {}) if isinstance(trace.get("critique"), dict) else {}
	res = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	return {
		"domain": obs.get("kind"),
		"obs": obs,
		"plan": plan,
		"action": action,
		"result": res,
		"risk": crit.get("risk"),
		"wm_prior": wm_prior,
		"reason": reason,
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--inputs", nargs="*", default=[
		str(root / "data" / "traces" / "loop_run.jsonl"),
		str(root / "data" / "traces" / "loop_web_dom.jsonl"),
		str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"),
	])
	ap.add_argument("--out", default=str(root / "data" / "replay" / "near_miss.jsonl"))
	args = ap.parse_args()

	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)

	count_in = 0
	count_near = 0
	for raw in args.inputs:
		p = Path(raw)
		for tr in iter_jsonl(p):
			count_in += 1
			ok, reason = is_near_miss(tr)
			if ok:
				rec = to_replay_record(tr, reason)
				with out.open("a", encoding="utf-8") as f:
					f.write(json.dumps(rec, ensure_ascii=False) + "\n")
				count_near += 1
	print(json.dumps({"inputs": len(args.inputs), "seen": count_in, "near_miss": count_near, "out": str(out)}))
	return 0

if __name__ == "__main__":
	import sys
	sys.exit(main())


import logging
import argparse
import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

def iter_jsonl(path: Path) -> Iterable[Dict[str, Any]]:
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def is_near_miss(trace: Dict[str, Any]) -> Tuple[bool, str]:
	# Domain and status
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	domain = str(obs.get("kind", "")).lower()
	result = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	status = str(result.get("status", "")).lower()
	# Basic success flag
	ok = status == "ok"
	# Verifier risk if present
	risk = None
	try:
		crit = trace.get("critique", {}) or {}
		risk = float(crit.get("risk", 0.5))
	except Exception:
		risk = None
	# WM prior signals if present
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	succ_prob = None


import argparse
import json
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple

def iter_jsonl(path: Path) -> Iterable[Dict[str, Any]]:
	if not path.exists():
		return
	with path.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				yield json.loads(line)
			except Exception:
				continue

def is_near_miss(trace: Dict[str, Any]) -> Tuple[bool, str]:
	# Domain and status
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	domain = str(obs.get("kind", "")).lower()
	result = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	status = str(result.get("status", "")).lower()
	# Basic success flag
	ok = status == "ok"
	# Verifier risk if present
	risk = None
	try:
		crit = trace.get("critique", {}) or {}
		risk = float(crit.get("risk", 0.5))
	except Exception:
		risk = None
	# WM prior signals if present
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	succ_prob = None
	try:
		succ_prob = float(wm_prior.get("success_prob", 0.5)) if wm_prior else None
	except Exception:
		succ_prob = None
	# Planner candidates self-eval
	pc = trace.get("planner_candidates") if isinstance(trace.get("planner_candidates"), dict) else {}
	self_evals: List[float] = []
	try:
		self_evals = [float(x) for x in (pc.get("self_eval") or [])]
	except Exception:
		self_evals = []
	se_best = max(self_evals) if self_evals else None

	# Heuristics for near-miss classification
	# 1) Not successful, but verifier risk indicates ambiguous (0.4..0.7)
	if not ok and risk is not None and 0.4 <= risk <= 0.7:
		return True, "verifier_ambiguous"
	# 2) Blocked by WM budgets but success_prob close to threshold (0.4..0.6)
	if status == "blocked" and succ_prob is not None and 0.4 <= succ_prob <= 0.6:
		return True, "wm_borderline"
	# 3) High self-eval from planner candidates but final status not ok
	if not ok and se_best is not None and se_best >= 0.6:
		return True, "planner_self_eval_high"
	# 4) Router uncertainty (if present)
	router = trace.get("router") if isinstance(trace.get("router"), dict) else {}
	prob = router.get("prob") if isinstance(router.get("prob"), (int, float)) else None
	if not ok and prob is not None:
		try:
			p = float(prob)
			if 0.4 <= p <= 0.6:
				return True, "router_uncertain"
		except Exception:
			pass
	# 5) DOM: net blocked counts indicate policy blocks; consider for replay
	if domain == "dom" and not ok and int(trace.get("net_blocked", 0) or 0) > 0:
		return True, "network_blocked"
	return False, ""

def to_replay_record(trace: Dict[str, Any], reason: str) -> Dict[str, Any]:
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	plan = trace.get("plan", {}) if isinstance(trace.get("plan"), dict) else {}
	action = trace.get("action", {}) if isinstance(trace.get("action"), dict) else {}
	crit = trace.get("critique", {}) if isinstance(trace.get("critique"), dict) else {}
	res = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	return {
		"domain": obs.get("kind"),
		"obs": obs,
		"plan": plan,
		"action": action,
		"result": res,
		"risk": crit.get("risk"),
		"wm_prior": wm_prior,
		"reason": reason,
	}


		return True, "wm_borderline"
	# 3) High self-eval from planner candidates but final status not ok
	if not ok and se_best is not None and se_best >= 0.6:
		return True, "planner_self_eval_high"
	# 4) Router uncertainty (if present)
	router = trace.get("router") if isinstance(trace.get("router"), dict) else {}
	prob = router.get("prob") if isinstance(router.get("prob"), (int, float)) else None
	if not ok and prob is not None:
		try:
			p = float(prob)
			if 0.4 <= p <= 0.6:
				return True, "router_uncertain"
		except Exception:
			pass
	# 5) DOM: net blocked counts indicate policy blocks; consider for replay
	if domain == "dom" and not ok and int(trace.get("net_blocked", 0) or 0) > 0:
		return True, "network_blocked"
	return False, ""

def to_replay_record(trace: Dict[str, Any], reason: str) -> Dict[str, Any]:
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	plan = trace.get("plan", {}) if isinstance(trace.get("plan"), dict) else {}
	action = trace.get("action", {}) if isinstance(trace.get("action"), dict) else {}
	crit = trace.get("critique", {}) if isinstance(trace.get("critique"), dict) else {}
	res = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	return {
		"domain": obs.get("kind"),
		"obs": obs,
		"plan": plan,
		"action": action,
		"result": res,
		"risk": crit.get("risk"),
		"wm_prior": wm_prior,
		"reason": reason,
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--inputs", nargs="*", default=[
		str(root / "data" / "traces" / "loop_run.jsonl"),
		str(root / "data" / "traces" / "loop_web_dom.jsonl"),
		str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"),
	])
	ap.add_argument("--out", default=str(root / "data" / "replay" / "near_miss.jsonl"))
	args = ap.parse_args()

	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)

	count_in = 0
	count_near = 0
	for raw in args.inputs:
		p = Path(raw)


def to_replay_record(trace: Dict[str, Any], reason: str) -> Dict[str, Any]:
	obs = trace.get("obs", {}) if isinstance(trace.get("obs"), dict) else {}
	plan = trace.get("plan", {}) if isinstance(trace.get("plan"), dict) else {}
	action = trace.get("action", {}) if isinstance(trace.get("action"), dict) else {}
	crit = trace.get("critique", {}) if isinstance(trace.get("critique"), dict) else {}
	res = trace.get("result", {}) if isinstance(trace.get("result"), dict) else {}
	wm_prior = trace.get("wm_prior", {}) if isinstance(trace.get("wm_prior"), dict) else {}
	return {
		"domain": obs.get("kind"),
		"obs": obs,
		"plan": plan,
		"action": action,
		"result": res,
		"risk": crit.get("risk"),
		"wm_prior": wm_prior,
		"reason": reason,
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--inputs", nargs="*", default=[
		str(root / "data" / "traces" / "loop_run.jsonl"),
		str(root / "data" / "traces" / "loop_web_dom.jsonl"),
		str(root / "data" / "traces" / "seed_os_cli.verified.llm.jsonl"),
	])
	ap.add_argument("--out", default=str(root / "data" / "replay" / "near_miss.jsonl"))
	args = ap.parse_args()

	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)

	count_in = 0
	count_near = 0
	for raw in args.inputs:
		p = Path(raw)
		for tr in iter_jsonl(p):
			count_in += 1
			ok, reason = is_near_miss(tr)
			if ok:
				rec = to_replay_record(tr, reason)
				with out.open("a", encoding="utf-8") as f:
					f.write(json.dumps(rec, ensure_ascii=False) + "\n")
				count_near += 1
	print(json.dumps({"inputs": len(args.inputs), "seen": count_in, "near_miss": count_near, "out": str(out)}))
	return 0

if __name__ == "__main__":
	import sys
	sys.exit(main())


import logging
import argparse
import json
from pathlib import Path
from typing import List

def discover_verified_traces(data_root: Path) -> List[str]:
	cands: List[str] = []
	for name in ["seed_os_cli.verified.jsonl", "seed_os_cli.verified.llm.jsonl", "web_dom.verified.jsonl"]:
		p = data_root / "traces" / name
		if p.exists():
			cands.append(str(p))
	return cands

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--data-root", default=str(root / "data"))
	ap.add_argument("--out", default=str(root / "models" / "memory"))
	ap.add_argument("--extra", nargs='*', default=[], help="Additional JSONL files to ingest")
	args = ap.parse_args()

	from agi_dw.core.memory.episodic import EpisodicMemory  # type: ignore

	data_root = Path(args.data_root)
	out_dir = Path(args.out)
	files = discover_verified_traces(data_root)
	files.extend([str(Path(p)) for p in (args.extra or []) if Path(p).exists()])
	mem = EpisodicMemory()
	# Note: currently ingest all verified traces; consider filtering to status==ok later
	n = mem.fit_from_jsonl(files)
	out_dir.mkdir(parents=True, exist_ok=True)
	mem.save(str(out_dir))
	print(json.dumps({"built": n, "out": str(out_dir)}))
	return 0

if __name__ == "__main__":
	raise SystemExit(main())


import logging
import argparse
import json
from pathlib import Path
from typing import List

def discover_verified_traces(data_root: Path) -> List[str]:
	cands: List[str] = []
	for name in ["seed_os_cli.verified.jsonl", "seed_os_cli.verified.llm.jsonl", "web_dom.verified.jsonl"]:
		p = data_root / "traces" / name
		if p.exists():
			cands.append(str(p))
	return cands

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--data-root", default=str(root / "data"))
	ap.add_argument("--out", default=str(root / "models" / "memory"))
	ap.add_argument("--extra", nargs='*', default=[], help="Additional JSONL files to ingest")
	args = ap.parse_args()

	from agi_dw.core.memory.episodic import EpisodicMemory  # type: ignore

	data_root = Path(args.data_root)
	out_dir = Path(args.out)
	files = discover_verified_traces(data_root)
	files.extend([str(Path(p)) for p in (args.extra or []) if Path(p).exists()])
	mem = EpisodicMemory()
	# Note: currently ingest all verified traces; consider filtering to status==ok later
	n = mem.fit_from_jsonl(files)
	out_dir.mkdir(parents=True, exist_ok=True)


import logging
import argparse
import json
from pathlib import Path
from typing import Any, Dict

from agi_dw.bench.web_dom.runner import fetch_text
from agi_dw.bench.common.trace import build_trace, write_jsonl
from agi_dw.core.planner.service import PlannerConfig, VerifierConfig, WMConfig, ContextAugment, plan_with_context
from agi_dw.core.utils.prompt_logger import get_prompt_logger  # type: ignore
from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, RouterVerifierConfig, WMPriorConfig, WMScreenConfig, RepairConfig, select_action
from agi_dw.core.verifier.llm_verifier import verify_trace_snippet
from agi_dw.core.memory.service import match_skill_action
from datetime import datetime
from agi_dw.core.ops.tracing import trace_span, meter_cost  # type: ignore

def main() -> int:
	parser = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	parser.add_argument("--url", default="https://www.iana.org/domains/reserved")
	parser.add_argument("--selector", default="h1")
	parser.add_argument("--out", default=str(root / "data" / "traces" / "loop_web_dom.jsonl"))
	parser.add_argument("--planner-backend", choices=["hf"], default="hf")
	parser.add_argument("--planner-model", default="meta-llama/Llama-3.2-3B")
	parser.add_argument("--planner-adapter", default=None)
	parser.add_argument("--planner-adapter-bank", default=None, help="Name of adapter bank to use for planner (skills registry)")
	parser.add_argument("--planner-structured", choices=["none", "json"], default="none")
	parser.add_argument("--verifier-backend", choices=["hf"], default="hf")
	parser.add_argument("--verifier-model", default="meta-llama/Llama-3.2-3B")
	parser.add_argument("--verifier-adapter", default=None)
	parser.add_argument("--verifier-adapter-bank", default=None, help="Name of adapter bank to use for verifier (skills registry)")
	parser.add_argument("--verifier-structured", choices=["none", "json"], default="json")
	parser.add_argument("--calibrate-verifier", action="store_true", help="Apply isotonic calibration to verifier risk")
	parser.add_argument("--calib-model", default=str(root / "models" / "verifier_calib" / "calib.joblib"))
	parser.add_argument("--timeout", type=int, default=30)
	parser.add_argument("--il", default=str(root / "data" / "skills" / "actuator_il_dom.jsonl"))
	parser.add_argument("--log-prompts", action="store_true")
	parser.add_argument("--strict-verify", action="store_true")
	parser.add_argument("--actuator", choices=["nn", "t5", "router"], default="t5")
	parser.add_argument("--t5-model", default=str(root / "models" / "actuator_dom_t5"))
	parser.add_argument("--dom-structured", action="store_true", help="Enable YAML/JSON structured decoding for DOM T5 actuator")
	parser.add_argument("--auto-t5", action="store_true")
	parser.add_argument("--learned-router", action="store_true", help="Enable learned router for DOM actuator selection")
	parser.add_argument("--router-model", default=str(root / "models" / "router" / "router.joblib"))
	parser.add_argument("--router-threshold", type=float, default=0.5, help="Threshold on P(success) to pick T5 over NN")
	parser.add_argument("--log-router", action="store_true")
	parser.add_argument("--use-memory", action="store_true")
	parser.add_argument("--mem-path", default=str(root / "models" / "memory"))
	parser.add_argument("--mem-topk", type=int, default=3)
	parser.add_argument("--mem-recency", type=float, default=0.0, help="Recency weighting [0..1] when ranking memory hits")
	parser.add_argument("--mem-query", default=None, help="Optional explicit memory query text to override default obs-based query")
	parser.add_argument("--wm-prior", action="store_true", help="Compute and log WM prior scores")
	parser.add_argument("--wm-model", default=str(root / "models" / "wm_mlp" / "wm_mlp.joblib"))
	parser.add_argument("--wm-screen", action="store_true", help="Use WM prior to screen between actuators (pick lower-risk)")
	parser.add_argument("--wm-threshold", type=float, default=0.7, help="Consider alternative if WM risk >= threshold")
	parser.add_argument("--wm-safety", action="store_true", help="Enforce WM safety budget: block actions with risk above max")
	parser.add_argument("--wm-max-risk", type=float, default=0.85, help="Max allowed WM risk before blocking [0..1]")
	parser.add_argument("--wm-min-success", type=float, default=0.0, help="If >0 and WM prior enabled, block action if success_prob < threshold")
	parser.add_argument("--prefer-obs-args", action="store_true", help="Force action url/selector to match provided obs/meta")
	parser.add_argument("--gold-selectors", nargs='*', default=[], help="Additional acceptable CSS selectors for success (multi-gold)")
	parser.add_argument("--require-approval", action="store_true", help="Require human approval for high-risk actions (verifier-gated)")
	parser.add_argument("--approval-threshold", type=float, default=0.8, help="Risk threshold to trigger approval checkpoint [0..1]")
	parser.add_argument("--approval-dir", default=str(root / "data" / "approvals"))
	parser.add_argument("--planner-candidates", type=int, default=1, help="If >1, generate multiple plans and pick by lowest verifier risk")
	parser.add_argument("--wm-plan-rank", action="store_true", help="Rank planner candidates with WM rollout/prior and pick lowest-risk")
	parser.add_argument("--wm-horizon", type=int, default=1, help="Short-horizon rollout steps for plan ranking")
	parser.add_argument("--planner-tot", action="store_true", help="Use ToT-style candidate generator for plans")
	parser.add_argument("--planner-pref-weights", default=None, help="Path to JSON weights for reranking candidates: {w_verifier, w_wm, w_self}")
	# Optional: include top-k code index candidates in planner observation
	parser.add_argument("--planner-index-k", type=int, default=0, help="If >0, include top-k code index candidates in planner obs")
	parser.add_argument("--planner-index-path", default=str(root / "data" / "sandbox" / "tmp" / "code_index.json"), help="Optional prebuilt code index JSON path")
	# Meta-controller integration
	parser.add_argument("--meta-state", default=str(Path(__file__).resolve().parents[2] / "data" / "sandbox" / "tmp" / "meta_state.json"))
	parser.add_argument("--apply-meta", action="store_true")
	args = parser.parse_args()
	# Apply meta-state if requested
	if bool(getattr(args, "apply_meta", False)):
		try:
			import os as _os, json as _json  # type: ignore
			m = _json.loads(Path(str(args.meta_state)).read_text(encoding="utf-8"))
			_os.environ["PLANNER_TOT"] = "1" if bool(m.get("enable_tot", False)) else "0"
			_os.environ["SAMPLING_TEMPERATURE"] = str(float(m.get("sampling_temperature", 0.2)))
			_os.environ["PLANNER_STEP_BUDGET"] = str(int(m.get("planner_step_budget", 1)))
		except Exception:
			pass
	# Default to strict verify and auto-load adapter when available
	try:
		parser.set_defaults(strict_verify=True)
	except Exception:
		pass
	if args.verifier_backend == "hf" and (args.verifier_adapter is None or args.verifier_adapter == ""):
		try:
			default_adapter = Path(__file__).resolve().parents[1] / "models" / "verifier_qlora"
			if default_adapter.exists():
				args.verifier_adapter = str(default_adapter)
		except Exception:
			pass
	# Flip DOM actuator to YAML-first structured decoding by default
	try:
		args.dom_structured = True
		# Prefer structured planner output (JSON) by default for consistency
		args.planner_structured = "json"
		# Enable WM prior + plan ranking by default (safe no-op if model missing)
		args.wm_prior = True
		args.wm_plan_rank = True
	except Exception:
		pass

	obs = {"kind": "dom", "content": "fetch text via selector", "meta": {"url": args.url, "selector": args.selector}}

    # Memory/index/policy augmentation is handled in planner service
    mem_snippets = []
    mem_query_ms = None
    obs_aug = dict(obs)

	# Plan (consolidated via planner service)
	planner_info = None
	plan = None
	with trace_span("plan", {"backend": str(args.planner_backend), "candidates": int(getattr(args, "planner_candidates", 1) or 1)}):
		pl_cfg = PlannerConfig(
			model=args.planner_model,
			backend=args.planner_backend,
			timeout_sec=args.timeout,
			adapter_dir=args.planner_adapter,
			adapter_bank=getattr(args, "planner_adapter_bank", None),
			structured_mode=str(args.planner_structured),
			candidates=int(getattr(args, "planner_candidates", 1) or 1),
			use_tot=bool(getattr(args, "planner_tot", False)),
			seeded=False,
			pref_weights_path=getattr(args, "planner_pref_weights", None),
		)
		vf_cfg = VerifierConfig(
			model=args.verifier_model,
			backend=args.verifier_backend,
			adapter_dir=args.verifier_adapter,
			adapter_bank=getattr(args, "verifier_adapter_bank", None),
			structured_mode=str(args.verifier_structured),
		)
		wm_cfg = WMConfig(
			enabled=bool(getattr(args, "wm_prior", False)),
			model_path=str(args.wm_model),
			horizon=int(getattr(args, "wm_horizon", 1) or 1),
			plan_rank=bool(getattr(args, "wm_plan_rank", False)),
		)
		ctx_cfg = ContextAugment(
			use_memory=bool(getattr(args, "use_memory", False)),
			mem_path=str(args.mem_path),
			mem_topk=int(getattr(args, "mem_topk", 3) or 3),
			mem_recency=float(getattr(args, "mem_recency", 0.0) or 0.0),
			mem_query=getattr(args, "mem_query", None),
			index_k=int(getattr(args, "planner_index_k", 0) or 0),
			index_path=str(getattr(args, "planner_index_path", "")),
			inject_dom_policy=True,
			inject_cli_policy=False,
			inject_caps=True,
		)
		critic_thr = float(getattr(args, "approval_threshold", 0.8) or 0.8) if int(getattr(args, "planner_candidates", 1) or 1) <= 1 else None
		plan, planner_info, obs_aug, mem_snippets, mem_query_ms = plan_with_context(
			obs_aug,
			"dom",
			pl_cfg,
			vf_cfg,
			wm_cfg,
			ctx_cfg,
			critic_fallback_threshold=critic_thr,
			log_prompts=bool(getattr(args, "log_prompts", False)),
			wm_quick_action_t5_model=str(args.t5_model),
		)

	# Optionally auto-select T5 actuator if a trained model exists
	if args.auto_t5:
		from pathlib import Path as _P
		model_dir = _P(args.t5_model)
		try:
			if model_dir.exists() and any(model_dir.iterdir()):
				args.actuator = "t5"
		except Exception:
			pass

	# Optional early WM prior (before routing) to inform router features
	wm_prior = None
	if args.wm_prior:
		try:
			from agi_dw.core.world_model.service import WorldModelService  # type: ignore
			model_path = Path(args.wm_model)
			if model_path.exists():
				wm = WorldModelService.load_if_exists(model_path)
				wm_prior = wm.predict_prior(obs, plan, action={}) if wm else None
		except Exception:
			wm_prior = None

	# Act (DOM actuator)
	with trace_span("act", {"actuator": str(args.actuator)}):
		act_cfg = ActuatorConfig(
			mode=str(args.actuator),
			t5_model=str(args.t5_model),
			il_path=str(args.il),
			learned_router=bool(getattr(args, "learned_router", False)),
			router_model_path=str(args.router_model),
			router_threshold=float(getattr(args, "router_threshold", 0.5) or 0.5),
			dom_structured=bool(getattr(args, "dom_structured", False)),
		)
		vf_cfg = RouterVerifierConfig(
			model=str(args.verifier_model),
			backend=str(args.verifier_backend),
			adapter_dir=getattr(args, "verifier_adapter", None),
			adapter_bank=getattr(args, "verifier_adapter_bank", None),
			timeout_sec=int(getattr(args, "timeout", 30) or 30),
			structured_mode=str(getattr(args, "verifier_structured", "json")),
		)
		wm_cfg = WMPriorConfig(enabled=bool(getattr(args, "wm_prior", False)), model_path=str(args.wm_model))
		wm_scr = WMScreenConfig(enabled=bool(getattr(args, "wm_screen", False)), threshold=float(getattr(args, "wm_threshold", 0.7) or 0.7))
		repair = RepairConfig(domain="dom", prefer_obs_args=bool(getattr(args, "prefer_obs_args", False)), default_url=str(args.url), default_selector=str(args.selector))
		extra = RouterExtras(domain="dom", wm_prior=wm_prior, task_name=str(getattr(args, "task", "") or ""), log_router=bool(getattr(args, "log_router", False)))
		action, router_decision = select_action(obs, plan, act_cfg, extra, verifier_cfg=vf_cfg, wm_prior_cfg=wm_cfg, wm_screen_cfg=wm_scr, repair_cfg=repair)
		if args.actuator not in ("router", "t5", "nn", "two_head", "template") or not isinstance(action, dict) or not isinstance(action.get("args"), dict):
			action = {"tool": "browser.read", "args": {"url": args.url, "selector": args.selector}}

	# Optional WM screen between T5 and NN candidates
	if args.wm_screen and args.wm_prior:
		try:
			from agi_dw.core.world_model.service import WorldModelService  # type: ignore
			wm_path = Path(args.wm_model)
			if wm_path.exists():
				wm = WorldModelService.load_if_exists(wm_path)
				prior_main = wm.predict_prior(obs, plan, action or {}) if wm else {"risk": 0.5}
				risk_main = float(prior_main.get("risk", 0.5))
				alt_action = None
                # Build alternative via actuator service for consistency
                try:
                    from agi_dw.core.actuator.service import ActuatorConfig, RouterExtras, select_action
                    alt_cfg = ActuatorConfig(mode=("nn" if args.actuator == "t5" else "t5"), t5_model=str(args.t5_model), il_path=str(args.il), dom_structured=bool(getattr(args, "dom_structured", False)))
                    alt_extra = RouterExtras(domain="dom")
                    alt_action, _ = select_action(obs, plan, alt_cfg, alt_extra)
                except Exception:
                    alt_action = {"tool": "browser.read", "args": {"url": args.url, "selector": args.selector}}
				prior_alt = wm.predict_prior(obs, plan, alt_action or {}) if wm else {"risk": 0.5}
				risk_alt = float(prior_alt.get("risk", 0.5))
				if risk_main >= float(args.wm_threshold) and (risk_alt < risk_main):
					action = alt_action or action
					if isinstance(action, dict):
						action["wm_screen"] = {"risk_main": risk_main, "risk_alt": risk_alt, "success_entropy_main": float(prior_main.get("success_entropy", 0.0)), "success_entropy_alt": float(prior_alt.get("success_entropy", 0.0))}
		except Exception:
			pass

    # Try Skill Library match to reuse promoted DOM skills
    try:
        action, adapters = match_skill_action("dom", Path(__file__).resolve().parents[1], obs, action)
        if adapters.get("verifier"):
            args.verifier_adapter = adapters["verifier"]
        if adapters.get("planner"):
            args.planner_adapter = adapters["planner"]
    except Exception:
        pass

	# Optionally override predicted args with obs/meta-provided values
	if args.prefer_obs_args:
		meta = obs.get("meta", {}) if isinstance(obs, dict) else {}
		obs_url = meta.get("url", args.url)
		obs_sel = meta.get("selector", args.selector)
		if not isinstance(action, dict):
			action = {"tool": "browser.read", "args": {"url": obs_url, "selector": obs_sel}}
		else:
			args_d = action.get("args") if isinstance(action.get("args"), dict) else {}
			args_d["url"] = obs_url
			args_d["selector"] = obs_sel
			action["args"] = args_d

# Repair is handled by actuator service; no extra repairs needed here
	url = action.get("args", {}).get("url", args.url)
	selector = action.get("args", {}).get("selector", args.selector)

	# WM safety budget: block execution if WM risk exceeds max
	if args.wm_safety and args.wm_prior:
		try:
			from agi_dw.core.world_model.service import WorldModelService  # type: ignore
			wm_path = Path(args.wm_model)
			if wm_path.exists():
				wm = WorldModelService.load_if_exists(wm_path)
				prior_now = wm.predict_prior(obs, plan, action or {}) if wm else {"risk": 0.5}
				r_now = float(prior_now.get("risk", 0.5))
				if r_now >= float(args.wm_max_risk):
					trace = build_trace(
						task_id="loop-web-dom",
						obs=obs,
						plan=plan,
						action=action,
						result={"dom": "", "status": "blocked"},
						reward={"scalar": 0.0, "components": {"success": 0, "latency": 0, "side_effect": 1}},
						critique={"issues": [], "risk": r_now, "proposal": "wm-safety-budget"},
					)
				try:
					import os as _os
					trace["meta_state"] = {
						"enable_tot": True if _os.environ.get("PLANNER_TOT", "0") == "1" else False,
						"temperature": float(_os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2),
						"step_budget": int(_os.environ.get("PLANNER_STEP_BUDGET", "1") or 1),
					}
				except Exception:
					pass
					trace["wm_prior"] = prior_now
					trace["wm_safety"] = {"max_risk": float(args.wm_max_risk)}
					write_jsonl(args.out, trace)
					print(json.dumps({"status": "blocked", "wm_risk": r_now, "wm_max_risk": float(args.wm_max_risk)}, ensure_ascii=False))
					return 0
		except Exception:
			pass

	# WM success budget: block if success_prob is below min
	if args.wm_prior and float(getattr(args, "wm_min_success", 0.0) or 0.0) > 0.0:
		try:
			from agi_dw.core.world_model.service import WorldModelService  # type: ignore
			wm_path = Path(args.wm_model)
			if wm_path.exists():
				wm = WorldModelService.load_if_exists(wm_path)
				prior_now = wm.predict_prior(obs, plan, action or {}) if wm else {"success_prob": 0.5}
				p_now = float(prior_now.get("success_prob", 0.5))
				if p_now < float(args.wm_min_success):
					trace = build_trace(
						task_id="loop-web-dom",
						obs=obs,
						plan=plan,
						action=action,
						result={"dom": "", "status": "blocked"},
						reward={"scalar": 0.0, "components": {"success": 0, "latency": 0, "side_effect": 1}},
						critique={"issues": [], "risk": float((prior_now or {}).get("risk", 0.5)), "proposal": "wm-success-budget"},
					)
				try:
					import os as _os
					trace["meta_state"] = {
						"enable_tot": True if _os.environ.get("PLANNER_TOT", "0") == "1" else False,
						"temperature": float(_os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2),
						"step_budget": int(_os.environ.get("PLANNER_STEP_BUDGET", "1") or 1),
					}
				except Exception:
					pass
					trace["wm_prior"] = prior_now
					trace["wm_success_budget"] = {"min_success": float(args.wm_min_success)}
					write_jsonl(args.out, trace)
					print(json.dumps({"status": "blocked", "wm_success_prob": p_now, "wm_min_success": float(args.wm_min_success)}, ensure_ascii=False))
					return 0
		except Exception:
			pass

	# Verifier-gated human approval (pre-execution)
	if args.require_approval:
		try:
			from agi_dw.core.verifier.service import VerifierServiceConfig, verify as verifier_run  # type: ignore
			pre_check = {"obs": obs, "plan": plan, "action": action, "result": {"status": "pending"}}
			v_cfg = VerifierServiceConfig(
				model=str(args.verifier_model),
				backend=str(args.verifier_backend),
				adapter_dir=getattr(args, "verifier_adapter", None),
				adapter_bank=getattr(args, "verifier_adapter_bank", None),
				structured_mode=str(args.verifier_structured),
				timeout_sec=max(2, int(args.timeout)),
				strict=False,
				calibrate=False,
				log_prompts=bool(getattr(args, "log_prompts", False)),
			)
			vpre = verifier_run(pre_check, v_cfg)
			risk_val = float(vpre.get("risk", 0.5))
			if risk_val >= float(args.approval_threshold):
				from datetime import datetime  # type: ignore
				ap_dir = Path(args.approval_dir)
				ap_dir.mkdir(parents=True, exist_ok=True)
				ap_id = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
				ap_path = ap_dir / f"approval_dom_{ap_id}.json"
				ap_path.write_text(json.dumps({"obs": obs, "plan": plan, "action": action, "verifier": vpre, "threshold": float(args.approval_threshold)}, ensure_ascii=False, indent=2), encoding="utf-8")
				print(json.dumps({"stage": "approval_block", "risk": risk_val, "threshold": float(args.approval_threshold), "request": str(ap_path)}, ensure_ascii=False))
			trace = build_trace(
					task_id="loop-web-dom",
					obs=obs,
					plan=plan,
					action=action,
					result={"dom": "", "status": "blocked"},
					reward={"scalar": 0.0, "components": {"success": 0, "latency": 0, "side_effect": 1}},
					critique={"issues": [], "risk": risk_val, "proposal": "approval-required"},
				)
			try:
				import os as _os
				trace["meta_state"] = {
					"enable_tot": True if _os.environ.get("PLANNER_TOT", "0") == "1" else Fal
# ... truncated ...


import logging
import argparse
import json
from pathlib import Path
from datetime import datetime
import os
from typing import Dict
import subprocess
import shlex

# Make script robust if PYTHONPATH is not set: add project root to sys.path
try:
	from agi_dw.tools.git import GitTool
	from agi_dw.tools.test_runner import TestRunner
	from agi_dw.core.actuator.patch_actuator import PatchActuator
	from agi_dw.tools.failure_classifier import classify_failures
	from agi_dw.tools.linter import LinterTool
	from agi_dw.core.llm.hf_client import HFClient
	from agi_dw.core.utils.prompt_logger import PromptLogger, get_prompt_logger  # type: ignore
	from agi_dw.core.actuator.code_actions import execute_code_action  # type: ignore
	from agi_dw.bench.common.trace import build_trace, write_jsonl  # type: ignore
except ModuleNotFoundError:
	import sys as _sys  # type: ignore
	_proj_root = Path(__file__).resolve().parents[1]      # .../agi_dw
	_repo_root = Path(__file__).resolve().parents[2]      # .../
	# Python needs the parent of the package directory on sys.path
	for p in (str(_repo_root), str(_proj_root)):
		if p not in _sys.path:
			_sys.path.insert(0, p)
	from agi_dw.tools.git import GitTool
	from agi_dw.tools.test_runner import TestRunner
	from agi_dw.core.actuator.patch_actuator import PatchActuator
	from agi_dw.tools.linter import LinterTool
	from agi_dw.core.llm.hf_client import HFClient
	from agi_dw.core.utils.prompt_logger import PromptLogger, get_prompt_logger  # type: ignore
	from agi_dw.bench.common.trace import build_trace, write_jsonl  # type: ignore

def main() -> int:
	parser = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[2]
	parser.add_argument("--repo", required=True, help="git URL or local path (local:/abs/path)")
	parser.add_argument("--workdir", default=str(root / "data" / "sandbox" / "dev_repo"))
	parser.add_argument("--pytest-args", nargs='*', default=[])
	parser.add_argument("--llm-model", default="meta-llama/Llama-3.2-3B")
	parser.add_argument("--setup", action="store_true", help="Run safe repo setup (install deps) before tests using repo manifest")
	parser.add_argument("--setup-timeout", type=int, default=600, help="Per-command timeout for setup (seconds)")
	parser.add_argument("--prefer-copy", action="store_true", default=True, help="For local repos, copy working tree instead of git clone to include uncommitted changes")
	parser.add_argument("--max-iters", type=int, default=1, help="Max patch attempts after initial test run (reserved)")
	parser.add_argument("--llm-candidates", type=int, default=1, help="Number of patch proposals to try per attempt (reserved)")
	parser.add_argument("--out", default=str(root / "data" / "traces" / "dev_loop.jsonl"), help="Path to write structured dev loop traces")
	# Optional: WM plan rank for candidate patches
	parser.add_argument("--wm-plan-rank", action="store_true")
	parser.add_argument("--wm-model", default=str(root / "models" / "wm_mlp" / "wm_mlp.joblib"))
	parser.add_argument("--disable-fallbacks", action="store_true", help="Disable deterministic heuristic patch fallbacks to force LLM diffs")
	# Multi-file control (Later item): default single-file unless explicitly allowed
	parser.add_argument("--allow-multifile", action="store_true", help="Permit multi-file diffs under budgeted max files")
	parser.add_argument("--max-files", type=int, default=1, help="Max files a patch may touch (effective when --allow-multifile)")
	# Optional: use auxiliary heads (targeting/size/risk) to bias decoding via prompt hints and budgets
	parser.add_argument("--use-heads", action="store_true", help="Use auxiliary head thresholds (size/files/risk) to guide budgets and prompt")
	# Optional: disable static analysis (linters/type-checkers) for faster runs
	parser.add_argument("--no-static", action="store_true", help="Disable static analysis (flake8/mypy/eslint) to speed up planning")
	# HITL approval checkpoint before apply
	parser.add_argument("--require-approval", action="store_true", help="Require human approval before applying candidate patches")
	parser.add_argument("--approval-timeout", type=int, default=60, help="Seconds to wait for approval decision")
	# Planner index integration knobs
	parser.add_argument("--planner-index-k", type=int, default=0, help="Top-K code index items to include (0 disables)")
	parser.add_argument("--planner-index-path", default=str(root / "data" / "sandbox" / "tmp" / "index.json"), help="Path to code index snapshot")
	# Self-play / meta-controller integration
	parser.add_argument("--meta-state", default=str(root / "data" / "sandbox" / "tmp" / "meta_state.json"), help="Path to meta-controller state JSON")
	parser.add_argument("--apply-meta", action="store_true", help="Apply meta-state (ToT, temperature, step budget) via environment vars for downstream components")
	args, extra = parser.parse_known_args()
	import time as _t  # type: ignore
	t0 = _t.perf_counter()

	workdir = Path(args.workdir)

	# Optionally apply meta-controller state to environment for planner/actuator
	if bool(getattr(args, "apply_meta", False)):
		try:
			import os as _os, json as _json  # type: ignore
			m = _json.loads(Path(str(args.meta_state)).read_text(encoding="utf-8"))
			enable_tot = bool(m.get("enable_tot", False))
			temp = float(m.get("sampling_temperature", 0.2))
			budget = int(m.get("planner_step_budget", 1))
			_os.environ["PLANNER_TOT"] = "1" if enable_tot else "0"
			_os.environ["SAMPLING_TEMPERATURE"] = str(temp)
			_os.environ["PLANNER_STEP_BUDGET"] = str(budget)
		except Exception:
			pass

	# Clone or copy local repo
	# If copying local repo, avoid placing workdir inside the source repo to prevent recursive nesting
	git = None
	repo_dir = None
	if args.repo.startswith("local:"):
		src_path = Path(args.repo.split(":", 1)[1])
		try:
			src_real = src_path.resolve()
			wd_real = workdir.resolve()
			if str(wd_real).startswith(str(src_real)):
				# Move workdir outside the source tree
				workdir = Path("/data/agiattempt/dev_sandbox/dev_repo")
		except Exception:
			pass
		# Clean and prepare workdir
		try:
			import shutil  # type: ignore
			if workdir.exists():
				shutil.rmtree(workdir, ignore_errors=True)
			workdir.mkdir(parents=True, exist_ok=True)
		except Exception:
			pass
		git = GitTool(str(workdir))
		repo_name = src_path.name
		dest_dir = workdir / repo_name
		# Prefer copying working tree for local sources to include uncommitted edits
		if True if bool(getattr(args, "prefer_copy", True)) else False:
			try:
				import shutil  # type: ignore
				if dest_dir.exists():
					shutil.rmtree(dest_dir, ignore_errors=True)
				dest_dir.mkdir(parents=True, exist_ok=True)
				# Copy only safe top-level entries to avoid recursive sandboxes/binaries
				exclude_names = {
					".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".venv",
					"node_modules", "dist", "build", "models", "models/", "data",
				}
				for entry in src_path.iterdir():
					name = entry.name
					if name in exclude_names:
						continue
					if name == "data":
						# Skip entire data tree to avoid nested dev_repo copies
						continue
					dst = dest_dir / name
					if entry.is_dir():
						shutil.copytree(entry, dst, dirs_exist_ok=False)
					else:
						shutil.copy2(entry, dst)
			except Exception as e:
				print(json.dumps({"stage": "copy", "ok": False, "error": str(e)}))
				return 1
		else:
			# Fallback to clone when explicitly requested
			try:
				import shutil  # type: ignore
				if dest_dir.exists():
					shutil.rmtree(dest_dir, ignore_errors=True)
			except Exception:
				pass
			r = git.clone(str(src_path), dest=str(dest_dir))
			if r.returncode != 0:
				print(json.dumps({"stage": "clone", "ok": False, "stdout": r.stdout, "stderr": r.stderr}))
				return 1
		repo_dir = dest_dir if dest_dir.exists() else workdir / repo_name
	else:
		workdir.mkdir(parents=True, exist_ok=True)
		git = GitTool(str(workdir))
		# Remote git: clean existing dest and clone explicitly into dest_dir
		repo_name = Path(args.repo.split("/")[-1]).stem
		dest_dir = workdir / repo_name
		try:
			import shutil  # type: ignore
			if dest_dir.exists():
				shutil.rmtree(dest_dir, ignore_errors=True)
		except Exception:
			pass
		r = git.clone(args.repo, dest=str(dest_dir))
		if r.returncode != 0:
			print(json.dumps({"stage": "clone", "ok": False, "stdout": r.stdout, "stderr": r.stderr}))
			return 1
		repo_dir = dest_dir

	# Optional safe environment setup (install deps) using repo manifest
	if bool(getattr(args, "setup", False)):
		try:
			from agi_dw.tools.repo_manifest import generate_manifest  # type: ignore
			mf = generate_manifest(str(repo_dir))
			build_cmds = [str(c) for c in (mf.get("build_cmds") or [])]
			# Allow only safe install commands by default
			allow_substrings = ["pip install", "npm install", "yarn install", "pnpm install"]
			safe_cmds = []
			for c in build_cmds:
				lc = c.strip().lower()
				if any(s in lc for s in allow_substrings) and ("sudo" not in lc):
					safe_cmds.append(c)
			# Deduplicate while preserving order
			seen = set()
			ordered_cmds = []
			for c in safe_cmds:
				if c not in seen:
					seen.add(c)
					ordered_cmds.append(c)
			for cmd_str in ordered_cmds:
				try:
					cmd = shlex.split(cmd_str)
					p = subprocess.run(cmd, cwd=str(repo_dir), capture_output=True, text=True, timeout=int(getattr(args, "setup_timeout", 600) or 600))
					print(json.dumps({"stage": "setup", "cmd": cmd_str, "returncode": p.returncode, "stdout": p.stdout, "stderr": p.stderr}))
				except Exception as e:
					print(json.dumps({"stage": "setup", "cmd": cmd_str, "error": str(e)}))
		except Exception as e:
			print(json.dumps({"stage": "setup", "ok": False, "error": str(e)}))

	# Ensure test package layout is importable (make tests a package if needed)
	try:
		from pathlib import Path as _P  # type: ignore
		tests_dir = _P(repo_dir) / "tests"
		if tests_dir.is_dir() and not (tests_dir / "__init__.py").exists():
			(tests_dir / "__init__.py").touch()
	except Exception:
		pass

	# Run tests first
	tr = TestRunner(str(repo_dir))
	# Avoid recursively picking up nested copies by restricting testpaths to repo root
	env = dict(**os.environ)
	# Put both the repo parent and the repo itself on PYTHONPATH so package-style imports work
	pp_val = f"{str(_P(repo_dir).parent)}:{str(repo_dir)}"
	prev_pp = env.get("PYTHONPATH", "")
	env["PYTHONPATH"] = (pp_val if not prev_pp else f"{pp_val}:{prev_pp}")
	env["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1"
	env["PYTHONDONTWRITEBYTECODE"] = "1"
	# Keep pytest cache/temp inside sandbox
	env["PYTEST_CACHE_DIR"] = str(Path(repo_dir) / ".pytest_cache")
	# Also set a per-run temp and ignore repo-level pytest.ini addopts by default
	addopts = (env.get("PYTEST_ADDOPTS", "").strip() + f" --basetemp {Path(repo_dir) / '.pytest_tmp'}").strip()
	env["PYTEST_ADDOPTS"] = addopts
	# Ensure pytest temp files go to a writable path
	base_tmp = str((Path(repo_dir) / ".pytest_tmp").resolve())
	# Prefer a dedicated tests directory if present to avoid collecting library files
	test_target = None
	for name in ("tests", "testing", "test"):
		cand = repo_dir / name
		if cand.exists() and cand.is_dir():
			test_target = str(cand)
			break
	# If no Python tests directory and package.json exists, run npm test instead
	if test_target is None and (repo_dir / "package.json").exists():
		npm_res = tr.run_npm_test()
		print(json.dumps({"stage": "npm_test", **npm_res}))
		return 0 if npm_res.get("ok") else 2
	import shlex  # type: ignore
	base_args = ["-k", "not benchmark and not slow and not test_runner", "--maxfail=1"]
	# Allow pytest args both via --pytest-args and via extra unknown tokens
	user_tokens: list[str] = []
	if args.pytest_args:
		user_tokens.extend(list(args.pytest_args))
	if extra:
		user_tokens.extend(extra)
	run_args = (user_tokens if user_tokens else base_args)
	# Force writable basetemp
	run_args += ["--basetemp", base_tmp]
	# Target tests directory if present, else the repo root
	run_args += ([test_target] if test_target else [str(repo_dir)])
	test_res = tr.run_pytest(args=run_args, timeout=600, env=env)
	# If usage error due to -k parsing, retry once stripping quotes/spaces
	if test_res.get("returncode") == 4 and any("Wrong expression passed to '-k'" in s for s in [test_res.get("stderr", ""), test_res.get("stdout", "")]):
		try:
			# Find index of -k and tighten expression
			if "-k" in run_args:
				idx = run_args.index("-k")
				if idx + 1 < len(run_args):
					expr = run_args[idx + 1]
					expr = expr.strip().strip('"\'')
					run_args[idx + 1] = expr
					test_res = tr.run_pytest(args=run_args, timeout=600, env=env)
		except Exception:
			pass
		# Only treat actual pytest success as ok; "no tests collected" is not success
		fails = test_res.get("failures") or []
	# Extract small failure excerpt around the first failing node if available
	failure_excerpt = ""
	try:
		stdout_text = str(test_res.get("stdout", ""))
		# Heuristic: capture lines from the first FAILED line onwards until a blank line or next summary
		lines = stdout_text.splitlines()
		start = -1
		for i, ln in enumerate(lines):
			if ln.strip().startswith("FAILED "):
				start = i
				break
		if start != -1:
			end = min(len(lines), start + 20)
			excerpt_lines = []
			for j in range(start, end):
				excerpt_lines.append(lines[j])
				if j > start and lines[j].strip() == "":
					break
			failure_excerpt = "\n".join(excerpt_lines).strip()
		except Exception:
			failure_excerpt = ""
		ok = bool(test_res.get("ok"))
	print(json.dumps({"stage": "pytest", **test_res, "ok": ok, "elapsed_sec": round(_t.perf_counter() - t0, 3)}))
	if ok:
		# Emit a structured trace for baseline run (no patch)
		try:
			obs0 = {"kind": "code", "repo": str(repo_dir), "failures": fails, "failure_excerpt": failure_excerpt}
			plan0 = {"prompt": ""}
			action0 = {"tool": "code.noop", "args": {}}
			result0 = {"status": "ok"}
			reward0 = {"scalar": 1.0, "components": {"success": 1, "latency": 0, "side_effect": 1}}
			crit0 = {"issues": [], "risk": 0.0, "proposal": "baseline"}
			trace0 = build_trace("devloop", obs0, plan0, action0, result0, reward0, crit0)
			# Attach applied meta-state if present
			try:
				import os as _os  # type: ignore
				trace0["meta_state"] = {
					"enable_tot": True if _os.environ.get("PLANNER_TOT", "0") == "1" else False,
					"temperature": float(_os.environ.get("SAMPLING_TEMPERATURE", "0.2") or 0.2),
					"step_budget": int(_os.environ.get("PLANNER_STEP_BUDGET", "1") or 1),
				}
			except Exception:
				pass
			write_jsonl(str(Path(args.out)), trace0)
		except Exception:
			pass
		return 0

	# On failure: try a minimal heuristic fix first; else build prompt and ask LLM for unified diff, apply, re-run
	fails = test_res.get("failures") or []
	try:
		fc = classify_failures(test_res.get("stdout", ""), test_res.get("stderr", ""))
		print(json.dumps({"stage": "classify", **fc}))
	except Exception:
		pass
	# Deterministic fallback removed for model-centric training (no-op)
	target_files: list[str] = []
	for f in fails:
		p = f.get("path")
		if not isinstance(p, str) or not p:
			continue
		# Normalize path relative to repo; prefer exact relpath if exists, else search by basename
		try:
			cand = Path(p)
			rel = cand if (Path(repo_dir) / cand).exists() else None
		except Exception:
			rel = None
		if rel is None:
			try:
				name = Path(p).name
				matches = list(Path(repo_dir).rglob(name))
				if matches:
					# Prefer paths under tests/
					matches_sorted = sorted(matches, key=lambda x: ("tests" not in str(x), len(str(x))))
					rel = matches_sorted[0].relative_to(repo_dir)
			except Exception:
				rel = None
		if rel is not None:
			rp = rel.as_posix()
			if rp not in target_files:
				target_files.append(rp)
		if len(target_files) >= 5:
			break
	# If nothing resolved, fall back to common locations
	if not target_files:
		for fallback in ("tests/test_*.py",):
			try:
				any_match = next(Path(repo_dir).glob(fallback), None)
				if any_match:
					target_files.append(any_match.relative_to(repo_dir).as_posix())
					break
			except Exception:
				pass
	file_blobs: list[dict] = []
	for rel in target_files:
		try:
			fp = Path(repo_dir) / rel
			if fp.exists() and fp.is_file():
				# cap large files
				txt = fp.read_text(encoding="utf-8")
				if len(txt) > 8000:
					txt = txt[:8000]
				file_blobs.append({"path": rel, "text": txt})
		except Exception:
			continue
	# Deterministic fallback removed for model-centric training (no-op)
	# Deterministic repository-wide fallback removed (no-op)
	if file_blobs:
		# Optional: run linters and capture a small, relevant summary (py + js/ts if available)
		# Build a conservative allowlist of paths for patch application based on failing targets
		allow_paths: list[str] = []
		block_paths: list[str] = ["data/**", "models/**", ".github/**", ".gitlab/**", "dist/**", "build/**", "node_modules/**"]
		# Heuristic: derive probable source files from tests (e.g., tests/test_foo.py -> foo.py)
		def _derive_sources_from_tests(paths: list[str]) -> list[str]:
			out: list[str] = []
			try:
				from pathlib import Path as __P  # type: ignore
				for rp in (paths or []):
					p = __P(rp)
					name = p.name
					stem = name
					if name.startswith("test_"):
						stem = name[len("test_"):]
					# Candidate in repo root
					cand1 = stem
					# Candidate next to tests directory (replace leading tests/)
					parts = list(p.parts)
					if parts and parts[0] == "tests":
						cand2 = __P(*(["/"] + parts[1:-1] + [stem])).as_posix().lstrip("/")
					else:
						cand2 = stem
					for c in (cand1, cand2):
						if c not in out:
							out.append(c)
			except Exception:
				pass
			return out
		try:
			from pathlib import Path as _P2  # type: ignore
			allow_dirs: set[str] = set()
			for rp in (target_files or []):
				try:
					dir_posix = str((_P2(rp).parent).as_posix()).strip()
					if dir_posix and dir_posix != ".":
						allow_dirs.add(dir_posix)
					# Also allow the specific file paths explicitly
					allow_paths.append(str(_P2(rp).as_posix()))
				except Exception:
					pass
			# Limit number of dirs and expand to py-files under them
			for d in sorted(list(allow_dirs))[:8]:
				allow_paths.append(f"{d}/**/*.py")
				# Expand to language-agnostic patterns for JS/TS when budgeting multi-file edits
				allow_paths.append(f"{d}/**/*.js")
				allow_paths.append(f"{d}/**/*.ts")
				allow_paths.append(f"{d}/**/*.jsx")
				allow_paths.append(f"{d}/**/*.tsx")
			# Add derived source candidates to allowlist explicitly
			for srcp in _derive_sources_from_tests(target_files or []):
				allow_paths.append(srcp)
		except Exception:
			pass

		lint_summary = ""
		if not bool(getattr(args, "no_static", False)):
			try:
				lt = LinterTool(str(repo_dir))
				fl = lt.run_flake8()
				my = lt.run_mypy()
				# Try JS/TS linters via TestRunner
				js_lines: list[str] = []
				try:
					tr = TestRunner(str(repo_dir))
					es = tr.run_eslint()
					if es.get("issues"):
						capped = 0
						for it in es.get("issues"):
							js_lines.append(f"[eslint] {it.get('path','')}:{it.get('line','')} {it.get('ruleId','')}: {it.get('msg','')}")
							capped += 1
							if capped >= 5:
								break
				except Exception:
					pass
				lines: list[str] = []
				for pack in (fl, my):
					if pack.get("available") and not pack.get("ok"):
						tool = str(pack.get("tool"))
						issues = pack.get("issues") or []
						# Keep only a few issues and prefer ones touching target files
						picked = 0
						for it in issues:
							p = str(it.get("path", ""))
							if any(p.endswith(tf) or tf in p for tf in (target_files or [])):
								lines.append(f"[{tool}] {p}:{it.get('line','')} {it.get('msg','')}")
								picked += 1
								if picked >= 5:
									break
				if js_lines:
					lines.extend(js_lines)
				if lines:
					lint_summary = "\n".join(lines[:12])
			except Exception:
				lint_summary = ""
		fail_summ = "\n".join([f"- {f.get('path','')}::{f.get('test','')}" for f in fails])
		# Prefer a derived source f
# ... truncated ...