working on improving the app spec page

This commit is contained in:
Cody Seibert
2025-12-14 17:38:12 -05:00
parent fa47264c76
commit b3ea506a73
26 changed files with 1283 additions and 871 deletions

View File

@@ -1,6 +1,36 @@
> **[!TIP]**
>
> **Learn more about Agentic Coding!**
>
> Automaker itself was built by a group of engineers using AI and agentic coding techniques to build features faster than ever. By leveraging tools like Cursor IDE and Claude Code CLI, the team orchestrated AI agents to implement complex functionality in days instead of weeks.
>
> **Learn how:** Master these same techniques and workflows in the [Agentic Jumpstart course](https://agenticjumpstart.com/).
# Automaker # Automaker
Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion. **Stop typing code. Start directing AI agents.**
Automaker is an autonomous AI development studio that transforms how you build software. Instead of manually writing every line of code, you describe features on a Kanban board and watch as AI agents powered by Claude Code automatically implement them.
## What Makes Automaker Different?
Traditional development tools help you write code. Automaker helps you **orchestrate AI agents** to build entire features autonomously. Think of it as having a team of AI developers working for you—you define what needs to be built, and Automaker handles the implementation.
### The Workflow
1. **Add Features** - Describe features you want built (with text, images, or screenshots)
2. **Move to "In Progress"** - Automaker automatically assigns an AI agent to implement the feature
3. **Watch It Build** - See real-time progress as the agent writes code, runs tests, and makes changes
4. **Review & Verify** - Review the changes, run tests, and approve when ready
5. **Ship Faster** - Build entire applications in days, not weeks
### Powered by Claude Code
Automaker leverages the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code) to give AI agents full access to your codebase. Agents can read files, write code, execute commands, run tests, and make git commits—all while working in isolated git worktrees to keep your main branch safe.
### Why This Matters
The future of software development is **agentic coding**—where developers become architects directing AI agents rather than manual coders. Automaker puts this future in your hands today, letting you experience what it's like to build software 10x faster with AI agents handling the implementation while you focus on architecture and business logic.
--- ---
@@ -36,20 +66,22 @@ cd automaker
# 2. Install dependencies # 2. Install dependencies
npm install npm install
# 3. Get your Claude Code OAuth token # 3. Run Automaker (pick your mode)
claude setup-token npm run dev
# ⚠️ This prints your token - don't share your screen! # Then choose your run mode when prompted, or use specific commands below
# 4. Set the token and run
export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat01-..."
npm run dev:electron
``` ```
## How to Run ## How to Run
### Development Modes ### Development Mode
Automaker can be run in several modes: Start Automaker in development mode:
```bash
npm run dev
```
This will prompt you to choose your run mode, or you can specify a mode directly:
#### Electron Desktop App (Recommended) #### Electron Desktop App (Recommended)
@@ -72,8 +104,6 @@ npm run dev:electron:wsl:gpu
```bash ```bash
# Run in web browser (http://localhost:3007) # Run in web browser (http://localhost:3007)
npm run dev:web npm run dev:web
# or
npm run dev
``` ```
### Building for Production ### Building for Production

View File

@@ -381,8 +381,6 @@ export function Sidebar() {
toast.success("App specification created", { toast.success("App specification created", {
description: "Your project is now set up and ready to go!", description: "Your project is now set up and ready to go!",
}); });
// Navigate to spec view to show the new spec
setCurrentView("spec");
} else if (event.type === "spec_regeneration_error") { } else if (event.type === "spec_regeneration_error") {
setSpecCreatingForProject(null); setSpecCreatingForProject(null);
toast.error("Failed to create specification", { toast.error("Failed to create specification", {

View File

@@ -31,6 +31,8 @@ import { getHttpApiClient } from "@/lib/http-api-client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useFileBrowser } from "@/contexts/file-browser-context"; import { useFileBrowser } from "@/contexts/file-browser-context";
const LAST_PROJECT_DIR_KEY = "automaker:lastProjectDir";
interface ValidationErrors { interface ValidationErrors {
projectName?: boolean; projectName?: boolean;
workspaceDir?: boolean; workspaceDir?: boolean;
@@ -80,6 +82,14 @@ export function NewProjectModal({
// Fetch workspace directory when modal opens // Fetch workspace directory when modal opens
useEffect(() => { useEffect(() => {
if (open) { if (open) {
// First, check localStorage for last used directory
const lastUsedDir = localStorage.getItem(LAST_PROJECT_DIR_KEY);
if (lastUsedDir) {
setWorkspaceDir(lastUsedDir);
return;
}
// Fall back to server config if no saved directory
setIsLoadingWorkspace(true); setIsLoadingWorkspace(true);
const httpClient = getHttpApiClient(); const httpClient = getHttpApiClient();
httpClient.workspace httpClient.workspace
@@ -201,6 +211,8 @@ export function NewProjectModal({
}); });
if (selectedPath) { if (selectedPath) {
setWorkspaceDir(selectedPath); setWorkspaceDir(selectedPath);
// Save to localStorage for next time
localStorage.setItem(LAST_PROJECT_DIR_KEY, selectedPath);
// Clear any workspace error when a valid directory is selected // Clear any workspace error when a valid directory is selected
if (errors.workspaceDir) { if (errors.workspaceDir) {
setErrors((prev) => ({ ...prev, workspaceDir: false })); setErrors((prev) => ({ ...prev, workspaceDir: false }));

View File

@@ -1554,6 +1554,13 @@ export function BoardView() {
} }
}); });
// Sort backlog by priority: 1 (high) -> 2 (medium) -> 3 (low) -> no priority
map.backlog.sort((a, b) => {
const aPriority = a.priority ?? 999; // Features without priority go last
const bPriority = b.priority ?? 999;
return aPriority - bPriority;
});
return map; return map;
}, [features, runningAutoTasks, searchQuery]); }, [features, runningAutoTasks, searchQuery]);

View File

@@ -57,6 +57,7 @@ import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Brain, Brain,
Flag,
} from "lucide-react"; } from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer"; import { CountUpTimer } from "@/components/ui/count-up-timer";
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from "@/lib/electron";
@@ -89,6 +90,33 @@ function formatThinkingLevel(level: ThinkingLevel | undefined): string {
return labels[level]; return labels[level];
} }
/**
* Formats priority for display
*/
function formatPriority(priority: number | undefined): string | null {
if (!priority) return null;
const labels: Record<number, string> = {
1: "High",
2: "Medium",
3: "Low",
};
return labels[priority] || null;
}
/**
* Gets priority badge color classes
*/
function getPriorityBadgeClasses(priority: number | undefined): string {
if (priority === 1) {
return "bg-red-500/20 border border-red-500/50 text-red-400";
} else if (priority === 2) {
return "bg-yellow-500/20 border border-yellow-500/50 text-yellow-400";
} else if (priority === 3) {
return "bg-blue-500/20 border border-blue-500/50 text-blue-400";
}
return "";
}
interface KanbanCardProps { interface KanbanCardProps {
feature: Feature; feature: Feature;
onEdit: () => void; onEdit: () => void;
@@ -198,6 +226,34 @@ export const KanbanCard = memo(function KanbanCard({
return () => clearInterval(interval); return () => clearInterval(interval);
}, [feature.justFinishedAt, feature.status, currentTime]); }, [feature.justFinishedAt, feature.status, currentTime]);
// Calculate priority badge position
const priorityLabel = formatPriority(feature.priority);
const hasPriority = !!priorityLabel;
// Calculate top position for badges (stacking vertically)
const getBadgeTopPosition = (badgeIndex: number) => {
return badgeIndex === 0
? "top-2"
: badgeIndex === 1
? "top-8"
: badgeIndex === 2
? "top-14"
: "top-20";
};
// Determine badge positions (must be after isJustFinished is defined)
let badgeIndex = 0;
const priorityBadgeIndex = hasPriority ? badgeIndex++ : -1;
const skipTestsBadgeIndex =
feature.skipTests && !feature.error ? badgeIndex++ : -1;
const errorBadgeIndex = feature.error ? badgeIndex++ : -1;
const justFinishedBadgeIndex = isJustFinished ? badgeIndex++ : -1;
const branchBadgeIndex =
hasWorktree && !isCurrentAutoTask ? badgeIndex++ : -1;
// Total number of badges displayed
const totalBadgeCount = badgeIndex;
// Load context file for in_progress, waiting_approval, and verified features // Load context file for in_progress, waiting_approval, and verified features
useEffect(() => { useEffect(() => {
const loadContext = async () => { const loadContext = async () => {
@@ -353,12 +409,29 @@ export const KanbanCard = memo(function KanbanCard({
style={{ opacity: opacity / 100 }} style={{ opacity: opacity / 100 }}
/> />
)} )}
{/* Priority badge */}
{hasPriority && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
getBadgeTopPosition(priorityBadgeIndex),
"left-2",
getPriorityBadgeClasses(feature.priority)
)}
data-testid={`priority-badge-${feature.id}`}
title={`Priority: ${priorityLabel}`}
>
<Flag className="w-3 h-3" />
<span>{priorityLabel}</span>
</div>
)}
{/* Skip Tests indicator badge */} {/* Skip Tests indicator badge */}
{feature.skipTests && !feature.error && ( {feature.skipTests && !feature.error && (
<div <div
className={cn( className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10", "absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
"top-2 left-2", getBadgeTopPosition(skipTestsBadgeIndex),
"left-2",
"bg-orange-500/20 border border-orange-500/50 text-orange-400" "bg-orange-500/20 border border-orange-500/50 text-orange-400"
)} )}
data-testid={`skip-tests-badge-${feature.id}`} data-testid={`skip-tests-badge-${feature.id}`}
@@ -373,7 +446,8 @@ export const KanbanCard = memo(function KanbanCard({
<div <div
className={cn( className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10", "absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
"top-2 left-2", getBadgeTopPosition(errorBadgeIndex),
"left-2",
"bg-red-500/20 border border-red-500/50 text-red-400" "bg-red-500/20 border border-red-500/50 text-red-400"
)} )}
data-testid={`error-badge-${feature.id}`} data-testid={`error-badge-${feature.id}`}
@@ -388,7 +462,8 @@ export const KanbanCard = memo(function KanbanCard({
<div <div
className={cn( className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10", "absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
feature.skipTests ? "top-8 left-2" : "top-2 left-2", getBadgeTopPosition(justFinishedBadgeIndex),
"left-2",
"bg-green-500/20 border border-green-500/50 text-green-400 animate-pulse" "bg-green-500/20 border border-green-500/50 text-green-400 animate-pulse"
)} )}
data-testid={`just-finished-badge-${feature.id}`} data-testid={`just-finished-badge-${feature.id}`}
@@ -407,10 +482,8 @@ export const KanbanCard = memo(function KanbanCard({
className={cn( className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default", "absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default",
"bg-purple-500/20 border border-purple-500/50 text-purple-400", "bg-purple-500/20 border border-purple-500/50 text-purple-400",
// Position below other badges if present, otherwise use normal position getBadgeTopPosition(branchBadgeIndex),
feature.error || feature.skipTests || isJustFinished "left-2"
? "top-8 left-2"
: "top-2 left-2"
)} )}
data-testid={`branch-badge-${feature.id}`} data-testid={`branch-badge-${feature.id}`}
> >
@@ -432,11 +505,11 @@ export const KanbanCard = memo(function KanbanCard({
className={cn( className={cn(
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout "p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
// Add extra top padding when badges are present to prevent text overlap // Add extra top padding when badges are present to prevent text overlap
(feature.skipTests || feature.error || isJustFinished) && "pt-10", // Calculate padding based on number of badges
// Add even more top padding when both badges and branch are shown totalBadgeCount === 1 && "pt-10",
hasWorktree && totalBadgeCount === 2 && "pt-14",
(feature.skipTests || feature.error || isJustFinished) && totalBadgeCount === 3 && "pt-20",
"pt-14" totalBadgeCount >= 4 && "pt-24"
)} )}
> >
{isCurrentAutoTask && ( {isCurrentAutoTask && (

View File

@@ -22,7 +22,6 @@ import {
Loader2, Loader2,
FilePlus2, FilePlus2,
AlertCircle, AlertCircle,
ListPlus,
CheckCircle2, CheckCircle2,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -47,12 +46,17 @@ export function SpecView() {
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false); const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
const [projectDefinition, setProjectDefinition] = useState(""); const [projectDefinition, setProjectDefinition] = useState("");
const [isRegenerating, setIsRegenerating] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false);
const [generateFeaturesOnRegenerate, setGenerateFeaturesOnRegenerate] =
useState(true);
const [analyzeProjectOnRegenerate, setAnalyzeProjectOnRegenerate] =
useState(true);
// Create spec state // Create spec state
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
const [projectOverview, setProjectOverview] = useState(""); const [projectOverview, setProjectOverview] = useState("");
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [generateFeatures, setGenerateFeatures] = useState(true); const [generateFeatures, setGenerateFeatures] = useState(true);
const [analyzeProjectOnCreate, setAnalyzeProjectOnCreate] = useState(true);
// Generate features only state // Generate features only state
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false); const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
@@ -66,6 +70,7 @@ export function SpecView() {
const [errorMessage, setErrorMessage] = useState<string>(""); const [errorMessage, setErrorMessage] = useState<string>("");
const statusCheckRef = useRef<boolean>(false); const statusCheckRef = useRef<boolean>(false);
const stateRestoredRef = useRef<boolean>(false); const stateRestoredRef = useRef<boolean>(false);
const pendingStatusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Load spec from file // Load spec from file
const loadSpec = useCallback(async () => { const loadSpec = useCallback(async () => {
@@ -99,6 +104,26 @@ export function SpecView() {
loadSpec(); loadSpec();
}, [loadSpec]); }, [loadSpec]);
// Reset all spec regeneration state when project changes
useEffect(() => {
// Clear all state when switching projects
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setErrorMessage("");
setLogs("");
logsRef.current = "";
stateRestoredRef.current = false;
statusCheckRef.current = false;
// Clear any pending timeout
if (pendingStatusTimeoutRef.current) {
clearTimeout(pendingStatusTimeoutRef.current);
pendingStatusTimeoutRef.current = null;
}
}, [currentProject?.path]);
// Check if spec regeneration is running when component mounts or project changes // Check if spec regeneration is running when component mounts or project changes
useEffect(() => { useEffect(() => {
const checkStatus = async () => { const checkStatus = async () => {
@@ -113,40 +138,44 @@ export function SpecView() {
} }
const status = await api.specRegeneration.status(); const status = await api.specRegeneration.status();
console.log("[SpecView] Status check on mount:", status); console.log(
"[SpecView] Status check on mount:",
status,
"for project:",
currentProject.path
);
if (status.success && status.isRunning) { if (status.success && status.isRunning) {
// Something is running - restore state using backend's authoritative phase // Something is running globally, but we can't verify it's for this project
// since the backend doesn't track projectPath in status
// Tentatively show loader - events will confirm if it's for this project
console.log( console.log(
"[SpecView] Spec generation is running - restoring state", "[SpecView] Spec generation is running globally. Tentatively showing loader, waiting for events to confirm project match."
{ phase: status.currentPhase }
); );
if (!stateRestoredRef.current) { // Tentatively set state - events will confirm or clear it
setIsCreating(true); setIsCreating(true);
setIsRegenerating(true); setIsRegenerating(true);
stateRestoredRef.current = true;
}
// Use the backend's currentPhase directly - single source of truth
if (status.currentPhase) { if (status.currentPhase) {
setCurrentPhase(status.currentPhase); setCurrentPhase(status.currentPhase);
} else { } else {
setCurrentPhase("in progress"); setCurrentPhase("initialization");
} }
// Add resume message to logs if needed // Set a timeout to clear state if no events arrive for this project within 3 seconds
if (!logsRef.current) { if (pendingStatusTimeoutRef.current) {
const resumeMessage = clearTimeout(pendingStatusTimeoutRef.current);
"[Status] Resumed monitoring existing spec generation process...\n";
logsRef.current = resumeMessage;
setLogs(resumeMessage);
} else if (!logsRef.current.includes("Resumed monitoring")) {
const resumeMessage =
"\n[Status] Resumed monitoring existing spec generation process...\n";
logsRef.current = logsRef.current + resumeMessage;
setLogs(logsRef.current);
} }
pendingStatusTimeoutRef.current = setTimeout(() => {
// If no events confirmed this is for current project, clear state
console.log(
"[SpecView] No events received for current project - clearing tentative state"
);
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase("");
pendingStatusTimeoutRef.current = null;
}, 3000);
} else if (status.success && !status.isRunning) { } else if (status.success && !status.isRunning) {
// Not running - clear all state // Not running - clear all state
setIsCreating(false); setIsCreating(false);
@@ -274,6 +303,8 @@ export function SpecView() {
// Subscribe to spec regeneration events // Subscribe to spec regeneration events
useEffect(() => { useEffect(() => {
if (!currentProject) return;
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.specRegeneration) return; if (!api.specRegeneration) return;
@@ -283,7 +314,9 @@ export function SpecView() {
"[SpecView] Regeneration event:", "[SpecView] Regeneration event:",
event.type, event.type,
"for project:", "for project:",
event.projectPath event.projectPath,
"current project:",
currentProject?.path
); );
// Only handle events for the current project // Only handle events for the current project
@@ -292,7 +325,20 @@ export function SpecView() {
return; return;
} }
// Clear any pending timeout since we received an event for this project
if (pendingStatusTimeoutRef.current) {
clearTimeout(pendingStatusTimeoutRef.current);
pendingStatusTimeoutRef.current = null;
console.log(
"[SpecView] Event confirmed this is for current project - clearing timeout"
);
}
if (event.type === "spec_regeneration_progress") { if (event.type === "spec_regeneration_progress") {
// Ensure state is set when we receive events for this project
setIsCreating(true);
setIsRegenerating(true);
// Extract phase from content if present // Extract phase from content if present
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/); const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
if (phaseMatch) { if (phaseMatch) {
@@ -475,7 +521,7 @@ export function SpecView() {
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [loadSpec]); }, [currentProject?.path, loadSpec, errorMessage, currentPhase]);
// Save spec to file // Save spec to file
const saveSpec = async () => { const saveSpec = async () => {
@@ -505,12 +551,16 @@ export function SpecView() {
if (!currentProject || !projectDefinition.trim()) return; if (!currentProject || !projectDefinition.trim()) return;
setIsRegenerating(true); setIsRegenerating(true);
setShowRegenerateDialog(false);
setCurrentPhase("initialization"); setCurrentPhase("initialization");
setErrorMessage(""); setErrorMessage("");
// Reset logs when starting new regeneration // Reset logs when starting new regeneration
logsRef.current = ""; logsRef.current = "";
setLogs(""); setLogs("");
console.log("[SpecView] Starting spec regeneration"); console.log(
"[SpecView] Starting spec regeneration, generateFeatures:",
generateFeaturesOnRegenerate
);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.specRegeneration) { if (!api.specRegeneration) {
@@ -520,7 +570,9 @@ export function SpecView() {
} }
const result = await api.specRegeneration.generate( const result = await api.specRegeneration.generate(
currentProject.path, currentProject.path,
projectDefinition.trim() projectDefinition.trim(),
generateFeaturesOnRegenerate,
analyzeProjectOnRegenerate
); );
if (!result.success) { if (!result.success) {
@@ -570,7 +622,8 @@ export function SpecView() {
const result = await api.specRegeneration.create( const result = await api.specRegeneration.create(
currentProject.path, currentProject.path,
projectOverview.trim(), projectOverview.trim(),
generateFeatures generateFeatures,
analyzeProjectOnCreate
); );
if (!result.success) { if (!result.success) {
@@ -839,6 +892,33 @@ export function SpecView() {
/> />
</div> </div>
<div className="flex items-start space-x-3 pt-2">
<Checkbox
id="analyze-project-create"
checked={analyzeProjectOnCreate}
onCheckedChange={(checked) =>
setAnalyzeProjectOnCreate(checked === true)
}
disabled={isCreating}
/>
<div className="space-y-1">
<label
htmlFor="analyze-project-create"
className={`text-sm font-medium ${
isCreating ? "" : "cursor-pointer"
}`}
>
Analyze current project for additional context
</label>
<p className="text-xs text-muted-foreground">
If checked, the agent will research your existing codebase
to understand the tech stack. If unchecked, defaults to
TanStack Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind
CSS, and React.
</p>
</div>
</div>
<div className="flex items-start space-x-3 pt-2"> <div className="flex items-start space-x-3 pt-2">
<Checkbox <Checkbox
id="generate-features" id="generate-features"
@@ -1052,27 +1132,61 @@ export function SpecView() {
disabled={isRegenerating} disabled={isRegenerating}
/> />
</div> </div>
<div className="flex items-start space-x-3 pt-2">
<Checkbox
id="analyze-project-regenerate"
checked={analyzeProjectOnRegenerate}
onCheckedChange={(checked) =>
setAnalyzeProjectOnRegenerate(checked === true)
}
disabled={isRegenerating}
/>
<div className="space-y-1">
<label
htmlFor="analyze-project-regenerate"
className={`text-sm font-medium ${
isRegenerating ? "" : "cursor-pointer"
}`}
>
Analyze current project for additional context
</label>
<p className="text-xs text-muted-foreground">
If checked, the agent will research your existing codebase to
understand the tech stack. If unchecked, defaults to TanStack
Start, Drizzle ORM, PostgreSQL, shadcn/ui, Tailwind CSS, and
React.
</p>
</div>
</div>
<div className="flex items-start space-x-3 pt-2">
<Checkbox
id="generate-features-regenerate"
checked={generateFeaturesOnRegenerate}
onCheckedChange={(checked) =>
setGenerateFeaturesOnRegenerate(checked === true)
}
disabled={isRegenerating}
/>
<div className="space-y-1">
<label
htmlFor="generate-features-regenerate"
className={`text-sm font-medium ${
isRegenerating ? "" : "cursor-pointer"
}`}
>
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder from the
implementation roadmap after the spec is regenerated.
</p>
</div>
</div>
</div> </div>
<DialogFooter className="flex justify-between sm:justify-between"> <DialogFooter>
<Button
variant="outline"
onClick={handleGenerateFeatures}
disabled={isRegenerating || isGeneratingFeatures}
title="Generate features from the existing app_spec.txt without regenerating the spec"
>
{isGeneratingFeatures ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<ListPlus className="w-4 h-4 mr-2" />
Generate Features
</>
)}
</Button>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="ghost" variant="ghost"

View File

@@ -147,11 +147,14 @@ export interface SpecRegenerationAPI {
create: ( create: (
projectPath: string, projectPath: string,
projectOverview: string, projectOverview: string,
generateFeatures?: boolean generateFeatures?: boolean,
analyzeProject?: boolean
) => Promise<{ success: boolean; error?: string }>; ) => Promise<{ success: boolean; error?: string }>;
generate: ( generate: (
projectPath: string, projectPath: string,
projectDefinition: string projectDefinition: string,
generateFeatures?: boolean,
analyzeProject?: boolean
) => Promise<{ success: boolean; error?: string }>; ) => Promise<{ success: boolean; error?: string }>;
generateFeatures: (projectPath: string) => Promise<{ generateFeatures: (projectPath: string) => Promise<{
success: boolean; success: boolean;
@@ -1850,7 +1853,11 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
return { success: true }; return { success: true };
}, },
generate: async (projectPath: string, projectDefinition: string) => { generate: async (
projectPath: string,
projectDefinition: string,
generateFeatures = false
) => {
if (mockSpecRegenerationRunning) { if (mockSpecRegenerationRunning) {
return { return {
success: false, success: false,
@@ -1859,10 +1866,16 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
} }
mockSpecRegenerationRunning = true; mockSpecRegenerationRunning = true;
console.log(`[Mock] Regenerating spec for: ${projectPath}`); console.log(
`[Mock] Regenerating spec for: ${projectPath}, generateFeatures: ${generateFeatures}`
);
// Simulate async spec regeneration // Simulate async spec regeneration
simulateSpecRegeneration(projectPath, projectDefinition); simulateSpecRegeneration(
projectPath,
projectDefinition,
generateFeatures
);
return { success: true }; return { success: true };
}, },
@@ -2007,7 +2020,8 @@ async function simulateSpecCreation(
async function simulateSpecRegeneration( async function simulateSpecRegeneration(
projectPath: string, projectPath: string,
projectDefinition: string projectDefinition: string,
generateFeatures = false
) { ) {
mockSpecRegenerationPhase = "initialization"; mockSpecRegenerationPhase = "initialization";
emitSpecRegenerationEvent({ emitSpecRegenerationEvent({
@@ -2056,6 +2070,25 @@ async function simulateSpecRegeneration(
</core_capabilities> </core_capabilities>
</project_specification>`; </project_specification>`;
if (generateFeatures) {
mockSpecRegenerationPhase = "spec_complete";
emitSpecRegenerationEvent({
type: "spec_regeneration_progress",
content:
"[Phase: spec_complete] Spec regenerated! Generating features...\n",
projectPath: projectPath,
});
await new Promise((resolve) => {
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
});
if (!mockSpecRegenerationRunning) return;
// Simulate feature generation
await simulateFeatureGeneration(projectPath);
if (!mockSpecRegenerationRunning) return;
}
mockSpecRegenerationPhase = "complete"; mockSpecRegenerationPhase = "complete";
emitSpecRegenerationEvent({ emitSpecRegenerationEvent({
type: "spec_regeneration_complete", type: "spec_regeneration_complete",

View File

@@ -581,17 +581,26 @@ export class HttpApiClient implements ElectronAPI {
create: ( create: (
projectPath: string, projectPath: string,
projectOverview: string, projectOverview: string,
generateFeatures?: boolean generateFeatures?: boolean,
analyzeProject?: boolean
) => ) =>
this.post("/api/spec-regeneration/create", { this.post("/api/spec-regeneration/create", {
projectPath, projectPath,
projectOverview, projectOverview,
generateFeatures, generateFeatures,
analyzeProject,
}), }),
generate: (projectPath: string, projectDefinition: string) => generate: (
projectPath: string,
projectDefinition: string,
generateFeatures?: boolean,
analyzeProject?: boolean
) =>
this.post("/api/spec-regeneration/generate", { this.post("/api/spec-regeneration/generate", {
projectPath, projectPath,
projectDefinition, projectDefinition,
generateFeatures,
analyzeProject,
}), }),
generateFeatures: (projectPath: string) => generateFeatures: (projectPath: string) =>
this.post("/api/spec-regeneration/generate-features", { projectPath }), this.post("/api/spec-regeneration/generate-features", { projectPath }),

View File

@@ -286,6 +286,7 @@ export interface Feature {
model?: AgentModel; // Model to use for this feature (defaults to opus) model?: AgentModel; // Model to use for this feature (defaults to opus)
thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none) thinkingLevel?: ThinkingLevel; // Thinking level for extended thinking (defaults to none)
error?: string; // Error message if the agent errored during processing error?: string; // Error message if the agent errored during processing
priority?: number; // Priority: 1 = high, 2 = medium, 3 = low
// Worktree info - set when a feature is being worked on in an isolated git worktree // Worktree info - set when a feature is being worked on in an isolated git worktree
worktreePath?: string; // Path to the worktree directory worktreePath?: string; // Path to the worktree directory
branchName?: string; // Name of the feature branch branchName?: string; // Name of the feature branch

View File

@@ -266,7 +266,8 @@ export interface SpecRegenerationAPI {
create: ( create: (
projectPath: string, projectPath: string,
projectOverview: string, projectOverview: string,
generateFeatures?: boolean generateFeatures?: boolean,
analyzeProject?: boolean
) => Promise<{ ) => Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
@@ -274,7 +275,9 @@ export interface SpecRegenerationAPI {
generate: ( generate: (
projectPath: string, projectPath: string,
projectDefinition: string projectDefinition: string,
generateFeatures?: boolean,
analyzeProject?: boolean
) => Promise<{ ) => Promise<{
success: boolean; success: boolean;
error?: string; error?: string;

View File

@@ -0,0 +1,74 @@
/**
* Simple logger utility with log levels
* Configure via LOG_LEVEL environment variable: error, warn, info, debug
* Defaults to 'info' if not set
*/
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
}
const LOG_LEVEL_NAMES: Record<string, LogLevel> = {
error: LogLevel.ERROR,
warn: LogLevel.WARN,
info: LogLevel.INFO,
debug: LogLevel.DEBUG,
};
let currentLogLevel: LogLevel = LogLevel.INFO;
// Initialize log level from environment variable
const envLogLevel = process.env.LOG_LEVEL?.toLowerCase();
if (envLogLevel && LOG_LEVEL_NAMES[envLogLevel] !== undefined) {
currentLogLevel = LOG_LEVEL_NAMES[envLogLevel];
}
/**
* Create a logger instance with a context prefix
*/
export function createLogger(context: string) {
const prefix = `[${context}]`;
return {
error: (...args: unknown[]): void => {
if (currentLogLevel >= LogLevel.ERROR) {
console.error(prefix, ...args);
}
},
warn: (...args: unknown[]): void => {
if (currentLogLevel >= LogLevel.WARN) {
console.warn(prefix, ...args);
}
},
info: (...args: unknown[]): void => {
if (currentLogLevel >= LogLevel.INFO) {
console.log(prefix, ...args);
}
},
debug: (...args: unknown[]): void => {
if (currentLogLevel >= LogLevel.DEBUG) {
console.log(prefix, "[DEBUG]", ...args);
}
},
};
}
/**
* Get the current log level
*/
export function getLogLevel(): LogLevel {
return currentLogLevel;
}
/**
* Set the log level programmatically (useful for testing)
*/
export function setLogLevel(level: LogLevel): void {
currentLogLevel = level;
}

View File

@@ -0,0 +1,73 @@
/**
* Common utilities and state management for spec regeneration
*/
import { createLogger } from "../../lib/logger.js";
const logger = createLogger("SpecRegeneration");
// Shared state for tracking generation status
export let isRunning = false;
export let currentAbortController: AbortController | null = null;
/**
* Set the running state and abort controller
*/
export function setRunningState(
running: boolean,
controller: AbortController | null = null
): void {
isRunning = running;
currentAbortController = controller;
}
/**
* Helper to log authentication status
*/
export function logAuthStatus(context: string): void {
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
logger.info(`${context} - Auth Status:`);
logger.info(
` CLAUDE_CODE_OAUTH_TOKEN: ${
hasOAuthToken
? "SET (" +
process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) +
"...)"
: "NOT SET"
}`
);
logger.info(
` ANTHROPIC_API_KEY: ${
hasApiKey
? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
: "NOT SET"
}`
);
if (!hasOAuthToken && !hasApiKey) {
logger.warn("⚠️ WARNING: No authentication configured! SDK will fail.");
}
}
/**
* Log error details consistently
*/
export function logError(error: unknown, context: string): void {
logger.error(`${context}:`);
logger.error("Error name:", (error as any)?.name);
logger.error("Error message:", (error as Error)?.message);
logger.error("Error stack:", (error as Error)?.stack);
logger.error(
"Full error object:",
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
);
}
/**
* Get error message from error object
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Unknown error";
}

View File

@@ -0,0 +1,154 @@
/**
* Generate features from existing app_spec.txt
*/
import { query, type Options } 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 { logAuthStatus } from "./common.js";
import { parseAndCreateFeatures } from "./parse-and-create-features.js";
const logger = createLogger("SpecRegeneration");
export async function generateFeaturesFromSpec(
projectPath: string,
events: EventEmitter,
abortController: AbortController
): Promise<void> {
logger.debug("========== generateFeaturesFromSpec() started ==========");
logger.debug("projectPath:", projectPath);
// Read existing spec
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
let spec: string;
logger.debug("Reading spec from:", specPath);
try {
spec = await fs.readFile(specPath, "utf-8");
logger.info(`Spec loaded successfully (${spec.length} chars)`);
} catch (readError) {
logger.error("❌ Failed to read spec file:", readError);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: "No project spec found. Generate spec first.",
projectPath: projectPath,
});
return;
}
const prompt = `Based on this project specification:
${spec}
Generate a prioritized list of implementable features. For each feature provide:
1. **id**: A unique lowercase-hyphenated identifier
2. **title**: Short descriptive title
3. **description**: What this feature does (2-3 sentences)
4. **priority**: 1 (high), 2 (medium), or 3 (low)
5. **complexity**: "simple", "moderate", or "complex"
6. **dependencies**: Array of feature IDs this depends on (can be empty)
Format as JSON:
{
"features": [
{
"id": "feature-id",
"title": "Feature Title",
"description": "What it does",
"priority": 1,
"complexity": "moderate",
"dependencies": []
}
]
}
Generate 5-15 features that build on each other logically.`;
logger.debug("Prompt length:", `${prompt.length} chars`);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: "Analyzing spec and generating features...\n",
projectPath: projectPath,
});
const options: Options = {
model: "claude-sonnet-4-20250514",
maxTurns: 5,
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...");
logAuthStatus("Right before SDK query() for features");
let stream;
try {
stream = query({ prompt, options });
logger.debug("query() returned stream successfully");
} catch (queryError) {
logger.error("❌ query() threw an exception:");
logger.error("Error:", queryError);
throw queryError;
}
let responseText = "";
let messageCount = 0;
logger.debug("Starting to iterate over feature stream...");
try {
for await (const msg of stream) {
messageCount++;
logger.debug(
`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;
logger.debug(
`Feature text block received (${block.text.length} chars)`
);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
}
}
} else if (msg.type === "result" && (msg as any).subtype === "success") {
logger.debug("Received success result for features");
responseText = (msg as any).result || responseText;
} else if ((msg as { type: string }).type === "error") {
logger.error("❌ Received error message from feature stream:");
logger.error("Error message:", JSON.stringify(msg, null, 2));
}
}
} catch (streamError) {
logger.error("❌ Error while iterating feature stream:");
logger.error("Stream error:", streamError);
throw streamError;
}
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
logger.info(`Feature response length: ${responseText.length} chars`);
await parseAndCreateFeatures(projectPath, responseText, events);
logger.debug("========== generateFeaturesFromSpec() completed ==========");
}

View File

@@ -0,0 +1,203 @@
/**
* Generate app_spec.txt from project overview
*/
import { query, type Options } 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 { logAuthStatus } from "./common.js";
import { generateFeaturesFromSpec } from "./generate-features-from-spec.js";
const logger = createLogger("SpecRegeneration");
export async function generateSpec(
projectPath: string,
projectOverview: string,
events: EventEmitter,
abortController: AbortController,
generateFeatures?: boolean,
analyzeProject?: boolean
): Promise<void> {
logger.debug("========== generateSpec() started ==========");
logger.debug("projectPath:", projectPath);
logger.debug("projectOverview length:", `${projectOverview.length} chars`);
logger.debug("generateFeatures:", generateFeatures);
logger.debug("analyzeProject:", analyzeProject);
// Build the prompt based on whether we should analyze the project
let analysisInstructions = "";
let techStackDefaults = "";
if (analyzeProject !== false) {
// Default to true - analyze the project
analysisInstructions = `Based on this overview, analyze the project directory (if it exists) and create a comprehensive specification. Use the Read, Glob, and Grep tools to explore the codebase and understand:
- Existing technologies and frameworks
- Project structure and architecture
- Current features and capabilities
- Code patterns and conventions`;
} else {
// Use default tech stack
techStackDefaults = `Default Technology Stack:
- Framework: TanStack Start (React-based full-stack framework)
- Database: PostgreSQL with Drizzle ORM
- UI Components: shadcn/ui
- Styling: Tailwind CSS
- Frontend: React
Use these technologies as the foundation for the specification.`;
}
const prompt = `You are helping to define a software project specification.
IMPORTANT: Never ask for clarification or additional information. Use the information provided and make reasonable assumptions to create the best possible specification. If details are missing, infer them based on common patterns and best practices.
Project Overview:
${projectOverview}
${techStackDefaults}
${analysisInstructions}
${getAppSpecFormatInstruction()}`;
logger.debug("Prompt length:", `${prompt.length} chars`);
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: "Starting spec generation...\n",
});
const options: Options = {
model: "claude-opus-4-5-20251101",
maxTurns: 10,
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()...");
// Log auth status right before the SDK call
logAuthStatus("Right before SDK query()");
let stream;
try {
stream = query({ prompt, options });
logger.debug("query() returned stream successfully");
} catch (queryError) {
logger.error("❌ query() threw an exception:");
logger.error("Error:", queryError);
throw queryError;
}
let responseText = "";
let messageCount = 0;
logger.debug("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
)
);
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,
});
}
}
} else if (msg.type === "result" && (msg as any).subtype === "success") {
logger.debug("Received success result");
responseText = (msg as any).result || responseText;
} else if ((msg as { type: string }).type === "error") {
logger.error("❌ Received error message from stream:");
logger.error("Error message:", JSON.stringify(msg, null, 2));
}
}
} catch (streamError) {
logger.error("❌ Error while iterating stream:");
logger.error("Stream error:", streamError);
throw streamError;
}
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
logger.info(`Response text length: ${responseText.length} chars`);
// Save spec
const specDir = path.join(projectPath, ".automaker");
const specPath = path.join(specDir, "app_spec.txt");
logger.info("Saving spec to:", specPath);
await fs.mkdir(specDir, { recursive: true });
await fs.writeFile(specPath, responseText);
logger.info("Spec saved successfully");
// Emit spec completion event
if (generateFeatures) {
// If features will be generated, emit intermediate completion
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: "[Phase: spec_complete] Spec created! Generating features...\n",
projectPath: projectPath,
});
} else {
// If no features, emit final completion
events.emit("spec-regeneration:event", {
type: "spec_regeneration_complete",
message: "Spec regeneration complete!",
projectPath: projectPath,
});
}
// If generate features was requested, generate them from the spec
if (generateFeatures) {
logger.info("Starting feature generation from spec...");
// Create a new abort controller for feature generation
const featureAbortController = new AbortController();
try {
await generateFeaturesFromSpec(
projectPath,
events,
featureAbortController
);
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
} catch (featureError) {
logger.error("Feature generation failed:", featureError);
// Don't throw - spec generation succeeded, feature generation is optional
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: (featureError as Error).message || "Feature generation failed",
projectPath: projectPath,
});
}
}
logger.debug("========== generateSpec() completed ==========");
}

View File

@@ -0,0 +1,23 @@
/**
* Spec Regeneration routes - HTTP API for AI-powered spec generation
*/
import { Router } from "express";
import type { EventEmitter } from "../../lib/events.js";
import { createCreateHandler } from "./routes/create.js";
import { createGenerateHandler } from "./routes/generate.js";
import { createGenerateFeaturesHandler } from "./routes/generate-features.js";
import { createStopHandler } from "./routes/stop.js";
import { createStatusHandler } from "./routes/status.js";
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
const router = Router();
router.post("/create", createCreateHandler(events));
router.post("/generate", createGenerateHandler(events));
router.post("/generate-features", createGenerateFeaturesHandler(events));
router.post("/stop", createStopHandler());
router.get("/status", createStatusHandler());
return router;
}

View File

@@ -0,0 +1,83 @@
/**
* Parse agent response and create feature files
*/
import path from "path";
import fs from "fs/promises";
import type { EventEmitter } from "../../lib/events.js";
import { createLogger } from "../../lib/logger.js";
const logger = createLogger("SpecRegeneration");
export async function parseAndCreateFeatures(
projectPath: string,
content: string,
events: EventEmitter
): Promise<void> {
logger.debug("========== parseAndCreateFeatures() started ==========");
logger.debug("Content length:", `${content.length} chars`);
try {
// Extract JSON from response
logger.debug("Extracting JSON from response...");
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));
throw new Error("No valid JSON found in response");
}
logger.debug(`JSON match found (${jsonMatch[0].length} chars)`);
const parsed = JSON.parse(jsonMatch[0]);
logger.info(`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) {
logger.debug("Creating feature:", feature.id);
const featureDir = path.join(featuresDir, feature.id);
await fs.mkdir(featureDir, { recursive: true });
const featureData = {
id: feature.id,
title: feature.title,
description: feature.description,
status: "backlog", // Features go to backlog - user must manually start them
priority: feature.priority || 2,
complexity: feature.complexity || "moderate",
dependencies: feature.dependencies || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await fs.writeFile(
path.join(featureDir, "feature.json"),
JSON.stringify(featureData, null, 2)
);
createdFeatures.push({ id: feature.id, title: feature.title });
}
logger.info(`✓ Created ${createdFeatures.length} features successfully`);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_complete",
message: `Spec regeneration complete! Created ${createdFeatures.length} features.`,
projectPath: projectPath,
});
} catch (error) {
logger.error("❌ parseAndCreateFeatures() failed:");
logger.error("Error:", error);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: (error as Error).message,
projectPath: projectPath,
});
}
logger.debug("========== parseAndCreateFeatures() completed ==========");
}

View File

@@ -0,0 +1,95 @@
/**
* POST /create endpoint - Create project spec from overview
*/
import type { Request, Response } from "express";
import type { EventEmitter } from "../../../lib/events.js";
import { createLogger } from "../../../lib/logger.js";
import {
isRunning,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from "../common.js";
import { generateSpec } from "../generate-spec.js";
const logger = createLogger("SpecRegeneration");
export function createCreateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /create endpoint called ==========");
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
try {
const { projectPath, projectOverview, generateFeatures, analyzeProject } =
req.body as {
projectPath: string;
projectOverview: string;
generateFeatures?: boolean;
analyzeProject?: boolean;
};
logger.debug("Parsed params:");
logger.debug(" projectPath:", projectPath);
logger.debug(
" projectOverview length:",
`${projectOverview?.length || 0} chars`
);
logger.debug(" generateFeatures:", generateFeatures);
logger.debug(" analyzeProject:", analyzeProject);
if (!projectPath || !projectOverview) {
logger.error("Missing required parameters");
res.status(400).json({
success: false,
error: "projectPath and projectOverview required",
});
return;
}
if (isRunning) {
logger.warn("Generation already running, rejecting request");
res.json({ success: false, error: "Spec generation already running" });
return;
}
logAuthStatus("Before starting generation");
const abortController = new AbortController();
setRunningState(true, abortController);
logger.info("Starting background generation task...");
// Start generation in background
generateSpec(
projectPath,
projectOverview,
events,
abortController,
generateFeatures,
analyzeProject
)
.catch((error) => {
logError(error, "Generation failed with error");
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: getErrorMessage(error),
projectPath: projectPath,
});
})
.finally(() => {
logger.info("Generation task finished (success or error)");
setRunningState(false, null);
});
logger.info(
"Returning success response (generation running in background)"
);
res.json({ success: true });
} catch (error) {
logger.error("❌ Route handler exception:");
logger.error("Error:", error);
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,70 @@
/**
* POST /generate-features endpoint - Generate features from existing spec
*/
import type { Request, Response } from "express";
import type { EventEmitter } from "../../../lib/events.js";
import { createLogger } from "../../../lib/logger.js";
import {
isRunning,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from "../common.js";
import { generateFeaturesFromSpec } from "../generate-features-from-spec.js";
const logger = createLogger("SpecRegeneration");
export function createGenerateFeaturesHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /generate-features endpoint called ==========");
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
try {
const { projectPath } = req.body as { projectPath: string };
logger.debug("projectPath:", projectPath);
if (!projectPath) {
logger.error("Missing projectPath parameter");
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
if (isRunning) {
logger.warn("Generation already running, rejecting request");
res.json({ success: false, error: "Generation already running" });
return;
}
logAuthStatus("Before starting feature generation");
const abortController = new AbortController();
setRunningState(true, abortController);
logger.info("Starting background feature generation task...");
generateFeaturesFromSpec(projectPath, events, abortController)
.catch((error) => {
logError(error, "Feature generation failed with error");
events.emit("spec-regeneration:event", {
type: "features_error",
error: getErrorMessage(error),
});
})
.finally(() => {
logger.info("Feature generation task finished (success or error)");
setRunningState(false, null);
});
logger.info(
"Returning success response (generation running in background)"
);
res.json({ success: true });
} catch (error) {
logger.error("❌ Route handler exception:");
logger.error("Error:", error);
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,98 @@
/**
* POST /generate endpoint - Generate spec from project definition
*/
import type { Request, Response } from "express";
import type { EventEmitter } from "../../../lib/events.js";
import { createLogger } from "../../../lib/logger.js";
import {
isRunning,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from "../common.js";
import { generateSpec } from "../generate-spec.js";
const logger = createLogger("SpecRegeneration");
export function createGenerateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /generate endpoint called ==========");
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
try {
const {
projectPath,
projectDefinition,
generateFeatures,
analyzeProject,
} = req.body as {
projectPath: string;
projectDefinition: string;
generateFeatures?: boolean;
analyzeProject?: boolean;
};
logger.debug("Parsed params:");
logger.debug(" projectPath:", projectPath);
logger.debug(
" projectDefinition length:",
`${projectDefinition?.length || 0} chars`
);
logger.debug(" generateFeatures:", generateFeatures);
logger.debug(" analyzeProject:", analyzeProject);
if (!projectPath || !projectDefinition) {
logger.error("Missing required parameters");
res.status(400).json({
success: false,
error: "projectPath and projectDefinition required",
});
return;
}
if (isRunning) {
logger.warn("Generation already running, rejecting request");
res.json({ success: false, error: "Spec generation already running" });
return;
}
logAuthStatus("Before starting generation");
const abortController = new AbortController();
setRunningState(true, abortController);
logger.info("Starting background generation task...");
generateSpec(
projectPath,
projectDefinition,
events,
abortController,
generateFeatures,
analyzeProject
)
.catch((error) => {
logError(error, "Generation failed with error");
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: getErrorMessage(error),
projectPath: projectPath,
});
})
.finally(() => {
logger.info("Generation task finished (success or error)");
setRunningState(false, null);
});
logger.info(
"Returning success response (generation running in background)"
);
res.json({ success: true });
} catch (error) {
logger.error("❌ Route handler exception:");
logger.error("Error:", error);
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,16 @@
/**
* GET /status endpoint - Get generation status
*/
import type { Request, Response } from "express";
import { isRunning, getErrorMessage } from "../common.js";
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
res.json({ success: true, isRunning });
} catch (error) {
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,24 @@
/**
* POST /stop endpoint - Stop generation
*/
import type { Request, Response } from "express";
import {
currentAbortController,
setRunningState,
getErrorMessage,
} from "../common.js";
export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
if (currentAbortController) {
currentAbortController.abort();
}
setRunningState(false, null);
res.json({ success: true });
} catch (error) {
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -1,764 +1,8 @@
/** /**
* Spec Regeneration routes - HTTP API for AI-powered spec generation * Spec Regeneration routes - HTTP API for AI-powered spec generation
*
* This file re-exports from the app-spec directory to maintain backward compatibility.
* The actual implementation has been split into smaller modules in ./app-spec/
*/ */
import { Router, type Request, type Response } from "express"; export { createSpecRegenerationRoutes } from "./app-spec/index.js";
import { query, type Options } 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";
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;
projectOverview: string;
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",
});
return;
}
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(
projectPath,
projectOverview,
events,
currentAbortController,
generateFeatures
)
.catch((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_regeneration_error",
error: error.message || String(error),
projectPath: projectPath,
});
})
.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 });
}
});
// 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",
});
return;
}
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,
projectDefinition,
events,
currentAbortController,
false
)
.catch((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_regeneration_error",
error: error.message || String(error),
projectPath: projectPath,
});
})
.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 });
}
});
// 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] ❌ 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 || 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 });
}
});
// Stop generation
router.post("/stop", async (_req: Request, res: Response) => {
try {
if (currentAbortController) {
currentAbortController.abort();
}
isRunning = false;
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get status
router.get("/status", async (_req: Request, res: Response) => {
try {
res.json({ success: true, isRunning });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}
async function generateSpec(
projectPath: string,
projectOverview: string,
events: EventEmitter,
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:
${projectOverview}
Based on this overview, analyze the project directory (if it exists) and create a comprehensive specification. Use the Read, Glob, and Grep tools to explore the codebase and understand:
- Existing technologies and frameworks
- Project structure and architecture
- Current features and capabilities
- Code patterns and conventions
${getAppSpecFormatInstruction()}`;
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: "Starting spec generation...\n",
});
const options: Options = {
model: "claude-opus-4-5-20251101",
maxTurns: 10,
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "acceptEdits",
abortController,
};
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()");
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_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
} 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 as { type: string }).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, "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");
// Emit spec completion event
if (generateFeatures) {
// If features will be generated, emit intermediate completion
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: "[Phase: spec_complete] Spec created! Generating features...\n",
projectPath: projectPath,
});
} else {
// If no features, emit final completion
events.emit("spec-regeneration:event", {
type: "spec_regeneration_complete",
message: "Spec regeneration complete!",
projectPath: projectPath,
});
}
// If generate features was requested, generate them from the spec
if (generateFeatures) {
console.log("[SpecRegeneration] Starting feature generation from spec...");
// Create a new abort controller for feature generation
const featureAbortController = new AbortController();
try {
await generateFeaturesFromSpec(
projectPath,
events,
featureAbortController
);
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
} catch (featureError) {
console.error(
"[SpecRegeneration] Feature generation failed:",
featureError
);
// Don't throw - spec generation succeeded, feature generation is optional
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: (featureError as Error).message || "Feature generation failed",
projectPath: projectPath,
});
}
}
console.log(
"[SpecRegeneration] ========== generateSpec() completed =========="
);
}
async function generateFeaturesFromSpec(
projectPath: string,
events: EventEmitter,
abortController: AbortController
) {
console.log(
"[SpecRegeneration] ========== generateFeaturesFromSpec() started =========="
);
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
// Read existing spec
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");
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: "spec_regeneration_error",
error: "No project spec found. Generate spec first.",
projectPath: projectPath,
});
return;
}
const prompt = `Based on this project specification:
${spec}
Generate a prioritized list of implementable features. For each feature provide:
1. **id**: A unique lowercase-hyphenated identifier
2. **title**: Short descriptive title
3. **description**: What this feature does (2-3 sentences)
4. **priority**: 1 (high), 2 (medium), or 3 (low)
5. **complexity**: "simple", "moderate", or "complex"
6. **dependencies**: Array of feature IDs this depends on (can be empty)
Format as JSON:
{
"features": [
{
"id": "feature-id",
"title": "Feature Title",
"description": "What it does",
"priority": 1,
"complexity": "moderate",
"dependencies": []
}
]
}
Generate 5-15 features that build on each other logically.`;
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: "Analyzing spec and generating features...\n",
projectPath: projectPath,
});
const options: Options = {
model: "claude-sonnet-4-20250514",
maxTurns: 5,
cwd: projectPath,
allowedTools: ["Read", "Glob"],
permissionMode: "acceptEdits",
abortController,
};
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");
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: "spec_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
}
}
} 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 as { type: string }).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(
projectPath: string,
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 });
const featureData = {
id: feature.id,
title: feature.title,
description: feature.description,
status: "backlog", // Features go to backlog - user must manually start them
priority: feature.priority || 2,
complexity: feature.complexity || "moderate",
dependencies: feature.dependencies || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await fs.writeFile(
path.join(featureDir, "feature.json"),
JSON.stringify(featureData, null, 2)
);
createdFeatures.push({ id: feature.id, title: feature.title });
}
console.log(
`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`
);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_complete",
message: `Spec regeneration complete! Created ${createdFeatures.length} features.`,
projectPath: projectPath,
});
} catch (error) {
console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:");
console.error("[SpecRegeneration] Error:", error);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
error: (error as Error).message,
projectPath: projectPath,
});
}
console.log(
"[SpecRegeneration] ========== parseAndCreateFeatures() completed =========="
);
}

View File

@@ -1129,7 +1129,7 @@ When done, summarize what you implemented and any notes for the developer.`;
if (msg.type === "assistant" && msg.message?.content) { if (msg.type === "assistant" && msg.message?.content) {
for (const block of msg.message.content) { for (const block of msg.message.content) {
if (block.type === "text") { if (block.type === "text") {
responseText = block.text || ""; responseText += block.text || "";
// Check for authentication errors in the response // Check for authentication errors in the response
if ( if (

View File

@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/Users/webdevcody/Workspace/claude-quickstarts/autonomous-coding/generations/automaker/logs/.bca9a6447fd747ee10d232b47d6c5442658e2b34-audit.json",
"files": [
{
"date": 1765139456329,
"name": "/Users/webdevcody/Workspace/claude-quickstarts/autonomous-coding/generations/automaker/logs/mcp-puppeteer-2025-12-07.log",
"hash": "bc94aff428bef4ee0318298404b592183f4191664fc0acee519ec9a995c543cc"
}
],
"hashType": "sha256"
}

View File

@@ -1,10 +0,0 @@
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-12-07 15:30:56.370"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-12-07 15:30:56.371"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-12-07 15:52:16.721"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-12-07 15:52:16.722"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-12-07 16:06:03.450"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-12-07 16:06:03.451"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-12-07 16:27:18.491"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-12-07 16:27:18.492"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-12-07 16:37:07.299"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-12-07 16:37:07.300"}

View File

@@ -8,7 +8,7 @@
], ],
"scripts": { "scripts": {
"postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\"", "postinstall": "node -e \"const fs=require('fs');if(process.platform==='darwin'){['darwin-arm64','darwin-x64'].forEach(a=>{const p='node_modules/node-pty/prebuilds/'+a+'/spawn-helper';if(fs.existsSync(p))fs.chmodSync(p,0o755)})}\"",
"dev": "npm run dev --workspace=apps/app", "dev": "./init.sh",
"dev:web": "npm run dev:web --workspace=apps/app", "dev:web": "npm run dev:web --workspace=apps/app",
"dev:electron": "npm run dev:electron --workspace=apps/app", "dev:electron": "npm run dev:electron --workspace=apps/app",
"dev:electron:debug": "npm run dev:electron:debug --workspace=apps/app", "dev:electron:debug": "npm run dev:electron:debug --workspace=apps/app",