feat: enhance project initialization and improve logging in auto mode service

- Added a default categories.json file to the project initialization structure.
- Improved code formatting and readability in the auto-mode-service.ts file by restructuring console log statements and method calls.
- Updated feature status checks to include "backlog" in addition to "pending" and "ready".
This commit is contained in:
Cody Seibert
2025-12-14 00:43:52 -05:00
parent 58f466b443
commit b52b9ba236
8 changed files with 311 additions and 112 deletions

View File

@@ -19,6 +19,7 @@ import { cn } from "@/lib/utils";
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from "@/lib/electron";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { useFileBrowser } from "@/contexts/file-browser-context"; import { useFileBrowser } from "@/contexts/file-browser-context";
import { toast } from "sonner";
interface InterviewMessage { interface InterviewMessage {
id: string; id: string;
@@ -290,7 +291,8 @@ export function InterviewView() {
const handleSelectDirectory = async () => { const handleSelectDirectory = async () => {
const selectedPath = await openFileBrowser({ const selectedPath = await openFileBrowser({
title: "Select Base Directory", title: "Select Base Directory",
description: "Choose the parent directory where your new project will be created", description:
"Choose the parent directory where your new project will be created",
}); });
if (selectedPath) { if (selectedPath) {
@@ -306,12 +308,23 @@ export function InterviewView() {
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
// Use platform-specific path separator // Use platform-specific path separator
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ? const pathSep =
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/'; typeof window !== "undefined" && (window as any).electronAPI
? navigator.platform.indexOf("Win") !== -1
? "\\"
: "/"
: "/";
const fullProjectPath = `${projectPath}${pathSep}${projectName}`; const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
// Create project directory // Create project directory
await api.mkdir(fullProjectPath); const mkdirResult = await api.mkdir(fullProjectPath);
if (!mkdirResult.success) {
toast.error("Failed to create project directory", {
description: mkdirResult.error || "Unknown error occurred",
});
setIsGenerating(false);
return;
}
// Write app_spec.txt with generated content // Write app_spec.txt with generated content
await api.writeFile( await api.writeFile(

View File

@@ -236,7 +236,13 @@ export function WelcomeView() {
const projectPath = `${parentDir}/${projectName}`; const projectPath = `${parentDir}/${projectName}`;
// Create project directory // Create project directory
await api.mkdir(projectPath); const mkdirResult = await api.mkdir(projectPath);
if (!mkdirResult.success) {
toast.error("Failed to create project directory", {
description: mkdirResult.error || "Unknown error occurred",
});
return;
}
// Initialize .automaker directory with all necessary files // Initialize .automaker directory with all necessary files
const initResult = await initializeProject(projectPath); const initResult = await initializeProject(projectPath);

View File

@@ -33,16 +33,21 @@ export function useAutoMode() {
); );
// Helper to look up project ID from path // Helper to look up project ID from path
const getProjectIdFromPath = useCallback((path: string): string | undefined => { const getProjectIdFromPath = useCallback(
const project = projects.find(p => p.path === path); (path: string): string | undefined => {
return project?.id; const project = projects.find((p) => p.path === path);
}, [projects]); return project?.id;
},
[projects]
);
// Get project-specific auto mode state // Get project-specific auto mode state
const projectId = currentProject?.id; const projectId = currentProject?.id;
const projectAutoModeState = useMemo(() => { const projectAutoModeState = useMemo(() => {
if (!projectId) return { isRunning: false, runningTasks: [] }; if (!projectId) return { isRunning: false, runningTasks: [] };
return autoModeByProject[projectId] || { isRunning: false, runningTasks: [] }; return (
autoModeByProject[projectId] || { isRunning: false, runningTasks: [] }
);
}, [autoModeByProject, projectId]); }, [autoModeByProject, projectId]);
const isAutoModeRunning = projectAutoModeState.isRunning; const isAutoModeRunning = projectAutoModeState.isRunning;
@@ -62,10 +67,10 @@ export function useAutoMode() {
// Events include projectPath from backend - use it to look up project ID // Events include projectPath from backend - use it to look up project ID
// Fall back to current projectId if not provided in event // Fall back to current projectId if not provided in event
let eventProjectId: string | undefined; let eventProjectId: string | undefined;
if ('projectPath' in event && event.projectPath) { if ("projectPath" in event && event.projectPath) {
eventProjectId = getProjectIdFromPath(event.projectPath); eventProjectId = getProjectIdFromPath(event.projectPath);
} }
if (!eventProjectId && 'projectId' in event && event.projectId) { if (!eventProjectId && "projectId" in event && event.projectId) {
eventProjectId = event.projectId; eventProjectId = event.projectId;
} }
if (!eventProjectId) { if (!eventProjectId) {
@@ -74,7 +79,10 @@ export function useAutoMode() {
// Skip event if we couldn't determine the project // Skip event if we couldn't determine the project
if (!eventProjectId) { if (!eventProjectId) {
console.warn("[AutoMode] Could not determine project for event:", event); console.warn(
"[AutoMode] Could not determine project for event:",
event
);
return; return;
} }
@@ -111,20 +119,41 @@ export function useAutoMode() {
} }
break; break;
case "auto_mode_complete": case "auto_mode_stopped":
// All features completed for this project // Auto mode was explicitly stopped (by user or error)
setAutoModeRunning(eventProjectId, false); setAutoModeRunning(eventProjectId, false);
clearRunningTasks(eventProjectId); clearRunningTasks(eventProjectId);
console.log("[AutoMode] All features completed!"); console.log("[AutoMode] Auto mode stopped");
break;
case "auto_mode_started":
// Auto mode started - ensure UI reflects running state
console.log("[AutoMode] Auto mode started:", event.message);
break;
case "auto_mode_idle":
// Auto mode is running but has no pending features to pick up
// This is NOT a stop - auto mode keeps running and will pick up new features
console.log("[AutoMode] Auto mode idle - waiting for new features");
break;
case "auto_mode_complete":
// Legacy event - only handle if it looks like a stop (for backwards compatibility)
if (event.message === "Auto mode stopped") {
setAutoModeRunning(eventProjectId, false);
clearRunningTasks(eventProjectId);
console.log("[AutoMode] Auto mode stopped (legacy event)");
}
break; break;
case "auto_mode_error": case "auto_mode_error":
console.error("[AutoMode Error]", event.error); console.error("[AutoMode Error]", event.error);
if (event.featureId && event.error) { if (event.featureId && event.error) {
// Check for authentication errors and provide a more helpful message // Check for authentication errors and provide a more helpful message
const isAuthError = event.errorType === "authentication" || const isAuthError =
event.error.includes("Authentication failed") || event.errorType === "authentication" ||
event.error.includes("Invalid API key"); event.error.includes("Authentication failed") ||
event.error.includes("Invalid API key");
const errorMessage = isAuthError const errorMessage = isAuthError
? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.` ? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.`
@@ -202,11 +231,12 @@ export function useAutoMode() {
if (!api?.autoMode) return; if (!api?.autoMode) return;
// Find all projects that have auto mode marked as running // Find all projects that have auto mode marked as running
const projectsToRestart: Array<{ projectId: string; projectPath: string }> = []; const projectsToRestart: Array<{ projectId: string; projectPath: string }> =
[];
for (const [projectId, state] of Object.entries(autoModeByProject)) { for (const [projectId, state] of Object.entries(autoModeByProject)) {
if (state.isRunning) { if (state.isRunning) {
// Find the project path for this project ID // Find the project path for this project ID
const project = projects.find(p => p.id === projectId); const project = projects.find((p) => p.id === projectId);
if (project) { if (project) {
projectsToRestart.push({ projectId, projectPath: project.path }); projectsToRestart.push({ projectId, projectPath: project.path });
} }
@@ -216,18 +246,27 @@ export function useAutoMode() {
// Restart auto mode for each project // Restart auto mode for each project
for (const { projectId, projectPath } of projectsToRestart) { for (const { projectId, projectPath } of projectsToRestart) {
console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`); console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`);
api.autoMode.start(projectPath, maxConcurrency).then(result => { api.autoMode
if (!result.success) { .start(projectPath, maxConcurrency)
console.error(`[AutoMode] Failed to restore auto mode for ${projectPath}:`, result.error); .then((result) => {
// Mark as not running if we couldn't restart if (!result.success) {
console.error(
`[AutoMode] Failed to restore auto mode for ${projectPath}:`,
result.error
);
// Mark as not running if we couldn't restart
setAutoModeRunning(projectId, false);
} else {
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
}
})
.catch((error) => {
console.error(
`[AutoMode] Error restoring auto mode for ${projectPath}:`,
error
);
setAutoModeRunning(projectId, false); setAutoModeRunning(projectId, false);
} else { });
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
}
}).catch(error => {
console.error(`[AutoMode] Error restoring auto mode for ${projectPath}:`, error);
setAutoModeRunning(projectId, false);
});
} }
// Only run once on mount - intentionally empty dependency array // Only run once on mount - intentionally empty dependency array
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -246,11 +285,16 @@ export function useAutoMode() {
throw new Error("Auto mode API not available"); throw new Error("Auto mode API not available");
} }
const result = await api.autoMode.start(currentProject.path, maxConcurrency); const result = await api.autoMode.start(
currentProject.path,
maxConcurrency
);
if (result.success) { if (result.success) {
setAutoModeRunning(currentProject.id, true); setAutoModeRunning(currentProject.id, true);
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`); console.log(
`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`
);
} else { } else {
console.error("[AutoMode] Failed to start:", result.error); console.error("[AutoMode] Failed to start:", result.error);
throw new Error(result.error || "Failed to start auto mode"); throw new Error(result.error || "Failed to start auto mode");
@@ -285,7 +329,9 @@ export function useAutoMode() {
// Stopping auto mode only turns off the toggle to prevent new features // Stopping auto mode only turns off the toggle to prevent new features
// from being picked up. Running tasks will complete naturally and be // from being picked up. Running tasks will complete naturally and be
// removed via the auto_mode_feature_complete event. // removed via the auto_mode_feature_complete event.
console.log("[AutoMode] Stopped successfully - running tasks will continue"); console.log(
"[AutoMode] Stopped successfully - running tasks will continue"
);
} else { } else {
console.error("[AutoMode] Failed to stop:", result.error); console.error("[AutoMode] Failed to stop:", result.error);
throw new Error(result.error || "Failed to stop auto mode"); throw new Error(result.error || "Failed to stop auto mode");

View File

@@ -29,7 +29,9 @@ const REQUIRED_STRUCTURE: {
".automaker/features", ".automaker/features",
".automaker/images", ".automaker/images",
], ],
files: {}, files: {
".automaker/categories.json": "[]",
},
}; };
/** /**

View File

@@ -203,6 +203,24 @@ export type AutoModeEvent =
projectId?: string; projectId?: string;
projectPath?: string; projectPath?: string;
} }
| {
type: "auto_mode_stopped";
message: string;
projectId?: string;
projectPath?: string;
}
| {
type: "auto_mode_started";
message: string;
projectId?: string;
projectPath?: string;
}
| {
type: "auto_mode_idle";
message: string;
projectId?: string;
projectPath?: string;
}
| { | {
type: "auto_mode_phase"; type: "auto_mode_phase";
featureId: string; featureId: string;

View File

@@ -26,6 +26,12 @@ export function initAllowedPaths(): void {
if (dataDir) { if (dataDir) {
allowedPaths.add(path.resolve(dataDir)); allowedPaths.add(path.resolve(dataDir));
} }
// Always allow the workspace directory (where projects are created)
const workspaceDir = process.env.WORKSPACE_DIR;
if (workspaceDir) {
allowedPaths.add(path.resolve(workspaceDir));
}
} }
/** /**
@@ -58,7 +64,9 @@ export function validatePath(filePath: string): string {
const resolved = path.resolve(filePath); const resolved = path.resolve(filePath);
if (!isPathAllowed(resolved)) { if (!isPathAllowed(resolved)) {
throw new Error(`Access denied: ${filePath} is not in an allowed directory`); throw new Error(
`Access denied: ${filePath} is not in an allowed directory`
);
} }
return resolved; return resolved;

View File

@@ -7,7 +7,11 @@ import { Router, type Request, type Response } from "express";
import fs from "fs/promises"; import fs from "fs/promises";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js"; import {
validatePath,
addAllowedPath,
isPathAllowed,
} from "../lib/security.js";
import type { EventEmitter } from "../lib/events.js"; import type { EventEmitter } from "../lib/events.js";
export function createFsRoutes(_events: EventEmitter): Router { export function createFsRoutes(_events: EventEmitter): Router {
@@ -69,9 +73,41 @@ export function createFsRoutes(_events: EventEmitter): Router {
return; return;
} }
const resolvedPath = validatePath(dirPath); const resolvedPath = path.resolve(dirPath);
// Security check: allow paths in allowed directories OR within home directory
const isAllowed = (() => {
// Check if path or parent is in allowed paths
if (isPathAllowed(resolvedPath)) return true;
const parentPath = path.dirname(resolvedPath);
if (isPathAllowed(parentPath)) return true;
// Also allow within home directory (like the /browse endpoint)
const homeDir = os.homedir();
const normalizedHome = path.normalize(homeDir);
if (
resolvedPath === normalizedHome ||
resolvedPath.startsWith(normalizedHome + path.sep)
) {
return true;
}
return false;
})();
if (!isAllowed) {
res.status(403).json({
success: false,
error: `Access denied: ${dirPath} is not in an allowed directory`,
});
return;
}
await fs.mkdir(resolvedPath, { recursive: true }); await fs.mkdir(resolvedPath, { recursive: true });
// Add the new directory to allowed paths so subsequent operations work
addAllowedPath(resolvedPath);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
@@ -197,7 +233,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
const stats = await fs.stat(resolvedPath); const stats = await fs.stat(resolvedPath);
if (!stats.isDirectory()) { if (!stats.isDirectory()) {
res.status(400).json({ success: false, error: "Path is not a directory" }); res
.status(400)
.json({ success: false, error: "Path is not a directory" });
return; return;
} }
@@ -229,7 +267,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
}; };
if (!directoryName) { if (!directoryName) {
res.status(400).json({ success: false, error: "directoryName is required" }); res
.status(400)
.json({ success: false, error: "directoryName is required" });
return; return;
} }
@@ -254,10 +294,16 @@ export function createFsRoutes(_events: EventEmitter): Router {
const searchPaths: string[] = [ const searchPaths: string[] = [
process.cwd(), // Current working directory process.cwd(), // Current working directory
process.env.HOME || process.env.USERPROFILE || "", // User home process.env.HOME || process.env.USERPROFILE || "", // User home
path.join(process.env.HOME || process.env.USERPROFILE || "", "Documents"), path.join(
process.env.HOME || process.env.USERPROFILE || "",
"Documents"
),
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"), path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
// Common project locations // Common project locations
path.join(process.env.HOME || process.env.USERPROFILE || "", "Projects"), path.join(
process.env.HOME || process.env.USERPROFILE || "",
"Projects"
),
].filter(Boolean); ].filter(Boolean);
// Also check parent of current working directory // Also check parent of current working directory
@@ -284,7 +330,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
// Remove directory name prefix from sample file path // Remove directory name prefix from sample file path
const relativeFile = sampleFile.startsWith(directoryName + "/") const relativeFile = sampleFile.startsWith(directoryName + "/")
? sampleFile.substring(directoryName.length + 1) ? sampleFile.substring(directoryName.length + 1)
: sampleFile.split("/").slice(1).join("/") || sampleFile.split("/").pop() || sampleFile; : sampleFile.split("/").slice(1).join("/") ||
sampleFile.split("/").pop() ||
sampleFile;
try { try {
const filePath = path.join(candidatePath, relativeFile); const filePath = path.join(candidatePath, relativeFile);
@@ -405,7 +453,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
const stats = await fs.stat(targetPath); const stats = await fs.stat(targetPath);
if (!stats.isDirectory()) { if (!stats.isDirectory()) {
res.status(400).json({ success: false, error: "Path is not a directory" }); res
.status(400)
.json({ success: false, error: "Path is not a directory" });
return; return;
} }
@@ -438,7 +488,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
} catch (error) { } catch (error) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: error instanceof Error ? error.message : "Failed to read directory", error:
error instanceof Error ? error.message : "Failed to read directory",
}); });
} }
} catch (error) { } catch (error) {
@@ -464,8 +515,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
const fullPath = path.isAbsolute(imagePath) const fullPath = path.isAbsolute(imagePath)
? imagePath ? imagePath
: projectPath : projectPath
? path.join(projectPath, imagePath) ? path.join(projectPath, imagePath)
: imagePath; : imagePath;
// Check if file exists // Check if file exists
try { try {
@@ -490,7 +541,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
".bmp": "image/bmp", ".bmp": "image/bmp",
}; };
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream"); res.setHeader(
"Content-Type",
mimeTypes[ext] || "application/octet-stream"
);
res.setHeader("Cache-Control", "public, max-age=3600"); res.setHeader("Cache-Control", "public, max-age=3600");
res.send(buffer); res.send(buffer);
} catch (error) { } catch (error) {
@@ -546,38 +600,42 @@ export function createFsRoutes(_events: EventEmitter): Router {
}); });
// Delete board background image // Delete board background image
router.post("/delete-board-background", async (req: Request, res: Response) => { router.post(
try { "/delete-board-background",
const { projectPath } = req.body as { projectPath: string }; async (req: Request, res: Response) => {
if (!projectPath) {
res.status(400).json({
success: false,
error: "projectPath is required",
});
return;
}
const boardDir = path.join(projectPath, ".automaker", "board");
try { try {
// Try to remove all files in the board directory const { projectPath } = req.body as { projectPath: string };
const files = await fs.readdir(boardDir);
for (const file of files) {
if (file.startsWith("background")) {
await fs.unlink(path.join(boardDir, file));
}
}
} catch {
// Directory may not exist, that's fine
}
res.json({ success: true }); if (!projectPath) {
} catch (error) { res.status(400).json({
const message = error instanceof Error ? error.message : "Unknown error"; success: false,
res.status(500).json({ success: false, error: message }); error: "projectPath is required",
});
return;
}
const boardDir = path.join(projectPath, ".automaker", "board");
try {
// Try to remove all files in the board directory
const files = await fs.readdir(boardDir);
for (const file of files) {
if (file.startsWith("background")) {
await fs.unlink(path.join(boardDir, file));
}
}
} catch {
// Directory may not exist, that's fine
}
res.json({ success: true });
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
} }
}); );
// Browse directories for file picker // Browse directories for file picker
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows // SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
@@ -614,7 +672,10 @@ export function createFsRoutes(_events: EventEmitter): Router {
const normalizedHome = path.resolve(homeDir); const normalizedHome = path.resolve(homeDir);
// Allow browsing within home directory // Allow browsing within home directory
if (resolved === normalizedHome || resolved.startsWith(normalizedHome + path.sep)) { if (
resolved === normalizedHome ||
resolved.startsWith(normalizedHome + path.sep)
) {
return true; return true;
} }
@@ -646,7 +707,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
if (!isSafePath(targetPath)) { if (!isSafePath(targetPath)) {
res.status(403).json({ res.status(403).json({
success: false, success: false,
error: "Access denied: browsing is restricted to your home directory and allowed project paths", error:
"Access denied: browsing is restricted to your home directory and allowed project paths",
}); });
return; return;
} }
@@ -655,7 +717,9 @@ export function createFsRoutes(_events: EventEmitter): Router {
const stats = await fs.stat(targetPath); const stats = await fs.stat(targetPath);
if (!stats.isDirectory()) { if (!stats.isDirectory()) {
res.status(400).json({ success: false, error: "Path is not a directory" }); res
.status(400)
.json({ success: false, error: "Path is not a directory" });
return; return;
} }
@@ -688,7 +752,8 @@ export function createFsRoutes(_events: EventEmitter): Router {
} catch (error) { } catch (error) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: error instanceof Error ? error.message : "Failed to read directory", error:
error instanceof Error ? error.message : "Failed to read directory",
}); });
} }
} catch (error) { } catch (error) {

View File

@@ -32,7 +32,15 @@ interface Feature {
priority?: number; priority?: number;
spec?: string; spec?: string;
model?: string; // Model to use for this feature model?: string; // Model to use for this feature
imagePaths?: Array<string | { path: string; filename?: string; mimeType?: string; [key: string]: unknown }>; imagePaths?: Array<
| string
| {
path: string;
filename?: string;
mimeType?: string;
[key: string]: unknown;
}
>;
} }
interface RunningFeature { interface RunningFeature {
@@ -78,7 +86,7 @@ export class AutoModeService {
projectPath, projectPath,
}; };
this.emitAutoModeEvent("auto_mode_complete", { this.emitAutoModeEvent("auto_mode_started", {
message: `Auto mode started with max ${maxConcurrency} concurrent features`, message: `Auto mode started with max ${maxConcurrency} concurrent features`,
projectPath, projectPath,
}); });
@@ -111,8 +119,9 @@ export class AutoModeService {
); );
if (pendingFeatures.length === 0) { if (pendingFeatures.length === 0) {
this.emitAutoModeEvent("auto_mode_complete", { this.emitAutoModeEvent("auto_mode_idle", {
message: "No pending features - auto mode idle", message: "No pending features - auto mode idle",
projectPath: this.config!.projectPath,
}); });
await this.sleep(10000); await this.sleep(10000);
continue; continue;
@@ -143,8 +152,9 @@ export class AutoModeService {
} }
this.autoLoopRunning = false; this.autoLoopRunning = false;
this.emitAutoModeEvent("auto_mode_complete", { this.emitAutoModeEvent("auto_mode_stopped", {
message: "Auto mode stopped", message: "Auto mode stopped",
projectPath: this.config?.projectPath,
}); });
} }
@@ -230,10 +240,19 @@ export class AutoModeService {
// Get model from feature // Get model from feature
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude); const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
console.log(`[AutoMode] Executing feature ${featureId} with model: ${model}`); console.log(
`[AutoMode] Executing feature ${featureId} with model: ${model}`
);
// Run the agent with the feature's model and images // Run the agent with the feature's model and images
await this.runAgent(workDir, featureId, prompt, abortController, imagePaths, model); await this.runAgent(
workDir,
featureId,
prompt,
abortController,
imagePaths,
model
);
// Mark as waiting_approval for user review // Mark as waiting_approval for user review
await this.updateFeatureStatus( await this.updateFeatureStatus(
@@ -422,7 +441,9 @@ Address the follow-up instructions above. Review the previous work and make the
try { try {
// Get model from feature (already loaded above) // Get model from feature (already loaded above)
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude); const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
console.log(`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`); console.log(
`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`
);
// Update feature status to in_progress // Update feature status to in_progress
await this.updateFeatureStatus(projectPath, featureId, "in_progress"); await this.updateFeatureStatus(projectPath, featureId, "in_progress");
@@ -458,9 +479,11 @@ Address the follow-up instructions above. Review the previous work and make the
filename filename
); );
copiedImagePaths.push(relativePath); copiedImagePaths.push(relativePath);
} catch (error) { } catch (error) {
console.error(`[AutoMode] Failed to copy follow-up image ${imagePath}:`, error); console.error(
`[AutoMode] Failed to copy follow-up image ${imagePath}:`,
error
);
} }
} }
} }
@@ -506,7 +529,14 @@ Address the follow-up instructions above. Review the previous work and make the
} }
// Use fullPrompt (already built above) with model and all images // Use fullPrompt (already built above) with model and all images
await this.runAgent(workDir, featureId, fullPrompt, abortController, allImagePaths.length > 0 ? allImagePaths : imagePaths, model); await this.runAgent(
workDir,
featureId,
fullPrompt,
abortController,
allImagePaths.length > 0 ? allImagePaths : imagePaths,
model
);
// Mark as waiting_approval for user review // Mark as waiting_approval for user review
await this.updateFeatureStatus( await this.updateFeatureStatus(
@@ -717,7 +747,10 @@ Format your response as a structured markdown document.`;
try { try {
// Use default Claude model for analysis (can be overridden in the future) // Use default Claude model for analysis (can be overridden in the future)
const analysisModel = resolveModelString(undefined, DEFAULT_MODELS.claude); const analysisModel = resolveModelString(
undefined,
DEFAULT_MODELS.claude
);
const provider = ProviderFactory.getProviderForModel(analysisModel); const provider = ProviderFactory.getProviderForModel(analysisModel);
const options: ExecuteOptions = { const options: ExecuteOptions = {
@@ -917,7 +950,11 @@ Format your response as a structured markdown document.`;
try { try {
const data = await fs.readFile(featurePath, "utf-8"); const data = await fs.readFile(featurePath, "utf-8");
const feature = JSON.parse(data); const feature = JSON.parse(data);
if (feature.status === "pending" || feature.status === "ready") { if (
feature.status === "pending" ||
feature.status === "ready" ||
feature.status === "backlog"
) {
features.push(feature); features.push(feature);
} }
} catch { } catch {
@@ -998,9 +1035,15 @@ ${feature.spec}
const imagesList = feature.imagePaths const imagesList = feature.imagePaths
.map((img, idx) => { .map((img, idx) => {
const path = typeof img === "string" ? img : img.path; const path = typeof img === "string" ? img : img.path;
const filename = typeof img === "string" ? path.split("/").pop() : img.filename || path.split("/").pop(); const filename =
const mimeType = typeof img === "string" ? "image/*" : img.mimeType || "image/*"; typeof img === "string"
return ` ${idx + 1}. ${filename} (${mimeType})\n Path: ${path}`; ? path.split("/").pop()
: img.filename || path.split("/").pop();
const mimeType =
typeof img === "string" ? "image/*" : img.mimeType || "image/*";
return ` ${
idx + 1
}. ${filename} (${mimeType})\n Path: ${path}`;
}) })
.join("\n"); .join("\n");
@@ -1038,7 +1081,9 @@ When done, summarize what you implemented and any notes for the developer.`;
model?: string model?: string
): Promise<void> { ): Promise<void> {
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude); const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
console.log(`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`); console.log(
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
);
// Get provider for this model // Get provider for this model
const provider = ProviderFactory.getProviderForModel(finalModel); const provider = ProviderFactory.getProviderForModel(finalModel);
@@ -1060,14 +1105,7 @@ When done, summarize what you implemented and any notes for the developer.`;
model: finalModel, model: finalModel,
maxTurns: 50, maxTurns: 50,
cwd: workDir, cwd: workDir,
allowedTools: [ allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Bash",
],
abortController, abortController,
}; };
@@ -1089,12 +1127,15 @@ When done, summarize what you implemented and any notes for the developer.`;
responseText = block.text || ""; responseText = block.text || "";
// Check for authentication errors in the response // Check for authentication errors in the response
if (block.text && (block.text.includes("Invalid API key") || if (
block.text &&
(block.text.includes("Invalid API key") ||
block.text.includes("authentication_failed") || block.text.includes("authentication_failed") ||
block.text.includes("Fix external API key"))) { block.text.includes("Fix external API key"))
) {
throw new Error( throw new Error(
"Authentication failed: Invalid or expired API key. " + "Authentication failed: Invalid or expired API key. " +
"Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate." "Please check your ANTHROPIC_API_KEY or GOOGLE_API_KEY, or run 'claude login' to re-authenticate."
); );
} }