Skip to content

Latest commit

 

History

History
1574 lines (1252 loc) · 83 KB

File metadata and controls

1574 lines (1252 loc) · 83 KB

Diffwarden CLI Specification

Point-in-time spec: 2026-05-14. This document captures the current product and architecture intent for the first implementation of diffwarden; re-verify SDK details before relying on them for long-lived decisions.

1. Product summary

diffwarden is a small command-line tool that brings Codex's review process, rubric, and output contract to multiple agent SDKs and CLIs, so coding agents can request a code review and receive a structured review result.

The public contract should stay boring:

diffwarden review --target base:main
diffwarden review --target uncommitted --reviewer cursor
diffwarden review --target base:main --reviewer claude
diffwarden review --target base:main --reviewer pi
diffwarden review --target commit:abc123
diffwarden review --target base:main --agent
diffwarden review --target base:main --json
diffwarden review --target base:main --reviewer-set 2
diffwarden review --target base:main --reviewer cursor --reviewer pi:openrouter-high
diffwarden review --target base:main --reviewer-set 2 --focus "focus on state" --focus "focus on localization"
diffwarden review --target base:main --reviewer-set 2 --no-overview --focus "focus on state"

Agents call diffwarden review --agent when they want direct text they can act on, or diffwarden review --json when they need the stable artifact. Humans call diffwarden review and get a terminal display. The CLI resolves the target diff, runs one or more reviewer agents through SDK or CLI transport adapters, validates the structured result, and renders the selected output mode.

2. Goals

  1. Provide a simple CLI that agents can call from any coding workflow.
  2. Recreate the useful parts of Codex /review outside Codex, including target resolution, reviewer prompting, parsing, validation, and rendering.
  3. Use Codex's review rubric and review output shape as the normalized contract across supported SDK and CLI adapters.
  4. Support Cursor Agent SDK, Claude Agent SDK, and Pi Agent SDK in v1.
  5. Support thin CLI transports for Codex, Gemini, OpenCode, Pi, Cursor, Antigravity, Grok, and Claude where the executable is the practical integration path.
  6. Keep engine differences behind reviewer profiles and adapters without changing the CLI contract.
  7. Keep code review read-only by default.
  8. Produce output that is useful to both humans and automation.
  9. Support one, two, or many reviewer agents in one command, including multiple instances of the same SDK with different provider/model configuration.
  10. Ship as a public GitHub repository named aurokin/diffwarden; npm publishes the diffwarden CLI package.

3. Scope boundaries

Permanent non-goals for this CLI:

  1. Do not publish review comments to external services.
  2. Do not let reviewers modify files.
  3. Do not expose write-capable tools.
  4. Do not require a daemon, service, database, or web UI.

Deferred, but potentially useful later:

  1. Additional read-only target forms, if they preserve the same output contract without adding posting behavior.

4. Primary user stories

4.1 Agent reviews local changes

A coding agent has edited files and wants a second opinion before reporting completion.

diffwarden review --target uncommitted --agent

Expected behavior:

  • The CLI finds the git repo root.
  • The CLI builds a diff for uncommitted changes.
  • The CLI asks the configured reviewer set to inspect the patch.
  • The CLI prints a concise agent-readable review.

4.2 Agent reviews branch against main

A coding agent has completed a branch and wants review relative to main.

diffwarden review --target base:main

Expected behavior:

  • The CLI computes git merge-base HEAD main.
  • The CLI asks the selected reviewer set to inspect git diff <merge-base>.
  • Findings must point to changed lines when possible.

4.3 Agent reviews a single commit

A coding agent wants review for one completed commit without including unrelated local work.

diffwarden review --target commit:abc123

Expected behavior:

  • The CLI resolves the commit SHA.
  • The CLI builds the patch for that commit.
  • The CLI asks the configured reviewer set to inspect only that patch.
  • Findings must point to changed lines when possible.

4.4 Automation consumes JSON

A script wants machine-readable findings.

diffwarden review --target base:main --json --out review.json

Expected behavior:

  • Stdout contains JSON if --json is selected.
  • --out writes the complete review artifact, including metadata and validation results.

5. CLI interface

5.1 Command

diffwarden
diffwarden review [options]
diffwarden review show <path> [--agent|--json]
diffwarden doctor [--reviewer <spec>|--reviewer-set <name>] [--model <id>] [--fallback-model <id>] [--effort <level>] [--timeout <seconds>] [--cwd <path>] [--json]
diffwarden reviewers list [--cwd <path>] [--json]
diffwarden reviewers discover [--deep] [--cwd <path>] [--json]
diffwarden reviewers add [engine] [--id <id>] [--transport <transport>] [--model <id>] [--effort <level>] [--provider <name>] [--set <name>] [--disabled] [--interactive] [--cwd <path>] [--json]
diffwarden reviewers edit [id] [--transport <transport>] [--model <id>] [--effort <level>] [--provider <name>] [--enabled] [--disabled] [--cwd <path>] [--json]
diffwarden reviewers remove [id] [--force] [--cwd <path>] [--json]
diffwarden reviewers set add <set> <reviewer> [--cwd <path>] [--json]
diffwarden reviewers set remove <set> <reviewer> [--force] [--cwd <path>] [--json]
diffwarden init [--discover] [--interactive] [--cwd <path>] [--json]

5.2 Options

--target <target>                 Review target. Required unless defaulting to uncommitted.
--reviewer <spec>                  Repeatable reviewer spec. Default: config defaultReviewerSet.
--reviewer-set <name|count>        Named or count-based reviewer set from config.
--cwd <path>                      Working directory. Default: process.cwd().
--model <id>                      Model override for single-reviewer runs.
--fallback-model <id>             Fallback model override for single-reviewer runs (Claude only).
--effort <level>                  Reasoning/effort override for single-reviewer runs.
--agent                           Emit plain text optimized for coding agents.
--json                            Emit final review artifact JSON.
--ndjson                          Emit newline-delimited review events.
--out <path>                      Write full review artifact JSON to a file.
--strict                          Fail if structured output cannot be parsed or validated.
--readonly                        Read-only mode. Default and only supported mode.
--timeout <seconds>               Reviewer timeout.
--fail-on-findings <P0|P1|P2|P3>  Exit 1 when prioritized findings meet the threshold.
--focus <text>                    Repeatable focused diff-backed review lane.
--overview                        Include overview lane for focus runs, overriding config.
--no-overview                     Suppress overview lane for focus runs, overriding config.
--help                            Print usage.

diffwarden review accepts the review target/reviewer/reporting options and renders a human-facing review display by default. That display is presentation, not a parsing contract. It should avoid full-screen terminal behavior and degrade to plain text outside capable TTYs. --agent, --json, and --ndjson are mutually exclusive opt-in modes. diffwarden review show <path> renders an existing review artifact JSON file through the human display path; review show --agent and review show --json render saved ReviewArtifact and ReviewBatchArtifact files through those final-output contracts. review show does not support --ndjson because there is no live event stream to replay.

--focus <text> is an instruction layer on normal diff-backed targets. Repeated focus flags create ordered lanes focus-1, focus-2, etc. When at least one focus lane is present, the normal full-diff overview lane runs by default as overview; --no-overview suppresses it and --overview overrides config that disables it. --overview and --no-overview are invalid without --focus and invalid together.

--reviewer is the primary reviewer selection primitive. A single --reviewer is a one-reviewer run; repeated --reviewer flags are a multi-reviewer run. If no reviewer is provided, the CLI uses defaultReviewerSet from config. Config is required for real SDK runs; do not silently run an unconfigured default.

--reviewer-set expands to one or more reviewer specs from config. It supports ergonomic defaults for common cases:

diffwarden review --reviewer-set 1      Use config reviewerSets["1"].
diffwarden review --reviewer-set 2      Use config reviewerSets["2"].
diffwarden review --reviewer-set 3      Use config reviewerSets["3"].
diffwarden review --reviewer-set deep   Use config reviewerSets["deep"].

diffwarden reviewers list loads the discovered config and lists defaultReviewerSet, reviewerSets, and configured reviewer IDs/profiles without resolving a target, invoking adapters, running preflight checks, or spending model/API time. JSON output is intended for agents and automation and must not include nested option bags such as providerOptions, sdkOptions, or cliOptions.

Reviewer-list JSON currently emits schema_version: 2. Each configured reviewer summary includes enabled: boolean; omitted enabled in config is rendered as true.

diffwarden doctor resolves reviewers from config or flags and runs adapter preflight without resolving a review target or collecting a diff. It is the host-side counterpart to reviewers list: list reports what is configured, doctor reports whether the configured reviewers can actually run.

diffwarden reviewers discover probes the host for which built-in reviewer engines and transports are usable without running a review, resolving a target, or spending model/API budget. It is read-only with respect to config: it never reads or writes diffwarden.config.json. Shallow discovery (the default) uses only token-free probes: executable presence on PATH, side-effect-free SDK package resolution, presence of relevant environment variables, and readability of credential files. --deep additionally runs the same adapter preflight as doctor for present engines, which may spawn CLIs or call provider APIs. Each candidate is classified as one of available, missing_executable, missing_auth, requires_env, unsupported_host, or preflight_failed, with an authState of verified, unverified, missing, or not_required. Discovery JSON emits schema_version: 1 and reports the environment-variable names and credential-file paths it probed but never secret values. available candidates include a recommended minimal config entry.

diffwarden reviewers add/edit/remove/set and diffwarden init are the only commands that write reviewer config, and they always write the user config path ($XDG_CONFIG_HOME/diffwarden/diffwarden.config.json or ~/.config/diffwarden/diffwarden.config.json), never a project config. reviewers add merges a reviewer by id in place, preserves all other config keys, writes atomically with a compare-and-swap guard, and only appends an id to a named reviewer set when --set is given. reviewers edit <id> patches only the named fields (preserving every untouched key) and rejects overrides the resolved transport cannot honor before writing. reviewers remove <id> deletes the reviewer and prunes its id from every reviewer set. reviewers set add/remove <set> <reviewer> manages set membership; set add requires the id to be a configured reviewer. remove and set remove refuse to leave the set named by defaultReviewerSet empty unless --force is given, and editing/removing an unknown id exits non-zero and writes nothing. None of these commands changes defaultReviewerSet for an existing config. init --discover scaffolds a fresh config from discovered ready-to-use reviewers with a defaultReviewerSet and readonly: true, and refuses to overwrite an existing config. The interactive picker these commands drop into (built on @clack/prompts) is the only place Diffwarden uses raw-mode arrow-key input, and it is reachable only behind the TTY gate; the review renderer never enters raw mode (see ADR 0001).

Setup is interactive-by-default in a TTY. With stdin attached to a terminal, a bare diffwarden init runs the discover/scaffold flow, a bare reviewers add opens an arrow-key multiselect of discovered reviewers that are not already configured followed by a per-reviewer field editor for transport, model, effort, and the reviewer id, a bare reviewers edit (or edit <id> with no field flags) opens a field editor for a configured reviewer's transport, model, effort, and enabled state, and a bare reviewers remove lets the user pick a reviewer and confirm (default No). Esc/Ctrl-C steps back one menu level (cancelling at the top) and a ✕ quit option exits immediately; submitting a blank model or choosing default effort clears that override, and the interactive edit replaces the editor-managed fields (so a cleared field is removed) rather than applying the declarative set-only patch. Prompts render to stderr so stdout stays machine-clean. Naming a target (an engine for add, an id for remove/edit), passing at least one field flag to edit, passing --json, or running without a TTY stays fully declarative; a no-target setup command without a TTY exits 2 with a usage error rather than blocking on input, and --json never prompts. --interactive (on reviewers add and init) forces the guided flow and requires a TTY, exiting 2 when stdin is not interactive. For reviewers add it applies only to the no-engine form; combining it with a named engine exits 2, since a named engine is the declarative path and has nothing to pick.

Reviewer specs should stay compact and SDK-agnostic at the public boundary:

cursor                            Cursor SDK with default config.
claude                            Claude SDK with default config.
pi                                Pi SDK with default config.
droid                             Droid SDK with default config; experimental if Factory UI session history matters.
codex                             Codex CLI transport.
gemini                            Gemini CLI transport.
opencode                          OpenCode CLI transport.
grok                              Grok CLI transport.
antigravity                       Antigravity CLI transport.
pi:openrouter-high                Named Pi reviewer profile or provider config.
claude:sonnet                     Named Claude reviewer profile or model config.
cursor:fast                       Named Cursor reviewer profile or model config.

Candidate reviewer-spec grammars:

  1. Recommended: engine[:profile], for example pi, claude, cursor, pi:openrouter-high, cursor:fast. The suffix is always a named config profile, not an inline model or provider expression. This keeps parsing simple, avoids shell-escaping problems, and pushes provider-heavy options into diffwarden.config.json.
  2. Direct model shorthand: engine/model, for example claude/sonnet or pi/anthropic/claude-sonnet. This is concise for single-reviewer use but becomes ambiguous for provider-qualified model IDs that already contain /.
  3. Query-string style: engine?model=sonnet&effort=high. This is expressive but awkward in shells and too much like exposing adapter internals as the public contract.

Use option 1 for v1. Model and effort still have first-class single-reviewer flags through --model and --effort; profile suffixes are for reusable named reviewer configs.

If any --reviewer flags are present, they define the full reviewer set. If both --reviewer and --reviewer-set are provided, exit 2.

Model, effort, provider, and SDK-specific options should be handled in two layers:

  1. Simple one-off flags for the single-reviewer path, such as --reviewer claude --model sonnet --effort high.
  2. Named reviewer profiles for multi-reviewer or provider-heavy setups, such as --reviewer pi:openrouter-high.

Avoid turning the main CLI into a generic SDK option transport. Provider API keys, base URLs, OpenRouter/OpenCode-style provider selection, effort mappings, executable paths, and SDK-specific options should live in config profiles and be passed to adapters as structured reviewer config.

Configured reviewers use SDK transport by default for SDK-backed families and may set transport: "cli" to opt into the direct executable adapter. CLI-only families default to transport: "cli". cliOptions.executable can point at non-standard installs without changing the public reviewer spec.

5.3 Model and effort selection

Model selection is a first-class CLI concern:

diffwarden review --target base:main --reviewer claude --model sonnet
diffwarden review --target base:main --reviewer pi --model anthropic/claude-sonnet --effort high
diffwarden review --target base:main --reviewer pi:openrouter-high

Rules:

  • --model applies to single-reviewer runs.
  • Multi-reviewer runs should put model selection in named reviewer profiles.
  • If more than one reviewer is selected and --model or --effort is provided, exit 2.
  • --effort is a closed public enum aligned with Pi's thinking levels plus Claude's top tier. Values: off, minimal, low, medium, high, xhigh, max.
  • Invalid effort values fail during CLI/config validation with exit 2.
  • Invalid model values fail gracefully with a specific error. Prefer local validation against the selected reviewer/profile model catalog; if the SDK/provider rejects the model during preflight or execution, surface that as a reviewer setup/execution failure with exit 3.
  • Effort is best understood as requested reasoning intensity. Adapters may record a different effective effort when the SDK or model maps/clamps the requested value.
  • When no effort is requested through flags, env, or config, diffwarden applies a per-transport default effort declared in the capability matrix (defaultEffort), recorded with source diffwarden-default. Claude's SDK and CLI transports default to high; other engines currently declare no default and leave effort to the engine. A configured effortCatalog that omits the default suppresses it. On the SDK transport, if model preflight proves the selected model cannot accept the default (for example a model without effort support), the adapter drops effort entirely instead of failing, recording effortDropped: "model-unsupported"; user-requested efforts still fail loudly. The CLI transport has no catalog access, so the default is passed through as --effort high and unsupported-level resolution stays with the platform, consistent with explicit effort handling.

Pi effort handling is the reference implementation:

  • Pi exposes ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" in /Users/auro/code/upstream/pi-mono/packages/agent/src/types.ts.
  • Pi model metadata includes reasoning and optional thinkingLevelMap values.
  • Pi's getSupportedThinkingLevels() and clampThinkingLevel() in /Users/auro/code/upstream/pi-mono/packages/ai/src/models.ts compute model-supported levels and map unsupported requests to a nearby supported value.
  • diffwarden passes Pi profiles a requested thinking level, clamps it through model metadata, sends the effective thinkingLevel, and records requested/effective/supported effort metadata. Provider-specific effort tables must stay inside the adapter/SDK metadata path.
  • Claude maps off to disabled thinking and minimal to native low; low, medium, high, xhigh, and max pass through natively. SDK preflight resolves xhigh/max against the model catalog's supportedEffortLevels and substitutes within the top tier when the model exposes only one of the two (for example xhigh resolves to max on models that list only max); the CLI transport has no catalog access and passes the requested level through, leaving unsupported-level resolution to the platform. Adapter metadata records both requested and effective values.
  • Cursor reports effort as ignored with the requested value because the current Cursor SDK path does not expose a concrete reasoning-control option.

5.4 Fallback model and run limits (Claude)

Claude reviewers accept three additional reviewer config fields, validated against the capability matrix at config load (supportsFallbackModel, supportsMaxTurns, supportsMaxBudgetUsd) so unsupported engines fail with exit 2 instead of silently ignoring them:

  • fallbackModel (also --fallback-model and DIFFWARDEN_FALLBACK_MODEL for single-reviewer runs): retry model for primary-model overload. When unset and the primary is not Sonnet-family, diffwarden defaults to Sonnet (fallbackModelSource: "diffwarden-default"); Sonnet primaries and unrecognized model ids get no default, and no default ever selects Haiku. SDK preflight validates the fallback (explicit or default) against the model catalog; runs report fallbackModelUsed when result modelUsage can prove whether the fallback family served the run. The CLI transport probes --fallback-model against --help: explicit-but-unsupported degrades with fallbackModelDropped: "cli-unsupported", a default is silently skipped.
  • maxTurns: agentic turn cap, SDK transport only (the Claude CLI has no --max-turns flag, so CLI-transport configs with maxTurns are rejected).
  • maxBudgetUsd: hard spend cap. An exhausted budget (error_max_budget_usd) fails the reviewer with a budget-specific error rather than returning a partial review, and a Claude CLI executable lacking --max-budget-usd fails the run — a spend cap is never silently dropped.

Adapters should implement a preflight step before running review:

  1. Verify SDK package/runtime requirements are available.
  2. Verify any required executable used internally by the SDK is available, when applicable.
  3. Verify required authentication is present.
  4. Verify selected provider/profile options are coherent.
  5. Verify selected model and effort can be mapped or rejected with a clear message.

5.5 Target syntax

uncommitted                       Review staged, unstaged, and untracked local changes.
base:<branch>                     Review HEAD against merge-base with branch.
commit:<sha>                      Review one commit.
custom:<text>                     Custom repository-scoped reviewer instructions.

custom:<text> is intentionally not diff-backed. It gives the reviewer a repository root and custom instructions, then uses the same reviewer preflight, prompt assembly, parsing, schema validation, path validation, aggregation, rendering, and artifact output as other targets. Because no patch is collected, custom targets do not populate changed_files, embed a patch in the prompt, or validate findings against changed-line overlap.

Focus lanes are not custom targets. They reuse one resolved diff-backed target, keep the patch fence and provenance in every lane prompt, and still validate findings against changed files and changed-line ranges. Reject --focus with custom:<text>.

5.6 Initial v1 targets

Implement in v1:

  • uncommitted
  • base:<branch>
  • commit:<sha>
  • custom:<text>

6. Exit codes

0 = review completed successfully
1 = review completed and findings met --fail-on-findings threshold
2 = invalid CLI usage or configuration
3 = reviewer execution failed
4 = output parse or validation failed in --strict mode

By default, findings should not make the command fail. The most common caller is another agent that needs to read and act on the review. --fail-on-findings P2 keeps normal output behavior, then exits 1 after the artifact is produced when the final aggregated findings include any P0, P1, or P2 finding. Unprioritized findings do not trigger the gate.

Error handling rules:

  • Invalid target syntax, invalid reviewer spec, invalid effort value, unknown configured profile, and locally invalid model values exit 2.
  • Missing SDK package/runtime requirements, missing SDK-required executable, missing authentication, provider setup failure, timeout, and SDK execution failure exit 3.
  • If an SDK/provider rejects a model that could not be validated locally, exit 3 and print a model-specific message.
  • In human and agent modes, errors should be concise human-readable text on stderr.
  • In JSON modes, errors should be a stable JSON error object on stdout or stderr. Choose one stream during implementation and document it in --help.

Suggested JSON error shape:

export type ReviewError = {
  schema_version: 1;
  error: {
    code:
      | "invalid_cli"
      | "invalid_config"
      | "invalid_model"
      | "invalid_effort"
      | "missing_requirement"
      | "missing_auth"
      | "reviewer_failed"
      | "reviewer_environment_failed"
      | "timeout"
      | "parse_failed"
      | "validation_failed";
    message: string;
    reason?: string;
    recovery?: string[];
    reviewer_id?: string;
    engine?: "cursor" | "claude" | "pi" | "droid" | "codex" | "gemini" | "opencode" | "grok" | "antigravity";
    hint?: string;
  };
};

7. Review data model

7.1 ReviewResult

The normalized model should mirror Codex's review output event so every supported SDK and CLI adapter returns the same review contract.

export type ReviewResult = {
  findings: ReviewFinding[];
  overall_correctness: "patch is correct" | "patch is incorrect" | string;
  overall_explanation: string;
  overall_confidence_score: number;
};

7.2 ReviewFinding

export type ReviewFinding = {
  title: string;
  body: string;
  confidence_score: number;
  priority: 0 | 1 | 2 | 3;
  code_location: {
    absolute_file_path: string;
    line_range: {
      start: number;
      end: number;
    };
  };
};

Rules:

  • title should start with [P0], [P1], [P2], or [P3].
  • body should explain why the issue is a bug and when it occurs.
  • body should be one concise paragraph, explain the specific failure mode, and avoid praise or accusatory phrasing.
  • line_range should be as small as possible, usually no more than 5-10 lines.
  • Findings should only identify issues introduced by the reviewed diff.
  • Findings should identify discrete, actionable issues the original author would likely fix.
  • Findings should avoid vague style comments.

7.3 ReviewArtifact

The CLI should wrap model output with local metadata.

export type ReviewArtifact = {
  schema_version: 2;
  engine?: "cursor" | "claude" | "pi" | "droid" | "codex" | "gemini" | "opencode" | "grok" | "antigravity";
  reviewers?: ReviewReviewerArtifact[];
  cwd: string;
  target: ReviewTargetResolved;
  result: ReviewResult;
  raw_text?: string;
  validation: ReviewValidation;
  timing_ms?: number;
};

For single-reviewer runs, engine and result are enough for simple consumers. For multi-reviewer runs, reviewers contains each individual reviewer result and result is the merged or selected summary used for rendering.

export type ReviewReviewerArtifact = {
  id: string;
  engine: "cursor" | "claude" | "pi" | "droid" | "codex" | "gemini" | "opencode" | "grok" | "antigravity";
  transport?: "native" | "cli";
  profile?: string;
  provider?: string;
  model?: string;
  effort?: string;
  result: ReviewResult;
  raw_text?: string;
  adapter_metadata?: ReviewAdapterOutput["metadata"];
  validation: ReviewValidation;
  timing_ms?: number;
};

7.3.1 ReviewBatchArtifact

When no focus lanes are supplied, keep returning the existing ReviewArtifact. When one or more focus lanes are supplied, return a ReviewBatchArtifact:

export type ReviewRunArtifact = ReviewArtifact | ReviewBatchArtifact;

export type ReviewLane = {
  id: "overview" | `focus-${number}`;
  kind: "overview" | "focus";
  focus?: string;
};

export type ReviewPlan = {
  include_overview: boolean;
  focus: string[];
  lanes: ReviewLane[];
};

export type ReviewBatchArtifact = {
  schema_version: 2;
  kind: "batch";
  cwd: string;
  target: ReviewTargetResolved;
  plan: ReviewPlan;
  result: ReviewBatchResult;
  validation: ReviewValidation;
  warnings?: string[];
  timing_ms?: number;
  lanes: ReviewBatchLaneArtifact[];
};

export type ReviewBatchLaneArtifact =
  | (ReviewLane & {
      status: "success";
      artifact: ReviewArtifact;
      timing_ms?: number;
    })
  | (ReviewLane & {
      status: "failed";
      error: ReviewerError;
      timing_ms?: number;
    });

export type ReviewBatchFinding = ReviewArtifactFinding & {
  lane_ids: string[];
};

export type ReviewBatchResult = Omit<ReviewArtifact["result"], "findings"> & {
  findings: ReviewBatchFinding[];
};

The batch top-level result is for gates and CI. It deduplicates findings across successful lanes with the same finding key used for reviewer aggregation, unions reviewer_ids, and adds separate lane_ids attribution. Per-lane artifacts remain normal ReviewArtifact objects so reviewer attribution, validation, warnings, and adapter metadata stay inspectable without overloading reviewer IDs.

If at least one lane succeeds and strict mode is off, failed lanes are represented in lanes[] with status: "failed" and summarized as top-level warnings. If all lanes fail, or if strict mode sees any failed lane or strict validation failure, the run emits one terminal error and no batch artifact.

7.4 ReviewReviewerConfig

Reviewer config is the internal representation produced from CLI flags, environment variables, and config files. Public config files use engine.

export type ReviewReviewerConfig = {
  id: string;
  sdk: "cursor" | "claude" | "pi" | "droid" | "codex" | "gemini" | "opencode" | "grok" | "antigravity";
  transport?: "sdk" | "cli";
  profile?: string;
  provider?: string;
  model?: string;
  effort?: string;
  modelCatalog?: string[];
  effortCatalog?: Array<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | string>;
  timeoutMs?: number;
  readonly: boolean;
  cliOptions?: Record<string, unknown>;
  sdkOptions?: Record<string, unknown>;
  providerOptions?: Record<string, unknown>;
};

Rules:

  • model is the model identifier passed to the adapter when supported.
  • modelCatalog is an optional allow-list used for local validation. If present, a model outside the list is an exit 2 configuration error.
  • effort is a normalized intent, not a guaranteed cross-SDK value. Each adapter maps it to the closest SDK-specific setting and records requested/effective values in verbose metadata when possible.
  • effortCatalog defaults to off, minimal, low, medium, high, xhigh, and max. Values outside the selected catalog are an exit 2 configuration error.
  • provider and providerOptions are mainly for SDKs that route through configurable providers, such as Pi profiles that target OpenRouter or other backends.
  • sdkOptions is an escape hatch for version-sensitive adapter configuration. Keep it out of the public CLI unless a specific option becomes common enough to promote.

7.5 ReviewTargetResolved

export type ReviewTargetResolved = {
  kind: "uncommitted" | "base" | "commit" | "pr" | "custom";
  repo_root: string;
  base_ref?: string;
  base_sha?: string;
  head_sha?: string;
  commit_sha?: string;
  pr?: {
    number?: number;
    url?: string;
  };
  diff_command: string;
  changed_files: string[];
};

7.6 ReviewValidation

export type ReviewValidation = {
  parse_mode: "strict-json" | "extracted-json" | "tool-output" | "fallback-text";
  valid_schema: boolean;
  findings_overlap_diff: boolean;
  valid_locations: boolean;
  invalid_locations: Array<{
    index: number;
    reason: string;
  }>;
};

parse_mode describes how core normalized adapter output into ReviewResult:

  • strict-json: adapter output was parsed directly as the expected JSON object.
  • extracted-json: core recovered the expected JSON object from surrounding text.
  • tool-output: adapter output came from a structured tool or native structured-output handoff.
  • fallback-text: core could not recover a valid JSON object and preserved useful text in overall_explanation.

8. Review prompt requirements

The prompt should be assembled from three parts:

  1. Codex review rubric.
  2. Target-specific instructions.
  3. ReviewResult schema / structured-output instructions.

8.1 System-prompt split

Transports that let diffwarden own the engine system prompt (capability supportsSystemPrompt; today the Claude SDK and CLI transports) receive the prompt as two parts instead of one concatenated string:

  • System prompt = the stable diffwarden contract: rubric, a tools section describing the read-only toolset when diffwarden controls it (capability systemPromptTools), and the ReviewResult output instructions. The contract is byte-stable per engine, transport, and diffwarden version, so it forms an ideal provider cache prefix.
  • User prompt = per-run engagement: the review request, target, patch provenance command, scope rules, focus instructions when set (focus is per-run and never enters the contract), and the fenced patch.

Direction settled by live experiment (15 sonnet runs, 2026-07-10): Claude Code's default system prompt missed a planted cross-file bug in 3/5 runs (its efficiency bias suppresses caller exploration), while the diffwarden contract found all planted bugs with zero false positives. Custom-instruction targets get the same split. Every other transport keeps today's single concatenated prompt byte-identical.

On the Claude SDK transport the contract is passed as systemPrompt (never during model preflight, which sends no prompt). On the Claude CLI transport --system-prompt is a probed optional flag: it is checked against the already-fetched --help output rather than added to the hard-failing review policy flag list, and an executable without it degrades to the concatenated single prompt (recorded as systemPromptMode: "concatenated"). The CLI also appends probed --bare (skip hooks, plugins, and session startup) only when the runtime selected api-key auth, because --bare restricts auth to API keys and does not honor delegated Claude Code logins; the decision is recorded as bare: "true"/"false" metadata.

The rubric should preserve Codex's core semantics:

  • Find real bugs that affect correctness, performance, security, or maintainability, not broad style issues.
  • Only report issues introduced by the diff; pre-existing bugs are out of scope.
  • Report every qualifying finding, but prefer no findings when there is no definite fix-worthy issue.
  • Do not report issues that the author clearly did not change.
  • Do not rely on unstated assumptions about author intent or speculative breakage; identify the affected code path or scenario.
  • Keep comments concise, actionable, matter-of-fact, and specific about the inputs or environment required for the bug.
  • Use [P0] for universal release-blocking issues, [P1] for urgent next-cycle fixes, [P2] for normal bugs, and [P3] for low-priority issues.
  • Treat overall_correctness as whether existing code and tests will continue to work and the patch is free of blocking bugs; ignore non-blocking nits for that verdict.
  • Use absolute file paths in structured output.
  • Make line ranges short and overlapping changed lines whenever possible.
  • Always produce the normalized structured result through the adapter when possible.
  • Let the CLI decide whether to render that result for humans, agents, JSON, or NDJSON.

Example target instruction:

Review the code changes in this repository.
The target is base:main.
The merge base is abc123.
Inspect the patch with:

  git diff abc123

Only report bugs introduced by this diff. Emit the ReviewResult structure exactly through the configured structured-output mechanism.

9. Architecture

9.1 Module layout

diffwarden/
  package.json
  tsconfig.json
  src/
    cli.ts
    core/
      config.ts
      git.ts
      target.ts
      prompt.ts
      schema.ts
      parse.ts
      validate.ts
      render.ts
      errors.ts
    adapters/
      types.ts
      cursor.ts
      claude.ts
      pi.ts
      cli.ts
  test/
    target.test.ts
    parse.test.ts
    validate.test.ts
    render.test.ts
  docs/
    SPEC.md or symlink/reference to root SPEC.md

9.2 Core pipeline

parse CLI args
  -> load config/env defaults
  -> resolve cwd and git repo root
  -> resolve review target
  -> build review prompt
  -> select reviewer adapter
  -> run adapter
  -> parse output into ReviewResult
  -> validate schema and locations
  -> render output
  -> write --out artifact if requested
  -> choose exit code

9.3 Adapter interface

export interface ReviewAdapter {
  name: string;
  run(input: ReviewAdapterInput): Promise<ReviewAdapterOutput>;
}

export type ReviewAdapterInput = {
  cwd: string;
  reviewer: ReviewReviewerConfig;
  target: ReviewTargetResolved;
  diff: string;
  changedFiles: string[];
  changedLineRanges?: Record<string, Array<{ start: number; end: number }>>;
  prompt: string;
  schema: object;
  timeoutMs?: number;
  readonly: boolean;
  env?: NodeJS.ProcessEnv;
};

export type ReviewAdapterOutput = {
  text?: string;
  structured?: unknown;
  events?: ReviewAdapterEvent[];
  usage?: unknown;
  metadata?: {
    captureMode?: "native-structured" | "tool-call" | "text";
    agentId?: string;
    runId?: string;
    readonlyCapability?: "enforced" | "tool-restricted" | "prompt-only";
    [key: string]: unknown;
  };
};

Central rule: adapters should not resolve diffs, validate findings, merge reviewer results, or render output. They only run the selected SDK-backed reviewer and return structured output when the SDK can provide it. The richer input object is for adapter observability, logging, and SDKs that can consume structured context; the core CLI still owns target resolution and prompt assembly.

The adapter contract should be shared across SDKs:

  1. Use the same review prompt and schema for every SDK.
  2. Use the SDK's native schema, tool, or structured-response mechanism when it is available and reliable.
  3. Return structured when the SDK provides a structured handoff.
  4. Return text when the SDK's reliable output surface is terminal text.
  5. Always let the centralized parser normalize adapter output into ReviewResult.
  6. Record the capture mode so validation and rendering can distinguish native structured output, tool calls, and terminal text.

captureMode describes how the adapter captured output from the SDK. parse_mode describes how core parsed that captured output. For example, Cursor can have captureMode: "text" with parse_mode: "extracted-json", while Pi can have captureMode: "tool-call" with parse_mode: "tool-output".

Codex's built-in review flow is the reference pattern: prompt the reviewer for an exact JSON review object, parse exact JSON first, attempt to extract a JSON object from surrounding text, and preserve useful plain text as a fallback rather than making the adapter responsible for rendering. Diffwarden may use a more robust extraction strategy than upstream Codex when adapter output can echo prompts, append logs, or contain multiple JSON objects; the contract remains Codex-derived even when the parser is hardened for multi-adapter output.

10. Review runner

The review runner coordinates one or more adapter invocations for the same resolved target.

Responsibilities:

  • Expand repeated --reviewer flags into concrete reviewer configs.
  • Expand no reviewer to the configured defaultReviewerSet, then to reviewerSets["1"].
  • Expand --reviewer-set <name|count> to configured reviewer sets.
  • Run reviewers concurrently up to a configurable limit.
  • Support multiple instances of the same SDK, such as two Pi reviewers with different provider profiles.
  • Preserve each reviewer result separately in ReviewArtifact.reviewers.
  • Produce a deterministic aggregate ReviewResult for human, agent, JSON, and NDJSON rendering.

Initial aggregation behavior should be conservative:

  • Human and agent renderers should sort findings by priority and location, with reviewer attribution on each finding.
  • Full JSON should preserve findings grouped by reviewer.
  • Deduplicate only when file path, line range, priority, and normalized title are the same. Do not merge fuzzy or merely related findings in v1.
  • If reviewers disagree, preserve the disagreement rather than pretending there is one consensus. Human and agent renderers should show lightweight attribution such as Reported by: claude, pi-openrouter-high or Only reported by: cursor-fast.

Failure behavior:

  • Single-reviewer run: any reviewer execution failure exits 3.
  • Multi-reviewer run: render successful partial results with a warning if at least one reviewer succeeds.
  • Multi-reviewer run: exit 3 if every reviewer fails.
  • --strict makes any reviewer failure fatal.
  • ReviewArtifact.reviewers must preserve per-reviewer success/failure metadata so automation can decide whether partial results are acceptable.

Structured-output repair (attempt-aware pipeline):

  • When an attempt's output fails schema parsing AND raw material survives (schema-invalid structured JSON, or unparseable text), the runner issues one short REPAIR request against the same reviewer via the optional adapter runStructured(input) method — a one-shot, tool-restricted, schema-constrained invocation. The repair prompt contains the malformed output plus the ReviewResult JSON schema and instructs transcription only (never invent or drop findings).
  • The repair response is wrapped — { fixable, confidence: high|medium|low, review|null } — so the model's meta judgment cannot leak into the review. Accepted only when fixable, confidence !== "low", and the review passes schema validation; accepted repairs record captureMode: "repaired", repairConfidence, and repairFailureReason, and keep the malformed original as raw_text.
  • Anything else — no raw material (e.g. Claude's error_max_structured_output_retries carries no text and no structured payload), no runStructured, an unfixable/low-confidence/schema-invalid repair, or a repair request error — falls through to ONE full re-run explicitly labeled with attempts: 2 and firstAttemptFailureReason in adapter metadata. A labeled retry beats a shaky repair: the label tells users the engine/model is not reliably producing valid output.
  • Repair triggers on schema-parse failure only, never on semantic validation failures (findings outside changed ranges) — "repairing" line numbers is fabrication. Repair never triggers another repair.
  • Adapters do not run their own recovery: the Claude adapter's former structured→text double-run collapsed into this shared pipeline. Engines without native structured output can implement runStructured by describing the schema in the prompt and returning text; core unwraps JSON (strict or embedded) before validation.

11. Engine adapters

11.1 Cursor adapter, v1

Use Cursor Agent SDK local mode.

Expected API shape from inspected SDK:

  • Agent.create({ apiKey, model, mode: "plan", mcpServers: {}, local: { cwd, autoReview, sandboxOptions, settingSources, store } })
  • agent.send(prompt)
  • run.stream()
  • run.wait()

Initial behavior:

  • Implement adapter preflight for SDK availability, auth, model, effort, and read-only capability reporting.
  • Use the Cursor Agent SDK directly.
  • Run local SDK reviews in plan mode with sandbox enabled, auto-review enabled, no MCP servers, empty setting sources, and an ephemeral JSONL local store.
  • Prove local SDK execution and reliable text capture before adding more SDK-specific plumbing.
  • Return { text } from the terminal run result for the first Cursor path, with metadata.captureMode = "text".
  • Let the shared parser recover exact JSON, extracted JSON, or fallback text from Cursor output.
  • Add a dedicated local MCP review_output tool as a reliability upgrade after the text path works.
  • When the MCP path is enabled, capture matching SDK tool_call stream events and validate the tool args as ReviewResult.
  • Return { structured } with metadata.captureMode = "tool-call" when a valid review_output call is captured.
  • If the MCP path does not produce a valid tool call, fall back to terminal text and mark the capture mode accordingly.
  • Document what readonly can and cannot enforce for Cursor local mode before treating it as a hard guarantee.

Verified SDK constraint:

  • @cursor/sdk@1.0.18 types expose local mode, autoReview, sandboxOptions, settingSources, store, customTools, AgentOptions.mcpServers, SendOptions.mcpServers, run.stream(), run.wait(), and stream tool_call events.
  • Those types do not expose a Claude-style outputFormat: { type: "json_schema" } option.
  • Therefore Cursor v1 should prove local SDK review execution and text output first, then prove the MCP review_output path as the preferred structured-output upgrade. Prompt-driven JSON is acceptable for the first useful Cursor adapter because centralized parsing and validation are core responsibilities.

11.2 Claude adapter, v1

Use Claude Agent SDK query().

Expected API shape from inspected SDK:

  • query({ prompt, options })
  • options.cwd
  • options.model
  • options.permissionMode
  • options.tools
  • options.allowedTools
  • options.disallowedTools
  • options.settingSources
  • options.mcpServers
  • options.strictMcpConfig
  • options.persistSession
  • options.outputFormat: { type: "json_schema", schema }

Initial behavior:

  • Implement adapter preflight for SDK availability, auth, model, effort, and read-only capability reporting.
  • Use native JSON schema output.
  • Restrict built-in tools to Read, Grep, and Glob; mirror those into allowedTools; use permissionMode: "dontAsk"; deny write, shell, web, skill, agent, and workflow tools; disable settings, use strict empty MCP config, and disable session persistence.
  • Do not set a Diffwarden-owned Claude review turn cap. Reviewer timeout is the run-level limiter.
  • Check local Claude Code executables for required review policy flags before using local auth; auto mode may fall back to API-key auth, while forced local auth fails preflight when the executable is too old.
  • Return { structured }.

Important caveat:

  • Claude Agent SDK auth may differ from Claude Code subscription or Max auth. Configured Claude reviewers can use transport: "cli" when preserving Claude Code executable auth is more important than the SDK path.

11.3 Pi adapter, v1

Use Pi Agent SDK directly.

  • Implement adapter preflight for SDK availability, SDK-required executable availability if applicable, auth, provider/profile coherence, model, effort, and read-only capability reporting.
  • Use createAgentSession().
  • Add a custom terminating review_output tool.
  • Tool captures ReviewResult and terminates the run.
  • Return { structured }.
  • Support named provider/profile configuration so callers can run multiple Pi reviewers with different providers.

11.4 CLI transport adapters

Direct executable adapters are thin transports. They must not own target resolution, prompt construction, parsing, validation, rendering, or aggregation.

Implemented CLI families:

  • Codex CLI uses codex exec with a read-only sandbox, JSON schema, and --output-last-message.
  • Codex CLI sets web_search = "disabled" by default, with cliOptions.webSearch allowing "enabled", "disabled", or "inherit".
  • Claude CLI uses claude -p with JSON output, JSON schema, --tools Read,Grep,Glob, matching --allowedTools, deny rules for write/shell/web/broad-agent tools, --permission-mode dontAsk, no session persistence, empty setting sources, strict empty MCP config, disabled slash commands, and disabled Chrome.
  • Cursor CLI uses cursor-agent -p with JSON output, plan mode, sandbox enabled, and workspace scoping.
  • Droid CLI uses droid exec --use-spec, default read-only autonomy with no mission/unsafe autonomy flags, an explicit read-cli/glob-search-cli/grep_tool_cli/ls-cli/ exit-spec-mode tool allowlist verified with --list-tools, JSON output, Diffwarden session tags/log group IDs, model/effort help validation where Droid exposes it, and no Diffwarden-owned tool-call, turn, step, retry, or equivalent cap.
  • Droid CLI streamed capture (stream-json / raw JSON-RPC) is not part of the current review contract; reviewer debug streaming should be designed as an explicit opt-in cross-adapter feature if needed.
  • Gemini CLI uses JSON output, plan approval mode, a generated all-modes Policy Engine policy/admin policy that allows only read_file, list_directory, glob, and Gemini grep names (grep_search plus legacy alias search_file_content), an empty MCP allowlist, disabled extensions, and isolated session trust for headless startup. Diffwarden does not add Gemini-specific tool-call, turn, step, or retry caps; reviewer timeout is the run-level limiter. Gemini remains supported for enterprise and paid API-key users after Google's June 18, 2026 consumer/free Gemini CLI transition to Antigravity CLI.
  • OpenCode CLI uses JSONL output and --pure; its read-only capability is prompt-only until a supported per-run permission-deny path is proven.
  • Pi CLI uses JSON mode, no session, explicit read/list/search tools, and extension/skill/template/theme disabling.
  • Grok CLI uses JSON output, --permission-mode dontAsk, --tools read_file,grep,list_dir, explicit read/search allow rules, explicit shell/edit/write/web/MCP deny rules, --sandbox read-only, disabled web search/subagents/memory, and no Diffwarden-owned turn/tool-call/step cap.
  • Antigravity CLI uses prompt-bearing print mode with a temp prompt file, sandbox mode, and an isolated temporary Antigravity CLI settings profile. The profile preserves valid non-policy user settings after filtering policy/control keys, then overrides the review policy to set strict tool permission, enable terminal sandboxing, use an empty MCP config, deny write, command, unsandboxed, web, and MCP actions, and allow file reads only inside run-scoped trusted roots for the repository and prompt directory. agy runs from the prompt directory with HOME/USERPROFILE pointed at the isolated profile and Windows drive/path home variables removed; copied auth identity files stay outside that cwd and outside trusted roots, temp homes or source .gemini directories inside the reviewed repository fail closed before credential copying, and explicit child environments without a home path are respected instead of copying host credentials. Preflight requires agy 1.0.6 or newer before reporting the path as tool-restricted because enforcement depends on Antigravity's settings and permission engine.

Each CLI adapter must run executable preflight, report readonlyCapability, pass model and effort only where the executable supports them, and return either { structured } or { text } to the common parser.

11.5 Point-in-time SDK research

Point-in-time research date: 2026-05-14. Re-check upstream docs and local clones before implementation.

Cursor Agent SDK

Sources:

Findings:

  • The TypeScript SDK supports local and cloud runs through @cursor/sdk.
  • Local SDK examples use Agent.create({ apiKey, model: { id }, local: { cwd } }), agent.send(), run.stream(), and run.wait().
  • Agent.prompt() is the simplest one-shot path and disposes automatically.
  • The SDK has a model-list API surface through Cursor.models.list(...); use it for model preflight where possible.
  • Cursor docs emphasize model IDs can change; local validation should call the model catalog rather than hardcoding unusual IDs.
  • Cursor startup/config/auth/network failures throw CursorAgentError; a run that starts and then fails returns a terminal run result with error status. The adapter must map those separately.
  • Local runs should pass local: { cwd } explicitly.
  • Cloud behavior can open PRs when configured. This CLI must not use cloud PR creation features.
  • Current @cursor/sdk@1.0.18 package types do not expose a native outputFormat: json_schema equivalent.
  • Cursor's first adapter should use local SDK execution and terminal text capture to prove the end-to-end review contract quickly.
  • The package types expose local custom tools, MCP server configuration, and stream tool_call events, so a later Cursor structured-result path should use a local review_output tool whose args are validated as ReviewResult.
  • Text JSON extraction is an ordinary core parser path, not Cursor-specific adapter logic. The validation artifact must still record whether the output came from native structure, a tool call, extracted JSON, or fallback text.

Claude Agent SDK

Sources:

Findings:

  • Claude Agent SDK supports structured outputs by passing options.outputFormat: { type: "json_schema", schema } to query().
  • The result message includes structured_output on success.
  • The SDK validates schema output and retries on mismatch; if validation still fails, the result is an error rather than structured data.
  • TypeScript options include model, fallbackModel, cwd, env, executable, pathToClaudeCodeExecutable, tools, allowedTools, disallowedTools, mcpServers, strictMcpConfig, settingSources, persistSession, stderr, and related execution controls.
  • Current Claude docs describe allowedTools as approval rules only. Use tools to constrain built-in tool availability and pair allowedTools with permissionMode: "dontAsk" for locked-down SDK agents.
  • Local Claude Code executable auth is only used when claude --help advertises the policy flags Diffwarden passes. This makes newer flag requirements explicit during preflight instead of allowing unknown-option failures during review execution.
  • The SDK may spawn a native Claude Code binary through optional per-platform dependencies. Preflight must check runtime/package/binary availability.
  • The SDK exports an EffortLevel type with low, medium, high, xhigh, and max; it also exposes model capability metadata such as supported effort levels. The catalog's top-tier composition is not stable across probes (2026-07 observations of sonnet listed max without xhigh in one probe and all five levels in another), so preflight resolves against the live catalog and substitutes within the top tier (xhigh/max) instead of failing.

Pi Agent SDK

Sources:

  • Official SDK docs: https://pi.dev/docs/latest/sdk
  • Local upstream clone: /Users/auro/code/upstream/pi-mono at commit f2b105dd56
  • Local docs: packages/coding-agent/docs/sdk.md, packages/coding-agent/docs/models.md, packages/agent/README.md, packages/ai/README.md
  • Local source: packages/agent/src/types.ts, packages/ai/src/models.ts, packages/coding-agent/src/core/sdk.ts

Findings:

  • Pi exposes createAgentSession() through @earendil-works/pi-coding-agent.
  • Model selection uses getModel(...) and ModelRegistry; modelRegistry.find(...) can include custom models, and modelRegistry.getAvailable() filters to models with valid credentials.
  • Auth resolution flows through AuthStorage: runtime overrides, stored auth.json, environment variables, then custom provider fallback.
  • AuthStorage offers inMemory() (isolated, env-only) and create(authPath?) (file-backed, reads the CLI's auth.json, auto-refreshes OAuth with file locking). The Pi adapter defaults to inMemory() and switches to create(authPath?) when a reviewer sets sdkOptions.authSource: "shared", with optional sdkOptions.authPath. This shares the CLI login (including OAuth providers like openai-codex) without spawning the executable; the isolated default keeps tests credential-free.
  • Pi supports thinkingLevel: off, minimal, low, medium, high, xhigh.
  • Pi computes supported thinking levels from model metadata. Non-reasoning models support only off; models with thinkingLevelMap can disable levels with null or map xhigh to provider-specific values such as max.
  • Pi exposes clampThinkingLevel(model, level), which maps unsupported requested levels to the nearest supported level. diffwarden mirrors that behavior through the Pi adapter and records requested/effective effort values.
  • The lower-level agent package supports TypeBox-schema tools, validated tool arguments, beforeToolCall, afterToolCall, and tool results with terminate: true.
  • A typed terminating review_output tool is the preferred structured-output path for Pi.
  • Pi includes read-only tool factory exports, including createReadOnlyTools, plus individual read/search/list tool factories. The adapter should construct an explicitly read-only tool set rather than relying on defaults.
  • Pi's provider/model system supports OpenRouter, OpenCode-family providers, provider compatibility flags, model overrides, custom models, and provider-specific reasoning controls. These belong in reviewer profiles and providerOptions/sdkOptions, not in the main CLI flag surface.

Multi-reviewer design implication

The current runner design should stay in v1 even if aggregation starts conservative. Single-reviewer, two-reviewer, and fully customized reviewer sets are the same runtime shape:

diffwarden review --target base:main
diffwarden review --target base:main --reviewer cursor --reviewer claude
diffwarden review --target base:main --reviewer cursor:fast --reviewer claude:sonnet-high --reviewer pi:openrouter-high

The runner should treat default review as a one-element reviewer set. This avoids special casing and keeps multi-reviewer behavior foundational rather than bolted on later.

12. Git behavior

12.1 Repo detection

Use git commands from cwd:

git rev-parse --show-toplevel
git rev-parse HEAD

If no git repo is found, exit 2 with a helpful error.

12.2 Uncommitted target

Need to include:

  • staged changes
  • unstaged changes
  • untracked files

Implementation notes:

  • git diff --staged
  • git diff
  • git ls-files --others --exclude-standard
  • For untracked files, include file path and content summary or synthesize a diff using git diff --no-index /dev/null <file>.
  • Preserve deleted file, renamed file, binary file, and mode-change metadata in the resolved target.
  • For large untracked files, include path, size, and a truncation note rather than sending unlimited content.

12.3 Base target

For base:main:

git merge-base HEAD main
git diff <merge-base>

12.4 Commit target

For commit:<sha>:

git show --format=fuller --stat --patch <sha>

Or:

git diff <sha>^ <sha>

Use the simpler reliable version first.

For v1, commit:<sha> is part of the required surface. Prefer a patch form that keeps changed-line information easy to validate:

git diff <sha>^ <sha>

Use git show --format=fuller --stat --patch <sha> only when the extra commit metadata is needed in the prompt or artifact.

13. Validation behavior

Validation should happen after parsing and before rendering.

Checks:

  1. Result matches the ReviewResult schema.
  2. priority is one of 0, 1, 2, 3.
  3. confidence_score is between 0 and 1.
  4. absolute_file_path exists or maps to a changed file.
  5. line_range.start <= line_range.end.
  6. Line ranges overlap changed lines when diff hunk data is available.
  7. Titles include a priority prefix.

Strict mode:

  • Any schema parse failure exits 4.
  • Invalid locations are reported and exit 4.
  • Valid schema with invalid locations should be distinguishable from invalid schema in the ReviewArtifact.

Non-strict mode:

  • Render what can be rendered.
  • Include validation warnings in verbose mode or artifact JSON.
  • A review with useful text but bad location mapping should still be readable in human and agent output.

14. Review output modes

14.1 Human default

By default, diffwarden review --target ... renders a frameworkless human display from runReviewEvents or runReviewBatchEvents and the final review artifact. The display should show reviewer fan-out, preflight/run status, warnings, failed reviewers, verdict, confidence, and finding summaries. Batch focus output should announce lanes, show a top-level merged summary, and keep each lane separately inspectable. It must not write ANSI presentation, icons, spinners, or human-only layout state into JSON or NDJSON contracts.

14.2 Agent output

diffwarden review --agent emits plain text optimized for coding agents. It is not JSON and does not include ANSI, spinner text, full-screen state, or decorative framing. It should include the target, verdict, confidence, finding count, reviewer status, warnings, failed reviewers, file/line references, reviewer attribution, finding bodies, and overall explanation. For focus batch runs, agent output should name the overview and focus lanes, show the top-level merged result, and include lane-specific summaries without ANSI or terminal presentation.

Example:

Diffwarden Review
Target: base:main
Verdict: patch is incorrect
Confidence: 0.84
Findings: 1 (P1 1)
Reviewers: cursor
Reviewer status: 1 passed, 0 failed

Findings:
1. P1 [P1] Null response can crash retry loop
File: src/client.ts:42-44
Confidence: 0.91
Body:
When the API returns 204, response.body is null, but this code unconditionally calls response.body.getReader().

14.3 JSON artifact

Example:

diffwarden review --target base:main --json --out review.json

--json prints the full review artifact JSON object to stdout. No-focus reviews emit ReviewArtifact; focus reviews emit ReviewBatchArtifact. The full artifact is the stable automation contract because callers need reviewer, target, validation, lane, and timing metadata. --out writes the same artifact to a file regardless of display mode. If a narrower payload becomes useful later, add an explicit --json-result-only option.

14.4 NDJSON event stream

diffwarden review --target base:main --ndjson

--ndjson streams typed ReviewEvent frames and terminates with exactly one final_result or error frame. Per-reviewer reviewer_result frames are provisional; only final_result.artifact is authoritative.

For batch runs, the stream starts with batch_started, then emits lane-scoped lifecycle events carrying lane_id. The existing lifecycle vocabulary is reused for lane work: run_started, preflight_started, preflight_finished, reviewer_started, reviewer_result, and reviewer_failed. A lane emits lane_finished with its lane artifact or lane_failed with its error. The stream still has exactly one terminal frame: final_result carrying the full ReviewBatchArtifact, or error when no valid batch artifact can be produced.

14.5 Saved artifact view

diffwarden review show <path> renders an existing ReviewArtifact or ReviewBatchArtifact JSON file through the human display path. diffwarden review show <path> --agent renders the same plain text as an agent-mode final summary, and diffwarden review show <path> --json normalizes and prints the artifact as JSON. --ndjson is rejected for saved artifacts because there are no live review events to replay.

See docs/adr/0001-human-review-experience.md for the terminal framework decision.

15. Configuration

Initial precedence:

  1. CLI flags
  2. environment variables
  3. project config file
  4. user config file (base merged with an optional host-local overlay)
  5. built-in defaults for non-reviewer behavior only

The user config may be split into a syncable base (diffwarden.config.json) and a host-local overlay (diffwarden.config.local.json in the same directory, never synced). At load time the overlay's raw JSON is deep-merged over the base before schema validation: objects merge key-wise with local winning per key, scalars and arrays take the local value wholesale (no deletion markers), and reviewers merge by id (a matching id overlays that entry's fields; a new id appends and must be complete). reviewerSets merge per set name with member arrays replacing wholesale. The overlay never applies to a project config, and an overlay without a base is ignored. With no overlay file, behavior is byte-identical to a single config file.

Possible environment variables:

DIFFWARDEN_REVIEWERS=cursor,claude,pi:openrouter-high
DIFFWARDEN_REVIEWER_SET=2
DIFFWARDEN_MODEL=<sdk-model-id>
DIFFWARDEN_EFFORT=high
DIFFWARDEN_TIMEOUT_SECONDS=300
CURSOR_API_KEY=...
ANTHROPIC_API_KEY=...

Config file is required for real SDK runs. Discover config in this order:

  1. diffwarden.config.json from cwd upward to the git repo root.
  2. $XDG_CONFIG_HOME/diffwarden/diffwarden.config.json.
  3. ~/.config/diffwarden/diffwarden.config.json when XDG_CONFIG_HOME is unset.

If no config exists, exit 2 with a message explaining where to create one. The CLI can still run credential-free unit tests and fake-adapter tests without a user config.

diffwarden init should create a starter config at $XDG_CONFIG_HOME/diffwarden/diffwarden.config.json, or ~/.config/diffwarden/diffwarden.config.json when XDG_CONFIG_HOME is unset. It should create parent directories as needed, refuse to overwrite an existing config unless a future explicit force flag is added, and print the created path.

{
  "defaultReviewerSet": "1",
  "reviewerSets": {
    "1": ["pi"],
    "2": ["pi", "claude"],
    "3": ["pi", "claude", "cursor"],
    "deep": ["pi:openrouter-high", "claude-deep", "cursor-fast"]
  },
  "reviewers": [
    {
      "id": "pi",
      "engine": "pi",
      "model": "claude-sonnet-4-20250514",
      "effort": "medium"
    },
    {
      "id": "cursor-fast",
      "engine": "cursor",
      "model": "composer-2.5",
      "modelCatalog": ["composer-2.5"],
      "effort": "medium"
    },
    {
      "id": "claude-deep",
      "engine": "claude",
      "model": "sonnet",
      "modelCatalog": ["sonnet", "opus"],
      "effort": "high"
    },
    {
      "id": "pi-openrouter-high",
      "engine": "pi",
      "profile": "openrouter-high",
      "provider": "openrouter",
      "model": "anthropic/claude-sonnet",
      "modelCatalog": ["anthropic/claude-sonnet", "openai/gpt-5.2"],
      "effort": "high",
      "providerOptions": {
        "baseUrlEnv": "OPENROUTER_BASE_URL",
        "apiKeyEnv": "OPENROUTER_API_KEY"
      },
      "sdkOptions": {
        "providerProfile": "openrouter"
      }
    }
  ],
  "readonly": true,
  "timeoutSeconds": 300,
  "reviewPlan": {
    "includeOverview": true
  }
}

Rules:

  • If no reviewer is provided, use defaultReviewerSet, then reviewerSets["1"].
  • --reviewer-set <name|count> must resolve to a configured set.
  • --reviewer <spec> may reference a built-in SDK id (pi, claude, cursor, droid) or a named profile.
  • Config validation is part of CLI startup. Unknown reviewer profiles, malformed reviewer sets, invalid model catalogs, and invalid effort catalogs exit 2.
  • reviewPlan.includeOverview controls whether focus runs include the overview lane by default. CLI --overview and --no-overview override it for one run.
  • Secrets in config must be env var references only. Do not support literal API keys in committed or user config.
  • Pi is the recommended default reviewer profile because it supports the broadest provider surface. Claude subscription users should configure a Claude profile, Cursor subscription users should configure a Cursor profile, and other provider routes should generally use Pi profiles.

The public effort vocabulary follows Pi thinking levels plus Claude's top tier: off, minimal, low, medium, high, xhigh, and max. Engines without a distinct max level treat max as xhigh (OpenCode passes it through verbatim because its effort values are model variant names). Adapter mappings are:

  • Pi: treat max as xhigh, clamp the requested level through model metadata, send the effective thinkingLevel, and record requested/effective/supported effort metadata.
  • Claude: off disables thinking; minimal maps to low; low through max pass through natively, with SDK preflight substituting within the top tier (xhigh/max) when the model catalog exposes only one of the two.
  • Cursor: record the requested value as ignored until the SDK exposes an effort control.
  • Droid: omit off, map minimal to low, and pass other supported values through as specModeReasoningEffort for spec-mode reviews.

16. Security and side effects

Default posture:

  • Read-only.
  • No file modification.
  • No external review comment publishing.
  • No network side effects beyond the selected SDK/API.
  • No secret printing.

The read-only posture applies to review runs and to host discovery. diffwarden reviewers discover performs only token-free probes (executable/package presence, environment-variable presence, credential-file readability) and prints probed names and paths but never secret values; shallow discovery makes no network calls and spends no model budget, and --deep reuses the same adapter preflight as doctor. The only commands that modify files are the explicit setup commands diffwarden reviewers add/edit/remove/set and diffwarden init, which write the user config (never a project config or repository file) atomically and never publish anywhere external. Review execution itself still modifies nothing outside the documented Pi shared-auth auth.json refresh case.

If a reviewer adapter requires shell access, restrict the prompt and adapter policy to read/grep/find/git inspection. Write-capable tools are permanently out of scope for this CLI.

Reviewers should be allowed to inspect surrounding code with read-only tools. A diff alone is often insufficient for a high-quality review. The reviewed surface remains the target diff, but adapters may expose safe context tools that allow reading files, listing/searching files, and running read-only git/shell inspection commands.

Codex review reference, refreshed from /Users/auro/code/upstream/codex at commit 462deb0426bf on 2026-05-28:

  • Codex review runs as a sub-Codex review task with target-specific prompts from codex-rs/core/src/review_prompts.rs.
  • Codex sets the review subagent base instructions to REVIEW_PROMPT, uses review_model when configured, and sets approval policy to never in codex-rs/core/src/tasks/review.rs.
  • The review-thread path disables web search, goal tools, CSV spawning, collaboration, and multi-agent features for reviews while preserving repository inspection context in codex-rs/core/src/session/review.rs and codex-rs/core/src/tasks/review.rs.
  • Codex review output parsing first tries to deserialize the final agent message as ReviewOutputEvent, then extracts a JSON object substring, then falls back to putting plain text into overall_explanation in codex-rs/core/src/tasks/review.rs.
  • Diffwarden intentionally uses codex exec --output-schema rather than codex review for the Codex CLI transport because exec exposes the same JSON-schema contract used by the shared parser.
  • Diffwarden's Codex app-server transport defaults to schema-constrained turn/start; appServerOptions.reviewMode: "native" opts into Codex review/start and returns rendered review text only unless that text contains parseable ReviewResult JSON.
  • Codex native review/start disables web search inside the review task, so Diffwarden reports native-mode webSearchMode/effectiveWebSearchMode as disabled even when the parent thread requested live.
  • Codex native review/start has no per-request effort field; Diffwarden applies configured effort overrides through thread config as model_reasoning_effort.
  • A future fallback translator may convert non-schema review text into ReviewResult JSON with a model after parsing fails, but translated output must be marked distinctly from native structured output.
  • The Codex tool registry includes shell/unified exec, MCP resource listing/reading, planning/goal tools, view-image, apply-patch, and multi-agent tools depending on configuration in codex-rs/core/src/tools/spec_plan.rs.
  • For diffwarden, only the read-only subset should be exposed: file read/list/search, git diff, git show, git status, git grep, rg, sed, nl, optional Codex web search, and equivalent inspection commands. Do not expose apply-patch, edit/write tools, external comment publishing tools, or multi-agent spawning inside reviewer adapters.

Each adapter must document its read-only capability level:

  • enforced: SDK or sandbox policy prevents writes.
  • tool-restricted: adapter only exposes read-only tools, but enforcement depends on the SDK.
  • prompt-only: the adapter asks for read-only behavior but cannot enforce it.

The CLI should surface this in verbose output and ReviewArtifact metadata once adapter capability reporting exists.

Cursor SDK local mode is tool-restricted with SDK 1.0.31. Diffwarden uses Cursor plan mode, sandbox options, auto-review, empty setting sources, no MCP servers, an ephemeral local store, and a read/grep/glob/ls tool allowlist. Cursor CLI remains prompt-only.

17. Test plan

Use fixtures for core behavior before live SDK calls.

17.1 Unit tests

  • target parsing
  • git target resolution with temp repos
  • prompt assembly
  • JSON extraction from messy model output
  • schema validation
  • diff hunk overlap validation
  • human and agent rendering
  • reviewer config expansion
  • model and effort validation
  • adapter preflight error mapping
  • multi-reviewer aggregation

17.2 Adapter smoke tests

Adapter smoke tests should be opt-in because they may require credentials and spend money.

Integration test controls:

pnpm test:live:sdk
pnpm test:live:cli
DIFFWARDEN_LIVE_E2E_REVIEWERS=codex,claude pnpm test:live:e2e
DIFFWARDEN_LIVE_E2E_REVIEWERS=droid DIFFWARDEN_LIVE_DROID_EFFORT=low pnpm test:live:e2e
INTEGRATION_DISABLE=cursor,claude pnpm test:live

The live scripts set INTEGRATION_TEST_ON=1. INTEGRATION_DISABLE is a comma-separated denylist for SDKs or CLIs that are not authenticated or should not spend tokens in the current environment. Default pnpm test, pnpm test:coverage, and pnpm test:e2e commands force INTEGRATION_TEST_ON=0 so inherited shell environment cannot accidentally trigger live model calls.

17.3 End-to-end fixture

Create a tiny fixture repo with an obvious bug:

  • base commit has a safe implementation
  • branch introduces a null dereference or off-by-one
  • expected review contains at least one P1/P2 finding on the changed line

17.4 Tooling

Use a TypeScript CLI stack:

  • tsx for local TypeScript execution.
  • vitest for unit and integration tests.
  • zod for CLI/config/artifact validation.
  • pnpm for package scripts and lockfile.
  • strong tsconfig with strict type checking.
  • linting, formatting, and complexity checks in CI.
  • Prefer Biome for formatting/linting if it covers the needed rules.
  • Add complexity enforcement immediately, even if it requires a second tool beyond Biome.

18. Implementation phases

Phase 1: Scaffold, core CLI, and structured contract

Deliverables:

  • TypeScript package scaffold.
  • pnpm, tsx, vitest, zod, strict TypeScript config, formatting, linting, and complexity-check setup.
  • CLI arg parsing.
  • target parser.
  • git repo resolution.
  • uncommitted, base:<branch>, and commit:<sha> target resolution.
  • prompt builder.
  • output parser.
  • human and agent renderers.
  • JSON ReviewArtifact output.
  • reviewer config expansion.
  • diffwarden.config.json project/user discovery and validation.
  • diffwarden init for creating the user-level config under XDG config paths.
  • default reviewer-set expansion from config.
  • review runner interface for one or more reviewers.
  • tests for core functions.

Phase 2: SDK adapters

Deliverables:

  • src/adapters/cursor.ts
  • src/adapters/claude.ts
  • src/adapters/pi.ts
  • src/adapters/droid.ts
  • SDK-backed execution remains the primary path for Cursor, Claude, and Pi. Droid CLI transport is recommended for routine Droid reviews while SDK behavior around Factory UI session history remains less desirable.
  • shared adapter output handling for { structured }, { text }, capture metadata, timeout, and execution errors.
  • structured output support where each SDK has a reliable schema/tool mechanism.
  • Cursor text-output proof through local SDK execution first; then structured-output proof through a local MCP review_output tool and captured SDK tool_call event args.
  • Claude structured-output implementation through query({ options: { outputFormat: { type: "json_schema", schema } } }).
  • Pi structured-output implementation through a typed terminating review_output tool.
  • Droid structured-output implementation through @factory/droid-sdk JSON Schema output.
  • preflight checks for SDK/runtime requirements, auth, provider/profile coherence, model, and effort.
  • graceful error messages for invalid model, invalid effort, missing requirements, missing auth, timeout, and SDK execution failures.
  • live smoke commands split into SDK, CLI transport, and built-binary e2e suites.
  • model, effort, provider, sdkOptions, and providerOptions mapping per adapter.

Phase 3: Multi-reviewer runner

Deliverables:

  • repeated --reviewer support.
  • --reviewer-set <name|count> support.
  • concurrent reviewer execution with timeout handling.
  • support for multiple reviewer configs using the same SDK.
  • per-reviewer artifacts plus deterministic aggregate rendering.
  • human and agent output sorted by priority/location with reviewer attribution.
  • full JSON grouped by reviewer.

Phase 4: Validation hardening

Deliverables:

  • diff hunk parser
  • location overlap checks
  • strict mode
  • clear schema-vs-location validation reporting

Phase 5: Deferred read-only target expansion

Deliverables:

  • no external review comment publishing.
  • no write-capable tools.

19. Acceptance criteria for v1

  1. Running diffwarden review --target uncommitted from a git repo uses the default reviewer set from config.
  2. Running diffwarden review --target uncommitted without any project or user config exits 2 with a clear config-required message.
  3. Running diffwarden review --target base:main --reviewer pi --json completes through the Pi Agent SDK and prints JSON.
  4. Running diffwarden review --target base:main --reviewer claude --json completes through the Claude Agent SDK and prints JSON.
  5. Running diffwarden review --target base:main --reviewer cursor --json completes through the Cursor Agent SDK and prints JSON.
  6. Running diffwarden review --target commit:<sha> --reviewer cursor reviews only that commit.
  7. Running diffwarden review --target base:main --reviewer-set 2 --json --out review.json writes a valid multi-reviewer ReviewArtifact.
  8. Running diffwarden review --target base:main --reviewer cursor --reviewer claude --reviewer pi:openrouter-high --json --out review.json writes a valid explicit multi-reviewer ReviewArtifact.
  9. Human and --agent output sort findings by priority/location and include reviewer attribution.
  10. Multi-reviewer runs render partial successful results with warnings unless all reviewers fail or --strict is set.
  11. The CLI exits 2 for invalid targets or non-git directories.
  12. The CLI exits 2 with a clear message for invalid effort values and locally invalid model values.
  13. The CLI exits 3 with a clear message for missing SDK/runtime requirements, missing SDK-required executables, missing auth, provider setup failures, timeouts, and SDK execution failures.
  14. Claude adapter returns structured from native structured output.
  15. Pi adapter returns structured from a terminating review_output tool.
  16. Cursor adapter documents and tests local SDK text capture and the later MCP review_output structured-output path; if Cursor does not call the tool, terminal text capture and centralized parser metadata remain explicit.
  17. The parser can recover JSON from typical fenced-code model output for adapters that cannot produce structured output directly.
  18. The renderer displays findings, file paths, line ranges, verdict, confidence, and a clear no-findings state.
  19. No files are modified by default.
  20. Core tests pass without live SDK credentials.

20. Open decisions

  1. Whether to add a separate direct executable adapter for Claude Code subscription auth later.

Resolved design decisions:

  • Reviewer spec grammar: use engine[:profile] for v1. Inline model/provider expressions are deferred.
  • Public effort mappings: Pi clamps through model metadata, Claude maps to native effort/thinking controls, and Cursor records effort as ignored until the SDK exposes a concrete control.
  • Multi-reviewer deduplication: exact file, exact line range, exact priority, and normalized-title match only. Do not fuzzy-merge findings in v1.
  • Adapter shape: adapters run SDKs and capture output; core code owns prompt assembly, parsing, validation, rendering, and aggregation.
  • Cursor sequencing: prove local SDK execution and terminal text capture first; then add a local review_output custom tool with streamed tool_call capture as the preferred structured-output upgrade, based on @cursor/sdk@1.0.18 exposing custom tools, MCP config, and tool-call events but no native JSON-schema output option.
  • Package/repository/CLI name: diffwarden; public GitHub repo is aurokin/diffwarden; npm publishing provides the CLI package.
  • Config file name is diffwarden.config.json.
  • Config is required for real SDK runs and should be discovered through project config, then XDG user config.
  • diffwarden init creates the user-level config.
  • ReviewArtifact.schema_version is 2; saved artifacts must use engine and v2 transport names.
  • Live integration tests are gated by INTEGRATION_TEST_ON with INTEGRATION_DISABLE as an SDK/CLI denylist.
  • License: MIT.
  • Package manager/tooling: pnpm, tsx, vitest, zod, strict TypeScript, formatting, linting, and complexity enforcement from the first scaffold.

21. First implementation recommendation

Start with the smallest useful SDK-backed tool that proves the full shape and answers SDK uncertainty before broader implementation:

diffwarden review --target uncommitted
diffwarden review --target base:main --reviewer claude --json
diffwarden review --target base:main --reviewer pi --json
diffwarden review --target commit:abc123 --reviewer cursor
diffwarden review --target base:main --reviewer-set 2
diffwarden review --target base:main --reviewer cursor --reviewer claude --reviewer pi:openrouter-high

Build the core around Codex's review schema and prompt, then implement fake adapters that return fixture ReviewResults. Once the contract is proven, implement the Cursor text-capture spike first because it answers whether the CLI can get useful review output from Cursor with a thin adapter. Then implement Claude native structured output, Pi terminating-tool output, and Cursor's MCP review_output upgrade. Keep diffwarden review human by default, make --agent the coding-agent text path, and keep full JSON available through --json.