diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index 318fc0c..5d8187d 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -339,13 +339,25 @@ For NEW repos / branches: \`gitea_repos_list\`, \`gitea_repo_get\`, \`gitea_repo - Compose stack weird → \`apps_repair { uuid }\` re-applies Traefik labels + port forwarding. - Nuke and redeploy → \`apps_delete { uuid, confirm }\` (\`confirm\` must equal exact name; fetch via \`apps_get\` first), then re-create. -## Plan tab — be the user's scribe -The Plan tab (Vision · Tasks · Decisions · Ideas) is the project's persistent memory. Capture things in the moment so the user doesn't context-switch. -- \`plan_vision_set\` PROACTIVELY when the user articulates or refines the high-level business case, elevator pitch, or primary objective of what they're building. The Objective is your north star and is separate from the technical Blueprint. -- \`plan_document_update\` PROACTIVELY when the user asks you to write, edit, or update a specific Technical or Product Specification document inside the Blueprint. You MUST use this tool to overwrite specific sections of the Blueprint. The valid document IDs are exactly: \`stories\`, \`acceptance\`, \`success\`, \`ui_design\`, \`tech_context\`, \`data_model\`, \`file_structure\`, \`tasks\`, and \`checklist\`. Do NOT use \`fs_write\` or \`ship\` to edit markdown files when asked to update the plan—the plan lives in the database. Don't ask permission. One-liner ack ("Updated the UI Design spec"), and move on. -- \`plan_task_add\` when you commit to multi-step work, the user says "remind me to X", or a chain ends with an obvious user follow-up (add Stripe webhook URL). One task per real next-action. -- \`plan_task_edit\` to update a task or change its status. Put a task in "review" status when you finish it, unless the user explicitly said it is "done". -- \`plan_idea_add\` sparingly, only for something worth remembering that isn't a task or decision. +## Product Requirements Docs & Spec Sheets (.vibncode/specs/) +The project's requirements, features list, specifications, and backlog checklists live in \`.vibncode/specs/\` as plain, Git-tracked Markdown files on disk. This is the single source of truth for all requirements: +1. \`01-master-prd.md\`: Executive Summary, Vision, Mission, and Master Checklist Backlog. +2. \`02-user-experience.md\`: UX Principles, Target Personas, and User Journeys. +3. \`03-api-and-integrations.md\`: REST/GraphQL endpoint specs, webhook payloads, and Missinglettr API. +4. \`04-compliance-security.md\`: COPPA Children's privacy, encryption, and Stripe billing compliance. +5. \`05-data-model.md\`: Database schema, tables, references, and database indexes. +6. \`06-mobile-experience.md\`: Responsive design viewports and touch targets. +7. \`07-provider-os.md\`: Session logs, provider listing controls, and administrative workflows. +8. \`08-ui-requirements.md\`: Style guidelines, Dracula theme values, and UI layout tokens. +9. \`09-open-source-references.md\`: Recommended NPM dependencies and code check guidelines. +10. \`10-growth-automation.md\`: Growth campaign trigger rules and distribution schedulers. + +### How to Utilize and Maintain Specs: +- **Prior Reference:** BEFORE starting any task or writing code, ALWAYS read the matching spec sheet (e.g., read \`05-data-model.md\` when setting up a database) using \`fs_read\` so you adhere exactly to the planned requirements and avoid drift. +- **Proactive Documenting:** Write, refine, and update these spec sheets whenever you co-design, make architectural choices, or when the user clarifies requirements. Use standard file tools (\`fs_write\`, \`fs_edit\`) directly on \`.vibncode/specs/\` markdown files. +- **Checklist Backlog Management:** Under section \`## 4. Development Checklist Backlog\` in \`01-master-prd.md\` (or relevant spec files), tasks are maintained as standard markdown checkmarks: \`- [ ] Task Description\` (open) or \`- [x]\` (done). +- **The Magic Toggle:** When you complete a feature or implement a user story, you MUST proactively edit the spec sheet to toggle \`- [ ]\` to \`- [x]\` for that task. Toggling the checkbox in the markdown file automatically updates the developer's desktop "Interactive Backlog" sidebar in real-time. +- **Legacy Obsolete Tools:** The database-backed plan tools (like \`plan_task_add\`, \`plan_document_update\`, etc.) are fully retired and obsolete—NEVER call them. Work exclusively with standard \`fs_\` file tools on the \`.vibncode/specs/*.md\` files! ## Hard rules (non-negotiable) - **Cite the tool result, don't claim from memory.** Before stating "I edited X" or "the server is running," you must point to a tool result from THIS turn. If you can't, say "I have not yet made that change — running the tool now" and then run it. A claim without a citable tool result is a hallucination. diff --git a/vibn-frontend/app/api/mcp/route.ts b/vibn-frontend/app/api/mcp/route.ts index 02f7e76..05f5e82 100644 --- a/vibn-frontend/app/api/mcp/route.ts +++ b/vibn-frontend/app/api/mcp/route.ts @@ -5684,17 +5684,64 @@ async function writePlanForProject( } } +// Mapping legacy docId -> new specification files +const SPEC_MAPPING: Record = { + stories: "01-master-prd.md", + acceptance: "01-master-prd.md", + success: "01-master-prd.md", + ui_design: "08-ui-requirements.md", + tech_context: "03-api-and-integrations.md", + data_model: "05-data-model.md", + file_structure: "09-open-source-references.md", + tasks: "01-master-prd.md", + checklist: "01-master-prd.md", +}; + +async function readPrdContent( + principal: Principal, + projectId: string, +): Promise { + try { + const res = await toolFsRead(principal, { + projectId, + path: ".vibncode/specs/01-master-prd.md", + }); + const data = await res.json(); + return data.result?.content || ""; + } catch { + return ""; + } +} + async function toolPlanGet(principal: Principal, params: Record) { const projectId = String(params.projectId ?? "").trim(); if (!projectId) return NextResponse.json({ error: "projectId required" }, { status: 400 }); - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, - ); - return NextResponse.json({ result: readPlanFromData(project.data) }); + + const content = await readPrdContent(principal, projectId); + const lines = content.split("\n"); + const tasks: any[] = []; + const checklistRegex = /^\s*-\s*\[([ xX])\]\s+(.+)$/; + + lines.forEach((line) => { + const match = line.match(checklistRegex); + if (match) { + const taskText = match[2].trim(); + tasks.push({ + id: taskText, + title: taskText, + status: match[1].toLowerCase() === "x" ? "done" : "open", + }); + } + }); + + return NextResponse.json({ + result: { + tasks, + decisions: [], + ideas: [], + }, + }); } async function toolPlanVisionSet( @@ -5708,21 +5755,33 @@ async function toolPlanVisionSet( { error: "projectId and text required" }, { status: 400 }, ); - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, + + let content = await readPrdContent(principal, projectId); + if (!content) { + content = `# Executive Master Product Requirements Document\n\n## 2. Product Vision\n`; + } + + // Prepend or replace under Product Vision section + const visionHeaderRegex = /(## 2\. Product Vision\n)([^#]*)/; + let updatedContent = ""; + if (content.match(visionHeaderRegex)) { + updatedContent = content.replace( + visionHeaderRegex, + `$1- **Vision Statement:** ${text}\n\n`, ); - const plan = readPlanFromData(project.data); - plan.vision = text; - await writePlanForProject(projectId, plan, text); + } else { + updatedContent = + content + `\n\n## 2. Product Vision\n- **Vision Statement:** ${text}\n`; + } + + await toolFsWrite(principal, { + projectId, + path: ".vibncode/specs/01-master-prd.md", + content: updatedContent, + }); + return NextResponse.json({ - result: { - ok: true, - vision: text, - summaryHint: `Vision saved. Tell the user it's been recorded; do not re-read.`, - }, + result: { ok: true }, }); } @@ -5737,26 +5796,22 @@ async function toolPlanIdeaAdd( { error: "projectId and text required" }, { status: 400 }, ); - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, - ); - const plan = readPlanFromData(project.data); - const idea: PlanIdea = { - id: planNewId(), - text, - createdAt: new Date().toISOString(), - }; - plan.ideas.unshift(idea); - await writePlanForProject(projectId, plan); + + let content = await readPrdContent(principal, projectId); + if (!content) { + content = `# Executive Master Product Requirements Document\n`; + } + + const updatedContent = content + `\n\n## Parked Ideas\n- ${text}\n`; + + await toolFsWrite(principal, { + projectId, + path: ".vibncode/specs/01-master-prd.md", + content: updatedContent, + }); + return NextResponse.json({ - result: { - ok: true, - idea, - summaryHint: `Idea captured to Plan → Ideas. Brief acknowledgment only.`, - }, + result: { ok: true }, }); } @@ -5765,39 +5820,54 @@ async function toolPlanTaskAdd( params: Record, ) { const projectId = String(params.projectId ?? "").trim(); - // Accept either {title, description} (preferred) or legacy {text}. const title = String(params.title ?? params.text ?? "").trim(); const description = - typeof params.description === "string" ? params.description : ""; + typeof params.description === "string" ? params.description.trim() : ""; + if (!projectId || !title) { return NextResponse.json( { error: "projectId and title required" }, { status: 400 }, ); } - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, + + let content = await readPrdContent(principal, projectId); + if (!content) { + content = `# Executive Master Product Requirements Document\n\n## 4. Development Checklist Backlog\n`; + } + + let taskBlock = `- [ ] ${title}`; + if (description) { + const indentedDesc = description + .split("\n") + .map((l) => ` ${l}`) + .join("\n"); + taskBlock += `\n${indentedDesc}`; + } + + const checklistHeader = "## 4. Development Checklist Backlog"; + let updatedContent = ""; + + if (content.includes(checklistHeader)) { + updatedContent = content.replace( + checklistHeader, + `${checklistHeader}\n${taskBlock}`, ); - const plan = readPlanFromData(project.data); - const task: PlanTask = { - id: planNewId(), - title, - description, - status: "open", - createdAt: new Date().toISOString(), - }; - plan.tasks.unshift(task); - await writePlanForProject(projectId, plan); + } else { + updatedContent = + content + `\n\n## 4. Development Checklist Backlog\n${taskBlock}\n`; + } + + await toolFsWrite(principal, { + projectId, + path: ".vibncode/specs/01-master-prd.md", + content: updatedContent, + }); + return NextResponse.json({ result: { ok: true, - task, - summaryHint: - `Task added to Plan → Tasks. Tell the user it's logged and ` + - `(if relevant) that the markdown spec is ready to delegate.`, + task: { id: title, title, status: "open" }, }, }); } @@ -5808,42 +5878,46 @@ async function toolPlanTaskEdit( ) { const projectId = String(params.projectId ?? "").trim(); const taskId = String(params.taskId ?? params.id ?? "").trim(); + const status = String(params.status ?? "").trim(); + if (!projectId || !taskId) return NextResponse.json( { error: "projectId and taskId required" }, { status: 400 }, ); - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, - ); - const plan = readPlanFromData(project.data); - const task = plan.tasks.find((t) => t.id === taskId); - if (!task) - return NextResponse.json({ error: "Task not found" }, { status: 404 }); - if (params.title !== undefined) { - task.title = String(params.title).trim(); + const content = await readPrdContent(principal, projectId); + if (!content) { + return NextResponse.json({ error: "Spec file not found" }, { status: 404 }); } - if (params.description !== undefined) { - task.description = String(params.description).trim(); - } - if (params.status !== undefined) { - task.status = params.status; - if (task.status === "done") { - task.doneAt = new Date().toISOString(); + + const lines = content.split("\n"); + let found = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/); + if (match && match[3].trim() === taskId.trim()) { + const indent = match[1] || ""; + const mark = status === "done" || status === "review" ? "x" : " "; + lines[i] = `${indent}- [${mark}] ${match[3]}`; + found = true; + break; } } - await writePlanForProject(projectId, plan); + if (!found) { + return NextResponse.json({ error: "Task not found" }, { status: 404 }); + } + + await toolFsWrite(principal, { + projectId, + path: ".vibncode/specs/01-master-prd.md", + content: lines.join("\n"), + }); + return NextResponse.json({ - result: { - ok: true, - task, - summaryHint: `Task updated. Brief acknowledgment only.`, - }, + result: { ok: true }, }); } @@ -5853,31 +5927,8 @@ async function toolPlanTaskComplete( ) { const projectId = String(params.projectId ?? "").trim(); const taskId = String(params.taskId ?? params.id ?? "").trim(); - if (!projectId || !taskId) - return NextResponse.json( - { error: "projectId and taskId required" }, - { status: 400 }, - ); - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, - ); - const plan = readPlanFromData(project.data); - const task = plan.tasks.find((t) => t.id === taskId); - if (!task) - return NextResponse.json({ error: "Task not found" }, { status: 404 }); - task.status = "done"; - task.doneAt = new Date().toISOString(); - await writePlanForProject(projectId, plan); - return NextResponse.json({ - result: { - ok: true, - task, - summaryHint: `Task marked done. Brief acknowledgment only.`, - }, - }); + + return toolPlanTaskEdit(principal, { projectId, taskId, status: "done" }); } async function toolPlanDocumentUpdate( @@ -5885,10 +5936,8 @@ async function toolPlanDocumentUpdate( params: Record, ) { const projectId = String(params.projectId ?? "").trim(); - // Strip any accidental 'prd_' prefix if the AI happens to hallucinate it, - // but natively expect clean keys like "stories" const rawDocId = String(params.docId ?? "").trim(); - const blueprintKey = rawDocId.replace(/^prd_/, "") as keyof BlueprintDocs; + const blueprintKey = rawDocId.replace(/^prd_/, ""); const content = String(params.content ?? "").trim(); if (!projectId || !blueprintKey || !content) { @@ -5897,29 +5946,15 @@ async function toolPlanDocumentUpdate( { status: 400 }, ); } - const project = await loadPlanProject(principal, projectId); - if (!project) - return NextResponse.json( - { error: "Project not found in workspace" }, - { status: 404 }, - ); - const plan = readPlanFromData(project.data); + const filename = SPEC_MAPPING[blueprintKey] || "01-master-prd.md"; - if (!plan.blueprint) { - plan.blueprint = {}; - } + await toolFsWrite(principal, { + projectId, + path: `.vibncode/specs/${filename}`, + content, + }); - // Update the strongly typed blueprint object - plan.blueprint[blueprintKey] = content; - - // We explicitly purge any legacy fallback copies of this document from the - // decisions array. The decisions array is ONLY for tiny choices like "use Stripe". - if (Array.isArray(plan.decisions)) { - plan.decisions = plan.decisions.filter((d) => !d.id.startsWith("prd_")); - } - - await writePlanForProject(projectId, plan); return NextResponse.json({ result: { ok: true }, });