@@ -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,70 @@ 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 && i > 1 ) {
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+
1542+ if ( progressTracker && progressEntry ) {
1543+ progressEntry . status = 'validation_failed' ;
1544+ progressEntry . summary = 'Agent review failed' ;
1545+ progressEntry . validationResults = validationResults ;
1546+ progressEntry . duration = Date . now ( ) - iterationStart ;
1547+ await progressTracker . appendEntry ( progressEntry ) ;
1548+ }
1549+
1550+ continue ;
1551+ }
1552+ if ( reviewResult ) {
1553+ const warnFindings = reviewResult . findings . filter ( ( f ) => f . severity === 'warning' ) ;
1554+ const infoFindings = reviewResult . findings . filter ( ( f ) => f . severity === 'info' ) ;
1555+ const suffix =
1556+ warnFindings . length > 0 || infoFindings . length > 0
1557+ ? ` (${ warnFindings . length } warning(s), ${ infoFindings . length } info)`
1558+ : '' ;
1559+ spinner . succeed ( chalk . green ( `Loop ${ i } : Agent review passed${ suffix } ` ) ) ;
1560+ for ( const f of [ ...warnFindings , ...infoFindings ] ) {
1561+ const icon = f . severity === 'warning' ? '⚠️' : 'ℹ️' ;
1562+ log ( chalk . dim ( ` ${ icon } ${ f . message } ` ) ) ;
1563+ }
1564+ circuitBreaker . recordSuccess ( ) ;
1565+ lastValidationFeedback = '' ;
1566+ } else {
1567+ spinner . info ( chalk . dim ( `Loop ${ i } : Agent review skipped (no diff or no LLM key)` ) ) ;
1568+ }
1569+ } catch ( err ) {
1570+ spinner . warn (
1571+ chalk . yellow (
1572+ `Loop ${ i } : Agent review skipped (${ err instanceof Error ? err . message : 'unknown error' } )`
1573+ )
1574+ ) ;
1575+ }
1576+ }
1577+
14991578 // Auto-commit if enabled and there are changes
15001579 let committed = false ;
15011580 let commitMsg = '' ;
@@ -1547,7 +1626,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
15471626 // Write iteration summary for inter-iteration memory
15481627 const iterSummary = summarizeChanges ( result . output ) ;
15491628 const iterValidationPassed = validationResults . every ( ( r ) => r . success ) ;
1550- appendIterationLog ( options . cwd , i , iterSummary , iterValidationPassed , hasChanges ) ;
1629+ appendIterationLog ( options . cwd , i , iterSummary , iterValidationPassed , hasChanges , dotDir ) ;
15511630
15521631 if ( status === 'done' ) {
15531632 const completionReason = completionResult . reason || 'Task marked as complete by agent' ;
@@ -1686,7 +1765,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
16861765 if ( costTracker ) {
16871766 memorySummary . push ( `Cost: ${ formatCost ( costTracker . getStats ( ) . totalCost . totalCost ) } ` ) ;
16881767 }
1689- appendProjectMemory ( options . cwd , memorySummary . join ( '\n' ) ) ;
1768+ appendProjectMemory ( options . cwd , memorySummary . join ( '\n' ) , dotDir ) ;
16901769
16911770 return {
16921771 success : exitReason === 'completed' || exitReason === 'file_signal' ,
0 commit comments