Merge branch 'main' of github.com:webdevcody/automaker

This commit is contained in:
Cody Seibert
2025-12-09 19:00:21 -05:00
24 changed files with 4783 additions and 155 deletions

View File

@@ -32,7 +32,7 @@ class AutoModeService {
query: null,
projectPath: null,
sendToRenderer: null,
isActive: () => this.runningFeatures.has(featureId)
isActive: () => this.runningFeatures.has(featureId),
};
return context;
}
@@ -126,7 +126,11 @@ class AutoModeService {
console.log(`[AutoMode] Running feature: ${feature.description}`);
// Update feature status to in_progress
await featureLoader.updateFeatureStatus(featureId, "in_progress", projectPath);
await featureLoader.updateFeatureStatus(
featureId,
"in_progress",
projectPath
);
sendToRenderer({
type: "auto_mode_feature_start",
@@ -135,13 +139,28 @@ class AutoModeService {
});
// Implement the feature
const result = await featureExecutor.implementFeature(feature, projectPath, sendToRenderer, execution);
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);
// For skipTests features, go to waiting_approval on success instead of verified
let newStatus;
if (result.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
newStatus = "backlog";
}
await featureLoader.updateFeatureStatus(
feature.id,
newStatus,
projectPath
);
// Delete context file if verified
// Delete context file only if verified (not for waiting_approval)
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, feature.id);
}
@@ -208,11 +227,20 @@ class AutoModeService {
});
// Verify the feature by running tests
const result = await featureVerifier.verifyFeatureTests(feature, projectPath, sendToRenderer, execution);
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);
await featureLoader.updateFeatureStatus(
featureId,
newStatus,
projectPath
);
// Delete context file if verified
if (newStatus === "verified") {
@@ -281,10 +309,19 @@ class AutoModeService {
});
// Read existing context
const previousContext = await contextManager.readContextFile(projectPath, featureId);
const previousContext = await contextManager.readContextFile(
projectPath,
featureId
);
// Resume implementation with context
const result = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution);
const result = await featureExecutor.resumeFeatureWithContext(
feature,
projectPath,
sendToRenderer,
previousContext,
execution
);
// If the agent ends early without finishing, automatically re-run
let attempts = 0;
@@ -298,11 +335,16 @@ class AutoModeService {
if (updatedFeature && updatedFeature.status === "in_progress") {
attempts++;
console.log(`[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...`);
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`);
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n`
);
sendToRenderer({
type: "auto_mode_progress",
@@ -311,20 +353,39 @@ class AutoModeService {
});
// Read updated context
const retryContext = await contextManager.readContextFile(projectPath, featureId);
const retryContext = await contextManager.readContextFile(
projectPath,
featureId
);
// Resume again with full context
finalResult = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, retryContext, execution);
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);
// For skipTests features, go to waiting_approval on success instead of verified
let newStatus;
if (finalResult.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
newStatus = "in_progress";
}
await featureLoader.updateFeatureStatus(
featureId,
newStatus,
projectPath
);
// Delete context file if verified
// Delete context file only if verified (not for waiting_approval)
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, featureId);
}
@@ -377,7 +438,9 @@ class AutoModeService {
// Skip if this feature is already running (via manual trigger)
if (this.runningFeatures.has(currentFeatureId)) {
console.log(`[AutoMode] Skipping ${currentFeatureId} - already running`);
console.log(
`[AutoMode] Skipping ${currentFeatureId} - already running`
);
await this.sleep(3000);
continue;
}
@@ -397,13 +460,28 @@ class AutoModeService {
this.runningFeatures.set(currentFeatureId, execution);
// Implement the feature
const result = await featureExecutor.implementFeature(nextFeature, projectPath, sendToRenderer, execution);
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);
// For skipTests features, go to waiting_approval on success instead of verified
let newStatus;
if (result.passes) {
newStatus = nextFeature.skipTests ? "waiting_approval" : "verified";
} else {
newStatus = "backlog";
}
await featureLoader.updateFeatureStatus(
nextFeature.id,
newStatus,
projectPath
);
// Delete context file if verified
// Delete context file only if verified (not for waiting_approval)
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, nextFeature.id);
}
@@ -477,7 +555,12 @@ class AutoModeService {
});
// Perform the analysis
const result = await projectAnalyzer.runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution);
const result = await projectAnalyzer.runProjectAnalysis(
projectPath,
analysisId,
sendToRenderer,
execution
);
sendToRenderer({
type: "auto_mode_feature_complete",
@@ -500,6 +583,239 @@ class AutoModeService {
}
}
/**
* Stop a specific feature by ID
*/
async stopFeature({ featureId }) {
if (!this.runningFeatures.has(featureId)) {
return { success: false, error: `Feature ${featureId} is not running` };
}
console.log(`[AutoMode] Stopping feature: ${featureId}`);
const execution = this.runningFeatures.get(featureId);
if (execution && execution.abortController) {
execution.abortController.abort();
}
// Clean up
this.runningFeatures.delete(featureId);
return { success: true };
}
/**
* Follow-up on a feature with additional prompt
* This continues work on a feature that's in waiting_approval status
*/
async followUpFeature({
projectPath,
featureId,
prompt,
imagePaths,
sendToRenderer,
}) {
// Check if this feature is already running
if (this.runningFeatures.has(featureId)) {
throw new Error(`Feature ${featureId} is already running`);
}
console.log(
`[AutoMode] Follow-up on feature: ${featureId} with prompt: ${prompt}`
);
// Register this feature as running
const execution = this.createExecutionContext(featureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(featureId, execution);
// Start the async work in the background (don't await)
// This allows the API to return immediately so the modal can close
this.runFollowUpWork({
projectPath,
featureId,
prompt,
imagePaths,
sendToRenderer,
execution,
}).catch((error) => {
console.error("[AutoMode] Follow-up work error:", error);
this.runningFeatures.delete(featureId);
});
// Return immediately so the frontend can close the modal
return { success: true };
}
/**
* Internal method to run follow-up work asynchronously
*/
async runFollowUpWork({
projectPath,
featureId,
prompt,
imagePaths,
sendToRenderer,
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] Following up on feature: ${feature.description}`);
// Update status to in_progress
await featureLoader.updateFeatureStatus(
featureId,
"in_progress",
projectPath
);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Read existing context and append follow-up prompt
const previousContext = await contextManager.readContextFile(
projectPath,
featureId
);
// Append follow-up prompt to context
const followUpContext = `${previousContext}\n\n## Follow-up Instructions\n\n${prompt}`;
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n## Follow-up Instructions\n\n${prompt}`
);
// Resume implementation with follow-up context and optional images
const result = await featureExecutor.resumeFeatureWithContext(
{ ...feature, followUpPrompt: prompt, followUpImages: imagePaths },
projectPath,
sendToRenderer,
followUpContext,
execution
);
// For skipTests features, go to waiting_approval on success instead of verified
const newStatus = result.passes
? feature.skipTests
? "waiting_approval"
: "verified"
: "in_progress";
await featureLoader.updateFeatureStatus(
feature.id,
newStatus,
projectPath
);
// Delete context file if verified (only for non-skipTests)
if (newStatus === "verified") {
await contextManager.deleteContextFile(projectPath, feature.id);
}
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: result.passes,
message: result.message,
});
} catch (error) {
console.error("[AutoMode] Error in follow-up:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
} finally {
this.runningFeatures.delete(featureId);
}
}
/**
* Commit changes for a feature without doing additional work
* This marks the feature as verified and commits the changes
*/
async commitFeature({ projectPath, featureId, sendToRenderer }) {
console.log(`[AutoMode] Committing feature: ${featureId}`);
// Register briefly as running for the commit operation
const execution = this.createExecutionContext(featureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(featureId, execution);
try {
// Load feature to get description for commit message
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: { ...feature, description: "Committing changes..." },
});
sendToRenderer({
type: "auto_mode_phase",
featureId,
phase: "action",
message: "Committing changes to git...",
});
// Run git commit via the agent
const commitResult = await featureExecutor.commitChangesOnly(
feature,
projectPath,
sendToRenderer,
execution
);
// Update status to verified
await featureLoader.updateFeatureStatus(
featureId,
"verified",
projectPath
);
// Delete context file
await contextManager.deleteContextFile(projectPath, featureId);
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: true,
message: "Changes committed successfully",
});
return { success: true };
} catch (error) {
console.error("[AutoMode] Error committing feature:", error);
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
throw error;
} finally {
this.runningFeatures.delete(featureId);
}
}
/**
* Sleep helper
*/