Skip to content

Integrate #26 and #24: the verdict gate learns two things - #27

Merged
rokbenko merged 24 commits into
mainfrom
integrate/verdict-gate
Sep 17, 2026
Merged

rokbenko merged 24 commits into
mainfrom
integrate/verdict-gate

Conversation

@rokbenko

Copy link
Copy Markdown
Owner

Scratch integration branch for #26 and #24, opened so CI runs on the merged result rather than on either fork. It is merged to main locally with --no-ff once this is green.

What is in it

#26 (@Bayway), merged whole. The verdict gate honours Verb.read_only beside BEFORE_VERDICT, so a body quackd never shipped can say its own sensing verb only looks. Every shipped verb carrying the flag was already in the set, so nothing shipped moves earlier.

#24 (@Vallhalen), merged whole. A feasible verdict whose needs the pilot's own datasheet does not meet is refused before it is recorded, the way a bid is refused at the coordinator. The merge commit is red on purpose: their branch predates the adapter package split, and the commit straight after it is the fix, so what they wrote stays separate from what main forced on it.

#25 (@Vallhalen), answered without narrowing the allowlist. assess_task told the pilot its verdict is judged against the datasheet and then named "the object is out of view" as a reason to answer uncertain. The description says what the verdict is about now. Whether that moves the measured numbers is unknown here and the issue asks for a rerun.

What landed on top

One commit per theme, so git log keeps each contributor's work and the corrections legible apart. Two invariant tests for the flag, the prompt rule line read off the body's own allowlist, MCP parity for the own-sheet check through one shared message, two matcher guards for an honest pilot, duration_min, the docs and two dated ADR amendments, and the measurement written up in docs/local-llms.md with PLAN.md corrected where 0.9.0's "no real model has ever refused a task on feasibility grounds" stopped being true.

Then six agents were pointed at the merged result with instructions to find it wrong. Twenty four findings survived an adversarial verify pass, and the last five commits are those: a refusal that refused the words and not the motion, a learned verb that could take one of the nine names, read_only outranking MOVES_THE_BODY, a float() that raised on a null need, a terrain exception reaching two bodies the prompt never told, and nine wrong claims in the branch's own prose, including a contributor's sentence that had been extended from the inside.

Checklist

  • uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest pass locally, and mypy on 3.12 as well
  • No network calls in tests; no API keys needed
  • No upstream assets added
  • CHANGELOG.md updated under Unreleased, both contributors credited
  • uv run quackd validate ducks/*.duck passes (14 files valid)

🤖 Generated with Claude Code

Vallhalen and others added 24 commits September 15, 2026 22:19
…eady is

`missing_needs` compared a `needs` against a datasheet at the coordinator, where
it judges another robot's bid, and nothing called it when a pilot judged its own
body. A `feasible` whose `needs` named a figure nobody published went straight
through and the body moved.

`_assess` now runs the same function against the pilot's own manifest and refuses
such a verdict the way it refuses one carrying `human`, naming the unmet need so
the pilot can assess again. `uncertain` is left alone, because it asks a person
and a reachable person knows things a sheet does not, and `infeasible` ends the
run anyway.

Measured on Qwen3-32B-AWQ over 54 runs before and 54 after: a 45 minute patrol on
a body whose endurance is not published came back `feasible` six times out of six
and walked until the step budget, twice with `needs: {"endurance_min": 45}` in the
same row. On the patched build the check fired three times, all on that task, none
on the other eight, and the body moved once instead of six times.

What it does not do: the pilot came back `uncertain` rather than `infeasible` each
time, and one run declared no endurance at all and so had nothing to be checked
against. The check reads what a pilot declares, so it rewards honesty and cannot
catch silence.

The two tests pass an adapter rather than a bare transport, because every existing
loop level verdict test carries no manifest and so has never had a datasheet in
hand. The second test, where the body meets the need, exists so the first one means
something.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The verdict gate reads `BEFORE_VERDICT`, the verbs quackd ships that speak, look
or read. A body quackd never shipped brings its own sensing verb, and the gate
refused it as "moves the body": a `locate` that reads where things are waited
for the very judgement it exists to inform. Measured with a humanoid sim whose
manifest offered `locate` and `reach`: Qwen2.5-Coder 14B declared the task
infeasible twice without one look, because the observation said "camera:
nothing detected" and the one verb that could answer was gated.

The gate now also honours `Verb.read_only`, the flag a verb's author already
sets for `--dry-run`. Every shipped read-only verb was in `BEFORE_VERDICT`
already, so nothing shipped changes hands; learned verbs never carry the flag,
so they still wait until somebody classifies them on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… looks before the verdict

Bayway's PR #26, merged whole. Their commit lands as written; the corrections
are in the commits after this one.

One conflict, CHANGELOG.md: both sides added under Unreleased, main a release's
worth of prose and three sections, theirs one entry. Their entry is kept word for
word and placed under main's `### Fixed`, which is where it belongs. Nothing else
moved.

What it changes: the verdict gate honours `Verb.read_only` beside BEFORE_VERDICT,
so a body quackd never shipped can say its own sensing verb only looks. Every
shipped read-only verb was already in the set, so nothing shipped moves earlier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, the way a bid already is

Vallhalen's PR #24, merged whole. Their commit lands as written; the corrections
are in the commits after this one.

Two conflicts, both resolved to their side. CHANGELOG.md: both sides added under
Unreleased, and their entry is kept word for word under main's `### Fixed`, after
#26's. tests/test_loop.py: their import block and main's collided, so both lines
are kept, theirs exactly as they wrote it.

**This merge commit is red, on purpose.** Their branch imports
`MicroduckAdapter` from `quackd.adapters.microduck`, and main moved every adapter
into its own package in 8611275 while the PR was open, so that module no longer
exists and `tests/test_loop.py` cannot be collected. Keeping their line and
fixing it in the commit after this one is the way this repository handles it, so
that what they wrote stays separate in the history from what had to change. The
next commit is green.

What it changes: `_assess` runs `missing_needs` against the pilot's own manifest
and refuses a `feasible` whose `needs` its own sheet does not meet, the way a bid
is refused at the coordinator. Measured on Qwen3-32B-AWQ over 54 runs; the
numbers are in the CHANGELOG entry. The MCP session does not do the same yet, and
the refusal offers `infeasible` where the measured pilot reached for `uncertain`.
Both are in the commits after this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`quackd.adapters.microduck` was the import in Vallhalen's branch and it was
right when they wrote it. 8611275 moved all seven adapters into their own
packages while the PR sat open, so the merge before this one cannot collect
tests/test_loop.py at all.

`from quackd_microduck import MicroduckAdapter`, which is the spelling every
other test uses. Their two tests are otherwise untouched and they pass.

This is the correction the merge commit's message promised, kept separate so
their commit and the change main forced on it stay legible apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s first

#26 gives `Verb.read_only` a second reader. The gate honours it, which is the
whole point for a body quackd never shipped: a stranger's `locate` cannot be put
in `BEFORE_VERDICT`, because that set is closed by a test which rejects any name
no official adapter ships.

For the bodies quackd does ship the flag must restate the set and never widen it,
and nothing said so. `test_every_shipped_verb_was_classified_on_purpose` reads
the two sets and cannot see the flag, so a shipped verb flagged read-only and
filed under `MOVES_THE_BODY` would have run before any verdict with nothing to
catch it. It is caught now. Both directions are pinned: every shipped read-only
verb is in `BEFORE_VERDICT`, and none is in `MOVES_THE_BODY`.

The other half is what the flag claims. Two gates believe a read-only verb sends
nothing, `--dry-run` since 0.4 and the verdict gate since #26, and neither ever
checked. `test_a_read_only_verb_sends_nothing` runs every flagged verb on the
mock and reads the wire.

Both were confirmed to fail before they were kept: dropping `introspect` from the
set trips the first, and flagging `move` read-only trips both, the second by
naming the ten move intents it sent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… recited

The rule line said "Until then only `observe`, `report_state`, `say`, the head
verbs and `stop` run" to every pilot, whatever body it was driving, and it was
wrong three ways. hello-world's pilot was told `observe` runs when its contract
allows no such verb and the executor would refuse it. An arm was told about head
verbs no arm has, which is the same fault as the `--goal` strategy paragraph
fixed under Unreleased. And since #26 a body quackd never shipped can bring its own
sensing verb, which the gate now lets through and that sentence had no way to
name: the pilot with a `locate` was told, in as many words, that it could not use
it before judging.

`before_verdict_clause` reads the run's own verbs by the same rule the gate
applies, `BEFORE_VERDICT` or `read_only`, so the sentence cannot drift from the
refusal. It spells a verb the way the allowlist spells it, so find-and-kick now
reads "only `quack`, `get_frame` and `stop` run", and `stop` is last and
unconditional because the brake is never gated.

The two tool descriptions stop reciting a fixed list. `assess_task` says what is
true of every body, that only what looks, speaks or brakes runs, and points at
the Rules for which verbs those are here. Over MCP there is no system prompt, so
`robot_list_verbs` marks each verb `before_verdict` and the description points at
that field instead, which is also the only answer that can be right for a robot
quackd never shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lassify a verb

Three documents said the gate reads `BEFORE_VERDICT` and nothing else, which #26
makes false, and the one page a third-party adapter author actually follows told
them to do something they cannot do: classify their verb in `quackd/verdict.py`,
a file they do not own, in a set whose own test rejects any name no shipped
adapter offers.

docs/adapters.md now says both halves. A verb shipped here is classified in that
file and a test insists on it. A verb quackd never ships waits for the verdict
unless its `Verb` carries `read_only=True`, which is the author saying it sends
no intent. A warning after the list says what that flag costs if it is wrong: it
defeats the verdict gate and `--dry-run` at once, and nothing can check it.

CONTRIBUTING said the same false sentence and now names the flag and the test
that keeps a shipped read-only verb inside the set. The flag's own docstring
described two consequences and not the fact underneath them, so it states the
fact first: the verb sends no intent. ADR-0032 gets a dated amendment beside the
0034 one rather than a quiet edit, because it is the design record and its
"anything not in that set waits" is exactly the sentence that moved.

Two smaller ones found while reading: `docs/safety.md` and `docs/duck-spec.md`
each hand-listed the read-only verbs that survive `--dry-run` and both had
omitted `introspect` since the rosbridge adapter shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ld to its own sheet too

#24 says "your rule, applied on both sides". It was applied on one. The
agent loop refused a `feasible` whose `needs` its own datasheet does not meet,
and `RobotSession._assess` recorded the same verdict and let the body move. Over
MCP that gap is a little worse than it sounds: `robot_assess_task` already ran
`missing_needs` against every OTHER robot in the fleet to fill in `could`, so the
one sheet it never compared against was the sheet of the robot it was about to
drive.

Both surfaces call `own_sheet_objection` now, so a pilot hears the same words
wherever it drives from, which is the standard ADR-0032 set for a role's `needs`
being checked three times "in the same words".

The message names three ways out where it named one. The pilot measured on
Qwen3-32B answered `uncertain` to a refusal that offered only `infeasible`, and
that is the right answer here rather than a dodge: an unpublished figure is
exactly the case where a person knows something the sheet does not, and ADR-0032
already says a reachable human beats a flag. The third way out is the honest one
where the pilot asked for more than the task turns on.

Where a pilot reads about `needs` now says the check exists, in the tool schema,
in the MCP description and in docs/mcp.md, because a refusal nobody was warned
about reads as a bug. docs/safety.md gets the section that was missing: what a
verdict contradicting itself is, what the three answers do, and the two limits
measured so far, that it turns a silent feasible into a question rather than a
refusal, and that it reads what the pilot declares and so cannot catch silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…terrain is the floor the prompt already assumes

#24 gave `missing_needs` the power to refuse a verdict where it could only rank a
body before, and two of its readings then contradicted the prompt. Neither could
show up in the 54 runs behind that PR, because every shipped mobile body
publishes `indoor_flat` and no run answered with a zero.

A body whose terrain nobody published is told, by the prompt and in as many
words, to "assume a flat indoor floor and decline anything else". The matcher
counted an unpublished terrain as meeting nothing, so a pilot that answered
`feasible` with `terrain: indoor_flat`, which is exactly what it was told to
assume, was refused for doing as it was told. That is any third-party body and
the rosbridge base. Unpublished still meets nothing above a flat indoor floor.

And `payload_kg: 0` is how a pilot says the task carries nothing, on a tool whose
own description asks it to fill `needs` in even when the verdict is feasible. It
read as a need nobody had published a figure for, and got refused. A working
height is not a minimum in the same way, so `work_height_m: 0` still names the
ground and a body either reaches it or does not.

Both changes live in `missing_needs_in`, so the coordinator judges a bid by the
same reading. That is deliberate: a refusal and a role are meant to say the same
thing, and a bid from a body that published no terrain deserves the same benefit
of the doubt the prompt already gives it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on for uncertain

Issue #25. `quackd run --goal "find the ball and kick it"` is the first command
the README shows with a model behind it, and on Qwen3-32B-AWQ it stopped at the
feasibility gate five times in six, `uncertain`, run aborted. The same task from
`ducks/find-and-kick.duck` passed six in six on the same body, seed and model.
The contributor rebuilt the goal contract as a task file, diffed its prompt to
zero against a real `--goal` run, and changed one thing at a time: persona does
nothing, a concrete success criterion does nothing, and narrowing the allowlist
from fifteen verbs to six carries it to six of six.

I am not narrowing what `--goal` allows. quackd cannot know what a sentence
needs, the README's own goal needs `kick`, which is not a core verb, and a
heuristic over the words would shape every model's vocabulary from one model's
behaviour on one task.

What is wrong on its own terms is the description the model reads. It said the
verdict is judged against the datasheet and then listed "the object is out of
view" as a reason to answer `uncertain`, which are two different questions: where
the ball is decides nothing about whether a duck can kick a ball on the floor it
is rated for. The pilot's own reasons in #25 are that confusion out loud, "Since
the camera currently detects nothing, I cannot determine feasibility yet". The
description now says the verdict is the task's needs against the body's limits
and not whether you can see the target, that a target you have not found yet is
what the run is for, and keeps `uncertain` for what it is for: a figure that
decides a limit, on a thing you have not seen, or a limit nobody published.

The same correction goes to the rule line, to the MCP description, and to
docs/safety.md, whose "When the pilot is unsure" now says finding the thing is
the task and that the looking verbs run before the verdict so the pilot can go
and look.

This is the contributor's first candidate and they flagged it as untested. It
still is. There is no local server on this machine, and the shipped Qwen3
transcripts show the six verb file answering `feasible` on a frame that detected
nothing, so the allowlist may be doing something these words do not undo. The
six cells want re-running on this branch, and #25 says so rather than closing.

One live test comes with it, `live_llm` and opt-in like its neighbours: the
README's goal through `duck_from_goal`, asserting the run does not end at the
gate with nothing moved. It is one paid run of one cloud model and it says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ady ask for

Found by @Vallhalen while measuring #24, and it is one tool disagreeing with
itself. `needs` speaks `endurance_min`, so a pilot may say a task requires
forty five minutes of running time. `Estimate.quantity` took mass, size,
distance, height, a count and "other", so the same pilot could not estimate one.
On the 45 minute patrol the model reached for `quantity: "endurance_min"`, got a
validation refusal, and spent an LLM call on it twice out of six.

`duration_min` is in both halves now. The enum is hand-written in two files, the
pydantic model and the tool schema, which is exactly how they came apart, so a
test holds them to each other rather than to a list typed out a third time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…opped being true

0.9.0's known limitations said "No real model has ever refused a task on
feasibility grounds here", and PLAN.md carried the same line as an open item.
#24 is the day that stopped being true, so the plan says what is measured and
what is still open instead. The CHANGELOG's 0.9.0 section keeps the old sentence,
because it was true when it shipped and correcting history is not the job.

docs/local-llms.md gets both measurements under Honest notes, beside that
contributor's earlier transcript pair: the 108 runs behind #24 with the noise
floor they measured for free, the hole that was the unpublished figure, the half
that is prompt shaped and still unfixed (a body that cannot carry hedging rather
than refusing), and the six cells of #25 with the one thing that moved them. The
transcripts for these are not published, so the numbers are attributed as their
report rather than as files a reader can check, and the last line of each says
what is still unmeasured: nobody here runs that model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…of each

Their entries stay as they wrote them. @Bayway's gains the credit sentence and
@Vallhalen's gains the issue id in theirs, which is the form the existing
credits in this file use, and then each gets a paragraph saying what was added
after it, so a reader can tell the contribution from the corrections without
reading the log.

The #25 wording change goes under Changed as its own entry, crediting the
measurement that found the cause and saying plainly that the allowlist is not
narrowed and that the fix is untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s now say

docs/safety.md claims the check turns a silent `feasible` into a question and
that `--yes` then answers it with go, so the same unmet need reaches the body one
word later and the contradiction is left in the transcript rather than stopping
the run. I established that by reading the code, which is the kind of claim this
repo turns into a test.

It is not a hole for the check to close. `--yes` is a person saying they have read
the contract, and ADR-0032 puts a reachable human above a flag. What the check
buys under it is the record: a refusal row and the doubt it became, where a silent
`feasible` left nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ot on two

The test I wrote a few commits ago read `default_registry()`, which holds two of
the four shipped read-only verbs. The arm's own `report_state` and the rosbridge
base's `introspect` live in their adapters and were not covered, and `introspect`
is the one that most deserves it: it is the only shipped read-only verb that talks
to the robot at all, asking the bridge what the body is.

Parametrised over every adapter's mock backend, with `send_intent` wrapped so the
assertion reads the wire rather than a transport's own bookkeeping, which differs
per body. Confirmed to fail: making `introspect` send a stop reports "a read-only
verb on rosbridge sent ['stop']".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erdict

An adversarial audit of this branch found the docstring on `BEFORE_VERDICT`
promising something only another gate was keeping. It says a learned verb is
refused until somebody classifies it on purpose, on the grounds that a learned
verb never carries `read_only`. True of the flag, and the other half of that
condition is a name test, so a policy named `observe` passed it.

The name is free to take. A body with no camera verb has no `observe` of its own,
which is every arm, so the registry accepts the registration. What actually
stopped such a verb was the confirm gate one row down, because
`register_learned_verb` marks a policy `confirm`, and `--yes` answers the confirm
gate. Proven both ways: without the exclusion the new test fails with
`ConfirmDenied` rather than `VerdictRequired`, which is the hole in one line of
output.

The gate excludes `kind == "learned"` from the name half now, and
`before_verdict_clause` applies the same rule so the prompt does not offer what
the gate refuses. This predates #26: the same name test has been the gate since
ADR-0032, and what #26 changed is that the docstring beside it is now read by
somebody deciding whether to trust the flag.

The set's docstring also says outright what the name half means, because it is
not obvious: those nine are a vocabulary rather than a list, so a body that ships
a verb called `gaze` which walks has said the wrong word about itself, the way a
verb carrying `read_only` that sends an intent has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nally

An audit of the previous commit found the Rules line and the tool description
beside it giving opposite answers to the same state. The Rules line, labelled
enforced by the executor and not optional, said flatly that a target you have not
found is not a reason for `uncertain`. The tool description kept `uncertain` for
the mass or size of a thing that decides a limit and that the pilot has not seen.
For "pick up the box" with the box out of frame, both describe the situation and
the more authoritative one is wrong, which would buy a confident `feasible` on
exactly the task class where the gate earns its keep.

All four surfaces carry both halves now, in the same words: not having found the
target is not by itself a reason, and it is one when a limit turns on that unseen
thing's mass or size, or on a figure the sheet does not publish.

The MCP description had lost the carve-out altogether, which is worse there than
in the loop: there is no terminal on that surface, so `uncertain` is the only
route to the person who might know an unpublished figure.

docs/safety.md stated the rule and then negated it two sentences later, and
offered a reason that is not true of the gate. It said the looking verbs run
first so the pilot can go and look. The pilot cannot go: `search_scan`, `go_to`
and `move` all wait for the verdict. It can take a frame from where it stands,
and when the deciding figure is on something out of that frame, `uncertain` and
the person are the way past it. That is what the page says now.

Two tests hold the surfaces to each other, one per surface that has a way to be
read: the prompt three in `tests/test_verdict.py`, and the MCP description
through a real client in `tests/test_mcp_server.py`, which also pins #24's check
and the `before_verdict` field into the only text an MCP pilot ever reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n prose

Six agents were pointed at this branch with instructions to find it wrong, and
its prose is where most of what they found lives. The gate and the prompt fixes
are in the two commits before this one; these are the claims.

**A contributor's own sentence had been extended.** The credit commit said their
entry gained the issue id, and it had also gained "across 108 runs and reported
the noise floor with the result" inside their sentence, which is not theirs and
put "54 runs" and "108 runs" in one bullet with nothing to reconcile them. Their
sentence is verbatim again and the addition is in the paragraph that is mine.

**A phrase was quoted to the wrong place.** "Both sides" is in #24's body, not
its title, so the changelog quoted their title saying something it does not say.

**Two counts were wrong.** Three documents said the gate reads `BEFORE_VERDICT`
and nothing else, not five: the other two of the five that commit touched were
`--dry-run` lists that had lost `introspect`, which the commit itself said two
paragraphs later. And 14 infeasible, 26 uncertain and 8 feasible sums to 48
rather than 54, because the 8 is from the noise floor subset. Over 54 runs #24's
own task table gives 14 feasible. Both the docs and PLAN.md said the 48 figure
under a 54 run heading.

**A measured number was rounded up.** Narrowing the allowlist under the duck's
verb names was feasible 5 of 6, not 6 of 6. Only the variant under the goal's own
verb names was 6 of 6, which is the whole reason that variant exists.

**A README claim had gone stale under us.** "It cost the README's own first
command" was true when #25 was filed and stopped being true when the README was
rewritten to lead with the arm: its first command with a model behind it is a
wave on an SO-101 now. What is true is that it is the goal the opening paragraph
names and the first one shown for a duck.

**Three surfaces stated the own-sheet rule without its exceptions.** The two
guards for an honest pilot, a minimum of zero and an unpublished terrain asked as
`indoor_flat`, landed after the wording that says a need the sheet does not
publish is refused. The safety page, the MCP page and the tool schema say them
now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… let the body move

**A refusal refused the words and not the motion.** The check runs before the
verdict is recorded, the way the `human` and validation refusals do, so a pilot
already cleared for one reading of the task could name a need this body cannot
meet, be refused, and go on moving on the older verdict while the newer and
better informed one was thrown away. That is the exact failure #24 exists to
stop, one re-assessment later. Both surfaces withdraw what was standing now, so
nothing moves until the pilot answers again, and both tests fail without it.

**A need that was not a number crashed the tool.** The zero guard called
`float()` on whatever arrived, and `robot_assess_task` computes `could` from the
raw tool argument before anything validates it, catching `ValueError` only. So
`needs: {"payload_kg": null}` raised `TypeError` out of the MCP tool where before
it read as a need nobody published. It falls through to the refusal again.

**The terrain exception reached two bodies the prompt never told.** It exists
because the prompt tells a body whose terrain nobody published to assume a flat
indoor floor, so the two readers agree. That sentence is only rendered for a body
that moves and has a datasheet at all: `body_lines` tells a body with no sheet to
treat every limit as not published, and a body that does not move is told "it
does not move" instead. The exception stops where the promise does, which also
keeps a flock bid that carried no datasheet from winning a role on it.

**A stranger's flag outranked quackd's own record.** `read_only` opened the gate
even for a verb arriving under a name this repository has classified as motion,
so a `kick` carrying the flag ran before any verdict. A verb saying both those
things about itself is saying two contradictory things, and the name is the one to
believe. The new test above it asserted this combination was impossible and could
only see the verbs quackd ships.

The refusal message now names the third way out properly. A person asked about an
unpublished figure can do something about it: publish it in the task file's own
`datasheet:` block, which ADR-0032 added for exactly that and which is the only
thing that makes a sheet say something new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r the wrong half

ADR-0032 is the design record for this gate and the branch amended it for #26
only. #24 changes what the ADR's Decision section describes, because a `feasible`
verdict is no longer recorded unconditionally, so it gets its own dated amendment
saying what the check is, what the two exceptions are, and what it cannot do.

Three documents still stated the old absolute rule that a figure nobody published
counts as not met. `docs/duck-spec.md` is the canonical per-key table that
`docs/flock.md` points at by anchor, so the two exceptions are in the table and in
the paragraph under it. `quackd/verdict.py`'s module docstring states the rule 135
lines above the function that now qualifies it, and points at it.

**A refused assessment was written into the transcript as the standing verdict.**
Every refusal in `_assess` returns before recording, and the row was built from
whatever verdict happened to be in the executor, so a refused re-assessment
carried the earlier verdict's word, reason and needs and read as though that one
had been refused. The row describes the call now, and a test covers the path that
is not the own-sheet check, because that one withdraws the verdict and so hid the
bug from its own test.

And docs/safety.md says out loud the thing the check does that is easy to resent.
It asks more of a pilot that answers fully: a duck asked to nudge a 60 g ball has
no published payload to compare against, so a pilot that honestly writes
`payload_kg: 0.06` is refused where one that writes nothing is not. That is
refuse by default working as ADR-0032 intends, and the answer that lasts longer
than one run is a `duck: 2` datasheet block, which is in the page now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three entries described the branch as it stood before six agents were pointed
at it. They describe what shipped now: the gate shutting on a refusal, which is
the one the audit found that let a body move; the learned verb that could take one
of the nine names; the terrain exception narrowed to the bodies the prompt
actually tells; and the #25 wording, which stated absolutely in the Rules line
what it meant conditionally in the tool description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rokbenko
rokbenko merged commit 02411b3 into main Sep 17, 2026
8 checks passed
@rokbenko
rokbenko deleted the integrate/verdict-gate branch September 17, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants