mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
- 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
513 lines
16 KiB
JavaScript
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();
|