@agent-plan/mcp 0.2.19-next.0 → 0.2.19-next.10
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/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -114
- package/package.json +3 -3
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAg8BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef } from "@agent-plan/core";
|
|
7
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, migrateToGlobalSequence } from "@agent-plan/core";
|
|
8
8
|
import { serve } from "@agent-plan/server";
|
|
9
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef } from "@agent-plan/core/naming";
|
|
9
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef } from "@agent-plan/core/naming";
|
|
10
10
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
11
11
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
12
12
|
function text(textValue, structuredContent) {
|
|
@@ -31,6 +31,7 @@ async function requireStore() {
|
|
|
31
31
|
if (!planBackfillDone) {
|
|
32
32
|
planBackfillDone = true;
|
|
33
33
|
await st.ensureShortIdsAndPriority().catch(() => { });
|
|
34
|
+
await migrateToGlobalSequence(st).catch(() => { });
|
|
34
35
|
}
|
|
35
36
|
return st;
|
|
36
37
|
}
|
|
@@ -55,65 +56,17 @@ async function ensureWebStarted() {
|
|
|
55
56
|
return { localUrl: "" };
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
|
-
/** Build a consolidated planner recap (project state + active work + pending
|
|
59
|
-
* handoff + web URL). Harness-agnostic equivalent of Pi's buildStartupResumeSummary. */
|
|
60
|
-
async function buildRecapText(st, web) {
|
|
61
|
-
const [plan, resume, handoffs] = await Promise.all([st.loadAll(), st.loadResume(), st.listHandoffs()]);
|
|
62
|
-
const allTasks = plan.phases.flatMap((p) => p.tasks.map((t) => ({ phase: p, task: t })));
|
|
63
|
-
const totalF = plan.features.features.length;
|
|
64
|
-
const doneF = plan.features.features.filter((f) => f.status === "done").length;
|
|
65
|
-
const activeF = plan.features.features.filter((f) => f.status === "in-progress").length;
|
|
66
|
-
const totalP = plan.phases.length;
|
|
67
|
-
const doneP = plan.phases.filter((p) => p.status === "done").length;
|
|
68
|
-
const activeP = plan.phases.filter((p) => p.status === "in-progress" || p.status === "discovery").length;
|
|
69
|
-
const totalT = allTasks.length;
|
|
70
|
-
const doneT = allTasks.filter(({ task }) => task.status === "done").length;
|
|
71
|
-
const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
|
|
72
|
-
const inProgress = allTasks.filter(({ task }) => task.status === "in-progress");
|
|
73
|
-
const focus = inProgress[0];
|
|
74
|
-
const focusFeature = focus ? plan.features.features.find((f) => f.id === focus.phase.featureId) : undefined;
|
|
75
|
-
const lines = [];
|
|
76
|
-
lines.push("## Planner recap");
|
|
77
|
-
const name = plan.project.name || "(unnamed project)";
|
|
78
|
-
lines.push(`Project: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
|
|
79
|
-
lines.push(`Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active)`);
|
|
80
|
-
if (focus) {
|
|
81
|
-
lines.push(`Current focus: ${focusFeature?.name ?? "?"} / ${focus.phase.title} / ${focus.task.title} (in-progress)`);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
lines.push("Current focus: no active task — review the plan and pick the next concrete task");
|
|
85
|
-
}
|
|
86
|
-
if (resume?.nextSteps?.length)
|
|
87
|
-
lines.push(`Next step: ${resume.nextSteps[0]}`);
|
|
88
|
-
if (handoffs.length > 0) {
|
|
89
|
-
lines.push("", `## Pending phase handoffs (${handoffs.length})`);
|
|
90
|
-
handoffs.forEach((h, i) => lines.push(`[${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`));
|
|
91
|
-
lines.push("", "→ Read the relevant one with planner-handoff-show <ref> (validate against current state), then call planner-handoff-clear <ref> once consumed (delete-on-resume).");
|
|
92
|
-
}
|
|
93
|
-
else if (activeT === 0) {
|
|
94
|
-
lines.push("", "No phase handoff pending and no task in-progress. Use planner-task-add / planner-task-start to begin work.");
|
|
95
|
-
}
|
|
96
|
-
if (web.localUrl) {
|
|
97
|
-
lines.push("", "## Web UI", `🌐 ${web.localUrl}${web.lanUrl ? " (LAN: " + web.lanUrl + ")" : ""}`);
|
|
98
|
-
}
|
|
99
|
-
return lines.join("\n");
|
|
100
|
-
}
|
|
101
59
|
function findFeatureByRef(features, ref) {
|
|
102
60
|
const normalized = ref.trim().toLowerCase();
|
|
103
61
|
return features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
104
62
|
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
105
63
|
?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
|
|
106
64
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|| entry.title.toLowerCase().includes(normalized));
|
|
113
|
-
if (task)
|
|
114
|
-
return { phase, task };
|
|
115
|
-
}
|
|
116
|
-
return undefined;
|
|
65
|
+
function featureNumberOfPhase(phase, features) {
|
|
66
|
+
return phase.featureId ? features.find((f) => f.id === phase.featureId)?.number : undefined;
|
|
67
|
+
}
|
|
68
|
+
function taskCompositeRef(task, phase, features) {
|
|
69
|
+
return `${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${String(task.number).padStart(3, "0")}`;
|
|
117
70
|
}
|
|
118
71
|
function applyTaskLifecycleDates(task, nextStatus, now) {
|
|
119
72
|
const previousStatus = task.status;
|
|
@@ -209,7 +162,7 @@ server.registerTool("planner-init", {
|
|
|
209
162
|
if (goal !== undefined)
|
|
210
163
|
project.goal = goal.trim();
|
|
211
164
|
await st.saveProject(project);
|
|
212
|
-
return writeAndSummarize(st, `.planner/ initialized for "${project.name}"
|
|
165
|
+
return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`);
|
|
213
166
|
});
|
|
214
167
|
server.registerTool("planner-show", {
|
|
215
168
|
description: "Show the current planner overview.",
|
|
@@ -227,11 +180,11 @@ server.registerTool("planner-show", {
|
|
|
227
180
|
].join("\n"), { plan });
|
|
228
181
|
});
|
|
229
182
|
server.registerTool("planner-repair", {
|
|
230
|
-
description: "Repair dangling feature→phase references and report integrity.",
|
|
183
|
+
description: "Repair dangling feature→phase references, rebuild phase containment from each task's own phaseId (heals the migrateToGlobalSequence task-shuffle bug where tasks land in the wrong phase file), and report integrity.",
|
|
231
184
|
}, async () => {
|
|
232
185
|
const st = await requireStore();
|
|
233
186
|
const report = await st.repair();
|
|
234
|
-
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 });
|
|
187
|
+
return text(`Repair done: renamed ${report.migrated.renamed}, repaired ${report.migrated.repaired} refs, inferred ${report.migrated.inferred}. Containment: ${report.containment.changed} phase files rewritten (${report.containment.tasks} tasks scanned, ${report.containment.orphan} orphan). Integrity: ${report.integrity.duplicatePhaseIds.length} duplicate, ${report.integrity.danglingPhaseIds.length} dangling.`, { report });
|
|
235
188
|
});
|
|
236
189
|
server.registerTool("planner-project-language", {
|
|
237
190
|
description: "Persist preferred languages for plan content and chat.",
|
|
@@ -247,7 +200,7 @@ server.registerTool("planner-project-language", {
|
|
|
247
200
|
if (chatLanguage !== undefined)
|
|
248
201
|
project.chatLanguage = chatLanguage.trim();
|
|
249
202
|
await st.saveProject(project);
|
|
250
|
-
return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}
|
|
203
|
+
return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}`);
|
|
251
204
|
});
|
|
252
205
|
server.registerTool("planner-project-discuss", {
|
|
253
206
|
description: "Record/update project-level discovery fields. This is the MCP equivalent of grouped project discuss without Pi UI prompts.",
|
|
@@ -278,20 +231,73 @@ server.registerTool("planner-project-discuss", {
|
|
|
278
231
|
if (params.decisions !== undefined)
|
|
279
232
|
project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
|
|
280
233
|
await st.saveProject(project);
|
|
281
|
-
return writeAndSummarize(st, `Project discussed/updated: ${project.name}
|
|
234
|
+
return writeAndSummarize(st, `Project discussed/updated: ${project.name}`);
|
|
282
235
|
});
|
|
283
236
|
server.registerTool("planner-feature-list", {
|
|
284
|
-
description: "List features
|
|
285
|
-
|
|
237
|
+
description: "List features (compact: F00x · shortId — name (status; N phases, M tasks)). Pass featureRef to filter. Use this to discover refs cheaply — do NOT read .planner/ files or planner-plan-get full=true to find entities.",
|
|
238
|
+
inputSchema: { featureRef: z.string().optional().describe("Optional: filter to one feature (F00x/shortId/UUID/name).") },
|
|
239
|
+
}, async ({ featureRef }) => {
|
|
286
240
|
const st = await requireStore();
|
|
287
|
-
const
|
|
241
|
+
const allFeatures = (await st.loadFeatures()).features;
|
|
242
|
+
const features = featureRef ? allFeatures.filter((f) => f.id === findFeatureByRef(allFeatures, featureRef)?.id) : allFeatures;
|
|
288
243
|
const phases = await st.loadAllPhases();
|
|
289
244
|
const lines = features.map((feature) => {
|
|
290
245
|
const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
|
|
291
246
|
const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
292
|
-
return `- ${feature.
|
|
247
|
+
return `- ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
|
|
293
248
|
});
|
|
294
|
-
return text(lines.join("\n") || "No features"
|
|
249
|
+
return text(lines.join("\n") || "No features");
|
|
250
|
+
});
|
|
251
|
+
server.registerTool("planner-phase-list", {
|
|
252
|
+
description: "List phases (compact: F00x/P00x · shortId — title (status; N tasks) [F00x]). Filters: featureRef, status. Cheap discovery — do NOT read .planner/ files or planner-plan-get full=true.",
|
|
253
|
+
inputSchema: { featureRef: z.string().optional().describe("Optional: filter to one feature (F00x/shortId/UUID/name)."), status: z.string().optional().describe("Optional: filter by status name.") },
|
|
254
|
+
}, async ({ featureRef, status }) => {
|
|
255
|
+
const st = await requireStore();
|
|
256
|
+
const features = (await st.loadFeatures()).features;
|
|
257
|
+
let phases = await st.loadAllPhases();
|
|
258
|
+
if (featureRef) {
|
|
259
|
+
const f = findFeatureByRef(features, featureRef);
|
|
260
|
+
if (!f)
|
|
261
|
+
return text(`Feature not found: ${featureRef}`);
|
|
262
|
+
phases = phases.filter((p) => p.featureId === f.id);
|
|
263
|
+
}
|
|
264
|
+
if (status)
|
|
265
|
+
phases = phases.filter((p) => p.status === status);
|
|
266
|
+
const lines = phases.map((phase) => {
|
|
267
|
+
const fNum = featureNumberOfPhase(phase, features);
|
|
268
|
+
const fTag = fNum !== undefined ? ` [F${String(fNum).padStart(3, "0")}]` : "";
|
|
269
|
+
return `- ${formatPhaseRef(phase.number, fNum)}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.status}; ${phase.tasks.length} tasks)${fTag}`;
|
|
270
|
+
});
|
|
271
|
+
return text(lines.join("\n") || "No phases");
|
|
272
|
+
});
|
|
273
|
+
server.registerTool("planner-task-list", {
|
|
274
|
+
description: "List tasks (compact: F00x/P00x/T00x · shortId — title (status)). Filters: featureRef, phaseRef, status. Cheap discovery — do NOT read .planner/ files or planner-plan-get full=true.",
|
|
275
|
+
inputSchema: { featureRef: z.string().optional(), phaseRef: z.string().optional(), status: z.string().optional().describe("Optional: filter by status name.") },
|
|
276
|
+
}, async ({ featureRef, phaseRef, status }) => {
|
|
277
|
+
const st = await requireStore();
|
|
278
|
+
const features = (await st.loadFeatures()).features;
|
|
279
|
+
let phases = await st.loadAllPhases();
|
|
280
|
+
if (featureRef) {
|
|
281
|
+
const f = findFeatureByRef(features, featureRef);
|
|
282
|
+
if (!f)
|
|
283
|
+
return text(`Feature not found: ${featureRef}`);
|
|
284
|
+
phases = phases.filter((p) => p.featureId === f.id);
|
|
285
|
+
}
|
|
286
|
+
if (phaseRef) {
|
|
287
|
+
const p = findPhaseByRef(phases, features, phaseRef);
|
|
288
|
+
if (!p)
|
|
289
|
+
return text(`Phase not found: ${phaseRef}`);
|
|
290
|
+
phases = [p];
|
|
291
|
+
}
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const phase of phases) {
|
|
294
|
+
for (const task of phase.tasks) {
|
|
295
|
+
if (status && task.status !== status)
|
|
296
|
+
continue;
|
|
297
|
+
out.push(`- ${taskCompositeRef(task, phase, features)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status})`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return text(out.join("\n") || "No tasks");
|
|
295
301
|
});
|
|
296
302
|
server.registerTool("planner-feature-add", {
|
|
297
303
|
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.",
|
|
@@ -309,7 +315,7 @@ server.registerTool("planner-feature-add", {
|
|
|
309
315
|
const priority = await st.nextPriority("feature");
|
|
310
316
|
const feature = {
|
|
311
317
|
id: createFeatureId(),
|
|
312
|
-
number:
|
|
318
|
+
number: await st.allocFeatureNumber(),
|
|
313
319
|
shortId,
|
|
314
320
|
priority,
|
|
315
321
|
name: name.trim(),
|
|
@@ -332,24 +338,25 @@ server.registerTool("planner-feature-add", {
|
|
|
332
338
|
doc.features.push(feature);
|
|
333
339
|
return doc;
|
|
334
340
|
});
|
|
335
|
-
return writeAndSummarize(st,
|
|
341
|
+
return writeAndSummarize(st, `✅ Feature created: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}`);
|
|
336
342
|
});
|
|
337
343
|
server.registerTool("planner-feature-show", {
|
|
338
344
|
description: "Show a feature by id or name.",
|
|
339
|
-
inputSchema: { feature: z.string().min(1).describe("Feature
|
|
340
|
-
}, async ({ feature: ref }) => {
|
|
345
|
+
inputSchema: { feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the feature description. Default: compact identity only (saves tokens).") },
|
|
346
|
+
}, async ({ feature: ref, full }) => {
|
|
341
347
|
const st = await requireStore();
|
|
342
348
|
const features = (await st.loadFeatures()).features;
|
|
343
349
|
const feature = findFeatureByRef(features, ref);
|
|
344
350
|
if (!feature)
|
|
345
351
|
return text(`Feature not found: ${ref}`);
|
|
346
352
|
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
347
|
-
|
|
353
|
+
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
|
|
354
|
+
return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
|
|
348
355
|
});
|
|
349
356
|
server.registerTool("planner-feature-update", {
|
|
350
357
|
description: "Update a feature.",
|
|
351
358
|
inputSchema: {
|
|
352
|
-
feature: z.string().min(1).describe("Feature
|
|
359
|
+
feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
353
360
|
name: z.string().optional(),
|
|
354
361
|
description: z.string().optional(),
|
|
355
362
|
status: z.enum(STATUS_VALUES).optional(),
|
|
@@ -389,12 +396,12 @@ server.registerTool("planner-feature-update", {
|
|
|
389
396
|
return doc;
|
|
390
397
|
});
|
|
391
398
|
const result = updated.features.find((entry) => entry.id === feature.id);
|
|
392
|
-
return writeAndSummarize(st,
|
|
399
|
+
return writeAndSummarize(st, `✅ Feature updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}`);
|
|
393
400
|
});
|
|
394
401
|
server.registerTool("planner-feature-delete", {
|
|
395
402
|
description: "Delete a feature. By default, phases are unlinked rather than deleted.",
|
|
396
403
|
inputSchema: {
|
|
397
|
-
feature: z.string().min(1).describe("Feature
|
|
404
|
+
feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
398
405
|
cascade: z.boolean().optional().describe("Also delete phases belonging to this feature"),
|
|
399
406
|
},
|
|
400
407
|
}, async ({ feature: ref, cascade }) => {
|
|
@@ -424,7 +431,7 @@ server.registerTool("planner-phase-add", {
|
|
|
424
431
|
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.",
|
|
425
432
|
inputSchema: {
|
|
426
433
|
title: z.string().min(1),
|
|
427
|
-
feature: z.string().optional().describe("Feature
|
|
434
|
+
feature: z.string().optional().describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
428
435
|
summary: z.string().optional().describe("One-line summary of the phase"),
|
|
429
436
|
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."),
|
|
430
437
|
},
|
|
@@ -444,7 +451,7 @@ server.registerTool("planner-phase-add", {
|
|
|
444
451
|
const priority = await st.nextPriority("phase", feature?.id);
|
|
445
452
|
phase = {
|
|
446
453
|
id: createPhaseId(),
|
|
447
|
-
number:
|
|
454
|
+
number: await st.allocPhaseNumber(),
|
|
448
455
|
shortId,
|
|
449
456
|
priority,
|
|
450
457
|
slug: normalizeSlug(title),
|
|
@@ -472,6 +479,8 @@ server.registerTool("planner-phase-add", {
|
|
|
472
479
|
updatedAt: timestamp,
|
|
473
480
|
handoff: "",
|
|
474
481
|
handoffUpdatedAt: "",
|
|
482
|
+
handoffReadAt: "",
|
|
483
|
+
handoffHistory: [],
|
|
475
484
|
};
|
|
476
485
|
await st.savePhase(phase);
|
|
477
486
|
if (feature) {
|
|
@@ -486,22 +495,24 @@ server.registerTool("planner-phase-add", {
|
|
|
486
495
|
});
|
|
487
496
|
if (!phase)
|
|
488
497
|
return text("Phase creation failed.");
|
|
489
|
-
return writeAndSummarize(st,
|
|
498
|
+
return writeAndSummarize(st, `✅ Phase created: ${formatPhaseRef(phase.number, feature?.number)} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
|
|
490
499
|
});
|
|
491
500
|
server.registerTool("planner-phase-show", {
|
|
492
501
|
description: "Show a phase by id or name.",
|
|
493
|
-
inputSchema: { phase: z.string().min(1).describe("Phase
|
|
494
|
-
}, async ({ phase: ref }) => {
|
|
502
|
+
inputSchema: { phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the phase description. Default: compact identity only (saves tokens).") },
|
|
503
|
+
}, async ({ phase: ref, full }) => {
|
|
495
504
|
const st = await requireStore();
|
|
496
|
-
const
|
|
505
|
+
const features = (await st.loadFeatures()).features;
|
|
506
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
497
507
|
if (!phase)
|
|
498
508
|
return text(`Phase not found: ${ref}`);
|
|
499
|
-
|
|
509
|
+
const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks)`;
|
|
510
|
+
return text(full ? `${summary}\n\n${phase.description || ""}` : summary);
|
|
500
511
|
});
|
|
501
512
|
server.registerTool("planner-phase-discuss", {
|
|
502
513
|
description: "Persist phase discovery fields and mark phase planned.",
|
|
503
514
|
inputSchema: {
|
|
504
|
-
phase: z.string().min(1).describe("Phase
|
|
515
|
+
phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
505
516
|
goal: z.string().optional(),
|
|
506
517
|
summary: z.string().optional(),
|
|
507
518
|
scope: z.string().optional(),
|
|
@@ -512,7 +523,8 @@ server.registerTool("planner-phase-discuss", {
|
|
|
512
523
|
},
|
|
513
524
|
}, async ({ phase: ref, ...updates }) => {
|
|
514
525
|
const st = await requireStore();
|
|
515
|
-
const
|
|
526
|
+
const features = (await st.loadFeatures()).features;
|
|
527
|
+
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
516
528
|
if (!found)
|
|
517
529
|
return text(`Phase not found: ${ref}`);
|
|
518
530
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
@@ -537,7 +549,7 @@ server.registerTool("planner-phase-discuss", {
|
|
|
537
549
|
entry.updatedAt = nowISO();
|
|
538
550
|
return entry;
|
|
539
551
|
});
|
|
540
|
-
return writeAndSummarize(st,
|
|
552
|
+
return writeAndSummarize(st, `✅ Phase discussed/planned: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
|
|
541
553
|
});
|
|
542
554
|
server.registerTool("planner-phase-update", {
|
|
543
555
|
description: "Update phase fields.",
|
|
@@ -551,7 +563,8 @@ server.registerTool("planner-phase-update", {
|
|
|
551
563
|
},
|
|
552
564
|
}, async ({ phase: ref, ...updates }) => {
|
|
553
565
|
const st = await requireStore();
|
|
554
|
-
const
|
|
566
|
+
const features = (await st.loadFeatures()).features;
|
|
567
|
+
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
555
568
|
if (!found)
|
|
556
569
|
return text(`Phase not found: ${ref}`);
|
|
557
570
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
@@ -568,7 +581,7 @@ server.registerTool("planner-phase-update", {
|
|
|
568
581
|
entry.updatedAt = nowISO();
|
|
569
582
|
return entry;
|
|
570
583
|
});
|
|
571
|
-
return writeAndSummarize(st,
|
|
584
|
+
return writeAndSummarize(st, `✅ Phase updated: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
|
|
572
585
|
});
|
|
573
586
|
server.registerTool("planner-phase-delete", {
|
|
574
587
|
description: "Delete a phase and unlink it from features.",
|
|
@@ -584,12 +597,13 @@ server.registerTool("planner-phase-delete", {
|
|
|
584
597
|
feature.phaseIds = feature.phaseIds.filter((id) => id !== phase.id);
|
|
585
598
|
return doc;
|
|
586
599
|
});
|
|
587
|
-
|
|
600
|
+
const dFeat = (await st.loadFeatures()).features.find((f) => f.id === phase.featureId);
|
|
601
|
+
return writeAndSummarize(st, `Phase deleted: ${formatPhaseRef(phase.number, dFeat?.number)}${phase.shortId ? ` · ${phase.shortId}` : ""}`, { deleted: phase.id });
|
|
588
602
|
});
|
|
589
603
|
server.registerTool("planner-task-add", {
|
|
590
604
|
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.",
|
|
591
605
|
inputSchema: {
|
|
592
|
-
phase: z.string().min(1).describe("Phase
|
|
606
|
+
phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
593
607
|
title: z.string().min(1),
|
|
594
608
|
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."),
|
|
595
609
|
checklist: z.array(z.string()).optional(),
|
|
@@ -606,7 +620,7 @@ server.registerTool("planner-task-add", {
|
|
|
606
620
|
const task = {
|
|
607
621
|
id: taskId,
|
|
608
622
|
phaseId: found.id,
|
|
609
|
-
number:
|
|
623
|
+
number: await st.allocTaskNumber(),
|
|
610
624
|
shortId,
|
|
611
625
|
priority,
|
|
612
626
|
shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
|
|
@@ -625,23 +639,29 @@ server.registerTool("planner-task-add", {
|
|
|
625
639
|
createdAt: timestamp,
|
|
626
640
|
updatedAt: timestamp,
|
|
627
641
|
};
|
|
628
|
-
|
|
642
|
+
await st.updatePhase(found.id, (entry) => {
|
|
629
643
|
entry.tasks.push(task);
|
|
630
644
|
entry.taskIds.push(task.id);
|
|
631
645
|
entry.updatedAt = timestamp;
|
|
632
646
|
return entry;
|
|
633
647
|
});
|
|
634
|
-
|
|
648
|
+
const features = (await st.loadFeatures()).features;
|
|
649
|
+
return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, features)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
|
|
635
650
|
});
|
|
636
651
|
server.registerTool("planner-task-show", {
|
|
637
652
|
description: "Show a task by id or name.",
|
|
638
|
-
inputSchema: { task: z.string().min(1).describe("Task
|
|
639
|
-
}, async ({ task: ref }) => {
|
|
653
|
+
inputSchema: { task: z.string().min(1).describe("Task ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the task description + statusLog. Default: compact identity only (saves tokens).") },
|
|
654
|
+
}, async ({ task: ref, full }) => {
|
|
640
655
|
const st = await requireStore();
|
|
641
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
656
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
642
657
|
if (!found)
|
|
643
658
|
return text(`Task not found: ${ref}`);
|
|
644
|
-
|
|
659
|
+
const features = (await st.loadFeatures()).features;
|
|
660
|
+
const summary = `${found.task.title} — ${taskCompositeRef(found.task, found.phase, features)}${found.task.shortId ? ` · ${found.task.shortId}` : ""} (${found.task.status}; phase ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))})`;
|
|
661
|
+
if (!full)
|
|
662
|
+
return text(summary);
|
|
663
|
+
const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
|
|
664
|
+
return text(`${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}`);
|
|
645
665
|
});
|
|
646
666
|
server.registerTool("planner-task-discuss", {
|
|
647
667
|
description: "Persist task execution notes/checklist.",
|
|
@@ -652,7 +672,7 @@ server.registerTool("planner-task-discuss", {
|
|
|
652
672
|
},
|
|
653
673
|
}, async ({ task: ref, description, checklist }) => {
|
|
654
674
|
const st = await requireStore();
|
|
655
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
675
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
656
676
|
if (!found)
|
|
657
677
|
return text(`Task not found: ${ref}`);
|
|
658
678
|
let updatedTask;
|
|
@@ -669,7 +689,9 @@ server.registerTool("planner-task-discuss", {
|
|
|
669
689
|
updatedTask = task;
|
|
670
690
|
return phase;
|
|
671
691
|
});
|
|
672
|
-
|
|
692
|
+
const features = (await st.loadFeatures()).features;
|
|
693
|
+
const t = updatedTask ?? found.task;
|
|
694
|
+
return writeAndSummarize(st, `✅ Task discussed/updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
673
695
|
});
|
|
674
696
|
server.registerTool("planner-task-update", {
|
|
675
697
|
description: "Update task fields. A motivation is REQUIRED when changing to blocked, canceled, deferred, rejected, waiting, or back to planned from another status.",
|
|
@@ -683,7 +705,7 @@ server.registerTool("planner-task-update", {
|
|
|
683
705
|
},
|
|
684
706
|
}, async ({ task: ref, title, status, description, motivation, priority }) => {
|
|
685
707
|
const st = await requireStore();
|
|
686
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
708
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
687
709
|
if (!found)
|
|
688
710
|
return text(`Task not found: ${ref}`);
|
|
689
711
|
// Validate motivation requirement for status transitions.
|
|
@@ -724,14 +746,16 @@ server.registerTool("planner-task-update", {
|
|
|
724
746
|
return phase;
|
|
725
747
|
});
|
|
726
748
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
727
|
-
|
|
749
|
+
const features = (await st.loadFeatures()).features;
|
|
750
|
+
const t = updatedTask ?? found.task;
|
|
751
|
+
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
728
752
|
});
|
|
729
753
|
server.registerTool("planner-task-delete", {
|
|
730
754
|
description: "Delete a task.",
|
|
731
755
|
inputSchema: { task: z.string().min(1) },
|
|
732
756
|
}, async ({ task: ref }) => {
|
|
733
757
|
const st = await requireStore();
|
|
734
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
758
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
735
759
|
if (!found)
|
|
736
760
|
return text(`Task not found: ${ref}`);
|
|
737
761
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -741,23 +765,30 @@ server.registerTool("planner-task-delete", {
|
|
|
741
765
|
return phase;
|
|
742
766
|
});
|
|
743
767
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
744
|
-
|
|
768
|
+
const tdFeatures = (await st.loadFeatures()).features;
|
|
769
|
+
return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
|
|
745
770
|
});
|
|
746
771
|
server.registerTool("planner-task-start", {
|
|
747
772
|
description: "Set a task to in-progress.",
|
|
748
773
|
inputSchema: { task: z.string().min(1) },
|
|
749
774
|
}, async ({ task: ref }) => {
|
|
750
775
|
const st = await requireStore();
|
|
751
|
-
|
|
752
|
-
// task_start — the handoff is a captured-context artifact, not a lock (e.g. a
|
|
753
|
-
// small modification after writing a handoff should start without forcing
|
|
754
|
-
// deletion, which would lose context).
|
|
755
|
-
const handoffNotice = (await st.listHandoffs()).length > 0
|
|
756
|
-
? "ℹ️ One or more phases have a pending handoff — if relevant to the task you're starting, read it with planner-handoff-show <ref>, then clear with planner-handoff-clear. Proceeding with task start.\n"
|
|
757
|
-
: "";
|
|
758
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
776
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
759
777
|
if (!found)
|
|
760
778
|
return text(`Task not found: ${ref}`);
|
|
779
|
+
// Auto-archive the handoff on THIS task's phase (reason: task-started) — the
|
|
780
|
+
// agent is now actively working, so the captured context is consumed. Other
|
|
781
|
+
// phases' handoffs are left untouched (informational, non-blocking).
|
|
782
|
+
const allHandoffs = await st.listHandoffs();
|
|
783
|
+
let handoffNotice = "";
|
|
784
|
+
if ((found.phase.handoff ?? "") !== "") {
|
|
785
|
+
await st.clearPhaseHandoff(found.phase.id, "task-started").catch(() => { });
|
|
786
|
+
handoffNotice = "📦 Archived handoff for this phase (task started) — recover via planner-handoff-list + .planner/handoff-archive/.\n";
|
|
787
|
+
}
|
|
788
|
+
const _otherHandoffs = allHandoffs.filter((h) => h.phaseId !== found.phase.id);
|
|
789
|
+
if (_otherHandoffs.length > 0) {
|
|
790
|
+
handoffNotice += `ℹ️ ${_otherHandoffs.length} other phase handoff(s) pending — review with planner-handoff-list if relevant.\n`;
|
|
791
|
+
}
|
|
761
792
|
const timestamp = nowISO();
|
|
762
793
|
let updatedTask;
|
|
763
794
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -782,7 +813,9 @@ server.registerTool("planner-task-start", {
|
|
|
782
813
|
return phase;
|
|
783
814
|
});
|
|
784
815
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
785
|
-
|
|
816
|
+
const features = (await st.loadFeatures()).features;
|
|
817
|
+
const t = updatedTask ?? found.task;
|
|
818
|
+
return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
786
819
|
});
|
|
787
820
|
server.registerTool("planner-task-complete", {
|
|
788
821
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -793,7 +826,7 @@ server.registerTool("planner-task-complete", {
|
|
|
793
826
|
},
|
|
794
827
|
}, async ({ task: ref, force, description_update }) => {
|
|
795
828
|
const st = await requireStore();
|
|
796
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
829
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
797
830
|
if (!found)
|
|
798
831
|
return text(`Task not found: ${ref}`);
|
|
799
832
|
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
@@ -827,7 +860,9 @@ server.registerTool("planner-task-complete", {
|
|
|
827
860
|
return phase;
|
|
828
861
|
});
|
|
829
862
|
const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
|
|
830
|
-
|
|
863
|
+
const features = (await st.loadFeatures()).features;
|
|
864
|
+
const t = updatedTask ?? found.task;
|
|
865
|
+
return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`);
|
|
831
866
|
});
|
|
832
867
|
server.registerTool("planner-handoff-list", {
|
|
833
868
|
description: "List all phases with a non-empty entity-scoped handoff (phase.handoff field). Returns composite refs (P00x / P00x(F00x)) with the first line and last-updated time.",
|
|
@@ -881,8 +916,8 @@ server.registerTool("planner-handoff-clear", {
|
|
|
881
916
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
882
917
|
if (!r.ok)
|
|
883
918
|
return text(`❌ ${r.error}`);
|
|
884
|
-
await st.clearPhaseHandoff(r.phase.id);
|
|
885
|
-
return text(`✅ Cleared handoff on ${r.compositeRef}
|
|
919
|
+
await st.clearPhaseHandoff(r.phase.id, "manual");
|
|
920
|
+
return text(`✅ Cleared handoff on ${r.compositeRef} (handoffUpdatedAt preserved as audit; archived to .planner/handoff-archive/).`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
886
921
|
});
|
|
887
922
|
server.registerTool("planner-web", {
|
|
888
923
|
description: "Manage the planner web dashboard (start/status/stop) from MCP stdio. Runs an in-process HTTP+WS server bound to LAN; it lives as long as this MCP process. plan-root comes from AGENT_PLAN_ROOT or cwd()/.planner.",
|
|
@@ -931,7 +966,7 @@ server.registerTool("planner-load", {
|
|
|
931
966
|
if (!(await st.exists()))
|
|
932
967
|
return text("No .planner/ found at " + planRoot() + ". Run planner-init first.");
|
|
933
968
|
const web = await ensureWebStarted();
|
|
934
|
-
const recap = await
|
|
969
|
+
const recap = await buildRecap(st, web, { harness: "mcp" });
|
|
935
970
|
return text(recap);
|
|
936
971
|
});
|
|
937
972
|
server.registerTool("planner-disable", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.19-next.
|
|
3
|
+
"version": "0.2.19-next.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
43
43
|
"zod": "^3.24.0",
|
|
44
|
-
"@agent-plan/core": "0.2.19-next.
|
|
45
|
-
"@agent-plan/server": "0.2.19-next.
|
|
44
|
+
"@agent-plan/core": "0.2.19-next.10",
|
|
45
|
+
"@agent-plan/server": "0.2.19-next.10"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc -p tsconfig.json",
|