- Status: Proposed — implemented 2026-08-20; §8's measurements are still owed
before Accepted. The implementation lane delivered everything this record
called for: the refusal (
coordinator.RequireBuildEvidence/*MissingBuildEvidenceError), the flag (--accept-no-build-evidence), the snapshot field (runstate.BuildEvidence), the two disclosure sites (VerifyAdvice's declared variant andnoteMissingBuildEvidenceon the plan screen), exit 3, the pinned tests of §4 (cmd/oh-my-graph/buildevidence_test.go), the documentation and plugin surfaces of §7, and the## [Unreleased]CHANGELOG entry. What is NOT done is §8: (a) the firing rate and the exits taken, and (b) the table's false-positive rate. Until those exist this stays Proposed. - Reviewed 2026-08-20, after implementation. One defect and four coverage
gaps, all at the seams between legs and surfaces rather than in the fresh
autoleg: the resumed leg's recorder erasedbuild_evidence, which is the one thing §2.6'sresumerow relies on (fixed; §2.6, §4.1(9));chat's launch, the goal loop's later cycles, Codex and the inherited"."hazard are now pinned (§4.1); §4's claim aboutt.Chdirwas wrong about its own lane's output and is corrected in place; the probe now reports by default so the command this header cites works off this machine; and three trades that were being inherited rather than chosen are written down — chat's session-long staleness window and the quickstart's opt-out-first order (§6), and the fact that no finished-run surface reads the record back yet (§2.5b). - Reviewed again 2026-08-20, on the final state of the lane. All gates green
and nothing blocking; four findings, all in this record's prose or its
honesty about its own numbers rather than in the mechanism. One was a real
user-visible defect: both receipt sentences said "this run's
state.jsonrecords it", which--plan-onlycontradicts twice on one screen because it writes nostate.jsonat all — the wording is now conditional at both sites and the missing--plan-only+ opt-out cell of §4's matrix is pinned (§2.5b). The other three are limits this record was overstating past: §8(a)'s N counts launches that got a run directory, not launches, so the firing rate it yields is a floor and an abandoned refusal is invisible (§8a, §9); and the one glob-matched marker has a false negative theos.Statmarkers cannot have (§6). - Date: 2026-08-20
- Revised 2026-08-20 after design review, before any code existed. Six
changes, each argued where it lands: the recording was inverted relative to
the measurement it exists to enable and is now written on every
autolaunch, not only the declared one (§2.5, §8a); the gate moved out ofautoFlags.parseto the site that already detects (§2.1); the contradiction refusal moved out ofVerifyCommandto the FlagSet that registers both flags (§2.3); the chat answer is filed as a disclosure, not a declaration (§2.5, §2.6); the refusal's channel and exit code are specified (§2.4); and the greenfield exemption, the agent aliaser, the documented invocations that break and the authorize-the-suggestion alternative are written down rather than left to be discovered (§5, §6, §7). - Measurement:
docs/measurements/0030-auto-runs-carry-no-build-evidence.md, re-derivable withpython3 docs/measurements/probes/0030-auto-build-evidence/count.py, which reports the reader's own corpus. Its--checkmode pins the frozen numbers in that file and is expected to reportCORPUS MOVEDon any other machine, and on this one from the nextautorun onward. That probe measures ADR 0016's definition (asuccess_check.verifyin the snapshot's graph) and cannot answer §8(a), whose strata are read from thebuild_evidenceblock this ADR adds; a second probe is part of what §8 owes. - Issues: #119 is the precedent this acts on; this record opens no new one.
- Amends
0016-build-evidence-is-a-user-supplied-engine-command.md§3 in exactly one direction: build-signal detection, which §3 confined to producing prose, may now decide a refusal. It still may never decide a grant. §3.5 below states the amended rule and why the direction is the whole of the safety argument. Everything else ADR 0016 decided — the untouchedplannedToolAllowlist, sink attachment, the serialization, the retry cap, theresumere-supply rule, the verdict provenance qualifier — stands exactly as written. - Line citations are anchors for a reader, not addresses the code maintains. When one disagrees with the file, trust the named symbol.
Of this machine's 8 auto runs, 1 carries engine-run build evidence — the
run of 2026-08-18, where the operator passed --verify-cmd. Seven ended with
every judgement in the run made by the model, about its own work, on a
result_matches gate a node passes by emitting the right word
(internal/graph/graph.go, ResultMatches: "Self-reported").
The measurement file above carries the rows, the definitions and the script.
It also carries the correction that must travel with the number: six of those
seven predate --verify-cmd, which shipped 2026-08-06 with ADR 0016. The
eligible corpus is n = 2 and splits 1/1. So:
- "7 of 8 auto runs verified nothing" is established. It is the shape of a normal run on this machine.
- "the printed notice does not work" is NOT established by it, and this ADR does not rest on it. Six of the seven never saw the notice.
One further figure was supplied with this task — that an advisory which fired 30 times moved the behaviour it warned about from 42% to 33% (n = 43+43, no significance test). No artifact in this repository records it, so it is carried here as reported and un-reproduced, and nothing below depends on it. It is a reason to doubt notices, not a finding about this one.
This is what the decision actually rests on, and it is checkable in the tree today:
- a planned node cannot carry
success_check.verify. A planner reply is untrusted input, andvalidatePlannedNodeVerifyrefuses the field outright (internal/coordinator/coordinator.go); - a planned node cannot declare a build tool either.
plannedToolAllowlistis exact-string-matched, soBash(./gradlew *),Bash(npm *)andBash(cargo *)fail the plan (ADR 0016 §1, kept); - therefore, without
--verify-cmdanautorun has, by construction, no engine-run evidence at all. The only terminal predicate left isresult_matcheson a node's own reply — and that node wrote the reply.
The archive records what that costs, from #119: the planner gave its verify
node Bash(git *) only, so the node checked that a branch existed, never
compiled anything, and replied PASS in 17 seconds for $0.13 — after the node
before it spent $11.01. The real build then failed on a compile error. Every
row in the ledger read PASS.
ADR 0016 built the remedy and made it opt-in. The remaining defect is not that the remedy is missing; it is that its absence is silent by default in a repository where a build plainly exists.
coordinator.VerifyAdvice (internal/coordinator/verifycmd.go) already prints,
before the planner call:
No build verification configured. Detected a Go module (go.mod).
Nothing in this run will compile or test your code — a planned node cannot
run a build command, and no node's PASS carries build evidence.
Re-run with
--verify-cmd 'go build ./...'
to have the engine verify the result itself.
Then the run proceeds. Every word of it is true, and it changes nothing about what happens next. A sentence that describes a defect and then commits it is the weakest instrument this repository has; it costs one screen of scrollback and buys a feeling of having been warned.
The change this ADR makes is not to the wording. It is to the control flow.
When auto is invoked, DetectBuildSignals(".") finds at least one marker,
and no --verify-cmd was supplied, auto refuses: it prints the message
in §2.4 to stdout and exits 3 (§2.4).
The refusal happens where the detection already happens: in
runAutoWithRuntime (cmd/oh-my-graph/main.go), at the line that printed the
advice — the one place in this CLI that scans the invocation directory for build
markers today. As shipped that line is
answerBuildEvidence(os.Stdout, verifyCommand, flags.buildDeclaration(), "."),
the helper §4 names, which does the scan and calls noteVerifyAdvice with its
result.
One
DetectBuildSignals(".") call in that scope now feeds three consumers: the
advice line it already fed, the gate, and the recording (§2.5).
That placement is before the planner call and therefore before any spend, which
is load-bearing twice over: a refusal that cost a planner call would be a worse
version of the notice, and it is what makes --plan-only refuse identically
(§2.6) without special-casing — planOnly is passed into planAndExecute
(main.go), which is downstream of this line.
Deliberately not in autoFlags.parse, and the first draft of this ADR was
wrong to put it there. The parse placement satisfies every property claimed
above — before the planner call, before any spend, --plan-only gated for free,
resume/chat/run untouched, auto --help still answered first — so the
argument has to be made against this site, not, as the first draft did,
against checkVerifyFlags. It loses on three counts:
parseis a pure function ofargs(cmd/oh-my-graph/flags.go), and every other check in it is flag-vs-flag consistency. Adding an environment probe makes every existing and futureparsetest's result depend on the directory the test binary runs in — passing today only by the accident thatcmd/oh-my-graph/holds no marker while the repo root holdsgo.mod. Testing the gate would then needt.Chdir, which foreclosest.Parallelfor that test.- It would be two
DetectBuildSignalssweeps per invocation, at two call sites that must agree on the directory, with nothing pinning the twodirarguments together — the refusal-message tests pin what is said, not where it was looked for, so a gate scanning one directory while the advice line scanned another would pass all of them. Here there is one sweep and the question does not arise; §4's test 8 pins that it stays one. - The detected signals are needed downstream. §2.5 records them, which means
they must reach
newRunRecorder(main.go) — already 9 parameters, reached throughcommonRunFlags, whichrunalso uses. Detected atparse, they arrive there through a structrunshares; detected here, they are already in the scope that builds the coordinator options and callsplanAndExecute.
answerBuildEvidence takes dir string precisely so this is testable without
process-global state (cmd/oh-my-graph/verifycmd.go), and the gate inherits
that.
Deliberately not inside checkVerifyFlags either, which resumeFlags.parse
also calls: that helper exists so the two subcommands cannot diverge on the flag
pair, and the gate is auto-only (§2.6). Sharing it would gate resume by
accident — the one change here that would strand a paused run.
The predicate itself is a pure function in internal/coordinator/verifycmd.go,
beside VerifyAdvice and DetectBuildSignals, taking the supplied command, the
human's declaration (§2.3) and the detected signals, and returning both the
recorded outcome (§2.5) and — when the answer is a refusal — a typed
*MissingBuildEvidenceError carrying the detected signals. It is not called
from Coordinator.Plan. Putting it in the library would gate chat too, and
chat has no flag to name — see §2.6 and #198's lesson about instructions the
tool cannot honour.
If DetectBuildSignals finds nothing, auto runs exactly as it does today,
and prints exactly the notice it prints today ("Detected no build signal in
this directory"). No flag is required and none is suggested.
This is not a softening; it is the boundary of the defect. #119 is a repository
that had a build and did not run it. A directory with no build system has
nothing for an evidence command to be evidence about, and demanding a flag
there is friction with no defect behind it. The existing notice already
distinguishes the two cases (VerifyAdvice's case 0), and this ADR keeps
that distinction rather than inventing one.
It is also the negative control the test list pins (§4): without a test asserting that the un-signalled case still runs, the gate could widen to "always refuse" and every other test would still pass.
One flag, on auto. It takes no value.
Why that name. The flag's job is to make the operator state a true thing about the run they are about to start — this run carries no build evidence — rather than to switch something off. Names were judged on what the operator is made to say, not on brevity:
| candidate | what typing it asserts | verdict |
|---|---|---|
--no-verify |
"turn verification off" | rejected. There is nothing to turn off: no verification was ever going to run. It describes a feature the run does not have, and it is the spelling every other tool uses for "skip the check I would otherwise have done" (git commit --no-verify), which is a different act. |
--skip-verification |
"skip the check" | rejected, same defect. Nothing is skipped; nothing exists. |
--no-build-evidence |
ambiguous — a switch or a statement | rejected. Reads as the --no-agent-mapping / --no-skill-activation family, which really are feature switches. Family resemblance is exactly the wrong signal here. |
--unverified |
"this run is unverified" | close, and rejected only on ambiguity: unverified by whom, and of what. It does not say build. |
--accept-no-build-evidence |
"I accept that this run carries no build evidence" | chosen. Subject, verb, object. It is a sentence the operator says, not a knob they turn, and it names the exact thing that is absent. It reads correctly in the three places it will be read: on the command line, in the snapshot, and in a CHANGELOG line someone skims a year from now. |
The name is deliberately long. A flag that is annoying to type once per run is working as designed; a flag that gets aliased into a shell function — or, more realistically, reached for by an agent that cannot satisfy the other exit (§6) — is the failure mode, and no name prevents either. Only the recording in §2.5 makes it visible.
With --verify-cmd it is a contradiction and is refused — in
autoFlags.parse (cmd/oh-my-graph/flags.go), beside the --plan-only /
--max-cycles refusal, because that is exactly what it is: a flag-vs-flag
consistency check over the one FlagSet that registers both flags
(newAutoFlags). The operator would be declaring an absence and supplying the
thing whose absence they declared; refusing beats picking a winner, because
either winner silently discards something they typed.
Not in VerifyCommand.Validate, and the first draft of this ADR was wrong to
put it there. That value object is shared with resume, through both
checkVerifyFlags and ReattachVerifyCommand
(internal/coordinator/verifycmd.go), so storing the opt-out there would create
a field resume can never set and a branch resume can never reach. Worse, it
contradicts this ADR's own reasoning twice over: §2.1 refuses to share
checkVerifyFlags because resume shares it, and §2.3's table spends its
whole length arguing that the opt-out is not a verification switch — then
the first draft stored it inside the type whose doc comment says it is "the
whole of --verify-cmd / --verify-timeout".
The stated driver was §7's "no existing exported signature changes." That was a
self-imposed constraint bought at the price of a cohesion violation:
internal/coordinator has no consumers outside this module, so a signature
there is free to change. VerifyCommand therefore gains no field; the
declaration is passed as an argument to the predicate of §2.1 and to
VerifyAdvice's declared variant (§2.5b), which is where a run-launch fact
belongs. (resume registers no opt-out flag, and now has no field for one
either; its refusals are unchanged in both spelling and reachability.)
With no build signal it is accepted and inert. A script that always passes
it must not break when run in a directory with no build system. The run is then
recorded as none-detected rather than declared (§2.5) — the flag answered a
question that was never put, and filing it as a declaration would inflate
exactly the stratum §8(a) exists to count.
One detected signal:
auto: this directory has a build system, and this run would check none of it.
Detected a Gradle project (gradlew).
A planned node cannot carry a build command: the planner's reply is untrusted
input, so success_check.verify is refused from a plan, and no allowed tool
runs a build. A check node's PASS is words it emitted, not a build that ran.
Without --verify-cmd, every judgement in this run is the model's, about its
own work.
Re-run with ONE of:
--verify-cmd './gradlew build'
the ENGINE runs that command at each sink node of the plan and judges
its exit code itself. No node is granted anything.
--accept-no-build-evidence
run anyway, on the record: this run carries no build evidence. The
choice is written to the run's state.json and printed with the plan.
Nothing has been spent — this is refused before the planner call.
Several signals swap one line, matching VerifyAdvice's existing three-way
shape (the suggestion is the first signal in the table's priority order, which
is why the wrapper beats the file it wraps):
Detected several build signals (gradlew, package.json), so the command below
is a guess.
Four properties of this text are part of the decision, not of its prose:
- It names what was detected, by ecosystem and by file, so an operator who thinks the detection is wrong can see the exact marker and say so.
- It names both exits and what each buys, so it is actionable in one read. A refusal with one exit is a wall; a refusal with two is a question.
- It names only flags
autoregisters. This is #198's rule, and the implementation lane owes the same automated checkresumehas (TestSnapshotVerifyRefusal_NamesOnlyFlagsResumeRegisters) — a refusal that sends someone to a flag the tool rejects costs more than silence, because the next message is not believed either. - It says nothing was spent. The single most likely reading of a refusal from a tool that bills is "I have been charged for this"; saying otherwise is one line.
Verbatim means a channel and a prefix, and "returns an error" gives it
neither. An error out of a subcommand reaches mainExitCode
(cmd/oh-my-graph/main.go), which prints
fmt.Fprintf(os.Stderr, "oh-my-graph: %v\n", err) — so the first line above
would arrive as oh-my-graph: auto: this directory has a build system…, double
prefixed, with the other 19 lines and their indentation on stderr, where the
notice this replaces goes to stdout. The precedent for the fix is three
lines up in the same function: usageRequest is matched with errors.As,
prints itself to stdout, and the oh-my-graph: prefix is suppressed.
*MissingBuildEvidenceError takes exactly that shape — matched by errors.As
in mainExitCode, printing its own text to stdout, unprefixed, so §2.4 is
achievable as written. Its Error() string stays the first line alone, which is
what a wrapping caller and a test assertion want.
The exit code is 3, and it is new. 1 is a failed run and 2 is a paused one
(exitCodeForError), and a refusal is neither: nothing ran, nothing is
resumable, nothing was billed. §7 says CI calling auto will "break loudly" —
that is only true if the break is distinguishable, and exit 1 would make a
refusal indistinguishable from the failing build the operator is trying to
catch. A script can then branch on 3 to add a flag rather than to page someone.
Note what 3 does not disturb: ADR 0023 §2.6 asserts that an exit code agrees
with the run's derived status, and a refused invocation creates no run
directory, so it is outside that assertion rather than a new case within it.
An absence that was chosen and an absence that was an accident look identical in a finished run today. After this they do not — and, equally, a run that was never asked is distinguishable from both, which is what makes §8(a) a measurement rather than a count of one stratum.
(a) The snapshot — internal/runstate, an additive optional field, written
on every auto-mode launch:
// BuildEvidence records the launch-time build-evidence question and its answer:
// what was detected in the invocation directory, and how the run answered.
// Written on every auto-mode launch (auto and chat's graph turns), including
// the ones that answered by attaching a command and the ones where there was
// nothing to answer. Absent means a run that predates this field, or a `run` of
// a hand-written graph, which never asks the question (§2.6).
BuildEvidence *BuildEvidence `json:"build_evidence,omitempty"`
type BuildEvidence struct {
// Answer is one of four values, and the set is closed:
// "attached" — --verify-cmd was supplied; the engine runs it at
// each sink. Signals may be empty or not; the
// attachment itself is in the graph.
// "declared" — signals were detected and a human typed
// --accept-no-build-evidence, answering this
// question and no other.
// "disclosed" — signals were detected and a chat [y/N] approved a
// plan screen that stated the absence. ONE keystroke
// covered two questions; this is weaker than
// "declared" and is filed apart from it (§2.6).
// "none-detected" — the directory raised no signal, so no gate applied
// and nothing was declared. The greenfield run lands
// here (§6).
Answer string `json:"answer"`
// DeclaredBy is the exact spelling of what the human typed, for the two
// answers a human gives: "--accept-no-build-evidence" or "chat-confirm".
// Empty for "attached" and "none-detected".
DeclaredBy string `json:"declared_by,omitempty"`
// Signals are the marker files detected at launch, in the detection
// table's order — what the human was told when they answered. Empty is
// meaningful and is the whole point of writing this on every run.
Signals []string `json:"signals,omitempty"`
}Why the wider version, and why the narrow one was a bug in this ADR. The
first draft wrote the field only when "no verification was attached, at least
one signal was detected, and the run proceeded by declaration" — the single
path that was already visible, because the operator had typed a declaration.
Every silent path stayed silent: a run that passed with --verify-cmd recorded
no signals (the attachment is in graph.json; the detection was nowhere), a
run in a signal-free directory recorded nothing at all. So §8(a) — "how many
directories raised a signal, how many answered with --verify-cmd, how many
with the opt-out" — had only its third number, and its denominator was exactly
as unrecoverable after shipping as it is today. The measurement file names
that same blind spot as the reason the current corpus cannot say which of its 7
unverified runs would have been gated: "A snapshot records no invocation
directory" (docs/measurements/0030-auto-runs-carry-no-build-evidence.md).
Shipping a field that reproduces the blind spot it was written to close would
make §8(a) a wish. Recording the detection outcome on every launch is one
field's worth of extra writing and is the difference.
Nothing in ADR 0016 §4 blocks the wider version: the argument below — that the
field is inert and carries no command — holds verbatim for attached and
none-detected rows too.
Schema stays 3. An absent field is a run that predates this, or a run of a
hand-written graph; no reader of either version can misread it (the same
reasoning ADR 0025 used for runtime, reaching the opposite conclusion on
omitempty because here absence is legitimately meaningful).
What this field is NOT: an input to anything. Nothing reads it to decide
behaviour, on this leg or on a resumed one. In particular it is not ADR 0016
§4's rejected mechanism (ii) wearing a new hat: it carries no command — the
Signals are marker filenames, not the suggested commands the detection table
holds beside them — so there is nothing in it a later leg could execute. The run
directory remains an inadmissible source of engine-run shell, on both legs.
(b) The printed disclosure, at both sites where a human meets the run:
- before the planner call,
VerifyAdvicegains a variant for the declared case — the same paragraph it prints today plus the sentence "You said so with--accept-no-build-evidence; a run launched this way records it instate.json." It takes the declaration as a new argument (§2.3), not as a field onVerifyCommand. The un-signalled case's text is unchanged; - with the plan, in the slot
noteVerifyAttachmentsoccupies (printPlan,cmd/oh-my-graph/main.go). That slot states either what the engine will run at each sink, or that nothing will — never neither. This is the screen--plan-onlyprints and the screen chat's[y/N]gates, which is what makes §2.6's chat answer work at all. Its declared line reads "you said so with--accept-no-build-evidence; a run started from this plan records it instate.json."
Neither receipt sentence says "this run", and that is load-bearing. Both
are printed before anything knows a run follows: --plan-only reaches the same
gate and the same plan screen (§2.6) and then mints no run id at any point, so
it writes no state.json for the sentence to be about — and the preview's own
last paragraph goes on to say exactly that ("it gets no run directory"). The
first implementation of both sentences said "this run's state.json records
it", which made auto --plan-only --accept-no-build-evidence contradict itself
twice on one screen. The disclosed line was already phrased conditionally
("approving this plan accepts that"); the declared ones now match it. Pinned by
TestRunAutoWith_PlanOnlyDeclaredPromisesNoRecordItDoesNotWrite, which asserts
the promise's absence and that no run directory follows — the text alone would
keep passing if the preview started writing snapshots, and the directory alone
would keep passing while the screen lied about it.
Both printed sites are before the run, and that is the whole of what this ADR
ships. Nothing reads the field back afterwards: show, runs, the dashboard
and the run feed are all untouched (§2.7, §7), so a reader of a finished run
gets the record by opening ~/.oh-my-graph/runs/<id>/state.json. Recorded here
as a deliberate limit rather than left to be discovered: the field exists for
§8(a) first and for a human reader second, show is the natural home for the
second, and adding a surface is a change to a command this ADR otherwise does
not touch. It is follow-up work, not part of this decision.
| surface | gated? | why |
|---|---|---|
auto |
yes | The defect's home. |
auto --plan-only |
yes, identically | A preview that refuses differently from the run it previews is its own defect. It falls out for free: the gate is upstream of planAndExecute, which --plan-only is passed into, so it is reached before the preview buys its planner call. It also saves the user money in the refused case, where today they would pay for a plan they then have to re-request with a flag. |
chat |
no refusal; disclosure and recording only | chat registers no verification flags at all, so a refusal there could only name a flag chat rejects — the exact dead end #198 was. What chat gets instead is the absence stated on the plan screen its [y/N] gates (§2.5b), and the run recorded with answer: "disclosed", declared_by: "chat-confirm" — filed apart from auto's declarations, never merged into them. |
run |
no, and no recording | A hand-written graph carries its author's own success_check.verify and is a reviewed artifact. Out of scope by construction, as ADR 0016 §2 has it — it never asks the question, so it writes no build_evidence field either (§2.5a). |
resume |
no | The gate is a launch-time gate. The choice was made once, and the snapshot the resume loads records it — and the resumed leg's recorder base carries build_evidence forward, which is what makes the previous clause true after the leg as well as before it. SnapshotRecorder rewrites the whole snapshot on every settled node, so omitting the field there does not fail to add one, it erases the first leg's (cmd/oh-my-graph/resume.go, TestResume_CarriesTheDeclarationIntoTheSecondLeg). Re-asking, meanwhile, would make a paused run un-resumable without re-typing a declaration already on file, which is ADR 0009's promise broken and #198's defect repeated. A run launched before this ADR resumes untouched. |
--runtime codex (ADR 0025) |
yes, identically to Claude | Not derived, stated: a Codex auto reaches the same runAutoWithRuntime, so it meets the gate at the same line, by construction rather than by intention. It is worth stating because the reason for the gate reads differently there — a Codex node's ceiling is a filesystem sandbox, not a tool allowlist, so §1.2's "plannedToolAllowlist refuses Bash(./gradlew *)" is not the argument on that runtime. The argument that does carry over is the one that matters: validatePlannedNodeVerify refuses success_check.verify from a plan on either runtime, so a Codex auto has no engine-run evidence without --verify-cmd either, and a sandbox that permits a build command still leaves the verdict to the node that ran it. The refusal text, flag and recording are runtime-independent. |
Chat's answer is the weakest thing in this record, and it is filed as what it
is. One y covers two questions — run this plan and accept that it proves
nothing — where auto's operator answers the second one separately. The first
draft of this ADR put both into one declared_by column under a field whose own
doc said it recorded "that a human said so", which files a non-declaration
under the name of a declaration: every future reader of §8(a) and §9 would have
had to remember to segregate them, with no structural help, while the chat
rows dominated by volume on any interactive machine — exactly the confound the
column exists to resolve. So the Answer values are split at the source:
declared for a flag typed at the question, disclosed for a plan screen that
stated it and was not challenged. The plan-screen disclosure stands on its own
and does not need to borrow the word.
The alternatives to disclosure were a dead-end refusal (worse: it strands the
user), a second confirm prompt (a second [y/N] for one action, bought before
anyone has complained), or giving chat the full flag pair (a larger change
than this ADR, and ADR 0016 §2 already carries chat --verify-cmd as unshipped
work). If the disclosed rows come to dominate, that is the evidence for
revisiting — and now it is a row count, not a recollection.
- The planner prompt is untouched. The corpus is 8 runs; nothing about planner quality is measurable on it, and ADR 0016 §5 already records that prompt changes here are hopes. Not in this change.
- No ceiling layer moves, no tool is granted, no seam is added.
plannedToolAllowlist, ADR 0004's layers 1–5 and the four-spawner invariant are exactly as they were. The gate reads directory entries and decides whether to stop. --verify-cmd's behaviour is unchanged in every respect: same value object, same sink attachment, same serialization, same ceiling, same disclosure.
ADR 0016 §3 says detection "produces PROSE, never policy" and that the detection table "is ALLOWED to be incomplete" precisely because of that. This ADR makes detection decide a refusal, which is policy. The amendment must be stated in the form a future reader can apply:
Build-signal detection may gate a refusal. It may never derive a grant. A repository file may cause oh-my-graph to stop; it may never cause oh-my-graph to run something, to widen a tool set, or to attach a command.
The direction is the entire safety argument, and it survives the sharp forms of the attack ADR 0016 §3 and §4 were written against:
- A hostile checkout plants a
Makefile. Result:autorefuses, before any spend, naming the file it found. The operator adds one flag. Compare the grant version, which ADR 0016 refused: unattended execution of repo-authored code underdontAsk, leaving no diff. - A checkout hides its build system. Result: no gate; the run proceeds unverified — exactly today's behaviour. A repo can decline the new protection; it cannot use the mechanism to obtain anything.
- A plan bootstraps its own signal. ADR 0016 §3's sharpest case: node 1
legitimately writes a
package.json, and a per-node detector would then widen node 2. It does not apply here, because detection happens once per invocation, before the planner call — nothing a node writes is ever detected, and there is no per-node evaluation to widen. - Under
--max-cycles, each cycle re-plans against a tree the previous cycle wrote, which §3 names as the reason plan-time restriction is not sufficient for a grant. It is sufficient for this: the gate is evaluated once per invocation, before the first planner call, outside the cycle loop. A cycle that creates ago.moddoes not retroactively gate its own run.
Once-per-invocation is a safety property in this direction and a coverage hole in the other, and the two must be counted separately. The three bullets above are the safety half. The cost is stated in §6 as a named false-negative class: a goal that creates the build system is never gated, because the marker did not exist when the question was asked.
The monotone property is what makes the incomplete table acceptable in its new job as well as its old one. A missing ecosystem means a run that is not gated — today's behaviour, no regression — never a run that is gated wrongly into something.
Eight, and the third is the one that would otherwise be forgotten.
Corrected 2026-08-20, after implementation, because the sentence that stood
here was wrong about its own lane's output. It claimed that each of these
names a temp directory explicitly — the gate's helper takes a dir (§2.1) — so
"none of these needs t.Chdir and none of them forecloses t.Parallel". The
helper does take a dir; the production call site passes "."
(runAutoWithRuntime), so every test that goes through auto, --plan-only,
chat or run has to be in the directory under test, and the shipped ones do
that with inBuildDir, which is t.Chdir. Only test 8 — which calls
answerBuildEvidence directly, and is the one that pins the seam rather than a
message — reaches the dir parameter. Harmless as it stands (package main has
no t.Parallel and already used t.Chdir before this lane), and recorded
because a design record that is wrong about what shipped is a bug in one of the
two.
- build signal + no flag → refused, and the message names both the
detected signal (by ecosystem and file) and the opt-out flag. Asserting only
"it refused" would pass on a bare
exit 1. - build signal +
--verify-cmd→ proceeds unchanged, with the same attachments and the same disclosure as before this ADR, and recordsanswer: "attached"with the detected signals. The recording half is what makes §8(a)'s denominator exist; without it this test passes on the first draft's inverted field. - no build signal + no flag → proceeds, and records
answer: "none-detected". The negative control, in both halves: without the first the gate could widen to "always refuse" and every other test here still passes; without the second the greenfield run stays invisible (§6) and §8(a) loses its third stratum. - build signal +
--accept-no-build-evidence→ proceeds AND the run records it —state.jsoncarriesbuild_evidencewithanswer: "declared", the declaring flag and the detected signals, and the plan screen states the absence. --plan-onlyrefuses identically to the run it previews (same message, same exit code, no planner call bought), andchatdoes not refuse but its plan screen states the absence and its run recordsanswer: "disclosed"withdeclared_by: "chat-confirm"— asserted asdisclosed, distinct from test 4'sdeclared, so the two kinds cannot be merged later without a test going red (§2.6).- The refusal reaches stdout, unprefixed, all 20 lines (§2.4) — asserted
through
mainExitCoderather than through the subcommand, since the channel and the prefix are that function's behaviour, not the error's. - The refusal exits 3, distinct from a failed run's 1 and a paused run's 2,
asserted in
exitCodeForError's own table beside them. - The gate and the advice line scan the same directory. One
DetectBuildSignalscall feeds both (§2.1); this pins that they cannot diverge, which is the failure the parse placement would have made possible and untested.
Plus two that fall out of §2.3 and §2.4: --verify-cmd together with
--accept-no-build-evidence is refused as a contradiction in
autoFlags.parse, with no directory involved (it is a pure flag-pair test);
and the refusal text names only flags auto registers, checked against the real
FlagSet the way TestSnapshotVerifyRefusal_NamesOnlyFlagsResumeRegisters does
for resume.
All of it runs against FakeRunner. No test here needs a real spawn: the gate
fires before the planner call, which is the whole point.
The eight above cover the fresh auto leg thoroughly and cover nothing
else, and the first item below is a real defect that shipped through the gap
rather than a coverage wish. All five are in
cmd/oh-my-graph/buildevidence_test.go.
resumecarries the record into the second leg (TestResume_CarriesTheDeclarationIntoTheSecondLeg). §2.6'sresumerow relies on the snapshot recording the choice;SnapshotRecorderrewrites the whole snapshot from a base the resumed leg builds field by field, so the omitted field was erased by the first node that settled. Declared runs that paused — the interactive class — silently left all four strata of §8(a).chat's launch declares its own kind (TestRunChatWith_TheLaunchItselfDeclaresAChatConfirmDisclosure). Test 5 constructs the outcome and hands it tochatLoop; the launch line between them was unpinned, soDeclaredByFlagthere would have filed every chat run underdeclared— the merge §2.6 and §8(a) forbid — with every test green.runChatWithis the seamrunAutoWithalready is forauto.- Every cycle of a goal loop records the one answer
(
TestRunAutoWith_EveryCycleOfAGoalLoopRecordsTheOneAnswer). Each cycle mints its own run id and recorder; a cycle that recorded nothing would drop out of §8(a) exactly as a resumed leg did. - Codex meets the same gate
(
TestRunAutoWithRuntime_CodexMeetsTheSameGate). §2.6 states this rather than deriving it, and a stated property with no case is one a runtime-specific early return breaks silently. - The package directory raises no build signal
(
TestDetectBuildSignals_ThisPackageDirectoryRaisesNone). The hazard §2.1 held againstautoFlags.parse— a result that depends on the directory the test binary runs in — is inherited by the chosen site, because its production call passes".". AMakefileor a straygo.modincmd/oh-my-graph/fails dozens of unrelated tests with an unrelated message; this converts that cascade into one failure that says what to do.
The --plan-only row of the matrix above had two cells and one test. Test 5
covers the preview met by silence — the refusal — and nothing covered the
preview met by the opt-out, which is the only combination where the two
disclosures can lie, because both are written for the run that normally follows
and a preview mints no run id at all. They did lie, in exactly that cell.
- A declared preview promises no record it does not write
(
TestRunAutoWith_PlanOnlyDeclaredPromisesNoRecordItDoesNotWrite). Asserts thatauto --plan-only --accept-no-build-evidencein a build-bearing directory proceeds, states the absence with its signal, does not print "this run's state.json", and leaves no run directory behind. Both halves are needed: the text alone would keep passing if the preview started writing snapshots, and the directory alone would keep passing while the screen lied about it (§2.5b).
-
Keep the notice and make it louder (colour, a blank line, an "ARE YOU SURE" banner). Rejected. The notice is already accurate and already early; the run proceeds regardless, and the thing being changed is the control flow, not the volume. Cheap to try and the cheapest thing to have to undo later, which is the honest argument for it — it lost because the defect it addresses is that nothing stops.
-
A confirmation prompt ("no build command detected — type one now"), the
chatshape applied toauto. Rejected:autois the unattended path. It runs underdontAsk, in CI, from cron, and from another agent'sBashtool; a prompt there hangs a run that has no keyboard. A flag is answerable by all of those.chatalready has the interactive form, which §2.6 uses. -
Infer the command from the detected signal and run it — zero config, and the detection table already holds the suggestion. Rejected on ADR 0016 §3/§4: it makes a repository file the source of an engine-run shell command, which is the untrusted-producer invariant, with the added twist that the first thing the tool would do in a fresh checkout is execute its build. The suggestion stays a suggestion.
-
Authorize the printed suggestion in one token —
--accept-suggested-verify-cmd, or a[y/N]on the refusal for the interactive case. The operator does not retype./gradlew build; they accept the string the refusal just showed them. This is the middle option between the two the bullets above and below cover, and it is the one that most directly moves §9's odds ratio: the whole worry there is that a refused operator types the opt-out forever, and this makes the verified exit the cheapest thing on the screen.It is also worth naming the tension it exposes. §2.4's refusal already hands the operator a repo-derived command to paste, so "detection may never derive a grant" (§3.5) is preserved right now by a clipboard round-trip — the human reads the string, can edit it, and retypes it as their own. That round-trip is thin, and this alternative is the honest test of whether it is load-bearing.
Rejected, and not comfortably. It is load-bearing, for two reasons. First, what the round-trip buys is not ceremony but reading: the detection table is a guess by construction — §2.4 says so out loud in the multi-signal case, and §9 records that its priority order is unmeasured in a monorepo — and a one-key accept is an authorization given without knowledge of what was authorized. When the guess is wrong, the result is worse than no evidence: it is a run carrying "evidence" that measured the wrong module, with the tool's own fingerprints on the choice, which is §6's
--verify-cmd 'true'failure arriving by the tool's suggestion rather than the user's. Second, §6's strongest failure mode is now an unattended agent picking the cheapest exit; adding an exit cheaper than the opt-out does not fix that, it just changes which repo-derived command an unattended process runs without a human ever seeing the string — which is precisely the shape ADR 0016 §4 refused. Revisit if §8(a) shows the opt-out dominating and §8(b) shows the table's suggestions are accurate; the second condition is what this alternative needs and does not have. -
Gate every run, signalled or not. Rejected — §2.2. Friction with no defect behind it, and it discards a distinction the existing notice already makes correctly.
-
No opt-out at all: refuse, full stop. Seriously considered, because a gate with an exit is a gate people learn to walk through. Rejected on three counts:
auto's identity is that it works with no setup; there are legitimate unverified runs (a documentation goal in a build-bearing repo, a build that exceeds the 10-minute verify ceiling, a repo whose build needs credentials the run does not have); and a gate with no exit is worked around out of band — an alias, a wrapper, atouch-and-rmdance — which produces the same unverified run with no record of the choice. The recorded exit is strictly better evidence than a bypass nobody can see. -
--no-verify/--skip-verificationas the opt-out. Rejected — §2.3. They describe switching off a check that was never going to run. -
Record the absence in the ledger's verdict column instead of the snapshot. Rejected as a substitute, kept as a complement: ADR 0016 §6's provenance qualifier (
self-reported) already says, per node and after the fact, how each verdict was reached. That is a different question from "was the absence chosen, and what was the human told when they chose it", which is a property of the run at launch. Both, not either. -
Emit the choice on the run feed as well. Not rejected, deferred. The feed is a consumer contract with its own additive discipline (
docs/RUN-FEED.md), and the reader this ADR is written for reads a finished run, which means the snapshot. One additive field onrun_startedis the obvious follow-up if a feed consumer asks. -
Put the rule in a user-level config (
$OMG_HOME/config.yaml, ADR 0016's alternative B′) so an operator can set their own default. Deferred for the same reason B′ was: it buys a config format, a merge story and a precedence story before anyone has typed the flag twice. Note that the direction matters here — a config that lowers the gate is exactly the alias problem in a file, and if this is ever built the recorded field must record that source too. -
Have the planner declare the build command, validated against a pattern. Rejected on ADR 0016 §4's invariant. A regex standing where a trust boundary belongs.
-
The opt-out becomes muscle memory — and the realistic aliaser is an agent, not a shell alias. The human version is familiar: an operator aliases
autoto always pass--accept-no-build-evidence, and this ADR has bought one screen of prose and a longer command line. But this repository shipsBash(oh-my-graph auto *)to a Claude Code agent (plugin/agents/oh-my-graph.md,plugin/commands/graph.md,plugin/README.md), and an LLM that meets a refusal naming two exits will take the one it can satisfy without knowing the repository's build command — the opt-out — very close to every time. That driver is stronger than any human habit, it is unattended by construction, and it is the shipped invocation, not a hypothetical one.So the plugin's invocation gets a considered answer rather than a discovered one, and it is documentation, because the tool has no way to tell an agent's argv from a human's:
plugin/agents/oh-my-graph.mdgains an explicit rule that on a build-evidence refusal the agent surfaces the refusal to the human and asks which exit, and never passes--accept-no-build-evidenceon its own initiative — the flag says a human accepts this, and an agent typing it is a false statement in the snapshot.plugin/commands/graph.mdgains the same sentence where it documentsauto. See §7 for the full list of surfaces.What that rule cannot do is enforce itself, and §9 records the consequence:
declared_by: "--accept-no-build-evidence"does not distinguish a human's keystroke from an agent's argv, so §8(a)'s declared rows are an upper bound on human declarations, not a count of them.What §2.5 buys against all three versions is that the habit is visible and countable — every such run carries
build_evidencein its snapshot with the signals it ignored, which is a measurement the notice never permitted. That is the honest claim: this converts an invisible default into a visible habit. -
The greenfield run is exempt, and it is the highest-risk unverified run there is.
auto "scaffold a new Go service"in an empty directory raises no signal, so it is not gated. Nothing about that is an oversight of the safety argument in §3 — it is the same once-per-invocation evaluation, read from the other side — but it is a coverage hole and this ADR states it rather than leaving it as a side effect: a goal whose whole purpose is to create the build system is never gated, and the build it creates is never run. Under--max-cycles Nit compounds: cycle 1 writes the build system, and cycles 2..N run against a build that now exists and is still never executed, because the question was asked once, before cycle 1.Accepted, with two things carrying it. First, re-detecting per cycle is not a free fix: it would make a file a node wrote decide policy, which is the bootstrapping shape §3 spends four bullets keeping out, and the amendment's refusal-only direction makes it survivable but not obviously worth the complication before anyone has hit it. Second — and this is new since the first draft — the greenfield run is no longer invisible: §2.5's widened recording files it as
answer: "none-detected", so §8(a) can count how large this class actually is instead of arguing about it. If it turns out to be the common shape of anautorun, that is the evidence for re-detecting at cycle boundaries, and it will be a number rather than this paragraph. -
chatasks once per SESSION, and a session outlives a launch. The question is put at REPL startup (runChatWith) and its answer serves every graph turn until the process ends, which is the same once-per-invocation ruleautofollows and is required by the same argument (§3.5: re-asking would let a file a turn wrote gate a later turn — the bootstrapping shape, one scope up). The cost is that chat's staleness window is materially larger than the greenfield case above it: a session whose turn 1 scaffolds apackage.jsonstill recordsnone-detectedon turn 9, and anautolaunch at least ends. The direction is the safe one — a stale answer never widens anything, it only fails to gate — so this is accepted, stated inchat.goat the call site, and countable: those rows arenone-detectedwith an emptysignalslist, so if the class matters it will show up in §8(a) rather than in an argument. -
A false positive. A
Makefilethat only builds documentation, apackage.jsonwith no test script, ago.modin a repo whose goal is a README edit. The gate refuses a run that genuinely had nothing to verify. Cost: one flag, one time, before any spend. This is the friction the decision accepts, and its size is the detection table's accuracy. -
A false negative. A private wrapper (
bin/verify-everything), a build system not in the table, or a monorepo whose build files live in subdirectories —DetectBuildSignalsreads the invocation directory only, not recursively. No gate fires; the run proceeds unverified exactly as today. The gate is exactly as complete as the table, and the table is allowed to be incomplete because incompleteness fails open (§3). One more member of this class comes from the table's shape rather than its contents:*.csprojis the single marker matched as a pattern (filepath.Glob) rather than byos.Stat, so an invocation directory whose own path contains[,?or*produces a valid-but-wrong pattern that matches nothing, silently —Globerrors only on a malformed pattern, never on a well-formed one that finds nothing. A .NET repository under such a path loses its only signal. Same direction as the rest of the bullet, so it is recorded rather than fixed. The created-during-the-run case above is a different and larger class than these and is listed separately for that reason: those are gaps in the table, and it is a gap in the timing, which no table entry closes. -
A build too slow for the ceiling.
--verify-cmdis bounded by 10 minutes, and a build that exceeds it fails as an Infrastructure fault ("could not verify"). The operator's route is the opt-out, and the refusal offers it. The ceiling itself is ADR 0016's and is not revisited here. -
The refusal is the first thing a new user meets — and the shipped quickstart is itself an instance of the false positive above.
README.mdandREADME.ko.mdtell a new user to runauto "lint this repo and summarize the findings" --input repo=$PWDin their own repository: a read-only analysis goal in a build-bearing directory, which is the exact class the bullet above accepts friction for. So the first thing a new user meets is not merely "a refusal" in the abstract; it is a refusal of the command the README just gave them, for a goal that genuinely had nothing to build. That is worse than the first draft of this ADR noticed.The fix is in the documentation, not the gate, and §7 lists every file: the read-only quickstart gains
--accept-no-build-evidencewith one sentence saying why ("this goal reads the repo and writes a summary; there is nothing to build, and the flag states that"), while the implementation-shaped examples gain--verify-cmd. Both halves matter — a quickstart that only ever shows the opt-out teaches the escape as the normal answer, which is the failure mode two bullets up, and one that shows neither is a command that no longer works. The residual cost stays accepted for the same reason as before: the run that starts unverified is the one that produced #119. §8(a) is what will say whether the teaching order took.What that leaves, named rather than inherited: the very first
autoa new user copies now carries--accept-no-build-evidence, so the paste-the-flag habit lands on the human this whole ADR is written to protect. Three things carry the trade, and it is a trade rather than a fix. The goal genuinely has nothing to build and the sentence beside it says so, which is the difference between teaching an exit and teaching a lie. The implementation-shaped--verify-cmdexample follows immediately, so the flag is never the last word a reader sees (README.md,README.ko.md,docs/EXAMPLES.md). And the agent-facing surfaces forbid taking that exit unilaterally (plugin/agents/oh-my-graph.md), which is where the aliaser bullet says the realistic version of this failure actually lives. If §8(a)'sdeclaredrows arrive dominated by first runs, the answer is to reorder the quickstart so--verify-cmdis the first thing shown, not to remove the flag from a command that would otherwise be refused. -
A run gated by a hostile checkout. A repository can plant a marker to make
autorefuse in it. Cost: one flag. See §3 — the direction is what makes this a nuisance rather than a hole. -
verifiedis still notcorrect. Unchanged from ADR 0016: a run that passes the gate by supplying--verify-cmd 'true'carries "evidence" that measures nothing, and a node holdingEditcan still edit the file the command runs. This ADR moves the default; it makes no new claim about adequacy.
- This is a behaviour change to the headline command, and the CHANGELOG entry
under
## [Unreleased]must say so plainly:autonow refuses to start in a directory where a build system is detected unless--verify-cmdor--accept-no-build-evidenceis passed. An invocation that ran yesterday can exit 3 today. No release and no tag in this lane. - Scripts and CI calling
autobreak loudly — exit 3 (§2.4), before any spend, with the two flags named on stdout — rather than silently continuing to produce unverified runs. That is the intended direction of the break, and the distinct code is what lets a script tell "add a flag" from "the build failed". 1 and 2 keep their current meanings exactly. run,lint,serve,watch,showand every shipped graph are untouched. So isresume, including every already-paused run (§2.6).- Snapshot: one additive optional field, schema stays 3. Every existing
state.jsonstays readable and unchanged in meaning; every consumer that does not know the field ignores it. It is now written on every auto-mode launch (§2.5a), which is more rows than the first draft, but not a different shape. - Run feed: no change, no schema bump (§5, deferred).
internal/coordinatorgains an exported predicate and error type (MissingBuildEvidenceError), andVerifyAdvicegains a parameter for the declaration (§2.3). That is an exported signature change, deliberately: the first draft avoided one by putting the opt-out field onVerifyCommand, which bought signature stability in a package with no consumers outside this module at the price of a fieldresumecan never set.VerifyCommandis therefore unchanged — no new field, andValidatekeeps exactly its blank, timeout and ceiling refusals.- The documented invocations that now refuse. Every place this repository
tells a user to type
autoin a build-bearing directory must be updated in the same lane as the gate, or the repo ships instructions the tool rejects — #198's defect, from the documentation side:README.md(quickstart) andREADME.ko.md(quickstart) — the read-only goal; gains--accept-no-build-evidenceplus the one-sentence why (§6).docs/EXAMPLES.md: theautorow of the subcommand table, which today describes--verify-cmdas purely optional; the read-only quickstart repeat; and the implementation-shaped example, which gains--verify-cmd. The one example that already passes--verify-cmdis correct as it stands and is the model for the others.plugin/README.md(both the/graph autoand bare-autoforms),plugin/agents/oh-my-graph.md(its synopsis lists no--verify-cmdat all, and it is the file that gains §6's rule against an agent declaring on a human's behalf), andplugin/commands/graph.md.usageLinesincmd/oh-my-graph/main.gomust gain the flag on theautoline — and this one fails loudly rather than silently, becauseusage_test.gopins each synopsis line's flags against that subcommand's realFlagSet. Named here anyway so the lane does not learn it from a red test.
- DESIGN.md is the spec and drift in it is a bug (CLAUDE.md). The auto-mode
section must gain the gate: today it describes
--verify-cmdas purely optional, which stops being true for a build-bearing directory. - ADR 0016 §3 carries a dated pointer here, in the same style ADR 0004 carries one to ADR 0016 — its "never policy" sentence is now "never a grant".
- SECURITY.md needs one sentence: repository content can now cause the tool to refuse to start, and cannot cause it to run, grant or attach anything. A reader auditing the trust boundary should not have to derive that from an ADR.
Neither gates correctness of the mechanism; both gate the claims made for it. Record each with cost and CLI version, as every prior E-number is.
-
(a) The firing rate and the exits taken. Over the first N
autoinvocations that produced a run directory after this ships, counted from the snapshots themselves — one row per such launch, four mutually exclusive strata, summing to N:stratum build_evidence.answerwhat it answers attached attachedthe run carries engine-run evidence declared declareda signal was met and a flag was typed at it disclosed discloseda signal was met and a chat [y/N]passed over itnone detected none-detectedno signal — including every greenfield run (§6) The denominator is the point: "how many directories raised a signal" is simply the rows whose
signalslist is non-empty, counted across all four strata — including theattachedones, which is whysignalsis recorded even when a command was supplied. It is recoverable only because §2.5 writes the field on every launch. The first draft recorded stratum 2 alone, which would have left the firing rate exactly as unknowable after shipping as it is today — the same blind spot the measurement file names when it says the current corpus cannot say which of its 7 unverified runs would have been gated (no snapshot records an invocation directory). The baseline is 1 of 8, with its address indocs/measurements/0030-…. Reportdeclaredanddisclosedseparately and never summed (§2.6), and read thedeclaredcount as an upper bound on human declarations — an agent's argv is indistinguishable from a human's keystroke in that column (§6, §9).N is not "launches"; it is launches that got a run directory, and the two classes it excludes both matter to this section's own headline. A refused invocation writes nothing anywhere — no run directory, no feed event, no snapshot, by design (§2.2,
TestRunAutoWith_RefusesABuildBearingDirectoryWithNoEvidenceCommand) — and a--plan-onlyone writes nostate.jsonat all (§2.5b). So an operator who was refused and then overrode leaves adeclaredrow, while one who was refused and walked away leaves nothing: the firing rate readable from snapshots is a floor, not the rate. Getting the true rate needs a surface this ADR does not add (§9). Do not report the four strata as the denominator of refusals. -
(b) The false-positive rate of the table. Over this machine's repositories, how many directories that raise a signal have no build command worth running for a typical goal. If it is high, the friction estimate in §6 is wrong and the table needs narrowing, not the gate.
- Whether the gate changes outcomes or only paperwork. An operator refused once may type the real build command, or may type the opt-out forever. Nothing in the corpus predicts which, and (a) is the only thing that will tell us.
- How often the gate fires, as opposed to how often it fires and is then
answered. A refusal leaves no artifact — that is the point of refusing before
a run directory exists (§2.2) — so an abandoned refusal is invisible to §8(a),
which counts only launches that got a run directory. The
declaredanddisclosedstrata therefore measure the answered firings and put a floor under the real rate; nothing here measures the numerator. Closing it means writing something on the refusal path, which would undo the property ADR 0023 §2.6 depends on (a refused invocation is not a run and gets no status), so it is not a small follow-up. Named here rather than left for a future reader to find by dividing by the wrong N. - How many of the 7 unverified runs would have been gated. Unknowable: no snapshot and no feed event records the invocation directory. Stated in the measurement file, not inferred around.
- Whether notices are weak in general. The 42%→33% figure has no address in this repository (§1.1) and is carried as reported. The argument here does not need it: the reason to prefer a gate is that a notice cannot stop a run, which is a property of the mechanism, not a finding about a corpus.
- Whether
chat's singleyis an adequate disclosure (§2.6). It is the strongest thing that surface has today; whether it is enough is what thedisclosedstratum will show — kept apart fromdeclaredat the source precisely so the question stays askable. - Whether a declaration was a human's.
declared_by: "--accept-no-build-evidence"records the flag, and the flag records that something typed it. This repository shipsautoto an agent (§6), and the tool cannot tell that agent's argv from a keystroke — nothing in argv, the environment or the snapshot distinguishes them, and inventing a--declared-byvalue the caller supplies would just move the honesty problem one flag along. So thedeclaredstratum is an upper bound on human declarations. The plugin rule in §6 is documentation, and this is the price of its being documentation. - How large the greenfield class is (§6). Not knowable from the current
corpus for the same reason as the row above it — no snapshot records the
invocation directory — and knowable after this ships, from the
none-detectedstratum. It is the input to whether per-cycle re-detection is worth its complication. - Whether the detection table's priority order suggests the right command in a monorepo. Unchanged from ADR 0016, unmeasured then and now. The refusal says "so the command below is a guess" rather than pretending otherwise.