Skip to content

Make the fallback review a panel, not a command (#26)#31

Merged
topij merged 10 commits into
mainfrom
dev/fallback-review-panel
Jul 25, 2026
Merged

Make the fallback review a panel, not a command (#26)#31
topij merged 10 commits into
mainfrom
dev/fallback-review-panel

Conversation

@topij

@topij topij commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Closes #26. Follow-up filed as #32.

What this is now

review.fallback_panel replaces review.fallback_commands as the primary
substitute when a review bot can't review: one isolated, fresh-context
reviewer per lens
, shipping the two the doctrine names (adversarial,
correctness). fallback_commands remains as the explicitly degraded one-lens
mode for a runtime that cannot isolate a reviewer.

The split follows Principle #10which 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:

  1. Match the receipt's source string. fallback:panel (adversarial + correctness) accepted with zero lenses and no warning — while the honest
    fallback:codex --lenses correctness produced the ⚠. The quietest receipt
    was the dishonest one.
  2. Report the recorded lens count instead. A newline in source split the
    render line so the first half read as a completed panel; , is both the lens
    separator and ordinary punctuation.
  3. Require names from the configured roster. Refused a genuine third ad-hoc
    lens, making the doc's own "two disjoint lenses is the floor, not the
    ceiling" false.
  4. Count roster names, allow extras. The record gate matched source with
    == while the render matched with startswith, so one decorated character
    defeated the gate and the render then affirmed the forgery.

safety-critical-changes.md rule 1: "Treat 'we tightened the matcher' as a
stopgap, not a fix."
Four tightenings is the signal. The mechanism is
deleted
pr_watch.py loses ~100 lines of unverifiable checking.

--lenses is still recorded and shown at merge time, labelled as what it is: a
claim, self-reported by whoever ran --record-review, which the engine does
not 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_hook now names the panel. It 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 degrades to the single command when
    fewer than two distinct lenses are configured.
  • fallback-review-panel.md carries the lens contract: fresh context, the
    raw 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
    .
  • A control-character hole in the renders. _flat collapsed whitespace, but
    \x1b is not whitespace, so ANSI cursor control passed through — and
    \x1b[1A\x1b[2K erases lines that exist. Fixed there and in _excerpt,
    which is worse: it renders comment bodies anyone can write, and renders last.
  • safety-critical-changes.md rules 2 and 3 now point at the panel doc, and
    rule 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:

  • Mutation testing this repo reports false kills. kit_doctor's self-check
    rehashes 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.
  • The two lenses corrupted each other. Run concurrently in one working tree,
    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

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

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
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Fallback Review Panel

Layer / File(s) Summary
Panel contract and configuration
.agents/skills/pr-watch/SKILL.md, config/dev-model.yaml, docs/agentic-dev-kit/*, docs/CLAUDE-sections.md, init.sh, scripts/kit_doctor.py, kit-manifest.json
Defines review.fallback_panel with adversarial and correctness lenses, documents isolated and degraded execution, migrates the schema, and tracks the new doctrine artifact.
Lens-aware review receipts
scripts/pr_watch.py, scripts/tests/test_pr_watch.py
Adds --lenses handling, persists and validates lens names, renders head-bound coverage, distinguishes single-lens receipts, and tests receipt/config behavior.
Fallback reminder routing
scripts/hooks/pr_followup_hook.py, scripts/tests/test_pr_followup_hook.py
Loads configured lenses and generates panel-based reminders when available, while retaining degraded command wording when isolation is unavailable.
Workflow and session integration
docs/agentic-dev-kit/workflows/pr-watch.md, docs/autonomous-session-playbook.md
Updates unavailable-review procedures to run isolated lenses, record panel receipts, pass executed lens names, and bind receipts to the polled head SHA.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the panel spec, lenses, receipt source, degraded fallback_commands mode, and review evidence required by #26.
Out of Scope Changes check ✅ Passed The touched docs, config, scripts, and tests all support the fallback-review panel workflow and related enforcement.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: replacing the fallback review command with a panel-based fallback review.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/fallback-review-panel

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef5fd6 and 7000b05.

📒 Files selected for processing (13)
  • .agents/skills/pr-watch/SKILL.md
  • config/dev-model.yaml
  • docs/CLAUDE-sections.md
  • docs/agentic-dev-kit/fallback-review-panel.md
  • docs/agentic-dev-kit/safety-critical-changes.md
  • docs/agentic-dev-kit/workflows/pr-watch.md
  • docs/autonomous-session-playbook.md
  • init.sh
  • kit-manifest.json
  • scripts/hooks/pr_followup_hook.py
  • scripts/pr_watch.py
  • scripts/tests/test_pr_followup_hook.py
  • scripts/tests/test_pr_watch.py

Comment thread docs/agentic-dev-kit/fallback-review-panel.md Outdated
Comment thread docs/agentic-dev-kit/fallback-review-panel.md Outdated
Comment thread docs/agentic-dev-kit/workflows/pr-watch.md Outdated
Comment thread docs/agentic-dev-kit/workflows/pr-watch.md Outdated
Comment thread docs/CLAUDE-sections.md
Comment thread scripts/hooks/pr_followup_hook.py
Comment thread scripts/hooks/pr_followup_hook.py Outdated
Comment thread scripts/pr_watch.py
Topi Jarvinen added 3 commits July 25, 2026 23:58
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

The bots_behind_head guard stops at the container.

isinstance(behind_map, dict) protects against a non-dict, but sha[:7] still assumes each value is a string — a hand-edited {"coderabbit": 5} raises TypeError in the render this guard exists to keep crash-free. Same argument as the lenses element-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 value

Rename the l loop variables (E741).

Ruff flags l as ambiguous at lines 2344, 2383, 2452, 2491, 2495 and 2499; line reads 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 value

Escape 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 re at 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7000b05 and 57a09ab.

📒 Files selected for processing (13)
  • .agents/skills/pr-watch/SKILL.md
  • docs/CLAUDE-sections.md
  • docs/agentic-dev-kit/fallback-review-panel.md
  • docs/agentic-dev-kit/workflows/pr-watch.md
  • docs/autonomous-session-playbook.md
  • init.sh
  • kit-manifest.json
  • scripts/hooks/pr_followup_hook.py
  • scripts/kit_doctor.py
  • scripts/pr_watch.py
  • scripts/tests/test_portability.py
  • scripts/tests/test_pr_followup_hook.py
  • scripts/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

Comment thread scripts/hooks/pr_followup_hook.py
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
@topij

topij commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Fixed — with one correction to the finding's framing: _DEFAULT_LENSES was [], so the hook already took the degraded path on an unreadable config. The hazard was latent, not live: the name implied a default panel existed, and populating it would have produced exactly the failure described. The constant is removed rather than emptied, and a test now pins that an unreadable config yields the single-command wording.

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
@topij

topij commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full 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
@topij
topij force-pushed the dev/fallback-review-panel branch from 910f0d5 to 30c7948 Compare July 25, 2026 23:05
Topi Jarvinen added 3 commits July 26, 2026 02:26
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
@topij

topij commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Merging. Receipt: fallback:panel --lenses adversarial,correctness at 6d56440, carrying bots_behind_head — CodeRabbit's last review was of 57a09ab, four commits back.

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, --lenses is labelled as the self-reported claim it always was, and #32 carries the design that would actually work, with all four defeats written up.

Two findings I'd read even if you skip the rest:

  1. Mutation testing this repo reports false kills. kit_doctor's self-check rehashes 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. I verified it directly. That invalidates confidence from several earlier rounds, on this branch and probably others this session. Contract item 5 now warns about it.
  2. The two lenses corrupted each other. Run concurrently in one working tree — one mutating files to test them, the other seeing that as external corruption and running git checkout -- to restore it. When I stopped the adversarial lens it left a live mutant behind that silently disabled a guard, and I caught it only because a test failed with an error string that should no longer have existed. Hence contract item 7: isolated worktrees.

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.

@topij
topij merged commit 7485512 into main Jul 25, 2026
3 checks passed
@topij
topij deleted the dev/fallback-review-panel branch July 25, 2026 23:41
topij pushed a commit that referenced this pull request Jul 26, 2026
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
topij added a commit that referenced this pull request Jul 26, 2026
…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>
topij pushed a commit that referenced this pull request Jul 26, 2026
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
topij added a commit that referenced this pull request Jul 26, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

review.fallback_commands should be a panel spec, not a single inline command

1 participant