Ctrl K
ComunidadGuías…

tests/verify_repo.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Local verification runner for caveman install surfaces."""

from __future__ import annotations

import ast
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path

# Windows consoles and piped stdout default to the ANSI code page (cp1252),
# which cannot encode the arrows, em-dashes and minus signs printed below —
# a diagnostic that crashes instead of printing is worse than useless
# (#203/#459). Replace unencodable characters rather than raising.
for _stream in (sys.stdout, sys.stderr):
    try:
        _stream.reconfigure(errors="replace")
    except Exception:
        pass

ROOT = Path(__file__).resolve().parents[1]

# A headless runner exports CLAUDE_CODE_ENTRYPOINT=sdk-*, which starts
# SessionStart under the manual policy (#377) and would fail the hook flow.
os.environ.pop("CLAUDE_CODE_ENTRYPOINT", None)


class CheckFailure(RuntimeError):
    pass


def section(title: str) -> None:
    print(f"\n== {title} ==")


def ensure(condition: bool, message: str) -> None:
    if not condition:
        raise CheckFailure(message)


def run(
    args: list[str],
    *,
    cwd: Path = ROOT,
    env: dict[str, str] | None = None,
    check: bool = True,
) -> subprocess.CompletedProcess[str]:
    merged_env = os.environ.copy()
    # Keep Python subprocess output decodable on Windows when the CLI prints Unicode.
    merged_env.setdefault("PYTHONIOENCODING", "utf-8")
    if env:
        merged_env.update(env)
    result = subprocess.run(
        args,
        cwd=cwd,
        env=merged_env,
        text=True,
        encoding="utf-8",
        stdin=subprocess.DEVNULL,
        capture_output=True,
        check=False,
    )
    if check and result.returncode != 0:
        raise CheckFailure(
            f"Command failed ({result.returncode}): {' '.join(args)}\n"
            f"stdout:\n{result.stdout}\n"
            f"stderr:\n{result.stderr}"
        )
    return result


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


def shell_path(path: Path) -> str:
    return str(path).replace("\\", "/") if os.name == "nt" else str(path)


def _frontmatter_description(path: Path) -> str:
    lines = path.read_text(encoding="utf-8").splitlines()
    ensure(lines and lines[0] == "---", f"{path} missing YAML frontmatter")

    description_lines: list[str] = []
    collecting = False
    block_indent: int | None = None
    for line in lines[1:]:
        if line == "---":
            break
        if collecting:
            stripped = line.strip()
            if not stripped:
                description_lines.append("")
                continue
            indent = len(line) - len(line.lstrip(" \t"))
            if block_indent is None:
                if indent == 0:
                    break
                block_indent = indent
            elif indent < block_indent:
                break
            description_lines.append(stripped)
            continue
        if line.startswith("description:"):
            value = line.split(":", 1)[1].strip()
            # Folded (>) and literal (|) block scalars, with optional chomping (-/+).
            if value and value[0] in ("|", ">"):
                collecting = True
                continue
            return value.strip("'\"")
    return " ".join(part for part in description_lines if part)


def verify_shipped_skills_are_documented() -> None:
    """Every skills/*/SKILL.md installs into end users' agents.

    .claude-plugin/marketplace.json sets source "./", so the plugin root is the
    repo root and Claude Code auto-discovers every skills/ subdirectory — there
    is no allowlist in plugin.json to gate it. A directory added here silently
    claims a slot in every user's skill list and competes for activation, so it
    must at least be named in the docs that tell users what they installed.
    """
    section("Shipped Skills Are Documented")

    shipped = sorted(
        path.parent.name
        for path in (ROOT / "skills").glob("*/SKILL.md")
    )
    ensure(shipped, "no skills found — check the glob")

    docs = "\n".join(
        (ROOT / name).read_text(encoding="utf-8")
        for name in ("README.md", "CLAUDE.md")
    )
    # Only count a skill as documented when it is named in a code span — a
    # path, a table cell, a slash command. A bare substring search over the
    # whole document passes on any sentence that happens to use the word, so
    # `skills/migration/` was "documented" by every prose mention of the word
    # migration, and the guard could not fail for the drift it exists to catch.
    spans = re.findall(r"`([^`\n]+)`", docs)
    # Brace groups (`skills/{a,b,c}/SKILL.md`) are how the tables list families.
    expanded = re.sub(r"[{}]", ",", "\n".join(spans))
    named = set(re.split(r"[^A-Za-z0-9-]+", expanded))
    undocumented = [name for name in shipped if name not in named]
    ensure(
        not undocumented,
        "these skills ship to users but are named in neither README.md nor "
        f"CLAUDE.md: {', '.join(undocumented)}. Document them, or move them "
        "out of skills/ so they stop auto-installing.",
    )

    print(f"All {len(shipped)} shipped skills are documented")


def verify_skills_root_holds_only_skills() -> None:
    """`skills/` is scanned wholesale, so only skill directories belong in it.

    pi loads a package's `skills/` directory as a skill root — "Packages:
    `skills/` directories or `pi.skills` entries in `package.json`" (pi docs,
    Skills > Locations) — which is a separate install path from
    `native_pack.targets` and applies even though pi is wrapped by an
    extension rather than the native pack. Its discovery rules read every
    direct `.md` child of that root as a skill candidate: one carrying
    frontmatter registers under the *parent directory* name, so a loose file
    here would ship as a skill literally called `skills`, and one without is
    opened and discarded at every launch (pi <= 0.84.2 also warned
    "description is required" for it, until earendil-works/pi#8012).
    Subdirectories are recursed for `SKILL.md` only, so build sources kept one
    level down are invisible to both rules.

    Same family as the `agents/**/*.md` and `commands/*.md` guards below: a
    directory that some host scans wholesale may not hold markdown that is not
    what the scan is looking for.
    """
    section("Skills Root Holds Only Skills")

    loose = sorted(path.name for path in (ROOT / "skills").glob("*.md"))
    ensure(
        not loose,
        "skills/*.md is read as a skill candidate by pi's package scan; "
        f"these are not skills: {', '.join(loose)}. Move them into a "
        "subdirectory (scanned for SKILL.md only) or out of skills/.",
    )

    core_source = json.loads(
        (ROOT / "skills/registry.json").read_text(encoding="utf-8")
    )["native_pack"]["core_source"]
    ensure(
        "/" in core_source and not core_source.endswith("/SKILL.md"),
        f"native_pack.core_source is {core_source!r}: the native pack's build "
        "source must live in a skills/ subdirectory and must not be named "
        "SKILL.md, or pi's scan picks it up as a skill.",
    )

    print(f"skills/ holds only skill directories; core source is {core_source}")


def verify_skill_frontmatter_upload_compatibility() -> None:
    section("Skill Frontmatter Upload Compatibility")

    skill_paths = [
        ROOT / "skills/caveman/SKILL.md",
        ROOT / "skills/ultracave/SKILL.md",
        ROOT / "skills/megacave/SKILL.md",
        ROOT / "skills/caveman-commit/SKILL.md",
        ROOT / "skills/caveman-help/SKILL.md",
        ROOT / "skills/caveman-review/SKILL.md",
        ROOT / "skills/caveman-compress/SKILL.md",
    ]
    for path in skill_paths:
        description = _frontmatter_description(path)
        ensure(
            "<" not in description and ">" not in description,
            f"{path} description contains XML-like angle brackets",
        )

    print("Skill frontmatter descriptions avoid XML-like tags")


def verify_synced_files() -> None:
    section("Synced Files")
    skill_source = ROOT / "skills/caveman/SKILL.md"

    # Every artifact sync-skill.yml mirrors, not just the first one. Checking one
    # of four let the other three drift silently between runs.
    skill_copies = [
        (ROOT / "plugins/caveman/skills/caveman/SKILL.md", skill_source),
        (ROOT / "plugins/caveman/skills/ultracave/SKILL.md", ROOT / "skills/ultracave/SKILL.md"),
        (ROOT / "plugins/caveman/skills/megacave/SKILL.md", ROOT / "skills/megacave/SKILL.md"),
        (ROOT / "plugins/caveman/skills/cavecrew/SKILL.md", ROOT / "skills/cavecrew/SKILL.md"),
        (
            ROOT / "plugins/caveman/skills/caveman-compress/SKILL.md",
            ROOT / "skills/caveman-compress/SKILL.md",
        ),
    ]
    for agent in ("cavecrew-investigator", "cavecrew-builder", "cavecrew-reviewer"):
        skill_copies.append(
            (ROOT / f"plugins/caveman/agents/{agent}.md", ROOT / f"agents/{agent}.md")
        )
    for copy, source in skill_copies:
        ensure(copy.exists(), f"Missing plugin mirror: {copy}")
        ensure(
            copy.read_text(encoding="utf-8") == source.read_text(encoding="utf-8"),
            f"Skill copy mismatch: {copy}",
        )

    with zipfile.ZipFile(ROOT / "dist" / "caveman.skill") as archive:
        ensure("caveman/SKILL.md" in archive.namelist(), "caveman.skill missing caveman/SKILL.md")
        ensure(
            archive.read("caveman/SKILL.md").decode("utf-8")
            == skill_source.read_text(encoding="utf-8"),
            "caveman.skill payload mismatch",
        )
        # EXTRA entries, not just missing ones. `zip -r` adds to an existing
        # archive, and dist/caveman.skill is tracked, so a file deleted from
        # skills/caveman/ stayed in the shipped ZIP forever — invisible to a
        # presence-only check.
        packaged = {
            name for name in archive.namelist() if not name.endswith("/")
        }
        on_disk = {
            f"caveman/{path.relative_to(ROOT / 'skills/caveman').as_posix()}"
            for path in (ROOT / "skills/caveman").rglob("*")
            if path.is_file() and "__pycache__" not in path.parts
        }
        ensure(
            packaged == on_disk,
            f"caveman.skill contents drifted: stale={sorted(packaged - on_disk)}, "
            f"missing={sorted(on_disk - packaged)}",
        )

    ensure(
        (ROOT / "installer" / "install.js").exists(),
        "installer/install.js missing — package.json bin entry would break npx github:JuliusBrussee/caveman",
    )
    ensure(
        (ROOT / "installer" / "lib" / "settings.js").exists(),
        "installer/lib/settings.js missing — installer would crash on JSONC settings.json",
    )
    # The Claude Code plugin root is the repo root (marketplace.json
    # "source": "./"), so a tracked top-level bin/ ships inside the plugin:
    # the CLI puts it on every plugin user's PATH, and claude.ai-hosted
    # marketplaces reject the plugin outright (#1035). Tracked files only —
    # gitignored files never reach a marketplace clone, and
    # scripts/install-local-cli.sh writes its shim to .local-bin/ instead.
    ensure(
        not run(["git", "ls-files", "--", "bin"]).stdout.strip(),
        "top-level bin/ ships inside the Claude plugin (plugin root = repo root) "
        "and claude.ai-hosted marketplaces reject it; see #1035",
    )

    print("Synced copies, caveman.skill zip, and installer entrypoints OK")


def verify_manifests_and_syntax() -> None:
    section("Manifests And Syntax")

    claude_manifest_path = ROOT / ".claude-plugin/plugin.json"
    manifest_paths = [
        claude_manifest_path,
        ROOT / ".claude-plugin/marketplace.json",
        ROOT / ".cursor-plugin/plugin.json",
        ROOT / "hooks/hooks-cursor.json",
        ROOT / ".codex/hooks.json",
        ROOT / "gemini-extension.json",
        ROOT / "plugins/caveman/.codex-plugin/plugin.json",
    ]
    for path in manifest_paths:
        read_json(path)

    # The repo root is also the Claude Code plugin and Gemini extension root;
    # both auto-load hooks/hooks.json, so one there would double-wire hooks.
    ensure(
        not (ROOT / "hooks/hooks.json").exists(),
        "hooks/hooks.json is auto-loaded by Claude Code and Gemini; "
        "Cursor's hook lives in hooks/hooks-cursor.json",
    )

    claude_manifest = read_json(claude_manifest_path)
    ensure(isinstance(claude_manifest, dict), "Claude plugin manifest must be an object")
    # An explicit `agents` array loads ZERO agents on Claude Code 2.1.235 —
    # `claude plugin details caveman` reports "Agents (0)" with the array and
    # "Agents (3)" without it, so the cavecrew subagents the cavecrew skill
    # delegates to simply did not exist for plugin users. The docs say
    # string|string[] is valid; the shipped loader disagrees, and a directory
    # string is rejected outright ("agents: Invalid input"). Rely on the
    # default agents/ scan instead.
    ensure(
        "agents" not in claude_manifest,
        "plugin.json must not declare `agents` — the array form loads no "
        "agents at all; the default agents/ scan is the only working path",
    )
    # Claude's default scan recurses through agents/: every markdown file there
    # becomes a user-visible subagent. Maintainer docs belong outside this tree.
    expected_agent_files = {
        "cavecrew-builder.md",
        "cavecrew-investigator.md",
        "cavecrew-reviewer.md",
    }
    all_agent_md = {
        path.relative_to(ROOT / "agents").as_posix()
        for path in (ROOT / "agents").rglob("*.md")
    }
    ensure(
        all_agent_md == expected_agent_files,
        "agents/**/*.md is scanned wholesale into every user's subagent list; "
        f"unexpected files ship as subagents: {sorted(all_agent_md - expected_agent_files)}. "
        "Move non-agent markdown outside agents/.",
    )

    # Claude Code loads commands/*.md as flat skills alongside skills/*/SKILL.md,
    # so a .md stub sharing a skill's name registers that name twice — `caveman`,
    # `caveman-commit`, `caveman-review` and `caveman-stats` each appeared twice
    # in `claude plugin details`, with a 3-line stub competing against the real
    # ruleset for the same slash command. The .toml stubs are Codex/Gemini-only
    # and are not scanned.
    skill_names = {path.parent.name for path in (ROOT / "skills").glob("*/SKILL.md")}
    command_stubs = {path.stem for path in (ROOT / "commands").glob("*.md")}
    collisions = sorted(skill_names & command_stubs)
    ensure(
        not collisions,
        f"commands/*.md shadows same-named skills/: {', '.join(collisions)}. "
        "Both register as skills, so the slash command is ambiguous.",
    )

    hook_dir = ROOT / "src/hooks"
    expected_hooks = {
        "package.json",
        "caveman-config.js",
        "caveman-parse.js",
        "caveman-activate.js",
        "caveman-mode-tracker.js",
        "caveman-stats.js",
        "caveman-statusline.sh",
        "caveman-statusline.ps1",
        "cavecrew-model-overrides.js",
        "caveman-host-session-start.js",
    }
    manifest: dict[str, str] = {}
    for line in (hook_dir / "checksums.sha256").read_text(encoding="utf-8").splitlines():
        digest, filename = line.split(maxsplit=1)
        manifest[filename] = digest
    ensure(set(manifest) == expected_hooks, "hook checksum manifest file set mismatch")
    for filename, expected in manifest.items():
        actual = hashlib.sha256((hook_dir / filename).read_bytes()).hexdigest()
        ensure(actual == expected, f"hook checksum mismatch: {filename}")

    run(["node", "--check", "src/hooks/caveman-config.js"])
    run(["node", "--check", "src/hooks/caveman-parse.js"])
    run(["node", "--check", "src/hooks/caveman-activate.js"])
    run(["node", "--check", "src/hooks/caveman-mode-tracker.js"])
    run(["node", "--check", "src/hooks/cavecrew-model-overrides.js"])
    run(["node", "--check", "src/hooks/caveman-host-session-start.js"])
    run(["node", "--check", "installer/install.js"])
    run(["node", "--check", "installer/lib/settings.js"])
    bash = shutil.which("bash")
    if bash is not None:
        run([bash, "-n", "src/hooks/install.sh"])
        run([bash, "-n", "src/hooks/uninstall.sh"])
        run([bash, "-n", "src/hooks/caveman-statusline.sh"])
    else:
        print("SKIP: Bash syntax checks require Bash; PowerShell static checks still run")

    # Ensure install/uninstall scripts include caveman-config.js
    install_sh = (ROOT / "src/hooks/install.sh").read_text(encoding="utf-8")
    uninstall_sh = (ROOT / "src/hooks/uninstall.sh").read_text(encoding="utf-8")
    ensure("caveman-config.js" in install_sh, "install.sh missing caveman-config.js")
    ensure("caveman-config.js" in uninstall_sh, "uninstall.sh missing caveman-config.js")

    print("JSON manifests and available script syntax OK")


def verify_package_contents() -> None:
    section("Package Contents")
    npm = shutil.which("npm")
    ensure(npm is not None, "npm missing — cannot audit launch tarball")
    with tempfile.TemporaryDirectory(prefix="caveman-pack-audit-") as tmp:
        result = run(
            [npm, "pack", "--dry-run", "--json", "--ignore-scripts"],
            env={"npm_config_cache": str(Path(tmp) / "npm-cache")},
        )
    payload = json.loads(result.stdout)
    ensure(isinstance(payload, list) and len(payload) == 1, "unexpected npm pack manifest")
    files = {entry["path"] for entry in payload[0]["files"]}
    required = {
        "installer/install.js",
        ".codex/codex-sessionstart.js",  # Codex always-on hook payload (#573)
        "agents/cavecrew-investigator.md",
        "agents/cavecrew-builder.md",
        "agents/cavecrew-reviewer.md",
        "skills/caveman-compress/scripts/compress.py",
        "src/hooks/caveman-parse.js",
        "src/hooks/caveman-statusline.sh",
        "dist/caveman.skill",
    }
    ensure(required <= files, f"launch tarball missing required files: {sorted(required - files)}")
    leaked = sorted(
        path for path in files
        if "__pycache__" in Path(path).parts or Path(path).suffix in {".pyc", ".pyo", ".pyd"}
    )
    ensure(not leaked, f"launch tarball contains Python cache artifacts: {leaked}")
    print(f"Launch tarball contains {len(files)} files with no Python cache artifacts")


def verify_powershell_static() -> None:
    section("PowerShell Static Checks")
    install_text = (ROOT / "src/hooks/install.ps1").read_text(encoding="utf-8")
    uninstall_text = (ROOT / "src/hooks/uninstall.ps1").read_text(encoding="utf-8")
    statusline_text = (ROOT / "src/hooks/caveman-statusline.ps1").read_text(encoding="utf-8")

    ensure("caveman-config.js" in install_text, "install.ps1 missing caveman-config.js")
    ensure("caveman-config.js" in uninstall_text, "uninstall.ps1 missing caveman-config.js")
    ensure("caveman-statusline.ps1" in install_text, "install.ps1 missing statusline.ps1")
    ensure("caveman-statusline.ps1" in uninstall_text, "uninstall.ps1 missing statusline.ps1")
    ensure("-AsHashtable" not in install_text, "install.ps1 should stay compatible with Windows PowerShell 5.1")
    ensure(
        "powershell -ExecutionPolicy Bypass -File" in install_text,
        "install.ps1 missing PowerShell statusline command",
    )
    ensure("[CAVEMAN" in statusline_text, "caveman-statusline.ps1 missing badge output")

    # uninstall.ps1's .bak probe deliberately runs a node that exits 1 whenever
    # the backup holds the comments the rewrite dropped, and it does not abort:
    # a non-zero there is the answer, not an error. `powershell.exe -File` on
    # Windows PowerShell 5.1 -- still the default shell on Windows -- ends a
    # script with the last native command's exit code, so a clean uninstall of a
    # JSONC settings.json reported failure. Clearing $LASTEXITCODE does not fix
    # it: that assignment updates the variable, not the engine state the process
    # exit code comes from. Only an explicit `exit 0` does. pwsh 7 exits 0 either
    # way, and every .ps1 case skips on the POSIX runners, so this grep is the
    # only guard here.
    ensure(
        uninstall_text.rstrip().endswith("exit 0"),
        "uninstall.ps1 must end with an explicit `exit 0`: it leaves a deliberate "
        "non-zero native exit code behind, which Windows PowerShell 5.1 returns as "
        "the script's status",
    )

    # Parity anchors between the two statusline ports and the JS source of
    # truth. There is no PowerShell behavioral test on the POSIX runners, so
    # these greps are the only thing standing between a bash-only fix and a
    # silently divergent Windows badge.
    sh_text = (ROOT / "src/hooks/caveman-statusline.sh").read_text(encoding="utf-8")
    config_text = (ROOT / "src/hooks/caveman-config.js").read_text(encoding="utf-8")

    match = re.search(r"SESSIONS_DIRNAME\s*=\s*'([^']+)'", config_text)
    ensure(match is not None, "caveman-config.js no longer defines SESSIONS_DIRNAME")
    sessions_dir = match.group(1)

    for name, text in (
        ("caveman-statusline.sh", sh_text),
        ("caveman-statusline.ps1", statusline_text),
    ):
        ensure(
            sessions_dir in text,
            f"{name} does not reference {sessions_dir} — session-scoped badge would silently "
            f"fall back to the machine-wide flag",
        )
        ensure(
            ".mode" in text,
            f"{name} missing the per-session .mode file extension",
        )

    # A durable 'off' must render nothing, not "[CAVEMAN:OFF]".
    ensure('[ "$MODE" = "off" ] && exit 0' in sh_text,
           "caveman-statusline.sh does not short-circuit on durable off")
    ensure('if ($Mode -eq "off") { exit 0 }' in statusline_text,
           "caveman-statusline.ps1 does not short-circuit on durable off")

    # The session-id whitelist must be the same rule in all three ports —
    # alphabet AND length. The bash port originally checked only the alphabet.
    id_re = re.search(r"SESSION_ID_RE\s*=\s*/\^\[A-Za-z0-9_-\]\{1,(\d+)\}\$/", config_text)
    ensure(id_re is not None, "caveman-config.js no longer defines SESSION_ID_RE in the expected shape")
    max_len = id_re.group(1)
    ensure(
        f"-gt {max_len}" in sh_text,
        f"caveman-statusline.sh does not cap the session id at {max_len} chars like the JS/ps1 ports",
    )
    ensure(
        f"{{1,{max_len}}}" in statusline_text,
        f"caveman-statusline.ps1 does not cap the session id at {max_len} chars",
    )

    # Both ports must guard against a blocking stdin read.
    ensure("[ ! -t 0 ]" in sh_text, "caveman-statusline.sh missing TTY guard on stdin read")
    ensure("-t 1" in sh_text, "caveman-statusline.sh missing bounded stdin read")
    ensure("IsInputRedirected" in statusline_text,
           "caveman-statusline.ps1 missing redirect guard on stdin read")
    ensure("Wait(1000)" in statusline_text, "caveman-statusline.ps1 missing bounded stdin read")

    # macOS ships bash 3.2, which rejects fractional read timeouts.
    ensure(
        not re.search(r"read\b[^\n]*-t\s+0\.", sh_text),
        "caveman-statusline.sh uses a fractional read timeout — bash 3.2 rejects it "
        "with 'invalid timeout specification'",
    )

    # The per-session store must be cleaned up by every uninstall path, or a
    # reinstall inherits stale modes for session ids that no longer exist.
    installer_text = (ROOT / "installer/install.js").read_text(encoding="utf-8")
    uninstall_sh_text = (ROOT / "src/hooks/uninstall.sh").read_text(encoding="utf-8")
    for name, text in (
        ("installer/install.js", installer_text),
        ("src/hooks/uninstall.sh", uninstall_sh_text),
        ("src/hooks/uninstall.ps1", uninstall_text),
    ):
        ensure(
            sessions_dir in text,
            f"{name} does not remove {sessions_dir} on uninstall",
        )

    print("Windows install path statically wired")
    print("Statusline session-state parity (sh/ps1/js) OK")


def load_compress_modules():
    sys.path.insert(0, str(ROOT / "skills/caveman-compress"))
    import scripts.benchmark  # noqa: F401
    import scripts.cli as cli
    import scripts.compress  # noqa: F401
    import scripts.detect as detect
    import scripts.validate as validate

    return cli, detect, validate


def verify_python_text_io_encoding() -> None:
    section("Python Text IO Encoding")

    # Windows defaults text IO to the ANSI code page (cp1252), so every
    # `open()` / `read_text()` / `write_text()` that omits `encoding=` reads and
    # writes in whatever the runner's locale happens to be. That is not a
    # portability nicety: `packages/sdk/parity/middleware.fixtures.json` carries
    # UTF-8 emoji, and cp1252 has no mapping for the 0x8d continuation byte at
    # offset 2105, so `engine-ci`'s windows job died with
    # `UnicodeDecodeError: 'charmap' codec can't decode byte 0x8d in position
    # 2105` while merely COLLECTING test_middleware_preflight.py. The failure
    # only surfaces on main (the windows job is skipped on PRs) and only for
    # files that happen to hold a byte cp1252 rejects, which is why ASCII-only
    # flag reads sat here undetected next to it.
    #
    # A guard beats fixing the five call sites that bite today: the next fixture
    # to gain an emoji re-breaks the build the same way, on a job nobody sees
    # until it is already red.
    skip_dirs = {
        "node_modules", ".git", ".venv", "venv", "__pycache__",
        "build", ".mypy_cache", "target", "dist",
    }
    # Routed to other repositories by CLAUDE.md, or a CI-generated mirror of a
    # source this check already covers — a violation there is not fixable from
    # this repo, so failing the build on one would only strand the next run.
    skip_prefixes = (
        "packages/agent/",
        "packages/create-caveman-agent/",
        "browse/",
        "plugins/",
    )

    offenders: list[str] = []
    scanned = 0
    for path in sorted(ROOT.rglob("*.py")):
        rel = path.relative_to(ROOT).as_posix()
        if any(part in skip_dirs for part in path.relative_to(ROOT).parts):
            continue
        if rel.startswith(skip_prefixes):
            continue
        try:
            source = path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            # A Python source that is not valid UTF-8 is the same defect one
            # layer down, and skipping it would let the file this check exists
            # to catch walk straight past the check.
            offenders.append(f"{rel}: not valid UTF-8")
            continue
        except OSError:
            continue
        try:
            tree = ast.parse(source)
        except SyntaxError:
            # Not this check's job to police syntax, and a real syntax error is
            # already loud everywhere else.
            continue
        scanned += 1
        for node in ast.walk(tree):
            if not isinstance(node, ast.Call):
                continue
            func = node.func
            name = None
            if isinstance(func, ast.Attribute) and func.attr in ("read_text", "write_text"):
                name = func.attr
            elif isinstance(func, ast.Name) and func.id == "open":
                name = "open"
            elif isinstance(func, ast.Attribute) and func.attr == "open":
                # `.open(` is overloaded: Path.open() is file IO, but
                # urllib's `build_opener(...).open(req, timeout=...)` is not,
                # and neither is os.open (a raw fd, with no encoding to pass).
                # Path.open's first positional argument is the mode string, so
                # a first argument that is anything other than a string literal
                # means this is not a file being opened.
                if isinstance(func.value, ast.Name) and func.value.id == "os":
                    continue
                if node.args and not (
                    isinstance(node.args[0], ast.Constant)
                    and isinstance(node.args[0].value, str)
                ):
                    continue
                name = "open"
            if name is None:
                continue
            if name == "open":
                mode = ""
                if len(node.args) > 1 and isinstance(node.args[1], ast.Constant):
                    mode = str(node.args[1].value)
                for kw in node.keywords:
                    if kw.arg == "mode" and isinstance(kw.value, ast.Constant):
                        mode = str(kw.value.value)
                # Binary mode has no encoding to specify.
                if "b" in mode:
                    continue
            if any(kw.arg == "encoding" for kw in node.keywords):
                continue
            offenders.append(f"{rel}:{node.lineno} {name}()")

    ensure(
        not offenders,
        "text IO without an explicit encoding= (breaks on Windows cp1252): "
        + ", ".join(offenders),
    )
    print(f"{scanned} Python sources checked; all text IO passes an explicit encoding")


def verify_test_entrypoints() -> None:
    """Every unittest file must run its whole suite when invoked directly.

    `python3 tests/test_x.py` executes the module top to bottom, so an
    `if __name__ == "__main__": unittest.main()` block placed anywhere but the
    end runs before the classes below it are defined -- those tests silently
    never run, and the file still exits 0. A file with TestCase classes and no
    guard at all is worse: it runs nothing and reports nothing. Both shapes
    have shipped here, so this is a build check rather than a convention.
    """
    section("Test Entrypoints")

    checked = 0
    for path in sorted((ROOT / "tests").glob("test_*.py")):
        tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))

        classes = [
            node for node in tree.body
            if isinstance(node, ast.ClassDef)
            and any("TestCase" in ast.dump(base) for base in node.bases)
        ]
        if not classes:
            continue
        checked += 1

        guards = [
            node for node in tree.body
            if isinstance(node, ast.If) and "__main__" in ast.dump(node.test)
        ]
        rel = path.relative_to(ROOT)
        ensure(
            guards,
            f"{rel} defines TestCase classes but has no "
            'if __name__ == "__main__": unittest.main() block, so running it '
            "directly executes nothing and still exits 0",
        )

        guard_line = guards[0].lineno
        stranded = [node.name for node in classes if node.lineno > guard_line]
        ensure(
            not stranded,
            f"{rel}: unittest.main() at line {guard_line} runs before "
            f"{len(stranded)} later TestCase class(es) are defined, so they "
            f"never run on a direct invocation: {', '.join(stranded)}. "
            "Move the __main__ block to the end of the file.",
        )

    print(f"Checked {checked} unittest files for a reachable entrypoint")


def verify_compress_fixtures() -> None:
    section("Compress Fixtures")
    _, detect, validate = load_compress_modules()

    fixtures = sorted((ROOT / "tests/caveman-compress").glob("*.original.md"))
    ensure(fixtures, "No caveman-compress fixtures found")

    for original in fixtures:
        compressed = original.with_name(original.name.replace(".original.md", ".md"))
        ensure(compressed.exists(), f"Missing compressed fixture for {original.name}")
        result = validate.validate(original, compressed)
        ensure(result.is_valid, f"Fixture validation failed for {compressed.name}: {result.errors}")
        ensure(detect.should_compress(compressed), f"Fixture should be compressible: {compressed.name}")

    print(f"Validated {len(fixtures)} caveman-compress fixture pairs")


def verify_compress_cli() -> None:
    section("Compress CLI")

    skip_result = run(
        [sys.executable, "-m", "scripts", "../../src/hooks/install.sh"],
        cwd=ROOT / "skills/caveman-compress",
        check=False,
    )
    ensure(skip_result.returncode == 0, "compress CLI skip path should exit 0")
    ensure("Detected: code" in skip_result.stdout, "compress CLI skip path missing detection output")
    ensure(
        "Skipping: file is not natural language" in skip_result.stdout,
        "compress CLI skip path missing skip output",
    )

    missing_result = run(
        [sys.executable, "-m", "scripts", "../../does-not-exist.md"],
        cwd=ROOT / "skills/caveman-compress",
        check=False,
    )
    ensure(missing_result.returncode == 1, "compress CLI missing-file path should exit 1")
    ensure("File not found" in missing_result.stdout, "compress CLI missing-file output mismatch")

    print("Compress CLI skip/error paths OK")


def verify_hook_install_flow() -> None:
    section("Claude Hook Flow")

    ensure(shutil.which("node") is not None, "node is required for hook verification")
    # Windows installs go through install.ps1. shutil.which("bash") here is often
    # WSL's System32\bash.exe, which does not share the Windows temp home this
    # check reads, so install.sh can exit 0 and leave SessionStart absent.
    if os.name == "nt":
        print("SKIP: POSIX hook install flow; Windows uses install.ps1")
        return
    bash = shutil.which("bash")
    if bash is None:
        print("SKIP: Bash hook install flow requires Bash; native PowerShell path covered statically")
        return

    with tempfile.TemporaryDirectory(prefix="caveman-verify-") as temp_root:
        temp_root_path = Path(temp_root)
        home = temp_root_path / "home"
        claude_dir = home / ".claude"
        claude_dir.mkdir(parents=True)

        existing_settings = {
            "statusLine": {"type": "command", "command": "bash /tmp/existing-statusline.sh"},
            "hooks": {"Notification": [{"hooks": [{"type": "command", "command": "echo keep-me"}]}]},
        }
        (claude_dir / "settings.json").write_text(json.dumps(existing_settings, indent=2) + "\n", encoding="utf-8")
        hook_env = {"HOME": shell_path(home), "CLAUDE_CONFIG_DIR": shell_path(claude_dir)}

        run([bash, "src/hooks/install.sh"], env=hook_env)

        settings = read_json(claude_dir / "settings.json")
        hooks = settings["hooks"]
        ensure(settings["statusLine"]["command"] == "bash /tmp/existing-statusline.sh", "install.sh clobbered existing statusLine")
        ensure("SessionStart" in hooks, "SessionStart hook missing after install")
        ensure(
            any("--subagent" in h.get("command", "") for e in hooks.get("SubagentStart", []) for h in e.get("hooks", [])),
            "SubagentStart hook missing after install",
        )
        ensure("UserPromptSubmit" in hooks, "UserPromptSubmit hook missing after install")
        ensure("SessionEnd" in hooks, "SessionEnd hook missing after install")

        activate = run(
            ["node", "src/hooks/caveman-activate.js"],
            env=hook_env,
        )
        ensure("CAVEMAN MODE ACTIVE" in activate.stdout, "activation output missing caveman banner")
        ensure("STATUSLINE SETUP NEEDED" not in activate.stdout, "activation should stay quiet when custom statusline exists")
        ensure((claude_dir / ".caveman-active").read_text(encoding="utf-8") == "caveman", "activation flag should default to caveman")

        # Test configurable default mode via CAVEMAN_DEFAULT_MODE env var
        activate_custom = run(
            ["node", "src/hooks/caveman-activate.js"],
            env={**hook_env, "CAVEMAN_DEFAULT_MODE": "ultra"},
        )
        ensure("CAVEMAN MODE ACTIVE" in activate_custom.stdout, "activation with custom default missing banner")
        ensure(
            (claude_dir / ".caveman-active").read_text(encoding="utf-8") == "ultracave",
            "CAVEMAN_DEFAULT_MODE=ultra (legacy) should set flag to ultracave",
        )
        # Test "off" mode — activation skipped, flag removed
        activate_off = run(
            ["node", "src/hooks/caveman-activate.js"],
            env={**hook_env, "CAVEMAN_DEFAULT_MODE": "off"},
        )
        ensure("CAVEMAN MODE ACTIVE" not in activate_off.stdout, "off mode should not emit caveman banner")
        ensure(not (claude_dir / ".caveman-active").exists(), "off mode should remove flag file")

        # Test mode tracker with /caveman when default is off — should NOT write flag
        subprocess.run(
            ["node", "src/hooks/caveman-mode-tracker.js"],
            cwd=ROOT,
            env={**os.environ, **hook_env, "CAVEMAN_DEFAULT_MODE": "off"},
            text=True,
            encoding="utf-8",
            input='{"prompt":"/caveman"}',
            capture_output=True,
            check=True,
        )
        ensure(not (claude_dir / ".caveman-active").exists(), "/caveman with off default should not write flag")

        # Reset back to caveman for subsequent tests
        (claude_dir / ".caveman-active").write_text("caveman", encoding="utf-8")

        run(
            ["node", "src/hooks/caveman-mode-tracker.js"],
            env=hook_env,
            check=True,
        )

        ultra_prompt = subprocess.run(
            ["node", "src/hooks/caveman-mode-tracker.js"],
            cwd=ROOT,
            env={**os.environ, **hook_env},
            text=True,
            encoding="utf-8",
            input='{"prompt":"/ultracave"}',
            capture_output=True,
            check=True,
        )
        ensure(
            "CAVEMAN MODE ACTIVE (ultracave)" in ultra_prompt.stdout,
            "mode tracker should emit active-mode reinforcement",
        )
        ensure((claude_dir / ".caveman-active").read_text(encoding="utf-8") == "ultracave", "mode tracker did not record ultracave")

        subprocess.run(
            ["node", "src/hooks/caveman-mode-tracker.js"],
            cwd=ROOT,
            env={**os.environ, **hook_env},
            text=True,
            encoding="utf-8",
            input='{"prompt":"normal mode"}',
            capture_output=True,
            check=True,
        )
        ensure(not (claude_dir / ".caveman-active").exists(), "normal mode should remove flag file")

        (claude_dir / ".caveman-active").write_text("wenyan-ultra", encoding="utf-8")
        statusline = run(
            [bash, "src/hooks/caveman-statusline.sh"],
            env=hook_env,
        )
        ensure("[MEGACAVE]" in statusline.stdout, "statusline badge output mismatch (legacy wenyan-ultra → megacave)")

        reinstall = run([bash, "src/hooks/install.sh"], env=hook_env)
        ensure("Nothing to do" in reinstall.stdout, "install.sh should be idempotent")

        run([bash, "src/hooks/uninstall.sh"], env=hook_env)
        settings_after = read_json(claude_dir / "settings.json")
        ensure(settings_after == existing_settings, "uninstall.sh did not restore non-caveman settings")
        ensure(not (claude_dir / ".caveman-active").exists(), "uninstall.sh should remove flag file")

    with tempfile.TemporaryDirectory(prefix="caveman-verify-fresh-") as temp_root:
        home = Path(temp_root) / "home"
        claude_dir = home / ".claude"
        hook_env = {"HOME": shell_path(home), "CLAUDE_CONFIG_DIR": shell_path(claude_dir)}
        run([bash, "src/hooks/install.sh"], env=hook_env)
        settings = read_json(claude_dir / "settings.json")
        ensure("statusLine" in settings, "fresh install should configure statusline")
        activate = run(["node", "src/hooks/caveman-activate.js"], env=hook_env)
        ensure("STATUSLINE SETUP NEEDED" not in activate.stdout, "fresh install should not nudge for statusline")
        run([bash, "src/hooks/uninstall.sh"], env=hook_env)
        ensure(read_json(claude_dir / "settings.json") == {}, "fresh uninstall should leave empty settings")

    # The settings.json backup must be written ONCE. Without the guard, a
    # --force reinstall copies the already-merged file over the only
    # pre-caveman copy, so the user's recovery path silently becomes a
    # caveman-flavoured settings.json.
    with tempfile.TemporaryDirectory(prefix="caveman-verify-bak-") as temp_root:
        home = Path(temp_root) / "home"
        claude_dir = home / ".claude"
        claude_dir.mkdir(parents=True)
        hook_env = {"HOME": shell_path(home), "CLAUDE_CONFIG_DIR": shell_path(claude_dir)}
        pristine = {"theme": "dark", "myImportantSetting": True}
        (claude_dir / "settings.json").write_text(
            json.dumps(pristine, indent=2) + "\n", encoding="utf-8"
        )
        for _ in range(3):
            run([bash, "src/hooks/install.sh", "--force"], env=hook_env)
        ensure(
            read_json(claude_dir / "settings.json.bak") == pristine,
            "install.sh --force must not overwrite the pre-caveman settings.json.bak",
        )

    # #593: uninstall must own ONLY its own scripts. A user hook that merely
    # mentions "caveman" in a path, and a foreign handler sharing a matcher
    # group with ours, both have to survive.
    with tempfile.TemporaryDirectory(prefix="caveman-verify-foreign-") as temp_root:
        home = Path(temp_root) / "home"
        claude_dir = home / ".claude"
        claude_dir.mkdir(parents=True)
        hook_env = {"HOME": shell_path(home), "CLAUDE_CONFIG_DIR": shell_path(claude_dir)}
        run([bash, "src/hooks/install.sh"], env=hook_env)

        settings = read_json(claude_dir / "settings.json")
        foreign = {"type": "command", "command": 'bash "/home/me/caveman-notes-sync.sh"'}
        settings["hooks"]["SessionStart"].append({"hooks": [dict(foreign)]})
        # Foreign handler sharing OUR entry's matcher group.
        settings["hooks"]["UserPromptSubmit"][0]["hooks"].append(dict(foreign))
        (claude_dir / "settings.json").write_text(
            json.dumps(settings, indent=2) + "\n", encoding="utf-8"
        )

        run([bash, "src/hooks/uninstall.sh"], env=hook_env)
        after = read_json(claude_dir / "settings.json")
        remaining = [
            h
            for entries in after.get("hooks", {}).values()
            for entry in entries
            for h in entry.get("hooks", [])
        ]
        ensure(
            remaining == [foreign, foreign],
            f"uninstall.sh must preserve foreign hooks mentioning 'caveman'; got {remaining}",
        )

    print("Claude hook install/uninstall flow OK")


def verify_license_boundaries() -> None:
    section("License Boundaries")

    # Whole repo is Apache-2.0 from Caveman 3.0.0. Root LICENSE is the verbatim
    # apache.org text (sha256 of the LF form); every other tracked LICENSE or
    # LICENSE.* is a byte copy. Upstream third-party texts are named NOTICE or
    # *_LICENSE.txt and are not matched.
    apache = (ROOT / "LICENSE").read_bytes().replace(b"\r\n", b"\n")
    ensure(
        hashlib.sha256(apache).hexdigest() == "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
        "root LICENSE is not the canonical Apache License 2.0 text",
    )
    notice = (ROOT / "NOTICE").read_text(encoding="utf-8")
    ensure("Copyright 2026 Julius Brussee" in notice, "root NOTICE missing copyright line")
    # Pre-3.0.0 contributions to the formerly MIT parts keep their MIT notice:
    # LICENSE-MIT is that verbatim text (not matched by the LICENSE copy check).
    ensure(
        hashlib.sha256((ROOT / "LICENSE-MIT").read_bytes().replace(b"\r\n", b"\n")).hexdigest()
        == "5eb826cd03151bcc7cce3f80d40e87733237fedfc6c36d6908aca5fd650a0bdb",
        "LICENSE-MIT is not the verbatim pre-3.0.0 MIT License text",
    )
    ensure("LICENSE-MIT" in notice, "root NOTICE must point to LICENSE-MIT")

    tracked = [p for p in run(["git", "ls-files", "-z"]).stdout.split("\0") if p and (ROOT / p).is_file()]
    license_files = [
        p for p in tracked
        if re.fullmatch(r"LICENSE(\.[^/]+)?", Path(p).name) and "vendor" not in Path(p).parts
    ]
    checked = license_files
    mismatched = sorted(p for p in checked if (ROOT / p).read_bytes().replace(b"\r\n", b"\n") != apache)
    ensure(not mismatched, f"LICENSE files differ from root Apache-2.0 LICENSE: {mismatched}")
    for relative in ("engine", "proxy", "browse", "mcp", "shrink", "mem", "shared/platform"):
        ensure((ROOT / relative / "LICENSE").is_file(), f"runtime directory missing LICENSE: {relative}")

    wrong_metadata = []
    for p in tracked:
        name = Path(p).name
        if name not in {"package.json", "plugin.json", "pyproject.toml"}:
            continue
        text = (ROOT / p).read_text(encoding="utf-8")
        if name == "pyproject.toml":
            match = re.search(r'^license = "([^"]*)"', text, re.M)
            declared = match.group(1) if match else None
        else:
            declared = json.loads(text).get("license")
        if declared is not None and declared != "Apache-2.0":
            wrong_metadata.append(f"{p}={declared}")
    ensure(not wrong_metadata, f"license metadata must be Apache-2.0: {wrong_metadata}")

    # The old split license survives only as history.
    history = {"LICENSING.md", "tests/verify_repo.py"}
    stale = re.compile(r"\bBUSL\b|\bBSL\b|Business Source")
    leftovers = []
    for p in tracked:
        if p in history or Path(p).name == "CHANGELOG.md":
            continue
        try:
            text = (ROOT / p).read_text(encoding="utf-8")
        except UnicodeDecodeError:
            continue
        if stale.search(text):
            leftovers.append(p)
    ensure(not leftovers, f"BSL license text outside history files: {leftovers}")

    readme = (ROOT / "README.md").read_text(encoding="utf-8")
    ensure("[Apache-2.0](./LICENSE)" in readme, "README license section must name Apache-2.0")

    print(f"{len(checked)} LICENSE files match root Apache-2.0 text; no BSL text outside history")


def _top_changelog_version(path: Path) -> str:
    # release-packages.yml takes the first word of a `## ` heading as the
    # version and uses that section as the GitHub Release notes.
    heading = next((line for line in path.read_text(encoding="utf-8").splitlines() if line.startswith("## ")), "")
    match = re.fullmatch(r"## \[?([0-9][^\]\s]*)\]? — \d{4}-\d{2}-\d{2}", heading)
    ensure(match is not None, f"{path.relative_to(ROOT)} must start with '## <version> — <YYYY-MM-DD>', not {heading!r}")
    return match.group(1)


def verify_release_metadata() -> None:
    section("Release Metadata")
    import tomllib

    def pyproject(relative: str) -> dict:
        return tomllib.loads((ROOT / relative / "pyproject.toml").read_text(encoding="utf-8"))["project"]

    def npm(relative: str) -> dict:
        return read_json(ROOT / relative / "package.json")

    def literal(relative: str, pattern: str) -> str | None:
        match = re.search(pattern, (ROOT / relative).read_text(encoding="utf-8"), re.M)
        return match.group(1) if match else None

    sdk_ts = npm("packages/sdk/typescript")["version"]
    sdk_py = pyproject("packages/sdk/python")["version"]
    mw_ts = npm("packages/middleware/typescript")
    mw_py = pyproject("packages/middleware/python")

    # Every version string a release carries agrees with the package metadata.
    agree = {
        "packages/sdk/typescript": {sdk_ts, literal("packages/sdk/typescript/src/middleware/types.ts", r"^export const SDK_VERSION = '([^']+)';")},
        "packages/sdk/python": {sdk_py},
        "packages/middleware/typescript": {mw_ts["version"], literal("packages/middleware/typescript/src/common.ts", r"^export const MIDDLEWARE_VERSION = '([^']+)';")},
        "packages/middleware/python": {mw_py["version"]},
        "packages/shared/contracts": {npm("packages/shared/contracts")["version"]},
    }
    for relative, versions in agree.items():
        versions.add(_top_changelog_version(ROOT / relative / "CHANGELOG.md"))
        ensure(len(versions) == 1, f"{relative}: package, version constant and top CHANGELOG heading disagree: {sorted(map(str, versions))}")
    # Python reads __version__ from the installed distribution, never a literal.
    ensure(
        literal("packages/middleware/python/caveman_middleware/__init__.py", r"^(__version__\s*=)") is None,
        "caveman_middleware.__version__ must come from importlib.metadata, not a literal",
    )
    for relative, project in (("packages/sdk/python", pyproject("packages/sdk/python")), ("packages/middleware/python", mw_py)):
        stable = re.fullmatch(r"\d+\.\d+\.\d+", project["version"]) is not None
        alpha = any("Development Status :: 3 - Alpha" in c for c in project.get("classifiers", []))
        ensure(not (stable and alpha), f"{relative}: stable version {project['version']} still classified Alpha")

    # R-1: each middleware package's SDK floor is exactly the repo SDK version, so
    # it can never resolve an SDK that lacks the APIs it imports.
    ts_range = mw_ts["dependencies"]["@caveman-ai/sdk"]
    ensure(ts_range in {"workspace:^", f"^{sdk_ts}"}, f"@caveman-ai/middleware SDK range {ts_range!r} must pack as ^{sdk_ts}")
    floors = [re.fullmatch(r"caveman-sdk>=([0-9.]+),<\d+", d) for d in mw_py["dependencies"] if d.startswith("caveman-sdk")]
    ensure(len(floors) == 1 and floors[0] is not None, f"caveman-middleware needs one 'caveman-sdk>=X,<Y' dependency: {mw_py['dependencies']}")
    floor = floors[0].group(1).split(".")
    ensure(floor + ["0"] * (3 - len(floor)) == sdk_py.split("."), f"caveman-middleware SDK floor {floors[0].group(1)} != repo caveman-sdk {sdk_py}")

    # Quickstart install lines name the versions this commit releases.
    pins = {
        "packages/sdk/typescript/README.md": {r"@caveman-ai/sdk@([0-9][^\s`]*)": sdk_ts},
        "packages/sdk/python/README.md": {r"caveman-sdk==([0-9][^\s'`]*)": sdk_py},
        "packages/middleware/typescript/README.md": {r"@caveman-ai/sdk@([0-9][^\s`]*)": sdk_ts, r"@caveman-ai/middleware@([0-9][^\s`]*)": mw_ts["version"]},
        "packages/middleware/python/README.md": {r"caveman-sdk==([0-9][^\s'`]*)": sdk_py, r"caveman-middleware\[[^\]]*\]==([0-9][^\s'`]*)": mw_py["version"]},
    }
    for relative, patterns in pins.items():
        text = (ROOT / relative).read_text(encoding="utf-8")
        for pattern, want in patterns.items():
            found = set(re.findall(pattern, text))
            ensure(found == {want}, f"{relative}: install pins {sorted(found)} must all be {want}")

    # Every published artifact carries LICENSE and NOTICE: npm packages list both
    # in `files`, Python packages in `license-files`.
    unpublished = {
        "packages/device-auth", "mem/js", "shared/provider-catalog", "src/hooks",  # not published from here
    }
    tracked = run(["git", "ls-files", "-z", "--", "*package.json", "*pyproject.toml"]).stdout.split("\0")
    missing = []
    for p in sorted(filter(None, tracked)):
        directory = Path(p).parent.as_posix()
        if directory in unpublished or {"node_modules", "fixtures", "testdata", "tests"} & set(Path(p).parts):
            continue
        if Path(p).name == "package.json":
            manifest = read_json(ROOT / p)
            if manifest.get("private") or "name" not in manifest:
                continue
            listed = set(manifest.get("files", ["LICENSE", "NOTICE"]))
        else:
            listed = set(tomllib.loads((ROOT / p).read_text(encoding="utf-8")).get("project", {}).get("license-files", []))
        for name in ("LICENSE", "NOTICE"):
            # npm packs LICENSE whatever `files` says; NOTICE only when listed.
            shipped = name in listed or (name == "LICENSE" and Path(p).name == "package.json")
            if not shipped or not (ROOT / directory / name).is_file():
                missing.append(f"{directory}/{name}")
        notice = ROOT / directory / "NOTICE"
        if notice.is_file() and "Copyright 2026 Julius Brussee" not in notice.read_text(encoding="utf-8"):
            missing.append(f"{directory}/NOTICE (copyright line)")
    ensure(not missing, f"published packages must ship LICENSE and NOTICE: {missing}")

    print(f"SDK {sdk_ts}/{sdk_py}, middleware {mw_ts['version']}/{mw_py['version']}: versions, SDK floors, pins and notices agree")


def verify_untrusted_git_invocations() -> None:
    section("Untrusted Git Invocations")

    # A repository's own .git/config names programs git will run — core.fsmonitor
    # runs while the index is read or refreshed, so `ls-files` and `status` are
    # each enough to execute attacker code the moment a session opens inside a
    # freshly cloned repo. Every git call against a user working directory must
    # go through the hardened wrappers, which pass `-c` overrides that beat
    # repository config. This catches a NEW call site, which is how the guard
    # was incomplete when it first landed.
    wrappers = {
        Path("proxy/internal/gitsafe/gitsafe.go"),
        Path("packages/cli/src/git-safe.ts"),
    }
    sources = [
        *(ROOT / "proxy").rglob("*.go"),
        *(ROOT / "packages/cli/src").rglob("*.ts"),
    ]
    raw = re.compile(r'"git"\s*,\s*\[?\s*"-C"')
    offenders = []
    for path in sources:
        relative = path.relative_to(ROOT)
        if relative in wrappers or path.name.endswith("_test.go") or ".generated." in path.name:
            continue
        if raw.search(path.read_text(encoding="utf-8")):
            offenders.append(str(relative))
    ensure(
        not offenders,
        "git invoked against a working directory without the hardened wrapper: " + ", ".join(sorted(offenders)),
    )

    for wrapper in wrappers:
        text = (ROOT / wrapper).read_text(encoding="utf-8")
        for override in ("core.fsmonitor=false", "core.hooksPath=", "protocol.ext.allow=never"):
            ensure(override in text, f"{wrapper} dropped a required git hardening override: {override}")

    print(f"{len(sources)} sources checked; git only reaches untrusted repositories through {len(wrappers)} hardened wrappers")


def verify_hidden_child_windows() -> None:
    """#1214: no host-side child process may open a console window on Windows.

    A hook or in-process extension runs inside a host that often has no console
    of its own (Claude Code desktop, OpenCode under a multiplexer, a GUI
    launcher). Windows then gives every console child a NEW console window, so
    the user gets one flashing window per prompt, tool call or bash command.
    `windowsHide: true` suppresses it and is ignored on every other platform.

    These files have no shared runtime with the CLI that already sets the
    option, so each spawn carries its own literal — exactly the shape that
    drifts. The generated plugin templates are guarded behaviorally instead, by
    assertHidesChildWindows in packages/cli/tests, because they are emitted as
    text and only the written artifact proves what a user actually runs.
    """
    section("Hidden Child Windows (Windows console flash)")
    call_re = re.compile(r"\b(?:execFileSync|execSync|spawnSync|execFile|spawn)\s*\(")
    targets = [
        ROOT / "src/hooks/caveman-mode-tracker.js",
        ROOT / "src/mcp-servers/caveman-shrink/index.js",
        ROOT / "packages/pi-extension/src/recovery.ts",
        ROOT / "packages/pi-extension/src/lifecycle.ts",
    ]
    for path in targets:
        text = path.read_text(encoding="utf-8")
        # An import/destructure of the API is not a call site.
        body = "\n".join(
            line for line in text.splitlines()
            if "require(" not in line and not line.lstrip().startswith("import ")
        )
        calls = len(call_re.findall(body))
        if calls == 0:
            continue
        hidden = body.count("windowsHide: true") + body.count("getSpawnOptions()")
        ensure(
            hidden >= calls,
            f"{path.relative_to(ROOT)}: {calls} child-process call(s) but {hidden} "
            "windowsHide/getSpawnOptions — a host child must not open a console window (#1214)",
        )
        print(f"  {path.relative_to(ROOT)}: {calls} call(s) hidden")
    print("Hidden child window checks OK")


def verify_wrap_recovery_tool_name() -> None:
    """The Go recovery allowlist must spell the wrap plugin's tool exactly (#1432).

    `caveman wrap claude` registers the recovery MCP server inside an ephemeral
    Claude Code plugin. Claude Code keys a plugin's MCP server as
    `plugin:<plugin>:<server>`, then replaces every character outside
    [A-Za-z0-9_-] with `_` to build `mcp__<server>__<tool>`. Nothing at runtime
    ties that spelling to proxy/providers/adapter.go, and when the two drifted
    apart a retrieve result stopped being exempt from recompression and the
    agent was handed back the elision it was escaping. Renaming the plugin or
    the server here must fail the build, not ship a dead allowlist entry.
    """
    section("Wrap recovery tool name")

    cli = (ROOT / "packages/cli/src/index.ts").read_text(encoding="utf-8")
    builder = cli.split("function buildClaudeEphemeralPlugin(", 1)
    ensure(len(builder) == 2, "buildClaudeEphemeralPlugin not found in packages/cli/src/index.ts")
    body = builder[1].split("\nfunction ", 1)[0]

    plugin_match = re.search(r'name:\s*"([^"]+)"', body)
    ensure(plugin_match is not None, "buildClaudeEphemeralPlugin declares no plugin name")
    plugin_name = plugin_match.group(1)

    server_match = re.search(r"mcpServers\.([A-Za-z0-9_]+)\s*=", body)
    ensure(server_match is not None, "buildClaudeEphemeralPlugin registers no recovery MCP server")
    server_name = server_match.group(1)

    def sanitize(value: str) -> str:
        return re.sub(r"[^A-Za-z0-9_-]", "_", value)

    expected = f"mcp__{sanitize(f'plugin:{plugin_name}:{server_name}')}__{sanitize('caveman_retrieve')}"

    adapter = (ROOT / "proxy/providers/adapter.go").read_text(encoding="utf-8")
    declared = re.search(r'claudeWrapRecoveryToolName\s*=\s*"([^"]+)"', adapter)
    ensure(declared is not None, "proxy/providers/adapter.go declares no claudeWrapRecoveryToolName")
    ensure(
        declared.group(1) == expected,
        "claudeWrapRecoveryToolName is "
        f"{declared.group(1)!r} but plugin {plugin_name!r} + server {server_name!r} yields {expected!r}",
    )
    ensure(
        "claudeWrapRecoveryToolName" in adapter.split("func IsRecoveryToolName(", 1)[-1],
        "claudeWrapRecoveryToolName is declared but IsRecoveryToolName does not accept it",
    )

    print(f"  {plugin_name}/{server_name} -> {expected}")


def main() -> int:
    checks = [
        verify_license_boundaries,
        verify_release_metadata,
        verify_untrusted_git_invocations,
        verify_shipped_skills_are_documented,
        verify_skills_root_holds_only_skills,
        verify_skill_frontmatter_upload_compatibility,
        verify_synced_files,
        verify_manifests_and_syntax,
        verify_package_contents,
        verify_powershell_static,
        verify_hidden_child_windows,
        verify_python_text_io_encoding,
        verify_test_entrypoints,
        verify_compress_fixtures,
        verify_compress_cli,
        verify_hook_install_flow,
        verify_wrap_recovery_tool_name,
    ]

    try:
        for check in checks:
            check()
    except CheckFailure as exc:
        print(f"\nFAIL: {exc}", file=sys.stderr)
        return 1

    print("\nAll local verification checks passed")
    return 0


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