andon
Hardens a repository stage by stage, proving each handoff between stages and halting rather than advancing past one it cannot prove.
The name. Andon is the Toyota Production System's signal: the cord a worker pulls to stop the line when a defect appears, so it is fixed where it happened instead of being passed downstream. This plugin applies the rule to a hardening loop. A wire it cannot prove stops the pass, and the halt is the intended outcome, not a failure.
- Use when a multi-stage repo (ingest, transform, publish) has handoffs nobody has proven, and you want gaps closed one stage at a time with each fix verified before the next begins.
- Not when you only want to know whether a test suite's assertions would catch a bug — use
zeugnis; or you want a planned modernization phase applied — usebefund.
Use case
Situation. A five-stage feature pipeline (ingest, normalize, enrich, score, publish) ships wrong numbers and nobody knows which handoff corrupts them. You want it walked in order, with every wire proven before the loop moves on. Call. /andon:andon-loopWrites. The OKF ledger under analysis/andon/ledger/ (log.md, stages/, gaps/, evidence/) and the detected topology in analysis/andon/current.json. Next. A wire reopened three times is marked escalated: true: the loop stops sub-cycling it and records the escalation in log.md. Fix its cause or decide it yourself, then run /andon:andon-loop again to resume from the cursor; /andon:andon-status shows the board.
### Pass 7 (cycle 2)
- stage: enrich
- wire: enrich->score
- gap: enrich-missing-provenance-check
- strategy: e
- verdict: red
- next_cursor: enrich::enrich-missing-provenance-check
- cycle: 2
- pass_number: 7
### Sub-cycle: enrich->score reopened (count 3)
- wire: enrich->score
- depth: 2
- reopen_count: 3
- escalated: trueExcerpt from plugins/andon/scripts/fixtures/sample_ledger/log.md.
Why this exists
Automated "fix loops" have an obvious failure mode: they propose something that looks right, move on, and the next stage inherits a fix that was never actually checked. andon borrows the Toyota andon cord — stop the line the moment a defect is found, rather than pass it downstream — and applies it to an AI hardening loop: every fix must be proven against its wire's contract by one of seven evidence-grounded strategies (adversarial tribunal, numerical V&V, and others) before the loop is allowed to advance to the next stage. A halt is the intended outcome for an unproven fix, not a bug in the loop.
What it is not
- Not for applying a single authorized phase from a
MODERNIZATION_BRIEF.md. That belongs tobefund(befund-transform-execute) — return toandon-loop, or straight toandon-verify, only for the proof afterward (skills/andon-loop/SKILL.md). - Not for judging whether an existing test suite's assertions would catch a mutation. That is
zeugnis's job (zeugnis:zeugnis-assertion-audit), notandon-verify's (skills/andon-verify/SKILL.md).
Install
/plugin marketplace add Anselmoo/werkstoff
/plugin install andon@werkstoffThe PreToolUse hook (see What is enforced, and what is not) is inert until a repo already has an andon ledger directory (analysis/andon/ledger by default, or wherever ledger_dir in .claude/andon.local.md points it) — the settings file only relocates or disables the check, it does not by itself activate anything — so installing andon changes nothing until andon-loop actually starts a pass.
Local development
Point Claude Code at a checkout without registering the marketplace:
claude --plugin-dir /path/to/werkstoff/plugins/andonOr copy this directory under a project's .claude-plugin/ for project-scoped testing.
Example Prompts
Say any of these to Claude Code once the plugin is installed — they're plain-language prompts, not exact phrasing Claude has to match. Claude routes them to the skill below by intent.
Check readiness first
"run andon-preflight against this repo"Triggers
andon-preflight— read-only readiness report (stage legibility, ledger writability, house-rules presence); never creates the ledger.
Start hardening
"harden this repo, one gap at a time"Triggers
andon-loop— detects the value stream, proposes and verifies a fix for the current stage's gap, and halts rather than advancing past a broken or unproven wire.
Check the board
"what does the andon board look like right now"Triggers
andon-status— read-only: stream table, cursor, pass/cycle counters, open gap counts; nothing advances.
Alongside the markdown board, andon-status can render the same evidence as a self-contained HTML report -- see The board, as an HTML report below.
Propose a fix
"propose a fix for this gap, only ask where it actually matters"Triggers
andon-propose— proposes maximally from the ledger/codebase/house-rules, then grills you one question at a time, only on genuinely load-bearing forks.
Prove a wire
"prove this wire is actually proven"Triggers
andon-verify— routes the wire to one of seven evidence-grounded strategies and returns a structured green/red verdict.
Resume a paused pass
"resume the andon ledger from where we left off"Triggers
andon-loop— continues an existing ledger's cycle rather than starting fresh, still refusing to advance past whatever gap stopped the last pass.
Run andon-preflight first in any repo — it's read-only and never creates the ledger — then andon-loop to start a pass, and andon-status at any point to see the board without advancing anything.
The andon rule
Three non-negotiable stop conditions enforced in code (check_stop_conditions() in scripts/andon_core.py), explained in full in references/andon-rule.md:
- A red wire verdict blocks advance until an explicit user re-run or override.
- A proposal's blast radius exceeding the configured authorization level blocks advance until the user explicitly confirms.
- A Tier 1 structural-evidence contradiction is never overridable, by anyone, under any circumstance -- there is no parameter in the enforcing function that can waive it.
Components
Skills (5)
| Skill | Purpose |
|---|---|
andon-loop | Orchestrates Phases 0-6: detect topology, init/resume the ledger, scan the cursor's stage for gaps, dispatch propose/verify, enforce the andon rule, advance the cursor, detect convergence. The sole writer to the ledger. |
andon-preflight | Read-only readiness report: stage legibility, ledger writability, house-rules presence, cross-plugin availability. Never creates the ledger. |
andon-propose | Proposes a fix for one gap maximally from the ledger/codebase/house-rules, then grills the user one question at a time only on genuinely load-bearing forks (gated by blast-radius tag). |
andon-verify | Routes a wire to one of seven evidence-grounded strategies via a deterministic classifier, runs the matching reference doc, and returns a structured verdict. Never writes to the ledger. |
andon-status | Read-only board: stream table, cursor, cycle/pass counters, active constraint, open gap counts, evidence-strategy mix, non-overridable holds. |
Agents (4, tribunal strategy, dispatched by andon-verify)
andon-defender, andon-challenger, andon-verifier, andon-adjudicator -- see agents/*.md for their exact refusal contracts. All four are read-only except andon-verifier, which may execute deterministic checks (tests, greps) but never modifies the artifact under review.
Scripts
scripts/andon_core.py is the single enforcement library + CLI. Every mechanical guarantee in the spec is implemented here as a real conditional that raises/exits non-zero on violation -- settings gating, OKF schema validation, write-scope enforcement, the three andon-rule stop conditions, sub-cycle bounds, the wire classifier, the Detection Ladder, the NO-PERSONA check, and untrusted-content fencing/masking. Skills invoke it as a CLI; the hook imports it as a library. No third-party dependencies.
scripts/build_board_html.py renders the andon board as a self-contained HTML report -- see The report below.
What is enforced, and what is not
Hooks
hooks/hooks.json registers a PreToolUse hook (hooks/andon_enforce.py) on Write/Edit that holds regardless of model cooperation. On every matched call it:
Checks the per-call escape hatch first -- if the
ANDON_DISABLE_GUARDenvironment variable is set to1, the hook allows unconditionally, before stdin is even read. See Escape hatch below.Checks the wholesale escape hatch -- if
.claude/andon.local.mdsetsenforcement: off(orfalse/disabled), the hook allows unconditionally.Is inert until a ledger exists -- if the configured ledger directory (
analysis/andon/ledgerby default;ledger_dirin the same settings file) is not a directory, the hook allows -- so it never polices a repo that hasn't started using andon.Allows any write outside the repository -- the target is resolved both lexically (
normpath) and via the filesystem (realpath); when both agree the target is outsidecwd, the hook allows unconditionally. A target that lexically looks like an escape but actually resolves back inside the repo (or vice versa) stays gated -- the two tests must agree before enforcement is skipped.Always allows a write targeting the ledger itself -- the loop must always be able to record its own halt.
Otherwise evaluates the andon rule's stop conditions (
stop_reason()inhooks/andon_enforce.py): an open or reopened gap with noblast_radiusvalue, ablast_radiusthat exceeds the configuredauthorization_level, a gap that has reopenedMAX_CONSECUTIVE_REOPENS(3) times, or an evidence doc recording aredorunknownverdict for its wire's latest evidence doc (an earlier verdict superseded by a later one for the same wire does not gate, matchingandon_core.py'scompute_wire_status) -- any of these denies theWrite/Editoutright, regardless of which file was targeted. Evidence whose gap has already closed (the gap'sresolved_bynames that evidence doc) is skipped too -- see Retiring a stale record below for the other way to stop a record from gating.An evidence doc's optional
superseded_bychains transitively to the record nobody supersedes -- its head -- and it is the head'sverdictandvalid_untilthat actually get judged, matchingcompute_wire_status(). A danglingsuperseded_by(names a slug with no evidence doc) or a cycle, wherever it occurs in the chain, denies outright -- fail closed, even when the record that started the resolution readsgreen.valid_until(an ISOYYYY-MM-DDdate) on the chain head makes the record gate as verdictunknownonce that date has passed, whatever it actually recorded; an unparseablevalid_untildenies the same way. A third optional field,measured_against, is purely informational and is named verbatim in any deny reason it causes. Seereferences/okf-ledger-schema.md's evidence section andreferences/andon-rule.md's supersession-and-expiry section for the full semantics.
Gating values are read tolerantly: a frontmatter key first (status, blast_radius, ...), then the legacy tags: ["kind:wire", "status:open"] array, then treated as genuinely absent -- a missing value is never inferred, only surfaced as a stop.
The hook fails closed on any internal error (malformed payload, import failure, unexpected exception), always naming the escape hatch -- see Escape hatch below.
Settings
.claude/andon.local.md is optional. See references/okf-ledger-schema.md for the full field table and defaults. Every andon skill reads this file first and halts immediately if enabled: false is set -- before running any phase, before touching the repo.
Git worktrees
A git worktree add checkout has neither analysis/andon/ledger nor .claude/andon.local.md of its own -- both live only in the main checkout. There is one shared ledger: both the PreToolUse hook and scripts/andon_core.py (its CLI and its library functions alike, so a direct caller like scripts/build_board_html.py gets this for free too) resolve the ledger and the settings file from the main checkout root, never from cwd or a caller-supplied repo_root directly, so:
- Editing source inside a linked worktree is gated by the same ledger that gates the main checkout -- a gap or a red/unknown verdict in the main ledger halts edits in the worktree exactly as it would in the main checkout, and
.claude/andon.local.mdat the worktree's own root (if one somehow existed) is never consulted. - A write issued from inside a worktree --
write-doc,retire,append-log, or the hook's own always-allowed write-to-the-ledger exception -- lands under the main checkout'sanalysis/andon/ledger, never a second, orphaned copy under the worktree. - The
git worktree add->PreToolUse hook denies an edit in the worktree using the main ledger's contentround trip is a permanent regression, covered byhooks/test_andon_enforce.py'sTestWorktreeLedgerResolutionandTestMainRootResolutionAgreement,scripts/test_andon_core.py'sResolveMainRootandLibraryFunctionsResolveFromWorktree, and (end to end, across realgit init+git worktree addfixtures)checks/probe_worktree.py's cases W1-W10 in the run that shipped this.
Resolution is a pure filesystem walk, no subprocess: walk up from the start directory to the nearest .git. An ordinary .git directory means its own parent is the main root (the non-worktree case, unchanged from before). A .git file (gitdir: <path>, what git worktree add writes) is read, and that gitdir's own commondir file is read in turn to find the common git directory shared with the main checkout -- that common directory's parent is the main root. Anything unreadable or malformed along the way (a .git file with no gitdir: line, a missing or empty commondir, a commondir naming something that isn't a directory) falls back to the start directory, exactly like running outside git entirely -- never a raise, never a hard failure. andon_core.py's resolve_main_root() is the implementation; the hook duplicates it verbatim in shape (it is stdlib-only and imports nothing from the plugin -- see What is enforced, and what is not above), with an agreement test pinning the two copies to the same behaviour.
The hook's pre-existing "a target outside cwd is none of this hook's business" containment check (#69) is unchanged -- it still compares against cwd, not the resolved main root. A write to the main checkout's own ledger path, issued from a worktree's cwd, still gets through: that path lies entirely outside the worktree's own cwd, so the existing outside-the-repo bypass already lets it through, for the same reason it always let any other out-of-repo write through.
The report
The board, as an HTML report
scripts/build_board_html.py renders the same evidence andon-status prints to the chat as one self-contained HTML file, written to <ledger_dir>/ANDON_BOARD.html.
The report answers one question -- where is the line stopped, and why? -- and states its answer in a sentence floating over the figure. The figure is the value stream, filling the first screen: a left-to-right diagram with a labelled edge per handoff -- green when linked evidence says green, amber when verification was attempted and hung, and dashed when no evidence doc exists at all. Those last two are different failures, not degrees of one, so the edge label spells the state out in words rather than leaving it to colour. Evidence documents hang under the wire they prove and open gaps under the stage they block. Clicking a stage, a wire, an evidence chip or a gap opens its detail -- description, ledger prose, verdict, tier, blast radius, proposal -- in a drawer beside the figure, and hovering shows a summary. The findings lists and the full tables follow below the figure.

That image is reproducible rather than a one-off capture — the ledger it shows is committed at scripts/fixtures/sample_ledger/, a real OKF directory tree (log.md + stages/ + gaps/ + evidence/) authored in the schema render_board() actually reads: status, kind, blast_radius, verdict and tier as first-class frontmatter keys, never buried in a tags: array. Its five stages are wired so that one handoff is proven, one hung, one contradicted by the structural index at Tier 1, and one never verified at all. To rebuild it:
python3 plugins/andon/scripts/build_board_html.py \
. plugins/andon/scripts/fixtures/sample_ledger \
--template plugins/andon/assets/board-viewer.html \
--d3 plugins/andon/assets/inline-d3.html \
--tokens plugins/andon/assets/tokens.css \
--out /tmp/andon-demo/ANDON_BOARD.htmlscripts/fixtures/sample_ledger/README.md documents exactly which defect each stage, gap and evidence doc is there to expose.
Design decisions
(spec was silent here)
The behavioral spec states obligations, not implementations. Where it was silent on a mechanical detail, these choices were made:
- Ledger doc parser is hand-rolled, not PyYAML. andon controls both the writer and reader of every OKF doc, so a minimal frontmatter codec (
parse_frontmatter/dump_frontmatterinandon_core.py) covers the constrained subset needed (scalars, bools, ints, flow/block lists) without adding a third-party dependency that might not be installed in every target repo's Python. - NO-PERSONA detection is a denylist + regex heuristic, not a semantic understanding of "appeal to authority." The spec requires this be checked in code rather than left to model discipline; a heuristic that catches common named-authority patterns (a curated denylist of frequently-invoked names, plus a regex for "as/per/according to
<Proper Name>said/argued") is a real, testable check, even though it cannot catch every phrasing. - Wire-classifier trigger order (
e -> b -> f -> g -> d -> c -> a) is the plugin's own choice, reasoned inskills/andon-verify/references/wire-classifier.md: strongest ground-truth evidence class first (structural index), then strategies with no external prerequisite (numerical, property, verify-the-verifier), then strategies with a plugin dependency (agentic-reliability), then epistemic claims, with tribunal as the universal prerequisite-free fallback -- never a starting default. - Detection Ladder defect-class taxonomy (
type-or-schema,structure-or-lint,deterministic-behavior,rendered-assertion,subjective-quality) is this plugin's own vocabulary mapped 1:1 onto the spec's five rungs, since the spec named the rungs but not a defect-class enum to key off of. - Sub-cycle escalation fires on the 3rd reopen, matching "reopens 3 or more times" literally (
new_count >= SUB_CYCLE_REOPEN_LIMITwhereSUB_CYCLE_REOPEN_LIMIT = 3) rather than waiting for a 4th. - The hook only guards
Write/Edit. The spec's mechanically-checked rules that are inherently semantic (strategy routing correctness, NO-PERSONA phrasing, Detection Ladder rung necessity) are enforced by the CLI script the skills are instructed to call, not by a blind file-content hook -- aPreToolUsehook can reliably gate where a write lands and whether the ledger's own gating fields are internally consistent, but cannot itself judge whether, say, the right verification strategy was chosen. okf visualizeis treated as an external, optional tool the plugin does not ship or implement --andon-status's instructions say to attempt it best-effort and fall back to the markdown board (which is always authoritative) when it's absent, per the spec's own "never fail or delay the primary path" requirement.plugin.jsonauthor is a placeholder (andon plugin/noreply@example.com) -- update it to the actual maintainer before publishing to a marketplace.
Verifying a change to this plugin
# Enforcement library smoke tests
python3 scripts/andon_core.py load-settings .
python3 scripts/andon_core.py route-wire '{"is_numerical": true}' '{}'
python3 scripts/andon_core.py check-stop-conditions --verdict red --authorization-level local+reversible
# Preflight against this repo (read-only)
python3 scripts/andon_core.py preflight .Escape hatch
Three remedies for a denial, narrowest first -- the deny message itself names all three:
- Retire the stale record. If the denial is a gap or evidence doc that should no longer gate (see Retiring a stale record below), retire it rather than reaching for an environment variable or a settings flag -- this fixes the ledger instead of bypassing the hook.
ANDON_DISABLE_GUARD=1bypasses thePreToolUsehook for one call -- set it in the environment of the single command that needs to get past a wrong or unwanted denial, then unset it again.hooks/andon_enforce.pychecks this first thing inmain(), before stdin is even read.enforcement: off(also acceptsfalseordisabled) in.claude/andon.local.mddisables thePreToolUsehook outright, for every future call in the whole repo, until the setting is reverted --hooks/andon_enforce.pyreads it insettings(). This is the wholesale option; reach for it only when the first two don't fit.
enforcement: off is a separate switch from enabled: false in the same file: that one is read by scripts/andon_core.py's enforce_enabled_or_halt(), and it is the skills (andon-loop, andon-status, ...) that read it and halt before running any phase -- it does nothing to the hook. The hook and the skills each have their own kill switch, and both live in .claude/andon.local.md.
Deleting the ledger directory (analysis/andon/ledger by default, or whatever ledger_dir names in that file) also makes the hook inert, since it allows immediately once the configured ledger directory does not exist.
Retiring a stale record
python3 plugins/andon/scripts/andon_core.py retire <repo_root> <ledger_dir> {gaps|evidence} <slug> --reason "<why>" moves a gap or evidence doc from gaps//evidence/ into the matching subdirectory under retired/, and appends a retire entry to log.md recording the kind, slug, and reason. This is how a gap closed by a later re-verify (whose old evidence doc is still sitting there recording a stale red/unknown verdict) or a mis-filed record stops gating -- without rewriting the append-only ledger's history by editing status or verdict in place. See references/okf-ledger-schema.md's retired/ section for why this is enough on its own: the enforcement hook's _list_md() and andon_core.py's read_all_docs()/render_board() never walk retired/, so a retired record disappears from both the gate and the board by construction, the same way a nonexistent one would.