from __future__ import annotations
import logging

import json
import shutil
from pathlib import Path
from typing import Dict, Any, List
import os

def detect_languages(root: Path) -> List[str]:
	langs: List[str] = []
	# Python
	if any(root.rglob("*.py")):
		langs.append("python")
	# JavaScript (package.json or .js files)
	if any(root.rglob("package.json")) or any(root.rglob("*.js")):
		langs.append("javascript")
	# TypeScript (tsconfig.json or any .ts excluding .d.ts)
	if (root / "tsconfig.json").exists() or any(p.suffix == ".ts" and not p.name.endswith(".d.ts") for p in root.rglob("*.ts")):
		langs.append("typescript")
	# Go
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		langs.append("go")
	# Rust
	if (root / "Cargo.toml").exists() or any(root.rglob("*.rs")):
		langs.append("rust")
	# Java
	if (root / "pom.xml").exists() or any(root.rglob("build.gradle")) or any(root.rglob("*.java")):
		langs.append("java")
	return langs

def detect_build_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python build/install
	if (root / "setup.cfg").exists() or (root / "pyproject.toml").exists():
		cmds.append("pip install -e .")
	# Poetry install if pyproject uses poetry
	try:
		pp = root / "pyproject.toml"
		if pp.exists():
			text = pp.read_text(encoding="utf-8")
			if "tool.poetry" in text:
				cmds.append("poetry install")
	except Exception:
		pass
	# Node install
	if (root / "package.json").exists():
		cmds.append("npm install")
		# Include common alternates if lockfiles found
		if (root / "yarn.lock").exists():
			cmds.append("yarn install")
		if (root / "pnpm-lock.yaml").exists():
			cmds.append("pnpm install")
	# Go build
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go build ./...")
	# Rust build
	if (root / "Cargo.toml").exists():
		cmds.append("cargo build -q")
	# Java build (prefer wrapper if present)
	if (root / "pom.xml").exists():
		cmds.append("mvn -q -DskipTests install")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew build -x test")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle build -x test")
	return cmds

def detect_test_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python tests
	if shutil.which("pytest") or (root / "tests").exists():
		cmds.append("pytest -q")
	if (root / "tox.ini").exists() and shutil.which("tox"):
		cmds.append("tox -q")
	# Node tests
	if (root / "package.json").exists():
		cmds.append("npm test --silent")
		if shutil.which("npx"):
			cmds.append("npx jest --runInBand")
	# Go tests
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go test ./...")
	# Rust tests
	if (root / "Cargo.toml").exists():
		cmds.append("cargo test -q")
	# Java tests
	if (root / "pom.xml").exists():
		cmds.append("mvn -q test")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew test")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle test")
	return sorted(list(dict.fromkeys(cmds)))

def detect_lint_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python linters/formatters
	if shutil.which("ruff"):
		cmds.append("ruff check .")
	if shutil.which("flake8"):
		cmds.append("flake8 .")
	if shutil.which("black"):
		cmds.append("black --check .")
	if shutil.which("isort"):
		cmds.append("isort --check-only .")
	# JS/TS linters/formatters
	if (root / "package.json").exists():
		if shutil.which("eslint"):
			cmds.append("eslint .")
		if shutil.which("prettier"):
			cmds.append("prettier -c .")
	# Go linters
	if shutil.which("golangci-lint"):
		cmds.append("golangci-lint run")
	if shutil.which("go"):
		cmds.append("go vet ./...")
	# Rust lints
	if shutil.which("cargo") and shutil.which("rustc"):
		if shutil.which("cargo") and shutil.which("rustc") and shutil.which("rustfmt"):
			cmds.append("cargo fmt -- --check")
		if shutil.which("cargo-clippy") or shutil.which("clippy-driver"):
			cmds.append("cargo clippy -q -- -D warnings")
	return cmds

def detect_type_check_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python type checkers
	if shutil.which("mypy"):
		cmds.append("mypy .")
	if shutil.which("pyright"):
		cmds.append("pyright")
	# TS type checker
	if (root / "tsconfig.json").exists() or any(root.rglob("*.ts")):
		if shutil.which("tsc"):
			cmds.append("tsc -b --pretty false")
	# Go static analysis (build / vet act as type checks)
	if shutil.which("go") and ((root / "go.mod").exists() or any(root.rglob("*.go"))):
		cmds.append("go build ./...")
	# Rust type check
	if shutil.which("cargo") and (root / "Cargo.toml").exists():
		cmds.append("cargo check -q")
	# Java compile-only as type check
	if (root / "pom.xml").exists():
		cmds.append("mvn -q -DskipTests compile")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew compileJava")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle compileJava")
	return cmds

def detect_entrypoints(root: Path) -> List[str]:
	"""Heuristically detect runnable entrypoints (CLI mains, common scripts)."""
	ents: List[str] = []
	# Common Python CLI patterns
	for p in root.rglob("*.py"):
		try:
			# Skip hidden and virtual envs
			if any(seg.startswith(".") for seg in p.parts):
				continue
			text = p.read_text(encoding="utf-8")
			if "if __name__ == \"__main__\"" in text:
				rp = p.relative_to(root).as_posix()
				ents.append(f"python {rp}")
		except Exception:
			continue
	# Node scripts
	if (root / "package.json").exists():
		ents.append("npm test")
		ents.append("npm run build")
	# Go main package
	try:
		for p in root.rglob("*.go"):
			if any(seg.startswith(".") for seg in p.parts):
				continue
			try:
				text = p.read_text(encoding="utf-8")
			except Exception:
				continue
			if "package main" in text and "func main()" in text:
				ents.append("go run .")
				break
	except Exception:
		pass
	# Rust
	if (root / "Cargo.toml").exists():
		ents.append("cargo run")
	# Java (generic)
	if (root / "pom.xml").exists():
		ents.append("mvn -q test")
	return sorted(list(dict.fromkeys(ents)))

def detect_env_hints(root: Path) -> Dict[str, Any]:
	"""Surface lightweight environment hints (venv, python version markers)."""
	hints: Dict[str, Any] = {}
	try:
		if (root / "requirements.txt").exists():
			hints["python"] = True
		if (root / "environment.yml").exists() or (root / "conda.yaml").exists():
			hints["conda"] = True
		# Look for pyproject tool markers
		pp = root / "pyproject.toml"
		if pp.exists():
			try:
				text = pp.read_text(encoding="utf-8")
				if "tool.poetry" in text:
					hints["poetry"] = True
			except Exception:
				pass
		# Node package managers
		if (root / "package.json").exists():
			hints["node"] = True
			if (root / "yarn.lock").exists():
				hints["yarn"] = True
			if (root / "pnpm-lock.yaml").exists():
				hints["pnpm"] = True
		# Go / Rust / Java markers
		if (root / "go.mod").exists() or any(root.rglob("*.go")):
			hints["go"] = True
		if (root / "Cargo.toml").exists() or any(root.rglob("*.rs")):
			hints["rust"] = True
		if (root / "pom.xml").exists() or any(root.rglob("build.gradle")):
			hints["java"] = True
		# CI markers
		if (root / ".github" / "workflows").exists():
			hints["ci"] = "github"
		elif (root / ".gitlab-ci.yml").exists():
			hints["ci"] = "gitlab"
		# Language hints
		hints["languages"] = detect_languages(root)
	except Exception:
		pass
	return hints

def generate_manifest(root_dir: str | Path) -> Dict[str, Any]:
	root = Path(root_dir)
	manifest: Dict[str, Any] = {
		"root": str(root),
		"languages": detect_languages(root),
		"build_cmds": detect_build_cmds(root),
		"test_cmds": detect_test_cmds(root),
		"lint_cmds": detect_lint_cmds(root),
		"type_check_cmds": detect_type_check_cmds(root),
		"entrypoints": detect_entrypoints(root),
		"env_hints": detect_env_hints(root),
	}
	return manifest

if __name__ == "__main__":
	import argparse
	ap = argparse.ArgumentParser()
	ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]))
	default_out = str(Path(__file__).resolve().parents[1] / "data" / "sandbox" / "tmp" / "repo_manifest.json")
	ap.add_argument("--out", default=default_out)
	args = ap.parse_args()
	m = generate_manifest(args.root)
	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)
	outp.write_text(json.dumps(m, ensure_ascii=False, indent=2), encoding="utf-8")
	print(f"wrote manifest -> {args.out}")


from __future__ import annotations
import logging

import json
import shutil
from pathlib import Path
from typing import Dict, Any, List
import os

def detect_languages(root: Path) -> List[str]:
	langs: List[str] = []
	# Python
	if any(root.rglob("*.py")):
		langs.append("python")
	# JavaScript (package.json or .js files)
	if any(root.rglob("package.json")) or any(root.rglob("*.js")):
		langs.append("javascript")
	# TypeScript (tsconfig.json or any .ts excluding .d.ts)
	if (root / "tsconfig.json").exists() or any(p.suffix == ".ts" and not p.name.endswith(".d.ts") for p in root.rglob("*.ts")):
		langs.append("typescript")
	# Go
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		langs.append("go")
	# Rust
	if (root / "Cargo.toml").exists() or any(root.rglob("*.rs")):
		langs.append("rust")
	# Java
	if (root / "pom.xml").exists() or any(root.rglob("build.gradle")) or any(root.rglob("*.java")):
		langs.append("java")
	return langs

def detect_build_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python build/install
	if (root / "setup.cfg").exists() or (root / "pyproject.toml").exists():
		cmds.append("pip install -e .")
	# Poetry install if pyproject uses poetry
	try:
		pp = root / "pyproject.toml"
		if pp.exists():
			text = pp.read_text(encoding="utf-8")
			if "tool.poetry" in text:
				cmds.append("poetry install")
	except Exception:
		pass
	# Node install
	if (root / "package.json").exists():
		cmds.append("npm install")
		# Include common alternates if lockfiles found


	if any(root.rglob("*.py")):
		langs.append("python")
	# JavaScript (package.json or .js files)
	if any(root.rglob("package.json")) or any(root.rglob("*.js")):
		langs.append("javascript")
	# TypeScript (tsconfig.json or any .ts excluding .d.ts)
	if (root / "tsconfig.json").exists() or any(p.suffix == ".ts" and not p.name.endswith(".d.ts") for p in root.rglob("*.ts")):
		langs.append("typescript")
	# Go
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		langs.append("go")
	# Rust
	if (root / "Cargo.toml").exists() or any(root.rglob("*.rs")):
		langs.append("rust")
	# Java
	if (root / "pom.xml").exists() or any(root.rglob("build.gradle")) or any(root.rglob("*.java")):
		langs.append("java")
	return langs

def detect_build_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python build/install
	if (root / "setup.cfg").exists() or (root / "pyproject.toml").exists():
		cmds.append("pip install -e .")
	# Poetry install if pyproject uses poetry
	try:
		pp = root / "pyproject.toml"
		if pp.exists():
			text = pp.read_text(encoding="utf-8")
			if "tool.poetry" in text:
				cmds.append("poetry install")
	except Exception:
		pass
	# Node install
	if (root / "package.json").exists():
		cmds.append("npm install")
		# Include common alternates if lockfiles found
		if (root / "yarn.lock").exists():
			cmds.append("yarn install")
		if (root / "pnpm-lock.yaml").exists():
			cmds.append("pnpm install")
	# Go build
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go build ./...")
	# Rust build
	if (root / "Cargo.toml").exists():
		cmds.append("cargo build -q")
	# Java build (prefer wrapper if present)
	if (root / "pom.xml").exists():
		cmds.append("mvn -q -DskipTests install")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew build -x test")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle build -x test")
	return cmds

def detect_test_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python tests
	if shutil.which("pytest") or (root / "tests").exists():
		cmds.append("pytest -q")
	if (root / "tox.ini").exists() and shutil.which("tox"):
		cmds.append("tox -q")
	# Node tests
	if (root / "package.json").exists():
		cmds.append("npm test --silent")
		if shutil.which("npx"):
			cmds.append("npx jest --runInBand")
	# Go tests
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go test ./...")
	# Rust tests
	if (root / "Cargo.toml").exists():
		cmds.append("cargo test -q")


		if (root / "yarn.lock").exists():
			cmds.append("yarn install")
		if (root / "pnpm-lock.yaml").exists():
			cmds.append("pnpm install")
	# Go build
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go build ./...")
	# Rust build
	if (root / "Cargo.toml").exists():
		cmds.append("cargo build -q")
	# Java build (prefer wrapper if present)
	if (root / "pom.xml").exists():
		cmds.append("mvn -q -DskipTests install")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew build -x test")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle build -x test")
	return cmds

def detect_test_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python tests
	if shutil.which("pytest") or (root / "tests").exists():
		cmds.append("pytest -q")
	if (root / "tox.ini").exists() and shutil.which("tox"):
		cmds.append("tox -q")
	# Node tests
	if (root / "package.json").exists():
		cmds.append("npm test --silent")
		if shutil.which("npx"):
			cmds.append("npx jest --runInBand")
	# Go tests
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go test ./...")
	# Rust tests
	if (root / "Cargo.toml").exists():
		cmds.append("cargo test -q")
	# Java tests
	if (root / "pom.xml").exists():
		cmds.append("mvn -q test")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew test")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle test")
	return sorted(list(dict.fromkeys(cmds)))

def detect_lint_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python linters/formatters
	if shutil.which("ruff"):
		cmds.append("ruff check .")
	if shutil.which("flake8"):
		cmds.append("flake8 .")
	if shutil.which("black"):
		cmds.append("black --check .")
	if shutil.which("isort"):
		cmds.append("isort --check-only .")
	# JS/TS linters/formatters
	if (root / "package.json").exists():
		if shutil.which("eslint"):
			cmds.append("eslint .")
		if shutil.which("prettier"):
			cmds.append("prettier -c .")
	# Go linters


	if (root / "package.json").exists():
		cmds.append("npm test --silent")
		if shutil.which("npx"):
			cmds.append("npx jest --runInBand")
	# Go tests
	if (root / "go.mod").exists() or any(root.rglob("*.go")):
		cmds.append("go test ./...")
	# Rust tests
	if (root / "Cargo.toml").exists():
		cmds.append("cargo test -q")
	# Java tests
	if (root / "pom.xml").exists():
		cmds.append("mvn -q test")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew test")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle test")
	return sorted(list(dict.fromkeys(cmds)))

def detect_lint_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python linters/formatters
	if shutil.which("ruff"):
		cmds.append("ruff check .")
	if shutil.which("flake8"):
		cmds.append("flake8 .")
	if shutil.which("black"):
		cmds.append("black --check .")
	if shutil.which("isort"):
		cmds.append("isort --check-only .")
	# JS/TS linters/formatters
	if (root / "package.json").exists():
		if shutil.which("eslint"):
			cmds.append("eslint .")
		if shutil.which("prettier"):
			cmds.append("prettier -c .")
	# Go linters
	if shutil.which("golangci-lint"):
		cmds.append("golangci-lint run")
	if shutil.which("go"):
		cmds.append("go vet ./...")
	# Rust lints
	if shutil.which("cargo") and shutil.which("rustc"):
		if shutil.which("cargo") and shutil.which("rustc") and shutil.which("rustfmt"):
			cmds.append("cargo fmt -- --check")
		if shutil.which("cargo-clippy") or shutil.which("clippy-driver"):
			cmds.append("cargo clippy -q -- -D warnings")
	return cmds

def detect_type_check_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python type checkers
	if shutil.which("mypy"):
		cmds.append("mypy .")
	if shutil.which("pyright"):
		cmds.append("pyright")
	# TS type checker
	if (root / "tsconfig.json").exists() or any(root.rglob("*.ts")):
		if shutil.which("tsc"):
			cmds.append("tsc -b --pretty false")
	# Go static analysis (build / vet act as type checks)
	if shutil.which("go") and ((root / "go.mod").exists() or any(root.rglob("*.go"))):
		cmds.append("go build ./...")
	# Rust type check
	if shutil.which("cargo") and (root / "Cargo.toml").exists():
		cmds.append("cargo check -q")
	# Java compile-only as type check


	# JS/TS linters/formatters
	if (root / "package.json").exists():
		if shutil.which("eslint"):
			cmds.append("eslint .")
		if shutil.which("prettier"):
			cmds.append("prettier -c .")
	# Go linters
	if shutil.which("golangci-lint"):
		cmds.append("golangci-lint run")
	if shutil.which("go"):
		cmds.append("go vet ./...")
	# Rust lints
	if shutil.which("cargo") and shutil.which("rustc"):
		if shutil.which("cargo") and shutil.which("rustc") and shutil.which("rustfmt"):
			cmds.append("cargo fmt -- --check")
		if shutil.which("cargo-clippy") or shutil.which("clippy-driver"):
			cmds.append("cargo clippy -q -- -D warnings")
	return cmds

def detect_type_check_cmds(root: Path) -> List[str]:
	cmds: List[str] = []
	# Python type checkers
	if shutil.which("mypy"):
		cmds.append("mypy .")
	if shutil.which("pyright"):
		cmds.append("pyright")
	# TS type checker
	if (root / "tsconfig.json").exists() or any(root.rglob("*.ts")):
		if shutil.which("tsc"):
			cmds.append("tsc -b --pretty false")
	# Go static analysis (build / vet act as type checks)
	if shutil.which("go") and ((root / "go.mod").exists() or any(root.rglob("*.go"))):
		cmds.append("go build ./...")
	# Rust type check
	if shutil.which("cargo") and (root / "Cargo.toml").exists():
		cmds.append("cargo check -q")
	# Java compile-only as type check
	if (root / "pom.xml").exists():
		cmds.append("mvn -q -DskipTests compile")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew compileJava")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle compileJava")
	return cmds

def detect_entrypoints(root: Path) -> List[str]:
	"""Heuristically detect runnable entrypoints (CLI mains, common scripts)."""
	ents: List[str] = []
	# Common Python CLI patterns
	for p in root.rglob("*.py"):
		try:
			# Skip hidden and virtual envs
			if any(seg.startswith(".") for seg in p.parts):
				continue
			text = p.read_text(encoding="utf-8")
			if "if __name__ == \"__main__\"" in text:
				rp = p.relative_to(root).as_posix()
				ents.append(f"python {rp}")
		except Exception:
			continue
	# Node scripts
	if (root / "package.json").exists():
		ents.append("npm test")


	# TS type checker
	if (root / "tsconfig.json").exists() or any(root.rglob("*.ts")):
		if shutil.which("tsc"):
			cmds.append("tsc -b --pretty false")
	# Go static analysis (build / vet act as type checks)
	if shutil.which("go") and ((root / "go.mod").exists() or any(root.rglob("*.go"))):
		cmds.append("go build ./...")
	# Rust type check
	if shutil.which("cargo") and (root / "Cargo.toml").exists():
		cmds.append("cargo check -q")
	# Java compile-only as type check
	if (root / "pom.xml").exists():
		cmds.append("mvn -q -DskipTests compile")
	if any(root.rglob("gradlew")):
		cmds.append("./gradlew compileJava")
	elif any(root.rglob("build.gradle")):
		cmds.append("gradle compileJava")
	return cmds

def detect_entrypoints(root: Path) -> List[str]:
	"""Heuristically detect runnable entrypoints (CLI mains, common scripts)."""
	ents: List[str] = []
	# Common Python CLI patterns
	for p in root.rglob("*.py"):
		try:
			# Skip hidden and virtual envs
			if any(seg.startswith(".") for seg in p.parts):
				continue
			text = p.read_text(encoding="utf-8")
			if "if __name__ == \"__main__\"" in text:
				rp = p.relative_to(root).as_posix()
				ents.append(f"python {rp}")
		except Exception:
			continue
	# Node scripts
	if (root / "package.json").exists():
		ents.append("npm test")
		ents.append("npm run build")
	# Go main package
	try:
		for p in root.rglob("*.go"):
			if any(seg.startswith(".") for seg in p.parts):
				continue
			try:
				text = p.read_text(encoding="utf-8")
			except Exception:
				continue
			if "package main" in text and "func main()" in text:
				ents.append("go run .")
				break
	except Exception:
		pass
	# Rust
	if (root / "Cargo.toml").exists():
		ents.append("cargo run")
	# Java (generic)
	if (root / "pom.xml").exists():
		ents.append("mvn -q test")
	return sorted(list(dict.fromkeys(ents)))

def detect_env_hints(root: Path) -> Dict[str, Any]:
	"""Surface lightweight environment hints (venv, python version markers)."""
	hints: Dict[str, Any] = {}
	try:
		if (root / "requirements.txt").exists():
			hints["python"] = True
		if (root / "environment.yml").exists() or (root / "conda.yaml").exists():
			hints["conda"] = True
		# Look for pyproject tool markers
		pp = root / "pyproject.toml"
		if pp.exists():
			try:
				text = pp.read_text(encoding="utf-8")
				if "tool.poetry" in text:
					hints["poetry"] = True
			except Exception:
				pass
		# Node package managers


			if any(seg.startswith(".") for seg in p.parts):
				continue
			try:
				text = p.read_text(encoding="utf-8")
			except Exception:
				continue
			if "package main" in text and "func main()" in text:
				ents.append("go run .")
				break
	except Exception:
		pass
	# Rust
	if (root / "Cargo.toml").exists():
		ents.append("cargo run")
	# Java (generic)
	if (root / "pom.xml").exists():
		ents.append("mvn -q test")
	return sorted(list(dict.fromkeys(ents)))

def detect_env_hints(root: Path) -> Dict[str, Any]:
	"""Surface lightweight environment hints (venv, python version markers)."""
	hints: Dict[str, Any] = {}
	try:
		if (root / "requirements.txt").exists():
			hints["python"] = True
		if (root / "environment.yml").exists() or (root / "conda.yaml").exists():
			hints["conda"] = True
		# Look for pyproject tool markers
		pp = root / "pyproject.toml"
		if pp.exists():
			try:
				text = pp.read_text(encoding="utf-8")
				if "tool.poetry" in text:
					hints["poetry"] = True
			except Exception:
				pass
		# Node package managers
		if (root / "package.json").exists():
			hints["node"] = True
			if (root / "yarn.lock").exists():
				hints["yarn"] = True
			if (root / "pnpm-lock.yaml").exists():
				hints["pnpm"] = True
		# Go / Rust / Java markers
		if (root / "go.mod").exists() or any(root.rglob("*.go")):
			hints["go"] = True
		if (root / "Cargo.toml").exists() or any(root.rglob("*.rs")):
			hints["rust"] = True
		if (root / "pom.xml").exists() or any(root.rglob("build.gradle")):
			hints["java"] = True
		# CI markers
		if (root / ".github" / "workflows").exists():
			hints["ci"] = "github"
		elif (root / ".gitlab-ci.yml").exists():
			hints["ci"] = "gitlab"
		# Language hints
		hints["languages"] = detect_languages(root)
	except Exception:
		pass
	return hints

def generate_manifest(root_dir: str | Path) -> Dict[str, Any]:
	root = Path(root_dir)
	manifest: Dict[str, Any] = {
		"root": str(root),
		"languages": detect_languages(root),
		"build_cmds": detect_build_cmds(root),
		"test_cmds": detect_test_cmds(root),
		"lint_cmds": detect_lint_cmds(root),
		"type_check_cmds": detect_type_check_cmds(root),
		"entrypoints": detect_entrypoints(root),
		"env_hints": detect_env_hints(root),
	}
	return manifest

if __name__ == "__main__":
	import argparse
	ap = argparse.ArgumentParser()


				hints["pnpm"] = True
		# Go / Rust / Java markers
		if (root / "go.mod").exists() or any(root.rglob("*.go")):
			hints["go"] = True
		if (root / "Cargo.toml").exists() or any(root.rglob("*.rs")):
			hints["rust"] = True
		if (root / "pom.xml").exists() or any(root.rglob("build.gradle")):
			hints["java"] = True
		# CI markers
		if (root / ".github" / "workflows").exists():
			hints["ci"] = "github"
		elif (root / ".gitlab-ci.yml").exists():
			hints["ci"] = "gitlab"
		# Language hints
		hints["languages"] = detect_languages(root)
	except Exception:
		pass
	return hints

def generate_manifest(root_dir: str | Path) -> Dict[str, Any]:
	root = Path(root_dir)
	manifest: Dict[str, Any] = {
		"root": str(root),
		"languages": detect_languages(root),
		"build_cmds": detect_build_cmds(root),
		"test_cmds": detect_test_cmds(root),
		"lint_cmds": detect_lint_cmds(root),
		"type_check_cmds": detect_type_check_cmds(root),
		"entrypoints": detect_entrypoints(root),
		"env_hints": detect_env_hints(root),
	}
	return manifest

if __name__ == "__main__":
	import argparse
	ap = argparse.ArgumentParser()
	ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]))
	default_out = str(Path(__file__).resolve().parents[1] / "data" / "sandbox" / "tmp" / "repo_manifest.json")
	ap.add_argument("--out", default=default_out)
	args = ap.parse_args()
	m = generate_manifest(args.root)
	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)
	outp.write_text(json.dumps(m, ensure_ascii=False, indent=2), encoding="utf-8")
	print(f"wrote manifest -> {args.out}")


from __future__ import annotations
import logging

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

def _allowed(path: Path, allow_globs: List[str], block_globs: List[str]) -> bool:
	from fnmatch import fnmatch
	sp = str(path)
	for g in block_globs:
		if fnmatch(sp, g):
			return False
	if not allow_globs:
		return True
	return any(fnmatch(sp, g) for g in allow_globs)

def validate_diff_policy(diff_text: str, allowed_exts: List[str] | None = None, max_hunk_lines: int = 1000, forbidden_patterns: List[str] | None = None) -> Dict[str, Any]:
	"""Lightweight static policy validation over unified diffs.

	Checks:
	- Only allows files with certain extensions (if provided)
	- Limits per-hunk line changes to avoid massive edits
	- Forbids sensitive patterns in diff (e.g., secret files, .git)
	"""
	issues: List[str] = []
	allowed_exts = allowed_exts or [".py", ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg"]
	forbidden_patterns = forbidden_patterns or ["/.git/", "id_rsa", "aws_secret", "GITHUB_TOKEN", "BEGIN PRIVATE KEY"]
	current_hunk = 0
	# verify file headers and extensions
	for line in diff_text.splitlines():
		if line.startswith("+++ b/") or line.startswith("--- a/"):
			try:
				path = line.split(" ", 1)[1].strip()
				# path like b/path; strip leading marker
				if path.startswith("a/") or path.startswith("b/"):
					sp = path[2:]
					ext = Path(sp).suffix
					if allowed_exts and (ext not in allowed_exts):
						issues.append(f"disallowed_extension:{ext}:{sp}")
			except Exception:
				continue
		if line.startswith("@@ "):
			# new hunk; reset counter
			current_hunk = 0
			continue
		if line.startswith("+") or line.startswith("-"):
			# count changed lines (skip file header markers '+++', '---')
			if not (line.startswith("+++") or line.startswith("---")):
				current_hunk += 1
				if current_hunk > max_hunk_lines:
					issues.append("hunk_too_large")
					# no early break; collect more issues
		for pat in forbidden_patterns:
			if pat and pat in line:
				issues.append(f"forbidden_pattern:{pat}")
				break
	return {"ok": len(issues) == 0, "issues": issues}

def apply_unified_diff(repo_dir: str | Path, diff_text: str, allow_globs: List[str], block_globs: List[str], max_files: int = 50, dry_run: bool = False) -> Dict[str, Any]:
	"""
	Apply a unified diff using git with safety checks.

	Safety gates:
	- allowlist and blocklist globs
	- file count limit
	- dry-run support via `git apply --check`
	"""
	repo = Path(repo_dir)
	# Scan diff for files
	files: List[str] = []
	for line in diff_text.splitlines():
		if line.startswith("+++ b/"):
			files.append(line[6:].strip())
	if len(files) > max_files:
		return {"ok": False, "error": f"too many files: {len(files)} > {max_files}"}
	for f in files:
		p = repo / f
		if not _allowed(p, allow_globs, block_globs):
			return {"ok": False, "error": f"blocked by policy: {f}"}
	# Policy validation prior to git application (shared policy)
	try:
		from agi_dw.tools.patch_policy import load_env_limits, validate_unified_diff  # type: ignore
		ok, why, meta = validate_unified_diff(diff_text, load_env_limits(strict_default=True))
		if not ok:
			return {"ok": False, "error": f"diff_validation_failed:{why}", **meta}
	except Exception:
		policy = validate_diff_policy(diff_text)
		if not policy.get("ok", False):
			return {"ok": False, "error": "diff_validation_failed", "policy_issues": policy.get("issues", [])}
	# Write diff to a temp file and apply
	tmp = repo / ".tmp.patch"
	tmp.write_text(diff_text, encoding="utf-8")
	try:
		cmd: List[str]
		if dry_run:
			cmd = ["git", "apply", "--check", str(tmp)]
		else:
			cmd = ["git", "apply", "--index", "--reject", str(tmp)]
		p = subprocess.Popen(cmd, cwd=str(repo), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
		out, _ = p.communicate()
		ok = (p.returncode == 0)
		return {"ok": ok, "output": out, "dry_run": dry_run}
	finally:
		try:
			tmp.unlink(missing_ok=True)
		except Exception:
			pass

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--repo", required=True)
	ap.add_argument("--diff-file", required=True)
	ap.add_argument("--allow", nargs="*", default=["**/*.py"])  # allowlist globs
	ap.add_argument("--block", nargs="*", default=["**/.git/**", "**/.venv/**"])  # blocklist globs
	ap.add_argument("--max-files", type=int, default=50)
	ap.add_argument("--dry-run", action="store_true")
	args = ap.parse_args()

	diff_text = Path(args.diff_file).read_text(encoding="utf-8")
	res = apply_unified_diff(args.repo, diff_text, list(args.allow or []), list(args.block or []), max_files=int(args.max_files), dry_run=bool(args.dry_run))
	print(res)
	return 0 if bool(res.get("ok")) else 1

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


from __future__ import annotations
import logging

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

def _allowed(path: Path, allow_globs: List[str], block_globs: List[str]) -> bool:
	from fnmatch import fnmatch
	sp = str(path)
	for g in block_globs:
		if fnmatch(sp, g):
			return False
	if not allow_globs:
		return True
	return any(fnmatch(sp, g) for g in allow_globs)

def validate_diff_policy(diff_text: str, allowed_exts: List[str] | None = None, max_hunk_lines: int = 1000, forbidden_patterns: List[str] | None = None) -> Dict[str, Any]:
	"""Lightweight static policy validation over unified diffs.

	Checks:
	- Only allows files with certain extensions (if provided)
	- Limits per-hunk line changes to avoid massive edits
	- Forbids sensitive patterns in diff (e.g., secret files, .git)
	"""
	issues: List[str] = []
	allowed_exts = allowed_exts or [".py", ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg"]
	forbidden_patterns = forbidden_patterns or ["/.git/", "id_rsa", "aws_secret", "GITHUB_TOKEN", "BEGIN PRIVATE KEY"]
	current_hunk = 0
	# verify file headers and extensions
	for line in diff_text.splitlines():
		if line.startswith("+++ b/") or line.startswith("--- a/"):
			try:
				path = line.split(" ", 1)[1].strip()
				# path like b/path; strip leading marker


from __future__ import annotations
import logging

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

def _allowed(path: Path, allow_globs: List[str], block_globs: List[str]) -> bool:
	from fnmatch import fnmatch
	sp = str(path)
	for g in block_globs:
		if fnmatch(sp, g):
			return False
	if not allow_globs:
		return True
	return any(fnmatch(sp, g) for g in allow_globs)

def validate_diff_policy(diff_text: str, allowed_exts: List[str] | None = None, max_hunk_lines: int = 1000, forbidden_patterns: List[str] | None = None) -> Dict[str, Any]:
	"""Lightweight static policy validation over unified diffs.

	Checks:
	- Only allows files with certain extensions (if provided)
	- Limits per-hunk line changes to avoid massive edits
	- Forbids sensitive patterns in diff (e.g., secret files, .git)
	"""
	issues: List[str] = []
	allowed_exts = allowed_exts or [".py", ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg"]
	forbidden_patterns = forbidden_patterns or ["/.git/", "id_rsa", "aws_secret", "GITHUB_TOKEN", "BEGIN PRIVATE KEY"]
	current_hunk = 0
	# verify file headers and extensions
	for line in diff_text.splitlines():
		if line.startswith("+++ b/") or line.startswith("--- a/"):
			try:
				path = line.split(" ", 1)[1].strip()
				# path like b/path; strip leading marker
				if path.startswith("a/") or path.startswith("b/"):
					sp = path[2:]
					ext = Path(sp).suffix
					if allowed_exts and (ext not in allowed_exts):
						issues.append(f"disallowed_extension:{ext}:{sp}")
			except Exception:
				continue
		if line.startswith("@@ "):
			# new hunk; reset counter
			current_hunk = 0
			continue
		if line.startswith("+") or line.startswith("-"):
			# count changed lines (skip file header markers '+++', '---')
			if not (line.startswith("+++") or line.startswith("---")):
				current_hunk += 1
				if current_hunk > max_hunk_lines:
					issues.append("hunk_too_large")
					# no early break; collect more issues
		for pat in forbidden_patterns:
			if pat and pat in line:
				issues.append(f"forbidden_pattern:{pat}")
				break
	return {"ok": len(issues) == 0, "issues": issues}

def apply_unified_diff(repo_dir: str | Path, diff_text: str, allow_globs: List[str], block_globs: List[str], max_files: int = 50, dry_run: bool = False) -> Dict[str, Any]:
	"""
	Apply a unified diff using git with safety checks.

	Safety gates:
	- allowlist and blocklist globs
	- file count limit
	- dry-run support via `git apply --check`
	"""
	repo = Path(repo_dir)
	# Scan diff for files
	files: List[str] = []
	for line in diff_text.splitlines():
		if line.startswith("+++ b/"):
			files.append(line[6:].strip())
	if len(files) > max_files:
		return {"ok": False, "error": f"too many files: {len(files)} > {max_files}"}
	for f in files:


			except Exception:
				continue
		if line.startswith("@@ "):
			# new hunk; reset counter
			current_hunk = 0
			continue
		if line.startswith("+") or line.startswith("-"):
			# count changed lines (skip file header markers '+++', '---')
			if not (line.startswith("+++") or line.startswith("---")):
				current_hunk += 1
				if current_hunk > max_hunk_lines:
					issues.append("hunk_too_large")
					# no early break; collect more issues
		for pat in forbidden_patterns:
			if pat and pat in line:
				issues.append(f"forbidden_pattern:{pat}")
				break
	return {"ok": len(issues) == 0, "issues": issues}

def apply_unified_diff(repo_dir: str | Path, diff_text: str, allow_globs: List[str], block_globs: List[str], max_files: int = 50, dry_run: bool = False) -> Dict[str, Any]:
	"""
	Apply a unified diff using git with safety checks.

	Safety gates:
	- allowlist and blocklist globs
	- file count limit
	- dry-run support via `git apply --check`
	"""
	repo = Path(repo_dir)
	# Scan diff for files
	files: List[str] = []
	for line in diff_text.splitlines():
		if line.startswith("+++ b/"):
			files.append(line[6:].strip())
	if len(files) > max_files:
		return {"ok": False, "error": f"too many files: {len(files)} > {max_files}"}
	for f in files:
		p = repo / f
		if not _allowed(p, allow_globs, block_globs):
			return {"ok": False, "error": f"blocked by policy: {f}"}
	# Policy validation prior to git application (shared policy)
	try:
		from agi_dw.tools.patch_policy import load_env_limits, validate_unified_diff  # type: ignore
		ok, why, meta = validate_unified_diff(diff_text, load_env_limits(strict_default=True))
		if not ok:
			return {"ok": False, "error": f"diff_validation_failed:{why}", **meta}
	except Exception:
		policy = validate_diff_policy(diff_text)
		if not policy.get("ok", False):
			return {"ok": False, "error": "diff_validation_failed", "policy_issues": policy.get("issues", [])}
	# Write diff to a temp file and apply
	tmp = repo / ".tmp.patch"
	tmp.write_text(diff_text, encoding="utf-8")
	try:
		cmd: List[str]
		if dry_run:
			cmd = ["git", "apply", "--check", str(tmp)]
		else:
			cmd = ["git", "apply", "--index", "--reject", str(tmp)]
		p = subprocess.Popen(cmd, cwd=str(repo), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
		out, _ = p.communicate()
		ok = (p.returncode == 0)
		return {"ok": ok, "output": out, "dry_run": dry_run}
	finally:
		try:
			tmp.unlink(missing_ok=True)
		except Exception:
			pass

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--repo", required=True)
	ap.add_argument("--diff-file", required=True)
	ap.add_argument("--allow", nargs="*", default=["**/*.py"])  # allowlist globs
	ap.add_argument("--block", nargs="*", default=["**/.git/**", "**/.venv/**"])  # blocklist globs
	ap.add_argument("--max-files", type=int, default=50)
	ap.add_argument("--dry-run", action="store_true")
	args = ap.parse_args()

	diff_text = Path(args.diff_file).read_text(encoding="utf-8")
	res = apply_unified_diff(args.repo, diff_text, list(args.allow or []), list(args.block or []), max_files=int(args.max_files), dry_run=bool(args.dry_run))
	print(res)
	return 0 if bool(res.get("ok")) else 1

if __name__ == "__main__":
	import sys


main

	# Write diff to a temp file and apply
	tmp = repo / ".tmp.patch"
	tmp.write_text(diff_text, encoding="utf-8")
	try:
		cmd: List[str]
		if dry_run:
			cmd = ["git", "apply", "--check", str(tmp)]
		else:
			cmd = ["git", "apply", "--index", "--reject", str(tmp)]
		p = subprocess.Popen(cmd, cwd=str(repo), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
		out, _ = p.communicate()
		ok = (p.returncode == 0)
		return {"ok": ok, "output": out, "dry_run": dry_run}
	finally:
		try:
			tmp.unlink(missing_ok=True)
		except Exception:
			pass

def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--repo", required=True)
	ap.add_argument("--diff-file", required=True)
	ap.add_argument("--allow", nargs="*", default=["**/*.py"])  # allowlist globs
	ap.add_argument("--block", nargs="*", default=["**/.git/**", "**/.venv/**"])  # blocklist globs
	ap.add_argument("--max-files", type=int, default=50)
	ap.add_argument("--dry-run", action="store_true")
	args = ap.parse_args()

	diff_text = Path(args.diff_file).read_text(encoding="utf-8")
	res = apply_unified_diff(args.repo, diff_text, list(args.allow or []), list(args.block or []), max_files=int(args.max_files), dry_run=bool(args.dry_run))
	print(res)
	return 0 if bool(res.get("ok")) else 1

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


from __future__ import annotations
import logging

import argparse
import json
from pathlib import Path

def validate_jsonl(path: Path, required_keys: list[str]) -> int:
	errs = 0
	for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
		try:
			obj = json.loads(line)
		except Exception:
			print(f"invalid json at {path}:{i}")
			errs += 1
			continue
		missing = [k for k in required_keys if k not in obj]
		if missing:
			print(f"missing keys {missing} at {path}:{i}")
			errs += 1
	return errs

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser()
	parser.add_argument("--root", default="/data/agiattempt/agi_dw/data/sft")
	args = parser.parse_args(argv)
	root = Path(args.root)

	errs = 0
	errs += validate_jsonl(root / "cli.jsonl", ["goal", "cwd", "command_args", "env_policy"])
	errs += validate_jsonl(root / "hitl.jsonl", ["preview", "risk", "policy", "decision"])
	print(f"errors={errs}")
	return 0 if errs == 0 else 2

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


import json
from pathlib import Path

def validate_jsonl(path: Path, required_keys: list[str]) -> int:
	errs = 0
	for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
		try:
			obj = json.loads(line)
		except Exception:
			print(f"invalid json at {path}:{i}")
			errs += 1
			continue
		missing = [k for k in required_keys if k not in obj]
		if missing:
			print(f"missing keys {missing} at {path}:{i}")
			errs += 1
	return errs

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser()
	parser.add_argument("--root", default="/data/agiattempt/agi_dw/data/sft")
	args = parser.parse_args(argv)
	root = Path(args.root)

	errs = 0
	errs += validate_jsonl(root / "cli.jsonl", ["goal", "cwd", "command_args", "env_policy"])
	errs += validate_jsonl(root / "hitl.jsonl", ["preview", "risk", "policy", "decision"])
	print(f"errors={errs}")
	return 0 if errs == 0 else 2

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


from __future__ import annotations
import logging

import argparse
import json
import os
import shlex
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

def _now_iso() -> str:
	return datetime.now(timezone.utc).isoformat()

def _git_info(repo: Path) -> Dict[str, Any]:
	def run_git(args: list[str]) -> str | None:
		try:
			res = subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=5)
			if res.returncode == 0:
				return (res.stdout or "").strip()
		except Exception:
			return None
		return None

	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for rec in records:
			f.write(json.dumps(rec, ensure_ascii=False))
			f.write("\n")

def run_pytest(repo: Path, pytest_args: list[str]) -> dict[str, Any]:
	env = os.environ.copy()
	cmd = [sys.executable, "-m", "pytest", *pytest_args]
	start = time.time()
	proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)
	end = time.time()
	return {
		"command": " ".join(shlex.quote(c) for c in cmd),
		"cwd": str(repo),
		"env_keys": sorted(list(env.keys()))[:50],
		"returncode": proc.returncode,
		"stdout": proc.stdout,
		"stderr": proc.stderr,
		"duration_ms": int((end - start) * 1000),
	}

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Trace-wrapped test runner for toy repos")
	parser.add_argument("--repo", required=True, help="Path to repo to test")
	parser.add_argument("--pytest-args", nargs=argparse.REMAINDER, default=None, help="Args to pass to pytest (after this flag)")
	args = parser.parse_args(argv)

	repo = Path(args.repo).resolve()
	trace_id = f"{datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')}_{uuid.uuid4().hex[:8]}"
	trace_dir = Path("/data/agiattempt/agi_dw/data/traces") / trace_id

	pre_state = {
		"trace_id": trace_id,
		"ts": _now_iso(),
		"repo": str(repo),
		"git": _git_info(repo),
		"pytest_args": args.pytest_args or ["-q"],
	}
	write_json(trace_dir / "pre_state.json", pre_state)

	actions: list[dict[str, Any]] = []
	actions.append({
		"ts": _now_iso(),
		"kind": "run_pytest",
		"details": {"pytest_args": args.pytest_args or ["-q"]},
	})
	write_jsonl(trace_dir / "actions" / "actions.jsonl", actions)

	pytest_args = args.pytest_args if args.pytest_args else ["-q"]
	res = run_pytest(repo, pytest_args)
	post_state = {
		"ts": _now_iso(),
		"trace_id": trace_id,
		"result": {
			"returncode": res["returncode"],
			"duration_ms": res["duration_ms"],
		},
		"stdout_tail": (res["stdout"] or "").splitlines()[-50:],
		"stderr_tail": (res["stderr"] or "").splitlines()[-50:],
	}
	write_json(trace_dir / "post_state.json", post_state)
	# Print the trace directory for callers to capture
	print(str(trace_dir))
	return 0

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


from __future__ import annotations
import logging

import argparse
import json
import os
import shlex
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

def _now_iso() -> str:
	return datetime.now(timezone.utc).isoformat()

def _git_info(repo: Path) -> Dict[str, Any]:
	def run_git(args: list[str]) -> str | None:
		try:
			res = subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=5)
			if res.returncode == 0:
				return (res.stdout or "").strip()
		except Exception:
			return None
		return None

	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)


from __future__ import annotations
import logging

import argparse
import json
import os
import shlex
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

def _now_iso() -> str:
	return datetime.now(timezone.utc).isoformat()

def _git_info(repo: Path) -> Dict[str, Any]:
	def run_git(args: list[str]) -> str | None:
		try:
			res = subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=5)
			if res.returncode == 0:
				return (res.stdout or "").strip()
		except Exception:
			return None
		return None

	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for rec in records:
			f.write(json.dumps(rec, ensure_ascii=False))
			f.write("\n")

def run_pytest(repo: Path, pytest_args: list[str]) -> dict[str, Any]:
	env = os.environ.copy()
	cmd = [sys.executable, "-m", "pytest", *pytest_args]
	start = time.time()
	proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)


def _now_iso() -> str:
	return datetime.now(timezone.utc).isoformat()

def _git_info(repo: Path) -> Dict[str, Any]:
	def run_git(args: list[str]) -> str | None:
		try:
			res = subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=5)
			if res.returncode == 0:
				return (res.stdout or "").strip()
		except Exception:
			return None
		return None

	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for rec in records:
			f.write(json.dumps(rec, ensure_ascii=False))
			f.write("\n")

def run_pytest(repo: Path, pytest_args: list[str]) -> dict[str, Any]:
	env = os.environ.copy()
	cmd = [sys.executable, "-m", "pytest", *pytest_args]
	start = time.time()
	proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)
	end = time.time()
	return {
		"command": " ".join(shlex.quote(c) for c in cmd),
		"cwd": str(repo),
		"env_keys": sorted(list(env.keys()))[:50],


	def run_git(args: list[str]) -> str | None:
		try:
			res = subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=5)
			if res.returncode == 0:
				return (res.stdout or "").strip()
		except Exception:
			return None
		return None

	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for rec in records:
			f.write(json.dumps(rec, ensure_ascii=False))
			f.write("\n")

def run_pytest(repo: Path, pytest_args: list[str]) -> dict[str, Any]:
	env = os.environ.copy()
	cmd = [sys.executable, "-m", "pytest", *pytest_args]
	start = time.time()
	proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)
	end = time.time()
	return {
		"command": " ".join(shlex.quote(c) for c in cmd),
		"cwd": str(repo),
		"env_keys": sorted(list(env.keys()))[:50],
		"returncode": proc.returncode,
		"stdout": proc.stdout,
		"stderr": proc.stderr,
		"duration_ms": int((end - start) * 1000),
	}

def main(argv: list[str] | None = None) -> int:


	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for rec in records:
			f.write(json.dumps(rec, ensure_ascii=False))
			f.write("\n")

def run_pytest(repo: Path, pytest_args: list[str]) -> dict[str, Any]:
	env = os.environ.copy()
	cmd = [sys.executable, "-m", "pytest", *pytest_args]
	start = time.time()
	proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)
	end = time.time()
	return {
		"command": " ".join(shlex.quote(c) for c in cmd),
		"cwd": str(repo),
		"env_keys": sorted(list(env.keys()))[:50],
		"returncode": proc.returncode,
		"stdout": proc.stdout,
		"stderr": proc.stderr,
		"duration_ms": int((end - start) * 1000),
	}

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Trace-wrapped test runner for toy repos")
	parser.add_argument("--repo", required=True, help="Path to repo to test")
	parser.add_argument("--pytest-args", nargs=argparse.REMAINDER, default=None, help="Args to pass to pytest (after this flag)")
	args = parser.parse_args(argv)

	repo = Path(args.repo).resolve()
	trace_id = f"{datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')}_{uuid.uuid4().hex[:8]}"
	trace_dir = Path("/data/agiattempt/agi_dw/data/traces") / trace_id

	pre_state = {
		"trace_id": trace_id,
		"ts": _now_iso(),
		"repo": str(repo),
		"git": _git_info(repo),
		"pytest_args": args.pytest_args or ["-q"],
	}
	write_json(trace_dir / "pre_state.json", pre_state)


			f.write("\n")

def run_pytest(repo: Path, pytest_args: list[str]) -> dict[str, Any]:
	env = os.environ.copy()
	cmd = [sys.executable, "-m", "pytest", *pytest_args]
	start = time.time()
	proc = subprocess.run(cmd, cwd=str(repo), capture_output=True, text=True)
	end = time.time()
	return {
		"command": " ".join(shlex.quote(c) for c in cmd),
		"cwd": str(repo),
		"env_keys": sorted(list(env.keys()))[:50],
		"returncode": proc.returncode,
		"stdout": proc.stdout,
		"stderr": proc.stderr,
		"duration_ms": int((end - start) * 1000),
	}

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Trace-wrapped test runner for toy repos")
	parser.add_argument("--repo", required=True, help="Path to repo to test")
	parser.add_argument("--pytest-args", nargs=argparse.REMAINDER, default=None, help="Args to pass to pytest (after this flag)")
	args = parser.parse_args(argv)

	repo = Path(args.repo).resolve()
	trace_id = f"{datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')}_{uuid.uuid4().hex[:8]}"
	trace_dir = Path("/data/agiattempt/agi_dw/data/traces") / trace_id

	pre_state = {
		"trace_id": trace_id,
		"ts": _now_iso(),
		"repo": str(repo),
		"git": _git_info(repo),
		"pytest_args": args.pytest_args or ["-q"],
	}
	write_json(trace_dir / "pre_state.json", pre_state)

	actions: list[dict[str, Any]] = []
	actions.append({
		"ts": _now_iso(),
		"kind": "run_pytest",
		"details": {"pytest_args": args.pytest_args or ["-q"]},
	})
	write_jsonl(trace_dir / "actions" / "actions.jsonl", actions)

	pytest_args = args.pytest_args if args.pytest_args else ["-q"]
	res = run_pytest(repo, pytest_args)
	post_state = {
		"ts": _now_iso(),
		"trace_id": trace_id,
		"result": {
			"returncode": res["returncode"],
			"duration_ms": res["duration_ms"],
		},
		"stdout_tail": (res["stdout"] or "").splitlines()[-50:],
		"stderr_tail": (res["stderr"] or "").splitlines()[-50:],
	}
	write_json(trace_dir / "post_state.json", post_state)
	# Print the trace directory for callers to capture
	print(str(trace_dir))
	return 0

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


import logging

import argparse
import json
import os
import shlex
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict

def _now_iso() -> str:
	return datetime.now(timezone.utc).isoformat()

def _git_info(repo: Path) -> Dict[str, Any]:
	def run_git(args: list[str]) -> str | None:
		try:
			res = subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, timeout=5)
			if res.returncode == 0:
				return (res.stdout or "").strip()
		except Exception:
			return None
		return None

	return {
		"sha": run_git(["rev-parse", "HEAD"]) or "",
		"branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]) or "",
	}

def write_json(path: Path, obj: Any) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	path.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")

def write_jsonl(path: Path, records: list[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for rec in records:
			f.write(json.dumps(rec, ensure_ascii=False))
			f.write("\n")


from __future__ import annotations
import logging

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

from agi_dw.tools.repo_manifest import generate_manifest
from agi_dw.tools.code_index import index_python_repo

def compact_code_index(idx: Dict[str, Any], root: Path) -> Dict[str, Any]:
	"""Create a compact, language-agnostic subset of the code index.

	Currently focuses on Python symbols. Safely caps long lists.
	"""
	max_items = 200
	files = list(idx.get("files", []))[:max_items]
	functions: Dict[str, List[Dict[str, Any]]] = {}
	classes: Dict[str, List[Dict[str, Any]]] = {}
	for fp in files:
		try:
			rel = str(Path(fp).resolve())
			fns = idx.get("functions", {}).get(fp, [])
			cls = idx.get("classes", {}).get(fp, [])
			# Keep only a few key fields
			functions[rel] = [
				{"name": it.get("name"), "qualname": it.get("qualname"), "lineno": it.get("lineno")}
				for it in (fns or [])
			][:max_items]
			classes[rel] = [
				{"name": it.get("name"), "lineno": it.get("lineno"), "bases": it.get("bases")}
				for it in (cls or [])
			][:max_items]
		except Exception:
			continue
	return {"files": files, "functions": functions, "classes": classes}

def generate_snapshot(root_dir: str | Path) -> Dict[str, Any]:
	root = Path(root_dir)
	manifest = generate_manifest(root)
	# Python-only index for now; other languages can be added later
	py_index = index_python_repo(root)
	return {
		"root": str(root),
		"manifest": manifest,
		"code_index": compact_code_index(py_index, root),
	}

if __name__ == "__main__":
	import argparse
	ap = argparse.ArgumentParser()
	ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]))
	default_out = str(Path(__file__).resolve().parents[1] / "data" / "sandbox" / "tmp" / "repo_snapshot.json")
	ap.add_argument("--out", default=default_out)
	args = ap.parse_args()
	shot = generate_snapshot(args.root)
	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)
	outp.write_text(json.dumps(shot, ensure_ascii=False, indent=2), encoding="utf-8")
	print(f"wrote snapshot -> {args.out}")


from __future__ import annotations
import logging

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

from agi_dw.tools.repo_manifest import generate_manifest
from agi_dw.tools.code_index import index_python_repo

def compact_code_index(idx: Dict[str, Any], root: Path) -> Dict[str, Any]:
	"""Create a compact, language-agnostic subset of the code index.

	Currently focuses on Python symbols. Safely caps long lists.
	"""
	max_items = 200
	files = list(idx.get("files", []))[:max_items]
	functions: Dict[str, List[Dict[str, Any]]] = {}
	classes: Dict[str, List[Dict[str, Any]]] = {}
	for fp in files:
		try:
			rel = str(Path(fp).resolve())
			fns = idx.get("functions", {}).get(fp, [])
			cls = idx.get("classes", {}).get(fp, [])
			# Keep only a few key fields
			functions[rel] = [
				{"name": it.get("name"), "qualname": it.get("qualname"), "lineno": it.get("lineno")}
				for it in (fns or [])
			][:max_items]
			classes[rel] = [
				{"name": it.get("name"), "lineno": it.get("lineno"), "bases": it.get("bases")}
				for it in (cls or [])
			][:max_items]
		except Exception:
			continue
	return {"files": files, "functions": functions, "classes": classes}

def generate_snapshot(root_dir: str | Path) -> Dict[str, Any]:
	root = Path(root_dir)
	manifest = generate_manifest(root)
	# Python-only index for now; other languages can be added later
	py_index = index_python_repo(root)
	return {
		"root": str(root),
		"manifest": manifest,
		"code_index": compact_code_index(py_index, root),
	}

if __name__ == "__main__":
	import argparse
	ap = argparse.ArgumentParser()
	ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]))
	default_out = str(Path(__file__).resolve().parents[1] / "data" / "sandbox" / "tmp" / "repo_snapshot.json")
	ap.add_argument("--out", default=default_out)


	classes: Dict[str, List[Dict[str, Any]]] = {}
	for fp in files:
		try:
			rel = str(Path(fp).resolve())
			fns = idx.get("functions", {}).get(fp, [])
			cls = idx.get("classes", {}).get(fp, [])
			# Keep only a few key fields
			functions[rel] = [
				{"name": it.get("name"), "qualname": it.get("qualname"), "lineno": it.get("lineno")}
				for it in (fns or [])
			][:max_items]
			classes[rel] = [
				{"name": it.get("name"), "lineno": it.get("lineno"), "bases": it.get("bases")}
				for it in (cls or [])
			][:max_items]
		except Exception:
			continue
	return {"files": files, "functions": functions, "classes": classes}

def generate_snapshot(root_dir: str | Path) -> Dict[str, Any]:
	root = Path(root_dir)
	manifest = generate_manifest(root)
	# Python-only index for now; other languages can be added later
	py_index = index_python_repo(root)
	return {
		"root": str(root),
		"manifest": manifest,
		"code_index": compact_code_index(py_index, root),
	}

if __name__ == "__main__":
	import argparse
	ap = argparse.ArgumentParser()
	ap.add_argument("--root", default=str(Path(__file__).resolve().parents[2]))
	default_out = str(Path(__file__).resolve().parents[1] / "data" / "sandbox" / "tmp" / "repo_snapshot.json")
	ap.add_argument("--out", default=default_out)
	args = ap.parse_args()
	shot = generate_snapshot(args.root)
	outp = Path(args.out)
	outp.parent.mkdir(parents=True, exist_ok=True)
	outp.write_text(json.dumps(shot, ensure_ascii=False, indent=2), encoding="utf-8")
	print(f"wrote snapshot -> {args.out}")


from __future__ import annotations
import logging

import argparse
import json
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Iterable, List

@dataclass
class CLIExample:
	goal: str
	cwd: str
	command_args: List[str]
	env_policy: dict[str, Any]

@dataclass
class HITLExample:
	preview: str
	risk: str
	policy: dict[str, Any]
	decision: str

def iter_traces(root: Path) -> Iterable[Path]:
	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:
	pre = load_json(trace_dir / "pre_state.json")
	post = load_json(trace_dir / "post_state.json")
	cmd_args = pre.get("pytest_args") or []
	return [CLIExample(
		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},
	)]

def normalize_hitl(trace_dir: Path) -> list[HITLExample]:
	post = load_json(trace_dir / "post_state.json")
	rc = int(post.get("result", {}).get("returncode", 1))
	decision = "approve" if rc == 0 else "deny"
	preview = "\n".join(post.get("stdout_tail", [])[-10:])
	return [HITLExample(
		preview=preview,
		risk="low" if rc == 0 else "medium",
		policy={"auto_approve_on_green": True},
		decision=decision,
	)]

def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for r in rows:
			f.write(json.dumps(r, ensure_ascii=False))
			f.write("\n")

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Normalize traces into SFT datasets")
	parser.add_argument("--traces", default="/data/agiattempt/agi_dw/data/traces", help="Traces root directory")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sft", help="Output directory for jsonl files")
	args = parser.parse_args(argv)

	traces_root = Path(args.traces)
	out_root = Path(args.out)

	cli_rows: list[dict[str, Any]] = []
	hitl_rows: list[dict[str, Any]] = []

	for trace in iter_traces(traces_root):
		try:
			for ex in normalize_cli(trace):
				cli_rows.append(asdict(ex))
			for ex in normalize_hitl(trace):
				hitl_rows.append(asdict(ex))
		except Exception:
			# Skip malformed traces
			continue

	write_jsonl(out_root / "cli.jsonl", cli_rows)
	write_jsonl(out_root / "hitl.jsonl", hitl_rows)

	# Stubs for plan/patch until integrated
	write_jsonl(out_root / "plan.jsonl", [])
	write_jsonl(out_root / "patch.jsonl", [])
	return 0

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


CLIExample

from __future__ import annotations
import logging

import argparse
import json
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Iterable, List

@dataclass
class CLIExample:
	goal: str
	cwd: str
	command_args: List[str]
	env_policy: dict[str, Any]

@dataclass
class HITLExample:
	preview: str
	risk: str
	policy: dict[str, Any]
	decision: str

def iter_traces(root: Path) -> Iterable[Path]:
	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:


HITLExample

from __future__ import annotations
import logging

import argparse
import json
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Iterable, List

@dataclass
class CLIExample:
	goal: str
	cwd: str
	command_args: List[str]
	env_policy: dict[str, Any]

@dataclass
class HITLExample:
	preview: str
	risk: str
	policy: dict[str, Any]
	decision: str

def iter_traces(root: Path) -> Iterable[Path]:
	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:
	pre = load_json(trace_dir / "pre_state.json")
	post = load_json(trace_dir / "post_state.json")
	cmd_args = pre.get("pytest_args") or []
	return [CLIExample(
		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},


from pathlib import Path
from typing import Any, Iterable, List

@dataclass
class CLIExample:
	goal: str
	cwd: str
	command_args: List[str]
	env_policy: dict[str, Any]

@dataclass
class HITLExample:
	preview: str
	risk: str
	policy: dict[str, Any]
	decision: str

def iter_traces(root: Path) -> Iterable[Path]:
	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:
	pre = load_json(trace_dir / "pre_state.json")
	post = load_json(trace_dir / "post_state.json")
	cmd_args = pre.get("pytest_args") or []
	return [CLIExample(
		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},
	)]

def normalize_hitl(trace_dir: Path) -> list[HITLExample]:
	post = load_json(trace_dir / "post_state.json")


class CLIExample:
	goal: str
	cwd: str
	command_args: List[str]
	env_policy: dict[str, Any]

@dataclass
class HITLExample:
	preview: str
	risk: str
	policy: dict[str, Any]
	decision: str

def iter_traces(root: Path) -> Iterable[Path]:
	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:
	pre = load_json(trace_dir / "pre_state.json")
	post = load_json(trace_dir / "post_state.json")
	cmd_args = pre.get("pytest_args") or []
	return [CLIExample(
		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},
	)]

def normalize_hitl(trace_dir: Path) -> list[HITLExample]:
	post = load_json(trace_dir / "post_state.json")
	rc = int(post.get("result", {}).get("returncode", 1))
	decision = "approve" if rc == 0 else "deny"
	preview = "\n".join(post.get("stdout_tail", [])[-10:])
	return [HITLExample(


	env_policy: dict[str, Any]

@dataclass
class HITLExample:
	preview: str
	risk: str
	policy: dict[str, Any]
	decision: str

def iter_traces(root: Path) -> Iterable[Path]:
	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:
	pre = load_json(trace_dir / "pre_state.json")
	post = load_json(trace_dir / "post_state.json")
	cmd_args = pre.get("pytest_args") or []
	return [CLIExample(
		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},
	)]

def normalize_hitl(trace_dir: Path) -> list[HITLExample]:
	post = load_json(trace_dir / "post_state.json")
	rc = int(post.get("result", {}).get("returncode", 1))
	decision = "approve" if rc == 0 else "deny"
	preview = "\n".join(post.get("stdout_tail", [])[-10:])
	return [HITLExample(
		preview=preview,
		risk="low" if rc == 0 else "medium",
		policy={"auto_approve_on_green": True},
		decision=decision,
	)]

def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for r in rows:
			f.write(json.dumps(r, ensure_ascii=False))


	for p in sorted(root.glob("*/pre_state.json")):
		yield p.parent

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

def normalize_cli(trace_dir: Path) -> list[CLIExample]:
	pre = load_json(trace_dir / "pre_state.json")
	post = load_json(trace_dir / "post_state.json")
	cmd_args = pre.get("pytest_args") or []
	return [CLIExample(
		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},
	)]

def normalize_hitl(trace_dir: Path) -> list[HITLExample]:
	post = load_json(trace_dir / "post_state.json")
	rc = int(post.get("result", {}).get("returncode", 1))
	decision = "approve" if rc == 0 else "deny"
	preview = "\n".join(post.get("stdout_tail", [])[-10:])
	return [HITLExample(
		preview=preview,
		risk="low" if rc == 0 else "medium",
		policy={"auto_approve_on_green": True},
		decision=decision,
	)]

def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for r in rows:
			f.write(json.dumps(r, ensure_ascii=False))
			f.write("\n")

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Normalize traces into SFT datasets")
	parser.add_argument("--traces", default="/data/agiattempt/agi_dw/data/traces", help="Traces root directory")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sft", help="Output directory for jsonl files")
	args = parser.parse_args(argv)

	traces_root = Path(args.traces)
	out_root = Path(args.out)

	cli_rows: list[dict[str, Any]] = []


		goal=f"Run pytest for repo {pre.get('repo')}",
		cwd=pre.get("repo", ""),
		command_args=["-m", "pytest", *cmd_args] if cmd_args else ["-m", "pytest"],
		env_policy={"allow": ["SEED_FAIL"], "deny": []},
	)]

def normalize_hitl(trace_dir: Path) -> list[HITLExample]:
	post = load_json(trace_dir / "post_state.json")
	rc = int(post.get("result", {}).get("returncode", 1))
	decision = "approve" if rc == 0 else "deny"
	preview = "\n".join(post.get("stdout_tail", [])[-10:])
	return [HITLExample(
		preview=preview,
		risk="low" if rc == 0 else "medium",
		policy={"auto_approve_on_green": True},
		decision=decision,
	)]

def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for r in rows:
			f.write(json.dumps(r, ensure_ascii=False))
			f.write("\n")

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Normalize traces into SFT datasets")
	parser.add_argument("--traces", default="/data/agiattempt/agi_dw/data/traces", help="Traces root directory")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sft", help="Output directory for jsonl files")
	args = parser.parse_args(argv)

	traces_root = Path(args.traces)
	out_root = Path(args.out)

	cli_rows: list[dict[str, Any]] = []
	hitl_rows: list[dict[str, Any]] = []

	for trace in iter_traces(traces_root):
		try:
			for ex in normalize_cli(trace):
				cli_rows.append(asdict(ex))
			for ex in normalize_hitl(trace):
				hitl_rows.append(asdict(ex))


main

	post = load_json(trace_dir / "post_state.json")
	rc = int(post.get("result", {}).get("returncode", 1))
	decision = "approve" if rc == 0 else "deny"
	preview = "\n".join(post.get("stdout_tail", [])[-10:])
	return [HITLExample(
		preview=preview,
		risk="low" if rc == 0 else "medium",
		policy={"auto_approve_on_green": True},
		decision=decision,
	)]

def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
	path.parent.mkdir(parents=True, exist_ok=True)
	with path.open("w", encoding="utf-8") as f:
		for r in rows:
			f.write(json.dumps(r, ensure_ascii=False))
			f.write("\n")

def main(argv: list[str] | None = None) -> int:
	parser = argparse.ArgumentParser(description="Normalize traces into SFT datasets")
	parser.add_argument("--traces", default="/data/agiattempt/agi_dw/data/traces", help="Traces root directory")
	parser.add_argument("--out", default="/data/agiattempt/agi_dw/data/sft", help="Output directory for jsonl files")
	args = parser.parse_args(argv)

	traces_root = Path(args.traces)
	out_root = Path(args.out)

	cli_rows: list[dict[str, Any]] = []
	hitl_rows: list[dict[str, Any]] = []

	for trace in iter_traces(traces_root):
		try:
			for ex in normalize_cli(trace):
				cli_rows.append(asdict(ex))
			for ex in normalize_hitl(trace):
				hitl_rows.append(asdict(ex))
		except Exception:
			# Skip malformed traces
			continue

	write_jsonl(out_root / "cli.jsonl", cli_rows)
	write_jsonl(out_root / "hitl.jsonl", hitl_rows)

	# Stubs for plan/patch until integrated
	write_jsonl(out_root / "plan.jsonl", [])
	write_jsonl(out_root / "patch.jsonl", [])
	return 0

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


from __future__ import annotations
import logging

import os
import hashlib
from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta

def _cache_root() -> Path:
	try:
		return Path(__file__).resolve().parents[1] / "data" / "cache"
	except Exception:
		return Path.cwd() / "data" / "cache"

def is_enabled(env_var: str = "AGI_ARTIFACT_CACHE") -> bool:
	try:
		v = os.environ.get(env_var, "0").strip()
		return v in ("1", "true", "True")
	except Exception:
		return False

def _now() -> datetime:
	try:
		return datetime.utcnow()
	except Exception:
		return datetime.fromtimestamp(0)

def _expired(p: Path, ttl_sec: int) -> bool:
	if ttl_sec <= 0:
		return True
	try:
		mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
	except Exception:
		return True
	return _now() - mtime > timedelta(seconds=int(ttl_sec))

def _safe_key(parts: list[str]) -> str:
	h = hashlib.sha256()
	for part in parts:
		try:
			h.update(part.encode("utf-8", errors="ignore"))
		except Exception:
			continue
		h.update(b"\x1f")
	return h.hexdigest()

def get_cached_text(category: str, key_parts: list[str], ttl_sec: int) -> Optional[str]:
	"""Return cached text if present and not expired; else None."""
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		if fp.exists() and not _expired(fp, int(ttl_sec)):
			return fp.read_text(encoding="utf-8", errors="ignore")
		return None
	except Exception:
		return None

def set_cached_text(category: str, key_parts: list[str], value: str) -> Optional[Path]:
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		fp.write_text(value, encoding="utf-8")
		return fp
	except Exception:
		return None


from __future__ import annotations
import logging

import os
import hashlib
from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta

def _cache_root() -> Path:
	try:
		return Path(__file__).resolve().parents[1] / "data" / "cache"
	except Exception:
		return Path.cwd() / "data" / "cache"

def is_enabled(env_var: str = "AGI_ARTIFACT_CACHE") -> bool:
	try:
		v = os.environ.get(env_var, "0").strip()
		return v in ("1", "true", "True")
	except Exception:
		return False

def _now() -> datetime:
	try:
		return datetime.utcnow()
	except Exception:
		return datetime.fromtimestamp(0)

def _expired(p: Path, ttl_sec: int) -> bool:
	if ttl_sec <= 0:
		return True


from __future__ import annotations
import logging

import os
import hashlib
from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta

def _cache_root() -> Path:
	try:
		return Path(__file__).resolve().parents[1] / "data" / "cache"
	except Exception:
		return Path.cwd() / "data" / "cache"

def is_enabled(env_var: str = "AGI_ARTIFACT_CACHE") -> bool:
	try:
		v = os.environ.get(env_var, "0").strip()
		return v in ("1", "true", "True")
	except Exception:
		return False

def _now() -> datetime:
	try:
		return datetime.utcnow()
	except Exception:
		return datetime.fromtimestamp(0)

def _expired(p: Path, ttl_sec: int) -> bool:
	if ttl_sec <= 0:
		return True
	try:
		mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
	except Exception:
		return True
	return _now() - mtime > timedelta(seconds=int(ttl_sec))

def _safe_key(parts: list[str]) -> str:


from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta

def _cache_root() -> Path:
	try:
		return Path(__file__).resolve().parents[1] / "data" / "cache"
	except Exception:
		return Path.cwd() / "data" / "cache"

def is_enabled(env_var: str = "AGI_ARTIFACT_CACHE") -> bool:
	try:
		v = os.environ.get(env_var, "0").strip()
		return v in ("1", "true", "True")
	except Exception:
		return False

def _now() -> datetime:
	try:
		return datetime.utcnow()
	except Exception:
		return datetime.fromtimestamp(0)

def _expired(p: Path, ttl_sec: int) -> bool:
	if ttl_sec <= 0:
		return True
	try:
		mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
	except Exception:
		return True
	return _now() - mtime > timedelta(seconds=int(ttl_sec))

def _safe_key(parts: list[str]) -> str:
	h = hashlib.sha256()
	for part in parts:
		try:
			h.update(part.encode("utf-8", errors="ignore"))
		except Exception:
			continue
		h.update(b"\x1f")


		return Path(__file__).resolve().parents[1] / "data" / "cache"
	except Exception:
		return Path.cwd() / "data" / "cache"

def is_enabled(env_var: str = "AGI_ARTIFACT_CACHE") -> bool:
	try:
		v = os.environ.get(env_var, "0").strip()
		return v in ("1", "true", "True")
	except Exception:
		return False

def _now() -> datetime:
	try:
		return datetime.utcnow()
	except Exception:
		return datetime.fromtimestamp(0)

def _expired(p: Path, ttl_sec: int) -> bool:
	if ttl_sec <= 0:
		return True
	try:
		mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
	except Exception:
		return True
	return _now() - mtime > timedelta(seconds=int(ttl_sec))

def _safe_key(parts: list[str]) -> str:
	h = hashlib.sha256()
	for part in parts:
		try:
			h.update(part.encode("utf-8", errors="ignore"))
		except Exception:
			continue
		h.update(b"\x1f")
	return h.hexdigest()

def get_cached_text(category: str, key_parts: list[str], ttl_sec: int) -> Optional[str]:
	"""Return cached text if present and not expired; else None."""
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname


		return False

def _now() -> datetime:
	try:
		return datetime.utcnow()
	except Exception:
		return datetime.fromtimestamp(0)

def _expired(p: Path, ttl_sec: int) -> bool:
	if ttl_sec <= 0:
		return True
	try:
		mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
	except Exception:
		return True
	return _now() - mtime > timedelta(seconds=int(ttl_sec))

def _safe_key(parts: list[str]) -> str:
	h = hashlib.sha256()
	for part in parts:
		try:
			h.update(part.encode("utf-8", errors="ignore"))
		except Exception:
			continue
		h.update(b"\x1f")
	return h.hexdigest()

def get_cached_text(category: str, key_parts: list[str], ttl_sec: int) -> Optional[str]:
	"""Return cached text if present and not expired; else None."""
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		if fp.exists() and not _expired(fp, int(ttl_sec)):
			return fp.read_text(encoding="utf-8", errors="ignore")
		return None
	except Exception:
		return None

def set_cached_text(category: str, key_parts: list[str], value: str) -> Optional[Path]:
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)


	if ttl_sec <= 0:
		return True
	try:
		mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
	except Exception:
		return True
	return _now() - mtime > timedelta(seconds=int(ttl_sec))

def _safe_key(parts: list[str]) -> str:
	h = hashlib.sha256()
	for part in parts:
		try:
			h.update(part.encode("utf-8", errors="ignore"))
		except Exception:
			continue
		h.update(b"\x1f")
	return h.hexdigest()

def get_cached_text(category: str, key_parts: list[str], ttl_sec: int) -> Optional[str]:
	"""Return cached text if present and not expired; else None."""
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		if fp.exists() and not _expired(fp, int(ttl_sec)):
			return fp.read_text(encoding="utf-8", errors="ignore")
		return None
	except Exception:
		return None

def set_cached_text(category: str, key_parts: list[str], value: str) -> Optional[Path]:
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		fp.write_text(value, encoding="utf-8")
		return fp
	except Exception:
		return None


		except Exception:
			continue
		h.update(b"\x1f")
	return h.hexdigest()

def get_cached_text(category: str, key_parts: list[str], ttl_sec: int) -> Optional[str]:
	"""Return cached text if present and not expired; else None."""
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		if fp.exists() and not _expired(fp, int(ttl_sec)):
			return fp.read_text(encoding="utf-8", errors="ignore")
		return None
	except Exception:
		return None

def set_cached_text(category: str, key_parts: list[str], value: str) -> Optional[Path]:
	try:
		root = _cache_root() / category
		root.mkdir(parents=True, exist_ok=True)
		fname = _safe_key(key_parts) + ".txt"
		fp = root / fname
		fp.write_text(value, encoding="utf-8")
		return fp
	except Exception:
		return None


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Optional

class GitTool:
	"""Minimal git adapter with safe cwd and simple commands."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)
		self.cwd.mkdir(parents=True, exist_ok=True)

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:
		"""Clean untracked files. By default remove untracked files and directories (git clean -fd)."""
		args = ["clean"]
		if directories:
			args.append("-d")
		if force:
			args.append("-f")
		return self._run(*args)

	def init(self) -> subprocess.CompletedProcess:
		"""Initialize a git repository."""
		return self._run("init")

	def get_current_branch(self) -> subprocess.CompletedProcess:
		"""Get the current branch name."""
		return self._run("branch", "--show-current")

	def checkout_branch(self, branch: str, create: bool = False) -> subprocess.CompletedProcess:
		"""Checkout a branch, optionally creating it."""
		args = ["checkout"]
		if create:
			args.extend(["-b", branch])
		else:
			args.append(branch)
		return self._run(*args)


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Optional

class GitTool:
	"""Minimal git adapter with safe cwd and simple commands."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)
		self.cwd.mkdir(parents=True, exist_ok=True)

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")


from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Optional

class GitTool:
	"""Minimal git adapter with safe cwd and simple commands."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)
		self.cwd.mkdir(parents=True, exist_ok=True)

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)


clone

from __future__ import annotations
import logging

import subprocess
from pathlib import Path
from typing import Optional

class GitTool:
	"""Minimal git adapter with safe cwd and simple commands."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)
		self.cwd.mkdir(parents=True, exist_ok=True)

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]


checkout

from pathlib import Path
from typing import Optional

class GitTool:
	"""Minimal git adapter with safe cwd and simple commands."""

	def __init__(self, cwd: str) -> None:
		self.cwd = Path(cwd)
		self.cwd.mkdir(parents=True, exist_ok=True)

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)


status

		self.cwd = Path(cwd)
		self.cwd.mkdir(parents=True, exist_ok=True)

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""


add

	def _run(self, *args: str, timeout: int = 120) -> subprocess.CompletedProcess:
		return subprocess.run(["git", *args], cwd=str(self.cwd), capture_output=True, text=True, timeout=timeout)

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:


commit

	def clone(self, repo: str, dest: Optional[str] = None) -> subprocess.CompletedProcess:
		args = ["clone", repo]
		if dest:
			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:
		"""Clean untracked files. By default remove untracked files and directories (git clean -fd)."""
		args = ["clean"]
		if directories:


			args.append(dest)
		return self._run(*args)

	def checkout(self, ref: str, new_branch: bool = False) -> subprocess.CompletedProcess:
		args = ["checkout"]
		if new_branch:
			args.extend(["-b", ref])
		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:
		"""Clean untracked files. By default remove untracked files and directories (git clean -fd)."""
		args = ["clean"]
		if directories:
			args.append("-d")
		if force:
			args.append("-f")
		return self._run(*args)

	def init(self) -> subprocess.CompletedProcess:
		"""Initialize a git repository."""


		else:
			args.append(ref)
		return self._run(*args)

	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:
		"""Clean untracked files. By default remove untracked files and directories (git clean -fd)."""
		args = ["clean"]
		if directories:
			args.append("-d")
		if force:
			args.append("-f")
		return self._run(*args)

	def init(self) -> subprocess.CompletedProcess:
		"""Initialize a git repository."""
		return self._run("init")

	def get_current_branch(self) -> subprocess.CompletedProcess:
		"""Get the current branch name."""


	def status(self) -> subprocess.CompletedProcess:
		return self._run("status", "--porcelain=v1")

	def add(self, pathspec: str = ".") -> subprocess.CompletedProcess:
		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:
		"""Clean untracked files. By default remove untracked files and directories (git clean -fd)."""
		args = ["clean"]
		if directories:
			args.append("-d")
		if force:
			args.append("-f")
		return self._run(*args)

	def init(self) -> subprocess.CompletedProcess:
		"""Initialize a git repository."""
		return self._run("init")

	def get_current_branch(self) -> subprocess.CompletedProcess:
		"""Get the current branch name."""
		return self._run("branch", "--show-current")

	def checkout_branch(self, branch: str, create: bool = False) -> subprocess.CompletedProcess:
		"""Checkout a branch, optionally creating it."""


clean

		return self._run("add", pathspec)

	def commit(self, message: str) -> subprocess.CompletedProcess:
		return self._run("commit", "-m", message)

	def apply_patch(self, patch_path: str, check: bool = True) -> subprocess.CompletedProcess:
		args = ["apply"]
		if check:
			args.append("--check")
		args.append(patch_path)
		return self._run(*args)

	def apply_reverse_patch(self, patch_path: str) -> subprocess.CompletedProcess:
		"""Reverse apply (undo) a patch: git apply -R <patch>."""
		return self._run("apply", "-R", patch_path)

	def reset_hard(self, ref: str = "HEAD") -> subprocess.CompletedProcess:
		"""Reset index and working tree to ref, discarding changes."""
		return self._run("reset", "--hard", ref)

	def clean(self, directories: bool = True, force: bool = True) -> subprocess.CompletedProcess:
		"""Clean untracked files. By default remove untracked files and directories (git clean -fd)."""
		args = ["clean"]
		if directories:
			args.append("-d")
		if force:
			args.append("-f")
		return self._run(*args)

	def init(self) -> subprocess.CompletedProcess:
		"""Initialize a git repository."""
		return self._run("init")

	def get_current_branch(self) -> subprocess.CompletedProcess:
		"""Get the current branch name."""
		return self._run("branch", "--show-current")

	def checkout_branch(self, branch: str, create: bool = False) -> subprocess.CompletedProcess:
		"""Checkout a branch, optionally creating it."""
		args = ["checkout"]
		if create:
			args.extend(["-b", branch])
		else:
			args.append(branch)
		return self._run(*args)