Make the fallback review a panel, not a command (#26)#31
Conversation
Closes #26. `review.fallback_commands.<runtime>` runs the runtime's own review command in the cockpit's context. When the cockpit authored the diff, that is the author re-reading their own work — so the mechanism meant to satisfy `safety-critical-changes.md` rule 2 ("a single-lens verdict is an incomplete review, not a green light") structurally could not. `review.fallback_panel` replaces it as the primary: one isolated, fresh-context reviewer per lens, shipping the two the doctrine names (adversarial, correctness). `fallback_commands` stays as the explicitly DEGRADED mode for a runtime that cannot isolate a reviewer, and must be recorded as one lens rather than as a panel. The split follows Principle #10: *which* lenses is adopter knowledge and lives in config; *how a lens must be run* is kit doctrine and lives in the new `docs/agentic-dev-kit/fallback-review-panel.md`. That contract is the part worth having written down — fresh context, the raw diff with no author framing, and the two things the trials showed matter most: - **execute, don't only read** — a guard turned out to be dead code for the exact bot it was written for, because that bot reports a zero timestamp; no amount of re-reading surfaced it, one live poll did - **mutation-test new branches** — five properties across the trial session were *named* by a test and pinned by nothing, including one where hardwiring a branch to a constant passed the entire suite The doc also records what the trials showed about stopping: across 13 rounds on three PRs every round found something, so rule 3's "until a full pass finds nothing new" may never terminate. The criterion it gives instead is blast radius — keep going on a gate, stop earlier on something reported but never acted on — and rule 3 now points at it. Engine change is small and is the audit half: `--record-review --lenses adversarial,correctness` records what actually ran, so a degraded one-lens pass no longer produces a receipt byte-identical to a panel. `render_record_review` calls out a single-lens receipt explicitly. Also repointed the surface that mattered most: `pr_followup_hook` fires on every `gh pr create`/`ready`, making it the most-read statement of fallback policy in the kit — and it was advertising the degraded mode. It now names the panel and its configured lenses, and falls back to the single command only when no panel is configured. Tests 300 → 306. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
📝 WalkthroughWalkthroughAdds a configurable two-lens fallback review panel, records executed lenses in review receipts, updates reminder and PR-watch workflows, documents degraded mode, and adds migration, drift-tracking, and validation coverage. ChangesFallback Review Panel
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ReviewBot
participant FallbackReviewers
participant pr_watch.py
participant ReviewReceipt
ReviewBot-->>FallbackReviewers: reports unavailable
FallbackReviewers->>pr_watch.py: submit executed lens names
pr_watch.py->>ReviewReceipt: store fallback:panel receipt
pr_watch.py-->>FallbackReviewers: display head-bound coverage
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/agentic-dev-kit/fallback-review-panel.md`:
- Line 103: The receipt workflow must record the selected blast-radius stopping
criterion. In scripts/pr_watch.py lines 1474-1481, add CLI input for the
stopping criterion and include it in the rendered receipt output; in
docs/agentic-dev-kit/fallback-review-panel.md lines 103-103, retain the
requirement and direct operators to this receipt field as its recording
location.
- Line 81: Specify the shell language on the fenced code block in
fallback-review-panel.md by changing its opening fence to use the sh language
identifier, resolving the MD040 lint warning.
In `@docs/agentic-dev-kit/workflows/pr-watch.md`:
- Around line 79-83: Update the command code fence surrounding the uv run
invocation in pr-watch documentation to specify the shell language as sh,
preserving the command and its formatting.
- Around line 79-88: Update the pr_watch command example and surrounding
guidance to record the configured lenses that actually ran, rather than
hard-coding adversarial,correctness. Replace the fixed lens list with a
placeholder representing the runtime-configured lens names, while preserving the
fallback:panel versus fallback:<runtime> recording rules.
In `@docs/CLAUDE-sections.md`:
- Around line 68-69: Update the clean-bar criteria in docs/CLAUDE-sections.md to
require a completed review.fallback_panel when isolated reviewers are available,
rather than accepting a bot or single fallback command as sufficient. Keep the
fallback command as an acceptable path only for explicitly degraded runtimes,
and align the surrounding requirement language with this panel-aware behavior.
In `@scripts/hooks/pr_followup_hook.py`:
- Around line 69-79: Update scripts/hooks/pr_followup_hook.py lines 69-79 to
load and validate review.fallback_panel.receipt_source alongside the lens names,
then update the reminder construction around lines 103-110 to interpolate that
loaded value. Update docs/agentic-dev-kit/workflows/pr-watch.md lines 79-88 and
docs/autonomous-session-playbook.md lines 73-80 to reference
<review.fallback_panel.receipt_source> instead of a literal receipt source,
keeping the configured value consistent across instructions and audit receipts.
- Around line 69-78: Update the lens extraction in the fallback panel loading
logic to strip surrounding whitespace from each string `name` and discard
results that are empty afterward. Preserve the existing list/type validation
while ensuring `lenses` contains only usable, normalized names.
In `@scripts/pr_watch.py`:
- Around line 1479-1481: Require fallback:panel receipts to contain at least two
distinct normalized lens names in the named_lenses and receipt construction
flow, rejecting empty, single, or duplicate lenses while preserving omitted-lens
compatibility for non-panel/human sources. Update scripts/pr_watch.py lines
1479-1481 and add corresponding rejection coverage for empty, single, and
duplicate panel lenses in scripts/tests/test_pr_watch.py lines 2152-2166.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b18aa46-7eaa-4b2e-b5ff-53444a485c9f
📒 Files selected for processing (13)
.agents/skills/pr-watch/SKILL.mdconfig/dev-model.yamldocs/CLAUDE-sections.mddocs/agentic-dev-kit/fallback-review-panel.mddocs/agentic-dev-kit/safety-critical-changes.mddocs/agentic-dev-kit/workflows/pr-watch.mddocs/autonomous-session-playbook.mdinit.shkit-manifest.jsonscripts/hooks/pr_followup_hook.pyscripts/pr_watch.pyscripts/tests/test_pr_followup_hook.pyscripts/tests/test_pr_watch.py
Eight findings, four Major, all valid. Two are the classes this session keeps producing. **I added `review.fallback_panel.receipt_source` and then ignored it.** The hook and both workflow docs prescribed the literal `fallback:panel`, so an adopter who renamed it would be handed a command writing a differently-labelled receipt than their own config declares — a config key that exists and does nothing (Principle #10). Now loaded, validated, and interpolated everywhere. **A panel receipt asserted two lenses on trust.** `--lenses adversarial,adversarial` rendered as multi-lens coverage, and omitting `--lenses` still produced a receipt labelled `fallback:panel`. That is prose where a deterministic gate belongs — the exact thing `safety-critical-changes.md` rule 1 says to stop doing. `--record-review` now refuses a panel-sourced receipt naming fewer than two *distinct* (case-folded) lenses, and the check follows the configured source rather than a literal. The escape is not a flag: it is recording what actually ran under a single-lens source. That gate immediately rejected six of my own existing tests, which had been using `fallback:panel` as a convenient string for scenarios that were really single-lens. They now say what they mean. **The clean bar still allowed the degraded path.** `CLAUDE-sections.md` required a panel two lines above, then said "bot *or* the fallback command" satisfies the clean bar — which let an agent treat one lens in its own context as sufficient while isolated reviewers were available. Also: blank lens names are discarded rather than advertised (a panel with an unnameable lens is worse than no panel); the stopping rationale now points at the PR, since receipts carry what a review did *not* cover rather than prose; docs use `<review.fallback_panel.receipt_source>` and placeholder lens names instead of this repo's defaults; and two fences got languages. Tests 306 → 310. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
Both lenses ran on this PR (the bot was rate-limited, so the panel reviewed
itself). The adversarial lens found the thing that matters, and it invalidates
how the previous commit described its own change.
**`decide_mergeable` never looks at `source` or `lenses`.** So the one-lens ⚠
printed exactly once — on the stdout of the `--record-review` call the agent
itself chose to make — and never again at the moment a merge is authorized. The
previous commit's message said "make the panel's claims enforceable". What it
made enforceable was the LABEL, not the merge.
**And the incentive gradient ran backwards.** `source` is free text an agent
picks, so equality against it is a matcher over attacker-chosen input — exactly
what `safety-critical-changes.md` rule 1 says to stop building. Measured:
ACCEPTED 'fallback:panel (adversarial + correctness)' no lenses, NO warning
ACCEPTED 'fallback: panel' no lenses, NO warning
ACCEPTED 'fallback-panel' no lenses, NO warning
while the honest escape — `fallback:codex --lenses correctness` — produced the
⚠. The quietest receipt was the dishonest one.
The fix is not a tighter matcher. `review_evidence` now carries the recorded
lenses, and the POLL render states them every time the PR is polled:
review evidence: fallback:panel — 2 lenses (adversarial, correctness)
review evidence: fallback:codex — ⚠ ONE lens (correctness) — not a dual-lens pass
review evidence: fallback:panel (…) — ⚠ lenses not stated
A relabelled receipt now reads as unstated coverage instead of buying silence,
and the source check is documented as what it actually is: a catch for the
accidental mislabel, not proof a panel ran.
Also from this round:
- **A string `lenses` was read as one lens per character.** `"adversarial"` is
iterable, so a hand-edited state file rendered as eleven lenses' worth of
coverage. Both render paths are now list-guarded, like the sibling receipt
fields already were.
- Stale receipts report no coverage — evidence bound to an older head is not
evidence for this one.
- The two `receipt_source` validation branches were unpinned, and both fail
toward switching the gate OFF (an unmatchable or unstripped source silently
accepts a lens-free panel receipt).
Tests 313 → 317.
Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
Panel round 3. Both lenses again, both found real defects in round 2's fix, and the correctness lens found the one that matters. **`⚠ lenses not stated` fired on every lens-free receipt** — including the ordinary bot receipt, which no workflow tells you to record with `--lenses`. Measured against this repo's own 14 stored receipts: all 14 warned. So the relabelled bypass round 2 set out to expose produced a line byte-identical to the healthy default, and the change achieved nothing. It also repeated the exact mistake this engine deliberately avoids 300 lines earlier for `bot_signal: "skipped"` — a permanent false warning on a correct receipt. Now only a receipt CLAIMING to be a panel owes a lens list. Of those same 14, 5 warn — and those 5 genuinely did claim a panel without naming lenses. **Two more forgeries in round 2's render.** A newline in `source` split the coverage line and left the first half reading as a completed panel; and `,` is both the lens separator and ordinary punctuation, so "correctness, i.e. does it do what it says" counted as two lenses. Untrusted text is flattened (the convention `_excerpt` already set for comment bodies) and the count deduped case-folded. **The lens roster is now checked against config.** That is the one part of the panel claim that is genuinely checkable — the lenses are declared in `review.fallback_panel.lenses`, not invented at record time. It does not prove a panel ran; nothing here can. The docs say so plainly rather than implying otherwise. **Claims of mine that were false, now corrected:** - "Both render paths are now list-guarded, like the sibling receipt fields already were" — the sibling was NOT guarded and raised `AttributeError` on a string `bots_behind_head`. Guarded now, and the test that promised to cover "either render" actually does. - "interpolated everywhere" — `SKILL.md` still hardcoded the literal. - The poll render still said "run the configured fallback review", naming the degraded mode this PR redefines, while the workflow doc described that line as pointing at the panel. Doc described output the code did not produce. - A test asserted `_PANEL_RECEIPT_SOURCE == panel["receipt_source"]` where both sides are the same string — a tautology that passed with config reading removed entirely. **And the family is consistent again.** Round 2's own argument — a caveat printed only on the stdout of the `--record-review` call the agent itself made is not visible at merge time — applied equally to `override` and `bot_signal`, which had not moved. Both now surface on the poll render too. The source-side casefold, unpinned for three rounds against this PR's own "mutation-test new branches" doctrine, is finally pinned. Tests 317 → 323. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/pr_watch.py (1)
1980-1985: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winThe
bots_behind_headguard stops at the container.
isinstance(behind_map, dict)protects against a non-dict, butsha[:7]still assumes each value is a string — a hand-edited{"coderabbit": 5}raisesTypeErrorin the render this guard exists to keep crash-free. Same argument as thelenseselement-level check above.🛡️ Proposed fix
behind_map = receipt.get("bots_behind_head") for bot, sha in (behind_map if isinstance(behind_map, dict) else {}).items(): lines.append( - f" ⚠ {bot}'s last review was of {sha[:7]}, not this head — this receipt " + f" ⚠ {_flat(bot, 40)}'s last review was of {_flat(sha, 7)}, not this head — this receipt " "does not stand for its review of this design" )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/pr_watch.py` around lines 1980 - 1985, Update the bots_behind_head rendering loop in the receipt formatting logic to validate each sha value before slicing it. Skip or safely handle entries whose values are not strings, while preserving the existing warning output for valid SHA strings and ensuring malformed receipt data cannot raise during rendering.
🧹 Nitpick comments (2)
scripts/tests/test_pr_watch.py (2)
2344-2344: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename the
lloop variables (E741).Ruff flags
las ambiguous at lines 2344, 2383, 2452, 2491, 2495 and 2499;linereads the same and clears the lint.Also applies to: 2383-2383, 2452-2452, 2491-2499
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/tests/test_pr_watch.py` at line 2344, Rename the ambiguous `l` loop variables to `line` in all affected comprehensions and loops around the `pr_watch.render(report)` assertions, including the occurrences at the referenced locations, and update each corresponding expression to use `line` consistently.Source: Linters/SAST tools
2525-2525: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEscape the
match=pattern (RUF043).
match=is a regex; the unescaped dots make the intent ambiguous and Ruff flags it.🧹 Proposed fix
- with pytest.raises(ValueError, match="not in review.fallback_panel.lenses"): + with pytest.raises(ValueError, match=re.escape("not in review.fallback_panel.lenses")):Add
import reat the top of the module if it is not already imported.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/tests/test_pr_watch.py` at line 2525, Escape the dots in the regex passed to match= within the pytest.raises assertion so it matches the literal message “not in review.fallback_panel.lenses”; add the re import only if needed and use the module’s existing regex-escaping convention.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/hooks/pr_followup_hook.py`:
- Around line 91-98: Update the configuration-loading exception path to return
an empty lenses collection instead of list(_DEFAULT_LENSES), while preserving
the other fallback values so the hook uses the compatible single-command
fallback when configuration is unreadable.
---
Outside diff comments:
In `@scripts/pr_watch.py`:
- Around line 1980-1985: Update the bots_behind_head rendering loop in the
receipt formatting logic to validate each sha value before slicing it. Skip or
safely handle entries whose values are not strings, while preserving the
existing warning output for valid SHA strings and ensuring malformed receipt
data cannot raise during rendering.
---
Nitpick comments:
In `@scripts/tests/test_pr_watch.py`:
- Line 2344: Rename the ambiguous `l` loop variables to `line` in all affected
comprehensions and loops around the `pr_watch.render(report)` assertions,
including the occurrences at the referenced locations, and update each
corresponding expression to use `line` consistently.
- Line 2525: Escape the dots in the regex passed to match= within the
pytest.raises assertion so it matches the literal message “not in
review.fallback_panel.lenses”; add the re import only if needed and use the
module’s existing regex-escaping convention.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5eb95e81-fcc7-4879-9c7e-f803b31570ea
📒 Files selected for processing (13)
.agents/skills/pr-watch/SKILL.mddocs/CLAUDE-sections.mddocs/agentic-dev-kit/fallback-review-panel.mddocs/agentic-dev-kit/workflows/pr-watch.mddocs/autonomous-session-playbook.mdinit.shkit-manifest.jsonscripts/hooks/pr_followup_hook.pyscripts/kit_doctor.pyscripts/pr_watch.pyscripts/tests/test_portability.pyscripts/tests/test_pr_followup_hook.pyscripts/tests/test_pr_watch.py
🚧 Files skipped from review as they are similar to previous changes (5)
- docs/CLAUDE-sections.md
- docs/autonomous-session-playbook.md
- .agents/skills/pr-watch/SKILL.md
- docs/agentic-dev-kit/fallback-review-panel.md
- docs/agentic-dev-kit/workflows/pr-watch.md
CodeRabbit. `_DEFAULT_LENSES` was `[]`, so the hazard was latent rather than live — the degraded wording already fired. But the name implied a default panel exists, and populating it would have made the hook advertise a panel, and a `--record-review` command the engine then refuses, on the strength of a config it had just failed to read. The constant is gone; that path now returns `[]` with the reason written down, and a test pins that an unreadable config produces the single-command wording. Tests 323 → 324. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
|
Fixed — with one correction to the finding's framing: |
Panel round 4. The correctness lens found three High; the adversarial lens had to be stopped mid-run (see below). **The roster check refused a genuine third lens.** Requiring every `--lenses` name to appear in `review.fallback_panel.lenses` made this PR's own doc false: `adversarial,correctness,security` was rejected, and the error told the operator to record a three-lens review under a *single-lens* source — pushing them to UNDER-claim. Roster names are now COUNTED rather than required, so extras pass while the forgery this replaced still fails (`"correctness, i.e. does it do what it says"` names one roster lens, not two). **Round 3's render fix landed on two of three sites.** The grace-cancelled branch and a docstring still said "run the configured fallback review" — naming the degraded mode this PR redefines, while the workflow doc described that same line as pointing at the panel. **Neither changed string was pinned.** The existing assertions matched the `review unavailable` prefix, never the pointer, so reverting round 3's change passed all 324 tests. That is the "named by a test and pinned by nothing" class this PR ships doctrine about, on this PR's own change. Now pinned across all three sites. Also: `_flat`'s docstring quoted a warning string round 3 deleted; the doc claimed the coverage line prints "every time the PR is polled" when it needs a current-head receipt; and the doc restated the lens briefs that config already owns — two copies which had already drifted (the doc's adversarial brief mentioned "inputs the author did not consider", config's did not). The doc now points at config for the briefs rather than duplicating them. **A finding about the panel itself, discovered by running it.** The two lenses ran concurrently in the same working tree. The adversarial lens mutates files to test them; the correctness lens saw those mutations as an external process corrupting the repo and ran `git checkout --` to "restore" it. They clobbered each other for ten minutes: one lens's results were unreliable, and when I stopped the adversarial lens it left a live mutant behind — `if False and _PANEL_LENS_NAMES:` — which silently disabled the roster check in my working tree. I caught it only because a test failed with an error string that should no longer have existed. `fallback-review-panel.md` gains contract item 7: mutate in an isolated copy, never the shared tree. That is a real gap in the doc this PR ships, found by using it. Tests 324 → 326. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
|
@coderabbitai full review |
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 10 minutes. |
Panel round 5, run with the lenses in isolated worktrees this time. The
adversarial lens showed the gate is defeated by one extra character — and that
the render then *affirms* the forgery:
--record-review "fallback:panel (2 lenses)" \
--lenses "correctness, i.e. does it do what it says"
→ review evidence: fallback:panel (2 lenses) — 2 lenses (correctness, i.e. …)
→ ✅ DONE — green, reviewed, merge-ready
That is verbatim the forgery the previous commit claimed to block. The record
gate matched `source` with `==` while the render matched it with `startswith`,
so one decorated character passed the first and satisfied the second. It also
found the render never consulted the roster at all (so any source with two
invented lens names rendered as a panel), and that an empty roster failed OPEN
from four plausible config shapes, silently.
**So the gate is gone, not tightened a fifth time.** Rule 1: "treat 'we
tightened the matcher' as a stopgap, not a fix." Four tightenings — source
equality, then lens names, then a required roster, then a counted roster — each
defeated by the next round. The mechanism cannot work from here: the caller
writes the source and the lens names in one invocation, with nothing binding
either to a review that happened.
What remains is honest and still useful: `--lenses` is recorded and the poll
render states it as a **claim** ("2 lenses claimed", "⚠ ONE lens claimed"), so a
one-lens pass is visible at merge time instead of buried in the record command's
stdout. The docs say plainly that this is an audit trail, not a gate, and that
the engine cannot verify it.
The version that would actually verify — each lens recording its own receipt
from its own context, with the panel derived by counting entries — is filed as
**#32** with the four defeats written up, so the next attempt starts from why
this one failed rather than rediscovering it.
Also fixed from this round: contract item 7 shipped DUPLICATED verbatim in the
doc (kit_doctor could not catch it — the manifest was rehashed in the same
commit); and `sha[:7]` crashed on a non-string value in a guard whose own
comment claimed to have handled exactly that.
Net: pr_watch.py loses ~60 lines of unverifiable checking. Tests 326 → 314,
which is the right direction — seven tests pinned a mechanism that did not work.
Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
910f0d5 to
30c7948
Compare
Round 6, adversarial lens (isolated worktree). Two real findings, one of them a regression the previous commit caused. **`--lenses` splits on `,`, which is also ordinary punctuation.** So `--lenses "adversarial, focused on the new merge gate"` — an HONEST way to record one lens — arrived as two entries and rendered "2 lenses claimed", suppressing the one-lens warning that is the entire remaining value of the field after the gate was removed. The deleted roster intersection had been catching this class; deleting it brought the bug back, and the same input is the forgery the previous commit's own message quoted as blocked. It was not. Fixed without reintroducing a roster or a gate: count only entries that look like lens NAMES (`adversarial`, `data-migration`, `lens.one`), not prose. Prose is still recorded verbatim on the receipt; it just does not count toward "how many lenses ran". That fixes the honest case and the forgery together, and it is a property of the value rather than a matcher over a label. **Control characters survived `_flat`.** It collapsed whitespace, and `\x1b` is not whitespace — so ANSI cursor control passed through. That is strictly worse than the newline `_flat` exists to stop: `\x1b[1A\x1b[2K` *erases* lines that already exist, so a receipt could delete the merge blockers printed above it. Fixed in `_flat` and also in `_excerpt`, which is pre-existing on main but worse: it renders a COMMENT BODY, which any account can write on a public repo, and it renders last, so cursor-up there walks over the whole report. `_flat`'s docstring cites `_excerpt` as the convention it copies — copying a defect forward is how a convention stops being one. Also pinned three guards whose reverts passed the entire suite, each cited by a comment or commit message as deliberate: `_flat` on lens names (not only on `source`); the list check that stops a bare string being read as one lens per character; and `_flat` on the SHA in `bots_behind_head`, which the previous commit advertised fixing while the test named for it checked only the wrapper type, never a dict with a non-string value. Corrected a hook comment that justified its behaviour by a refusal the engine no longer performs. Tests 314 → 317. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
… guards Round 7, correctness lens. Its most valuable finding is about the method, not the code. **`kit_doctor`'s self-check rehashes every kit-owned file, so ANY byte change fails it.** Mutate `pr_watch.py` and that test goes red regardless of behaviour — so a mutation run reports "killed" when nothing behavioural caught anything. The lens's first pass here reported 17/17 killed; re-run with that test excluded, 7 had survived. I verified it directly: disabling `_flat`'s truncation entirely fails only the manifest test. That invalidates confidence from earlier rounds on this branch, and contract item 5 of the doc this PR ships said "mutation-test new branches" without warning about it. It now does. **The deletion took three tests that pinned LIVE code**, not the gate: - `test_the_cli_threads_lenses_through_to_the_receipt` covered the CLI seam. Without it, `lenses=args.lenses` → `None` in `main()` passed all 314 tests — `--lenses`, this change's entire engine surface, was unpinned end to end. - the hand-edited-receipt test pinned three type-guards; each could be removed with the suite green. Without the `isinstance(..., list)` check, a hand-edited `"lenses": "adversarial"` renders as `8 lenses claimed (a, d, v, e, r, s, …)`. - the stale-receipt test pinned report level; its replacement only pinned the render, leaving `review_evidence.lenses` readable off an old receipt in the `--json` payload. All three restored, and the `bots_behind_head` test now exercises the level that actually crashed — a dict with a non-string VALUE, not junk at the map level, which is what the previous commit advertised fixing while the test named for it checked only the wrapper. Re-ran the mutation battery with the drift test excluded: 7/7 caught by named behavioural tests. Also: `_flat`'s docstring quoted a warning string deleted two commits ago (a verbatim recurrence of a defect an earlier round fixed in the same docstring); two hook comments cited a refusal the engine no longer performs; a workflow doc still pointed at "the configured fallback" where the branch's own test pins all three engine sites; the new poll line was undocumented in the Notes reference that documents every other render line; and `init.sh` now ignores `.claude/worktrees/` for adopters, since contract item 7 tells them to give each lens its own worktree and the kit had protected only itself. Tests 317 → 320. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
The `review evidence:` line prints on the poll render, which a human reads. `dev_session.sh merge` consumes the JSON and gates on `mergeable` alone, so on an autonomous self-merge path nobody sees it — which is exactly the path `autonomous-session-playbook.md` argues review independence matters most on. Not fixed here: surfacing it would mean changing a second safety-critical file at the end of a seven-round review, and the real fix is #32's per-lens receipts rather than another render call. Stated as a known gap instead of left implicit. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
|
Merging. Receipt: Seven review rounds, every one with findings. Two CodeRabbit, five panel. Three rounds found defects introduced by the previous round's fix, including one where deleting the gate reintroduced a bug the gate had been masking. The headline is a retraction. I built four successive versions of a mechanism to verify that a panel actually ran, and each was defeated by the next round. The last was defeated by a single decorated character in a free-text field the caller writes themselves — after which the render affirmed the forgery. Rule 1 says treat a tightened matcher as a stopgap, not a fix; four tightenings is well past the point of taking that seriously. The mechanism is deleted, Two findings I'd read even if you skip the rest:
Conflict of interest, stated plainly. I filed #26, built this, ran the panel on it, and am merging it. This is the PR arguing for review independence, merging on a self-reported panel — because CodeRabbit was rate-limited on all but two heads. Everything above is checkable in this thread; none of it is independently attested. The PR body's "Known gaps" section is the honest inventory of what this does not do. |
I had the wrap-up fact-checked against `git log` / `gh` / the code rather than self-certifying it. Ten problems, two HIGH. **The handoff never mentioned #33.** That file is what `/session-start` reads, so a next session would not have learned that an open, H-severity, self-described *retroactive* issue exists — while the friction log called it the session's most consequential finding. Now in both the session block and "Open, and owned by nothing yet". **"Verified directly" was attached to the wrong claim.** I verified the drift check's *mechanism* — trivially reproducible. The *figure* (17/17 reported, 7 survivors when excluded) is a reviewing lens's report; the 17 mutants are enumerated nowhere. Both files presented it as first-party measurement, and the friction log's "Measured on #31" was the stronger overclaim. Now labelled attested. Three MEDIUM omissions, each of which would have misled: - the 7 survivors were **closed inside #31** (7/7 caught once real coverage was added) — without that, a reader concludes 7 mutants are live on `main` - the comma-as-punctuation regression is described in past tense with no resolution; it was **fixed inside #31**, so a next session could re-fix something already shipped - the **autonomous self-merge path never displays review coverage** — `dev_session.sh merge` gates on `mergeable` alone. That is the caveat that qualifies the session's headline decision, and it appeared in neither file Also: "a third field" where the same sentence names three predecessors; the unverifiable-artifact conclusion attributed to the adversarial lens when the commit records it in the author's voice; the invalidation scope now naming #31 itself rather than a date; and the friction log flagged as at budget, so a triage sweep is required before the next entry. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp
…eted (#34) * Wrap-up: the fallback review is a panel, and its verification was deleted #31 merged, closing #26. `review.fallback_panel` is the primary substitute when a review bot can't review — one isolated, fresh-context reviewer per lens — with `fallback_commands` kept as the explicitly degraded one-lens mode. The lens contract lives in the new kit-owned `docs/agentic-dev-kit/fallback-review-panel.md`. Two issues filed from the session: **#32** (verify lens coverage instead of self-reporting it) and **#33** (the drift self-check makes mutation testing report false kills). Measured, not asserted: suite 300 at `1ef5fd6` → 320 at `7485512`. **The session's result is a retraction.** Four successive mechanisms were built to make a receipt's coverage claim verifiable by the engine, and each was defeated by the next review round — the last by one decorated character in a field the caller writes themselves, after which the render affirmed the forgery. Rule 1 already says to treat a tightened matcher as a stopgap; four tightenings is well past taking that seriously. What ships records the claim and labels it a claim. Three findings in the friction log matter beyond this PR: - **#33 is retroactive.** Mutation evidence cited across #25, #28 and #29 may be worthless wherever the reviewer did not exclude the drift test — and "N mutants died" was used as a reason to stop reviewing. A false-negative testing tool is worse than none, because it justifies confidence. - **Concurrent review lenses in one working tree destroy each other's work.** Fixed as contract item 7, but recorded for the pattern: any doctrine that says "run N reviewers concurrently" owes them isolation, and mine did not until it bit. - **Deleting a check reintroduced the bug it was masking.** When removing a mechanism as unfit, enumerate what it was rejecting and confirm each case is still handled — the deletion commit did neither. Adopter upgrade instructions are written and now unblocked (they were held until #31 landed): `~/Documents/openkitchen-devkit-upgrade.md` and `~/Documents/cs-toolkit-devkit-adoption.md`. cs-toolkit turned out **not** to be an adopter at all — no config, no init.sh, its own divergent engines — so that one is an adoption plus a fixer change from `done` to `converged`, not an upgrade. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp * Correct ten errors the wrap-up fact-check found I had the wrap-up fact-checked against `git log` / `gh` / the code rather than self-certifying it. Ten problems, two HIGH. **The handoff never mentioned #33.** That file is what `/session-start` reads, so a next session would not have learned that an open, H-severity, self-described *retroactive* issue exists — while the friction log called it the session's most consequential finding. Now in both the session block and "Open, and owned by nothing yet". **"Verified directly" was attached to the wrong claim.** I verified the drift check's *mechanism* — trivially reproducible. The *figure* (17/17 reported, 7 survivors when excluded) is a reviewing lens's report; the 17 mutants are enumerated nowhere. Both files presented it as first-party measurement, and the friction log's "Measured on #31" was the stronger overclaim. Now labelled attested. Three MEDIUM omissions, each of which would have misled: - the 7 survivors were **closed inside #31** (7/7 caught once real coverage was added) — without that, a reader concludes 7 mutants are live on `main` - the comma-as-punctuation regression is described in past tense with no resolution; it was **fixed inside #31**, so a next session could re-fix something already shipped - the **autonomous self-merge path never displays review coverage** — `dev_session.sh merge` gates on `mergeable` alone. That is the caveat that qualifies the session's headline decision, and it appeared in neither file Also: "a third field" where the same sentence names three predecessors; the unverifiable-artifact conclusion attributed to the adversarial lens when the commit records it in the author's voice; the invalidation scope now naming #31 itself rather than a date; and the friction log flagged as at budget, so a triage sweep is required before the next entry. Claude-Session: https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp --------- Co-authored-by: Topi Jarvinen <topi.jarvinen@gmail..com>
Review findings on #57. All LOW; the content-preservation core held. - the active file's intro still said "entries below are the ones that are not yet issue-shaped" — after the sweep there are no entries below. It is preserved verbatim per the doctrine's "H1 + intro", so an engine run would have left it too; reworded to describe the marker convention instead. - the archive header named TWO buckets for the un-graduated remainder; there are three. The 11th entry (a guard firing correctly, "No change proposed") is neither already-tagged nor already-shipped. Both files now give the full 11-entry accounting: 3 graduated + 3 already shipped + 4 already tagged + 1 no-change. - a line wrapped so it began `#31 —`. Not an ATX heading under CommonMark (no space after `#`) and no kit tooling mis-parses it — both `init.sh` and `archive_plan_sessions.py` match `"## "` with the space — but an unfortunate wrap in a file whose own sweep engine is heading-anchored. Reflowed. Also updates the handoff, which this PR's own sweep had just falsified: it said "the friction log is AT budget (150/150) and triage-friction-log is now blocking". It is 32 lines and not blocking. Step 5 of the doctrine constrains the finalize commit to the two doc paths, so this is a deliberate scope deviation — `session-start` reads the handoff and would otherwise hand the next session a false blocking signal, which is precisely the failure #54 (filed in this PR) exists to stop. Re-verified with the stronger check the reviewer used, rather than the set-membership one the PR body originally cited: the entire original body, demoted, appears as one CONTIGUOUS byte-identical block in the archive — which establishes verbatim-and-in-order, not merely present. Claude-Session: https://claude.ai/code/session_01V27wAgECMEApqAvmJMFyqm
The inbox was AT its 150-line budget carrying an explicit "sweep required before the next entry" tripwire. Run inline in the skill's documented LLM-only mode — scripts/triage_friction_log.py and scripts/finalize_triage.py are not vendored yet (blocked on #6), so parse / draft / sweep were done by hand against the doctrine. Graduated: #54 every verification claim must name the command that establishes it #55 safety-critical rule 1 should name a tightening threshold #56 removing a mechanism requires enumerating what it was rejecting #54 SUBSUMES the inbox entry's own narrower proposal ("wrap-up should fact-check the handoff"), which it retains: the pattern turned out to be broader than the handoff and broader than authorship — two instances this session (#51, #53) are generated by ENGINES, so they reproduce in every adopter regardless of who is writing. Three further entries needed no ticket because their fixes had ALREADY SHIPPED in #31 — checked rather than assumed, and independently confirmed in review by git blame to 7485512: panel contract items 4-5 (execute, mutation-test), item 7 (isolated worktrees), and rule 3's blast-radius stopping criterion. Filing them would have produced three issues closed on arrival. Full accounting, 11 entries: 3 graduated + 3 already shipped + 4 already tagged (#10, #18, #19, #33) + 1 recording a guard working correctly with "No change proposed". The sweep moves the whole prior inbox verbatim into kit-friction-log-archive.md with headings demoted one level. Verified with a contiguity check rather than set membership: the entire original body, demoted, appears as ONE CONTIGUOUS byte-identical block in the archive, which establishes verbatim AND in order. Inbox 150 -> 35 lines. Reviewed by one isolated correctness lens (recorded as one lens, not a panel) after CodeRabbit went rate-limited on both surfaces. Nine LOW findings, no HIGH or MEDIUM; six fixed, three answered with reasons — including that the PR was deliberately not opened as a draft, since CLAUDE-sections.md holds that a finished PR must never sit in draft and pr_watch treats draft as a merge blocker. Also corrects the handoff, which this PR's own sweep had falsified. Receipt: fallback:correctness-lens, one lens, bound to 9fa6fd3. Claude-Session: https://claude.ai/code/session_01V27wAgECMEApqAvmJMFyqm
Closes #26. Follow-up filed as #32.
What this is now
review.fallback_panelreplacesreview.fallback_commandsas the primarysubstitute when a review bot can't review: one isolated, fresh-context
reviewer per lens, shipping the two the doctrine names (adversarial,
correctness).
fallback_commandsremains as the explicitly degraded one-lensmode for a runtime that cannot isolate a reviewer.
The split follows Principle #10 — which lenses is adopter config; how a lens
must be run is kit doctrine, in the new
docs/agentic-dev-kit/fallback-review-panel.md.The honest headline: I built a verification mechanism, then deleted it
Four rounds went into making the receipt's coverage claim verifiable by the
engine. Each was defeated by the next round's adversarial lens:
sourcestring.fallback:panel (adversarial + correctness)accepted with zero lenses and no warning — while the honestfallback:codex --lenses correctnessproduced the ⚠. The quietest receiptwas the dishonest one.
sourcesplit therender line so the first half read as a completed panel;
,is both the lensseparator and ordinary punctuation.
lens, making the doc's own "two disjoint lenses is the floor, not the
ceiling" false.
sourcewith==while the render matched withstartswith, so one decorated characterdefeated the gate and the render then affirmed the forgery.
safety-critical-changes.mdrule 1: "Treat 'we tightened the matcher' as astopgap, not a fix." Four tightenings is the signal. The mechanism is
deleted —
pr_watch.pyloses ~100 lines of unverifiable checking.--lensesis still recorded and shown at merge time, labelled as what it is: aclaim, self-reported by whoever ran
--record-review, which the engine doesnot verify. That is genuinely useful — a one-lens pass is visible when a merge
is considered rather than buried in the record command's stdout — and it is
worth exactly what an honest operator puts into it.
#32 carries the design that would actually verify it (each lens recording
its own receipt from its own context), written up with all four defeats so the
next attempt starts from why this one failed.
Also in here
pr_followup_hooknow names the panel. It fires on everygh pr create/ready, making it the most-read statement of fallback policy in the kit — andit was advertising the degraded mode. It degrades to the single command when
fewer than two distinct lenses are configured.
fallback-review-panel.mdcarries the lens contract: fresh context, theraw diff with no author framing, execute rather than only read, mutation-test
new branches, report-don't-fix, and — learned by running it — mutate in an
isolated copy, never the shared tree.
_flatcollapsed whitespace, but\x1bis not whitespace, so ANSI cursor control passed through — and\x1b[1A\x1b[2Kerases lines that exist. Fixed there and in_excerpt,which is worse: it renders comment bodies anyone can write, and renders last.
safety-critical-changes.mdrules 2 and 3 now point at the panel doc, andrule 3 at its stopping criterion.
What review found
Seven rounds, every one with findings. Two were CodeRabbit; five were the
panel (rate-limited on nearly every head). Three rounds found defects introduced
by the previous round's fix, including one where deleting the gate
reintroduced a bug the gate had been masking.
Two findings worth reading even if you skip the rest:
kit_doctor's self-checkrehashes every kit-owned file, so any byte change fails it — a run can report
100% killed while nothing behavioural caught anything. One lens's first pass
reported 17/17; excluded, 7 had survived. Contract item 5 now warns about it.
one mutating files and the other "restoring" them — leaving a live mutant that
silently disabled a guard. Hence contract item 7.
Known gaps, stated rather than implied
--lensesis unverifiable from the engine (Verify review-lens coverage instead of self-reporting it (per-lens receipts) #32).review evidence:line prints on the poll render;dev_session.sh mergegates on
mergeablealone, so an autonomous lane never displays it.review.fallback_panel.lenses[].focusis agent-read, not engine-read.Verification
Tests 300 → 320. Mutation battery re-run with the drift test excluded: 7/7
caught by named behavioural tests.
Conflict of interest, stated: I filed #26, built this, ran the panel on it,
and am merging it. The panel is the review of record for the current head
because CodeRabbit was rate-limited on all but two of them. Everything above is
checkable in the thread; none of it is independently attested.
https://claude.ai/code/session_011KVJzDj7Z2nYkUjqbGgSVp