	except Exception:
		return ""

def _run_cmd(repo: Path, argv: List[str]) -> Dict[str, Any]:
	"""Run a command in repo cwd and capture stdout/stderr/rc."""
	start = time.time()
	try:
		p = subprocess.run(argv, cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
		out = (p.stdout or "")
		err = (p.stderr or "")
		rc = int(p.returncode)
	except Exception as e:
		out = ""
		err = str(e)
		rc = 1
	end = time.time()
	return {"argv": argv, "rc": rc, "stdout": out, "stderr": err, "elapsed_sec": float(round(max(0.0, end - start), 3))}

def run_refactor_skill(repo: Path) -> Dict[str, Any]:
	"""Execute refactor modularization skill as a dry-run sequence of safe tools."""
	steps: List[Dict[str, Any]] = []
	ok = True
	# 1) Audit scripts
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.check_scripts_modularization", "--out", "data/ci/scripts_audit.json"]))
	# 2) Emit scripts move plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.emit_scripts_refactor_plan"]))
	# 3) Dry-run apply move plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.apply_refactor_plan", "--plan", "data/traces/scripts_refactor_plan.json", "--repo", ".", "--validate", "--max-edits", "500", "--dry-run"]))
	# 4) Emit references update plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.emit_scripts_refs_update_plan", "--out", "data/traces/scripts_refs_update_plan.json"]))
	# 5) Dry-run apply references plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.apply_refactor_plan", "--plan", "data/traces/scripts_refs_update_plan.json", "--repo", ".", "--validate", "--max-edits", "500", "--dry-run"]))
	# 6) Smoke targets (names only)
	steps.append(_run_cmd(repo, ["make", "-s", "tools.smoke"]))
	for s in steps:
		if int(s.get("rc", 1)) != 0:
			ok = False
	return {"ok": bool(ok), "steps": steps}

def _load_json_safe(p: Path) -> Dict[str, Any]:
	try:
		if p.exists():
			return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		pass
	return {}

def planner_hook_propose(registry_path: Path, metrics_path: Path, world_path: Optional[Path] = None) -> Dict[str, Any]:
	"""Minimal planner hook: read tool registry and recent metrics to propose next tool.

	Heuristic: if lint/test rates are < 1.0, propose dry-run refactor skill; else noop.
	Always default to dry-run and keep apply guarded by verifier.
	"""
	registry = _load_json_safe(registry_path)
	metrics = _load_json_safe(metrics_path)
	world = _load_json_safe(world_path) if world_path else {}


	steps: List[Dict[str, Any]] = []
	ok = True
	# 1) Audit scripts
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.check_scripts_modularization", "--out", "data/ci/scripts_audit.json"]))
	# 2) Emit scripts move plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.emit_scripts_refactor_plan"]))
	# 3) Dry-run apply move plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.apply_refactor_plan", "--plan", "data/traces/scripts_refactor_plan.json", "--repo", ".", "--validate", "--max-edits", "500", "--dry-run"]))
	# 4) Emit references update plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.emit_scripts_refs_update_plan", "--out", "data/traces/scripts_refs_update_plan.json"]))
	# 5) Dry-run apply references plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.apply_refactor_plan", "--plan", "data/traces/scripts_refs_update_plan.json", "--repo", ".", "--validate", "--max-edits", "500", "--dry-run"]))
	# 6) Smoke targets (names only)
	steps.append(_run_cmd(repo, ["make", "-s", "tools.smoke"]))
	for s in steps:
		if int(s.get("rc", 1)) != 0:
			ok = False
	return {"ok": bool(ok), "steps": steps}

def _load_json_safe(p: Path) -> Dict[str, Any]:
	try:
		if p.exists():
			return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		pass
	return {}

def planner_hook_propose(registry_path: Path, metrics_path: Path, world_path: Optional[Path] = None) -> Dict[str, Any]:
	"""Minimal planner hook: read tool registry and recent metrics to propose next tool.

	Heuristic: if lint/test rates are < 1.0, propose dry-run refactor skill; else noop.
	Always default to dry-run and keep apply guarded by verifier.
	"""
	registry = _load_json_safe(registry_path)
	metrics = _load_json_safe(metrics_path)
	world = _load_json_safe(world_path) if world_path else {}
	ok_rate = float(metrics.get("ok_rate", 0.0))
	lint_ok_rate = float(metrics.get("lint_ok_rate", 0.0))
	test_ok_rate = float(metrics.get("test_ok_rate", 0.0))
	# Confidence is higher when there is room to improve but tests generally pass
	if (ok_rate < 1.0) or (lint_ok_rate < 1.0):
		return {
			"tool": "skill.refactor_modularization",
			"args": {"dry_run": True},
			"confidence": 0.9 if test_ok_rate >= 0.8 else 0.7,


	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.emit_scripts_refs_update_plan", "--out", "data/traces/scripts_refs_update_plan.json"]))
	# 5) Dry-run apply references plan
	steps.append(_run_cmd(repo, ["python3", "-m", "agi_dw.scripts.misc.apply_refactor_plan", "--plan", "data/traces/scripts_refs_update_plan.json", "--repo", ".", "--validate", "--max-edits", "500", "--dry-run"]))
	# 6) Smoke targets (names only)
	steps.append(_run_cmd(repo, ["make", "-s", "tools.smoke"]))
	for s in steps:
		if int(s.get("rc", 1)) != 0:
			ok = False
	return {"ok": bool(ok), "steps": steps}

def _load_json_safe(p: Path) -> Dict[str, Any]:
	try:
		if p.exists():
			return json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		pass
	return {}

def planner_hook_propose(registry_path: Path, metrics_path: Path, world_path: Optional[Path] = None) -> Dict[str, Any]:
	"""Minimal planner hook: read tool registry and recent metrics to propose next tool.

	Heuristic: if lint/test rates are < 1.0, propose dry-run refactor skill; else noop.
	Always default to dry-run and keep apply guarded by verifier.
	"""
	registry = _load_json_safe(registry_path)
	metrics = _load_json_safe(metrics_path)
	world = _load_json_safe(world_path) if world_path else {}
	ok_rate = float(metrics.get("ok_rate", 0.0))
	lint_ok_rate = float(metrics.get("lint_ok_rate", 0.0))
	test_ok_rate = float(metrics.get("test_ok_rate", 0.0))
	# Confidence is higher when there is room to improve but tests generally pass
	if (ok_rate < 1.0) or (lint_ok_rate < 1.0):
		return {
			"tool": "skill.refactor_modularization",
			"args": {"dry_run": True},
			"confidence": 0.9 if test_ok_rate >= 0.8 else 0.7,
			"evidence": {"ok_rate": ok_rate, "lint_ok_rate": lint_ok_rate, "test_ok_rate": test_ok_rate},
			"registry_loaded": bool(registry.get("tools"))
		}
	# Default noop with low confidence
	return {"tool": "noop", "args": {}, "confidence": 0.4, "evidence": {"ok_rate": ok_rate, "lint_ok_rate": lint_ok_rate, "test_ok_rate": test_ok_rate}, "world_loaded": bool(world)}

def make_import_rewrite_diff(repo: Path, pkg: str, only_tests: bool = False, max_files: int = 200) -> str:
	"""Generate a unified diff that rewrites simple relative imports to absolute under pkg.

	Patterns:
	  from .mod import X   -> from <pkg>.mod import X
	  from . import X      -> from <pkg> import X
	"""
	files: List[Path] = []
	if only_tests:
		tdir = repo / "tests"
		if tdir.exists():
			files = list(tdir.rglob("*.py"))
	else:
		files = list(repo.rglob("*.py"))
	diffs: List[str] = []
	changed = 0
	for fp in files:
		try:


	"""
	registry = _load_json_safe(registry_path)
	metrics = _load_json_safe(metrics_path)
	world = _load_json_safe(world_path) if world_path else {}
	ok_rate = float(metrics.get("ok_rate", 0.0))
	lint_ok_rate = float(metrics.get("lint_ok_rate", 0.0))
	test_ok_rate = float(metrics.get("test_ok_rate", 0.0))
	# Confidence is higher when there is room to improve but tests generally pass
	if (ok_rate < 1.0) or (lint_ok_rate < 1.0):
		return {
			"tool": "skill.refactor_modularization",
			"args": {"dry_run": True},
			"confidence": 0.9 if test_ok_rate >= 0.8 else 0.7,
			"evidence": {"ok_rate": ok_rate, "lint_ok_rate": lint_ok_rate, "test_ok_rate": test_ok_rate},
			"registry_loaded": bool(registry.get("tools"))
		}
	# Default noop with low confidence
	return {"tool": "noop", "args": {}, "confidence": 0.4, "evidence": {"ok_rate": ok_rate, "lint_ok_rate": lint_ok_rate, "test_ok_rate": test_ok_rate}, "world_loaded": bool(world)}

def make_import_rewrite_diff(repo: Path, pkg: str, only_tests: bool = False, max_files: int = 200) -> str:
	"""Generate a unified diff that rewrites simple relative imports to absolute under pkg.

	Patterns:
	  from .mod import X   -> from <pkg>.mod import X
	  from . import X      -> from <pkg> import X
	"""
	files: List[Path] = []
	if only_tests:
		tdir = repo / "tests"
		if tdir.exists():
			files = list(tdir.rglob("*.py"))
	else:
		files = list(repo.rglob("*.py"))
	diffs: List[str] = []
	changed = 0
	for fp in files:
		try:
			orig = _read_text_safe(fp)
			if not orig:
				continue
			text = orig
			text = re.sub(r"^\s*from\s+\.([\w\.]+)\s+import\s+", rf"from {pkg}.\\1 import ", text, flags=re.M)
			text = re.sub(r"^\s*from\s+\.\s+import\s+", rf"from {pkg} import ", text, flags=re.M)
			if text != orig:
				rel = fp.relative_to(repo).as_posix()
				ud = difflib.unified_diff(
					orig.splitlines(keepends=True),
					text.splitlines(keepends=True),
					fromfile=f"a/{rel}",
					tofile=f"b/{rel}",
				)
				diffs.append("".join(ud))
				changed += 1
				if changed >= max_files:
					break
		except Exception:
			continue
	return "".join(diffs)

def make_flake8_whitespace_fix_diff(repo: Path, only_tests: bool = False, max_files: int = 200) -> str:
	"""Generate a unified diff that applies safe whitespace fixes:

	- Strip trailing whitespace
	- Ensure newline at EOF
	"""
	files: List[Path] = []
	if only_tests:
		tdir = repo / "tests"
		if tdir.exists():
			files = list(tdir.rglob("*.py"))
	else:
		files = list(repo.rglob("*.py"))
	diffs: List[str] = []
	changed = 0
	for fp in files:
		try:
			orig = _read_text_safe(fp)


			text = orig
			text = re.sub(r"^\s*from\s+\.([\w\.]+)\s+import\s+", rf"from {pkg}.\\1 import ", text, flags=re.M)
			text = re.sub(r"^\s*from\s+\.\s+import\s+", rf"from {pkg} import ", text, flags=re.M)
			if text != orig:
				rel = fp.relative_to(repo).as_posix()
				ud = difflib.unified_diff(
					orig.splitlines(keepends=True),
					text.splitlines(keepends=True),
					fromfile=f"a/{rel}",
					tofile=f"b/{rel}",
				)
				diffs.append("".join(ud))
				changed += 1
				if changed >= max_files:
					break
		except Exception:
			continue
	return "".join(diffs)

def make_flake8_whitespace_fix_diff(repo: Path, only_tests: bool = False, max_files: int = 200) -> str:
	"""Generate a unified diff that applies safe whitespace fixes:

	- Strip trailing whitespace
	- Ensure newline at EOF
	"""
	files: List[Path] = []
	if only_tests:
		tdir = repo / "tests"
		if tdir.exists():
			files = list(tdir.rglob("*.py"))
	else:
		files = list(repo.rglob("*.py"))
	diffs: List[str] = []
	changed = 0
	for fp in files:
		try:
			orig = _read_text_safe(fp)
			if not orig:
				continue
			# Apply fixes
			lines = orig.splitlines()
			fixed_lines = [ln.rstrip() for ln in lines]
			fixed = "\n".join(fixed_lines) + "\n"
			if fixed != (orig if orig.endswith("\n") else orig + "\n"):
				rel = fp.relative_to(repo).as_posix()
				ud = difflib.unified_diff(
					(orig if orig.endswith("\n") else orig + "\n").splitlines(keepends=True),
					fixed.splitlines(keepends=True),
					fromfile=f"a/{rel}",
					tofile=f"b/{rel}",
				)
				diffs.append("".join(ud))
				changed += 1
				if changed >= max_files:
					break
		except Exception:
			continue
	return "".join(diffs)

def _extract_unused_names(issues: List[Dict[str, Any]]) -> Dict[str, List[str]]:
	"""Extract F401 unused import names per file from flake8 issues.

	Returns: mapping file->list of names to remove.
	"""
	per_file: Dict[str, List[str]] = {}
	for it in (issues or []):
		try:
			code = str(it.get("code") or "")
			if code != "F401":
				continue
			msg = str(it.get("message") or it.get("msg") or "")
			# F401 'name' imported but unused
			m = None
			if "'" in msg:
				parts = msg.split("'")
				if len(parts) >= 2:
					m = parts[1]


			lines = orig.splitlines()
			fixed_lines = [ln.rstrip() for ln in lines]
			fixed = "\n".join(fixed_lines) + "\n"
			if fixed != (orig if orig.endswith("\n") else orig + "\n"):
				rel = fp.relative_to(repo).as_posix()
				ud = difflib.unified_diff(
					(orig if orig.endswith("\n") else orig + "\n").splitlines(keepends=True),
					fixed.splitlines(keepends=True),
					fromfile=f"a/{rel}",
					tofile=f"b/{rel}",
				)
				diffs.append("".join(ud))
				changed += 1
				if changed >= max_files:
					break
		except Exception:
			continue
	return "".join(diffs)

def _extract_unused_names(issues: List[Dict[str, Any]]) -> Dict[str, List[str]]:
	"""Extract F401 unused import names per file from flake8 issues.

	Returns: mapping file->list of names to remove.
	"""
	per_file: Dict[str, List[str]] = {}
	for it in (issues or []):
		try:
			code = str(it.get("code") or "")
			if code != "F401":
				continue
			msg = str(it.get("message") or it.get("msg") or "")
			# F401 'name' imported but unused
			m = None
			if "'" in msg:
				parts = msg.split("'")
				if len(parts) >= 2:
					m = parts[1]
			if not m:
				continue
			path = str(it.get("file") or it.get("path") or "")
			if not path:
				continue
			per_file.setdefault(path, []).append(m)
		except Exception:
			continue
	return per_file

def make_flake8_unused_import_fix_diff(repo: Path, fl_issues: Dict[str, Any], only_tests: bool = True, max_files: int = 100) -> str:
	"""Generate a unified diff that removes clearly unused imports (F401) in test files.

	Heuristic: delete lines that contain the unused name on import statements.
	"""
	files_map = _extract_unused_names(fl_issues.get("issues") or []) if isinstance(fl_issues, dict) else {}
	diffs: List[str] = []
	edits = 0
	for path, names in files_map.items():
		fp = Path(path)
		try:
			rel = fp.relative_to(repo)
		except Exception:
			# If lint returned absolute paths, try to map from repo
			try:
				rel = Path(path)
				if not (repo / rel).exists():
					continue


			if code != "F401":
				continue
			msg = str(it.get("message") or it.get("msg") or "")
			# F401 'name' imported but unused
			m = None
			if "'" in msg:
				parts = msg.split("'")
				if len(parts) >= 2:
					m = parts[1]
			if not m:
				continue
			path = str(it.get("file") or it.get("path") or "")
			if not path:
				continue
			per_file.setdefault(path, []).append(m)
		except Exception:
			continue
	return per_file

def make_flake8_unused_import_fix_diff(repo: Path, fl_issues: Dict[str, Any], only_tests: bool = True, max_files: int = 100) -> str:
	"""Generate a unified diff that removes clearly unused imports (F401) in test files.

	Heuristic: delete lines that contain the unused name on import statements.
	"""
	files_map = _extract_unused_names(fl_issues.get("issues") or []) if isinstance(fl_issues, dict) else {}
	diffs: List[str] = []
	edits = 0
	for path, names in files_map.items():
		fp = Path(path)
		try:
			rel = fp.relative_to(repo)
		except Exception:
			# If lint returned absolute paths, try to map from repo
			try:
				rel = Path(path)
				if not (repo / rel).exists():
					continue
			except Exception:
				continue
		if only_tests and ("/tests/" not in ("/" + rel.as_posix() + "/")):
			continue
		try:
			orig = _read_text_safe(repo / rel)
			if not orig:
				continue
			out_lines: List[str] = []
			changed = False
			for ln in orig.splitlines():
				s = ln.strip()
				if s.startswith("import ") or s.startswith("from "):
					# If any unused name appears on this import line, drop it entirely (conservative)
					if any(name in s for name in names):
						changed = True
						continue
				out_lines.append(ln)
			if changed:
				fixed = "\n".join(out_lines) + "\n"
				ud = difflib.unified_diff(
					(orig if orig.endswith("\n") else orig + "\n").splitlines(keepends=True),
					fixed.splitlines(keepends=True),
					fromfile=f"a/{rel.as_posix()}",
					tofile=f"b/{rel.as_posix()}",
				)
				diffs.append("".join(ud))
				edits += 1
				if edits >= max_files:
					break
		except Exception:
			continue
	return "".join(diffs)

def aggregate_metrics(traces_path: Path) -> Dict[str, Any]:
	total = 0
	ok = 0
	elapsed: List[float] = []
	categories: Dict[str, int] = {}
	test_ok = 0
	lint_ok = 0
	type_ok = 0
	try:
		if traces_path.exists():
			with traces_path.open("r", encoding="utf-8") as f:
				for line in f:
					line = line.strip()
					if not line:
						continue
					try:
						rec = json.loads(line)
					except Exception:


						changed = True
						continue
				out_lines.append(ln)
			if changed:
				fixed = "\n".join(out_lines) + "\n"
				ud = difflib.unified_diff(
					(orig if orig.endswith("\n") else orig + "\n").splitlines(keepends=True),
					fixed.splitlines(keepends=True),
					fromfile=f"a/{rel.as_posix()}",
					tofile=f"b/{rel.as_posix()}",
				)
				diffs.append("".join(ud))
				edits += 1
				if edits >= max_files:
					break
		except Exception:
			continue
	return "".join(diffs)

def aggregate_metrics(traces_path: Path) -> Dict[str, Any]:
	total = 0
	ok = 0
	elapsed: List[float] = []
	categories: Dict[str, int] = {}
	test_ok = 0
	lint_ok = 0
	type_ok = 0
	try:
		if traces_path.exists():
			with traces_path.open("r", encoding="utf-8") as f:
				for line in f:
					line = line.strip()
					if not line:
						continue
					try:
						rec = json.loads(line)
					except Exception:
						continue
					total += 1
					if rec.get("result", {}).get("status") == "ok":
						ok += 1
					try:
						elapsed.append(float(rec.get("elapsed_sec", 0.0)))
					except Exception:
						pass
					for c in (rec.get("critique", {}).get("issues") or []):
						categories[c] = categories.get(c, 0) + 1
					checks = rec.get("checks") or {}
					if isinstance(checks, dict):
						test_ok += 1 if bool(checks.get("test_ok", False)) else 0
						lint_ok += 1 if bool(checks.get("lint_ok", False)) else 0
						type_ok += 1 if bool(checks.get("type_ok", False)) else 0
	except Exception:
		pass
	ok_rate = float(round((float(ok) / max(1.0, float(total))), 3))
	test_ok_rate = float(round((float(test_ok) / max(1.0, float(total))), 3))
	lint_ok_rate = float(round((float(lint_ok) / max(1.0, float(total))), 3))
	type_ok_rate = float(round((float(type_ok) / max(1.0, float(total))), 3))
	p50 = float(round(sorted(elapsed)[len(elapsed)//2], 3)) if elapsed else 0.0
	p90 = float(round(sorted(elapsed)[int(len(elapsed)*0.9)], 3)) if elapsed else 0.0
	return {"runs": int(total), "ok": int(ok), "ok_rate": ok_rate, "test_ok_rate": test_ok_rate, "lint_ok_rate": lint_ok_rate, "type_ok_rate": type_ok_rate, "time_to_fix_p50": p50, "time_to_fix_p90": p90, "by_issue": categories}

def main() -> int:
	ap = argparse.ArgumentParser()
	# Repo root is two levels up from this script (agi_dw)
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--repo", default=str(root))
	ap.add_argument("--pkg", default="agi_dw")
	ap.add_argument("--only-tests", action="store_true")
	ap.add_argument("--out-traces", default=str(root / "data" / "devtools" / "traces.jsonl"))
	ap.add_argument("--out-metrics", default=str(root / "data" / "devtools" / "metrics.json"))
	ap.add_argument("--use-planner-hook", action="store_true", help="Consult minimal planner hook to choose next tool")
	ap.add_argument("--planner-min-confidence", type=float, default=0.5, help="Min confidence to execute the proposed tool")
	ap.add_argument("--tools-registry", default=str(root / "config" / "tools_refactor.json"), help="Path to tool registry JSON")
	ap.add_argument("--world-snapshot", default=str(root / "data" / "sandbox" / "tmp" / "world.json"), help="Path to world snapshot JSON")
	ap.add_argument("--max-steps", type=int, default=1)
	ap.add_argument("--apply-fixes", action="store_true")
	ap.add_argument("--require-tests", action="store_true", help="Count run as failure if pytest fails")
	ap.add_argument("--lint-paths", default="tests", help="Comma-separated paths for lint/type checks (defaults to 'tests')")
	ap.add_argument("--ignore-lint", action="store_true", help="Do not count flake8/mypy issues against success")


main

					except Exception:
						pass
					for c in (rec.get("critique", {}).get("issues") or []):
						categories[c] = categories.get(c, 0) + 1
					checks = rec.get("checks") or {}
					if isinstance(checks, dict):
						test_ok += 1 if bool(checks.get("test_ok", False)) else 0
						lint_ok += 1 if bool(checks.get("lint_ok", False)) else 0
						type_ok += 1 if bool(checks.get("type_ok", False)) else 0
	except Exception:
		pass
	ok_rate = float(round((float(ok) / max(1.0, float(total))), 3))
	test_ok_rate = float(round((float(test_ok) / max(1.0, float(total))), 3))
	lint_ok_rate = float(round((float(lint_ok) / max(1.0, float(total))), 3))
	type_ok_rate = float(round((float(type_ok) / max(1.0, float(total))), 3))
	p50 = float(round(sorted(elapsed)[len(elapsed)//2], 3)) if elapsed else 0.0
	p90 = float(round(sorted(elapsed)[int(len(elapsed)*0.9)], 3)) if elapsed else 0.0
	return {"runs": int(total), "ok": int(ok), "ok_rate": ok_rate, "test_ok_rate": test_ok_rate, "lint_ok_rate": lint_ok_rate, "type_ok_rate": type_ok_rate, "time_to_fix_p50": p50, "time_to_fix_p90": p90, "by_issue": categories}

def main() -> int:
	ap = argparse.ArgumentParser()
	# Repo root is two levels up from this script (agi_dw)
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--repo", default=str(root))
	ap.add_argument("--pkg", default="agi_dw")
	ap.add_argument("--only-tests", action="store_true")
	ap.add_argument("--out-traces", default=str(root / "data" / "devtools" / "traces.jsonl"))
	ap.add_argument("--out-metrics", default=str(root / "data" / "devtools" / "metrics.json"))
	ap.add_argument("--use-planner-hook", action="store_true", help="Consult minimal planner hook to choose next tool")
	ap.add_argument("--planner-min-confidence", type=float, default=0.5, help="Min confidence to execute the proposed tool")
	ap.add_argument("--tools-registry", default=str(root / "config" / "tools_refactor.json"), help="Path to tool registry JSON")
	ap.add_argument("--world-snapshot", default=str(root / "data" / "sandbox" / "tmp" / "world.json"), help="Path to world snapshot JSON")
	ap.add_argument("--max-steps", type=int, default=1)
	ap.add_argument("--apply-fixes", action="store_true")
	ap.add_argument("--require-tests", action="store_true", help="Count run as failure if pytest fails")
	ap.add_argument("--lint-paths", default="tests", help="Comma-separated paths for lint/type checks (defaults to 'tests')")
	ap.add_argument("--ignore-lint", action="store_true", help="Do not count flake8/mypy issues against success")
	ap.add_argument("--ignore-tests", action="store_true", help="Do not count pytest result against success")
	ap.add_argument("--skip-tests-pattern", default="", help="Pytest -k expression to exclude known failing tests (e.g., 'not slow and not failing_test')")
	ap.add_argument("--truncate", action="store_true", help="Truncate traces before writing (no history)")
	ap.add_argument("--run-refactor-skill", action="store_true", help="Force running refactor_modularization skill (dry-run)")
	args = ap.parse_args()

	repo = Path(args.repo)
	traces_path = Path(args.out_traces)
	traces_path.parent.mkdir(parents=True, exist_ok=True)
	if bool(args.truncate) and traces_path.exists():
		try:
			traces_path.unlink()
		except Exception:
			pass

	start = time.time()
	# Determine lint/type scope
	lint_paths: List[str] = []
	for token in str(args.lint_paths or "").split(","):
		t = token.strip()
		if not t:
			continue
		if (repo / t).exists():
			lint_paths.append(t)
	if not lint_paths:
		lint_paths = ["tests"] if (repo / "tests").exists() else ["."]

	# Run lints and tests
	fl = lt_run_flake8(repo_dir=repo, paths=lint_paths)
	my = lt_run_mypy(repo_dir=repo, paths=lint_paths)
	tr = TestRunner(str(repo))
	# Run pytest scoped to lint paths to avoid repo-wide failures during refactors
	pytest_args = list(lint_paths)
	if str(args.skip_tests_pattern or "").strip():
		pytest_args += ["-k", str(args.skip_tests_pattern)]
	t_res = tr.run_pytest(args=pytest_args)

	# Observation summarization
	stdout = (t_res.get("stdout") or "")
	stderr = (t_res.get("stderr") or "")
	lint_text = json.dumps({"flake8": fl, "mypy": my}, ensure_ascii=False)
	observation = {
		"kind": "code",
		"content": stdout[-5000:] + "\n" + lint_text[:5000],
		"meta": {"repo": str(repo)},
	}

	# Classify failures and propose fix or skill
	classify = classify_failures(stdout, stderr)
	plan: Dict[str, Any] = {"actions": []}
	applied = False
	diff_text = ""
	# If planner hook is enabled, consult it for a proposed next action (dry-run by default)
	proposal: Optional[Dict[str, Any]] = None
	if bool(args.use_planner_hook):
		try:
			proposal = planner_hook_propose(Path(args.tools_registry), Path(args.out_metrics), Path(args.world_snapshot))
			plan["proposal"] = proposal
		except Exception:
			proposal = None

	# If refactor skill requested/detected OR hook proposes it with sufficient confidence, run it (dry-run)
	skill_result: Optional[Dict[str, Any]] = None
	should_run_skill = bool(args.run_refactor_skill) or any(k in (classify.get("categories") or []) for k in ("scripts_alignment", "import_error"))
	if not should_run_skill and isinstance(proposal, dict):
		try:
			should_run_skill = (str(proposal.get("tool") or "") == "skill.refactor_modularization" and float(proposal.get("confidence", 0.0)) >= float(args.planner_min_confidence))
		except Exception:
			should_run_skill = False
	if should_run_skill:
		try:
			skill_result = run_refactor_skill(repo)
			plan["actions"].append({"tool": "skill.refactor_modularization", "args": {"dry_run": True}, "status": skill_result})
		except Exception:
			pass
	if args.apply_fixes and ("import_error" in classify.get("categories", [])):
		diff_text = make_import_rewrite_diff(repo, args.pkg, only_tests=bool(args.only_tests))
		if diff_text:
			res = apply_unified_diff(repo, diff_text, allow_globs=["**/*.py"], block_globs=["**/.venv/**", "**/.git/**"], max_files=200)
			applied = bool(res.get("ok"))
			plan["actions"].append({"tool": "ide.patch", "args": {"diff": "<omitted>", "status": res}})
	# Safe whitespace autofix: if flake8 has issues, attempt minimal whitespace cleanup
	if args.apply_fixes and not applied and isinstance(fl.get("issues"), list) and len(fl.get("issues", [])) > 0:
		wdiff = make_flake8_whitespace_fix_diff(repo, only_tests=bool(args.only_tests))
		if wdiff:
			res = apply_unified_diff(repo, wdiff, allow_globs=["**/*.py"], block_globs=["**/.venv/**", "**/.git/**"], max_files=200)
			applied = bool(res.get("ok"))
			plan["actions"].append({"tool": "ide.patch", "args": {"diff": "<omitted>", "status": res, "kind": "whitespace"}})
	# Remove trivially unused imports in tests for F401
	if args.apply_fixes and not applied and isinstance(fl.get("issues"), list) and len(fl.get("issues", [])) > 0:
		udiff = make_flake8_unused_import_fix_diff(repo, fl, only_tests=True)
		if udiff:
			res = apply_unified_diff(repo, udiff, allow_globs=["**/*.py"], block_globs=["**/.venv/**", "**/.git/**"], max_files=100)
			applied = bool(res.get("ok"))
			plan["actions"].append({"tool": "ide.patch", "args": {"diff": "<omitted>", "status": res, "kind": "flake8_unused"}})
	else:
		# No-op plan
		plan["actions"].append({"tool": "noop", "args": {}})

	# Validate by re-running after patch
	if applied:
		fl = lt_run_flake8(repo_dir=repo)
		my = lt_run_mypy(repo_dir=repo)
		t_res = tr.run_pytest()

	end = time.time()
	# Determine success with sensible defaults even if tools are missing
	rc = int(t_res.get("returncode", 1)) if isinstance(t_res, dict) else 1
	# Treat pytest rc=5 (no tests collected) as success for devtools bring-up
	no_tests = (rc == 5) or ("collected 0 items" in str(t_res.get("stdout", "")).lower())
	t_ok = True if (rc == 0 or no_tests or bool(args.ignore_tests)) else False
	if bool(args.ignore_lint):
		fl_ok = True
		my_ok = True
	else:
		fl_ok = (len(fl.get("issues", []) or []) == 0) if isinstance(fl, dict) else True
		my_ok = (len(my.get("errors", []) or []) == 0) if isinstance(my, dict) else True
	ok = bool(fl_ok and my_ok and (t_ok or (not bool(args.require_tests))))
	stdout_tail = (t_res.get("stdout") or "")[-2000:]
	stderr_tail = (t_res.get("stderr") or "")[-1000:]
	# Write debug tails for quick inspection
	try:
		(traces_path.parent / "last_stdout.txt").write_text(stdout_tail, encoding="utf-8")
		(traces_path.parent / "last_stderr.txt").write_text(stderr_tail, encoding="utf-8")
	except Exception:
		pass
	result = {"status": "ok" if ok else "error", "stdout_tail": stdout_tail, "stderr_tail": stderr_tail, "failures": t_res.get("failures")}
	rec = {
		"task_id": "devtools",
		"obs": observation,
		"plan": plan,
		"action": {"tool": "composite", "args": {}},
		"result": result,
		"reward": {"scalar": 1.0 if ok else 0.0, "components": {"success": 1 if ok else 0}},
		"critique": {"issues": classify.get("categories", []), "risk": 0.0, "proposal": "; ".join(classify.get("advice", []))},
		"checks": {"lint_ok": bool(fl_ok), "type_ok": bool(my_ok), "test_ok": bool(t_ok)},
		"elapsed_sec": float(round(max(0.0, end - start), 3)),
	}
	with traces_path.open("a", encoding="utf-8") as f:
		f.write(json.dumps(rec, ensure_ascii=False) + "\n")

	# Update metrics
	metrics = aggregate_metrics(traces_path)
	metrics_path = Path(args.out_metrics)
	metrics_path.parent.mkdir(parents=True, exist_ok=True)
	metrics_path.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "metrics": str(metrics_path), "traces": str(traces_path)}))
	return 0

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


from __future__ import annotations
import logging

import argparse
import json
from pathlib import Path
from shutil import copy2

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--outdir", default=str(root / "data" / "hitl_export"))
	args = ap.parse_args()

	q = root / "data" / "hitl" / "queue.jsonl"
	d = root / "data" / "hitl" / "decisions.jsonl"
	a = root / "data" / "hitl" / "audit.jsonl"
	out = Path(args.outdir)
	out.mkdir(parents=True, exist_ok=True)
	for p in (q, d, a):
		if p.exists():
			copy2(p, out / p.name)
	print(json.dumps({"ok": True, "out": str(out)}))
	return 0

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


from __future__ import annotations
import logging

import argparse
import json
from pathlib import Path
import subprocess

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--tasks", default=str(root / "data" / "ci" / "dev_repos.seeds.txt"))
	ap.add_argument("--timeout", type=int, default=120)
	args = ap.parse_args()

	seeds = []
	for ln in Path(args.tasks).read_text(encoding="utf-8").splitlines():
		ln = ln.strip()
		if not ln or ln.startswith("#"):
			continue
		seeds.append(ln)
	results = []
	for repo in seeds:
		cmd = ["python", str(root / "scripts" / "loops" / "run_loop_dev.py"), "--repo", repo, "--pytest-args", "-q", "--require-approval", "--approval-timeout", "2"]
		p = subprocess.run(cmd, cwd=str(root), capture_output=True, text=True, timeout=int(args.timeout))
		results.append({"repo": repo, "returncode": p.returncode})
	print(json.dumps({"ok": all(r["returncode"] == 0 for r in results), "results": results}))
	return 0

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


from __future__ import annotations
import logging

import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--decisions", default=str(root / "data" / "hitl" / "decisions.jsonl"))
	ap.add_argument("--audit", default=str(root / "data" / "hitl" / "audit.jsonl"))
	ap.add_argument("--min_tasks", type=int, default=15)
	args = ap.parse_args()

	decisions = 0
	violations = 0
	for p in [Path(args.decisions), Path(args.audit)]:
		if not p.exists():
			continue
		for line in p.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				obj = json.loads(line)
				action = str(obj.get("action", ""))
				if action.startswith("decision."):
					decisions += 1
				# Simple safety violation heuristic: forbid renames/mode changes markers in audit
				if action.startswith("patch.") and any(k in json.dumps(obj) for k in ["mode", "rename"]):
					violations += 1
			except Exception:
				continue
	ok = (decisions >= int(args.min_tasks)) and (violations == 0)
	print(json.dumps({"ok": ok, "decisions": decisions, "violations": violations, "min_tasks": int(args.min_tasks)}))
	return 0 if ok else 2

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


from __future__ import annotations
import logging

import json
from pathlib import Path

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	q = root / 'data' / 'hitl' / 'queue.jsonl'
	d = root / 'data' / 'hitl' / 'decisions.jsonl'
	a = root / 'data' / 'hitl' / 'audit.jsonl'
	def _count_lines(p: Path) -> int:
		try:
			return sum(1 for _ in p.open('r', encoding='utf-8')) if p.exists() else 0
		except Exception:
			return 0
	queue_total = _count_lines(q)
	decisions_total = _count_lines(d)
	audit_total = _count_lines(a)
	# Compute pending count and decision breakdown
	pending = 0
	try:
		if q.exists():
			for line in q.read_text(encoding='utf-8').splitlines():
				if not line.strip():
					continue
				obj = json.loads(line)
				if str(obj.get('status', 'pending')) == 'pending':
					pending += 1
	except Exception:
		pending = 0
	by_decision: dict[str, int] = {}
	try:
		if d.exists():
			for line in d.read_text(encoding='utf-8').splitlines():
				if not line.strip():
					continue
				obj = json.loads(line)
				dec = str(obj.get('decision', '')).lower()
				if dec:
					by_decision[dec] = int(by_decision.get(dec, 0)) + 1
	except Exception:
		by_decision = {}
	print(json.dumps({
		"queue_lines": queue_total,
		"pending": pending,
		"decisions": decisions_total,
		"by_decision": by_decision,
		"audit_records": audit_total,
	}, ensure_ascii=False))
	return 0

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


from __future__ import annotations
import logging

import json
from pathlib import Path

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	q = root / 'data' / 'hitl' / 'queue.jsonl'
	d = root / 'data' / 'hitl' / 'decisions.jsonl'
	a = root / 'data' / 'hitl' / 'audit.jsonl'
	def _count_lines(p: Path) -> int:
		try:
			return sum(1 for _ in p.open('r', encoding='utf-8')) if p.exists() else 0
		except Exception:
			return 0
	queue_total = _count_lines(q)
	decisions_total = _count_lines(d)
	audit_total = _count_lines(a)
	# Compute pending count and decision breakdown
	pending = 0
	try:
		if q.exists():
			for line in q.read_text(encoding='utf-8').splitlines():
				if not line.strip():
					continue
				obj = json.loads(line)
				if str(obj.get('status', 'pending')) == 'pending':
					pending += 1
	except Exception:
		pending = 0
	by_decision: dict[str, int] = {}
	try:
		if d.exists():
			for line in d.read_text(encoding='utf-8').splitlines():
				if not line.strip():


from __future__ import annotations
import logging

import argparse
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List
try:
	from agi_dw.core.hitl.audit_log import AuditLog
except ModuleNotFoundError:
	import sys as _sys  # type: ignore
	_proj_root = Path(__file__).resolve().parents[1]      # .../agi_dw
	_repo_root = Path(__file__).resolve().parents[2]      # .../
	for p in (str(_repo_root), str(_proj_root)):
		if p not in _sys.path:
			_sys.path.insert(0, p)
	from agi_dw.core.hitl.audit_log import AuditLog

@dataclass
class ApprovalItem:
	id: str
	ts: str
	status: str  # pending | approved | denied | modified | deferred
	kind: str  # code.patch | cli.op
	preview_path: str
	repo: str
	meta: Dict[str, Any]

class QueueIO:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.queue_path = root / "data" / "hitl" / "queue.jsonl"
		self.decisions_path = root / "data" / "hitl" / "decisions.jsonl"
		self.previews_dir = root / "data" / "hitl" / "previews"
		self.queue_path.parent.mkdir(parents=True, exist_ok=True)
		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),
		}))
	return 0

def cmd_show(io: QueueIO, args: argparse.Namespace) -> int:
	id_ = str(args.id)
	items = io.read_queue()
	obj = next((x for x in items if str(x.get("id")) == id_), None)
	if not obj:
		print(f"not found: {id_}")
		return 1
	pp = obj.get("preview_path")
	if pp and Path(pp).exists():
		print(Path(pp).read_text(encoding="utf-8"))
		# Also show brief meta if available
		meta = obj.get("meta") or {}
		try:
			risk = meta.get("risk", None)
			sig = meta.get("signature", None)
			if (risk is not None) or sig:
				print(json.dumps({"risk": risk, "signature": sig}, ensure_ascii=False))
		except Exception:
			pass
		return 0
	print(json.dumps(obj, ensure_ascii=False, indent=2))
	return 0

def _decision(io: QueueIO, id_: str, decision: str, note: str | None) -> int:
	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")
		return 1
	latest = sorted(pending, key=lambda x: str(x.get("ts", "")))[-1]
	return _decision(io, str(latest.get("id")), "approved", getattr(args, "note", ""))

def cmd_enqueue_dummy(io: QueueIO, args: argparse.Namespace) -> int:
	# Create a small preview file and enqueue a pending item for smoke testing
	id_ = now_ts()
	preview_path = io.previews_dir / f"dummy_{id_}.txt"
	preview_path.write_text("Dummy patch preview for smoke test", encoding="utf-8")
	item = ApprovalItem(
		id=id_,
		ts=id_,
		status="pending",
		kind="code.patch",
		preview_path=str(preview_path),
		repo=str(Path.cwd()),
		meta={"note": "smoke"},
	)
	io.append_queue(item)
	print(json.dumps(asdict(item)))
	return 0

def cmd_sweep_ttl(io: QueueIO, args: argparse.Namespace) -> int:
	# Defer import to avoid hard dependency during packaging
	try:
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	except ModuleNotFoundError:
		import sys as _sys  # type: ignore
		_proj_root = Path(__file__).resolve().parents[1]
		_repo_root = Path(__file__).resolve().parents[2]
		for p in (str(_repo_root), str(_proj_root)):
			if p not in _sys.path:
				_sys.path.insert(0, p)
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	root = Path(__file__).resolve().parents[2]
	q = ApprovalQueue(root)
	ttl = int(getattr(args, "ttl", 600) or 600)
	wrote = q.sweep_expired(ttl_sec=ttl)
	print(json.dumps({"swept": len(wrote), "ttl_sec": ttl}))
	return 0

def main(argv: list[str] | None = None) -> int:
	root = Path(__file__).resolve().parents[2]
	parser = argparse.ArgumentParser(description="HITL approver CLI")
	parser.set_defaults(func=None)
	sub = parser.add_subparsers()

	sp = sub.add_parser("list", help="list pending items")
	sp.set_defaults(func=cmd_list)

	sp = sub.add_parser("show", help="show preview for an item")
	sp.add_argument("--id", required=True)
	sp.set_defaults(func=cmd_show)

	sp = sub.add_parser("approve", help="approve item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_approve)

	sp = sub.add_parser("deny", help="deny item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_deny)

	sp = sub.add_parser("defer", help="defer item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_defer)

	sp = sub.add_parser("modify", help="mark item as modified with note")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_modify)

	sp = sub.add_parser("sweep-ttl", help="expire old pending items by TTL (seconds)")
	sp.add_argument("--ttl", type=int, default=600)
	sp.set_defaults(func=cmd_sweep_ttl)

	sp = sub.add_parser("approve-latest", help="approve latest pending item")
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_approve_latest)

	sp = sub.add_parser("enqueue-dummy", help="enqueue a dummy pending item for smoke test")
	sp.set_defaults(func=cmd_enqueue_dummy)

	args = parser.parse_args(argv)
	io = QueueIO(root)
	if not hasattr(args, "func") or args.func is None:
		parser.print_help()
		return 2
	return int(args.func(io, args) or 0)

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


ApprovalItem

import argparse
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List
try:
	from agi_dw.core.hitl.audit_log import AuditLog
except ModuleNotFoundError:
	import sys as _sys  # type: ignore
	_proj_root = Path(__file__).resolve().parents[1]      # .../agi_dw
	_repo_root = Path(__file__).resolve().parents[2]      # .../
	for p in (str(_repo_root), str(_proj_root)):
		if p not in _sys.path:
			_sys.path.insert(0, p)
	from agi_dw.core.hitl.audit_log import AuditLog

@dataclass
class ApprovalItem:
	id: str
	ts: str
	status: str  # pending | approved | denied | modified | deferred
	kind: str  # code.patch | cli.op
	preview_path: str
	repo: str
	meta: Dict[str, Any]

class QueueIO:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.queue_path = root / "data" / "hitl" / "queue.jsonl"
		self.decisions_path = root / "data" / "hitl" / "decisions.jsonl"
		self.previews_dir = root / "data" / "hitl" / "previews"
		self.queue_path.parent.mkdir(parents=True, exist_ok=True)
		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:


QueueIO

	import sys as _sys  # type: ignore
	_proj_root = Path(__file__).resolve().parents[1]      # .../agi_dw
	_repo_root = Path(__file__).resolve().parents[2]      # .../
	for p in (str(_repo_root), str(_proj_root)):
		if p not in _sys.path:
			_sys.path.insert(0, p)
	from agi_dw.core.hitl.audit_log import AuditLog

@dataclass
class ApprovalItem:
	id: str
	ts: str
	status: str  # pending | approved | denied | modified | deferred
	kind: str  # code.patch | cli.op
	preview_path: str
	repo: str
	meta: Dict[str, Any]

class QueueIO:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.queue_path = root / "data" / "hitl" / "queue.jsonl"
		self.decisions_path = root / "data" / "hitl" / "decisions.jsonl"
		self.previews_dir = root / "data" / "hitl" / "previews"
		self.queue_path.parent.mkdir(parents=True, exist_ok=True)
		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),
		}))
	return 0


	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),
		}))
	return 0

def cmd_show(io: QueueIO, args: argparse.Namespace) -> int:
	id_ = str(args.id)
	items = io.read_queue()
	obj = next((x for x in items if str(x.get("id")) == id_), None)


				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),
		}))
	return 0

def cmd_show(io: QueueIO, args: argparse.Namespace) -> int:
	id_ = str(args.id)
	items = io.read_queue()
	obj = next((x for x in items if str(x.get("id")) == id_), None)
	if not obj:
		print(f"not found: {id_}")
		return 1
	pp = obj.get("preview_path")
	if pp and Path(pp).exists():
		print(Path(pp).read_text(encoding="utf-8"))
		# Also show brief meta if available
		meta = obj.get("meta") or {}
		try:
			risk = meta.get("risk", None)
			sig = meta.get("signature", None)
			if (risk is not None) or sig:
				print(json.dumps({"risk": risk, "signature": sig}, ensure_ascii=False))
		except Exception:


def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),
		}))
	return 0

def cmd_show(io: QueueIO, args: argparse.Namespace) -> int:
	id_ = str(args.id)
	items = io.read_queue()
	obj = next((x for x in items if str(x.get("id")) == id_), None)
	if not obj:
		print(f"not found: {id_}")
		return 1
	pp = obj.get("preview_path")
	if pp and Path(pp).exists():
		print(Path(pp).read_text(encoding="utf-8"))
		# Also show brief meta if available
		meta = obj.get("meta") or {}
		try:
			risk = meta.get("risk", None)
			sig = meta.get("signature", None)
			if (risk is not None) or sig:
				print(json.dumps({"risk": risk, "signature": sig}, ensure_ascii=False))
		except Exception:
			pass
		return 0
	print(json.dumps(obj, ensure_ascii=False, indent=2))
	return 0

def _decision(io: QueueIO, id_: str, decision: str, note: str | None) -> int:
	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:


	if not obj:
		print(f"not found: {id_}")
		return 1
	pp = obj.get("preview_path")
	if pp and Path(pp).exists():
		print(Path(pp).read_text(encoding="utf-8"))
		# Also show brief meta if available
		meta = obj.get("meta") or {}
		try:
			risk = meta.get("risk", None)
			sig = meta.get("signature", None)
			if (risk is not None) or sig:
				print(json.dumps({"risk": risk, "signature": sig}, ensure_ascii=False))
		except Exception:
			pass
		return 0
	print(json.dumps(obj, ensure_ascii=False, indent=2))
	return 0

def _decision(io: QueueIO, id_: str, decision: str, note: str | None) -> int:
	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()


			risk = meta.get("risk", None)
			sig = meta.get("signature", None)
			if (risk is not None) or sig:
				print(json.dumps({"risk": risk, "signature": sig}, ensure_ascii=False))
		except Exception:
			pass
		return 0
	print(json.dumps(obj, ensure_ascii=False, indent=2))
	return 0

def _decision(io: QueueIO, id_: str, decision: str, note: str | None) -> int:
	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")


		except Exception:
			pass
		return 0
	print(json.dumps(obj, ensure_ascii=False, indent=2))
	return 0

def _decision(io: QueueIO, id_: str, decision: str, note: str | None) -> int:
	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")
		return 1
	latest = sorted(pending, key=lambda x: str(x.get("ts", "")))[-1]
	return _decision(io, str(latest.get("id")), "approved", getattr(args, "note", ""))


	return 0

def _decision(io: QueueIO, id_: str, decision: str, note: str | None) -> int:
	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")
		return 1
	latest = sorted(pending, key=lambda x: str(x.get("ts", "")))[-1]
	return _decision(io, str(latest.get("id")), "approved", getattr(args, "note", ""))

def cmd_enqueue_dummy(io: QueueIO, args: argparse.Namespace) -> int:
	# Create a small preview file and enqueue a pending item for smoke testing
	id_ = now_ts()


	rec = {"id": id_, "decision": decision, "note": note or "", "ts": now_ts()}
	io.append_decision(rec)
	# Write to immutable audit log
	AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor="approver", data=rec)
	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")
		return 1
	latest = sorted(pending, key=lambda x: str(x.get("ts", "")))[-1]
	return _decision(io, str(latest.get("id")), "approved", getattr(args, "note", ""))

def cmd_enqueue_dummy(io: QueueIO, args: argparse.Namespace) -> int:
	# Create a small preview file and enqueue a pending item for smoke testing
	id_ = now_ts()
	preview_path = io.previews_dir / f"dummy_{id_}.txt"
	preview_path.write_text("Dummy patch preview for smoke test", encoding="utf-8")
	item = ApprovalItem(
		id=id_,


	print(json.dumps(rec))
	return 0

def cmd_approve(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "approved", args.note)

def cmd_deny(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "denied", args.note)

def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")
		return 1
	latest = sorted(pending, key=lambda x: str(x.get("ts", "")))[-1]
	return _decision(io, str(latest.get("id")), "approved", getattr(args, "note", ""))

def cmd_enqueue_dummy(io: QueueIO, args: argparse.Namespace) -> int:
	# Create a small preview file and enqueue a pending item for smoke testing
	id_ = now_ts()
	preview_path = io.previews_dir / f"dummy_{id_}.txt"
	preview_path.write_text("Dummy patch preview for smoke test", encoding="utf-8")
	item = ApprovalItem(
		id=id_,
		ts=id_,
		status="pending",
		kind="code.patch",
		preview_path=str(preview_path),
		repo=str(Path.cwd()),
		meta={"note": "smoke"},
	)
	io.append_queue(item)
	print(json.dumps(asdict(item)))
	return 0


def cmd_defer(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "deferred", args.note)

def cmd_modify(io: QueueIO, args: argparse.Namespace) -> int:
	return _decision(io, str(args.id), "modified", args.note)

def cmd_approve_latest(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	# pick latest pending by ts/id ordering
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	if not pending:
		print("no pending items")
		return 1
	latest = sorted(pending, key=lambda x: str(x.get("ts", "")))[-1]
	return _decision(io, str(latest.get("id")), "approved", getattr(args, "note", ""))

def cmd_enqueue_dummy(io: QueueIO, args: argparse.Namespace) -> int:
	# Create a small preview file and enqueue a pending item for smoke testing
	id_ = now_ts()
	preview_path = io.previews_dir / f"dummy_{id_}.txt"
	preview_path.write_text("Dummy patch preview for smoke test", encoding="utf-8")
	item = ApprovalItem(
		id=id_,
		ts=id_,
		status="pending",
		kind="code.patch",
		preview_path=str(preview_path),
		repo=str(Path.cwd()),
		meta={"note": "smoke"},
	)
	io.append_queue(item)
	print(json.dumps(asdict(item)))
	return 0

def cmd_sweep_ttl(io: QueueIO, args: argparse.Namespace) -> int:
	# Defer import to avoid hard dependency during packaging
	try:
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	except ModuleNotFoundError:
		import sys as _sys  # type: ignore
		_proj_root = Path(__file__).resolve().parents[1]
		_repo_root = Path(__file__).resolve().parents[2]
		for p in (str(_repo_root), str(_proj_root)):
			if p not in _sys.path:
				_sys.path.insert(0, p)
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	root = Path(__file__).resolve().parents[2]
	q = ApprovalQueue(root)
	ttl = int(getattr(args, "ttl", 600) or 600)
	wrote = q.sweep_expired(ttl_sec=ttl)
	print(json.dumps({"swept": len(wrote), "ttl_sec": ttl}))
	return 0


def cmd_enqueue_dummy(io: QueueIO, args: argparse.Namespace) -> int:
	# Create a small preview file and enqueue a pending item for smoke testing
	id_ = now_ts()
	preview_path = io.previews_dir / f"dummy_{id_}.txt"
	preview_path.write_text("Dummy patch preview for smoke test", encoding="utf-8")
	item = ApprovalItem(
		id=id_,
		ts=id_,
		status="pending",
		kind="code.patch",
		preview_path=str(preview_path),
		repo=str(Path.cwd()),
		meta={"note": "smoke"},
	)
	io.append_queue(item)
	print(json.dumps(asdict(item)))
	return 0

def cmd_sweep_ttl(io: QueueIO, args: argparse.Namespace) -> int:
	# Defer import to avoid hard dependency during packaging
	try:
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	except ModuleNotFoundError:
		import sys as _sys  # type: ignore
		_proj_root = Path(__file__).resolve().parents[1]
		_repo_root = Path(__file__).resolve().parents[2]
		for p in (str(_repo_root), str(_proj_root)):
			if p not in _sys.path:
				_sys.path.insert(0, p)
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	root = Path(__file__).resolve().parents[2]
	q = ApprovalQueue(root)
	ttl = int(getattr(args, "ttl", 600) or 600)
	wrote = q.sweep_expired(ttl_sec=ttl)
	print(json.dumps({"swept": len(wrote), "ttl_sec": ttl}))
	return 0

def main(argv: list[str] | None = None) -> int:
	root = Path(__file__).resolve().parents[2]
	parser = argparse.ArgumentParser(description="HITL approver CLI")
	parser.set_defaults(func=None)
	sub = parser.add_subparsers()

	sp = sub.add_parser("list", help="list pending items")
	sp.set_defaults(func=cmd_list)

	sp = sub.add_parser("show", help="show preview for an item")
	sp.add_argument("--id", required=True)
	sp.set_defaults(func=cmd_show)

	sp = sub.add_parser("approve", help="approve item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_approve)


main

def cmd_sweep_ttl(io: QueueIO, args: argparse.Namespace) -> int:
	# Defer import to avoid hard dependency during packaging
	try:
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	except ModuleNotFoundError:
		import sys as _sys  # type: ignore
		_proj_root = Path(__file__).resolve().parents[1]
		_repo_root = Path(__file__).resolve().parents[2]
		for p in (str(_repo_root), str(_proj_root)):
			if p not in _sys.path:
				_sys.path.insert(0, p)
		from agi_dw.core.hitl.approval_queue import ApprovalQueue  # type: ignore
	root = Path(__file__).resolve().parents[2]
	q = ApprovalQueue(root)
	ttl = int(getattr(args, "ttl", 600) or 600)
	wrote = q.sweep_expired(ttl_sec=ttl)
	print(json.dumps({"swept": len(wrote), "ttl_sec": ttl}))
	return 0

def main(argv: list[str] | None = None) -> int:
	root = Path(__file__).resolve().parents[2]
	parser = argparse.ArgumentParser(description="HITL approver CLI")
	parser.set_defaults(func=None)
	sub = parser.add_subparsers()

	sp = sub.add_parser("list", help="list pending items")
	sp.set_defaults(func=cmd_list)

	sp = sub.add_parser("show", help="show preview for an item")
	sp.add_argument("--id", required=True)
	sp.set_defaults(func=cmd_show)

	sp = sub.add_parser("approve", help="approve item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_approve)

	sp = sub.add_parser("deny", help="deny item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_deny)

	sp = sub.add_parser("defer", help="defer item")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_defer)

	sp = sub.add_parser("modify", help="mark item as modified with note")
	sp.add_argument("--id", required=True)
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_modify)

	sp = sub.add_parser("sweep-ttl", help="expire old pending items by TTL (seconds)")
	sp.add_argument("--ttl", type=int, default=600)
	sp.set_defaults(func=cmd_sweep_ttl)

	sp = sub.add_parser("approve-latest", help="approve latest pending item")
	sp.add_argument("--note", default="")
	sp.set_defaults(func=cmd_approve_latest)

	sp = sub.add_parser("enqueue-dummy", help="enqueue a dummy pending item for smoke test")
	sp.set_defaults(func=cmd_enqueue_dummy)

	args = parser.parse_args(argv)
	io = QueueIO(root)
	if not hasattr(args, "func") or args.func is None:
		parser.print_help()
		return 2
	return int(args.func(io, args) or 0)

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


	_proj_root = Path(__file__).resolve().parents[1]      # .../agi_dw
	_repo_root = Path(__file__).resolve().parents[2]      # .../
	for p in (str(_repo_root), str(_proj_root)):
		if p not in _sys.path:
			_sys.path.insert(0, p)
	from agi_dw.core.hitl.audit_log import AuditLog

@dataclass
class ApprovalItem:
	id: str
	ts: str
	status: str  # pending | approved | denied | modified | deferred
	kind: str  # code.patch | cli.op
	preview_path: str
	repo: str
	meta: Dict[str, Any]

class QueueIO:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.queue_path = root / "data" / "hitl" / "queue.jsonl"
		self.decisions_path = root / "data" / "hitl" / "decisions.jsonl"
		self.previews_dir = root / "data" / "hitl" / "previews"
		self.queue_path.parent.mkdir(parents=True, exist_ok=True)
		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")


	ts: str
	status: str  # pending | approved | denied | modified | deferred
	kind: str  # code.patch | cli.op
	preview_path: str
	repo: str
	meta: Dict[str, Any]

class QueueIO:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.queue_path = root / "data" / "hitl" / "queue.jsonl"
		self.decisions_path = root / "data" / "hitl" / "decisions.jsonl"
		self.previews_dir = root / "data" / "hitl" / "previews"
		self.queue_path.parent.mkdir(parents=True, exist_ok=True)
		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),


		self.queue_path = root / "data" / "hitl" / "queue.jsonl"
		self.decisions_path = root / "data" / "hitl" / "decisions.jsonl"
		self.previews_dir = root / "data" / "hitl" / "previews"
		self.queue_path.parent.mkdir(parents=True, exist_ok=True)
		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),


		self.previews_dir.mkdir(parents=True, exist_ok=True)
		for p in (self.queue_path, self.decisions_path):
			if not p.exists():
				p.write_text("", encoding="utf-8")

	def read_queue(self) -> List[Dict[str, Any]]:
		items: List[Dict[str, Any]] = []
		for line in self.queue_path.read_text(encoding="utf-8").splitlines():
			if not line.strip():
				continue
			try:
				items.append(json.loads(line))
			except Exception:
				continue
		return items

	def append_queue(self, item: ApprovalItem) -> None:
		with self.queue_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")

	def append_decision(self, rec: Dict[str, Any]) -> None:
		with self.decisions_path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(rec, ensure_ascii=False) + "\n")

def now_ts() -> str:
	return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")

def cmd_list(io: QueueIO, args: argparse.Namespace) -> int:
	items = io.read_queue()
	pending = [x for x in items if str(x.get("status", "pending")) == "pending"]
	for x in pending:
		print(json.dumps({
			"id": x.get("id"),
			"ts": x.get("ts"),
			"kind": x.get("kind"),
			"repo": x.get("repo"),
			"preview": x.get("preview_path"),
		}))
	return 0


import logging
import argparse
import json
from pathlib import Path

def _is_rate(x) -> bool:
	try:
		v = float(x)
		return 0.0 <= v <= 1.0
	except Exception:
		return False

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--summary", default=str(root / "data" / "dashboards" / "summary.json"))
	args = ap.parse_args()

	p = Path(args.summary)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "dashboard_missing", "path": str(p)}))
		return 1
	try:
		obj = json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		print(json.dumps({"ok": False, "error": "invalid_json", "path": str(p)}))
		return 1
	# Required top-level keys
	required_keys = ["bench", "wm", "verifier", "planner_self_eval", "practice", "registry", "external"]
	missing = [k for k in required_keys if k not in obj]
	valid = (len(missing) == 0)
	# Basic rate checks when present
	bench = obj.get("bench", {}) if isinstance(obj, dict) else {}
	cli_sum = bench.get("cli_summary", {}) if isinstance(bench, dict) else {}
	dom_sum = bench.get("dom_summary", {}) if isinstance(bench, dict) else {}
	off_sum = bench.get("office_summary", {}) if isinstance(bench, dict) else {}
	rates = {}
	for name, blk in (("cli", cli_sum), ("dom", dom_sum), ("office", off_sum)):
		if isinstance(blk, dict) and "success_rate" in blk:
			r = blk.get("success_rate")
			rates[name] = r
			valid = valid and _is_rate(r)
	external = obj.get("external", {}) if isinstance(obj, dict) else {}
	if isinstance(external, dict) and "success_rate" in external:
		valid = valid and _is_rate(external.get("success_rate"))
	print(json.dumps({
		"ok": bool(valid),
		"missing_keys": missing,
		"rates": rates,
		"checked": {"summary": str(p)},
	}))
	return 0 if valid else 1

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


import logging
import argparse
import json
from pathlib import Path

def _is_rate(x) -> bool:
	try:
		v = float(x)
		return 0.0 <= v <= 1.0
	except Exception:
		return False

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--summary", default=str(root / "data" / "dashboards" / "summary.json"))
	args = ap.parse_args()

	p = Path(args.summary)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "dashboard_missing", "path": str(p)}))
		return 1
	try:
		obj = json.loads(p.read_text(encoding="utf-8"))
	except Exception:
		print(json.dumps({"ok": False, "error": "invalid_json", "path": str(p)}))
		return 1
	# Required top-level keys
	required_keys = ["bench", "wm", "verifier", "planner_self_eval", "practice", "registry", "external"]
	missing = [k for k in required_keys if k not in obj]


from __future__ import annotations
import logging

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

def load_json(path: Path) -> Any:
	try:
		return json.loads(path.read_text(encoding="utf-8"))
	except Exception:
		return None

def main() -> int:
	ap = argparse.ArgumentParser()
	repo_root = Path(__file__).resolve().parents[1]
	ap.add_argument("--scripts-json", default=str(repo_root / "data" / "sandbox" / "tmp" / "scripts_index.json"))
	ap.add_argument("--docs-md", default=str(repo_root / "docs" / "scripts_index.md"))
	ap.add_argument("--max-missing-doc", type=int, default=0, help="max scripts allowed without doc summary")
	ap.add_argument("--require-main", action="store_true", help="require a main() function if runnable")
	args = ap.parse_args()

	scripts = load_json(Path(args.scripts_json)) or []
	missing_doc: List[str] = []
	main_missing: List[str] = []
	for it in scripts:
		name = it.get("name")
		doc = (it.get("doc") or "").strip()
		if not doc:
			missing_doc.append(name)
		if args.require_main and it.get("runnable") and not it.get("has_main"):
			main_missing.append(name)

	ok = True
	if len(missing_doc) > args.max_missing_doc:
		ok = False
	if args.require_main and main_missing:
		ok = False

	out = {
		"ok": ok,
		"scripts": len(scripts),
		"missing_doc": missing_doc,
		"main_missing": main_missing,
		"docs_index_md": args.docs_md,
	}
	print(json.dumps(out))
	return 0 if ok else 1

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


from __future__ import annotations
import logging

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

def load_json(path: Path) -> Any:
	try:
		return json.loads(path.read_text(encoding="utf-8"))
	except Exception:
		return None

def main() -> int:
	ap = argparse.ArgumentParser()
	repo_root = Path(__file__).resolve().parents[1]
	ap.add_argument("--scripts-json", default=str(repo_root / "data" / "sandbox" / "tmp" / "scripts_index.json"))
	ap.add_argument("--docs-md", default=str(repo_root / "docs" / "scripts_index.md"))
	ap.add_argument("--max-missing-doc", type=int, default=0, help="max scripts allowed without doc summary")
	ap.add_argument("--require-main", action="store_true", help="require a main() function if runnable")
	args = ap.parse_args()

	scripts = load_json(Path(args.scripts_json)) or []
	missing_doc: List[str] = []
	main_missing: List[str] = []
	for it in scripts:
		name = it.get("name")
		doc = (it.get("doc") or "").strip()
		if not doc:
			missing_doc.append(name)
		if args.require_main and it.get("runnable") and not it.get("has_main"):


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--metrics", default=str(root / "data" / "planner_prefs" / "metrics.json"))
	ap.add_argument("--min-sr", type=float, default=0.5, help="Minimum best success rate to pass gate [0..1]")
	args = ap.parse_args()

	mp = Path(args.metrics)
	if not mp.exists():
		print(json.dumps({"ok": False, "reason": "metrics_not_found", "metrics": str(mp)}))
		return 2
	try:
		m = json.loads(mp.read_text(encoding="utf-8"))
	except Exception:
		print(json.dumps({"ok": False, "reason": "metrics_parse_error", "metrics": str(mp)}))
		return 2
	best_sr = float(m.get("best_success_rate", m.get("best_score", 0.0)))
	passed = bool(best_sr >= float(args.min_sr))
	out = {"ok": passed, "best_success_rate": best_sr, "min_sr": float(args.min_sr)}
	print(json.dumps(out))
	return 0 if passed else 1

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


#!/usr/bin/env python3
import logging
import argparse
import json
from pathlib import Path

def parse_args():
	root = Path(__file__).resolve().parents[1]
	ap = argparse.ArgumentParser(description="CI gate for contamination rate and coverage avg")
	ap.add_argument("--results", default=str(root / "data" / "llm_bench" / "code_results.json"))
	ap.add_argument("--contam", default=str(root / "data" / "llm_bench" / "contamination.json"))
	ap.add_argument("--max-contam-rate", type=float, default=1.0)
	ap.add_argument("--min-coverage", type=float, default=0.0)
	return ap.parse_args()

def main() -> int:
	args = parse_args()
	failures = []
	# Contamination
	try:
		co = json.loads(Path(args.contam).read_text(encoding="utf-8"))
		rate = float(co.get("rate", 0.0))
		if rate > float(args.max_contam_rate):
			failures.append({"metric": "contamination_rate", "value": rate, "max": float(args.max_contam_rate)})
	except Exception:
		pass
	# Coverage
	try:
		res = json.loads(Path(args.results).read_text(encoding="utf-8"))
		covs = []
		for name, data in (res.get("benchmarks") or {}).items():
			c = data.get("coverage_avg")
			if isinstance(c, (int, float)):
				covs.append(float(c))
		if covs:
			cov_avg = sum(covs) / max(1, len(covs))
			if cov_avg < float(args.min_coverage):
				failures.append({"metric": "coverage_avg", "value": cov_avg, "min": float(args.min_coverage)})
	except Exception:
		pass
	ok = len(failures) == 0
	print(json.dumps({"ok": ok, "failures": failures}))
	return 0 if ok else 1

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


#!/usr/bin/env python3
import logging
import argparse
import json
from pathlib import Path

def parse_args():
	root = Path(__file__).resolve().parents[1]
	ap = argparse.ArgumentParser(description="CI gate for contamination rate and coverage avg")
	ap.add_argument("--results", default=str(root / "data" / "llm_bench" / "code_results.json"))
	ap.add_argument("--contam", default=str(root / "data" / "llm_bench" / "contamination.json"))
	ap.add_argument("--max-contam-rate", type=float, default=1.0)
	ap.add_argument("--min-coverage", type=float, default=0.0)
	return ap.parse_args()

def main() -> int:
	args = parse_args()
	failures = []
	# Contamination
	try:
		co = json.loads(Path(args.contam).read_text(encoding="utf-8"))
		rate = float(co.get("rate", 0.0))
		if rate > float(args.max_contam_rate):
			failures.append({"metric": "contamination_rate", "value": rate, "max": float(args.max_contam_rate)})
	except Exception:
		pass
	# Coverage
	try:
		res = json.loads(Path(args.results).read_text(encoding="utf-8"))
		covs = []
		for name, data in (res.get("benchmarks") or {}).items():
			c = data.get("coverage_avg")
			if isinstance(c, (int, float)):


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--results", default=str(root / "data" / "benchmarks" / "external_results.jsonl"))
	ap.add_argument("--min-total", type=int, default=1)
	ap.add_argument("--min-ok", type=int, default=0)
	ap.add_argument("--min-rate", type=float, default=0.0)
	args = ap.parse_args()

	p = Path(args.results)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "external_missing", "path": str(p)}))
		return 1
	total = 0
	ok = 0
	by_domain = {}
	with p.open("r", encoding="utf-8") as f:
		for line in f:
			line = line.strip()
			if not line:
				continue
			try:
				rec = json.loads(line)
			except Exception:
				continue
			dom = str(rec.get("domain", ""))
			res = rec.get("result", {}) if isinstance(rec, dict) else {}
			status = str(res.get("status", "")).lower()
			total += 1
			if dom:
				dd = by_domain.get(dom, {"total": 0, "ok": 0})
				dd["total"] += 1
				by_domain[dom] = dd
			if status == "ok":
				ok += 1
				if dom:
					by_domain[dom]["ok"] += 1
	pass_rate = (float(ok) / max(1.0, float(total)))
	overall_ok = (total >= int(args.min_total)) and (ok >= int(args.min_ok)) and (pass_rate >= float(args.min_rate))
	print(json.dumps({
		"ok": bool(overall_ok),
		"total": int(total),
		"ok_n": int(ok),
		"pass_rate": pass_rate,
		"thresholds": {"min_total": int(args.min_total), "min_ok": int(args.min_ok), "min_rate": float(args.min_rate)},
		"by_domain": by_domain,
	}))
	return 0 if overall_ok else 1

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


import logging
import argparse
import json
import subprocess
from pathlib import Path

def run(cmd: list[str]) -> subprocess.CompletedProcess:
	return subprocess.run(cmd, capture_output=True, text=True)

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--domain", choices=["dom"], default="dom")
	ap.add_argument("--out", default=str(root / "data" / "traces" / "web_dom.verified.jsonl"))
	args = ap.parse_args()

	if args.domain == "dom":
		# Seed from YAML only
		seed = run(["python3", str(root / "scripts" / "seed_web_dom.py"), "--only-yaml", "--out", str(root / "data" / "traces" / "web_dom.jsonl")])
		if seed.returncode != 0:
			print(seed.stderr)
			return 1
		# Verify with strict HF path
		ver = run([
			"python3", str(root / "scripts" / "verify_traces.py"),
			str(root / "data" / "traces" / "web_dom.jsonl"),
			str(root / "data" / "traces" / "web_dom.verified.jsonl"),
			"--backend", "hf",
			"--structured", "json",
			"--model", "meta-llama/Llama-3.2-3B",
			"--timeout", "30",
			"--warmup",
			"--max", "250",
		])
		if ver.returncode != 0:
			print(ver.stderr)
			return 2
		print(ver.stdout[-4000:])
		# Summarize
		try:
			from agi_dw.scripts.eval.ci_assert_dom_verify import main as dom_ci  # type: ignore
			dom_ci()
		except Exception:
			pass
		print(json.dumps({"verified": str(root / "data" / "traces" / "web_dom.verified.jsonl")}, ensure_ascii=False))
		return 0
	return 0

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


run

import logging
import argparse
import json
import subprocess
from pathlib import Path

def run(cmd: list[str]) -> subprocess.CompletedProcess:
	return subprocess.run(cmd, capture_output=True, text=True)

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--domain", choices=["dom"], default="dom")
	ap.add_argument("--out", default=str(root / "data" / "traces" / "web_dom.verified.jsonl"))
	args = ap.parse_args()

	if args.domain == "dom":
		# Seed from YAML only
		seed = run(["python3", str(root / "scripts" / "seed_web_dom.py"), "--only-yaml", "--out", str(root / "data" / "traces" / "web_dom.jsonl")])
		if seed.returncode != 0:
			print(seed.stderr)
			return 1
		# Verify with strict HF path
		ver = run([
			"python3", str(root / "scripts" / "verify_traces.py"),
			str(root / "data" / "traces" / "web_dom.jsonl"),
			str(root / "data" / "traces" / "web_dom.verified.jsonl"),


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	parser = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	parser.add_argument("--verified", default=str(root / "data" / "traces" / "web_dom.verified.jsonl"))
	parser.add_argument("--min-ok", type=int, default=2)
	args = parser.parse_args()

	path = Path(args.verified)
	if not path.exists():
		print(f"verified file not found: {path}")
		return 2

	total = 0
	ok = 0
	risks: list[float] = []
	with path.open("r", encoding="utf-8") as fin:
		for line in fin:
			line = line.strip()
			if not line:
				continue
			try:
				obj = json.loads(line)
			except Exception:
				continue
			total += 1
			# Count success by result.status if present and optional content assertion
			try:
				status = str(obj.get("result", {}).get("status", "")).lower()
				passed = (status == "ok")
				# Enforce optional assertion: obs.meta.assert.contains substring in result.dom
				try:
					meta = (obj.get("obs", {}) or {}).get("meta", {})
					assert_cfg = (meta or {}).get("assert", {})
					if isinstance(assert_cfg, dict) and "contains" in assert_cfg:
						needle = str(assert_cfg.get("contains", "")).strip()
						if needle:
							text = str(obj.get("result", {}).get("dom", ""))
							if needle not in text:
								passed = False
				except Exception:
					pass
				if passed:
					ok += 1
			except Exception:
				pass
			# Track risk if present
			try:
				risks.append(float(obj.get("critique", {}).get("risk", 0.5)))
			except Exception:
				pass

	avg_risk = sum(risks) / len(risks) if risks else 0.5
	print(json.dumps({"total": total, "ok": ok, "avg_risk": avg_risk}, ensure_ascii=False))

	if ok < int(args.min_ok):
		print(f"CI gate failed: ok={ok} < min_ok={args.min_ok}")
		return 1
	return 0

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


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--summary", default=str(root / "data" / "traces" / "summary.json"))
	ap.add_argument("--min-success", type=float, default=0.7)
	args = ap.parse_args()

	p = Path(args.summary)
	if not p.exists():
		print("summary not found:", str(p))
		return 2
	try:
		obj = json.loads(p.read_text(encoding="utf-8"))
	except Exception as e:
		print("failed to parse summary:", e)
		return 2
	sr = float(obj.get("success_rate") or obj.get("metrics", {}).get("success_rate") or 0.0)
	print(json.dumps({"success_rate": sr, "min_success": float(args.min_success)}, ensure_ascii=False))
	if sr < float(args.min_success):
		print(f"CI gate failed: success_rate={sr:.3f} < min={args.min_success:.3f}")
		return 1
	return 0

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


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--summary", default=str(root / "data" / "dashboards" / "summary.json"))
	ap.add_argument("--min-cli", type=float, default=0.5)
	ap.add_argument("--min-dom", type=float, default=0.5)
	ap.add_argument("--min-office", type=float, default=None)
	ap.add_argument("--use-budgeted", action="store_true", help="Gate on budgeted/effective success rates if present")
	ap.add_argument("--min-mem-hit", type=float, default=None, help="Optional minimum memory hit rate [0..1]")
	ap.add_argument("--max-cli-over-budget", type=int, default=None, help="Optional max total over-budget runs in CLI summary")
	ap.add_argument("--max-dom-over-budget", type=int, default=None, help="Optional max total over-budget runs in DOM summary")
	ap.add_argument("--min-batch-speedup", type=float, default=None, help="Optional minimum speedup required when using batched verifier (from batch_audit)")
	args = ap.parse_args()

	p = Path(args.summary)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "dashboard_missing", "path": str(p)}))
		return 1
	s = json.loads(p.read_text(encoding="utf-8"))
	bench = s.get("bench", {})
	cli = bench.get("cli_summary", {})
	dom = bench.get("dom_summary", {})
	if bool(args.use_budgeted):
		cli_rate = float(cli.get("success_rate_effective", cli.get("success_rate_budgeted", cli.get("success_rate", 0.0))))
		dom_rate = float(dom.get("success_rate_effective", dom.get("success_rate_budgeted", dom.get("success_rate", 0.0))))
	else:
		cli_rate = float(cli.get("success_rate", 0.0))
		dom_rate = float(dom.get("success_rate", 0.0))
	ok = bool(cli_rate >= float(args.min_cli) and dom_rate >= float(args.min_dom))
	# Optional office gate
	if ok and args.min_office is not None:
		off = (s.get("bench", {}) or {}).get("office_summary", {}) or {}
		off_rate = float(off.get("success_rate", 0.0)) if isinstance(off, dict) else 0.0
		ok = ok and (off_rate >= float(args.min_office))
	# Optional memory hit rate gate
	if ok and args.min_mem_hit is not None:
		cli = (s.get("bench", {}) or {}).get("cli_summary", {}) or {}
		dom = (s.get("bench", {}) or {}).get("dom_summary", {}) or {}
		cli_m = float(cli.get("memory_hit_rate", 0.0)) if isinstance(cli, dict) else 0.0
		dom_m = float(dom.get("memory_hit_rate", 0.0)) if isinstance(dom, dict) else 0.0
		ok = ok and (cli_m >= float(args.min_mem_hit)) and (dom_m >= float(args.min_mem_hit))
	# Optional over-budget thresholds
	if ok and (args.max_cli_over_budget is not None or args.max_dom_over_budget is not None):
		cli = (s.get("bench", {}) or {}).get("cli_summary", {}) or {}
		dom = (s.get("bench", {}) or {}).get("dom_summary", {}) or {}
		cli_over = int(cli.get("over_budget_total_runs", 0)) if isinstance(cli, dict) else 0
		dom_over = int(dom.get("over_budget_total_runs", 0)) if isinstance(dom, dict) else 0
		if args.max_cli_over_budget is not None:
			ok = ok and (cli_over <= int(args.max_cli_over_budget))
		if args.max_dom_over_budget is not None:
			ok = ok and (dom_over <= int(args.max_dom_over_budget))
	# Optional batch speedup gate
	if ok and args.min_batch_speedup is not None:
		ba = s.get("batch_audit", {}) or {}
		spd = float(ba.get("speedup", 0.0)) if isinstance(ba, dict) else 0.0
		ok = ok and (spd >= float(args.min_batch_speedup))
	print(json.dumps({"ok": ok, "cli": cli_rate, "dom": dom_rate, "office": (((s.get("bench", {}) or {}).get("office_summary", {}) or {}).get("success_rate") if args.min_office is not None else None), "budgeted": bool(args.use_budgeted), "mem": {"cli": ((s.get("bench", {}) or {}).get("cli_summary", {}) or {}).get("memory_hit_rate"), "dom": ((s.get("bench", {}) or {}).get("dom_summary", {}) or {}).get("memory_hit_rate")}, "batch": (s.get("batch_audit", {}) or {})}))
	return 0 if ok else 1

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


#!/usr/bin/env python3
import logging
import argparse
import json
from pathlib import Path

def parse_args():
	root = Path(__file__).resolve().parents[1]
	ap = argparse.ArgumentParser(description="CI assert for coding benchmarks (pass@k)")
	ap.add_argument("--results", default=str(root / "data" / "llm_bench" / "code_results.json"))
	ap.add_argument("--min-humaneval", type=float, default=0.0)
	ap.add_argument("--min-mbpp", type=float, default=0.0)
	ap.add_argument("--min-apps", type=float, default=0.0)
	ap.add_argument("--min-ds1000", type=float, default=0.0)
	ap.add_argument("--min-cruxeval", type=float, default=0.0)
	return ap.parse_args()

def main() -> int:
	args = parse_args()
	p = Path(args.results)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "results_missing", "path": str(p)}))
		return 1
	try:
		res = json.loads(p.read_text(encoding="utf-8"))
	except Exception as e:
		print(json.dumps({"ok": False, "error": "parse_error", "detail": str(e)}))
		return 1

	failures = []
	# These scaffolds don't yet compute pass@k; accept presence with tool indicators for now
	for name, data in res.get("benchmarks", {}).items():
		if name == "humaneval" and args.min_humaneval > 0:
			score = data.get("pass@k") or 0.0
			if score < args.min_humaneval:
				failures.append({"name": name, "score": score, "min": args.min_humaneval})
		if name == "mbpp" and args.min_mbpp > 0:
			score = data.get("pass@k") or 0.0
			if score < args.min_mbpp:
				failures.append({"name": name, "score": score, "min": args.min_mbpp})
		if name == "apps" and args.min_apps > 0:
			score = data.get("pass@k") or 0.0
			if score < args.min_apps:
				failures.append({"name": name, "score": score, "min": args.min_apps})
		if name == "ds1000" and args.min_ds1000 > 0:
			score = data.get("pass@k") or 0.0
			if score < args.min_ds1000:
				failures.append({"name": name, "score": score, "min": args.min_ds1000})
		if name == "cruxeval" and args.min_cruxeval > 0:
			score = data.get("pass@k") or 0.0
			if score < args.min_cruxeval:
				failures.append({"name": name, "score": score, "min": args.min_cruxeval})

	ok = len(failures) == 0
	print(json.dumps({"ok": ok, "failures": failures}))
	return 0 if ok else 1

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


#!/usr/bin/env python3
import logging
import argparse
import json
from pathlib import Path

def parse_args():
	root = Path(__file__).resolve().parents[1]
	ap = argparse.ArgumentParser(description="CI assert for coding benchmarks (pass@k)")
	ap.add_argument("--results", default=str(root / "data" / "llm_bench" / "code_results.json"))
	ap.add_argument("--min-humaneval", type=float, default=0.0)
	ap.add_argument("--min-mbpp", type=float, default=0.0)
	ap.add_argument("--min-apps", type=float, default=0.0)
	ap.add_argument("--min-ds1000", type=float, default=0.0)
	ap.add_argument("--min-cruxeval", type=float, default=0.0)
	return ap.parse_args()

def main() -> int:
	args = parse_args()
	p = Path(args.results)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "results_missing", "path": str(p)}))
		return 1
	try:
		res = json.loads(p.read_text(encoding="utf-8"))
	except Exception as e:
		print(json.dumps({"ok": False, "error": "parse_error", "detail": str(e)}))
		return 1

	failures = []
	# These scaffolds don't yet compute pass@k; accept presence with tool indicators for now
	for name, data in res.get("benchmarks", {}).items():
		if name == "humaneval" and args.min_humaneval > 0:
			score = data.get("pass@k") or 0.0
			if score < args.min_humaneval:


import logging
import argparse
import json
from pathlib import Path

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--registry", default=str(root / "data" / "registry" / "registry.json"))
	ap.add_argument("--require-models", default="actuator_il_t5,actuator_dom_t5,verifier_calib,wm_mlp")
	args = ap.parse_args()

	p = Path(args.registry)
	if not p.exists():
		print(json.dumps({"ok": False, "error": "registry_missing", "path": str(p)}))
		return 1
	obj = json.loads(p.read_text(encoding="utf-8"))
	models = obj.get("models", {}) if isinstance(obj, dict) else {}
	required = [x.strip() for x in str(args.require_models).split(",") if x.strip()]
	missing = []
	for name in required:
		info = models.get(name, {}) if isinstance(models, dict) else {}
		exists = bool((info or {}).get("exists", False))
		if not exists:
			missing.append(name)
	ok = (len(missing) == 0)
	print(json.dumps({"ok": bool(ok), "missing": missing, "checked": required}))
	return 0 if ok else 1

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


import logging
import argparse
import json
import sys
from typing import Dict

def rate(n: float, s: float) -> float:
	return (s / n) if n else 0.0

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--max-drop", type=float, default=0.0, help="Allow up to this drop vs best expert [absolute]")
	ap.add_argument("--per-task", action="store_true", help="Also enforce per-task lift vs best expert")
	args = ap.parse_args()

	data = sys.stdin.read().strip()
	if not data:
		print(json.dumps({"ok": False, "error": "no_input"}))
		return 2
	try:
		summary: Dict = json.loads(data.splitlines()[-1])
	except Exception as e:
		print(json.dumps({"ok": False, "error": f"parse:{e}"}))
		return 2

	all_sr = summary.get("all", {}) if isinstance(summary, dict) else {}
	router_sr = float((all_sr.get("router", {}) or {}).get("success_rate", 0.0))
	t5_sr = float((all_sr.get("t5", {}) or {}).get("success_rate", 0.0))
	nn_sr = float((all_sr.get("nn", {}) or {}).get("success_rate", 0.0))
	best_sr = max(t5_sr, nn_sr)
	ok = bool((router_sr + 1e-9) >= (best_sr - float(args.max_drop)))

	results = {
		"ok": bool(ok),
		"aggregate": {"router": router_sr, "t5": t5_sr, "nn": nn_sr, "best": best_sr},
		"max_drop": float(args.max_drop),
	}

	if args.per_task:
		# Build per-task rates from counts
		per_task = summary.get("per_task", {}) if isinstance(summary, dict) else {}
		# Keys like "router:count_lines"
		tasks = set()
		for k in per_task.keys():
			try:
				act, task = k.split(":", 1)
			except Exception:
				continue
			tasks.add(task)
		pt = {}
		per_ok = True
		for task in sorted(tasks):
			def get_counts(act: str) -> tuple[float, float]:
				st = per_task.get(f"{act}:{task}", {}) or {}
				return float(st.get("n", 0.0)), float(st.get("success", 0.0))

			n_r, s_r = get_counts("router")
			n_t, s_t = get_counts("t5")
			n_n, s_n = get_counts("nn")
			r = rate(n_r, s_r)
			best = max(rate(n_t, s_t), rate(n_n, s_n))
			pt[task] = {"router": r, "best": best}
			if (r + 1e-9) < (best - float(args.max_drop)):
				per_ok = False
		results["per_task"] = pt
		ok = ok and per_ok
		results["ok"] = bool(ok)

	print(json.dumps(results))
	return 0 if ok else 1

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


rate

import logging
import argparse
import json
import sys
from typing import Dict

def rate(n: float, s: float) -> float:
	return (s / n) if n else 0.0

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--max-drop", type=float, default=0.0, help="Allow up to this drop vs best expert [absolute]")
	ap.add_argument("--per-task", action="store_true", help="Also enforce per-task lift vs best expert")
	args = ap.parse_args()

	data = sys.stdin.read().strip()
	if not data:
		print(json.dumps({"ok": False, "error": "no_input"}))
		return 2
	try:
		summary: Dict = json.loads(data.splitlines()[-1])
	except Exception as e:
		print(json.dumps({"ok": False, "error": f"parse:{e}"}))
		return 2

	all_sr = summary.get("all", {}) if isinstance(summary, dict) else {}
	router_sr = float((all_sr.get("router", {}) or {}).get("success_rate", 0.0))