Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
439 changes: 439 additions & 0 deletions .agents/skills/humanizer/SKILL.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .claude/skills/humanizer
1 change: 1 addition & 0 deletions .cline/skills/humanizer
1 change: 1 addition & 0 deletions .cursor/skills/humanizer
1 change: 1 addition & 0 deletions .windsurf/skills/humanizer
Binary file added docs/static/img/ralph/banana.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ program
'--no-visual-check',
'Disable visual comparison validation (auto-enabled when Figma screenshots exist)'
)
.option('--review', 'Run LLM-powered diff review before commit (catches security/logic issues)')
// Swarm mode options
.option('--swarm', 'Run with multiple agents in parallel (swarm mode)')
.option(
Expand Down
3 changes: 3 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ export interface RunCommandOptions {
strategy?: 'race' | 'consensus' | 'pipeline';
// Amp options
ampMode?: 'smart' | 'rush' | 'deep';
// Agent reviewer
review?: boolean;
}

export async function runCommand(
Expand Down Expand Up @@ -1490,6 +1492,7 @@ Focus on one task at a time. After completing a task, update IMPLEMENTATION_PLAN
visualValidation,
figmaScreenshotPaths,
ampMode: options.ampMode,
review: options.review,
headless,
enableSkills: options.autoSkills !== false,
};
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export { CostTracker, resolveModelPricing } from './loop/cost-tracker.js';
export type { IterationUpdate, LoopOptions, LoopResult } from './loop/executor.js';
export { runLoop } from './loop/executor.js';
export { appendProjectMemory, readProjectMemory } from './loop/memory.js';
export type { ReviewFinding, ReviewResult, ReviewSeverity } from './loop/reviewer.js';
export { runReview } from './loop/reviewer.js';
export type { SwarmAgentResult, SwarmConfig, SwarmResult, SwarmStrategy } from './loop/swarm.js';
export { runSwarm } from './loop/swarm.js';
export { detectValidationCommands, runAllValidations, runValidation } from './loop/validation.js';
Expand Down
119 changes: 105 additions & 14 deletions src/loop/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { estimateLoop, formatEstimateDetailed } from './estimator.js';
import { appendProjectMemory, formatMemoryPrompt, readProjectMemory } from './memory.js';
import { checkFileBasedCompletion, createProgressTracker, type ProgressEntry } from './progress.js';
import { RateLimiter } from './rate-limiter.js';
import { formatReviewAsValidation, formatReviewFeedback, runReview } from './reviewer.js';
import { analyzeResponse, hasExitSignal } from './semantic-analyzer.js';
import { detectClaudeSkills, formatSkillsForPrompt } from './skills.js';
import { detectStepFromOutput } from './step-detector.js';
Expand Down Expand Up @@ -270,6 +271,12 @@ export type LoopOptions = {
env?: Record<string, string>;
/** Amp agent mode: smart, rush, deep */
ampMode?: import('./agents.js').AmpMode;
/** Run LLM-powered diff review after validation passes (before commit) */
review?: boolean;
/** Product name shown in logs/UI (default: 'Ralph-Starter'). Set to white-label when embedding. */
productName?: string;
/** Dot-directory for memory/iteration-log/activity (default: '.ralph'). */
dotDir?: string;
/** API key for SDK-based agents */
apiKey?: string;
/** Allow the anthropic-sdk agent to execute shell commands. Disabled by default for safety. */
Expand Down Expand Up @@ -406,13 +413,14 @@ function appendIterationLog(
iteration: number,
summary: string,
validationPassed: boolean,
hasChanges: boolean
hasChanges: boolean,
dotDir = '.ralph'
): void {
try {
const ralphDir = join(cwd, '.ralph');
if (!existsSync(ralphDir)) mkdirSync(ralphDir, { recursive: true });
const stateDir = join(cwd, dotDir);
if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true });

const logPath = join(ralphDir, 'iteration-log.md');
const logPath = join(stateDir, 'iteration-log.md');
const entry = `## Iteration ${iteration}
- Status: ${validationPassed ? 'validation passed' : 'validation failed'}
- Changes: ${hasChanges ? 'yes' : 'no files changed'}
Expand All @@ -428,9 +436,13 @@ function appendIterationLog(
* Read the last N iteration summaries from .ralph/iteration-log.md.
* Used by context-builder to give the agent memory of previous iterations.
*/
export function readIterationLog(cwd: string, maxEntries = 3): string | undefined {
export function readIterationLog(
cwd: string,
maxEntries = 3,
dotDir = '.ralph'
): string | undefined {
try {
const logPath = join(cwd, '.ralph', 'iteration-log.md');
const logPath = join(cwd, dotDir, 'iteration-log.md');
if (!existsSync(logPath)) return undefined;

const content = readFileSync(logPath, 'utf-8');
Expand Down Expand Up @@ -508,6 +520,9 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
isSpinning: false,
}
: ora();
const productName = options.productName || 'Ralph-Starter';
const dotDir = options.dotDir || '.ralph';

let maxIterations = options.maxIterations || 50;
const commits: string[] = [];
const startTime = Date.now();
Expand All @@ -528,7 +543,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {

// Initialize progress tracker
const progressTracker = options.trackProgress
? createProgressTracker(options.cwd, options.task)
? createProgressTracker(options.cwd, options.task, dotDir)
: null;

// Initialize cost tracker
Expand Down Expand Up @@ -565,10 +580,10 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
}

// Inject project memory from previous runs (if available)
const projectMemory = readProjectMemory(options.cwd);
const projectMemory = readProjectMemory(options.cwd, dotDir);
if (projectMemory) {
taskWithSkills = `${taskWithSkills}\n\n${formatMemoryPrompt(projectMemory)}`;
log(chalk.dim(' Project memory loaded from .ralph/memory.md'));
taskWithSkills = `${taskWithSkills}\n\n${formatMemoryPrompt(projectMemory, dotDir)}`;
log(chalk.dim(` Project memory loaded from ${dotDir}/memory.md`));
}

// Build abbreviated spec summary for context builder (iterations 2+)
Expand All @@ -590,7 +605,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {

// Show startup summary box
const startupLines: string[] = [];
startupLines.push(chalk.cyan.bold(' Ralph-Starter'));
startupLines.push(chalk.cyan.bold(` ${productName}`));
startupLines.push(` Agent: ${chalk.white(options.agent.name)}`);
startupLines.push(` Max loops: ${chalk.white(String(maxIterations))}`);
if (validationCommands.length > 0) {
Expand Down Expand Up @@ -876,7 +891,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {

// Build iteration-specific task with smart context windowing
// Read iteration log for inter-iteration memory (iterations 2+)
const iterationLog = i > 1 ? readIterationLog(options.cwd) : undefined;
const iterationLog = i > 1 ? readIterationLog(options.cwd, 3, dotDir) : undefined;

const builtContext = buildIterationContext({
fullTask: options.task,
Expand Down Expand Up @@ -1503,6 +1518,82 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
}
}

// --- Agent reviewer: LLM-powered diff review before commit ---
if (options.review && hasChanges && i > 1 && pastWarmup) {
spinner.start(chalk.yellow(`Loop ${i}: Running agent review...`));
try {
const reviewResult = await runReview(options.cwd);
if (reviewResult && !reviewResult.passed) {
const reviewValidation = formatReviewAsValidation(reviewResult);
validationResults.push(reviewValidation);
const feedback = formatReviewFeedback(reviewResult);
spinner.fail(
chalk.red(
`Loop ${i}: Agent review found ${reviewResult.findings.filter((f) => f.severity === 'error').length} error(s)`
)
);
for (const f of reviewResult.findings) {
const icon = f.severity === 'error' ? '❌' : f.severity === 'warning' ? '⚠️' : 'ℹ️';
const location = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ''})` : '';
log(chalk.dim(` ${icon}${location} ${f.message}`));
}

validationFailures++;
const reviewErrorMsg = reviewResult.findings
.map((f) => `[${f.severity}] ${f.file ?? ''} ${f.message}`)
.join('\n');
const tripped = circuitBreaker.recordFailure(reviewErrorMsg);
if (tripped) {
if (progressTracker && progressEntry) {
progressEntry.status = 'failed';
progressEntry.summary = `Circuit breaker tripped (agent-review)`;
progressEntry.validationResults = validationResults;
progressEntry.duration = Date.now() - iterationStart;
await progressTracker.appendEntry(progressEntry);
}
finalIteration = i;
exitReason = 'circuit_breaker';
break;
}

lastValidationFeedback = feedback;

if (progressTracker && progressEntry) {
progressEntry.status = 'validation_failed';
progressEntry.summary = 'Agent review failed';
progressEntry.validationResults = validationResults;
progressEntry.duration = Date.now() - iterationStart;
await progressTracker.appendEntry(progressEntry);
}

continue;
}
if (reviewResult) {
const warnFindings = reviewResult.findings.filter((f) => f.severity === 'warning');
const infoFindings = reviewResult.findings.filter((f) => f.severity === 'info');
const parts: string[] = [];
if (warnFindings.length > 0) parts.push(`${warnFindings.length} warning(s)`);
if (infoFindings.length > 0) parts.push(`${infoFindings.length} info`);
const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : '';
spinner.succeed(chalk.green(`Loop ${i}: Agent review passed${suffix}`));
for (const f of [...warnFindings, ...infoFindings]) {
const icon = f.severity === 'warning' ? '⚠️' : 'ℹ️';
log(chalk.dim(` ${icon} ${f.message}`));
}
circuitBreaker.recordSuccess();
lastValidationFeedback = '';
} else {
spinner.info(chalk.dim(`Loop ${i}: Agent review skipped (no diff or no LLM key)`));
}
} catch (err) {
spinner.warn(
chalk.yellow(
`Loop ${i}: Agent review skipped (${err instanceof Error ? err.message : 'unknown error'})`
)
);
}
}

// Auto-commit if enabled and there are changes
let committed = false;
let commitMsg = '';
Expand Down Expand Up @@ -1554,7 +1645,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
// Write iteration summary for inter-iteration memory
const iterSummary = summarizeChanges(result.output);
const iterValidationPassed = validationResults.every((r) => r.success);
appendIterationLog(options.cwd, i, iterSummary, iterValidationPassed, hasChanges);
appendIterationLog(options.cwd, i, iterSummary, iterValidationPassed, hasChanges, dotDir);

if (status === 'done') {
const completionReason = completionResult.reason || 'Task marked as complete by agent';
Expand Down Expand Up @@ -1693,7 +1784,7 @@ export async function runLoop(options: LoopOptions): Promise<LoopResult> {
if (costTracker) {
memorySummary.push(`Cost: ${formatCost(costTracker.getStats().totalCost.totalCost)}`);
}
appendProjectMemory(options.cwd, memorySummary.join('\n'));
appendProjectMemory(options.cwd, memorySummary.join('\n'), dotDir);

return {
success: exitReason === 'completed' || exitReason === 'file_signal',
Expand Down
18 changes: 9 additions & 9 deletions src/loop/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ const MAX_MEMORY_BYTES = 8 * 1024; // 8KB max — keeps context window usage rea
* Read the project memory file.
* Returns undefined if no memory exists yet.
*/
export function readProjectMemory(cwd: string): string | undefined {
export function readProjectMemory(cwd: string, dotDir = '.ralph'): string | undefined {
try {
const memoryPath = join(cwd, '.ralph', MEMORY_FILE);
const memoryPath = join(cwd, dotDir, MEMORY_FILE);
if (!existsSync(memoryPath)) return undefined;

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

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

Expand All @@ -63,13 +63,13 @@ export function appendProjectMemory(cwd: string, entry: string): void {
/**
* Format memory content as a prompt section for injection into agent context.
*/
export function formatMemoryPrompt(memory: string): string {
export function formatMemoryPrompt(memory: string, dotDir = '.ralph'): string {
return `## Project Memory (from previous runs)
The following notes were saved from previous ralph-starter runs on this project.
The following notes were saved from previous runs on this project.
Use them to understand project conventions and avoid repeating mistakes.

${memory}

If you discover new project conventions or important patterns, append them to \`.ralph/memory.md\`.
If you discover new project conventions or important patterns, append them to \`${dotDir}/memory.md\`.
`;
}
10 changes: 7 additions & 3 deletions src/loop/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ProgressTracker {
clear(): Promise<void>;
}

const ACTIVITY_FILE = '.ralph/activity.md';
const DEFAULT_ACTIVITY_DIR = '.ralph';

/**
* Format a progress entry as markdown
Expand Down Expand Up @@ -128,8 +128,12 @@ function getFileHeader(task: string): string {
/**
* Create a progress tracker for a directory
*/
export function createProgressTracker(cwd: string, task: string): ProgressTracker {
const filePath = path.join(cwd, ACTIVITY_FILE);
export function createProgressTracker(
cwd: string,
task: string,
dotDir = DEFAULT_ACTIVITY_DIR
): ProgressTracker {
const filePath = path.join(cwd, dotDir, 'activity.md');
const dirPath = path.dirname(filePath);
let initialized = false;

Expand Down
Loading
Loading