Files
automaker/app/electron/auto-mode-service.js
Cody Seibert 7bfc489efa Change description field to textarea in Add New Feature modal
The description field in the Add New Feature modal is now a textarea instead of
an input, allowing users to enter multi-line feature descriptions more easily.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 22:53:33 -05:00

850 lines
25 KiB
JavaScript

const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const path = require("path");
const fs = require("fs/promises");
/**
* Auto Mode Service - Autonomous feature implementation
* Automatically picks and implements features from the kanban board
*/
class AutoModeService {
constructor() {
this.isRunning = false;
this.currentFeatureId = null;
this.abortController = null;
this.currentQuery = null;
this.projectPath = null;
this.sendToRenderer = null;
}
/**
* Start auto mode - continuously implement features
*/
async start({ projectPath, sendToRenderer }) {
if (this.isRunning) {
throw new Error("Auto mode is already running");
}
this.isRunning = true;
this.projectPath = projectPath;
this.sendToRenderer = sendToRenderer;
console.log("[AutoMode] Starting auto mode for project:", projectPath);
// Run the autonomous loop
this.runLoop().catch((error) => {
console.error("[AutoMode] Loop error:", error);
this.stop();
});
return { success: true };
}
/**
* Stop auto mode
*/
async stop() {
console.log("[AutoMode] Stopping auto mode");
this.isRunning = false;
// Abort current agent execution
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.currentQuery = null;
this.currentFeatureId = null;
this.projectPath = null;
this.sendToRenderer = null;
return { success: true };
}
/**
* Get status of auto mode
*/
getStatus() {
return {
isRunning: this.isRunning,
currentFeatureId: this.currentFeatureId,
};
}
/**
* Run a specific feature by ID
*/
async runFeature({ projectPath, featureId, sendToRenderer }) {
if (this.isRunning) {
throw new Error("Auto mode is already running");
}
this.isRunning = true;
this.projectPath = projectPath;
this.sendToRenderer = sendToRenderer;
console.log(`[AutoMode] Running specific feature: ${featureId}`);
try {
// Load features
const features = await this.loadFeatures();
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Running feature: ${feature.description}`);
this.currentFeatureId = feature.id;
// Update feature status to in_progress
await this.updateFeatureStatus(featureId, "in_progress");
this.sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Implement the feature
const result = await this.implementFeature(feature);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "backlog";
await this.updateFeatureStatus(feature.id, newStatus);
this.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);
this.sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: this.currentFeatureId,
});
throw error;
} finally {
this.isRunning = false;
this.currentFeatureId = null;
this.projectPath = null;
this.sendToRenderer = null;
}
}
/**
* Verify a specific feature by running its tests
*/
async verifyFeature({ projectPath, featureId, sendToRenderer }) {
console.log(`[AutoMode] verifyFeature called with:`, {
projectPath,
featureId,
});
if (this.isRunning) {
throw new Error("Auto mode is already running");
}
this.isRunning = true;
this.projectPath = projectPath;
this.sendToRenderer = sendToRenderer;
console.log(`[AutoMode] Verifying feature: ${featureId}`);
try {
// Load features
const features = await this.loadFeatures();
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
console.log(`[AutoMode] Verifying feature: ${feature.description}`);
this.currentFeatureId = feature.id;
this.sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: feature,
});
// Verify the feature by running tests
const result = await this.verifyFeatureTests(feature);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "in_progress";
await this.updateFeatureStatus(featureId, newStatus);
this.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);
this.sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: this.currentFeatureId,
});
throw error;
} finally {
this.isRunning = false;
this.currentFeatureId = null;
this.projectPath = null;
this.sendToRenderer = null;
}
}
/**
* Main autonomous loop - picks and implements features
*/
async runLoop() {
while (this.isRunning) {
try {
// Load features from .automaker/feature_list.json
const features = await this.loadFeatures();
// Find highest priority incomplete feature
const nextFeature = this.selectNextFeature(features);
if (!nextFeature) {
console.log("[AutoMode] No more features to implement");
this.sendToRenderer({
type: "auto_mode_complete",
message: "All features completed!",
});
break;
}
console.log(`[AutoMode] Selected feature: ${nextFeature.description}`);
this.currentFeatureId = nextFeature.id;
this.sendToRenderer({
type: "auto_mode_feature_start",
featureId: nextFeature.id,
feature: nextFeature,
});
// Implement the feature
const result = await this.implementFeature(nextFeature);
// Update feature status based on result
const newStatus = result.passes ? "verified" : "backlog";
await this.updateFeatureStatus(nextFeature.id, newStatus);
this.sendToRenderer({
type: "auto_mode_feature_complete",
featureId: nextFeature.id,
passes: result.passes,
message: result.message,
});
// Small delay before next feature
if (this.isRunning) {
await this.sleep(3000);
}
} catch (error) {
console.error("[AutoMode] Error in loop iteration:", error);
this.sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: this.currentFeatureId,
});
// Wait before retrying
await this.sleep(5000);
}
}
console.log("[AutoMode] Loop ended");
this.isRunning = false;
}
/**
* Load features from .automaker/feature_list.json
*/
async loadFeatures() {
const featuresPath = path.join(
this.projectPath,
".automaker",
"feature_list.json"
);
try {
const content = await fs.readFile(featuresPath, "utf-8");
const features = JSON.parse(content);
// Ensure each feature has an ID
return features.map((f, index) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
}));
} catch (error) {
console.error("[AutoMode] Failed to load features:", error);
return [];
}
}
/**
* Select the next feature to implement
* Prioritizes: earlier features in the list that are not verified
*/
selectNextFeature(features) {
// Find first feature that is in backlog or in_progress status
return features.find((f) => f.status !== "verified");
}
/**
* Write output to feature context file
*/
async writeToContextFile(featureId, content) {
if (!this.projectPath) return;
try {
const contextDir = path.join(this.projectPath, ".automaker", "context");
// Ensure directory exists
try {
await fs.access(contextDir);
} catch {
await fs.mkdir(contextDir, { recursive: true });
}
const filePath = path.join(contextDir, `${featureId}.md`);
// Append to existing file or create new one
try {
const existing = await fs.readFile(filePath, "utf-8");
await fs.writeFile(filePath, existing + content, "utf-8");
} catch {
await fs.writeFile(filePath, content, "utf-8");
}
} catch (error) {
console.error("[AutoMode] Failed to write to context file:", error);
}
}
/**
* Implement a single feature using Claude Agent SDK
* Uses a Plan-Act-Verify loop with detailed phase logging
*/
async implementFeature(feature) {
console.log(`[AutoMode] Implementing: ${feature.description}`);
try {
// ========================================
// PHASE 1: PLANNING
// ========================================
const planningMessage = `📋 Planning implementation for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, planningMessage);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "planning",
message: `Planning implementation for: ${feature.description}`,
});
console.log(`[AutoMode] Phase: PLANNING for ${feature.description}`);
this.abortController = new AbortController();
// Configure options for the SDK query
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: this.getCodingPrompt(),
maxTurns: 30,
cwd: this.projectPath,
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: this.abortController,
};
// Build the prompt for this specific feature
const prompt = this.buildFeaturePrompt(feature);
// Planning: Analyze the codebase and create implementation plan
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content:
"Analyzing codebase structure and creating implementation plan...",
});
// Small delay to show planning phase
await this.sleep(500);
// ========================================
// PHASE 2: ACTION
// ========================================
const actionMessage = `⚡ Executing implementation for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, actionMessage);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "action",
message: `Executing implementation for: ${feature.description}`,
});
console.log(`[AutoMode] Phase: ACTION for ${feature.description}`);
// Send query
this.currentQuery = query({ prompt, options });
// Stream responses
let responseText = "";
let hasStartedToolUse = false;
for await (const msg of this.currentQuery) {
if (!this.isRunning) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
// Write to context file
await this.writeToContextFile(feature.id, block.text);
// Stream progress to renderer
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
// First tool use indicates we're actively implementing
if (!hasStartedToolUse) {
hasStartedToolUse = true;
const startMsg = "Starting code implementation...\n";
await this.writeToContextFile(feature.id, startMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: startMsg,
});
}
// Write tool use to context file
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await this.writeToContextFile(feature.id, toolMsg);
// Notify about tool use
this.sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
this.currentQuery = null;
this.abortController = null;
// ========================================
// PHASE 3: VERIFICATION
// ========================================
const verificationMessage = `✅ Verifying implementation for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, verificationMessage);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying implementation for: ${feature.description}`,
});
console.log(`[AutoMode] Phase: VERIFICATION for ${feature.description}`);
const checkingMsg =
"Verifying implementation and checking test results...\n";
await this.writeToContextFile(feature.id, checkingMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: checkingMsg,
});
// Re-load features to check if it was marked as verified
const updatedFeatures = await this.loadFeatures();
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
const passes = updatedFeature?.status === "verified";
// Send verification result
const resultMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Verification: Tests need attention\n";
await this.writeToContextFile(feature.id, resultMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: resultMsg,
});
return {
passes,
message: responseText.substring(0, 500), // First 500 chars
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[AutoMode] Feature run aborted");
this.abortController = null;
this.currentQuery = null;
return {
passes: false,
message: "Auto mode aborted",
};
}
console.error("[AutoMode] Error implementing feature:", error);
// Clean up
this.abortController = null;
this.currentQuery = null;
throw error;
}
}
/**
* Update feature status in .automaker/feature_list.json
*/
async updateFeatureStatus(featureId, status) {
const features = await this.loadFeatures();
const feature = features.find((f) => f.id === featureId);
if (!feature) {
console.error(`[AutoMode] Feature ${featureId} not found`);
return;
}
// Update the status field
feature.status = status;
// Save back to file
const featuresPath = path.join(
this.projectPath,
".automaker",
"feature_list.json"
);
const toSave = features.map((f) => ({
id: f.id,
category: f.category,
description: f.description,
steps: f.steps,
status: f.status,
}));
await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8");
console.log(`[AutoMode] Updated feature ${featureId}: status=${status}`);
}
/**
* Verify feature tests (runs tests and checks if they pass)
*/
async verifyFeatureTests(feature) {
console.log(`[AutoMode] Verifying tests for: ${feature.description}`);
try {
const verifyMsg = `\n✅ Verifying tests for: ${feature.description}\n`;
await this.writeToContextFile(feature.id, verifyMsg);
this.sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying tests for: ${feature.description}`,
});
this.abortController = new AbortController();
const options = {
model: "claude-opus-4-5-20251101",
systemPrompt: this.getVerificationPrompt(),
maxTurns: 15,
cwd: this.projectPath,
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: this.abortController,
};
const prompt = this.buildVerificationPrompt(feature);
const runningTestsMsg =
"Running Playwright tests to verify feature implementation...\n";
await this.writeToContextFile(feature.id, runningTestsMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: runningTestsMsg,
});
this.currentQuery = query({ prompt, options });
let responseText = "";
for await (const msg of this.currentQuery) {
if (!this.isRunning) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
await this.writeToContextFile(feature.id, block.text);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "tool_use") {
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await this.writeToContextFile(feature.id, toolMsg);
this.sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
this.currentQuery = null;
this.abortController = null;
// Re-load features to check if it was marked as verified
const updatedFeatures = await this.loadFeatures();
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
const passes = updatedFeature?.status === "verified";
const finalMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Tests failed or not all passing - feature remains in progress\n";
await this.writeToContextFile(feature.id, finalMsg);
this.sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: finalMsg,
});
return {
passes,
message: responseText.substring(0, 500),
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[AutoMode] Verification aborted");
this.abortController = null;
this.currentQuery = null;
return {
passes: false,
message: "Verification aborted",
};
}
console.error("[AutoMode] Error verifying feature:", error);
this.abortController = null;
this.currentQuery = null;
throw error;
}
}
/**
* Build the prompt for implementing a specific feature
*/
buildFeaturePrompt(feature) {
return `You are working on a feature implementation task.
**Current Feature to Implement:**
Category: ${feature.category}
Description: ${feature.description}
**Steps to Complete:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the project files to understand the current codebase structure
2. Implement the feature according to the description and steps
3. Write Playwright tests to verify the feature works correctly
4. Run the tests and ensure they pass
5. **DELETE the test file(s) you created** - tests are only for immediate verification
6. Update .automaker/feature_list.json to mark this feature as "status": "verified"
7. Commit your changes with git
**Important Guidelines:**
- Focus ONLY on implementing this specific feature
- Write clean, production-quality code
- Add proper error handling
- Write comprehensive Playwright tests
- Ensure all existing tests still pass
- Mark the feature as passing only when all tests are green
- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle
- Make a git commit when complete
**Test Deletion Policy:**
After tests pass, delete them immediately:
\`\`\`bash
rm tests/[feature-name].spec.ts
\`\`\`
Begin by reading the project structure and then implementing the feature.`;
}
/**
* Build the prompt for verifying a specific feature
*/
buildVerificationPrompt(feature) {
return `You are verifying that a feature implementation is complete and working correctly.
**Feature to Verify:**
ID: ${feature.id}
Category: ${feature.category}
Description: ${feature.description}
Current Status: ${feature.status}
**Steps that should be implemented:**
${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
**Your Task:**
1. Read the .automaker/feature_list.json file to see the current status
2. Look for Playwright tests related to this feature
3. Run the Playwright tests for this feature: npx playwright test tests/[feature-name].spec.ts
4. Check if all tests pass
5. If ALL tests pass:
- **DELETE the test file(s) for this feature** - tests are only for immediate verification
- Update .automaker/feature_list.json to set this feature's "status" to "verified"
- Explain what tests passed and that you deleted them
6. If ANY tests fail:
- Keep the feature "status" as "in_progress" in .automaker/feature_list.json
- Explain what tests failed and why
7. Fix the issues until the tests pass again
**Test Deletion Policy:**
After tests pass, delete them immediately:
\`\`\`bash
rm tests/[feature-name].spec.ts
\`\`\`
**Important:**
- Only mark as "verified" if Playwright tests pass
- **CRITICAL: Delete test files after they pass** - tests should not accumulate
- Focus on running tests, deleting them, and updating the status accurately
- Be thorough in checking test results
Begin by reading .automaker/feature_list.json and finding the appropriate tests to run.`;
}
/**
* Get the system prompt for verification agent
*/
getVerificationPrompt() {
return `You are an AI verification agent focused on testing and validation.
Your role is to:
- Run Playwright tests to verify feature implementations
- If other tests fail, verify if those tests are still accurate or should be updated or deleted
- Continue rerunning tests until all tests pass
- **DELETE test files after successful verification** - tests are only for immediate feature verification
- Update feature status to verified in .automaker/feature_list.json after all tests pass
**Test Deletion Policy:**
Tests should NOT accumulate. After a feature is verified:
1. Delete the test file for that feature
2. Mark the feature as "verified" in feature_list.json
This prevents test brittleness as the app changes rapidly.
You have access to:
- Read and edit files
- Run bash commands (especially Playwright tests)
- Delete files (rm command)
- Analyze test output
Be accurate and thorough in your verification process. Always delete tests after they pass.`;
}
/**
* Get the system prompt for coding agent
*/
getCodingPrompt() {
return `You are an AI coding agent working autonomously to implement features.
Your role is to:
- Implement features exactly as specified
- Write production-quality code
- Create comprehensive Playwright tests
- Ensure all tests pass before marking features complete
- **DELETE test files after successful verification** - tests are only for immediate feature verification
- Commit working code to git
- Be thorough and detail-oriented
**Test Deletion Policy:**
Tests should NOT accumulate. After a feature is verified:
1. Run the tests to ensure they pass
2. Delete the test file for that feature
3. Mark the feature as "verified" in .automaker/feature_list.json
This prevents test brittleness as the app changes rapidly.
You have full access to:
- Read and write files
- Run bash commands
- Execute tests
- Delete files (rm command)
- Make git commits
- Search and analyze the codebase
Focus on one feature at a time and complete it fully before finishing. Always delete tests after they pass.`;
}
/**
* Sleep helper
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Export singleton instance
module.exports = new AutoModeService();