openairev 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openairev",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Cross-model AI code reviewer — independent review for AI-assisted coding workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@ export class ClaudeCodeAdapter {
25
25
  stream = false,
26
26
  signal,
27
27
  } = {}) {
28
- const args = ['-p', prompt];
28
+ const args = ['-p', prompt, '--max-budget-usd', '5'];
29
29
 
30
30
  if (stream) {
31
31
  args.push('--output-format', 'stream-json', '--verbose', '--include-partial-messages');
@@ -24,7 +24,7 @@ export class CodexAdapter {
24
24
  }
25
25
 
26
26
  args.push(prompt);
27
- args.push('--json');
27
+ args.push('--json', '--sandbox', 'read-only');
28
28
 
29
29
  if (useSchema) {
30
30
  const schemaPath = join(__dirname, '../config', schemaFile);
package/src/cli/review.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { readFileSync } from 'fs';
3
2
  import { loadConfig, getReviewer, getMaxIterations } from '../config/config-loader.js';
4
- import { getDiff } from '../tools/git-tools.js';
3
+ import { hasDiff, buildDiffCmd } from '../tools/git-tools.js';
5
4
  import { runReview } from '../review/review-runner.js';
6
5
  import { runWorkflow } from '../orchestrator/orchestrator.js';
7
6
  import { createSession, saveSession } from '../session/session-manager.js';
@@ -37,13 +36,17 @@ export async function reviewCommand(options) {
37
36
  console.log(` Mode: ${chalk.cyan('implement → review loop')}`);
38
37
  }
39
38
 
40
- let diff = '';
39
+ let diffCmd = '';
40
+ let hasChanges = false;
41
41
  if (options.file) {
42
42
  console.log(` Source: ${chalk.cyan(options.file)}`);
43
- diff = readFileSync(options.file, 'utf-8');
43
+ diffCmd = `cat ${JSON.stringify(options.file)}`;
44
+ hasChanges = true;
44
45
  } else {
45
- try { diff = getDiff(options.diff); } catch { /* no diff yet is ok for workflow mode */ }
46
- if (diff?.trim()) console.log(` Diff lines: ${chalk.cyan(diff.split('\n').length)}`);
46
+ const ref = options.diff || '';
47
+ try { hasChanges = hasDiff(ref); } catch { /* no diff yet is ok for workflow mode */ }
48
+ diffCmd = buildDiffCmd(ref);
49
+ if (hasChanges) console.log(` Diff cmd: ${chalk.cyan(diffCmd)}`);
47
50
  }
48
51
 
49
52
  if (options.specRef) console.log(` Spec: ${chalk.cyan(options.specRef)}`);
@@ -56,13 +59,13 @@ export async function reviewCommand(options) {
56
59
  console.log('');
57
60
 
58
61
  if (options.once) {
59
- if (!diff?.trim()) {
62
+ if (!hasChanges) {
60
63
  console.log(chalk.yellow('No changes found. Stage some changes or specify --diff <ref>.'));
61
64
  process.exit(0);
62
65
  }
63
66
  try {
64
67
  console.log(chalk.dim('Starting review...\n'));
65
- const review = await runReview(diff, { config, reviewerName, cwd, stream: true });
68
+ const review = await runReview(diffCmd, { config, reviewerName, cwd, stream: true });
66
69
 
67
70
  const session = createSession({ executor, reviewer: reviewerName, diff_ref: options.diff || 'auto' });
68
71
  session.iterations.push({ round: 1, review, timestamp: new Date().toISOString() });
@@ -83,7 +86,7 @@ export async function reviewCommand(options) {
83
86
  } else {
84
87
  try {
85
88
  const result = await runWorkflow({
86
- config, executor, reviewerName, maxRounds, diff, diffRef: options.diff,
89
+ config, executor, reviewerName, maxRounds, diffRef: options.diff,
87
90
  taskDescription: options.task, specRef: options.specRef, tools: config.tools,
88
91
  cwd, skipAnalyze, skipPlan,
89
92
  onStageChange: (stage) => console.log(chalk.bold(`\n[${stageLabel(stage)}]\n`)),
package/src/cli/wait.js CHANGED
@@ -61,6 +61,9 @@ function printResult(data) {
61
61
  }
62
62
 
63
63
  console.log('');
64
+ if (data.partial_notice) {
65
+ console.log(`\n⚠ ${data.partial_notice}\n`);
66
+ }
64
67
  if (data.executor_feedback) {
65
68
  console.log(data.executor_feedback);
66
69
  } else if (data.verdict) {
@@ -6,7 +6,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { z } from 'zod';
8
8
  import { loadConfig, getReviewer } from '../config/config-loader.js';
9
- import { getDiff } from '../tools/git-tools.js';
9
+ import { getDiff, hasDiff, buildDiffCmd } from '../tools/git-tools.js';
10
10
  import { runToolGates } from '../tools/tool-runner.js';
11
11
  import { runReview } from '../review/review-runner.js';
12
12
  import { createSession, saveSession } from '../session/session-manager.js';
@@ -29,28 +29,19 @@ server.tool(
29
29
  'TRIGGER: Use this tool when the user says "review", "review my code", "get a review", "check my changes", "openairev", or asks for independent/cross-model code review. Sends current code changes to a DIFFERENT AI model for independent review. The review starts in the background and returns immediately. After calling this, run `openairev wait` via Bash to stream progress and get the verdict — one blocking call, no polling needed.',
30
30
  {
31
31
  executor: z.string().optional().describe('Which agent wrote the code (claude_code or codex). If you are Claude Code, set this to "claude_code". If you are Codex, set this to "codex".'),
32
- diff: z.string().optional().describe('The diff to review. IMPORTANT: Pass only the diff for files YOU changed, not the entire repo. Use `git diff HEAD -- file1 file2` to scope it. If omitted, auto-detects from git which may be too large.'),
33
- diff_cmd: z.string().optional().describe('The git command used to get the diff, e.g. "git diff HEAD -- src/auth.ts src/routes.ts". If provided instead of diff, the server will run this command to get the diff.'),
32
+ diff_cmd: z.string().optional().describe('The git diff command for the reviewer to run, e.g. "git diff HEAD -- src/auth.ts src/routes.ts". The reviewer runs in the same repo and executes this itself. If omitted, auto-detects staged or unstaged changes.'),
34
33
  task_description: z.string().optional().describe('What the code is supposed to do. Used for requirement checking.'),
35
34
  },
36
- async ({ executor, diff, diff_cmd, task_description }) => {
35
+ async ({ executor, diff_cmd, task_description }) => {
37
36
  const execAgent = executor || Object.keys(config.agents || {}).find(a => config.agents[a].available);
38
37
  const reviewerName = getReviewer(config, execAgent);
39
38
  if (!reviewerName) {
40
39
  return { content: [{ type: 'text', text: `No reviewer configured for executor "${execAgent}"` }] };
41
40
  }
42
41
 
43
- let diffContent = diff;
44
- if (!diffContent && diff_cmd) {
45
- try {
46
- const { execSync } = await import('child_process');
47
- diffContent = execSync(diff_cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, cwd });
48
- } catch (e) {
49
- return { content: [{ type: 'text', text: `diff_cmd failed: ${e.message}` }] };
50
- }
51
- }
52
- if (!diffContent) diffContent = getDiff();
53
- if (!diffContent?.trim()) {
42
+ const reviewDiffCmd = diff_cmd || buildDiffCmd();
43
+
44
+ if (!hasDiff()) {
54
45
  return { content: [{ type: 'text', text: 'No changes found to review.' }] };
55
46
  }
56
47
 
@@ -62,7 +53,7 @@ server.tool(
62
53
  };
63
54
 
64
55
  activeAbort = new AbortController();
65
- activeReview = runReview(diffContent, {
56
+ activeReview = runReview(reviewDiffCmd, {
66
57
  config,
67
58
  reviewerName,
68
59
  taskDescription: task_description,
@@ -76,13 +67,16 @@ server.tool(
76
67
  session.status = 'completed';
77
68
  saveSession(session, cwd);
78
69
 
79
- writeProgress({
80
- status: 'completed',
70
+ const progressData = {
71
+ status: review.error ? 'error' : 'completed',
81
72
  reviewer: reviewerName,
82
73
  progress: review.progress || [],
83
74
  verdict: review.verdict,
84
75
  executor_feedback: review.executor_feedback,
85
- });
76
+ };
77
+ if (review.error) progressData.error = review.error;
78
+ if (review.partial_notice) progressData.partial_notice = review.partial_notice;
79
+ writeProgress(progressData);
86
80
  activeReview = null;
87
81
  activeAbort = null;
88
82
  return review;
@@ -1,7 +1,7 @@
1
1
  import { createAdapter } from '../agents/registry.js';
2
2
  import { runReview } from '../review/review-runner.js';
3
3
  import { loadPromptFile } from '../review/prompt-loader.js';
4
- import { getDiff } from '../tools/git-tools.js';
4
+ import { hasDiff, buildDiffCmd } from '../tools/git-tools.js';
5
5
  import { runToolGates } from '../tools/tool-runner.js';
6
6
  import {
7
7
  createChain, transitionTo, addRound, setArtifact,
@@ -44,7 +44,7 @@ export async function runWorkflow({
44
44
  }
45
45
  }
46
46
 
47
- let currentDiff = initialDiff;
47
+ let hasChanges = !!initialDiff;
48
48
  let codeReviewCount = 0;
49
49
  let planReviewCount = 0;
50
50
 
@@ -140,13 +140,7 @@ export async function runWorkflow({
140
140
  const prompt = buildImplementationPrompt(chain, specRef);
141
141
  await runExecutor(executor, config, prompt, chain, cwd);
142
142
 
143
- try {
144
- currentDiff = getDiff(diffRef);
145
- } catch {
146
- currentDiff = '';
147
- }
148
-
149
- if (currentDiff?.trim()) {
143
+ if (hasDiff(diffRef)) {
150
144
  setArtifact(chain, 'current_diff_ref', diffRef || 'auto', cwd);
151
145
  transitionTo(chain, 'code_review', cwd);
152
146
  } else {
@@ -168,7 +162,8 @@ export async function runWorkflow({
168
162
  toolResults = runToolGates(Object.keys(tools), cwd, tools);
169
163
  }
170
164
 
171
- const review = await runReviewRound(reviewerName, config, currentDiff, {
165
+ const reviewDiffCmd = diffRef ? `git diff ${diffRef}` : 'git diff --staged || git diff';
166
+ const review = await runReviewRound(reviewerName, config, reviewDiffCmd, {
172
167
  kind: 'code_review', chain, specRef, cwd,
173
168
  });
174
169
 
@@ -205,14 +200,7 @@ export async function runWorkflow({
205
200
  const feedback = buildFeedback(lastVerdict, cwd);
206
201
  await runExecutor(executor, config, feedback, chain, cwd);
207
202
 
208
- try {
209
- currentDiff = getDiff(diffRef);
210
- } catch (e) {
211
- closeChain(chain, 'error', cwd);
212
- return { chain, status: 'error', message: `Failed to get diff: ${e.message}` };
213
- }
214
-
215
- if (!currentDiff?.trim()) {
203
+ if (!hasDiff(diffRef)) {
216
204
  closeChain(chain, 'error', cwd);
217
205
  return { chain, status: 'error', message: 'No changes after fix attempt' };
218
206
  }
@@ -255,15 +243,16 @@ async function runExecutor(executor, config, prompt, chain, cwd) {
255
243
  return { output: result?.result || result?.raw || null, session_id: sessionId };
256
244
  }
257
245
 
258
- async function runReviewRound(reviewerName, config, content, { kind, chain, specRef, cwd }) {
246
+ async function runReviewRound(reviewerName, config, input, { kind, chain, specRef, cwd }) {
259
247
  const promptFile = kind === 'plan_review' ? 'plan-reviewer.md' : 'reviewer.md';
260
248
  const sessionId = getReviewerSession(chain, kind);
261
249
 
262
- return runReview(content, {
250
+ return runReview(input, {
263
251
  config, reviewerName, promptFile,
264
252
  taskDescription: chain.task?.user_request,
265
253
  specRef: specRef || chain.task?.spec_ref,
266
254
  cwd, sessionId, stream: true,
255
+ inputMode: kind === 'plan_review' ? 'inline' : 'diff_cmd',
267
256
  });
268
257
  }
269
258
 
@@ -1,10 +1,9 @@
1
1
  import { mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { createAdapter } from '../agents/registry.js';
4
- import { stageInput, buildInputReference } from './input-stager.js';
5
4
  import { loadPromptFile } from './prompt-loader.js';
6
5
 
7
- export async function runReview(content, {
6
+ export async function runReview(input, {
8
7
  config,
9
8
  reviewerName,
10
9
  promptFile = 'reviewer.md',
@@ -14,6 +13,7 @@ export async function runReview(content, {
14
13
  sessionId = null,
15
14
  stream = false,
16
15
  signal,
16
+ inputMode = 'diff_cmd',
17
17
  }) {
18
18
  const adapter = createAdapter(reviewerName, config, { cwd });
19
19
 
@@ -22,8 +22,6 @@ export async function runReview(content, {
22
22
  }
23
23
 
24
24
  const reviewerPrompt = loadPromptFile(promptFile, cwd);
25
- const staged = stageInput(content, { cwd });
26
- const inputRef = buildInputReference(staged);
27
25
 
28
26
  let prompt = reviewerPrompt;
29
27
  if (taskDescription) {
@@ -32,7 +30,12 @@ export async function runReview(content, {
32
30
  if (specRef) {
33
31
  prompt += `\n\nSpec reference: ${specRef}\nRead the spec file for requirements and acceptance criteria.`;
34
32
  }
35
- prompt = `${prompt}${inputRef}`;
33
+
34
+ if (inputMode === 'diff_cmd') {
35
+ prompt += `\n\n--- CHANGES TO REVIEW ---\nRun this command to get the diff:\n\`${input}\`\n\nReview the changed files. You are in the same repo as the executor — run the diff command yourself, read the files, and produce your verdict.`;
36
+ } else {
37
+ prompt += `\n\n--- CONTENT TO REVIEW ---\n${input}`;
38
+ }
36
39
 
37
40
  const schemaFile = promptFile === 'plan-reviewer.md' ? 'plan-verdict-schema.json' : 'verdict-schema.json';
38
41
 
@@ -51,7 +54,7 @@ export async function runReview(content, {
51
54
 
52
55
  logReviewerOutput(rawOutput, reviewerName, cwd);
53
56
 
54
- return {
57
+ const reviewResult = {
55
58
  reviewer: reviewerName,
56
59
  verdict,
57
60
  executor_feedback: executorFeedback,
@@ -59,6 +62,17 @@ export async function runReview(content, {
59
62
  progress: result?.progress || [],
60
63
  session_id: adapter.sessionName || adapter.sessionId,
61
64
  };
65
+
66
+ if (!verdict) {
67
+ const explicitError = result?.error;
68
+ if (explicitError) {
69
+ reviewResult.error = `Reviewer (${reviewerName}) failed: ${explicitError}`;
70
+ } else {
71
+ reviewResult.error = `Reviewer (${reviewerName}) produced no verdict. Possible causes: context budget exceeded, auth failure, or schema mismatch. Check .openairev/logs/ for details.`;
72
+ }
73
+ }
74
+
75
+ return reviewResult;
62
76
  }
63
77
 
64
78
  function logReviewerOutput(rawOutput, reviewerName, cwd) {
@@ -34,6 +34,38 @@ export function getDiff(ref, { context = 1, excludes = EXCLUDE_PATTERNS } = {})
34
34
  return '';
35
35
  }
36
36
 
37
+ /**
38
+ * Check if there are changes without reading the diff content.
39
+ * Uses git diff --quiet (exit code only, no output).
40
+ */
41
+ export function hasDiff(ref) {
42
+ try {
43
+ if (ref) {
44
+ execFileSync('git', ['diff', '--quiet', ref]);
45
+ return false;
46
+ }
47
+ // Try staged first
48
+ try {
49
+ execFileSync('git', ['diff', '--quiet', '--cached']);
50
+ // exit 0 = no staged changes, try unstaged
51
+ execFileSync('git', ['diff', '--quiet']);
52
+ return false; // no changes at all
53
+ } catch {
54
+ return true; // either staged or unstaged has changes
55
+ }
56
+ } catch {
57
+ return true; // exit 1 = has changes
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Build a diff command string for the reviewer to run.
63
+ */
64
+ export function buildDiffCmd(ref) {
65
+ if (ref) return `git diff ${ref}`;
66
+ return 'git diff --staged || git diff';
67
+ }
68
+
37
69
  function gitExec(args) {
38
70
  try {
39
71
  return execFileSync('git', args, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
@@ -1,35 +0,0 @@
1
- import { writeFileSync, mkdirSync } from 'fs';
2
- import { join } from 'path';
3
-
4
- const INLINE_THRESHOLD = 8_000; // characters — inline if under this
5
-
6
- /**
7
- * Stage review input. For small content, returns it inline.
8
- * For large content, writes to .openairev/tmp/ and returns a file reference.
9
- */
10
- export function stageInput(content, { cwd = process.cwd(), label = 'review-input' } = {}) {
11
- if (content.length <= INLINE_THRESHOLD) {
12
- return { mode: 'inline', content };
13
- }
14
-
15
- const tmpDir = join(cwd, '.openairev', 'tmp');
16
- mkdirSync(tmpDir, { recursive: true });
17
-
18
- const filename = `${label}-${Date.now()}.diff`;
19
- const filePath = join(tmpDir, filename);
20
- const relativePath = `.openairev/tmp/${filename}`;
21
-
22
- writeFileSync(filePath, content);
23
-
24
- return { mode: 'file', filePath, relativePath };
25
- }
26
-
27
- /**
28
- * Build the prompt prefix for the first pass based on staging result.
29
- */
30
- export function buildInputReference(staged) {
31
- if (staged.mode === 'inline') {
32
- return `\n\n--- DIFF ---\n${staged.content}`;
33
- }
34
- return `\n\nThe diff to review is stored at: ${staged.relativePath}\nRead that file to see the full changes. It is too large to include inline.`;
35
- }
@@ -1,53 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
4
- import { join } from 'path';
5
- import { stageInput, buildInputReference } from './input-stager.js';
6
-
7
- const TMP = join(process.cwd(), '.test-tmp-stager');
8
-
9
- describe('input-stager', () => {
10
- beforeEach(() => {
11
- mkdirSync(TMP, { recursive: true });
12
- });
13
-
14
- afterEach(() => {
15
- rmSync(TMP, { recursive: true, force: true });
16
- });
17
-
18
- it('inlines small content', () => {
19
- const result = stageInput('small diff', { cwd: TMP });
20
- assert.equal(result.mode, 'inline');
21
- assert.equal(result.content, 'small diff');
22
- });
23
-
24
- it('writes large content to file', () => {
25
- const large = 'x'.repeat(10_000);
26
- const result = stageInput(large, { cwd: TMP });
27
- assert.equal(result.mode, 'file');
28
- assert.ok(result.filePath);
29
- assert.ok(result.relativePath.startsWith('.openairev/tmp/'));
30
- assert.ok(existsSync(result.filePath));
31
- assert.equal(readFileSync(result.filePath, 'utf-8'), large);
32
- });
33
-
34
- it('buildInputReference returns inline content for small input', () => {
35
- const staged = { mode: 'inline', content: 'diff here' };
36
- const ref = buildInputReference(staged);
37
- assert.ok(ref.includes('diff here'));
38
- assert.ok(ref.includes('--- DIFF ---'));
39
- });
40
-
41
- it('buildInputReference returns file path for large input', () => {
42
- const staged = { mode: 'file', relativePath: '.openairev/tmp/test.diff' };
43
- const ref = buildInputReference(staged);
44
- assert.ok(ref.includes('.openairev/tmp/test.diff'));
45
- assert.ok(ref.includes('Read that file'));
46
- });
47
-
48
- it('uses custom label for filename', () => {
49
- const large = 'y'.repeat(10_000);
50
- const result = stageInput(large, { cwd: TMP, label: 'my-review' });
51
- assert.ok(result.relativePath.includes('my-review'));
52
- });
53
- });