← Files trigger-treeARCHIVED FILE

scripts/tt-instructions.py

11.5 KB · Oct 3, 2026 · 06:30 UTC

↓ Download file

#!/usr/bin/env python3
"""Author and report deterministic instruction-adherence probes.

Exit codes: 0 success, 1 adherence threshold failed, 2 usage/manifest error.
"""

import argparse
import json
import os
import stat
import subprocess
import sys
import tempfile

import tt_adherence

ROOT = os.environ.get("TT_PROJECT_DIR") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
MANIFEST_PATH = os.path.join(ROOT, ".trigger-tree", "directives.json")


def _regular_file(path):
    try:
        return stat.S_ISREG(os.lstat(path).st_mode)
    except OSError:
        return False


def _instruction_paths():
    return [
        name
        for name in ("CLAUDE.md", "AGENTS.md", "GEMINI.md")
        if _regular_file(os.path.join(ROOT, name))
    ]


def init_manifest():
    """Create a hash-bound scaffold, or refresh hashes without changing probes."""
    paths = _instruction_paths()
    if not paths:
        raise tt_adherence.ManifestError(
            "no root CLAUDE.md, AGENTS.md, or GEMINI.md instruction file found"
        )
    directives = []
    if _regular_file(MANIFEST_PATH):
        existing = tt_adherence.load_manifest(MANIFEST_PATH)
        directives = existing["directives"]
        paths = [item["path"] for item in existing["instruction_files"]]
    manifest = tt_adherence.validate_manifest(
        {
            "schema": tt_adherence.SCHEMA_VERSION,
            "instruction_files": [
                {
                    "path": path,
                    "sha256": tt_adherence.instruction_hash(os.path.join(ROOT, *path.split("/"))),
                }
                for path in paths
            ],
            "directives": directives,
        }
    )
    telemetry = os.path.dirname(MANIFEST_PATH)
    os.makedirs(telemetry, mode=0o700, exist_ok=True)
    if os.path.islink(telemetry):
        raise tt_adherence.ManifestError("refusing symlinked .trigger-tree directory")
    fd, temporary = tempfile.mkstemp(prefix=".directives-", suffix=".json", dir=telemetry)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(manifest, handle, indent=2)
            handle.write("\n")
        os.chmod(temporary, 0o600)
        os.replace(temporary, MANIFEST_PATH)
    finally:
        try:
            os.unlink(temporary)
        except FileNotFoundError:
            pass
    return manifest


def stats_payload():
    process = subprocess.run(
        [sys.executable, os.path.join(SCRIPT_DIR, "tt-stats.py")],
        capture_output=True,
        text=True,
        check=False,
        env={**os.environ, "TT_PROJECT_DIR": ROOT},
    )
    if process.returncode:
        raise tt_adherence.ManifestError(process.stderr.strip() or "stats failed")
    try:
        return json.loads(process.stdout)
    except json.JSONDecodeError as error:
        raise tt_adherence.ManifestError("stats returned invalid JSON") from error


def _adherence():
    if not _regular_file(MANIFEST_PATH):
        return None
    return stats_payload().get("adherence")


def _print_report(adherence):
    if adherence["status"] == "stale":
        print("Instruction manifest is stale; run `tt instructions --init`.")
        return
    if adherence["status"] == "invalid":
        print(f"Instruction manifest is invalid: {adherence['error']}")
        return
    summary = adherence["summary"]
    print(
        f"Instruction adherence · {summary['observable']} observable · "
        f"{summary['unobservable']} unobservable · "
        f"{summary['capture_disabled']} capture-disabled"
    )
    print("Unobserved means evidence was not captured; it does not mean violated.")
    for item in adherence["directives"]:
        status = item["status"]
        if status == "measured":
            rate = f"{round(item['rate'] * 100)}%"
            detail = (
                f"{item['followed']}/{item['opportunities']} followed · "
                f"{rate} · {item['confidence']}"
            )
        elif status == "never-triggered":
            detail = (
                f"never-triggered in {item.get('measurable_sessions', 0)} measurable sessions "
                "(review prompt, not a removal recommendation)"
            )
        elif status == "awaiting-capture":
            detail = "awaiting-capture (no session yet recorded the signal this probe needs)"
        elif status == "no-violations-observed":
            detail = "no-violations-observed (the rule held wherever it could be checked)"
        elif status == "capture-disabled":
            detail = "capture-disabled (excluded from rates)"
        else:
            reason = item.get("reason")
            detail = f"unobservable{f' · {reason}' if reason else ''} · {item['why']}"
        print(f"- {item['id']}: {detail}")
    # Only advice the author can act on earns a line; tool boundaries do not.
    actionable = summary.get("no_testable_condition", 0)
    if actionable:
        print(
            f"{actionable} directive(s) have no objectively testable condition; "
            "a rule whose trigger cannot be evaluated is unlikely to fire at all."
        )
    print(adherence["cost"]["headline"])


def _print_selftest(result):
    summary = result["summary"]
    print(
        f"Probe self-test · {summary['reachable']} reachable · "
        f"{summary['unreachable']} unreachable · "
        f"{summary['unsatisfiable']} unsatisfiable · {summary['unobservable']} unobservable"
    )
    print("Reachability only; a command pattern is not tested against real commands.")
    for item in result["probes"]:
        if item["status"] == "reachable":
            detail = f"reachable · {item['probe']}"
        elif item["status"] == "unobservable":
            detail = "unobservable · nothing to exercise"
        elif item["status"] == "unsatisfiable":
            detail = f"UNSATISFIABLE · {item['probe']} · {item['reason']}"
        else:
            detail = f"UNREACHABLE · {item['probe']} · {item['reason']}"
        print(f"- {item['id']}: {detail}")
    if summary["unreachable"] or summary["unsatisfiable"]:
        print("A probe that cannot fire measures nothing; its silence is not evidence.")


def _explain(adherence, directive_id):
    item = next(
        (entry for entry in adherence.get("directives", []) if entry["id"] == directive_id),
        None,
    )
    if item is None:
        raise tt_adherence.ManifestError(f"unknown directive: {directive_id}")
    return {
        "id": item["id"],
        "source": item["source"],
        "status": item["status"],
        "opportunities": item.get("opportunities"),
        "followed": item.get("followed"),
        "unobserved": item.get("unobserved"),
        "measurable_sessions": item.get("measurable_sessions"),
        "rate": item.get("rate"),
        "confidence": item.get("confidence"),
        "evidence": item.get("evidence", []),
        "uncertainty": adherence.get("uncertainty"),
    }


def _check(adherence, minimum, min_measured=0):
    if adherence.get("status") != "current":
        raise tt_adherence.ManifestError(
            "instruction manifest must be current before adherence can be checked"
        )
    failed = [
        item
        for item in adherence["directives"]
        if item.get("status") == "measured"
        and item.get("rate") is not None
        and item["rate"] < minimum
    ]
    summary = adherence.get("summary", {})
    measured = summary.get(
        "measured",
        sum(item.get("status") == "measured" for item in adherence["directives"]),
    )
    observable = summary.get("observable", 0)
    if failed:
        for item in failed:
            print(
                f"FAIL {item['id']}: {item['rate']:.2f} < {minimum:.2f} "
                "(unobserved does not mean violated)"
            )
        return 1
    # A gate that reports PASS without saying how much evidence it saw is the
    # same silent-degradation bug as a green tick on a privacy-limited mode.
    coverage = f"{measured}/{observable} observable directives measured"
    if measured < min_measured:
        print(f"FAIL instruction adherence: {coverage}, fewer than the required {min_measured}")
        return 1
    print(f"PASS instruction adherence: {coverage}, all measured rates >= {minimum:.2f}")
    return 0


def main(argv=None):
    parser = argparse.ArgumentParser()
    parser.add_argument("--init", action="store_true")
    parser.add_argument("--selftest", action="store_true")
    parser.add_argument("--explain", metavar="ID")
    parser.add_argument("--json", action="store_true")
    parser.add_argument("--check", action="store_true")
    parser.add_argument("--min-rate", type=float, default=0.0)
    parser.add_argument("--min-measured", type=int, default=0)
    args = parser.parse_args(argv)
    if not 0 <= args.min_rate <= 1:
        parser.error("--min-rate must be between 0 and 1")
    if args.min_measured < 0:
        parser.error("--min-measured must not be negative")
    if args.init and (args.explain or args.check):
        parser.error("--init cannot be combined with --explain or --check")
    if args.selftest and (args.init or args.explain or args.check):
        parser.error("--selftest cannot be combined with --init, --explain, or --check")
    try:
        if args.init:
            manifest = init_manifest()
            if args.json:
                json.dump(manifest, sys.stdout, indent=2)
                print()
            else:
                print(
                    f"Initialized {os.path.relpath(MANIFEST_PATH, ROOT)}; "
                    "review and confirm probes before measurement."
                )
            return 0
        if args.selftest:
            if not _regular_file(MANIFEST_PATH):
                print("No instruction manifest; run `tt instructions --init`.")
                return 0
            # Deliberately manifest-only: no history, no config, no agent run.
            result = tt_adherence.selftest(tt_adherence.load_manifest(MANIFEST_PATH), ROOT)
            if args.json:
                json.dump(result, sys.stdout, indent=2)
                print()
            else:
                _print_selftest(result)
            broken = result["summary"]["unreachable"] + result["summary"]["unsatisfiable"]
            return 1 if broken else 0
        adherence = _adherence()
        if adherence is None:
            print("No instruction manifest; run `tt instructions --init`.")
            return 0
        if args.explain:
            explanation = _explain(adherence, args.explain)
            if args.json:
                json.dump(explanation, sys.stdout, indent=2)
                print()
            else:
                print(
                    f"{explanation['id']}: {explanation['status']} · "
                    f"{explanation['followed'] or 0}/{explanation['opportunities'] or 0} "
                    "followed"
                )
                print("Unobserved means evidence was not captured; it does not mean violated.")
                for evidence in explanation["evidence"]:
                    print(
                        f"- {evidence['session']} · {evidence.get('ts') or 'time unavailable'} "
                        f"· {evidence['result']}"
                    )
            return 0
        if args.check:
            return _check(adherence, args.min_rate, args.min_measured)
        if args.json:
            json.dump(adherence, sys.stdout, indent=2)
            print()
        else:
            _print_report(adherence)
        return 0
    except tt_adherence.ManifestError as error:
        print(f"tt instructions: {error}", file=sys.stderr)
        return 2


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

SHA-256: 85a9b84f3930e9080ed618a937fb454c5e64d914847ad10a3b1c904b762aa78d