@agent-plan/mcp 0.2.19-next.2 → 0.2.19-next.21

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":";AAkvCA,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, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, recommendNextTask } 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, validateResolvedTarget } 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
  }
@@ -56,39 +57,51 @@ async function ensureWebStarted() {
56
57
  }
57
58
  }
58
59
  function findFeatureByRef(features, ref) {
59
- const normalized = ref.trim().toLowerCase();
60
- return features.find((feature) => feature.id.toLowerCase() === normalized)
61
- ?? features.find((feature) => feature.name.toLowerCase() === normalized)
62
- ?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
63
- }
64
- function findTaskByRef(phases, ref) {
65
60
  const normalized = ref.trim().toLowerCase();
66
61
  if (!normalized)
67
62
  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 };
63
+ const fMatch = normalized.match(/^f(\d+)$/);
64
+ if (fMatch) {
65
+ const n = parseInt(fMatch[1], 10);
66
+ const byNum = features.find((feature) => feature.number === n);
67
+ if (byNum)
68
+ return byNum;
89
69
  }
90
- return undefined;
70
+ return features.find((feature) => feature.shortId?.toLowerCase() === normalized)
71
+ ?? features.find((feature) => feature.id.toLowerCase() === normalized)
72
+ ?? features.find((feature) => feature.name.toLowerCase() === normalized)
73
+ ?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
74
+ }
75
+ function resolveFeatureRefStrict(features, ref) {
76
+ const raw = ref.trim();
77
+ if (!raw)
78
+ return { ok: false, error: "Feature ref is required." };
79
+ const normalized = raw.toLowerCase();
80
+ const byNumber = normalized.match(/^f(\d+)$/)
81
+ ? features.find((feature) => feature.number === parseInt(normalized.slice(1), 10))
82
+ : undefined;
83
+ if (byNumber)
84
+ return { ok: true, feature: byNumber };
85
+ const byShortId = features.find((feature) => feature.shortId?.toLowerCase() === normalized);
86
+ if (byShortId)
87
+ return { ok: true, feature: byShortId };
88
+ const byId = features.find((feature) => feature.id.toLowerCase() === normalized);
89
+ if (byId)
90
+ return { ok: true, feature: byId };
91
+ const exactName = features.filter((feature) => feature.name.toLowerCase() === normalized);
92
+ if (exactName.length === 1)
93
+ return { ok: true, feature: exactName[0] };
94
+ if (exactName.length > 1)
95
+ return { ok: false, error: `Ambiguous feature ref: ${raw}. Multiple features have that exact name; use F00x, shortId, or UUID.` };
96
+ const partialName = features.filter((feature) => feature.name.toLowerCase().includes(normalized));
97
+ if (partialName.length === 1)
98
+ return { ok: true, feature: partialName[0] };
99
+ if (partialName.length > 1)
100
+ return { ok: false, error: `Ambiguous feature ref: ${raw}. Matches: ${partialName.map((feature) => formatFeatureRef(feature.number)).join(", ")}. Use a specific F00x, shortId, or UUID.` };
101
+ return { ok: false, error: `Feature not found: ${raw}` };
91
102
  }
103
+ /** Belt-and-suspenders validation: a resolved ref must be a real UUID and the target
104
+ * must still exist in the store before we allocate numbers or write. */
92
105
  function featureNumberOfPhase(phase, features) {
93
106
  return phase.featureId ? features.find((f) => f.id === phase.featureId)?.number : undefined;
94
107
  }
@@ -124,16 +137,12 @@ async function resolvePhaseForHandoff(st, ref) {
124
137
  if (ref && ref.trim()) {
125
138
  phase = findPhaseByRef(phases, features, ref.trim());
126
139
  }
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
140
  if (!phase) {
132
141
  return {
133
142
  ok: false,
134
143
  error: ref && ref.trim()
135
144
  ? `Phase not found: "${ref.trim()}". Use P00x, P00x(F00x), UUID, or title.`
136
- : "No in-progress phase. Specify a phaseRef (P00x or P00x(F00x)).",
145
+ : "A phaseRef is required. First identify the exact feature and phase with the user, then pass P00x or P00x(F00x).",
137
146
  };
138
147
  }
139
148
  const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
@@ -151,7 +160,7 @@ server.registerTool("planner-export", {
151
160
  const markdown = exportService.exportToMarkdown(plan, full);
152
161
  const fs = await import("node:fs/promises");
153
162
  await fs.writeFile(join(st.root, "EXPORT.md"), markdown, "utf-8");
154
- return text(`Project export generated. Summary results:\n\n${markdown.slice(0, 1000)}${markdown.length > 1000 ? "... (full report in .planner/EXPORT.md)" : ""}`, { markdown });
163
+ return text(`Project export generated. Summary results:\n\n${markdown.slice(0, 1000)}${markdown.length > 1000 ? "... (full report in .planner/EXPORT.md)" : ""}`);
155
164
  });
156
165
  server.registerTool("planner-authorize-bypass", {
157
166
  description: "Authorize a temporary guard bypass (default 15 minutes) so edit/write tools can proceed even when no task is in-progress. Use ONLY after the user explicitly authorizes proceeding without a task. Harness-agnostic: stored in resume.json so all adapters (Pi, Claude Code, Codex, ...) respect it.",
@@ -189,29 +198,80 @@ server.registerTool("planner-init", {
189
198
  if (goal !== undefined)
190
199
  project.goal = goal.trim();
191
200
  await st.saveProject(project);
192
- return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`, { project });
201
+ return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`);
193
202
  });
194
203
  server.registerTool("planner-show", {
195
- description: "Show the current planner overview.",
204
+ description: "Show a compact planner overview: project metadata, counts, and a one-line summary of each feature (F00x · shortId — name (status; N phases, M tasks)). Does NOT include full descriptions, phases, or tasks — use planner-feature-list / planner-phase-list / planner-task-list for discovery and planner-*-show for full detail of a single entity. Keeps the result small to avoid token overflow.",
196
205
  }, async () => {
197
206
  const st = await requireStore();
198
- const plan = await st.loadAll();
199
- return text([
200
- `📋 ${plan.project.name}`,
201
- `Description: ${plan.project.description || "(not set)"}`,
202
- `Goal: ${plan.project.goal || "(not set)"}`,
203
- `Features: ${plan.features.features.length}`,
204
- `Phases: ${plan.phases.length}`,
205
- `Tasks: ${plan.phases.reduce((total, phase) => total + phase.tasks.length, 0)}`,
206
- `Updated: ${plan.manifest.updatedAt}`,
207
- ].join("\n"), { plan });
207
+ const features = (await st.loadFeatures()).features;
208
+ const phases = await st.loadAllPhases();
209
+ const totalTasks = phases.reduce((total, phase) => total + phase.tasks.length, 0);
210
+ const project = await st.loadProject();
211
+ const manifest = await st.loadManifest();
212
+ const featureLines = features.map((feature) => {
213
+ const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
214
+ const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
215
+ return `- ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
216
+ });
217
+ const summary = [
218
+ `📋 ${project.name}`,
219
+ `Description: ${project.description || "(not set)"}`,
220
+ `Goal: ${project.goal || "(not set)"}`,
221
+ `Features: ${features.length} | Phases: ${phases.length} | Tasks: ${totalTasks}`,
222
+ `Updated: ${manifest.updatedAt || "(unknown)"}`,
223
+ "",
224
+ "Features:",
225
+ featureLines.join("\n") || "(none)",
226
+ ].join("\n");
227
+ // Compact structured overview (no full plan: avoids 1M+ char token overflow).
228
+ const overview = {
229
+ project: { name: project.name, description: project.description || null, goal: project.goal || null, updatedAt: manifest.updatedAt || null },
230
+ counts: { features: features.length, phases: phases.length, tasks: totalTasks },
231
+ features: features.map((feature) => ({
232
+ ref: formatFeatureRef(feature.number),
233
+ shortId: feature.shortId || null,
234
+ name: feature.name,
235
+ status: feature.status,
236
+ phases: phases.filter((p) => p.featureId === feature.id).length,
237
+ tasks: phases.filter((p) => p.featureId === feature.id).reduce((t, p) => t + p.tasks.length, 0),
238
+ })),
239
+ };
240
+ return text(summary, { overview });
208
241
  });
209
242
  server.registerTool("planner-repair", {
210
- description: "Repair dangling feature→phase references and report integrity.",
243
+ 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
244
  }, async () => {
212
245
  const st = await requireStore();
213
246
  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 });
247
+ 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). Handoffs: archived ${report.handoffs.archived} stale completed/canceled handoff(s). Integrity: ${report.integrity.duplicatePhaseIds.length} duplicate, ${report.integrity.danglingPhaseIds.length} dangling.`, { report });
248
+ });
249
+ server.registerTool("planner-cleanup-orphan-phases", {
250
+ description: "Discover phase files that no longer resolve to a valid owning feature, and optionally delete them. Run once with confirm=false (default) to inspect, then rerun with confirm=true to remove them.",
251
+ inputSchema: {
252
+ confirm: z.boolean().optional().describe("Set true to actually delete the discovered orphan phase files. Default: false (dry-run/list only)."),
253
+ },
254
+ }, async ({ confirm }) => {
255
+ const st = await requireStore();
256
+ const found = await st.listOrphanPhases();
257
+ if (!confirm) {
258
+ if (found.length === 0)
259
+ return text("No orphan phases found.", { found: [] });
260
+ const lines = [
261
+ `Found ${found.length} orphan phase${found.length === 1 ? "" : "s"}.`,
262
+ ...found.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.reason})`),
263
+ "Rerun with confirm=true to delete these orphan phase files.",
264
+ ];
265
+ return text(lines.join("\n"), { found, confirmRequired: true });
266
+ }
267
+ const report = await st.cleanupOrphanPhases();
268
+ if (report.removed.length === 0)
269
+ return text("No orphan phases found.", report);
270
+ const lines = [
271
+ `Removed ${report.removed.length} orphan phase${report.removed.length === 1 ? "" : "s"}.`,
272
+ ...report.removed.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title}`),
273
+ ];
274
+ return text(lines.join("\n"), report);
215
275
  });
216
276
  server.registerTool("planner-project-language", {
217
277
  description: "Persist preferred languages for plan content and chat.",
@@ -227,7 +287,7 @@ server.registerTool("planner-project-language", {
227
287
  if (chatLanguage !== undefined)
228
288
  project.chatLanguage = chatLanguage.trim();
229
289
  await st.saveProject(project);
230
- return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}`, { project });
290
+ return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}`);
231
291
  });
232
292
  server.registerTool("planner-project-discuss", {
233
293
  description: "Record/update project-level discovery fields. This is the MCP equivalent of grouped project discuss without Pi UI prompts.",
@@ -258,20 +318,73 @@ server.registerTool("planner-project-discuss", {
258
318
  if (params.decisions !== undefined)
259
319
  project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
260
320
  await st.saveProject(project);
261
- return writeAndSummarize(st, `Project discussed/updated: ${project.name}`, { project });
321
+ return writeAndSummarize(st, `Project discussed/updated: ${project.name}`);
262
322
  });
263
323
  server.registerTool("planner-feature-list", {
264
- description: "List features with phase/task counts.",
265
- }, async () => {
324
+ 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.",
325
+ inputSchema: { featureRef: z.string().optional().describe("Optional: filter to one feature (F00x/shortId/UUID/name).") },
326
+ }, async ({ featureRef }) => {
266
327
  const st = await requireStore();
267
- const features = (await st.loadFeatures()).features;
328
+ const allFeatures = (await st.loadFeatures()).features;
329
+ const features = featureRef ? allFeatures.filter((f) => f.id === findFeatureByRef(allFeatures, featureRef)?.id) : allFeatures;
268
330
  const phases = await st.loadAllPhases();
269
331
  const lines = features.map((feature) => {
270
332
  const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
271
333
  const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
272
- return `- ${feature.id} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
334
+ return `- ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
273
335
  });
274
- return text(lines.join("\n") || "No features", { features });
336
+ return text(lines.join("\n") || "No features");
337
+ });
338
+ server.registerTool("planner-phase-list", {
339
+ 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.",
340
+ 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.") },
341
+ }, async ({ featureRef, status }) => {
342
+ const st = await requireStore();
343
+ const features = (await st.loadFeatures()).features;
344
+ let phases = await st.loadAllPhases();
345
+ if (featureRef) {
346
+ const f = findFeatureByRef(features, featureRef);
347
+ if (!f)
348
+ return text(`Feature not found: ${featureRef}`);
349
+ phases = phases.filter((p) => p.featureId === f.id);
350
+ }
351
+ if (status)
352
+ phases = phases.filter((p) => p.status === status);
353
+ const lines = phases.map((phase) => {
354
+ const fNum = featureNumberOfPhase(phase, features);
355
+ const fTag = fNum !== undefined ? ` [F${String(fNum).padStart(3, "0")}]` : "";
356
+ return `- ${formatPhaseRef(phase.number, fNum)}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.status}; ${phase.tasks.length} tasks)${fTag}`;
357
+ });
358
+ return text(lines.join("\n") || "No phases");
359
+ });
360
+ server.registerTool("planner-task-list", {
361
+ 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.",
362
+ inputSchema: { featureRef: z.string().optional(), phaseRef: z.string().optional(), status: z.string().optional().describe("Optional: filter by status name.") },
363
+ }, async ({ featureRef, phaseRef, status }) => {
364
+ const st = await requireStore();
365
+ const features = (await st.loadFeatures()).features;
366
+ let phases = await st.loadAllPhases();
367
+ if (featureRef) {
368
+ const f = findFeatureByRef(features, featureRef);
369
+ if (!f)
370
+ return text(`Feature not found: ${featureRef}`);
371
+ phases = phases.filter((p) => p.featureId === f.id);
372
+ }
373
+ if (phaseRef) {
374
+ const p = findPhaseByRef(phases, features, phaseRef);
375
+ if (!p)
376
+ return text(`Phase not found: ${phaseRef}`);
377
+ phases = [p];
378
+ }
379
+ const out = [];
380
+ for (const phase of phases) {
381
+ for (const task of phase.tasks) {
382
+ if (status && task.status !== status)
383
+ continue;
384
+ out.push(`- ${taskCompositeRef(task, phase, features)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status})`);
385
+ }
386
+ }
387
+ return text(out.join("\n") || "No tasks");
275
388
  });
276
389
  server.registerTool("planner-feature-add", {
277
390
  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 +402,7 @@ server.registerTool("planner-feature-add", {
289
402
  const priority = await st.nextPriority("feature");
290
403
  const feature = {
291
404
  id: createFeatureId(),
292
- number: existingFeatures.length + 1,
405
+ number: await st.allocFeatureNumber(),
293
406
  shortId,
294
407
  priority,
295
408
  name: name.trim(),
@@ -305,6 +418,7 @@ server.registerTool("planner-feature-add", {
305
418
  acceptedDecisions: [],
306
419
  phaseIds: [],
307
420
  dependsOn: [],
421
+ statusLog: [],
308
422
  createdAt: timestamp,
309
423
  updatedAt: timestamp,
310
424
  };
@@ -312,24 +426,63 @@ server.registerTool("planner-feature-add", {
312
426
  doc.features.push(feature);
313
427
  return doc;
314
428
  });
315
- return writeAndSummarize(st, `Feature created: ${feature.id} — ${feature.name}`, { feature });
429
+ return writeAndSummarize(st, `✅ Feature created: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}`);
316
430
  });
317
431
  server.registerTool("planner-feature-show", {
318
432
  description: "Show a feature by id or name.",
319
- inputSchema: { feature: z.string().min(1).describe("Feature id or name") },
320
- }, async ({ feature: ref }) => {
433
+ 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).") },
434
+ }, async ({ feature: ref, full }) => {
321
435
  const st = await requireStore();
322
436
  const features = (await st.loadFeatures()).features;
323
- const feature = findFeatureByRef(features, ref);
324
- if (!feature)
325
- return text(`Feature not found: ${ref}`);
437
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
438
+ if (!resolvedFeature.ok)
439
+ return text(resolvedFeature.error);
440
+ const feature = resolvedFeature.feature;
326
441
  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 });
442
+ const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
443
+ return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
444
+ });
445
+ server.registerTool("planner-feature-discuss", {
446
+ description: "Persist feature discovery/governance fields and mark the feature context as ready.",
447
+ inputSchema: {
448
+ feature: z.string().min(1).describe("Feature ref. Accepts F00x, shortId, UUID, or title."),
449
+ description: z.string().optional().describe("Current implementation state, scope, and goals for this feature."),
450
+ workDone: z.string().optional().describe("What is already implemented / decided."),
451
+ workRemaining: z.string().optional().describe("What still needs to be done."),
452
+ dependencies: z.array(z.string()).optional().describe("Cross-feature or external dependencies."),
453
+ },
454
+ }, async ({ feature: ref, description, workDone, workRemaining, dependencies }) => {
455
+ const st = await requireStore();
456
+ const features = (await st.loadFeatures()).features;
457
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
458
+ if (!resolvedFeature.ok)
459
+ return text(resolvedFeature.error);
460
+ const feature = resolvedFeature.feature;
461
+ const updated = await st.updateFeatures((doc) => {
462
+ const target = doc.features.find((entry) => entry.id === feature.id);
463
+ if (!target)
464
+ return doc;
465
+ if (description !== undefined)
466
+ target.description = description.trim();
467
+ if (workDone !== undefined)
468
+ target.workDone = workDone.trim();
469
+ if (workRemaining !== undefined)
470
+ target.workRemaining = workRemaining.trim();
471
+ if (dependencies !== undefined)
472
+ target.dependsOn = dependencies.map((item) => item.trim()).filter(Boolean);
473
+ target.discussedAt = nowISO();
474
+ target.contextReady = true;
475
+ target.contextReadyReason = "Updated through planner-feature-discuss MCP tool.";
476
+ target.updatedAt = nowISO();
477
+ return doc;
478
+ });
479
+ const result = updated.features.find((entry) => entry.id === feature.id);
480
+ return writeAndSummarize(st, `✅ Feature discussed/updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}`);
328
481
  });
329
482
  server.registerTool("planner-feature-update", {
330
483
  description: "Update a feature.",
331
484
  inputSchema: {
332
- feature: z.string().min(1).describe("Feature id or name"),
485
+ feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
333
486
  name: z.string().optional(),
334
487
  description: z.string().optional(),
335
488
  status: z.enum(STATUS_VALUES).optional(),
@@ -342,9 +495,10 @@ server.registerTool("planner-feature-update", {
342
495
  }, async ({ feature: ref, ...updates }) => {
343
496
  const st = await requireStore();
344
497
  const features = (await st.loadFeatures()).features;
345
- const feature = findFeatureByRef(features, ref);
346
- if (!feature)
347
- return text(`Feature not found: ${ref}`);
498
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
499
+ if (!resolvedFeature.ok)
500
+ return text(resolvedFeature.error);
501
+ const feature = resolvedFeature.feature;
348
502
  const updated = await st.updateFeatures((doc) => {
349
503
  const target = doc.features.find((entry) => entry.id === feature.id);
350
504
  if (!target)
@@ -369,20 +523,21 @@ server.registerTool("planner-feature-update", {
369
523
  return doc;
370
524
  });
371
525
  const result = updated.features.find((entry) => entry.id === feature.id);
372
- return writeAndSummarize(st, `Feature updated: ${result.id} — ${result.name}`, { feature: result });
526
+ return writeAndSummarize(st, `✅ Feature updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}`);
373
527
  });
374
528
  server.registerTool("planner-feature-delete", {
375
529
  description: "Delete a feature. By default, phases are unlinked rather than deleted.",
376
530
  inputSchema: {
377
- feature: z.string().min(1).describe("Feature id or name"),
531
+ feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
378
532
  cascade: z.boolean().optional().describe("Also delete phases belonging to this feature"),
379
533
  },
380
534
  }, async ({ feature: ref, cascade }) => {
381
535
  const st = await requireStore();
382
536
  const features = (await st.loadFeatures()).features;
383
- const feature = findFeatureByRef(features, ref);
384
- if (!feature)
385
- return text(`Feature not found: ${ref}`);
537
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
538
+ if (!resolvedFeature.ok)
539
+ return text(resolvedFeature.error);
540
+ const feature = resolvedFeature.feature;
386
541
  const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
387
542
  await st.updateFeatures((doc) => {
388
543
  doc.features = doc.features.filter((entry) => entry.id !== feature.id);
@@ -401,19 +556,27 @@ server.registerTool("planner-feature-delete", {
401
556
  return writeAndSummarize(st, `Feature deleted: ${feature.id}${cascade ? `; deleted ${phases.length} phases` : `; unlinked ${phases.length} phases`}`, { deleted: feature.id, affectedPhases: phases.length, cascade: Boolean(cascade) });
402
557
  });
403
558
  server.registerTool("planner-phase-add", {
404
- 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.",
559
+ description: "Create a phase linked to a feature with a rich description. REQUIRED: feature ref and description. The 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
560
  inputSchema: {
406
561
  title: z.string().min(1),
407
- feature: z.string().optional().describe("Feature id or name"),
562
+ feature: z.string().min(1).optional().describe("Feature ref (required). Accepts F00x/P00x/T00x composite, bare F00x (global), 5-char shortId, UUID, or title."),
408
563
  summary: z.string().optional().describe("One-line summary of the phase"),
409
564
  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
565
  },
411
566
  }, async ({ title, feature: featureRef, summary, description }) => {
412
567
  const st = await requireStore();
568
+ if (!featureRef?.trim())
569
+ return text("feature is required: a phase must belong to a feature.");
413
570
  const featuresDoc = await st.loadFeatures();
414
- const feature = featureRef ? findFeatureByRef(featuresDoc.features, featureRef) : featuresDoc.features[0];
415
- if (featureRef && !feature)
416
- return text(`Feature not found: ${featureRef}`);
571
+ const resolvedFeature = resolveFeatureRefStrict(featuresDoc.features, featureRef);
572
+ if (!resolvedFeature.ok)
573
+ return text(resolvedFeature.error);
574
+ const featureValidation = await validateResolvedTarget("feature", resolvedFeature.feature.id, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === resolvedFeature.feature.id)).catch(() => undefined));
575
+ if (!featureValidation.ok)
576
+ return text(featureValidation.error);
577
+ const feature = resolvedFeature.feature;
578
+ if (!feature)
579
+ return text(`Resolved feature ${featureRef} no longer exists. Refusing to create phase.`);
417
580
  const lockKey = feature?.id ?? "__unscoped__";
418
581
  let phase;
419
582
  await withFeatureLock(lockKey, async () => {
@@ -424,7 +587,7 @@ server.registerTool("planner-phase-add", {
424
587
  const priority = await st.nextPriority("phase", feature?.id);
425
588
  phase = {
426
589
  id: createPhaseId(),
427
- number: featurePhases.reduce((max, entry) => Math.max(max, entry.number), 0) + 1,
590
+ number: await st.allocPhaseNumber(),
428
591
  shortId,
429
592
  priority,
430
593
  slug: normalizeSlug(title),
@@ -452,6 +615,9 @@ server.registerTool("planner-phase-add", {
452
615
  updatedAt: timestamp,
453
616
  handoff: "",
454
617
  handoffUpdatedAt: "",
618
+ handoffReadAt: "",
619
+ handoffHistory: [],
620
+ statusLog: [],
455
621
  };
456
622
  await st.savePhase(phase);
457
623
  if (feature) {
@@ -466,22 +632,29 @@ server.registerTool("planner-phase-add", {
466
632
  });
467
633
  if (!phase)
468
634
  return text("Phase creation failed.");
469
- return writeAndSummarize(st, `Phase created: ${phase.id} — ${phase.title}`, { phase });
635
+ return writeAndSummarize(st, `✅ Phase created: ${formatPhaseRef(phase.number, feature?.number)} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
470
636
  });
471
637
  server.registerTool("planner-phase-show", {
472
- description: "Show a phase by id or name.",
473
- inputSchema: { phase: z.string().min(1).describe("Phase id or name") },
474
- }, async ({ phase: ref }) => {
638
+ description: "Show a phase by id or name, including derived linked requirements in the full view.",
639
+ 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).") },
640
+ }, async ({ phase: ref, full }) => {
475
641
  const st = await requireStore();
476
- const phase = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
642
+ const features = (await st.loadFeatures()).features;
643
+ const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
477
644
  if (!phase)
478
645
  return text(`Phase not found: ${ref}`);
479
- return text(`${phase.title} (${phase.id}) — ${phase.status}; ${phase.tasks.length} tasks`, { phase });
646
+ const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
647
+ const reqCount = linkedRequirements.length;
648
+ const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks${reqCount ? `; ${reqCount} linked requirement${reqCount === 1 ? "" : "s"}` : ""})`;
649
+ const requirementsBlock = reqCount > 0
650
+ ? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
651
+ : "";
652
+ return text(full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary, { linkedRequirements });
480
653
  });
481
654
  server.registerTool("planner-phase-discuss", {
482
655
  description: "Persist phase discovery fields and mark phase planned.",
483
656
  inputSchema: {
484
- phase: z.string().min(1).describe("Phase id or name"),
657
+ phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
485
658
  goal: z.string().optional(),
486
659
  summary: z.string().optional(),
487
660
  scope: z.string().optional(),
@@ -492,7 +665,8 @@ server.registerTool("planner-phase-discuss", {
492
665
  },
493
666
  }, async ({ phase: ref, ...updates }) => {
494
667
  const st = await requireStore();
495
- const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
668
+ const features = (await st.loadFeatures()).features;
669
+ const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
496
670
  if (!found)
497
671
  return text(`Phase not found: ${ref}`);
498
672
  const phase = await st.updatePhase(found.id, (entry) => {
@@ -517,7 +691,7 @@ server.registerTool("planner-phase-discuss", {
517
691
  entry.updatedAt = nowISO();
518
692
  return entry;
519
693
  });
520
- return writeAndSummarize(st, `Phase discussed/planned: ${phase.id} — ${phase.title}`, { phase });
694
+ return writeAndSummarize(st, `✅ Phase discussed/planned: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
521
695
  });
522
696
  server.registerTool("planner-phase-update", {
523
697
  description: "Update phase fields.",
@@ -531,7 +705,8 @@ server.registerTool("planner-phase-update", {
531
705
  },
532
706
  }, async ({ phase: ref, ...updates }) => {
533
707
  const st = await requireStore();
534
- const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
708
+ const features = (await st.loadFeatures()).features;
709
+ const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
535
710
  if (!found)
536
711
  return text(`Phase not found: ${ref}`);
537
712
  const phase = await st.updatePhase(found.id, (entry) => {
@@ -548,7 +723,7 @@ server.registerTool("planner-phase-update", {
548
723
  entry.updatedAt = nowISO();
549
724
  return entry;
550
725
  });
551
- return writeAndSummarize(st, `Phase updated: ${phase.id} — ${phase.title}`, { phase });
726
+ return writeAndSummarize(st, `✅ Phase updated: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
552
727
  });
553
728
  server.registerTool("planner-phase-delete", {
554
729
  description: "Delete a phase and unlink it from features.",
@@ -564,21 +739,43 @@ server.registerTool("planner-phase-delete", {
564
739
  feature.phaseIds = feature.phaseIds.filter((id) => id !== phase.id);
565
740
  return doc;
566
741
  });
567
- return writeAndSummarize(st, `Phase deleted: ${phase.id}`, { deleted: phase.id });
742
+ const dFeat = (await st.loadFeatures()).features.find((f) => f.id === phase.featureId);
743
+ return writeAndSummarize(st, `Phase deleted: ${formatPhaseRef(phase.number, dFeat?.number)}${phase.shortId ? ` · ${phase.shortId}` : ""}`, { deleted: phase.id });
568
744
  });
569
745
  server.registerTool("planner-task-add", {
570
746
  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
747
  inputSchema: {
572
- phase: z.string().min(1).describe("Phase id or name"),
748
+ feature: z.string().min(1).optional().describe("Feature ref the task's phase belongs to. Accepts F00x/P00x/T00x composite, bare F00x (global), 5-char shortId, UUID, or title. REQUIRED."),
749
+ phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
573
750
  title: z.string().min(1),
574
751
  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
752
  checklist: z.array(z.string()).optional(),
576
753
  },
577
- }, async ({ phase: ref, title, description, checklist }) => {
754
+ }, async ({ feature: featureRef, phase: ref, title, description, checklist }) => {
578
755
  const st = await requireStore();
579
- const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
756
+ const features = (await st.loadFeatures()).features;
757
+ if (!featureRef?.trim())
758
+ return text("feature is required: a task must belong to a feature.");
759
+ const resolvedFeature = resolveFeatureRefStrict(features, featureRef);
760
+ if (!resolvedFeature.ok)
761
+ return text(resolvedFeature.error);
762
+ const featureValidation = await validateResolvedTarget("feature", resolvedFeature.feature.id, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === resolvedFeature.feature.id)).catch(() => undefined));
763
+ if (!featureValidation.ok)
764
+ return text(featureValidation.error);
765
+ const feature = resolvedFeature.feature;
766
+ if (!feature)
767
+ return text(`Resolved feature ${featureRef} no longer exists. Refusing to create task.`);
768
+ const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
580
769
  if (!found)
581
770
  return text(`Phase not found: ${ref}`);
771
+ if (found.featureId !== feature.id)
772
+ return text(`Phase ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} does not belong to feature ${formatFeatureRef(feature.number)}. Refusing to create task.`);
773
+ const phaseValidation = await validateResolvedTarget("phase", found.id, () => st.loadPhase(found.id).catch(() => undefined));
774
+ if (!phaseValidation.ok)
775
+ return text(phaseValidation.error);
776
+ const existingPhase = await st.loadPhase(found.id);
777
+ if (!existingPhase)
778
+ return text(`Resolved phase ${found.id} no longer exists. Refusing to create task.`);
582
779
  const timestamp = nowISO();
583
780
  const taskId = createTaskId();
584
781
  const shortId = createShortId(await st.assignedShortIds());
@@ -586,7 +783,7 @@ server.registerTool("planner-task-add", {
586
783
  const task = {
587
784
  id: taskId,
588
785
  phaseId: found.id,
589
- number: found.tasks.length + 1,
786
+ number: await st.allocTaskNumber(),
590
787
  shortId,
591
788
  priority,
592
789
  shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
@@ -597,7 +794,7 @@ server.registerTool("planner-task-add", {
597
794
  statusLog: [],
598
795
  decisions: [],
599
796
  acceptedDecisions: [],
600
- checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), title: item, checked: false })),
797
+ checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
601
798
  subtasks: [],
602
799
  dependsOn: [],
603
800
  startedAt: "",
@@ -611,18 +808,23 @@ server.registerTool("planner-task-add", {
611
808
  entry.updatedAt = timestamp;
612
809
  return entry;
613
810
  });
614
- const features = (await st.loadFeatures()).features;
615
- return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, features)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
811
+ const finalFeatures = (await st.loadFeatures()).features;
812
+ return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, finalFeatures)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
616
813
  });
617
814
  server.registerTool("planner-task-show", {
618
815
  description: "Show a task by id or name.",
619
- inputSchema: { task: z.string().min(1).describe("Task id or name") },
620
- }, async ({ task: ref }) => {
816
+ 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).") },
817
+ }, async ({ task: ref, full }) => {
621
818
  const st = await requireStore();
622
- const found = findTaskByRef(await st.loadAllPhases(), ref);
819
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
623
820
  if (!found)
624
821
  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 });
822
+ const features = (await st.loadFeatures()).features;
823
+ 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))})`;
824
+ if (!full)
825
+ return text(summary);
826
+ const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
827
+ return text(`${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}`);
626
828
  });
627
829
  server.registerTool("planner-task-discuss", {
628
830
  description: "Persist task execution notes/checklist.",
@@ -633,7 +835,7 @@ server.registerTool("planner-task-discuss", {
633
835
  },
634
836
  }, async ({ task: ref, description, checklist }) => {
635
837
  const st = await requireStore();
636
- const found = findTaskByRef(await st.loadAllPhases(), ref);
838
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
637
839
  if (!found)
638
840
  return text(`Task not found: ${ref}`);
639
841
  let updatedTask;
@@ -644,7 +846,9 @@ server.registerTool("planner-task-discuss", {
644
846
  if (description !== undefined)
645
847
  task.description = description.trim();
646
848
  if (checklist !== undefined)
647
- task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), title: item, checked: false }));
849
+ task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), number: index + 1, title: item, checked: false }));
850
+ if (checklist !== undefined)
851
+ task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), number: index + 1, title: item, checked: false }));
648
852
  task.updatedAt = nowISO();
649
853
  phase.updatedAt = task.updatedAt;
650
854
  updatedTask = task;
@@ -663,10 +867,11 @@ server.registerTool("planner-task-update", {
663
867
  description: z.string().optional(),
664
868
  motivation: z.string().optional(),
665
869
  priority: z.number().int().nonnegative().optional().describe("Display order within the phase (lower = higher)."),
870
+ checklist: z.array(z.string()).optional().describe("Replace the task checklist (implementation steps, plain strings). Agents should tick steps via planner-task-checklist-toggle, not write DONE in titles."),
666
871
  },
667
- }, async ({ task: ref, title, status, description, motivation, priority }) => {
872
+ }, async ({ task: ref, title, status, description, motivation, priority, checklist }) => {
668
873
  const st = await requireStore();
669
- const found = findTaskByRef(await st.loadAllPhases(), ref);
874
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
670
875
  if (!found)
671
876
  return text(`Task not found: ${ref}`);
672
877
  // Validate motivation requirement for status transitions.
@@ -711,12 +916,113 @@ server.registerTool("planner-task-update", {
711
916
  const t = updatedTask ?? found.task;
712
917
  return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
713
918
  });
919
+ server.registerTool("planner-task-checklist-toggle", {
920
+ description: "Tick/untick a task checklist item (a 'step') without rewriting the whole list. Accepts the item as C{n} (e.g. C2), the item id, or the title (case-insensitive, first match). Pass checked=true/false to set explicitly, or omit to toggle. Use this instead of writing DONE in step titles. Items are numbered C1/C2… per task.",
921
+ inputSchema: {
922
+ task: z.string().min(1).describe("Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title"),
923
+ item: z.string().min(1).describe("Checklist item selector: C{n} (e.g. C2), item id, or title (case-insensitive)"),
924
+ checked: z.boolean().optional().describe("Set explicitly: true=done, false=open. Omit to toggle."),
925
+ },
926
+ }, async ({ task: ref, item, checked }) => {
927
+ const st = await requireStore();
928
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
929
+ if (!found)
930
+ return text(`Task not found: ${ref}`);
931
+ let result = "";
932
+ await st.updatePhase(found.phase.id, (phase) => {
933
+ const task = phase.tasks.find((entry) => entry.id === found.task.id);
934
+ if (!task) {
935
+ result = `Task not found: ${ref}`;
936
+ return phase;
937
+ }
938
+ const ck = task.checklist ?? [];
939
+ if (ck.length === 0) {
940
+ result = `Task "${found.task.title}" has no checklist.`;
941
+ return phase;
942
+ }
943
+ const target = toggleChecklistItem(ck, item, checked);
944
+ if (!target) {
945
+ result = `No checklist item matching "${item}".`;
946
+ return phase;
947
+ }
948
+ task.updatedAt = nowISO();
949
+ phase.updatedAt = nowISO();
950
+ const doneCount = ck.filter((i) => i.checked).length;
951
+ result = `C${target.number} "${target.title}" → ${target.checked ? "done" : "open"} (${doneCount}/${ck.length} checked)`;
952
+ return phase;
953
+ });
954
+ return writeAndSummarize(st, `✅ ${result}`);
955
+ });
956
+ server.registerTool("planner-task-checklist-add", {
957
+ description: "Add a single checklist item (a 'step') to a task without rewriting the list. The new item is appended as C{n} (next progressive number, stable id, unchecked). Use this to subdivide a task into smaller steps INSTEAD of spawning sub-tasks — the checklist keeps description, notes, statusLog and steps concentrated in one task (sub-tasks disperse context). Also use for granular adds instead of replacing the whole checklist via planner-task-update.",
958
+ inputSchema: {
959
+ task: z.string().min(1).describe("Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title"),
960
+ title: z.string().min(1).describe("Checklist item text (a single step)"),
961
+ },
962
+ }, async ({ task: ref, title }) => {
963
+ const st = await requireStore();
964
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
965
+ if (!found)
966
+ return text(`Task not found: ${ref}`);
967
+ let result = "";
968
+ await st.updatePhase(found.phase.id, (phase) => {
969
+ const task = phase.tasks.find((entry) => entry.id === found.task.id);
970
+ if (!task) {
971
+ result = `Task not found: ${ref}`;
972
+ return phase;
973
+ }
974
+ const ck = task.checklist ?? [];
975
+ const item = addChecklistItem(ck, task.id, title);
976
+ ck.push(item);
977
+ task.checklist = ck;
978
+ task.updatedAt = nowISO();
979
+ phase.updatedAt = nowISO();
980
+ result = `Added C${item.number} "${item.title}" (${ck.length} items)`;
981
+ return phase;
982
+ });
983
+ return writeAndSummarize(st, `✅ ${result}`);
984
+ });
985
+ server.registerTool("planner-task-checklist-remove", {
986
+ description: "Remove a single checklist item (a 'step') from a task by C{n} (e.g. C2), item id, or title (case-insensitive). Remaining items are renumbered C1..Cn for readability; their stable ids are preserved. Use this for granular removes instead of replacing the whole checklist via planner-task-update.",
987
+ inputSchema: {
988
+ task: z.string().min(1).describe("Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title"),
989
+ item: z.string().min(1).describe("Checklist item selector: C{n} (e.g. C2), item id, or title (case-insensitive)"),
990
+ },
991
+ }, async ({ task: ref, item }) => {
992
+ const st = await requireStore();
993
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
994
+ if (!found)
995
+ return text(`Task not found: ${ref}`);
996
+ let result = "";
997
+ await st.updatePhase(found.phase.id, (phase) => {
998
+ const task = phase.tasks.find((entry) => entry.id === found.task.id);
999
+ if (!task) {
1000
+ result = `Task not found: ${ref}`;
1001
+ return phase;
1002
+ }
1003
+ const ck = task.checklist ?? [];
1004
+ if (ck.length === 0) {
1005
+ result = `Task "${found.task.title}" has no checklist.`;
1006
+ return phase;
1007
+ }
1008
+ const removed = removeChecklistItem(ck, item);
1009
+ if (!removed) {
1010
+ result = `No checklist item matching "${item}".`;
1011
+ return phase;
1012
+ }
1013
+ task.updatedAt = nowISO();
1014
+ phase.updatedAt = nowISO();
1015
+ result = `Removed C${removed.number} "${removed.title}" (${ck.length} items left)`;
1016
+ return phase;
1017
+ });
1018
+ return writeAndSummarize(st, `✅ ${result}`);
1019
+ });
714
1020
  server.registerTool("planner-task-delete", {
715
1021
  description: "Delete a task.",
716
1022
  inputSchema: { task: z.string().min(1) },
717
1023
  }, async ({ task: ref }) => {
718
1024
  const st = await requireStore();
719
- const found = findTaskByRef(await st.loadAllPhases(), ref);
1025
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
720
1026
  if (!found)
721
1027
  return text(`Task not found: ${ref}`);
722
1028
  await st.updatePhase(found.phase.id, (phase) => {
@@ -726,23 +1032,79 @@ server.registerTool("planner-task-delete", {
726
1032
  return phase;
727
1033
  });
728
1034
  await st.syncTaskStatusRollup(found.phase.id);
729
- return writeAndSummarize(st, `Task deleted: ${found.task.id}`, { deleted: found.task.id });
1035
+ const tdFeatures = (await st.loadFeatures()).features;
1036
+ return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
1037
+ });
1038
+ server.registerTool("planner-task-recommend", {
1039
+ description: "Return the harness-agnostic recommended task: continue one active task, otherwise choose ready work by feature → phase → task priority. Reports dependency, availability, and approved deviation/resume context without starting or blocking work.",
1040
+ inputSchema: {},
1041
+ }, async () => {
1042
+ const st = await requireStore();
1043
+ const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
1044
+ const result = recommendNextTask(features.features, phases, project.workDeviations);
1045
+ if (!result.candidate)
1046
+ return text(`No task recommendation: ${result.reason}`, { kind: result.kind, reason: result.reason, activeTaskIds: result.activeCandidates?.map((candidate) => candidate.task.id) ?? [] });
1047
+ const { candidate } = result;
1048
+ const ref = taskCompositeRef(candidate.task, candidate.phase, features.features);
1049
+ return text(`Recommended (${result.kind}): ${ref} — ${candidate.task.title}\n${result.reason}${result.deviation ? `\nDeviation: ${result.deviation.id}; resume target ${result.deviation.resumeTaskId}.` : ""}`, {
1050
+ kind: result.kind, taskId: candidate.task.id, phaseId: candidate.phase.id, featureId: candidate.feature?.id, deviation: result.deviation,
1051
+ });
1052
+ });
1053
+ server.registerTool("planner-task-deviation", {
1054
+ description: "Record a user-approved temporary task deviation. It preserves the recommended/resume task and never starts, pauses, or blocks work; use normal lifecycle tools for those transitions.",
1055
+ inputSchema: { temporary_task: z.string().min(1), resume_task: z.string().min(1).optional(), reason: z.string().min(1) },
1056
+ }, async ({ temporary_task, resume_task, reason }) => {
1057
+ const st = await requireStore();
1058
+ const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
1059
+ const temporary = findTaskByRef(phases, features.features, temporary_task);
1060
+ if (!temporary)
1061
+ return text(`Task not found: ${temporary_task}`);
1062
+ const selected = recommendNextTask(features.features, phases, project.workDeviations);
1063
+ const resume = resume_task ? findTaskByRef(phases, features.features, resume_task) : selected.candidate;
1064
+ if (!resume)
1065
+ return text(`No resume task is available. Provide resume_task explicitly. ${selected.reason}`);
1066
+ if (temporary.task.id === resume.task.id)
1067
+ return text("A temporary task must differ from its resume target.");
1068
+ if (temporary.task.status !== "planned")
1069
+ return text(`Temporary task is not startable: ${temporary.task.status}.`);
1070
+ const timestamp = nowISO();
1071
+ const record = { id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id, temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason, requestedBy: "user", approvedBy: "user", state: "approved", createdAt: timestamp, activatedAt: "", resolvedAt: "" };
1072
+ await st.addWorkDeviation(record);
1073
+ return writeAndSummarize(st, `✅ Approved deviation: ${taskCompositeRef(temporary.task, temporary.phase, features.features)} temporarily overrides ${taskCompositeRef(resume.task, resume.phase, features.features)}. Resume target retained.`, { deviation: record });
730
1074
  });
731
1075
  server.registerTool("planner-task-start", {
732
- description: "Set a task to in-progress.",
1076
+ description: "Set a task to in-progress. BEFORE calling, use planner-task-show with full=true and planner-phase-show with full=true for the parent phase, including linked requirements. Continue an already in-progress task; otherwise select ready work by feature → phase → task priority (lower number first), respecting dependencies and blocked/waiting states. Returns a phase/requirements briefing.",
733
1077
  inputSchema: { task: z.string().min(1) },
734
1078
  }, async ({ task: ref }) => {
735
1079
  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);
1080
+ const features = (await st.loadFeatures()).features;
1081
+ const found = findTaskByRef(await st.loadAllPhases(), features, ref);
744
1082
  if (!found)
745
1083
  return text(`Task not found: ${ref}`);
1084
+ if (found.task.status === "in-progress")
1085
+ return text(`Task ${found.task.id} is already in-progress.`);
1086
+ if (found.task.status === "done")
1087
+ return text(`Task ${found.task.id} is done. Use planner-task-update to reopen.`);
1088
+ const project = await st.loadProject();
1089
+ const selection = recommendNextTask(features, await st.loadAllPhases(), project.workDeviations);
1090
+ if (!selection.candidate || selection.candidate.task.id !== found.task.id) {
1091
+ const recommended = selection.candidate ? ` Start ${selection.candidate.task.id} instead, or record a user-approved planner-task-deviation first.` : "";
1092
+ return text(`Task start denied: ${selection.reason}.${recommended}`, {
1093
+ kind: selection.kind,
1094
+ reason: selection.reason,
1095
+ recommendedTaskId: selection.candidate?.task.id,
1096
+ });
1097
+ }
1098
+ // Assemble the mandatory parent context before changing task lifecycle state.
1099
+ const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
1100
+ const [phaseWithRequirements, featureRequirements] = await Promise.all([
1101
+ st.loadPhaseWithRequirements(found.phase.id),
1102
+ parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
1103
+ ]);
1104
+ const phaseContext = buildPhaseContextBlock(phaseWithRequirements, parentFeature, phaseWithRequirements.linkedRequirements ?? [], featureRequirements);
1105
+ // A pending handoff is context, not a lock: task start RETAINS it. It is
1106
+ // archived only when the phase completes (auto) or the user explicitly
1107
+ // clears it.
746
1108
  const timestamp = nowISO();
747
1109
  let updatedTask;
748
1110
  await st.updatePhase(found.phase.id, (phase) => {
@@ -767,9 +1129,11 @@ server.registerTool("planner-task-start", {
767
1129
  return phase;
768
1130
  });
769
1131
  await st.syncTaskStatusRollup(found.phase.id);
770
- const features = (await st.loadFeatures()).features;
1132
+ if (selection.deviation?.temporaryTaskId === found.task.id && selection.deviation.state === "approved") {
1133
+ await st.setWorkDeviationState(selection.deviation.id, "active", timestamp);
1134
+ }
771
1135
  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}` : ""}`);
1136
+ return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}`);
773
1137
  });
774
1138
  server.registerTool("planner-task-complete", {
775
1139
  description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
@@ -780,7 +1144,7 @@ server.registerTool("planner-task-complete", {
780
1144
  },
781
1145
  }, async ({ task: ref, force, description_update }) => {
782
1146
  const st = await requireStore();
783
- const found = findTaskByRef(await st.loadAllPhases(), ref);
1147
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
784
1148
  if (!found)
785
1149
  return text(`Task not found: ${ref}`);
786
1150
  const unchecked = found.task.checklist.filter((item) => !item.checked);
@@ -814,6 +1178,11 @@ server.registerTool("planner-task-complete", {
814
1178
  return phase;
815
1179
  });
816
1180
  const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
1181
+ const completedDeviation = (await st.loadProject()).workDeviations
1182
+ .filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === found.task.id)
1183
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
1184
+ if (completedDeviation)
1185
+ await st.setWorkDeviationState(completedDeviation.id, "resolved", timestamp);
817
1186
  const features = (await st.loadFeatures()).features;
818
1187
  const t = updatedTask ?? found.task;
819
1188
  return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`);
@@ -829,8 +1198,8 @@ server.registerTool("planner-handoff-list", {
829
1198
  return text(`Phase handoffs (${list.length}):\n${lines.join("\n")}`, { count: list.length, handoffs: list });
830
1199
  });
831
1200
  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.") },
1201
+ description: "Read the entity-scoped handoff of a phase. phaseRef is required; never infer a phase from in-progress status or stale context.",
1202
+ inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title. Ask the user when ambiguous.") },
834
1203
  }, async ({ phaseRef }) => {
835
1204
  const st = await requireStore();
836
1205
  const r = await resolvePhaseForHandoff(st, phaseRef);
@@ -842,36 +1211,61 @@ server.registerTool("planner-handoff-show", {
842
1211
  return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
843
1212
  });
844
1213
  server.registerTool("planner-handoff-write", {
845
- description: "Write/refresh the entity-scoped handoff on a phase. Pass content (markdown). phaseRef optional (default: current in-progress phase). Captures design context for a resuming agent on that phase.",
1214
+ description: "Write/refresh the entity-scoped handoff on a phase. phaseRef is required and must be the exact phase selected with the user. Never guess from in-progress status or stale context; completed/canceled phases reject new handoffs. Pass content (markdown) and a meaningful title.",
846
1215
  inputSchema: {
847
- phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase."),
1216
+ phaseRef: z.string().min(1).describe("Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title."),
1217
+ title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work (becomes the H1 / first line in lists). REQUIRED if your markdown's first heading is generic. Example: 'P049 — featureId validation: tests + adapter wiring'."),
1218
+ confirmed: z.boolean().describe("Set true only after the user explicitly confirms the proposed feature+phase target."),
848
1219
  content: z.string().min(1).describe("Handoff text (markdown)."),
849
1220
  },
850
- }, async ({ phaseRef, content }) => {
1221
+ }, async ({ phaseRef, title, confirmed, content }) => {
851
1222
  const st = await requireStore();
1223
+ let body = content.trim();
1224
+ const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1225
+ const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
1226
+ const isGeneric = !firstHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(firstHeadingText);
1227
+ if (!title && isGeneric) {
1228
+ return text("❌ Generic handoff title. Provide a meaningful `title` (or start your markdown with a descriptive H1) summarizing the work — e.g. 'P049 — featureId validation: tests + adapter wiring'. Generic titles like 'Handoff' or 'Canonical handoff' are not accepted.");
1229
+ }
1230
+ if (title && title.trim()) {
1231
+ const t = title.trim();
1232
+ const lines = body.split(/\r?\n/);
1233
+ const firstIdx = lines.findIndex((l) => l.trim().length > 0);
1234
+ if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? "")) {
1235
+ lines[firstIdx] = `# ${t}`;
1236
+ body = lines.join("\n");
1237
+ }
1238
+ else {
1239
+ body = `# ${t}\n\n${body}`;
1240
+ }
1241
+ }
852
1242
  const r = await resolvePhaseForHandoff(st, phaseRef);
853
1243
  if (!r.ok)
854
1244
  return text(`❌ ${r.error}`);
855
- await st.setPhaseHandoff(r.phase.id, content);
1245
+ if (!confirmed)
1246
+ return text(`Proposal only: I would write this handoff on ${r.compositeRef}. Ask the user to confirm this exact feature/phase, then retry with confirmed=true.`, { phaseRef: r.compositeRef, confirmationRequired: true });
1247
+ await st.setPhaseHandoff(r.phase.id, body);
856
1248
  return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
857
1249
  });
858
1250
  server.registerTool("planner-handoff-prepare", {
859
- description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write (entity-scoped, phase.handoff).",
1251
+ description: "Prepare a canonical handoff proposal. Do not write until the agent has identified the exact feature+phase from the conversation and the user has confirmed that target.",
860
1252
  }, async () => text([
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).",
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.",
863
- "Stored on the phase.handoff field (entity-scoped). .planner/HANDOFF.md is deprecated.",
1253
+ "First identify the exact feature and phase actually discussed/worked on in this session. Do not use the first in-progress phase and do not target a phase that just became done.",
1254
+ "Before calling planner-handoff-write, tell the user: 'I propose writing this handoff on P00x(F00x) — <phase title>. Confirm?' Wait for explicit confirmation.",
1255
+ "After confirmation, call planner-handoff-write with that exact phaseRef. If the phase is done/canceled, do not write an operational handoff.",
1256
+ "Pass a meaningful title. Required sections: Created at, Updated at, Reason, Current focus, What was being done, How to resume, Files touched, Blockers, Next steps, Recent decisions, Reminder.",
1257
+ "Stored on phase.handoff; .planner/HANDOFF.md is deprecated.",
864
1258
  ].join("\n")));
865
1259
  server.registerTool("planner-handoff-clear", {
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.") },
1260
+ description: "Clear/archive the entity-scoped handoff of a phase. phaseRef is required; never infer a phase automatically.",
1261
+ inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title.") },
868
1262
  }, async ({ phaseRef }) => {
869
1263
  const st = await requireStore();
870
1264
  const r = await resolvePhaseForHandoff(st, phaseRef);
871
1265
  if (!r.ok)
872
1266
  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 });
1267
+ await st.clearPhaseHandoff(r.phase.id, "manual");
1268
+ return text(`✅ Cleared handoff on ${r.compositeRef} (handoffUpdatedAt preserved as audit; archived to .planner/.local/handoff-archive/).`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
875
1269
  });
876
1270
  server.registerTool("planner-web", {
877
1271
  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.2",
3
+ "version": "0.2.19-next.21",
4
4
  "private": false,
5
5
  "description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
6
6
  "license": "MIT",
@@ -17,9 +17,11 @@
17
17
  "dist/**/*.js",
18
18
  "dist/**/*.d.ts",
19
19
  "dist/**/*.d.ts.map",
20
+ "server.json",
20
21
  "README.md",
21
22
  "LICENSE"
22
23
  ],
24
+ "mcpName": "io.github.ovidius72/agent-plan",
23
25
  "publishConfig": {
24
26
  "access": "public"
25
27
  },
@@ -41,8 +43,8 @@
41
43
  "dependencies": {
42
44
  "@modelcontextprotocol/sdk": "^1.17.0",
43
45
  "zod": "^3.24.0",
44
- "@agent-plan/core": "0.2.19-next.2",
45
- "@agent-plan/server": "0.2.19-next.2"
46
+ "@agent-plan/server": "0.2.19-next.21",
47
+ "@agent-plan/core": "0.2.19-next.21"
46
48
  },
47
49
  "scripts": {
48
50
  "build": "tsc -p tsconfig.json",
package/server.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "io.github.ovidius72/agent-plan",
3
+ "version": "0.2.19-next.18",
4
+ "description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/ovidius72/agent-planner.git"
9
+ },
10
+ "homepage": "https://github.com/ovidius72/agent-planner#readme",
11
+ "package": {
12
+ "type": "npm",
13
+ "name": "@agent-plan/mcp"
14
+ }
15
+ }