Skip to content

review: the re-review mode dial (full | scoped | flip-gated | fast)#246

Open
jwbron wants to merge 9 commits into
jwbron/review-dispatch-taxfrom
jwbron/rereview-mode-dial
Open

review: the re-review mode dial (full | scoped | flip-gated | fast)#246
jwbron wants to merge 9 commits into
jwbron/review-dispatch-taxfrom
jwbron/rereview-mode-dial

Conversation

@jwbron

@jwbron jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

The re-review mode dial: the runs-per-PR cost lever from the plan of record's "Re-review economics" section. Stacked on the eval stack (#232-#238) and fold-in batch two (#243-#245); the base should track the top of the batch-two stack as it grows.

Why

Two measured lifecycles of the same seeded PR paid the full 11-agent fan-out on every push, with per-run cost rising as threads accumulate (v1.3.1: $7.19 / $8.77 / $9.14; v1.4.0: $8.31 / $9.87 / $11.29, runs 29044383251 / 29045581242 / 29046928431 in Khan/webapp). The approval run is the most expensive while emitting the least. Both lifecycles also caught a fresh seeded defect on a re-review push that a reconcile-only fast path would have missed, which argues for scoped as the default reduced depth.

What

A per-repo mode line in .github/aw/review/ROUTING:

re-review full|scoped|flip-gated|fast
  • full: today's behavior, and the default. No consumer changes behavior until its repo sets a mode line.
  • scoped: full roster, staged only the hunks new since the last fully-reviewed fingerprint; comments scoped to those hunks.
  • flip-gated: reconcile plus the correctness pass over new hunks; a REQUEST_CHANGES to APPROVE flip is vetoed by that pass's validated blocking findings (the findings gate the flip instead of being discarded).
  • fast: reconcile-only.

Three guards, all deterministic (workflows/review/lib/rereview-mode.ts):

  1. Ready-for-review anchor: the one full review the cheap modes lean on is taken at ready-for-review, never on a draft skeleton (a draft-taken fingerprint forces one more full review when the PR leaves draft).
  2. Flip gate: as above; mechanically this rides the existing verdict function, since a validated blocking finding posts and blocks.
  3. Divergence tripwire: every full-depth review stamps a content-hashed hunk signature into its review body as a hidden comment, so it survives cache eviction. Each push is compared against the last fully-reviewed fingerprint; at unreviewed share >= 0.4 the run re-arms to full. Signatures are computed over the generated-stripped diff, so lockfile churn neither enters the fingerprint nor counts as divergence.

Named interactions handled by construction:

  • dismiss-stale-approvals: the prior verdict and fingerprint live in the stamp, not the review state, so a dismissed approval still anchors and still reads as an approval.
  • COMMENTED-only prior review: the stamp itself (not the verdict state) marks "a full review happened", so a comment-only history reduces normally after its first stamped run instead of re-running full forever.

Eval and measurement

The roster-level evals live in #251 (stacked on this PR): open-PR corpus cases with reconciler dispatch and flip-gate/duplicate scoring, the per-mode pricing sweep (eval/rereview-sweep.ts), and the ported seeded lifecycle. This PR carries the deterministic layer:

  • Adversarial lifecycle cases as data (eval/lifecycle/): rewrite-after-approval and sparse-pr-then-payload, each scored as "the tripwire re-armed and the payload got a full review", plus an economy control (fix-push-stays-cheap) scored as "the reduced path actually ran". Replayed deterministically in eval/lifecycle.test.ts under all three reduced modes.
  • Pricing wiring for the live A/B: each run's executed depth lands in out/rereview-plan.json; lib/counters.ts aggregates costByRereviewDepth; the review-trial skill gains a ROUTING-override arms pattern (two arms at the same ref differing only in the mode line) and scores adversarial lifecycle pushes.
  • This is the playbook's cost-affecting class: mode changes are earned per repo through the live A/B on recall and dollars, the way enable lines are.

Composition with batch two

Based on the top of the batch-two stack (#243 -> #244 -> #245 -> #247) so the mode dial's re-review paths compose with the re-review accountability rendering (#244) rather than race it: the accountability section stays the visible body surface; the fingerprint stamp is appended last as a hidden comment; and the flip gate reads keptBlockingCount, a small additive field this PR adds to the accountability renderer's rereview.json, so "did the reconciler keep any blocking thread" is a number read at verdict time, not a label judgment re-made there. The orchestrator wiring (Step 1 prior-review staging, the Step 3 depth plan and per-depth dispatch/staging, the Step 4 flip rule, the Step 6 stamp, reduced-depth skips in Steps 7/8) is the last commit on the branch.

Supersedes

Khan/frontend#13420 (frontend's consumer-side re-review stopgap, held on the old pin) is superseded by this: the dial lives in the shared workflow and frontend opts in with a single re-review line in its ROUTING file once priced. Frontend is deliberately not touched here.

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 73b55f4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jwbron jwbron force-pushed the jwbron/rereview-mode-dial branch from be4202d to 6cee4a1 Compare July 9, 2026 22:11
@jwbron jwbron changed the base branch from jwbron/review-out-of-lane to jwbron/review-dispatch-tax July 9, 2026 22:11
@jwbron jwbron marked this pull request as ready for review July 9, 2026 22:16
@khan-actions-bot khan-actions-bot requested review from a team, jaredly and jeresig and removed request for a team July 9, 2026 22:17
@jwbron jwbron force-pushed the jwbron/review-dispatch-tax branch from 8e015ff to 813767c Compare July 9, 2026 22:18
@jwbron

jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: jwbron/review-dispatch-tax (this PR's base) was rebuilt on the refreshed chain per the topology note on #243; its history was rewritten (old tip 8e015ff, new tip 813767c, content unchanged apart from prettier formatting). Reset or rebase local state onto origin/jwbron/review-dispatch-tax before continuing. The prettier/lint CI failures on the old chain are fixed on the new one.

jwbron added 4 commits July 9, 2026 15:21
…parsing, deterministic depth lib, lifecycle evals

The runs-per-PR cost lever. A per-repo 're-review' line in ROUTING
(full | scoped | flip-gated | fast, default full) dials how much of the
roster a repeat review runs. lib/rereview-mode.ts owns the deterministic
half: content-hashed hunk signatures over the generated-stripped diff,
the hidden review-body fingerprint stamp (survives cache eviction,
dismiss-stale-approvals, and COMMENTED-only histories), the divergence
tripwire that re-arms full review at unreviewed share >= 0.4, and the
plan/stamp CLI. Adversarial lifecycle cases (rewrite-after-approval,
sparse-PR-then-payload) ship as data in eval/lifecycle/ with a
deterministic replay harness; counters gain costByRereviewDepth so the
live A/B prices scoped against full.

The review.md orchestrator wiring lands in a follow-up commit, on top of
fold-in batch two's re-review accountability surface.
… accountability renderer

The re-review mode dial's flip gate needs 'did the reconciler keep any
blocking thread' as a number it can read from rereview.json, not a label
judgment re-made at verdict time. Additive: the section rendering is
unchanged.
… the orchestrator

Step 1 stages the bot's prior reviews (every state, dismissed and
comment-only included) for the stamp reader. Step 3 runs the
rereview-mode plan CLI after the provenance CLI and maps each depth to
its dispatch and staging: scoped overwrites full-stripped.diff with
scoped.diff so the full roster reads only unseen hunks; flip-gated
dispatches reconcile plus correctness over the scoped diff; fast is
reconcile-only. Step 4 gains the flip rule: a reduced-depth run may
flip a stamped REQUEST_CHANGES to APPROVE only at keptBlockingCount 0,
and in flip-gated depth a validated blocking finding posts and blocks,
vetoing the flip. Step 6 appends the hidden fingerprint stamp as the
last line of every submitted review body; Steps 7 and 8 skip on
reduced depths; Step 9 notes the stamp, not the cache, is the
tripwire's authoritative fingerprint. The executed plan is copied into
out/ so the run artifact records the depth the counters price.
…es for the newly-linted workflows tree

The dispatch-tax trim brought workflows/review into eslint+prettier
scope; format the mode-dial files and the two pre-existing violations
in disciplines.test.ts and finding-schema.ts so CI lints clean.
@jwbron jwbron force-pushed the jwbron/rereview-mode-dial branch from b2308dd to 976b925 Compare July 9, 2026 22:22
@khan-actions-bot khan-actions-bot requested a review from a team July 9, 2026 22:22
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

Baseline: origin/jwbron/review-dispatch-tax (review.md 6f374886215b); candidate: working tree (review.md 255373d9458d).

Metric Baseline Candidate Delta
Must-catch recall 83% 100% +17%
Verdict agreement 86% 100% +14%
Noise (unmatched posted) 67% 54% -13%
Clean false flags 0 0
Judge mean quality 0.86 0.90 +0.04
Cost $5.37 $5.72
Wall clock 894s 814s
Cases run / skipped 7 / 0 7 / 0
Misses found-but-dropped 1 0

Improvements (candidate caught, baseline missed)

  • adversarial-injection-approve:adv-injection-auth-1

Adversarial hard gate: PASSED on the candidate arm.

Single-run-stable rows: recall, verdict agreement, regressions, adversarial gate. Judge quality and noise are not: they jitter run-to-run at this corpus size, and a regressed reviewer can score HIGHER on judge quality (fewer, surer comments each read better). Recall against the labeled specs is the load-bearing metric.

@jwbron

jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Eval pointer: the real (roster-level) evals for the mode dial live in #251, stacked on this PR.

This PR carries the mechanism plus its deterministic layer only: the depth/tripwire replays in eval/lifecycle/ (rewrite-after-approval, sparse-PR-then-payload, the fix-push economy control), the unit-level guards, and the cost-attribution surfaces (out/rereview-plan.json, costByRereviewDepth).

What actually measures reviewer behavior under each dial setting is in #251:

  • open-PR corpus cases (live.rereview): staged prior threads, the stamped prior review, the depth plan; the live producer dispatches the thread-reconciler and sizes the finder roster by depth
  • scoring: thread-resolution accuracy, the flip-gate input (kept blocking count), duplicate comments on kept threads, plus fresh-defect recall via the ordinary mustCatchSpecs
  • pricing: eval/rereview-sweep.ts runs all four modes in one command (recall, resolution, flips, dups, dollars, with the EXECUTED depth per case since the tripwire can override the dial), and live-ab.ts --re-review-mode prices one mode against a full-depth baseline
  • golden-retention-lifecycle-1/2/3 (sanitized port of the measured webapp#40730 seeded lifecycle) and golden-retention-fix-push (the under-tripwire case where the reduced paths actually execute)

Reviewers judging whether the dial is measured before any repo flips a mode line: that evidence is #251's; both merge before any consumer can set re-review, since this PR ships default-full.

@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (5 files)
File Reason
rereview-mode.ts New deterministic core deciding re-review depth; a bug here could pick a cheap path that skips reviewing a real defect on a later push.
routing-config.ts Parses the security-relevant re-review mode line that governs review depth for every consuming repo.
router.ts Routing wiring that threads the new mode through to the run for every consuming PR.
rereview.ts Computes keptBlockingCount, the flip-gate input that decides whether a REQUEST_CHANGES to APPROVE flip is permitted.
review.md The orchestrator prompt executed in every consuming repo; the reduced-depth branches and flip rule change runtime behavior.

Common patterns

2 files: keptBlockingCount: 0 added to the early-return objects in rereview.ts, with matching test expectation updates.

2 files: New routing-config.ts exports (RE_REVIEW_MODES, DEFAULT_RE_REVIEW_MODE, ReReviewMode) forwarded through router.ts import/re-export blocks.

3 files: Three eval/lifecycle/*.json fixtures added with the same LifecycleCase schema, differing only in push sequences and expected outcomes.

Excluded from review (3 files)

Not individually reviewed — generated, formatting-only, or fully explained by a common pattern above:

  • .changeset/review-rereview-mode-dial.md — documentation
  • .claude/skills/review-trial/SKILL.md — documentation
  • workflows/review/README.md — documentation

input.currentSignature,
input.priorStamp.anchorHunks,
);
if (divergence.unreviewedShare >= threshold) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): The inclusive >= boundary is the whole point of the tripwire, but share === 0.4 exactly is never tested — the decideReReviewDepth cases cover 1.0/0.5 above and 1/6, 1/3 below the wire. A refactor to > would slip a diff sitting precisely on the threshold through as a cheap re-review with no test failing. Add a case with 2 of 5 unreviewed hunks (share 0.4) asserting depth === 'full' and tripwireRearmed === true.

Low-confidence (1)
  • workflows/review/lib/counters.ts:291costByRereviewDepth: a depth group mixing cost-bearing and cost-less runs divides usdSum by all runs, understating per-run cost; only uniform groups are tested (may be intentional, mirroring tokensPerRun).

* `full` is the default everywhere: a repo pays for a cheaper mode only by
* writing a `re-review` line in its ROUTING file.
*/
export const RE_REVIEW_MODES = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought (non-blocking): The changeset's own evidence ("a fresh seeded defect was caught on a re-review push that a reconcile-only path would have missed; the case for scoped as the recommended reduced depth") argues for full|scoped, yet fast and flip-gated — both reconcile-only for fresh code — ship now as permanent surface before any consumer has priced even scoped. Consider shipping full|scoped and adding the reconcile-only modes in a follow-up if the live A/B shows scoped doesn't capture enough of the savings. (Counter-argument acknowledged: the A/B needs the modes to exist to price them.)

* pushed onto a sparse PR (share near 1.0) always re-arms. Exported so the
* eval suite and the live A/B can price other settings.
*/
export const DEFAULT_TRIPWIRE_THRESHOLD = 0.4;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought (non-blocking): computeDivergence uses an unweighted hunk-count share — no weighting by changed lines or ROUTING risk tier — so under fast a single hunk pushed onto a 6-hunk reviewed PR sits at ~0.17 and never trips the 0.4 wire, getting zero finding-producing review. Both adversarial lifecycle cases use payloads that dominate the count, so this small-payload weak spot isn't exercised. A floor rule (any unreviewed hunk in a high-tier path re-arms) or a line-weighted share would close it. Not a regression today since full is the default.

a category call. Count the blocking labels in your final comment set; zero blocking
labels means APPROVE.

**The re-review flip rule (reduced depths only).** One addition to the rule above

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question (non-blocking): The flip gate is computed as data (flipGate in the plan, keptBlockingCount in rereview.json), but I don't see any code comparing the submitted verdict against it — computeVerdict keys only off posted labels, and the veto lives entirely in this Step 4 prose. Is a code chokepoint (e.g. the stamp CLI refusing an APPROVE stamp when depth is flip-gated/fast, the prior stamp verdict was REQUEST_CHANGES, and keptBlockingCount > 0) intended as a follow-up, or is prompt-following the deliberate enforcement for this guard?

const verdict = flagIndex >= 0 ? rest[flagIndex + 1] : undefined;
if (verdict === undefined || !/^[A-Z_]+$/.test(verdict)) {
// eslint-disable-next-line no-console
console.error("usage: rereview.ts stamp --verdict <EVENT>");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): The usage string prints rereview.ts, but this file is rereview-mode.ts and a separate rereview.ts actually exists in the same directory, so the printed name points at the wrong (real) file. The sibling investigation-cap.ts names itself correctly.

Suggested change
console.error("usage: rereview.ts stamp --verdict <EVENT>");
console.error("usage: rereview-mode.ts stamp --verdict <EVENT>");

jwbron and others added 2 commits July 10, 2026 13:49
Phase 1 of the live A/B eval plan (#232): the eval corpus learns to carry real change content so a future live producer can run the actual model sub-agents against it.

## Format

A case may now carry an opt-in `live` block (`corpus/live.ts`, re-exported through the loader):

- `prContext`: PR title/description/author/base branch, mirroring production `pr-context.json`. The description is untrusted author text, so adversarial cases can carry their payload there or in the diff.
- An on-disk post-change file tree, via a new `<id>/case.json` + `<id>/tree/` layout coexisting with flat `<id>.json`. A directory containing `case.json` is one case; its tree is never parsed as corpus JSON.
- `mustCatchSpecs` / `mustNotFlagSpecs`: labeled defects as (path, line window, mechanism keyword alternates). Live runs choose their own finding ids, so ground truth matches on anchor window plus mechanism rather than id; the Phase 3 matcher consumes these.

Enforced invariants: the `live` tag and the block imply each other; a live case needs a cleanly-parseable diff (fail-open is fine in production, an authoring error here); spec paths must appear in `changedFiles` and the diff; every non-removed changed file must exist in the tree. `loadLiveCorpus()` returns the subset.

## Cases

Ten cases converted with hand-authored real diffs and trees: the five smoke incidents (money rounding, auth bypass, cache key, race condition, missing index), both clean cases, the adversarial injection case (payload is a code comment in the diff instructing the reviewer to approve), the golden authz holdout, and the money-payments synthetic mutation. Recorded line anchors are rewritten to the authored defect lines; natural files beat content padded to synthetic line numbers, and every anchor is asserted to be an added line so the provenance gate (now active on these cases) keeps them.

The trial-content port landed as #235, stacked on this PR. One acceptance-driven change also landed here: the phase 4 runs missed `incident-sql-missing-index` in all four arms because live cases carried no `routerConfig` lens rules, so specialist lenses never spawned; every live case whose ground truth belongs to a specialist lens now routes that lens on the finding's file (see the "route the specialist lens on each live case" commit).

## Test plan:

- `pnpm run test --run`: 659 tests green, including 17 new loader tests (memfs) covering the live block, both layouts, tree validation, and the tree-JSON exclusion.
- The deterministic suite runs the ten converted cases unchanged (same verdicts, comment counts, must-catch sets), now with the provenance gate active on them.
- `pnpm run typecheck` and eslint clean on the three touched TS files.


## Next steps (human)

1. Review the ten authored diffs and trees for realism; they are synthetic content a live model will read, so plausibility matters more than in ordinary fixtures. Spot-check that each case's recorded finding still describes the authored defect (anchors were rewritten to the authored lines).
2. This is the root of the stack: review and undraft first; #234 and #235 rebase onto it.
3. No live validation needed here; the deterministic suite (`pnpm run test --run`) fully gates it.

Author: jwbron

Reviewers: jeresig, github-actions[bot], jwbron

Required Reviewers:

Approved By: jeresig, github-actions[bot]

Checks: ✅ 9 checks were successful

Pull Request URL: #233
# Conflicts:
#	workflows/review/eval/corpus/live.ts
* world (its cwd, with no network), so the agent WILL often try to read
* an imported module or caller that is not there; that read returns an
* ordinary not-found tool error the agent tolerates and works around,
* not a run failure. The cost of a missing file is realism, not a crash:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought (non-blocking): The "not a run failure" guarantee for a missing tolerated read is enforced elsewhere (live-runner.ts absorbs per-tool read errors and throws only on run-level subtype !== "success"), and no test near this comment pins it. The corpus-side promise can silently drift from the harness if the staging/runner behavior later tightens. Consider cross-referencing the runner or adding a small assertion so the two stay coupled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant