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 — 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.
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.
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.completeness: partial, and andon wait reports what the lane still owes.| attestation | means | counts downstream |
|---|---|---|
| confirmed | CI recomputed the deterministic set; every digest matched. | yes |
| confirmed-static | Fork tier: recomputed from an unprivileged job with no self-report to compare against. A pass, labelled as the weaker one it is. | yes |
| divergent | Digests disagreed on an equal tuple at an equal regime, or a tamper signal fired. | no |
| unwitnessed | No CI recompute, or nothing was actually compared. Neutral, not negative. | no |
| unwitnessed-version-skew | The regimes differed, so the digests were never comparable. | no |
| unwitnessed-base-mismatch | The 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.
-
artifactsengineartifacts- 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 -
clonesengineclones- 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 -
processengineprocess- 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 -
staticenginestatic-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 -
tamperenginetamper- 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 -
testsenginetests- 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-evidenceat version 1, for any language, predictingtest-evidence-withdrawal. Pass it toexplain_findingand 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
falsehere 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.
constructed changes in the public corpus — 51 that must fire, 51 that must not — frozen before measurement.
fixtures/adversarial · corpus v1, 2026-08-17detectors 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.rson 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-27detectors 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-runEvasions 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
extendsswapped 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,
2→0threshold-config-edit - a runtime early return in every test case test-removal
- one blanket file-level
eslint-disablesuppression-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.
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 -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"
brew install gtm-k/tap/andon-cli gtm-k/tap/andon-mcp
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.
andon init
andon measure