			run(["python3", "-m", "pip", "install", "-r", str(req)], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass
	dev_req = repo_dir / "requirements-dev.txt"
	if dev_req.exists():
		try:
			run(["python3", "-m", "pip", "install", "-r", str(dev_req)], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_install_editable(repo_dir: Path, timeout: int = 900) -> None:
	# If it's a Python package (pyproject.toml or setup.py), try editable install
	if (repo_dir / "pyproject.toml").exists() or (repo_dir / "setup.py").exists():
		try:
			run(["python3", "-m", "pip", "install", "-e", "."], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_install_test_extras(repo_dir: Path, timeout: int = 900) -> None:
	# Best-effort install of common extras to satisfy tests
	if (repo_dir / "pyproject.toml").exists() or (repo_dir / "setup.cfg").exists() or (repo_dir / "setup.py").exists():
		for extra in ("tests", "test", "dev"):
			try:
				run(["python3", "-m", "pip", "install", f".[{extra}]"], cwd=repo_dir, timeout=timeout)
			except Exception:
				continue

def ensure_pytest(repo_dir: Path, timeout: int = 900) -> None:
	chk = run(["python3", "-c", "import pytest; print(pytest.__version__)"] , cwd=repo_dir, timeout=timeout)
	if chk.returncode != 0:
		try:
			run(["python3", "-m", "pip", "install", "pytest"], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_npm_install(repo_dir: Path, timeout: int = 900) -> bool:
	if (repo_dir / "package.json").exists():
		try:
			# prefer ci if lockfile present
			if (repo_dir / "package-lock.json").exists():
				run(["npm", "ci"], cwd=repo_dir, timeout=timeout)
			else:
				run(["npm", "install"], cwd=repo_dir, timeout=timeout)
			return True


def maybe_install_editable(repo_dir: Path, timeout: int = 900) -> None:
	# If it's a Python package (pyproject.toml or setup.py), try editable install
	if (repo_dir / "pyproject.toml").exists() or (repo_dir / "setup.py").exists():
		try:
			run(["python3", "-m", "pip", "install", "-e", "."], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_install_test_extras(repo_dir: Path, timeout: int = 900) -> None:
	# Best-effort install of common extras to satisfy tests
	if (repo_dir / "pyproject.toml").exists() or (repo_dir / "setup.cfg").exists() or (repo_dir / "setup.py").exists():
		for extra in ("tests", "test", "dev"):
			try:
				run(["python3", "-m", "pip", "install", f".[{extra}]"], cwd=repo_dir, timeout=timeout)
			except Exception:
				continue

def ensure_pytest(repo_dir: Path, timeout: int = 900) -> None:
	chk = run(["python3", "-c", "import pytest; print(pytest.__version__)"] , cwd=repo_dir, timeout=timeout)
	if chk.returncode != 0:
		try:
			run(["python3", "-m", "pip", "install", "pytest"], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_npm_install(repo_dir: Path, timeout: int = 900) -> bool:
	if (repo_dir / "package.json").exists():
		try:
			# prefer ci if lockfile present
			if (repo_dir / "package-lock.json").exists():
				run(["npm", "ci"], cwd=repo_dir, timeout=timeout)
			else:
				run(["npm", "install"], cwd=repo_dir, timeout=timeout)
			return True
		except Exception:
			return False
	return False

def tail(s: str, n: int = 200) -> str:
	lines = s.strip().splitlines()
	return "\n".join(lines[-n:])


def maybe_install_test_extras(repo_dir: Path, timeout: int = 900) -> None:
	# Best-effort install of common extras to satisfy tests
	if (repo_dir / "pyproject.toml").exists() or (repo_dir / "setup.cfg").exists() or (repo_dir / "setup.py").exists():
		for extra in ("tests", "test", "dev"):
			try:
				run(["python3", "-m", "pip", "install", f".[{extra}]"], cwd=repo_dir, timeout=timeout)
			except Exception:
				continue

def ensure_pytest(repo_dir: Path, timeout: int = 900) -> None:
	chk = run(["python3", "-c", "import pytest; print(pytest.__version__)"] , cwd=repo_dir, timeout=timeout)
	if chk.returncode != 0:
		try:
			run(["python3", "-m", "pip", "install", "pytest"], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_npm_install(repo_dir: Path, timeout: int = 900) -> bool:
	if (repo_dir / "package.json").exists():
		try:
			# prefer ci if lockfile present
			if (repo_dir / "package-lock.json").exists():
				run(["npm", "ci"], cwd=repo_dir, timeout=timeout)
			else:
				run(["npm", "install"], cwd=repo_dir, timeout=timeout)
			return True
		except Exception:
			return False
	return False

def tail(s: str, n: int = 200) -> str:
	lines = s.strip().splitlines()
	return "\n".join(lines[-n:])

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--repo", required=True)
	ap.add_argument("--branch", default=None)
	ap.add_argument("--dir", default=str(root / "data" / "sandbox" / "dev_repo"))
	ap.add_argument("--pytest-args", default="")
	args = ap.parse_args()

	repo_root = Path(args.dir)
	repo_root.parent.mkdir(parents=True, exist_ok=True)
	git = GitTool(str(repo_root))


tail

		try:
			run(["python3", "-m", "pip", "install", "pytest"], cwd=repo_dir, timeout=timeout)
		except Exception:
			pass

def maybe_npm_install(repo_dir: Path, timeout: int = 900) -> bool:
	if (repo_dir / "package.json").exists():
		try:
			# prefer ci if lockfile present
			if (repo_dir / "package-lock.json").exists():
				run(["npm", "ci"], cwd=repo_dir, timeout=timeout)
			else:
				run(["npm", "install"], cwd=repo_dir, timeout=timeout)
			return True
		except Exception:
			return False
	return False

def tail(s: str, n: int = 200) -> str:
	lines = s.strip().splitlines()
	return "\n".join(lines[-n:])

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--repo", required=True)
	ap.add_argument("--branch", default=None)
	ap.add_argument("--dir", default=str(root / "data" / "sandbox" / "dev_repo"))
	ap.add_argument("--pytest-args", default="")
	args = ap.parse_args()

	repo_root = Path(args.dir)
	repo_root.parent.mkdir(parents=True, exist_ok=True)
	git = GitTool(str(repo_root))

	# Populate repo dir
	if not any(repo_root.iterdir()) and args.repo.startswith("local:"):
		src = Path(args.repo.split("local:", 1)[1]).resolve()
		try:
			shutil.copytree(src, repo_root, dirs_exist_ok=True)


main

def maybe_npm_install(repo_dir: Path, timeout: int = 900) -> bool:
	if (repo_dir / "package.json").exists():
		try:
			# prefer ci if lockfile present
			if (repo_dir / "package-lock.json").exists():
				run(["npm", "ci"], cwd=repo_dir, timeout=timeout)
			else:
				run(["npm", "install"], cwd=repo_dir, timeout=timeout)
			return True
		except Exception:
			return False
	return False

def tail(s: str, n: int = 200) -> str:
	lines = s.strip().splitlines()
	return "\n".join(lines[-n:])

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--repo", required=True)
	ap.add_argument("--branch", default=None)
	ap.add_argument("--dir", default=str(root / "data" / "sandbox" / "dev_repo"))
	ap.add_argument("--pytest-args", default="")
	args = ap.parse_args()

	repo_root = Path(args.dir)
	repo_root.parent.mkdir(parents=True, exist_ok=True)
	git = GitTool(str(repo_root))

	# Populate repo dir
	if not any(repo_root.iterdir()) and args.repo.startswith("local:"):
		src = Path(args.repo.split("local:", 1)[1]).resolve()
		try:
			shutil.copytree(src, repo_root, dirs_exist_ok=True)
		except Exception as e:
			print(json.dumps({"stage": "copy", "ok": False, "error": str(e)}))
			return 1
	elif not any(repo_root.iterdir()) and not args.repo.startswith("local:"):
		cl = git.clone(args.repo, str(repo_root))
		if cl.returncode != 0:
			print(json.dumps({"stage": "clone", "ok": False, "stderr": cl.stderr}))
			return 1

	# Checkout branch if requested
	if args.branch:
		co = git.checkout(args.branch)
		if co.returncode != 0:
			print(json.dumps({"stage": "checkout", "ok": False, "stderr": co.stderr}))
			return 1

	# Install dependencies
	maybe_install_requirements(repo_root)
	maybe_install_editable(repo_root)
	maybe_install_test_extras(repo_root)
	used_npm = maybe_npm_install(repo_root)

	# Run tests
	if used_npm:
		# Prefer npm test if JS project
		p = run(["npm", "test", "--silent"], cwd=repo_root, timeout=1200)
		ok = (p.returncode == 0)
		print(json.dumps({"stage": "npm test", "ok": ok, "returncode": p.returncode, "stderr_tail": tail(p.stderr), "stdout_tail": tail(p.stdout)}))
		return 0 if ok else 1
	else:
		ensure_pytest(repo_root)
		# Ensure local repo is preferred on import
		env = os.environ.copy()
		env["PYTHONPATH"] = str(repo_root) + (":" + env["PYTHONPATH"] if env.get("PYTHONPATH") else "")
		pytest_cmd = ["pytest", "-q"]
		if args.pytest_args:
			pytest_cmd += shlex.split(args.pytest_args)
		else:
			pytest_cmd += ["-k", "not validators and not version_info and not make and not benchmark and not slow"]
		p = run(pytest_cmd, cwd=repo_root, timeout=1200, env=env)
		if p.returncode != 0:
			# One retry: attempt to install missing module if ModuleNotFoundError is present
			m = re.search(r"ModuleNotFoundError: No module named '([^']+)'", p.stdout or "")
			if m:
				missing = m.group(1)
				try:
					run(["python3", "-m", "pip", "install", missing], cwd=repo_root, timeout=600)
					p = run(pytest_cmd, cwd=repo_root, timeout=1200, env=env)
				except Exception:
					pass
		ok = (p.returncode == 0)
		print(json.dumps({
			"stage": "pytest",
			"ok": ok,
			"returncode": p.returncode,
			"stderr_tail": tail(p.stderr),
			"stdout_tail": tail(p.stdout),
		}))
		return 0 if ok else 1

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


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

from agi_dw.core.planner.service import PlannerConfig, VerifierConfig, WMConfig, ContextAugment, plan_with_context  # type: ignore

def call_planner(obs: Dict[str, Any], backend: str, model: str, timeout: int, adapter: str | None, structured: str) -> Dict[str, Any]:
    pl = PlannerConfig(model=model, backend=backend, timeout_sec=timeout, adapter_dir=adapter, structured_mode=structured, candidates=1)
    vf = VerifierConfig(model=model, backend=backend, adapter_dir=None, structured_mode=structured)
    wm = WMConfig(enabled=False, model_path=None, horizon=1, plan_rank=False)
    ctx = ContextAugment(use_memory=False, index_k=0, inject_cli_policy=False, inject_dom_policy=False, inject_caps=False)
    plan, _info, _obs_aug, _mem, _ms = plan_with_context(obs, obs.get("kind", "cli"), pl, vf, wm, ctx, critic_fallback_threshold=None, log_prompts=False)
    return plan

def call_planner_candidates(obs: Dict[str, Any], backend: str, model: str, timeout: int, adapter: str | None, structured: str, n: int = 3) -> List[Dict[str, Any]]:
    pl = PlannerConfig(model=model, backend=backend, timeout_sec=timeout, adapter_dir=adapter, structured_mode=structured, candidates=int(n))
    vf = VerifierConfig(model=model, backend=backend, adapter_dir=None, structured_mode=structured)
    wm = WMConfig(enabled=False, model_path=None, horizon=1, plan_rank=False)
    ctx = ContextAugment(use_memory=False, index_k=0, inject_cli_policy=False, inject_dom_policy=False, inject_caps=False)
    plan, info, _obs_aug, _mem, _ms = plan_with_context(obs, obs.get("kind", "cli"), pl, vf, wm, ctx, critic_fallback_threshold=None, log_prompts=False)
    # plan_with_context already returns the best; to simulate multiple, call repeatedly if needed
    if int(n) <= 1:
        return [plan]
    out: List[Dict[str, Any]] = [plan]
    for _ in range(max(0, int(n) - 1)):
        p, _i, _o, _m, _ms2 = plan_with_context(obs, obs.get("kind", "cli"), pl, vf, wm, ctx, critic_fallback_threshold=None, log_prompts=False)
        out.append(p)
    return out

def call_executor(obs: Dict[str, Any], plan: Dict[str, Any], domain: str, args: argparse.Namespace) -> Dict[str, Any]:
	# Centralized actuator service for both CLI and DOM
	from agi_dw.core.actuator.service import (
		ActuatorConfig,
		RouterExtras,
		RouterVerifierConfig,
		WMPriorConfig,
		WMScreenConfig,
		RepairConfig,
		select_action,
	)
	if domain == "cli":
		act_cfg = ActuatorConfig(mode="template", t5_model=str(getattr(args, "t5_model", "")), il_path=str(getattr(args, "il", "")))
		vf_cfg = RouterVerifierConfig(model=str(getattr(args, "verifier_model", "meta-llama/Llama-3.2-3B")), backend=str(getattr(args, "verifier_backend", "hf")), timeout_sec=int(getattr(args, "timeout", 30) or 30))
		wm_cfg = WMPriorConfig(enabled=False, model_path=None)
		wm_scr = WMScreenConfig(enabled=False, threshold=0.7)
		repair = RepairConfig(domain="cli")
		extra = RouterExtras(domain="cli", task_name=str(getattr(args, "task", "") or ""))
		action, _ = select_action(obs, plan, act_cfg, extra, verifier_cfg=vf_cfg, wm_prior_cfg=wm_cfg, wm_screen_cfg=wm_scr, repair_cfg=repair)
		return {"action": action}
	else:
		act_cfg = ActuatorConfig(mode="t5", t5_model=str(getattr(args, "dom_t5_model", "")), il_path=str(getattr(args, "dom_il", "")), dom_structured=True)
		vf_cfg = RouterVerifierConfig(model=str(getattr(args, "verifier_model", "meta-llama/Llama-3.2-3B")), backend=str(getattr(args, "verifier_backend", "hf")), timeout_sec=int(getattr(args, "timeout", 30) or 30))
		wm_cfg = WMPriorConfig(enabled=False, model_path=None)
		wm_scr = WMScreenConfig(enabled=False, threshold=0.7)
		repair = RepairConfig(domain="dom", prefer_obs_args=True, default_url=str((obs.get("meta") or {}).get("url", "")), default_selector=str((obs.get("meta") or {}).get("selector", "")))
		extra = RouterExtras(domain="dom")
		action, _ = select_action(obs, plan, act_cfg, extra, verifier_cfg=vf_cfg, wm_prior_cfg=wm_cfg, wm_screen_cfg=wm_scr, repair_cfg=repair)
		return {"action": action}

def call_verifier(trace: Dict[str, Any], backend: str, model: str, timeout: int, adapter: str | None, structured: str, strict: bool) -> Dict[str, Any]:
	from agi_dw.core.verifier.service import VerifierServiceConfig, verify as verifier_run  # type: ignore
	v_cfg = VerifierServiceConfig(
		model=str(model),
		backend=str(backend),
		adapter_dir=adapter,
		structured_mode=str(structured),
		timeout_sec=int(timeout),
		strict=bool(strict),
		calibrate=False,
		log_prompts=False,
	)
	return verifier_run(trace, v_cfg)

def revise_with_critic(obs: Dict[str, Any], plan: Dict[str, Any], action: Dict[str, Any], critique: Dict[str, Any]) -> Dict[str, Any]:
	# Simple revision: if missing args, attempt to fill from obs/meta
	try:
		if (obs.get("kind") == "dom") and (not action or not isinstance(action.get("args"), dict)):
			meta = obs.get("meta") or {}
			url = meta.get("url", "")
			selector = meta.get("selector", "")
			action = {"tool": "browser.read", "args": {"url": url, "selector": selector}}
	except Exception:
		pass
	return action or {}

class Orchestrator:
	def __init__(self, roles: List[str], dom_t5: str, dom_il: str) -> None:
		self.roles = roles
		self.dom_t5 = dom_t5
		self.dom_il = dom_il

	def run(self, obs: Dict[str, Any], plan: Dict[str, Any]) -> List[Dict[str, Any]]:
		from agi_dw.core.multi_agent.roles import WebBrowserRole, CoderRole, DocWriterRole  # type: ignore
		messages: List[Dict[str, Any]] = []
		for r in self.roles:
			if r == "WebBrowser":
				msg = WebBrowserRole(self.dom_t5, self.dom_il).act({"obs": obs, "plan": plan})
				messages.append(msg)
			elif r == "Coder":
				messages.append(CoderRole().act({"obs": obs, "plan": plan}))
			elif r == "DocWriter":
				messages.append(DocWriterRole().act({"obs": obs, "plan": plan}))
			else:
				messages.append({"role": r, "note": "unknown role"})
		return messages

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--domain", choices=["cli", "dom"], default="cli")
	ap.add_argument("--planner-backend", choices=["hf"], default="hf")
	ap.add_argument("--planner-model", default="meta-llama/Llama-3.2-3B")
	ap.add_argument("--verifier-backend", choices=["hf"], default="hf")
	ap.add_argument("--verifier-model", default="meta-llama/Llama-3.2-3B")
	ap.add_argument("--verifier-adapter", default=None)
	ap.add_argument("--structured", choices=["none", "json"], default="json")
	ap.add_argument("--strict-verify", action="store_true")
	ap.add_argument("--timeout", type=int, default=30)
	# Actuator models
	ap.add_argument("--t5-model", default=str(root / "models" / "actuator_il_t5"))
	ap.add_argument("--il", default=str(root / "data" / "skills" / "actuator_il.jsonl"))
	ap.add_argument("--dom-t5-model", default=str(root / "models" / "actuator_dom_t5"))
	ap.add_argument("--dom-il", default=str(root / "data" / "skills" / "actuator_il_dom.jsonl"))
	ap.add_argument("--roles", nargs='*', default=["WebBrowser", "DocWriter"], help="Role list: WebBrowser, Coder, DocWriter")
	ap.add_argument("--debate", action="store_true", help="Enable multi-planner candidates and pick by lowest verifier risk")
	args = ap.parse_args()

	# Example observations for demo
	obs_cli = {"kind": "cli", "content": "Count file lines", "meta": {"cwd": str(root / "data" / "sandbox")}}
	obs_dom = {"kind": "dom", "content": "fetch text via selector", "meta": {"url": "https://example.com", "selector": "h1"}}
	obs = obs_cli if args.domain == "cli" else obs_dom

    if args.debate:
        plans = call_planner_candidates(obs, args.planner_backend, args.planner_model, int(args.timeout), None, args.structured, n=3)
		best = None
		best_risk = 1e9
		for p in plans:
			ver_tmp = call_verifier({"obs": obs, "plan": p, "action": {}, "result": {"status": "pending"}}, args.verifier_backend, args.verifier_model, int(args.timeout), args.verifier_adapter, args.structured, bool(args.strict_verify))
			r = float(ver_tmp.get("risk", 0.5))
			if r < best_risk:
				best_risk = r
				best = p
		plan = best or plans[0]
	else:
		plan = call_planner(obs, args.planner_backend, args.planner_model, int(args.timeout), None, args.structured)
	exec_res = call_executor(obs, plan, args.domain, args)
	trace = {"obs": obs, "plan": plan, "action": exec_res.get("action", {}), "result": {"status": "unknown"}}
	ver = call_verifier(trace, args.verifier_backend, args.verifier_model, int(args.timeout), args.verifier_adapter, args.structured, bool(args.strict_verify))

	orch = Orchestrator(args.roles, args.dom_t5_model, args.dom_il)
	team_msgs = orch.run(obs, plan)

	print(json.dumps({"plan": plan, "action": exec_res.get("action", {}), "verify": ver, "team": team_msgs}, ensure_ascii=False))
	return 0

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


import logging
import argparse
from pathlib import Path
import re

def rewrite_file(fp: Path, pkg: str) -> bool:
	orig = fp.read_text(encoding="utf-8")
	text = orig
	# from .mod import X -> from pkg.mod import X
	text = re.sub(r"^\s*from\s+\.([\w\.]+)\s+import\s+", rf"from {pkg}.\1 import ", text, flags=re.M)
	# from . import X -> from pkg import X
	text = re.sub(r"^\s*from\s+\.\s+import\s+", rf"from {pkg} import ", text, flags=re.M)
	if text != orig:
		fp.write_text(text, encoding="utf-8")
		return True
	return False

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--repo", required=True, help="Path to local repo root")
	ap.add_argument("--pkg", required=True, help="Top-level package name to rewrite to (guarded; no-op if missing)")
	ap.add_argument("--only-tests", action="store_true", help="Restrict rewrites to tests/ directory")
	args = ap.parse_args()
	repo = Path(args.repo)
	if not repo.exists():
		print("repo not found:", str(repo))
		return 2
	changed = 0
	paths = []
	if args.only_tests:
		paths = list((repo / "tests").rglob("*.py")) if (repo / "tests").exists() else []
	else:
		paths = list(repo.rglob("*.py"))
	for p in paths:
		try:
			if rewrite_file(p, args.pkg):
				changed += 1
		except Exception:
			continue
	print({"changed": changed})
	return 0

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


import logging
import argparse
from pathlib import Path
import re

def rewrite_file(fp: Path, pkg: str) -> bool:
	orig = fp.read_text(encoding="utf-8")
	text = orig
	# from .mod import X -> from pkg.mod import X
	text = re.sub(r"^\s*from\s+\.([\w\.]+)\s+import\s+", rf"from {pkg}.\1 import ", text, flags=re.M)
	# from . import X -> from pkg import X
	text = re.sub(r"^\s*from\s+\.\s+import\s+", rf"from {pkg} import ", text, flags=re.M)
	if text != orig:
		fp.write_text(text, encoding="utf-8")
		return True
	return False

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--repo", required=True, help="Path to local repo root")
	ap.add_argument("--pkg", required=True, help="Top-level package name to rewrite to (guarded; no-op if missing)")
	ap.add_argument("--only-tests", action="store_true", help="Restrict rewrites to tests/ directory")
	args = ap.parse_args()
	repo = Path(args.repo)
	if not repo.exists():
		print("repo not found:", str(repo))
		return 2
	changed = 0
	paths = []
	if args.only_tests:
		paths = list((repo / "tests").rglob("*.py")) if (repo / "tests").exists() else []
	else:
		paths = list(repo.rglob("*.py"))
	for p in paths:
		try:


from __future__ import annotations
import logging

import re
from pathlib import Path

def apply_codemod(path: Path) -> bool:
	text = path.read_text(encoding="utf-8")
	orig = text
	# Ensure bench_utils import presence when helpers detected
	if "strip_fences(" in text or "precheck_code(" in text or "retry_with_backoff(" in text:
		if "from agi_dw.core.utils.bench_utils" not in text:
			text = (
				text.replace(
					"import json\n",
					"import json\nfrom agi_dw.core.utils.bench_utils import ensure_safe_env, strip_fences, precheck_code, retry_with_backoff, add_common_bench_args\n",
				)
				if "import json\n" in text
				else text
			)
	if text != orig:
		path.write_text(text, encoding="utf-8")
		return True
	return False

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	changed = 0
	for p in root.rglob("scripts/bench/*.py"):
		try:
			if apply_codemod(p):
				changed += 1
		except Exception:
			continue
	print(f"Codemod applied to {changed} files")
	return 0

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


def apply_codemod(path: Path) -> bool:
	text = path.read_text(encoding="utf-8")
	orig = text
	# Ensure bench_utils import presence when helpers detected
	if "strip_fences(" in text or "precheck_code(" in text or "retry_with_backoff(" in text:
		if "from agi_dw.core.utils.bench_utils" not in text:
			text = (
				text.replace(
					"import json\n",
					"import json\nfrom agi_dw.core.utils.bench_utils import ensure_safe_env, strip_fences, precheck_code, retry_with_backoff, add_common_bench_args\n",
				)
				if "import json\n" in text
				else text
			)
	if text != orig:
		path.write_text(text, encoding="utf-8")
		return True
	return False

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	changed = 0
	for p in root.rglob("scripts/bench/*.py"):
		try:
			if apply_codemod(p):
				changed += 1
		except Exception:
			continue
	print(f"Codemod applied to {changed} files")
	return 0

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


from __future__ import annotations
import logging

import argparse
import ast
import hashlib
import json
from pathlib import Path
from typing import Dict, List, Tuple

def normalize_function(node: ast.AST) -> str:
	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]
		h = min(hashlib.sha1(t.encode("utf-8")).hexdigest() for t in chunk)
		sig.append(h)
	return sig

def process_file(p: Path) -> List[Tuple[str, str, int]]:
	out: List[Tuple[str, str, int]] = []
	try:
		text = p.read_text(encoding="utf-8", errors="ignore")
		tree = ast.parse(text, filename=str(p))
	except Exception:
		return out
	for node in ast.walk(tree):
		if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
			norm = normalize_function(node)
			shingles = shingle(norm)
			sig = ":".join(minhash_signature(shingles))
			out.append((sig, str(p), getattr(node, "lineno", 0) or 0))
	return out

def main() -> int:
	ap = argparse.ArgumentParser(description="Detect near-miss function clones via normalized AST minhash")
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--src", default=str(root))
	ap.add_argument("--out", default=str(root / "data" / "sandbox" / "tmp" / "clone_clusters.json"))
	ap.add_argument("--min-size", type=int, default=2, help="Min cluster size")
	args = ap.parse_args()

	src = Path(args.src)
	sig_to_locs: Dict[str, List[Dict[str, object]]] = {}
	for p in src.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		for sig, path, lineno in process_file(p):
			sig_to_locs.setdefault(sig, []).append({"path": path, "lineno": lineno})

	clusters = [locs for locs in sig_to_locs.values() if len(locs) >= int(args.min_size)]
	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)
	outp.write_text(json.dumps({"clusters": clusters}, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "out": str(outp), "n_clusters": len(clusters)}))
	return 0

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


from __future__ import annotations
import logging

import argparse
import ast
import hashlib
import json
from pathlib import Path
from typing import Dict, List, Tuple

def normalize_function(node: ast.AST) -> str:
	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]
		h = min(hashlib.sha1(t.encode("utf-8")).hexdigest() for t in chunk)
		sig.append(h)
	return sig

def process_file(p: Path) -> List[Tuple[str, str, int]]:


shingle

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

def normalize_function(node: ast.AST) -> str:
	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]
		h = min(hashlib.sha1(t.encode("utf-8")).hexdigest() for t in chunk)
		sig.append(h)
	return sig

def process_file(p: Path) -> List[Tuple[str, str, int]]:
	out: List[Tuple[str, str, int]] = []
	try:
		text = p.read_text(encoding="utf-8", errors="ignore")
		tree = ast.parse(text, filename=str(p))
	except Exception:


	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]
		h = min(hashlib.sha1(t.encode("utf-8")).hexdigest() for t in chunk)
		sig.append(h)
	return sig

def process_file(p: Path) -> List[Tuple[str, str, int]]:
	out: List[Tuple[str, str, int]] = []
	try:
		text = p.read_text(encoding="utf-8", errors="ignore")
		tree = ast.parse(text, filename=str(p))
	except Exception:
		return out
	for node in ast.walk(tree):
		if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
			norm = normalize_function(node)
			shingles = shingle(norm)
			sig = ":".join(minhash_signature(shingles))
			out.append((sig, str(p), getattr(node, "lineno", 0) or 0))
	return out

def main() -> int:
	ap = argparse.ArgumentParser(description="Detect near-miss function clones via normalized AST minhash")


	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]
		h = min(hashlib.sha1(t.encode("utf-8")).hexdigest() for t in chunk)
		sig.append(h)
	return sig

def process_file(p: Path) -> List[Tuple[str, str, int]]:
	out: List[Tuple[str, str, int]] = []
	try:
		text = p.read_text(encoding="utf-8", errors="ignore")
		tree = ast.parse(text, filename=str(p))
	except Exception:
		return out
	for node in ast.walk(tree):
		if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
			norm = normalize_function(node)
			shingles = shingle(norm)
			sig = ":".join(minhash_signature(shingles))
			out.append((sig, str(p), getattr(node, "lineno", 0) or 0))
	return out

def main() -> int:
	ap = argparse.ArgumentParser(description="Detect near-miss function clones via normalized AST minhash")
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--src", default=str(root))
	ap.add_argument("--out", default=str(root / "data" / "sandbox" / "tmp" / "clone_clusters.json"))
	ap.add_argument("--min-size", type=int, default=2, help="Min cluster size")
	args = ap.parse_args()

	src = Path(args.src)
	sig_to_locs: Dict[str, List[Dict[str, object]]] = {}
	for p in src.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		for sig, path, lineno in process_file(p):
			sig_to_locs.setdefault(sig, []).append({"path": path, "lineno": lineno})

	clusters = [locs for locs in sig_to_locs.values() if len(locs) >= int(args.min_size)]
	outp = Path(args.out)


main

		sig.append(h)
	return sig

def process_file(p: Path) -> List[Tuple[str, str, int]]:
	out: List[Tuple[str, str, int]] = []
	try:
		text = p.read_text(encoding="utf-8", errors="ignore")
		tree = ast.parse(text, filename=str(p))
	except Exception:
		return out
	for node in ast.walk(tree):
		if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
			norm = normalize_function(node)
			shingles = shingle(norm)
			sig = ":".join(minhash_signature(shingles))
			out.append((sig, str(p), getattr(node, "lineno", 0) or 0))
	return out

def main() -> int:
	ap = argparse.ArgumentParser(description="Detect near-miss function clones via normalized AST minhash")
	root = Path(__file__).resolve().parents[2]
	ap.add_argument("--src", default=str(root))
	ap.add_argument("--out", default=str(root / "data" / "sandbox" / "tmp" / "clone_clusters.json"))
	ap.add_argument("--min-size", type=int, default=2, help="Min cluster size")
	args = ap.parse_args()

	src = Path(args.src)
	sig_to_locs: Dict[str, List[Dict[str, object]]] = {}
	for p in src.rglob("*.py"):
		if "/.venv/" in str(p):
			continue
		for sig, path, lineno in process_file(p):
			sig_to_locs.setdefault(sig, []).append({"path": path, "lineno": lineno})

	clusters = [locs for locs in sig_to_locs.values() if len(locs) >= int(args.min_size)]
	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)
	outp.write_text(json.dumps({"clusters": clusters}, ensure_ascii=False, indent=2), encoding="utf-8")
	print(json.dumps({"ok": True, "out": str(outp), "n_clusters": len(clusters)}))
	return 0

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


Normalizer

from __future__ import annotations
import logging

import argparse
import ast
import hashlib
import json
from pathlib import Path
from typing import Dict, List, Tuple

def normalize_function(node: ast.AST) -> str:
	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]
		h = min(hashlib.sha1(t.encode("utf-8")).hexdigest() for t in chunk)
		sig.append(h)
	return sig


from __future__ import annotations
import logging

import argparse
import ast
import hashlib
import json
from pathlib import Path
from typing import Dict, List, Tuple

def normalize_function(node: ast.AST) -> str:
	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []


from __future__ import annotations
import logging

import argparse
import ast
import hashlib
import json
from pathlib import Path
from typing import Dict, List, Tuple

def normalize_function(node: ast.AST) -> str:
	"""Return a normalized string of a function AST (strip names/consts)."""
	class Normalizer(ast.NodeTransformer):
		def visit_Name(self, n: ast.Name):  # type: ignore
			return ast.copy_location(ast.Name(id="_", ctx=n.ctx), n)

		def visit_Constant(self, n: ast.Constant):  # type: ignore
			return ast.copy_location(ast.Constant(value="_"), n)

		def visit_arg(self, n: ast.arg):  # type: ignore
			return ast.copy_location(ast.arg(arg="_", annotation=None), n)

	norm = Normalizer().visit(ast.fix_missing_locations(node))
	return ast.dump(norm, annotate_fields=False, include_attributes=False)

def shingle(s: str, k: int = 6) -> List[str]:
	tokens = s.split()
	return [" ".join(tokens[i : i + k]) for i in range(max(0, len(tokens) - k + 1))]

def minhash_signature(tokens: List[str], bands: int = 4) -> List[str]:
	if not tokens:
		return []
	sig: List[str] = []
	size = max(1, len(tokens) // bands)
	for i in range(0, len(tokens), size):
		chunk = tokens[i : i + size]


import logging
import argparse
import subprocess
from pathlib import Path
from agi_dw.core.updater import Updater

def run(cmd):
	print("[updater] ", " ".join(cmd))
	return subprocess.call(cmd)

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--fast", action="store_true", help="Use fast/CI settings where available")
	args = ap.parse_args()
	Updater(root, fast=bool(args.fast)).run()
	return 0

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


#!/usr/bin/env python3
import logging
import argparse
import json
import os
import shutil
import signal
import subprocess
import sys
import tempfile
from pathlib import Path

def parse_args():
	ap = argparse.ArgumentParser(description="Lightweight multi-language sandbox executor (scaffold)")
	ap.add_argument("--lang", required=True, choices=["py", "js", "cpp", "java"], help="Language runtime")
	ap.add_argument("--code", required=True, help="Path to solution code file or literal when --literal")
	ap.add_argument("--tests", required=True, help="Path to tests code file or literal when --literal")
	ap.add_argument("--timeout", type=int, default=15)
	ap.add_argument("--memmb", type=int, default=512)
	ap.add_argument("--literal", action="store_true")
	return ap.parse_args()

def run(cmd: list[str], cwd: Path, timeout: int) -> tuple[int, str, str]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout)
	return p.returncode, p.stdout, p.stderr

def sandbox_limits(mem_mb: int) -> None:
	try:
		import resource  # type: ignore
		bytes_limit = mem_mb * 1024 * 1024
		resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
		resource.setrlimit(resource.RLIMIT_DATA, (bytes_limit, bytes_limit))
	except Exception:
		pass

def main() -> int:
	args = parse_args()
	code_text = Path(args.code).read_text(encoding="utf-8") if not args.literal else args.code
	test_text = Path(args.tests).read_text(encoding="utf-8") if not args.literal else args.tests
	with tempfile.TemporaryDirectory() as tmp:
		root = Path(tmp)
		ok = False
		try:
			pid = os.fork()
		except AttributeError:
			pid = -1
		if pid == 0:
			# child
			try:
				sandbox_limits(int(args.memmb))
				os.chdir(root)
				if args.lang == "py":
					(root / "solution.py").write_text(code_text, encoding="utf-8")
					(root / "test_runner.py").write_text(
						"import json\nfrom solution import *\nok=True\ntry:\n\texec(\"" + test_text.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"") + "\")\nexcept Exception as e:\n\tok=False\n\terr=str(e)\nres={'success':ok}\nif not ok: res['error']=err\nprint(json.dumps(res))\n",
						encoding="utf-8",
					)
					os.execv(sys.executable, [sys.executable, str(root / "test_runner.py")])
				elif args.lang == "js":
					(root / "solution.js").write_text(code_text, encoding="utf-8")
					(root / "test_runner.js").write_text(
						"const fs=require('fs'); const path=require('path'); const sol=require('./solution.js'); let ok=true; let err=''; try {\n" +
						test_text +
						"\n} catch(e){ ok=false; err=String(e);} const out={success:ok}; if(!ok){out.error=err;} console.log(JSON.stringify(out));\n",
						encoding="utf-8",
					)
					os.execvp("node", ["node", str(root / "test_runner.js")])
				elif args.lang == "cpp":
					(root / "solution.cpp").write_text(code_text, encoding="utf-8")
					(root / "test_runner.cpp").write_text(
						"#include <bits/stdc++.h>\nusing namespace std;\n" + code_text + "\nint main(){ bool ok=true; try{\n" + test_text + "\n} catch(...) { ok=false; } cout << (ok?\"{\\\"success\\\": true}\":\"{\\\"success\\\": false}\"); return ok?0:1;}\n",
						encoding="utf-8",
					)
					rc, out, err = run(["g++", "-O2", str(root / "test_runner.cpp"), "-o", "a.out"], root, int(args.timeout))
					if rc != 0:
						print(json.dumps({"success": False, "error": err.strip()}))
						os._exit(0)
					os.execv(str(root / "a.out"), [str(root / "a.out")])
				elif args.lang == "java":
					(root / "Solution.java").write_text(code_text, encoding="utf-8")
					(root / "TestRunner.java").write_text(
						"public class TestRunner { public static void main(String[] args){ boolean ok=true; try {\n" + test_text + "\n } catch(Throwable t){ ok=false;} System.out.println(ok?\"{\\\"success\\\": true}\":\"{\\\"success\\\": false}\"); System.exit(ok?0:1);} }\n",
						encoding="utf-8",
					)
					rc, out, err = run(["javac", "Solution.java", "TestRunner.java"], root, int(args.timeout))
					if rc != 0:
						print(json.dumps({"success": False, "error": err.strip()}))
						os._exit(0)
					os.execvp("java", ["java", "TestRunner"])
				else:
					print(json.dumps({"success": False, "error": "unsupported_lang"}))
					os._exit(0)
			except Exception as e:
				print(json.dumps({"success": False, "error": str(e)}))
				os._exit(0)
		else:
			# parent
			try:
				_, status = os.waitpid(pid, 0)
				ok = os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
			except Exception:
				ok = False
			print(json.dumps({"success": ok}))
			return 0 if ok else 1

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


#!/usr/bin/env python3
import logging
import argparse
import json
import os
import shutil
import signal
import subprocess
import sys
import tempfile
from pathlib import Path

def parse_args():
	ap = argparse.ArgumentParser(description="Lightweight multi-language sandbox executor (scaffold)")
	ap.add_argument("--lang", required=True, choices=["py", "js", "cpp", "java"], help="Language runtime")
	ap.add_argument("--code", required=True, help="Path to solution code file or literal when --literal")
	ap.add_argument("--tests", required=True, help="Path to tests code file or literal when --literal")
	ap.add_argument("--timeout", type=int, default=15)
	ap.add_argument("--memmb", type=int, default=512)
	ap.add_argument("--literal", action="store_true")
	return ap.parse_args()

def run(cmd: list[str], cwd: Path, timeout: int) -> tuple[int, str, str]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout)
	return p.returncode, p.stdout, p.stderr

def sandbox_limits(mem_mb: int) -> None:
	try:
		import resource  # type: ignore
		bytes_limit = mem_mb * 1024 * 1024
		resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
		resource.setrlimit(resource.RLIMIT_DATA, (bytes_limit, bytes_limit))
	except Exception:
		pass

def main() -> int:
	args = parse_args()
	code_text = Path(args.code).read_text(encoding="utf-8") if not args.literal else args.code


run

import os
import shutil
import signal
import subprocess
import sys
import tempfile
from pathlib import Path

def parse_args():
	ap = argparse.ArgumentParser(description="Lightweight multi-language sandbox executor (scaffold)")
	ap.add_argument("--lang", required=True, choices=["py", "js", "cpp", "java"], help="Language runtime")
	ap.add_argument("--code", required=True, help="Path to solution code file or literal when --literal")
	ap.add_argument("--tests", required=True, help="Path to tests code file or literal when --literal")
	ap.add_argument("--timeout", type=int, default=15)
	ap.add_argument("--memmb", type=int, default=512)
	ap.add_argument("--literal", action="store_true")
	return ap.parse_args()

def run(cmd: list[str], cwd: Path, timeout: int) -> tuple[int, str, str]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout)
	return p.returncode, p.stdout, p.stderr

def sandbox_limits(mem_mb: int) -> None:
	try:
		import resource  # type: ignore
		bytes_limit = mem_mb * 1024 * 1024
		resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
		resource.setrlimit(resource.RLIMIT_DATA, (bytes_limit, bytes_limit))
	except Exception:
		pass

def main() -> int:
	args = parse_args()
	code_text = Path(args.code).read_text(encoding="utf-8") if not args.literal else args.code
	test_text = Path(args.tests).read_text(encoding="utf-8") if not args.literal else args.tests
	with tempfile.TemporaryDirectory() as tmp:
		root = Path(tmp)
		ok = False
		try:


import tempfile
from pathlib import Path

def parse_args():
	ap = argparse.ArgumentParser(description="Lightweight multi-language sandbox executor (scaffold)")
	ap.add_argument("--lang", required=True, choices=["py", "js", "cpp", "java"], help="Language runtime")
	ap.add_argument("--code", required=True, help="Path to solution code file or literal when --literal")
	ap.add_argument("--tests", required=True, help="Path to tests code file or literal when --literal")
	ap.add_argument("--timeout", type=int, default=15)
	ap.add_argument("--memmb", type=int, default=512)
	ap.add_argument("--literal", action="store_true")
	return ap.parse_args()

def run(cmd: list[str], cwd: Path, timeout: int) -> tuple[int, str, str]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout)
	return p.returncode, p.stdout, p.stderr

def sandbox_limits(mem_mb: int) -> None:
	try:
		import resource  # type: ignore
		bytes_limit = mem_mb * 1024 * 1024
		resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
		resource.setrlimit(resource.RLIMIT_DATA, (bytes_limit, bytes_limit))
	except Exception:
		pass

def main() -> int:
	args = parse_args()
	code_text = Path(args.code).read_text(encoding="utf-8") if not args.literal else args.code
	test_text = Path(args.tests).read_text(encoding="utf-8") if not args.literal else args.tests
	with tempfile.TemporaryDirectory() as tmp:
		root = Path(tmp)
		ok = False
		try:
			pid = os.fork()
		except AttributeError:
			pid = -1
		if pid == 0:
			# child
			try:
				sandbox_limits(int(args.memmb))
				os.chdir(root)
				if args.lang == "py":
					(root / "solution.py").write_text(code_text, encoding="utf-8")


main

	ap.add_argument("--memmb", type=int, default=512)
	ap.add_argument("--literal", action="store_true")
	return ap.parse_args()

def run(cmd: list[str], cwd: Path, timeout: int) -> tuple[int, str, str]:
	p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True, timeout=timeout)
	return p.returncode, p.stdout, p.stderr

def sandbox_limits(mem_mb: int) -> None:
	try:
		import resource  # type: ignore
		bytes_limit = mem_mb * 1024 * 1024
		resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit))
		resource.setrlimit(resource.RLIMIT_DATA, (bytes_limit, bytes_limit))
	except Exception:
		pass

def main() -> int:
	args = parse_args()
	code_text = Path(args.code).read_text(encoding="utf-8") if not args.literal else args.code
	test_text = Path(args.tests).read_text(encoding="utf-8") if not args.literal else args.tests
	with tempfile.TemporaryDirectory() as tmp:
		root = Path(tmp)
		ok = False
		try:
			pid = os.fork()
		except AttributeError:
			pid = -1
		if pid == 0:
			# child
			try:
				sandbox_limits(int(args.memmb))
				os.chdir(root)
				if args.lang == "py":
					(root / "solution.py").write_text(code_text, encoding="utf-8")
					(root / "test_runner.py").write_text(
						"import json\nfrom solution import *\nok=True\ntry:\n\texec(\"" + test_text.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"") + "\")\nexcept Exception as e:\n\tok=False\n\terr=str(e)\nres={'success':ok}\nif not ok: res['error']=err\nprint(json.dumps(res))\n",
						encoding="utf-8",
					)
					os.execv(sys.executable, [sys.executable, str(root / "test_runner.py")])
				elif args.lang == "js":
					(root / "solution.js").write_text(code_text, encoding="utf-8")
					(root / "test_runner.js").write_text(
						"const fs=require('fs'); const path=require('path'); const sol=require('./solution.js'); let ok=true; let err=''; try {\n" +
						test_text +
						"\n} catch(e){ ok=false; err=String(e);} const out={success:ok}; if(!ok){out.error=err;} console.log(JSON.stringify(out));\n",
						encoding="utf-8",
					)
					os.execvp("node", ["node", str(root / "test_runner.js")])
				elif args.lang == "cpp":
					(root / "solution.cpp").write_text(code_text, encoding="utf-8")
					(root / "test_runner.cpp").write_text(
						"#include <bits/stdc++.h>\nusing namespace std;\n" + code_text + "\nint main(){ bool ok=true; try{\n" + test_text + "\n} catch(...) { ok=false; } cout << (ok?\"{\\\"success\\\": true}\":\"{\\\"success\\\": false}\"); return ok?0:1;}\n",
						encoding="utf-8",
					)
					rc, out, err = run(["g++", "-O2", str(root / "test_runner.cpp"), "-o", "a.out"], root, int(args.timeout))
					if rc != 0:
						print(json.dumps({"success": False, "error": err.strip()}))
						os._exit(0)
					os.execv(str(root / "a.out"), [str(root / "a.out")])
				elif args.lang == "java":
					(root / "Solution.java").write_text(code_text, encoding="utf-8")
					(root / "TestRunner.java").write_text(
						"public class TestRunner { public static void main(String[] args){ boolean ok=true; try {\n" + test_text + "\n } catch(Throwable t){ ok=false;} System.out.println(ok?\"{\\\"success\\\": true}\":\"{\\\"success\\\": false}\"); System.exit(ok?0:1);} }\n",
						encoding="utf-8",
					)
					rc, out, err = run(["javac", "Solution.java", "TestRunner.java"], root, int(args.timeout))
					if rc != 0:
						print(json.dumps({"success": False, "error": err.strip()}))
						os._exit(0)
					os.execvp("java", ["java", "TestRunner"])
				else:
					print(json.dumps({"success": False, "error": "unsupported_lang"}))
					os._exit(0)
			except Exception as e:
				print(json.dumps({"success": False, "error": str(e)}))
				os._exit(0)
		else:
			# parent
			try:
				_, status = os.waitpid(pid, 0)
				ok = os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
			except Exception:
				ok = False
			print(json.dumps({"success": ok}))
			return 0 if ok else 1

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


from __future__ import annotations
import logging

import re
from collections import defaultdict
from pathlib import Path

PATTERNS = {
	"strip_fences": r"def\s+strip_fences\(",
	"precheck_code": r"def\s+precheck_code\(",
	"retry_with_backoff": r"def\s+retry_with_backoff\(",
	"ensure_safe_env": r"def\s+ensure_safe_env\(",
}

def main() -> int:
	root = Path(__file__).resolve().parents[2]
	found = defaultdict(list)
	for p in root.rglob("*.py"):
		try:
			text = p.read_text(encoding="utf-8")
		except Exception:
			continue
		for name, pat in PATTERNS.items():
			if re.search(pat, text):
				found[name].append(str(p))
	for name, files in found.items():
		print(f"Function {name} defined in {len(files)} files:")
		for f in files:
			print(" -", f)
		print()
	return 0

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


import logging
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, Any, List

try:
	import yaml  # type: ignore
except Exception:
	yaml = None

def run_dev_repo(root: Path, repo: str, pytest_args: List[str]) -> Dict[str, Any]:
	cmd = [
		sys.executable,
		str(root / "scripts" / "run_loop_dev.py"),
		"--repo",
		repo,
	]
	cmd.extend(pytest_args or [])
	import time as _t  # type: ignore
	_t0 = _t.time()
	res = subprocess.run(cmd, capture_output=True, text=True)
	dur = float(_t.time() - _t0)
	return {
		"repo": repo,
		"returncode": res.returncode,
		"stdout_tail": (res.stdout or "")[-2000:],
		"stderr_tail": (res.stderr or "")[-2000:],
		"elapsed_sec": dur,
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--curriculum", default=str(root / "data" / "practice_repos.yaml"))
	ap.add_argument("--tier", default="T1")
	ap.add_argument("--out", default=str(root / "data" / "logs" / "scheduler_runs.jsonl"))
	ap.add_argument("--auto-progress", action="store_true", help="Automatically progress tier based on recent pass rate >= 0.7")
	ap.add_argument("--state", default=str(root / "data" / "logs" / "scheduler_state.json"))
	args = ap.parse_args()

	cur_path = Path(args.curriculum)
	if yaml is None:
		print("pyyaml not installed; pip install pyyaml")
		return 2
	if not cur_path.exists():
		print("curriculum not found:", str(cur_path))
		return 2
	cur = yaml.safe_load(cur_path.read_text(encoding="utf-8"))
	tier_specs = (cur.get("tiers", {}) or {}).get(args.tier, [])
	# Optionally adjust tier based on recent state
	if args.auto_progress:
		try:
			st_path = Path(args.state)
			if st_path.exists():
				st = json.loads(st_path.read_text(encoding="utf-8"))
				last = st.get("history", {}).get(args.tier, [])
				# Use last 10 outcomes to estimate pass rate
				recent = last[-10:]
				if recent:
					rate = sum(1 for x in recent if bool(x)) / float(len(recent))
					# Simple tier order
					order = ["T1", "T2", "T3"]
					if rate >= 0.7 and args.tier in order:
						idx = order.index(args.tier)
						if idx + 1 < len(order):
							args.tier = order[idx + 1]
		except Exception:
			pass

	queue: List[Dict[str, Any]] = []
	for spec in tier_specs:
		if not isinstance(spec, dict):
			continue
		repo = str(spec.get("repo", "")).strip()
		if not repo:
			continue
		pytest_args = list(spec.get("pytest_args", []) or [])
		queue.append({"repo": repo, "pytest_args": pytest_args})

	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	completed = 0
	pass_outcomes: List[bool] = []
	for job in queue:
		res = run_dev_repo(root, job["repo"], job.get("pytest_args") or [])
		with out.open("a", encoding="utf-8") as f:
			f.write(json.dumps(res, ensure_ascii=False) + "\n")
		completed += 1
		print(json.dumps({"completed": completed, "last_repo": res.get("repo", "" )}))
		pass_outcomes.append(bool(res.get("returncode", 1) == 0))
	# Update state file with outcomes for this tier
	try:
		st_path = Path(args.state)
		st_path.parent.mkdir(parents=True, exist_ok=True)
		st = {}
		if st_path.exists():
			st = json.loads(st_path.read_text(encoding="utf-8"))
		hist = st.get("history", {})
		arr = hist.get(args.tier, [])
		arr.extend(pass_outcomes)
		hist[args.tier] = arr[-50:]  # cap history
		st["history"] = hist
		st_path.write_text(json.dumps(st, ensure_ascii=False, indent=2), encoding="utf-8")
	except Exception:
		pass
	return 0

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


import logging
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, Any, List

try:
	import yaml  # type: ignore
except Exception:
	yaml = None

def run_dev_repo(root: Path, repo: str, pytest_args: List[str]) -> Dict[str, Any]:
	cmd = [
		sys.executable,
		str(root / "scripts" / "run_loop_dev.py"),
		"--repo",
		repo,
	]
	cmd.extend(pytest_args or [])
	import time as _t  # type: ignore
	_t0 = _t.time()
	res = subprocess.run(cmd, capture_output=True, text=True)
	dur = float(_t.time() - _t0)
	return {
		"repo": repo,
		"returncode": res.returncode,
		"stdout_tail": (res.stdout or "")[-2000:],
		"stderr_tail": (res.stderr or "")[-2000:],
		"elapsed_sec": dur,
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--curriculum", default=str(root / "data" / "practice_repos.yaml"))
	ap.add_argument("--tier", default="T1")
	ap.add_argument("--out", default=str(root / "data" / "logs" / "scheduler_runs.jsonl"))
	ap.add_argument("--auto-progress", action="store_true", help="Automatically progress tier based on recent pass rate >= 0.7")
	ap.add_argument("--state", default=str(root / "data" / "logs" / "scheduler_state.json"))
	args = ap.parse_args()

	cur_path = Path(args.curriculum)
	if yaml is None:
		print("pyyaml not installed; pip install pyyaml")
		return 2
	if not cur_path.exists():
		print("curriculum not found:", str(cur_path))
		return 2
	cur = yaml.safe_load(cur_path.read_text(encoding="utf-8"))


	cmd = [
		sys.executable,
		str(root / "scripts" / "run_loop_dev.py"),
		"--repo",
		repo,
	]
	cmd.extend(pytest_args or [])
	import time as _t  # type: ignore
	_t0 = _t.time()
	res = subprocess.run(cmd, capture_output=True, text=True)
	dur = float(_t.time() - _t0)
	return {
		"repo": repo,
		"returncode": res.returncode,
		"stdout_tail": (res.stdout or "")[-2000:],
		"stderr_tail": (res.stderr or "")[-2000:],
		"elapsed_sec": dur,
	}

def main() -> int:
	ap = argparse.ArgumentParser()
	root = Path(__file__).resolve().parents[1]
	ap.add_argument("--curriculum", default=str(root / "data" / "practice_repos.yaml"))
	ap.add_argument("--tier", default="T1")
	ap.add_argument("--out", default=str(root / "data" / "logs" / "scheduler_runs.jsonl"))
	ap.add_argument("--auto-progress", action="store_true", help="Automatically progress tier based on recent pass rate >= 0.7")
	ap.add_argument("--state", default=str(root / "data" / "logs" / "scheduler_state.json"))
	args = ap.parse_args()

	cur_path = Path(args.curriculum)
	if yaml is None:
		print("pyyaml not installed; pip install pyyaml")
		return 2
	if not cur_path.exists():
		print("curriculum not found:", str(cur_path))
		return 2
	cur = yaml.safe_load(cur_path.read_text(encoding="utf-8"))
	tier_specs = (cur.get("tiers", {}) or {}).get(args.tier, [])
	# Optionally adjust tier based on recent state
	if args.auto_progress:
		try:
			st_path = Path(args.state)
			if st_path.exists():
				st = json.loads(st_path.read_text(encoding="utf-8"))
				last = st.get("history", {}).get(args.tier, [])
				# Use last 10 outcomes to estimate pass rate
				recent = last[-10:]
				if recent:
					rate = sum(1 for x in recent if bool(x)) / float(len(recent))
					# Simple tier order
					order = ["T1", "T2", "T3"]
					if rate >= 0.7 and args.tier in order:
						idx = order.index(args.tier)
						if idx + 1 < len(order):
							args.tier = order[idx + 1]
		except Exception:
			pass

	queue: List[Dict[str, Any]] = []
	for spec in tier_specs:
		if not isinstance(spec, dict):
			continue
		repo = str(spec.get("repo", "")).strip()
		if not repo:
			continue
		pytest_args = list(spec.get("pytest_args", []) or [])
		queue.append({"repo": repo, "pytest_args": pytest_args})

	out = Path(args.out)
	out.parent.mkdir(parents=True, exist_ok=True)
	completed = 0
	pass_outcomes: List[bool] = []
	for job in queue:
		res = run_dev_repo(root, job["repo"], job.get("pytest_args") or [])
		with out.open("a", encoding="utf-8") as f:
			f.write(json.dumps(res, ensure_ascii=False) + "\n")
		completed += 1
		print(json.dumps({"completed": completed, "last_repo": res.get("repo", "" )}))
		pass_outcomes.append(bool(res.get("returncode", 1) == 0))
	# Update state file with outcomes for this tier
	try:
		st_path = Path(args.state)
		st_path.parent.mkdir(parents=True, exist_ok=True)
		st = {}
		if st_path.exists():
			st = json.loads(st_path.read_text(encoding="utf-8"))
		hist = st.get("history", {})
		arr = hist.get(args.tier, [])
		arr.extend(pass_outcomes)
		hist[args.tier] = arr[-50:]  # cap history
		st["history"] = hist
		st_path.write_text(json.dumps(st, ensure_ascii=False, indent=2), encoding="utf-8")
	except Exception:
		pass
	return 0

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


from __future__ import annotations

import argparse
import re
from pathlib import Path
from typing import List, Tuple

FUTURE_LINE = "from __future__ import annotations\n"
ENCODING_RE = re.compile(r"coding[:=]\s*[-\w.]+")

def find_docstring_block(lines: List[str], start_idx: int) -> Tuple[int, int] | None:
    """Return (start, end_exclusive) for top-level module docstring if present."""
    # Skip blank lines and comments to detect a top-level docstring
    i = start_idx
    while i < len(lines) and (lines[i].strip() == "" or lines[i].lstrip().startswith("#")):
        i += 1
    if i >= len(lines):
        return None
    ls = lines[i].lstrip()
    if ls.startswith('"""') or ls.startswith("'''"):
        quote = '"""' if ls.startswith('"""') else "'''"
        # If the closing quote is on the same line
        if ls.count(quote) >= 2:
            return (i, i + 1)
        j = i + 1
        while j < len(lines):
            if quote in lines[j]:
                return (i, j + 1)
            j += 1
        # Unterminated docstring; treat as absent
    return None

def compute_insertion_index(lines: List[str]) -> int:
    """Compute the correct insertion index for the future import.

    Must be after: optional shebang, optional encoding cookie, optional module docstring.
    """
    idx = 0
    # Shebang
    if idx < len(lines) and lines[idx].startswith("#!"):
        idx += 1
    # Optional encoding cookie (usually first or second line)
    if idx < len(lines) and ENCODING_RE.search(lines[idx]):
        idx += 1
    # Module docstring block
    ds = find_docstring_block(lines, idx)
    if ds is not None:
        _, end = ds
        idx = end
    return idx

def normalize_future_import(text: str) -> str:
    lines = text.splitlines(keepends=True)
    if not any("from __future__ import annotations" in ln for ln in lines):
        return text  # nothing to do

    # Remove all occurrences of the future import (any whitespace, optional comment tail)
    new_lines: List[str] = []
    pattern = re.compile(r"^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$")
    for ln in lines:
        if pattern.match(ln.rstrip("\n")):
            continue
        new_lines.append(ln)

    # Ensure no leading BOM/whitespace-only that would precede the future import improperly
    insert_at = compute_insertion_index(new_lines)
    # Make sure the future import is the very first line if there is no docstring/shebang/encoding
    # and avoid inserting duplicates if it is already in place (after removal, it won't be)
    new_lines.insert(insert_at, FUTURE_LINE)
    return "".join(new_lines)

def process_file(path: Path, write: bool) -> bool:
    try:
        orig = path.read_text(encoding="utf-8")
    except Exception:
        return False
    fixed = normalize_future_import(orig)
    if fixed == orig:
        return False
    if write:
        try:
            path.write_text(fixed, encoding="utf-8")
        except Exception:
            return False
    return True

def should_skip(p: Path) -> bool:
    parts = set(p.parts)
    skip_dirs = {"data", "node_modules", ".git", ".venv", "__pycache__", "data_cache"}
    # Quick path-based filter to avoid huge vendored/venv trees
    if any(sd in parts for sd in skip_dirs):
        # Allow under data/scripts, but skip data/conda_envs specifically
        if "data" in parts and "conda_envs" in parts:
            return True
    return False

def main() -> int:
    ap = argparse.ArgumentParser(description="Move 'from __future__ import annotations' to the top of files")
    ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]), help="Repo root to scan")
    ap.add_argument("--write", action="store_true", help="Apply changes in-place (default: dry-run)")
    args = ap.parse_args()

    root = Path(args.root)
    changed = 0
    scanned = 0
    for path in root.rglob("*.py"):
        if should_skip(path):
            continue
        scanned += 1
        if process_file(path, write=bool(args.write)):
            changed += 1

    print({"ok": True, "scanned": scanned, "changed": changed, "write": bool(args.write)})
    return 0

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


from __future__ import annotations

import argparse
import re
from pathlib import Path
from typing import List, Tuple

FUTURE_LINE = "from __future__ import annotations\n"
ENCODING_RE = re.compile(r"coding[:=]\s*[-\w.]+")

def find_docstring_block(lines: List[str], start_idx: int) -> Tuple[int, int] | None:
    """Return (start, end_exclusive) for top-level module docstring if present."""
    # Skip blank lines and comments to detect a top-level docstring
    i = start_idx
    while i < len(lines) and (lines[i].strip() == "" or lines[i].lstrip().startswith("#")):
        i += 1
    if i >= len(lines):
        return None
    ls = lines[i].lstrip()
    if ls.startswith('"""') or ls.startswith("'''"):
        quote = '"""' if ls.startswith('"""') else "'''"
        # If the closing quote is on the same line
        if ls.count(quote) >= 2:
            return (i, i + 1)
        j = i + 1
        while j < len(lines):
            if quote in lines[j]:
                return (i, j + 1)
            j += 1
        # Unterminated docstring; treat as absent
    return None

def compute_insertion_index(lines: List[str]) -> int:
    """Compute the correct insertion index for the future import.

    Must be after: optional shebang, optional encoding cookie, optional module docstring.
    """
    idx = 0
    # Shebang
    if idx < len(lines) and lines[idx].startswith("#!"):
        idx += 1
    # Optional encoding cookie (usually first or second line)
    if idx < len(lines) and ENCODING_RE.search(lines[idx]):
        idx += 1
    # Module docstring block
    ds = find_docstring_block(lines, idx)
    if ds is not None:
        _, end = ds
        idx = end
    return idx


    i = start_idx
    while i < len(lines) and (lines[i].strip() == "" or lines[i].lstrip().startswith("#")):
        i += 1
    if i >= len(lines):
        return None
    ls = lines[i].lstrip()
    if ls.startswith('"""') or ls.startswith("'''"):
        quote = '"""' if ls.startswith('"""') else "'''"
        # If the closing quote is on the same line
        if ls.count(quote) >= 2:
            return (i, i + 1)
        j = i + 1
        while j < len(lines):
            if quote in lines[j]:
                return (i, j + 1)
            j += 1
        # Unterminated docstring; treat as absent
    return None

def compute_insertion_index(lines: List[str]) -> int:
    """Compute the correct insertion index for the future import.

    Must be after: optional shebang, optional encoding cookie, optional module docstring.
    """
    idx = 0
    # Shebang
    if idx < len(lines) and lines[idx].startswith("#!"):
        idx += 1
    # Optional encoding cookie (usually first or second line)
    if idx < len(lines) and ENCODING_RE.search(lines[idx]):
        idx += 1
    # Module docstring block
    ds = find_docstring_block(lines, idx)
    if ds is not None:
        _, end = ds
        idx = end
    return idx

def normalize_future_import(text: str) -> str:
    lines = text.splitlines(keepends=True)
    if not any("from __future__ import annotations" in ln for ln in lines):
        return text  # nothing to do

    # Remove all occurrences of the future import (any whitespace, optional comment tail)
    new_lines: List[str] = []
    pattern = re.compile(r"^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$")
    for ln in lines:
        if pattern.match(ln.rstrip("\n")):
            continue
        new_lines.append(ln)

    # Ensure no leading BOM/whitespace-only that would precede the future import improperly
    insert_at = compute_insertion_index(new_lines)
    # Make sure the future import is the very first line if there is no docstring/shebang/encoding
    # and avoid inserting duplicates if it is already in place (after removal, it won't be)
    new_lines.insert(insert_at, FUTURE_LINE)


def compute_insertion_index(lines: List[str]) -> int:
    """Compute the correct insertion index for the future import.

    Must be after: optional shebang, optional encoding cookie, optional module docstring.
    """
    idx = 0
    # Shebang
    if idx < len(lines) and lines[idx].startswith("#!"):
        idx += 1
    # Optional encoding cookie (usually first or second line)
    if idx < len(lines) and ENCODING_RE.search(lines[idx]):
        idx += 1
    # Module docstring block
    ds = find_docstring_block(lines, idx)
    if ds is not None:
        _, end = ds
        idx = end
    return idx

def normalize_future_import(text: str) -> str:
    lines = text.splitlines(keepends=True)
    if not any("from __future__ import annotations" in ln for ln in lines):
        return text  # nothing to do

    # Remove all occurrences of the future import (any whitespace, optional comment tail)
    new_lines: List[str] = []
    pattern = re.compile(r"^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$")
    for ln in lines:
        if pattern.match(ln.rstrip("\n")):
            continue
        new_lines.append(ln)

    # Ensure no leading BOM/whitespace-only that would precede the future import improperly
    insert_at = compute_insertion_index(new_lines)
    # Make sure the future import is the very first line if there is no docstring/shebang/encoding
    # and avoid inserting duplicates if it is already in place (after removal, it won't be)
    new_lines.insert(insert_at, FUTURE_LINE)
    return "".join(new_lines)

def process_file(path: Path, write: bool) -> bool:
    try:
        orig = path.read_text(encoding="utf-8")
    except Exception:
        return False
    fixed = normalize_future_import(orig)
    if fixed == orig:
        return False
    if write:
        try:
            path.write_text(fixed, encoding="utf-8")
        except Exception:
            return False
    return True

def should_skip(p: Path) -> bool:
    parts = set(p.parts)


    lines = text.splitlines(keepends=True)
    if not any("from __future__ import annotations" in ln for ln in lines):
        return text  # nothing to do

    # Remove all occurrences of the future import (any whitespace, optional comment tail)
    new_lines: List[str] = []
    pattern = re.compile(r"^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$")
    for ln in lines:
        if pattern.match(ln.rstrip("\n")):
            continue
        new_lines.append(ln)

    # Ensure no leading BOM/whitespace-only that would precede the future import improperly
    insert_at = compute_insertion_index(new_lines)
    # Make sure the future import is the very first line if there is no docstring/shebang/encoding
    # and avoid inserting duplicates if it is already in place (after removal, it won't be)
    new_lines.insert(insert_at, FUTURE_LINE)
    return "".join(new_lines)

def process_file(path: Path, write: bool) -> bool:
    try:
        orig = path.read_text(encoding="utf-8")
    except Exception:
        return False
    fixed = normalize_future_import(orig)
    if fixed == orig:
        return False
    if write:
        try:
            path.write_text(fixed, encoding="utf-8")
        except Exception:
            return False
    return True

def should_skip(p: Path) -> bool:
    parts = set(p.parts)
    skip_dirs = {"data", "node_modules", ".git", ".venv", "__pycache__", "data_cache"}
    # Quick path-based filter to avoid huge vendored/venv trees
    if any(sd in parts for sd in skip_dirs):
        # Allow under data/scripts, but skip data/conda_envs specifically
        if "data" in parts and "conda_envs" in parts:
            return True
    return False

def main() -> int:
    ap = argparse.ArgumentParser(description="Move 'from __future__ import annotations' to the top of files")
    ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]), help="Repo root to scan")
    ap.add_argument("--write", action="store_true", help="Apply changes in-place (default: dry-run)")
    args = ap.parse_args()

    root = Path(args.root)


    new_lines.insert(insert_at, FUTURE_LINE)
    return "".join(new_lines)

def process_file(path: Path, write: bool) -> bool:
    try:
        orig = path.read_text(encoding="utf-8")
    except Exception:
        return False
    fixed = normalize_future_import(orig)
    if fixed == orig:
        return False
    if write:
        try:
            path.write_text(fixed, encoding="utf-8")
        except Exception:
            return False
    return True

def should_skip(p: Path) -> bool:
    parts = set(p.parts)
    skip_dirs = {"data", "node_modules", ".git", ".venv", "__pycache__", "data_cache"}
    # Quick path-based filter to avoid huge vendored/venv trees
    if any(sd in parts for sd in skip_dirs):
        # Allow under data/scripts, but skip data/conda_envs specifically
        if "data" in parts and "conda_envs" in parts:
            return True
    return False

def main() -> int:
    ap = argparse.ArgumentParser(description="Move 'from __future__ import annotations' to the top of files")
    ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]), help="Repo root to scan")
    ap.add_argument("--write", action="store_true", help="Apply changes in-place (default: dry-run)")
    args = ap.parse_args()

    root = Path(args.root)
    changed = 0
    scanned = 0
    for path in root.rglob("*.py"):
        if should_skip(path):
            continue
        scanned += 1
        if process_file(path, write=bool(args.write)):
            changed += 1

    print({"ok": True, "scanned": scanned, "changed": changed, "write": bool(args.write)})
    return 0


        return False
    if write:
        try:
            path.write_text(fixed, encoding="utf-8")
        except Exception:
            return False
    return True

def should_skip(p: Path) -> bool:
    parts = set(p.parts)
    skip_dirs = {"data", "node_modules", ".git", ".venv", "__pycache__", "data_cache"}
    # Quick path-based filter to avoid huge vendored/venv trees
    if any(sd in parts for sd in skip_dirs):
        # Allow under data/scripts, but skip data/conda_envs specifically
        if "data" in parts and "conda_envs" in parts:
            return True
    return False

def main() -> int:
    ap = argparse.ArgumentParser(description="Move 'from __future__ import annotations' to the top of files")
    ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]), help="Repo root to scan")
    ap.add_argument("--write", action="store_true", help="Apply changes in-place (default: dry-run)")
    args = ap.parse_args()

    root = Path(args.root)
    changed = 0
    scanned = 0
    for path in root.rglob("*.py"):
        if should_skip(path):
            continue
        scanned += 1
        if process_file(path, write=bool(args.write)):
            changed += 1

    print({"ok": True, "scanned": scanned, "changed": changed, "write": bool(args.write)})
    return 0

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


from __future__ import annotations
import logging

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

from agi_dw.tools.failure_classifier import classify_failures
from agi_dw.tools.test_runner import TestRunner
from agi_dw.tools.lint_type import run_flake8 as lt_run_flake8, run_mypy as lt_run_mypy
from agi_dw.tools.patch_actuator import apply_unified_diff

def _read_text_safe(p: Path) -> str:
	try:
		return p.read_text(encoding="utf-8")
	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 {}
	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)
			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]
			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:
						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")
	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 difflib
import json
import re
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import subprocess

from agi_dw.tools.failure_classifier import classify_failures
from agi_dw.tools.test_runner import TestRunner
from agi_dw.tools.lint_type import run_flake8 as lt_run_flake8, run_mypy as lt_run_mypy
from agi_dw.tools.patch_actuator import apply_unified_diff

def _read_text_safe(p: Path) -> str:
	try:
		return p.read_text(encoding="utf-8")
	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."""


import json
import re
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import subprocess

from agi_dw.tools.failure_classifier import classify_failures
from agi_dw.tools.test_runner import TestRunner
from agi_dw.tools.lint_type import run_flake8 as lt_run_flake8, run_mypy as lt_run_mypy
from agi_dw.tools.patch_actuator import apply_unified_diff

def _read_text_safe(p: Path) -> str:
	try:
		return p.read_text(encoding="utf-8")
	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: