Three gates for what a session actually costs, all three configurable - #31
Merged
Conversation
…cal store
USER-906, option B. One invariant, `bin/perry-task § refuse_to_shrink`, called
at every canonical store: `tasks.jsonl` in `commit()` and each of the three
register stores in `register_change()`. Not a fourth predicate.
if after >= before or event_name in SHRINK_ALLOWED: return
raise Refused(...)
`SHRINK_ALLOWED` is `{purge, resolve-intake, intake-sweep}` — the three
commands USER-906 named — and it is a frozenset of three literals rather than
a question about board state, which is what the first three rounds each got
wrong one level apart:
- round 1 keyed the exemption on the COMMAND NAME. `intake-sweep` is the only
command that moves rows itself; it is not the only way rows move.
- round 2 keyed it on `(request, arrived)`, which is not unique — the same
thing filed twice on the same day is the ordinary reason a row is dropped.
- round 3 keyed it on the section's SHAPE, and the shape is read at a moment
the command controls: `cmd_add`'s queue branch calls `ensure_section` before
`commit()` ever asks, so the gate saw a freshly created, readable, EMPTY
table.
The count does not care when it is read, which is why option A — snapshotting
the gate at command entry — is not needed and was rejected. All four doors are
the same sentence: the derivation produced fewer records than the store holds.
The previous commit is the reproduction, red. Under it,
`test_register_store_invariant` was 24 failures / 7 errors. Under this one it
is 37 green, and the merge-hold reproduction refuses:
perry-task: refused — `add` would take …/intake.jsonl from 3 record(s)
to 0, and an ordinary write may never make a canonical store smaller
(USER-906). Nothing was written.
perry-lint: 1 error(s) · intake store: 3 record(s), 3 row(s) drifted
Two existing tests are converted, both because they asserted the behaviour
this row exists to change, and one said so in its own docstring:
- `test_asks_store.test_the_ordinary_writer_still_writes_the_section_and_that_
is_drift` — *"deliberately not converted (TASK-203)"*. It now asserts the
other half of the same fact, plus the answer's content in the record.
- `test_intake_store.test_a_sweep_moves_n_and_the_store_is_what_says_so` — the
sweep now records its own renumbering, so `drifted` is 0 rather than 3. The
reading the test exists for (`n=2` is a different request) is unchanged and
still asserted; the drift half belongs to a hand edit and is proved by
`test_a_row_deleted_by_hand_reports_every_row_it_renumbered` thirty lines up.
bash tests/run, this worktree: 99 modules / 2919 tests / 3 failures
(test_diagnose 2, test_kr_progress_provenance 1) — byte-identical to the
failure set on main at 6c0d041, which is 98 / 2882 / 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… lane gets three things
CORRECTION, recorded because it reverses an ordering. Reviewing the open
rows against DESIGN-013 first read TASK-182 as a CONFLICT: it builds a
renderer for OKR.md tables that DESIGN-013 deletes. That reading was
wrong. DESIGN-009 section 6 states step 2's purpose in its own words —
'This is the GATE: if the renderer cannot rebuild the five headings from
records, the records are wrong. Same bar as risks-diff.' It is a
completeness proof for the store, not a deliverable renderer, and under
DESIGN-013 it stops being incidental and becomes load-bearing: the KR
tables must not be deleted until something has proved okr.jsonl holds
them.
The risk runs the OTHER way. If TASK-236 ran first, TASK-182's gate would
evaporate — with the tables already gone there is nothing to rebuild, so
the proof passes vacuously and nobody learns whether the store was
complete. That is the defect class this project has caught six times: a
check that cannot fail on the thing it names.
- TASK-236 now depends on TASK-181 and TASK-182 as well as TASK-235.
- TASK-182 records that it is a precondition and that its byte-for-byte
target is the CURRENT OKR.md, tables included.
- DESIGN-013 section 9 (append-only after lock) carries the correction.
Handed to the goals lane, which owns OKR.md and phase/ and which the work
lane may not write: P003-O2-KR3 becomes unmeetable under ADR-010; where
TASK-235/236/237 belong (the work lane's read is: the next phase, not
patches to this one); and P003-O2-KR1's target is still the wrong number.
handoff/2026-08-29-goals-lane-after-design-013.md
USER-907 filed for the KR decision, because dropping a KR changes what a
live phase's Definition of Done means. TASK-199 is blocked on it and left
untouched on purpose — dropping the row is the visible half of dropping
the KR, and doing that half first would make the record say the KR FAILED
rather than that it was WITHDRAWN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ck to store-default
USER-905 binds. Three decisions, each measured.
Decision 1 — principle A, computed once. `tracks_missing_from_the_register`
compared a set of NAMES, so a register record that CONTRADICTED a declared row
counted as carrying it: one table against two stores differing only in whether
a `main` record exists got opposite responses while `perry-lint` reported the
same rule on both. It is replaced by `tracks_the_register_contradicts`, which
does not re-derive anything — it calls `perry_md_store.plan`, the same
comparison `bin/perry-lint § check_md_store_drift` makes, and reads the
`track/…` rows out of its report. Re-deriving that rule on the write side was
the root cause across rounds 3, 4 and 5.
`tracks_the_projection_declares` now walks `perry_md_store.CONFIG.scan` rather
than `parse_tracks`, so the `## Tracks` heading and every column name come from
`schema/state-schema.json § i18n` — the source `perry-lint` reads. `grep -n
"parse_tracks(" bin/*` is two lines: the definition and the adoption path.
The file's self-contradiction is resolved. `store-default` means the store
ANSWERED; whether that answer contradicts the table is a separate question,
asked identically for `store` and `store-default`.
Decision 2 — the write refusal reverts to `store-default`, as
`tracks_the_register_cannot_place`: refuse only when a declared track has no
row in the register at all. The three hand-edit workflows the round 5 reviewer
measured as blocked now write, exit 0, each with a stderr warning; W3's named
remedy still exits 1, which is why the refusal is not widened.
Decision 3 — `TestTheGoalsLaneRefusesToo` gains the assertion it lacked.
Deleting the `perry-goals` refusal is now 1 RED, not a suite at baseline.
`perry-diagnose` is made consistent rather than exempted: `tracks_contradicted`
on the payload and a `MODE-02` finding, catalogued in reference/diagnose.md.
28 mutations, 28 restored OK, 0 anchor misses. Four green ones are reported as
equivalent mutants with the reason rather than counted, including round 4's
literal at perry-state:1058, which is still behaviourally intact on its branch.
Baselines, `bash tests/run`, both on the board as committed at 6c0d041:
clean `git archive` copy of 6c0d041 : 98 modules / 2882 tests / 3 failures
this branch : 98 modules / 2902 tests / 3 failures
Sorted FAIL/ERROR lines diff to the identical set.
perry/evidence/2026-08/TASK-095-round6-result.md carries the full record,
including what was not done: perry-config's --from-file defect, perry-task
list's blank `mode` cell, and P003-O2-KR1's literal wording are all untouched
and named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
USER-907 — option (a): P003-O2-KR3 is RESTATED rather than withdrawn.
ADR-010 deletes BOARD.md, so 'the two truth models are marked in the
file' cannot be met; but the KR was never buying the marking, it was
buying a reader's ability to tell truth from projection, and that
survives on the render. TASK-199 is re-scoped to 'the render
distinguishes what is projected from what is canonical' and now depends
on TASK-237. Phase 003 does not have to record a withdrawn KR. The KR's
own wording is a goals-lane edit, already handed off.
USER-908 — (b) then (c) authorised, and the ORDER IS INVERTED for a
measured reason rather than a change of mind: rewriting 0d68034 changes
every SHA after it, including 6c0d041 and 8abd30d, which are the merge
bases of all four in-flight branches — three of them still running.
Doing it now would push finished work through an unnecessary rebase, and
those rows carry phase 003's Must-Haves.
(c) NOW — TASK-238, a check that no commit on main fails to build
standalone. Its live fixture is 0d68034 itself, and the row records
that the fixture stops being reproducible once (b) runs.
(b) AFTER the four branches land, filed as an intake row so it is not
lost. The window closes on push: origin/main is still at 45a355d
and all 27 commits are unpushed. Once pushed the answer reverts to
(a) permanently — the hook rule protects SHARED history, and this
is not shared yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Branch coding/task-095-round6 (a917a43), unmerged, forked from 6c0d041. Moved to review and a fresh-context reviewer dispatched against the spec's binding amendment — not against the original spec, which predates USER-905. What the round self-reports as holding: tracks_missing_from_the_register replaced by tracks_the_register_contradicts, which calls perry_md_store.plan — the same comparison perry-lint makes — instead of re-deriving the rule, which was the root cause across three rounds. The refusal reverted from source=store to round 4's source=store-default, and all three hand-edit workflows the round 5 reviewer measured as hard-blocked are claimed writing again. The perry-goals guard is claimed to redden when deleted, which is the tautology the amendment named. 28 mutations, 0 anchor misses. What it self-reports as NOT holding, and this is why the review leads with it: FOUR mutations came back green and are reported as findings rather than passes. One is load-bearing — perry-state:1058's is claimed provably equivalent to round 4's failed literal, which would mean round 4's defect is behaviourally intact on that path and the code is only accidentally right. The reviewer's first job is to rule on that equivalence argument. Also handed to the reviewer: a stderr drift warning the amendment did not ask for, which the author flagged himself and offered as deletable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dispatched to keep pushing while four rows are already in flight. Chosen
for file surface rather than priority order, because six agents on one
repository conflict at merge, not at edit:
in flight TASK-050 parsers.py, tables.py, perry-task, perry-tasks,
perry-state, perry-diagnose
TASK-203 perry-task, perry_store.py
TASK-157 perry-goals, parsers.py, phase/
TASK-095 read-only (V4 review)
new TASK-235 perry-decide
TASK-226 perry-conform
TASK-230 tests/run
TASK-235 got the spec a P1 row is owed (evidence/2026-08/TASK-235-spec.md)
before it was dispatched, not after — V4 grades against written criteria
and a row dispatched without them cannot be reviewed.
TASK-214 is now blocked on TASK-235 and may be CLOSED by it. Its defect is
that mint_id reads max(files INTERSECT index) while render_index rebuilds
the index from the files, so the departed half erases itself. TASK-235
deletes the index; if mint_id then reads the files alone there is no
departed half. The spec makes answering that part of the deliverable, and
asks a contract question rather than assuming one: perry-task purge's rule
is that an id is never reissued, and perry-decide must not silently
disagree.
TASK-230 was chosen deliberately, not opportunistically. Two dispatches
died on the suite on 2026-08-28 — both subagents were killed by a 600s
watchdog before they could commit — and this session is running six agents
against it. Its prompt leads with the constraint that matters more than
speed: a faster suite that is quietly less thorough is a worse outcome,
and every reduction must be shown not to reduce coverage.
Dispatch cap raised to 8 for this batch, deliberately and on the user's
explicit instruction to push as many rows as possible. Recorded here
because the same cap was overrun by accident earlier tonight and filed
against the PMO; this one is a decision, not a slip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven rounds built a better DETECTOR of a second header rule and seven
reviewers defeated it. USER-904 answered with option C: stop detecting, make
the defect structurally impossible by shrinking the surface.
`viewer/tables.py § header_index()` is now the one function allowed to fold a
header cell. It returns a `HeaderIndex` — a `list[str]` subclass of the folded
keys, so every call site it replaced (`zip`, `.index`, `in`, `enumerate`, `==`)
carries no behaviour change — plus `.column(*names)` and `.row(cells)`, the two
lookups those sites kept re-deriving. An optional `alias` runs AFTER the fold,
which is how `perry-task`'s glossary turns `状态` and `Status` into one key.
Converted, in nine files: viewer/parsers.py (6 row folds + 6 scalar header
tests), bin/perry-task (21 sites, behind `header_keys`), bin/perry_store.py (2),
bin/perry-tasks, bin/perry-lint (5), bin/perry-goals (5), bin/perry-diagnose,
bin/perry-state, bin/perry-explain, bin/perry-migrate. All six sites the
amendment names are among them; `markdown_tables`'s `norm` parameter became the
alias step, which is exact because `norm` is idempotent on a squashed key.
The check is now two nets, and they are not the same kind of thing:
- `offenders_by_symbol` — *nothing outside `header_index` maps `squash` across
a row's cells.* Zero in this tree, over one symbol, with no allowlist and no
shape. It cannot fire on a value normalizer because a value normalizer folds
a value and not a row.
- `offenders` — the shape net, kept and improved: `ROW_NAMES` is no longer the
gate. A row is recognised by local dataflow from `split_row`, including what
a file-local function RETURNS, which is what closes `_, ihdr =
section_table(...)` and the `cells_of` escape the amendment names.
- `tests/test_header_index_is_the_only_fold.py` — the runtime half, the
`ADR-007` instrument: watch the real readers parse a decorated document and
ask who called `squash`, plus the complement (did every decorated header cell
REACH `header_index`?), which is what makes `parsers.py`'s KR loss red.
Planting harness: **30 of 30 planted readers caught, 1 of 8 legitimate shapes
flagged.** The denominator is 30, not round 7's 25, because that corpus lives
in a verdict and had to be re-derived — what is planted is the union of every
shape rounds 5 and 7 name. The one false positive is `cell.split("|")`, a
multi-value cell; it is DECLARED, with a test that runs it beside
`line.split("|")` and asserts they are inseparable, because they differ only in
the receiver's name and separating them means reading names again.
`test_the_cross_module_case_is_the_price_of_a_file_local_walk` — the test that
grepped its own source for a phrase in its own docstring — is deleted.
`bash tests/run`: 98 modules · 2882 tests · 3 failures before (test_diagnose 2,
test_kr_progress_provenance 1, all pre-existing); 99 · 2893 · the same 3 after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Branch coding/task-050-header-index (f1eb3f5), unmerged. Option C as decided in USER-904, and the first round on this row that changed the SHAPE of the answer rather than widening the previous one. What it claims: viewer/tables.py header_index(cells, alias=None) is the only thing in the repo that folds a header cell, with HeaderIndex a list[str] subclass so 67 converted call sites keep zip/.index/in/== unchanged. All six named escaping sites converted. The guard is a symbol net — nothing outside header_index maps squash across a row, no allowlist under any spelling — plus a runtime net, plus a static walk that DROPPED ROW_NAMES rather than extending it. Nine mutations, all anchored with a pre-assert, all reddening a named test; parsers.py:1828 reddens three including a behavioural one, where on main the same revert loses a KR with 2882 tests green. Planting harness: 30 of 30 caught, up from round 7's 4 of 25. SELF-REPORTED SHORT, and the review is told to weigh it rather than wave it through or fail it reflexively: 1 of 8 legitimate shapes is STILL falsely flagged where the amendment requires zero. The author declares it rather than hiding it, and argues the remaining case is indistinguishable from a real offender because the two differ only in the receiver's name. The reviewer has two separate questions: is that argument true, or does it stop one step early the way rounds 5, 6 and 7 each did; and does one false positive on a legitimate shape defeat option C's thesis that a one-symbol surface need not recognise shapes. The reviewer is also told to rebuild the planting corpus itself. The author re-derived a superset of 30 because round 7's corpus lives in its verdict — and a denominator the author chose is exactly where a favourable number hides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r was told The author amended the result file after delivery to retract a baseline claim: no python3 -m unittest discover count was measured this round. The job meant to produce it captured stray test stdout instead of the summary line, and had been launched against an intermediate tree anyway, so the runners-disagree-by-3 figure is carried from the brief rather than measured here. Seven added lines, one file, zero source change — verified with git diff --stat f1eb3f5 68e63cf. The reviewer keeps its worktree at f1eb3f5 because that is the same code. Relayed to the reviewer with three consequences rather than as a notice: check whether the retraction is COMPLETE, since a partial retraction that leaves a sibling claim standing is the same defect one step over and is the exact pattern that failed rounds 5, 6 and 7; my own review brief carried the same unmeasured discover claim and was flagged as unverified input; and the fact that the retracted job independently re-confirmed 30 of 30 / 1 of 8 does not reduce the instruction to rebuild the planting corpus, because two runs of the author's harness against the author's corpus is one corpus, not two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d that exists Two gaps found by re-reading the refusal rather than by a test. **`perry-tasks tasks-write` is not a thing.** `refuse_to_shrink` built its recovery line by pasting the store's name in front of `-write`, which is right for the three registers (`intake-write`, `asks-write`, `risks-write`) and wrong for the task store, whose own subcommands are bare (`write`, `render`). The path is unreachable today — only `purge` can shrink `tasks.jsonl` and it is allowlisted — but a wrong way forward on the one store with no second copy is not a thing to leave lying in the file. `test_the_refusal_names_the_store_and_a_way_forward` now asserts both spellings and asserts `tasks-write` does NOT appear. **`--dry-run` had no test.** `commit()` calls `register_change` before `if dry_run: return plan`, so a dry run reaches the same refusal — deliberately, for the reason `cmd_add`'s own docstring gives about previews that are not the write. Asserted now by `test_a_dry_run_previews_the_refusal_rather_than_the_write`. `test_register_store_invariant`: 38 tests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as written
The agent stopped mid-mutation-run under load average 34 — seven
concurrent dispatches, which is mine and not its fault. Two consequences
were left on disk and neither is cosmetic:
- bin/perry-task in the worktree is dirty with 'if True: return' at
:2212, which disables the whole invariant. The committed code is
clean, but anything run in that tree gets a false green.
- perry/evidence/2026-08/TASK-203-round4-result.md was never written.
Resumed with restore-first instructions and an explicit licence to run
FEWER mutations and name the ones skipped, rather than report a set it
could not finish. A named gap is worth more than a number that cannot be
trusted, and this row has failed three rounds on exactly that distinction.
Both gaps the agent flagged before stopping are to stay first-class in
the RESULT rather than becoming footnotes: removing the tasks.jsonl call
site reddens NOTHING, which is a guard surviving its own deletion — the
defect TASK-095 was failed for and which the amendment says 'does not
count'; and resolve-intake reduces no count at all despite being one of
the three names USER-906 put in the invariant, which is either a wrong
list or a wrong command and must be named as one.
No further dispatches until the load clears.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fails when it is deleted
A guard that survives its own deletion does not count, and the first mutation
round measured exactly that: removing
refuse_to_shrink("tasks", perry_store.store_path(state_root),
event.get("event") or "", len(current), len(records))
from `commit()` reddened NOTHING. `TestTheInvariantItself` unit-tests the rule;
it says nothing about whether `commit()` asks it.
Why it was hard to reach, stated rather than worked around: `commit()` builds
`records` FROM `current` by removing at most one record and appending at most
one, so the only branch that shortens `tasks.jsonl` is `purge` — which is in
`SHRINK_ALLOWED`. The one other input that shortens it is a store carrying the
subject's id twice, and `load_task_records` refuses a duplicate id before
`commit()` sees it.
`TestTheTaskStoreCallSiteIsWired.test_commit_asks_the_invariant_about_tasks_jsonl`
constructs that state deliberately, replacing `load_task_records` for the
duration of one `commit(..., dry_run=True)` call. It proves the CALL SITE is
wired. It does NOT claim the state is reachable through the CLI, and the class
docstring says so in those words — the RESULT repeats it as a finding rather
than a footnote.
`--dry-run` is used deliberately: a build with the call site deleted then writes
nothing while still going red.
`test_register_store_invariant`: 39 tests, green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
evidence/2026-08/TASK-095-round6-v4-review.md, 583 lines, reviewed
worktree clean and every mutation restored md5-verified.
The reviewer attacked the load-bearing claim first, which is what it was
asked to do, and ruled the M11 equivalence argument CORRECT by reading
the control flow rather than accepting it: both production callers derive
tracks/source from the same declared_tracks_detail call, and stored_tracks
reaches TRACKS_STORE_DEFAULT on exactly one return, so after the gate
`have` provably equals round 4's literal. Round 4's literal IS
behaviourally intact there — and that is what USER-905 decision 2 asked
for, because it sits on the REFUSAL, not on the drift rule. The drift rule
is now perry_md_store.plan, the same call perry-lint makes. The three
other green mutations are genuine equivalents; M22, the one that was a
crash path, is now 1 ERROR.
Verified with the reviewer's OWN fixtures rather than the author's
helpers: two stores one verdict, reproducing opposite responses at base
and agreement across all five tools at head; parse_tracks( at 2 lines
where base has 3; W1/W2/W3 going exit=1 to exit=0 with W3's remedy still
failing on both trees; the remedy pin proved real by simulating the fix
and watching exactly that test go red; M5 killing the tautology; state 7
now contradicted with MODE-02 where base is silent; the zh path identical
at every state; all 27 anchors matching their claimed text with 17
mutations re-run and every count and RED name matching.
The reviewer also ran the runner the author declined to — unittest
discover, 2882/6 vs 2902/6, identical sets, the three extra being the
predicted test_risks_store artifact. That is the claim 68e63cf retracted
on TASK-050, measured here.
The author's own judgement call — a stderr drift warning the amendment
did not ask for — was ruled IN SCOPE and correct, and measured rather
than reasoned: three previously-clean workflows byte-identical between
the trees, no exit code going 0 to 1 anywhere.
ONE NON-BLOCKING FINDING, sent back rather than waived: a guard this
round ADDED survives its own deletion. bin/perry-state:1022's
startswith("track/") filter can be removed with all 56 tests green, and
without it perry-task says "the track register disagrees" about a
hand-edited SETTING cell. The shipped code is correct; nothing fails when
the line goes. Under the amendment this round was graded against, a guard
that can be deleted with the suite unchanged does not count — and that
rule does not stop applying because the round passed. One named test on
the user-facing message, then merge.
Filed from the review: a hand-REORDERED Tracks table is config-store-drift
to perry-lint and silent to every other tool — defensible under principle
A as written, but neither documented nor guarded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The agent stopped with SIXTY files staged and zero commits — 1,082 insertions, 799 deletions, including the deletions of perry/DECISIONS.md, decide/state/DECISIONS_TEMPLATE.md and three fixture copies, plus an untracked RESULT. A worktree is not a deliverable; resumed with commit-first instructions before anything else, including before finishing its suite run. Checked before resuming, because TASK-203 had a mutation left applied an hour earlier: the one unstaged edit here is the notes/decision-log.md fixture move, which is real work in progress and not a live mutation. WHAT IT FOUND, and it is bigger than the row: perry-decide REISSUES a retired ADR id and perry-task does not. Delete ADR-011's file and the next mint hands out 011 again. perry-task purge retires an id through the append-only event log; perry-decide writes NO events at all, so it has nothing to retire one with. Before this row the behaviour was NON-DETERMINISTIC — on main an unrelated write re-rendered the index and the next mint reissued anyway. That closes TASK-214 by proving its defect was LARGER than filed, which is a better close than meeting its own description. USER-909 filed, because an ADR id is an address: ADR-007 is cited by name in ADR-010, in DESIGN-013, in three task rows and in this project's own commit messages. If 011 can name two decisions, every one of those citations goes ambiguous the moment an ADR is deleted. It did not matter while nobody deleted ADRs — TASK-235 is the row that made deletion ordinary. Recommendation is (b) then (a): stop the deletion that creates the problem, then give perry-decide the event surface so the two tools agree on principle rather than by accident. The agent pinned the disagreement with a named test instead of resolving it silently, which is why the row can still close while the question is open. Merge note recorded for later: viewer/parsers.py had to change here — otherwise decisions.count goes to 0 forever — and that file is being edited concurrently on coding/task-050-header-index, which converted 16 header sites in it. The agent was asked for hunks precise enough to merge against those. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…face DESIGN-013 § 5.3, User Decision 3 answered 2026-08-29: delete it. It was twelve rows of pure projection, 76% of its bytes inside table cells, whose own third line told the reader it was generated and not to hand-edit it. Deleted: `perry/DECISIONS.md`, `decide/state/DECISIONS_TEMPLATE.md`, its `claims[]` entry and `files[id=decisions]` shape in `schema/state-schema.json`, its `.perry/conformance.md` declaration row, and four fixture indexes. No replacement index, under any name — DESIGN-013 § 4.1 records the web link surface as GIVEN UP and forbids re-adding one, and `tests/test_decide_writer.py § TestNothingWritesAnIndex` asserts the complete set of files each write command may leave behind rather than one filename, so an index called `ADRS.md` fails it too. `mint_id` reads the ADR files alone — TASK-214, closed here rather than beside this. The union `max(files ∪ index)` had a one-command half-life: measured on main at ee0b36a, deleting an ADR file and running an UNRELATED `status` flip re-rendered the index and the next `new` reissued the number. Reissue was non-deterministic. It is now deterministic and it does reissue, which DISAGREES with `perry-task purge`'s rule that the log retires an id forever. `perry-decide` writes no events at all, so it has no log to consult; the disagreement is declared in `mint_id`'s docstring and pinned by a named test, not resolved. `perry-decide/list` goes to 2.0 — three `conformance` keys removed, each of which compared the deleted file against `decisions/`. Removing a key is the break that contract page names. The two shape baselines were spliced for that one contract only, never re-recorded wholesale, and `test_the_shipped_version_is_recorded_in_its_own_changelog` is new: a re-record cannot fabricate a Changelog row. `viewer/parsers.py` had to change and is on another branch's list; the RESULT names the hunks. Without it `perry-state`'s `decisions.count` would be zero on every project forever, which is verbatim the defect `perry-decide` was built to end. Nine mutations, each anchored by line number with the old text asserted, `__pycache__` cleared, past the whole-second boundary, restored under md5 — all red with named tests; three go red alone. Evidence, findings and the gaps I did not close: `perry/evidence/2026-08/TASK-235-result.md`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s named Coordinator's ask, addressed in the document rather than in a summary: 1. § 2 — `perry-decide` reissues a deleted ADR id and `perry-task purge` does not. Stated as a disagreement between two tools over one contract, and as DECLARED rather than fixed: an ADR id is an address, so a reissue means two decisions can share one, and that call is not this row's to make. 2. § 3 — TASK-214 closed, with the reproduction showing reissue on main was NON-DETERMINISTIC rather than merely self-erasing. 3. § 5 — `viewer/parsers.py`: why it was mandatory, the three hunks by @@ header, and the one place it can collide with coding/task-050-header-index (both deleted header sites are inside the replaced section — take the deletion; the new reader has zero). 4. § 6.1 — the three mutations that go red ALONE, and TestNothingWritesAnIndex.test_status_writes_no_index named as the guard that catches an index re-added as ADRS.md, which DESIGN-013 § 4.1 forbids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…test
The V4 review PASSED and named one non-blocking gap: a guard this round ADDED
survived its own deletion. `bin/perry-state:1022`'s `startswith("track/")`
filter could be removed with all 56 tests green, and the line was missing from
the RESULT's guard table.
`cells_the_store_and_the_file_disagree_on` is not filtered by record kind, so
that line is the only thing keeping a hand-edited SETTING out of an answer
about the track register. Reproduced before fixing, on a table whose track row
agrees with the store plus one edited setting: without the filter `perry-task`
prints "the track register disagrees with `.perry/config.md § Tracks` on
document_language" — a sentence about the track register, naming a setting,
pointing at a section that does not contain it.
`test_a_hand_edited_SETTING_is_not_reported_as_a_track` asserts on that
sentence rather than on the predicate's return value, and carries
`perry-lint --json` as its own control so it cannot pass on a clean project.
M23: removing the filter is 1 RED, and it is that test.
Auditing the whole guard table rather than only the named line added M24-M27.
M26 and M27 are red and are now in the table; M24 (unreachable behind the
TRACKS_ANSWERED gate, kept for the TOCTOU window) and M25 (masked by M23's
filter two lines below) are green, equivalent, and written down as such.
33 mutations total, 33 restored OK, 0 anchor misses. The shared scratchpad's
`mutate.py` was overwritten mid-session by another agent's harness pointing at
a different tree, so M23-M27 ran under a privately named copy; the 28 earlier
mutations ran before that and their record stands.
No `bin/` file changed. Baseline `bash tests/run`, board as committed at
6c0d041: 98 modules / 2903 tests / 3 failures, sorted FAIL/ERROR lines
identical to the clean 6c0d041 copy's 98 / 2882 / 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…FAILs
TASK-095 round 6, V4 PASS 2026-08-29. The review is
perry/evidence/2026-08/TASK-095-round6-v4-review.md, 583 lines, filed on
main rather than carried in this diff — the reviewer's own worktree was a
detached copy and its verdict is not the branch's to claim.
The root cause across three of the five failed rounds was re-deriving the
drift rule on the write side, differently each time. This round stops
deriving it: `tracks_missing_from_the_register` is gone, and
`tracks_the_register_contradicts` calls `perry_md_store.plan` — the exact
comparison `perry-lint § check_md_store_drift` makes. `grep parse_tracks(`
is two lines where round 5 had three.
USER-905's three decisions, each verified by the reviewer with its own
fixtures rather than the author's helpers:
1. Principle A applied ONCE. One table against two stores differing only
in a contradicting record gave OPPOSITE responses at 6c0d041; here
perry-lint, perry-state, perry-task, perry-goals and perry-diagnose
all agree on both.
2. The refusal reverted to store-default. W1/W2/W3 go exit=1 to exit=0
with a stderr warning, and W3's named remedy still exits 1 on both
trees — pinned by a test the reviewer proved REAL by simulating the
fix and watching exactly that test go red.
3. The perry-goals guard is no longer a tautology: deleting it is 1 RED.
The reviewer ruled the author's self-reported M11 equivalence CORRECT by
reading the control flow: round 4's literal IS behaviourally intact there,
and that is what decision 2 asked for, because it sits on the refusal and
not on the drift rule. It also ruled the author's unasked-for stderr drift
warning in scope and correct, measured rather than argued — three
previously-clean workflows byte-identical, no exit code going 0 to 1.
The review's one non-blocking finding was sent back rather than waived: a
guard this round ADDED survived its own deletion. 037cc44 closes it with
`test_a_hand_edited_SETTING_is_not_reported_as_a_track`, which asserts on
the user-facing message rather than the predicate — the harm was
perry-task saying "the track register disagrees" about a hand-edited
SETTING cell — and carries `perry-lint --json` as its own control so it
cannot pass on a clean project. The author then audited the whole guard
table unasked and found two more untested guards that are real (4 RED and
15 RED) and two that are green, written into the RESULT with reasons
rather than counted. 33 mutations, 33 restored, 0 anchor misses.
Baselines. main's code is byte-identical to 6c0d041 — every commit since
touched only PMO records — so the branch's measurement is the merge's:
`bash tests/run` 98 modules / 2903 tests / 3 failures, sorted FAIL/ERROR
lines identical to 6c0d041's 98 / 2882 / 3. The reviewer independently ran
`unittest discover` too, which the author had declined: 2882/6 vs 2902/6,
identical sets, the three extra being the predicted test_risks_store
double-import artifact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trimming SKILL.md back under its 20,480-byte cap rewrote the hand-off sentence as "the decision record (`decisions/`) moved from `work` to `decide`, and `OKR.md § Commitments` became explicitly `goals`" — which put a write verb inside `test_procedures_call_the_tool`'s 60-character window before the `OKR.md § Commitments` target and made SKILL.md:75 an R1 finding. The guard was right; the sentence was mine. Restored to the paired-path form main used and this branch's own history needs anyway: "`decisions/` + its then-index moved from `work` to `decide`". 20,439 bytes, 41 under the cap. Candidate wordings were run through `test_procedures_call_the_tool.scan` directly before picking one, rather than reworded until the suite went quiet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`perry/evidence/2026-08/TASK-203-round4-result.md` — the invariant and where it lives, the four doors with the named test that proves each, the twelve mutations with their exact anchors, the baselines with the runner, the tree AND the machine load, and the things I did not do. Two findings are stated as findings rather than footnotes, because the coordinator asked for both and both deserve it: 1. **The `tasks.jsonl` call site.** M2 (empty the allowlist) reddens 21 tests, 14 in `test_purge` — the call site IS reached by an ordinary command. M3 (delete it) reddened NOTHING before `70dfa96` and reddens exactly one test after it. What was unreachable was never the call, only its refusal branch, and the RESULT says so, says why (`commit()` builds `records` from `current`; `load_task_records` refuses a duplicate id first), and says the new test proves wiring rather than reachability. 2. **`resolve-intake` does not reduce any count.** It edits an `Outcome` cell. The RESULT's answer to "whose mistake" is *neither*: USER-906's list is a permission list, not a prediction, and discharge and removal are two commands in today's implementation. `SHRINK_ALLOWED` was NOT adjusted to match the finding, and the RESULT says that too. Also recorded: the earlier six "successful write" failures were not load flakes — they are M10's red set, produced by two instances of my own mutation harness running against one worktree. The harness now refuses to start on a dirty tree or beside a sibling instance; both refusals fired in practice. Twelve mutations, twelve reddened a named test, none green, all twelve `[restored, md5 ok]`. bash tests/run at the tip: 99 modules / 2921 tests / 3 failures — the same pre-existing set as `main` at 6c0d041 (98 / 2882 / 3), measured at load 34–48. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bash tests/run` on the pre-fix tree came back 98 modules / 2892 tests / 4 failures across 3 modules — the three that were already red at ee0b36a, plus test_no_procedure_hand_edits_a_tool_owned_file, which was mine and is fixed by b57a34a. § 8.1 records that rather than quoting the green number from before it. What is stated as MEASURED: the 19 modules this change touches, 683 tests, all green on the committed tree. What is stated as EXPECTATION: the clean full run on b57a34a, which had not finished under load average 32-48 with several agents' suites in flight. Named as a gap in § 9 rather than reported as a number. Also § 7 G: main moved to 7f934d5 while this branch was open. The two files both sides touch — bin/perry-diagnose and bin/perry-goals — have no overlapping hunks. This branch is 61 files, +1505/-800 against its fork point, which is the number to use; git diff main..HEAD now reads 72 files and counts other people's work as deletions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CORRECTION. I recorded in 2ac4de7 that this agent was "killed mid-mutation-run under load average 34" and that the left-applied mutation was a consequence of my seven concurrent dispatches. The agent's own diagnosis is better evidenced and it is different: the dirty tree was TWO INSTANCES OF ITS OWN HARNESS running against one worktree, each taking the other's mutation as its `original`. Restored and md5-verified against the committed blob; the harness now refuses to start on a dirty tree or beside a sibling instance, and both refusals fired in practice. That correction earns its keep: the same collision also explains six "successful write" failures the agent had earlier called possible load flakes. They were neither flakes nor load — they are M10's red set, attributed to M1/M2/M3/M5 in turn by the collision. A wrong cause had already produced a wrong reading of the evidence, which is the thing this project grades hardest. The load was real and it did slow the run. It was not what left the mutation applied, and saying so is the difference between a measured cause and a plausible one. Branch coding/task-203-round4, tip afb3a48, five commits from 6c0d041. The invariant is refuse_to_shrink — one function, two call sites, asking nothing about the command, the identity or the board, which is why option A was unnecessary. Twelve mutations, twelve reddened a named test, none green. M6 is the one that matters most: uniqueness weakened to consecutive-only now reddens a named test, and round 3 measured that exact weakening as GREEN across 2815 tests. Three gaps the author declared rather than buried, and the reviewer is told to RULE on each rather than note them: the tasks.jsonl call site proves wiring and not reachability, and he states plainly that a reviewer who holds a monkeypatch-only call site should not ship has a fair case for deleting those two lines; resolve-intake reduces no count at all, and he did NOT adjust SHRINK_ALLOWED to match — the test asserts the allowance is unused rather than pretending it fires; and nobody has measured how often a real board sits in the drifted state that now refuses the NEXT write, which is the mistake TASK-095 round 5 made in the other direction. Merges into main with one conflicting region now that TASK-095 has landed — both touch bin/perry-task. That is resolved at merge, not here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Listed `I did not verify the viewer's HTML rendering of ADRs` as unverified, then checked: viewer/ is parsers.py and tables.py, no template. A gap I cannot close is worth naming; one I never looked at is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g more than it buys
MEASURED, not felt. Load average went 25 → 32 → 37 → 52 → 59 across this
session's dispatch batches, and past roughly four concurrent agents the
throughput went DOWN while the evidence got worse:
- TASK-203's agent ran two instances of its own harness against one
worktree and each took the other's mutation as its `original`. Six
failures were misattributed across four mutations before it caught it.
- TASK-095's agent had the shared scratchpad's `mutate.py` OVERWRITTEN
mid-run at 14:56 by another agent's harness of the same name.
- TASK-235 could not finish a clean full `tests/run` at all and had to
ship a named gap, marking 2892/3 as an expectation rather than a
measurement.
- My own post-merge verification of the TASK-095 merge was starved to
six lines of output and I killed it rather than let it compete with
five agents doing real work. It will be re-run when the machine is
quiet; the merge itself was already measured independently twice, by
the author and by the reviewer.
That is three separate corrupted or incomplete evidence runs, on a project
whose entire verification model is "a claim about verification must itself
be verified". The dispatch cap I raised to 8 was the wrong number, and
TASK-230 — the row that exists because two dispatches died on this exact
cost on 2026-08-28 — is one of the agents currently being starved by it.
So TASK-235's V4 review is NOT dispatched, deliberately. Its Next action
says to dispatch it when load falls below ~15. Nothing is lost by waiting;
something is lost by reviewing under conditions that have already
corrupted three runs tonight.
TASK-235 itself is complete and committed: six commits, 61 files, tree
clean. Its own full run caught a defect of the author's own making — a
byte-cap trim to SKILL.md put a write verb inside a hand-edit guard's
60-character window — and the fix was found by running candidate wordings
through the scanner rather than rewording until the suite went quiet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ified
**This commit was made by the PMO, not by the agent that wrote the code.**
The agent was terminated by a session rate limit with 21 files modified,
3 files new and 526 insertions sitting uncommitted in its worktree after
101 minutes of work. Committing it preserves the work; it does not
endorse it.
What is NOT true of this commit, and must not be assumed by a reader or a
reviewer:
- No test run was verified by the PMO. The agent's own last words were
"drafting the result document while the suite runs", so its final
suite may never have completed.
- No mutation was checked. Nothing here has been shown to redden a
named test.
- The RESULT file (219 lines) is the AGENT's account, written by the
agent, and reads complete — it carries a "What I did NOT do, and what
I could not verify" section and merge guidance — but the PMO has
verified none of its claims.
What the work appears to be, from the diff alone: the KR table is removed
from the phase documents and the linkage YAML becomes the single
declaration, which is option (b) — the option the agent was redirected to
mid-run after DESIGN-013's User Decision 1 was answered "adopt as
stated". New test `tests/test_phase_kr_declared_once.py`. Touches
bin/perry-goals, bin/perry-lint, bin/perry-state, viewer/parsers.py, the
five phase files, two fixture projects, and the phase template.
The row stays in progress. It needs its suite run and its mutations
before it can go to review, and whoever picks it up should treat this
commit as a restore point rather than a delivery.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nation **Committed by the PMO, not by the agent, and it is WORK IN PROGRESS.** The agent was terminated by a session rate limit. There is no RESULT file, no mutation record, and no verified baseline — this is a restore point so that 70 minutes of work is not lost to a 429. What is here: `tests/parallel` rewritten (+160/-25), a new `tests/durations.json`, and a new `tests/test_parallel_runner.py`. What is NOT here, and matters more than usual for this row: the row's own acceptance criteria require that every reduction in wall-clock be SHOWN not to reduce coverage, with at least three sped-up tests each proved still red when its subject is reverted. None of that has been done or checked. A faster suite that is quietly less thorough is the failure mode this row was written to avoid, and nothing here rules it out yet. The agent's last observation is worth keeping and is the reason the row exists: the same suite took 264s, 354s and 726s within one hour on this machine, under load driven by the PMO's own concurrent dispatches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All six terminated together on a 429 at ~15:25, resetting 19:00
Asia/Shanghai. Triaged by measuring each worktree rather than by reading
the notifications.
TWO WORKTREES HELD UNCOMMITTED WORK and would have lost it:
TASK-157 21 files modified, 3 new, 526 insertions, ZERO commits, after
101 minutes. Committed by the PMO as f15d234 on its branch.
TASK-230 tests/parallel rewritten +160/-25 plus two new files, ZERO
commits, after 70 minutes. Committed as 23e6197.
Both commit messages say plainly what they are: restore points made by
the PMO, not deliveries by the agents that wrote them. Neither has a
verified suite run, neither has a checked mutation, and TASK-157's
219-line RESULT is the agent's own account with none of its claims
confirmed. A reader or a reviewer must not take either as evidence.
TASK-226 SOLVED IT, and the answer is that there was no defect. The
phantom row in .perry/conformance.md was written by writer #1 — the
documented one — run BY THE USER in their own terminal, 52 seconds after
the status line printed that exact command and 2 seconds before their next
prompt to the agent. ~/.zsh_history line 3763, epoch 1787912711 =
2026-08-28T10:25:11Z. ADR-004's contract was never violated.
What failed was the INFERENCE: the session read "no perry-conform declare
was run" off its own transcript, and its own transcript is not the
machine. Filed as intake, because every "nobody did X" claim this project
makes carries the same blind spot, and the machine's own record — shell
history, mtimes, the event log — is the check that was never run.
TASK-050 and TASK-203 both DELIVERED and both reviewers died before
reading anything; their briefs stand and their review worktrees are still
detached at the right commits. TASK-235 delivered and its review was
already being held on load.
No dispatch is possible until 19:00. The verification suite for the
TASK-095 merge is re-running now that load has fallen from 59 to 19.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hoke point
The round-4 V4 FAIL: `_root_flag` built ` --root {root_arg}` unquoted, so a
project at `.../My Project` was handed a command that exits 1 with a usage
error about a file argument the reader never typed. `shlex.quote` appeared
nowhere in bin/ or viewer/.
`_q()` is the choke point — every argument of every command either tool hands
back goes through it. Four `{v.path}` sites went through it too; a reader who
runs `perry-conform check 'My Notes.md'` was handed the same broken line.
The DRIFTED branch is a second member found by the same rule: it appended the
unreadable-lines parenthetical to the command line itself, so the last line the
reader copies is `syntax error near unexpected token '('`, rc=2. Measured.
The three members `§ 10.9` excused now carry the root: `Plan` holds the root
the caller TYPED, `plan_project` requires it with no default, and `apply_plan`
and `render` read it off the plan rather than taking a parameter a caller can
decline to fill.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row's own end-to-end proof ran shlex.split on the handed-back command -- the exact parser that exposes the round-4 FAIL -- and stayed green, because tempfile.TemporaryDirectory() never yields a path with a space. Every fixture project in both modules now lives under a directory named with the nine characters that change how a shell reads a line. 19 tests went red on that change alone. The extractor and the assertion move to tests/handed_back.py so there is one of them: test_migrate held a second, hand-written spelling, and a substring test cannot tell a runnable command from one that parses as five arguments. The extractor also required four spaces of indentation where the source sweep required two, so do_restore's listing was a handed-back command to one rule and invisible to the other. test_the_declare_command_the_refusal_names_is_runnable_verbatim was not running it verbatim: it split on whitespace and appended its own --root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ations Section 9. The blocker bullet with the hole's real scope re-derived (a write inside an already-existing ignored directory, and the name match at any depth, both measured with controls) and pinned by a test. The pin's claim narrowed to what it reads and widened where a string search can reach. The IndexError and a second crash on the same path. The case-differing spellings the round-2 fix missed and the relative paths it newly accepted, decided by inode identity and an explicit refusal. The 24/18 out of the docstring. Twenty mutations: five reproducing round 3's attacks on the unfixed tip, and fifteen against the fixes. Four green, all four reported and three of them explained as structural. Three of my own fixes were green under their first mutation and are tightened in the commits above; one harness bug — a restore that kept the first of two edits to one file — is recorded with the diff that caught it. Four full suites measured this session: main at 1cbc025 and at 4d21513 (it moved again mid-round), the branch tip df8d536, and the merge probe 52e6089. 4 failures across 3 red modules on every one, the same four by name, counted as the sum of the per-module FAILED (failures=N) lines. 3124 / 3122 / 3148 tests, and the arithmetic closes to the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 4 asked only whether the phrase carried the root. The round-4 FAIL
carried it and spelled it `--root /Users/ada/My Project`. So every {...}
inside a handed-back command must now be a spelling that is shell-safe by
construction, and FLAG_VALUE reads a long flag's value in any template --
which is the only rule that can reach the choke point, since `_root_flag`'s
own body names no tool.
IS_WHOLLY_A_COMMAND closes four of the round-4 reviewer's five misses: all
four were the command reaching the message through a name, and a literal that
is nothing but a command is one. Provenance values (writer="perry-conform
declare") are excluded as named values, not by a special case.
tests/fixtures/handed_back_spellings.py plants 19 defects and 4 correct
rulings, one per spelling, so recall is recomputed by the suite instead of
quoted from a review: 18/19, and 14/15 on the reviewer's own set where round 4
scored 10/15. That fixture is also R-N8's missing positive control -- the
sweep's ok/bad decision now has one.
The suite guard sweeps bin/perry-migrate too; both tools are at zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tip carrying all of section 9 reads 104 modules, 3122 tests, 4 failures across 3 red modules, the same four by name, md5 identical at both ends — the same as df8d536 before the document existed. This commit's only delta is that table row and the paragraph that reads it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-N3 and R-N4: apply_plan's write-failed and digest-mismatch rollback paths could drop the caller's root with both modules green. The digest-mismatch path had no test at all; the write-failed one asserted only that a command was named. Both now assert the parsed root, from a plan built with a real one. R-N13: no test applied a migration to a project holding a legacy record and then restored it, so the update_expected_after call this branch added on the recovery path was unpinned. That round trip is a test now. The end-to-end proof runs the named command through /bin/sh, not only through shlex.split -- globbing and $ expansion are things a shell does and a splitter does not, and the fixture root ends in '*'. The one residual is pinned and named: a backtick in the root is quoted correctly, and truncates the two INLINE backticked commands in the same message. The test goes red when that is closed. Two pre-existing invalid escape sequences in docstrings became visible when the suite guard started parsing bin/perry-migrate; both docstrings are raw now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mutations R-N11 and R-N12 were green because no caller omits the argument today: a shape that protects a future caller cannot be held by a test that exercises present ones. It can be asserted directly, and now is -- for both tools, including that apply_plan and render have NO root of their own, so there is one root per plan and no second place to disagree with it. 56 mutations: M41-M44 for the quoting choke point and the three layers that see it, M45-M48 for the sweep's own rulings (R-N8's missing control), M49-M51 for R-N3/R-N4/R-N13, M52-M53 for the two members section 10.9 excused, M54-M56 for the shape and for the extractor/sweep agreement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The V4 round-4 reviewer put nine plausible overclaims to the CRLF guard: it caught 3 and evaded 5. It catches 9 now. The looser widening -- any short run of characters between the phrase and its object -- also catches 9 and fires on two correct sentences, one of them the correcting comment itself, so the object has to follow through a connector from a closed list. Both measured, and the residual false-positive shape is named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… survive Section 1.3 is the FAIL and the shape chosen for it: one choke point plus a source rule that makes the bypass spelling red, because a choke point alone is a convention and a convention is enforced by whoever remembers it. Section 6.1's three-layer argument is rewritten to the measurement: M34 is not invisible to the helper, M40 is byte-for-byte M30, and no mutation of that line is caught by exactly one layer. What IS measured is that M44, M52 and M53 are red on the source guard and nothing else -- three real defects in messages no fixture reaches. Section 10.9's 'no root in scope' was untrue for two of three, and the harm was understated in the direction that matters: the copied command rewrites a different project's board. All three carry the root now. The census is restated as a lower bound wherever it is quoted, with the recall that bounds it measured by the suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ds both ways Scope was the delta 03493d6..f8bc100 only. The blocker is closed by a test rather than a promise: deleting the bullet is red, and adding a fifth ignored directory with the equality pin moved with it is red too. Three ways to satisfy that test without a truthful bullet are recorded — it checks a substring of the section, not a bullet. Both tests/run changes swept, 18 spellings plus 13 more. Relative values are refused; -ef accepts exactly the casings the filesystem folds. The skip path is now EXERCISED, on a case-sensitive APFS image, not reasoned about. MC1 kills exactly one test and it is the new one. All three df8d536 fixes closed, each with a paired revert. The four open green mutations reproduce and each is open for a true reason. f8bc100 verified against git ls-tree/hash-object — no restore residue. main 4716e39, tip f8bc100 and merge probe 4f93630 all read 4 failures across 3 red modules, the same four by name. 3124 - 26 + 24 = 3122; 3124 + 24 = 3148. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bash tests/run` wrote four Perry files into its own repo. Idempotent, so a
second run looked clean, which is why it went unnoticed and why four agents
confirmed it independently before anyone could see it.
Round 2 PASSED the row and blocked the merge on three defects; round 3 closed
them and blocked on one bullet; round 4 closed that and the confirmation
passes it. What ships:
* `tests/run` REFUSES to start when `PERRY_PROJECT` names anything but this
root. The alternative — exporting it — was tried and rejected because it
reddens nine tests (7 failures + 2 errors), and a guard that bends the
suite to fit will be bent back. Root comparison is `test -ef` on device
and inode, so a symlink alias, a trailing slash and a case-folded spelling
are accepted while a foreign root is refused; relative paths are refused
deliberately, with their own banner.
* All three ignore lists pinned by equality AND by consequence — a planted
change in each of the four files, which for `IGNORE_NAMES` is the only
thing that kills round 1's defeat.
* The `IGNORE_DIRS` blind spot is written into the "What it does NOT catch"
list with its real scope: the match is on the name at ANY DEPTH, and a
file written inside an already-present ignored directory is invisible.
Pinned by a test that is also red when a fifth ignored name is added with
the equality pin moved with it.
* "eleven executables" is gone rather than corrected; the set is derived.
Confirmation review at `review/task-249-round4`: main 104/3124/4, tip
104/3122/4, merge probe 105/3148/4 — same four pre-existing failures by name,
none in a file this branch touches, arithmetic closing to the test. It
verified the tip file-by-file with `git hash-object` against `git ls-tree`
rather than against any digest the row produced, and it exercised the
case-sensitivity skip path the row had left reasoned-but-unexercised.
Three fix-or-file items remain, none blocking; they are filed as TASK-257.
…e guard to BOARD.md
…ge probe Counted as the sum of per-module FAILED (failures=N), with errors summed separately and grep -c '^FAIL:' shown as the trap it is. 4 / 4 / 5, the fifth being test_host_support, which re-ran OK three times on the probe tree and is reported rather than netted out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the split 63 handed-back commands across the other twelve bin/perry-* tools, 25 without the caller's root -- six more places handing back 'perry-tasks render --write', which writes. The 19 raw-interpolation findings over-report there and the split is stated: 3 inside genuine command phrases, 16 FLAG_VALUE reading a long flag in prose. Not fixed; written down with the command that produces it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es them main 4, tip 4, merge probe 4 -- 0 errors in all three, same red set by name, no new red from the merge. The earlier probe run that read 5 is recorded with its fifth failure named as the known test_host_support intermittent, which re-ran OK three times and is absent from all three runs in the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main went 4716e39 -> 5b69572 while the first three runs were finishing, and the move brings TASK-249's tree guard with it -- new machinery watching the suite for writes to the working tree, which a merge probe against the older base would not have exercised. Both bases measured, both merges clean, both probes 4 failures, same three red modules by name in all five runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight refusal surfaces driven on planted hostile-root projects, every command extracted by the shipped extractor and pasted into a real /bin/sh: all parse, all name the reader's own project, all do what the sentence says. Newline and backtick both measured. 57/57 of the row's harness reproduced independently with the tree digest bracketed, plus 16 reviewer mutations — R5-16 shows the source rule still catches round 4's defect with every runtime layer disarmed, and R5-15 is the shell-layer-only mutation the RESULT said it could not build. Corrections, none blocking: § 10.14's headline census is over fourteen tools, not twelve (42/208, not 63/232); the backtick residual is eight sites, not two; the hostile fixture root has no assertion on it; one sweep survivor (R5-11). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rounds
Rounds 3 and 4 failed on the SAME standard in the SAME sentence, one register
deeper each time — `bin/perry-conform:360`, "a wall — every branch here ends
in a command the reader can run". Round 3: the handed-back command dropped the
root. Round 4: the command with the root would not run through a shell.
Round 5 went looking for the third layer with eight refusal surfaces driven on
planted hostile-root projects, every command extracted by the SHIPPED
extractor and pasted into a real /bin/sh. It is not there.
What ships is a choke point PLUS a source rule, on the row's own argument that
a choke point alone is a convention — `_root_flag` was already one, and that
is exactly why it failed. The sweep flags any `{...}` in a handed-back command
that is not a sanctioned quoting spelling, and FLAG_VALUE reads a long flag's
value in ANY template, which is the only rule that can reach `_root_flag`'s
own body.
Two measurements make that more than a story:
* R5-16 — friendly fixture root plus round 4's defect put back drops 24 red
methods to 2, and both survivors are the source rule and the backtick
test. The argument is now an experiment.
* R5-15 — `_q` double-quoting with escapes leaves shlex.split reading the
right root, so all 16 helper invocations and the source guard stay green,
while /bin/sh expands `$x` and the end-to-end proof goes red. That is the
shell-layer-only mutation the RESULT said it could not construct, so the
/bin/sh paste is load-bearing rather than decorative.
Also in: every fixture root carries nine shell-hostile characters (19 tests
went red on that change alone); the helper parses instead of substring-matching
and moved to tests/handed_back.py, which surfaced that a test had been
asserting over an empty extraction; sweep recall 10/15 → 14/15 on the
reviewer's own set; apply_plan and render no longer take a root at all.
57/57 of the row's mutations reproduced independently, plus 16 of the
reviewer's. Suite: main 105/3148/4, tip 103/3150/4, probe 105/3200/4, zero
errors throughout. Five non-blocking corrections are filed as TASK-259.
The bill was measured before anything was written. Across 25 sessions of this
project's own transcripts and 18,941 turns: 8.43 BILLION tokens, of which
99.1% is `cache_read` — the accumulated context, re-read on every single turn.
Output was 0.3%. So cost is not what a session loads, it is
Σ over turns ( context size at that turn )
and both factors grow together inside one run, which makes a five-hour session
superlinear rather than five times a one-hour one. The largest session held a
mean context of 504,651 tokens across 8,174 turns and touched 997,717.
That reordered the three suspects. Verbose CLI output was the smallest of
them and the sign was backwards: Bash results averaged 202 tokens a call while
the commands INVOKING them averaged 353, and `tool_use` input was 52% of
everything accumulated against 26% for all results. Always-loaded skill docs
were real but capped — the whole fixed baseline is ~62k, worth 14.6% of spend.
Review rounds were the biggest lever, and replaying the measured turns against
a context cap put a second one beside it: 200k costs 58.3% less for the same
work, 300k 42.3% less.
1 · `perry-lint --reviews` gains `review-rounds-exhausted`
20 rows on this board entered V4 and 74 rounds were burned. Ten needed three
or more; TASK-050 and TASK-249 each reached round 11. TASK-095 FAILed five
times and all five read the same in the journal — "two situations answered as
one, one step to the left of the last". The escalation that ended it was filed
BY HAND at round 5, after which the user picked a principle and round 6 PASSed
first try. The rounds after the second were not finding new defects; they were
re-deriving one undecided principle differently, at a dispatch plus a review
plus a fix cycle each.
No new field: `round` was measured and refused a bearer once already
(`perry-task.evidence_relations` — it lives only in some filenames), and it
does not need one. A round that returned is a verdict block, so the count is
the FAILs already on disk, and an open ask naming the row in `blocks` is the
escalation that clears it.
Scoped to LIVE rows after the first cut reported five and four were long
closed. `done` removes the row, so a row absent from the board can receive no
next round and this check has nothing to say about it. That those four closed
carrying FAILs and no PASS is real and separate — `v4-close-without-verdict`
territory, not widened into here.
2 · `bin/perry-context-budget`, and `autopilot` stops on it
Reads the host's own accounting from the session transcript rather than
estimating, seeks from the end because a 41 MB transcript was measured here,
exits 1 at the ceiling, and `--composition` reports what the context is made
of. `autopilot` runs it as a stop check and writes a handoff before exiting —
crossing the ceiling loses nothing, which is what `handoff/` has always been
for.
It abstains LOUDLY. On a host with no transcript the verdict is `unknown` and
the exit status is 0, because a gate that answers "fine" about a measurement
it never made is worse than no gate.
3 · One line of shell discipline in AGENTS.md
The always-loaded file has a hard 60-line budget and sat at 59, so the rule is
one line and its reasoning lives in the tool. Paid for before it was added:
+119 baseline tokens costs ~1.0M over the largest session and saves ~791M,
because 1,161 `cd <repo> &&` preambles alone put 379k tokens into one session
and every one of them is re-read on every turn that follows.
Configurable, three registers, most specific wins
Both numbers follow the precedence `perry-conform § gate_mode` established for
`Conformance gate`: env beats the project's declared field beats the shipped
default in `schema § thresholds`. `PERRY_REVIEW_ROUNDS` /
`- Review rounds before escalation:`; `PERRY_CONTEXT_CEILING` /
`- Session context ceiling:`, with `--ceiling` above both. Every consumer
reports WHICH register answered, and names `.perry/config.jsonl` apart from
`.perry/config.md` — reporting a store value as though the markdown set it
sends the reader to edit a projection. A store that lacks the key is an
answer, not a reason to read the markdown.
A limit below 1 is refused rather than clamped, on all three branches. The
comparison is `len(fails) < limit`, so a declared 0 does not tighten the gate,
it INVERTS it — the finding would fire on every live row carrying any verdict
block, including rows with zero FAILs. Found by reading the diff. A second
guard was written at the comparison and then DELETED: it was unreachable while
the resolver holds, and two implementations of one rule is the defect this
repository finds most often. The invariant is pinned by a test instead.
Measurements
Both runners, and the second one is reported because naming only `tests/run`
is the omission two reviewers have flagged on this board:
bash tests/run 106 modules · 3248 tests · 3 failures
baseline 105 · 3200 · the SAME 3
unittest discover 3241 tests · 9 failures (7F/2E)
baseline 3193 · 9 · IDENTICAL SETS
The discover figures are from `discover -s .` inside `tests/`, which is not
this suite's supported invocation — `test_task_summary.py` does
`from tests.gate import GATE_OFF` and needs the repo root as top-level dir, so
six of those nine are artifacts of the cwd. Baseline was run the same wrong
way in a detached worktree at HEAD precisely so they cancel; the sets are
identical either way. +48 tests, zero regressions on both runners.
Every guard was mutation-checked and two came back GREEN, both real gaps:
`transcript_dir`'s slug fold had zero coverage — delete it and the gate
abstains FOREVER while looking exactly like a host that legitimately has no
transcript, with all 16 other tests green — and the `--json` abstain branch
printed prose, so the one output a caller most needs to parse was the one it
could not. Both fixed and covered. A third green was equivalent, and the dead
code it named was removed rather than tested around.
One regression was introduced here and fixed here: the new tests spawned a
subprocess per case, and under 8-worker `tests/run` that was enough added load
to flake `test_host_support`'s global-concurrency-cap assertion — a test
measuring contention, perturbed by a suite creating it. Failed 2 of 3 runs
with the changes, passed at baseline under the same parallel run. Both
resolvers are pure functions, so the arithmetic moved in-process and only the
wiring is still spawned. The two precedence classes went from 20 spawning
tests to 2 — 11 and 9 cases each keeping exactly one end-to-end spawn, to
prove the resolved number reaches the finding and the exit code rather than
just the report. Three consecutive clean `tests/run`s after.
Not done here: TASK-067 is the one live row the new check names, and no ask
has been filed for it. That is a decision this branch does not get to make.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TASK-050 ran eleven V4 rounds. Rounds 8, 9 and 10 each found a real escape — a pruned corpus, a one-line alias, a header row carried through a dict key — and round 11 PASSed on "a measured, listed remainder of 8 out of 76". It ended on the round the criterion became DECIDABLE, not on the round the last hole closed. The reviewer was right every time; the question had no last answer. review.md § 1 · the criteria must be bounded. A criterion is bounded when the author can name the finite set and its size BEFORE the round. The criteria file carries a `## Bound` block, and three unboundable shapes get a rewrite each. This does not soften § 2 rule 1 — the bound is what makes "enumerate the category" finishable rather than a universal negative over a live tree. review.md § 2 · what V4 does not judge. Measured on this board: 79 review documents, 54 `## Finding` headlines, 5 meta, and 22 of the remaining 49 are about the round's own artifact rather than the product — a pruned corpus, an incomplete baseline, a misreported mutation, three citations pointing at a file the branch does not carry. The protocol manufactures an exhibit, and the exhibit has more failure modes than the code. Round N+1 then audits round N's exhibit, which is a loop with no product in it. A GREEN MUTATION STAYS V4 — it is a product finding wearing a test's clothes. Bookkeeping moves to a pre-check. perry-lint --reviews gains the two pre-check findings, and --strict now EXITS NON-ZERO so a red exhibit can actually stop a dispatch. Without that the pre-check would be one more rule stated in prose that nothing implements, which is the defect review.md exists over. citation-not-on-branch a `criteria:`/`proof:` path the branch lacks criteria-unbounded a criteria file with no `## Bound` Both are scoped to OPEN rows: a closed row's exhibit cannot be re-filed, and a backlog nobody can clear is a gate that is red forever, which is a gate people delete. On this repository that took the run from 136 findings to 29. What counts as a path claim is deliberately narrow, and every exclusion is a token these 79 reviews actually produced: `tables.py` is a mention, `/pmo` is a command, `bin/perry-goals:927/:908` is a sentence about two lines, `evidence/3` is a count, and `scratchpad/…` is the reviewer obeying review-constraints.md. `checked:` is not mined at all — it is a sentence by design. MUTATIONS. Ten run, and the first pass left four green. Each was a finding: the `"/" not in tok` clause was dead code the head-is-a-directory rule already covered, so it is deleted rather than tested; two tests never reached the clause they named — `scratchpad/` did not exist in the temp tree and a trailing slash was caught by an earlier rule — and are now built to reach it; and the harness's own red-detector missed a failure it had caused. Second pass: all ten red, md5-verified restores. tests/run: the same three modules are red as on a clean `git archive HEAD` export — test_diagnose, test_heading_title, test_kr_progress_provenance, all reading live state this change does not touch. No new red. Also files the representation-layer delete list, which is analysis and not a row. Its own first draft was written as a table whose first cell was a task id, so perry-explain harvested it as state, gave TASK-050 the title "11", and silently turned test_heading_title green by displacing the real entry. It is a list now, and that paragraph is in the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e fork The gate read a stored DECLARATION out of `.perry/conformance.jsonl` and refused a write when the file's live shape no longer matched it. Keeping the declaration and the check apart was the whole design — "a stored decision plus a live check can disagree, and that disagreement is a finding". It never disagreed. The ledger held 23 records. All 23 were `route: declare`. All 23 were files in this repository. Zero carried `route: migrate`. The disagreement the design exists to surface needs a FOREIGN project that drifts, and Perry has never been pointed at one — TASK-097 has been `not_started` since the day it was filed. Three of the open rows on this board (TASK-223, 246, 248) were defects in the gate itself: work about itself. 40 files, -5,245 lines. THREE GATE CALL SITES, not the two the delete list named: `perry-task`, `perry-goals` and `perry_md_store § render --write`. The third was found by grep after the list was written, which is the delete list being wrong in the cheap direction. `bin/perry-conform` is GUTTED AND RENAMED, not deleted, and the list was wrong about that too. 598 of its 974 lines were the ledger and the gate; the rest — `state_files`, `load_schema`, `spec_for`, `shape_version`, `_q`, `_root_flag` — are generic, and `bin/perry-migrate` imports every one of them and nothing about conformance. So it is `bin/perry_schema.py` now, 161 lines, and says what it is. `perry-decide` removed its own gate first and named the hole rather than faking it; this is the same move with the measurement attached. `tests/gate.py` goes with it. `GATE_OFF` was a `.perry/config.md` line 38 fixtures appended so their writes would not be refused by a gate they were not testing. With no gate it is inert, and leaving it means every future fixture author copies a line that does nothing. WHAT SURVIVED ON PURPOSE. `perry-task list --json`'s `conformance.*` payload — `evidence_not_found`, `depends_on_unknown`, `blocked_by_closed_rows` — is read-time integrity reporting, a published contract in `schema/task-list-contract.md`, and a completely different thing wearing the same word. It is byte-identical. `perry-lint`'s schema pass is untouched; only its conformance census line is gone. ONE BUG I INTRODUCED AND CAUGHT. Removing the gate block from `perry-goals` took `with project_lock(...)` and `result = COMMANDS[...]` with it — the write path itself — and every write exited 1 on `UnboundLocalError`. Restored from HEAD and re-verified; `test_goals_writer` is green. THE FORK, and it is why this row is blocked rather than done. `perry-migrate` calls `C.declare` at 14 sites: its OUTPUT is a conformance record carrying `route: migrate`. It cannot be repaired without restoring the ledger, because the ledger is what it writes. Migration and conformance were never two subsystems, and the delete list putting them in tiers A and C was wrong. That is a product question — is Perry ever pointed at a foreign project — so it is filed as USER-910 with both readings in `evidence/2026-08/2026-08-31-TASK-261-migration-fork.md`, not decided here. tests/run: `test_migrate` is red pending USER-910. The other three red modules — test_diagnose, test_heading_title, test_kr_progress_provenance — are red the same way on a clean `git archive HEAD` export and are untouched by this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ask was whether Perry is ever pointed at a foreign project. It is not, so `bin/perry-migrate` (2,393 lines) and `tests/test_migrate.py` (2,900) are out, and `TASK-097` — "migrate the two real projects, at V5", `not_started` since the day it was filed — is dropped with them. So are `TASK-223`, `TASK-246` and `TASK-248`, three defects in a gate that no longer exists. `bin/perry_schema.py` goes too, and that was not planned. I gutted and renamed it one commit ago BECAUSE `perry-migrate` imported its `state_files`, `load_schema`, `_q` and `_root_flag`. With migrate gone it has no importer at all — every other tool reaches `lib.load_schema` in `bin/lib/__init__.py`. The 161 lines I kept turned out to be kept for one consumer, and the consumer left. Three manifests still named the tool and each one is a guard, not prose: `reference/glossary.md § restore point` (an entry whose `Implemented:` pointed at a file that no longer exists — `perry-lint --glossary` caught it), `test_one_primitive`'s WRITERS tuple, and `test_header_index_is_the_only_fold`'s watch list, which drove `fix_tables` as one of the twelve readers TASK-050 spent eleven rounds enumerating. tests/run: back to exactly the three modules that are red on a clean `git archive HEAD` — test_diagnose, test_heading_title, test_kr_progress_provenance. Nothing this branch touched is red. STILL OPEN, and it is the next commit rather than a loose end: `/perry adopt` is a user-facing command implemented BY `perry-migrate`, and the prose still promises it. `reference/adoption.md § Migration`, `bin/README.md`'s tool table, `reference/config.md`'s conformance-gate setting, `SKILL.md`'s "never run `perry-conform declare` for the user", both READMEs' ADR-004 paragraph, and the schema's `enum_aliases` / `negations` / `conformance_gate` fields — the first two of which say "Read only by bin/perry-migrate" in their own description. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code deletion without this is a lie with a passing test suite. Nine documents described the ADR-004 conformance gate, `perry-conform` or `perry-migrate` as live, and three of them were user-facing. WHAT WAS FALSE, not merely stale: - **Both READMEs** said "a project that will not migrate stays readable rather than drivable". That was the gate's refusal, and with the gate deleted nothing is read-only for want of a declaration — writes just work. Rewritten around what `/perry adopt` actually does: read what is there as evidence, write Perry's own state, never rewrite your files in place. - **`SKILL.md`** told the agent "never run `perry-conform declare` for the user" — an instruction naming a command that does not exist. - **`reference/config.md`** documented a `Conformance gate` setting as enforcing. Now marked deleted, with the sentence a user needs: a `- Conformance gate:` line left in an existing config is INERT. Which is why the schema keeps tolerating the key rather than rejecting it — removing the definition would have made that sentence false in the other direction. - **`work/reference/review-constraints.md`** told every V4 reviewer not to run two tools that are gone. - **`reference/glossary.md § conformance`** defined the word as two things. It is one thing now. `reference/adoption.md § Migration` (57 lines) and `bin/README.md`'s gate section (152 lines) are replaced rather than trimmed: both described a mechanism end to end, and a shortened description of a deleted mechanism is worse than a paragraph saying it is deleted and why. `/perry adopt` itself is untouched — stages 0–5 never called `perry-migrate`; only the "project that already has Perry-shaped state" case did, and that case is gone. `schema § migration` is deleted — `enum_aliases` and `negations`, 1,766 bytes whose own descriptions read "Read only by bin/perry-migrate". Zero readers. ONE TEST CAUGHT ME. `test_router_budget` went red: my `SKILL.md` edit pushed the file 17 bytes past its 20,480 cap, and the failure message says what to do about it — "do not raise the cap without deciding that the file should be bigger". Shortened to 20,452. Worth recording that I nearly missed it: two full-suite runs overlapped with my own edits to the tree they were reading, so their results were not evidence about anything. The run reported below is a clean one. tests/run: 3 modules red — test_diagnose, test_heading_title, test_kr_progress_provenance — the same three, failing the same way, on a clean `git archive HEAD` export. Every module this branch touched is green. TASK-261 closes at V3: the tests and the mutation work are mine, and V4 needs a reviewer that did not write it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gate TASK-261: the ADR-004 conformance gate and perry-migrate come out — −10,907 lines
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 this is
Three gates for what a Perry session actually costs, all three configurable, plus the
0d68034history repair authorized in USER-908.The bill was measured before anything was written. Across 25 sessions of this project's own transcripts and 18,941 turns: 8.43 billion tokens, 99.1% of it
cache_read— the accumulated context, re-read on every single turn. Output was 0.3%. So cost is not what a session loads, it isand both factors grow together inside one run, which makes a five-hour session superlinear rather than five times a one-hour one.
That reordered the three suspects. Verbose CLI output was the smallest and the sign was backwards — Bash results averaged 202 tokens a call while the commands invoking them averaged 353. Always-loaded docs were real but capped at ~14.6% of spend. Review rounds were the biggest lever, and a context cap turned out to be the second: replaying the measured turns, 200k costs 58.3% less for the same work.
The three gates
review-rounds-exhausted- Review rounds before escalation:PERRY_REVIEW_ROUNDSbin/perry-context-budgetautopilotstops and hands off at the ceiling- Session context ceiling:PERRY_CONTEXT_CEILING(--ceilingabove it)AGENTS.md;--compositionmakes it measurablePrecedence follows what
perry-conform § gate_modeestablished forConformance gate, and every consumer reports which register answered — including naming.perry/config.jsonlapart from.perry/config.md, because reporting a store value as though the markdown set it sends the reader to edit a projection.Why 2 and 200k. 20 rows on this board entered V4 and 74 rounds were burned; ten needed three or more, TASK-050 and TASK-249 each reached round 11. TASK-095 FAILed five times and all five read the same in the journal — "two situations answered as one, one step to the left of the last". The escalation that ended it was filed by hand at round 5, after which the user picked a principle and round 6 PASSed first try.
The history repair (USER-908)
0d68034called_ps.defaulted_over_a_declaring_tableone commit before1075830defined it, so everyperry-taskwrite on a project carrying.perry/config.jsonldied withAttributeErrorat that commit — reproduced atbin/perry-task:6773before touching anything. Agit bisectacross the range got a false verdict there.The 23-line guard hunk moved into the commit that owns it.
rebasewas the wrong tool — the range holds 20 merge commits, and bothrebaseandrebase --rebase-mergesconflicted, which would have meant re-resolving 20 merges by hand and 20 chances to change content silently. Instead the DAG was re-grafted: each commit keeps its own tree object and only its parent pointers change.Verification
Both runners, because naming only
tests/runis the omission two reviewers have flagged on this board:The discover figures come from
discover -s .insidetests/, which is not this suite's supported invocation —test_task_summary.pydoesfrom tests.gate import GATE_OFFand needs the repo root as top-level dir, so six of those nine are artifacts of the cwd. Baseline was run the same wrong way in a detached worktree at HEAD precisely so they cancel. +48 tests, zero regressions on both runners.Every guard was mutation-checked. Two came back green and both were real gaps:
transcript_dir's slug fold had zero coverage — delete it and the gate abstains forever while looking exactly like a host that legitimately has no transcript, with all 16 other tests green — and the--jsonabstain branch printed prose, so the one output a caller most needs to parse was the one it could not. A third green was equivalent, and the dead code it named was deleted rather than tested around.Two things worth a reviewer's attention
A regression was introduced here and fixed here. The new tests spawned a subprocess per case, and under 8-worker
tests/runthat was enough added load to flaketest_host_support's global-concurrency-cap assertion — a test measuring contention, perturbed by a suite creating it. Failed 2 of 3 runs with the changes; passed at baseline under the same parallel run. Both resolvers are pure functions, so the arithmetic moved in-process and the two precedence classes went from 20 spawning tests to 2. Three consecutive clean runs after.A limit below 1 is refused, not clamped. The comparison is
len(fails) < limit, so a declared0does not tighten the gate, it inverts it — the finding would fire on every live row carrying any verdict block, including rows with zero FAILs. Found by reading the diff, guarded on all three registers, and pinned by a test rather than by a second guard at the comparison (two implementations of one rule is the defect this repository finds most often).Not done here
TASK-067is the one live row the new check names, and no ask has been filed for it. That is a decision this branch does not get to make.🤖 Generated with Claude Code