@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA+5BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
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 findTaskByRef(phases, ref) {
108
- const normalized = ref.trim().toLowerCase();
109
- for (const phase of phases) {
110
- const task = phase.tasks.find((entry) => entry.id.toLowerCase() === normalized
111
- || entry.title.toLowerCase() === normalized
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}"`, { project });
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)"}`, { project });
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}`, { project });
234
+ return writeAndSummarize(st, `Project discussed/updated: ${project.name}`);
282
235
  });
283
236
  server.registerTool("planner-feature-list", {
284
- description: "List features with phase/task counts.",
285
- }, async () => {
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 features = (await st.loadFeatures()).features;
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.id} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
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", { 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: existingFeatures.length + 1,
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, `Feature created: ${feature.id} — ${feature.name}`, { feature });
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 id or name") },
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
- return text(`${feature.name} (${feature.id}) — ${feature.status}; ${phases.length} phases`, { feature, phases });
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 id or name"),
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, `Feature updated: ${result.id} — ${result.name}`, { feature: result });
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 id or name"),
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 id or name"),
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: featurePhases.reduce((max, entry) => Math.max(max, entry.number), 0) + 1,
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, `Phase created: ${phase.id} — ${phase.title}`, { phase });
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 id or name") },
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 phase = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
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
- return text(`${phase.title} (${phase.id}) — ${phase.status}; ${phase.tasks.length} tasks`, { phase });
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 id or name"),
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 found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
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, `Phase discussed/planned: ${phase.id} — ${phase.title}`, { phase });
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 found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
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, `Phase updated: ${phase.id} — ${phase.title}`, { phase });
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
- return writeAndSummarize(st, `Phase deleted: ${phase.id}`, { deleted: phase.id });
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 id or name"),
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: found.tasks.length + 1,
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
- const phase = await st.updatePhase(found.id, (entry) => {
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
- return writeAndSummarize(st, `Task created: ${task.id} — ${task.title}`, { task, phase });
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 id or name") },
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
- return text(`${found.task.title} (${found.task.id}) — ${found.task.status}; phase ${found.phase.id}`, { task: found.task, phase: found.phase });
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
- return writeAndSummarize(st, `Task discussed/updated: ${found.task.id}`, { task: updatedTask ?? found.task });
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
- return writeAndSummarize(st, `Task updated: ${found.task.id}`, { task: updatedTask ?? found.task });
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
- return writeAndSummarize(st, `Task deleted: ${found.task.id}`, { deleted: found.task.id });
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
- // Hygiene notice (non-blocking): surface an existing handoff but never block
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
- return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${found.task.id}`, { task: updatedTask ?? found.task });
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
- return writeAndSummarize(st, `✅ Task completed: ${found.task.id}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`, { task: updatedTask ?? found.task });
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}. (handoffUpdatedAt preserved as audit)`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
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 buildRecapText(st, web);
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.0",
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.0",
45
- "@agent-plan/server": "0.2.19-next.0"
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",