Turn an adversarial review into acceptance checks and one regression test per defect

Claude Code turns each defect from a review of one AI output into a check that runs on every result, a mock-model replay and a named offline regression test.

Claude Code advanced Tested Opus 5.5 · Nov 2026May be dated

The prompt

Run it in Claude Code at the root of the app that generated the output, on a branch: it changes how results are generated and saved (a re-prompt loop, and results with a P0 failure are no longer saved), not only the tests. First commit the request and the model's raw reply that the review covers. Paste the review's defect list as written, one defect per line, each with an ID and a severity. If the review has none, number the defects and give each a severity from the scale in the prompt.

Fill in the blanks4 placeholders · empty ones stay as {{…}}

Example

Input:

  • output_type: weekly dinner plan (seven dinners with recipes, a shopping list, a cost estimate and a one-line summary)
  • defect_list:
    D1 · P0 · Thursday's "Peanut satay noodles" uses peanut butter; the household brief excludes peanuts (allergy).
    D2 · P0 · The summary says "about EUR 52 for the week"; the shopping-list lines add up to EUR 57.10, and the recipes' own ingredient costs come to EUR 63.00, over the EUR 60 budget. No warning.
    D3 · P1 · The shopping list doesn't cover the recipes: feta (Wed) and coconut milk (Fri) are missing; chickpeas are listed at 400 g, but Mon and Wed need 800 g.
    D4 · P2 · Weekday dinners over the 30-minute cap: Tuesday's beef lasagne takes 75 minutes.
    D5 · P3 · Rice appears twice on the shopping list in different units: "300 g" and "2 cups".
    
  • reviewed_output: samples/brief.json and samples/reviewed-plan.json
  • test_runner: node:test, run as node --experimental-strip-types --test test/*.test.ts (TypeScript, no dependencies)
  • The repo: a small TypeScript meal planner with no tests. src/generate.ts makes one model call for the whole week and parses the JSON reply into the MealPlan type; src/api.ts saves the plan and returns it as done. The household brief asks for 4 people, no peanuts, weekday dinners of 30 minutes at most and EUR 60 a week.

Output (trimmed to the opening lines, the coverage table and part of the test output):

Code fixes and checks now catch all five defects, and re-prompts repair the rest. On the reviewed reply, the pipeline makes 5 model calls and ends with all 8 checks passing. All 27 tests pass offline, and a strict tsc check is clean.

…

ID Sev Check id Fix Test
D1 P0 no-excluded-ingredients (searches titles, ingredients and steps, including other names: peanut, groundnut, satay, arachis, monkey nut) Re-prompt Thu. Taking the peanut butter out would break "Peanut satay noodles". The shopping list drops it automatically because it is rebuilt from the recipes. D1: no dinner contains an excluded ingredient (peanuts), in any form
D2 P0 cost-figures-match-recipes, prose-figures-match-plan, within-budget Code: recompute the cost, rebuild the list, rewrite the summary's figures, add an over-budget warning. Re-prompt: the EUR 3.00 gap goes to the costliest dinners (Fri cuts 2.54, Sun cuts 0.46). The result is blocked if it is still over budget. D2: the estimated cost, the shopping-list total and the summary state the recipes' real cost, which stays within the weekly budget
D3 P1 shopping-list-covers-recipes Code: the list is rebuilt from the recipes D3: the shopping list holds every recipe ingredient in the total quantity the recipes use
D4 P2 weekday-prep-cap (uses the longer of prepMin and the longest timed step) Code swaps the dinner with a weekend dinner that fits the cap; otherwise re-prompt. Here Sat (60 min) and Sun (90 min) don't fit, so Tue is re-prompted. D4: weekday dinners (Mon-Fri) take at most the brief's 30-minute cap
D5 P3 shopping-list-one-line-per-ingredient Code merges lines by converting units (rice at 185 g a cup gives 670 g). If the units can't be converted, it only warns. D5: each ingredient is on the shopping list once, in one unit

…

No defect is marked "review only". The one limit: the checks trust the model's cost estimate for each ingredient. Telling whether those prices are realistic would need a price source, and no defect asked for that.

…
✔ before repair: code fixes already pass D2's figures, D3 and D5; D1, D2's budget and D4 still fail
…
✔ re-prompts reach only the failing units and the units handed the budget shortfall, within 2 rounds
✔ severity P0: a repair that leaves a P0 failing after the last round blocks the result, and nothing is saved
✔ severity P2: a repair that leaves a P2 failing ships, with that check visible and listed first
✔ severity P3: a P3 failure only warns and makes no re-prompt
✔ discard: a repair reply that doesn't parse is dropped, and the unit keeps its content and reasons
✔ discard: a repair that fails at a worse level than the unit it would replace is dropped
✔ clean: a reply with none of the defects passes every check, before and after the code fixes, with no re-prompt
…
ℹ tests 27  ℹ pass 27  ℹ fail 0

The code fixes alone cleared D2's figures, D3 and D5. One round of re-prompts then went to Tue (D4), Thu (D1) and the two costliest dinners, Fri and Sun, for the EUR 3.00 budget gap, and left Mon, Wed and Sat untouched.

Why it works

  • One defect, three artefacts. Each ID becomes a check that runs in production, a replay in the mock and a named test, tied together by the coverage table, so the same mistake won't come back unflagged, as far as its check is precise. The fixture is the reviewed reply itself, not a reply rebuilt from the defect list, so the replay proves something. "Review only" rows show what code can't judge instead of hiding it behind a weak check.
  • Code computes, the model writes. Totals, derived lists and dates are what models get wrong and code gets right every time, so computing them removes the defect rather than catching it later; re-prompting for them would be wasted, since code discards the model's version. Dropping a duplicate or an item past a limit in code costs no call and can't add a new mistake, so the model only fills the gap. One rules module keeps checks, fixes, mock and tests on the same limits.
  • The failure message is the re-prompt. A failing check names the items and the rule, so one string drives the re-prompt, the UI row and the test failure. Only failing units and those handed a shortfall go back, at most twice, never what the user edited, and the "Already used" list stops a redone unit from repeating the others. A redo that fails worse is dropped, so a fix for a slow recipe can't bring back the allergen. Severity sets the consequence: P0 blocks, P1 and P2 ship flagged, P3 only warns.
  • Tests prove both directions. Asserting the property directly catches a buggy check that would pass its own test. Running the checks on the raw reply proves that even the checks behind code fixes fire, so a fix that regresses is caught; the clean fixture proves the checks stay quiet on good output. The heading is a shared constant, so the mock can tell a re-prompt from a first call, and the scripted replies let CI run it all offline in seconds.

Variations

  • No UI yet. Return checks in the API response and log the failed check ids. The pass rate per check is a cheap quality metric when you change the model or the prompt.
  • Live eval. Swap the mock for the real model and run the defect fixtures' requests as an eval: same checks, real replies.
  • The next review. Append new defects to the same fixture and table as D6, D7… and never renumber, so test names stay stable across reviews.
  • Staged generation. Pairs with Model proposes, code disposes, which splits one big call into checked stages and gives the full re-prompt layout under the same heading.
  • Generalised from a trip-planner app.