@@ -7,12 +7,13 @@ import { track } from './utils/analytics.js';
77const 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 ; }
0 commit comments