@agent-plan/mcp 0.2.18 → 0.2.19-next.1
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 +141 -84
- 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":";AAm5BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
6
|
import { pathToFileURL } from "node:url";
|
|
8
|
-
import { PlanStore, ExportService, withFeatureLock, needsMotivation } from "@agent-plan/core";
|
|
7
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, buildRecap } from "@agent-plan/core";
|
|
9
8
|
import { serve } from "@agent-plan/server";
|
|
10
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug } from "@agent-plan/core/naming";
|
|
9
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef } from "@agent-plan/core/naming";
|
|
11
10
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
12
11
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
13
12
|
function text(textValue, structuredContent) {
|
|
@@ -28,8 +27,14 @@ async function requireStore() {
|
|
|
28
27
|
const st = store();
|
|
29
28
|
if (!(await st.exists()))
|
|
30
29
|
throw new Error(`No .planner/ found at ${st.root}. Use planner-init first.`);
|
|
30
|
+
// One-time idempotent backfill of shortId + priority (safe, best-effort).
|
|
31
|
+
if (!planBackfillDone) {
|
|
32
|
+
planBackfillDone = true;
|
|
33
|
+
await st.ensureShortIdsAndPriority().catch(() => { });
|
|
34
|
+
}
|
|
31
35
|
return st;
|
|
32
36
|
}
|
|
37
|
+
let planBackfillDone = false;
|
|
33
38
|
// In-process web server handle, managed by the planner-web tool and planner-load.
|
|
34
39
|
// Lives as long as this MCP stdio process (i.e. the host session). Null when not running.
|
|
35
40
|
let webHandle = null;
|
|
@@ -50,63 +55,33 @@ async function ensureWebStarted() {
|
|
|
50
55
|
return { localUrl: "" };
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
|
-
/** Build a consolidated planner recap (project state + active work + pending
|
|
54
|
-
* handoff + web URL). Harness-agnostic equivalent of Pi's buildStartupResumeSummary. */
|
|
55
|
-
async function buildRecapText(st, web) {
|
|
56
|
-
const [plan, resume, handoff] = await Promise.all([st.loadAll(), st.loadResume(), st.loadHandoff()]);
|
|
57
|
-
const allTasks = plan.phases.flatMap((p) => p.tasks.map((t) => ({ phase: p, task: t })));
|
|
58
|
-
const totalF = plan.features.features.length;
|
|
59
|
-
const doneF = plan.features.features.filter((f) => f.status === "done").length;
|
|
60
|
-
const activeF = plan.features.features.filter((f) => f.status === "in-progress").length;
|
|
61
|
-
const totalP = plan.phases.length;
|
|
62
|
-
const doneP = plan.phases.filter((p) => p.status === "done").length;
|
|
63
|
-
const activeP = plan.phases.filter((p) => p.status === "in-progress" || p.status === "discovery").length;
|
|
64
|
-
const totalT = allTasks.length;
|
|
65
|
-
const doneT = allTasks.filter(({ task }) => task.status === "done").length;
|
|
66
|
-
const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
|
|
67
|
-
const inProgress = allTasks.filter(({ task }) => task.status === "in-progress");
|
|
68
|
-
const focus = inProgress[0];
|
|
69
|
-
const focusFeature = focus ? plan.features.features.find((f) => f.id === focus.phase.featureId) : undefined;
|
|
70
|
-
const lines = [];
|
|
71
|
-
lines.push("## Planner recap");
|
|
72
|
-
const name = plan.project.name || "(unnamed project)";
|
|
73
|
-
lines.push(`Project: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
|
|
74
|
-
lines.push(`Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active)`);
|
|
75
|
-
if (focus) {
|
|
76
|
-
lines.push(`Current focus: ${focusFeature?.name ?? "?"} / ${focus.phase.title} / ${focus.task.title} (in-progress)`);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
lines.push("Current focus: no active task — review the plan and pick the next concrete task");
|
|
80
|
-
}
|
|
81
|
-
if (resume?.nextSteps?.length)
|
|
82
|
-
lines.push(`Next step: ${resume.nextSteps[0]}`);
|
|
83
|
-
if (handoff) {
|
|
84
|
-
lines.push("", "## Pending handoff (read me first)", handoff.content || "(empty handoff)", "", "→ After processing the handoff above, call planner-handoff-clear to remove it.");
|
|
85
|
-
}
|
|
86
|
-
else if (activeT === 0) {
|
|
87
|
-
lines.push("", "No handoff pending and no task in-progress. Use planner-task-add / planner-task-start to begin work.");
|
|
88
|
-
}
|
|
89
|
-
if (web.localUrl) {
|
|
90
|
-
lines.push("", "## Web UI", `🌐 ${web.localUrl}${web.lanUrl ? " (LAN: " + web.lanUrl + ")" : ""}`);
|
|
91
|
-
}
|
|
92
|
-
return lines.join("\n");
|
|
93
|
-
}
|
|
94
58
|
function findFeatureByRef(features, ref) {
|
|
95
59
|
const normalized = ref.trim().toLowerCase();
|
|
96
60
|
return features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
97
61
|
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
98
62
|
?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
|
|
99
63
|
}
|
|
100
|
-
function findPhaseByRef(phases, ref) {
|
|
101
|
-
const normalized = ref.trim().toLowerCase();
|
|
102
|
-
return phases.find((phase) => phase.id.toLowerCase() === normalized)
|
|
103
|
-
?? phases.find((phase) => phase.title.toLowerCase() === normalized)
|
|
104
|
-
?? phases.find((phase) => phase.title.toLowerCase().includes(normalized));
|
|
105
|
-
}
|
|
106
64
|
function findTaskByRef(phases, ref) {
|
|
107
65
|
const normalized = ref.trim().toLowerCase();
|
|
66
|
+
if (!normalized)
|
|
67
|
+
return undefined;
|
|
68
|
+
// Composite ref like "P00x(F00x)/T00x" or "T00x(P00x/F00x)": match by phase + task number.
|
|
69
|
+
const tMatch = normalized.match(/t0*(\d+)/);
|
|
70
|
+
const pMatch = normalized.match(/p0*(\d+)/);
|
|
71
|
+
if (tMatch && pMatch) {
|
|
72
|
+
const tNum = parseInt(tMatch[1], 10);
|
|
73
|
+
const pNum = parseInt(pMatch[1], 10);
|
|
74
|
+
for (const phase of phases) {
|
|
75
|
+
if (phase.number !== pNum)
|
|
76
|
+
continue;
|
|
77
|
+
const task = phase.tasks.find((t) => t.number === tNum);
|
|
78
|
+
if (task)
|
|
79
|
+
return { phase, task };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
108
82
|
for (const phase of phases) {
|
|
109
83
|
const task = phase.tasks.find((entry) => entry.id.toLowerCase() === normalized
|
|
84
|
+
|| (entry.shortId && entry.shortId.toLowerCase() === normalized)
|
|
110
85
|
|| entry.title.toLowerCase() === normalized
|
|
111
86
|
|| entry.title.toLowerCase().includes(normalized));
|
|
112
87
|
if (task)
|
|
@@ -114,6 +89,12 @@ function findTaskByRef(phases, ref) {
|
|
|
114
89
|
}
|
|
115
90
|
return undefined;
|
|
116
91
|
}
|
|
92
|
+
function featureNumberOfPhase(phase, features) {
|
|
93
|
+
return phase.featureId ? features.find((f) => f.id === phase.featureId)?.number : undefined;
|
|
94
|
+
}
|
|
95
|
+
function taskCompositeRef(task, phase, features) {
|
|
96
|
+
return `${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${String(task.number).padStart(3, "0")}`;
|
|
97
|
+
}
|
|
117
98
|
function applyTaskLifecycleDates(task, nextStatus, now) {
|
|
118
99
|
const previousStatus = task.status;
|
|
119
100
|
if (nextStatus === "in-progress" && !task.startedAt)
|
|
@@ -136,6 +117,28 @@ const server = new McpServer({
|
|
|
136
117
|
name: "agent-plan-planner",
|
|
137
118
|
version: "0.1.0",
|
|
138
119
|
});
|
|
120
|
+
async function resolvePhaseForHandoff(st, ref) {
|
|
121
|
+
const phases = await st.loadAllPhases();
|
|
122
|
+
const features = (await st.loadFeatures()).features;
|
|
123
|
+
let phase;
|
|
124
|
+
if (ref && ref.trim()) {
|
|
125
|
+
phase = findPhaseByRef(phases, features, ref.trim());
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
phase = phases.find((p) => p.status === "in-progress")
|
|
129
|
+
?? phases.find((p) => p.tasks.some((t) => t.status === "in-progress"));
|
|
130
|
+
}
|
|
131
|
+
if (!phase) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: ref && ref.trim()
|
|
135
|
+
? `Phase not found: "${ref.trim()}". Use P00x, P00x(F00x), UUID, or title.`
|
|
136
|
+
: "No in-progress phase. Specify a phaseRef (P00x or P00x(F00x)).",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
|
|
140
|
+
return { ok: true, phase, compositeRef: formatPhaseRef(phase.number, feat?.number) };
|
|
141
|
+
}
|
|
139
142
|
server.registerTool("planner-export", {
|
|
140
143
|
description: "Export the project plan as a Markdown report. Supports a concise summary or full hierarchical detail.",
|
|
141
144
|
inputSchema: {
|
|
@@ -282,9 +285,13 @@ server.registerTool("planner-feature-add", {
|
|
|
282
285
|
const timestamp = nowISO();
|
|
283
286
|
const effectiveStatus = status ?? "planned";
|
|
284
287
|
const existingFeatures = (await st.loadFeatures()).features;
|
|
288
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
289
|
+
const priority = await st.nextPriority("feature");
|
|
285
290
|
const feature = {
|
|
286
291
|
id: createFeatureId(),
|
|
287
292
|
number: existingFeatures.length + 1,
|
|
293
|
+
shortId,
|
|
294
|
+
priority,
|
|
288
295
|
name: name.trim(),
|
|
289
296
|
description: description?.trim() ?? "",
|
|
290
297
|
status: effectiveStatus,
|
|
@@ -330,6 +337,7 @@ server.registerTool("planner-feature-update", {
|
|
|
330
337
|
workRemaining: z.string().optional(),
|
|
331
338
|
startDate: z.string().optional(),
|
|
332
339
|
endDate: z.string().optional(),
|
|
340
|
+
priority: z.number().int().nonnegative().optional().describe("Display order within the project (lower = higher). Tiebreak by number then createdAt."),
|
|
333
341
|
},
|
|
334
342
|
}, async ({ feature: ref, ...updates }) => {
|
|
335
343
|
const st = await requireStore();
|
|
@@ -355,6 +363,8 @@ server.registerTool("planner-feature-update", {
|
|
|
355
363
|
target.startDate = updates.startDate.trim();
|
|
356
364
|
if (updates.endDate !== undefined)
|
|
357
365
|
target.endDate = updates.endDate.trim();
|
|
366
|
+
if (updates.priority !== undefined)
|
|
367
|
+
target.priority = updates.priority;
|
|
358
368
|
target.updatedAt = nowISO();
|
|
359
369
|
return doc;
|
|
360
370
|
});
|
|
@@ -410,9 +420,13 @@ server.registerTool("planner-phase-add", {
|
|
|
410
420
|
const phases = await st.loadAllPhases();
|
|
411
421
|
const featurePhases = feature ? phases.filter((phase) => phase.featureId === feature.id) : phases;
|
|
412
422
|
const timestamp = nowISO();
|
|
423
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
424
|
+
const priority = await st.nextPriority("phase", feature?.id);
|
|
413
425
|
phase = {
|
|
414
426
|
id: createPhaseId(),
|
|
415
427
|
number: featurePhases.reduce((max, entry) => Math.max(max, entry.number), 0) + 1,
|
|
428
|
+
shortId,
|
|
429
|
+
priority,
|
|
416
430
|
slug: normalizeSlug(title),
|
|
417
431
|
title: title.trim(),
|
|
418
432
|
featureId: feature?.id,
|
|
@@ -436,6 +450,8 @@ server.registerTool("planner-phase-add", {
|
|
|
436
450
|
tasks: [],
|
|
437
451
|
createdAt: timestamp,
|
|
438
452
|
updatedAt: timestamp,
|
|
453
|
+
handoff: "",
|
|
454
|
+
handoffUpdatedAt: "",
|
|
439
455
|
};
|
|
440
456
|
await st.savePhase(phase);
|
|
441
457
|
if (feature) {
|
|
@@ -457,7 +473,7 @@ server.registerTool("planner-phase-show", {
|
|
|
457
473
|
inputSchema: { phase: z.string().min(1).describe("Phase id or name") },
|
|
458
474
|
}, async ({ phase: ref }) => {
|
|
459
475
|
const st = await requireStore();
|
|
460
|
-
const phase = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
476
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
461
477
|
if (!phase)
|
|
462
478
|
return text(`Phase not found: ${ref}`);
|
|
463
479
|
return text(`${phase.title} (${phase.id}) — ${phase.status}; ${phase.tasks.length} tasks`, { phase });
|
|
@@ -476,7 +492,7 @@ server.registerTool("planner-phase-discuss", {
|
|
|
476
492
|
},
|
|
477
493
|
}, async ({ phase: ref, ...updates }) => {
|
|
478
494
|
const st = await requireStore();
|
|
479
|
-
const found = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
495
|
+
const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
480
496
|
if (!found)
|
|
481
497
|
return text(`Phase not found: ${ref}`);
|
|
482
498
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
@@ -511,10 +527,11 @@ server.registerTool("planner-phase-update", {
|
|
|
511
527
|
status: z.enum(PHASE_STATUS_VALUES).optional(),
|
|
512
528
|
summary: z.string().optional(),
|
|
513
529
|
description: z.string().optional(),
|
|
530
|
+
priority: z.number().int().nonnegative().optional().describe("Display order within the feature (lower = higher)."),
|
|
514
531
|
},
|
|
515
532
|
}, async ({ phase: ref, ...updates }) => {
|
|
516
533
|
const st = await requireStore();
|
|
517
|
-
const found = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
534
|
+
const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
518
535
|
if (!found)
|
|
519
536
|
return text(`Phase not found: ${ref}`);
|
|
520
537
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
@@ -526,6 +543,8 @@ server.registerTool("planner-phase-update", {
|
|
|
526
543
|
entry.summary = updates.summary.trim();
|
|
527
544
|
if (updates.description !== undefined)
|
|
528
545
|
entry.description = updates.description.trim();
|
|
546
|
+
if (updates.priority !== undefined)
|
|
547
|
+
entry.priority = updates.priority;
|
|
529
548
|
entry.updatedAt = nowISO();
|
|
530
549
|
return entry;
|
|
531
550
|
});
|
|
@@ -536,7 +555,7 @@ server.registerTool("planner-phase-delete", {
|
|
|
536
555
|
inputSchema: { phase: z.string().min(1) },
|
|
537
556
|
}, async ({ phase: ref }) => {
|
|
538
557
|
const st = await requireStore();
|
|
539
|
-
const phase = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
558
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
540
559
|
if (!phase)
|
|
541
560
|
return text(`Phase not found: ${ref}`);
|
|
542
561
|
await st.deletePhase(phase.id);
|
|
@@ -557,15 +576,19 @@ server.registerTool("planner-task-add", {
|
|
|
557
576
|
},
|
|
558
577
|
}, async ({ phase: ref, title, description, checklist }) => {
|
|
559
578
|
const st = await requireStore();
|
|
560
|
-
const found = findPhaseByRef(await st.loadAllPhases(), ref);
|
|
579
|
+
const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
561
580
|
if (!found)
|
|
562
581
|
return text(`Phase not found: ${ref}`);
|
|
563
582
|
const timestamp = nowISO();
|
|
564
583
|
const taskId = createTaskId();
|
|
584
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
585
|
+
const priority = await st.nextPriority("task", found.id);
|
|
565
586
|
const task = {
|
|
566
587
|
id: taskId,
|
|
567
588
|
phaseId: found.id,
|
|
568
589
|
number: found.tasks.length + 1,
|
|
590
|
+
shortId,
|
|
591
|
+
priority,
|
|
569
592
|
shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
|
|
570
593
|
title: title.trim(),
|
|
571
594
|
status: "planned",
|
|
@@ -582,13 +605,14 @@ server.registerTool("planner-task-add", {
|
|
|
582
605
|
createdAt: timestamp,
|
|
583
606
|
updatedAt: timestamp,
|
|
584
607
|
};
|
|
585
|
-
|
|
608
|
+
await st.updatePhase(found.id, (entry) => {
|
|
586
609
|
entry.tasks.push(task);
|
|
587
610
|
entry.taskIds.push(task.id);
|
|
588
611
|
entry.updatedAt = timestamp;
|
|
589
612
|
return entry;
|
|
590
613
|
});
|
|
591
|
-
|
|
614
|
+
const features = (await st.loadFeatures()).features;
|
|
615
|
+
return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, features)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
|
|
592
616
|
});
|
|
593
617
|
server.registerTool("planner-task-show", {
|
|
594
618
|
description: "Show a task by id or name.",
|
|
@@ -626,7 +650,9 @@ server.registerTool("planner-task-discuss", {
|
|
|
626
650
|
updatedTask = task;
|
|
627
651
|
return phase;
|
|
628
652
|
});
|
|
629
|
-
|
|
653
|
+
const features = (await st.loadFeatures()).features;
|
|
654
|
+
const t = updatedTask ?? found.task;
|
|
655
|
+
return writeAndSummarize(st, `✅ Task discussed/updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
630
656
|
});
|
|
631
657
|
server.registerTool("planner-task-update", {
|
|
632
658
|
description: "Update task fields. A motivation is REQUIRED when changing to blocked, canceled, deferred, rejected, waiting, or back to planned from another status.",
|
|
@@ -636,8 +662,9 @@ server.registerTool("planner-task-update", {
|
|
|
636
662
|
status: z.enum(STATUS_VALUES).optional(),
|
|
637
663
|
description: z.string().optional(),
|
|
638
664
|
motivation: z.string().optional(),
|
|
665
|
+
priority: z.number().int().nonnegative().optional().describe("Display order within the phase (lower = higher)."),
|
|
639
666
|
},
|
|
640
|
-
}, async ({ task: ref, title, status, description, motivation }) => {
|
|
667
|
+
}, async ({ task: ref, title, status, description, motivation, priority }) => {
|
|
641
668
|
const st = await requireStore();
|
|
642
669
|
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
643
670
|
if (!found)
|
|
@@ -657,6 +684,8 @@ server.registerTool("planner-task-update", {
|
|
|
657
684
|
return phase;
|
|
658
685
|
if (title !== undefined)
|
|
659
686
|
task.title = title.trim();
|
|
687
|
+
if (priority !== undefined)
|
|
688
|
+
task.priority = priority;
|
|
660
689
|
if (description !== undefined)
|
|
661
690
|
task.description = description.trim();
|
|
662
691
|
if (status !== undefined && status !== task.status) {
|
|
@@ -678,7 +707,9 @@ server.registerTool("planner-task-update", {
|
|
|
678
707
|
return phase;
|
|
679
708
|
});
|
|
680
709
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
681
|
-
|
|
710
|
+
const features = (await st.loadFeatures()).features;
|
|
711
|
+
const t = updatedTask ?? found.task;
|
|
712
|
+
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
682
713
|
});
|
|
683
714
|
server.registerTool("planner-task-delete", {
|
|
684
715
|
description: "Delete a task.",
|
|
@@ -706,8 +737,8 @@ server.registerTool("planner-task-start", {
|
|
|
706
737
|
// task_start — the handoff is a captured-context artifact, not a lock (e.g. a
|
|
707
738
|
// small modification after writing a handoff should start without forcing
|
|
708
739
|
// deletion, which would lose context).
|
|
709
|
-
const handoffNotice =
|
|
710
|
-
? "ℹ️
|
|
740
|
+
const handoffNotice = (await st.listHandoffs()).length > 0
|
|
741
|
+
? "ℹ️ 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"
|
|
711
742
|
: "";
|
|
712
743
|
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
713
744
|
if (!found)
|
|
@@ -736,7 +767,9 @@ server.registerTool("planner-task-start", {
|
|
|
736
767
|
return phase;
|
|
737
768
|
});
|
|
738
769
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
739
|
-
|
|
770
|
+
const features = (await st.loadFeatures()).features;
|
|
771
|
+
const t = updatedTask ?? found.task;
|
|
772
|
+
return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
740
773
|
});
|
|
741
774
|
server.registerTool("planner-task-complete", {
|
|
742
775
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -780,41 +813,65 @@ server.registerTool("planner-task-complete", {
|
|
|
780
813
|
updatedTask = task;
|
|
781
814
|
return phase;
|
|
782
815
|
});
|
|
783
|
-
await st.syncTaskStatusRollup(found.phase.id);
|
|
784
|
-
|
|
816
|
+
const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
|
|
817
|
+
const features = (await st.loadFeatures()).features;
|
|
818
|
+
const t = updatedTask ?? found.task;
|
|
819
|
+
return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`);
|
|
785
820
|
});
|
|
786
|
-
server.registerTool("planner-handoff-
|
|
787
|
-
description: "
|
|
821
|
+
server.registerTool("planner-handoff-list", {
|
|
822
|
+
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.",
|
|
788
823
|
}, async () => {
|
|
789
824
|
const st = await requireStore();
|
|
790
|
-
const
|
|
791
|
-
|
|
825
|
+
const list = await st.listHandoffs();
|
|
826
|
+
if (list.length === 0)
|
|
827
|
+
return text("No phase handoffs set.");
|
|
828
|
+
const lines = list.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt})`);
|
|
829
|
+
return text(`Phase handoffs (${list.length}):\n${lines.join("\n")}`, { count: list.length, handoffs: list });
|
|
830
|
+
});
|
|
831
|
+
server.registerTool("planner-handoff-show", {
|
|
832
|
+
description: "Read the entity-scoped handoff of a phase. phaseRef accepts P00x, P00x(F00x), UUID, or title. Omit to target the current in-progress phase.",
|
|
833
|
+
inputSchema: { phaseRef: z.string().optional().describe("Phase ref: P00x | P00x(F00x) | UUID | title. Default: current in-progress phase.") },
|
|
834
|
+
}, async ({ phaseRef }) => {
|
|
835
|
+
const st = await requireStore();
|
|
836
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
837
|
+
if (!r.ok)
|
|
838
|
+
return text(`❌ ${r.error}`);
|
|
839
|
+
const content = await st.getPhaseHandoff(r.phase.id);
|
|
840
|
+
if (!content.trim())
|
|
841
|
+
return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, empty: true });
|
|
842
|
+
return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
792
843
|
});
|
|
793
844
|
server.registerTool("planner-handoff-write", {
|
|
794
|
-
description: "Write .
|
|
845
|
+
description: "Write/refresh the entity-scoped handoff on a phase. Pass content (markdown). phaseRef optional (default: current in-progress phase). Captures design context for a resuming agent on that phase.",
|
|
795
846
|
inputSchema: {
|
|
796
|
-
|
|
797
|
-
|
|
847
|
+
phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase."),
|
|
848
|
+
content: z.string().min(1).describe("Handoff text (markdown)."),
|
|
798
849
|
},
|
|
799
|
-
}, async ({
|
|
850
|
+
}, async ({ phaseRef, content }) => {
|
|
800
851
|
const st = await requireStore();
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
852
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
853
|
+
if (!r.ok)
|
|
854
|
+
return text(`❌ ${r.error}`);
|
|
855
|
+
await st.setPhaseHandoff(r.phase.id, content);
|
|
856
|
+
return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
804
857
|
});
|
|
805
858
|
server.registerTool("planner-handoff-prepare", {
|
|
806
|
-
description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write.",
|
|
859
|
+
description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write (entity-scoped, phase.handoff).",
|
|
807
860
|
}, async () => text([
|
|
808
|
-
"Prepare the canonical session handoff and write it with planner-handoff-write.",
|
|
861
|
+
"Prepare the canonical session handoff and write it on the current in-progress phase with planner-handoff-write (omit phaseRef to target the in-progress phase).",
|
|
809
862
|
"Required sections: Created at, Updated at, Reason, Current focus, What was being done, How to resume, Files touched, Blockers, Next steps, Recent decisions, Reminder.",
|
|
810
|
-
"
|
|
863
|
+
"Stored on the phase.handoff field (entity-scoped). .planner/HANDOFF.md is deprecated.",
|
|
811
864
|
].join("\n")));
|
|
812
865
|
server.registerTool("planner-handoff-clear", {
|
|
813
|
-
description: "
|
|
814
|
-
|
|
866
|
+
description: "Clear the entity-scoped handoff of a phase (sets handoff to empty, keeps handoffUpdatedAt as an audit trail). phaseRef optional (default: current in-progress phase).",
|
|
867
|
+
inputSchema: { phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase.") },
|
|
868
|
+
}, async ({ phaseRef }) => {
|
|
815
869
|
const st = await requireStore();
|
|
816
|
-
await st
|
|
817
|
-
|
|
870
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
871
|
+
if (!r.ok)
|
|
872
|
+
return text(`❌ ${r.error}`);
|
|
873
|
+
await st.clearPhaseHandoff(r.phase.id);
|
|
874
|
+
return text(`✅ Cleared handoff on ${r.compositeRef}. (handoffUpdatedAt preserved as audit)`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
818
875
|
});
|
|
819
876
|
server.registerTool("planner-web", {
|
|
820
877
|
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.",
|
|
@@ -863,7 +920,7 @@ server.registerTool("planner-load", {
|
|
|
863
920
|
if (!(await st.exists()))
|
|
864
921
|
return text("No .planner/ found at " + planRoot() + ". Run planner-init first.");
|
|
865
922
|
const web = await ensureWebStarted();
|
|
866
|
-
const recap = await
|
|
923
|
+
const recap = await buildRecap(st, web, { harness: "mcp" });
|
|
867
924
|
return text(recap);
|
|
868
925
|
});
|
|
869
926
|
server.registerTool("planner-disable", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19-next.1",
|
|
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.
|
|
45
|
-
"@agent-plan/server": "0.2.
|
|
44
|
+
"@agent-plan/core": "0.2.19-next.1",
|
|
45
|
+
"@agent-plan/server": "0.2.19-next.1"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc -p tsconfig.json",
|