		if not self.decisions_path.exists():
			self.decisions_path.write_text("", encoding="utf-8")

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

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

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

	def wait_for_decision(self, item_id: str, timeout_sec: int = 60, poll_ms: int = 500) -> Optional[Dict[str, Any]]:
		end = time.time() + max(1, int(timeout_sec))
		pos = 0
		while time.time() < end:
			try:
				with self.decisions_path.open("r", encoding="utf-8") as f:
					f.seek(pos)
					for line in f:
						pos = f.tell()
						if not line.strip():
							continue
						obj = json.loads(line)
						if str(obj.get("id")) == item_id:
							return obj
			except Exception:
				pass
			time.sleep(max(0.001, float(poll_ms) / 1000.0))
		return None


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

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

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

	def wait_for_decision(self, item_id: str, timeout_sec: int = 60, poll_ms: int = 500) -> Optional[Dict[str, Any]]:
		end = time.time() + max(1, int(timeout_sec))
		pos = 0
		while time.time() < end:
			try:
				with self.decisions_path.open("r", encoding="utf-8") as f:
					f.seek(pos)
					for line in f:
						pos = f.tell()
						if not line.strip():
							continue
						obj = json.loads(line)
						if str(obj.get("id")) == item_id:
							return obj
			except Exception:
				pass
			time.sleep(max(0.001, float(poll_ms) / 1000.0))
		return None

	# --- New helpers and maintenance APIs ---

	def _parse_ts(self, ts_str: str) -> float:
		"""Parse TS string like 20250101T000000Z or ISO8601 into epoch seconds."""
		try:
			# Fast-path: canonical "%Y%m%dT%H%M%SZ"
			return datetime.strptime(str(ts_str), "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc).timestamp()
		except Exception:
			pass
		try:
			# Fallback to fromisoformat variants
			s = str(ts_str).replace("Z", "+00:00")
			return datetime.fromisoformat(s).timestamp()
		except Exception:
			return 0.0

	def _has_decision(self, item_id: str) -> bool:
		"""Return True if a decision exists for given id."""
		try:


		end = time.time() + max(1, int(timeout_sec))
		pos = 0
		while time.time() < end:
			try:
				with self.decisions_path.open("r", encoding="utf-8") as f:
					f.seek(pos)
					for line in f:
						pos = f.tell()
						if not line.strip():
							continue
						obj = json.loads(line)
						if str(obj.get("id")) == item_id:
							return obj
			except Exception:
				pass
			time.sleep(max(0.001, float(poll_ms) / 1000.0))
		return None

	# --- New helpers and maintenance APIs ---

	def _parse_ts(self, ts_str: str) -> float:
		"""Parse TS string like 20250101T000000Z or ISO8601 into epoch seconds."""
		try:
			# Fast-path: canonical "%Y%m%dT%H%M%SZ"
			return datetime.strptime(str(ts_str), "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc).timestamp()
		except Exception:
			pass
		try:
			# Fallback to fromisoformat variants
			s = str(ts_str).replace("Z", "+00:00")
			return datetime.fromisoformat(s).timestamp()
		except Exception:
			return 0.0

	def _has_decision(self, item_id: str) -> bool:
		"""Return True if a decision exists for given id."""
		try:
			for line in self.decisions_path.read_text(encoding="utf-8").splitlines():
				if not line.strip():
					continue
				obj = json.loads(line)
				if str(obj.get("id")) == str(item_id):
					return True
		except Exception:
			return False
		return False

	def update_status(self, item_id: str, decision: str, note: str | None = None, actor: str = "system") -> Dict[str, Any]:
		"""Append a decision record for an item. Keeps queue JSONL append-only.

		Returns the decision object written.
		"""
		# Build record


				pass
			time.sleep(max(0.001, float(poll_ms) / 1000.0))
		return None

	# --- New helpers and maintenance APIs ---

	def _parse_ts(self, ts_str: str) -> float:
		"""Parse TS string like 20250101T000000Z or ISO8601 into epoch seconds."""
		try:
			# Fast-path: canonical "%Y%m%dT%H%M%SZ"
			return datetime.strptime(str(ts_str), "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc).timestamp()
		except Exception:
			pass
		try:
			# Fallback to fromisoformat variants
			s = str(ts_str).replace("Z", "+00:00")
			return datetime.fromisoformat(s).timestamp()
		except Exception:
			return 0.0

	def _has_decision(self, item_id: str) -> bool:
		"""Return True if a decision exists for given id."""
		try:
			for line in self.decisions_path.read_text(encoding="utf-8").splitlines():
				if not line.strip():
					continue
				obj = json.loads(line)
				if str(obj.get("id")) == str(item_id):
					return True
		except Exception:
			return False
		return False

	def update_status(self, item_id: str, decision: str, note: str | None = None, actor: str = "system") -> Dict[str, Any]:
		"""Append a decision record for an item. Keeps queue JSONL append-only.

		Returns the decision object written.
		"""
		# Build record
		rec: Dict[str, Any] = {
			"id": str(item_id),
			"decision": str(decision),
			"note": str(note or ""),
			"ts": datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ"),
		}
		# Append to decisions log
		self.write_decision(rec)
		# Append to immutable audit log if available
		try:
			try:
				from agi_dw.core.hitl.audit_log import AuditLog  # type: ignore
			except ModuleNotFoundError:


		try:
			# Fallback to fromisoformat variants
			s = str(ts_str).replace("Z", "+00:00")
			return datetime.fromisoformat(s).timestamp()
		except Exception:
			return 0.0

	def _has_decision(self, item_id: str) -> bool:
		"""Return True if a decision exists for given id."""
		try:
			for line in self.decisions_path.read_text(encoding="utf-8").splitlines():
				if not line.strip():
					continue
				obj = json.loads(line)
				if str(obj.get("id")) == str(item_id):
					return True
		except Exception:
			return False
		return False

	def update_status(self, item_id: str, decision: str, note: str | None = None, actor: str = "system") -> Dict[str, Any]:
		"""Append a decision record for an item. Keeps queue JSONL append-only.

		Returns the decision object written.
		"""
		# Build record
		rec: Dict[str, Any] = {
			"id": str(item_id),
			"decision": str(decision),
			"note": str(note or ""),
			"ts": datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ"),
		}
		# Append to decisions log
		self.write_decision(rec)
		# Append to immutable audit log if available
		try:
			try:
				from agi_dw.core.hitl.audit_log import AuditLog  # type: ignore
			except ModuleNotFoundError:
				import sys as _sys  # type: ignore
				_root = Path(__file__).resolve().parents[2]
				for p in (str(_root), str(_root / "agi_dw")):
					if p not in _sys.path:
						_sys.path.insert(0, p)
				from agi_dw.core.hitl.audit_log import AuditLog  # type: ignore
			AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor=str(actor), data=rec)
		except Exception:
			pass
		return rec

	def sweep_expired(self, ttl_sec: int = 600, actor: str = "sweeper") -> List[Dict[str, Any]]:
		"""Mark pending items older than ttl as expired by writing decisions.

		This does not rewrite the queue; it appends to decisions.jsonl only.
		Returns a list of decision objects written.
		"""
		written: List[Dict[str, Any]] = []
		try:
			now = time.time()
			pend = self.read_pending()
			for obj in pend:
				try:
					id_ = str(obj.get("id", ""))
					if not id_ or self._has_decision(id_):
						continue
					ts_str = str(obj.get("ts", ""))
					age = now - float(self._parse_ts(ts_str))
					if ttl_sec > 0 and age >= float(ttl_sec):
						dec = self.update_status(id_, "expired", note="ttl_expired", actor=actor)


			"ts": datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ"),
		}
		# Append to decisions log
		self.write_decision(rec)
		# Append to immutable audit log if available
		try:
			try:
				from agi_dw.core.hitl.audit_log import AuditLog  # type: ignore
			except ModuleNotFoundError:
				import sys as _sys  # type: ignore
				_root = Path(__file__).resolve().parents[2]
				for p in (str(_root), str(_root / "agi_dw")):
					if p not in _sys.path:
						_sys.path.insert(0, p)
				from agi_dw.core.hitl.audit_log import AuditLog  # type: ignore
			AuditLog(Path(__file__).resolve().parents[2]).append(action=f"decision.{decision}", actor=str(actor), data=rec)
		except Exception:
			pass
		return rec

	def sweep_expired(self, ttl_sec: int = 600, actor: str = "sweeper") -> List[Dict[str, Any]]:
		"""Mark pending items older than ttl as expired by writing decisions.

		This does not rewrite the queue; it appends to decisions.jsonl only.
		Returns a list of decision objects written.
		"""
		written: List[Dict[str, Any]] = []
		try:
			now = time.time()
			pend = self.read_pending()
			for obj in pend:
				try:
					id_ = str(obj.get("id", ""))
					if not id_ or self._has_decision(id_):
						continue
					ts_str = str(obj.get("ts", ""))
					age = now - float(self._parse_ts(ts_str))
					if ttl_sec > 0 and age >= float(ttl_sec):
						dec = self.update_status(id_, "expired", note="ttl_expired", actor=actor)
						written.append(dec)
				except Exception:
					continue
		except Exception:
			return written
		return written


from __future__ import annotations

import logging
import hashlib
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

@dataclass
class AuditRecord:
	ts: str
	action: str
	actor: str
	data: Dict[str, Any]
	prev_hash: str
	hash: str

class AuditLog:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.path = root / "data" / "hitl" / "audit.jsonl"
		self.path.parent.mkdir(parents=True, exist_ok=True)
		if not self.path.exists():
			self.path.write_text("", encoding="utf-8")

	def _last_hash(self) -> str:
		try:
			last = ""
			for line in self.path.read_text(encoding="utf-8").splitlines():
				last = line
			if last:
				obj = json.loads(last)
				return str(obj.get("hash", ""))
			return ""
		except Exception:
			return ""

	def append(self, action: str, actor: str, data: Dict[str, Any]) -> AuditRecord:
		prev = self._last_hash()
		rec = {
			"ts": datetime.now(timezone.utc).isoformat(),
			"action": action,
			"actor": actor,
			"data": data,
			"prev_hash": prev,
		}
		h = hashlib.sha256(json.dumps(rec, sort_keys=True).encode("utf-8")).hexdigest()
		obj = AuditRecord(ts=rec["ts"], action=action, actor=actor, data=data, prev_hash=prev, hash=h)
		with self.path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(obj), ensure_ascii=False) + "\n")
		return obj


AuditRecord

from __future__ import annotations

import logging
import hashlib
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

@dataclass
class AuditRecord:
	ts: str
	action: str
	actor: str
	data: Dict[str, Any]
	prev_hash: str
	hash: str

class AuditLog:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.path = root / "data" / "hitl" / "audit.jsonl"
		self.path.parent.mkdir(parents=True, exist_ok=True)
		if not self.path.exists():
			self.path.write_text("", encoding="utf-8")

	def _last_hash(self) -> str:
		try:
			last = ""
			for line in self.path.read_text(encoding="utf-8").splitlines():
				last = line
			if last:
				obj = json.loads(last)
				return str(obj.get("hash", ""))
			return ""
		except Exception:


AuditLog

import logging
import hashlib
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

@dataclass
class AuditRecord:
	ts: str
	action: str
	actor: str
	data: Dict[str, Any]
	prev_hash: str
	hash: str

class AuditLog:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.path = root / "data" / "hitl" / "audit.jsonl"
		self.path.parent.mkdir(parents=True, exist_ok=True)
		if not self.path.exists():
			self.path.write_text("", encoding="utf-8")

	def _last_hash(self) -> str:
		try:
			last = ""
			for line in self.path.read_text(encoding="utf-8").splitlines():
				last = line
			if last:
				obj = json.loads(last)
				return str(obj.get("hash", ""))
			return ""
		except Exception:
			return ""

	def append(self, action: str, actor: str, data: Dict[str, Any]) -> AuditRecord:
		prev = self._last_hash()
		rec = {
			"ts": datetime.now(timezone.utc).isoformat(),
			"action": action,
			"actor": actor,
			"data": data,
			"prev_hash": prev,
		}
		h = hashlib.sha256(json.dumps(rec, sort_keys=True).encode("utf-8")).hexdigest()
		obj = AuditRecord(ts=rec["ts"], action=action, actor=actor, data=data, prev_hash=prev, hash=h)
		with self.path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(obj), ensure_ascii=False) + "\n")
		return obj


import logging
import hashlib
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

@dataclass
class AuditRecord:
	ts: str
	action: str
	actor: str
	data: Dict[str, Any]
	prev_hash: str
	hash: str

class AuditLog:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.path = root / "data" / "hitl" / "audit.jsonl"
		self.path.parent.mkdir(parents=True, exist_ok=True)
		if not self.path.exists():
			self.path.write_text("", encoding="utf-8")

	def _last_hash(self) -> str:
		try:
			last = ""
			for line in self.path.read_text(encoding="utf-8").splitlines():
				last = line
			if last:
				obj = json.loads(last)
				return str(obj.get("hash", ""))
			return ""
		except Exception:
			return ""

	def append(self, action: str, actor: str, data: Dict[str, Any]) -> AuditRecord:
		prev = self._last_hash()
		rec = {
			"ts": datetime.now(timezone.utc).isoformat(),
			"action": action,
			"actor": actor,
			"data": data,


@dataclass
class AuditRecord:
	ts: str
	action: str
	actor: str
	data: Dict[str, Any]
	prev_hash: str
	hash: str

class AuditLog:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.path = root / "data" / "hitl" / "audit.jsonl"
		self.path.parent.mkdir(parents=True, exist_ok=True)
		if not self.path.exists():
			self.path.write_text("", encoding="utf-8")

	def _last_hash(self) -> str:
		try:
			last = ""
			for line in self.path.read_text(encoding="utf-8").splitlines():
				last = line
			if last:
				obj = json.loads(last)
				return str(obj.get("hash", ""))
			return ""
		except Exception:
			return ""

	def append(self, action: str, actor: str, data: Dict[str, Any]) -> AuditRecord:
		prev = self._last_hash()
		rec = {
			"ts": datetime.now(timezone.utc).isoformat(),
			"action": action,
			"actor": actor,
			"data": data,
			"prev_hash": prev,
		}
		h = hashlib.sha256(json.dumps(rec, sort_keys=True).encode("utf-8")).hexdigest()
		obj = AuditRecord(ts=rec["ts"], action=action, actor=actor, data=data, prev_hash=prev, hash=h)
		with self.path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(obj), ensure_ascii=False) + "\n")
		return obj


append

class AuditLog:
	def __init__(self, root_dir: str | Path) -> None:
		root = Path(root_dir)
		self.path = root / "data" / "hitl" / "audit.jsonl"
		self.path.parent.mkdir(parents=True, exist_ok=True)
		if not self.path.exists():
			self.path.write_text("", encoding="utf-8")

	def _last_hash(self) -> str:
		try:
			last = ""
			for line in self.path.read_text(encoding="utf-8").splitlines():
				last = line
			if last:
				obj = json.loads(last)
				return str(obj.get("hash", ""))
			return ""
		except Exception:
			return ""

	def append(self, action: str, actor: str, data: Dict[str, Any]) -> AuditRecord:
		prev = self._last_hash()
		rec = {
			"ts": datetime.now(timezone.utc).isoformat(),
			"action": action,
			"actor": actor,
			"data": data,
			"prev_hash": prev,
		}
		h = hashlib.sha256(json.dumps(rec, sort_keys=True).encode("utf-8")).hexdigest()
		obj = AuditRecord(ts=rec["ts"], action=action, actor=actor, data=data, prev_hash=prev, hash=h)
		with self.path.open("a", encoding="utf-8") as f:
			f.write(json.dumps(asdict(obj), ensure_ascii=False) + "\n")
		return obj


from __future__ import annotations

import logging
from pathlib import Path
from typing import Dict, Any
import json
from agi_dw.tools.redaction import redact_text

def render_patch_preview(repo_dir: str | Path, diff_text: str, policy_meta: Dict[str, Any]) -> str:
	lines = []
	lines.append("=== PATCH PREVIEW ===")
	lines.append(f"repo: {Path(repo_dir).resolve()}")
	try:
		files = policy_meta.get("files") or []
		added = int(policy_meta.get("added", 0))
		deleted = int(policy_meta.get("deleted", 0))
		lines.append(f"files: {len(files)}")
		for fp in (files[:20] if isinstance(files, list) else []):
			lines.append(f"  - {fp}")
		lines.append(f"churn: +{added} / -{deleted}")
		# Optional policy annotations
		try:
			risk = policy_meta.get("risk", None)
			if risk is not None:
				lines.append(f"risk: {float(risk):.3f}")
		except Exception:
			pass
		try:
			al = policy_meta.get("allowlist_hit", None)
			if al is not None:
				lines.append(f"allowlist_hit: {bool(al)}")
		except Exception:
			pass
		try:
			sig = str(policy_meta.get("signature", ""))
			if sig:
				lines.append(f"signature: {sig}")
		except Exception:
			pass
		try:
			docp = str(policy_meta.get("design_doc", ""))
			if docp:
				lines.append(f"design_doc: {docp}")
		except Exception:
			pass
		try:
			crit = str(policy_meta.get("critic", ""))
			if crit:
				lines.append("critic:")
				for ln in crit.splitlines()[:10]:
					lines.append(f"  - {ln}")
		except Exception:
			pass
	except Exception:
		pass
	lines.append("--- unified diff (truncated to 2000 chars) ---")
	try:
		txt = str(diff_text or "")
		if len(txt) > 2000:
			txt = txt[:2000] + "\n... (truncated)"
		lines.append(txt)
	except Exception:
		lines.append("(unavailable)")
	return "\n".join(lines)

def write_preview(path: str | Path, text: str) -> str:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	# Redact sensitive patterns before writing
	p.write_text(redact_text(text), encoding="utf-8")
	return str(p)

def render_cli_preview(cwd: str | Path, command: str, env: Dict[str, Any] | None = None) -> str:
	lines = []
	lines.append("=== CLI OP PREVIEW ===")
	try:
		lines.append(f"cwd: {Path(cwd).resolve()}")
	except Exception:
		lines.append(f"cwd: {cwd}")
	lines.append(f"command: {str(command)}")
	try:
		if env:
			# Only show a filtered set of env keys to avoid leaking secrets
			safe_env = {k: ("[REDACTED]" if "KEY" in k or "TOKEN" in k or "SECRET" in k else str(v)) for k, v in (env or {}).items()}
			lines.append("env:")
			for k in sorted(safe_env.keys())[:20]:
				lines.append(f"  {k}={safe_env[k]}")
	except Exception:
		pass
	return "\n".join(lines)


from __future__ import annotations

import logging
from pathlib import Path
from typing import Dict, Any
import json
from agi_dw.tools.redaction import redact_text

def render_patch_preview(repo_dir: str | Path, diff_text: str, policy_meta: Dict[str, Any]) -> str:
	lines = []
	lines.append("=== PATCH PREVIEW ===")
	lines.append(f"repo: {Path(repo_dir).resolve()}")
	try:
		files = policy_meta.get("files") or []
		added = int(policy_meta.get("added", 0))
		deleted = int(policy_meta.get("deleted", 0))
		lines.append(f"files: {len(files)}")
		for fp in (files[:20] if isinstance(files, list) else []):
			lines.append(f"  - {fp}")
		lines.append(f"churn: +{added} / -{deleted}")
		# Optional policy annotations
		try:
			risk = policy_meta.get("risk", None)
			if risk is not None:
				lines.append(f"risk: {float(risk):.3f}")
		except Exception:
			pass
		try:
			al = policy_meta.get("allowlist_hit", None)
			if al is not None:
				lines.append(f"allowlist_hit: {bool(al)}")
		except Exception:
			pass
		try:
			sig = str(policy_meta.get("signature", ""))
			if sig:
				lines.append(f"signature: {sig}")
		except Exception:
			pass
		try:
			docp = str(policy_meta.get("design_doc", ""))
			if docp:
				lines.append(f"design_doc: {docp}")
		except Exception:
			pass
		try:
			crit = str(policy_meta.get("critic", ""))
			if crit:
				lines.append("critic:")
				for ln in crit.splitlines()[:10]:
					lines.append(f"  - {ln}")
		except Exception:
			pass
	except Exception:
		pass
	lines.append("--- unified diff (truncated to 2000 chars) ---")
	try:
		txt = str(diff_text or "")
		if len(txt) > 2000:
			txt = txt[:2000] + "\n... (truncated)"
		lines.append(txt)
	except Exception:
		lines.append("(unavailable)")
	return "\n".join(lines)

def write_preview(path: str | Path, text: str) -> str:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	# Redact sensitive patterns before writing
	p.write_text(redact_text(text), encoding="utf-8")
	return str(p)

def render_cli_preview(cwd: str | Path, command: str, env: Dict[str, Any] | None = None) -> str:
	lines = []
	lines.append("=== CLI OP PREVIEW ===")
	try:
		lines.append(f"cwd: {Path(cwd).resolve()}")
	except Exception:
		lines.append(f"cwd: {cwd}")
	lines.append(f"command: {str(command)}")
	try:
		if env:


			crit = str(policy_meta.get("critic", ""))
			if crit:
				lines.append("critic:")
				for ln in crit.splitlines()[:10]:
					lines.append(f"  - {ln}")
		except Exception:
			pass
	except Exception:
		pass
	lines.append("--- unified diff (truncated to 2000 chars) ---")
	try:
		txt = str(diff_text or "")
		if len(txt) > 2000:
			txt = txt[:2000] + "\n... (truncated)"
		lines.append(txt)
	except Exception:
		lines.append("(unavailable)")
	return "\n".join(lines)

def write_preview(path: str | Path, text: str) -> str:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	# Redact sensitive patterns before writing
	p.write_text(redact_text(text), encoding="utf-8")
	return str(p)

def render_cli_preview(cwd: str | Path, command: str, env: Dict[str, Any] | None = None) -> str:
	lines = []
	lines.append("=== CLI OP PREVIEW ===")
	try:
		lines.append(f"cwd: {Path(cwd).resolve()}")
	except Exception:
		lines.append(f"cwd: {cwd}")
	lines.append(f"command: {str(command)}")
	try:
		if env:
			# Only show a filtered set of env keys to avoid leaking secrets
			safe_env = {k: ("[REDACTED]" if "KEY" in k or "TOKEN" in k or "SECRET" in k else str(v)) for k, v in (env or {}).items()}
			lines.append("env:")
			for k in sorted(safe_env.keys())[:20]:
				lines.append(f"  {k}={safe_env[k]}")
	except Exception:
		pass
	return "\n".join(lines)


		pass
	lines.append("--- unified diff (truncated to 2000 chars) ---")
	try:
		txt = str(diff_text or "")
		if len(txt) > 2000:
			txt = txt[:2000] + "\n... (truncated)"
		lines.append(txt)
	except Exception:
		lines.append("(unavailable)")
	return "\n".join(lines)

def write_preview(path: str | Path, text: str) -> str:
	p = Path(path)
	p.parent.mkdir(parents=True, exist_ok=True)
	# Redact sensitive patterns before writing
	p.write_text(redact_text(text), encoding="utf-8")
	return str(p)

def render_cli_preview(cwd: str | Path, command: str, env: Dict[str, Any] | None = None) -> str:
	lines = []
	lines.append("=== CLI OP PREVIEW ===")
	try:
		lines.append(f"cwd: {Path(cwd).resolve()}")
	except Exception:
		lines.append(f"cwd: {cwd}")
	lines.append(f"command: {str(command)}")
	try:
		if env:
			# Only show a filtered set of env keys to avoid leaking secrets
			safe_env = {k: ("[REDACTED]" if "KEY" in k or "TOKEN" in k or "SECRET" in k else str(v)) for k, v in (env or {}).items()}
			lines.append("env:")
			for k in sorted(safe_env.keys())[:20]:
				lines.append(f"  {k}={safe_env[k]}")
	except Exception:
		pass
	return "\n".join(lines)


from __future__ import annotations
import logging

import hashlib
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple, Any

import json

APPROVAL_DECISIONS = {"approved", "denied", "modified", "deferred", "expired"}

def _sanitize_diff_for_hash(diff_text: str) -> str:
	"""Keep only stable, schema-relevant lines for signature hashing."""
	kept: List[str] = []
	for line in (diff_text or "").splitlines():
		if line.startswith("diff --git") or line.startswith("index ") or line.startswith("--- ") or line.startswith("+++ ") or line.startswith("@@ "):
			kept.append(line)
		elif line.startswith("+") or line.startswith("-") or line.startswith(" "):
			kept.append(line)
	return "\n".join(kept).strip()

def compute_signature(diff_text: str) -> str:
	"""Stable content hash to link preview/decision records."""
	stable = _sanitize_diff_for_hash(diff_text)
	return hashlib.sha256(stable.encode("utf-8")).hexdigest()

def _glob_match(path: str, patterns: List[str]) -> bool:
	try:
		import fnmatch as _fn
		for pat in (patterns or []):
			if _fn.fnmatch(path, pat):
				return True
	except Exception:
		return False
	return False

def estimate_risk(
	diff_text: str,
	files: List[str],
	added: int,
	deleted: int,
	allow_paths: List[str] | None = None,
	block_paths: List[str] | None = None,
) -> Tuple[float, Dict[str, float]]:
	"""Heuristic risk in [0,1] with components for transparency."""
	allow_paths = allow_paths or []
	block_paths = block_paths or []
	score = 0.2
	components: Dict[str, float] = {}
	# File count risk
	file_pen = min(0.5, 0.1 * max(0, len(files) - 1))
	components["files"] = file_pen
	score += file_pen
	# Churn risk
	churn = max(0, int(added)) + max(0, int(deleted))
	churn_pen = min(0.5, 0.0008 * churn)
	components["churn"] = churn_pen
	score += churn_pen
	# Blocklist hits
	block_hits = 0
	for p in (files or []):
		if _glob_match(p, block_paths):
			block_hits += 1
	block_pen = min(0.4, 0.15 * block_hits)
	components["block"] = block_pen
	score += block_pen
	# Allowlist misses
	allow_miss = 0
	if allow_paths:
		for p in (files or []):
			if not _glob_match(p, allow_paths):
				allow_miss += 1
	allow_pen = min(0.3, 0.1 * allow_miss)
	components["allow_miss"] = allow_pen
	score += allow_pen
	return (max(0.0, min(1.0, score)), components)

def allowlist_hit(files: List[str], allow_paths: List[str] | None = None) -> bool:
	if not allow_paths:
		return True
	return all(_glob_match(p, allow_paths or []) for p in (files or []))

@dataclass
class PolicyThresholds:
	max_files_without_approval: int = 1
	risk_threshold: float = 0.5

def should_require_approval(files: List[str], risk: float, thresholds: PolicyThresholds | None = None) -> bool:
	thr = thresholds or PolicyThresholds()
	if len(files) > int(thr.max_files_without_approval):
		return True
	if float(risk) >= float(thr.risk_threshold):
		return True
	return False

# Repo-level deterministic risk (index + policy) — integrated here to avoid new modules
def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except Exception:
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}

def _normalize(value: float, ref: float) -> float:
    if ref <= 0:
        return 0.0
    return max(0.0, min(1.0, float(value) / float(ref)))

def compute_repo_risk(index_obj: Dict[str, Any], policies: Dict[str, Any], repo_root: Path) -> Dict[str, Any]:
    graph = (index_obj or {}).get("graph", {})
    calls: Dict[str, List[Dict[str, Any]]] = (graph.get("calls") or {}) if isinstance(graph, dict) else {}
    indeg: Dict[str, int] = {}
    for fp, call_list in calls.items():
        indeg[fp] = indeg.get(fp, 0) + int(len(call_list or []))
    values = sorted(indeg.values())
    p95 = values[int(0.95 * (len(values) - 1))] if values else 0
    cc = _normalize(sum(values[-10:]) if len(values) >= 10 else sum(values), max(1.0, float(p95) * 10.0))

    rw = policies.get("risk_weights") or {}
    w_call = float(rw.get("call_centrality", 0.4) or 0.4)
    w_api = float(rw.get("api_break", 0.3) or 0.3)
    w_test = float(rw.get("test_fragility", 0.2) or 0.2)
    w_pol = float(rw.get("policy_violations", 0.1) or 0.1)

    # Simple test fragility proxy
    tests_dir = repo_root / "tests"
    if not tests_dir.exists():
        test_frag = 1.0
    else:
        ntests = sum(1 for _ in tests_dir.rglob("test_*.py"))
        test_frag = 0.8 if ntests == 0 else (0.4 if ntests < 5 else 0.2)

    # Policy violations: layering/naming hints
    violations: List[str] = []
    layering = policies.get("layering") or []
    if isinstance(layering, list) and layering:
        for fp in ((graph.get("functions") or {}).keys() if isinstance(graph, dict) else []):
            try:
                head = (Path(fp).parts or [""])[0]
                if head and head not in set(layering):
                    violations.append(f"layering:{head}:{fp}")
            except Exception:
                continue
    naming_templates = policies.get("naming_templates") or {}
    if naming_templates.get("module"):
        for fp in ((graph.get("functions") or {}).keys() if isinstance(graph, dict) else []):
            if "lib/" not in fp.replace("\\", "/"):
                violations.append(f"naming:module:{fp}")
    viol_norm = _normalize(float(len(violations)), 20.0)

    components = {
        "call_centrality": round(cc, 4),
        "api_break": 0.0,
        "test_fragility": round(float(test_frag), 4),
        "policy_violations": round(viol_norm, 4),
    }
    score = (
        w_call * components["call_centrality"]
        + w_api * components["api_break"]
        + w_test * components["test_fragility"]
        + w_pol * components["policy_violations"]
    )
    bpr = ((policies.get("budgets") or {}).get("pr") or {})
    max_risk = float(bpr.get("max_risk", 2.0) or 2.0)
    caps = {
        "max_files": int(bpr.get("max_files", 60) or 60),
        "max_lines": int(bpr.get("max_lines", 1000) or 1000),
        "max_risk": max_risk,
    }
    return {
        "score": float(round(score, 4)),
        "components": components,
        "violations": violations,
        "caps": caps,
        "ok": bool(score <= max_risk),
    }


from __future__ import annotations
import logging

import hashlib
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple, Any

import json

APPROVAL_DECISIONS = {"approved", "denied", "modified", "deferred", "expired"}

def _sanitize_diff_for_hash(diff_text: str) -> str:
	"""Keep only stable, schema-relevant lines for signature hashing."""
	kept: List[str] = []
	for line in (diff_text or "").splitlines():
		if line.startswith("diff --git") or line.startswith("index ") or line.startswith("--- ") or line.startswith("+++ ") or line.startswith("@@ "):
			kept.append(line)
		elif line.startswith("+") or line.startswith("-") or line.startswith(" "):
			kept.append(line)
	return "\n".join(kept).strip()

def compute_signature(diff_text: str) -> str:
	"""Stable content hash to link preview/decision records."""
	stable = _sanitize_diff_for_hash(diff_text)
	return hashlib.sha256(stable.encode("utf-8")).hexdigest()

def _glob_match(path: str, patterns: List[str]) -> bool:
	try:
		import fnmatch as _fn
		for pat in (patterns or []):
			if _fn.fnmatch(path, pat):
				return True
	except Exception:
		return False
	return False

def estimate_risk(


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

import json

APPROVAL_DECISIONS = {"approved", "denied", "modified", "deferred", "expired"}

def _sanitize_diff_for_hash(diff_text: str) -> str:
	"""Keep only stable, schema-relevant lines for signature hashing."""
	kept: List[str] = []
	for line in (diff_text or "").splitlines():
		if line.startswith("diff --git") or line.startswith("index ") or line.startswith("--- ") or line.startswith("+++ ") or line.startswith("@@ "):
			kept.append(line)
		elif line.startswith("+") or line.startswith("-") or line.startswith(" "):
			kept.append(line)
	return "\n".join(kept).strip()

def compute_signature(diff_text: str) -> str:
	"""Stable content hash to link preview/decision records."""
	stable = _sanitize_diff_for_hash(diff_text)
	return hashlib.sha256(stable.encode("utf-8")).hexdigest()

def _glob_match(path: str, patterns: List[str]) -> bool:
	try:
		import fnmatch as _fn
		for pat in (patterns or []):
			if _fn.fnmatch(path, pat):
				return True
	except Exception:
		return False
	return False

def estimate_risk(
	diff_text: str,
	files: List[str],
	added: int,
	deleted: int,
	allow_paths: List[str] | None = None,
	block_paths: List[str] | None = None,


APPROVAL_DECISIONS = {"approved", "denied", "modified", "deferred", "expired"}

def _sanitize_diff_for_hash(diff_text: str) -> str:
	"""Keep only stable, schema-relevant lines for signature hashing."""
	kept: List[str] = []
	for line in (diff_text or "").splitlines():
		if line.startswith("diff --git") or line.startswith("index ") or line.startswith("--- ") or line.startswith("+++ ") or line.startswith("@@ "):
			kept.append(line)
		elif line.startswith("+") or line.startswith("-") or line.startswith(" "):
			kept.append(line)
	return "\n".join(kept).strip()

def compute_signature(diff_text: str) -> str:
	"""Stable content hash to link preview/decision records."""
	stable = _sanitize_diff_for_hash(diff_text)
	return hashlib.sha256(stable.encode("utf-8")).hexdigest()

def _glob_match(path: str, patterns: List[str]) -> bool:
	try:
		import fnmatch as _fn
		for pat in (patterns or []):
			if _fn.fnmatch(path, pat):
				return True
	except Exception:
		return False
	return False

def estimate_risk(
	diff_text: str,
	files: List[str],
	added: int,
	deleted: int,
	allow_paths: List[str] | None = None,
	block_paths: List[str] | None = None,
) -> Tuple[float, Dict[str, float]]:
	"""Heuristic risk in [0,1] with components for transparency."""
	allow_paths = allow_paths or []
	block_paths = block_paths or []
	score = 0.2
	components: Dict[str, float] = {}
	# File count risk
	file_pen = min(0.5, 0.1 * max(0, len(files) - 1))
	components["files"] = file_pen
	score += file_pen
	# Churn risk


	return "\n".join(kept).strip()

def compute_signature(diff_text: str) -> str:
	"""Stable content hash to link preview/decision records."""
	stable = _sanitize_diff_for_hash(diff_text)
	return hashlib.sha256(stable.encode("utf-8")).hexdigest()

def _glob_match(path: str, patterns: List[str]) -> bool:
	try:
		import fnmatch as _fn
		for pat in (patterns or []):
			if _fn.fnmatch(path, pat):
				return True
	except Exception:
		return False
	return False

def estimate_risk(
	diff_text: str,
	files: List[str],
	added: int,
	deleted: int,
	allow_paths: List[str] | None = None,
	block_paths: List[str] | None = None,
) -> Tuple[float, Dict[str, float]]:
	"""Heuristic risk in [0,1] with components for transparency."""
	allow_paths = allow_paths or []
	block_paths = block_paths or []
	score = 0.2
	components: Dict[str, float] = {}
	# File count risk
	file_pen = min(0.5, 0.1 * max(0, len(files) - 1))
	components["files"] = file_pen
	score += file_pen
	# Churn risk
	churn = max(0, int(added)) + max(0, int(deleted))
	churn_pen = min(0.5, 0.0008 * churn)
	components["churn"] = churn_pen
	score += churn_pen
	# Blocklist hits
	block_hits = 0
	for p in (files or []):
		if _glob_match(p, block_paths):
			block_hits += 1
	block_pen = min(0.4, 0.15 * block_hits)
	components["block"] = block_pen
	score += block_pen
	# Allowlist misses
	allow_miss = 0
	if allow_paths:
		for p in (files or []):
			if not _glob_match(p, allow_paths):
				allow_miss += 1
	allow_pen = min(0.3, 0.1 * allow_miss)
	components["allow_miss"] = allow_pen
	score += allow_pen
	return (max(0.0, min(1.0, score)), components)

def allowlist_hit(files: List[str], allow_paths: List[str] | None = None) -> bool:
	if not allow_paths:
		return True
	return all(_glob_match(p, allow_paths or []) for p in (files or []))

@dataclass
class PolicyThresholds:
	max_files_without_approval: int = 1
	risk_threshold: float = 0.5

def should_require_approval(files: List[str], risk: float, thresholds: PolicyThresholds | None = None) -> bool:
	thr = thresholds or PolicyThresholds()
	if len(files) > int(thr.max_files_without_approval):
		return True
	if float(risk) >= float(thr.risk_threshold):
		return True


	# Blocklist hits
	block_hits = 0
	for p in (files or []):
		if _glob_match(p, block_paths):
			block_hits += 1
	block_pen = min(0.4, 0.15 * block_hits)
	components["block"] = block_pen
	score += block_pen
	# Allowlist misses
	allow_miss = 0
	if allow_paths:
		for p in (files or []):
			if not _glob_match(p, allow_paths):
				allow_miss += 1
	allow_pen = min(0.3, 0.1 * allow_miss)
	components["allow_miss"] = allow_pen
	score += allow_pen
	return (max(0.0, min(1.0, score)), components)

def allowlist_hit(files: List[str], allow_paths: List[str] | None = None) -> bool:
	if not allow_paths:
		return True
	return all(_glob_match(p, allow_paths or []) for p in (files or []))

@dataclass
class PolicyThresholds:
	max_files_without_approval: int = 1
	risk_threshold: float = 0.5

def should_require_approval(files: List[str], risk: float, thresholds: PolicyThresholds | None = None) -> bool:
	thr = thresholds or PolicyThresholds()
	if len(files) > int(thr.max_files_without_approval):
		return True
	if float(risk) >= float(thr.risk_threshold):
		return True
	return False

# Repo-level deterministic risk (index + policy) — integrated here to avoid new modules
def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():


PolicyThresholds

	score += block_pen
	# Allowlist misses
	allow_miss = 0
	if allow_paths:
		for p in (files or []):
			if not _glob_match(p, allow_paths):
				allow_miss += 1
	allow_pen = min(0.3, 0.1 * allow_miss)
	components["allow_miss"] = allow_pen
	score += allow_pen
	return (max(0.0, min(1.0, score)), components)

def allowlist_hit(files: List[str], allow_paths: List[str] | None = None) -> bool:
	if not allow_paths:
		return True
	return all(_glob_match(p, allow_paths or []) for p in (files or []))

@dataclass
class PolicyThresholds:
	max_files_without_approval: int = 1
	risk_threshold: float = 0.5

def should_require_approval(files: List[str], risk: float, thresholds: PolicyThresholds | None = None) -> bool:
	thr = thresholds or PolicyThresholds()
	if len(files) > int(thr.max_files_without_approval):
		return True
	if float(risk) >= float(thr.risk_threshold):
		return True
	return False

# Repo-level deterministic risk (index + policy) — integrated here to avoid new modules
def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except Exception:


			if not _glob_match(p, allow_paths):
				allow_miss += 1
	allow_pen = min(0.3, 0.1 * allow_miss)
	components["allow_miss"] = allow_pen
	score += allow_pen
	return (max(0.0, min(1.0, score)), components)

def allowlist_hit(files: List[str], allow_paths: List[str] | None = None) -> bool:
	if not allow_paths:
		return True
	return all(_glob_match(p, allow_paths or []) for p in (files or []))

@dataclass
class PolicyThresholds:
	max_files_without_approval: int = 1
	risk_threshold: float = 0.5

def should_require_approval(files: List[str], risk: float, thresholds: PolicyThresholds | None = None) -> bool:
	thr = thresholds or PolicyThresholds()
	if len(files) > int(thr.max_files_without_approval):
		return True
	if float(risk) >= float(thr.risk_threshold):
		return True
	return False

# Repo-level deterministic risk (index + policy) — integrated here to avoid new modules
def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except Exception:
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}

def _normalize(value: float, ref: float) -> float:
    if ref <= 0:
        return 0.0


		return True
	return all(_glob_match(p, allow_paths or []) for p in (files or []))

@dataclass
class PolicyThresholds:
	max_files_without_approval: int = 1
	risk_threshold: float = 0.5

def should_require_approval(files: List[str], risk: float, thresholds: PolicyThresholds | None = None) -> bool:
	thr = thresholds or PolicyThresholds()
	if len(files) > int(thr.max_files_without_approval):
		return True
	if float(risk) >= float(thr.risk_threshold):
		return True
	return False

# Repo-level deterministic risk (index + policy) — integrated here to avoid new modules
def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except Exception:
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}

def _normalize(value: float, ref: float) -> float:
    if ref <= 0:
        return 0.0
    return max(0.0, min(1.0, float(value) / float(ref)))

def compute_repo_risk(index_obj: Dict[str, Any], policies: Dict[str, Any], repo_root: Path) -> Dict[str, Any]:
    graph = (index_obj or {}).get("graph", {})
    calls: Dict[str, List[Dict[str, Any]]] = (graph.get("calls") or {}) if isinstance(graph, dict) else {}
    indeg: Dict[str, int] = {}
    for fp, call_list in calls.items():
        indeg[fp] = indeg.get(fp, 0) + int(len(call_list or []))
    values = sorted(indeg.values())
    p95 = values[int(0.95 * (len(values) - 1))] if values else 0
    cc = _normalize(sum(values[-10:]) if len(values) >= 10 else sum(values), max(1.0, float(p95) * 10.0))

    rw = policies.get("risk_weights") or {}
    w_call = float(rw.get("call_centrality", 0.4) or 0.4)


	if float(risk) >= float(thr.risk_threshold):
		return True
	return False

# Repo-level deterministic risk (index + policy) — integrated here to avoid new modules
def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except Exception:
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}

def _normalize(value: float, ref: float) -> float:
    if ref <= 0:
        return 0.0
    return max(0.0, min(1.0, float(value) / float(ref)))

def compute_repo_risk(index_obj: Dict[str, Any], policies: Dict[str, Any], repo_root: Path) -> Dict[str, Any]:
    graph = (index_obj or {}).get("graph", {})
    calls: Dict[str, List[Dict[str, Any]]] = (graph.get("calls") or {}) if isinstance(graph, dict) else {}
    indeg: Dict[str, int] = {}
    for fp, call_list in calls.items():
        indeg[fp] = indeg.get(fp, 0) + int(len(call_list or []))
    values = sorted(indeg.values())
    p95 = values[int(0.95 * (len(values) - 1))] if values else 0
    cc = _normalize(sum(values[-10:]) if len(values) >= 10 else sum(values), max(1.0, float(p95) * 10.0))

    rw = policies.get("risk_weights") or {}
    w_call = float(rw.get("call_centrality", 0.4) or 0.4)
    w_api = float(rw.get("api_break", 0.3) or 0.3)
    w_test = float(rw.get("test_fragility", 0.2) or 0.2)
    w_pol = float(rw.get("policy_violations", 0.1) or 0.1)

    # Simple test fragility proxy
    tests_dir = repo_root / "tests"


def _safe_load_yaml(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        import yaml  # type: ignore
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        return data if isinstance(data, dict) else {}
    except Exception:
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}

def _normalize(value: float, ref: float) -> float:
    if ref <= 0:
        return 0.0
    return max(0.0, min(1.0, float(value) / float(ref)))

def compute_repo_risk(index_obj: Dict[str, Any], policies: Dict[str, Any], repo_root: Path) -> Dict[str, Any]:
    graph = (index_obj or {}).get("graph", {})
    calls: Dict[str, List[Dict[str, Any]]] = (graph.get("calls") or {}) if isinstance(graph, dict) else {}
    indeg: Dict[str, int] = {}
    for fp, call_list in calls.items():
        indeg[fp] = indeg.get(fp, 0) + int(len(call_list or []))
    values = sorted(indeg.values())
    p95 = values[int(0.95 * (len(values) - 1))] if values else 0
    cc = _normalize(sum(values[-10:]) if len(values) >= 10 else sum(values), max(1.0, float(p95) * 10.0))

    rw = policies.get("risk_weights") or {}
    w_call = float(rw.get("call_centrality", 0.4) or 0.4)
    w_api = float(rw.get("api_break", 0.3) or 0.3)
    w_test = float(rw.get("test_fragility", 0.2) or 0.2)
    w_pol = float(rw.get("policy_violations", 0.1) or 0.1)

    # Simple test fragility proxy
    tests_dir = repo_root / "tests"
    if not tests_dir.exists():
        test_frag = 1.0
    else:
        ntests = sum(1 for _ in tests_dir.rglob("test_*.py"))
        test_frag = 0.8 if ntests == 0 else (0.4 if ntests < 5 else 0.2)

    # Policy violations: layering/naming hints
    violations: List[str] = []
    layering = policies.get("layering") or []
    if isinstance(layering, list) and layering:
        for fp in ((graph.get("functions") or {}).keys() if isinstance(graph, dict) else []):
            try:
                head = (Path(fp).parts or [""])[0]
                if head and head not in set(layering):
                    violations.append(f"layering:{head}:{fp}")
            except Exception:
                continue
    naming_templates = policies.get("naming_templates") or {}
    if naming_templates.get("module"):
        for fp in ((graph.get("functions") or {}).keys() if isinstance(graph, dict) else []):
            if "lib/" not in fp.replace("\\", "/"):
                violations.append(f"naming:module:{fp}")
    viol_norm = _normalize(float(len(violations)), 20.0)

    components = {
        "call_centrality": round(cc, 4),
        "api_break": 0.0,
        "test_fragility": round(float(test_frag), 4),
        "policy_violations": round(viol_norm, 4),
    }
    score = (
        w_call * components["call_centrality"]
        + w_api * components["api_break"]
        + w_test * components["test_fragility"]
        + w_pol * components["policy_violations"]
    )
    bpr = ((policies.get("budgets") or {}).get("pr") or {})
    max_risk = float(bpr.get("max_risk", 2.0) or 2.0)
    caps = {
        "max_files": int(bpr.get("max_files", 60) or 60),
        "max_lines": int(bpr.get("max_lines", 1000) or 1000),
        "max_risk": max_risk,
    }
    return {
        "score": float(round(score, 4)),
        "components": components,
        "violations": violations,
        "caps": caps,
        "ok": bool(score <= max_risk),
    }


from __future__ import annotations

from typing import Any, Dict, Optional

from agi_dw.bench.web_dom.runner import fetch_text  # type: ignore

def dom_assert(url: str, selector: str, props: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
	"""Assert DOM properties for a selector on a URL.

	Currently supports matching substring of text via props={"text_contains": "..."}.
	Returns a report dict with fields: ok, url, selector, text, reason.
	"""
	res = fetch_text(url, selector)
	text = str(res.get("text", ""))
	ok = True
	reason = ""
	if props and "text_contains" in props:
		exp = str(props.get("text_contains", ""))
		if exp and exp not in text:
			ok = False
			reason = "text_missing"
	return {"ok": bool(ok), "url": url, "selector": selector, "text": text, "reason": reason}


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

class MetaController:
    """
    Monitors subsystem metrics and emits meta decisions (planning mode, temps, step budgets).
    """

    def __init__(self, root: Path) -> None:
        self.root = root
        self.state_path = root / "data" / "sandbox" / "tmp" / "meta_state.json"
        self.state_path.parent.mkdir(parents=True, exist_ok=True)

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

    def read_metrics(self) -> Dict[str, Any]:
        wm = self._read_json(self.root / "models" / "wm_mlp" / "metrics.json")
        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }

    def write_state(self, state: Dict[str, Any]) -> None:
        self.state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    def tick(self) -> Dict[str, Any]:
        m = self.read_metrics()
        s = self.decide(m)
        self.write_state(s)
        return s

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    ctrl = MetaController(root)
    s = ctrl.tick()
    print(json.dumps(s))
    return 0

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


main

        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }

    def write_state(self, state: Dict[str, Any]) -> None:
        self.state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    def tick(self) -> Dict[str, Any]:
        m = self.read_metrics()
        s = self.decide(m)
        self.write_state(s)
        return s

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    ctrl = MetaController(root)
    s = ctrl.tick()
    print(json.dumps(s))
    return 0

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


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

class MetaController:
    """
    Monitors subsystem metrics and emits meta decisions (planning mode, temps, step budgets).
    """

    def __init__(self, root: Path) -> None:
        self.root = root
        self.state_path = root / "data" / "sandbox" / "tmp" / "meta_state.json"
        self.state_path.parent.mkdir(parents=True, exist_ok=True)

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

    def read_metrics(self) -> Dict[str, Any]:
        wm = self._read_json(self.root / "models" / "wm_mlp" / "metrics.json")
        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {


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

class MetaController:
    """
    Monitors subsystem metrics and emits meta decisions (planning mode, temps, step budgets).
    """

    def __init__(self, root: Path) -> None:
        self.root = root
        self.state_path = root / "data" / "sandbox" / "tmp" / "meta_state.json"
        self.state_path.parent.mkdir(parents=True, exist_ok=True)

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

    def read_metrics(self) -> Dict[str, Any]:
        wm = self._read_json(self.root / "models" / "wm_mlp" / "metrics.json")
        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }


from pathlib import Path
from typing import Dict, Any

class MetaController:
    """
    Monitors subsystem metrics and emits meta decisions (planning mode, temps, step budgets).
    """

    def __init__(self, root: Path) -> None:
        self.root = root
        self.state_path = root / "data" / "sandbox" / "tmp" / "meta_state.json"
        self.state_path.parent.mkdir(parents=True, exist_ok=True)

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

    def read_metrics(self) -> Dict[str, Any]:
        wm = self._read_json(self.root / "models" / "wm_mlp" / "metrics.json")
        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }

    def write_state(self, state: Dict[str, Any]) -> None:
        self.state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    def tick(self) -> Dict[str, Any]:
        m = self.read_metrics()


decide

    """
    Monitors subsystem metrics and emits meta decisions (planning mode, temps, step budgets).
    """

    def __init__(self, root: Path) -> None:
        self.root = root
        self.state_path = root / "data" / "sandbox" / "tmp" / "meta_state.json"
        self.state_path.parent.mkdir(parents=True, exist_ok=True)

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

    def read_metrics(self) -> Dict[str, Any]:
        wm = self._read_json(self.root / "models" / "wm_mlp" / "metrics.json")
        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }

    def write_state(self, state: Dict[str, Any]) -> None:
        self.state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    def tick(self) -> Dict[str, Any]:
        m = self.read_metrics()
        s = self.decide(m)
        self.write_state(s)
        return s

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    ctrl = MetaController(root)
    s = ctrl.tick()
    print(json.dumps(s))
    return 0

if __name__ == "__main__":


    def read_metrics(self) -> Dict[str, Any]:
        wm = self._read_json(self.root / "models" / "wm_mlp" / "metrics.json")
        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }

    def write_state(self, state: Dict[str, Any]) -> None:
        self.state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    def tick(self) -> Dict[str, Any]:
        m = self.read_metrics()
        s = self.decide(m)
        self.write_state(s)
        return s

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    ctrl = MetaController(root)
    s = ctrl.tick()
    print(json.dumps(s))
    return 0

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


tick

        vf = self._read_json(self.root / "models" / "verifier_calib" / "metrics.json")
        return {"wm": wm, "verifier": vf}

    def decide(self, metrics: Dict[str, Any]) -> Dict[str, Any]:
        wm_ece = float(metrics.get("wm", {}).get("wm_ece", 0.5) or 0.5)
        cal_ece = float(metrics.get("verifier", {}).get("cal_ece", 0.5) or metrics.get("verifier", {}).get("base_ece", 0.5) or 0.5)
        # Heuristics
        enable_tot = (cal_ece > 0.2) or (wm_ece > 0.25)
        temp = 0.7 if enable_tot else 0.2
        step_budget = 3 if enable_tot else 1
        return {
            "enable_tot": bool(enable_tot),
            "sampling_temperature": float(temp),
            "planner_step_budget": int(step_budget),
            "inputs": {"wm_ece": wm_ece, "verifier_ece": cal_ece},
        }

    def write_state(self, state: Dict[str, Any]) -> None:
        self.state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")

    def tick(self) -> Dict[str, Any]:
        m = self.read_metrics()
        s = self.decide(m)
        self.write_state(s)
        return s

def main() -> int:
    root = Path(__file__).resolve().parents[2]
    ctrl = MetaController(root)
    s = ctrl.tick()
    print(json.dumps(s))
    return 0

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


from __future__ import annotations
import logging

from typing import Literal

SuiteName = Literal["humaneval", "mbpp"]

def build_prompt(suite: SuiteName, base: str) -> str:
    """Return a suite-specific instruction-prefixed prompt.

    Ensures the model outputs only Python code without fences or chatter.
    """
    common_hint = (
        "Complete the Python function from the given prompt by writing ONLY the function body. "
        "Rules: (1) Output only Python code, no fences or prose. (2) Do not add tests, asserts, prints, or a main. "
        "(3) Do not add imports or extra functions unless strictly necessary. (4) Do not rewrite the def line. "
        "(5) The first non-empty line of your output must be indented under the provided def.\n\n"
    )
    if suite == "humaneval":
        return common_hint + base
    if suite == "mbpp":
        return common_hint + base
    # Default
    return common_hint + base


import logging
import uuid
import random
from pathlib import Path
from typing import Dict, List, Tuple

from bench.common.safe_shell import SafeShellRunner
from bench.common.trace import build_trace, write_jsonl

def setup_count_lines(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	(sandbox / "docs" / "a.txt").write_text("one\nTwo\nthree\n", encoding="utf-8")
	(sandbox / "docs" / "b.txt").write_text("alpha\nBeta\nGamma\n", encoding="utf-8")

def setup_grep_word(sandbox: Path) -> None:
	(sandbox / "logs").mkdir(exist_ok=True)
	(sandbox / "logs" / "app.log").write_text(
		"INFO start\nWARN cpu high\nERROR disk full\nINFO done\n",
		encoding="utf-8",
	)

def setup_dups_file(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	# Create adjacent duplicates so uniq works without pipes
	(sandbox / "docs" / "dups.txt").write_text(
		"alpha\nalpha\nBeta\nBeta\nERROR\nERROR\nINFO\ninfo\n",
		encoding="utf-8",
	)

def run_task_pwd(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["pwd"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print working directory", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_mkdir_move_cp_rm(runner: SafeShellRunner, traces_out: Path) -> None:
	# mkdir newdir
	cmd = ["mkdir", "newdir"]
	res = runner.run(cmd)
	_write_trace(traces_out, "make directory newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# cp docs/a.txt newdir/a.txt
	cmd = ["cp", "docs/a.txt", "newdir/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "copy docs/a.txt to newdir/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	# mv newdir/a.txt newdir/renamed.txt
	cmd = ["mv", "newdir/a.txt", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "rename file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# rm newdir/renamed.txt
	cmd = ["rm", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "remove file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_touch_date(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["touch", "docs/timestamp.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "create empty file timestamp.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	cmd = ["date"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print system date", runner, cmd, res.stdout, res.stderr, res.returncode)

def _write_trace(traces_out: Path, task_desc: str, runner: SafeShellRunner, cmd: List[str], result_stdout: str, result_stderr: str, returncode: int) -> None:
	task_id = str(uuid.uuid4())
	obs = {"kind": "cli", "content": task_desc, "meta": {"cwd": str(runner.sandbox_path)}}
	plan = {"subgoals": [task_desc], "tools": ["cli"], "constraints": {}}
	action = {"tool": "cli.run", "args": {"argv": cmd, "cwd": str(runner.sandbox_path)}}
	status = "ok" if returncode == 0 or (cmd and cmd[0] == "grep" and returncode in (0, 1)) else "error"
	reward = {"scalar": 1.0 if status == "ok" else 0.0, "components": {"success": 1 if status == "ok" else 0, "latency": 0, "side_effect": 1}}
	critique = {"issues": [], "risk": 0.1 if status == "ok" else 0.5, "proposal": ""}
	trace = build_trace(
		task_id,
		obs,
		plan,
		action,
		{"stdout": result_stdout, "stderr": result_stderr, "status": status},
		reward,
		critique,
	)
	write_jsonl(str(traces_out), trace)

def run_task_count_lines(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_word(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep ERROR in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_count_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-c", "INFO", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count INFO lines in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_i_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-i", "info", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep -i info in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_head_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["head", "-n", "2", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "head 2 lines of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_tail_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["tail", "-n", "1", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "tail last line of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_tail_plus_n(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["tail", "+2", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "tail from line 2 of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_sort_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["sort", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "sort logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_sort_r_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["sort", "-r", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "sort -r logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_cut_first_field(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["cut", "-d", " ", "-f", "1", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "cut first field (space-delimited) from logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_head_bytes(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["head", "-c", "10", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "head first 10 bytes of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_uniq_counts(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["uniq", "-c", "docs/dups.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "uniq -c on docs/dups.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_v_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-v", "INFO", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep -v INFO lines in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_pipeline_grep_wc(runner: SafeShellRunner, traces_out: Path) -> None:
	"""Simulate: grep ERROR logs/app.log | wc -l without pipes via temp file."""
	tmp_dir = runner.sandbox_path / "tmp"
	tmp_dir.mkdir(parents=True, exist_ok=True)
	grep_cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	grep_res = runner.run(grep_cmd)
	_write_trace(traces_out, "pipeline step: grep -n ERROR logs/app.log", runner, grep_cmd, grep_res.stdout, grep_res.stderr, grep_res.returncode)
	(tmp_dir / "errors.txt").write_text(grep_res.stdout, encoding="utf-8")
	wc_cmd = ["wc", "-l", "tmp/errors.txt"]
	wc_res = runner.run(wc_cmd)
	_write_trace(traces_out, "pipeline step: wc -l tmp/errors.txt", runner, wc_cmd, wc_res.stdout, wc_res.stderr, wc_res.returncode)

def run_task_pipeline_cut_uniq(runner: SafeShellRunner, traces_out: Path) -> None:
	"""Simulate: cut first field | uniq -c without pipes via temp file."""
	tmp_dir = runner.sandbox_path / "tmp"
	tmp_dir.mkdir(parents=True, exist_ok=True)
	cut_cmd = ["cut", "-d", " ", "-f", "1", "logs/app.log"]
	cut_res = runner.run(cut_cmd)
	_write_trace(traces_out, "pipeline step: cut first field logs/app.log", runner, cut_cmd, cut_res.stdout, cut_res.stderr, cut_res.returncode)
	(tmp_dir / "fields.txt").write_text(cut_res.stdout, encoding="utf-8")
	uniq_cmd = ["uniq", "-c", "tmp/fields.txt"]
	uniq_res = runner.run(uniq_cmd)
	_write_trace(traces_out, "pipeline step: uniq -c tmp/fields.txt", runner, uniq_cmd, uniq_res.stdout, uniq_res.stderr, uniq_res.returncode)

def run_task_pipeline_sort_uniq_counts(runner: SafeShellRunner, traces_out: Path) -> None:
	"""Simulate: sort logs/app.log | uniq -c | sort -nr via temp files."""
	tmp_dir = runner.sandbox_path / "tmp"
	tmp_dir.mkdir(parents=True, exist_ok=True)
	sort_cmd = ["sort", "logs/app.log"]
	sort_res = runner.run(sort_cmd)
	_write_trace(traces_out, "pipeline step: sort logs/app.log", runner, sort_cmd, sort_res.stdout, sort_res.stderr, sort_res.returncode)
	(tmp_dir / "sorted.txt").write_text(sort_res.stdout, encoding="utf-8")
	uniq_cmd = ["uniq", "-c", "tmp/sorted.txt"]
	uniq_res = runner.run(uniq_cmd)
	_write_trace(traces_out, "pipeline step: uniq -c tmp/sorted.txt", runner, uniq_cmd, uniq_res.stdout, uniq_res.stderr, uniq_res.returncode)
	(tmp_dir / "counts.txt").write_text(uniq_res.stdout, encoding="utf-8")
	sortnr_cmd = ["sort", "-nr", "tmp/counts.txt"]
	sortnr_res = runner.run(sortnr_cmd)
	_write_trace(traces_out, "pipeline step: sort -nr tmp/counts.txt", runner, sortnr_cmd, sortnr_res.stdout, sortnr_res.stderr, sortnr_res.returncode)

# Negative/failure cases to produce router negatives
def run_task_wc_missing(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/missing.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in missing file (should error)", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_missing_file(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "ERROR", "logs/does_not_exist.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep on missing file (should error)", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_head_invalid_flag(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["head", "-z", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "head with invalid flag (should error)", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_rm_missing(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["rm", "does_not_exist.tmp"]
	res = runner.run(cmd)
	_write_trace(traces_out, "rm missing file (should error)", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_cp_missing_source(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["cp", "docs/nope.txt", "docs/copy_nope.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "cp missing source (should error)", runner, cmd, res.stdout, res.stderr, res.returncode)

def _generate_varied_content(task_type: str, episode: int) -> str:
	"""Generate varied task content to avoid duplicates"""
	variations = {
		"count lines": [
			"count lines in docs/a.txt",
			"count lines in docs/b.txt",
			"count lines in logs/app.log",
			f"count lines in file_{episode}.txt"
		],
		"grep word": [
			"grep ERROR in logs/app.log",
			"grep INFO in logs/app.log",
			"grep WARN in logs/app.log",
			f"grep pattern_{episode} in logs/app.log"
		],
		"print working directory": [
			"print working directory",
			"show current directory",
			"display current path",
			f"show directory for task_{episode}"
		]
	}

	if task_type in variations:
		return random.choice(variations[task_type])
	return f"{task_type} (episode {episode})"

def generate_seed_traces(sandbox_dir: str, out_jsonl: str, episodes: int = 2) -> Tuple[int, Path]:
	sandbox = Path(sandbox_dir)
	traces_path = Path(out_jsonl)
	runner = SafeShellRunner(sandbox_dir)
	total = 0
	generated_tasks = set()  # Track generated task-content pairs to avoid duplicates

	# Define all available tasks with their functions
	all_tasks = [
		("count_lines", run_task_count_lines),
		("wc_words", run_task_wc_words),
		("wc_chars", run_task_wc_chars),
		("grep_word", run_task_grep_word),
		("grep_count_info", run_task_grep_count_info),
		("grep_i_info", run_task_grep_i_info),
		("grep_v_info", run_task_grep_v_info),
		("head_logs", run_task_head_logs),
		("tail_logs", run_task_tail_logs),
		("tail_plus_n", run_task_tail_plus_n),
		("head_bytes", run_task_head_bytes),
		("sort_logs", run_task_sort_logs),
		("sort_r_logs", run_task_sort_r_logs),
		("cut_first_field", run_task_cut_first_field),
		("uniq_counts", run_task_uniq_counts),
		("pwd", run_task_pwd),
		("mkdir_move_cp_rm", run_task_mkdir_move_cp_rm),
		("touch_date", run_task_touch_date),
		("pipeline_grep_wc", run_task_pipeline_grep_wc),
		("pipeline_cut_uniq", run_task_pipeline_cut_uniq),
		("pipeline_sort_uniq_counts", run_task_pipeline_sort_uniq_counts),
		("wc_missing", run_task_wc_missing),
		("grep_missing_file", run_task_grep_missing_file),
		("head_invalid_flag", run_task_head_invalid_flag),
		("rm_missing", run_task_rm_missing),
		("cp_missing_source", run_task_cp_missing_source),
	]

	for episode in range(episodes):
		setup_count_lines(sandbox)
		setup_grep_word(sandbox)
		setup_dups_file(sandbox)

		# Randomly select and shuffle tasks to avoid predictable patterns
		random.shuffle(all_tasks)

		for task_name, task_func in all_tasks:
			# Create unique task identifier
			task_key = f"{task_name}_ep{episode}"

			# Skip if we've already generated this exact task
			if task_key in generated_tasks:
				continue

			generated_tasks.add(task_key)

			# Run the task
			if task_name in ["pipeline_grep_wc", "pipeline_cut_uniq"]:
				task_func(runner, traces_path)
				total += 2
			elif task_name == "pipeline_sort_uniq_counts":
				task_func(runner, traces_path)
				total += 3
			elif task_name == "mkdir_move_cp_rm":
				task_func(runner, traces_path)
				total += 1  # This function generates multiple traces internally
			else:
				task_func(runner, traces_path)
				total += 1

		# Add some random variation by occasionally skipping tasks
		if random.random() < 0.3:  # 30% chance to skip some tasks
			skip_count = random.randint(1, 3)
			# Tasks are already shuffled, so this naturally creates variation

	return total, traces_path


import logging
import uuid
import random
from pathlib import Path
from typing import Dict, List, Tuple

from bench.common.safe_shell import SafeShellRunner
from bench.common.trace import build_trace, write_jsonl

def setup_count_lines(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	(sandbox / "docs" / "a.txt").write_text("one\nTwo\nthree\n", encoding="utf-8")
	(sandbox / "docs" / "b.txt").write_text("alpha\nBeta\nGamma\n", encoding="utf-8")

def setup_grep_word(sandbox: Path) -> None:
	(sandbox / "logs").mkdir(exist_ok=True)
	(sandbox / "logs" / "app.log").write_text(
		"INFO start\nWARN cpu high\nERROR disk full\nINFO done\n",
		encoding="utf-8",
	)

def setup_dups_file(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	# Create adjacent duplicates so uniq works without pipes
	(sandbox / "docs" / "dups.txt").write_text(
		"alpha\nalpha\nBeta\nBeta\nERROR\nERROR\nINFO\ninfo\n",
		encoding="utf-8",
	)

def run_task_pwd(runner: SafeShellRunner, traces_out: Path) -> None:


import logging
import uuid
import random
from pathlib import Path
from typing import Dict, List, Tuple

from bench.common.safe_shell import SafeShellRunner
from bench.common.trace import build_trace, write_jsonl

def setup_count_lines(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	(sandbox / "docs" / "a.txt").write_text("one\nTwo\nthree\n", encoding="utf-8")
	(sandbox / "docs" / "b.txt").write_text("alpha\nBeta\nGamma\n", encoding="utf-8")

def setup_grep_word(sandbox: Path) -> None:
	(sandbox / "logs").mkdir(exist_ok=True)
	(sandbox / "logs" / "app.log").write_text(
		"INFO start\nWARN cpu high\nERROR disk full\nINFO done\n",
		encoding="utf-8",
	)

def setup_dups_file(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	# Create adjacent duplicates so uniq works without pipes
	(sandbox / "docs" / "dups.txt").write_text(
		"alpha\nalpha\nBeta\nBeta\nERROR\nERROR\nINFO\ninfo\n",
		encoding="utf-8",
	)

def run_task_pwd(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["pwd"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print working directory", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_mkdir_move_cp_rm(runner: SafeShellRunner, traces_out: Path) -> None:
	# mkdir newdir
	cmd = ["mkdir", "newdir"]


from typing import Dict, List, Tuple

from bench.common.safe_shell import SafeShellRunner
from bench.common.trace import build_trace, write_jsonl

def setup_count_lines(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	(sandbox / "docs" / "a.txt").write_text("one\nTwo\nthree\n", encoding="utf-8")
	(sandbox / "docs" / "b.txt").write_text("alpha\nBeta\nGamma\n", encoding="utf-8")

def setup_grep_word(sandbox: Path) -> None:
	(sandbox / "logs").mkdir(exist_ok=True)
	(sandbox / "logs" / "app.log").write_text(
		"INFO start\nWARN cpu high\nERROR disk full\nINFO done\n",
		encoding="utf-8",
	)

def setup_dups_file(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	# Create adjacent duplicates so uniq works without pipes
	(sandbox / "docs" / "dups.txt").write_text(
		"alpha\nalpha\nBeta\nBeta\nERROR\nERROR\nINFO\ninfo\n",
		encoding="utf-8",
	)

def run_task_pwd(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["pwd"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print working directory", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_mkdir_move_cp_rm(runner: SafeShellRunner, traces_out: Path) -> None:
	# mkdir newdir
	cmd = ["mkdir", "newdir"]
	res = runner.run(cmd)
	_write_trace(traces_out, "make directory newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# cp docs/a.txt newdir/a.txt
	cmd = ["cp", "docs/a.txt", "newdir/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "copy docs/a.txt to newdir/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	# mv newdir/a.txt newdir/renamed.txt
	cmd = ["mv", "newdir/a.txt", "newdir/renamed.txt"]
	res = runner.run(cmd)


	(sandbox / "docs" / "b.txt").write_text("alpha\nBeta\nGamma\n", encoding="utf-8")

def setup_grep_word(sandbox: Path) -> None:
	(sandbox / "logs").mkdir(exist_ok=True)
	(sandbox / "logs" / "app.log").write_text(
		"INFO start\nWARN cpu high\nERROR disk full\nINFO done\n",
		encoding="utf-8",
	)

def setup_dups_file(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	# Create adjacent duplicates so uniq works without pipes
	(sandbox / "docs" / "dups.txt").write_text(
		"alpha\nalpha\nBeta\nBeta\nERROR\nERROR\nINFO\ninfo\n",
		encoding="utf-8",
	)

def run_task_pwd(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["pwd"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print working directory", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_mkdir_move_cp_rm(runner: SafeShellRunner, traces_out: Path) -> None:
	# mkdir newdir
	cmd = ["mkdir", "newdir"]
	res = runner.run(cmd)
	_write_trace(traces_out, "make directory newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# cp docs/a.txt newdir/a.txt
	cmd = ["cp", "docs/a.txt", "newdir/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "copy docs/a.txt to newdir/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	# mv newdir/a.txt newdir/renamed.txt
	cmd = ["mv", "newdir/a.txt", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "rename file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# rm newdir/renamed.txt
	cmd = ["rm", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "remove file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)


		"INFO start\nWARN cpu high\nERROR disk full\nINFO done\n",
		encoding="utf-8",
	)

def setup_dups_file(sandbox: Path) -> None:
	(sandbox / "docs").mkdir(exist_ok=True)
	# Create adjacent duplicates so uniq works without pipes
	(sandbox / "docs" / "dups.txt").write_text(
		"alpha\nalpha\nBeta\nBeta\nERROR\nERROR\nINFO\ninfo\n",
		encoding="utf-8",
	)

def run_task_pwd(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["pwd"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print working directory", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_mkdir_move_cp_rm(runner: SafeShellRunner, traces_out: Path) -> None:
	# mkdir newdir
	cmd = ["mkdir", "newdir"]
	res = runner.run(cmd)
	_write_trace(traces_out, "make directory newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# cp docs/a.txt newdir/a.txt
	cmd = ["cp", "docs/a.txt", "newdir/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "copy docs/a.txt to newdir/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	# mv newdir/a.txt newdir/renamed.txt
	cmd = ["mv", "newdir/a.txt", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "rename file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# rm newdir/renamed.txt
	cmd = ["rm", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "remove file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_touch_date(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["touch", "docs/timestamp.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "create empty file timestamp.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	cmd = ["date"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print system date", runner, cmd, res.stdout, res.stderr, res.returncode)

def _write_trace(traces_out: Path, task_desc: str, runner: SafeShellRunner, cmd: List[str], result_stdout: str, result_stderr: str, returncode: int) -> None:
	task_id = str(uuid.uuid4())
	obs = {"kind": "cli", "content": task_desc, "meta": {"cwd": str(runner.sandbox_path)}}
	plan = {"subgoals": [task_desc], "tools": ["cli"], "constraints": {}}
	action = {"tool": "cli.run", "args": {"argv": cmd, "cwd": str(runner.sandbox_path)}}
	status = "ok" if returncode == 0 or (cmd and cmd[0] == "grep" and returncode in (0, 1)) else "error"
	reward = {"scalar": 1.0 if status == "ok" else 0.0, "components": {"success": 1 if status == "ok" else 0, "latency": 0, "side_effect": 1}}
	critique = {"issues": [], "risk": 0.1 if status == "ok" else 0.5, "proposal": ""}
	trace = build_trace(


def run_task_mkdir_move_cp_rm(runner: SafeShellRunner, traces_out: Path) -> None:
	# mkdir newdir
	cmd = ["mkdir", "newdir"]
	res = runner.run(cmd)
	_write_trace(traces_out, "make directory newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# cp docs/a.txt newdir/a.txt
	cmd = ["cp", "docs/a.txt", "newdir/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "copy docs/a.txt to newdir/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	# mv newdir/a.txt newdir/renamed.txt
	cmd = ["mv", "newdir/a.txt", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "rename file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# rm newdir/renamed.txt
	cmd = ["rm", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "remove file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_touch_date(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["touch", "docs/timestamp.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "create empty file timestamp.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	cmd = ["date"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print system date", runner, cmd, res.stdout, res.stderr, res.returncode)

def _write_trace(traces_out: Path, task_desc: str, runner: SafeShellRunner, cmd: List[str], result_stdout: str, result_stderr: str, returncode: int) -> None:
	task_id = str(uuid.uuid4())
	obs = {"kind": "cli", "content": task_desc, "meta": {"cwd": str(runner.sandbox_path)}}
	plan = {"subgoals": [task_desc], "tools": ["cli"], "constraints": {}}
	action = {"tool": "cli.run", "args": {"argv": cmd, "cwd": str(runner.sandbox_path)}}
	status = "ok" if returncode == 0 or (cmd and cmd[0] == "grep" and returncode in (0, 1)) else "error"
	reward = {"scalar": 1.0 if status == "ok" else 0.0, "components": {"success": 1 if status == "ok" else 0, "latency": 0, "side_effect": 1}}
	critique = {"issues": [], "risk": 0.1 if status == "ok" else 0.5, "proposal": ""}
	trace = build_trace(
		task_id,
		obs,
		plan,
		action,
		{"stdout": result_stdout, "stderr": result_stderr, "status": status},
		reward,
		critique,
	)
	write_jsonl(str(traces_out), trace)


	_write_trace(traces_out, "copy docs/a.txt to newdir/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	# mv newdir/a.txt newdir/renamed.txt
	cmd = ["mv", "newdir/a.txt", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "rename file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)
	# rm newdir/renamed.txt
	cmd = ["rm", "newdir/renamed.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "remove file in newdir", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_touch_date(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["touch", "docs/timestamp.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "create empty file timestamp.txt", runner, cmd, res.stdout, res.stderr, res.returncode)
	cmd = ["date"]
	res = runner.run(cmd)
	_write_trace(traces_out, "print system date", runner, cmd, res.stdout, res.stderr, res.returncode)

def _write_trace(traces_out: Path, task_desc: str, runner: SafeShellRunner, cmd: List[str], result_stdout: str, result_stderr: str, returncode: int) -> None:
	task_id = str(uuid.uuid4())
	obs = {"kind": "cli", "content": task_desc, "meta": {"cwd": str(runner.sandbox_path)}}
	plan = {"subgoals": [task_desc], "tools": ["cli"], "constraints": {}}
	action = {"tool": "cli.run", "args": {"argv": cmd, "cwd": str(runner.sandbox_path)}}
	status = "ok" if returncode == 0 or (cmd and cmd[0] == "grep" and returncode in (0, 1)) else "error"
	reward = {"scalar": 1.0 if status == "ok" else 0.0, "components": {"success": 1 if status == "ok" else 0, "latency": 0, "side_effect": 1}}
	critique = {"issues": [], "risk": 0.1 if status == "ok" else 0.5, "proposal": ""}
	trace = build_trace(
		task_id,
		obs,
		plan,
		action,
		{"stdout": result_stdout, "stderr": result_stderr, "status": status},
		reward,
		critique,
	)
	write_jsonl(str(traces_out), trace)

def run_task_count_lines(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)


def _write_trace(traces_out: Path, task_desc: str, runner: SafeShellRunner, cmd: List[str], result_stdout: str, result_stderr: str, returncode: int) -> None:
	task_id = str(uuid.uuid4())
	obs = {"kind": "cli", "content": task_desc, "meta": {"cwd": str(runner.sandbox_path)}}
	plan = {"subgoals": [task_desc], "tools": ["cli"], "constraints": {}}
	action = {"tool": "cli.run", "args": {"argv": cmd, "cwd": str(runner.sandbox_path)}}
	status = "ok" if returncode == 0 or (cmd and cmd[0] == "grep" and returncode in (0, 1)) else "error"
	reward = {"scalar": 1.0 if status == "ok" else 0.0, "components": {"success": 1 if status == "ok" else 0, "latency": 0, "side_effect": 1}}
	critique = {"issues": [], "risk": 0.1 if status == "ok" else 0.5, "proposal": ""}
	trace = build_trace(
		task_id,
		obs,
		plan,
		action,
		{"stdout": result_stdout, "stderr": result_stderr, "status": status},
		reward,
		critique,
	)
	write_jsonl(str(traces_out), trace)

def run_task_count_lines(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_word(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep ERROR in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)


	reward = {"scalar": 1.0 if status == "ok" else 0.0, "components": {"success": 1 if status == "ok" else 0, "latency": 0, "side_effect": 1}}
	critique = {"issues": [], "risk": 0.1 if status == "ok" else 0.5, "proposal": ""}
	trace = build_trace(
		task_id,
		obs,
		plan,
		action,
		{"stdout": result_stdout, "stderr": result_stderr, "status": status},
		reward,
		critique,
	)
	write_jsonl(str(traces_out), trace)

def run_task_count_lines(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_word(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep ERROR in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_count_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-c", "INFO", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count INFO lines in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)


		action,
		{"stdout": result_stdout, "stderr": result_stderr, "status": status},
		reward,
		critique,
	)
	write_jsonl(str(traces_out), trace)

def run_task_count_lines(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_word(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep ERROR in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_count_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-c", "INFO", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count INFO lines in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_i_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-i", "info", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep -i info in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)


def run_task_count_lines(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-l", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count lines in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_word(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep ERROR in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_count_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-c", "INFO", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count INFO lines in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_i_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-i", "info", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep -i info in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_head_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["head", "-n", "2", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "head 2 lines of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)


def run_task_wc_words(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-w", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count words in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_wc_chars(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["wc", "-m", "docs/a.txt"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count chars in docs/a.txt", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_word(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-n", "ERROR", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep ERROR in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_count_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-c", "INFO", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "count INFO lines in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_grep_i_info(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["grep", "-i", "info", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "grep -i info in logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_head_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["head", "-n", "2", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "head 2 lines of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)

def run_task_tail_logs(runner: SafeShellRunner, traces_out: Path) -> None:
	cmd = ["tail", "-n", "1", "logs/app.log"]
	res = runner.run(cmd)
	_write_trace(traces_out, "tail last line of logs/app.log", runner, cmd, res.stdout, res.stderr, res.returncode)