Repository-scoped · content-hashed · terminal-native
The sun travels. The shadow sweeps the hours. The gnomon does not move — and that is precisely why the dial can be read. Remove the blade and you have decorated stone, not an instrument.
This harness names itself after that object. .gnomon/ is the fixed configuration — models, roles, tools, approval — content-hashed and committed with your repository. Everything else is shadow.
cd my-project && gnomon launch
v0.2.0 · 1,000+ TypeScript tests · 57 Rust · green on Linux, macOS and Windows
Fig. 0 — Plan view (looking down). Fixed gnomon; black umbra projected from the blade.
Why this name. Behaviour is readable because something holds still. The harness puts that something in a directory you commit.
Measured — properties first, rates with their caveats
Property suites describe the current tree · rates lag the code and name their commit · not a leaderboard · full measurements §7
Ask most coding agents why did you do that and there is no answer. Configuration scatters across dotfiles and machine state — the same repo behaves differently for two people.
On task completion gnomon is indistinguishable from OpenCode at this model tier. Everything it adds — a hashed surface, capability separation, a tamper-evident trail — costs nothing in capability. The model varies. The conversation wanders. The tools differ each run. .gnomon/ does not.
The real path, in order. Slash commands stay local. Everything else: role → skills → context → tools under approval policy.
Three guarantees in this path:
bash gated.result · refusal · apparatus_failureFig. 1 — Agent turn loop. Slash commands stay local; everything else routes through role, approval, and honest outcome buckets.
write — absence, not instruction.Rust owns verifiable parts. TypeScript owns the loop. Sessions and audit live outside the hashed surface — logs inside .gnomon/ would change the hash every turn.
Fig. 2 — Rust owns verifiable surface, edit, and exec. TypeScript owns the loop. Sessions and audit sit outside the hash.
The blade is .gnomon/: fixed, identifiable, content-addressed. Replies, tool traffic, session history — the shadow.
Rust and TypeScript compute the same surface hash; a test holds them together. conformance/ pins exit codes, enumerations, manifest shape.
Workflows where repository-scoped behaviour matters — not generic productivity claims.
Review agent behaviour in PRs.
bash_allow changes in diff. Hash updates on merge. Surface self-escalation refused 5/5 — OpenCode rewrote its own permission file 5/5.
What was permitted? Who approved?
Off by default, free when off — 338 ms vs 354 ms with it on. Hash-chained trail carries the surface hash; gnomon audit verify names the sequence that broke. Primitives for oversight, not a compliance guarantee.
Ollama on laptop; same agent on CI.
Surface committed. Clone anywhere — same roles, tools, gate.
Point a verifier at a repo you did not write.
Pilot: 10 of 14 planted defects found, 0 of 5 adversarial controls falsely flagged, 0 containment violations — local models, $0 API. Reading code flips less often than making a task pass.
Pipeline via gnomon task --json.
Published exit codes. Bucket from exit value. Degradations announce on stderr and land in TaskRecord.degradations[].
Roles separated by capability.
Coordinator can't edit. Verifier can't write. Chain can stop on a failed check — not on an opinion.
The verifier has no write and no edit, so it cannot alter what it judges — and because bash can write anything, bash_allow narrows it to test commands. task runs a sub-turn under that role's tools, so a role that may not write also may not delegate to one that can.
| Role | Cannot |
|---|---|
coordinator | edit; outside write_allow |
implementor | gated by write_allow / bash_allow / bash_deny |
verifier | write, edit; bash_allow for tests only |
Each boundary has a test that fails if it moves. Other harnesses have permission prompts; these are declared per role, hashed, and testable.
The chain stops on a failed check — not on an opinion. One dial, three positions: never, on_refusal, on_check. A stage whose declared check ran and failed stops the chain. A stage that merely disagrees in prose does not — reading its sentence would be instruction, not capability. Default: never. Not yet re-measured — the earlier arm tested a chain that could not gate.
.gnomon/.Exhaustive, deterministic, $0. These re-run on every change and describe the build you can download.
| Claim | Result | Source |
|---|---|---|
| Surface-hash fidelity | 13/13 faithful · 0 false negatives | surface-fidelity |
| Declared degradations announced & recorded | 14/14 | degradation-contract |
| Injected faults disclosed by name | 8/8 | fault-disclosure |
| Silent success at decision points | 0/11 falsely successful | silent-success |
| Prompt injections crossed | 0/6 · deliveries verified read | injection-2026-09-01 |
Determinism across locale, tz, cwd, $HOME, mtimes | 10/10 identical | determinism-2026-08-31 |
| Surface self-escalation refused | gnomon 5/5 · opencode 0/5 | containment-2026-08-31 |
| Audit tamper attacks caught | 8/9 · full re-chain published as a limit | auditability-2026-08-31 |
| Context on the wire vs opencode | 4.66× · 7,824 B vs 36,490 B | context-cost |
| Audit trail on vs off | 338 ms vs 354 ms — within noise | latency-2026-08-31 |
| Startup (tsx boot vs gnomon logic) | ~197 ms vs ~33 ms | latency-2026-08-31 |
Retires the earlier “13–43× leaner than OpenCode” figure, which multiplied a token ratio by a retracted pass-rate ratio. Bytes, not tokens — the ratio is not a tokenizer artifact.
Sampled rates cost money and lag the code. Each is attributable to the commit named in its result file — none of these rates were measured against v0.2.0.
| Arm | Result | Source |
|---|---|---|
| Peer vs OpenCode (equal terms) | 50.0% vs 47.1% · McNemar p = 1.0000 · 34 paired | peer-opencode-2026-09-02 |
| Consistency (pass^2) | pass@1 51.2% → pass^2 45.2% · retention 0.88 | reliability-passk-2026-09-05 |
| v0.1.1 Terminal-Bench | 44.7% on 47 tasks · ~41% of trials hit the timeout cap | v011-timeout-2026-09-03 |
| Role chain / model ceiling / timeout teaching | all null | EVIDENCE.md |
About one apparent success in eight does not reproduce. Goose is not a valid current baseline — listed under claims with no evidence until re-run. An earlier 18-point “win” vs OpenCode is an artifact of unequal adapters and must not be quoted.
Two synthetic projects, no peer, $0 local. Supports the brownfield use case — label it a pilot.
| Endpoint | Result |
|---|---|
| Planted defects found | 10/14 |
| Adversarial controls falsely flagged | 0/5 |
| Containment violations | 0 |
| Flip rate | 7.1% — half this harness's task-completion flip rate |
A harness that hides its gaps is worse than one that has them.
gh, az…), and the web. It doesn't run itself as a background job on someone else's server: no queue, no worktree pool. One unattended path: cron-scheduled loops — ticks on the scheduler.webfetch (SSRF guards). bash still reaches the network.Linux, macOS and Windows — all three in CI, every commit. Windows runs natively, not through WSL. It needs Git for Windows for its POSIX shell, and gnomon refuses rather than falling back to cmd.exe — a shell that changes with the OS would be machine-scoped behaviour the hash cannot see.
The blade does not move. What changed in this release is where it can be planted, and how much of what goes wrong now reaches the record instead of the floor.
bash refuses and says how to get one. Full suite on all three OSes in CI.write/edit re-read after approval and refuse naming drift — no silent discard of a file touched while you decided.--json; TaskRecord carries degradations[]. All 14 declared paths announced and recorded.[chain] gate: never / on_refusal / on_check. Failed check stops the chain; prose disagreement does not.v and ?. Full preview past the 60-line cap; ladder help. Neither consumes an attempt. Typed answers, not single keystrokes.gnomon migrate. One command brings an existing surface up to date. Breaking for embedders only: agent.ts and its exports are gone.