feat(ai): automate end-to-end PRD, architecture, and task generation directly from Objective

This commit is contained in:
2026-05-19 19:32:07 -07:00
parent 14541c32aa
commit 02de32958f
2 changed files with 315 additions and 53 deletions

View File

@@ -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() {
}}>
<TabButton active={activeTab === "objective"} onClick={() => setActiveTab("objective")} icon={<Target size={14} />} label="Objective" />
<TabButton active={activeTab === "prd"} onClick={() => setActiveTab("prd")} icon={<FileText size={14} />} label="PRD" />
<TabButton active={activeTab === "delegate"} onClick={() => setActiveTab("delegate")} icon={<Layers size={14} />} label="Delegate" />
</div>
</div>
@@ -95,6 +96,10 @@ export default function PlanPageV2() {
<PRDView plan={plan} projectId={projectId} onChange={setPlan} />
</div>
<div style={{ display: activeTab === "delegate" ? "block" : "none" }}>
<DelegateView plan={plan} projectId={projectId} onChange={setPlan} />
</div>
</div>
);
}
@@ -148,11 +153,36 @@ function ObjectiveView({ plan, projectId, onChange }: { plan: Plan, projectId: s
}}
placeholder="Describe the business objective..."
/>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, padding: "12px 20px", background: INK.bgHover, borderTop: `1px solid ${INK.border}` }}>
<button onClick={() => setEditing(false)} className="btn-secondary">Cancel</button>
<button onClick={save} disabled={saving} className="btn-primary">
{saving ? "Saving..." : "Save Objective"}
<div style={{ display: "flex", justifyContent: "space-between", padding: "12px 20px", background: INK.bgHover, borderTop: `1px solid ${INK.border}` }}>
<button
onClick={async () => {
if (!confirm("This will overwrite the PRD and Execution Plan based on the current objective. Continue?")) return;
setSaving(true);
try {
const r = await fetch(`/api/projects/${projectId}/plan/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ objective: draft }),
});
const d = await r.json();
if (d.plan) onChange(d.plan);
} finally {
setSaving(false);
setEditing(false);
}
}}
disabled={saving}
className="btn-secondary"
style={{ background: "#fff", borderColor: INK.border }}
>
{saving ? "Generating PRD..." : "Generate Complete PRD"}
</button>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => setEditing(false)} className="btn-ghost">Cancel</button>
<button onClick={save} disabled={saving} className="btn-primary">
{saving ? "Saving..." : "Save Objective"}
</button>
</div>
</div>
</div>
) : (
@@ -224,26 +254,7 @@ function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string,
>
<GitBranch size={16} /> Tech Architecture
</button>
<button
onClick={() => setActiveDoc("tasks")}
style={{
textAlign: "left",
padding: "12px 16px",
borderRadius: 8,
background: activeDoc === "tasks" ? "#fff" : "transparent",
border: activeDoc === "tasks" ? `1px solid ${INK.border}` : "1px solid transparent",
boxShadow: activeDoc === "tasks" ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
color: activeDoc === "tasks" ? INK.main : INK.muted,
fontWeight: activeDoc === "tasks" ? 600 : 500,
cursor: "pointer",
transition: "all 0.15s ease",
display: "flex",
alignItems: "center",
gap: 8
}}
>
<ListTodo size={16} /> Execution Plan
</button>
</div>
{/* RIGHT COLUMN: The Document Content */}
@@ -259,43 +270,122 @@ function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string,
}}>
{activeDoc === "spec" && (
<div className="markdown-prose">
<h1>Product Specification</h1>
<p style={{ color: INK.muted }}>The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.</p>
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
<BookOpen size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
<p style={{ fontWeight: 500 }}>Spec is empty.</p>
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Use Architect mode to generate the Product Spec.</p>
</div>
{plan.decisions?.find(d => d.id === "prd_spec")?.why ? (
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{plan.decisions.find(d => d.id === "prd_spec")!.why!}
</ReactMarkdown>
) : (
<>
<h1>Product Specification</h1>
<p style={{ color: INK.muted }}>The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.</p>
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
<BookOpen size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
<p style={{ fontWeight: 500 }}>Spec is empty.</p>
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Click "Generate Complete PRD" on the Objective tab to generate this document.</p>
</div>
</>
)}
</div>
)}
{activeDoc === "plan" && (
<div className="markdown-prose">
<h1>Technical Architecture</h1>
<p style={{ color: INK.muted }}>The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.</p>
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
<GitBranch size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
<p style={{ fontWeight: 500 }}>Plan is empty.</p>
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Use Architect mode to define the technical approach.</p>
</div>
{plan.decisions?.find(d => d.id === "prd_arch")?.why ? (
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{plan.decisions.find(d => d.id === "prd_arch")!.why!}
</ReactMarkdown>
) : (
<>
<h1>Technical Architecture</h1>
<p style={{ color: INK.muted }}>The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.</p>
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
<GitBranch size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
<p style={{ fontWeight: 500 }}>Plan is empty.</p>
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Click "Generate Complete PRD" on the Objective tab to generate this document.</p>
</div>
</>
)}
</div>
)}
{activeDoc === "tasks" && (
<div className="markdown-prose">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<h1>Execution Plan</h1>
<button className="btn-primary">Delegate Build</button>
</div>
<p style={{ color: INK.muted }}>The atomic, dependency-ordered tasks the AI will execute to build the application.</p>
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
<ListTodo size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
<p style={{ fontWeight: 500 }}>No tasks queued.</p>
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Generate the task breakdown from the Product Spec.</p>
</div>
</div>
);
}
// ──────────────────────────────────────────────────
// 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 (
<div className="panel-container">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }}>
<div>
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, color: INK.main }}>Execution Plan</h2>
<p style={{ color: INK.muted, fontSize: "0.85rem", margin: "4px 0 0" }}>The prioritized roadmap for the AI background runner to execute.</p>
</div>
<button className="btn-primary" onClick={handleDelegate} disabled={delegating || openTasks.length === 0}>
{delegating ? "Starting Jarvis..." : "Delegate Build"}
</button>
</div>
<div style={{ display: "grid", gap: 12 }}>
{openTasks.length > 0 ? openTasks.map(t => (
<div key={t.id} style={{
border: `1px solid ${INK.border}`,
borderRadius: 8,
padding: 16,
background: "#fff",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ width: 16, height: 16, borderRadius: "50%", border: `2px solid ${INK.muted}` }} />
<div>
<div style={{ fontWeight: 600, fontSize: "0.95rem", color: INK.main }}>{t.title}</div>
{t.description && <div style={{ fontSize: "0.8rem", color: INK.muted, marginTop: 4 }}>{t.description}</div>}
</div>
</div>
<span style={{ fontSize: "0.8rem", color: INK.faint, background: INK.bgHover, padding: "4px 8px", borderRadius: 4 }}>Queued</span>
</div>
)) : (
<div style={{ padding: 40, textAlign: "center", border: `1px solid ${INK.border}`, borderRadius: 8, color: INK.muted }}>
<ListTodo size={32} style={{ margin: "0 auto 16px", opacity: 0.5 }} />
<p style={{ fontWeight: 500 }}>No tasks queued.</p>
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px auto 0" }}>Use Architect mode to generate the task breakdown.</p>
</div>
)}
</div>