@agent-plan/mcp 0.2.19-next.1 → 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":";AAm5BA,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, buildRecap } 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
  }
@@ -61,34 +62,6 @@ function findFeatureByRef(features, ref) {
61
62
  ?? features.find((feature) => feature.name.toLowerCase() === normalized)
62
63
  ?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
63
64
  }
64
- function findTaskByRef(phases, ref) {
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
- }
82
- for (const phase of phases) {
83
- const task = phase.tasks.find((entry) => entry.id.toLowerCase() === normalized
84
- || (entry.shortId && entry.shortId.toLowerCase() === normalized)
85
- || entry.title.toLowerCase() === normalized
86
- || entry.title.toLowerCase().includes(normalized));
87
- if (task)
88
- return { phase, task };
89
- }
90
- return undefined;
91
- }
92
65
  function featureNumberOfPhase(phase, features) {
93
66
  return phase.featureId ? features.find((f) => f.id === phase.featureId)?.number : undefined;
94
67
  }
@@ -189,7 +162,7 @@ server.registerTool("planner-init", {
189
162
  if (goal !== undefined)
190
163
  project.goal = goal.trim();
191
164
  await st.saveProject(project);
192
- return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`, { project });
165
+ return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`);
193
166
  });
194
167
  server.registerTool("planner-show", {
195
168
  description: "Show the current planner overview.",
@@ -207,11 +180,11 @@ server.registerTool("planner-show", {
207
180
  ].join("\n"), { plan });
208
181
  });
209
182
  server.registerTool("planner-repair", {
210
- 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.",
211
184
  }, async () => {
212
185
  const st = await requireStore();
213
186
  const report = await st.repair();
214
- 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 });
215
188
  });
216
189
  server.registerTool("planner-project-language", {
217
190
  description: "Persist preferred languages for plan content and chat.",
@@ -227,7 +200,7 @@ server.registerTool("planner-project-language", {
227
200
  if (chatLanguage !== undefined)
228
201
  project.chatLanguage = chatLanguage.trim();
229
202
  await st.saveProject(project);
230
- 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)"}`);
231
204
  });
232
205
  server.registerTool("planner-project-discuss", {
233
206
  description: "Record/update project-level discovery fields. This is the MCP equivalent of grouped project discuss without Pi UI prompts.",
@@ -258,20 +231,73 @@ server.registerTool("planner-project-discuss", {
258
231
  if (params.decisions !== undefined)
259
232
  project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
260
233
  await st.saveProject(project);
261
- return writeAndSummarize(st, `Project discussed/updated: ${project.name}`, { project });
234
+ return writeAndSummarize(st, `Project discussed/updated: ${project.name}`);
262
235
  });
263
236
  server.registerTool("planner-feature-list", {
264
- description: "List features with phase/task counts.",
265
- }, 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 }) => {
266
240
  const st = await requireStore();
267
- 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;
268
243
  const phases = await st.loadAllPhases();
269
244
  const lines = features.map((feature) => {
270
245
  const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
271
246
  const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
272
- 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)`;
273
248
  });
274
- 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");
275
301
  });
276
302
  server.registerTool("planner-feature-add", {
277
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.",
@@ -289,7 +315,7 @@ server.registerTool("planner-feature-add", {
289
315
  const priority = await st.nextPriority("feature");
290
316
  const feature = {
291
317
  id: createFeatureId(),
292
- number: existingFeatures.length + 1,
318
+ number: await st.allocFeatureNumber(),
293
319
  shortId,
294
320
  priority,
295
321
  name: name.trim(),
@@ -312,24 +338,25 @@ server.registerTool("planner-feature-add", {
312
338
  doc.features.push(feature);
313
339
  return doc;
314
340
  });
315
- 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}` : ""}`);
316
342
  });
317
343
  server.registerTool("planner-feature-show", {
318
344
  description: "Show a feature by id or name.",
319
- inputSchema: { feature: z.string().min(1).describe("Feature id or name") },
320
- }, 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 }) => {
321
347
  const st = await requireStore();
322
348
  const features = (await st.loadFeatures()).features;
323
349
  const feature = findFeatureByRef(features, ref);
324
350
  if (!feature)
325
351
  return text(`Feature not found: ${ref}`);
326
352
  const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
327
- 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);
328
355
  });
329
356
  server.registerTool("planner-feature-update", {
330
357
  description: "Update a feature.",
331
358
  inputSchema: {
332
- 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."),
333
360
  name: z.string().optional(),
334
361
  description: z.string().optional(),
335
362
  status: z.enum(STATUS_VALUES).optional(),
@@ -369,12 +396,12 @@ server.registerTool("planner-feature-update", {
369
396
  return doc;
370
397
  });
371
398
  const result = updated.features.find((entry) => entry.id === feature.id);
372
- 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}` : ""}`);
373
400
  });
374
401
  server.registerTool("planner-feature-delete", {
375
402
  description: "Delete a feature. By default, phases are unlinked rather than deleted.",
376
403
  inputSchema: {
377
- 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."),
378
405
  cascade: z.boolean().optional().describe("Also delete phases belonging to this feature"),
379
406
  },
380
407
  }, async ({ feature: ref, cascade }) => {
@@ -404,7 +431,7 @@ server.registerTool("planner-phase-add", {
404
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.",
405
432
  inputSchema: {
406
433
  title: z.string().min(1),
407
- 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."),
408
435
  summary: z.string().optional().describe("One-line summary of the phase"),
409
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."),
410
437
  },
@@ -424,7 +451,7 @@ server.registerTool("planner-phase-add", {
424
451
  const priority = await st.nextPriority("phase", feature?.id);
425
452
  phase = {
426
453
  id: createPhaseId(),
427
- number: featurePhases.reduce((max, entry) => Math.max(max, entry.number), 0) + 1,
454
+ number: await st.allocPhaseNumber(),
428
455
  shortId,
429
456
  priority,
430
457
  slug: normalizeSlug(title),
@@ -452,6 +479,8 @@ server.registerTool("planner-phase-add", {
452
479
  updatedAt: timestamp,
453
480
  handoff: "",
454
481
  handoffUpdatedAt: "",
482
+ handoffReadAt: "",
483
+ handoffHistory: [],
455
484
  };
456
485
  await st.savePhase(phase);
457
486
  if (feature) {
@@ -466,22 +495,24 @@ server.registerTool("planner-phase-add", {
466
495
  });
467
496
  if (!phase)
468
497
  return text("Phase creation failed.");
469
- 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}` : ""}`);
470
499
  });
471
500
  server.registerTool("planner-phase-show", {
472
501
  description: "Show a phase by id or name.",
473
- inputSchema: { phase: z.string().min(1).describe("Phase id or name") },
474
- }, 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 }) => {
475
504
  const st = await requireStore();
476
- 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);
477
507
  if (!phase)
478
508
  return text(`Phase not found: ${ref}`);
479
- 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);
480
511
  });
481
512
  server.registerTool("planner-phase-discuss", {
482
513
  description: "Persist phase discovery fields and mark phase planned.",
483
514
  inputSchema: {
484
- 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."),
485
516
  goal: z.string().optional(),
486
517
  summary: z.string().optional(),
487
518
  scope: z.string().optional(),
@@ -492,7 +523,8 @@ server.registerTool("planner-phase-discuss", {
492
523
  },
493
524
  }, async ({ phase: ref, ...updates }) => {
494
525
  const st = await requireStore();
495
- 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);
496
528
  if (!found)
497
529
  return text(`Phase not found: ${ref}`);
498
530
  const phase = await st.updatePhase(found.id, (entry) => {
@@ -517,7 +549,7 @@ server.registerTool("planner-phase-discuss", {
517
549
  entry.updatedAt = nowISO();
518
550
  return entry;
519
551
  });
520
- 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}` : ""}`);
521
553
  });
522
554
  server.registerTool("planner-phase-update", {
523
555
  description: "Update phase fields.",
@@ -531,7 +563,8 @@ server.registerTool("planner-phase-update", {
531
563
  },
532
564
  }, async ({ phase: ref, ...updates }) => {
533
565
  const st = await requireStore();
534
- 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);
535
568
  if (!found)
536
569
  return text(`Phase not found: ${ref}`);
537
570
  const phase = await st.updatePhase(found.id, (entry) => {
@@ -548,7 +581,7 @@ server.registerTool("planner-phase-update", {
548
581
  entry.updatedAt = nowISO();
549
582
  return entry;
550
583
  });
551
- 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}` : ""}`);
552
585
  });
553
586
  server.registerTool("planner-phase-delete", {
554
587
  description: "Delete a phase and unlink it from features.",
@@ -564,12 +597,13 @@ server.registerTool("planner-phase-delete", {
564
597
  feature.phaseIds = feature.phaseIds.filter((id) => id !== phase.id);
565
598
  return doc;
566
599
  });
567
- 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 });
568
602
  });
569
603
  server.registerTool("planner-task-add", {
570
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.",
571
605
  inputSchema: {
572
- 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."),
573
607
  title: z.string().min(1),
574
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."),
575
609
  checklist: z.array(z.string()).optional(),
@@ -586,7 +620,7 @@ server.registerTool("planner-task-add", {
586
620
  const task = {
587
621
  id: taskId,
588
622
  phaseId: found.id,
589
- number: found.tasks.length + 1,
623
+ number: await st.allocTaskNumber(),
590
624
  shortId,
591
625
  priority,
592
626
  shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
@@ -616,13 +650,18 @@ server.registerTool("planner-task-add", {
616
650
  });
617
651
  server.registerTool("planner-task-show", {
618
652
  description: "Show a task by id or name.",
619
- inputSchema: { task: z.string().min(1).describe("Task id or name") },
620
- }, 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 }) => {
621
655
  const st = await requireStore();
622
- const found = findTaskByRef(await st.loadAllPhases(), ref);
656
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
623
657
  if (!found)
624
658
  return text(`Task not found: ${ref}`);
625
- 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}` : ""}`);
626
665
  });
627
666
  server.registerTool("planner-task-discuss", {
628
667
  description: "Persist task execution notes/checklist.",
@@ -633,7 +672,7 @@ server.registerTool("planner-task-discuss", {
633
672
  },
634
673
  }, async ({ task: ref, description, checklist }) => {
635
674
  const st = await requireStore();
636
- const found = findTaskByRef(await st.loadAllPhases(), ref);
675
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
637
676
  if (!found)
638
677
  return text(`Task not found: ${ref}`);
639
678
  let updatedTask;
@@ -666,7 +705,7 @@ server.registerTool("planner-task-update", {
666
705
  },
667
706
  }, async ({ task: ref, title, status, description, motivation, priority }) => {
668
707
  const st = await requireStore();
669
- const found = findTaskByRef(await st.loadAllPhases(), ref);
708
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
670
709
  if (!found)
671
710
  return text(`Task not found: ${ref}`);
672
711
  // Validate motivation requirement for status transitions.
@@ -716,7 +755,7 @@ server.registerTool("planner-task-delete", {
716
755
  inputSchema: { task: z.string().min(1) },
717
756
  }, async ({ task: ref }) => {
718
757
  const st = await requireStore();
719
- const found = findTaskByRef(await st.loadAllPhases(), ref);
758
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
720
759
  if (!found)
721
760
  return text(`Task not found: ${ref}`);
722
761
  await st.updatePhase(found.phase.id, (phase) => {
@@ -726,23 +765,30 @@ server.registerTool("planner-task-delete", {
726
765
  return phase;
727
766
  });
728
767
  await st.syncTaskStatusRollup(found.phase.id);
729
- 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 });
730
770
  });
731
771
  server.registerTool("planner-task-start", {
732
772
  description: "Set a task to in-progress.",
733
773
  inputSchema: { task: z.string().min(1) },
734
774
  }, async ({ task: ref }) => {
735
775
  const st = await requireStore();
736
- // Hygiene notice (non-blocking): surface an existing handoff but never block
737
- // task_start — the handoff is a captured-context artifact, not a lock (e.g. a
738
- // small modification after writing a handoff should start without forcing
739
- // deletion, which would lose context).
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"
742
- : "";
743
- const found = findTaskByRef(await st.loadAllPhases(), ref);
776
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
744
777
  if (!found)
745
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
+ }
746
792
  const timestamp = nowISO();
747
793
  let updatedTask;
748
794
  await st.updatePhase(found.phase.id, (phase) => {
@@ -780,7 +826,7 @@ server.registerTool("planner-task-complete", {
780
826
  },
781
827
  }, async ({ task: ref, force, description_update }) => {
782
828
  const st = await requireStore();
783
- const found = findTaskByRef(await st.loadAllPhases(), ref);
829
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
784
830
  if (!found)
785
831
  return text(`Task not found: ${ref}`);
786
832
  const unchecked = found.task.checklist.filter((item) => !item.checked);
@@ -870,8 +916,8 @@ server.registerTool("planner-handoff-clear", {
870
916
  const r = await resolvePhaseForHandoff(st, phaseRef);
871
917
  if (!r.ok)
872
918
  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 });
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 });
875
921
  });
876
922
  server.registerTool("planner-web", {
877
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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.19-next.1",
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.1",
45
- "@agent-plan/server": "0.2.19-next.1"
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",