#!/usr/bin/env python3
import os
from pathlib import Path
import git

def is_git_repo(path):
    """Check if a directory is a git repository."""
    try:
        git.Repo(path)
        return True
    except git.exc.InvalidGitRepositoryError:
        return False

def list_repos(inspiration_dir, output_file="inspiration_repos.txt"):
    """List all git repositories in the inspiration directory and save to a file."""
    inspiration_path = Path(inspiration_dir)

    # Prepare the output lines
    output_lines = []

    if not inspiration_path.exists():
        output_lines.append(f"Error: Directory '{inspiration_dir}' does not exist")
        return output_lines

    if not inspiration_path.is_dir():
        output_lines.append(f"Error: '{inspiration_dir}' is not a directory")
        return output_lines

    output_lines.append(f"Repositories in {inspiration_dir}:\n")

    repos_found = False
    for item in inspiration_path.iterdir():
        if item.is_dir() and is_git_repo(item):
            repos_found = True
            try:
                repo = git.Repo(item)
                remote_url = next((remote.url for remote in repo.remotes), "No remote URL found")
                output_lines.append(f"Repository: {item.name}")
                output_lines.append(f"Remote URL: {remote_url}")
                output_lines.append("-" * 50)
            except Exception as e:
                output_lines.append(f"Error reading repository {item.name}: {e}")
                output_lines.append("-" * 50)

    if not repos_found:
        output_lines.append("No git repositories found in the inspiration directory.")

    # Write to file
    with open(output_file, 'w') as f:
        f.write('\n'.join(output_lines))

    # Also print to console
    for line in output_lines:
        print(line)

    return output_lines

if __name__ == "__main__":
    inspiration_dir = "/data/agiattempt/inspiration"
    list_repos(inspiration_dir)


#!/usr/bin/env python3
import os
from pathlib import Path
import git

def is_git_repo(path):
    """Check if a directory is a git repository."""
    try:
        git.Repo(path)
        return True
    except git.exc.InvalidGitRepositoryError:
        return False

def list_repos(inspiration_dir, output_file="inspiration_repos.txt"):
    """List all git repositories in the inspiration directory and save to a file."""
    inspiration_path = Path(inspiration_dir)

    # Prepare the output lines
    output_lines = []

    if not inspiration_path.exists():
        output_lines.append(f"Error: Directory '{inspiration_dir}' does not exist")
        return output_lines

    if not inspiration_path.is_dir():
        output_lines.append(f"Error: '{inspiration_dir}' is not a directory")
        return output_lines

    output_lines.append(f"Repositories in {inspiration_dir}:\n")

    repos_found = False
    for item in inspiration_path.iterdir():


downloadarxiv

#!/usr/bin/env python3
import argparse
import os
import time
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup

LIST_URL = "https://arxiv.org/list/cs/recent"

def get_all_page(url: str) -> str:
    resp = requests.get(url, timeout=30)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    all_link = None
    for a in soup.select("a"):
        if a.get_text(strip=True).lower() == "all" and a.get("href"):
            all_link = urljoin(url, a["href"])
            break
    # Fallback to show=5000 if "all" not found
    return all_link or (LIST_URL + "?show=5000")

def extract_pdf_links(list_url: str) -> list[str]:
    resp = requests.get(list_url, timeout=60, headers={"User-Agent": "arXiv-CS-downloader/1.0"})
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    pdf_paths = set()
    for a in soup.select('a[href^="/pdf/"]'):
        href = a["href"]
        # Normalize to end with .pdf (some links are /pdf/XXXX)
        if not href.endswith(".pdf"):
            href = href.rstrip("/") + ".pdf"
        pdf_paths.add(urljoin(list_url, href))
    return sorted(pdf_paths)

def filename_from_url(pdf_url: str) -> str:
    name = os.path.basename(urlparse(pdf_url).path)
    return name if name.endswith(".pdf") else (name + ".pdf")

def main():
    parser = argparse.ArgumentParser(description="Download all PDFs from arXiv CS recent listing.")
    parser.add_argument("-o", "--outdir", default="arxiv_cs_recent_pdfs", help="Output directory")
    parser.add_argument("--delay", type=float, default=0.5, help="Delay between downloads (seconds)")
    parser.add_argument("--max", type=int, default=0, help="Max PDFs to download (0 = no limit)")
    args = parser.parse_args()

    os.makedirs(args.outdir, exist_ok=True)

    list_url = get_all_page(LIST_URL)
    pdf_urls = extract_pdf_links(list_url)
    if args.max > 0:
        pdf_urls = pdf_urls[:args.max]

    print(f"Found {len(pdf_urls)} PDFs. Downloading to: {args.outdir}")
    session = requests.Session()
    session.headers.update({"User-Agent": "arXiv-CS-downloader/1.0"})

    for idx, pdf_url in enumerate(pdf_urls, 1):
        fname = filename_from_url(pdf_url)
        dest = os.path.join(args.outdir, fname)
        if os.path.exists(dest):
            print(f"[{idx}/{len(pdf_urls)}] Skip (exists): {fname}")
            continue
        try:
            with session.get(pdf_url, stream=True, timeout=120) as r:
                r.raise_for_status()
                with open(dest, "wb") as f:
                    for chunk in r.iter_content(chunk_size=1024 * 64):
                        if chunk:
                            f.write(chunk)
            print(f"[{idx}/{len(pdf_urls)}] OK: {fname}")
        except Exception as e:
            print(f"[{idx}/{len(pdf_urls)}] FAIL: {fname} ({e})")
        time.sleep(args.delay)

if __name__ == "__main__":
    main()


#!/usr/bin/env python3
import argparse
import os
import time
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup

LIST_URL = "https://arxiv.org/list/cs/recent"

def get_all_page(url: str) -> str:
    resp = requests.get(url, timeout=30)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    all_link = None
    for a in soup.select("a"):
        if a.get_text(strip=True).lower() == "all" and a.get("href"):
            all_link = urljoin(url, a["href"])
            break
    # Fallback to show=5000 if "all" not found
    return all_link or (LIST_URL + "?show=5000")

def extract_pdf_links(list_url: str) -> list[str]:
    resp = requests.get(list_url, timeout=60, headers={"User-Agent": "arXiv-CS-downloader/1.0"})
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    pdf_paths = set()
    for a in soup.select('a[href^="/pdf/"]'):
        href = a["href"]
        # Normalize to end with .pdf (some links are /pdf/XXXX)
        if not href.endswith(".pdf"):
            href = href.rstrip("/") + ".pdf"
        pdf_paths.add(urljoin(list_url, href))
    return sorted(pdf_paths)

def filename_from_url(pdf_url: str) -> str:
    name = os.path.basename(urlparse(pdf_url).path)
    return name if name.endswith(".pdf") else (name + ".pdf")

def main():
    parser = argparse.ArgumentParser(description="Download all PDFs from arXiv CS recent listing.")


import time
from urllib.parse import urljoin, urlparse

import requests
from bs4 import BeautifulSoup

LIST_URL = "https://arxiv.org/list/cs/recent"

def get_all_page(url: str) -> str:
    resp = requests.get(url, timeout=30)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    all_link = None
    for a in soup.select("a"):
        if a.get_text(strip=True).lower() == "all" and a.get("href"):
            all_link = urljoin(url, a["href"])
            break
    # Fallback to show=5000 if "all" not found
    return all_link or (LIST_URL + "?show=5000")

def extract_pdf_links(list_url: str) -> list[str]:
    resp = requests.get(list_url, timeout=60, headers={"User-Agent": "arXiv-CS-downloader/1.0"})
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    pdf_paths = set()
    for a in soup.select('a[href^="/pdf/"]'):
        href = a["href"]
        # Normalize to end with .pdf (some links are /pdf/XXXX)
        if not href.endswith(".pdf"):
            href = href.rstrip("/") + ".pdf"
        pdf_paths.add(urljoin(list_url, href))
    return sorted(pdf_paths)

def filename_from_url(pdf_url: str) -> str:
    name = os.path.basename(urlparse(pdf_url).path)
    return name if name.endswith(".pdf") else (name + ".pdf")

def main():
    parser = argparse.ArgumentParser(description="Download all PDFs from arXiv CS recent listing.")
    parser.add_argument("-o", "--outdir", default="arxiv_cs_recent_pdfs", help="Output directory")
    parser.add_argument("--delay", type=float, default=0.5, help="Delay between downloads (seconds)")
    parser.add_argument("--max", type=int, default=0, help="Max PDFs to download (0 = no limit)")
    args = parser.parse_args()

    os.makedirs(args.outdir, exist_ok=True)

    list_url = get_all_page(LIST_URL)
    pdf_urls = extract_pdf_links(list_url)
    if args.max > 0:
        pdf_urls = pdf_urls[:args.max]

    print(f"Found {len(pdf_urls)} PDFs. Downloading to: {args.outdir}")


    for a in soup.select("a"):
        if a.get_text(strip=True).lower() == "all" and a.get("href"):
            all_link = urljoin(url, a["href"])
            break
    # Fallback to show=5000 if "all" not found
    return all_link or (LIST_URL + "?show=5000")

def extract_pdf_links(list_url: str) -> list[str]:
    resp = requests.get(list_url, timeout=60, headers={"User-Agent": "arXiv-CS-downloader/1.0"})
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    pdf_paths = set()
    for a in soup.select('a[href^="/pdf/"]'):
        href = a["href"]
        # Normalize to end with .pdf (some links are /pdf/XXXX)
        if not href.endswith(".pdf"):
            href = href.rstrip("/") + ".pdf"
        pdf_paths.add(urljoin(list_url, href))
    return sorted(pdf_paths)

def filename_from_url(pdf_url: str) -> str:
    name = os.path.basename(urlparse(pdf_url).path)
    return name if name.endswith(".pdf") else (name + ".pdf")

def main():
    parser = argparse.ArgumentParser(description="Download all PDFs from arXiv CS recent listing.")
    parser.add_argument("-o", "--outdir", default="arxiv_cs_recent_pdfs", help="Output directory")
    parser.add_argument("--delay", type=float, default=0.5, help="Delay between downloads (seconds)")
    parser.add_argument("--max", type=int, default=0, help="Max PDFs to download (0 = no limit)")
    args = parser.parse_args()

    os.makedirs(args.outdir, exist_ok=True)

    list_url = get_all_page(LIST_URL)
    pdf_urls = extract_pdf_links(list_url)
    if args.max > 0:
        pdf_urls = pdf_urls[:args.max]

    print(f"Found {len(pdf_urls)} PDFs. Downloading to: {args.outdir}")
    session = requests.Session()
    session.headers.update({"User-Agent": "arXiv-CS-downloader/1.0"})

    for idx, pdf_url in enumerate(pdf_urls, 1):


main

    # Fallback to show=5000 if "all" not found
    return all_link or (LIST_URL + "?show=5000")

def extract_pdf_links(list_url: str) -> list[str]:
    resp = requests.get(list_url, timeout=60, headers={"User-Agent": "arXiv-CS-downloader/1.0"})
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    pdf_paths = set()
    for a in soup.select('a[href^="/pdf/"]'):
        href = a["href"]
        # Normalize to end with .pdf (some links are /pdf/XXXX)
        if not href.endswith(".pdf"):
            href = href.rstrip("/") + ".pdf"
        pdf_paths.add(urljoin(list_url, href))
    return sorted(pdf_paths)

def filename_from_url(pdf_url: str) -> str:
    name = os.path.basename(urlparse(pdf_url).path)
    return name if name.endswith(".pdf") else (name + ".pdf")

def main():
    parser = argparse.ArgumentParser(description="Download all PDFs from arXiv CS recent listing.")
    parser.add_argument("-o", "--outdir", default="arxiv_cs_recent_pdfs", help="Output directory")
    parser.add_argument("--delay", type=float, default=0.5, help="Delay between downloads (seconds)")
    parser.add_argument("--max", type=int, default=0, help="Max PDFs to download (0 = no limit)")
    args = parser.parse_args()

    os.makedirs(args.outdir, exist_ok=True)

    list_url = get_all_page(LIST_URL)
    pdf_urls = extract_pdf_links(list_url)
    if args.max > 0:
        pdf_urls = pdf_urls[:args.max]

    print(f"Found {len(pdf_urls)} PDFs. Downloading to: {args.outdir}")
    session = requests.Session()
    session.headers.update({"User-Agent": "arXiv-CS-downloader/1.0"})

    for idx, pdf_url in enumerate(pdf_urls, 1):
        fname = filename_from_url(pdf_url)
        dest = os.path.join(args.outdir, fname)
        if os.path.exists(dest):
            print(f"[{idx}/{len(pdf_urls)}] Skip (exists): {fname}")
            continue
        try:
            with session.get(pdf_url, stream=True, timeout=120) as r:
                r.raise_for_status()
                with open(dest, "wb") as f:
                    for chunk in r.iter_content(chunk_size=1024 * 64):
                        if chunk:
                            f.write(chunk)
            print(f"[{idx}/{len(pdf_urls)}] OK: {fname}")
        except Exception as e:
            print(f"[{idx}/{len(pdf_urls)}] FAIL: {fname} ({e})")
        time.sleep(args.delay)

if __name__ == "__main__":
    main()


#!/usr/bin/env python3
from __future__ import annotations

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

DEFAULT_EXCLUDES = [
    ".git",
    "__pycache__",
    ".mypy_cache",
    ".pytest_cache",
    "node_modules",
    "agi_dw.egg-info",
    "venv",
    ".venv",
    "env",
    "build",
    "dist",
    "data",
    "third_party",
    "data/bench/tmp",
    "data/bench/cache",
    "data/llm_bench",
    "data/sandbox/tmp",
]

def should_skip_dir(dirpath: Path, exclude_roots: List[str], root: Path) -> bool:
    # Compare against path relative to scan root to avoid matching system paths (e.g. "/data/...")
    try:
        rel = dirpath.resolve().relative_to(root.resolve())
        rel_str = str(rel).replace("\\", "/").lstrip("./")
    except Exception:
        # Fallback to directory name if relative path cannot be determined
        rel_str = dirpath.name

    dir_name = dirpath.name
    for ex in exclude_roots:
        ex_norm = ex.strip().strip("/").replace("\\", "/")
        if not ex_norm:
            continue
        # If the exclude is a nested path (e.g. "data/bench/tmp"), match by prefix on the relative path
        if "/" in ex_norm:
            if rel_str == ex_norm or rel_str.startswith(ex_norm + "/"):
                return True
        else:
            # Bare directory name exclusion (e.g. "build"): match on current directory name
            if dir_name == ex_norm:
                return True
    return False

def count_file_lines(py_file: Path) -> Tuple[int, int, int]:
    total = 0
    blank = 0
    comment = 0
    try:
        with py_file.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                total += 1
                s = line.strip()
                if not s:
                    blank += 1
                elif s.startswith("#"):
                    comment += 1
    except Exception:
        # Count unreadable files as zero lines, continue
        return 0, 0, 0
    return total, blank, comment

def scan_repo(root: Path, exclude_dirs: List[str]) -> Dict[str, int]:
    totals = {
        "files": 0,
        "total_lines": 0,
        "blank_lines": 0,
        "comment_lines": 0,
        "code_lines": 0,
    }
    # Normalize exclude entries once
    normalized_excludes: List[str] = [s.strip().strip("/").replace("\\", "/") for s in exclude_dirs]

    for dirpath, dirnames, filenames in os.walk(root):
        dpath = Path(dirpath)
        # In-place prune dirnames to avoid descending into excluded directories
        pruned: List[str] = []
        for dn in list(dirnames):
            candidate = dpath / dn
            if should_skip_dir(candidate, normalized_excludes, root):
                pruned.append(dn)
        for dn in pruned:
            try:
                dirnames.remove(dn)
            except ValueError:
                pass

        for fn in filenames:
            if not fn.endswith(".py"):
                continue
            fp = dpath / fn
            t, b, c = count_file_lines(fp)
            if t == 0 and (not fp.exists()):
                continue
            totals["files"] += 1
            totals["total_lines"] += t
            totals["blank_lines"] += b
            totals["comment_lines"] += c
    totals["code_lines"] = totals["total_lines"] - totals["blank_lines"] - totals["comment_lines"]
    return totals

def main() -> int:
    ap = argparse.ArgumentParser(description="Count Python lines of code in the repository.")
    ap.add_argument("--root", default=".", help="Root directory to scan (default: current directory)")
    ap.add_argument(
        "--exclude-dirs",
        default=",".join(DEFAULT_EXCLUDES),
        help="Comma-separated list of directory names or paths to exclude",
    )
    args = ap.parse_args()

    root = Path(args.root).resolve()
    exclude_dirs = [s.strip() for s in str(args.exclude_dirs).split(",") if s.strip()]

    totals = scan_repo(root, exclude_dirs)

    print(
        {
            "root": str(root),
            "files": totals["files"],
            "total_lines": totals["total_lines"],
            "code_lines": totals["code_lines"],
            "comment_lines": totals["comment_lines"],
            "blank_lines": totals["blank_lines"],
            "exclude_dirs": exclude_dirs,
        }
    )
    return 0

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


    ".git",
    "__pycache__",
    ".mypy_cache",
    ".pytest_cache",
    "node_modules",
    "agi_dw.egg-info",
    "venv",
    ".venv",
    "env",
    "build",
    "dist",
    "data",
    "third_party",
    "data/bench/tmp",
    "data/bench/cache",
    "data/llm_bench",
    "data/sandbox/tmp",
]

def should_skip_dir(dirpath: Path, exclude_roots: List[str], root: Path) -> bool:
    # Compare against path relative to scan root to avoid matching system paths (e.g. "/data/...")
    try:
        rel = dirpath.resolve().relative_to(root.resolve())
        rel_str = str(rel).replace("\\", "/").lstrip("./")
    except Exception:
        # Fallback to directory name if relative path cannot be determined
        rel_str = dirpath.name

    dir_name = dirpath.name
    for ex in exclude_roots:
        ex_norm = ex.strip().strip("/").replace("\\", "/")
        if not ex_norm:
            continue
        # If the exclude is a nested path (e.g. "data/bench/tmp"), match by prefix on the relative path
        if "/" in ex_norm:
            if rel_str == ex_norm or rel_str.startswith(ex_norm + "/"):
                return True
        else:
            # Bare directory name exclusion (e.g. "build"): match on current directory name
            if dir_name == ex_norm:
                return True
    return False

def count_file_lines(py_file: Path) -> Tuple[int, int, int]:
    total = 0
    blank = 0
    comment = 0
    try:
        with py_file.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                total += 1
                s = line.strip()
                if not s:
                    blank += 1
                elif s.startswith("#"):
                    comment += 1
    except Exception:
        # Count unreadable files as zero lines, continue
        return 0, 0, 0
    return total, blank, comment


    except Exception:
        # Fallback to directory name if relative path cannot be determined
        rel_str = dirpath.name

    dir_name = dirpath.name
    for ex in exclude_roots:
        ex_norm = ex.strip().strip("/").replace("\\", "/")
        if not ex_norm:
            continue
        # If the exclude is a nested path (e.g. "data/bench/tmp"), match by prefix on the relative path
        if "/" in ex_norm:
            if rel_str == ex_norm or rel_str.startswith(ex_norm + "/"):
                return True
        else:
            # Bare directory name exclusion (e.g. "build"): match on current directory name
            if dir_name == ex_norm:
                return True
    return False

def count_file_lines(py_file: Path) -> Tuple[int, int, int]:
    total = 0
    blank = 0
    comment = 0
    try:
        with py_file.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                total += 1
                s = line.strip()
                if not s:
                    blank += 1
                elif s.startswith("#"):
                    comment += 1
    except Exception:
        # Count unreadable files as zero lines, continue
        return 0, 0, 0
    return total, blank, comment

def scan_repo(root: Path, exclude_dirs: List[str]) -> Dict[str, int]:
    totals = {
        "files": 0,
        "total_lines": 0,
        "blank_lines": 0,
        "comment_lines": 0,
        "code_lines": 0,
    }
    # Normalize exclude entries once
    normalized_excludes: List[str] = [s.strip().strip("/").replace("\\", "/") for s in exclude_dirs]

    for dirpath, dirnames, filenames in os.walk(root):
        dpath = Path(dirpath)
        # In-place prune dirnames to avoid descending into excluded directories
        pruned: List[str] = []
        for dn in list(dirnames):
            candidate = dpath / dn
            if should_skip_dir(candidate, normalized_excludes, root):


def count_file_lines(py_file: Path) -> Tuple[int, int, int]:
    total = 0
    blank = 0
    comment = 0
    try:
        with py_file.open("r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                total += 1
                s = line.strip()
                if not s:
                    blank += 1
                elif s.startswith("#"):
                    comment += 1
    except Exception:
        # Count unreadable files as zero lines, continue
        return 0, 0, 0
    return total, blank, comment

def scan_repo(root: Path, exclude_dirs: List[str]) -> Dict[str, int]:
    totals = {
        "files": 0,
        "total_lines": 0,
        "blank_lines": 0,
        "comment_lines": 0,
        "code_lines": 0,
    }
    # Normalize exclude entries once
    normalized_excludes: List[str] = [s.strip().strip("/").replace("\\", "/") for s in exclude_dirs]

    for dirpath, dirnames, filenames in os.walk(root):
        dpath = Path(dirpath)
        # In-place prune dirnames to avoid descending into excluded directories
        pruned: List[str] = []
        for dn in list(dirnames):
            candidate = dpath / dn
            if should_skip_dir(candidate, normalized_excludes, root):
                pruned.append(dn)
        for dn in pruned:
            try:
                dirnames.remove(dn)
            except ValueError:
                pass

        for fn in filenames:
            if not fn.endswith(".py"):
                continue
            fp = dpath / fn
            t, b, c = count_file_lines(fp)
            if t == 0 and (not fp.exists()):
                continue
            totals["files"] += 1
            totals["total_lines"] += t
            totals["blank_lines"] += b
            totals["comment_lines"] += c
    totals["code_lines"] = totals["total_lines"] - totals["blank_lines"] - totals["comment_lines"]
    return totals

def main() -> int:
    ap = argparse.ArgumentParser(description="Count Python lines of code in the repository.")
    ap.add_argument("--root", default=".", help="Root directory to scan (default: current directory)")
    ap.add_argument(
        "--exclude-dirs",
        default=",".join(DEFAULT_EXCLUDES),
        help="Comma-separated list of directory names or paths to exclude",
    )
    args = ap.parse_args()

    root = Path(args.root).resolve()
    exclude_dirs = [s.strip() for s in str(args.exclude_dirs).split(",") if s.strip()]

    totals = scan_repo(root, exclude_dirs)

    print(
        {
            "root": str(root),


            try:
                dirnames.remove(dn)
            except ValueError:
                pass

        for fn in filenames:
            if not fn.endswith(".py"):
                continue
            fp = dpath / fn
            t, b, c = count_file_lines(fp)
            if t == 0 and (not fp.exists()):
                continue
            totals["files"] += 1
            totals["total_lines"] += t
            totals["blank_lines"] += b
            totals["comment_lines"] += c
    totals["code_lines"] = totals["total_lines"] - totals["blank_lines"] - totals["comment_lines"]
    return totals

def main() -> int:
    ap = argparse.ArgumentParser(description="Count Python lines of code in the repository.")
    ap.add_argument("--root", default=".", help="Root directory to scan (default: current directory)")
    ap.add_argument(
        "--exclude-dirs",
        default=",".join(DEFAULT_EXCLUDES),
        help="Comma-separated list of directory names or paths to exclude",
    )
    args = ap.parse_args()

    root = Path(args.root).resolve()
    exclude_dirs = [s.strip() for s in str(args.exclude_dirs).split(",") if s.strip()]

    totals = scan_repo(root, exclude_dirs)

    print(
        {
            "root": str(root),
            "files": totals["files"],
            "total_lines": totals["total_lines"],
            "code_lines": totals["code_lines"],
            "comment_lines": totals["comment_lines"],
            "blank_lines": totals["blank_lines"],
            "exclude_dirs": exclude_dirs,
        }
    )
    return 0

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


repo.b

__all__ = ["util", "Base"]

def util():
    return 42

def _secret():
    return 0

class Base:
    def base_method(self):
        return "base"


repo.a

from b import *

class A(Base):
    def n(self):
        return "n"

    def m(self):
        self.n()
        super().base_method()

def top():
    util()
    _secret()  # should remain unresolved due to star import skipping private names


from a import A, top

def test_top():
    assert top() is None

class TestA:
    def test_methods(self):
        a = A()
        assert a.n() == "n"
        assert a.m() is None


import logging
"""
Setup script for agi_dw package.
"""

from setuptools import setup, find_packages

setup(
	name="agi_dw",
	version="0.1.0",
	description="AGI Development Workflow",
	packages=find_packages(),
	python_requires=">=3.8",
	install_requires=[
		"pytest>=7.0.0",
		"pytest-cov>=4.0.0",
		"pathlib",
	],
	extras_require={
		"dev": [
			"pytest>=7.0.0",
			"pytest-cov>=4.0.0",
			"pytest-mock>=3.0.0",
		]
	}
)


import logging
from pathlib import Path
from agi_dw.tools.patch_actuator import apply_unified_diff

def _make_repo(tmp_path: Path) -> Path:
	repo = tmp_path / "repo"
	repo.mkdir(parents=True, exist_ok=True)
	(repo / ".git").mkdir(exist_ok=True)  # minimal marker
	(repo / "a.py").write_text("print('a')\n", encoding="utf-8")
	return repo

def test_apply_respects_max_files(tmp_path: Path):
	repo = _make_repo(tmp_path)
	diff = (
		"diff --git a/a.py b/a.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/a.py\n"
		"+++ b/a.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
		"diff --git a/b.py b/b.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/b.py\n"
		"+++ b/b.py\n"
		"@@ -0,0 +1,1 @@\n"
		"+print('x')\n"
	)
	res = apply_unified_diff(repo, diff, allow_globs=["**/*.py"], block_globs=[], max_files=1, dry_run=True)
	assert not res.get("ok"), res
	assert "too many files" in str(res.get("error"))

def test_apply_blocklist(tmp_path: Path):
	repo = _make_repo(tmp_path)
	diff = (
		"diff --git a/a.py b/a.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/a.py\n"
		"+++ b/a.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
	)
	res = apply_unified_diff(repo, diff, allow_globs=["**/*.py"], block_globs=["**/a.py"], max_files=5, dry_run=True)
	assert not res.get("ok"), res
	assert "blocked by policy" in str(res.get("error"))


import logging
from pathlib import Path
from agi_dw.tools.patch_actuator import apply_unified_diff

def _make_repo(tmp_path: Path) -> Path:
	repo = tmp_path / "repo"
	repo.mkdir(parents=True, exist_ok=True)
	(repo / ".git").mkdir(exist_ok=True)  # minimal marker
	(repo / "a.py").write_text("print('a')\n", encoding="utf-8")
	return repo

def test_apply_respects_max_files(tmp_path: Path):
	repo = _make_repo(tmp_path)
	diff = (
		"diff --git a/a.py b/a.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/a.py\n"
		"+++ b/a.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
		"diff --git a/b.py b/b.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/b.py\n"
		"+++ b/b.py\n"
		"@@ -0,0 +1,1 @@\n"
		"+print('x')\n"
	)
	res = apply_unified_diff(repo, diff, allow_globs=["**/*.py"], block_globs=[], max_files=1, dry_run=True)


	diff = (
		"diff --git a/a.py b/a.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/a.py\n"
		"+++ b/a.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
		"diff --git a/b.py b/b.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/b.py\n"
		"+++ b/b.py\n"
		"@@ -0,0 +1,1 @@\n"
		"+print('x')\n"
	)
	res = apply_unified_diff(repo, diff, allow_globs=["**/*.py"], block_globs=[], max_files=1, dry_run=True)
	assert not res.get("ok"), res
	assert "too many files" in str(res.get("error"))

def test_apply_blocklist(tmp_path: Path):
	repo = _make_repo(tmp_path)
	diff = (
		"diff --git a/a.py b/a.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/a.py\n"
		"+++ b/a.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
	)
	res = apply_unified_diff(repo, diff, allow_globs=["**/*.py"], block_globs=["**/a.py"], max_files=5, dry_run=True)
	assert not res.get("ok"), res
	assert "blocked by policy" in str(res.get("error"))


from __future__ import annotations
import logging

from agi_dw.tools.patch_actuator import validate_diff_policy

def test_validate_diff_policy_allows_py_and_limits_hunk():
	diff = """--- a/x.py
+++ b/x.py
@@ -1,1 +1,2 @@
-print('a')
+print('a')
+print('b')
"""
	res = validate_diff_policy(diff, allowed_exts=[".py"], max_hunk_lines=10)
	assert res["ok"] is True
	assert not res.get("issues")

def test_validate_diff_policy_blocks_extension():
	diff = """--- a/secret.pem
+++ b/secret.pem
@@ -1,1 +1,1 @@
-abc
+def
"""
	res = validate_diff_policy(diff, allowed_exts=[".py"])  # pem disallowed
	assert res["ok"] is False
	assert any("disallowed_extension" in it for it in res.get("issues") or [])


import logging
from agi_dw.tools.patch_safety import validate_unified_diff_schema, check_python_syntax
from pathlib import Path

def test_validate_unified_diff_schema_ok():
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, why

def test_validate_unified_diff_schema_blocks_binary_and_modes():
	diff = (
		"diff --git a/bin b/bin\n"
		"index 0000000..0000000 100644\n"
		"--- a/bin\n"
		"+++ b/bin\n"
		"gitattributes\n"
		"rename from a\n"
		"@@ -1,1 +1,1 @@\n"
		"-x\n"
		"+y\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path):
	p = tmp_path / "foo.py"
	p.write_text("def f():\n    return 1\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(p)])
	assert ok and not bad

def test_check_python_syntax_bad(tmp_path: Path):
	p = tmp_path / "foo.py"
	p.write_text("def f(:\n    return 1\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(p)])
	assert not ok and str(p) in bad


import logging
from agi_dw.tools.patch_safety import validate_unified_diff_schema, check_python_syntax
from pathlib import Path

def test_validate_unified_diff_schema_ok():
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-print('a')\n"
		"+print('b')\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, why

def test_validate_unified_diff_schema_blocks_binary_and_modes():
	diff = (
		"diff --git a/bin b/bin\n"
		"index 0000000..0000000 100644\n"
		"--- a/bin\n"
		"+++ b/bin\n"
		"gitattributes\n"
		"rename from a\n"
		"@@ -1,1 +1,1 @@\n"
		"-x\n"
		"+y\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path):
	p = tmp_path / "foo.py"


	ok, why = validate_unified_diff_schema(diff)
	assert ok, why

def test_validate_unified_diff_schema_blocks_binary_and_modes():
	diff = (
		"diff --git a/bin b/bin\n"
		"index 0000000..0000000 100644\n"
		"--- a/bin\n"
		"+++ b/bin\n"
		"gitattributes\n"
		"rename from a\n"
		"@@ -1,1 +1,1 @@\n"
		"-x\n"
		"+y\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path):
	p = tmp_path / "foo.py"
	p.write_text("def f():\n    return 1\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(p)])
	assert ok and not bad

def test_check_python_syntax_bad(tmp_path: Path):
	p = tmp_path / "foo.py"
	p.write_text("def f(:\n    return 1\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(p)])
	assert not ok and str(p) in bad


		"index 0000000..0000000 100644\n"
		"--- a/bin\n"
		"+++ b/bin\n"
		"gitattributes\n"
		"rename from a\n"
		"@@ -1,1 +1,1 @@\n"
		"-x\n"
		"+y\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path):
	p = tmp_path / "foo.py"
	p.write_text("def f():\n    return 1\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(p)])
	assert ok and not bad

def test_check_python_syntax_bad(tmp_path: Path):
	p = tmp_path / "foo.py"
	p.write_text("def f(:\n    return 1\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(p)])
	assert not ok and str(p) in bad


import logging
import json
from pathlib import Path
from agi_dw.scripts.eval.ci_assert_safe_edits import main as ci_main

def test_ci_assert_safe_edits_tmp(tmp_path: Path, monkeypatch):
	log = tmp_path / "scheduler_runs.jsonl"
	# Write two lines: one safe, one unsafe
	line_ok = {"stdout_tail": "", "stderr_tail": ""}
	line_bad = {"stdout_tail": "diff_validation_failed:too_many_files"}
	log.write_text("\n".join([json.dumps(line_ok), json.dumps(line_bad)]), encoding="utf-8")

	# Point script to temp log path
	from importlib import reload
	import agi_dw.scripts.eval.ci_assert_safe_edits as mod
	def _fake_args():
		class A:
			logs = str(log)
		return A()
	monkeypatch.setattr(mod, "argparse", type("X", (), {"ArgumentParser": lambda *a, **k: type("Y", (), {"add_argument": lambda *a, **k: None, "parse_args": staticmethod(_fake_args)})()})())

	# Expect non-zero exit due to one unsafe
	try:
		ci_main()
		assert False, "Expected non-zero exit"
	except SystemExit as e:
		assert int(e.code) == 1


from __future__ import annotations
import logging

from pathlib import Path

from agi_dw.scripts.devtools.devtools_orchestrator import make_flake8_whitespace_fix_diff

def test_whitespace_autofix_diff(tmp_path: Path):
	p = tmp_path / "a.py"
	p.write_text("print('x')  \n", encoding="utf-8")  # trailing spaces
	diff = make_flake8_whitespace_fix_diff(tmp_path)
	assert "--- a/" in diff and "+++ b/" in diff
	# Apply diff via a simple check: ensure it proposes removing trailing spaces
	assert "-print('x')  " in diff
	assert "+print('x')" in diff


import logging

from agi_dw.tools.index_rank import rank_index_candidates

def test_rank_index_candidates_basic():
	obs = {"content": "count lines", "meta": {"cwd": "/tmp"}}
	idx = {
		"functions": {
			"/tmp/app.py": [
				{"name": "count_lines"},
				{"name": "helper"},
			]
		},
		"classes": {
			"/tmp/app.py": [
				{"name": "Counter"}
			]
		}
	}
	out = rank_index_candidates(obs, idx, 2)
	assert "top_functions" in out
	fn = [x["name"] for x in out["top_functions"]]
	assert "count_lines" in fn

def test_rank_index_candidates_empty_or_disabled():
	obs = {"content": "", "meta": {}}
	assert rank_index_candidates(obs, {}, 0) == {}
	assert rank_index_candidates({}, {"functions": {}}, 2) == {}


	obs = {"content": "count lines", "meta": {"cwd": "/tmp"}}
	idx = {
		"functions": {
			"/tmp/app.py": [
				{"name": "count_lines"},
				{"name": "helper"},
			]
		},
		"classes": {
			"/tmp/app.py": [
				{"name": "Counter"}
			]
		}
	}
	out = rank_index_candidates(obs, idx, 2)
	assert "top_functions" in out
	fn = [x["name"] for x in out["top_functions"]]
	assert "count_lines" in fn

def test_rank_index_candidates_empty_or_disabled():
	obs = {"content": "", "meta": {}}
	assert rank_index_candidates(obs, {}, 0) == {}
	assert rank_index_candidates({}, {"functions": {}}, 2) == {}


from __future__ import annotations
import logging

import tempfile
from pathlib import Path

def test_dual_route_inject_idempotent():
    src = """
def new_api(x):
    return x + 1

def old_api(x):
    return x
"""
    with tempfile.TemporaryDirectory() as td:
        p = Path(td) / "mod.py"
        p.write_text(src, encoding="utf-8")
        # First inject
        import subprocess
        rc = subprocess.run(["python", "scripts/codemods/dual_route_inject.py", str(p), "new_api", "old_api", "NEW_API"], capture_output=True, text=True).returncode
        assert rc == 0, "first inject should succeed"
        text1 = p.read_text(encoding="utf-8")
        assert "def new_api_dual_route(" in text1
        # Second inject (idempotent)
        rc2 = subprocess.run(["python", "scripts/codemods/dual_route_inject.py", str(p), "new_api", "old_api", "NEW_API"], capture_output=True, text=True).returncode
        assert rc2 == 0, "second inject should also succeed"
        text2 = p.read_text(encoding="utf-8")
        # Should not duplicate wrapper
        assert text2.count("def new_api_dual_route(") == 1


from __future__ import annotations
import logging

from agi_dw.core.utils.bench_utils import (
    strip_fences,
    precheck_code,
    retry_with_backoff,
    load_code_index,
    inject_similar_functions,
)

def test_strip_fences_basic():
    code = """```
print('hi')
```"""
    assert strip_fences(code) == "print('hi')"

def test_strip_fences_no_fences():
    code = "print('ok')"
    assert strip_fences(code) == "print('ok')"

def test_precheck_code_ok():
    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)
    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out

def test_inject_similar_functions_no_index():
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "q", None, 1)
    assert out == base

def test_load_code_index_invalid_path():
    assert load_code_index("/path/does/not/exist/index.json") is None


from __future__ import annotations
import logging

from agi_dw.core.utils.bench_utils import (
    strip_fences,
    precheck_code,
    retry_with_backoff,
    load_code_index,
    inject_similar_functions,
)

def test_strip_fences_basic():
    code = """```
print('hi')
```"""
    assert strip_fences(code) == "print('hi')"

def test_strip_fences_no_fences():
    code = "print('ok')"
    assert strip_fences(code) == "print('ok')"

def test_precheck_code_ok():
    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}


from __future__ import annotations
import logging

from agi_dw.core.utils.bench_utils import (
    strip_fences,
    precheck_code,
    retry_with_backoff,
    load_code_index,
    inject_similar_functions,
)

def test_strip_fences_basic():
    code = """```
print('hi')
```"""
    assert strip_fences(code) == "print('hi')"

def test_strip_fences_no_fences():
    code = "print('ok')"
    assert strip_fences(code) == "print('ok')"

def test_precheck_code_ok():
    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)


    strip_fences,
    precheck_code,
    retry_with_backoff,
    load_code_index,
    inject_similar_functions,
)

def test_strip_fences_basic():
    code = """```
print('hi')
```"""
    assert strip_fences(code) == "print('hi')"

def test_strip_fences_no_fences():
    code = "print('ok')"
    assert strip_fences(code) == "print('ok')"

def test_precheck_code_ok():
    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}


)

def test_strip_fences_basic():
    code = """```
print('hi')
```"""
    assert strip_fences(code) == "print('hi')"

def test_strip_fences_no_fences():
    code = "print('ok')"
    assert strip_fences(code) == "print('ok')"

def test_precheck_code_ok():
    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")


print('hi')
```"""
    assert strip_fences(code) == "print('hi')"

def test_strip_fences_no_fences():
    code = "print('ok')"
    assert strip_fences(code) == "print('ok')"

def test_precheck_code_ok():
    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)


    ok, err = precheck_code("x=1\nprint(x)")
    assert ok and err is None

def test_precheck_code_fail():
    ok, err = precheck_code("def :\n pass")
    assert not ok and isinstance(err, str)

def test_retry_with_backoff_success_first_try():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)
    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }


        calls["n"] += 1
        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)
    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out

def test_inject_similar_functions_no_index():


    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)
    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out

def test_inject_similar_functions_no_index():
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "q", None, 1)
    assert out == base


        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)
    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out

def test_inject_similar_functions_no_index():
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "q", None, 1)
    assert out == base

def test_load_code_index_invalid_path():
    assert load_code_index("/path/does/not/exist/index.json") is None


    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out

def test_inject_similar_functions_no_index():
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "q", None, 1)
    assert out == base

def test_load_code_index_invalid_path():
    assert load_code_index("/path/does/not/exist/index.json") is None


def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out

def test_inject_similar_functions_no_index():
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "q", None, 1)
    assert out == base

def test_load_code_index_invalid_path():
    assert load_code_index("/path/does/not/exist/index.json") is None


fn

        return 42

    out = retry_with_backoff(fn, retries=2, backoff=0.0)
    assert out == 42 and calls["n"] == 1

def test_retry_with_backoff_eventual_success():
    calls = {"n": 0}

    def fn():
        calls["n"] += 1
        if calls["n"] < 2:
            raise RuntimeError("boom")
        return "ok"

    out = retry_with_backoff(fn, retries=3, backoff=0.0)
    assert out == "ok" and calls["n"] == 2

def test_retry_with_backoff_failure():
    def fn():
        raise RuntimeError("boom")

    out = retry_with_backoff(fn, retries=1, backoff=0.0)
    assert out is None

def test_load_code_index_none():
    assert load_code_index("") is None

def test_inject_similar_functions_minimal():
    idx = {
        "functions": {
            "/tmp/a.py": [{"name": "solve_problem", "lineno": 1}],
            "/tmp/b.py": [{"name": "helper", "lineno": 1}],
        }
    }
    base = "def foo():\n    pass"
    out = inject_similar_functions(base, "solve this problem", idx, 1)
    assert "Similar functions:" in out
    assert "solve_problem" in out


from __future__ import annotations
import logging

from pathlib import Path

from agi_dw.tools.patch_safety import validate_unified_diff_schema, check_python_syntax, count_files_in_diff

def test_validate_unified_diff_schema_ok() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x = 1\n"
		"+x = 2\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok, got reason: {why}"

def test_validate_unified_diff_schema_js_ok() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok for js diff, got: {why}"

def test_validate_unified_diff_schema_blocks_binary() -> None:
	diff = (
		"diff --git a/foo.bin b/foo.bin\n"
		"GIT binary patch\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_validate_unified_diff_schema_requires_hunks() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path) -> None:
	fp = tmp_path / "good.py"
	fp.write_text("def add(a,b):\n    return a+b\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert ok
	assert bad == []

def test_check_python_syntax_bad(tmp_path: Path) -> None:
	fp = tmp_path / "bad.py"
	fp.write_text("def oops(:\n    pass\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert not ok
	assert str(fp) in bad

def test_count_files_in_diff_multiple() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x=1\n"
		"+x=2\n"
		"diff --git a/bar.py b/bar.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/bar.py\n"
		"+++ b/bar.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-y=1\n"
		"+y=2\n"
	)
	files = count_files_in_diff(diff)
	assert "foo.py" in files
	assert "bar.py" in files
	assert len(files) == 2

def test_count_files_in_diff_js_ts() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
		"diff --git a/util.ts b/util.ts\n"
		"index 0000000..0000000 100644\n"
		"--- a/util.ts\n"
		"+++ b/util.ts\n"
		"@@ -1,1 +1,1 @@\n"
		"-export const y=1;\n"
		"+export const y=2;\n"
	)
	files = count_files_in_diff(diff)
	assert "app.js" in files
	assert "util.ts" in files
	assert len(files) == 2


from __future__ import annotations
import logging

from pathlib import Path

from agi_dw.tools.patch_safety import validate_unified_diff_schema, check_python_syntax, count_files_in_diff

def test_validate_unified_diff_schema_ok() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x = 1\n"
		"+x = 2\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok, got reason: {why}"

def test_validate_unified_diff_schema_js_ok() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok for js diff, got: {why}"

def test_validate_unified_diff_schema_blocks_binary() -> None:
	diff = (
		"diff --git a/foo.bin b/foo.bin\n"
		"GIT binary patch\n"


from pathlib import Path

from agi_dw.tools.patch_safety import validate_unified_diff_schema, check_python_syntax, count_files_in_diff

def test_validate_unified_diff_schema_ok() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x = 1\n"
		"+x = 2\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok, got reason: {why}"

def test_validate_unified_diff_schema_js_ok() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok for js diff, got: {why}"

def test_validate_unified_diff_schema_blocks_binary() -> None:
	diff = (
		"diff --git a/foo.bin b/foo.bin\n"
		"GIT binary patch\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_validate_unified_diff_schema_requires_hunks() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok


		"+x = 2\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok, got reason: {why}"

def test_validate_unified_diff_schema_js_ok() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok for js diff, got: {why}"

def test_validate_unified_diff_schema_blocks_binary() -> None:
	diff = (
		"diff --git a/foo.bin b/foo.bin\n"
		"GIT binary patch\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_validate_unified_diff_schema_requires_hunks() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path) -> None:
	fp = tmp_path / "good.py"
	fp.write_text("def add(a,b):\n    return a+b\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert ok
	assert bad == []


		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
	)
	ok, why = validate_unified_diff_schema(diff)
	assert ok, f"expected ok for js diff, got: {why}"

def test_validate_unified_diff_schema_blocks_binary() -> None:
	diff = (
		"diff --git a/foo.bin b/foo.bin\n"
		"GIT binary patch\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_validate_unified_diff_schema_requires_hunks() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path) -> None:
	fp = tmp_path / "good.py"
	fp.write_text("def add(a,b):\n    return a+b\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert ok
	assert bad == []

def test_check_python_syntax_bad(tmp_path: Path) -> None:
	fp = tmp_path / "bad.py"
	fp.write_text("def oops(:\n    pass\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert not ok
	assert str(fp) in bad

def test_count_files_in_diff_multiple() -> None:
	diff = (


def test_validate_unified_diff_schema_blocks_binary() -> None:
	diff = (
		"diff --git a/foo.bin b/foo.bin\n"
		"GIT binary patch\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_validate_unified_diff_schema_requires_hunks() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path) -> None:
	fp = tmp_path / "good.py"
	fp.write_text("def add(a,b):\n    return a+b\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert ok
	assert bad == []

def test_check_python_syntax_bad(tmp_path: Path) -> None:
	fp = tmp_path / "bad.py"
	fp.write_text("def oops(:\n    pass\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert not ok
	assert str(fp) in bad

def test_count_files_in_diff_multiple() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x=1\n"
		"+x=2\n"
		"diff --git a/bar.py b/bar.py\n"


def test_validate_unified_diff_schema_requires_hunks() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
	)
	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path) -> None:
	fp = tmp_path / "good.py"
	fp.write_text("def add(a,b):\n    return a+b\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert ok
	assert bad == []

def test_check_python_syntax_bad(tmp_path: Path) -> None:
	fp = tmp_path / "bad.py"
	fp.write_text("def oops(:\n    pass\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert not ok
	assert str(fp) in bad

def test_count_files_in_diff_multiple() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x=1\n"
		"+x=2\n"
		"diff --git a/bar.py b/bar.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/bar.py\n"
		"+++ b/bar.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-y=1\n"
		"+y=2\n"
	)
	files = count_files_in_diff(diff)


	ok, _ = validate_unified_diff_schema(diff)
	assert not ok

def test_check_python_syntax_ok(tmp_path: Path) -> None:
	fp = tmp_path / "good.py"
	fp.write_text("def add(a,b):\n    return a+b\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert ok
	assert bad == []

def test_check_python_syntax_bad(tmp_path: Path) -> None:
	fp = tmp_path / "bad.py"
	fp.write_text("def oops(:\n    pass\n", encoding="utf-8")
	ok, bad = check_python_syntax([str(fp)])
	assert not ok
	assert str(fp) in bad

def test_count_files_in_diff_multiple() -> None:
	diff = (
		"diff --git a/foo.py b/foo.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x=1\n"
		"+x=2\n"
		"diff --git a/bar.py b/bar.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/bar.py\n"
		"+++ b/bar.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-y=1\n"
		"+y=2\n"
	)
	files = count_files_in_diff(diff)
	assert "foo.py" in files
	assert "bar.py" in files
	assert len(files) == 2

def test_count_files_in_diff_js_ts() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
		"diff --git a/util.ts b/util.ts\n"
		"index 0000000..0000000 100644\n"
		"--- a/util.ts\n"
		"+++ b/util.ts\n"
		"@@ -1,1 +1,1 @@\n"
		"-export const y=1;\n"
		"+export const y=2;\n"
	)
	files = count_files_in_diff(diff)


		"index 0000000..0000000 100644\n"
		"--- a/foo.py\n"
		"+++ b/foo.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-x=1\n"
		"+x=2\n"
		"diff --git a/bar.py b/bar.py\n"
		"index 0000000..0000000 100644\n"
		"--- a/bar.py\n"
		"+++ b/bar.py\n"
		"@@ -1,1 +1,1 @@\n"
		"-y=1\n"
		"+y=2\n"
	)
	files = count_files_in_diff(diff)
	assert "foo.py" in files
	assert "bar.py" in files
	assert len(files) == 2

def test_count_files_in_diff_js_ts() -> None:
	diff = (
		"diff --git a/app.js b/app.js\n"
		"index 0000000..0000000 100644\n"
		"--- a/app.js\n"
		"+++ b/app.js\n"
		"@@ -1,1 +1,1 @@\n"
		"-const x=1;\n"
		"+const x=2;\n"
		"diff --git a/util.ts b/util.ts\n"
		"index 0000000..0000000 100644\n"
		"--- a/util.ts\n"
		"+++ b/util.ts\n"
		"@@ -1,1 +1,1 @@\n"
		"-export const y=1;\n"
		"+export const y=2;\n"
	)
	files = count_files_in_diff(diff)
	assert "app.js" in files
	assert "util.ts" in files
	assert len(files) == 2


from __future__ import annotations

import logging
import os
from typing import Optional

class Flags:
	"""Minimal feature flag SDK with env override.

	- is_enabled("NAME") checks environment variable AGI_FLAG_NAME.
	  Values considered true: 1, true, yes, on (case-insensitive).
	- fallback default can be provided per-call.
	"""

	def __init__(self) -> None:
		self._cache: dict[str, bool] = {}

	@staticmethod
	def _normalize_flag_env_name(name: str) -> str:
		return f"AGI_FLAG_{str(name or '').strip().upper()}"

	@staticmethod
	def _parse_bool(val: Optional[str]) -> Optional[bool]:
		if val is None:
			return None
		s = str(val).strip().lower()
		if s in ("1", "true", "yes", "on"):  # truthy
			return True
		if s in ("0", "false", "no", "off"):  # falsy
			return False
		return None

	def is_enabled(self, name: str, default: bool = False) -> bool:
		key = self._normalize_flag_env_name(name)
		if key in self._cache:
			return bool(self._cache[key])
		env_val = os.environ.get(key)
		parsed = self._parse_bool(env_val)
		if parsed is None:
			parsed = bool(default)
		self._cache[key] = bool(parsed)
		return bool(parsed)

# Convenience singleton
FLAGS = Flags()


from __future__ import annotations

import logging
import os
from typing import Optional

class Flags:
	"""Minimal feature flag SDK with env override.

	- is_enabled("NAME") checks environment variable AGI_FLAG_NAME.
	  Values considered true: 1, true, yes, on (case-insensitive).
	- fallback default can be provided per-call.
	"""

	def __init__(self) -> None:
		self._cache: dict[str, bool] = {}

	@staticmethod
	def _normalize_flag_env_name(name: str) -> str:
		return f"AGI_FLAG_{str(name or '').strip().upper()}"

	@staticmethod
	def _parse_bool(val: Optional[str]) -> Optional[bool]:
		if val is None:
			return None
		s = str(val).strip().lower()
		if s in ("1", "true", "yes", "on"):  # truthy
			return True
		if s in ("0", "false", "no", "off"):  # falsy
			return False
		return None

	def is_enabled(self, name: str, default: bool = False) -> bool:
		key = self._normalize_flag_env_name(name)
		if key in self._cache:
			return bool(self._cache[key])


from __future__ import annotations

import logging
import os
from typing import Optional

class Flags:
	"""Minimal feature flag SDK with env override.

	- is_enabled("NAME") checks environment variable AGI_FLAG_NAME.
	  Values considered true: 1, true, yes, on (case-insensitive).
	- fallback default can be provided per-call.
	"""

	def __init__(self) -> None:
		self._cache: dict[str, bool] = {}

	@staticmethod
	def _normalize_flag_env_name(name: str) -> str:
		return f"AGI_FLAG_{str(name or '').strip().upper()}"

	@staticmethod
	def _parse_bool(val: Optional[str]) -> Optional[bool]:
		if val is None:
			return None
		s = str(val).strip().lower()
		if s in ("1", "true", "yes", "on"):  # truthy
			return True
		if s in ("0", "false", "no", "off"):  # falsy
			return False
		return None

	def is_enabled(self, name: str, default: bool = False) -> bool:
		key = self._normalize_flag_env_name(name)
		if key in self._cache:
			return bool(self._cache[key])
		env_val = os.environ.get(key)
		parsed = self._parse_bool(env_val)
		if parsed is None:
			parsed = bool(default)


import os
from typing import Optional

class Flags:
	"""Minimal feature flag SDK with env override.

	- is_enabled("NAME") checks environment variable AGI_FLAG_NAME.
	  Values considered true: 1, true, yes, on (case-insensitive).
	- fallback default can be provided per-call.
	"""

	def __init__(self) -> None:
		self._cache: dict[str, bool] = {}

	@staticmethod
	def _normalize_flag_env_name(name: str) -> str:
		return f"AGI_FLAG_{str(name or '').strip().upper()}"

	@staticmethod
	def _parse_bool(val: Optional[str]) -> Optional[bool]:
		if val is None:
			return None
		s = str(val).strip().lower()
		if s in ("1", "true", "yes", "on"):  # truthy
			return True
		if s in ("0", "false", "no", "off"):  # falsy
			return False
		return None

	def is_enabled(self, name: str, default: bool = False) -> bool:
		key = self._normalize_flag_env_name(name)
		if key in self._cache:
			return bool(self._cache[key])
		env_val = os.environ.get(key)
		parsed = self._parse_bool(env_val)
		if parsed is None:
			parsed = bool(default)
		self._cache[key] = bool(parsed)
		return bool(parsed)

# Convenience singleton
FLAGS = Flags()


	"""

	def __init__(self) -> None:
		self._cache: dict[str, bool] = {}

	@staticmethod
	def _normalize_flag_env_name(name: str) -> str:
		return f"AGI_FLAG_{str(name or '').strip().upper()}"

	@staticmethod
	def _parse_bool(val: Optional[str]) -> Optional[bool]:
		if val is None:
			return None
		s = str(val).strip().lower()
		if s in ("1", "true", "yes", "on"):  # truthy
			return True
		if s in ("0", "false", "no", "off"):  # falsy
			return False
		return None

	def is_enabled(self, name: str, default: bool = False) -> bool:
		key = self._normalize_flag_env_name(name)
		if key in self._cache:
			return bool(self._cache[key])
		env_val = os.environ.get(key)
		parsed = self._parse_bool(env_val)
		if parsed is None:
			parsed = bool(default)
		self._cache[key] = bool(parsed)
		return bool(parsed)

# Convenience singleton
FLAGS = Flags()


from __future__ import annotations

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

def _count_bytes(text: str) -> int:
	try:
		return len(text.encode("utf-8"))
	except Exception:
		return len(text)

def assert_diff_safe(old: str, new: str, rules: Dict[str, Any] | None = None) -> Tuple[bool, Dict[str, Any]]:
	"""Assess risk of a text replacement using simple heuristics.

	Rules (all optional):
	- max_bytes_added
	- max_bytes_deleted
	- forbid_patterns: list[str] regex
	"""
	rules = dict(rules or {})
	added = 0
	deleted = 0
	try:
		# Approximate: byte delta vs common prefix/suffix
		prefix = 0
		for a, b in zip(old, new):
			if a == b:
				prefix += 1
			else:
				break
		suffix = 0
		for a, b in zip(old[::-1], new[::-1]):
			if a == b:
				suffix += 1
			else:
				break
		old_mid = old[prefix:len(old) - suffix if suffix > 0 else len(old)]
		new_mid = new[prefix:len(new) - suffix if suffix > 0 else len(new)]
		deleted = _count_bytes(old_mid)
		added = _count_bytes(new_mid)
	except Exception:
		added = max(0, _count_bytes(new) - _count_bytes(old))
		deleted = max(0, _count_bytes(old) - _count_bytes(new))

	issues: List[str] = []
	max_add = int(rules.get("max_bytes_added", 200_000) or 200_000)
	max_del = int(rules.get("max_bytes_deleted", 200_000) or 200_000)
	if added > max_add:
		issues.append(f"bytes_added_exceeds:{added}>{max_add}")
	if deleted > max_del:
		issues.append(f"bytes_deleted_exceeds:{deleted}>{max_del}")
	forbid: List[str] = list(rules.get("forbid_patterns", []) or [])
	for pat in forbid:
		try:
			rx = re.compile(pat)
			if rx.search(new or ""):
				issues.append(f"forbidden_pattern:{pat}")
		except Exception:
			continue
	return (len(issues) == 0), {"issues": issues, "bytes_added": added, "bytes_deleted": deleted}


from __future__ import annotations

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

def _count_bytes(text: str) -> int:
	try:
		return len(text.encode("utf-8"))
	except Exception:
		return len(text)

def assert_diff_safe(old: str, new: str, rules: Dict[str, Any] | None = None) -> Tuple[bool, Dict[str, Any]]:
	"""Assess risk of a text replacement using simple heuristics.

	Rules (all optional):
	- max_bytes_added
	- max_bytes_deleted
	- forbid_patterns: list[str] regex
	"""
	rules = dict(rules or {})
	added = 0
	deleted = 0
	try:
		# Approximate: byte delta vs common prefix/suffix
		prefix = 0
		for a, b in zip(old, new):
			if a == b:
				prefix += 1
			else:


from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple

from agi_dw.core.ops.tracing import trace_span  # type: ignore
from agi_dw.core.planner.llm_planner import emit_plan

# Domain-agnostic callable: given (obs, plan) returns a quick action dict for WM rollout
ActionProvider = Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]]

@dataclass
class PlannerConfig:
	model: str
	backend: str = "hf"
	timeout_sec: int = 30
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"
	candidates: int = 1
	use_tot: bool = False
	seeded: bool = False
	pref_weights_path: Optional[str] = None

@dataclass
class VerifierConfig:
	model: str
	backend: str = "hf"
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"

@dataclass
class WMConfig:
	enabled: bool = False
	model_path: Optional[str] = None
	horizon: int = 1
	plan_rank: bool = False

@dataclass
class ContextAugment:
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	index_k: int = 0
	index_path: Optional[str] = None
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True

def plan_with_context(
	obs: Dict[str, Any],
	domain: str,
	planner: PlannerConfig,
	verifier: VerifierConfig,
	wm: WMConfig,
	ctx: ContextAugment,
	action_for_rollout: Optional[ActionProvider] = None,
	critic_fallback_threshold: Optional[float] = None,
	log_prompts: bool = False,
	task_name: Optional[str] = None,
	wm_quick_action_t5_model: Optional[str] = None,
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (plan, planner_info, obs_aug, mem_snippets, mem_query_ms)
	"""
	# Provide default quick action synthesis without loops importing actuators
	"""
	Return (plan, planner_info, obs_aug, mem_snippets, mem_query_ms)
	"""
	root_dir = Path(__file__).resolve().parents[2]
	obs_aug = dict(obs)
	mem_snippets: List[Dict[str, Any]] = []
	mem_query_ms: Optional[float] = None

	# Centralized memory/index/policy augmentation
	try:
		from agi_dw.core.memory.service import MemoryAugmentConfig, augment_observation  # type: ignore
		mem_cfg = MemoryAugmentConfig(
			use_memory=bool(getattr(ctx, "use_memory", False)),
			mem_path=getattr(ctx, "mem_path", None),
			mem_topk=int(getattr(ctx, "mem_topk", 3) or 3),
			mem_recency=float(getattr(ctx, "mem_recency", 0.0) or 0.0),
			mem_query=getattr(ctx, "mem_query", None),
			index_k=int(getattr(ctx, "index_k", 0) or 0),
			index_path=getattr(ctx, "index_path", None),
			inject_dom_policy=bool(getattr(ctx, "inject_dom_policy", True)),
			inject_cli_policy=bool(getattr(ctx, "inject_cli_policy", True)),
			inject_caps=bool(getattr(ctx, "inject_caps", True)),
		)
		obs_aug, mem_snippets, mem_query_ms = augment_observation(obs, root_dir, mem_cfg)
	except Exception:
		mem_snippets = []
		mem_query_ms = None

	# Adapter bank resolution
	def _resolve_adapter(bank_name: Optional[str], kind: str) -> Optional[str]:
		if not bank_name:
			return None
		try:
			from agi_dw.core.llm.adapter_router import pick_from_bank  # type: ignore
			bank = pick_from_bank(root_dir, bank_name)
			return bank.get(kind)
		except Exception:
			return None

	planner_adapter = planner.adapter_dir or _resolve_adapter(planner.adapter_bank, "planner")
	verifier_adapter = verifier.adapter_dir or _resolve_adapter(verifier.adapter_bank, "verifier")

	# Candidate generation
	candidates: List[Dict[str, Any]] = []
	if planner.candidates > 1:
		if planner.seeded and domain == "cli":
			try:
				def _seeded(tname: Optional[str]) -> List[Dict[str, Any]]:
					if tname == "count_lines":
						return [
							{"subgoals": ["read file", "count lines"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["count lines"], "tools": ["cli"], "constraints": {"prefer_wc": True}},
						]
					elif tname == "grep_error":
						return [
							{"subgoals": ["search ERROR"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["scan file", "filter lines"], "tools": ["cli"], "constraints": {"case_sensitive": True}},
						]
					return [{"subgoals": ["plan"], "tools": ["cli"], "constraints": {}}]
				candidates = _seeded(task_name)
			except Exception:
				candidates = []
		if not candidates and planner.use_tot:
			try:
				from scripts.misc.plan_tot import generate_plan_candidates  # type: ignore
				candidates = generate_plan_candidates(obs_aug, planner.model, num_candidates=int(planner.candidates)) or []
			except Exception:
				candidates = []
		if not candidates:
			for _ in range(max(1, int(planner.candidates))):
				with trace_span("plan_emit", {"structured": str(planner.structured_mode)}):
					p = emit_plan(
						obs_aug,
						model=planner.model,
						timeout_sec=planner.timeout_sec,
						use_llm=True,
						backend=planner.backend,
						log_prompts=log_prompts,
						adapter_dir=planner_adapter,
						structured_mode=str(planner.structured_mode),
					)
				candidates.append(p)
	else:
		with trace_span("plan_emit", {"structured": str(planner.structured_mode)}):
			plan_single = emit_plan(
				obs_aug,
				model=planner.model,
				timeout_sec=planner.timeout_sec,
				use_llm=True,
				backend=planner.backend,
				log_prompts=log_prompts,
				adapter_dir=planner_adapter,
				structured_mode=str(planner.structured_mode),
			)
		candidates = [plan_single]

	# Score candidates
	scored: List[Tuple[Optional[float], Tuple[float, float], float, Dict[str, Any]]] = []
	# Load preference weights if provided
	pref_w: Optional[Dict[str, float]] = None
	try:
		if planner.pref_weights_path:
			pw_path = Path(planner.pref_weights_path)
			if pw_path.exists():
				obj = json.loads(pw_path.read_text(encoding="utf-8"))
				wv = float(obj.get("w_verifier", 0.5))
				ww = float(obj.get("w_wm", 0.5))
				ws = float(obj.get("w_self", 0.1))
				pref_w = {"w_verifier": wv, "w_wm": ww, "w_self": ws}
	except Exception:
		pref_w = None

	for p in candidates:
		vr = 0.5
		try:
			from agi_dw.core.verifier.service import VerifierServiceConfig, quick_risk  # type: ignore
			vsc = VerifierServiceConfig(
				model=verifier.model,
				backend=verifier.backend,
				adapter_dir=verifier_adapter,
				structured_mode=verifier.structured_mode,
				timeout_sec=max(2, int(planner.timeout_sec)),
				strict=False,
				calibrate=False,
			)
			vr = float(quick_risk(obs, p, vsc))
		except Exception:
			vr = 0.5
		se = 0.5
		try:
			subgoals = p.get("subgoals") if isinstance(p, dict) else []
			constraints = p.get("constraints") if isinstance(p, dict) else {}
			se = 1.0 - min(1.0, max(0, (len(subgoals) if isinstance(subgoals, list) else 3)) / 6.0)
			if isinstance(constraints, dict):
				bonus = 0.0
				if len(constraints) > 0:
					bonus = 0.1
				elif domain == "dom" and ("stay_on_url" in constraints):
					bonus = 0.1
				se = min(1.0, se + bonus)
		except Exception:
			se = 0.5
		wmr: Optional[float] = None
		if wm.enabled and wm.plan_rank:
			try:
				from agi_dw.core.world_model.service import WorldModelService  # type: ignore
				wm_path = Path(wm.model_path or "")
				if wm_path.exists():
					wm_service = WorldModelService.load_if_exists(wm_path)
					if wm_service:
						# Synthesize a quick action if none provided
						a0: Dict[str, Any] = {}
						try:
							if action_for_rollout is not None:
								a0 = action_for_rollout(obs, p) or {}
							elif domain == "dom" and wm_quick_action_t5_model:
								from agi_dw.core.actuator.t5_actuator import ActuatorT5Predictor  # type: ignore
								a0 = ActuatorT5Predictor(wm_quick_action_t5_model, mode="dom").predict_action(obs, p) or {}
							elif domain == "cli":
								from agi_dw.core.actuator.template_actuator import TemplateActuator  # type: ignore
								th = TemplateActuator().predict_action(obs, p)
								a0 = {"tool": th.tool, "args": th.args} if th else {}
						except Exception:
							a0 = {}
						roll = wm_service.rollout(obs, p, [a0] if isinstance(a0, dict) else [], horizon=max(1, int(getattr(wm, "horizon", 1) or 1)))
						wmr = float(roll.get("avg_risk", 0.5) if roll else 0.5)
			except Exception:
				wmr = None
		if pref_w is not None:
			wm_eff = float(wmr if wmr is not None else vr)
			comp = float(pref_w["w_verifier"]) * float(vr) + float(pref_w["w_wm"]) * float(wm_eff) - float(pref_w["w_self"]) * float(se)
		else:
			comp = None
		scored.append((comp, (vr, (wmr if wmr is not None else vr)), float(se), p))

	# If single plan and critic fallback is requested, try a second plan and pick lower risk
	chosen_override: Optional[Dict[str, Any]] = None
	try:
		if len(candidates) == 1 and isinstance(critic_fallback_threshold, (int, float)):
			thr = float(critic_fallback_threshold)
			pre_risk = float(scored[0][1][0]) if scored else 0.5
			if pre_risk >= thr:
				alt = emit_plan(
					obs_aug,
					model=planner.model,
					timeout_sec=planner.timeout_sec,
					use_llm=True,
					backend=planner.backend,
					log_prompts=log_prompts,
					adapter_dir=planner_adapter,
					structured_mode=str(planner.structured_mode),
				)
				try:
					from agi_dw.core.verifier.service import VerifierServiceConfig, quick_risk  # type: ignore
					vsc2 = VerifierServiceConfig(
						model=verifier.model,
						backend=verifier.backend,
						adapter_dir=verifier_adapter,
						structured_mode=verifier.structured_mode,
						timeout_sec=max(2, int(planner.timeout_sec)),
						strict=False,
						calibrate=False,
					)
					vr_alt = float(quick_risk(obs, alt, vsc2))
					# Insert alt scored row
					se_alt = 0.5
					try:
						subgoals = alt.get("subgoals") if isinstance(alt, dict) else []
						constraints = alt.get("constraints") if isinstance(alt, dict) else {}
						se_alt = 1.0 - min(1.0, max(0, (len(subgoals) if isinstance(subgoals, list) else 3)) / 6.0)
						if isinstance(constraints, dict):
							bonus = 0.0
							if len(constraints) > 0:
								bonus = 0.1
							elif domain == "dom" and ("stay_on_url" in constraints):
								bonus = 0.1
							se_alt = min(1.0, se_alt + bonus)
					except Exception:
						se_alt = 0.5
					scored.append((None, (vr_alt, vr_alt), float(se_alt), alt))
					# Strictly compare verifier risk like original CLI fallback
					if vr_alt < pre_risk:
						chosen_override = alt
				except Exception:
					pass
	except Exception:
		pass

	# Pick best
	scored.sort(key=lambda x: (x[0] if x[0] is not None else x[1][1], x[1][0] if x[0] is None else 0.0, -x[2] if x[0] is None else 0.0))
	best = scored[0]
	plan = chosen_override if chosen_override is not None else best[3]
	planner_info = {
		"candidates": len(scored),
		"verifier_risks": [float(a[1][0]) for a in scored],
		"wm_risks": [float(a[1][1]) for a in scored],
		"self_eval": [float(a[2]) for a in scored] if len(scored) > 0 else [],
		"pref_score": [float(a[0]) if a[0] is not None else None for a in scored],
		"picked_risk": {"verifier": float(best[1][0]), "wm": float(best[1][1])},
	}

	return plan, planner_info, obs_aug, mem_snippets, mem_query_ms


PlannerConfig

from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple

from agi_dw.core.ops.tracing import trace_span  # type: ignore
from agi_dw.core.planner.llm_planner import emit_plan

# Domain-agnostic callable: given (obs, plan) returns a quick action dict for WM rollout
ActionProvider = Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]]

@dataclass
class PlannerConfig:
	model: str
	backend: str = "hf"
	timeout_sec: int = 30
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"
	candidates: int = 1
	use_tot: bool = False
	seeded: bool = False
	pref_weights_path: Optional[str] = None

@dataclass
class VerifierConfig:
	model: str
	backend: str = "hf"
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"

@dataclass
class WMConfig:
	enabled: bool = False
	model_path: Optional[str] = None
	horizon: int = 1
	plan_rank: bool = False

@dataclass


VerifierConfig

# Domain-agnostic callable: given (obs, plan) returns a quick action dict for WM rollout
ActionProvider = Callable[[Dict[str, Any], Dict[str, Any]], Dict[str, Any]]

@dataclass
class PlannerConfig:
	model: str
	backend: str = "hf"
	timeout_sec: int = 30
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"
	candidates: int = 1
	use_tot: bool = False
	seeded: bool = False
	pref_weights_path: Optional[str] = None

@dataclass
class VerifierConfig:
	model: str
	backend: str = "hf"
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"

@dataclass
class WMConfig:
	enabled: bool = False
	model_path: Optional[str] = None
	horizon: int = 1
	plan_rank: bool = False

@dataclass
class ContextAugment:
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	index_k: int = 0
	index_path: Optional[str] = None
	inject_dom_policy: bool = True


WMConfig

	timeout_sec: int = 30
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"
	candidates: int = 1
	use_tot: bool = False
	seeded: bool = False
	pref_weights_path: Optional[str] = None

@dataclass
class VerifierConfig:
	model: str
	backend: str = "hf"
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"

@dataclass
class WMConfig:
	enabled: bool = False
	model_path: Optional[str] = None
	horizon: int = 1
	plan_rank: bool = False

@dataclass
class ContextAugment:
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	index_k: int = 0
	index_path: Optional[str] = None
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True

def plan_with_context(
	obs: Dict[str, Any],
	domain: str,
	planner: PlannerConfig,


ContextAugment

@dataclass
class VerifierConfig:
	model: str
	backend: str = "hf"
	adapter_dir: Optional[str] = None
	adapter_bank: Optional[str] = None
	structured_mode: str = "json"

@dataclass
class WMConfig:
	enabled: bool = False
	model_path: Optional[str] = None
	horizon: int = 1
	plan_rank: bool = False

@dataclass
class ContextAugment:
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	index_k: int = 0
	index_path: Optional[str] = None
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True

def plan_with_context(
	obs: Dict[str, Any],
	domain: str,
	planner: PlannerConfig,
	verifier: VerifierConfig,
	wm: WMConfig,
	ctx: ContextAugment,
	action_for_rollout: Optional[ActionProvider] = None,
	critic_fallback_threshold: Optional[float] = None,
	log_prompts: bool = False,
	task_name: Optional[str] = None,
	wm_quick_action_t5_model: Optional[str] = None,
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (plan, planner_info, obs_aug, mem_snippets, mem_query_ms)
	"""
	# Provide default quick action synthesis without loops importing actuators
	"""


	enabled: bool = False
	model_path: Optional[str] = None
	horizon: int = 1
	plan_rank: bool = False

@dataclass
class ContextAugment:
	use_memory: bool = False
	mem_path: Optional[str] = None
	mem_topk: int = 3
	mem_recency: float = 0.0
	mem_query: Optional[str] = None
	index_k: int = 0
	index_path: Optional[str] = None
	inject_dom_policy: bool = True
	inject_cli_policy: bool = True
	inject_caps: bool = True

def plan_with_context(
	obs: Dict[str, Any],
	domain: str,
	planner: PlannerConfig,
	verifier: VerifierConfig,
	wm: WMConfig,
	ctx: ContextAugment,
	action_for_rollout: Optional[ActionProvider] = None,
	critic_fallback_threshold: Optional[float] = None,
	log_prompts: bool = False,
	task_name: Optional[str] = None,
	wm_quick_action_t5_model: Optional[str] = None,
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any], List[Dict[str, Any]], Optional[float]]:
	"""
	Return (plan, planner_info, obs_aug, mem_snippets, mem_query_ms)
	"""
	# Provide default quick action synthesis without loops importing actuators
	"""
	Return (plan, planner_info, obs_aug, mem_snippets, mem_query_ms)
	"""
	root_dir = Path(__file__).resolve().parents[2]
	obs_aug = dict(obs)
	mem_snippets: List[Dict[str, Any]] = []
	mem_query_ms: Optional[float] = None

	# Centralized memory/index/policy augmentation
	try:
		from agi_dw.core.memory.service import MemoryAugmentConfig, augment_observation  # type: ignore
		mem_cfg = MemoryAugmentConfig(
			use_memory=bool(getattr(ctx, "use_memory", False)),
			mem_path=getattr(ctx, "mem_path", None),
			mem_topk=int(getattr(ctx, "mem_topk", 3) or 3),
			mem_recency=float(getattr(ctx, "mem_recency", 0.0) or 0.0),
			mem_query=getattr(ctx, "mem_query", None),
			index_k=int(getattr(ctx, "index_k", 0) or 0),
			index_path=getattr(ctx, "index_path", None),
			inject_dom_policy=bool(getattr(ctx, "inject_dom_policy", True)),
			inject_cli_policy=bool(getattr(ctx, "inject_cli_policy", True)),
			inject_caps=bool(getattr(ctx, "inject_caps", True)),
		)
		obs_aug, mem_snippets, mem_query_ms = augment_observation(obs, root_dir, mem_cfg)
	except Exception:
		mem_snippets = []
		mem_query_ms = None

	# Adapter bank resolution
	def _resolve_adapter(bank_name: Optional[str], kind: str) -> Optional[str]:
		if not bank_name:
			return None
		try:
			from agi_dw.core.llm.adapter_router import pick_from_bank  # type: ignore
			bank = pick_from_bank(root_dir, bank_name)
			return bank.get(kind)
		except Exception:
			return None

	planner_adapter = planner.adapter_dir or _resolve_adapter(planner.adapter_bank, "planner")
	verifier_adapter = verifier.adapter_dir or _resolve_adapter(verifier.adapter_bank, "verifier")

	# Candidate generation
	candidates: List[Dict[str, Any]] = []
	if planner.candidates > 1:
		if planner.seeded and domain == "cli":
			try:
				def _seeded(tname: Optional[str]) -> List[Dict[str, Any]]:
					if tname == "count_lines":
						return [
							{"subgoals": ["read file", "count lines"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["count lines"], "tools": ["cli"], "constraints": {"prefer_wc": True}},
						]
					elif tname == "grep_error":
						return [
							{"subgoals": ["search ERROR"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["scan file", "filter lines"], "tools": ["cli"], "constraints": {"case_sensitive": True}},
						]
					return [{"subgoals": ["plan"], "tools": ["cli"], "constraints": {}}]
				candidates = _seeded(task_name)
			except Exception:
				candidates = []
		if not candidates and planner.use_tot:
			try:
				from scripts.misc.plan_tot import generate_plan_candidates  # type: ignore
				candidates = generate_plan_candidates(obs_aug, planner.model, num_candidates=int(planner.candidates)) or []
			except Exception:
				candidates = []
		if not candidates:
			for _ in range(max(1, int(planner.candidates))):
				with trace_span("plan_emit", {"structured": str(planner.structured_mode)}):
					p = emit_plan(
						obs_aug,
						model=planner.model,
						timeout_sec=planner.timeout_sec,
						use_llm=True,
						backend=planner.backend,
						log_prompts=log_prompts,
						adapter_dir=planner_adapter,
						structured_mode=str(planner.structured_mode),
					)
				candidates.append(p)
	else:
		with trace_span("plan_emit", {"structured": str(planner.structured_mode)}):
			plan_single = emit_plan(
				obs_aug,
				model=planner.model,
				timeout_sec=planner.timeout_sec,
				use_llm=True,
				backend=planner.backend,
				log_prompts=log_prompts,
				adapter_dir=planner_adapter,
				structured_mode=str(planner.structured_mode),
			)
		candidates = [plan_single]

	# Score candidates
	scored: List[Tuple[Optional[float], Tuple[float, float], float, Dict[str, Any]]] = []
	# Load preference weights if provided
	pref_w: Optional[Dict[str, float]] = None
	try:
		if planner.pref_weights_path:
			pw_path = Path(planner.pref_weights_path)
			if pw_path.exists():
				obj = json.loads(pw_path.read_text(encoding="utf-8"))
				wv = float(obj.get("w_verifier", 0.5))
				ww = float(obj.get("w_wm", 0.5))
				ws = float(obj.get("w_self", 0.1))
				pref_w = {"w_verifier": wv, "w_wm": ww, "w_self": ws}
	except Exception:
		pref_w = None

	for p in candidates:
		vr = 0.5
		try:
			from agi_dw.core.verifier.service import VerifierServiceConfig, quick_risk  # type: ignore
			vsc = VerifierServiceConfig(
				model=verifier.model,
				backend=verifier.backend,
				adapter_dir=verifier_adapter,
				structured_mode=verifier.structured_mode,
				timeout_sec=max(2, int(planner.timeout_sec)),
				strict=False,
				calibrate=False,
			)
			vr = float(quick_risk(obs, p, vsc))
		except Exception:
			vr = 0.5
		se = 0.5
		try:
			subgoals = p.get("subgoals") if isinstance(p, dict) else []
			constraints = p.get("constraints") if isinstance(p, dict) else {}
			se = 1.0 - min(1.0, max(0, (len(subgoals) if isinstance(subgoals, list) else 3)) / 6.0)
			if isinstance(constraints, dict):
				bonus = 0.0
				if len(constraints) > 0:
					bonus = 0.1
				elif domain == "dom" and ("stay_on_url" in constraints):
					bonus = 0.1
				se = min(1.0, se + bonus)
		except Exception:
			se = 0.5
		wmr: Optional[float] = None
		if wm.enabled and wm.plan_rank:
			try:
				from agi_dw.core.world_model.service import WorldModelService  # type: ignore
				wm_path = Path(wm.model_path or "")
				if wm_path.exists():
					wm_service = WorldModelService.load_if_exists(wm_path)
					if wm_service:
						# Synthesize a quick action if none provided
						a0: Dict[str, Any] = {}
						try:
							if action_for_rollout is not None:
								a0 = action_for_rollout(obs, p) or {}
							elif domain == "dom" and wm_quick_action_t5_model:
								from agi_dw.core.actuator.t5_actuator import ActuatorT5Predictor  # type: ignore
								a0 = ActuatorT5Predictor(wm_quick_action_t5_model, mode="dom").predict_action(obs, p) or {}
							elif domain == "cli":
								from agi_dw.core.actuator.template_actuator import TemplateActuator  # type: ignore
								th = TemplateActuator().predict_action(obs, p)
								a0 = {"tool": th.tool, "args": th.args} if th else {}
						except Exception:
							a0 = {}
						roll = wm_service.rollout(obs, p, [a0] if isinstance(a0, dict) else [], horizon=max(1, int(getattr(wm, "horizon", 1) or 1)))
						wmr = float(roll.get("avg_risk", 0.5) if roll else 0.5)
			except Exception:
				wmr = None
		if pref_w is not None:
			wm_eff = float(wmr if wmr is not None else vr)
			comp = float(pref_w["w_verifier"]) * float(vr) + float(pref_w["w_wm"]) * float(wm_eff) - float(pref_w["w_self"]) * float(se)
		else:
			comp = None
		scored.append((comp, (vr, (wmr if wmr is not None else vr)), float(se), p))

	# If single plan and critic fallback is requested, try a second plan and pick lower risk
	chosen_override: Optional[Dict[str, Any]] = None
	try:
		if len(candidates) == 1 and isinstance(critic_fallback_threshold, (int, float)):
			thr = float(critic_fallback_threshold)
			pre_risk = float(scored[0][1][0]) if scored else 0.5
			if pre_risk >= thr:
				alt = emit_plan(
					obs_aug,
					model=planner.model,
					timeout_sec=planner.timeout_sec,
					use_llm=True,
					backend=planner.backend,
					log_prompts=log_prompts,
					adapter_dir=planner_adapter,
					structured_mode=str(planner.structured_mode),
				)
				try:
					from agi_dw.core.verifier.service import VerifierServiceConfig, quick_risk  # type: ignore
					vsc2 = VerifierServiceConfig(
						model=verifier.model,
						backend=verifier.backend,
						adapter_dir=verifier_adapter,
						structured_mode=verifier.structured_mode,
						timeout_sec=max(2, int(planner.timeout_sec)),
						strict=False,
						calibrate=False,
					)
					vr_alt = float(quick_risk(obs, alt, vsc2))
					# Insert alt scored row
					se_alt = 0.5
					try:
						subgoals = alt.get("subgoals") if isinstance(alt, dict) else []
						constraints = alt.get("constraints") if isinstance(alt, dict) else {}
						se_alt = 1.0 - min(1.0, max(0, (len(subgoals) if isinstance(subgoals, list) else 3)) / 6.0)
						if isinstance(constraints, dict):
							bonus = 0.0
							if len(constraints) > 0:
								bonus = 0.1
							elif domain == "dom" and ("stay_on_url" in constraints):
								bonus = 0.1
							se_alt = min(1.0, se_alt + bonus)
					except Exception:
						se_alt = 0.5
					scored.append((None, (vr_alt, vr_alt), float(se_alt), alt))
					# Strictly compare verifier risk like original CLI fallback
					if vr_alt < pre_risk:
						chosen_override = alt
				except Exception:
					pass
	except Exception:
		pass

	# Pick best
	scored.sort(key=lambda x: (x[0] if x[0] is not None else x[1][1], x[1][0] if x[0] is None else 0.0, -x[2] if x[0] is None else 0.0))
	best = scored[0]
	plan = chosen_override if chosen_override is not None else best[3]
	planner_info = {
		"candidates": len(scored),
		"verifier_risks": [float(a[1][0]) for a in scored],
		"wm_risks": [float(a[1][1]) for a in scored],
		"self_eval": [float(a[2]) for a in scored] if len(scored) > 0 else [],
		"pref_score": [float(a[0]) if a[0] is not None else None for a in scored],
		"picked_risk": {"verifier": float(best[1][0]), "wm": float(best[1][1])},
	}

	return plan, planner_info, obs_aug, mem_snippets, mem_query_ms


	try:
		from agi_dw.core.memory.service import MemoryAugmentConfig, augment_observation  # type: ignore
		mem_cfg = MemoryAugmentConfig(
			use_memory=bool(getattr(ctx, "use_memory", False)),
			mem_path=getattr(ctx, "mem_path", None),
			mem_topk=int(getattr(ctx, "mem_topk", 3) or 3),
			mem_recency=float(getattr(ctx, "mem_recency", 0.0) or 0.0),
			mem_query=getattr(ctx, "mem_query", None),
			index_k=int(getattr(ctx, "index_k", 0) or 0),
			index_path=getattr(ctx, "index_path", None),
			inject_dom_policy=bool(getattr(ctx, "inject_dom_policy", True)),
			inject_cli_policy=bool(getattr(ctx, "inject_cli_policy", True)),
			inject_caps=bool(getattr(ctx, "inject_caps", True)),
		)
		obs_aug, mem_snippets, mem_query_ms = augment_observation(obs, root_dir, mem_cfg)
	except Exception:
		mem_snippets = []
		mem_query_ms = None

	# Adapter bank resolution
	def _resolve_adapter(bank_name: Optional[str], kind: str) -> Optional[str]:
		if not bank_name:
			return None
		try:
			from agi_dw.core.llm.adapter_router import pick_from_bank  # type: ignore
			bank = pick_from_bank(root_dir, bank_name)
			return bank.get(kind)
		except Exception:
			return None

	planner_adapter = planner.adapter_dir or _resolve_adapter(planner.adapter_bank, "planner")
	verifier_adapter = verifier.adapter_dir or _resolve_adapter(verifier.adapter_bank, "verifier")

	# Candidate generation
	candidates: List[Dict[str, Any]] = []
	if planner.candidates > 1:
		if planner.seeded and domain == "cli":
			try:
				def _seeded(tname: Optional[str]) -> List[Dict[str, Any]]:
					if tname == "count_lines":
						return [
							{"subgoals": ["read file", "count lines"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["count lines"], "tools": ["cli"], "constraints": {"prefer_wc": True}},
						]
					elif tname == "grep_error":
						return [
							{"subgoals": ["search ERROR"], "tools": ["cli"], "constraints": {}},
							{"subgoals": ["scan file", "filter lines"], "tools": ["cli"], "constraints": {"case_sensitive": True}},
						]