From 2b799e490793ff7a4c7c87f909642dd5efc9eda9 Mon Sep 17 00:00:00 2001 From: mawkone Date: Tue, 19 May 2026 19:32:07 -0700 Subject: [PATCH] feat(ai): automate end-to-end PRD, architecture, and task generation directly from Objective --- .../project/[projectId]/(home)/plan/page.tsx | 196 +++++++++++++----- .../[projectId]/plan/generate/route.ts | 172 +++++++++++++++ 2 files changed, 315 insertions(+), 53 deletions(-) create mode 100644 app/api/projects/[projectId]/plan/generate/route.ts diff --git a/app/[workspace]/project/[projectId]/(home)/plan/page.tsx b/app/[workspace]/project/[projectId]/(home)/plan/page.tsx index 6f8369e7..82ba44f4 100644 --- a/app/[workspace]/project/[projectId]/(home)/plan/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/plan/page.tsx @@ -14,7 +14,7 @@ type Plan = { decisions: Array<{ id: string; title: string; choice: string; why?: string }>; }; -type Tab = "objective" | "prd"; +type Tab = "objective" | "prd" | "delegate"; // Shared Theme Variables const INK = { @@ -79,6 +79,7 @@ export default function PlanPageV2() { }}> setActiveTab("objective")} icon={} label="Objective" /> setActiveTab("prd")} icon={} label="PRD" /> + setActiveTab("delegate")} icon={} label="Delegate" /> @@ -95,6 +96,10 @@ export default function PlanPageV2() { +
+ +
+ ); } @@ -148,11 +153,36 @@ function ObjectiveView({ plan, projectId, onChange }: { plan: Plan, projectId: s }} placeholder="Describe the business objective..." /> -
- - +
+ + +
) : ( @@ -224,26 +254,7 @@ function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string, > Tech Architecture - + {/* RIGHT COLUMN: The Document Content */} @@ -259,43 +270,122 @@ function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string, }}> {activeDoc === "spec" && (
-

Product Specification

-

The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.

-
-
- -

Spec is empty.

-

Use Architect mode to generate the Product Spec.

-
+ {plan.decisions?.find(d => d.id === "prd_spec")?.why ? ( + + {plan.decisions.find(d => d.id === "prd_spec")!.why!} + + ) : ( + <> +

Product Specification

+

The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.

+
+
+ +

Spec is empty.

+

Click "Generate Complete PRD" on the Objective tab to generate this document.

+
+ + )}
)} {activeDoc === "plan" && (
-

Technical Architecture

-

The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.

-
-
- -

Plan is empty.

-

Use Architect mode to define the technical approach.

-
+ {plan.decisions?.find(d => d.id === "prd_arch")?.why ? ( + + {plan.decisions.find(d => d.id === "prd_arch")!.why!} + + ) : ( + <> +

Technical Architecture

+

The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.

+
+
+ +

Plan is empty.

+

Click "Generate Complete PRD" on the Objective tab to generate this document.

+
+ + )}
)} - {activeDoc === "tasks" && ( -
-
-

Execution Plan

- -
-

The atomic, dependency-ordered tasks the AI will execute to build the application.

-
-
- -

No tasks queued.

-

Generate the task breakdown from the Product Spec.

+ +
+
+ ); +} + +// ────────────────────────────────────────────────── +// 3. DELEGATE VIEW (The Factory) +// ────────────────────────────────────────────────── +function DelegateView({ plan, projectId, onChange }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) { + const [delegating, setDelegating] = useState(false); + const openTasks = plan.tasks.filter(t => t.status === "open"); + + const handleDelegate = async () => { + if (!confirm("Start the background runner to build this feature? You can safely close your browser while it works.")) return; + setDelegating(true); + + try { + const r = await fetch(`/api/projects/${projectId}/agent/sessions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + appName: "frontend", + appPath: ".", + task: "Execute all open tasks in the Execution Plan sequentially.", + }), + }); + if (!r.ok) { + const err = await r.json().catch(() => ({})); + throw new Error(err.error || `HTTP ${r.status}`); + } + alert("Background runner started successfully!"); + } catch (err) { + alert(`Failed to start runner: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setDelegating(false); + } + }; + + return ( +
+
+
+

Execution Plan

+

The prioritized roadmap for the AI background runner to execute.

+
+ +
+ +
+ {openTasks.length > 0 ? openTasks.map(t => ( +
+
+
+
+
{t.title}
+ {t.description &&
{t.description}
} +
+ Queued +
+ )) : ( +
+ +

No tasks queued.

+

Use Architect mode to generate the task breakdown.

)}
diff --git a/app/api/projects/[projectId]/plan/generate/route.ts b/app/api/projects/[projectId]/plan/generate/route.ts new file mode 100644 index 00000000..f26894ff --- /dev/null +++ b/app/api/projects/[projectId]/plan/generate/route.ts @@ -0,0 +1,172 @@ +import { NextResponse } from "next/server"; +import { authSession } from "@/lib/auth/session-server"; +import { query, queryOne } from "@/lib/db-postgres"; +import { callVibnChat } from "@/lib/ai/vibn-chat-model"; + +// Generate a complete PRD, Tech Architecture, and Execution Plan based on the objective + +export async function POST( + req: Request, + ctx: { params: Promise<{ projectId: string }> } +) { + const { projectId } = await ctx.params; + const session = await authSession(); + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const projectRow = await queryOne<{ data: any; slug: string }>( + `SELECT data, slug FROM fs_projects + JOIN fs_users u ON u.id = fs_projects.user_id + WHERE fs_projects.id = $1 AND u.data->>'email' = $2 LIMIT 1`, + [projectId, session.user.email] + ); + if (!projectRow) return NextResponse.json({ error: "Project not found" }, { status: 404 }); + + const body = await req.json().catch(() => ({})); + const objective = String(body.objective || projectRow.data?.plan?.vision || "").trim(); + + if (!objective) { + return NextResponse.json({ error: "Objective is required to generate a PRD." }, { status: 400 }); + } + + // We run three sequential LLM calls to construct the complete Spec Kit documentation + + // 1. Generate the Product Spec (User Stories & Requirements) + const specPrompt = `You are a Principal Product Manager writing a Product Requirements Document (PRD). +Based on the following Objective, generate a structured PRD using Markdown. +DO NOT talk about technology (databases, frameworks, APIs). Focus purely on user value and capabilities. + +OBJECTIVE: +${objective} + +You MUST follow this exact structure: + +## User Scenarios & Testing +List the 3-5 most critical User Stories, prioritized. +### User Story 1 - [Brief Title] (Priority: P1) +[Description] +**Acceptance Scenarios**: +1. **Given** [state], **When** [action], **Then** [outcome] + +### User Story 2 - [Brief Title] (Priority: P2) +[Repeat...] + +## Functional Requirements +- **FR-001**: System MUST [capability] +- **FR-002**: Users MUST be able to [interaction] + +## Success Criteria +- **SC-001**: [Measurable business outcome]`; + + const specResponse = await callVibnChat({ + systemPrompt: specPrompt, + messages: [{ role: "user", content: "Generate the PRD." }], + temperature: 0.2 + }); + const specMd = specResponse.text || "Failed to generate PRD."; + + // 2. Generate the Technical Architecture (Plan) + const planPrompt = `You are a Principal Software Architect. +Based on the following PRD, define the Technical Architecture required to build it. +Your choices must default to: Next.js (App Router), Postgres, Tailwind CSS, NextAuth, and Stripe (if payments). + +PRD: +${specMd} + +You MUST follow this exact structure: + +## Technical Context +- **Language/Framework**: Next.js 15 (App Router), TypeScript +- **Database**: Postgres (via Prisma or raw SQL) +- **Styling**: Tailwind CSS +- **Authentication**: [Decide auth strategy] + +## Data Model +List the core database tables and their relationships. +- **[EntityName]**: [fields...] + +## File Structure +Provide a high-level file tree representing the required Next.js pages and API routes. +\`\`\`text +src/app/ + page.tsx + api/ +\`\`\``; + + const planResponse = await callVibnChat({ + systemPrompt: planPrompt, + messages: [{ role: "user", content: "Generate the Technical Architecture." }], + temperature: 0.1 + }); + const planMd = planResponse.text || "Failed to generate Architecture."; + + // 3. Generate the Execution Plan (Tasks) + const tasksPrompt = `You are a Technical Project Manager. +Based on the PRD and Technical Architecture, break the implementation down into an atomic, dependency-ordered Task List. +You MUST format each task EXACTLY as a markdown bullet with a checkbox, a phase tag, and a description containing the file paths. + +PRD: +${specMd} + +ARCHITECTURE: +${planMd} + +You MUST follow this exact structure: + +## Phase 1: Setup & Foundation +- [ ] [Phase 1] Initialize Next.js project and layout +- [ ] [Phase 1] Setup Postgres database schema + +## Phase 2: [User Story 1 Title] +- [ ] [US1] Build API route for [feature] +- [ ] [US1] Build frontend UI component in src/app/[route]/page.tsx + +## Phase 3: [User Story 2 Title] +- [ ] [US2] ...`; + + const tasksResponse = await callVibnChat({ + systemPrompt: tasksPrompt, + messages: [{ role: "user", content: "Generate the Execution Plan tasks." }], + temperature: 0.1 + }); + const tasksMd = tasksResponse.text || ""; + + // 4. Parse the tasks string into the JSON array expected by the DB + // Extract all `- [ ] [Group] Task description` lines + const taskLines = tasksMd.match(/- \[[ x]\] \[(.*?)\] (.*)/g) || []; + const parsedTasks = taskLines.map(line => { + const match = line.match(/- \[[ x]\] \[(.*?)\] (.*)/); + if (!match) return null; + return { + id: Math.random().toString(36).slice(2, 11), + title: `[${match[1]}] ${match[2]}`, // Fixed backticks + description: "", + status: "open", + createdAt: new Date().toISOString() + }; + }).filter(Boolean); + + // 5. Update the Database Plan + const currentPlan = projectRow.data?.plan || {}; + + // Save the PRD under decisions (we can render it specially in the UI) + const newDecisions = [ + { id: "prd_spec", title: "Product Specification", choice: "Auto-generated PRD", why: specMd }, + { id: "prd_arch", title: "Technical Architecture", choice: "Auto-generated Plan", why: planMd } + ]; + + const updatedPlan = { + ...currentPlan, + vision: objective, + tasks: parsedTasks.length > 0 ? parsedTasks : currentPlan.tasks, + decisions: newDecisions + }; + + await query( + `UPDATE fs_projects SET data = jsonb_set(data, '{plan}', $2::jsonb) WHERE id = $1`, + [projectId, JSON.stringify(updatedPlan)] + ); + + return NextResponse.json({ plan: updatedPlan }); +}