@@ -38,6 +38,7 @@ import { estimateLoop, formatEstimateDetailed } from './estimator.js';
3838import { appendProjectMemory , formatMemoryPrompt , readProjectMemory } from './memory.js' ;
3939import { checkFileBasedCompletion , createProgressTracker , type ProgressEntry } from './progress.js' ;
4040import { RateLimiter } from './rate-limiter.js' ;
41+ import { formatReviewAsValidation , formatReviewFeedback , runReview } from './reviewer.js' ;
4142import { analyzeResponse , hasExitSignal } from './semantic-analyzer.js' ;
4243import { detectClaudeSkills , formatSkillsForPrompt } from './skills.js' ;
4344import { detectStepFromOutput } from './step-detector.js' ;
@@ -269,6 +270,12 @@ export type LoopOptions = {
269270 env ?: Record < string , string > ;
270271 /** Amp agent mode: smart, rush, deep */
271272 ampMode ?: import ( './agents.js' ) . AmpMode ;
273+ /** Run LLM-powered diff review after validation passes (before commit) */
274+ review ?: boolean ;
275+ /** Product name shown in logs/UI (default: 'Ralph-Starter'). Set to white-label when embedding. */
276+ productName ?: string ;
277+ /** Dot-directory for memory/iteration-log/activity (default: '.ralph'). */
278+ dotDir ?: string ;
272279} ;
273280
274281export type LoopResult = {
@@ -401,13 +408,14 @@ function appendIterationLog(
401408 iteration : number ,
402409 summary : string ,
403410 validationPassed : boolean ,
404- hasChanges : boolean
411+ hasChanges : boolean ,
412+ dotDir = '.ralph'
405413) : void {
406414 try {
407- const ralphDir = join ( cwd , '.ralph' ) ;
408- if ( ! existsSync ( ralphDir ) ) mkdirSync ( ralphDir , { recursive : true } ) ;
415+ const stateDir = join ( cwd , dotDir ) ;
416+ if ( ! existsSync ( stateDir ) ) mkdirSync ( stateDir , { recursive : true } ) ;
409417
410- const logPath = join ( ralphDir , 'iteration-log.md' ) ;
418+ const logPath = join ( stateDir , 'iteration-log.md' ) ;
411419 const entry = `## Iteration ${ iteration }
412420- Status: ${ validationPassed ? 'validation passed' : 'validation failed' }
413421- Changes: ${ hasChanges ? 'yes' : 'no files changed' }
@@ -423,9 +431,13 @@ function appendIterationLog(
423431 * Read the last N iteration summaries from .ralph/iteration-log.md.
424432 * Used by context-builder to give the agent memory of previous iterations.
425433 */
426- export function readIterationLog ( cwd : string , maxEntries = 3 ) : string | undefined {
434+ export function readIterationLog (
435+ cwd : string ,
436+ maxEntries = 3 ,
437+ dotDir = '.ralph'
438+ ) : string | undefined {
427439 try {
428- const logPath = join ( cwd , '.ralph' , 'iteration-log.md' ) ;
440+ const logPath = join ( cwd , dotDir , 'iteration-log.md' ) ;
429441 if ( ! existsSync ( logPath ) ) return undefined ;
430442
431443 const content = readFileSync ( logPath , 'utf-8' ) ;
@@ -503,6 +515,9 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
503515 isSpinning : false ,
504516 }
505517 : ora ( ) ;
518+ const productName = options . productName || 'Ralph-Starter' ;
519+ const dotDir = options . dotDir || '.ralph' ;
520+
506521 let maxIterations = options . maxIterations || 50 ;
507522 const commits : string [ ] = [ ] ;
508523 const startTime = Date . now ( ) ;
@@ -523,7 +538,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
523538
524539 // Initialize progress tracker
525540 const progressTracker = options . trackProgress
526- ? createProgressTracker ( options . cwd , options . task )
541+ ? createProgressTracker ( options . cwd , options . task , dotDir )
527542 : null ;
528543
529544 // Initialize cost tracker
@@ -560,10 +575,10 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
560575 }
561576
562577 // Inject project memory from previous runs (if available)
563- const projectMemory = readProjectMemory ( options . cwd ) ;
578+ const projectMemory = readProjectMemory ( options . cwd , dotDir ) ;
564579 if ( projectMemory ) {
565- taskWithSkills = `${ taskWithSkills } \n\n${ formatMemoryPrompt ( projectMemory ) } ` ;
566- log ( chalk . dim ( ' Project memory loaded from .ralph /memory.md' ) ) ;
580+ taskWithSkills = `${ taskWithSkills } \n\n${ formatMemoryPrompt ( projectMemory , dotDir ) } ` ;
581+ log ( chalk . dim ( ` Project memory loaded from ${ dotDir } /memory.md` ) ) ;
567582 }
568583
569584 // Build abbreviated spec summary for context builder (iterations 2+)
@@ -585,7 +600,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
585600
586601 // Show startup summary box
587602 const startupLines : string [ ] = [ ] ;
588- startupLines . push ( chalk . cyan . bold ( ' Ralph-Starter' ) ) ;
603+ startupLines . push ( chalk . cyan . bold ( ` ${ productName } ` ) ) ;
589604 startupLines . push ( ` Agent: ${ chalk . white ( options . agent . name ) } ` ) ;
590605 startupLines . push ( ` Max loops: ${ chalk . white ( String ( maxIterations ) ) } ` ) ;
591606 if ( validationCommands . length > 0 ) {
@@ -871,7 +886,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
871886
872887 // Build iteration-specific task with smart context windowing
873888 // Read iteration log for inter-iteration memory (iterations 2+)
874- const iterationLog = i > 1 ? readIterationLog ( options . cwd ) : undefined ;
889+ const iterationLog = i > 1 ? readIterationLog ( options . cwd , 3 , dotDir ) : undefined ;
875890
876891 const builtContext = buildIterationContext ( {
877892 fullTask : options . task ,
@@ -1496,6 +1511,56 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
14961511 }
14971512 }
14981513
1514+ // --- Agent reviewer: LLM-powered diff review before commit ---
1515+ if ( options . review && hasChanges ) {
1516+ spinner . start ( chalk . yellow ( `Loop ${ i } : Running agent review...` ) ) ;
1517+ try {
1518+ const reviewResult = await runReview ( options . cwd ) ;
1519+ if ( reviewResult && ! reviewResult . passed ) {
1520+ const reviewValidation = formatReviewAsValidation ( reviewResult ) ;
1521+ validationResults . push ( reviewValidation ) ;
1522+ const feedback = formatReviewFeedback ( reviewResult ) ;
1523+ spinner . fail (
1524+ chalk . red (
1525+ `Loop ${ i } : Agent review found ${ reviewResult . findings . filter ( ( f ) => f . severity === 'error' ) . length } error(s)`
1526+ )
1527+ ) ;
1528+ for ( const f of reviewResult . findings ) {
1529+ const icon = f . severity === 'error' ? '❌' : f . severity === 'warning' ? '⚠️' : 'ℹ️' ;
1530+ log ( chalk . dim ( ` ${ icon } ${ f . message } ` ) ) ;
1531+ }
1532+
1533+ const tripped = circuitBreaker . recordFailure ( 'agent-review' ) ;
1534+ if ( tripped ) {
1535+ finalIteration = i ;
1536+ exitReason = 'circuit_breaker' ;
1537+ break ;
1538+ }
1539+
1540+ lastValidationFeedback = feedback ;
1541+ continue ;
1542+ }
1543+ if ( reviewResult ) {
1544+ const warnFindings = reviewResult . findings . filter ( ( f ) => f . severity === 'warning' ) ;
1545+ const suffix = warnFindings . length > 0 ? ` (${ warnFindings . length } warning(s))` : '' ;
1546+ spinner . succeed ( chalk . green ( `Loop ${ i } : Agent review passed${ suffix } ` ) ) ;
1547+ for ( const f of warnFindings ) {
1548+ log ( chalk . dim ( ` ⚠️ ${ f . message } ` ) ) ;
1549+ }
1550+ circuitBreaker . recordSuccess ( ) ;
1551+ lastValidationFeedback = '' ;
1552+ } else {
1553+ spinner . info ( chalk . dim ( `Loop ${ i } : Agent review skipped (no diff or no LLM key)` ) ) ;
1554+ }
1555+ } catch ( err ) {
1556+ spinner . warn (
1557+ chalk . yellow (
1558+ `Loop ${ i } : Agent review skipped (${ err instanceof Error ? err . message : 'unknown error' } )`
1559+ )
1560+ ) ;
1561+ }
1562+ }
1563+
14991564 // Auto-commit if enabled and there are changes
15001565 let committed = false ;
15011566 let commitMsg = '' ;
@@ -1547,7 +1612,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
15471612 // Write iteration summary for inter-iteration memory
15481613 const iterSummary = summarizeChanges ( result . output ) ;
15491614 const iterValidationPassed = validationResults . every ( ( r ) => r . success ) ;
1550- appendIterationLog ( options . cwd , i , iterSummary , iterValidationPassed , hasChanges ) ;
1615+ appendIterationLog ( options . cwd , i , iterSummary , iterValidationPassed , hasChanges , dotDir ) ;
15511616
15521617 if ( status === 'done' ) {
15531618 const completionReason = completionResult . reason || 'Task marked as complete by agent' ;
@@ -1686,7 +1751,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
16861751 if ( costTracker ) {
16871752 memorySummary . push ( `Cost: ${ formatCost ( costTracker . getStats ( ) . totalCost . totalCost ) } ` ) ;
16881753 }
1689- appendProjectMemory ( options . cwd , memorySummary . join ( '\n' ) ) ;
1754+ appendProjectMemory ( options . cwd , memorySummary . join ( '\n' ) , dotDir ) ;
16901755
16911756 return {
16921757 success : exitReason === 'completed' || exitReason === 'file_signal' ,
0 commit comments