feat(agent-runner): add diff review mode to review method #218
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/agent-runner-diff-review-mode"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
diffinput to thereviewmethod schema that passes PR diff content directly in the prompt, replacing the file list. The agent reviews inline changes without needing to Read each file, cutting review time significantly for large changesets.diffis provided, agent tools are restricted toRead,Grep,Glob, andBash(Bash needed for theteeoutput mechanism). Thefilesinput becomes optional — callers must provide one or the other (validated at runtime).2026.08.23.1with an upgrade entry for the new optional field.Files changed
schemas.tsfilesnow optional, addeddiff: z.string().optional()review.tsbuildReviewPrompttakes discriminatedmodeparam, newinjectDiffModeTools, input validationagent_runner.tsmanifest.yamlreview_test.tsbuildReviewPrompt,injectDiffModeTools, schema shapesrunner_test.tsbuildTestPromptmirror with real import, added diff-mode testagent_runner_test.tsfiles?.lengthfor optional type, removed stale commentKnown limitation
Codex provider's config schema doesn't define
allowedTools, so the diff-mode tool restriction silently has no effect for Codex. The diff content still lands in the prompt (speed benefit preserved), just no tool scoping. Not a regression — Codex had no tool restrictions in file-list mode either.Test plan
deno test --allow-read --allow-write --allow-net --allow-env --allow-run)deno check)deno lint,deno fmt --check)fileswork identicallyfilesnordiffallowedToolsoverriderunner_test.tsuses realbuildReviewPrompt— no stale mirrors🤖 Generated with Claude Code
https://claude.ai/code/session_01M1Sjoft1zdsi2chnbRDKrL
Code Review
Blocking Issues
None.
Suggestions
diff: z.string()should have amin(1)constraint (schemas.ts).An empty-string
diff: ""passes schema validation today, but!args.diffistruthy for
"", so the runtime guard inexecuteReviewthrows"Either 'files' or 'diff' must be provided"even though the caller did supplythe field. If both
diff: ""andfiles: [...]are passed, the empty diff issilently ignored and files mode runs instead — surprising to callers who expect
diff mode.
Scope
BashinDIFF_MODE_ALLOWED_TOOLSto the write operation(
review.ts:196).Bashis included so the agent can write its result viatee. Claude Codesupports tool patterns like
"Bash(tee:*)"that restrict which shell commandsare permitted. Using the pattern instead of bare
"Bash"minimises blast radiusif a malicious diff contains prompt-injection instructions that try to abuse the
shell.
This is a nice-to-have hardening step, not a requirement — the existing
behaviour is intentional and documented.
Adversarial Review
Medium
review.ts:198-219 -- injectDiffModeTools can create allowedTools/disallowedTools conflict with no validation.
When the merged provider config already has disallowedTools but no allowedTools, the function injects allowedTools alongside the existing disallowedTools. If a profile intentionally blocks Bash via disallowedTools, the result is both allowedTools and disallowedTools containing Bash.
Whether Bash is actually available depends entirely on how the provider resolves the conflict. This code does not check for or prevent the contradiction. In Claude Code, disallowedTools takes precedence, so this is likely safe in practice, but the behavior is implicit and undocumented.
Breaking scenario: A future provider (or a change to Claude Code semantics) that treats allowedTools as authoritative over disallowedTools would silently grant Bash access to a review agent that was explicitly denied it.
Suggested fix: Before injecting, filter out any tools present in disallowedTools using a Set to avoid the contradiction.
Low
review.ts:35-37,62 -- When both files and diff are provided, diff silently wins.
The validation on line 35 only checks that at least one is present. When both are supplied, line 62 silently uses diff and discards files. This is undocumented and untested. A caller providing both inputs will not know that files was ignored. Not harmful, but could confuse CI pipeline authors debugging unexpected review scope.
agent_runner_test.ts:502 -- Misleading test name.
The test is named "review args require either profile or promptFile" but actually demonstrates the opposite: it parses input with neither profile nor promptFile and asserts success. The old explanatory comment was removed in this PR. The test validates the schema correctly (the constraint is enforced at runtime, not schema level), but the name will mislead anyone reading the test suite.
review.ts:35 -- Whitespace-only diff passes validation.
args.diff is checked with truthiness, so a whitespace-only diff string passes validation and enters diff mode with no meaningful content. The agent would receive a prompt with an effectively empty diff. Unlikely in practice but trivially prevented with a trim check.
Verdict
PASS -- The changes are clean and well-tested. The diff mode feature is a straightforward extension of the existing review flow. The discriminated union approach for mode is sound, the runtime validation correctly compensates for the schema inability to express the files-XOR-diff constraint, and injectDiffModeTools correctly respects explicit allowedTools overrides. The medium finding is a defense-in-depth concern, not a blocking issue in current provider implementations. Test coverage for the new helpers (buildReviewPrompt, injectDiffModeTools) is thorough, including edge cases like empty configs and cross-provider key preservation.
Code Review
The PR adds a
diffinput mode to thereviewmethod, allowing callers to pass an inline diff string instead of a file list. The agent reviews the diff directly without reading files, and tool access is narrowed toRead,Grep,Glob, andBash(tee:*)in that mode. The implementation is clean and well-tested.Blocking Issues
None.
Suggestions
injectDiffModeToolsinjects Claude-specific tool names for all providers. When the provider iscodex, the function still writesallowedToolsinto thecodexconfig key. Codex usessandbox/approvalPolicyrather thanallowedTools, so this is silently ignored and diff mode with Codex has no tool restriction applied. If Codex diff-mode restriction matters, a guard returning early for non-Claude providers (with a comment) would make the intent explicit.Undocumented behavior when both
filesanddiffare provided. The code resolves the conflict viaargs.diff ? ... : ...sodiffsilently wins. The schema field descriptions say each is "Required when the other is not provided" without mentioning precedence. Az.refine()enforcing mutual exclusivity, or at minimum a comment in the schema, would prevent caller confusion.No test for the runtime guard in
executeReview. The guard that rejects calls where neitherfilesnordiffis set is the only place that enforces that invariant — the schema itself permits neither field. The existing harness inagent_runner_test.tswires up a mockctxwithout needing a real agent binary, so a test callingexecuteReviewwith neither field and asserting the thrown error message would close this coverage gap at low cost.Adversarial Review
Medium
Silent diff precedence when both diff and files are provided - review.ts:62,71
The schema (ReviewArgsSchema) allows both diff and files to be provided simultaneously. The runtime validation on line 35 only checks that at least one is present. When both are supplied, diff silently wins. The diff-mode tool injection also kicks in, restricting tool access.
Breaking example: A caller passes both diff and files expecting the agent to review both the diff context and explore the listed files. Instead, files is silently ignored, tool access is restricted to diff-mode tools, and the agent never reads the listed files.
Suggested fix: Either (a) throw an error if both diff and files are provided, (b) log a warning that files is being ignored in diff mode, or (c) document the precedence in the diff field schema description.
Low
injectDiffModeTools injects Claude-specific allowedTools for any provider - review.ts:196-226
DIFF_MODE_ALLOWED_TOOLS includes Bash(tee:*) which is Claude Code CLI scoping syntax. When providerName is codex, the function injects codex-keyed allowedTools. The codex provider likely ignores unknown config keys, so this is harmless in practice, but it is conceptually leaky.
Suggested fix: Guard the injection behind a provider check, e.g. early-return when providerName is not claude.
mergeProviderConfig is a shallow merge - review.ts:333-341
mergeProviderConfig uses spread-based shallow merging. If both a profile and args specify providerConfig with the same provider key, the override entirely replaces the profile provider block rather than deep-merging them. This is likely intentional but callers may expect deep merging of provider sub-configs.
Verdict
PASS - The PR is well-structured and backwards-compatible. The files field is correctly made optional, the new diff mode is cleanly integrated with appropriate tool scoping, and the test coverage is thorough. The medium finding (silent precedence of diff over files) is a minor API clarity issue, not a correctness bug. No critical or high severity findings.