Skip to content

Commit 2e29b80

Browse files
authored
Merge pull request #38 from CodeAnt-AI/Chhinna/edmonton
Improve consistency of local reviews
2 parents c781378 + bb32038 commit 2e29b80

8 files changed

Lines changed: 159 additions & 92 deletions

File tree

‎changelog.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [0.5.3] - 24/08/2026
4+
- Improved consistency between interactive and headless local reviews
5+
- Added shared multi-file planning and verification before returning findings
6+
- Increased local review coverage to 15 files, processed in batches of up to five
7+
8+
## [0.5.2] - 07/08/2026
9+
- Use the configured CodeAnt dashboard URL during login
10+
311
## [0.5.1] - 31/05/2026
412
- AI code review increasing coverage
513

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.5.2",
3+
"version": "0.5.3",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"bin": {

‎src/commands/review.js‎

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,24 +81,39 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
8181

8282
// ── Split into per-file requests ───────────────────────────────────
8383
const perFileRequests = ReviewApiHelper.splitIntoPerFileRequests(requestBody);
84+
let riskHypotheses = '';
85+
try {
86+
setCurrentMessage('Planning review risks...');
87+
const plannerResult = await runTurnLoop(
88+
{
89+
diff_content: requestBody.diff_content,
90+
prompt_template_name: 'risk_planner',
91+
},
92+
gitRoot,
93+
() => cancelled,
94+
() => {},
95+
false
96+
);
97+
riskHypotheses = plannerResult.finalOutput?.risk_hypotheses || '';
98+
} catch (plannerError) {
99+
console.error(`Risk planner failed; continuing without hypotheses: ${plannerError.message}`);
100+
}
101+
102+
if (cancelled) return;
84103

85104
// ── Per-file agent turn loops (parallel) ──────────────────────────
86105
setCurrentMessage(`Analyzing ${perFileRequests.length} file${perFileRequests.length !== 1 ? 's' : ''} in parallel...`);
87106

88107
const perFileResults = await Promise.all(
89108
perFileRequests.map((fileReq) => {
90-
const filename = fileReq._filename;
91-
delete fileReq._filename;
92-
93-
// Convert file_contents dict → file_content/file_path for API
94-
if (fileReq.file_contents?.[filename]) {
95-
fileReq.file_content = fileReq.file_contents[filename];
96-
fileReq.file_path = filename;
97-
}
98-
delete fileReq.file_contents;
109+
const payload = {
110+
...fileReq,
111+
extra_variables: { risk_hypotheses: riskHypotheses },
112+
};
113+
delete payload._filenames;
99114

100115
return runTurnLoop(
101-
fileReq,
116+
payload,
102117
gitRoot,
103118
() => cancelled,
104119
() => {},
@@ -110,21 +125,23 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
110125
// Pair each file's suggestions with its own diff — skip files with no real suggestions
111126
const perFileWithSuggestions = perFileRequests.map((fileReq, i) => ({
112127
diff_content: fileReq.diff_content,
113-
suggestions: perFileResults[i].finalMessage?.content,
114-
output: perFileResults[i].finalOutput,
115-
})).filter(r => r.output?.code_suggestions?.length > 0);
128+
file_contents: fileReq.file_contents,
129+
generator_output: perFileResults[i].finalOutput,
130+
rejector_input: perFileResults[i].rejectorInput,
131+
})).filter(r => r.generator_output?.code_suggestions?.length > 0);
116132

117133
if (cancelled) return;
118134

119-
// ── Per-file reflector loops (parallel) ─────────────────────────────
120-
setCurrentMessage('Running reflector...');
121-
const reflectorResults = await Promise.all(
122-
perFileWithSuggestions.map(({ diff_content, suggestions }) =>
135+
// ── Per-batch rejector loops (parallel) ─────────────────────────────
136+
setCurrentMessage('Running rejector...');
137+
const rejectorResults = await Promise.all(
138+
perFileWithSuggestions.map(({ diff_content, file_contents, rejector_input }) =>
123139
runTurnLoop(
124140
{
125141
diff_content,
126-
prompt_template_name: 'reflector',
127-
extra_variables: { suggestion_str: suggestions },
142+
file_contents,
143+
prompt_template_name: 'rejector',
144+
extra_variables: rejector_input,
128145
},
129146
gitRoot,
130147
() => cancelled,
@@ -136,11 +153,23 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
136153

137154
if (cancelled) return;
138155

139-
const reviewIssues = reflectorResults.flatMap(r =>
140-
(r.finalOutput?.code_suggestions || []).map(issue => ({
141-
issue_content: issue.issue_content || '',
156+
const finalizedResults = await Promise.all(
157+
rejectorResults.map((result, i) => fetchApi(
158+
'/extension/pr-review/agent/finalize',
159+
'POST',
160+
{
161+
diff_content: perFileWithSuggestions[i].diff_content,
162+
generator_output: perFileWithSuggestions[i].generator_output,
163+
rejector_output: result.finalOutput || { code_suggestions: [] },
164+
}
165+
))
166+
);
167+
168+
const reviewIssues = finalizedResults.flatMap(r =>
169+
(r.output?.code_suggestions || []).map(issue => ({
170+
issue_content: issue.suggestion_content || issue.one_sentence_summary || '',
142171
relevant_file: issue.relevant_file || 'Unknown',
143-
start_line: issue.start_line || 0,
172+
start_line: issue.relevant_lines_start || 0,
144173
label: issue.label || 'Code Quality',
145174
}))
146175
);
@@ -213,10 +242,11 @@ export default function Review({ scanType = 'all', lastNCommits = 1, failOn = 'C
213242

214243
// ── Turn loop ──────────────────────────────────────────────────────────────
215244

216-
async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isReflectorLoop) {
245+
async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isRejectorLoop) {
217246
let nextPayload = initialPayload;
218247
let finalMessage = null;
219248
let finalOutput = null;
249+
let rejectorInput = null;
220250

221251
for (let turn = 0; turn < MAX_TURNS; turn+=1) {
222252
if (isCancelled()) break;
@@ -239,10 +269,11 @@ async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isRe
239269

240270
finalMessage = assistantMsg;
241271
if (resp.output) finalOutput = resp.output;
272+
if (resp.rejector_input) rejectorInput = resp.rejector_input;
242273

243274
if (done) {
244-
// Handle parsing errors in final message by dumping response content (only on reflector loop)
245-
if (isReflectorLoop && resp.parsing_error) {
275+
// Preserve malformed rejector output for debugging.
276+
if (isRejectorLoop && resp.parsing_error) {
246277
const dumpDir = path.join(gitRoot, '.codeant', 'codeSuggestions');
247278
const dumpFile = path.join(dumpDir, `code_suggestions_${Date.now()}.txt`);
248279
let dumpContent = resp.response?.content || JSON.stringify(resp, null, 2);
@@ -289,6 +320,5 @@ async function runTurnLoop(initialPayload, gitRoot, isCancelled, onMessage, isRe
289320
nextPayload = { session_id: sessionId, tool_results: toolResults };
290321
}
291322

292-
return { finalMessage, finalOutput };
323+
return { finalMessage, finalOutput, rejectorInput };
293324
}
294-

‎src/components/ReviewUI.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export function renderFetchingDiff(startTime) {
180180
}
181181

182182
export function renderReviewing(currentMessage, startTime, meta) {
183-
const step = currentMessage?.toLowerCase().includes('reflector') ? 3 : 2;
183+
const step = currentMessage?.toLowerCase().includes('rejector') ? 3 : 2;
184184
return React.createElement(ReviewLoading, {
185185
message: currentMessage || 'Analyzing code...',
186186
step,

‎src/reviewHeadless.js‎

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import { track } from './utils/analytics.js';
77
const MAX_TURNS = 5;
88

99
/**
10-
* Run a single agent turn loop (per-file or reflector).
10+
* Run a single agent turn loop (planner, generator, or rejector).
1111
*/
12-
async function runTurnLoop(initialPayload, gitRoot, isReflectorLoop) {
12+
async function runTurnLoop(initialPayload, gitRoot, isRejectorLoop) {
1313
let nextPayload = initialPayload;
1414
let finalMessage = null;
1515
let finalOutput = null;
16+
let rejectorInput = null;
1617

1718
for (let turn = 0; turn < MAX_TURNS; turn += 1) {
1819
const resp = await fetchApi('/extension/pr-review/agent/turn', 'POST', nextPayload);
@@ -31,10 +32,11 @@ async function runTurnLoop(initialPayload, gitRoot, isReflectorLoop) {
3132

3233
finalMessage = assistantMsg;
3334
if (resp.output) finalOutput = resp.output;
35+
if (resp.rejector_input) rejectorInput = resp.rejector_input;
3436

3537
if (done) {
36-
if (isReflectorLoop && resp.parsing_error) {
37-
console.error('Warning: parsing error in reflector response');
38+
if (isRejectorLoop && resp.parsing_error) {
39+
console.error('Warning: parsing error in rejector response');
3840
}
3941
break;
4042
}
@@ -52,7 +54,7 @@ async function runTurnLoop(initialPayload, gitRoot, isReflectorLoop) {
5254
nextPayload = { session_id: sessionId, tool_results: toolResults };
5355
}
5456

55-
return { finalMessage, finalOutput };
57+
return { finalMessage, finalOutput, rejectorInput };
5658
}
5759

5860
/**
@@ -126,29 +128,38 @@ export async function runReviewHeadless(options = {}) {
126128
const perFileRequests = ReviewApiHelper.splitIntoPerFileRequests(requestBody);
127129

128130
const totalFiles = perFileRequests.reduce((n, r) => n + (r._filenames?.length || 0), 0);
131+
let riskHypotheses = '';
132+
try {
133+
onProgress(`Planning risks across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}...`);
134+
const plannerResult = await runTurnLoop(
135+
{
136+
diff_content: requestBody.diff_content,
137+
prompt_template_name: 'risk_planner',
138+
},
139+
gitRoot,
140+
false
141+
);
142+
riskHypotheses = plannerResult.finalOutput?.risk_hypotheses || '';
143+
} catch (err) {
144+
console.error(`[warning] Risk planner failed; continuing without hypotheses: ${err.message}`);
145+
}
129146
onProgress(`Analyzing ${totalFiles} file${totalFiles !== 1 ? 's' : ''}...`);
130147

131148
// ── Per-batch agent turn loops (parallel, fault-tolerant) ────────────
132-
// Each batch covers up to 5 files; backend reviews the multi-file diff
133-
// in a single session (matches pragent's FileBatcher).
149+
// Each review session covers a batch of up to five files.
134150
const perFileResults = await Promise.all(
135151
perFileRequests.map(async (fileReq) => {
136152
const filenames = fileReq._filenames || [];
137-
delete fileReq._filenames;
153+
const payload = {
154+
...fileReq,
155+
extra_variables: { risk_hypotheses: riskHypotheses },
156+
};
157+
delete payload._filenames;
138158
const label = filenames.join(', ');
139159

140-
// Single-file batch: still pass file_content/file_path so backend can
141-
// build head_file_str. Multi-file batches drop these (head_file_str
142-
// becomes empty; agent gathers context via tools instead).
143-
if (filenames.length === 1 && fileReq.file_contents?.[filenames[0]]) {
144-
fileReq.file_content = fileReq.file_contents[filenames[0]];
145-
fileReq.file_path = filenames[0];
146-
}
147-
delete fileReq.file_contents;
148-
149160
try {
150161
onProgress(`Reviewing ${label}...`);
151-
const result = await runTurnLoop(fileReq, gitRoot, false);
162+
const result = await runTurnLoop(payload, gitRoot, false);
152163
onProgress(`Done reviewing ${label}`);
153164
return result;
154165
} catch (err) {
@@ -161,65 +172,59 @@ export async function runReviewHeadless(options = {}) {
161172
// Pair each file's suggestions with its own diff — skip files with no real suggestions
162173
const perFileWithSuggestions = perFileRequests.map((fileReq, i) => ({
163174
diff_content: fileReq.diff_content,
164-
suggestions: perFileResults[i].finalMessage?.content,
165-
output: perFileResults[i].finalOutput,
166-
})).filter(r => r.output?.code_suggestions?.length > 0);
175+
file_contents: fileReq.file_contents,
176+
generator_output: perFileResults[i].finalOutput,
177+
rejector_input: perFileResults[i].rejectorInput,
178+
})).filter(r => r.generator_output?.code_suggestions?.length > 0);
167179

168180
const filesWithSuggestions = new Set(
169181
perFileWithSuggestions.flatMap(r =>
170-
(r.output?.code_suggestions || []).map(s => (s.relevant_file || '').trim()).filter(Boolean)
182+
(r.generator_output?.code_suggestions || []).map(s => (s.relevant_file || '').trim()).filter(Boolean)
171183
)
172184
).size;
173-
onProgress(`${filesWithSuggestions} file${filesWithSuggestions !== 1 ? 's' : ''} have suggestions, running reflector...`);
185+
onProgress(`${filesWithSuggestions} file${filesWithSuggestions !== 1 ? 's' : ''} have suggestions, running rejector...`);
174186

175-
// ── Per-file reflector loops (parallel, fault-tolerant) ──────────────
176-
const reflectorResults = await Promise.all(
177-
perFileWithSuggestions.map(async ({ diff_content, suggestions }, i) => {
187+
// ── Per-batch rejector loops (parallel, fault-tolerant) ──────────────
188+
const rejectorResults = await Promise.all(
189+
perFileWithSuggestions.map(async ({ diff_content, file_contents, rejector_input }, i) => {
178190
try {
179191
return await runTurnLoop(
180192
{
181193
diff_content,
182-
prompt_template_name: 'reflector',
183-
extra_variables: { suggestion_str: suggestions },
194+
file_contents,
195+
prompt_template_name: 'rejector',
196+
extra_variables: rejector_input,
184197
},
185198
gitRoot,
186199
true
187200
);
188201
} catch (err) {
189-
console.error(`[error] Reflector failed for file ${i}: ${err.message}`);
202+
console.error(`[error] Rejector failed for batch ${i}: ${err.message}`);
190203
return { finalMessage: null, finalOutput: null };
191204
}
192205
})
193206
);
194207

195-
// ── Parse results, carrying generator labels through reflector ──────
196-
// Pragent's rejector schema drops `label`; preserve it by matching the
197-
// reflector's per-issue (file, start_line) back to the generator's output.
198-
const issues = reflectorResults.flatMap((r, i) => {
199-
const genSuggestions = perFileWithSuggestions[i]?.output?.code_suggestions || [];
200-
const norm = (v) => (typeof v === 'string' ? v.trim() : v);
201-
const labelFor = (issue) => {
202-
if (norm(issue.label)) return norm(issue.label);
203-
// Match by summary (pragent's primary strategy): rejector's
204-
// suggestion_summary (renamed to issue_content server-side) is
205-
// "Repeated from the input" — i.e. the generator's one_sentence_summary.
206-
const summary = norm(issue.issue_content);
207-
if (summary) {
208-
const exact = genSuggestions.find(g => norm(g.one_sentence_summary) === summary);
209-
if (norm(exact?.label)) return norm(exact.label);
208+
const finalizedResults = await Promise.all(
209+
rejectorResults.map((result, i) => fetchApi(
210+
'/extension/pr-review/agent/finalize',
211+
'POST',
212+
{
213+
diff_content: perFileWithSuggestions[i].diff_content,
214+
generator_output: perFileWithSuggestions[i].generator_output,
215+
rejector_output: result.finalOutput || { code_suggestions: [] },
210216
}
211-
// Fallback: first generator suggestion in the same file with a label.
212-
const file = norm(issue.relevant_file);
213-
const sameFile = genSuggestions.find(g => norm(g.relevant_file) === file && norm(g.label));
214-
return norm(sameFile?.label) || 'Code Quality';
215-
};
216-
return (r.finalOutput?.code_suggestions || []).map((issue) => ({
217-
issue_content: issue.issue_content || '',
217+
))
218+
);
219+
220+
const issues = finalizedResults.flatMap(r =>
221+
(r.output?.code_suggestions || []).map(issue => ({
222+
issue_content: issue.suggestion_content || issue.one_sentence_summary || '',
218223
relevant_file: issue.relevant_file || 'Unknown',
219-
start_line: issue.start_line || 0,
220-
label: labelFor(issue),
221-
}));
222-
});
224+
start_line: issue.relevant_lines_start || 0,
225+
label: issue.label || 'Code Quality',
226+
}))
227+
);
223228

224229
const labelCounts = {};
225230
for (const i of issues) { labelCounts[i.label] = (labelCounts[i.label] || 0) + 1; }

‎src/utils/reviewApiHelper.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ class ReviewApiHelper extends CommonApiHelper {
266266

267267
/**
268268
* Split a combined review request into batched payloads.
269-
* Files are grouped in chunks of up to FILES_PER_BATCH so each backend
270-
* session reviews multiple files at once (matches pragent's batching).
269+
* Files are grouped in chunks of up to FILES_PER_BATCH so each review
270+
* session can process multiple files together.
271271
*/
272272
static splitIntoPerFileRequests(requestBody, batchSize = 5) {
273273
if (!requestBody?.diff_content?.length) return [];
@@ -280,6 +280,7 @@ class ReviewApiHelper extends CommonApiHelper {
280280
if (!nameMatch) continue;
281281
perFile.push({ section, filename: nameMatch[1] });
282282
}
283+
perFile.sort((a, b) => (a.filename < b.filename ? -1 : a.filename > b.filename ? 1 : 0));
283284

284285
const batches = [];
285286
for (let i = 0; i < perFile.length; i += batchSize) {

0 commit comments

Comments
 (0)