diff --git a/.claude/commands/create-spec.md b/.claude/commands/create-spec.md index 9c8aa8c..f8cae28 100644 --- a/.claude/commands/create-spec.md +++ b/.claude/commands/create-spec.md @@ -473,7 +473,50 @@ After: **CRITICAL:** You must create exactly **25** features using the `feature **Verify the update:** After editing, read the file again to confirm the feature count appears correctly. If `[FEATURE_COUNT]` still appears in the file, the update failed and you must try again. -**Note:** You do NOT need to update `coding_prompt.md` - the coding agent works through features one at a time regardless of total count. +**Note:** You may also update `coding_prompt.md` if the user requests changes to how the coding agent should work. Include it in the status file if modified. + +## 3. Write Status File (REQUIRED - Do This Last) + +**Output path:** `$ARGUMENTS/prompts/.spec_status.json` + +**CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear. + +Write this JSON file: + +```json +{ + "status": "complete", + "version": 1, + "timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]", + "files_written": [ + "prompts/app_spec.txt", + "prompts/initializer_prompt.md" + ], + "feature_count": [the feature count from Phase 4L] +} +``` + +**Include ALL files you modified** in the `files_written` array. If the user asked you to also modify `coding_prompt.md`, include it: + +```json +{ + "status": "complete", + "version": 1, + "timestamp": "2025-01-15T14:30:00.000Z", + "files_written": [ + "prompts/app_spec.txt", + "prompts/initializer_prompt.md", + "prompts/coding_prompt.md" + ], + "feature_count": 35 +} +``` + +**IMPORTANT:** +- Write this file LAST, after all other files are successfully written +- Only write it when you consider ALL requested work complete +- The UI polls this file to detect completion and show the Continue button +- If the user asks for additional changes after you've written this, you may update it again when the new changes are complete --- @@ -487,7 +530,9 @@ Once files are generated, tell the user what to do next: > - `$ARGUMENTS/prompts/app_spec.txt` > - `$ARGUMENTS/prompts/initializer_prompt.md` > -> **Next step:** Type `/exit` to exit this Claude session. The autonomous coding agent will start automatically. +> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent! +> +> **If you don't see the button:** Type `/exit` or click **Exit to Project** in the header. > > **Important timing expectations:** > diff --git a/server/routers/spec_creation.py b/server/routers/spec_creation.py index 2ce66be..639f1b5 100644 --- a/server/routers/spec_creation.py +++ b/server/routers/spec_creation.py @@ -98,6 +98,67 @@ async def cancel_session(project_name: str): return {"success": True, "message": "Session cancelled"} +class SpecFileStatus(BaseModel): + """Status of spec files on disk (from .spec_status.json).""" + exists: bool + status: str # "complete" | "in_progress" | "not_started" + feature_count: Optional[int] = None + timestamp: Optional[str] = None + files_written: list[str] = [] + + +@router.get("/status/{project_name}", response_model=SpecFileStatus) +async def get_spec_file_status(project_name: str): + """ + Get spec creation status by reading .spec_status.json from the project. + + This is used for polling to detect when Claude has finished writing spec files. + Claude writes this status file as the final step after completing all spec work. + """ + if not validate_project_name(project_name): + raise HTTPException(status_code=400, detail="Invalid project name") + + project_dir = _get_project_path(project_name) + if not project_dir: + raise HTTPException(status_code=404, detail="Project not found in registry") + + if not project_dir.exists(): + raise HTTPException(status_code=404, detail="Project directory not found") + + status_file = project_dir / "prompts" / ".spec_status.json" + + if not status_file.exists(): + return SpecFileStatus( + exists=False, + status="not_started", + feature_count=None, + timestamp=None, + files_written=[], + ) + + try: + data = json.loads(status_file.read_text(encoding="utf-8")) + return SpecFileStatus( + exists=True, + status=data.get("status", "unknown"), + feature_count=data.get("feature_count"), + timestamp=data.get("timestamp"), + files_written=data.get("files_written", []), + ) + except json.JSONDecodeError as e: + logger.warning(f"Invalid JSON in spec status file: {e}") + return SpecFileStatus( + exists=True, + status="error", + feature_count=None, + timestamp=None, + files_written=[], + ) + except Exception as e: + logger.error(f"Error reading spec status file: {e}") + raise HTTPException(status_code=500, detail="Failed to read status file") + + # ============================================================================ # WebSocket Endpoint # ============================================================================ diff --git a/ui/src/components/NewProjectModal.tsx b/ui/src/components/NewProjectModal.tsx index b6bcfa8..2590d7e 100644 --- a/ui/src/components/NewProjectModal.tsx +++ b/ui/src/components/NewProjectModal.tsx @@ -148,6 +148,12 @@ export function NewProjectModal({ setSpecMethod(null) } + const handleExitToProject = () => { + // Exit chat and go directly to project - user can start agent manually + onProjectCreated(projectName.trim()) + handleClose() + } + const handleClose = () => { setStep('name') setProjectName('') @@ -178,6 +184,7 @@ export function NewProjectModal({ projectName={projectName.trim()} onComplete={handleSpecComplete} onCancel={handleChatCancel} + onExitToProject={handleExitToProject} initializerStatus={initializerStatus} initializerError={initializerError} onRetryInitializer={handleRetryInitializer} diff --git a/ui/src/components/SpecCreationChat.tsx b/ui/src/components/SpecCreationChat.tsx index 578cd55..acfb24d 100644 --- a/ui/src/components/SpecCreationChat.tsx +++ b/ui/src/components/SpecCreationChat.tsx @@ -6,7 +6,7 @@ */ import { useCallback, useEffect, useRef, useState } from 'react' -import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip } from 'lucide-react' +import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink } from 'lucide-react' import { useSpecChat } from '../hooks/useSpecChat' import { ChatMessage } from './ChatMessage' import { QuestionOptions } from './QuestionOptions' @@ -23,6 +23,7 @@ interface SpecCreationChatProps { projectName: string onComplete: (specPath: string, yoloMode?: boolean) => void onCancel: () => void + onExitToProject: () => void // Exit to project without starting agent initializerStatus?: InitializerStatus initializerError?: string | null onRetryInitializer?: () => void @@ -32,6 +33,7 @@ export function SpecCreationChat({ projectName, onComplete, onCancel, + onExitToProject, initializerStatus = 'idle', initializerError = null, onRetryInitializer, @@ -86,6 +88,13 @@ export function SpecCreationChat({ // Allow sending if there's text OR attachments if ((!trimmed && pendingAttachments.length === 0) || isLoading) return + // Detect /exit command - exit to project without sending to Claude + if (/^\s*\/exit\s*$/i.test(trimmed)) { + setInput('') + onExitToProject() + return + } + sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined) setInput('') setPendingAttachments([]) // Clear attachments after sending @@ -210,6 +219,16 @@ export function SpecCreationChat({ )} + {/* Exit to Project - always visible escape hatch */} + +