line stopped · divergent counts downstream: no — the recompute disagreed, and named where

andon · 行灯 · the cord that stops the line

The signal that stops the line, for code an agent just wrote.

Andon measures a change and returns a verdict that carries its evidence: the claim each number stands on, the tier of that claim, and what the number does not predict. An agent calls it mid-loop over MCP; a person runs andon measure.

Every measurement is a self-report until an independent recompute confirms it. A forged one comes back divergent, with the disagreeing metrics named — the transcript on the right is that happening.

verdicts pass · advise · block · escalate_to_human — exit codes 0 · 0 · 2 · 3. Rust, two binaries — andon and andon-mcp — nothing hosted, no accounts.

$ andon demo tamper Transcript

  ANDON DEMO — a forged measurement, caught. Everything below happens in a
  throwaway repository under %TEMP%\andon-demo-26308-1787870809251363600; nothing else is touched.

  LEG 1 — an honest change.

  The agent measured its change (35e5e38e188d → e9b7844f9954) and recorded the result as a git note —
  a self-report. Verdict: PASS, 35 finding(s). Trust so far: unwitnessed — counts
  downstream: no. A self-report is a claim; nothing has checked it yet.

  Now the verifier recomputes the same change from the repository's own
  objects and compares per-result digests:

  attestation   confirmed
  meaning       CI recomputed this change independently and every compared digest matched
  counts        yes — a record with this value counts as evidence downstream
  self-report   found, measured against 35e5e38e188d (Equal relative to the base this verifier resolved)
  digests       35 matched, 0 disagreed, 0 unpaired

  verifier's own verdict on this change, from its own recompute: PASS
    measurement-incomplete     this measurement is unwitnessed: some of what it set out to measure was not measured, and the results say which
  The two are reported side by side and not combined. Composing them is P9's two-axis rule, and it is not implemented here.

  THIS IS A STUB. It recomputed the change and compared digests. It did NOT run a hermetic version-matched recompute, did NOT run held-out verification sampling, did NOT compose the verifier's own verdict with this attestation, and did NOT write an attestation record to refs/notes/andon-attest. P9 builds the verifier; treat this output as a demonstration of the compare path, not as an attestation anyone should rely on.

  LEG 2 — a forged self-report.

  The second change is just as ordinary (35 finding(s) when measured honestly).
  The lie is not in the diff — it is about to be written into the ledger.

  A DIFFERENT program — the adversary binary, andon-spike-forge.exe —
  rewrote the note: every count one higher than measured, every digest
  re-sealed. The forged record is internally consistent and false; no
  inspection of the record alone can catch it. (`andon` ships no forging
  path of its own: the forging logic lives in one file compiled only into
  the adversary, a build-failing test guards that file's crate, and the
  rest of the workspace this binary links is kept clean by review, not by the scan.)

    adversary: forged 1 record(s) on b7218bcc340d903696ddec03d46b95bf120a5e8f (inflate-metric)

  The same verifier, on the forged report:

  attestation   divergent
  meaning       CI recomputed this change and the numbers disagree, or a tamper signal fired
  counts        no — this record does not count as attested evidence downstream
  self-report   found, measured against 35e5e38e188d (Equal relative to the base this verifier resolved)
  digests       0 matched, 35 disagreed, 0 unpaired
                  disagreed: clones.clone-groups
                  disagreed: clones.duplicated-token-ratio
                  disagreed: clones.duplicated-tokens
                  disagreed: clones.file-duplicated-tokens
                  disagreed: clones.largest-clone-tokens
                  disagreed: process.change-coupling
                  disagreed: process.churn-commits
                  disagreed: process.churn-lines
                  disagreed: process.code-age-days
                  disagreed: process.hotspot
                  disagreed: process.ownership-entropy
                  disagreed: static.cognitive-complexity.typescript
                  disagreed: static.cognitive-complexity.typescript
                  disagreed: static.cyclomatic-complexity.typescript
                  disagreed: static.cyclomatic-complexity.typescript
                  disagreed: static.parse-errors
                  disagreed: static.parse-missing
                  disagreed: static.sloc
                  disagreed: static.sloc
                  disagreed: static.sloc
                  disagreed: static.unmeasured-files
                  disagreed: tamper.assertion-free-test
                  disagreed: tamper.assertion-free-test.magnitude
                  disagreed: tamper.coverage-exclusion-drift
                  disagreed: tamper.coverage-exclusion-drift.magnitude
                  disagreed: tamper.lookup-table-blowup
                  disagreed: tamper.lookup-table-blowup.magnitude
                  disagreed: tamper.parse-error-delta
                  disagreed: tamper.parse-error-delta.magnitude
                  disagreed: tamper.suppression-density
                  disagreed: tamper.suppression-density.magnitude
                  disagreed: tamper.test-removal
                  disagreed: tamper.test-removal.magnitude
                  disagreed: tamper.threshold-config-edit
                  disagreed: tamper.threshold-config-edit.magnitude

  verifier's own verdict on this change, from its own recompute: PASS
    measurement-incomplete     this measurement is unwitnessed: some of what it set out to measure was not measured, and the results say which
  The two are reported side by side and not combined. Composing them is P9's two-axis rule, and it is not implemented here.

  THIS IS A STUB. It recomputed the change and compared digests. It did NOT run a hermetic version-matched recompute, did NOT run held-out verification sampling, did NOT compose the verifier's own verdict with this attestation, and did NOT write an attestation record to refs/notes/andon-attest. P9 builds the verifier; treat this output as a demonstration of the compare path, not as an attestation anyone should rely on.

  WHAT THIS SHOWED
  Both legs wrote the same kind of self-report. Inspection could not tell
  them apart — the forged one is correctly formatted and self-consistent.
  The independent recompute told them apart: confirmed on the honest one,
  divergent on the forged one, with the disagreeing metrics named. Until
  that recompute runs, a measurement is self-reported and counts for
  nothing downstream. In real use the recompute is CI's job — `andon init
  --ci` prints the recipe — and this command exists so you can watch the
  distinction work without setting any of that up.
Real output of andon demo tamper (andon 0.1.0, a development build), captured 2026-08-27 on Windows and reproduced verbatim — except that the temp directory is shown as %TEMP%. The demo stages a throwaway repository, measures an honest change and a forged self-report, and asserts both outcomes rather than narrating them. This is a typed replay of the captured text, not a screen recording; a .cast recording is deferred.

the trust boundary

A measurement is a claim until something you do not control recomputes it.

Two channels, and the difference between them is the design. The agent's binary measures from git blob bytes named by OID — never from the working tree — and appends a self-report to refs/notes/andon-measure. The verifier resolves the base for itself, checks out the exact head SHA, recomputes the same deterministic set from the same objects, and compares per-result digests byte for byte.

agent side git objects — the only shared truth verifier (ci) agent or harness measure_change (MCP) · hook · CLI one change andon measure reads blobs by OID, never the tree one digest per result, fixed order self-report refs/notes/andon-measure record_kind: self-report counts downstream: no blobs, named by OID same bytes on every OS — a CRLF checkout changes nothing notes refs andon-measure · andon-attest several reports? the worst counts reads appends same OIDs reports base = merge-base(main, head) resolved by the verifier itself; checkout pinned to the head SHA recompute the deterministic set compare set: the verifier's own registry flag, never the record's compare, in this order 1 tuple 2 regime 3 digests 4 a confirmation has to be earned confirmed · confirmed-static divergent unwitnessed · -version-skew unwitnessed-base-mismatch → refs/notes/andon-attest writes
What the compare reads, and in what order. An unequal base/head tuple is classified before anything else — an ancestor of the trusted branch is a stale base and demotes to unwitnessed-base-mismatch; an OID the repository has never seen is base-fabrication and divergent. A different engine, grammar or git version is unwitnessed-version-skew, never an accusation. Values that legitimately differ between two honest machines — freshness.*, invocation.*, policy_hash, tool.version — are recorded and never compared.
one change base → head, by OID fast lane · sub-second static · clones · tamper · process · artifacts content read from blob OIDs; a cold cap says when a content engine may still start — past it, it spills (completeness: partial) lane: fast deterministic = true → digest-compared the verifier can recompute every one and compare it byte for byte async lane · deferred the repository's own test command sandboxed in a cold worktree of the snapshot; never in the fast budget — measure leaves a job file and andon wait runs it, then merges lane: async deterministic = false → reported, never digest-compared; a failing suite still blocks under the default policy
The two lanes of one measurement. The fast lane is the agent's sub-second answer, and everything in it that is seed-free and byte-reproducible enters the compare. The async lane holds what no fast budget can contain — the test suite, sandboxed — and the content engines when a cold, very large repository would blow the cap. Nothing in the async lane is silently dropped: a spill is named as completeness: partial, and andon wait reports what the lane still owes.
Attestation values and whether each counts as evidence downstream
attestationmeanscounts downstream
confirmedCI recomputed the deterministic set; every digest matched.yes
confirmed-staticFork tier: recomputed from an unprivileged job with no self-report to compare against. A pass, labelled as the weaker one it is.yes
divergentDigests disagreed on an equal tuple at an equal regime, or a tamper signal fired.no
unwitnessedNo CI recompute, or nothing was actually compared. Neutral, not negative.no
unwitnessed-version-skewThe regimes differed, so the digests were never comparable.no
unwitnessed-base-mismatchThe claimed base is an ancestor of the trusted branch — a stale base or a rebase.no

What the attestation does not prove.

In v1, attestation trust is GitHub Actions provenance, not cryptography. refs/notes/andon-attest is an ordinary git ref: anyone with push access can write a record by hand, claiming any value including confirmed, and nothing in v1 can tell it from one the workflow produced. What a genuine attestation establishes is that the numbers were recomputed by a process the measured party did not control, from a clean checkout pinned to the head SHA, against a base the verifier resolved itself. Treat confirmed from a repository whose push access you do not already trust as unverified. Keyless signing of the record is the named v1.5 hardening.

One more open hole, stated rather than hidden: engine_version is self-asserted and the regime check runs before any digest is compared, so a forger can buy silence — an unwitnessed-version-skew that looks like a stale binary. It cannot buy a pass; the record stays self-reported, repeated skew on one head escalates to a person, and a hermetic version-matched recompute is what closes it.

the evidence registry

Every number stands on a claim, and every claim says what it does not predict.

There is no composite score anywhere in the system. Each metric names the claim it rests on — a tuple of measurement, version, language and predicted outcome — and each claim carries a tier, a citation, a population, and a does_not_predict list that andon explain prints beside the number. The section below is generated from the registry files, not written by hand.

Derived from registry/*.toml, last changed in 429034d: 39 metrics in 6 families, standing on 20 claims. 36 of the 39 are deterministic and enter the digest compare; the other 3 are reported and never compared.

A 3 B 9 C 1 D 0 N 7
Claims by evidence tier, A to N. A validated against outcomes at scale · B published validation, narrower population or weaker linkage · C weak or contested on its own · D critiqued; not to be used as a headline · N novel and unvalidated — motivated by evidence, not yet supported by it. Tier N carries every tamper detector: they are calibrated on Andon's own corpus, not a study.
  • artifacts engine artifacts

    metrics
    1
    claims
    1
    tier
    C
    compared
    0 of 1

    does not predict test quality — a line executed by a test with no assertion counts as covered

    andon.artifacts.diff-coverage@1|any|test-gap

  • clones engine clones

    metrics
    5
    claims
    1
    tier
    N
    compared
    5 of 5

    does not predict that removing the duplication would improve anything — the literature reports prevalence and co-change, not remediation benefit, and an extracted abstraction can be worse than the repetition it replaces

    andon.clones.token-duplication@1|any|token-duplication

  • process engine process

    metrics
    6
    claims
    5
    tiers
    A B
    compared
    6 of 6

    does not predict that this file will fail — the studies rank metric families for prediction, not individual files for outcomes

    andon.process.churn@1|any|defect-proneness

  • static engine static-metrics

    metrics
    11
    claims
    8
    tiers
    A B N
    compared
    11 of 11

    does not predict that a smaller file is a better file — size is the confound to control for, not the thing to reduce

    andon.static.sloc@1|any|maintenance-effort

  • tamper engine tamper

    metrics
    14
    claims
    4
    tier
    N
    compared
    14 of 14

    does not predict that the removal was illegitimate — deleting the tests for a deleted feature is correct, and this signal cannot tell the two apart

    andon.tamper.test-evidence@1|any|test-evidence-withdrawal

  • tests engine tests

    metrics
    2
    claims
    1
    tier
    N
    compared
    0 of 2

    does not predict that the suite exercises the changed code — an empty or trivial test_command passes everything, and declaring one is visible in policy, not prevented by it

    andon.tests.suite@1|any|regression-signal

Which languages, and which not.

Four grammars are read: JavaScript, TypeScript, TSX and Python — plus Rust for size only, with no complexity and no parse health. The process and artifacts families are language-independent because they read git history and report files. A file in any other language is not measured: a change touching only Go, Java, Ruby, PHP, C, C++, C#, Swift or Kotlin can return pass because nothing looked at it, not because nothing was wrong. Andon is not a security scanner and ships no SAST family; it will not find an injection or a hard-coded credential in any language.

the agent profile

The verdict carries its evidence.

This is the bounded profile an agent receives — andon measure --profile agent-mode, the same shape the MCP measure_change tool returns — for a change that added bulk-pricing tiers, deleted one failing test instead of fixing it, and left another test calling the function without asserting anything. Findings come worst-first. Nothing is added up.

{
  "schema_version": 2,
  "profile": "agent-mode",
  "attestation": "unwitnessed",
  "counts_downstream": false,
  "completeness": "unwitnessed",
  "base_oid": "25241e8397ad23a51739a31f48eaa76e1ebcb5af",
  "head_oid": "8991206123cfb7b7e23929240647aad823e08440",
  "verdict": "block",
  "reasons": [
    {
      "code": "tamper-signal",
      "message": "tamper.assertion-free-test fired: the line stops",
      "severity": "critical"
    },
    {
      "code": "tamper-signal",
      "message": "tamper.test-removal fired: the line stops",
      "severity": "critical"
    },
    {
      "code": "measurement-incomplete",
      "message": "this measurement is unwitnessed: some of what it set out to measure was not measured, and the results say which",
      "severity": "info"
    }
  ],
  "findings": [
    {
      "claim_id": "andon.tamper.test-evidence@1|any|test-evidence-withdrawal",
      "delta": null,
      "diff_actionable": true,
      "evidence_stale": false,
      "evidence_tier": "N",
      "metric_id": "tamper.assertion-free-test",
      "scope": "change",
      "severity": "low",
      "value": {
        "kind": "flag",
        "value": true
      }
    },
    {
      "claim_id": "andon.tamper.test-evidence@1|any|test-evidence-withdrawal",
      "delta": null,
      "diff_actionable": true,
      "evidence_stale": false,
      "evidence_tier": "N",
      "metric_id": "tamper.test-removal",
      "scope": "change",
      "severity": "low",
      "value": {
        "kind": "flag",
        "value": true
      }
    },
    /* … 9 findings elided: clones, artifacts, process … */
    {
      "claim_id": "andon.process.churn@1|any|defect-proneness",
      "delta": null,
      "diff_actionable": false,
      "evidence_stale": false,
      "evidence_tier": "A",
      "metric_id": "process.churn-commits",
      "scope": "src/cart.ts",
      "severity": "info",
      "value": {
        "kind": "count",
        "value": 2
      }
    }
  ],
  "total_findings": 52,
  "truncated": true,
  "iteration": {
    "cap": 3,
    "count": 1,
    "escalated": false
  }
}

Real output, 2026-08-27, against a two-commit scratch repository; an excerpt of the 52-finding profile with nine findings and a few scalar fields elided, marked where they are. The full record is andon measure --json.

verdict: block
The line stops. Reserved for tamper signals, test failures, and medium-or-worse findings on metrics the agent can fix inside its own change. Exit code 2.
reasons[].code
Why the verdict is what it is, in the tool's own vocabulary — here two tamper signals, and the note that the record is unwitnessed. An agent acts on reasons; a person reads the findings.
claim_id
The evidence tuple a number stands on: andon.tamper.test-evidence at version 1, for any language, predicting test-evidence-withdrawal. Pass it to explain_finding and the citation, population, effect and re-review date come back.
evidence_tier: N
Novel and unvalidated — calibrated on Andon's own corpus, not on a study. Stated on the wire so a consumer can weigh it, rather than implied by the severity.
diff_actionable: true
The agent can fix this inside the change it just made. A false here is the signal not to grind: the finding is real, and it is not this change's to fix.
counts_downstream: false
A self-report. Until CI recomputes the change, every rendering says so.

andon explain tamper.test-removal — "What this number does NOT tell you"

What the number that stopped the line does not tell you.

  • that the removal was illegitimate — deleting the tests for a deleted feature is correct, and this signal cannot tell the two apart
  • that an assertion-free case is worthless — a smoke test that checks a call does not throw is a real test
  • defect density, or the quality of the tests that remain
  • anything about tests in languages this engine has no grammar for, which are not counted at all
  • how it behaves on a language this claim's own corpus barely covers. The should-pass half is the only source of the false-positive rate, and for these two detectors it is 8 TypeScript cases for `test-removal` — every case it has — and 6 TypeScript beside 1 Python for `assertion-free-test`. No JavaScript case, no TSX case, for either. The claim tuple says `any` because the rules are written on node kinds rather than per-language branches, and the four grammars are read the same way; that is a statement about the MECHANISM. The precision figure is a statement about the EVIDENCE, and the evidence is one language
  • anything about test cases the parser could not read — a case inside an ERROR region is not counted on either side, so a removal there is reported as no removal at all. Results over a file the parser did not finish are marked `parse-degraded` and the count is a lower bound (PREMORTEM T3)

One thing to weigh before trusting a block.

The two findings above are severity: low — tier N is not admitted to the blocking band under the default policy — and the verdict is still block. A tamper signal stops the line regardless of tier. That is deliberate: a gaming signal that could only advise would not be a gaming signal. It also means the one verdict class not capped by evidence tier rests on the project's own constructed corpus rather than on external study. The next section is what that corpus can and cannot show.

the tamper suite, measured

The miss rate, published.

Seven detectors answer four questions: has the suite stopped verifying things, is there code the static engines can no longer read, did the quality bar move instead of the code, was an implementation replaced by its expected answers. The precision and recall floors were set before the corpus was measured, and a set of evasions is withheld from the repository to measure whether the public cases have been fitted to. Here is what both say.

102

constructed changes in the public corpus — 51 that must fire, 51 that must not — frozen before measurement.

fixtures/adversarial · corpus v1, 2026-08-17
7 of 7

detectors clear the ex-ante floors on that corpus: precision ≥ 0.80, recall ≥ 0.70. A build-failing test keeps the published table equal to the one the code produces.

tests/corpus_floors.rs
3 caught · 8 evading

on the eleven withheld cases, re-run against this build. The three caught (filled) are shapes a detector has since been fixed for; the eight (hollow) the detectors have never been shown all evade — 0 of 8.

held-back set · re-run 2026-08-27
6 of 7

detectors sit below the 0.70 recall floor on the withheld cases — four of them at 0.00 — against 0.88 or better on the public corpus. The one that clears it does so on a single case.

same re-run

Evasions the suite is known to miss.

Written down rather than discovered, with the detector each one walks past. None needs insider knowledge; these are the first moves anyone probing a rule-based detector tries. Closing each is a named piece of work — reachability, constant folding, a config graph, data flow — not a threshold.

  • an extends swapped to a looser base config threshold-config-edit
  • real cases replaced by differently-named tautologies assertion-free-test · test-removal
  • a rule deleted rather than downgraded threshold-config-edit
  • an option raised inside an array-form rule whose name does not say "ceiling" threshold-config-edit
  • a rule value spread across several lines, then downgraded threshold-config-edit
  • an eslint severity written as a number, 20 threshold-config-edit
  • a runtime early return in every test case test-removal
  • one blanket file-level eslint-disable suppression-density
  • an exclusion pattern replaced by one not anchored above it coverage-exclusion-drift
  • a lookup table assembled at run time lookup-table-blowup
  • logic moved into a string and evaluated parse-error-delta

What these numbers mean, and what they do not.

They are not field precision and recall. The public table describes how the detectors do on 102 changes written by the same person who wrote the detectors. The perfect columns in it were the weakest evidence: a first pass scored 1.00 everywhere and was a corpus restating its own implementation, until boundary cases were added and found four real bugs.

The withheld set holds two kinds of case, and they are given separately. Overfitting probes are evasions the detectors have never been shown in any form; regression specimens are shapes a detector was fixed for, kept so the fix stays proven. Only the first kind measures generalisation. On this build the probes stand at 0 of 8 caught and the specimens at 3 of 3 — a combined "3 of 11" would rise with every bug fixed while the property it appears to measure stayed flat, which is why it is not the headline.

The number that would mean something is the false-positive rate on real, honest changes. That is measured in a running window — at least thirty changes over at least fourteen days — and gates the public release; it is not a corpus figure.

The defensible claim is narrow: these detectors catch the patterns they enumerate. They are not a general defence against a determined adversary, and the project does not claim they are. The public corpus is refreshed quarterly precisely because a published list of what fires is also a list of what to avoid.

install

Two binaries. Pick a channel.

Two executables ship together: andon, the command line, and andon-mcp, the MCP server that andon init registers for an agent's harness. The shell and PowerShell installers take one line per executable; the Homebrew line installs both; npm carries them as @gtm-k/andon-cli and @gtm-k/andon-mcp. Nothing hosted, no account, no configuration file to write before the first measurement.

shell
curl --proto '=https' --tlsv1.2 -fsSL https://github.com/gtm-k/andon/releases/latest/download/andon-cli-installer.sh | sh
curl --proto '=https' --tlsv1.2 -fsSL https://github.com/gtm-k/andon/releases/latest/download/andon-mcp-installer.sh | sh
powershell
powershell -ExecutionPolicy Bypass -c "irm https://github.com/gtm-k/andon/releases/latest/download/andon-cli-installer.ps1 | iex"
powershell -ExecutionPolicy Bypass -c "irm https://github.com/gtm-k/andon/releases/latest/download/andon-mcp-installer.ps1 | iex"
homebrew
brew install gtm-k/tap/andon-cli gtm-k/tap/andon-mcp
npm
npx @gtm-k/andon-cli

Artifacts are named after the packages, andon-cli and andon-mcp; the commands they install are andon and andon-mcp.

Then, in a repository.

andon init installs a gate-shaped hook for your harness, removably, and andon init --ci prints the recipe that turns self-reports into attestations. andon measure measures the change in flight — with uncommitted work in the tree it measures the working tree, and the report says which it used. andon explain <metric-id> prints the claim behind any number, and what it does not predict.

first measurement
andon init
andon measure