@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA21BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
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
- const phase = await st.updatePhase(found.id, (entry) => {
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
- return writeAndSummarize(st, `Task created: ${task.id} — ${task.title}`, { task, phase });
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
- return writeAndSummarize(st, `Task discussed/updated: ${found.task.id}`, { task: updatedTask ?? found.task });
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
- return writeAndSummarize(st, `Task updated: ${found.task.id}`, { task: updatedTask ?? found.task });
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 = existsSync(join(st.root, "HANDOFF.md"))
710
- ? "ℹ️ A handoff exists at .planner/HANDOFF.md — read it for context, then delete it with planner-handoff-delete when no longer needed. Proceeding with task start.\n"
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
- return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${found.task.id}`, { task: updatedTask ?? found.task });
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
- return writeAndSummarize(st, `✅ Task completed: ${found.task.id}`, { task: updatedTask ?? found.task });
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-show", {
787
- description: "Show .planner/HANDOFF.md if present.",
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 handoff = await st.loadHandoff();
791
- return text(handoff?.content ?? "No .planner/HANDOFF.md present.", { handoff: handoff ?? null });
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 .planner/HANDOFF.md content.",
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
- content: z.string().min(1),
797
- reason: z.string().optional(),
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 ({ content, reason }) => {
850
+ }, async ({ phaseRef, content }) => {
800
851
  const st = await requireStore();
801
- const finalContent = reason && !content.includes("Reason:") ? `Reason: ${reason}\n\n${content}` : content;
802
- await st.saveHandoff(finalContent);
803
- return text("Wrote .planner/HANDOFF.md");
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
- "Canonical path: .planner/HANDOFF.md.",
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: "Delete .planner/HANDOFF.md.",
814
- }, async () => {
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.deleteHandoff();
817
- return text("Deleted .planner/HANDOFF.md");
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 buildRecapText(st, web);
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.18",
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.18",
45
- "@agent-plan/server": "0.2.18"
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",