feat: redirect legacy plan MCP tools to Git-backed Markdown specifications
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user