Skip to content

review: the live producer (live A/B phase 2)#234

Open
jwbron wants to merge 9 commits into
jwbron/live-eval-corpusfrom
jwbron/live-eval-producer-staging
Open

review: the live producer (live A/B phase 2)#234
jwbron wants to merge 9 commits into
jwbron/live-eval-corpusfrom
jwbron/live-eval-producer-staging

Conversation

@jwbron

@jwbron jwbron commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Phase 2 of the live A/B eval plan (#232), complete, stacked on the Phase 1 corpus format (#233): everything needed to run the real model sub-agents over a live-enabled corpus case.

2a: eval/agent-extract.ts

Parses review.md's ## agent: sections into {name, description, model, prompt}. String in, no filesystem access, because the baseline arm of an A/B extracts the merge-base review.md via git show. Parsing is strict: missing/unterminated frontmatter, a name mismatch, a missing model, or an empty body throws with every problem listed, since a silently dropped agent would skew an eval arm without failing it. An integration test extracts the real review.md (21 agents) and pins the default roster's reference to the production staging root so a rename fails a test instead of silently staging nothing.

2b: eval/live-stage.ts

Materializes the production /tmp/gh-aw/review/ layout for one case, per review.md Step 1's staging contract: pr-context.json (synthetic identity fields), full.diff/pr.diff/full-stripped.diff, files.json + review-files.json with the hasPatch cross-check, provenance.json, routing.json from the deterministic router, out/, and the post-change checkout from the case's tree. rewriteAgentPrompt swaps the production staging root across a prompt.

2c: eval/live-producer.ts + eval/live-runner.ts

produceLive dispatches the live roster (default finders plus the router's lensesToSpawn; no pattern-triage or thread-reconciler in eval) behind an injected LiveAgentRunner seam, the same pattern as judge.ts's injected model, so all its logic is stub-tested with zero model calls. It maps the three sub-agent output contracts into exactly the shapes the deterministic runner consumes (RecordedFinding[] for produceFindings, CaseVerification[] for the validation replay): label-shape findings get the code-assigned lens (correctness, or conventions for the skill-auditor so labelForFinding reproduces the best-practice label variants) and the production 0.7 confidence default; lens findings validate against the schema as-is; the claim-validator's {claims: [...]} output becomes verifications. It stages claims.json, resolves {{#runtime-import}} directives from the case tree (a case opts into a skills index by carrying the file), retries once on malformed output with the rejection fed back, keeps partial results when an agent fails twice, prefixes colliding finding ids, and accounts per-agent cost/turns/wall-clock.

live-runner.ts is the only module touching a real model runtime: one Agent SDK query() per dispatch, Read/Grep/Glob only, cwd pinned to the staged checkout, the agent's pinned model, hard turn and wall-clock caps, plus the CLI smoke entry: pnpm dlx tsx workflows/review/eval/live-runner.ts --case <id> (requires ANTHROPIC_API_KEY). Adds @anthropic-ai/claude-agent-sdk as a dev dependency.

Documented deviations from production (module doc): no pattern-triage pass, runtime imports resolve from the case tree or a fixed note, and the investigation-cap CLI is not staged (its own denied-budget fallback applies).

Test plan:

  • pnpm run test --run: 680 tests green (21 new across the three test files: extraction fixtures + real-review.md integration, staging via memfs, producer via a scripted stub runner covering happy path, retry, double-failure, empty-findings skip, id collisions, missing-agent error, and runtime-import resolution).
  • pnpm run typecheck and eslint clean.
  • The CLI smoke run against a live case needs ANTHROPIC_API_KEY, which this environment does not have; it is the first thing to run when testing this PR (any of the 13 live case ids, e.g. --case incident-money-rounding). Expected cost: roughly $0.50 to $2 per case with the default roster.

Next steps (human)

  1. Key-bearing validation DONE, superseded by the phase 4 acceptance runs, which exercised this producer end to end with a real key over 7 live cases x 2 arms x 2 PRs (runs 29052332224 and 29052334404); measured ~$0.55-0.72 per case per arm, inside the plan's expected envelope. The CLI smoke remains available for local iteration.
  2. Sanity-check the SDK runner's settings (live-runner.ts): permissionMode: "bypassPermissions" with tools restricted to Read/Grep/Glob and cwd pinned to the staged checkout. Confirm you are comfortable with that posture for CI.
  3. Review the documented deviations from production in live-producer.ts's module doc (no pattern-triage, runtime-import resolution, investigation-cap absence); each is a measurement caveat for the A/B.
  4. New dev dependency: @anthropic-ai/claude-agent-sdk. Confirm it clears whatever dependency review applies to this repo.

…action and case staging

Phases 2a/2b of the live A/B eval plan (#232), stacked on the Phase 1
corpus format (#233).

agent-extract.ts turns review.md's '## agent:' sections into data
(name, description, pinned model, prompt body). It takes the markdown
as a string with no fs access, because the baseline arm of an A/B
reads the merge-base review.md via git show. Parsing is strict; a
malformed or model-less section throws listing every problem, since a
silently dropped agent would skew an eval arm without failing it. An
integration test extracts the real review.md (21 agents) and pins the
staging-root reference so a future rename fails a test instead of
silently staging nothing.

live-stage.ts materializes the production staging layout for one
live-enabled corpus case: pr-context.json (review.md Step 1's shape,
synthetic identity fields), full.diff / pr.diff / full-stripped.diff
(all the case diff; corpus diffs carry no generated files and no
pattern-triage pass runs), files.json + review-files.json with the
hasPatch cross-check derived from the diff parse, provenance.json,
routing.json from the deterministic router, an out/ directory, and
the post-change checkout copied from the case tree.
rewriteAgentPrompt swaps the production staging root for the staged
context dir. Everything sits behind an injected-fs seam and is
memfs-tested.

Model dispatch (phase 2c) is deliberately absent; it needs the Agent
SDK dependency decision and arrives separately.
@jwbron jwbron self-assigned this Jul 9, 2026
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 82bc5ed

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

… runner (phase 2c)

live-producer.ts runs the live roster over one staged case behind an
injected LiveAgentRunner seam (the judge.ts pattern), so its logic is
stub-tested with zero model calls. Roster: the default finders
(correctness-reviewer, skill-auditor) plus the router's lensesToSpawn;
no pattern-triage or thread-reconciler in eval. It maps all three
sub-agent output contracts into the shapes the deterministic runner
consumes: label-shape findings (correctness lens, or conventions for
the skill-auditor so labelForFinding reproduces the best-practice
variants; confidence defaulted to 0.7 per the production claims rule),
structured-schema lens findings validated as-is, and the validator's
{claims: [...]} verdicts into CaseVerification[]. It stages
claims.json for the validator, resolves {{#runtime-import}} directives
against the case tree (a case opts into a skills index by carrying the
file), retries once on malformed output with the rejection fed back,
keeps partial results when an agent fails twice, prefixes colliding
finding ids, and reports per-agent cost/turns/wall-clock.

live-runner.ts is the one module that touches a real model runtime:
an Agent SDK query() per dispatch with Read/Grep/Glob only, cwd pinned
to the staged checkout, the agent's pinned model, and hard turn and
wall-clock caps; plus the CLI smoke entry
(tsx live-runner.ts --case <id>, requires ANTHROPIC_API_KEY). The
investigation-cap CLI the prompts reference is not staged; its own
denied-budget fallback applies. Adds @anthropic-ai/claude-agent-sdk
as a dev dependency.
@jwbron jwbron changed the title review: live-producer prompt extraction and case staging review: the live producer (live A/B phase 2) Jul 9, 2026
jwbron added 2 commits July 9, 2026 13:48
…rpus' into jwbron/live-eval-producer-staging
…s with the case id

Live agents choose their own finding ids, so every case's first
correctness finding was live-correctness-reviewer-1; ids were unique
within a case but collided across cases, and judge.ts's score join
requires arm-global uniqueness. Caught by the first real A/B run
(both arms completed, then judge aggregation threw). Ids are now
<caseId>:<id> from the moment of parsing, so claims.json, the
validator round-trip, the matcher, and the judge all see the same
namespaced id.
jwbron added 3 commits July 9, 2026 15:20
…rpus' into jwbron/live-eval-producer-staging
…gin/jwbron/live-eval-producer-staging' into jwbron/live-eval-producer-staging
…rpus' into jwbron/live-eval-producer-staging
@jwbron jwbron marked this pull request as ready for review July 10, 2026 18:36
@khan-actions-bot khan-actions-bot requested review from a team, jaredly and jeresig and removed request for a team July 10, 2026 18:36
@jeresig jeresig removed the request for review from jaredly July 10, 2026 19:05
@khan-actions-bot khan-actions-bot requested review from a team and jaredly and removed request for a team July 10, 2026 19:48

@jeresig jeresig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me!

@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (4 files)
File Reason
agent-extract.ts Strict parser that decides which review.md agents are extracted for the live A/B arm; a parse bug would skew arm results.
live-producer.ts Parses and schema-validates live sub-agent output into the shapes downstream metrics score; a mapping bug corrupts which reviewers earn their line.
live-runner.ts The only module invoking a real model runtime (Agent SDK query(), bypassPermissions, read-only tools, cwd-pinned).
live-stage.ts Materializes the production staging layout for a case and rewrites prompt paths; a wrong path or shape makes live arms diverge from production.

Common patterns

2 test files: Identical volFs adapter (memfs Volume -> StageFs seam) copy-pasted into live-stage.test.ts and live-producer.test.ts.

2 implementation files: Identical real-node:fs -> StageFs adapter object duplicated across live-stage.ts (DEFAULT_FS) and live-producer.ts (NODE_FS).

Excluded from review (2 files)

Not individually reviewed — generated or formatting-only:

  • pnpm-lock.yaml — generated
  • .changeset/review-live-producer-staging.md — formatting-only

if (entry.isDirectory()) {
copyDir(from, to, fs);
} else if (entry.isFile()) {
fs.writeFileSync(to, fs.readFileSync(from, "utf8"));

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): copyDir round-trips every file through readFileSync(from, "utf8") + writeFileSync, and StageFs exposes no byte-preserving read/write — so a case tree containing a real binary (image, fixture archive) is silently corrupted in the staged checkout, and the live arm then investigates files that differ from what the case recorded. Consider adding a copyFileSync (or Buffer-based) member to StageFs and using it here; memfs supports both, so the test seam need not force the utf8 restriction.

Low-confidence notes (2)
  • workflows/review/eval/live-producer.ts:307 — cross-agent id-collision resolution is order-dependent under concurrent dispatch, so a genuine collision is not resolved reproducibly across runs.
  • workflows/review/eval/live-runner.ts:56 — unverified (SDK not in checkout): does allowedTools restrict the toolset under permissionMode: "bypassPermissions", or only auto-approve it? If it does not restrict, Bash/Write/WebFetch stay reachable despite the read-only module doc.

(output) => parseVerifications(output, knownIds),
);
perAgent.push(report);
validation = parsed ?? [];

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 documented validator-failure fallback here (a twice-failed claim-validator yields validation = [] while produced findings survive) has no test — every produceLive test drives the validator happy path or skips it. Current behavior is correct; the gap is that a regression making a failed validator throw or drop findings would go uncaught. Consider a case that scripts the validator to return non-JSON twice and asserts findings.length > 0, validation is [], and the validator agent is marked failed.

});

headings.forEach((heading, i) => {
const sectionEnd = headings[i + 1]?.index ?? lines.length;

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): A section ends at the next ## agent: heading or EOF, so a non-agent ## section appended after the last agent would be folded into that agent's prompt with no parse error — the silent skew the strict parser exists to prevent. It holds today only because agent sections are currently terminal in review.md. Ending a section at the next ## heading of any kind (or asserting in the integration test that none follows the agents) would close the gap.

const DEFAULT_CONCURRENCY = 4;

/** The real filesystem, in the staging seam's shape (mirrors live-stage). */
const NODE_FS: StageFs = {

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 sibling live-stage.ts:54 and corpus/loader.ts:724 both name this real-fs StageFs adapter DEFAULT_FS; here it is NODE_FS, though the line-133 comment says it "mirrors live-stage." Renaming to DEFAULT_FS keeps one searchable name for the concept across the eval directory.

const verification = raw["verification"];
if (typeof id !== "string" || !knownIds.has(id)) {
throw new Error(
`claims[${index}].id does not match a produced finding`,

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): parseVerifications's rejection guards (unknown id here, invalid verification state just below) are untested — every scripted validator supplies valid ids and states. Consider a case emitting an id not among the produced findings (or verification: "maybe") and asserting the validator is marked failed and validation is [].

anchor: {
type: "line",
path: raw["path"],
line: raw["line"],

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): fromLabelShape hardcodes anchor.type: "line" with line: raw["line"], which validateFinding rejects for any non-positive-integer line. Since the label-shape contract in review.md uses "line": 0 as its placeholder and parseAgentFindings maps eagerly, a single line: 0 / omitted-line / PR-level entry throws and sinks the whole agent's batch (retry, then agent marked failed, losing its other valid findings). Consider treating a missing/zero line as a file- or PR-level anchor rather than throwing.

producing_hunt: `live:${agentName}`,
model_authored_prose:
discussion === "" ? subject : `${subject} ${discussion}`.trim(),
...(typeof raw["suggestion"] === "string" && raw["suggestion"] !== ""

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 suggestion -> suggested_patch round-trip (here, then carried back out as suggestion in buildClaims) is never asserted — no LABEL_FINDING fixture sets suggestion. A regression dropping it would silently strip suggested patches from live output with no failing test.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants