overview / benchmarks / gate latency
The gate runs on every coding-agent tool call (PreToolUse hook), on the critical path, so it has to be cheap. native/bench.py builds a scratch project with one guard rule, feeds hook events to native/rabadon-gate, first checks the verdicts (allow=0, deny=2 — a fast wrong gate is worthless), then times it over 40 runs per case.
BENCHMARK.md §1 · marked proven. Machine: Apple M2, arm64, macOS (Darwin 24.2.0). Toolchain: Apple clang 17.0.0, node v26.5.0, python 3.14.6.
The node rows below were measured on 2026-09-02, when the table was taken against two gates: the C++ core and the legacy Node gate (hooks/gate.mjs), with verdict parity proved first. The Node gate was retired in fbd6f6e; that comparison reproduces in a tree that still has it, the last being fbd6f6e^ (git worktree add <dir> fbd6f6e^, then make and python3 native/bench.py there). Today's make bench times the native gate only.
Parity held then: native == node on both verdicts (allow=0, deny=2). It only held because the harness ARMS the isolated home it benchmarks in. RABADON_DIR relocates the whole rabadon home, mode flags included, so a fresh temp home starts in WATCH — where the native gate records a deny and exits 0 while the node gate, which has no watch mode, exits 2. That read as a parity break for as long as the isolation existed; it was an unset switch, and a bench that leaves it unset is timing a gate nobody runs.
| gate | case | median | p95 | n |
|---|---|---|---|---|
| native | allow | 2.78 ms | 5.26 ms | 40 |
| native | deny | 3.70 ms | 8.60 ms | 40 |
| node | allow | 101.44 ms | 126.54 ms | 40 |
| node | deny | 101.20 ms | 103.27 ms | 40 |
Measured 2026-09-02 with RABADON_NOTIFY=0 make bench. Median hook tax drops from ~100 ms (node process startup dominates) to 2.8 ms (native), about 36x at the median. The native p95 is noisier on a laptop (scheduler / spawn jitter), so the median is the robust number.
Re-measured on 2026-09-02 because the gate gained a whole mechanism since the August table: the move ring, five detectors, and the injection channel all run on this path now. The allow median did not move (3.14 → 2.78 ms, inside this machine's run-to-run spread). A separate two-arm measurement isolates the new work rather than inferring it — same binary, 40 samples, one sandbox each:
| session state | median | p95 |
|---|---|---|
| cold: no moves recorded, nothing queued | 2.84 ms | 3.54 ms |
| armed: a full move ring and a diagnosis pending | 2.88 ms | 3.27 ms |
0.04 ms between them. Recording every move and carrying an undelivered diagnosis is free at this resolution; what the hook costs is process startup, which is what the native rewrite bought back.
This table read 2.29 ms on 31 July. The honest reading of the difference is that it is a different binary on a different day and not a regression anybody measured — the gate has since grown the multi-agent event normaliser — and two numbers taken in separate sessions do not make a difference. Where one number is needed, quote this table's, re-run rather than remembered.
Judging alone, with the process cost taken out: 245.3 µs median, p95 361.5 µs, over 34 real fixture cases at 200 judgements each. That comes from ./native/gate_bench.sh, which refuses to print any number unless the in-process judge and the shipped binary first agree on all 34 verdicts.
One path is deliberately not 2.8 ms: a git push after code changed since the last green test run makes the gate run the project's suite inside that same hook call. That is bounded by pushGate.timeoutSec (default 900 s), and the installed hook ceiling sits above it at 960 s on purpose — if the outer timeout fired first, the action would be neither allowed with a verdict nor refused with a reason.
Command: RABADON_NOTIFY=0 make bench.