Skip to content

Commit 282f255

Browse files
feat: add agent reviewer (--review) and bump to v0.4.4
- Add LLM-powered diff review step that runs after lint/build/test pass but before commit, catching security issues, logic errors, and pattern violations that automated checks miss - New src/loop/reviewer.ts module using existing tryCallLLM infrastructure (Anthropic/OpenAI/OpenRouter) with structured JSON findings output - Errors block commit and feed back via lastValidationFeedback; warnings are logged but non-blocking; gracefully skips if no diff or no API key - Wire --review CLI flag on run command and export public API types - Close 14 delivered issues (#212, #224, #225, #226, #227, #228, #229, #231, #232, #233, #237, #239, #240, #241) - Bump version to 0.4.4 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019ce2d4-0f5f-742e-9721-3181f57267df
1 parent 2b3385a commit 282f255

File tree

8 files changed

+302
-27
lines changed

8 files changed

+302
-27
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ralph-starter",
3-
"version": "0.4.3",
3+
"version": "0.4.4",
44
"description": "Ralph Wiggum made easy. One command to run autonomous AI coding loops with auto-commit, PRs, and Docker sandbox.",
55
"main": "dist/index.js",
66
"bin": {

src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ program
132132
'--no-visual-check',
133133
'Disable visual comparison validation (auto-enabled when Figma screenshots exist)'
134134
)
135+
.option('--review', 'Run LLM-powered diff review before commit (catches security/logic issues)')
135136
// Swarm mode options
136137
.option('--swarm', 'Run with multiple agents in parallel (swarm mode)')
137138
.option(

src/commands/run.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ export interface RunCommandOptions {
312312
strategy?: 'race' | 'consensus' | 'pipeline';
313313
// Amp options
314314
ampMode?: 'smart' | 'rush' | 'deep';
315+
// Agent reviewer
316+
review?: boolean;
315317
}
316318

317319
export async function runCommand(
@@ -1434,6 +1436,7 @@ Focus on one task at a time. After completing a task, update IMPLEMENTATION_PLAN
14341436
visualValidation,
14351437
figmaScreenshotPaths,
14361438
ampMode: options.ampMode,
1439+
review: options.review,
14371440
};
14381441

14391442
// Swarm mode: run with multiple agents in parallel

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export { CostTracker, resolveModelPricing } from './loop/cost-tracker.js';
4545
export type { IterationUpdate, LoopOptions, LoopResult } from './loop/executor.js';
4646
export { runLoop } from './loop/executor.js';
4747
export { appendProjectMemory, readProjectMemory } from './loop/memory.js';
48+
export type { ReviewFinding, ReviewResult, ReviewSeverity } from './loop/reviewer.js';
49+
export { runReview } from './loop/reviewer.js';
4850
export type { SwarmAgentResult, SwarmConfig, SwarmResult, SwarmStrategy } from './loop/swarm.js';
4951
export { runSwarm } from './loop/swarm.js';
5052
export { detectValidationCommands, runAllValidations, runValidation } from './loop/validation.js';

src/loop/executor.ts

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { estimateLoop, formatEstimateDetailed } from './estimator.js';
3838
import { appendProjectMemory, formatMemoryPrompt, readProjectMemory } from './memory.js';
3939
import { checkFileBasedCompletion, createProgressTracker, type ProgressEntry } from './progress.js';
4040
import { RateLimiter } from './rate-limiter.js';
41+
import { formatReviewAsValidation, formatReviewFeedback, runReview } from './reviewer.js';
4142
import { analyzeResponse, hasExitSignal } from './semantic-analyzer.js';
4243
import { detectClaudeSkills, formatSkillsForPrompt } from './skills.js';
4344
import { 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

274281
export 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',

src/loop/memory.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ const MAX_MEMORY_BYTES = 8 * 1024; // 8KB max — keeps context window usage rea
1515
* Read the project memory file.
1616
* Returns undefined if no memory exists yet.
1717
*/
18-
export function readProjectMemory(cwd: string): string | undefined {
18+
export function readProjectMemory(cwd: string, dotDir = '.ralph'): string | undefined {
1919
try {
20-
const memoryPath = join(cwd, '.ralph', MEMORY_FILE);
20+
const memoryPath = join(cwd, dotDir, MEMORY_FILE);
2121
if (!existsSync(memoryPath)) return undefined;
2222

2323
const content = readFileSync(memoryPath, 'utf-8').trim();
@@ -45,12 +45,12 @@ export function readProjectMemory(cwd: string): string | undefined {
4545
/**
4646
* Append an entry to the project memory file.
4747
*/
48-
export function appendProjectMemory(cwd: string, entry: string): void {
48+
export function appendProjectMemory(cwd: string, entry: string, dotDir = '.ralph'): void {
4949
try {
50-
const ralphDir = join(cwd, '.ralph');
51-
if (!existsSync(ralphDir)) mkdirSync(ralphDir, { recursive: true });
50+
const stateDir = join(cwd, dotDir);
51+
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });
5252

53-
const memoryPath = join(ralphDir, MEMORY_FILE);
53+
const memoryPath = join(stateDir, MEMORY_FILE);
5454
const timestamp = new Date().toISOString().split('T')[0];
5555
const formatted = `## ${timestamp}\n${entry.trim()}\n\n`;
5656

@@ -63,13 +63,13 @@ export function appendProjectMemory(cwd: string, entry: string): void {
6363
/**
6464
* Format memory content as a prompt section for injection into agent context.
6565
*/
66-
export function formatMemoryPrompt(memory: string): string {
66+
export function formatMemoryPrompt(memory: string, dotDir = '.ralph'): string {
6767
return `## Project Memory (from previous runs)
68-
The following notes were saved from previous ralph-starter runs on this project.
68+
The following notes were saved from previous runs on this project.
6969
Use them to understand project conventions and avoid repeating mistakes.
7070
7171
${memory}
7272
73-
If you discover new project conventions or important patterns, append them to \`.ralph/memory.md\`.
73+
If you discover new project conventions or important patterns, append them to \`${dotDir}/memory.md\`.
7474
`;
7575
}

src/loop/progress.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface ProgressTracker {
2121
clear(): Promise<void>;
2222
}
2323

24-
const ACTIVITY_FILE = '.ralph/activity.md';
24+
const DEFAULT_ACTIVITY_DIR = '.ralph';
2525

2626
/**
2727
* Format a progress entry as markdown
@@ -128,8 +128,12 @@ function getFileHeader(task: string): string {
128128
/**
129129
* Create a progress tracker for a directory
130130
*/
131-
export function createProgressTracker(cwd: string, task: string): ProgressTracker {
132-
const filePath = path.join(cwd, ACTIVITY_FILE);
131+
export function createProgressTracker(
132+
cwd: string,
133+
task: string,
134+
dotDir = DEFAULT_ACTIVITY_DIR
135+
): ProgressTracker {
136+
const filePath = path.join(cwd, dotDir, 'activity.md');
133137
const dirPath = path.dirname(filePath);
134138
let initialized = false;
135139

0 commit comments

Comments
 (0)