Files
automaker/app/electron/auto-mode-service.js
Cody Seibert 08014f3a4a Refactor Auto Mode Service and add feature verification and project analysis capabilities
- Refactored AutoModeService to delegate tasks to specialized services: featureLoader, featureExecutor, featureVerifier, contextManager, and projectAnalyzer.
- Implemented feature verification logic in a new FeatureVerifier service, which runs tests and updates feature status.
- Added ProjectAnalyzer service to scan project structure and update app_spec.txt.
- Removed obsolete methods related to feature loading and context management from AutoModeService.
- Updated feature status handling to ensure context files are deleted when features are verified.

This refactor enhances modularity and maintainability of the codebase, allowing for better separation of concerns in feature management.

🤖 Generated with Claude Code
2025-12-09 13:03:03 -05:00

513 lines
16 KiB
JavaScript

const featureLoader = require("./services/feature-loader");
const featureExecutor = require("./services/feature-executor");
const featureVerifier = require("./services/feature-verifier");
const contextManager = require("./services/context-manager");
const projectAnalyzer = require("./services/project-analyzer");
/**
* Auto Mode Service - Autonomous feature implementation
* Automatically picks and implements features from the kanban board
*
* This service acts as the main orchestrator, delegating work to specialized services:
* - featureLoader: Loading and selecting features
* - featureExecutor: Implementing features
* - featureVerifier: Running tests and verification
* - contextManager: Managing context files
* - projectAnalyzer: Analyzing project structure
*/
class AutoModeService {
constructor() {
// Track multiple concurrent feature executions
this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer }
this.autoLoopRunning = false; // Separate flag for the auto loop
this.autoLoopAbortController = null;
}
/**
* Helper to create execution context with isActive check
*/
createExecutionContext(featureId) {
const context = {
abortController: null,
query: null,
projectPath: null,
sendToRenderer: null,
isActive: () => this.runningFeatures.has(featureId)
};
return context;
}
/**
* Start auto mode - continuously implement features
*/
async start({ projectPath, sendToRenderer }) {
if (this.autoLoopRunning) {
throw new Error("Auto mode loop is already running");
}
this.autoLoopRunning = true;
console.log("[AutoMode] Starting auto mode for project:", projectPath);
// Run the autonomous loop
this.runLoop(projectPath, sendToRenderer).catch((error) => {
console.error("[AutoMode] Loop error:", error);
this.stop();
});
return { success: true };
}
/**
* Stop auto mode - stops the auto loop and all running features
*/
async stop() {
console.log("[AutoMode] Stopping auto mode");
this.autoLoopRunning = false;
// Abort auto loop if running
if (this.autoLoopAbortController) {
this.autoLoopAbortController.abort();
this.autoLoopAbortController = null;
}
// Abort all running features
for (const [featureId, execution] of this.runningFeatures.entries()) {
console.log(`[AutoMode] Aborting feature: ${featureId}`);
if (execution.abortController) {
execution.abortController.abort();
}
}
// Clear all running features
this.runningFeatures.clear();
return { success: true };
}
/**
* Get status of auto mode
*/
getStatus() {
return {
autoLoopRunning: this.autoLoopRunning,
runningFeatures: Array.from(this.runningFeatures.keys()),
runningCount: this.runningFeatures.size,
};
}
/**
* Run a specific feature by ID
*/
async runFeature({ projectPath, featureId, sendToRenderer }) {
// Check if this specific feature is already running
if (this.runningFeatures.has(featureId)) {
throw new Error(`Feature ${featureId} is already running`);
}
console.log(`[AutoMode] Running specific feature: ${featureId}`);
// Register this feature as running
const execution = this.createExecutionContext(featureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(featureId, execution);
try {
// Load features
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Running feature: ${feature.description}`);
// Update feature status to in_progress
await featureLoader.updateFeatureStatus(featureId, "in_progress", projectPath);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Implement the feature
const result = await featureExecutor.implementFeature(feature, projectPath, sendToRenderer, execution);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "backlog";
await featureLoader.updateFeatureStatus(feature.id, newStatus, projectPath);
// Delete context file if verified
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, feature.id);
}
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: result.passes,
message: result.message,
});
return { success: true, passes: result.passes };
} catch (error) {
console.error("[AutoMode] Error running feature:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
throw error;
} finally {
// Clean up this feature's execution
this.runningFeatures.delete(featureId);
}
}
/**
* Verify a specific feature by running its tests
*/
async verifyFeature({ projectPath, featureId, sendToRenderer }) {
console.log(`[AutoMode] verifyFeature called with:`, {
projectPath,
featureId,
});
// Check if this specific feature is already running
if (this.runningFeatures.has(featureId)) {
throw new Error(`Feature ${featureId} is already running`);
}
console.log(`[AutoMode] Verifying feature: ${featureId}`);
// Register this feature as running
const execution = this.createExecutionContext(featureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(featureId, execution);
try {
// Load features
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Verifying feature: ${feature.description}`);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Verify the feature by running tests
const result = await featureVerifier.verifyFeatureTests(feature, projectPath, sendToRenderer, execution);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "in_progress";
await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath);
// Delete context file if verified
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, featureId);
}
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: result.passes,
message: result.message,
});
return { success: true, passes: result.passes };
} catch (error) {
console.error("[AutoMode] Error verifying feature:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
throw error;
} finally {
// Clean up this feature's execution
this.runningFeatures.delete(featureId);
}
}
/**
* Resume a feature that has previous context - loads existing context and continues implementation
*/
async resumeFeature({ projectPath, featureId, sendToRenderer }) {
console.log(`[AutoMode] resumeFeature called with:`, {
projectPath,
featureId,
});
// Check if this specific feature is already running
if (this.runningFeatures.has(featureId)) {
throw new Error(`Feature ${featureId} is already running`);
}
console.log(`[AutoMode] Resuming feature: ${featureId}`);
// Register this feature as running
const execution = this.createExecutionContext(featureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(featureId, execution);
try {
// Load features
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Resuming feature: ${feature.description}`);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Read existing context
const previousContext = await contextManager.readContextFile(projectPath, featureId);
// Resume implementation with context
const result = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution);
// If the agent ends early without finishing, automatically re-run
let attempts = 0;
const maxAttempts = 3;
let finalResult = result;
while (!finalResult.passes && attempts < maxAttempts) {
// Check if feature is still in progress (not verified)
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
const updatedFeature = updatedFeatures.find((f) => f.id === featureId);
if (updatedFeature && updatedFeature.status === "in_progress") {
attempts++;
console.log(`[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...`);
// Update context file with retry message
await contextManager.writeToContextFile(projectPath, featureId,
`\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n`);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: `\n🔄 Auto-retry #${attempts} - Agent ended early, continuing...\n`,
});
// Read updated context
const retryContext = await contextManager.readContextFile(projectPath, featureId);
// Resume again with full context
finalResult = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, retryContext, execution);
} else {
break;
}
}
// Update feature status based on final result
const newStatus = finalResult.passes ? "verified" : "in_progress";
await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath);
// Delete context file if verified
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, featureId);
}
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: finalResult.passes,
message: finalResult.message,
});
return { success: true, passes: finalResult.passes };
} catch (error) {
console.error("[AutoMode] Error resuming feature:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
throw error;
} finally {
// Clean up this feature's execution
this.runningFeatures.delete(featureId);
}
}
/**
* Main autonomous loop - picks and implements features
*/
async runLoop(projectPath, sendToRenderer) {
while (this.autoLoopRunning) {
let currentFeatureId = null;
try {
// Load features from .automaker/feature_list.json
const features = await featureLoader.loadFeatures(projectPath);
// Find highest priority incomplete feature
const nextFeature = featureLoader.selectNextFeature(features);
if (!nextFeature) {
console.log("[AutoMode] No more features to implement");
sendToRenderer({
type: "auto_mode_complete",
message: "All features completed!",
});
break;
}
currentFeatureId = nextFeature.id;
// Skip if this feature is already running (via manual trigger)
if (this.runningFeatures.has(currentFeatureId)) {
console.log(`[AutoMode] Skipping ${currentFeatureId} - already running`);
await this.sleep(3000);
continue;
}
console.log(`[AutoMode] Selected feature: ${nextFeature.description}`);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: nextFeature.id,
feature: nextFeature,
});
// Register this feature as running
const execution = this.createExecutionContext(currentFeatureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(currentFeatureId, execution);
// Implement the feature
const result = await featureExecutor.implementFeature(nextFeature, projectPath, sendToRenderer, execution);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "backlog";
await featureLoader.updateFeatureStatus(nextFeature.id, newStatus, projectPath);
// Delete context file if verified
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, nextFeature.id);
}
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: nextFeature.id,
passes: result.passes,
message: result.message,
});
// Clean up
this.runningFeatures.delete(currentFeatureId);
// Small delay before next feature
if (this.autoLoopRunning) {
await this.sleep(3000);
}
} catch (error) {
console.error("[AutoMode] Error in loop iteration:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: currentFeatureId,
});
// Clean up on error
if (currentFeatureId) {
this.runningFeatures.delete(currentFeatureId);
}
// Wait before retrying
await this.sleep(5000);
}
}
console.log("[AutoMode] Loop ended");
this.autoLoopRunning = false;
}
/**
* Analyze a new project - scans codebase and updates app_spec.txt
* This is triggered when opening a project for the first time
*/
async analyzeProject({ projectPath, sendToRenderer }) {
console.log(`[AutoMode] Analyzing project at: ${projectPath}`);
const analysisId = `project-analysis-${Date.now()}`;
// Check if already analyzing this project
if (this.runningFeatures.has(analysisId)) {
throw new Error("Project analysis is already running");
}
// Register as running
const execution = this.createExecutionContext(analysisId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(analysisId, execution);
try {
sendToRenderer({
type: "auto_mode_feature_start",
featureId: analysisId,
feature: {
id: analysisId,
category: "Project Analysis",
description: "Analyzing project structure and tech stack",
},
});
// Perform the analysis
const result = await projectAnalyzer.runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution);
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: analysisId,
passes: result.success,
message: result.message,
});
return { success: true, message: result.message };
} catch (error) {
console.error("[AutoMode] Error analyzing project:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: analysisId,
});
throw error;
} finally {
this.runningFeatures.delete(analysisId);
}
}
/**
* Sleep helper
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Export singleton instance
module.exports = new AutoModeService();