From 62082fbaf584973ca8b01e492aea426ff3cfe23d Mon Sep 17 00:00:00 2001 From: trueheads Date: Fri, 12 Dec 2025 15:59:21 -0600 Subject: [PATCH] massive overhaul of auth system logic, and subtle fixes to UI --- .../services/spec-regeneration-service.js | 98 +++--- .../src/components/views/analysis-view.tsx | 2 +- apps/app/src/components/views/kanban-card.tsx | 4 +- .../authentication-status-display.tsx | 4 +- .../settings-view/hooks/use-cli-status.ts | 4 +- .../views/setup-view/hooks/use-cli-status.ts | 2 + apps/app/src/components/views/spec-view.tsx | 41 ++- apps/app/src/store/setup-store.ts | 1 + apps/server/src/index.ts | 24 +- apps/server/src/routes/setup.ts | 26 +- apps/server/src/routes/spec-regeneration.ts | 279 +++++++++++++++--- 11 files changed, 373 insertions(+), 112 deletions(-) diff --git a/apps/app/electron/services/spec-regeneration-service.js b/apps/app/electron/services/spec-regeneration-service.js index a977a0a6..90a37817 100644 --- a/apps/app/electron/services/spec-regeneration-service.js +++ b/apps/app/electron/services/spec-regeneration-service.js @@ -588,7 +588,7 @@ You should: 4. Based on the user's project overview, create a comprehensive app specification 5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application 6. Use the XML template format provided -7. Write the specification to .automaker/app_spec.txt +7. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives** When analyzing, look at: - package.json, cargo.toml, requirements.txt or similar config files for tech stack @@ -598,11 +598,17 @@ When analyzing, look at: - API structures and patterns You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your primary target) +- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename) -You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec. +You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt. -**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase.`; +**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase. + +**CRITICAL FILE NAMING RULES:** +- The spec file MUST be named exactly \`app_spec.txt\` +- Do NOT create project-spec.md, spec.md, or any other filename +- Do NOT use markdown (.md) extension - use .txt +- The full path must be: \`.automaker/app_spec.txt\``; } /** @@ -639,7 +645,11 @@ ${APP_SPEC_XML_TEMPLATE} - **development_workflow**: Note any testing or development patterns - **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built -4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` +4. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\` + - The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name + - Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename + - Do NOT output the spec in your response - write it to the file + - Use the Write tool with path \`.automaker/app_spec.txt\` **Guidelines:** - Be comprehensive! Include ALL features needed for a complete application @@ -648,8 +658,9 @@ ${APP_SPEC_XML_TEMPLATE} - The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually - Consider user flows, error states, and edge cases when defining features - Each phase should have multiple specific, actionable features +- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!** -Begin by exploring the project structure.`; +Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`; } /** @@ -848,7 +859,7 @@ You should: 3. Understand the current architecture and patterns used 4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision 5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application -6. Write the specification to .automaker/app_spec.txt +6. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives** When analyzing, look at: - package.json, cargo.toml, or similar config files for tech stack @@ -861,9 +872,15 @@ When analyzing, look at: Your task is ONLY to update the app_spec.txt file - feature files will be managed separately. You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your primary target) +- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename) -You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`; +You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt. + +**CRITICAL FILE NAMING RULES:** +- The spec file MUST be named exactly \`app_spec.txt\` +- Do NOT create project-spec.md, spec.md, or any other filename +- Do NOT use markdown (.md) extension - use .txt +- The full path must be: \`.automaker/app_spec.txt\``; } /** @@ -892,37 +909,40 @@ ${projectDefinition} - Think about user experience, error handling, edge cases, etc. - Architecture Notes: Any important architectural decisions or patterns -3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` +3. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\` + - The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name + - Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename + - Do NOT output the spec in your response - write it to the file + - Use the Write tool with path \`.automaker/app_spec.txt\` -**Format Guidelines for the Spec:** +**Format Guidelines for the Spec (use XML format in app_spec.txt):** -Use this general structure: +Use this XML structure inside app_spec.txt: -\`\`\` -# [App Name] - Application Specification - -## Product Overview -[Description of what the app does and its purpose] - -## Tech Stack -- Frontend: [frameworks, libraries] -- Backend: [frameworks, APIs] -- Database: [if applicable] -- Other: [other relevant tech] - -## Features - -### [Category 1] -- **[Feature Name]**: [Detailed description of the feature] -- **[Feature Name]**: [Detailed description] -... - -### [Category 2] -- **[Feature Name]**: [Detailed description] -... - -## Architecture Notes -[Any important architectural notes, patterns, or conventions] +\`\`\`xml + + [App Name] + + + [Description of what the app does and its purpose] + + + + [frameworks, libraries] + [frameworks, APIs] + [if applicable] + + + + [List all the major capabilities] + + + + [Foundation features] + [Core features] + [Polish features] + + \`\`\` **Remember:** @@ -930,9 +950,9 @@ Use this general structure: - Consider user flows, error states, loading states, etc. - Include authentication, authorization if relevant - Think about what would make this a polished, production-ready app -- The more detailed and complete the spec, the better +- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!** -Begin by exploring the project structure.`; +Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`; } /** diff --git a/apps/app/src/components/views/analysis-view.tsx b/apps/app/src/components/views/analysis-view.tsx index 530ef2f9..a7a9e85f 100644 --- a/apps/app/src/components/views/analysis-view.tsx +++ b/apps/app/src/components/views/analysis-view.tsx @@ -399,7 +399,7 @@ ${Object.entries(projectAnalysis.filesByExtension) `; // Write the spec file - const specPath = `${currentProject.path}/app_spec.txt`; + const specPath = `${currentProject.path}/.automaker/app_spec.txt`; const writeResult = await api.writeFile(specPath, specContent); if (writeResult.success) { diff --git a/apps/app/src/components/views/kanban-card.tsx b/apps/app/src/components/views/kanban-card.tsx index 7888de3e..41a35729 100644 --- a/apps/app/src/components/views/kanban-card.tsx +++ b/apps/app/src/components/views/kanban-card.tsx @@ -207,10 +207,12 @@ export const KanbanCard = memo(function KanbanCard({ // - Backlog items can always be dragged // - skipTests items can be dragged even when in_progress or verified (unless currently running) // - waiting_approval items can always be dragged (to allow manual verification via drag) - // - Non-skipTests (TDD) items in progress or verified cannot be dragged + // - verified items can always be dragged (to allow moving back to waiting_approval or backlog) + // - Non-skipTests (TDD) items in progress cannot be dragged (they are running) const isDraggable = feature.status === "backlog" || feature.status === "waiting_approval" || + feature.status === "verified" || (feature.skipTests && !isCurrentAutoTask); const { attributes, diff --git a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx index 94a49338..0c36b2ef 100644 --- a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx +++ b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx @@ -61,13 +61,15 @@ export function AuthenticationStatusDisplay({ {claudeAuthStatus.method === "oauth_token_env" ? "Using CLAUDE_CODE_OAUTH_TOKEN" : claudeAuthStatus.method === "oauth_token" - ? "Using stored OAuth token (claude login)" + ? "Using stored OAuth token (subscription)" : claudeAuthStatus.method === "api_key_env" ? "Using ANTHROPIC_API_KEY" : claudeAuthStatus.method === "api_key" ? "Using stored API key" : claudeAuthStatus.method === "credentials_file" ? "Using credentials file" + : claudeAuthStatus.method === "cli_authenticated" + ? "Using Claude CLI authentication" : `Using ${claudeAuthStatus.method || "detected"} authentication`} diff --git a/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts b/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts index 3f4422c4..600a5f67 100644 --- a/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts +++ b/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts @@ -74,8 +74,8 @@ export function useCliStatus() { apiKeyValid?: boolean; }; // Map server method names to client method types - // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, none - const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "none"] as const; + // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none + const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "cli_authenticated", "none"] as const; type AuthMethod = typeof validMethods[number]; const method: AuthMethod = validMethods.includes(auth.method as AuthMethod) ? (auth.method as AuthMethod) diff --git a/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts b/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts index b7a31685..1aa0d094 100644 --- a/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts +++ b/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts @@ -40,6 +40,8 @@ export function useCliStatus({ "oauth_token", "api_key", "api_key_env", + "credentials_file", + "cli_authenticated", "none", ] as const; type AuthMethod = (typeof validMethods)[number]; diff --git a/apps/app/src/components/views/spec-view.tsx b/apps/app/src/components/views/spec-view.tsx index 87173e9b..3fbf4ffe 100644 --- a/apps/app/src/components/views/spec-view.tsx +++ b/apps/app/src/components/views/spec-view.tsx @@ -14,7 +14,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus } from "lucide-react"; +import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus, CheckCircle2 } from "lucide-react"; +import { toast } from "sonner"; import { Checkbox } from "@/components/ui/checkbox"; import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor"; import type { SpecRegenerationEvent } from "@/types/electron"; @@ -311,14 +312,22 @@ export function SpecView() { // The backend sends explicit signals for completion: // 1. "All tasks completed" in the message // 2. [Phase: complete] marker in logs + // 3. "Spec regeneration complete!" for regeneration + // 4. "Initial spec creation complete!" for creation without features const isFinalCompletionMessage = event.message?.includes("All tasks completed") || event.message === "All tasks completed!" || - event.message === "All tasks completed"; + event.message === "All tasks completed" || + event.message === "Spec regeneration complete!" || + event.message === "Initial spec creation complete!"; const hasCompletePhase = logsRef.current.includes("[Phase: complete]"); + // Intermediate completion means features are being generated after spec creation + const isIntermediateCompletion = event.message?.includes("Features are being generated") || + event.message?.includes("features are being generated"); + // Rely solely on explicit backend signals - const shouldComplete = isFinalCompletionMessage || hasCompletePhase; + const shouldComplete = (isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion; if (shouldComplete) { // Fully complete - clear all states immediately @@ -337,9 +346,29 @@ export function SpecView() { setProjectOverview(""); setErrorMessage(""); stateRestoredRef.current = false; - // Reload the spec to show the new content - loadSpec(); - } else { + + // Reload the spec with delay to ensure file is written to disk + setTimeout(() => { + loadSpec(); + }, SPEC_FILE_WRITE_DELAY); + + // Show success toast notification + const isRegeneration = event.message?.includes("regeneration"); + const isFeatureGeneration = event.message?.includes("Feature generation"); + toast.success( + isFeatureGeneration + ? "Feature Generation Complete" + : isRegeneration + ? "Spec Regeneration Complete" + : "Spec Creation Complete", + { + description: isFeatureGeneration + ? "Features have been created from the app specification." + : "Your app specification has been saved.", + icon: , + } + ); + } else if (isIntermediateCompletion) { // Intermediate completion - keep state active for feature generation setIsCreating(true); setIsRegenerating(true); diff --git a/apps/app/src/store/setup-store.ts b/apps/app/src/store/setup-store.ts index 0265e64b..15714d28 100644 --- a/apps/app/src/store/setup-store.ts +++ b/apps/app/src/store/setup-store.ts @@ -17,6 +17,7 @@ export type ClaudeAuthMethod = | "api_key_env" // ANTHROPIC_API_KEY environment variable | "api_key" // Manually stored API key | "credentials_file" // Generic credentials file detection + | "cli_authenticated" // Claude CLI is installed and has active sessions/activity | "none"; // Claude Auth Status diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 2c4821b2..90238848 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -39,30 +39,30 @@ const PORT = parseInt(process.env.PORT || "3008", 10); const DATA_DIR = process.env.DATA_DIR || "./data"; // Check for required environment variables +// Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use) const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN; -if (!hasAnthropicKey) { +if (!hasAnthropicKey && !hasOAuthToken) { console.warn(` ╔═══════════════════════════════════════════════════════════════════════╗ -║ ⚠️ WARNING: ANTHROPIC_API_KEY not set ║ +║ ⚠️ WARNING: No Claude authentication configured ║ ║ ║ -║ The Claude Agent SDK requires ANTHROPIC_API_KEY to function. ║ -║ ${ - hasOAuthToken - ? " You have CLAUDE_CODE_OAUTH_TOKEN set - this is for CLI auth only." - : "" - } +║ The Claude Agent SDK requires authentication to function. ║ ║ ║ -║ Set your API key: ║ +║ Option 1 - Subscription (OAuth Token): ║ +║ export CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" ║ +║ ║ +║ Option 2 - Pay-per-use (API Key): ║ ║ export ANTHROPIC_API_KEY="sk-ant-..." ║ ║ ║ -║ Or add to apps/server/.env: ║ -║ ANTHROPIC_API_KEY=sk-ant-... ║ +║ Or use the setup wizard in Settings to configure authentication. ║ ╚═══════════════════════════════════════════════════════════════════════╝ `); +} else if (hasOAuthToken) { + console.log("[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)"); } else { - console.log("[Server] ✓ ANTHROPIC_API_KEY detected"); + console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)"); } // Initialize security diff --git a/apps/server/src/routes/setup.ts b/apps/server/src/routes/setup.ts index 48cbec89..a1b5b38b 100644 --- a/apps/server/src/routes/setup.ts +++ b/apps/server/src/routes/setup.ts @@ -107,12 +107,14 @@ export function createSetupRoutes(): Router { } // Check authentication - detect all possible auth methods + // Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth + // apiKeys.anthropic stores direct API keys for pay-per-use let auth = { authenticated: false, method: "none" as string, hasCredentialsFile: false, hasToken: false, - hasStoredOAuthToken: false, + hasStoredOAuthToken: !!apiKeys.anthropic_oauth_token, hasStoredApiKey: !!apiKeys.anthropic, hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY, hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN, @@ -199,9 +201,17 @@ export function createSetupRoutes(): Router { auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var } - // In-memory stored API key (from settings UI) + // In-memory stored OAuth token (from setup wizard - subscription auth) + if (!auth.authenticated && apiKeys.anthropic_oauth_token) { + auth.authenticated = true; + auth.oauthTokenValid = true; + auth.method = "oauth_token"; // Stored OAuth token from setup wizard + } + + // In-memory stored API key (from settings UI - pay-per-use) if (!auth.authenticated && apiKeys.anthropic) { auth.authenticated = true; + auth.apiKeyValid = true; auth.method = "api_key"; // Manually stored API key } @@ -393,9 +403,19 @@ export function createSetupRoutes(): Router { apiKeys[provider] = apiKey; // Also set as environment variable and persist to .env - if (provider === "anthropic" || provider === "anthropic_oauth_token") { + // IMPORTANT: OAuth tokens and API keys must be stored separately + // - OAuth tokens (subscription auth) -> CLAUDE_CODE_OAUTH_TOKEN + // - API keys (pay-per-use) -> ANTHROPIC_API_KEY + if (provider === "anthropic_oauth_token") { + // OAuth token from claude setup-token (subscription-based auth) + process.env.CLAUDE_CODE_OAUTH_TOKEN = apiKey; + await persistApiKeyToEnv("CLAUDE_CODE_OAUTH_TOKEN", apiKey); + console.log("[Setup] Stored OAuth token as CLAUDE_CODE_OAUTH_TOKEN"); + } else if (provider === "anthropic") { + // Direct API key (pay-per-use) process.env.ANTHROPIC_API_KEY = apiKey; await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey); + console.log("[Setup] Stored API key as ANTHROPIC_API_KEY"); } else if (provider === "openai") { process.env.OPENAI_API_KEY = apiKey; await persistApiKeyToEnv("OPENAI_API_KEY", apiKey); diff --git a/apps/server/src/routes/spec-regeneration.ts b/apps/server/src/routes/spec-regeneration.ts index f2409d34..85148f42 100644 --- a/apps/server/src/routes/spec-regeneration.ts +++ b/apps/server/src/routes/spec-regeneration.ts @@ -11,11 +11,28 @@ import type { EventEmitter } from "../lib/events.js"; let isRunning = false; let currentAbortController: AbortController | null = null; +// Helper to log authentication status +function logAuthStatus(context: string): void { + const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN; + const hasApiKey = !!process.env.ANTHROPIC_API_KEY; + + console.log(`[SpecRegeneration] ${context} - Auth Status:`); + console.log(`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${hasOAuthToken ? 'SET (' + process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...)' : 'NOT SET'}`); + console.log(`[SpecRegeneration] ANTHROPIC_API_KEY: ${hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'}`); + + if (!hasOAuthToken && !hasApiKey) { + console.error(`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`); + } +} + export function createSpecRegenerationRoutes(events: EventEmitter): Router { const router = Router(); // Create project spec from overview router.post("/create", async (req: Request, res: Response) => { + console.log("[SpecRegeneration] ========== /create endpoint called =========="); + console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2)); + try { const { projectPath, projectOverview, generateFeatures } = req.body as { projectPath: string; @@ -23,7 +40,13 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { generateFeatures?: boolean; }; + console.log(`[SpecRegeneration] Parsed params:`); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + console.log(`[SpecRegeneration] projectOverview length: ${projectOverview?.length || 0} chars`); + console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`); + if (!projectPath || !projectOverview) { + console.error("[SpecRegeneration] Missing required parameters"); res.status(400).json({ success: false, error: "projectPath and projectOverview required", @@ -32,12 +55,16 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { } if (isRunning) { + console.warn("[SpecRegeneration] Generation already running, rejecting request"); res.json({ success: false, error: "Spec generation already running" }); return; } + logAuthStatus("Before starting generation"); + isRunning = true; currentAbortController = new AbortController(); + console.log("[SpecRegeneration] Starting background generation task..."); // Start generation in background generateSpec( @@ -48,19 +75,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { generateFeatures ) .catch((error) => { - console.error("[SpecRegeneration] Error:", error); + console.error("[SpecRegeneration] ❌ Generation failed with error:"); + console.error("[SpecRegeneration] Error name:", error?.name); + console.error("[SpecRegeneration] Error message:", error?.message); + console.error("[SpecRegeneration] Error stack:", error?.stack); + console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); events.emit("spec-regeneration:event", { type: "spec_error", - error: error.message, + error: error.message || String(error), }); }) .finally(() => { + console.log("[SpecRegeneration] Generation task finished (success or error)"); isRunning = false; currentAbortController = null; }); + console.log("[SpecRegeneration] Returning success response (generation running in background)"); res.json({ success: true }); } catch (error) { + console.error("[SpecRegeneration] ❌ Route handler exception:"); + console.error("[SpecRegeneration] Error:", error); const message = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, error: message }); } @@ -68,13 +103,21 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { // Generate from project definition router.post("/generate", async (req: Request, res: Response) => { + console.log("[SpecRegeneration] ========== /generate endpoint called =========="); + console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2)); + try { const { projectPath, projectDefinition } = req.body as { projectPath: string; projectDefinition: string; }; + console.log(`[SpecRegeneration] Parsed params:`); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + console.log(`[SpecRegeneration] projectDefinition length: ${projectDefinition?.length || 0} chars`); + if (!projectPath || !projectDefinition) { + console.error("[SpecRegeneration] Missing required parameters"); res.status(400).json({ success: false, error: "projectPath and projectDefinition required", @@ -83,12 +126,16 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { } if (isRunning) { + console.warn("[SpecRegeneration] Generation already running, rejecting request"); res.json({ success: false, error: "Spec generation already running" }); return; } + logAuthStatus("Before starting generation"); + isRunning = true; currentAbortController = new AbortController(); + console.log("[SpecRegeneration] Starting background generation task..."); generateSpec( projectPath, @@ -98,19 +145,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { false ) .catch((error) => { - console.error("[SpecRegeneration] Error:", error); + console.error("[SpecRegeneration] ❌ Generation failed with error:"); + console.error("[SpecRegeneration] Error name:", error?.name); + console.error("[SpecRegeneration] Error message:", error?.message); + console.error("[SpecRegeneration] Error stack:", error?.stack); + console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); events.emit("spec-regeneration:event", { type: "spec_error", - error: error.message, + error: error.message || String(error), }); }) .finally(() => { + console.log("[SpecRegeneration] Generation task finished (success or error)"); isRunning = false; currentAbortController = null; }); + console.log("[SpecRegeneration] Returning success response (generation running in background)"); res.json({ success: true }); } catch (error) { + console.error("[SpecRegeneration] ❌ Route handler exception:"); + console.error("[SpecRegeneration] Error:", error); const message = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, error: message }); } @@ -118,37 +173,55 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { // Generate features from existing spec router.post("/generate-features", async (req: Request, res: Response) => { + console.log("[SpecRegeneration] ========== /generate-features endpoint called =========="); + console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2)); + try { const { projectPath } = req.body as { projectPath: string }; + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + if (!projectPath) { + console.error("[SpecRegeneration] Missing projectPath parameter"); res.status(400).json({ success: false, error: "projectPath required" }); return; } if (isRunning) { + console.warn("[SpecRegeneration] Generation already running, rejecting request"); res.json({ success: false, error: "Generation already running" }); return; } + logAuthStatus("Before starting feature generation"); + isRunning = true; currentAbortController = new AbortController(); + console.log("[SpecRegeneration] Starting background feature generation task..."); generateFeaturesFromSpec(projectPath, events, currentAbortController) .catch((error) => { - console.error("[SpecRegeneration] Error:", error); + console.error("[SpecRegeneration] ❌ Feature generation failed with error:"); + console.error("[SpecRegeneration] Error name:", error?.name); + console.error("[SpecRegeneration] Error message:", error?.message); + console.error("[SpecRegeneration] Error stack:", error?.stack); + console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); events.emit("spec-regeneration:event", { type: "features_error", - error: error.message, + error: error.message || String(error), }); }) .finally(() => { + console.log("[SpecRegeneration] Feature generation task finished (success or error)"); isRunning = false; currentAbortController = null; }); + console.log("[SpecRegeneration] Returning success response (generation running in background)"); res.json({ success: true }); } catch (error) { + console.error("[SpecRegeneration] ❌ Route handler exception:"); + console.error("[SpecRegeneration] Error:", error); const message = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, error: message }); } @@ -188,6 +261,11 @@ async function generateSpec( abortController: AbortController, generateFeatures?: boolean ) { + console.log("[SpecRegeneration] ========== generateSpec() started =========="); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + console.log(`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`); + console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`); + const prompt = `You are helping to define a software project specification. Project Overview: @@ -214,6 +292,8 @@ Also generate a list of features to implement. For each feature provide: Format your response as markdown. Be specific and actionable.`; + console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`); + events.emit("spec-regeneration:event", { type: "spec_progress", content: "Starting spec generation...\n", @@ -228,38 +308,78 @@ Format your response as markdown. Be specific and actionable.`; abortController, }; - const stream = query({ prompt, options }); - let responseText = ""; + console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2)); + console.log("[SpecRegeneration] Calling Claude Agent SDK query()..."); + + // Log auth status right before the SDK call + logAuthStatus("Right before SDK query()"); - for await (const msg of stream) { - if (msg.type === "assistant" && msg.message.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText = block.text; - events.emit("spec-regeneration:event", { - type: "spec_progress", - content: block.text, - }); - } else if (block.type === "tool_use") { - events.emit("spec-regeneration:event", { - type: "spec_tool", - tool: block.name, - input: block.input, - }); - } - } - } else if (msg.type === "result" && msg.subtype === "success") { - responseText = msg.result || responseText; - } + let stream; + try { + stream = query({ prompt, options }); + console.log("[SpecRegeneration] query() returned stream successfully"); + } catch (queryError) { + console.error("[SpecRegeneration] ❌ query() threw an exception:"); + console.error("[SpecRegeneration] Error:", queryError); + throw queryError; } + let responseText = ""; + let messageCount = 0; + + console.log("[SpecRegeneration] Starting to iterate over stream..."); + + try { + for await (const msg of stream) { + messageCount++; + console.log(`[SpecRegeneration] Stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)); + + if (msg.type === "assistant" && msg.message.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + responseText = block.text; + console.log(`[SpecRegeneration] Text block received (${block.text.length} chars)`); + events.emit("spec-regeneration:event", { + type: "spec_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + console.log(`[SpecRegeneration] Tool use: ${block.name}`); + events.emit("spec-regeneration:event", { + type: "spec_tool", + tool: block.name, + input: block.input, + }); + } + } + } else if (msg.type === "result" && (msg as any).subtype === "success") { + console.log("[SpecRegeneration] Received success result"); + responseText = (msg as any).result || responseText; + } else if (msg.type === "error") { + console.error("[SpecRegeneration] ❌ Received error message from stream:"); + console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2)); + } + } + } catch (streamError) { + console.error("[SpecRegeneration] ❌ Error while iterating stream:"); + console.error("[SpecRegeneration] Stream error:", streamError); + throw streamError; + } + + console.log(`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`); + console.log(`[SpecRegeneration] Response text length: ${responseText.length} chars`); + // Save spec const specDir = path.join(projectPath, ".automaker"); - const specPath = path.join(specDir, "project-spec.md"); + const specPath = path.join(specDir, "app_spec.txt"); + console.log(`[SpecRegeneration] Saving spec to: ${specPath}`); + await fs.mkdir(specDir, { recursive: true }); await fs.writeFile(specPath, responseText); + console.log("[SpecRegeneration] Spec saved successfully"); + events.emit("spec-regeneration:event", { type: "spec_complete", specPath, @@ -268,8 +388,11 @@ Format your response as markdown. Be specific and actionable.`; // If generate features was requested, parse and create them if (generateFeatures) { + console.log("[SpecRegeneration] Starting feature generation..."); await parseAndCreateFeatures(projectPath, responseText, events); } + + console.log("[SpecRegeneration] ========== generateSpec() completed =========="); } async function generateFeaturesFromSpec( @@ -277,13 +400,20 @@ async function generateFeaturesFromSpec( events: EventEmitter, abortController: AbortController ) { + console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() started =========="); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + // Read existing spec - const specPath = path.join(projectPath, ".automaker", "project-spec.md"); + const specPath = path.join(projectPath, ".automaker", "app_spec.txt"); let spec: string; + console.log(`[SpecRegeneration] Reading spec from: ${specPath}`); + try { spec = await fs.readFile(specPath, "utf-8"); - } catch { + console.log(`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`); + } catch (readError) { + console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError); events.emit("spec-regeneration:event", { type: "features_error", error: "No project spec found. Generate spec first.", @@ -320,6 +450,8 @@ Format as JSON: Generate 5-15 features that build on each other logically.`; + console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`); + events.emit("spec-regeneration:event", { type: "features_progress", content: "Analyzing spec and generating features...\n", @@ -334,26 +466,62 @@ Generate 5-15 features that build on each other logically.`; abortController, }; - const stream = query({ prompt, options }); - let responseText = ""; + console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2)); + console.log("[SpecRegeneration] Calling Claude Agent SDK query() for features..."); + + logAuthStatus("Right before SDK query() for features"); - for await (const msg of stream) { - if (msg.type === "assistant" && msg.message.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText = block.text; - events.emit("spec-regeneration:event", { - type: "features_progress", - content: block.text, - }); - } - } - } else if (msg.type === "result" && msg.subtype === "success") { - responseText = msg.result || responseText; - } + let stream; + try { + stream = query({ prompt, options }); + console.log("[SpecRegeneration] query() returned stream successfully"); + } catch (queryError) { + console.error("[SpecRegeneration] ❌ query() threw an exception:"); + console.error("[SpecRegeneration] Error:", queryError); + throw queryError; } + let responseText = ""; + let messageCount = 0; + + console.log("[SpecRegeneration] Starting to iterate over feature stream..."); + + try { + for await (const msg of stream) { + messageCount++; + console.log(`[SpecRegeneration] Feature stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)); + + if (msg.type === "assistant" && msg.message.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + responseText = block.text; + console.log(`[SpecRegeneration] Feature text block received (${block.text.length} chars)`); + events.emit("spec-regeneration:event", { + type: "features_progress", + content: block.text, + }); + } + } + } else if (msg.type === "result" && (msg as any).subtype === "success") { + console.log("[SpecRegeneration] Received success result for features"); + responseText = (msg as any).result || responseText; + } else if (msg.type === "error") { + console.error("[SpecRegeneration] ❌ Received error message from feature stream:"); + console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2)); + } + } + } catch (streamError) { + console.error("[SpecRegeneration] ❌ Error while iterating feature stream:"); + console.error("[SpecRegeneration] Stream error:", streamError); + throw streamError; + } + + console.log(`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`); + console.log(`[SpecRegeneration] Feature response length: ${responseText.length} chars`); + await parseAndCreateFeatures(projectPath, responseText, events); + + console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() completed =========="); } async function parseAndCreateFeatures( @@ -361,20 +529,31 @@ async function parseAndCreateFeatures( content: string, events: EventEmitter ) { + console.log("[SpecRegeneration] ========== parseAndCreateFeatures() started =========="); + console.log(`[SpecRegeneration] Content length: ${content.length} chars`); + try { // Extract JSON from response + console.log("[SpecRegeneration] Extracting JSON from response..."); const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/); if (!jsonMatch) { + console.error("[SpecRegeneration] ❌ No valid JSON found in response"); + console.error("[SpecRegeneration] Content preview:", content.substring(0, 500)); throw new Error("No valid JSON found in response"); } + console.log(`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`); + const parsed = JSON.parse(jsonMatch[0]); + console.log(`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`); + const featuresDir = path.join(projectPath, ".automaker", "features"); await fs.mkdir(featuresDir, { recursive: true }); const createdFeatures: Array<{ id: string; title: string }> = []; for (const feature of parsed.features) { + console.log(`[SpecRegeneration] Creating feature: ${feature.id}`); const featureDir = path.join(featuresDir, feature.id); await fs.mkdir(featureDir, { recursive: true }); @@ -398,15 +577,21 @@ async function parseAndCreateFeatures( createdFeatures.push({ id: feature.id, title: feature.title }); } + console.log(`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`); + events.emit("spec-regeneration:event", { type: "features_complete", features: createdFeatures, count: createdFeatures.length, }); } catch (error) { + console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:"); + console.error("[SpecRegeneration] Error:", error); events.emit("spec-regeneration:event", { type: "features_error", error: (error as Error).message, }); } + + console.log("[SpecRegeneration] ========== parseAndCreateFeatures() completed =========="); }