Skip to content

TASK-109 — a V5 sign-off is selected from measured facts, not authored from memory - #12

Merged
ranjiao merged 4 commits into
feat/work-modesfrom
coding/task-109-v5-signoff-selection
Aug 20, 2026
Merged

TASK-109 — a V5 sign-off is selected from measured facts, not authored from memory#12
ranjiao merged 4 commits into
feat/work-modesfrom
coding/task-109-v5-signoff-selection

Conversation

@ranjiao

@ranjiao ranjiao commented Aug 20, 2026

Copy link
Copy Markdown
Owner

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.

# read-only. Numbers the items; the numbering is what `done --checked` reads.
"$PERRY_HOME/bin/perry-task" signoff-offer <ID> --json \
    --measured "claims[] carries zero changed lines in the diff" \
    --restated "the branch carrying this change is unmerged"

# the close, with the answer
"$PERRY_HOME/bin/perry-task" done <ID> --evidence <path> --rung V5 \
    --measured "" --restated "" \
    --checked "1,3" [--not-looked-at "4"] [--also ""]

Why

TASK-047 is the case. Perry ran the claims[] diff, the three write-behaviour runs and the perry-migrate exemption, 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:

  1. the user re-derives by hand a record the system already holds, and
  2. free text flattens I checked this into I accepted this, and it gets worse the more Perry does.

The frequency was never the problem: 3 of 80 closed rows carry V5, 4%.

The line that keeps this from becoming a rubber stamp

Perry may draft only facts it measured. It may never draft a claim about what the user did.

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_claim enforces 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. TestNoDraftedOptionAssertsAUserAction runs 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 report is already the outer edge of what Perry may assert — it describes the scope of the signature, not an act the user performed. So not looked at is never a default; it is reached only by the user naming the item. Pinned by TestThreeDispositionsNotTwo, whose docstring names the rejected alternative.

Deliverables

#
1 one selection prompt built from what Perry measured cmd_signoff_offer
2 every item labelled with its provenance Perry verified / restated — Perry did not verify this, verbatim in record and journal
3 name and date filled automatically git config user.name + today; --signer only for when git has no name
4 unselected items recorded, not dropped accepted on report
5 free text stays, additive --also, and it is not run through the drafting guard — those are the user's own words
6 Perry never drafts a claim about what the user did check_no_user_claim, mechanical
7 nothing selected + no free text → refused build_signoff, nothing written

Host degradation

The record is designed first and is identical on every host. Only the rendering moves: Claude Code renders options with AskUserQuestion (multiSelect: true), OpenCode with question / multiple: true, and Codex prints the payload's prompt — the numbered free-text fallback of reference/host-capabilities.md § Prompt rendering. --checked therefore accepts 1,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_spelling asserts the two spellings produce one record.

Out of scope, asserted rather than assumed

  • V1–V4 closes are byte-identicalTestV1toV4ClosesAreUntouched.
  • No recorded signature is rewrittenTestHistoryIsNotRewritten asserts 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 a signoff key.
  • A bare done --rung V5 with 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".
  • Nothing under perry/, schema/state-schema.json, claims, or the other tools' branches was touched.

Verification

python3 tests/parallel 60 modules · 1749 tests · 3 red — the same 3 that were red before this branch (test_board_render on perry/BOARD.md, test_diagnose LOAD-03, test_router_budget SKILL.md 21030 > 20480). None is this row's. Baseline measured on the worktree before any edit.
tests/test_v5_signoff.py 32/32 new tests, all V5 verification items 1–5
bash tests/run exit 1, 4 red: the 3 baseline modules above plus test_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 a tests/parallel running 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 is tests/parallel, above.
python3 bin/perry-lint ✓ clean, 0 drifted
git diff --check clean
test_procedures_call_the_tool 22/22 — the § close-task edit still teaches the tool call
test_host_support 24/24 alone, ×4 — see the bash tests/run row

🤖 Generated with Claude Code

ranjiao and others added 4 commits August 20, 2026 15:27
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>
@ranjiao
ranjiao merged commit e9d8c4d into feat/work-modes Aug 20, 2026
1 check failed
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.

1 participant