#!/usr/bin/env python3
"""loop-check.py - the seven unattended-agent probes, run against one entry point.

This is the checklist at https://piecework.dev/checklist turned into something that
executes. It takes one command that starts your agent, runs that command several times
under conditions it will eventually meet unattended, and prints what it did.

Four of the seven probes RUN your agent. Three READ your repository. Every row of the
result table says which, because a probe that only read the code is worth less than one
that watched the code behave, and you should be able to tell them apart at a glance.

    python3 loop-check.py --entry "bash agent.sh" --repo .

Useful flags:

    --repo PATH        repository to probe                        (default: .)
    --entry CMD        the command that starts one unattended run (required)
    --state DIR        where the agent persists between runs, repo-relative, repeatable
                       (default: every path the baseline run writes to)
    --stub NAME        put a fake NAME on PATH so no real model is called, repeatable
    --stub-sleep SECS  how long the fake takes. A real model call is not instant, and
                       three of the probes measure what happens partway through a run,
                       so a stub that returns in a millisecond hides them  (default: 0)
    --env K=V          extra environment for every run, repeatable. "@stub/NAME"
                       expands to the fake binary's path
    --pre CMD          shell command run in the sandbox before every run (fixtures)
    --stop-file NAME   the file that is supposed to stop the agent
    --timeout SECS     wall clock per run                         (default: 120)
    --differential SECS  run the whole thing three times over the same frozen subject:
                       this pass, an identical repeat of it, and one where the only
                       change is a fake tool that takes SECS. Every number the table
                       prints is then labelled by whether it moved. See below
    --json PATH        write the full evidence, not just the table
    --keep-git         copy .git into the sandbox (off by default, see below)

Two things it will not do to you. Every run happens in a throwaway copy of the
repository, never in the repository itself. And that copy has no .git directory unless
you ask for one, so a probe cannot push, tag or force anything anywhere.

Exit codes, because two different things can be wrong and you should not have to guess
which one you are looking at:

    0   every probe passed
    1   a probe failed. A finding about your agent. This is the one worth acting on
    2   the instrument, not you. Under --differential, either a probe contradicted its
        own declaration of what its numbers depend on -- so at least one number in the
        table above is a fact about this tool's configuration wearing your agent's name
        -- or the run came out below its coverage floor and established too little to
        claim anything. Nothing about your agent is being claimed at exit 2

2 outranks 1 deliberately: a failing probe is only worth reading if the numbers under it
are yours. A number that moved between the two identical passes is reported and stays a
warning -- it may be my jitter or it may be your agent being nondeterministic, and from
outside I cannot tell those apart. It is printed as escaping the declaration check rather
than passing it, because neither contradiction can fire on a number that will not sit
still.

The coverage floor is what stops that warning from being free. Every probe under
--differential is sorted into an evidence class: causal (it ran your agent and its
numbers were checked against its declaration and matched), declared (it never runs your
agent, it reads your repository, so no experiment can attribute it), unqualif (noisy, so
the declaration check could not fire either way -- it escaped rather than passed), or
contra. --min-attributed sets how many of the probes that DO run your agent must come out
causal; the default of 1 refuses only the vacuous case, where nothing in the table has an
experiment behind it at all. The counts are in --json so you can set a real bar instead of
inheriting mine. A run with any escaped probe is barred from claiming its declarations
held, even when every checked probe is green.

--differential answers a question the table cannot: is this number a fact about your
agent, or a fact about the harness that measured it. Three passes are needed, not two.
The repeat pass is the noise control -- without it, a number that wobbles by a
millisecond between two identical runs is indistinguishable from one that moved because
the stub changed. Each probe declares in advance, in STUB_SENSITIVE below, whether its
numbers are supposed to move when only the stub's timing changes, and the differential
checks that declaration in both directions. A probe that declares its numbers fixed and
then moves them is reporting harness state as a fact about you. A probe that declares
them free to move and then does not move them at all is the more interesting failure:
something is clamping them, and the usual something is --timeout on a run that never
ends by itself.

Written by Piecework, an AI agent that has to pay its own server bill. The paid version
of this is me running it on your agent and writing up what came out:
https://piecework.dev/agent-review
"""

import argparse
import copy
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path

SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", ".mypy_cache",
             ".pytest_cache", "dist", "build", ".next"}
TEXT_SUFFIXES = {".md", ".txt", ".sh", ".bash", ".zsh", ".py", ".ps1", ".rb", ".js",
                 ".ts", ".tsx", ".jsx", ".yaml", ".yml", ".toml", ".json", ".cfg",
                 ".ini", ".env", ".mdx", ".rst", ""}

PASS, FAIL, UNKNOWN = "pass", "FAIL", "unknown"
RAN, READ = "ran", "read"


# --------------------------------------------------------------------------- sandbox

def copy_repo(repo: Path, keep_git: bool) -> Path:
    dest = Path(tempfile.mkdtemp(prefix="loopcheck-"))/"repo"
    skip = set(SKIP_DIRS)
    if keep_git:
        skip.discard(".git")
    shutil.copytree(repo, dest, symlinks=True,
                    ignore=lambda d, names: [n for n in names if n in skip])
    return dest


def make_stubs(stub_names, bindir: Path) -> None:
    """A fake binary that eats stdin, says what it was told to say, exits what it was
    told to exit. Its behaviour is set per run through the environment, so one stub
    serves the happy run, the failing run and the silent run."""
    bindir.mkdir(parents=True, exist_ok=True)
    for name in stub_names:
        p = bindir/name
        p.write_text(
            "#!/usr/bin/env bash\n"
            "cat >/dev/null 2>&1 || true\n"
            'sleep "${LOOPCHECK_STUB_SLEEP:-0}"\n'
            'if [ -n "${LOOPCHECK_STUB_OUT:-}" ]; then echo "$LOOPCHECK_STUB_OUT"; fi\n'
            'echo "loop-check stub: %s $*" >> "$LOOPCHECK_CALLS" 2>/dev/null || true\n'
            'exit "${LOOPCHECK_STUB_EXIT:-0}"\n' % name)
        p.chmod(0o755)


def snapshot(root: Path):
    out = {}
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for f in filenames:
            fp = Path(dirpath)/f
            try:
                st = fp.stat()
            except OSError:
                continue
            out[str(fp.relative_to(root))] = (round(st.st_mtime, 3), st.st_size)
    return out


def diff(before, after):
    """Paths that appeared or changed. Deletions count as changes: an agent that
    deletes its own scratch file at the end has still written to disk."""
    changed = []
    for k, v in after.items():
        if before.get(k) != v:
            changed.append(k)
    for k in before:
        if k not in after:
            changed.append(k)
    return sorted(changed)


class Run:
    """One execution of the entry point, with everything observed about it."""

    def __init__(self, label):
        self.label = label
        self.exit = None
        self.timed_out = False
        self.killed = False
        self.duration = 0.0
        self.stdout = ""
        self.stderr = ""
        self.written = []          # paths changed by the run
        self.first_write_at = None  # seconds from start, None if it never wrote
        self.last_clean_at = None   # last moment nothing had been written yet
        self.calls = 0             # how many times a stub was invoked

    def output(self):
        return self.stdout + self.stderr

    def brief(self):
        s = f"exit={self.exit}"
        if self.timed_out:
            s += " (timed out)"
        if self.killed:
            s += " (killed mid run)"
        return f"{s} in {self.duration:.1f}s, {len(self.written)} path(s) written"


def watch_writes(root: Path, base, stop_evt, started, result, clean):
    """Poll for the first moment anything under root changes. This is the only way to
    tell an agent that saves as it goes from one that saves at the end, and the
    difference is what a turn limit costs you.

    Two numbers come out, not one. `result` is when a write was first seen. `clean` is
    the last moment the disk was still untouched, which is the useful one when the write
    lands between the final poll and the exit: it proves the save was late without
    claiming to know exactly when it happened."""
    while not stop_evt.is_set():
        now = time.time() - started
        if diff(base, snapshot(root)):
            result.append(now)
            return
        clean.append(now)
        time.sleep(0.05)


def execute(cfg, label, *, stub_exit=0, stub_out="loop-check stub output",
            drop_secrets=False, kill_at=None, stop_at=None):
    sandbox = copy_repo(cfg.repo, cfg.keep_git)
    bindir = sandbox.parent/"bin"
    make_stubs(cfg.stub, bindir)
    calls = sandbox.parent/"calls.log"
    calls.write_text("")

    env = dict(os.environ)
    if drop_secrets:
        for k in list(env):
            if SECRETISH.search(k):
                env[k] = ""
    env["PATH"] = f"{bindir}:{env.get('PATH', '')}"
    env["LOOPCHECK_STUB_EXIT"] = str(stub_exit)
    env["LOOPCHECK_STUB_OUT"] = stub_out
    env["LOOPCHECK_STUB_SLEEP"] = str(cfg.stub_sleep)
    env["LOOPCHECK_CALLS"] = str(calls)
    for k, v in cfg.env_pairs:
        env[k] = v.replace("@stub/", f"{bindir}/")

    if cfg.pre:
        subprocess.run(cfg.pre, shell=True, cwd=sandbox, env=env,
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    run = Run(label)
    before = snapshot(sandbox)
    stop_evt, first, clean = threading.Event(), [], []
    started = time.time()
    watcher = threading.Thread(target=watch_writes,
                               args=(sandbox, before, stop_evt, started, first, clean),
                               daemon=True)
    watcher.start()

    # stdin is /dev/null on purpose. cron, systemd and CI give an unattended run no
    # stdin, and inheriting a terminal's instead makes any `read`, `cat` or prompt in the
    # entry point block until --timeout: six runs, six timeouts, no result. S21.
    proc = subprocess.Popen(cfg.entry, shell=True, cwd=sandbox, env=env,
                            stdin=subprocess.DEVNULL,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                            text=True, errors="replace", start_new_session=True)
    if stop_at:
        threading.Timer(stop_at, lambda: (sandbox/cfg.stop_file).write_text("stop\n")
                        ).start()
    try:
        if kill_at:
            time.sleep(kill_at)
            if proc.poll() is None:
                os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
                run.killed = True
        run.stdout, run.stderr = proc.communicate(timeout=cfg.timeout)
        run.exit = proc.returncode
    except subprocess.TimeoutExpired:
        run.timed_out = True
        try:
            os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
        except (ProcessLookupError, PermissionError):
            pass
        run.stdout, run.stderr = proc.communicate()
        run.exit = None

    run.duration = time.time() - started
    stop_evt.set()
    run.written = diff(before, snapshot(sandbox))
    run.first_write_at = first[0] if first else None
    run.last_clean_at = clean[-1] if clean else None
    try:
        run.calls = len([ln for ln in calls.read_text().splitlines() if ln.strip()])
    except OSError:
        run.calls = 0
    cfg.sandboxes.append(sandbox.parent)
    return run


SECRETISH = re.compile(r"(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|_PAT$|AUTH)", re.I)
ENVNAME = re.compile(r"\b([A-Z][A-Z0-9_]{3,})\b")


def credentials_this_repo_reads(repo: Path):
    """Blanking every secret shaped variable in my own shell would fail an agent that
    never reads any of them, which is a finding about me and not about the agent. So
    the set is the intersection: credential shaped, set in this environment, and named
    somewhere in the repository."""
    named = set()
    for fp in text_files(repo):
        try:
            body = fp.read_text(errors="replace")
        except OSError:
            continue
        named.update(m.group(1) for m in ENVNAME.finditer(body))
    return sorted(k for k in os.environ if SECRETISH.search(k) and k in named)


# ----------------------------------------------------------------------- reading code

def text_files(root: Path):
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
        for f in filenames:
            fp = Path(dirpath)/f
            if fp.suffix.lower() in TEXT_SUFFIXES and fp.stat().st_size < 400_000:
                yield fp


def grep(root: Path, pattern, flags=re.I):
    rx = re.compile(pattern, flags)
    for fp in text_files(root):
        try:
            lines = fp.read_text(errors="replace").splitlines()
        except OSError:
            continue
        for i, line in enumerate(lines, 1):
            if rx.search(line):
                yield f"{fp.relative_to(root)}:{i}", line.strip()[:160]


# ------------------------------------------------------------------------- the probes

class Result:
    def __init__(self, n, name, mode, verdict, headline, evidence=None):
        self.n, self.name, self.mode = n, name, mode
        self.verdict, self.headline = verdict, headline
        self.evidence = evidence or []

    def as_dict(self):
        return {"probe": self.n, "name": self.name, "mode": self.mode,
                "verdict": self.verdict, "headline": self.headline,
                "evidence": self.evidence}


def probe1_persistence(cfg, runs):
    """What the agent knows and never writes down. An unattended run gets killed: by a
    turn limit, an OOM, a deploy. The question is whether the next run can tell."""
    base, killed = runs["baseline"], runs.get("killed")
    ev = []
    if killed is None:
        return Result(1, "Interrupted run leaves a trace", RAN, UNKNOWN,
                      f"the baseline run finished in {base.duration:.2f}s, too short to "
                      "interrupt. A real model call is not instant: give the fake tool "
                      "a realistic duration with --stub-sleep and run it again", ev)
    ev.append(f"baseline: {base.brief()}")
    ev.append(f"killed at {cfg.kill_at:.1f}s: {killed.brief()}")
    if not killed.written:
        return Result(1, "Interrupted run leaves a trace", RAN, FAIL,
                      "killed mid run, the agent had written nothing to disk, so the "
                      "next run cannot tell an interrupted predecessor from a clean one",
                      ev)
    partial = set(killed.written) - set(base.written)
    if partial:
        ev.append("only after the kill: " + ", ".join(sorted(partial)[:6]))
    ev.append("wrote before dying: " + ", ".join(killed.written[:6]))
    return Result(1, "Interrupted run leaves a trace", RAN, PASS,
                  f"the interrupted run had already written {len(killed.written)} "
                  "path(s), so there is something for the next run to read", ev)


def probe2_capabilities(cfg, runs):
    """What the agent believes about its own hands. A blanked credential is the
    cheapest possible outage, and an agent that reports success through one will report
    success through the real thing."""
    r = runs.get("nocreds")
    if r is None:
        return Result(2, "Missing credentials are noticed", RAN, UNKNOWN,
                      "no credential shaped environment variable that this repository "
                      "names by hand was set in this shell, so there was nothing to "
                      "take away that the agent would have missed", [])
    base = runs["baseline"]
    ev = [f"blanked {len(cfg.blanked)} env var(s): " + ", ".join(cfg.blanked[:8]),
          f"baseline: {base.brief()}",
          f"run with them blank: {r.brief()}"]
    if base.exit != 0:
        return Result(2, "Missing credentials are noticed", RAN, UNKNOWN,
                      f"the baseline run already exited {base.exit}, so a run that also "
                      "exits non zero with the credentials blanked proves nothing: there "
                      "is no successful run to contrast it against", ev)
    if r.exit == 0 and not r.timed_out:
        return Result(2, "Missing credentials are noticed", RAN, FAIL,
                      "with every credential blanked the entry point still exited 0, so "
                      "a rotated or expired key produces a run that looks successful",
                      ev)
    return Result(2, "Missing credentials are noticed", RAN, PASS,
                  f"the run exited {r.exit} with credentials blanked", ev)


PROMISE = re.compile(
    r"(within\s+\d+\s*(second|minute|hour|day|week)|"
    r"\b(sla|refund|money.back|guarantee|we will (get back|respond|reply))\b|"
    r"\b\d+\s*(hour|minute|day)s?\s+(reply|response|turnaround|delivery)\b)", re.I)
CLOCKISH = re.compile(
    r"(due[_ ]?date|deadline|overdue|strptime|fromisoformat|date_parse|dateutil|"
    r"timedelta|expires_at|Date\.now|new Date\()", re.I)


HUMAN_PROCESS = ("CONTRIBUTING", "SECURITY.md", "CODE_OF_CONDUCT", "LICENSE",
                 "NOTICE", "CHANGELOG", "CITATION", "PULL_REQUEST_TEMPLATE",
                 "ISSUE_TEMPLATE")


def probe3_promises(cfg, runs):
    """Promises with nothing watching the clock. This one reads, because a promise to a
    human is not something an entry point can be made to demonstrate in a sandbox."""
    # A two week review turnaround in CONTRIBUTING.md is a promise the maintainers
    # made to contributors, not one the agent makes and has to keep unattended. Left in,
    # it fails every repository that has the usual files, and a probe that always fails
    # tells you nothing.
    promises = [(loc, line) for loc, line in grep(cfg.repo, PROMISE.pattern)
                if not any(h in loc for h in HUMAN_PROCESS)]
    clocks = list(grep(cfg.repo, CLOCKISH.pattern))
    ev = [f"{loc}  {line}" for loc, line in promises[:10]]
    if not promises:
        return Result(3, "Promises have a clock on them", READ, UNKNOWN,
                      "no time bounded promise language found in the repository", ev)
    if not clocks:
        return Result(3, "Promises have a clock on them", READ, FAIL,
                      f"{len(promises)} time bounded promise(s) in the text and nothing "
                      "anywhere in the repository parses a due date, so no code can "
                      "tell you one came due", ev)
    ev.append(f"{len(clocks)} date parsing site(s), first: {clocks[0][0]}")
    return Result(3, "Promises have a clock on them", READ, PASS,
                  f"{len(promises)} promise(s) found and {len(clocks)} place(s) parse a "
                  "date; check by hand that they are the same promises", ev)


def probe4_quiet_failure(cfg, runs):
    """Failures that exit quietly. The dangerous failure returns success, and a
    scheduler cannot see anything else."""
    base, fail, empty = runs["baseline"], runs["toolfail"], runs["emptyout"]
    ev = [f"baseline: {base.brief()}",
          f"every underlying tool exits 3: {fail.brief()}",
          f"every underlying tool exits 0 and prints nothing: {empty.brief()}"]
    if fail.exit == 0:
        return Result(4, "A failed run exits non zero", RAN, FAIL,
                      "every underlying tool call failed and the entry point still "
                      "exited 0; cron, CI and systemd all read that as a good run", ev)
    if empty.exit == 0 and empty.output().strip() == base.output().strip():
        return Result(4, "A failed run exits non zero", RAN, FAIL,
                      "a run in which every tool produced no output is byte identical "
                      "to a successful run, so an empty result is indistinguishable "
                      "from work", ev)
    return Result(4, "A failed run exits non zero", RAN, PASS,
                  f"the failing run exited {fail.exit}", ev)


BUDGETISH = re.compile(
    r"(max_turns|max-turns|turn[_ ]budget|token[s]?[_ ]used|usage|rate.?limit|"
    r"budget|--max|context[_ ]window|spend)", re.I)


def probe5_budget(cfg, runs):
    """Budget spent before the work is saved. A limit arrives without warning, so the
    only question that matters is whether anything was on disk when it did."""
    base = runs["baseline"]
    ev = [f"baseline ran {base.duration:.1f}s and wrote {len(base.written)} path(s)"]
    reads = list(grep(cfg.repo, BUDGETISH.pattern))
    if base.first_write_at is None and base.written and base.last_clean_at is not None \
            and base.last_clean_at/max(base.duration, 0.001) > 0.8:
        pct = base.last_clean_at/base.duration*100
        ev.append(f"disk still untouched at {base.last_clean_at:.2f}s, {pct:.0f}% of the "
                  "way through the run; every write landed after that")
        return Result(5, "Work is saved as it goes", RAN, FAIL,
                      f"nothing was on disk {pct:.0f}% of the way through the run, so "
                      "the save is terminal and a limit reached before the end loses "
                      "the whole run", ev)
    if base.first_write_at is None and base.written:
        return Result(5, "Work is saved as it goes", RAN, UNKNOWN,
                      f"the run wrote {len(base.written)} path(s) but finished in "
                      f"{base.duration:.2f}s, faster than this probe can sample, so "
                      "when it saved is not measurable. Give the fake tool a realistic "
                      "duration with --stub-sleep and run it again", ev)
    if base.first_write_at is None:
        return Result(5, "Work is saved as it goes", RAN, FAIL,
                      "nothing was written to disk at any point during the run, so a "
                      "limit reached at any moment loses the whole run", ev)
    ev.append(f"{len(reads)} line(s) mention a budget or usage figure" +
              (f", first: {reads[0][0]}" if reads else ""))
    # A baseline that never ended on its own has no length of its own. Everything below
    # would otherwise be a percentage of --timeout, a number the operator chose. S22.
    if base.timed_out:
        ev.append(f"the baseline never ended on its own; the harness stopped it at the "
                  f"{cfg.timeout:.1f}s timeout, so there is no run length to express "
                  "this as a fraction of")
        return Result(5, "Work is saved as it goes", RAN, PASS,
                      f"something was on disk {base.first_write_at:.1f}s in and the run "
                      f"was still going {base.duration:.1f}s later", ev)
    frac = base.first_write_at/max(base.duration, 0.001)
    ev.insert(1, f"first write to disk at {base.first_write_at:.1f}s, "
                 f"{frac*100:.0f}% of the way through the run")
    if frac > 0.8:
        return Result(5, "Work is saved as it goes", RAN, FAIL,
                      f"the first write to disk came {frac*100:.0f}% of the way in, so "
                      "the run is all or nothing against a limit that gives no warning",
                      ev)
    return Result(5, "Work is saved as it goes", RAN, PASS,
                  f"the run had saved something by {frac*100:.0f}% of its length", ev)


IRREVERSIBLE = (r"(git\s+push|git\s+tag|gh\s+(pr|release|issue)\s+create|npm\s+publish|"
                r"docker\s+push|aws\s+s3|rm\s+-rf|curl[^\n]*-X\s*(POST|PUT|DELETE)|"
                r"requests\.(post|put|delete)|\.send_message|sendmail|smtplib|"
                r"stripe\.|terraform\s+apply|kubectl\s+apply)")


def probe6_gates(cfg, runs):
    """Gates that one bad step walks straight through, and the stop switch. Read once
    at startup means the true latency of your stop switch is one whole run."""
    ev = []
    hits = list(grep(cfg.repo, IRREVERSIBLE))
    ev += [f"irreversible: {loc}  {line}" for loc, line in hits[:8]]
    if not cfg.stop_file:
        return Result(6, "The stop switch stops it", READ, UNKNOWN,
                      f"{len(hits)} irreversible call site(s) found; no stop file was "
                      "named, so the latency of the stop switch was not measured", ev)
    base, stopped = runs["baseline"], runs.get("stopped")
    if stopped is None:
        return Result(6, "The stop switch stops it", RAN, UNKNOWN,
                      f"the baseline run finished in {runs['baseline'].duration:.2f}s, "
                      "too short to write the stop file into partway. Give the fake "
                      "tool a realistic duration with --stub-sleep", ev)
    ev.append(f"baseline: {base.brief()}")
    ev.append(f"{cfg.stop_file} written at {cfg.kill_at:.1f}s: {stopped.brief()}")
    # Same trap as probe 5, and worse: "ended 13.9s earlier" reads like a measurement of
    # the subject when the baseline was killed by --timeout. What is actually knowable
    # here is stronger than a delta -- whether the run ended by itself at all. S22.
    if base.timed_out:
        if stopped.timed_out or stopped.killed:
            return Result(6, "The stop switch stops it", RAN, FAIL,
                          f"the baseline never ends on its own and writing "
                          f"{cfg.stop_file} into it did not end it either", ev)
        return Result(6, "The stop switch stops it", RAN, PASS,
                      f"the baseline never ended on its own and was stopped by the "
                      f"{cfg.timeout:.1f}s timeout; with {cfg.stop_file} written at "
                      f"{cfg.kill_at:.1f}s the run ended by itself at "
                      f"{stopped.duration:.1f}s", ev)
    saved = base.duration - stopped.duration
    # A delta smaller than half the printed resolution renders as "0.0s" or "-0.0s",
    # and which one you get is scheduler noise rather than anything about the subject:
    # six same-order runs of this fixture printed minus zero five times and zero once
    # (S38). Below the resolution the honest sentence is a bound with no sign in it.
    # The signed number is kept when it is large enough to mean something -- a stopped
    # run that took materially LONGER than the baseline is a real thing to report. C-0011.
    delta = "by less than 0.1s" if abs(saved) < 0.05 else f"by {saved:.1f}s"
    if saved < 0.2 and not stopped.killed:
        return Result(6, "The stop switch stops it", RAN, FAIL,
                      f"writing {cfg.stop_file} mid run changed the run length "
                      f"{delta}, so nothing read it after startup and the real "
                      "latency of the stop switch is one whole run", ev)
    return Result(6, "The stop switch stops it", RAN, PASS,
                  f"the run ended {saved:.1f}s earlier once {cfg.stop_file} appeared",
                  ev)


INSTRUCTION_FILES = ("CLAUDE.md", "AGENTS.md", "AGENT.md", "prompt.md", "PROMPT.md",
                     "README.md", ".cursorrules", "GEMINI.md", "CONVENTIONS.md")
# A file the documentation says gets created, or watched for, is supposed to be absent
# from a clean checkout. Its absence is the design, not a broken instruction.
SIGNALLED = re.compile(r"\b(create|creating|creates|write|writing|writes|touch|"
                       r"generated|generates|drop|place|checks? for|detect(s|ed)?|"
                       r"if (it |a |an )?(is )?(found|present|exists)|already exists|"
                       r"signal|on first run|is deleted|deleting|"
                       # A name the docs call excluded, ignored or optional, or that they
                       # say the reader supplies, is supposed to be absent here too. S21.
                       r"exclude(s|d)?|ignore(s|d)?|optional|gitignore|"
                       r"you (put|add|create|supply|provide))\b", re.I)
ADJECTIVE = re.compile(r"\b(remember to|remember that|don't forget|do not forget|"
                       r"be careful|make sure you notice|keep in mind|bear in mind)\b",
                       re.I)
NAMED_PATH = re.compile(r"`([A-Za-z0-9_./-]+\.(?:sh|py|md|json|ya?ml|toml|ts|js|ps1))`")


def gitignored(repo: Path):
    """The names .gitignore says are not supposed to be in a clean checkout.

    S21 taught this probe to accept English: documentation that calls a file excluded,
    ignored or optional gets it exempted. S22 found the other half. Helicone's cc-agent
    README says `prompt.md` is generated and gitignored on one line and names it in
    backticks on another, and the exemption missed it because the two facts were not on
    the same line -- while `.gitignore` beside it said so unambiguously, in a file whose
    entire purpose is to say so. Prose is the weaker source and it was the only one read.

    Not a gitignore implementation: exact names, basenames and directory prefixes, which
    is what an ignore file for an agent's scratch state actually contains. A pattern with
    a wildcard or a negation in it is skipped rather than guessed at."""
    out = set()
    f = repo/".gitignore"
    if not f.exists():
        return out
    for line in f.read_text(errors="replace").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or line.startswith("!"):
            continue
        if any(c in line for c in "*?["):
            continue
        out.add(line.rstrip("/").lstrip("/"))
    return out


def is_ignored(target: str, ignored) -> bool:
    t = target.lstrip("./")
    if t in ignored or Path(t).name in ignored:
        return True
    parts = Path(t).parts
    return any("/".join(parts[:i]) in ignored for i in range(1, len(parts)))


def probe7_contradictions(cfg, runs):
    """Instructions that cannot all be true. The cheap mechanical half of this is
    instruction versus implementation: a file the prompt tells the agent to use, that
    is not in the repository."""
    ev, missing, adjectives = [], [], []
    ignored = gitignored(cfg.repo)
    present = [(n, (cfg.repo/n).read_text(errors="replace").splitlines())
               for n in INSTRUCTION_FILES if (cfg.repo/n).exists()]

    # First pass, over every instruction file at once: which names does the
    # documentation anywhere describe as created, watched for or deleted? A name
    # discussed on six lines and explained on one is still explained.
    signalled = set()
    for _, lines in present:
        for line in lines:
            if SIGNALLED.search(line):
                signalled.update(m.group(1) for m in NAMED_PATH.finditer(line))

    for name, lines in present:
        for i, line in enumerate(lines, 1):
            for m in NAMED_PATH.finditer(line):
                target = m.group(1)
                if target in signalled:
                    continue
                # An absolute path names a file on some machine, not in this checkout.
                # Whether it exists is not a question this probe can answer. S21.
                if target.startswith(("/", "~")):
                    continue
                # .gitignore naming it is the repository saying, mechanically, that a
                # clean checkout is not supposed to carry this file. S22.
                if is_ignored(target, ignored):
                    continue
                if any((cfg.repo/p).exists() for p in (target, target.lstrip("./"))):
                    continue
                if list(cfg.repo.rglob(Path(target).name)):
                    continue
                missing.append((f"{name}:{i}", target))
            if ADJECTIVE.search(line):
                adjectives.append((f"{name}:{i}", line.strip()[:120]))
    ev += [f"names {t}, which is not in the repository: {loc}" for loc, t in missing[:8]]
    ev += [f"tells a memoryless run to remember: {loc}  {line}"
           for loc, line in adjectives[:6]]
    if not present:
        return Result(7, "Instructions match the implementation", READ, UNKNOWN,
                      "no instruction file found at the repository root, so there is "
                      "nothing to compare the implementation against", ev)
    if missing:
        return Result(7, "Instructions match the implementation", READ, FAIL,
                      f"{len(missing)} file(s) named in the instructions do not exist "
                      "in the repository, so the agent will spend real budget looking "
                      "for them", ev)
    if adjectives:
        return Result(7, "Instructions match the implementation", READ, FAIL,
                      f"{len(adjectives)} instruction(s) ask the agent to remember, "
                      "notice or be careful, which contradicts a run that starts with "
                      "no memory; those need to be a step or a check", ev)
    return Result(7, "Instructions match the implementation", READ, PASS,
                  "every file named in the instructions exists and nothing asks a "
                  "memoryless run to remember something", ev)


PROBES = [probe1_persistence, probe2_capabilities, probe3_promises, probe4_quiet_failure,
          probe5_budget, probe6_gates, probe7_contradictions]


# ------------------------------------------------------- the differential (--differential)

# Each probe declares here whether the numbers it prints are expected to move when the
# only thing that changed is how long the fake tool takes. This is the dependency set,
# written down in advance rather than argued about afterwards, and --differential checks
# it in both directions. Asked for by `pressuretestagent` on Moltbook, 2026-08-30.
STUB_SENSITIVE = {
    1: True,   # prints the baseline duration and the moment of the kill
    2: False,  # counts environment variables and the lines that name them
    3: False,  # counts promise-shaped lines in the repository text
    4: True,   # prints the durations of the runs whose tools were made to fail
    5: True,   # prints when the first write landed, against the length of the run
    6: True,   # prints when the run ended, against when the stop file was written
    7: False,  # reads the repository text and never runs anything
}

NUMBER = re.compile(r"-?\d+\.\d+|-?\d+")


def numbers_of(result):
    """Every number this probe printed, each with the line it printed it on. Order is
    the comparison key: two passes down the same branch print the same numbers in the
    same places, so a positional mismatch is itself a difference."""
    out = []
    for line in [result.headline] + list(result.evidence):
        for m in NUMBER.finditer(line):
            out.append((line, float(m.group())))
    return out


def moved(a, b, rel=0.05, absolute=0.05):
    """Two printed numbers differ by more than measurement noise. Both tolerances are
    needed: 0.05s absolute so a sub-tenth wobble in a duration is not a finding, and 5%
    relative so a 30s number is not called stable when it shifts by a second."""
    return abs(a - b) > max(absolute, rel * max(abs(a), abs(b)))


def classify(a, repeat, alt):
    """Label one probe's numbers against the two other passes.

    stable    the same in all three passes
    noisy     it moved between two identical passes, so nothing can be attributed to it
    harness   it held still across the identical passes and moved when only the stub did
    """
    if a.verdict != repeat.verdict:
        return "noisy", [], f"the verdict changed between two identical passes"
    if a.verdict != alt.verdict:
        return "harness", [], (f"the verdict itself changed from {a.verdict} to "
                               f"{alt.verdict} when only the stub timing changed")
    na, nr, nb = numbers_of(a), numbers_of(repeat), numbers_of(alt)
    if len(na) != len(nr):
        return "noisy", [], "two identical passes printed a different set of numbers"
    if len(na) != len(nb):
        return "harness", [], ("the stub timing changed which numbers this probe "
                               "prints at all")
    noisy, harness = [], []
    for (line, x), (_, r), (_, b) in zip(na, nr, nb):
        if moved(x, r):
            noisy.append((line, x, r, b))
        elif moved(x, b):
            harness.append((line, x, r, b))
    if noisy:
        return "noisy", noisy, f"{len(noisy)} number(s) moved between two identical passes"
    if harness:
        return "harness", harness, (f"{len(harness)} number(s) held still across two "
                                    "identical passes and moved when only the stub did")
    return "stable", [], "every number was the same in all three passes"


# The exit code the differential can produce, and the one thing it must never do, which
# is let a fact about my instrument leave here wearing the subject's name. Asked for by
# `pressuretestagent` and `orbitsingulariti` on Moltbook, 2026-08-30: make the
# contradictions hard failures, and say which one exits nonzero and which stays a warning.
EXIT_OK = 0        # every probe passed, and every number reproduced and matched its declaration
EXIT_FINDING = 1   # a probe failed. A fact about the subject. This is the useful one.
EXIT_UNTRUSTED = 2 # the table cannot be read as facts about the subject. Fix the instrument.


# ------------------------------------------------------------------ the coverage floor
#
# Two people arrived at the same hole from opposite sides on 2026-08-31, neither of them
# talking to the other. `pressuretestagent`: do exit 2 and the escaped-check count feed a
# machine-readable coverage floor, or can a customer still accept a run where most metrics
# are unqualified? `orbitsingulariti`: if escaped_count > 0 the run should be barred from
# claiming declarations held, even if every checked probe is green.
#
# They were right, and the defect was literally in the output. A run in which every probe
# was noisy has no contradictions, so the summary printed "every probe measured what it
# declared" — a sentence that was true only because nothing had been measured against its
# declaration at all. Exit 0, empty table, confident line.
#
# The fix is to stop reporting one number (contradictions) and start reporting the
# qualification of every probe, in three classes that `pressuretestagent` named first:
QUAL_CAUSAL = "causal"        # it ran the subject, and its numbers were checked against
                              # its declaration across three passes and matched. This is
                              # the only class with an experiment behind it.
QUAL_DECLARED = "declared"    # it never ran the subject: it reads the repository text, so
                              # no intervention on a run can move its numbers. Honestly
                              # fixed, and attributed by declaration alone with no
                              # experiment behind it. Not a failure. Not evidence either.
QUAL_UNQUALIFIED = "unqualif" # noisy: its numbers moved between two identical passes, so
                              # neither contradiction can fire on it. It escaped the
                              # declaration check rather than passing it.
QUAL_CONTRADICTED = "contra"  # it was checked and it contradicted itself. Exit 2.


def qualify(mode, label, flag):
    """One probe's evidence class. A function of what the probe is (mode) and what the
    three passes did to it (label, flag), and of nothing else."""
    if flag:
        return QUAL_CONTRADICTED
    if label == "noisy":
        return QUAL_UNQUALIFIED
    if mode == READ:
        return QUAL_DECLARED
    return QUAL_CAUSAL


def coverage(quals, min_attributed):
    """The machine-readable floor. `runnable` is the probes that execute the entry point
    and could therefore have been attributed by experiment; `causal` is how many of them
    were. Everything else here is derived, so a customer can set their own bar off the
    JSON rather than take mine.

    The default floor is 1, and it is deliberately the weakest bar that is not vacuous
    rather than a quality bar. At zero causally attributed probes, no number in the table
    has an experiment behind it and the whole table is declaration: printing timings in
    that state is a fiction, whatever the probes said. A customer who wants a real bar
    raises it; the numbers to raise it against are all in the JSON."""
    runnable = sum(1 for q in quals if q != QUAL_DECLARED)
    causal = sum(1 for q in quals if q == QUAL_CAUSAL)
    return {
        "runnable": runnable,
        "causal": causal,
        "declared": sum(1 for q in quals if q == QUAL_DECLARED),
        "unqualified": sum(1 for q in quals if q == QUAL_UNQUALIFIED),
        "contradicted": sum(1 for q in quals if q == QUAL_CONTRADICTED),
        "min_attributed": min_attributed,
        "below_floor": causal < min_attributed,
        # The sentence the old code printed unconditionally, now computed. A run may only
        # claim its declarations held if every probe that could be checked was checked.
        "declarations_held": all(q in (QUAL_CAUSAL, QUAL_DECLARED) for q in quals),
    }


def exit_code(failed, contradictions: int, below_floor: bool = False) -> int:
    """Which nonzero exit a customer gets. `orbitsingulariti` asked which contradiction
    exits nonzero and which stays a warning; this is the whole answer.

    Untrusted outranks a finding. If the differential says a number in the table is mine
    rather than the subject's, the failing probes above it are not worth reading yet, and
    a customer who sees exit 1 should be able to act on it without wondering which of the
    two things I meant. Nothing about the subject is claimed at exit 2.

    A run below the coverage floor exits 2 for the same reason and not a different one.
    It is not a finding about the subject and it is not a contradiction; it is a run that
    did not establish enough for the table above it to be read as the subject's. That is
    what exit 2 has always meant. Note what this does NOT do: a noisy probe on its own
    still does not fail. Noise stays a warning about the subject, because I cannot tell my
    own jitter from a nondeterministic agent (`forgeloop`). What noise now destroys is the
    separate claim that the declarations were checked — a claim about coverage, not about
    the subject, and those are two different sentences that used to be one."""
    if contradictions or below_floor:
        return EXIT_UNTRUSTED
    return EXIT_FINDING if failed else EXIT_OK


def contradiction_of(probe_n: int, label: str) -> str:
    """The whole declaration check, as one function of two independent inputs: what the
    probe declared in STUB_SENSITIVE, and what the three passes measured. Nothing else.

    Kept separate from the printing so the selftest can cross every declaration with
    every measurement and prove both inputs are load-bearing — `orbitsingulariti` asked
    for exactly that, on the grounds that a declaration written by the same author as the
    metric can name the dependency it wants to own rather than the one it has.

    Both contradictions are hard. Declared fixed and moved means a number sold as a fact
    about the subject tracks my configuration. Declared free to move and perfectly still
    means something is holding it, and on the one subject where this fired the thing
    holding it was my own timeout at both ends. The direction changes the diagnosis, not
    the customer's position: either way the ownership stated in the table is wrong."""
    declared_moves = bool(STUB_SENSITIVE.get(probe_n))
    if label == "harness" and not declared_moves:
        return "CONTRADICTION: declared fixed, moved when only the stub did"
    if label == "stable" and declared_moves:
        return ("CONTRADICTION: declared to move with the stub and did not move at "
                "all, so something is holding it")
    return ""


def report_differential(cfg, base_pass, repeat_pass, alt_pass, alt_sleep,
                       min_attributed):
    """Print the differential table. Returns (contradictions, noisy, cov) — the first and
    the floor inside the third decide the exit code, the second is reported and
    deliberately does not."""
    a_runs, a_res = base_pass
    r_runs, r_res = repeat_pass
    b_runs, b_res = alt_pass
    print()
    print("DIFFERENTIAL  three passes over the same frozen subject")
    print(f"  A   the pass above, --stub-sleep {cfg.stub_sleep}")
    print(f"  A'  identical to A. The noise control, without which a wobble and a "
          f"finding look the same")
    print(f"  B   --stub-sleep {alt_sleep}. The only thing changed")
    print("-"*78)
    print(f"{'PROBE':<7}{'DECLARED':<11}{'MEASURED':<11}{'CLASS':<10}NOTE")
    contradictions, noisy, quals = [], [], []
    for a, r, b in zip(a_res, r_res, b_res):
        label, moved_nums, why = classify(a, r, b)
        declared = "moves" if STUB_SENSITIVE.get(a.n) else "fixed"
        measured = {"stable": "fixed", "harness": "moves", "noisy": "noisy"}[label]
        flag = contradiction_of(a.n, label)
        qual = qualify(a.mode, label, flag)
        quals.append(qual)
        if flag:
            contradictions.append((a, flag))
        if label == "noisy":
            noisy.append((a, why))
        print(f"{a.n:<7}{declared:<11}{measured:<11}{qual:<10}{flag or why}")
        for line, x, rr, bb in moved_nums:
            print(f"       A {x:<10.2f} A' {rr:<10.2f} B {bb:<10.2f} {line[:44]}")
    cov = coverage(quals, min_attributed)
    print()
    if contradictions:
        for a, flag in contradictions:
            print(f"probe {a.n}  {a.name}")
            print(f"         {flag}")
        print(f"\n  {len(contradictions)} contradiction(s): exit {EXIT_UNTRUSTED}. Read "
              f"nothing in the table above as a fact about the subject until these are\n"
              f"  fixed. This is not a finding about the agent under test, it is a "
              f"finding about this tool.")
    elif cov["declarations_held"]:
        print("every probe that could be checked against its declaration was checked, "
              "and matched.")
    else:
        # The sentence this used to print unconditionally was "every probe measured what
        # it declared", and with no contradictions and every probe noisy it was true only
        # because nothing had been checked. `orbitsingulariti` called it: a run with
        # escaped probes is barred from claiming its declarations held.
        print(f"no contradiction fired, but {cov['unqualified']} probe(s) were never "
              f"checked against their declaration,\nso this run does NOT establish that "
              f"the declarations held.")
    if noisy:
        # Noise is the one outcome that stays a warning, and the reason is not that it is
        # harmless. A number that moves between two identical passes has two explanations
        # I cannot tell apart from outside — jitter in my own harness, or a subject that
        # is genuinely nondeterministic — and the second is a fact about the customer's
        # agent they may well want. Failing on it would fail on almost every real subject,
        # and a guard whose false alarms look exactly like its true ones gets muted within
        # a week (`forgeloop`, same thread, 2026-08-30). What it must not do is pass
        # quietly, because noise is also where a contradiction goes to hide:
        print(f"\n  {len(noisy)} probe(s) printed a number that moved between two "
              f"identical passes. A noisy probe is\n  NOT checked against its "
              f"declaration — neither contradiction can fire on it — so these escaped\n"
              f"  the declaration check rather than passing it, and they stay a warning "
              f"rather than exit {EXIT_UNTRUSTED}:")
        for a, why in noisy:
            print(f"    probe {a.n}  {a.name}: {why}")
    # The blind spot, printed by the tool rather than left for the reader to work out.
    clamped = [n for n, runs in (("A", a_runs), ("A'", r_runs), ("B", b_runs))
               if runs["baseline"].timed_out]
    if len(clamped) == 3:
        print()
        print(f"  the baseline never ended by itself in any of the three passes, so its "
              f"length was the {cfg.timeout:.1f}s --timeout every time. A number that is "
              f"entirely\n  decided by the harness does not move when the stub changes: "
              f"this differential cannot see it, and the contradictions above are how "
              f"it shows up instead.")
    # The coverage floor, printed as the arithmetic rather than as a verdict, because a
    # customer setting their own bar needs the terms and not my conclusion.
    print()
    print(f"COVERAGE  {cov['causal']} of {cov['runnable']} probe(s) that run the subject "
          f"were causally attributed")
    print(f"  causal      {cov['causal']:>2}  ran the subject, checked against its "
          f"declaration across three passes, matched")
    print(f"  declared    {cov['declared']:>2}  never runs the subject: reads the "
          f"repository text. No experiment can attribute these")
    print(f"  unqualif    {cov['unqualified']:>2}  noisy, so the declaration check could "
          f"not fire either way. Escaped, not passed")
    print(f"  contra      {cov['contradicted']:>2}  checked and contradicted itself")
    print(f"  floor       --min-attributed {cov['min_attributed']}"
          f"{'  NOT MET' if cov['below_floor'] else ''}")
    if cov["below_floor"]:
        print(f"\n  below the floor: exit {EXIT_UNTRUSTED}. Fewer than "
              f"{cov['min_attributed']} probe(s) that ran the subject came out causally\n"
              f"  attributed, so no number in the table above has an experiment behind "
              f"it. This is not a\n  finding about the agent under test. It is this run "
              f"declining to claim anything about it.")
    return len(contradictions), len(noisy), cov


# --------------------------------------------------------------------------- one pass

def one_pass(cfg, *, verbose):
    """Every run of the entry point this tool makes, and the seven answers off them.
    Factored out of main in S25 so --differential can make the same pass three times
    with one thing changed; the single-pass path calls it exactly once."""
    say = print if verbose else (lambda *a, **k: None)
    runs = {}
    runs["baseline"] = execute(cfg, "baseline")
    say(f"  baseline   {runs['baseline'].brief()}")
    if runs["baseline"].timed_out:
        say(f"  note       the baseline never ended on its own. The harness stopped "
            f"it at the {cfg.timeout:.1f}s --timeout, so that number, and not "
            f"anything\n             this entry point decided, is the length every "
            f"run below is compared against.")
    runs["toolfail"] = execute(cfg, "toolfail", stub_exit=3)
    say(f"  tools fail {runs['toolfail'].brief()}")
    runs["emptyout"] = execute(cfg, "emptyout", stub_out="")
    say(f"  no output  {runs['emptyout'].brief()}")
    if cfg.blanked:
        runs["nocreds"] = execute(cfg, "nocreds", drop_secrets=True)
        say(f"  no creds   {runs['nocreds'].brief()}")

    # Interrupt halfway through however long the baseline actually took. Below half a
    # second there is nothing to interrupt and the result would be noise, not evidence.
    cfg.kill_at = runs["baseline"].duration/2
    if cfg.kill_at >= 0.5:
        runs["killed"] = execute(cfg, "killed", kill_at=cfg.kill_at)
        say(f"  killed     {runs['killed'].brief()}")
        if cfg.stop_file:
            runs["stopped"] = execute(cfg, "stopped", stop_at=cfg.kill_at)
            say(f"  stop file  {runs['stopped'].brief()}")

    return runs, [p(cfg, runs) for p in PROBES]


# ------------------------------------------------------------------------------ main

def main(argv=None):
    ap = argparse.ArgumentParser(add_help=True, description=__doc__.split("\n")[0])
    ap.add_argument("--repo", default=".")
    ap.add_argument("--entry", required=True)
    ap.add_argument("--state", action="append", default=[])
    ap.add_argument("--stub", action="append", default=[])
    ap.add_argument("--stub-sleep", type=float, default=0.0)
    ap.add_argument("--env", action="append", default=[])
    ap.add_argument("--pre", default="")
    ap.add_argument("--stop-file", default="")
    ap.add_argument("--timeout", type=float, default=120.0)
    ap.add_argument("--differential", type=float, default=None)
    ap.add_argument("--min-attributed", type=int, default=1,
                    help="under --differential, the run exits 2 unless at least this "
                         "many probes that actually run the subject came out causally "
                         "attributed. Default 1: the weakest bar that is not vacuous. "
                         "Raise it off the coverage numbers in --json.")
    ap.add_argument("--json", default="")
    ap.add_argument("--keep-git", action="store_true")
    cfg = ap.parse_args(argv)
    cfg.repo = Path(cfg.repo).resolve()
    cfg.env_pairs = [tuple(e.split("=", 1)) for e in cfg.env if "=" in e]
    cfg.sandboxes = []
    cfg.blanked = credentials_this_repo_reads(cfg.repo)
    if not cfg.repo.is_dir():
        sys.exit(f"loop-check: {cfg.repo} is not a directory")

    if cfg.differential is not None and cfg.differential == cfg.stub_sleep:
        sys.exit("loop-check: --differential must differ from --stub-sleep, or the "
                 "second profile is the first one again")

    print(f"loop-check  repo={cfg.repo}  entry={cfg.entry!r}")
    print("running the entry point. Every run is in a throwaway copy of the repository.")
    runs, results = one_pass(cfg, verbose=True)
    print()
    print("PROBE  MODE  VERDICT  WHAT IT FOUND")
    print("-"*78)
    for r in results:
        print(f"{r.n}  {r.name}")
        print(f"       {r.mode:4}  {r.verdict:7}  {r.headline}")
        for line in r.evidence:
            print(f"                        {line}")
        print()

    failed = [r for r in results if r.verdict == FAIL]
    unknown = [r for r in results if r.verdict == UNKNOWN]
    print(f"{len(failed)} failed, {len(results)-len(failed)-len(unknown)} passed, "
          f"{len(unknown)} could not be answered from what was given.")
    if unknown:
        print("An unknown is not a pass. It is a probe that needs a flag you did not "
              "pass, and it is listed above with the flag it wants.")

    contradictions = noisy = 0
    cov = None
    if cfg.differential is not None:
        print()
        print(f"differential: running the same subject twice more, once identically and "
              f"once with --stub-sleep {cfg.differential}")
        repeat = one_pass(copy.copy(cfg), verbose=False)
        alt_cfg = copy.copy(cfg)
        alt_cfg.stub_sleep = cfg.differential
        alt = one_pass(alt_cfg, verbose=False)
        contradictions, noisy, cov = report_differential(
            cfg, (runs, results), repeat, alt, cfg.differential, cfg.min_attributed)

    if cfg.json:
        Path(cfg.json).write_text(json.dumps(
            {"repo": str(cfg.repo), "entry": cfg.entry,
             "runs": {k: {"exit": v.exit, "duration": round(v.duration, 2),
                          "timed_out": v.timed_out, "killed": v.killed,
                          "written": v.written, "first_write_at": v.first_write_at,
                          "last_clean_at": v.last_clean_at,
                          "stub_calls": v.calls} for k, v in runs.items()},
             "probes": [r.as_dict() for r in results],
             "differential": None if cfg.differential is None else {
                 "alt_stub_sleep": cfg.differential,
                 "contradictions": contradictions,
                 "noisy": noisy,
                 "coverage": cov},
             "exit": exit_code(failed, contradictions,
                               bool(cov and cov["below_floor"]))}, indent=2) + "\n")
        print(f"evidence written to {cfg.json}")

    for d in cfg.sandboxes:
        shutil.rmtree(d, ignore_errors=True)
    below_floor = bool(cov and cov["below_floor"])
    code = exit_code(failed, contradictions, below_floor)
    if code == EXIT_UNTRUSTED:
        why = ("a contradicted declaration" if contradictions
               else "a run below its coverage floor")
        print(f"\nexit {EXIT_UNTRUSTED}: the instrument, not the subject. {why} outranks "
              f"a failing probe here\nbecause a failing probe is only worth reading if "
              f"the numbers under it are the subject's.")
    return code


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