mirror of
https://github.com/Heretek-AI/agentic-hooks.git
synced 2026-11-07 10:46:10 -10:00
Multi-plugin marketplace of forced mechanical gates (the Turnstile): agents cannot end a turn with mechanically failing code. Verdicts are deterministic CLI results — never LLM-judged. - core/gate: stdlib-only dispatcher (Python 3.11+) with SARIF-lite finding protocol (content-based fingerprints, line-drift proof), policy loader with includes/tiers, tool-chain resolver with fallback order + explicit DEGRADED verdicts, verdict cache, ratchet baseline, expiring waivers, guard-the-guard, loop-guard, turn ledger, doctor, report - 14 tool adapters: py_ast, ruff_format/check, secret_scan, node_check, tsc, oxlint, biome_*, prettier, ty, pyright, mypy, gitleaks (pure parsers, unit- testable without the tools installed) - packs: gates-essential (engine + all enforcement hooks + commands), gates-polyglot/security/strict (declarative content packs, registration-only hooks, dependencies on the engine — multi-fire structurally prevented) - Quality Ladder wiring: L0 PostToolUse (format/syntax/secrets, unchanged-hash fast path), L1 PostToolBatch (lint+types), L2 Stop turnstile with loop-guard, L3 gate check for CI parity - 56 tests: unit + golden-stdin hook contracts + packaging invariants + registration idempotence; manifest validator; seed fixtures - Spike results: H4 ratchet PASSED (0 false blocks / 10-10 caught); H2 partial falsification recorded (unchanged-hash <15ms unreachable at ~51ms p95 with per-edit CPython spawn — escalation path documented) - Design history: .research/brainstorm_20260926T1835.md (dialectic + tool matrix) Co-Authored-By: Claude Code <[email protected]>
111 lines
3.1 KiB
Python
111 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Launcher shim: `python3 <plugin>/core/gate.py <level>` (hooks.json exec form).
|
|
|
|
Two responsibilities:
|
|
1. Bootstrap `gate` package importability from its own directory (repo or vendored).
|
|
2. L0 unchanged-hash fast-path: when the edited file's (mtime,size) signature is
|
|
unchanged since the last clean L0 pass, exit 0 WITHOUT importing the engine.
|
|
Interpreter startup is the latency floor, so the engine is only loaded on a miss.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_HERE = Path(__file__).resolve().parent
|
|
if str(_HERE) not in sys.path:
|
|
sys.path.insert(0, str(_HERE))
|
|
|
|
|
|
def _read_payload() -> tuple[str, dict]:
|
|
raw = sys.stdin.read()
|
|
if not raw.strip():
|
|
return raw, {}
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
return raw, {}
|
|
return raw, data if isinstance(data, dict) else {}
|
|
|
|
|
|
def _file_target(payload: dict) -> Path | None:
|
|
tool_input = payload.get("tool_input") or {}
|
|
raw_path = tool_input.get("file_path") or tool_input.get("notebook_path")
|
|
if not isinstance(raw_path, str) or not raw_path:
|
|
return None
|
|
target = Path(raw_path)
|
|
return target if target.is_absolute() else (Path.cwd() / target)
|
|
|
|
|
|
def _signature(path: Path) -> str | None:
|
|
try:
|
|
st = path.stat()
|
|
except OSError:
|
|
return None
|
|
return f"{st.st_mtime_ns}:{st.st_size}"
|
|
|
|
|
|
def _store_path() -> Path:
|
|
return Path.cwd() / ".gate" / "cache" / "l0-seen.json"
|
|
|
|
|
|
def _load_store() -> dict:
|
|
try:
|
|
return json.loads(_store_path().read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
|
|
def _save_store(store: dict) -> None:
|
|
try:
|
|
path = _store_path()
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(store), encoding="utf-8")
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def main() -> int:
|
|
raw, payload = _read_payload()
|
|
argv = sys.argv[1:]
|
|
is_l0 = bool(argv) and argv[0] == "l0"
|
|
|
|
target: Path | None = None
|
|
sig: str | None = None
|
|
if is_l0 and payload:
|
|
target = _file_target(payload)
|
|
if target is not None:
|
|
sig = _signature(target)
|
|
key = str(target)
|
|
store = _load_store()
|
|
if sig is not None and store.get(key) == sig:
|
|
return 0 # unchanged since last clean L0 pass
|
|
elif is_l0:
|
|
return 0 # nothing to do
|
|
|
|
# Engine path: hand over the payload we already consumed.
|
|
if raw.strip():
|
|
os.environ["GATE_HOOK_INPUT"] = raw
|
|
from gate.cli import main as engine_main
|
|
|
|
code = engine_main()
|
|
|
|
if code == 0 and is_l0 and target is not None and sig is not None:
|
|
# Re-stat: format may have rewritten the file during the run.
|
|
fresh = _signature(target)
|
|
if fresh is not None:
|
|
store = _load_store()
|
|
store[str(target)] = fresh
|
|
_save_store(store)
|
|
return code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except SystemExit as exc:
|
|
raise SystemExit(int(exc.code or 0))
|