Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2d746ac
[jwbron/live-eval-corpus] review: live-enabled corpus format and ten …
jwbron Jul 9, 2026
6727eb6
[jwbron/live-eval-producer-staging] review: live-producer prompt extr…
jwbron Jul 9, 2026
5c00cf3
[jwbron/live-eval-producer-staging] review: the live producer and SDK…
jwbron Jul 9, 2026
52073ef
[jwbron/live-eval-ab-runner] review: the live A/B runner (phase 3)
jwbron Jul 9, 2026
93770e6
[jwbron/live-eval-ab-ci] review: per-PR live A/B workflow (phase 4)
jwbron Jul 9, 2026
30e7527
[jwbron/live-eval-corpus] review: exclude eval-corpus trees from lint…
jwbron Jul 9, 2026
19f06cb
[jwbron/live-eval-producer-staging] Merge branch 'jwbron/live-eval-co…
jwbron Jul 9, 2026
d1b5288
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
254dfc4
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
917cc11
[jwbron/live-eval-producer-staging] review: namespace live finding id…
jwbron Jul 9, 2026
0ee295a
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
58cd4ed
[jwbron/live-eval-ab-runner] review: judge failures degrade the A/B r…
jwbron Jul 9, 2026
b19b08d
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
bced291
[jwbron/review-trial-skill] review: add the review-trial skill (live …
jwbron Jul 9, 2026
da1115c
[tmp-refresh] Merge remote-tracking branch 'origin/main' into tmp-ref…
jwbron Jul 9, 2026
a6883be
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-c…
jwbron Jul 9, 2026
0d02672
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-p…
jwbron Jul 9, 2026
93d8dec
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-a…
jwbron Jul 9, 2026
e3b34eb
[tmp-refresh] Merge remote-tracking branch 'origin/jwbron/live-eval-a…
jwbron Jul 9, 2026
491a983
[jwbron/review-rereview-accountability] review: re-review accountabil…
jwbron Jul 9, 2026
5dd182b
[jwbron/review-out-artifact-upload] review: fix the out/ artifact upl…
jwbron Jul 9, 2026
92bffa2
[jwbron/review-rereview-accountability] review: prettier-format the r…
jwbron Jul 9, 2026
2812679
[jwbron/live-eval-corpus] review: route the specialist lens on each l…
jwbron Jul 9, 2026
2ce35a0
[jwbron/live-eval-producer-staging] Merge branch 'jwbron/live-eval-co…
jwbron Jul 9, 2026
c0fece2
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
e02ac40
[jwbron/live-eval-ab-runner] review: carry agent-failure reasons into…
jwbron Jul 9, 2026
d225bd4
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
cbc838d
[jwbron/live-eval-ab-runner] review: close every A/B report with a pe…
jwbron Jul 9, 2026
63097f1
[jwbron/live-eval-corpus] Merge remote-tracking branch 'origin/jwbron…
jwbron Jul 9, 2026
9012508
[jwbron/live-eval-producer-staging] Merge remote-tracking branch 'ori…
jwbron Jul 9, 2026
25133b4
[jwbron/live-eval-producer-staging] Merge branch 'jwbron/live-eval-co…
jwbron Jul 9, 2026
0a3d212
[jwbron/live-eval-ab-runner] Merge remote-tracking branch 'origin/jwb…
jwbron Jul 9, 2026
a547972
[jwbron/live-eval-ab-runner] Merge branch 'jwbron/live-eval-producer-…
jwbron Jul 9, 2026
391151b
[jwbron/live-eval-ab-ci] Merge remote-tracking branch 'origin/jwbron/…
jwbron Jul 9, 2026
d2c4c70
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 9, 2026
996766f
[jwbron/live-eval-ab-runner] review: identity short-circuit, gate-fli…
jwbron Jul 10, 2026
fb81be8
[jwbron/live-eval-ab-runner] review: judge economics (Haiku pin, retr…
jwbron Jul 10, 2026
a659be8
[jwbron/live-eval-ab-ci] review: document why the baseline is the bas…
jwbron Jul 10, 2026
b7c3786
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 10, 2026
541e413
[jwbron/review-trial-skill] Merge branch 'jwbron/live-eval-ab-ci' int…
jwbron Jul 10, 2026
fd42efd
[jwbron/review-out-artifact-upload] Merge branch 'jwbron/review-trial…
jwbron Jul 10, 2026
6d3459a
[jwbron/review-rereview-accountability] Merge branch 'jwbron/review-o…
jwbron Jul 10, 2026
193ae69
[jwbron/live-eval-ab-runner] review: type the judge response via the …
jwbron Jul 10, 2026
7a2065c
[jwbron/live-eval-ab-ci] Merge branch 'jwbron/live-eval-ab-runner' in…
jwbron Jul 10, 2026
00ce9d4
[jwbron/review-trial-skill] Merge branch 'jwbron/live-eval-ab-ci' int…
jwbron Jul 10, 2026
4bd445c
[jwbron/review-out-artifact-upload] Merge branch 'jwbron/review-trial…
jwbron Jul 10, 2026
da1e1db
[jwbron/review-rereview-accountability] Merge branch 'jwbron/review-o…
jwbron Jul 10, 2026
80da666
Merge branch 'main' into jwbron/live-eval-corpus
jwbron Jul 10, 2026
82bc5ed
Merge branch 'jwbron/live-eval-corpus' into jwbron/live-eval-producer…
jwbron Jul 10, 2026
7f2fc3a
Merge branch 'jwbron/live-eval-producer-staging' into jwbron/live-eva…
jwbron Jul 10, 2026
9cc1a89
Merge branch 'jwbron/live-eval-ab-runner' into jwbron/live-eval-ab-ci
jwbron Jul 10, 2026
da93344
Merge branch 'jwbron/live-eval-ab-ci' into jwbron/review-trial-skill
jwbron Jul 10, 2026
aa5a246
Merge branch 'jwbron/review-trial-skill' into jwbron/review-out-artif…
jwbron Jul 10, 2026
248cdf8
Merge branch 'jwbron/review-out-artifact-upload' into jwbron/review-r…
jwbron Jul 10, 2026
c76b75d
[landtmp] Merge remote-tracking branch 'origin/main' into landtmp
jwbron Jul 10, 2026
1739c9b
[jwbron/review-rereview-accountability] Merge remote-tracking branch …
jwbron Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/review-rereview-accountability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"review": minor
---

Re-review accountability, code-rendered. Observed on the review-v1.4.0 re-run lifecycle (Khan/webapp#40730): run 2 resolved the fixed threads and said nothing about the three blocking threads it left open, under a bare "Changes requested" body; run 3 approved with an empty body while resolving 11 threads. The verdict body now accounts for every prior thread, rendered deterministically from the `thread-reconciler`'s keep/resolve lists by the new `lib/rereview.ts` (never composed by the model): each still-unaddressed prior thread is enumerated as a link to its earlier comment ("as of \<sha\>", blocking first, with a verbatim excerpt of the earlier comment's first line), with the resolved count stated; an approval that resolved the last open threads says every prior thread is resolved. `threads.json` staging gains a `url` field (the thread's first comment `html_url`) to power the links, `renderReviewBody` gains a `rereviewSection` slot, and the redundant-approval skip treats a non-empty section as content. Fail-open: a missing or unparseable staging input renders an empty section, leaving the body exactly as before.
6 changes: 6 additions & 0 deletions workflows/review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ read-only **sub-agents** (it makes every GitHub and comment call itself):
routing, plus a reconciler that resolves earlier bot threads the changes have addressed.
Every finding names a concrete `failure_scenario`: the specific inputs or state
and the wrong outcome they produce.
On a re-review the verdict body carries a code-rendered accountability section
(`lib/rereview.ts`, built from the reconciler's keep/resolve lists): every
still-unaddressed prior thread is enumerated as a link to its earlier comment,
blocking first, with the resolved count, and an approval that resolved the last
open threads states that every prior thread is resolved — resolving some threads
never leaves the rest silently open.
3. If those reviewers proposed any comments, **`claim-validator`** re-checks each one
against the actual code (attacking the finding's stated failure scenario) and,
for best-practice claims, against the relevant skill's
Expand Down
9 changes: 8 additions & 1 deletion workflows/review/lib/render-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ export type ReviewBodyInput = {
* `0` leaves the APPROVE body exactly as #194 rendered it.
*/
obligationCount?: number;
/**
* The code-rendered re-review accountability section
* (`rereview.ts`'s `renderRereviewSection`), spliced verbatim between the
* verdict head and the note lines. Empty or absent leaves the body exactly
* as before — a first review has no prior threads to account for.
*/
rereviewSection?: string;
};

/**
Expand Down Expand Up @@ -251,7 +258,7 @@ export const renderReviewBody = (input: ReviewBodyInput): string => {
`Note: ${dimension} not assessed this run (${subAgent} output unavailable).`,
);

const lines = [head, ...notes];
const lines = [head, input.rereviewSection ?? "", ...notes];

if (input.event === "HOLD_FOR_HUMAN") {
lines.push(
Expand Down
313 changes: 313 additions & 0 deletions workflows/review/lib/rereview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
import {describe, it, expect} from "vitest";

import {renderReviewBody} from "./render-comment";
import {
excerptOpeningComment,
parseLeadingLabel,
renderRereviewSection,
runRereviewCli,
type RereviewCliFs,
type StagedThread,
} from "./rereview";

/**
* Re-review accountability tests.
*
* The production failure this module exists for (the review-v1.4.0 re-run
* lifecycle on Khan/webapp#40730): run 2 resolved fixed threads and said
* nothing about the three blocking threads it kept open, under a bare
* "Changes requested" body; run 3 approved with an empty body while resolving
* 11 threads. The section must therefore (a) enumerate every kept thread as a
* link to its prior comment, blocking first, and (b) state the resolution
* count, including the all-resolved case an approval rides on.
*/

const thread = (overrides: Partial<StagedThread>): StagedThread => ({
thread_id: "PRRT_x",
path: "services/foo/foo.go",
line: 12,
url: "https://github.com/o/r/pull/1#discussion_r100",
comments: [
{
author: "github-actions",
body: "**issue (blocking):** The guard was removed.",
},
],
...overrides,
});

describe("parseLeadingLabel", () => {
it("extracts the label from the workflow's own comment template", () => {
expect(parseLeadingLabel("**issue (blocking):** Broken.")).toBe(
"issue (blocking)",
);
expect(
parseLeadingLabel(
"**suggestion (non-blocking, best-practice):** Consider X.",
),
).toBe("suggestion (non-blocking, best-practice)");
expect(parseLeadingLabel("**todo (blocking):** Add the field.")).toBe(
"todo (blocking)",
);
});

it("returns null for a body that does not start with the template", () => {
expect(parseLeadingLabel("Plain reply text.")).toBeNull();
expect(parseLeadingLabel("prefix **issue (blocking):** x")).toBeNull();
});
});

describe("excerptOpeningComment", () => {
it("strips the label prefix and keeps the first line", () => {
expect(
excerptOpeningComment(
"**issue (blocking):** First line.\nSecond line.",
),
).toBe("First line.");
});

it("truncates deterministically past the cap", () => {
const long = `**issue (blocking):** ${"a".repeat(300)}`;
const excerpt = excerptOpeningComment(long);
expect(excerpt.endsWith("...")).toBe(true);
expect(excerpt.length).toBeLessThanOrEqual(123);
});

it("passes a label-less body through verbatim", () => {
expect(excerptOpeningComment("No label here.")).toBe("No label here.");
});
});

describe("renderRereviewSection", () => {
it("renders nothing when the run started with no prior threads", () => {
const result = renderRereviewSection({
threads: [],
reconciler: {resolve: [], keep: []},
});
expect(result.section).toBe("");
expect(result.keptCount).toBe(0);
expect(result.resolvedCount).toBe(0);
});

it("states the all-resolved case an approval rides on", () => {
const result = renderRereviewSection({
threads: [thread({thread_id: "a"}), thread({thread_id: "b"})],
reconciler: {resolve: ["a", "b"], keep: []},
});
expect(result.section).toBe("All 2 prior review threads are resolved.");
});

it("uses singular wording for one resolved thread", () => {
const result = renderRereviewSection({
threads: [thread({thread_id: "a"})],
reconciler: {resolve: ["a"], keep: []},
});
expect(result.section).toBe("The 1 prior review thread is resolved.");
});

it("enumerates kept threads as links, blocking first", () => {
const threads = [
thread({
thread_id: "nb",
path: "a/a.go",
line: 1,
url: "https://github.com/o/r/pull/1#discussion_r1",
comments: [
{
author: "github-actions",
body: "**suggestion (non-blocking):** Nicer name.",
},
],
}),
thread({
thread_id: "blk",
path: "z/z.go",
line: 9,
url: "https://github.com/o/r/pull/1#discussion_r2",
}),
];
const result = renderRereviewSection({
threads,
reconciler: {resolve: ["other"], keep: ["nb", "blk"]},
headSha: "abcdef1234567890",
});
const lines = result.section.split("\n");
expect(lines[0]).toBe(
"1 of 3 prior review threads resolved; 2 still unaddressed as of abcdef1:",
);
// Blocking thread sorts first even though it was listed second.
expect(lines[1]).toBe(
"- **issue (blocking)** [`z/z.go:9`](https://github.com/o/r/pull/1#discussion_r2): The guard was removed.",
);
expect(lines[2]).toBe(
"- **suggestion (non-blocking)** [`a/a.go:1`](https://github.com/o/r/pull/1#discussion_r1): Nicer name.",
);
expect(result.keptCount).toBe(2);
expect(result.resolvedCount).toBe(1);
});

it("renders the zero-resolved header without a resolved clause", () => {
const result = renderRereviewSection({
threads: [thread({thread_id: "a"})],
reconciler: {resolve: [], keep: ["a"]},
});
expect(result.section.split("\n")[0]).toBe(
"1 of 1 prior review thread is still unaddressed:",
);
});

it("falls back to a plain token when the thread has no url", () => {
const result = renderRereviewSection({
threads: [thread({thread_id: "a", url: undefined})],
reconciler: {resolve: [], keep: ["a"]},
});
expect(result.section).toContain(
"- **issue (blocking)** `services/foo/foo.go:12`:",
);
expect(result.section).not.toContain("](");
});

it("anchors a null-line thread on the bare path", () => {
const result = renderRereviewSection({
threads: [thread({thread_id: "a", line: null, url: undefined})],
reconciler: {resolve: [], keep: ["a"]},
});
expect(result.section).toContain("`services/foo/foo.go`:");
});

it("still accounts for a keep id missing from the staging", () => {
const result = renderRereviewSection({
threads: [],
reconciler: {resolve: [], keep: ["ghost"]},
});
expect(result.section).toContain("thread ghost");
expect(result.keptCount).toBe(1);
});
});

describe("renderReviewBody with a re-review section", () => {
it("splices the section between the head and the notes", () => {
const body = renderReviewBody({
event: "REQUEST_CHANGES",
hasInlineComments: false,
rereviewSection:
"1 of 1 prior review thread is still unaddressed:\n- **issue (blocking)** `a.go:1`: x",
skippedDimensions: [
{dimension: "patterns", subAgent: "pattern-triage"},
],
});
expect(body.split("\n")).toEqual([
"Changes requested — see inline comments.",
"1 of 1 prior review thread is still unaddressed:",
"- **issue (blocking)** `a.go:1`: x",
"Note: patterns not assessed this run (pattern-triage output unavailable).",
]);
});

it("leaves the body untouched when the section is empty or absent", () => {
const withEmpty = renderReviewBody({
event: "APPROVE",
hasInlineComments: false,
rereviewSection: "",
});
const without = renderReviewBody({
event: "APPROVE",
hasInlineComments: false,
});
expect(withEmpty).toBe("Approved — no blocking issues found.");
expect(withEmpty).toBe(without);
});

it("makes an otherwise-empty body carry the accounting", () => {
// Run 3 of the lifecycle approved with an empty body while resolving
// 11 threads; with the section, that approval says so.
const body = renderReviewBody({
event: "APPROVE",
hasInlineComments: true,
rereviewSection: "All 11 prior review threads are resolved.",
});
expect(body).toBe("All 11 prior review threads are resolved.");
});
});

describe("runRereviewCli", () => {
const makeFs = (files: Record<string, string>) => {
const written: Record<string, string> = {};
const fs: RereviewCliFs = {
existsSync: (p) => p in files,
readFileSync: (p) => files[p],
writeFileSync: (p, data) => {
written[p] = data;
},
mkdirSync: () => {},
};
return {fs, written};
};

const THREADS = "/tmp/gh-aw/review/threads.json";
const RECONCILER = "/tmp/gh-aw/review/out/thread-reconciler.json";
const PR_CONTEXT = "/tmp/gh-aw/review/pr-context.json";
const RESULT = "/tmp/gh-aw/review/rereview.json";

it("renders and writes the section from the staged inputs", () => {
const {fs, written} = makeFs({
[THREADS]: JSON.stringify([
{
thread_id: "a",
path: "x.go",
line: 3,
url: "https://github.com/o/r/pull/1#discussion_r1",
comments: [
{
author: "github-actions",
body: "**todo (blocking):** Missing field.",
},
],
},
]),
[RECONCILER]: JSON.stringify({
resolve: [],
keep: ["a"],
skipLines: [],
}),
[PR_CONTEXT]: JSON.stringify({headSha: "1234567890abcdef"}),
});
const result = runRereviewCli(fs);
expect(result.keptCount).toBe(1);
expect(result.section).toContain("still unaddressed as of 1234567");
expect(result.section).toContain(
"- **todo (blocking)** [`x.go:3`](https://github.com/o/r/pull/1#discussion_r1): Missing field.",
);
expect(JSON.parse(written[RESULT])).toEqual(result);
});

it("fails open to an empty section when the reconciler output is missing", () => {
const {fs, written} = makeFs({
[THREADS]: JSON.stringify([]),
});
const result = runRereviewCli(fs);
expect(result).toEqual({section: "", keptCount: 0, resolvedCount: 0});
expect(JSON.parse(written[RESULT])).toEqual(result);
});

it("fails open when the reconciler output is unparseable", () => {
const {fs} = makeFs({
[RECONCILER]: "not json",
});
expect(runRereviewCli(fs).section).toBe("");
});

it("tolerates threads.json entries with unexpected shapes", () => {
const {fs} = makeFs({
[THREADS]: JSON.stringify([
{thread_id: "a"},
{no_id: true},
"junk",
]),
[RECONCILER]: JSON.stringify({resolve: [], keep: ["a"]}),
});
const result = runRereviewCli(fs);
expect(result.keptCount).toBe(1);
expect(result.section).toContain("still unaddressed");
});
});
Loading
Loading