openairev 0.3.9 → 0.3.11

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.9",
3
+ "version": "0.3.11",
4
4
  "description": "Cross-model AI code reviewer — independent review for AI-assisted coding workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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/init.js CHANGED
@@ -257,13 +257,15 @@ ${marker}
257
257
 
258
258
  This project uses OpenAIRev for independent AI code review. When the user asks to "review", "get a review", or "check my changes":
259
259
 
260
- 1. Call \`openairev_review\` (MCP tool) with \`executor: "claude_code"\` and \`diff_cmd\` scoped to the files you changed (e.g. \`"git diff HEAD -- src/auth.ts src/routes.ts"\`). It returns immediately.
261
- 2. Run \`openairev wait\` via Bash. It streams live progress and blocks until the verdict is ready — one command, that's it.
260
+ 1. Call \`openairev_review\` (MCP tool) with \`executor: "claude_code"\` and \`diff_cmd\` scoped to the files you changed (e.g. \`"git diff HEAD -- src/auth.ts src/routes.ts"\`). It returns immediately with a progress file path.
261
+ 2. Run \`openairev wait --file <progress_file_path>\` via Bash using the path from step 1. It streams live progress and blocks until the verdict is ready.
262
262
 
263
263
  **Rules — you MUST follow these:**
264
+ - ALWAYS pass \`--file\` to \`openairev wait\` with the exact path from the review response. Without it, wait may look in the wrong directory.
264
265
  - ALWAYS scope \`diff_cmd\` to your changed files. Never send the full repo diff — the reviewer will run out of budget.
265
- - NEVER use \`sleep\`, polling loops, or manually read progress.json. \`openairev wait\` handles everything.
266
+ - NEVER use \`sleep\`, polling loops, or manually read progress.json. \`openairev wait --file\` handles everything.
266
267
  - NEVER call \`openairev_status\`. Use \`openairev wait\` instead.
268
+ - NEVER re-call \`openairev_review\` if wait fails — the progress file may take a few seconds to appear. Just retry \`openairev wait --file\`.
267
269
  - If a review is stuck or you want to retry, call \`openairev_cancel\` to kill the running review, then start a new one.
268
270
  - You MAY launch the review + wait in a sub-agent and continue other work in parallel.
269
271
  - Treat review feedback as peer review — use your judgment, focus on \`critical_issues\` and \`repair_instructions\`.
@@ -313,13 +315,15 @@ ${marker}
313
315
 
314
316
  This project uses OpenAIRev for independent AI code review. When the user asks to "review", "get a review", or "check my changes":
315
317
 
316
- 1. Call \`openairev_review\` (MCP tool) with \`executor: "codex"\` and \`diff_cmd\` scoped to the files you changed (e.g. \`"git diff HEAD -- src/auth.ts src/routes.ts"\`). It returns immediately.
317
- 2. Run \`openairev wait\` via Bash. It streams live progress and blocks until the verdict is ready — one command, that's it.
318
+ 1. Call \`openairev_review\` (MCP tool) with \`executor: "codex"\` and \`diff_cmd\` scoped to the files you changed (e.g. \`"git diff HEAD -- src/auth.ts src/routes.ts"\`). It returns immediately with a progress file path.
319
+ 2. Run \`openairev wait --file <progress_file_path>\` via Bash using the path from step 1. It streams live progress and blocks until the verdict is ready.
318
320
 
319
321
  **Rules — you MUST follow these:**
322
+ - ALWAYS pass \`--file\` to \`openairev wait\` with the exact path from the review response. Without it, wait may look in the wrong directory.
320
323
  - ALWAYS scope \`diff_cmd\` to your changed files. Never send the full repo diff — the reviewer will run out of budget.
321
- - NEVER use \`sleep\`, polling loops, or manually read progress.json. \`openairev wait\` handles everything.
324
+ - NEVER use \`sleep\`, polling loops, or manually read progress.json. \`openairev wait --file\` handles everything.
322
325
  - NEVER call \`openairev_status\`. Use \`openairev wait\` instead.
326
+ - NEVER re-call \`openairev_review\` if wait fails — the progress file may take a few seconds to appear. Just retry \`openairev wait --file\`.
323
327
  - If a review is stuck or you want to retry, call \`openairev_cancel\` to kill the running review, then start a new one.
324
328
  - Treat review feedback as peer review — use your judgment, focus on \`critical_issues\` and \`repair_instructions\`.
325
329
  ${marker}
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) {
@@ -76,13 +76,16 @@ server.tool(
76
76
  session.status = 'completed';
77
77
  saveSession(session, cwd);
78
78
 
79
- writeProgress({
80
- status: 'completed',
79
+ const progressData = {
80
+ status: review.error ? 'error' : 'completed',
81
81
  reviewer: reviewerName,
82
82
  progress: review.progress || [],
83
83
  verdict: review.verdict,
84
84
  executor_feedback: review.executor_feedback,
85
- });
85
+ };
86
+ if (review.error) progressData.error = review.error;
87
+ if (review.partial_notice) progressData.partial_notice = review.partial_notice;
88
+ writeProgress(progressData);
86
89
  activeReview = null;
87
90
  activeAbort = null;
88
91
  return review;
@@ -96,7 +99,7 @@ server.tool(
96
99
  return {
97
100
  content: [{
98
101
  type: 'text',
99
- text: `Review started. Reviewer: ${reviewerName}\nProgress file: ${PROGRESS_FILE}\n\nRun \`openairev wait\` from ${cwd} or read ${PROGRESS_FILE} directly to stream progress and get the verdict.`,
102
+ text: `Review started. Reviewer: ${reviewerName}\nProgress file: ${PROGRESS_FILE}\n\nRun \`openairev wait --file ${PROGRESS_FILE}\` via Bash to stream progress and get the verdict. The file may take a few seconds to appear — wait will handle this automatically. Do NOT re-call openairev_review or use sleep/polling.`,
100
103
  }],
101
104
  };
102
105
  }
@@ -3,6 +3,108 @@ import { join } from 'path';
3
3
 
4
4
  const INLINE_THRESHOLD = 8_000; // characters — inline if under this
5
5
 
6
+ // Rough estimate: ~4 characters per token for code/diffs
7
+ const CHARS_PER_TOKEN = 4;
8
+
9
+ // Reviewer context budgets (in tokens). Leave room for prompt, schema, and output.
10
+ // These are conservative — better to compact than to blow the budget.
11
+ const REVIEWER_BUDGETS = {
12
+ codex: 100_000, // Codex has ~200k context, reserve half for output + tools
13
+ claude_code: 150_000, // Claude has ~200k context, needs less reserve
14
+ };
15
+
16
+ const DEFAULT_BUDGET = 100_000;
17
+
18
+ /**
19
+ * Estimate token count from character length.
20
+ */
21
+ export function estimateTokens(text) {
22
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
23
+ }
24
+
25
+ /**
26
+ * Get the token budget for a reviewer.
27
+ */
28
+ export function getReviewerBudget(reviewerName) {
29
+ return REVIEWER_BUDGETS[reviewerName] || DEFAULT_BUDGET;
30
+ }
31
+
32
+ /**
33
+ * Compact a diff to fit within a token budget.
34
+ * Strategy: parse into per-file hunks, sort by size ascending,
35
+ * drop the largest files first until it fits.
36
+ * Returns { content, compacted, stats }.
37
+ */
38
+ export function compactDiff(diff, { maxTokens }) {
39
+ const tokens = estimateTokens(diff);
40
+ if (tokens <= maxTokens) {
41
+ return { content: diff, compacted: false, partial: false, stats: { originalTokens: tokens, finalTokens: tokens, filesDropped: 0 } };
42
+ }
43
+
44
+ const files = parseDiffFiles(diff);
45
+ // Sort by size descending — drop largest first
46
+ files.sort((a, b) => b.content.length - a.content.length);
47
+
48
+ // Reserve chars for the omission notice (worst case)
49
+ const noticeOverhead = 500;
50
+ const maxChars = (maxTokens * CHARS_PER_TOKEN) - noticeOverhead;
51
+ let totalChars = diff.length;
52
+ const dropped = [];
53
+
54
+ while (totalChars > maxChars && files.length > 1) {
55
+ const largest = files.shift();
56
+ totalChars -= largest.content.length;
57
+ dropped.push(largest.name);
58
+ }
59
+
60
+ // If single file still too large, truncate it
61
+ if (files.length === 1 && totalChars > maxChars) {
62
+ const file = files[0];
63
+ const truncateAt = maxChars - 200;
64
+ file.content = file.content.slice(0, Math.max(0, truncateAt)) + `\n\n... [TRUNCATED — file too large for reviewer context] ...\n`;
65
+ }
66
+
67
+ let compactedDiff = files.map(f => f.content).join('\n');
68
+ if (dropped.length > 0) {
69
+ const notice = `\n\n--- FILES OMITTED (too large for reviewer context) ---\n${dropped.map(f => ` ${f}`).join('\n')}\n--- This is a PARTIAL review. Only the files below were included. ---\n`;
70
+ compactedDiff = notice + compactedDiff;
71
+ }
72
+
73
+ const finalTokens = estimateTokens(compactedDiff);
74
+
75
+ return {
76
+ content: compactedDiff,
77
+ compacted: true,
78
+ partial: dropped.length > 0,
79
+ stats: {
80
+ originalTokens: tokens,
81
+ finalTokens,
82
+ filesDropped: dropped.length,
83
+ droppedFiles: dropped,
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Parse a unified diff into per-file sections.
90
+ */
91
+ function parseDiffFiles(diff) {
92
+ const files = [];
93
+ // Split on diff headers (diff --git, or --- a/ lines preceded by blank)
94
+ const parts = diff.split(/(?=^diff --git )/m);
95
+
96
+ for (const part of parts) {
97
+ if (!part.trim()) continue;
98
+ const nameMatch = part.match(/^diff --git a\/(.+?) b\//m)
99
+ || part.match(/^---\s+a\/(.+)/m)
100
+ || part.match(/^\+\+\+\s+b\/(.+)/m);
101
+ const name = nameMatch ? nameMatch[1] : 'unknown';
102
+ files.push({ name, content: part });
103
+ }
104
+
105
+ return files;
106
+ }
107
+
6
108
  /**
7
109
  * Stage review input. For small content, returns it inline.
8
110
  * For large content, writes to .openairev/tmp/ and returns a file reference.
@@ -1,7 +1,7 @@
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';
4
+ import { stageInput, buildInputReference, compactDiff, getReviewerBudget, estimateTokens } from './input-stager.js';
5
5
  import { loadPromptFile } from './prompt-loader.js';
6
6
 
7
7
  export async function runReview(content, {
@@ -22,7 +22,12 @@ export async function runReview(content, {
22
22
  }
23
23
 
24
24
  const reviewerPrompt = loadPromptFile(promptFile, cwd);
25
- const staged = stageInput(content, { cwd });
25
+
26
+ // Compact diff if it exceeds the reviewer's context budget
27
+ const budget = getReviewerBudget(reviewerName);
28
+ const { content: compactedContent, compacted, stats } = compactDiff(content, { maxTokens: budget });
29
+
30
+ const staged = stageInput(compactedContent, { cwd });
26
31
  const inputRef = buildInputReference(staged);
27
32
 
28
33
  let prompt = reviewerPrompt;
@@ -51,7 +56,7 @@ export async function runReview(content, {
51
56
 
52
57
  logReviewerOutput(rawOutput, reviewerName, cwd);
53
58
 
54
- return {
59
+ const reviewResult = {
55
60
  reviewer: reviewerName,
56
61
  verdict,
57
62
  executor_feedback: executorFeedback,
@@ -59,6 +64,31 @@ export async function runReview(content, {
59
64
  progress: result?.progress || [],
60
65
  session_id: adapter.sessionName || adapter.sessionId,
61
66
  };
67
+
68
+ if (compacted) {
69
+ reviewResult.context_stats = stats;
70
+ }
71
+
72
+ // Mark partial reviews so callers know files were omitted
73
+ if (compacted && stats.filesDropped > 0 && verdict) {
74
+ reviewResult.partial = true;
75
+ reviewResult.partial_notice = `PARTIAL REVIEW: ${stats.filesDropped} files were omitted to fit the reviewer's context budget (${stats.droppedFiles.join(', ')}). Re-run with a smaller diff to cover them.`;
76
+ }
77
+
78
+ // Diagnose null verdicts — check for explicit errors before blaming context budget
79
+ if (!verdict) {
80
+ const explicitError = result?.error;
81
+ if (explicitError) {
82
+ reviewResult.error = `Reviewer (${reviewerName}) failed: ${explicitError}`;
83
+ } else {
84
+ reviewResult.error = `Reviewer (${reviewerName}) produced no verdict. ` +
85
+ `Diff was ~${stats.originalTokens} tokens` +
86
+ (compacted ? `, compacted to ~${stats.finalTokens} tokens (${stats.filesDropped} files dropped)` : '') +
87
+ `. Possible causes: context budget exceeded, auth failure, or schema mismatch. Check .openairev/logs/ for details.`;
88
+ }
89
+ }
90
+
91
+ return reviewResult;
62
92
  }
63
93
 
64
94
  function logReviewerOutput(rawOutput, reviewerName, cwd) {