feat: redirect legacy plan MCP tools to Git-backed Markdown specifications

This commit is contained in:
2026-06-03 10:03:47 -07:00
parent 052b5e913f
commit 4768dd6169
2 changed files with 187 additions and 140 deletions

View File

@@ -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.

View File

@@ -5684,17 +5684,64 @@ async function writePlanForProject(
}
}
// Mapping legacy docId -> new specification files
const SPEC_MAPPING: Record<string, string> = {
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<string> {
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<string, any>) {
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<string, any>,
) {
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)
const content = await readPrdContent(principal, projectId);
if (!content) {
return NextResponse.json({ error: "Spec file not found" }, { status: 404 });
}
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;
}
}
if (!found) {
return NextResponse.json({ error: "Task not found" }, { status: 404 });
if (params.title !== undefined) {
task.title = String(params.title).trim();
}
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();
}
}
await writePlanForProject(projectId, plan);
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<string, any>,
) {
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 },
});