@agent-plan/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +781 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antonio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA0vBA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import * as z from "zod/v4";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation } from "@agent-plan/core";
|
|
9
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, normalizeSlug } from "@agent-plan/core/naming";
|
|
10
|
+
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
11
|
+
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
12
|
+
function text(textValue, structuredContent) {
|
|
13
|
+
return structuredContent ? { content: [{ type: "text", text: textValue }], structuredContent } : { content: [{ type: "text", text: textValue }] };
|
|
14
|
+
}
|
|
15
|
+
function nowISO() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function planRoot() {
|
|
19
|
+
return process.env.AGENT_PLAN_ROOT || join(process.cwd(), ".planner");
|
|
20
|
+
}
|
|
21
|
+
function store() {
|
|
22
|
+
const st = new PlanStore(planRoot());
|
|
23
|
+
st.enableAutoSync(true);
|
|
24
|
+
return st;
|
|
25
|
+
}
|
|
26
|
+
async function requireStore() {
|
|
27
|
+
const st = store();
|
|
28
|
+
if (!(await st.exists()))
|
|
29
|
+
throw new Error(`No .planner/ found at ${st.root}. Use planner-init first.`);
|
|
30
|
+
return st;
|
|
31
|
+
}
|
|
32
|
+
function findFeatureByRef(features, ref) {
|
|
33
|
+
const normalized = ref.trim().toLowerCase();
|
|
34
|
+
return features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
35
|
+
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
36
|
+
?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
|
|
37
|
+
}
|
|
38
|
+
function findPhaseByRef(phases, ref) {
|
|
39
|
+
const normalized = ref.trim().toLowerCase();
|
|
40
|
+
return phases.find((phase) => phase.id.toLowerCase() === normalized)
|
|
41
|
+
?? phases.find((phase) => phase.title.toLowerCase() === normalized)
|
|
42
|
+
?? phases.find((phase) => phase.title.toLowerCase().includes(normalized));
|
|
43
|
+
}
|
|
44
|
+
function findTaskByRef(phases, ref) {
|
|
45
|
+
const normalized = ref.trim().toLowerCase();
|
|
46
|
+
for (const phase of phases) {
|
|
47
|
+
const task = phase.tasks.find((entry) => entry.id.toLowerCase() === normalized
|
|
48
|
+
|| entry.title.toLowerCase() === normalized
|
|
49
|
+
|| entry.title.toLowerCase().includes(normalized));
|
|
50
|
+
if (task)
|
|
51
|
+
return { phase, task };
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function applyTaskLifecycleDates(task, nextStatus, now) {
|
|
56
|
+
const previousStatus = task.status;
|
|
57
|
+
if (nextStatus === "in-progress" && !task.startedAt)
|
|
58
|
+
task.startedAt = now;
|
|
59
|
+
if (nextStatus === "done") {
|
|
60
|
+
if (!task.startedAt)
|
|
61
|
+
task.startedAt = now;
|
|
62
|
+
task.completedAt = now;
|
|
63
|
+
}
|
|
64
|
+
else if (previousStatus === "done") {
|
|
65
|
+
task.completedAt = "";
|
|
66
|
+
}
|
|
67
|
+
task.status = nextStatus;
|
|
68
|
+
}
|
|
69
|
+
async function writeAndSummarize(st, message, structuredContent) {
|
|
70
|
+
await st.writeGenerated();
|
|
71
|
+
return text(message, structuredContent);
|
|
72
|
+
}
|
|
73
|
+
const server = new McpServer({
|
|
74
|
+
name: "agent-plan-planner",
|
|
75
|
+
version: "0.1.0",
|
|
76
|
+
});
|
|
77
|
+
server.registerTool("planner-export", {
|
|
78
|
+
description: "Export the project plan as a Markdown report. Supports a concise summary or full hierarchical detail.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
full: z.boolean().optional().describe("If true, include full detail for every feature, phase, and task. Defaults to false (summary only)."),
|
|
81
|
+
},
|
|
82
|
+
}, async ({ full = false }) => {
|
|
83
|
+
const st = await requireStore();
|
|
84
|
+
const plan = await st.loadAll();
|
|
85
|
+
const exportService = new ExportService();
|
|
86
|
+
const markdown = exportService.exportToMarkdown(plan, full);
|
|
87
|
+
const fs = await import("node:fs/promises");
|
|
88
|
+
await fs.writeFile(join(st.root, "EXPORT.md"), markdown, "utf-8");
|
|
89
|
+
return text(`Project export generated. Summary results:\n\n${markdown.slice(0, 1000)}${markdown.length > 1000 ? "... (full report in .planner/EXPORT.md)" : ""}`, { markdown });
|
|
90
|
+
});
|
|
91
|
+
server.registerTool("planner-authorize-bypass", {
|
|
92
|
+
description: "Authorize a temporary guard bypass (default 15 minutes) so edit/write tools can proceed even when no task is in-progress. Use ONLY after the user explicitly authorizes proceeding without a task. Harness-agnostic: stored in resume.json so all adapters (Pi, Claude Code, Codex, ...) respect it.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
durationMinutes: z.number().optional().describe("Bypass window in minutes. Default 15."),
|
|
95
|
+
},
|
|
96
|
+
}, async ({ durationMinutes }) => {
|
|
97
|
+
const st = await requireStore();
|
|
98
|
+
const mins = durationMinutes ?? 15;
|
|
99
|
+
const until = await st.authorizeGuardBypass(mins);
|
|
100
|
+
return text(`Guard bypass authorized until ${until}. edit/write is allowed without a task in-progress for ${mins} minutes.`, { until });
|
|
101
|
+
});
|
|
102
|
+
server.registerTool("planner-clear-bypass", {
|
|
103
|
+
description: "Revoke any active guard bypass so edit/write again requires a task in-progress.",
|
|
104
|
+
}, async () => {
|
|
105
|
+
const st = await requireStore();
|
|
106
|
+
await st.clearGuardBypass();
|
|
107
|
+
return text("Guard bypass revoked.", { cleared: true });
|
|
108
|
+
});
|
|
109
|
+
server.registerTool("planner-init", {
|
|
110
|
+
description: "Initialize .planner/ in the current project.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
projectName: z.string().min(1).describe("Concise project title"),
|
|
113
|
+
description: z.string().optional().describe("Short project description"),
|
|
114
|
+
goal: z.string().optional().describe("Main project goal"),
|
|
115
|
+
},
|
|
116
|
+
}, async ({ projectName, description, goal }) => {
|
|
117
|
+
const st = store();
|
|
118
|
+
if (await st.exists())
|
|
119
|
+
return text(`.planner/ already exists at ${st.root}`);
|
|
120
|
+
await st.init(projectName.trim());
|
|
121
|
+
const project = await st.loadProject();
|
|
122
|
+
if (description !== undefined)
|
|
123
|
+
project.description = description.trim();
|
|
124
|
+
if (goal !== undefined)
|
|
125
|
+
project.goal = goal.trim();
|
|
126
|
+
await st.saveProject(project);
|
|
127
|
+
return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`, { project });
|
|
128
|
+
});
|
|
129
|
+
server.registerTool("planner-show", {
|
|
130
|
+
description: "Show the current planner overview.",
|
|
131
|
+
}, async () => {
|
|
132
|
+
const st = await requireStore();
|
|
133
|
+
const plan = await st.loadAll();
|
|
134
|
+
return text([
|
|
135
|
+
`📋 ${plan.project.name}`,
|
|
136
|
+
`Description: ${plan.project.description || "(not set)"}`,
|
|
137
|
+
`Goal: ${plan.project.goal || "(not set)"}`,
|
|
138
|
+
`Features: ${plan.features.features.length}`,
|
|
139
|
+
`Phases: ${plan.phases.length}`,
|
|
140
|
+
`Tasks: ${plan.phases.reduce((total, phase) => total + phase.tasks.length, 0)}`,
|
|
141
|
+
`Updated: ${plan.manifest.updatedAt}`,
|
|
142
|
+
].join("\n"), { plan });
|
|
143
|
+
});
|
|
144
|
+
server.registerTool("planner-repair", {
|
|
145
|
+
description: "Repair dangling feature→phase references and report integrity.",
|
|
146
|
+
}, async () => {
|
|
147
|
+
const st = await requireStore();
|
|
148
|
+
const report = await st.repair();
|
|
149
|
+
return text(`Repair done: renamed ${report.migrated.renamed}, repaired ${report.migrated.repaired} refs, inferred ${report.migrated.inferred}. Integrity: ${report.integrity.duplicatePhaseIds.length} duplicate, ${report.integrity.danglingPhaseIds.length} dangling.`, { report });
|
|
150
|
+
});
|
|
151
|
+
server.registerTool("planner-project-language", {
|
|
152
|
+
description: "Persist preferred languages for plan content and chat.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
contentLanguage: z.string().optional(),
|
|
155
|
+
chatLanguage: z.string().optional(),
|
|
156
|
+
},
|
|
157
|
+
}, async ({ contentLanguage, chatLanguage }) => {
|
|
158
|
+
const st = await requireStore();
|
|
159
|
+
const project = await st.loadProject();
|
|
160
|
+
if (contentLanguage !== undefined)
|
|
161
|
+
project.contentLanguage = contentLanguage.trim();
|
|
162
|
+
if (chatLanguage !== undefined)
|
|
163
|
+
project.chatLanguage = chatLanguage.trim();
|
|
164
|
+
await st.saveProject(project);
|
|
165
|
+
return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}`, { project });
|
|
166
|
+
});
|
|
167
|
+
server.registerTool("planner-project-discuss", {
|
|
168
|
+
description: "Record/update project-level discovery fields. This is the MCP equivalent of grouped project discuss without Pi UI prompts.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
goal: z.string().optional(),
|
|
171
|
+
scope: z.array(z.string()).optional(),
|
|
172
|
+
outOfScope: z.array(z.string()).optional(),
|
|
173
|
+
technologies: z.array(z.string()).optional(),
|
|
174
|
+
tools: z.array(z.string()).optional(),
|
|
175
|
+
globalRules: z.array(z.string()).optional(),
|
|
176
|
+
decisions: z.array(z.string()).optional(),
|
|
177
|
+
},
|
|
178
|
+
}, async (params) => {
|
|
179
|
+
const st = await requireStore();
|
|
180
|
+
const project = await st.loadProject();
|
|
181
|
+
if (params.goal !== undefined)
|
|
182
|
+
project.goal = params.goal.trim();
|
|
183
|
+
if (params.scope !== undefined)
|
|
184
|
+
project.scope = params.scope.map((entry) => entry.trim()).filter(Boolean);
|
|
185
|
+
if (params.outOfScope !== undefined)
|
|
186
|
+
project.outOfScope = params.outOfScope.map((entry) => entry.trim()).filter(Boolean);
|
|
187
|
+
if (params.technologies !== undefined)
|
|
188
|
+
project.technologies = params.technologies.map((entry) => entry.trim()).filter(Boolean);
|
|
189
|
+
if (params.tools !== undefined)
|
|
190
|
+
project.tools = params.tools.map((entry) => entry.trim()).filter(Boolean);
|
|
191
|
+
if (params.globalRules !== undefined)
|
|
192
|
+
project.globalRules = params.globalRules.map((entry) => entry.trim()).filter(Boolean);
|
|
193
|
+
if (params.decisions !== undefined)
|
|
194
|
+
project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
|
|
195
|
+
await st.saveProject(project);
|
|
196
|
+
return writeAndSummarize(st, `Project discussed/updated: ${project.name}`, { project });
|
|
197
|
+
});
|
|
198
|
+
server.registerTool("planner-feature-list", {
|
|
199
|
+
description: "List features with phase/task counts.",
|
|
200
|
+
}, async () => {
|
|
201
|
+
const st = await requireStore();
|
|
202
|
+
const features = (await st.loadFeatures()).features;
|
|
203
|
+
const phases = await st.loadAllPhases();
|
|
204
|
+
const lines = features.map((feature) => {
|
|
205
|
+
const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
|
|
206
|
+
const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
207
|
+
return `- ${feature.id} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
|
|
208
|
+
});
|
|
209
|
+
return text(lines.join("\n") || "No features", { features });
|
|
210
|
+
});
|
|
211
|
+
server.registerTool("planner-feature-add", {
|
|
212
|
+
description: "Create a feature with a rich description. REQUIRED: description must include code references (file:line), current implementation state (what exists, what is unimplemented), systems/structs/traits involved, concrete goals, and behaviors to preserve. The description is the primary context for future agents resuming this feature; one-liners cause misalignment.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
name: z.string().min(1),
|
|
215
|
+
description: z.string().min(50, "Description must be at least 50 characters — include code references (file:line), current state, goals, and behaviors to preserve. Prefix with 'design-only' for pre-implementation design tasks without code refs.").describe("Required code references (file:line), current state of the art, structs/traits/systems involved, goals, and behaviors to preserve. Not a one-liner."),
|
|
216
|
+
status: z.enum(STATUS_VALUES).optional(),
|
|
217
|
+
},
|
|
218
|
+
}, async ({ name, description, status }) => {
|
|
219
|
+
const st = await requireStore();
|
|
220
|
+
const timestamp = nowISO();
|
|
221
|
+
const effectiveStatus = status ?? "planned";
|
|
222
|
+
const existingFeatures = (await st.loadFeatures()).features;
|
|
223
|
+
const feature = {
|
|
224
|
+
id: createFeatureId(),
|
|
225
|
+
number: existingFeatures.length + 1,
|
|
226
|
+
name: name.trim(),
|
|
227
|
+
description: description?.trim() ?? "",
|
|
228
|
+
status: effectiveStatus,
|
|
229
|
+
discussedAt: "",
|
|
230
|
+
contextReady: false,
|
|
231
|
+
contextReadyReason: "",
|
|
232
|
+
startDate: effectiveStatus === "in-progress" ? new Date().toISOString().slice(0, 10) : "",
|
|
233
|
+
endDate: effectiveStatus === "done" ? new Date().toISOString().slice(0, 10) : "",
|
|
234
|
+
workDone: "",
|
|
235
|
+
workRemaining: "",
|
|
236
|
+
acceptedDecisions: [],
|
|
237
|
+
phaseIds: [],
|
|
238
|
+
dependsOn: [],
|
|
239
|
+
createdAt: timestamp,
|
|
240
|
+
updatedAt: timestamp,
|
|
241
|
+
};
|
|
242
|
+
await st.updateFeatures((doc) => {
|
|
243
|
+
doc.features.push(feature);
|
|
244
|
+
return doc;
|
|
245
|
+
});
|
|
246
|
+
return writeAndSummarize(st, `Feature created: ${feature.id} — ${feature.name}`, { feature });
|
|
247
|
+
});
|
|
248
|
+
server.registerTool("planner-feature-show", {
|
|
249
|
+
description: "Show a feature by id or name.",
|
|
250
|
+
inputSchema: { feature: z.string().min(1).describe("Feature id or name") },
|
|
251
|
+
}, async ({ feature: ref }) => {
|
|
252
|
+
const st = await requireStore();
|
|
253
|
+
const features = (await st.loadFeatures()).features;
|
|
254
|
+
const feature = findFeatureByRef(features, ref);
|
|
255
|
+
if (!feature)
|
|
256
|
+
return text(`Feature not found: ${ref}`);
|
|
257
|
+
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
258
|
+
return text(`${feature.name} (${feature.id}) — ${feature.status}; ${phases.length} phases`, { feature, phases });
|
|
259
|
+
});
|
|
260
|
+
server.registerTool("planner-feature-update", {
|
|
261
|
+
description: "Update a feature.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
feature: z.string().min(1).describe("Feature id or name"),
|
|
264
|
+
name: z.string().optional(),
|
|
265
|
+
description: z.string().optional(),
|
|
266
|
+
status: z.enum(STATUS_VALUES).optional(),
|
|
267
|
+
workDone: z.string().optional(),
|
|
268
|
+
workRemaining: z.string().optional(),
|
|
269
|
+
startDate: z.string().optional(),
|
|
270
|
+
endDate: z.string().optional(),
|
|
271
|
+
},
|
|
272
|
+
}, async ({ feature: ref, ...updates }) => {
|
|
273
|
+
const st = await requireStore();
|
|
274
|
+
const features = (await st.loadFeatures()).features;
|
|
275
|
+
const feature = findFeatureByRef(features, ref);
|
|
276
|
+
if (!feature)
|
|
277
|
+
return text(`Feature not found: ${ref}`);
|
|
278
|
+
const updated = await st.updateFeatures((doc) => {
|
|
279
|
+
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
280
|
+
if (!target)
|
|
281
|
+
return doc;
|
|
282
|
+
if (updates.name !== undefined)
|
|
283
|
+
target.name = updates.name.trim();
|
|
284
|
+
if (updates.description !== undefined)
|
|
285
|
+
target.description = updates.description.trim();
|
|
286
|
+
if (updates.status !== undefined)
|
|
287
|
+
target.status = updates.status;
|
|
288
|
+
if (updates.workDone !== undefined)
|
|
289
|
+
target.workDone = updates.workDone.trim();
|
|
290
|
+
if (updates.workRemaining !== undefined)
|
|
291
|
+
target.workRemaining = updates.workRemaining.trim();
|
|
292
|
+
if (updates.startDate !== undefined)
|
|
293
|
+
target.startDate = updates.startDate.trim();
|
|
294
|
+
if (updates.endDate !== undefined)
|
|
295
|
+
target.endDate = updates.endDate.trim();
|
|
296
|
+
target.updatedAt = nowISO();
|
|
297
|
+
return doc;
|
|
298
|
+
});
|
|
299
|
+
const result = updated.features.find((entry) => entry.id === feature.id);
|
|
300
|
+
return writeAndSummarize(st, `Feature updated: ${result.id} — ${result.name}`, { feature: result });
|
|
301
|
+
});
|
|
302
|
+
server.registerTool("planner-feature-delete", {
|
|
303
|
+
description: "Delete a feature. By default, phases are unlinked rather than deleted.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
feature: z.string().min(1).describe("Feature id or name"),
|
|
306
|
+
cascade: z.boolean().optional().describe("Also delete phases belonging to this feature"),
|
|
307
|
+
},
|
|
308
|
+
}, async ({ feature: ref, cascade }) => {
|
|
309
|
+
const st = await requireStore();
|
|
310
|
+
const features = (await st.loadFeatures()).features;
|
|
311
|
+
const feature = findFeatureByRef(features, ref);
|
|
312
|
+
if (!feature)
|
|
313
|
+
return text(`Feature not found: ${ref}`);
|
|
314
|
+
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
315
|
+
await st.updateFeatures((doc) => {
|
|
316
|
+
doc.features = doc.features.filter((entry) => entry.id !== feature.id);
|
|
317
|
+
return doc;
|
|
318
|
+
});
|
|
319
|
+
for (const phase of phases) {
|
|
320
|
+
if (cascade) {
|
|
321
|
+
await st.deletePhase(phase.id);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
phase.featureId = undefined;
|
|
325
|
+
phase.updatedAt = nowISO();
|
|
326
|
+
await st.savePhase(phase);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return writeAndSummarize(st, `Feature deleted: ${feature.id}${cascade ? `; deleted ${phases.length} phases` : `; unlinked ${phases.length} phases`}`, { deleted: feature.id, affectedPhases: phases.length, cascade: Boolean(cascade) });
|
|
330
|
+
});
|
|
331
|
+
server.registerTool("planner-phase-add", {
|
|
332
|
+
description: "Create a phase with a rich description. REQUIRED: description must include code references (file:line), current implementation state, dependencies, specific files/systems to modify, and behaviors to preserve. The description is the primary context for future agents; one-liners cause misalignment.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
title: z.string().min(1),
|
|
335
|
+
feature: z.string().optional().describe("Feature id or name"),
|
|
336
|
+
summary: z.string().optional().describe("One-line summary of the phase"),
|
|
337
|
+
description: z.string().min(50, "Description must be at least 50 characters — include code references (file:line), current state, structs/traits involved, concrete work items, behaviors to preserve. Prefix with 'design-only' for pre-implementation design tasks.").describe("Required code references (file:line), current state, structs/traits involved, concrete work items, behaviors to preserve. Not a one-liner."),
|
|
338
|
+
},
|
|
339
|
+
}, async ({ title, feature: featureRef, summary, description }) => {
|
|
340
|
+
const st = await requireStore();
|
|
341
|
+
const featuresDoc = await st.loadFeatures();
|
|
342
|
+
const feature = featureRef ? findFeatureByRef(featuresDoc.features, featureRef) : featuresDoc.features[0];
|
|
343
|
+
if (featureRef && !feature)
|
|
344
|
+
return text(`Feature not found: ${featureRef}`);
|
|
345
|
+
const lockKey = feature?.id ?? "__unscoped__";
|
|
346
|
+
let phase;
|
|
347
|
+
await withFeatureLock(lockKey, async () => {
|
|
348
|
+
const phases = await st.loadAllPhases();
|
|
349
|
+
const featurePhases = feature ? phases.filter((phase) => phase.featureId === feature.id) : phases;
|
|
350
|
+
const timestamp = nowISO();
|
|
351
|
+
phase = {
|
|
352
|
+
id: createPhaseId(),
|
|
353
|
+
number: featurePhases.reduce((max, entry) => Math.max(max, entry.number), 0) + 1,
|
|
354
|
+
slug: normalizeSlug(title),
|
|
355
|
+
title: title.trim(),
|
|
356
|
+
featureId: feature?.id,
|
|
357
|
+
status: "draft",
|
|
358
|
+
discussedAt: "",
|
|
359
|
+
contextReady: false,
|
|
360
|
+
contextReadyReason: "",
|
|
361
|
+
summary: summary?.trim() ?? "",
|
|
362
|
+
description: description?.trim() ?? "",
|
|
363
|
+
notes: "",
|
|
364
|
+
goals: [],
|
|
365
|
+
nonGoals: [],
|
|
366
|
+
dependencies: [],
|
|
367
|
+
dependsOn: [],
|
|
368
|
+
risks: [],
|
|
369
|
+
openQuestions: [],
|
|
370
|
+
decisions: [],
|
|
371
|
+
acceptedDecisions: [],
|
|
372
|
+
completionCriteria: [],
|
|
373
|
+
taskIds: [],
|
|
374
|
+
tasks: [],
|
|
375
|
+
createdAt: timestamp,
|
|
376
|
+
updatedAt: timestamp,
|
|
377
|
+
};
|
|
378
|
+
await st.savePhase(phase);
|
|
379
|
+
if (feature) {
|
|
380
|
+
await st.updateFeatures((doc) => {
|
|
381
|
+
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
382
|
+
if (target && !target.phaseIds.includes(phase.id))
|
|
383
|
+
target.phaseIds.push(phase.id);
|
|
384
|
+
return doc;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
await st.writeGenerated();
|
|
388
|
+
});
|
|
389
|
+
if (!phase)
|
|
390
|
+
return text("Phase creation failed.");
|
|
391
|
+
return writeAndSummarize(st, `Phase created: ${phase.id} — ${phase.title}`, { phase });
|
|
392
|
+
});
|
|
393
|
+
server.registerTool("planner-phase-show", {
|
|
394
|
+
description: "Show a phase by id or name.",
|
|
395
|
+
inputSchema: { phase: z.string().min(1).describe("Phase id or name") },
|
|
396
|
+
}, async ({ phase: ref }) => {
|
|
397
|
+
const st = await requireStore();
|
|
398
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
399
|
+
if (!phase)
|
|
400
|
+
return text(`Phase not found: ${ref}`);
|
|
401
|
+
return text(`${phase.title} (${phase.id}) — ${phase.status}; ${phase.tasks.length} tasks`, { phase });
|
|
402
|
+
});
|
|
403
|
+
server.registerTool("planner-phase-discuss", {
|
|
404
|
+
description: "Persist phase discovery fields and mark phase planned.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
phase: z.string().min(1).describe("Phase id or name"),
|
|
407
|
+
goal: z.string().optional(),
|
|
408
|
+
summary: z.string().optional(),
|
|
409
|
+
scope: z.string().optional(),
|
|
410
|
+
nonGoals: z.array(z.string()).optional(),
|
|
411
|
+
dependencies: z.array(z.string()).optional(),
|
|
412
|
+
risks: z.array(z.string()).optional(),
|
|
413
|
+
completionCriteria: z.array(z.string()).optional(),
|
|
414
|
+
},
|
|
415
|
+
}, async ({ phase: ref, ...updates }) => {
|
|
416
|
+
const st = await requireStore();
|
|
417
|
+
const found = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
418
|
+
if (!found)
|
|
419
|
+
return text(`Phase not found: ${ref}`);
|
|
420
|
+
const phase = await st.updatePhase(found.id, (entry) => {
|
|
421
|
+
if (updates.goal !== undefined)
|
|
422
|
+
entry.goals = [updates.goal.trim()].filter(Boolean);
|
|
423
|
+
if (updates.summary !== undefined)
|
|
424
|
+
entry.summary = updates.summary.trim();
|
|
425
|
+
if (updates.scope !== undefined)
|
|
426
|
+
entry.description = updates.scope.trim();
|
|
427
|
+
if (updates.nonGoals !== undefined)
|
|
428
|
+
entry.nonGoals = updates.nonGoals.map((item) => item.trim()).filter(Boolean);
|
|
429
|
+
if (updates.dependencies !== undefined)
|
|
430
|
+
entry.dependencies = updates.dependencies.map((item) => item.trim()).filter(Boolean);
|
|
431
|
+
if (updates.risks !== undefined)
|
|
432
|
+
entry.risks = updates.risks.map((item) => item.trim()).filter(Boolean);
|
|
433
|
+
if (updates.completionCriteria !== undefined)
|
|
434
|
+
entry.completionCriteria = updates.completionCriteria.map((item) => item.trim()).filter(Boolean);
|
|
435
|
+
entry.status = "planned";
|
|
436
|
+
entry.discussedAt = nowISO();
|
|
437
|
+
entry.contextReady = true;
|
|
438
|
+
entry.contextReadyReason = "Updated through planner-phase-discuss MCP tool.";
|
|
439
|
+
entry.updatedAt = nowISO();
|
|
440
|
+
return entry;
|
|
441
|
+
});
|
|
442
|
+
return writeAndSummarize(st, `Phase discussed/planned: ${phase.id} — ${phase.title}`, { phase });
|
|
443
|
+
});
|
|
444
|
+
server.registerTool("planner-phase-update", {
|
|
445
|
+
description: "Update phase fields.",
|
|
446
|
+
inputSchema: {
|
|
447
|
+
phase: z.string().min(1),
|
|
448
|
+
title: z.string().optional(),
|
|
449
|
+
status: z.enum(PHASE_STATUS_VALUES).optional(),
|
|
450
|
+
summary: z.string().optional(),
|
|
451
|
+
description: z.string().optional(),
|
|
452
|
+
},
|
|
453
|
+
}, async ({ phase: ref, ...updates }) => {
|
|
454
|
+
const st = await requireStore();
|
|
455
|
+
const found = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
456
|
+
if (!found)
|
|
457
|
+
return text(`Phase not found: ${ref}`);
|
|
458
|
+
const phase = await st.updatePhase(found.id, (entry) => {
|
|
459
|
+
if (updates.title !== undefined)
|
|
460
|
+
entry.title = updates.title.trim();
|
|
461
|
+
if (updates.status !== undefined)
|
|
462
|
+
entry.status = updates.status;
|
|
463
|
+
if (updates.summary !== undefined)
|
|
464
|
+
entry.summary = updates.summary.trim();
|
|
465
|
+
if (updates.description !== undefined)
|
|
466
|
+
entry.description = updates.description.trim();
|
|
467
|
+
entry.updatedAt = nowISO();
|
|
468
|
+
return entry;
|
|
469
|
+
});
|
|
470
|
+
return writeAndSummarize(st, `Phase updated: ${phase.id} — ${phase.title}`, { phase });
|
|
471
|
+
});
|
|
472
|
+
server.registerTool("planner-phase-delete", {
|
|
473
|
+
description: "Delete a phase and unlink it from features.",
|
|
474
|
+
inputSchema: { phase: z.string().min(1) },
|
|
475
|
+
}, async ({ phase: ref }) => {
|
|
476
|
+
const st = await requireStore();
|
|
477
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
478
|
+
if (!phase)
|
|
479
|
+
return text(`Phase not found: ${ref}`);
|
|
480
|
+
await st.deletePhase(phase.id);
|
|
481
|
+
await st.updateFeatures((doc) => {
|
|
482
|
+
for (const feature of doc.features)
|
|
483
|
+
feature.phaseIds = feature.phaseIds.filter((id) => id !== phase.id);
|
|
484
|
+
return doc;
|
|
485
|
+
});
|
|
486
|
+
return writeAndSummarize(st, `Phase deleted: ${phase.id}`, { deleted: phase.id });
|
|
487
|
+
});
|
|
488
|
+
server.registerTool("planner-task-add", {
|
|
489
|
+
description: "Create a task with a rich description. REQUIRED: description must include code references (file:line), what already exists vs what needs to be built, specific structs/traits/systems to modify, concrete implementation steps, and edge cases to handle. The description is the execution context for agents; one-liners cause misalignment.",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
phase: z.string().min(1).describe("Phase id or name"),
|
|
492
|
+
title: z.string().min(1),
|
|
493
|
+
description: z.string().min(50, "Description must be at least 50 characters — include code references (file:line), current state vs desired state, structs/traits to modify, concrete implementation steps, edge cases. Prefix with 'design-only' for pre-implementation design tasks.").describe("Required code references (file:line), current state vs desired state, structs/traits to modify, concrete implementation steps, edge cases. Not a one-liner."),
|
|
494
|
+
checklist: z.array(z.string()).optional(),
|
|
495
|
+
},
|
|
496
|
+
}, async ({ phase: ref, title, description, checklist }) => {
|
|
497
|
+
const st = await requireStore();
|
|
498
|
+
const found = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
499
|
+
if (!found)
|
|
500
|
+
return text(`Phase not found: ${ref}`);
|
|
501
|
+
const timestamp = nowISO();
|
|
502
|
+
const taskId = createTaskId();
|
|
503
|
+
const task = {
|
|
504
|
+
id: taskId,
|
|
505
|
+
phaseId: found.id,
|
|
506
|
+
number: found.tasks.length + 1,
|
|
507
|
+
shortName: normalizeSlug(title).slice(0, 30),
|
|
508
|
+
title: title.trim(),
|
|
509
|
+
status: "planned",
|
|
510
|
+
description: description?.trim() ?? "",
|
|
511
|
+
notes: "",
|
|
512
|
+
statusLog: [],
|
|
513
|
+
decisions: [],
|
|
514
|
+
acceptedDecisions: [],
|
|
515
|
+
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), title: item, checked: false })),
|
|
516
|
+
subtasks: [],
|
|
517
|
+
dependsOn: [],
|
|
518
|
+
startedAt: "",
|
|
519
|
+
completedAt: "",
|
|
520
|
+
createdAt: timestamp,
|
|
521
|
+
updatedAt: timestamp,
|
|
522
|
+
};
|
|
523
|
+
const phase = await st.updatePhase(found.id, (entry) => {
|
|
524
|
+
entry.tasks.push(task);
|
|
525
|
+
entry.taskIds.push(task.id);
|
|
526
|
+
entry.updatedAt = timestamp;
|
|
527
|
+
return entry;
|
|
528
|
+
});
|
|
529
|
+
return writeAndSummarize(st, `Task created: ${task.id} — ${task.title}`, { task, phase });
|
|
530
|
+
});
|
|
531
|
+
server.registerTool("planner-task-show", {
|
|
532
|
+
description: "Show a task by id or name.",
|
|
533
|
+
inputSchema: { task: z.string().min(1).describe("Task id or name") },
|
|
534
|
+
}, async ({ task: ref }) => {
|
|
535
|
+
const st = await requireStore();
|
|
536
|
+
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
537
|
+
if (!found)
|
|
538
|
+
return text(`Task not found: ${ref}`);
|
|
539
|
+
return text(`${found.task.title} (${found.task.id}) — ${found.task.status}; phase ${found.phase.id}`, { task: found.task, phase: found.phase });
|
|
540
|
+
});
|
|
541
|
+
server.registerTool("planner-task-discuss", {
|
|
542
|
+
description: "Persist task execution notes/checklist.",
|
|
543
|
+
inputSchema: {
|
|
544
|
+
task: z.string().min(1),
|
|
545
|
+
description: z.string().optional(),
|
|
546
|
+
checklist: z.array(z.string()).optional(),
|
|
547
|
+
},
|
|
548
|
+
}, async ({ task: ref, description, checklist }) => {
|
|
549
|
+
const st = await requireStore();
|
|
550
|
+
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
551
|
+
if (!found)
|
|
552
|
+
return text(`Task not found: ${ref}`);
|
|
553
|
+
let updatedTask;
|
|
554
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
555
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
556
|
+
if (!task)
|
|
557
|
+
return phase;
|
|
558
|
+
if (description !== undefined)
|
|
559
|
+
task.description = description.trim();
|
|
560
|
+
if (checklist !== undefined)
|
|
561
|
+
task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), title: item, checked: false }));
|
|
562
|
+
task.updatedAt = nowISO();
|
|
563
|
+
phase.updatedAt = task.updatedAt;
|
|
564
|
+
updatedTask = task;
|
|
565
|
+
return phase;
|
|
566
|
+
});
|
|
567
|
+
return writeAndSummarize(st, `Task discussed/updated: ${found.task.id}`, { task: updatedTask ?? found.task });
|
|
568
|
+
});
|
|
569
|
+
server.registerTool("planner-task-update", {
|
|
570
|
+
description: "Update task fields. A motivation is REQUIRED when changing to blocked, canceled, deferred, rejected, waiting, or back to planned from another status.",
|
|
571
|
+
inputSchema: {
|
|
572
|
+
task: z.string().min(1),
|
|
573
|
+
title: z.string().optional(),
|
|
574
|
+
status: z.enum(STATUS_VALUES).optional(),
|
|
575
|
+
description: z.string().optional(),
|
|
576
|
+
motivation: z.string().optional(),
|
|
577
|
+
},
|
|
578
|
+
}, async ({ task: ref, title, status, description, motivation }) => {
|
|
579
|
+
const st = await requireStore();
|
|
580
|
+
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
581
|
+
if (!found)
|
|
582
|
+
return text(`Task not found: ${ref}`);
|
|
583
|
+
// Validate motivation requirement for status transitions.
|
|
584
|
+
if (status !== undefined && needsMotivation(found.task.status, status)) {
|
|
585
|
+
if (!motivation || !motivation.trim()) {
|
|
586
|
+
return text(`Status transition \"${found.task.status} → ${status}\" requires a motivation. ` +
|
|
587
|
+
`Provide the \"motivation\" parameter with a detailed explanation of why this change is needed.`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
let updatedTask;
|
|
591
|
+
const timestamp = nowISO();
|
|
592
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
593
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
594
|
+
if (!task)
|
|
595
|
+
return phase;
|
|
596
|
+
if (title !== undefined)
|
|
597
|
+
task.title = title.trim();
|
|
598
|
+
if (description !== undefined)
|
|
599
|
+
task.description = description.trim();
|
|
600
|
+
if (status !== undefined && status !== task.status) {
|
|
601
|
+
// Record status change in the incremental statusLog.
|
|
602
|
+
const entry = {
|
|
603
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${task.status}-${status}`),
|
|
604
|
+
date: timestamp,
|
|
605
|
+
fromStatus: task.status,
|
|
606
|
+
toStatus: status,
|
|
607
|
+
title: motivation?.split("\n")[0]?.trim() || `${task.status} → ${status}`,
|
|
608
|
+
description: motivation?.trim() || "",
|
|
609
|
+
};
|
|
610
|
+
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
611
|
+
applyTaskLifecycleDates(task, status, timestamp);
|
|
612
|
+
}
|
|
613
|
+
task.updatedAt = timestamp;
|
|
614
|
+
phase.updatedAt = timestamp;
|
|
615
|
+
updatedTask = task;
|
|
616
|
+
return phase;
|
|
617
|
+
});
|
|
618
|
+
await st.syncTaskStatusRollup(found.phase.id);
|
|
619
|
+
return writeAndSummarize(st, `Task updated: ${found.task.id}`, { task: updatedTask ?? found.task });
|
|
620
|
+
});
|
|
621
|
+
server.registerTool("planner-task-delete", {
|
|
622
|
+
description: "Delete a task.",
|
|
623
|
+
inputSchema: { task: z.string().min(1) },
|
|
624
|
+
}, async ({ task: ref }) => {
|
|
625
|
+
const st = await requireStore();
|
|
626
|
+
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
627
|
+
if (!found)
|
|
628
|
+
return text(`Task not found: ${ref}`);
|
|
629
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
630
|
+
phase.tasks = phase.tasks.filter((task) => task.id !== found.task.id);
|
|
631
|
+
phase.taskIds = phase.taskIds.filter((id) => id !== found.task.id);
|
|
632
|
+
phase.updatedAt = nowISO();
|
|
633
|
+
return phase;
|
|
634
|
+
});
|
|
635
|
+
await st.syncTaskStatusRollup(found.phase.id);
|
|
636
|
+
return writeAndSummarize(st, `Task deleted: ${found.task.id}`, { deleted: found.task.id });
|
|
637
|
+
});
|
|
638
|
+
server.registerTool("planner-task-start", {
|
|
639
|
+
description: "Set a task to in-progress.",
|
|
640
|
+
inputSchema: { task: z.string().min(1) },
|
|
641
|
+
}, async ({ task: ref }) => {
|
|
642
|
+
const st = await requireStore();
|
|
643
|
+
// Hygiene Gate: block starting work if a pending handoff exists.
|
|
644
|
+
if (existsSync(join(st.root, "HANDOFF.md"))) {
|
|
645
|
+
return text(`🚨 HYGIENE VIOLATION: A pending handoff file exists at .planner/HANDOFF.md. ` +
|
|
646
|
+
`You MUST read and delete it before you can officially start a task. ` +
|
|
647
|
+
`This is a non-negotiable rule in AGENTS.md.`);
|
|
648
|
+
}
|
|
649
|
+
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
650
|
+
if (!found)
|
|
651
|
+
return text(`Task not found: ${ref}`);
|
|
652
|
+
const timestamp = nowISO();
|
|
653
|
+
let updatedTask;
|
|
654
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
655
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
656
|
+
if (!task)
|
|
657
|
+
return phase;
|
|
658
|
+
applyTaskLifecycleDates(task, "in-progress", timestamp);
|
|
659
|
+
if (task.status !== "in-progress") {
|
|
660
|
+
const entry = {
|
|
661
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${task.status}-in-progress`),
|
|
662
|
+
date: timestamp,
|
|
663
|
+
fromStatus: task.status,
|
|
664
|
+
toStatus: "in-progress",
|
|
665
|
+
title: task.status === "done" ? "Reopened" : `→ in-progress`,
|
|
666
|
+
description: task.status === "done" ? "Task reopened from done status." : "",
|
|
667
|
+
};
|
|
668
|
+
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
669
|
+
}
|
|
670
|
+
task.updatedAt = timestamp;
|
|
671
|
+
phase.updatedAt = timestamp;
|
|
672
|
+
updatedTask = task;
|
|
673
|
+
return phase;
|
|
674
|
+
});
|
|
675
|
+
await st.syncTaskStatusRollup(found.phase.id);
|
|
676
|
+
return writeAndSummarize(st, `✅ Task started: ${found.task.id}`, { task: updatedTask ?? found.task });
|
|
677
|
+
});
|
|
678
|
+
server.registerTool("planner-task-complete", {
|
|
679
|
+
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
680
|
+
inputSchema: {
|
|
681
|
+
task: z.string().min(1),
|
|
682
|
+
force: z.boolean().optional(),
|
|
683
|
+
description_update: z.string().min(10).optional().describe("Post-hoc summary of what was done: commit hash(s), files touched, decisions made, updated code references with new line numbers. Keeps the planner alive and traceable."),
|
|
684
|
+
},
|
|
685
|
+
}, async ({ task: ref, force, description_update }) => {
|
|
686
|
+
const st = await requireStore();
|
|
687
|
+
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
688
|
+
if (!found)
|
|
689
|
+
return text(`Task not found: ${ref}`);
|
|
690
|
+
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
691
|
+
if (unchecked.length > 0 && !force)
|
|
692
|
+
return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
|
|
693
|
+
const timestamp = nowISO();
|
|
694
|
+
let updatedTask;
|
|
695
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
696
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
697
|
+
if (!task)
|
|
698
|
+
return phase;
|
|
699
|
+
applyTaskLifecycleDates(task, "done", timestamp);
|
|
700
|
+
if (task.status !== "done") {
|
|
701
|
+
const entry = {
|
|
702
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${task.status}-done`),
|
|
703
|
+
date: timestamp,
|
|
704
|
+
fromStatus: task.status,
|
|
705
|
+
toStatus: "done",
|
|
706
|
+
title: `→ done`,
|
|
707
|
+
description: "",
|
|
708
|
+
};
|
|
709
|
+
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
710
|
+
}
|
|
711
|
+
if (description_update) {
|
|
712
|
+
const sep = task.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
|
|
713
|
+
task.description = task.description + sep + description_update;
|
|
714
|
+
}
|
|
715
|
+
task.updatedAt = timestamp;
|
|
716
|
+
phase.updatedAt = timestamp;
|
|
717
|
+
updatedTask = task;
|
|
718
|
+
return phase;
|
|
719
|
+
});
|
|
720
|
+
await st.syncTaskStatusRollup(found.phase.id);
|
|
721
|
+
return writeAndSummarize(st, `✅ Task completed: ${found.task.id}`, { task: updatedTask ?? found.task });
|
|
722
|
+
});
|
|
723
|
+
server.registerTool("planner-handoff-show", {
|
|
724
|
+
description: "Show .planner/HANDOFF.md if present.",
|
|
725
|
+
}, async () => {
|
|
726
|
+
const st = await requireStore();
|
|
727
|
+
const handoff = await st.loadHandoff();
|
|
728
|
+
return text(handoff?.content ?? "No .planner/HANDOFF.md present.", { handoff: handoff ?? null });
|
|
729
|
+
});
|
|
730
|
+
server.registerTool("planner-handoff-write", {
|
|
731
|
+
description: "Write .planner/HANDOFF.md content.",
|
|
732
|
+
inputSchema: {
|
|
733
|
+
content: z.string().min(1),
|
|
734
|
+
reason: z.string().optional(),
|
|
735
|
+
},
|
|
736
|
+
}, async ({ content, reason }) => {
|
|
737
|
+
const st = await requireStore();
|
|
738
|
+
const finalContent = reason && !content.includes("Reason:") ? `Reason: ${reason}\n\n${content}` : content;
|
|
739
|
+
await st.saveHandoff(finalContent);
|
|
740
|
+
return text("Wrote .planner/HANDOFF.md");
|
|
741
|
+
});
|
|
742
|
+
server.registerTool("planner-handoff-prepare", {
|
|
743
|
+
description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write.",
|
|
744
|
+
}, async () => text([
|
|
745
|
+
"Prepare the canonical session handoff and write it with planner-handoff-write.",
|
|
746
|
+
"Required sections: Created at, Updated at, Reason, Current focus, What was being done, How to resume, Files touched, Blockers, Next steps, Recent decisions, Reminder.",
|
|
747
|
+
"Canonical path: .planner/HANDOFF.md.",
|
|
748
|
+
].join("\n")));
|
|
749
|
+
server.registerTool("planner-handoff-clear", {
|
|
750
|
+
description: "Delete .planner/HANDOFF.md.",
|
|
751
|
+
}, async () => {
|
|
752
|
+
const st = await requireStore();
|
|
753
|
+
await st.deleteHandoff();
|
|
754
|
+
return text("Deleted .planner/HANDOFF.md");
|
|
755
|
+
});
|
|
756
|
+
server.registerTool("planner-web", {
|
|
757
|
+
description: "Planner web command placeholder for MCP stdio. Use plan-server CLI directly for now.",
|
|
758
|
+
inputSchema: {
|
|
759
|
+
action: z.enum(["start", "stop", "status"]).default("status"),
|
|
760
|
+
},
|
|
761
|
+
}, async ({ action }) => text(`planner-web ${action}: MCP stdio package does not manage the web server yet. Use \`pnpm --filter @agent-plan/server build\` and the plan-server CLI, or Pi /planner web ${action}.`));
|
|
762
|
+
server.registerTool("planner-load", {
|
|
763
|
+
description: "MCP no-op equivalent of Pi /planner load. MCP server is already loaded when tools are available.",
|
|
764
|
+
}, async () => text("Planner MCP server is loaded."));
|
|
765
|
+
server.registerTool("planner-disable", {
|
|
766
|
+
description: "MCP no-op equivalent of Pi /planner disable. Stop the MCP process from the host to disable it.",
|
|
767
|
+
}, async () => text("To disable planner MCP, remove/disable this MCP server from the host config or stop the process."));
|
|
768
|
+
export async function startStdioServer() {
|
|
769
|
+
const transport = new StdioServerTransport();
|
|
770
|
+
await server.connect(transport);
|
|
771
|
+
console.error(`agent-plan MCP server running on stdio (root: ${planRoot()})`);
|
|
772
|
+
}
|
|
773
|
+
function isDirectExecution() {
|
|
774
|
+
return process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
|
|
775
|
+
}
|
|
776
|
+
if (isDirectExecution()) {
|
|
777
|
+
startStdioServer().catch((error) => {
|
|
778
|
+
console.error("agent-plan MCP server failed:", error);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
});
|
|
781
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-plan/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"agent-plan-mcp": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**/*.js",
|
|
18
|
+
"dist/**/*.d.ts",
|
|
19
|
+
"dist/**/*.d.ts.map",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/ovidius72/agent-planner.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/ovidius72/agent-planner#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/ovidius72/agent-planner/issues"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"mcp",
|
|
36
|
+
"model-context-protocol",
|
|
37
|
+
"planner",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"ai-agent"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
43
|
+
"zod": "^3.24.0",
|
|
44
|
+
"@agent-plan/core": "0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p tsconfig.json",
|
|
48
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|