Files
automaker/app/electron/services/feature-executor.js
Kacper 179eef6685 feat(electron): enhance feature execution with async prompt handling and authentication checks
- Updated the FeatureExecutor to wrap content blocks in an async generator for multimodal prompt compatibility.
- Added authentication checks in the ClaudeProvider to ensure proper API key or token availability, including loading from local CLI config.
- Improved error handling for missing authentication, providing clear console messages for user guidance.

This update enhances the robustness of feature execution and ensures proper authentication management for the Claude SDK.
2025-12-10 04:22:59 +01:00

1023 lines
36 KiB
JavaScript

const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
const promptBuilder = require("./prompt-builder");
const contextManager = require("./context-manager");
const featureLoader = require("./feature-loader");
const mcpServerFactory = require("./mcp-server-factory");
const { ModelRegistry } = require("./model-registry");
const { ModelProviderFactory } = require("./model-provider");
// Model name mappings for Claude (legacy - kept for backwards compatibility)
const MODEL_MAP = {
haiku: "claude-haiku-4-5",
sonnet: "claude-sonnet-4-20250514",
opus: "claude-opus-4-5-20251101",
};
// Thinking level to budget_tokens mapping
// These values control how much "thinking time" the model gets for extended thinking
const THINKING_BUDGET_MAP = {
none: null, // No extended thinking
low: 4096, // Light thinking
medium: 16384, // Moderate thinking
high: 65536, // Deep thinking
ultrathink: 262144, // Ultra-deep thinking (maximum reasoning)
};
/**
* Feature Executor - Handles feature implementation using Claude Agent SDK
* Now supports multiple model providers (Claude, Codex/OpenAI)
*/
class FeatureExecutor {
/**
* Get the model string based on feature's model setting
* Supports both Claude and Codex/OpenAI models
*/
getModelString(feature) {
const modelKey = feature.model || "opus"; // Default to opus
// First check if this is a Codex model - they use the model key directly as the string
if (ModelRegistry.isCodexModel(modelKey)) {
const model = ModelRegistry.getModel(modelKey);
if (model && model.modelString) {
console.log(`[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${model.modelString} (Codex model)`);
return model.modelString;
}
// If model exists in registry but somehow no modelString, use the key itself
console.log(`[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${modelKey} (Codex fallback)`);
return modelKey;
}
// For Claude models, use the registry lookup
let modelString = ModelRegistry.getModelString(modelKey);
// Fallback to MODEL_MAP if registry doesn't have it (legacy support)
if (!modelString) {
modelString = MODEL_MAP[modelKey];
}
// Final fallback to opus for Claude models only
if (!modelString) {
modelString = MODEL_MAP.opus;
}
// Validate model string format - ensure it's not incorrectly constructed
// Prevent incorrect formats like "claude-haiku-4-20250514" (mixing haiku with sonnet date)
if (modelString.includes('haiku') && modelString.includes('20250514')) {
console.error(`[FeatureExecutor] Invalid model string detected: ${modelString}, using correct format`);
modelString = MODEL_MAP.haiku || 'claude-haiku-4-5';
}
console.log(`[FeatureExecutor] getModelString: modelKey=${modelKey}, modelString=${modelString}`);
return modelString;
}
/**
* Determine if the feature uses a Codex/OpenAI model
*/
isCodexModel(feature) {
const modelKey = feature.model || "opus";
return ModelRegistry.isCodexModel(modelKey);
}
/**
* Get the appropriate provider for the feature's model
*/
getProvider(feature) {
const modelKey = feature.model || "opus";
return ModelProviderFactory.getProviderForModel(modelKey);
}
/**
* Get thinking configuration based on feature's thinkingLevel
*/
getThinkingConfig(feature) {
const modelId = feature.model || "opus";
// Skip thinking config for models that don't support it (e.g., Codex CLI)
if (!ModelRegistry.modelSupportsThinking(modelId)) {
return null;
}
const level = feature.thinkingLevel || "none";
const budgetTokens = THINKING_BUDGET_MAP[level];
if (budgetTokens === null) {
return null; // No extended thinking
}
return {
type: "enabled",
budget_tokens: budgetTokens,
};
}
/**
* Prepare for ultrathink execution - validate and warn
*/
prepareForUltrathink(feature, thinkingConfig) {
if (feature.thinkingLevel !== 'ultrathink') {
return { ready: true };
}
const warnings = [];
const recommendations = [];
// Check CLI installation
const claudeCliDetector = require('./claude-cli-detector');
const cliInfo = claudeCliDetector.getInstallationInfo();
if (cliInfo.status === 'not_installed') {
warnings.push('Claude Code CLI not detected - ultrathink may have timeout issues');
recommendations.push('Install Claude Code CLI for optimal ultrathink performance');
}
// Validate budget tokens
if (thinkingConfig && thinkingConfig.budget_tokens > 32000) {
warnings.push(`Ultrathink budget (${thinkingConfig.budget_tokens} tokens) exceeds recommended 32K - may cause long-running requests`);
recommendations.push('Consider using batch processing for budgets above 32K');
}
// Cost estimate (rough)
const estimatedCost = (thinkingConfig?.budget_tokens || 0) / 1000 * 0.015; // Rough estimate
if (estimatedCost > 1.0) {
warnings.push(`Estimated cost: ~$${estimatedCost.toFixed(2)} per execution`);
}
// Time estimate
warnings.push('Ultrathink tasks typically take 45-180 seconds');
return {
ready: true,
warnings,
recommendations,
estimatedCost,
estimatedTime: '45-180 seconds',
cliInfo
};
}
/**
* Sleep helper
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Implement a single feature using Claude Agent SDK
* Uses a Plan-Act-Verify loop with detailed phase logging
*/
async implementFeature(feature, projectPath, sendToRenderer, execution) {
console.log(`[FeatureExecutor] Implementing: ${feature.description}`);
// Declare variables outside try block so they're available in catch
let modelString;
let providerName;
let isCodex;
try {
// ========================================
// PHASE 1: PLANNING
// ========================================
const planningMessage = `📋 Planning implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, planningMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "planning",
message: `Planning implementation for: ${feature.description}`,
});
console.log(`[FeatureExecutor] Phase: PLANNING for ${feature.description}`);
const abortController = new AbortController();
execution.abortController = abortController;
// Create custom MCP server with UpdateFeatureStatus tool
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
featureLoader.updateFeatureStatus.bind(featureLoader),
projectPath
);
// Ensure feature has a model set (for backward compatibility with old features)
if (!feature.model) {
console.warn(`[FeatureExecutor] Feature ${feature.id} missing model property, defaulting to 'opus'`);
feature.model = "opus";
}
// Get model and thinking configuration from feature settings
const modelString = this.getModelString(feature);
const thinkingConfig = this.getThinkingConfig(feature);
// Prepare for ultrathink if needed
if (feature.thinkingLevel === 'ultrathink') {
const preparation = this.prepareForUltrathink(feature, thinkingConfig);
console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation);
// Log warnings
if (preparation.warnings && preparation.warnings.length > 0) {
preparation.warnings.forEach(warning => {
console.warn(`[FeatureExecutor] ⚠️ ${warning}`);
});
}
// Send preparation info to renderer
sendToRenderer({
type: 'auto_mode_ultrathink_preparation',
featureId: feature.id,
warnings: preparation.warnings || [],
recommendations: preparation.recommendations || [],
estimatedCost: preparation.estimatedCost,
estimatedTime: preparation.estimatedTime
});
}
providerName = this.isCodexModel(feature) ? 'Codex/OpenAI' : 'Claude';
console.log(`[FeatureExecutor] Using provider: ${providerName}, model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`);
// Note: Claude Agent SDK handles authentication automatically - it can use:
// 1. CLAUDE_CODE_OAUTH_TOKEN env var (for SDK mode)
// 2. Claude CLI's own authentication (if CLI is installed)
// 3. ANTHROPIC_API_KEY (fallback)
// We don't need to validate here - let the SDK/CLI handle auth errors
// Configure options for the SDK query
const options = {
model: modelString,
systemPrompt: promptBuilder.getCodingPrompt(),
maxTurns: 1000,
cwd: projectPath,
mcpServers: {
"automaker-tools": featureToolsServer
},
allowedTools: [
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
"WebSearch",
"WebFetch",
"mcp__automaker-tools__UpdateFeatureStatus",
],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
// Add thinking configuration if enabled
if (thinkingConfig) {
options.thinking = thinkingConfig;
}
// Build the prompt for this specific feature
let prompt = promptBuilder.buildFeaturePrompt(feature);
// Add images to prompt if feature has imagePaths
if (feature.imagePaths && feature.imagePaths.length > 0) {
const contentBlocks = [];
// Add text block
contentBlocks.push({
type: "text",
text: prompt,
});
// Add image blocks
const fs = require("fs");
const path = require("path");
for (const imagePathObj of feature.imagePaths) {
try {
const imagePath = imagePathObj.path;
const imageBuffer = fs.readFileSync(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || imagePathObj.mimeType || "image/png";
contentBlocks.push({
type: "image",
source: {
type: "base64",
media_type: mediaType,
data: base64Data,
},
});
console.log(`[FeatureExecutor] Added image to prompt: ${imagePath}`);
} catch (error) {
console.error(
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
error
);
}
}
// Wrap content blocks in async generator for SDK (required format for multimodal prompts)
prompt = (async function* () {
yield {
type: "user",
session_id: "",
message: {
role: "user",
content: contentBlocks,
},
parent_tool_use_id: null,
};
})();
}
// Planning: Analyze the codebase and create implementation plan
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 contextManager.writeToContextFile(projectPath, feature.id, actionMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "action",
message: `Executing implementation for: ${feature.description}`,
});
console.log(`[FeatureExecutor] Phase: ACTION for ${feature.description}`);
// Send query - use appropriate provider based on model
let currentQuery;
isCodex = this.isCodexModel(feature);
// Ensure provider auth is available (especially for Claude SDK)
const provider = this.getProvider(feature);
if (provider?.ensureAuthEnv && !provider.ensureAuthEnv()) {
const authMsg =
"Missing Anthropic auth. Set ANTHROPIC_API_KEY or run `claude login` so ~/.claude/config.json contains oauth_token.";
console.error(`[FeatureExecutor] ${authMsg}`);
throw new Error(authMsg);
}
// Validate that model string matches the provider
if (isCodex) {
// Ensure model string is actually a Codex model, not a Claude model
if (modelString.startsWith('claude-')) {
console.error(`[FeatureExecutor] ERROR: Codex provider selected but Claude model string detected: ${modelString}`);
console.error(`[FeatureExecutor] Feature model: ${feature.model || 'not set'}, modelString: ${modelString}`);
throw new Error(`Invalid model configuration: Codex provider cannot use Claude model '${modelString}'. Please check feature model setting.`);
}
// Use Codex provider for OpenAI models
console.log(`[FeatureExecutor] Using Codex provider for model: ${modelString}`);
currentQuery = provider.executeQuery({
prompt,
model: modelString,
cwd: projectPath,
systemPrompt: promptBuilder.getCodingPrompt(),
maxTurns: 20, // Codex CLI typically uses fewer turns
allowedTools: options.allowedTools,
abortController: abortController,
env: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY
}
});
} else {
// Ensure model string is actually a Claude model, not a Codex model
if (!modelString.startsWith('claude-') && !modelString.match(/^(gpt-|o\d)/)) {
console.warn(`[FeatureExecutor] WARNING: Claude provider selected but unexpected model string: ${modelString}`);
}
// Use Claude SDK (original implementation)
currentQuery = query({ prompt, options });
}
execution.query = currentQuery;
// Stream responses
let responseText = "";
let hasStartedToolUse = false;
for await (const msg of currentQuery) {
// Check if this specific feature was aborted
if (!execution.isActive()) break;
// Handle error messages
if (msg.type === "error") {
const errorMsg = `\n❌ Error: ${msg.error}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, errorMsg);
sendToRenderer({
type: "auto_mode_error",
featureId: feature.id,
error: msg.error,
});
throw new Error(msg.error);
}
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 contextManager.writeToContextFile(projectPath, feature.id, block.text);
// Stream progress to renderer
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: block.text,
});
} else if (block.type === "thinking") {
// Handle thinking output from Codex O-series models
const thinkingMsg = `\n💭 Thinking: ${block.thinking?.substring(0, 200)}...\n`;
await contextManager.writeToContextFile(projectPath, feature.id, thinkingMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: thinkingMsg,
});
} 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 contextManager.writeToContextFile(projectPath, feature.id, startMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: startMsg,
});
}
// Write tool use to context file
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, toolMsg);
// Notify about tool use
sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
// ========================================
// PHASE 3: VERIFICATION
// ========================================
const verificationMessage = `✅ Verifying implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, verificationMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "verification",
message: `Verifying implementation for: ${feature.description}`,
});
console.log(`[FeatureExecutor] Phase: VERIFICATION for ${feature.description}`);
const checkingMsg =
"Verifying implementation and checking test results...\n";
await contextManager.writeToContextFile(projectPath, feature.id, checkingMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: checkingMsg,
});
// Re-load features to check if it was marked as verified or waiting_approval (for skipTests)
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
// For skipTests features, waiting_approval is also considered a success
const passes = updatedFeature?.status === "verified" ||
(updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval");
// Send verification result
const resultMsg = passes
? "✓ Verification successful: All tests passed\n"
: "✗ Verification: Tests need attention\n";
await contextManager.writeToContextFile(projectPath, feature.id, resultMsg);
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("[FeatureExecutor] Feature run aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
passes: false,
message: "Auto mode aborted",
};
}
console.error("[FeatureExecutor] Error implementing feature:", error);
// Safely get model info for error logging (may not be set if error occurred early)
const modelInfo = modelString ? {
message: error.message,
stack: error.stack,
name: error.name,
code: error.code,
model: modelString,
provider: providerName || 'unknown',
isCodex: isCodex !== undefined ? isCodex : 'unknown'
} : {
message: error.message,
stack: error.stack,
name: error.name,
code: error.code,
model: 'not initialized',
provider: 'unknown',
isCodex: 'unknown'
};
console.error("[FeatureExecutor] Error details:", modelInfo);
// Check if this is a Claude CLI process error
if (error.message && error.message.includes("process exited with code")) {
const modelDisplay = modelString ? `Model: ${modelString}` : 'Model: not initialized';
const errorMsg = `Claude Code CLI failed with exit code 1. This might be due to:\n` +
`- Invalid or unsupported model (${modelDisplay})\n` +
`- Missing or invalid CLAUDE_CODE_OAUTH_TOKEN\n` +
`- Claude CLI configuration issue\n` +
`- Model not available in your Claude account\n\n` +
`Original error: ${error.message}`;
await contextManager.writeToContextFile(projectPath, feature.id, `\n${errorMsg}\n`);
sendToRenderer({
type: "auto_mode_error",
featureId: feature.id,
error: errorMsg,
});
}
// Clean up
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
/**
* Resume feature implementation with previous context
*/
async resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution) {
console.log(`[FeatureExecutor] Resuming with context for: ${feature.description}`);
try {
const resumeMessage = `\n🔄 Resuming implementation for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, resumeMessage);
sendToRenderer({
type: "auto_mode_phase",
featureId: feature.id,
phase: "action",
message: `Resuming implementation for: ${feature.description}`,
});
const abortController = new AbortController();
execution.abortController = abortController;
// Create custom MCP server with UpdateFeatureStatus tool
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
featureLoader.updateFeatureStatus.bind(featureLoader),
projectPath
);
// Ensure feature has a model set (for backward compatibility with old features)
if (!feature.model) {
console.warn(`[FeatureExecutor] Feature ${feature.id} missing model property, defaulting to 'opus'`);
feature.model = "opus";
}
// Get model and thinking configuration from feature settings
const modelString = this.getModelString(feature);
const thinkingConfig = this.getThinkingConfig(feature);
// Prepare for ultrathink if needed
if (feature.thinkingLevel === 'ultrathink') {
const preparation = this.prepareForUltrathink(feature, thinkingConfig);
console.log(`[FeatureExecutor] Ultrathink preparation:`, preparation);
// Log warnings
if (preparation.warnings && preparation.warnings.length > 0) {
preparation.warnings.forEach(warning => {
console.warn(`[FeatureExecutor] ⚠️ ${warning}`);
});
}
// Send preparation info to renderer
sendToRenderer({
type: 'auto_mode_ultrathink_preparation',
featureId: feature.id,
warnings: preparation.warnings || [],
recommendations: preparation.recommendations || [],
estimatedCost: preparation.estimatedCost,
estimatedTime: preparation.estimatedTime
});
}
const isCodex = this.isCodexModel(feature);
const providerName = isCodex ? 'Codex/OpenAI' : 'Claude';
console.log(`[FeatureExecutor] Resuming with provider: ${providerName}, model: ${modelString}, thinking: ${feature.thinkingLevel || 'none'}`);
const options = {
model: modelString,
systemPrompt: promptBuilder.getVerificationPrompt(),
maxTurns: 1000,
cwd: projectPath,
mcpServers: {
"automaker-tools": featureToolsServer
},
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "WebSearch", "WebFetch", "mcp__automaker-tools__UpdateFeatureStatus"],
permissionMode: "acceptEdits",
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
abortController: abortController,
};
// Add thinking configuration if enabled
if (thinkingConfig) {
options.thinking = thinkingConfig;
}
// Build prompt with previous context
let prompt = promptBuilder.buildResumePrompt(feature, previousContext);
// Add images to prompt if feature has imagePaths or followUpImages
const imagePaths = feature.followUpImages || feature.imagePaths;
if (imagePaths && imagePaths.length > 0) {
const contentBlocks = [];
// Add text block
contentBlocks.push({
type: "text",
text: prompt,
});
// Add image blocks
const fs = require("fs");
const path = require("path");
for (const imagePathObj of imagePaths) {
try {
const imagePath = imagePathObj.path;
const imageBuffer = fs.readFileSync(imagePath);
const base64Data = imageBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypeMap = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
};
const mediaType = mimeTypeMap[ext] || imagePathObj.mimeType || "image/png";
contentBlocks.push({
type: "image",
source: {
type: "base64",
media_type: mediaType,
data: base64Data,
},
});
console.log(`[FeatureExecutor] Added image to resume prompt: ${imagePath}`);
} catch (error) {
console.error(
`[FeatureExecutor] Failed to load image ${imagePathObj.path}:`,
error
);
}
}
// Wrap content blocks in async generator for SDK (required format for multimodal prompts)
prompt = (async function* () {
yield {
type: "user",
session_id: "",
message: {
role: "user",
content: contentBlocks,
},
parent_tool_use_id: null,
};
})();
}
// Use appropriate provider based on model type
let currentQuery;
if (isCodex) {
// Validate that model string is actually a Codex model
if (modelString.startsWith('claude-')) {
console.error(`[FeatureExecutor] ERROR: Codex provider selected but Claude model string detected: ${modelString}`);
throw new Error(`Invalid model configuration: Codex provider cannot use Claude model '${modelString}'. Please check feature model setting.`);
}
console.log(`[FeatureExecutor] Using Codex provider for resume with model: ${modelString}`);
const provider = this.getProvider(feature);
currentQuery = provider.executeQuery({
prompt,
model: modelString,
cwd: projectPath,
systemPrompt: promptBuilder.getVerificationPrompt(),
maxTurns: 20,
allowedTools: options.allowedTools,
abortController: abortController,
env: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY
}
});
} else {
// Use Claude SDK
currentQuery = query({ prompt, options });
}
execution.query = currentQuery;
let responseText = "";
for await (const msg of currentQuery) {
// Check if this specific feature was aborted
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
await contextManager.writeToContextFile(projectPath, feature.id, block.text);
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 contextManager.writeToContextFile(projectPath, feature.id, toolMsg);
sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
// Check if feature was marked as verified or waiting_approval (for skipTests)
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
// For skipTests features, waiting_approval is also considered a success
const passes = updatedFeature?.status === "verified" ||
(updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval");
const finalMsg = passes
? "✓ Feature successfully verified and completed\n"
: "⚠ Feature still in progress - may need additional work\n";
await contextManager.writeToContextFile(projectPath, feature.id, finalMsg);
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("[FeatureExecutor] Resume aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
passes: false,
message: "Resume aborted",
};
}
console.error("[FeatureExecutor] Error resuming feature:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
/**
* Commit changes for a feature without doing additional work
* Analyzes changes and creates a proper conventional commit message
*/
async commitChangesOnly(feature, projectPath, sendToRenderer, execution) {
console.log(`[FeatureExecutor] Committing changes for: ${feature.description}`);
try {
const commitMessage = `\n📝 Committing changes for: ${feature.description}\n`;
await contextManager.writeToContextFile(projectPath, feature.id, commitMessage);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: "Analyzing changes and creating commit...",
});
const abortController = new AbortController();
execution.abortController = abortController;
// Create custom MCP server with UpdateFeatureStatus tool
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
featureLoader.updateFeatureStatus.bind(featureLoader),
projectPath
);
const options = {
model: "claude-sonnet-4-20250514", // Use sonnet for commit task
systemPrompt: `You are a git commit assistant that creates professional conventional commit messages.
IMPORTANT RULES:
- DO NOT modify any code
- DO NOT write tests
- DO NOT do anything except analyzing changes and committing them
- Use the git command line tools via Bash
- Create proper conventional commit messages based on what was actually changed`,
maxTurns: 15, // Allow some turns to analyze and commit
cwd: projectPath,
mcpServers: {
"automaker-tools": featureToolsServer
},
allowedTools: ["Bash", "mcp__automaker-tools__UpdateFeatureStatus"],
permissionMode: "acceptEdits",
sandbox: {
enabled: false, // Need to run git commands
},
abortController: abortController,
};
// Prompt that guides the agent to create a proper conventional commit
const prompt = `Please commit the current changes with a proper conventional commit message.
**Feature Context:**
Category: ${feature.category}
Description: ${feature.description}
**Your Task:**
1. First, run \`git status\` to see all untracked and modified files
2. Run \`git diff\` to see the actual changes (both staged and unstaged)
3. Run \`git log --oneline -5\` to see recent commit message styles in this repo
4. Analyze all the changes and draft a proper conventional commit message:
- Use conventional commit format: \`type(scope): description\`
- Types: feat, fix, refactor, style, docs, test, chore
- The description should be concise (under 72 chars) and focus on "what" was done
- Summarize the nature of the changes (new feature, enhancement, bug fix, etc.)
- Make sure the commit message accurately reflects the actual code changes
5. Run \`git add .\` to stage all changes
6. Create the commit with a message ending with:
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
Use a HEREDOC for the commit message to ensure proper formatting:
\`\`\`bash
git commit -m "$(cat <<'EOF'
type(scope): Short description here
Optional longer description if needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
EOF
)"
\`\`\`
**IMPORTANT:**
- DO NOT use the feature description verbatim as the commit message
- Analyze the actual code changes to determine the appropriate commit message
- The commit message should be professional and follow conventional commit standards
- DO NOT modify any code or run tests - ONLY commit the existing changes`;
const currentQuery = query({ prompt, options });
execution.query = currentQuery;
let responseText = "";
for await (const msg of currentQuery) {
if (!execution.isActive()) break;
if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
await contextManager.writeToContextFile(projectPath, feature.id, block.text);
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 contextManager.writeToContextFile(projectPath, feature.id, toolMsg);
sendToRenderer({
type: "auto_mode_tool",
featureId: feature.id,
tool: block.name,
input: block.input,
});
}
}
}
}
execution.query = null;
execution.abortController = null;
const finalMsg = "✓ Changes committed successfully\n";
await contextManager.writeToContextFile(projectPath, feature.id, finalMsg);
sendToRenderer({
type: "auto_mode_progress",
featureId: feature.id,
content: finalMsg,
});
return {
passes: true,
message: responseText.substring(0, 500),
};
} catch (error) {
if (error instanceof AbortError || error?.name === "AbortError") {
console.log("[FeatureExecutor] Commit aborted");
if (execution) {
execution.abortController = null;
execution.query = null;
}
return {
passes: false,
message: "Commit aborted",
};
}
console.error("[FeatureExecutor] Error committing feature:", error);
if (execution) {
execution.abortController = null;
execution.query = null;
}
throw error;
}
}
}
module.exports = new FeatureExecutor();