Files
automaker/app/electron/auto-mode-service.js
Kacper a78b6763de feat(core): implement git worktree checkpoint system
Add comprehensive worktree management system to enable task isolation and rollback capabilities. This allows users to revert agent changes if they don't satisfy requirements or break functionality.

Key components:
- New WorktreeManager service for branch and worktree operations
- GitDiffPanel component for visualizing changes
- Enhanced UI components with worktree integration
- Auto-mode service enhancements for worktree workflow

Modified files: worktree-manager.js, git-diff-panel.tsx, main.js, preload.js, feature-loader.js, agent-output-modal.tsx, board-view.tsx, kanban-card.tsx, electron.ts, app-store.ts, electron.d.ts

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

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
2025-12-10 12:28:13 +01:00

1225 lines
37 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");
const worktreeManager = require("./services/worktree-manager");
/**
* 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;
this.autoLoopInterval = null; // Timer for periodic checking
this.checkIntervalMs = 5000; // Check every 5 seconds
this.maxConcurrency = 3; // Default max concurrency
}
/**
* Helper to create execution context with isActive check
*/
createExecutionContext(featureId) {
const context = {
abortController: null,
query: null,
projectPath: null, // Original project path
worktreePath: null, // Path to worktree (where agent works)
branchName: null, // Feature branch name
sendToRenderer: null,
isActive: () => this.runningFeatures.has(featureId),
};
return context;
}
/**
* Setup worktree for a feature
* Creates an isolated git worktree where the agent can work
*/
async setupWorktreeForFeature(feature, projectPath, sendToRenderer) {
// Check if worktrees are enabled (project must be a git repo)
const isGit = await worktreeManager.isGitRepo(projectPath);
if (!isGit) {
console.log(`[AutoMode] Project is not a git repo, skipping worktree creation`);
return { useWorktree: false, workPath: projectPath };
}
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: "Creating isolated worktree for feature...\n",
});
const result = await worktreeManager.createWorktree(projectPath, feature);
if (!result.success) {
console.warn(`[AutoMode] Failed to create worktree: ${result.error}. Falling back to main project.`);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: `Warning: Could not create worktree (${result.error}). Working directly on main project.\n`,
});
return { useWorktree: false, workPath: projectPath };
}
console.log(`[AutoMode] Created worktree at: ${result.worktreePath}, branch: ${result.branchName}`);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: `Working in isolated branch: ${result.branchName}\n`,
});
// Update feature with worktree info in feature_list.json
await featureLoader.updateFeatureWorktree(
feature.id,
projectPath,
result.worktreePath,
result.branchName
);
return {
useWorktree: true,
workPath: result.worktreePath,
branchName: result.branchName,
baseBranch: result.baseBranch,
};
}
/**
* Start auto mode - continuously implement features
*/
async start({ projectPath, sendToRenderer, maxConcurrency }) {
if (this.autoLoopRunning) {
throw new Error("Auto mode loop is already running");
}
this.autoLoopRunning = true;
this.maxConcurrency = maxConcurrency || 3;
console.log(
`[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${this.maxConcurrency}`
);
// Start the periodic checking loop
this.runPeriodicLoop(projectPath, sendToRenderer);
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;
// Clear the interval timer
if (this.autoLoopInterval) {
clearInterval(this.autoLoopInterval);
this.autoLoopInterval = null;
}
// 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}`);
// Setup worktree for isolated work
const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer);
execution.worktreePath = worktreeSetup.workPath;
execution.branchName = worktreeSetup.branchName;
// Determine working path (worktree or main project)
const workPath = worktreeSetup.workPath;
// Update feature status to in_progress
await featureLoader.updateFeatureStatus(
featureId,
"in_progress",
projectPath
);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName },
});
// Implement the feature (agent works in worktree)
const result = await featureExecutor.implementFeature(
feature,
workPath, // Use worktree path instead of main project
sendToRenderer,
execution
);
// Update feature status based on result
// For skipTests features, go to waiting_approval on success instead of verified
// On failure, skipTests features should also go to waiting_approval for user review
let newStatus;
if (result.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
// For skipTests features, keep in waiting_approval so user can review
// For normal TDD features, move to backlog for retry
newStatus = feature.skipTests ? "waiting_approval" : "backlog";
}
await featureLoader.updateFeatureStatus(
feature.id,
newStatus,
projectPath
);
// Keep context file for viewing output later (deleted only when card is removed)
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);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
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
);
// Keep context file for viewing output later (deleted only when card is removed)
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);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
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
// 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
);
// Keep context file for viewing output later (deleted only when card is removed)
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);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
throw error;
} finally {
// Clean up this feature's execution
this.runningFeatures.delete(featureId);
}
}
/**
* New periodic loop - checks available slots and starts features up to max concurrency
* This loop continues running even if there are no backlog items
*/
runPeriodicLoop(projectPath, sendToRenderer) {
console.log(
`[AutoMode] Starting periodic loop with interval: ${this.checkIntervalMs}ms`
);
// Initial check immediately
this.checkAndStartFeatures(projectPath, sendToRenderer);
// Then check periodically
this.autoLoopInterval = setInterval(() => {
if (this.autoLoopRunning) {
this.checkAndStartFeatures(projectPath, sendToRenderer);
}
}, this.checkIntervalMs);
}
/**
* Check how many features are running and start new ones if under max concurrency
*/
async checkAndStartFeatures(projectPath, sendToRenderer) {
try {
// Check how many are currently running
const currentRunningCount = this.runningFeatures.size;
console.log(
`[AutoMode] Checking features - Running: ${currentRunningCount}/${this.maxConcurrency}`
);
// Calculate available slots
const availableSlots = this.maxConcurrency - currentRunningCount;
if (availableSlots <= 0) {
console.log("[AutoMode] At max concurrency, waiting...");
return;
}
// Load features from backlog
const features = await featureLoader.loadFeatures(projectPath);
const backlogFeatures = features.filter((f) => f.status === "backlog");
if (backlogFeatures.length === 0) {
console.log("[AutoMode] No backlog features available, waiting...");
return;
}
// Grab up to availableSlots features from backlog
const featuresToStart = backlogFeatures.slice(0, availableSlots);
console.log(
`[AutoMode] Starting ${featuresToStart.length} feature(s) from backlog`
);
// Start each feature (don't await - run in parallel like drag operations)
for (const feature of featuresToStart) {
this.startFeatureAsync(feature, projectPath, sendToRenderer);
}
} catch (error) {
console.error("[AutoMode] Error checking/starting features:", error);
}
}
/**
* Start a feature asynchronously (similar to drag operation)
*/
async startFeatureAsync(feature, projectPath, sendToRenderer) {
const featureId = feature.id;
// Skip if already running
if (this.runningFeatures.has(featureId)) {
console.log(`[AutoMode] Feature ${featureId} already running, skipping`);
return;
}
try {
console.log(
`[AutoMode] Starting feature: ${feature.description.slice(0, 50)}...`
);
// Register this feature as running
const execution = this.createExecutionContext(featureId);
execution.projectPath = projectPath;
execution.sendToRenderer = sendToRenderer;
this.runningFeatures.set(featureId, execution);
// Setup worktree for isolated work
const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer);
execution.worktreePath = worktreeSetup.workPath;
execution.branchName = worktreeSetup.branchName;
// Determine working path (worktree or main project)
const workPath = worktreeSetup.workPath;
// Update status to in_progress with timestamp
await featureLoader.updateFeatureStatus(
featureId,
"in_progress",
projectPath
);
sendToRenderer({
type: "auto_mode_feature_start",
featureId: feature.id,
feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName },
});
// Implement the feature (agent works in worktree)
const result = await featureExecutor.implementFeature(
feature,
workPath, // Use worktree path instead of main project
sendToRenderer,
execution
);
// Update feature status based on result
// For skipTests features, go to waiting_approval on success instead of verified
// On failure, skipTests features should also go to waiting_approval for user review
let newStatus;
if (result.passes) {
newStatus = feature.skipTests ? "waiting_approval" : "verified";
} else {
// For skipTests features, keep in waiting_approval so user can review
// For normal TDD features, move to backlog for retry
newStatus = feature.skipTests ? "waiting_approval" : "backlog";
}
await featureLoader.updateFeatureStatus(
feature.id,
newStatus,
projectPath
);
// Keep context file for viewing output later (deleted only when card is removed)
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: feature.id,
passes: result.passes,
message: result.message,
});
} catch (error) {
console.error(`[AutoMode] Error running feature ${featureId}:`, error);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
} finally {
// Clean up this feature's execution
this.runningFeatures.delete(featureId);
}
}
/**
* 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);
}
}
/**
* 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
);
// Keep context file for viewing output later (deleted only when card is removed)
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);
// Write error to context file
try {
await contextManager.writeToContextFile(
projectPath,
featureId,
`\n\n❌ ERROR: ${error.message}\n\n${error.stack || ''}\n`
);
} catch (contextError) {
console.error("[AutoMode] Failed to write error to context:", contextError);
}
// Update feature status to waiting_approval so user can review the error
try {
await featureLoader.updateFeatureStatus(
featureId,
"waiting_approval",
projectPath,
null, // no summary
error.message // pass error message
);
} catch (statusError) {
console.error("[AutoMode] Failed to update feature status after error:", statusError);
}
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
await featureExecutor.commitChangesOnly(
feature,
projectPath,
sendToRenderer,
execution
);
// Update status to verified
await featureLoader.updateFeatureStatus(
featureId,
"verified",
projectPath
);
// Keep context file for viewing output later (deleted only when card is removed)
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
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Revert feature changes by removing the worktree
* This effectively discards all changes made by the agent
*/
async revertFeature({ projectPath, featureId, sendToRenderer }) {
console.log(`[AutoMode] Reverting feature: ${featureId}`);
try {
// Stop the feature if it's running
if (this.runningFeatures.has(featureId)) {
await this.stopFeature({ featureId });
}
// Remove the worktree and delete the branch
const result = await worktreeManager.removeWorktree(projectPath, featureId, true);
if (!result.success) {
throw new Error(result.error || "Failed to remove worktree");
}
// Clear worktree info from feature
await featureLoader.updateFeatureWorktree(featureId, projectPath, null, null);
// Update feature status back to backlog
await featureLoader.updateFeatureStatus(featureId, "backlog", projectPath);
// Delete context file
await contextManager.deleteContextFile(projectPath, featureId);
if (sendToRenderer) {
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: featureId,
passes: false,
message: "Feature reverted - all changes discarded",
});
}
console.log(`[AutoMode] Feature ${featureId} reverted successfully`);
return { success: true, removedPath: result.removedPath };
} catch (error) {
console.error("[AutoMode] Error reverting feature:", error);
if (sendToRenderer) {
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
}
return { success: false, error: error.message };
}
}
/**
* Merge feature worktree changes back to main branch
*/
async mergeFeature({ projectPath, featureId, options = {}, sendToRenderer }) {
console.log(`[AutoMode] Merging feature: ${featureId}`);
try {
// Load feature to get worktree info
const features = await featureLoader.loadFeatures(projectPath);
const feature = features.find((f) => f.id === featureId);
if (!feature) {
throw new Error(`Feature ${featureId} not found`);
}
if (sendToRenderer) {
sendToRenderer({
type: "auto_mode_progress",
featureId: featureId,
content: "Merging feature branch into main...\n",
});
}
// Merge the worktree
const result = await worktreeManager.mergeWorktree(projectPath, featureId, {
...options,
cleanup: true, // Remove worktree after successful merge
});
if (!result.success) {
throw new Error(result.error || "Failed to merge worktree");
}
// Clear worktree info from feature
await featureLoader.updateFeatureWorktree(featureId, projectPath, null, null);
// Update feature status to verified
await featureLoader.updateFeatureStatus(featureId, "verified", projectPath);
if (sendToRenderer) {
sendToRenderer({
type: "auto_mode_feature_complete",
featureId: featureId,
passes: true,
message: `Feature merged into ${result.intoBranch}`,
});
}
console.log(`[AutoMode] Feature ${featureId} merged successfully`);
return { success: true, mergedBranch: result.mergedBranch };
} catch (error) {
console.error("[AutoMode] Error merging feature:", error);
if (sendToRenderer) {
sendToRenderer({
type: "auto_mode_error",
error: error.message,
featureId: featureId,
});
}
return { success: false, error: error.message };
}
}
/**
* Get worktree info for a feature
*/
async getWorktreeInfo({ projectPath, featureId }) {
return await worktreeManager.getWorktreeInfo(projectPath, featureId);
}
/**
* Get worktree status (changed files, commits, etc.)
*/
async getWorktreeStatus({ projectPath, featureId }) {
const worktreeInfo = await worktreeManager.getWorktreeInfo(projectPath, featureId);
if (!worktreeInfo.success) {
return { success: false, error: "Worktree not found" };
}
return await worktreeManager.getWorktreeStatus(worktreeInfo.worktreePath);
}
/**
* List all feature worktrees
*/
async listWorktrees({ projectPath }) {
const worktrees = await worktreeManager.getAllFeatureWorktrees(projectPath);
return { success: true, worktrees };
}
/**
* Get file diffs for a feature worktree
*/
async getFileDiffs({ projectPath, featureId }) {
const worktreeInfo = await worktreeManager.getWorktreeInfo(projectPath, featureId);
if (!worktreeInfo.success) {
return { success: false, error: "Worktree not found" };
}
return await worktreeManager.getFileDiffs(worktreeInfo.worktreePath);
}
/**
* Get diff for a specific file in a feature worktree
*/
async getFileDiff({ projectPath, featureId, filePath }) {
const worktreeInfo = await worktreeManager.getWorktreeInfo(projectPath, featureId);
if (!worktreeInfo.success) {
return { success: false, error: "Worktree not found" };
}
return await worktreeManager.getFileDiff(worktreeInfo.worktreePath, filePath);
}
}
// Export singleton instance
module.exports = new AutoModeService();