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 */}
+
+