diff --git a/apps/app/src/app/api/chat/route.ts b/apps/app/src/app/api/chat/route.ts deleted file mode 100644 index 11f3db6a..00000000 --- a/apps/app/src/app/api/chat/route.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - query, - Options, - SDKAssistantMessage, -} from "@anthropic-ai/claude-agent-sdk"; -import { NextRequest, NextResponse } from "next/server"; -import path from "path"; - -const systemPrompt = `You are an AI assistant helping users build software. You are part of the Automaker application, -which is designed to help developers plan, design, and implement software projects autonomously. - -Your role is to: -- Help users define their project requirements and specifications -- Ask clarifying questions to better understand their needs -- Suggest technical approaches and architectures -- Guide them through the development process -- Be conversational and helpful -- Write, edit, and modify code files as requested -- Execute commands and tests -- Search and analyze the codebase - -When discussing projects, help users think through: -- Core functionality and features -- Technical stack choices -- Data models and architecture -- User experience considerations -- Testing strategies - -You have full access to the codebase and can: -- Read files to understand existing code -- Write new files -- Edit existing files -- Run bash commands -- Search for code patterns -- Execute tests and builds`; - -export async function POST(request: NextRequest) { - try { - const { messages, workingDirectory } = await request.json(); - - console.log( - "[API] CLAUDE_CODE_OAUTH_TOKEN present:", - !!process.env.CLAUDE_CODE_OAUTH_TOKEN - ); - - if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) { - return NextResponse.json( - { error: "CLAUDE_CODE_OAUTH_TOKEN not configured" }, - { status: 500 } - ); - } - - // Get the last user message - const lastMessage = messages[messages.length - 1]; - - // Determine working directory - default to parent of app directory - const cwd = workingDirectory || path.resolve(process.cwd(), ".."); - - console.log("[API] Working directory:", cwd); - - // Create query with options that enable code modification - const options: Options = { - // model: "claude-sonnet-4-20250514", - model: "claude-opus-4-5-20251101", - systemPrompt, - maxTurns: 20, - cwd, - // Enable all core tools for code modification - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "WebSearch", - "WebFetch", - ], - // Auto-accept file edits within the working directory - permissionMode: "acceptEdits", - // Enable sandbox for safer bash execution - sandbox: { - enabled: true, - autoAllowBashIfSandboxed: true, - }, - }; - - // Convert message history to SDK format to preserve conversation context - // Include both user and assistant messages for full context - const sessionId = `api-session-${Date.now()}`; - const conversationMessages = messages.map( - (msg: { role: string; content: string }) => { - if (msg.role === "user") { - return { - type: "user" as const, - message: { - role: "user" as const, - content: msg.content, - }, - parent_tool_use_id: null, - session_id: sessionId, - }; - } else { - // Assistant message - return { - type: "assistant" as const, - message: { - role: "assistant" as const, - content: [ - { - type: "text" as const, - text: msg.content, - }, - ], - }, - session_id: sessionId, - }; - } - } - ); - - // Execute query with full conversation context - const queryResult = query({ - prompt: - conversationMessages.length > 0 - ? conversationMessages - : lastMessage.content, - options, - }); - - let responseText = ""; - const toolUses: Array<{ name: string; input: unknown }> = []; - - // Collect the response from the async generator - for await (const msg of queryResult) { - if (msg.type === "assistant") { - const assistantMsg = msg as SDKAssistantMessage; - if (assistantMsg.message.content) { - for (const block of assistantMsg.message.content) { - if (block.type === "text") { - responseText += block.text; - } else if (block.type === "tool_use") { - // Track tool usage for transparency - toolUses.push({ - name: block.name, - input: block.input, - }); - } - } - } - } else if (msg.type === "result") { - if (msg.subtype === "success") { - if (msg.result) { - responseText = msg.result; - } - } - } - } - - return NextResponse.json({ - content: responseText || "Sorry, I couldn't generate a response.", - toolUses: toolUses.length > 0 ? toolUses : undefined, - }); - } catch (error: unknown) { - console.error("Claude API error:", error); - const errorMessage = - error instanceof Error - ? error.message - : "Failed to get response from Claude"; - return NextResponse.json({ error: errorMessage }, { status: 500 }); - } -} diff --git a/apps/app/src/components/layout/project-setup-dialog.tsx b/apps/app/src/components/layout/project-setup-dialog.tsx new file mode 100644 index 00000000..4b4ce15e --- /dev/null +++ b/apps/app/src/components/layout/project-setup-dialog.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { Sparkles } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; + +interface ProjectSetupDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + projectOverview: string; + onProjectOverviewChange: (value: string) => void; + generateFeatures: boolean; + onGenerateFeaturesChange: (value: boolean) => void; + onCreateSpec: () => void; + onSkip: () => void; + isCreatingSpec: boolean; +} + +export function ProjectSetupDialog({ + open, + onOpenChange, + projectOverview, + onProjectOverviewChange, + generateFeatures, + onGenerateFeaturesChange, + onCreateSpec, + onSkip, + isCreatingSpec, +}: ProjectSetupDialogProps) { + return ( + { + if (!open && !isCreatingSpec) { + onSkip(); + } + }} + > + + + Set Up Your Project + + We didn't find an app_spec.txt file. Let us help you generate + your app_spec.txt to help describe your project for our system. + We'll analyze your project's tech stack and create a + comprehensive specification. + + + + + + Project Overview + + Describe what your project does and what features you want to + build. Be as detailed as you want - this will help us create a + better specification. + + onProjectOverviewChange(e.target.value)} + placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..." + autoFocus + /> + + + + + onGenerateFeaturesChange(checked === true) + } + /> + + + Generate feature list + + + Automatically create features in the features folder from the + implementation roadmap after the spec is generated. + + + + + + + + Skip for now + + + + Generate Spec + + + + + ); +} + diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/app/src/components/layout/sidebar.tsx index cf163102..ae81b122 100644 --- a/apps/app/src/components/layout/sidebar.tsx +++ b/apps/app/src/components/layout/sidebar.tsx @@ -79,10 +79,10 @@ import { } from "@/lib/project-init"; import { toast } from "sonner"; import { themeOptions } from "@/config/theme-options"; -import { Checkbox } from "@/components/ui/checkbox"; import type { SpecRegenerationEvent } from "@/types/electron"; import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog"; import { NewProjectModal } from "@/components/new-project-modal"; +import { ProjectSetupDialog } from "@/components/layout/project-setup-dialog"; import { DndContext, DragEndEvent, @@ -471,6 +471,12 @@ export function Sidebar() { toast.error("Failed to create specification", { description: result.error, }); + } else { + // Show processing toast to inform user + toast.info("Generating app specification...", { + description: + "This may take a minute. You'll be notified when complete.", + }); } // If successful, we'll wait for the events to update the state } catch (error) { @@ -1904,79 +1910,17 @@ export function Sidebar() { {/* New Project Setup Dialog */} - { - if (!open && !isCreatingSpec) { - handleSkipSetup(); - } - }} - > - - - Set Up Your Project - - We didn't find an app_spec.txt file. Let us help you generate - your app_spec.txt to help describe your project for our system. - We'll analyze your project's tech stack and create a - comprehensive specification. - - - - - - Project Overview - - Describe what your project does and what features you want to - build. Be as detailed as you want - this will help us create a - better specification. - - setProjectOverview(e.target.value)} - placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..." - autoFocus - /> - - - - - setGenerateFeatures(checked === true) - } - /> - - - Generate feature list - - - Automatically create features in the features folder from the - implementation roadmap after the spec is generated. - - - - - - - - Skip for now - - - - Generate Spec - - - - + onOpenChange={setShowSetupDialog} + projectOverview={projectOverview} + onProjectOverviewChange={setProjectOverview} + generateFeatures={generateFeatures} + onGenerateFeaturesChange={setGenerateFeatures} + onCreateSpec={handleCreateInitialSpec} + onSkip={handleSkipSetup} + isCreatingSpec={isCreatingSpec} + /> {/* New Project Onboarding Dialog */} { const api = getElectronAPI(); if (!api.specRegeneration) return; @@ -495,6 +495,30 @@ export function BoardView() { } }, [currentProject, setFeatures]); + // Subscribe to spec regeneration complete events to refresh kanban board + useEffect(() => { + const api = getElectronAPI(); + if (!api.specRegeneration) return; + + const unsubscribe = api.specRegeneration.onEvent((event) => { + // Refresh the kanban board when spec regeneration completes for the current project + if ( + event.type === "spec_regeneration_complete" && + currentProject && + event.projectPath === currentProject.path + ) { + console.log( + "[BoardView] Spec regeneration complete, refreshing features" + ); + loadFeatures(); + } + }); + + return () => { + unsubscribe(); + }; + }, [currentProject, loadFeatures]); + // Load persisted categories from file const loadCategories = useCallback(async () => { if (!currentProject) return; diff --git a/apps/app/src/config/model-config.ts b/apps/app/src/config/model-config.ts new file mode 100644 index 00000000..63a929b4 --- /dev/null +++ b/apps/app/src/config/model-config.ts @@ -0,0 +1,93 @@ +/** + * Model Configuration - Centralized model settings for the app + * + * Models can be overridden via environment variables: + * - AUTOMAKER_MODEL_CHAT: Model for chat interactions + * - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations + */ + +/** + * Claude model aliases for convenience + */ +export const CLAUDE_MODEL_MAP: Record = { + haiku: "claude-haiku-4-5", + sonnet: "claude-sonnet-4-20250514", + opus: "claude-opus-4-5-20251101", +} as const; + +/** + * Default models per use case + */ +export const DEFAULT_MODELS = { + chat: "claude-opus-4-5-20251101", + default: "claude-opus-4-5-20251101", +} as const; + +/** + * Resolve a model alias to a full model string + */ +export function resolveModelString( + modelKey?: string, + defaultModel: string = DEFAULT_MODELS.default +): string { + if (!modelKey) { + return defaultModel; + } + + // Full Claude model string - pass through + if (modelKey.includes("claude-")) { + return modelKey; + } + + // Check alias map + const resolved = CLAUDE_MODEL_MAP[modelKey]; + if (resolved) { + return resolved; + } + + // Unknown key - use default + return defaultModel; +} + +/** + * Get the model for chat operations + * + * Priority: + * 1. Explicit model parameter + * 2. AUTOMAKER_MODEL_CHAT environment variable + * 3. AUTOMAKER_MODEL_DEFAULT environment variable + * 4. Default chat model + */ +export function getChatModel(explicitModel?: string): string { + if (explicitModel) { + return resolveModelString(explicitModel); + } + + const envModel = + process.env.AUTOMAKER_MODEL_CHAT || process.env.AUTOMAKER_MODEL_DEFAULT; + + if (envModel) { + return resolveModelString(envModel); + } + + return DEFAULT_MODELS.chat; +} + +/** + * Default allowed tools for chat interactions + */ +export const CHAT_TOOLS = [ + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "Bash", + "WebSearch", + "WebFetch", +] as const; + +/** + * Default max turns for chat + */ +export const CHAT_MAX_TURNS = 1000; diff --git a/apps/server/src/lib/model-resolver.ts b/apps/server/src/lib/model-resolver.ts index d4bbff5e..e49d9b94 100644 --- a/apps/server/src/lib/model-resolver.ts +++ b/apps/server/src/lib/model-resolver.ts @@ -48,7 +48,9 @@ export function resolveModelString( // Look up Claude model alias const resolved = CLAUDE_MODEL_MAP[modelKey]; if (resolved) { - console.log(`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`); + console.log( + `[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"` + ); return resolved; } @@ -73,8 +75,5 @@ export function getEffectiveModel( sessionModel?: string, defaultModel?: string ): string { - return resolveModelString( - explicitModel || sessionModel, - defaultModel - ); + return resolveModelString(explicitModel || sessionModel, defaultModel); } diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts new file mode 100644 index 00000000..1ff16c25 --- /dev/null +++ b/apps/server/src/lib/sdk-options.ts @@ -0,0 +1,291 @@ +/** + * SDK Options Factory - Centralized configuration for Claude Agent SDK + * + * Provides presets for common use cases: + * - Spec generation: Long-running analysis with read-only tools + * - Feature generation: Quick JSON generation from specs + * - Feature building: Autonomous feature implementation with full tool access + * - Suggestions: Analysis with read-only tools + * - Chat: Full tool access for interactive coding + * + * Uses model-resolver for consistent model handling across the application. + */ + +import type { Options } from "@anthropic-ai/claude-agent-sdk"; +import { + resolveModelString, + DEFAULT_MODELS, + CLAUDE_MODEL_MAP, +} from "./model-resolver.js"; + +/** + * Tool presets for different use cases + */ +export const TOOL_PRESETS = { + /** Read-only tools for analysis */ + readOnly: ["Read", "Glob", "Grep"] as const, + + /** Tools for spec generation that needs to read the codebase */ + specGeneration: ["Read", "Glob", "Grep"] as const, + + /** Full tool access for feature implementation */ + fullAccess: [ + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "Bash", + "WebSearch", + "WebFetch", + ] as const, + + /** Tools for chat/interactive mode */ + chat: [ + "Read", + "Write", + "Edit", + "Glob", + "Grep", + "Bash", + "WebSearch", + "WebFetch", + ] as const, +} as const; + +/** + * Max turns presets for different use cases + */ +export const MAX_TURNS = { + /** Quick operations that shouldn't need many iterations */ + quick: 5, + + /** Standard operations */ + standard: 20, + + /** Long-running operations like full spec generation */ + extended: 50, + + /** Very long operations that may require extensive exploration */ + maximum: 1000, +} as const; + +/** + * Model presets for different use cases + * + * These can be overridden via environment variables: + * - AUTOMAKER_MODEL_SPEC: Model for spec generation + * - AUTOMAKER_MODEL_FEATURES: Model for feature generation + * - AUTOMAKER_MODEL_SUGGESTIONS: Model for suggestions + * - AUTOMAKER_MODEL_CHAT: Model for chat + * - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations + */ +export function getModelForUseCase( + useCase: "spec" | "features" | "suggestions" | "chat" | "auto" | "default", + explicitModel?: string +): string { + // Explicit model takes precedence + if (explicitModel) { + return resolveModelString(explicitModel); + } + + // Check environment variable override for this use case + const envVarMap: Record = { + spec: process.env.AUTOMAKER_MODEL_SPEC, + features: process.env.AUTOMAKER_MODEL_FEATURES, + suggestions: process.env.AUTOMAKER_MODEL_SUGGESTIONS, + chat: process.env.AUTOMAKER_MODEL_CHAT, + auto: process.env.AUTOMAKER_MODEL_AUTO, + default: process.env.AUTOMAKER_MODEL_DEFAULT, + }; + + const envModel = envVarMap[useCase] || envVarMap.default; + if (envModel) { + return resolveModelString(envModel); + } + + const defaultModels: Record = { + spec: CLAUDE_MODEL_MAP["haiku"], // used to generate app specs + features: CLAUDE_MODEL_MAP["haiku"], // used to generate features from app specs + suggestions: CLAUDE_MODEL_MAP["haiku"], // used for suggestions + chat: CLAUDE_MODEL_MAP["haiku"], // used for chat + auto: CLAUDE_MODEL_MAP["opus"], // used to implement kanban cards + default: CLAUDE_MODEL_MAP["opus"], + }; + + return resolveModelString(defaultModels[useCase] || DEFAULT_MODELS.claude); +} + +/** + * Base options that apply to all SDK calls + */ +function getBaseOptions(): Partial { + return { + permissionMode: "acceptEdits", + }; +} + +/** + * Options configuration for creating SDK options + */ +export interface CreateSdkOptionsConfig { + /** Working directory for the agent */ + cwd: string; + + /** Optional explicit model override */ + model?: string; + + /** Optional session model (used as fallback if explicit model not provided) */ + sessionModel?: string; + + /** Optional system prompt */ + systemPrompt?: string; + + /** Optional abort controller for cancellation */ + abortController?: AbortController; +} + +/** + * Create SDK options for spec generation + * + * Configuration: + * - Uses read-only tools for codebase analysis + * - Extended turns for thorough exploration + * - Opus model by default (can be overridden) + */ +export function createSpecGenerationOptions( + config: CreateSdkOptionsConfig +): Options { + return { + ...getBaseOptions(), + model: getModelForUseCase("spec", config.model), + maxTurns: MAX_TURNS.maximum, + cwd: config.cwd, + allowedTools: [...TOOL_PRESETS.specGeneration], + ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...(config.abortController && { abortController: config.abortController }), + }; +} + +/** + * Create SDK options for feature generation from specs + * + * Configuration: + * - Uses read-only tools (just needs to read the spec) + * - Quick turns since it's mostly JSON generation + * - Sonnet model by default for speed + */ +export function createFeatureGenerationOptions( + config: CreateSdkOptionsConfig +): Options { + return { + ...getBaseOptions(), + model: getModelForUseCase("features", config.model), + maxTurns: MAX_TURNS.quick, + cwd: config.cwd, + allowedTools: [...TOOL_PRESETS.readOnly], + ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...(config.abortController && { abortController: config.abortController }), + }; +} + +/** + * Create SDK options for generating suggestions + * + * Configuration: + * - Uses read-only tools for analysis + * - Quick turns for focused suggestions + * - Opus model by default for thorough analysis + */ +export function createSuggestionsOptions( + config: CreateSdkOptionsConfig +): Options { + return { + ...getBaseOptions(), + model: getModelForUseCase("suggestions", config.model), + maxTurns: MAX_TURNS.quick, + cwd: config.cwd, + allowedTools: [...TOOL_PRESETS.readOnly], + ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...(config.abortController && { abortController: config.abortController }), + }; +} + +/** + * Create SDK options for chat/interactive mode + * + * Configuration: + * - Full tool access for code modification + * - Standard turns for interactive sessions + * - Model priority: explicit model > session model > chat default + * - Sandbox enabled for bash safety + */ +export function createChatOptions(config: CreateSdkOptionsConfig): Options { + // Model priority: explicit model > session model > chat default + const effectiveModel = config.model || config.sessionModel; + + return { + ...getBaseOptions(), + model: getModelForUseCase("chat", effectiveModel), + maxTurns: MAX_TURNS.standard, + cwd: config.cwd, + allowedTools: [...TOOL_PRESETS.chat], + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...(config.abortController && { abortController: config.abortController }), + }; +} + +/** + * Create SDK options for autonomous feature building/implementation + * + * Configuration: + * - Full tool access for code modification and implementation + * - Extended turns for thorough feature implementation + * - Uses default model (can be overridden) + * - Sandbox enabled for bash safety + */ +export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options { + return { + ...getBaseOptions(), + model: getModelForUseCase("auto", config.model), + maxTurns: MAX_TURNS.maximum, + cwd: config.cwd, + allowedTools: [...TOOL_PRESETS.fullAccess], + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...(config.abortController && { abortController: config.abortController }), + }; +} + +/** + * Create custom SDK options with explicit configuration + * + * Use this when the preset options don't fit your use case. + */ +export function createCustomOptions( + config: CreateSdkOptionsConfig & { + maxTurns?: number; + allowedTools?: readonly string[]; + sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean }; + } +): Options { + return { + ...getBaseOptions(), + model: getModelForUseCase("default", config.model), + maxTurns: config.maxTurns ?? MAX_TURNS.maximum, + cwd: config.cwd, + allowedTools: config.allowedTools + ? [...config.allowedTools] + : [...TOOL_PRESETS.readOnly], + ...(config.sandbox && { sandbox: config.sandbox }), + ...(config.systemPrompt && { systemPrompt: config.systemPrompt }), + ...(config.abortController && { abortController: config.abortController }), + }; +} diff --git a/apps/server/src/routes/app-spec/generate-features-from-spec.ts b/apps/server/src/routes/app-spec/generate-features-from-spec.ts index 44df4cd3..98355a68 100644 --- a/apps/server/src/routes/app-spec/generate-features-from-spec.ts +++ b/apps/server/src/routes/app-spec/generate-features-from-spec.ts @@ -2,16 +2,19 @@ * Generate features from existing app_spec.txt */ -import { query, type Options } from "@anthropic-ai/claude-agent-sdk"; +import { query } from "@anthropic-ai/claude-agent-sdk"; import path from "path"; import fs from "fs/promises"; import type { EventEmitter } from "../../lib/events.js"; import { createLogger } from "../../lib/logger.js"; +import { createFeatureGenerationOptions } from "../../lib/sdk-options.js"; import { logAuthStatus } from "./common.js"; import { parseAndCreateFeatures } from "./parse-and-create-features.js"; const logger = createLogger("SpecRegeneration"); +const MAX_FEATURES = 100; + export async function generateFeaturesFromSpec( projectPath: string, events: EventEmitter, @@ -29,6 +32,10 @@ export async function generateFeaturesFromSpec( try { spec = await fs.readFile(specPath, "utf-8"); logger.info(`Spec loaded successfully (${spec.length} chars)`); + logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`); + logger.info( + `Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}` + ); } catch (readError) { logger.error("❌ Failed to read spec file:", readError); events.emit("spec-regeneration:event", { @@ -66,9 +73,16 @@ Format as JSON: ] } -Generate 5-15 features that build on each other logically.`; +Generate ${MAX_FEATURES} features that build on each other logically. - logger.debug("Prompt length:", `${prompt.length} chars`); +IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`; + + logger.info("========== PROMPT BEING SENT =========="); + logger.info(`Prompt length: ${prompt.length} chars`); + logger.info( + `Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}` + ); + logger.info("========== END PROMPT PREVIEW =========="); events.emit("spec-regeneration:event", { type: "spec_regeneration_progress", @@ -76,14 +90,10 @@ Generate 5-15 features that build on each other logically.`; projectPath: projectPath, }); - const options: Options = { - model: "claude-sonnet-4-20250514", - maxTurns: 5, + const options = createFeatureGenerationOptions({ cwd: projectPath, - allowedTools: ["Read", "Glob"], - permissionMode: "acceptEdits", abortController, - }; + }); logger.debug("SDK Options:", JSON.stringify(options, null, 2)); logger.info("Calling Claude Agent SDK query() for features..."); @@ -120,7 +130,7 @@ Generate 5-15 features that build on each other logically.`; if (msg.type === "assistant" && msg.message.content) { for (const block of msg.message.content) { if (block.type === "text") { - responseText = block.text; + responseText += block.text; logger.debug( `Feature text block received (${block.text.length} chars)` ); @@ -147,6 +157,9 @@ Generate 5-15 features that build on each other logically.`; logger.info(`Feature stream complete. Total messages: ${messageCount}`); logger.info(`Feature response length: ${responseText.length} chars`); + logger.info("========== FULL RESPONSE TEXT =========="); + logger.info(responseText); + logger.info("========== END RESPONSE TEXT =========="); await parseAndCreateFeatures(projectPath, responseText, events); diff --git a/apps/server/src/routes/app-spec/generate-spec.ts b/apps/server/src/routes/app-spec/generate-spec.ts index 4217a8dc..c17082ae 100644 --- a/apps/server/src/routes/app-spec/generate-spec.ts +++ b/apps/server/src/routes/app-spec/generate-spec.ts @@ -2,12 +2,13 @@ * Generate app_spec.txt from project overview */ -import { query, type Options } from "@anthropic-ai/claude-agent-sdk"; +import { query } from "@anthropic-ai/claude-agent-sdk"; import path from "path"; import fs from "fs/promises"; import type { EventEmitter } from "../../lib/events.js"; import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js"; import { createLogger } from "../../lib/logger.js"; +import { createSpecGenerationOptions } from "../../lib/sdk-options.js"; import { logAuthStatus } from "./common.js"; import { generateFeaturesFromSpec } from "./generate-features-from-spec.js"; @@ -21,11 +22,12 @@ export async function generateSpec( generateFeatures?: boolean, analyzeProject?: boolean ): Promise { - logger.debug("========== generateSpec() started =========="); - logger.debug("projectPath:", projectPath); - logger.debug("projectOverview length:", `${projectOverview.length} chars`); - logger.debug("generateFeatures:", generateFeatures); - logger.debug("analyzeProject:", analyzeProject); + logger.info("========== generateSpec() started =========="); + logger.info("projectPath:", projectPath); + logger.info("projectOverview length:", `${projectOverview.length} chars`); + logger.info("projectOverview preview:", projectOverview.substring(0, 300)); + logger.info("generateFeatures:", generateFeatures); + logger.info("analyzeProject:", analyzeProject); // Build the prompt based on whether we should analyze the project let analysisInstructions = ""; @@ -63,21 +65,20 @@ ${analysisInstructions} ${getAppSpecFormatInstruction()}`; - logger.debug("Prompt length:", `${prompt.length} chars`); + logger.info("========== PROMPT BEING SENT =========="); + logger.info(`Prompt length: ${prompt.length} chars`); + logger.info(`Prompt preview (first 500 chars):\n${prompt.substring(0, 500)}`); + logger.info("========== END PROMPT PREVIEW =========="); events.emit("spec-regeneration:event", { type: "spec_progress", content: "Starting spec generation...\n", }); - const options: Options = { - model: "claude-opus-4-5-20251101", - maxTurns: 10, + const options = createSpecGenerationOptions({ cwd: projectPath, - allowedTools: ["Read", "Glob", "Grep"], - permissionMode: "acceptEdits", abortController, - }; + }); logger.debug("SDK Options:", JSON.stringify(options, null, 2)); logger.info("Calling Claude Agent SDK query()..."); @@ -98,45 +99,96 @@ ${getAppSpecFormatInstruction()}`; let responseText = ""; let messageCount = 0; - logger.debug("Starting to iterate over stream..."); + logger.info("Starting to iterate over stream..."); try { for await (const msg of stream) { messageCount++; - logger.debug( - `Stream message #${messageCount}:`, - JSON.stringify( - { type: msg.type, subtype: (msg as any).subtype }, - null, - 2 - ) + logger.info( + `Stream message #${messageCount}: type=${msg.type}, subtype=${ + (msg as any).subtype + }` ); - if (msg.type === "assistant" && msg.message.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText += block.text; - logger.debug(`Text block received (${block.text.length} chars)`); - events.emit("spec-regeneration:event", { - type: "spec_regeneration_progress", - content: block.text, - projectPath: projectPath, - }); - } else if (block.type === "tool_use") { - logger.debug("Tool use:", block.name); - events.emit("spec-regeneration:event", { - type: "spec_tool", - tool: block.name, - input: block.input, - }); + if (msg.type === "assistant") { + // Log the full message structure to debug + logger.info(`Assistant msg keys: ${Object.keys(msg).join(", ")}`); + const msgAny = msg as any; + if (msgAny.message) { + logger.info( + `msg.message keys: ${Object.keys(msgAny.message).join(", ")}` + ); + if (msgAny.message.content) { + logger.info( + `msg.message.content length: ${msgAny.message.content.length}` + ); + for (const block of msgAny.message.content) { + logger.info( + `Block keys: ${Object.keys(block).join(", ")}, type: ${ + block.type + }` + ); + if (block.type === "text") { + responseText += block.text; + logger.info( + `Text block received (${block.text.length} chars), total now: ${responseText.length} chars` + ); + logger.info(`Text preview: ${block.text.substring(0, 200)}...`); + events.emit("spec-regeneration:event", { + type: "spec_regeneration_progress", + content: block.text, + projectPath: projectPath, + }); + } else if (block.type === "tool_use") { + logger.info("Tool use:", block.name); + events.emit("spec-regeneration:event", { + type: "spec_tool", + tool: block.name, + input: block.input, + }); + } + } + } else { + logger.warn("msg.message.content is falsy"); } + } else { + logger.warn("msg.message is falsy"); + // Log full message to see structure + logger.info( + `Full assistant msg: ${JSON.stringify(msg).substring(0, 1000)}` + ); } } else if (msg.type === "result" && (msg as any).subtype === "success") { - logger.debug("Received success result"); - responseText = (msg as any).result || responseText; + logger.info("Received success result"); + logger.info(`Result value: "${(msg as any).result}"`); + logger.info( + `Current responseText length before result: ${responseText.length}` + ); + // Only use result if it has content, otherwise keep accumulated text + if ((msg as any).result && (msg as any).result.length > 0) { + logger.info("Using result value as responseText"); + responseText = (msg as any).result; + } else { + logger.info("Result is empty, keeping accumulated responseText"); + } + } else if (msg.type === "result") { + // Handle all result types + const subtype = (msg as any).subtype; + logger.info(`Result message: subtype=${subtype}`); + if (subtype === "error_max_turns") { + logger.error( + "❌ Hit max turns limit! Claude used too many tool calls." + ); + logger.info(`responseText so far: ${responseText.length} chars`); + } } else if ((msg as { type: string }).type === "error") { logger.error("❌ Received error message from stream:"); logger.error("Error message:", JSON.stringify(msg, null, 2)); + } else if (msg.type === "user") { + // Log user messages (tool results) + logger.info( + `User message (tool result): ${JSON.stringify(msg).substring(0, 500)}` + ); } } } catch (streamError) { @@ -147,16 +199,31 @@ ${getAppSpecFormatInstruction()}`; logger.info(`Stream iteration complete. Total messages: ${messageCount}`); logger.info(`Response text length: ${responseText.length} chars`); + logger.info("========== FINAL RESPONSE TEXT =========="); + logger.info(responseText || "(empty)"); + logger.info("========== END RESPONSE TEXT =========="); + + if (!responseText || responseText.trim().length === 0) { + logger.error("❌ WARNING: responseText is empty! Nothing to save."); + } // Save spec const specDir = path.join(projectPath, ".automaker"); const specPath = path.join(specDir, "app_spec.txt"); logger.info("Saving spec to:", specPath); + logger.info(`Content to save (${responseText.length} chars)`); await fs.mkdir(specDir, { recursive: true }); await fs.writeFile(specPath, responseText); + // Verify the file was written + const savedContent = await fs.readFile(specPath, "utf-8"); + logger.info(`Verified saved file: ${savedContent.length} chars`); + if (savedContent.length === 0) { + logger.error("❌ File was saved but is empty!"); + } + logger.info("Spec saved successfully"); // Emit spec completion event diff --git a/apps/server/src/routes/app-spec/parse-and-create-features.ts b/apps/server/src/routes/app-spec/parse-and-create-features.ts index af97c108..8a119972 100644 --- a/apps/server/src/routes/app-spec/parse-and-create-features.ts +++ b/apps/server/src/routes/app-spec/parse-and-create-features.ts @@ -14,23 +14,32 @@ export async function parseAndCreateFeatures( content: string, events: EventEmitter ): Promise { - logger.debug("========== parseAndCreateFeatures() started =========="); - logger.debug("Content length:", `${content.length} chars`); + logger.info("========== parseAndCreateFeatures() started =========="); + logger.info(`Content length: ${content.length} chars`); + logger.info("========== CONTENT RECEIVED FOR PARSING =========="); + logger.info(content); + logger.info("========== END CONTENT =========="); try { // Extract JSON from response - logger.debug("Extracting JSON from response..."); + logger.info("Extracting JSON from response..."); + logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`); const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/); if (!jsonMatch) { logger.error("❌ No valid JSON found in response"); - logger.error("Content preview:", content.substring(0, 500)); + logger.error("Full content received:"); + logger.error(content); throw new Error("No valid JSON found in response"); } - logger.debug(`JSON match found (${jsonMatch[0].length} chars)`); + logger.info(`JSON match found (${jsonMatch[0].length} chars)`); + logger.info("========== MATCHED JSON =========="); + logger.info(jsonMatch[0]); + logger.info("========== END MATCHED JSON =========="); const parsed = JSON.parse(jsonMatch[0]); logger.info(`Parsed ${parsed.features?.length || 0} features`); + logger.info("Parsed features:", JSON.stringify(parsed.features, null, 2)); const featuresDir = path.join(projectPath, ".automaker", "features"); await fs.mkdir(featuresDir, { recursive: true }); diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index 14c02abe..5cc5abef 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -2,9 +2,10 @@ * Business logic for generating suggestions */ -import { query, type Options } from "@anthropic-ai/claude-agent-sdk"; +import { query } from "@anthropic-ai/claude-agent-sdk"; import type { EventEmitter } from "../../lib/events.js"; import { createLogger } from "../../lib/logger.js"; +import { createSuggestionsOptions } from "../../lib/sdk-options.js"; const logger = createLogger("Suggestions"); @@ -54,14 +55,10 @@ Format your response as JSON: content: `Starting ${suggestionType} analysis...\n`, }); - const options: Options = { - model: "claude-opus-4-5-20251101", - maxTurns: 5, + const options = createSuggestionsOptions({ cwd: projectPath, - allowedTools: ["Read", "Glob", "Grep"], - permissionMode: "acceptEdits", abortController, - }; + }); const stream = query({ prompt, options }); let responseText = ""; diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 262678b9..9e2e4b36 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -11,7 +11,7 @@ import { ProviderFactory } from "../providers/provider-factory.js"; import type { ExecuteOptions } from "../providers/types.js"; import { readImageAsBase64 } from "../lib/image-handler.js"; import { buildPromptWithImages } from "../lib/prompt-builder.js"; -import { getEffectiveModel } from "../lib/model-resolver.js"; +import { createChatOptions } from "../lib/sdk-options.js"; import { isAbortError } from "../lib/error-handler.js"; interface Message { @@ -176,8 +176,19 @@ export class AgentService { await this.saveSession(sessionId, session.messages); try { - // Use session model, parameter model, or default - const effectiveModel = getEffectiveModel(model, session.model); + // Build SDK options using centralized configuration + const sdkOptions = createChatOptions({ + cwd: workingDirectory || session.workingDirectory, + model: model, + sessionModel: session.model, + systemPrompt: this.getSystemPrompt(), + abortController: session.abortController!, + }); + + // Extract model, maxTurns, and allowedTools from SDK options + const effectiveModel = sdkOptions.model!; + const maxTurns = sdkOptions.maxTurns; + const allowedTools = sdkOptions.allowedTools as string[] | undefined; // Get provider for this model const provider = ProviderFactory.getProviderForModel(effectiveModel); @@ -192,17 +203,8 @@ export class AgentService { model: effectiveModel, cwd: workingDirectory || session.workingDirectory, systemPrompt: this.getSystemPrompt(), - maxTurns: 20, - allowedTools: [ - "Read", - "Write", - "Edit", - "Glob", - "Grep", - "Bash", - "WebSearch", - "WebFetch", - ], + maxTurns: maxTurns, + allowedTools: allowedTools, abortController: session.abortController!, conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index edbe7ec1..00bfd0f7 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -9,16 +9,16 @@ * - Verification and merge workflows */ -import { AbortError } from "@anthropic-ai/claude-agent-sdk"; import { ProviderFactory } from "../providers/provider-factory.js"; import type { ExecuteOptions } from "../providers/types.js"; import { exec } from "child_process"; import { promisify } from "util"; import path from "path"; import fs from "fs/promises"; -import type { EventEmitter, EventType } from "../lib/events.js"; +import type { EventEmitter } from "../lib/events.js"; import { buildPromptWithImages } from "../lib/prompt-builder.js"; import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js"; +import { createAutoModeOptions } from "../lib/sdk-options.js"; import { isAbortError, classifyError } from "../lib/error-handler.js"; const execAsync = promisify(exec); @@ -1085,7 +1085,18 @@ When done, summarize what you implemented and any notes for the developer.`; imagePaths?: string[], model?: string ): Promise { - const finalModel = resolveModelString(model, DEFAULT_MODELS.claude); + // Build SDK options using centralized configuration for feature implementation + const sdkOptions = createAutoModeOptions({ + cwd: workDir, + model: model, + abortController, + }); + + // Extract model, maxTurns, and allowedTools from SDK options + const finalModel = sdkOptions.model!; + const maxTurns = sdkOptions.maxTurns; + const allowedTools = sdkOptions.allowedTools as string[] | undefined; + console.log( `[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}` ); @@ -1108,9 +1119,9 @@ When done, summarize what you implemented and any notes for the developer.`; const options: ExecuteOptions = { prompt: promptContent, model: finalModel, - maxTurns: 50, + maxTurns: maxTurns, cwd: workDir, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], + allowedTools: allowedTools, abortController, };
+ Describe what your project does and what features you want to + build. Be as detailed as you want - this will help us create a + better specification. +
+ Automatically create features in the features folder from the + implementation roadmap after the spec is generated. +
- Describe what your project does and what features you want to - build. Be as detailed as you want - this will help us create a - better specification. -
- Automatically create features in the features folder from the - implementation roadmap after the spec is generated. -