aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
1,067 lines (926 loc) • 34 kB
JavaScript
/**
* Autonomous Build Loop - Story 8.1
*
* Executes builds autonomously in a loop: load spec → create plan → execute subtasks → verify → retry/complete.
* Inspired by Auto-Claude's Coder Agent but with AIOS extensibility.
*
* Features:
* - AC1: Located in `.aios-core/core/execution/`
* - AC2: Loop: load spec → create plan → execute subtasks → verify → retry/complete
* - AC3: Maximum 10 iterations per subtask (configurable)
* - AC4: Global timeout of 30 minutes per story (configurable)
* - AC5: Command `*build-autonomous {story-id}` in @dev
* - AC6: State persisted in `plan/build-state.json` for recovery
* - AC7: Events emitted: build_started, subtask_completed, build_failed, build_success
* - AC8: Integration with Epic 5 (Recovery System) for resume
*
* @module autonomous-build-loop
* @version 1.0.0
*/
const fs = require('fs');
const path = require('path');
const { EventEmitter } = require('events');
// Import Epic 8.4 Build State Manager
const { BuildStateManager } = require('./build-state-manager');
// Import Epic 5 Recovery System
let RecoveryTracker;
try {
RecoveryTracker = require('../../infrastructure/scripts/recovery-tracker').RecoveryTracker;
} catch {
RecoveryTracker = null;
}
// Import Epic 8.2 Worktree Manager (Story 8.2 integration - AC8)
let WorktreeManager;
try {
WorktreeManager = require('../../infrastructure/scripts/worktree-manager');
} catch {
WorktreeManager = null;
}
// Optional chalk for CLI output
let chalk;
try {
chalk = require('chalk');
} catch {
chalk = {
blue: (s) => s,
green: (s) => s,
red: (s) => s,
yellow: (s) => s,
cyan: (s) => s,
gray: (s) => s,
bold: (s) => s,
dim: (s) => s,
bgRed: (s) => s,
bgGreen: (s) => s,
};
}
// ═══════════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════════
const DEFAULT_CONFIG = {
maxIterations: 10, // AC3: Max attempts per subtask
globalTimeout: 30 * 60 * 1000, // AC4: 30 minutes global timeout
subtaskTimeout: 5 * 60 * 1000, // 5 minutes per subtask
selfCritiqueEnabled: true, // Enable self-critique steps (5.5, 6.5)
verificationEnabled: true, // Run verification after each subtask
autoCommit: true, // Auto-commit after successful subtask
pauseOnFailure: false, // Pause build on failure (vs continue)
verbose: false, // Verbose logging
// Worktree isolation (Story 8.2 - AC8)
useWorktree: false, // Execute in isolated worktree
worktreeCleanup: true, // Auto-cleanup worktree after success
};
const BuildEvent = {
BUILD_STARTED: 'build_started', // AC7
SUBTASK_STARTED: 'subtask_started',
SUBTASK_COMPLETED: 'subtask_completed', // AC7
SUBTASK_FAILED: 'subtask_failed',
ITERATION_STARTED: 'iteration_started',
ITERATION_COMPLETED: 'iteration_completed',
SELF_CRITIQUE: 'self_critique',
VERIFICATION_STARTED: 'verification_started',
VERIFICATION_COMPLETED: 'verification_completed',
BUILD_FAILED: 'build_failed', // AC7
BUILD_SUCCESS: 'build_success', // AC7
BUILD_TIMEOUT: 'build_timeout',
BUILD_PAUSED: 'build_paused',
};
const SubtaskResult = {
SUCCESS: 'success',
FAILED: 'failed',
TIMEOUT: 'timeout',
SKIPPED: 'skipped',
};
// ═══════════════════════════════════════════════════════════════════════════════════
// AUTONOMOUS BUILD LOOP CLASS
// ═══════════════════════════════════════════════════════════════════════════════════
/**
* AutonomousBuildLoop - Executes builds autonomously
*
* Integrates with:
* - BuildStateManager (Epic 8.4) for state persistence and recovery
* - RecoveryTracker (Epic 5) for attempt tracking
* - SubtaskExecutor (Epic 4) for subtask execution
*/
class AutonomousBuildLoop extends EventEmitter {
/**
* Create a new AutonomousBuildLoop
*
* @param {Object} config - Configuration options
*/
constructor(config = {}) {
super();
this.config = { ...DEFAULT_CONFIG, ...config };
this.stateManager = null;
this.recoveryTracker = null;
this.worktreeManager = null; // Story 8.2 integration
this.worktreePath = null; // Story 8.2 integration
this.startTime = null;
this.isRunning = false;
this.isPaused = false;
this.currentSubtask = null;
// Statistics
this.stats = {
totalSubtasks: 0,
completedSubtasks: 0,
failedSubtasks: 0,
totalIterations: 0,
successfulIterations: 0,
failedIterations: 0,
};
}
// ─────────────────────────────────────────────────────────────────────────────────
// MAIN LOOP (AC2)
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Run the autonomous build loop (AC2)
*
* Loop: load spec → create plan → execute subtasks → verify → retry/complete
*
* @param {string} storyId - Story identifier
* @param {Object} options - Run options
* @returns {Promise<Object>} Build result
*/
async run(storyId, options = {}) {
if (this.isRunning) {
throw new Error('Build loop is already running');
}
this.isRunning = true;
this.isPaused = false;
this.startTime = Date.now();
// Initialize state manager (AC6)
this.stateManager = new BuildStateManager(storyId, {
planDir: options.planDir || path.join(process.cwd(), 'plan'),
rootPath: options.rootPath || process.cwd(),
config: this.config,
});
// Initialize recovery tracker (AC8)
if (RecoveryTracker) {
this.recoveryTracker = new RecoveryTracker({
storyId,
rootPath: options.rootPath || process.cwd(),
});
}
// Initialize worktree manager (Story 8.2 - AC8)
if (this.config.useWorktree && WorktreeManager) {
this.worktreeManager = new WorktreeManager(options.rootPath || process.cwd());
try {
const worktreeInfo = await this.worktreeManager.create(storyId);
this.worktreePath = worktreeInfo.path;
this.log(`Created worktree at ${this.worktreePath}`, 'success');
} catch (error) {
// Worktree might already exist
const existing = await this.worktreeManager.get(storyId);
if (existing) {
this.worktreePath = existing.path;
this.log(`Using existing worktree at ${this.worktreePath}`, 'info');
} else {
this.log(
`Worktree creation failed: ${error.message}, continuing without isolation`,
'warn',
);
}
}
}
try {
// Load or create state
const state = this.stateManager.loadOrCreateState({
totalSubtasks: options.totalSubtasks || 0,
worktree: options.worktree,
plan: options.plan,
});
// Emit build started (AC7)
this.emit(BuildEvent.BUILD_STARTED, {
storyId,
startTime: this.startTime,
config: this.config,
resuming: state.checkpoints.length > 0,
});
// Load implementation plan
const plan = await this.loadPlan(storyId, options);
if (!plan) {
throw new Error(`No implementation plan found for ${storyId}`);
}
this.stats.totalSubtasks = this.countSubtasks(plan);
this.stateManager._state.metrics.totalSubtasks = this.stats.totalSubtasks;
this.stateManager.saveState();
// Main execution loop
const result = await this.executeLoop(plan, state);
// Finalize
if (result.success) {
this.stateManager.completeBuild();
// Cleanup worktree on success (Story 8.2)
if (this.worktreeManager && this.worktreePath && this.config.worktreeCleanup) {
try {
await this.worktreeManager.remove(storyId);
this.log(`Cleaned up worktree for ${storyId}`, 'success');
} catch (err) {
this.log(`Worktree cleanup failed: ${err.message}`, 'warn');
}
}
this.emit(BuildEvent.BUILD_SUCCESS, {
storyId,
duration: Date.now() - this.startTime,
stats: this.stats,
worktreePath: this.worktreePath,
});
} else {
this.stateManager.failBuild(result.error || 'Build failed');
this.emit(BuildEvent.BUILD_FAILED, {
storyId,
error: result.error,
duration: Date.now() - this.startTime,
stats: this.stats,
});
}
return this.generateReport(storyId, result);
} catch (error) {
this.emit(BuildEvent.BUILD_FAILED, {
storyId,
error: error.message,
duration: Date.now() - this.startTime,
});
if (this.stateManager) {
this.stateManager.failBuild(error.message);
}
throw error;
} finally {
this.isRunning = false;
this.currentSubtask = null;
}
}
/**
* Execute the main loop over all subtasks
* @private
*/
async executeLoop(plan, state) {
const completedSubtasks = new Set(state.completedSubtasks || []);
const results = [];
for (const phase of plan.phases || []) {
for (const subtask of phase.subtasks || []) {
// Check global timeout (AC4)
if (this.isTimedOut()) {
this.emit(BuildEvent.BUILD_TIMEOUT, {
elapsed: Date.now() - this.startTime,
timeout: this.config.globalTimeout,
});
return {
success: false,
error: 'Global timeout exceeded',
results,
};
}
// Check if paused
if (this.isPaused) {
this.emit(BuildEvent.BUILD_PAUSED, {
currentSubtask: subtask.id,
});
return {
success: false,
error: 'Build paused',
paused: true,
results,
};
}
// Skip completed subtasks
if (completedSubtasks.has(subtask.id)) {
this.log(`Skipping completed subtask: ${subtask.id}`);
continue;
}
// Execute subtask with retry loop
const subtaskResult = await this.executeSubtaskWithRetry(subtask, phase);
results.push(subtaskResult);
if (subtaskResult.status === SubtaskResult.SUCCESS) {
completedSubtasks.add(subtask.id);
this.stats.completedSubtasks++;
} else if (subtaskResult.status === SubtaskResult.FAILED) {
this.stats.failedSubtasks++;
if (this.config.pauseOnFailure) {
return {
success: false,
error: `Subtask ${subtask.id} failed after max iterations`,
failedSubtask: subtask.id,
results,
};
}
}
}
}
// Check if all subtasks completed
const allCompleted = this.stats.completedSubtasks >= this.stats.totalSubtasks;
return {
success: allCompleted,
error: allCompleted ? null : 'Not all subtasks completed',
results,
};
}
/**
* Execute a single subtask with retry loop (AC3)
* @private
*/
async executeSubtaskWithRetry(subtask, phase) {
this.currentSubtask = subtask.id;
this.emit(BuildEvent.SUBTASK_STARTED, {
subtaskId: subtask.id,
phase: phase.id,
description: subtask.description,
});
// Update state
this.stateManager.startSubtask(subtask.id, { phase: phase.id });
let lastError = null;
let attempts = 0;
// Retry loop (AC3)
for (let iteration = 1; iteration <= this.config.maxIterations; iteration++) {
attempts++;
this.stats.totalIterations++;
// Check timeout
if (this.isTimedOut()) {
return {
subtaskId: subtask.id,
status: SubtaskResult.TIMEOUT,
attempts,
error: 'Global timeout',
};
}
this.emit(BuildEvent.ITERATION_STARTED, {
subtaskId: subtask.id,
iteration,
maxIterations: this.config.maxIterations,
});
// Track attempt (AC8)
if (this.recoveryTracker) {
this.recoveryTracker.startAttempt(subtask.id, {
approach: `Iteration ${iteration}: ${subtask.description}`,
changes: subtask.files || [],
});
}
try {
// Execute the subtask
const startTime = Date.now();
const result = await this.executeSubtask(subtask, iteration);
const duration = Date.now() - startTime;
if (result.success) {
this.stats.successfulIterations++;
// Complete recovery tracking
if (this.recoveryTracker) {
this.recoveryTracker.completeAttempt(subtask.id, {
success: true,
notes: `Completed in iteration ${iteration}`,
});
}
// Save checkpoint
this.stateManager.completeSubtask(subtask.id, {
duration,
filesModified: result.filesModified || subtask.files || [],
attempts: iteration,
});
this.emit(BuildEvent.SUBTASK_COMPLETED, {
subtaskId: subtask.id,
iteration,
duration,
filesModified: result.filesModified,
});
return {
subtaskId: subtask.id,
status: SubtaskResult.SUCCESS,
attempts: iteration,
duration,
result,
};
}
// Failed iteration
lastError = result.error || 'Unknown error';
this.stats.failedIterations++;
// Record failure
this.stateManager.recordFailure(subtask.id, {
attempt: iteration,
error: lastError,
duration,
});
if (this.recoveryTracker) {
this.recoveryTracker.completeAttempt(subtask.id, {
success: false,
error: lastError,
});
}
this.emit(BuildEvent.ITERATION_COMPLETED, {
subtaskId: subtask.id,
iteration,
success: false,
error: lastError,
});
// Self-critique before retry
if (this.config.selfCritiqueEnabled && iteration < this.config.maxIterations) {
await this.performSelfCritique(subtask, result, iteration);
}
} catch (error) {
lastError = error.message;
this.stats.failedIterations++;
this.stateManager.recordFailure(subtask.id, {
attempt: iteration,
error: lastError,
});
if (this.recoveryTracker) {
this.recoveryTracker.completeAttempt(subtask.id, {
success: false,
error: lastError,
});
}
this.log(`Iteration ${iteration} error: ${lastError}`, 'error');
}
}
// Max iterations reached
this.emit(BuildEvent.SUBTASK_FAILED, {
subtaskId: subtask.id,
attempts,
error: lastError,
});
return {
subtaskId: subtask.id,
status: SubtaskResult.FAILED,
attempts,
error: lastError,
};
}
/**
* Execute a single subtask iteration
* @private
*/
async executeSubtask(subtask, iteration) {
// This is where the actual subtask execution happens
// In a full implementation, this would:
// 1. Load context (project-context, files-context)
// 2. Understand the subtask requirements
// 3. Write code
// 4. Run self-critique (Step 5.5)
// 5. Run tests
// 6. Run self-critique (Step 6.5)
// 7. Fix issues
// 8. Run linter
// 9. Fix lint issues
// 10. Verify
// For now, we provide a hook for external execution
if (this.config.executor) {
return await this.config.executor(subtask, {
iteration,
storyId: this.stateManager.storyId,
config: this.config,
});
}
// Default: simulate execution (for testing)
this.log(`Executing subtask ${subtask.id} (iteration ${iteration})`, 'info');
// Check for verification command
if (subtask.verification && this.config.verificationEnabled) {
const verifyResult = await this.runVerification(subtask);
if (!verifyResult.success) {
return {
success: false,
error: verifyResult.error || 'Verification failed',
};
}
}
return {
success: true,
filesModified: subtask.files || [],
};
}
/**
* Run verification for a subtask
* @private
*/
async runVerification(subtask) {
this.emit(BuildEvent.VERIFICATION_STARTED, {
subtaskId: subtask.id,
verification: subtask.verification,
});
try {
// Different verification types
const verification = subtask.verification;
if (verification.type === 'command') {
// Run shell command
const { execSync } = require('child_process');
execSync(verification.command, {
timeout: this.config.subtaskTimeout,
stdio: 'pipe',
});
} else if (verification.type === 'test') {
// Run specific test
const { execSync } = require('child_process');
execSync(verification.testCommand || 'npm test', {
timeout: this.config.subtaskTimeout,
stdio: 'pipe',
});
}
this.emit(BuildEvent.VERIFICATION_COMPLETED, {
subtaskId: subtask.id,
success: true,
});
return { success: true };
} catch (error) {
this.emit(BuildEvent.VERIFICATION_COMPLETED, {
subtaskId: subtask.id,
success: false,
error: error.message,
});
return {
success: false,
error: error.message,
};
}
}
/**
* Perform self-critique before retry
* @private
*/
async performSelfCritique(subtask, failedResult, iteration) {
this.emit(BuildEvent.SELF_CRITIQUE, {
subtaskId: subtask.id,
iteration,
failedResult,
});
// Self-critique analysis
const critique = {
predictedBugs: [],
edgeCases: [],
errorHandling: [],
patternAdherence: [],
};
// In a full implementation, this would analyze:
// Step 5.5: Predicted bugs, edge cases, error handling
// Step 6.5: Pattern adherence, no hardcoded values, tests, docs
this.log(`Self-critique for ${subtask.id}: analyzing failure...`, 'info');
return critique;
}
// ─────────────────────────────────────────────────────────────────────────────────
// PLAN MANAGEMENT
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Load implementation plan for story
* @private
*/
async loadPlan(storyId, options) {
// Try different plan locations
const planPaths = [
options.planPath,
path.join(process.cwd(), 'plan', 'implementation.yaml'),
path.join(process.cwd(), 'plan', 'implementation.json'),
path.join(process.cwd(), 'docs', 'stories', storyId, 'plan', 'implementation.yaml'),
path.join(process.cwd(), 'docs', 'stories', storyId, 'plan', 'implementation.json'),
].filter(Boolean);
for (const planPath of planPaths) {
if (fs.existsSync(planPath)) {
const content = fs.readFileSync(planPath, 'utf-8');
if (planPath.endsWith('.yaml') || planPath.endsWith('.yml')) {
const yaml = require('js-yaml');
return yaml.load(content);
}
return JSON.parse(content);
}
}
// Check if plan is provided directly
if (options.plan) {
return options.plan;
}
return null;
}
/**
* Count total subtasks in plan
* @private
*/
countSubtasks(plan) {
let count = 0;
for (const phase of plan.phases || []) {
count += (phase.subtasks || []).length;
}
return count;
}
// ─────────────────────────────────────────────────────────────────────────────────
// CONTROL METHODS
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Pause the build loop
*/
pause() {
if (this.isRunning && !this.isPaused) {
this.isPaused = true;
this.log('Build paused', 'warn');
}
}
/**
* Resume the build loop
*
* @param {string} storyId - Story identifier
* @param {Object} options - Resume options
* @returns {Promise<Object>} Build result
*/
async resume(storyId, options = {}) {
if (this.isRunning) {
throw new Error('Build loop is already running');
}
// Load state
const stateManager = new BuildStateManager(storyId, {
planDir: options.planDir || path.join(process.cwd(), 'plan'),
rootPath: options.rootPath || process.cwd(),
});
const resumeContext = stateManager.resumeBuild();
this.log(`Resuming build from checkpoint: ${resumeContext.lastCheckpoint?.id}`, 'info');
// Run with existing state
return this.run(storyId, {
...options,
plan: resumeContext.plan,
worktree: resumeContext.worktree,
});
}
/**
* Stop the build loop
*/
stop() {
if (this.isRunning) {
this.isRunning = false;
this.isPaused = false;
this.log('Build stopped', 'warn');
}
}
/**
* Check if build has timed out (AC4)
* @private
*/
isTimedOut() {
if (!this.startTime || !this.config.globalTimeout) {
return false;
}
return Date.now() - this.startTime > this.config.globalTimeout;
}
/**
* Check if build is complete
*/
isComplete() {
return this.stats.completedSubtasks >= this.stats.totalSubtasks;
}
// ─────────────────────────────────────────────────────────────────────────────────
// REPORTING
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Generate build report
* @private
*/
generateReport(storyId, result) {
const duration = Date.now() - this.startTime;
return {
storyId,
success: result.success,
error: result.error,
duration,
durationFormatted: this.formatDuration(duration),
stats: { ...this.stats },
config: {
maxIterations: this.config.maxIterations,
globalTimeout: this.config.globalTimeout,
},
results: result.results,
completedAt: new Date().toISOString(),
};
}
/**
* Format duration for display
* @private
*/
formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
}
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
}
/**
* Log message
* @private
*/
log(message, level = 'info') {
if (!this.config.verbose && level === 'debug') {
return;
}
const prefix =
{
info: chalk.blue('ℹ'),
warn: chalk.yellow('⚠'),
error: chalk.red('✗'),
success: chalk.green('✓'),
debug: chalk.gray('…'),
}[level] || '';
console.log(`${prefix} ${message}`);
}
// ─────────────────────────────────────────────────────────────────────────────────
// CLI FORMATTING
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Format status for CLI display
*/
formatStatus() {
const lines = [];
lines.push('');
lines.push(chalk.bold('Autonomous Build Loop Status'));
lines.push('─'.repeat(50));
lines.push(`Running: ${this.isRunning ? chalk.green('Yes') : chalk.gray('No')}`);
lines.push(`Paused: ${this.isPaused ? chalk.yellow('Yes') : chalk.gray('No')}`);
if (this.currentSubtask) {
lines.push(`Current: ${chalk.cyan(this.currentSubtask)}`);
}
if (this.startTime) {
const elapsed = Date.now() - this.startTime;
const timeout = this.config.globalTimeout;
const remaining = timeout - elapsed;
lines.push(`Elapsed: ${this.formatDuration(elapsed)}`);
lines.push(
`Remaining: ${remaining > 0 ? this.formatDuration(remaining) : chalk.red('TIMEOUT')}`,
);
}
lines.push('');
lines.push(chalk.bold('Statistics'));
lines.push('─'.repeat(50));
const progress =
this.stats.totalSubtasks > 0
? Math.round((this.stats.completedSubtasks / this.stats.totalSubtasks) * 100)
: 0;
lines.push(
`Progress: ${progress}% (${this.stats.completedSubtasks}/${this.stats.totalSubtasks})`,
);
lines.push(`Completed: ${chalk.green(this.stats.completedSubtasks)}`);
lines.push(`Failed: ${chalk.red(this.stats.failedSubtasks)}`);
lines.push(
`Iterations: ${this.stats.totalIterations} (${chalk.green(this.stats.successfulIterations)} ok, ${chalk.red(this.stats.failedIterations)} fail)`,
);
lines.push('');
return lines.join('\n');
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// CLI INTERFACE
// ═══════════════════════════════════════════════════════════════════════════════════
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
console.log(`
${chalk.bold('Autonomous Build Loop')} - AIOS Coder Agent Loop (Story 8.1)
${chalk.cyan('Usage:')}
autonomous-build-loop <command> <story-id> [options]
${chalk.cyan('Commands:')}
run <story-id> Run autonomous build
resume <story-id> Resume build from checkpoint
status <story-id> Show build status
${chalk.cyan('Options:')}
--max-iterations <n> Max iterations per subtask (default: 10)
--timeout <ms> Global timeout in ms (default: 1800000)
--no-self-critique Disable self-critique steps
--no-verification Disable verification
--pause-on-failure Pause on first failure
--worktree Execute in isolated worktree (Story 8.2)
--no-worktree-cleanup Keep worktree after success
--verbose, -v Enable verbose logging
--plan <path> Path to implementation plan
--help, -h Show this help
${chalk.cyan('Events Emitted:')} (AC7)
build_started, subtask_completed, build_failed, build_success
${chalk.cyan('Examples:')}
autonomous-build-loop run STORY-42
autonomous-build-loop run STORY-42 --max-iterations 5 --verbose
autonomous-build-loop resume STORY-42
autonomous-build-loop status STORY-42
${chalk.cyan('Acceptance Criteria:')}
AC1: Located in .aios-core/core/execution/
AC2: Loop: load spec → create plan → execute → verify → retry/complete
AC3: Maximum 10 iterations per subtask (configurable)
AC4: Global timeout of 30 minutes (configurable)
AC5: Command *build-autonomous {story-id} in
AC6: State persisted in plan/build-state.json
AC7: Events: build_started, subtask_completed, build_failed, build_success
AC8: Integration with Epic 5 Recovery System
`);
process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1);
}
const command = args[0];
let storyId = null;
let maxIterations = null;
let timeout = null;
let selfCritique = true;
let verification = true;
let pauseOnFailure = false;
let verbose = false;
let planPath = null;
let useWorktree = false;
let worktreeCleanup = true;
// Parse arguments
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg === '--max-iterations') {
maxIterations = parseInt(args[++i], 10);
} else if (arg === '--timeout') {
timeout = parseInt(args[++i], 10);
} else if (arg === '--no-self-critique') {
selfCritique = false;
} else if (arg === '--no-verification') {
verification = false;
} else if (arg === '--pause-on-failure') {
pauseOnFailure = true;
} else if (arg === '--worktree') {
useWorktree = true;
} else if (arg === '--no-worktree-cleanup') {
worktreeCleanup = false;
} else if (arg === '--verbose' || arg === '-v') {
verbose = true;
} else if (arg === '--plan') {
planPath = args[++i];
} else if (!arg.startsWith('-')) {
if (!storyId) {
storyId = arg;
}
}
}
if (!storyId && command !== 'help') {
console.error(chalk.red('Error: story-id is required'));
process.exit(1);
}
const config = {
maxIterations: maxIterations || DEFAULT_CONFIG.maxIterations,
globalTimeout: timeout || DEFAULT_CONFIG.globalTimeout,
selfCritiqueEnabled: selfCritique,
verificationEnabled: verification,
pauseOnFailure,
verbose,
useWorktree, // Story 8.2
worktreeCleanup, // Story 8.2
};
const loop = new AutonomousBuildLoop(config);
// Event listeners for verbose output
if (verbose) {
loop.on(BuildEvent.BUILD_STARTED, (e) =>
console.log(chalk.blue(`\n▶ Build started: ${e.storyId}`)),
);
loop.on(BuildEvent.SUBTASK_STARTED, (e) =>
console.log(chalk.cyan(` → Starting subtask: ${e.subtaskId}`)),
);
loop.on(BuildEvent.ITERATION_STARTED, (e) =>
console.log(chalk.dim(` Iteration ${e.iteration}/${e.maxIterations}`)),
);
loop.on(BuildEvent.SUBTASK_COMPLETED, (e) =>
console.log(chalk.green(` ✓ Subtask completed: ${e.subtaskId}`)),
);
loop.on(BuildEvent.SUBTASK_FAILED, (e) =>
console.log(chalk.red(` ✗ Subtask failed: ${e.subtaskId} - ${e.error}`)),
);
loop.on(BuildEvent.BUILD_SUCCESS, (e) =>
console.log(chalk.green(`\n✓ Build successful! Duration: ${loop.formatDuration(e.duration)}`)),
);
loop.on(BuildEvent.BUILD_FAILED, (e) => console.log(chalk.red(`\n✗ Build failed: ${e.error}`)));
}
try {
switch (command) {
case 'run': {
const result = await loop.run(storyId, { planPath });
console.log(loop.formatStatus());
console.log(chalk.dim(JSON.stringify(result, null, 2)));
process.exit(result.success ? 0 : 1);
break;
}
case 'resume': {
const result = await loop.resume(storyId, { planPath });
console.log(loop.formatStatus());
process.exit(result.success ? 0 : 1);
break;
}
case 'status': {
const stateManager = new BuildStateManager(storyId);
console.log(stateManager.formatStatus());
break;
}
default:
console.error(chalk.red(`Unknown command: ${command}`));
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}`));
if (verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════════════════════════
module.exports = {
AutonomousBuildLoop,
BuildEvent,
SubtaskResult,
DEFAULT_CONFIG,
};
// Run CLI if executed directly
if (require.main === module) {
main();
}