@agent-plan/mcp 0.2.17 → 0.2.19-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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":";AA+5BA,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 } 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;
@@ -53,7 +58,7 @@ async function ensureWebStarted() {
53
58
  /** Build a consolidated planner recap (project state + active work + pending
54
59
  * handoff + web URL). Harness-agnostic equivalent of Pi's buildStartupResumeSummary. */
55
60
  async function buildRecapText(st, web) {
56
- const [plan, resume, handoff] = await Promise.all([st.loadAll(), st.loadResume(), st.loadHandoff()]);
61
+ const [plan, resume, handoffs] = await Promise.all([st.loadAll(), st.loadResume(), st.listHandoffs()]);
57
62
  const allTasks = plan.phases.flatMap((p) => p.tasks.map((t) => ({ phase: p, task: t })));
58
63
  const totalF = plan.features.features.length;
59
64
  const doneF = plan.features.features.filter((f) => f.status === "done").length;
@@ -80,11 +85,13 @@ async function buildRecapText(st, web) {
80
85
  }
81
86
  if (resume?.nextSteps?.length)
82
87
  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.");
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).");
85
92
  }
86
93
  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.");
94
+ lines.push("", "No phase handoff pending and no task in-progress. Use planner-task-add / planner-task-start to begin work.");
88
95
  }
89
96
  if (web.localUrl) {
90
97
  lines.push("", "## Web UI", `🌐 ${web.localUrl}${web.lanUrl ? " (LAN: " + web.lanUrl + ")" : ""}`);
@@ -97,12 +104,6 @@ function findFeatureByRef(features, ref) {
97
104
  ?? features.find((feature) => feature.name.toLowerCase() === normalized)
98
105
  ?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
99
106
  }
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
107
  function findTaskByRef(phases, ref) {
107
108
  const normalized = ref.trim().toLowerCase();
108
109
  for (const phase of phases) {
@@ -136,6 +137,28 @@ const server = new McpServer({
136
137
  name: "agent-plan-planner",
137
138
  version: "0.1.0",
138
139
  });
140
+ async function resolvePhaseForHandoff(st, ref) {
141
+ const phases = await st.loadAllPhases();
142
+ const features = (await st.loadFeatures()).features;
143
+ let phase;
144
+ if (ref && ref.trim()) {
145
+ phase = findPhaseByRef(phases, features, ref.trim());
146
+ }
147
+ else {
148
+ phase = phases.find((p) => p.status === "in-progress")
149
+ ?? phases.find((p) => p.tasks.some((t) => t.status === "in-progress"));
150
+ }
151
+ if (!phase) {
152
+ return {
153
+ ok: false,
154
+ error: ref && ref.trim()
155
+ ? `Phase not found: "${ref.trim()}". Use P00x, P00x(F00x), UUID, or title.`
156
+ : "No in-progress phase. Specify a phaseRef (P00x or P00x(F00x)).",
157
+ };
158
+ }
159
+ const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
160
+ return { ok: true, phase, compositeRef: formatPhaseRef(phase.number, feat?.number) };
161
+ }
139
162
  server.registerTool("planner-export", {
140
163
  description: "Export the project plan as a Markdown report. Supports a concise summary or full hierarchical detail.",
141
164
  inputSchema: {
@@ -282,9 +305,13 @@ server.registerTool("planner-feature-add", {
282
305
  const timestamp = nowISO();
283
306
  const effectiveStatus = status ?? "planned";
284
307
  const existingFeatures = (await st.loadFeatures()).features;
308
+ const shortId = createShortId(await st.assignedShortIds());
309
+ const priority = await st.nextPriority("feature");
285
310
  const feature = {
286
311
  id: createFeatureId(),
287
312
  number: existingFeatures.length + 1,
313
+ shortId,
314
+ priority,
288
315
  name: name.trim(),
289
316
  description: description?.trim() ?? "",
290
317
  status: effectiveStatus,
@@ -330,6 +357,7 @@ server.registerTool("planner-feature-update", {
330
357
  workRemaining: z.string().optional(),
331
358
  startDate: z.string().optional(),
332
359
  endDate: z.string().optional(),
360
+ priority: z.number().int().nonnegative().optional().describe("Display order within the project (lower = higher). Tiebreak by number then createdAt."),
333
361
  },
334
362
  }, async ({ feature: ref, ...updates }) => {
335
363
  const st = await requireStore();
@@ -355,6 +383,8 @@ server.registerTool("planner-feature-update", {
355
383
  target.startDate = updates.startDate.trim();
356
384
  if (updates.endDate !== undefined)
357
385
  target.endDate = updates.endDate.trim();
386
+ if (updates.priority !== undefined)
387
+ target.priority = updates.priority;
358
388
  target.updatedAt = nowISO();
359
389
  return doc;
360
390
  });
@@ -410,9 +440,13 @@ server.registerTool("planner-phase-add", {
410
440
  const phases = await st.loadAllPhases();
411
441
  const featurePhases = feature ? phases.filter((phase) => phase.featureId === feature.id) : phases;
412
442
  const timestamp = nowISO();
443
+ const shortId = createShortId(await st.assignedShortIds());
444
+ const priority = await st.nextPriority("phase", feature?.id);
413
445
  phase = {
414
446
  id: createPhaseId(),
415
447
  number: featurePhases.reduce((max, entry) => Math.max(max, entry.number), 0) + 1,
448
+ shortId,
449
+ priority,
416
450
  slug: normalizeSlug(title),
417
451
  title: title.trim(),
418
452
  featureId: feature?.id,
@@ -436,6 +470,8 @@ server.registerTool("planner-phase-add", {
436
470
  tasks: [],
437
471
  createdAt: timestamp,
438
472
  updatedAt: timestamp,
473
+ handoff: "",
474
+ handoffUpdatedAt: "",
439
475
  };
440
476
  await st.savePhase(phase);
441
477
  if (feature) {
@@ -457,7 +493,7 @@ server.registerTool("planner-phase-show", {
457
493
  inputSchema: { phase: z.string().min(1).describe("Phase id or name") },
458
494
  }, async ({ phase: ref }) => {
459
495
  const st = await requireStore();
460
- const phase = findPhaseByRef(await st.loadAllPhases(), ref);
496
+ const phase = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
461
497
  if (!phase)
462
498
  return text(`Phase not found: ${ref}`);
463
499
  return text(`${phase.title} (${phase.id}) — ${phase.status}; ${phase.tasks.length} tasks`, { phase });
@@ -476,7 +512,7 @@ server.registerTool("planner-phase-discuss", {
476
512
  },
477
513
  }, async ({ phase: ref, ...updates }) => {
478
514
  const st = await requireStore();
479
- const found = findPhaseByRef(await st.loadAllPhases(), ref);
515
+ const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
480
516
  if (!found)
481
517
  return text(`Phase not found: ${ref}`);
482
518
  const phase = await st.updatePhase(found.id, (entry) => {
@@ -511,10 +547,11 @@ server.registerTool("planner-phase-update", {
511
547
  status: z.enum(PHASE_STATUS_VALUES).optional(),
512
548
  summary: z.string().optional(),
513
549
  description: z.string().optional(),
550
+ priority: z.number().int().nonnegative().optional().describe("Display order within the feature (lower = higher)."),
514
551
  },
515
552
  }, async ({ phase: ref, ...updates }) => {
516
553
  const st = await requireStore();
517
- const found = findPhaseByRef(await st.loadAllPhases(), ref);
554
+ const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
518
555
  if (!found)
519
556
  return text(`Phase not found: ${ref}`);
520
557
  const phase = await st.updatePhase(found.id, (entry) => {
@@ -526,6 +563,8 @@ server.registerTool("planner-phase-update", {
526
563
  entry.summary = updates.summary.trim();
527
564
  if (updates.description !== undefined)
528
565
  entry.description = updates.description.trim();
566
+ if (updates.priority !== undefined)
567
+ entry.priority = updates.priority;
529
568
  entry.updatedAt = nowISO();
530
569
  return entry;
531
570
  });
@@ -536,7 +575,7 @@ server.registerTool("planner-phase-delete", {
536
575
  inputSchema: { phase: z.string().min(1) },
537
576
  }, async ({ phase: ref }) => {
538
577
  const st = await requireStore();
539
- const phase = findPhaseByRef(await st.loadAllPhases(), ref);
578
+ const phase = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
540
579
  if (!phase)
541
580
  return text(`Phase not found: ${ref}`);
542
581
  await st.deletePhase(phase.id);
@@ -557,15 +596,19 @@ server.registerTool("planner-task-add", {
557
596
  },
558
597
  }, async ({ phase: ref, title, description, checklist }) => {
559
598
  const st = await requireStore();
560
- const found = findPhaseByRef(await st.loadAllPhases(), ref);
599
+ const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
561
600
  if (!found)
562
601
  return text(`Phase not found: ${ref}`);
563
602
  const timestamp = nowISO();
564
603
  const taskId = createTaskId();
604
+ const shortId = createShortId(await st.assignedShortIds());
605
+ const priority = await st.nextPriority("task", found.id);
565
606
  const task = {
566
607
  id: taskId,
567
608
  phaseId: found.id,
568
609
  number: found.tasks.length + 1,
610
+ shortId,
611
+ priority,
569
612
  shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
570
613
  title: title.trim(),
571
614
  status: "planned",
@@ -636,8 +679,9 @@ server.registerTool("planner-task-update", {
636
679
  status: z.enum(STATUS_VALUES).optional(),
637
680
  description: z.string().optional(),
638
681
  motivation: z.string().optional(),
682
+ priority: z.number().int().nonnegative().optional().describe("Display order within the phase (lower = higher)."),
639
683
  },
640
- }, async ({ task: ref, title, status, description, motivation }) => {
684
+ }, async ({ task: ref, title, status, description, motivation, priority }) => {
641
685
  const st = await requireStore();
642
686
  const found = findTaskByRef(await st.loadAllPhases(), ref);
643
687
  if (!found)
@@ -657,6 +701,8 @@ server.registerTool("planner-task-update", {
657
701
  return phase;
658
702
  if (title !== undefined)
659
703
  task.title = title.trim();
704
+ if (priority !== undefined)
705
+ task.priority = priority;
660
706
  if (description !== undefined)
661
707
  task.description = description.trim();
662
708
  if (status !== undefined && status !== task.status) {
@@ -706,8 +752,8 @@ server.registerTool("planner-task-start", {
706
752
  // task_start — the handoff is a captured-context artifact, not a lock (e.g. a
707
753
  // small modification after writing a handoff should start without forcing
708
754
  // 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"
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"
711
757
  : "";
712
758
  const found = findTaskByRef(await st.loadAllPhases(), ref);
713
759
  if (!found)
@@ -780,41 +826,63 @@ server.registerTool("planner-task-complete", {
780
826
  updatedTask = task;
781
827
  return phase;
782
828
  });
783
- await st.syncTaskStatusRollup(found.phase.id);
784
- return writeAndSummarize(st, `✅ Task completed: ${found.task.id}`, { task: updatedTask ?? found.task });
829
+ const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
830
+ return writeAndSummarize(st, `✅ Task completed: ${found.task.id}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`, { task: updatedTask ?? found.task });
785
831
  });
786
- server.registerTool("planner-handoff-show", {
787
- description: "Show .planner/HANDOFF.md if present.",
832
+ server.registerTool("planner-handoff-list", {
833
+ 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
834
  }, async () => {
789
835
  const st = await requireStore();
790
- const handoff = await st.loadHandoff();
791
- return text(handoff?.content ?? "No .planner/HANDOFF.md present.", { handoff: handoff ?? null });
836
+ const list = await st.listHandoffs();
837
+ if (list.length === 0)
838
+ return text("No phase handoffs set.");
839
+ const lines = list.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt})`);
840
+ return text(`Phase handoffs (${list.length}):\n${lines.join("\n")}`, { count: list.length, handoffs: list });
841
+ });
842
+ server.registerTool("planner-handoff-show", {
843
+ 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.",
844
+ inputSchema: { phaseRef: z.string().optional().describe("Phase ref: P00x | P00x(F00x) | UUID | title. Default: current in-progress phase.") },
845
+ }, async ({ phaseRef }) => {
846
+ const st = await requireStore();
847
+ const r = await resolvePhaseForHandoff(st, phaseRef);
848
+ if (!r.ok)
849
+ return text(`❌ ${r.error}`);
850
+ const content = await st.getPhaseHandoff(r.phase.id);
851
+ if (!content.trim())
852
+ return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, empty: true });
853
+ return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
792
854
  });
793
855
  server.registerTool("planner-handoff-write", {
794
- description: "Write .planner/HANDOFF.md content.",
856
+ 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
857
  inputSchema: {
796
- content: z.string().min(1),
797
- reason: z.string().optional(),
858
+ phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase."),
859
+ content: z.string().min(1).describe("Handoff text (markdown)."),
798
860
  },
799
- }, async ({ content, reason }) => {
861
+ }, async ({ phaseRef, content }) => {
800
862
  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");
863
+ const r = await resolvePhaseForHandoff(st, phaseRef);
864
+ if (!r.ok)
865
+ return text(`❌ ${r.error}`);
866
+ await st.setPhaseHandoff(r.phase.id, content);
867
+ return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
804
868
  });
805
869
  server.registerTool("planner-handoff-prepare", {
806
- description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write.",
870
+ description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write (entity-scoped, phase.handoff).",
807
871
  }, async () => text([
808
- "Prepare the canonical session handoff and write it with planner-handoff-write.",
872
+ "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
873
  "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.",
874
+ "Stored on the phase.handoff field (entity-scoped). .planner/HANDOFF.md is deprecated.",
811
875
  ].join("\n")));
812
876
  server.registerTool("planner-handoff-clear", {
813
- description: "Delete .planner/HANDOFF.md.",
814
- }, async () => {
877
+ 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).",
878
+ inputSchema: { phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase.") },
879
+ }, async ({ phaseRef }) => {
815
880
  const st = await requireStore();
816
- await st.deleteHandoff();
817
- return text("Deleted .planner/HANDOFF.md");
881
+ const r = await resolvePhaseForHandoff(st, phaseRef);
882
+ if (!r.ok)
883
+ return text(`❌ ${r.error}`);
884
+ await st.clearPhaseHandoff(r.phase.id);
885
+ return text(`✅ Cleared handoff on ${r.compositeRef}. (handoffUpdatedAt preserved as audit)`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
818
886
  });
819
887
  server.registerTool("planner-web", {
820
888
  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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.17",
3
+ "version": "0.2.19-next.0",
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.17",
45
- "@agent-plan/server": "0.2.17"
44
+ "@agent-plan/core": "0.2.19-next.0",
45
+ "@agent-plan/server": "0.2.19-next.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.json",