TASK-109 — a V5 sign-off is selected from measured facts, not authored from memory - #12
Merged
Merged
Conversation
PR #8 merged, so the conformance gate now enforces on this branch. That frees bin/perry-decide and schema/state-schema.json, which four rows were queued behind — the constraint on concurrency was never the dispatch cap, it was the 24 files an unmerged branch was holding. TASK-085 dispatched: a decision's status is a tuple in bin/perry-decide with two more prose copies and no schema binding under any of them, and there is no value meaning "drafted, awaiting the user". TASK-047's Source path is corrected. It cited a file that never existed — ADR-004 is mandatory-migration, not conformance-marker. The id resolved, so the dangling-id check passed; nothing validates that a spec's Source path exists. Both TASK-047 and TASK-085 passed the escalation scan only because their own Out of scope sections name the fragment that matched them. Recorded in each spec: a disclaimer written by the spec's author is not clearance, and the clearance that counts came from the user in chat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The switch-over checklist said a brand-new project asks for one declaration before its first write. Every file Perry creates after the last declaration is born undeclared, in an old project as much as a new one — measured, not argued: creation is not gated and the next write is. Also records the user's declare --all: 16/16 declared with the gate enforcing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TASK-047 closed at V5 with the user's signature recorded verbatim, in the language it was written in — the rung's value is saying what was actually checked, and a translation is a paraphrase. TASK-109 opens the thing that signature exposed: Perry asks a human to compose from memory a description of checks Perry itself ran and printed minutes earlier, and the resulting sentence cannot distinguish "I checked this" from "Perry checked this and I read the output". TASK-108 is at review. It took the open-decision count from 7 to 2, matching asks.open, by counting decision records rather than occurrences of decision-words in prose. Its baseline correction stands against the PMO: the dispatch prompt quoted 1 red module, measured in the main checkout where uncommitted edits mask two failures; the agent's tree had 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
V5 is the one rung whose content is a human's — "name, date, and what
they checked" — and the tool took none of it. `done --rung V5` wrote a
rung; the signature was a paragraph the user composed by hand.
TASK-047 is the case that named the defect. Perry ran three checks,
printed their output and showed the user, who then wrote from memory a
sentence describing those same three checks. Two things are wrong, and
the second is the one that matters: the user re-derives by hand a record
the system already holds, and free text cannot distinguish *I re-ran
this* from *Perry ran this and I read the output*.
`done` now takes the sign-off as a selection over what Perry measured:
perry-task signoff-offer <ID> --measured "…" --restated "…" read-only
perry-task done <ID> --rung V5 --checked 1,3 [--not-looked-at 4]
[--also "…"] [--signer …]
- Every offered item is labelled with its provenance — `Perry verified`
or `restated — Perry did not verify this`. Selecting the first means
*I checked this too*; the label is what keeps that from reading as
*I accept Perry's word*.
- Name and date are filled in from `git config user.name` and today.
- Unselected items are recorded as `accepted on report`, never dropped.
`not looked at` is a third disposition and is never a default — Perry's
own three signatures already write all three categories by hand.
- Free text stays, additive, for what Perry does not know about.
- **Perry may draft only facts it measured, never a claim about what the
user did.** `check_no_user_claim` enforces that at the option builder,
mechanically, because a review note holds until the first hurried close.
- A V5 close with nothing checked and no free text is refused.
The record is identical on every host; only the rendering degrades to the
numbered free-text prompt on a host with no selection UI.
V1–V4 closes are byte-identical, and the three signatures already recorded
in evidence/2026-08/ are untouched — this adds a path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
A V5 close no longer asks the human to compose a paragraph from memory. It offers one selection prompt built from what Perry measured during the task, and records what came back.
Why
TASK-047 is the case. Perry ran the
claims[]diff, the three write-behaviour runs and theperry-migrateexemption, printed their output, and showed the user — who then wrote, from memory, a sentence describing those same three checks. Two defects, and the second is the one that matters:The frequency was never the problem: 3 of 80 closed rows carry V5, 4%.
The line that keeps this from becoming a rubber stamp
claims[] carries zero changed lines in the diff— allowed, Perry ran the diff.the user reviewed the diff— refused, Perry cannot know that.check_no_user_claimenforces this mechanically at the option builder, the only place a drafted string is minted, because a rule enforced by remembering it is a rule that holds until the first hurried close.TestNoDraftedOptionAssertsAUserActionruns 13 user claims (English and Chinese) against it, plus 8 measurable facts as the anti-vacuity guard — several of them near-misses on purpose (the user-facing message…,the acceptance-criteria file…).The subjective question, decided
Three dispositions, not two.
checked/accepted on report/not looked at.Rejected: collapsing the last two. It was rejected on the corpus — all three V5 signatures already in this repository write the third category by hand. TASK-034's carries a section headed "Not checked, and recorded because V5's whole value is saying so"; TASK-047's distinguishes fixture opt-out 的理由已读并接受 from two costs taken on the strength of Perry's printed output.
The second reason is the drafting rule. Defaulting an unselected item to
accepted on reportis already the outer edge of what Perry may assert — it describes the scope of the signature, not an act the user performed. Sonot looked atis never a default; it is reached only by the user naming the item. Pinned byTestThreeDispositionsNotTwo, whose docstring names the rejected alternative.Deliverables
cmd_signoff_offerPerry verified/restated — Perry did not verify this, verbatim in record and journalgit config user.name+ today;--signeronly for when git has no nameaccepted on report--also, and it is not run through the drafting guard — those are the user's own wordscheck_no_user_claim, mechanicalbuild_signoff, nothing writtenHost degradation
The record is designed first and is identical on every host. Only the rendering moves: Claude Code renders
optionswithAskUserQuestion(multiSelect: true), OpenCode withquestion/multiple: true, and Codex prints the payload'sprompt— the numbered free-text fallback ofreference/host-capabilities.md § Prompt rendering.--checkedtherefore accepts1,3,all,none, and one flag per number, because that is what a person typing into the fallback hands back.test_the_same_selection_records_the_same_thing_from_either_spellingasserts the two spellings produce one record.Out of scope, asserted rather than assumed
TestV1toV4ClosesAreUntouched.TestHistoryIsNotRewrittenasserts the three V5 closes in the log still resolve to their evidence files, that each file still carries a name and a date, and that none was back-filled with asignoffkey.done --rung V5with no sign-off flags is unchanged and writes no signature rather than a blank one: rungs are advisory this release (DESIGN-003 § 4 decision 4) and hardening the rung itself is out of scope. The refusal fires when the sign-off path was engaged and produced nothing — which is exactly "reachable by pressing return".perry/,schema/state-schema.json,claims, or the other tools' branches was touched.Verification
python3 tests/paralleltest_board_renderonperry/BOARD.md,test_diagnoseLOAD-03,test_router_budgetSKILL.md 21030 > 20480). None is this row's. Baseline measured on the worktree before any edit.tests/test_v5_signoff.pybash tests/runtest_host_support.py::TestOpenCodeDispatchLimit::test_concurrent_registers_do_not_exceed_opencode_cap. That module is the documented flake — it shares~/.cache/perry/, and this invocation overlapped atests/parallelrunning in a second worktree. Re-run alone 4 times, 24/24 every time. Steps 1, 3 and 4 (--templates, script compile/--help, both sample projects) green; step 2 istests/parallel, above.python3 bin/perry-lintgit diff --checktest_procedures_call_the_tool§ close-taskedit still teaches the tool calltest_host_supportbash tests/runrow🤖 Generated with Claude Code