@agent-plan/mcp 0.2.19-next.16 → 0.2.19-next.18

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":";AA4iCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA+pCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, migrateToGlobalSequence, addChecklistItem, removeChecklistItem, toggleChecklistItem } from "@agent-plan/core";
8
8
  import { serve } from "@agent-plan/server";
9
- import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef } 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) {
@@ -58,10 +58,50 @@ async function ensureWebStarted() {
58
58
  }
59
59
  function findFeatureByRef(features, ref) {
60
60
  const normalized = ref.trim().toLowerCase();
61
- return features.find((feature) => feature.id.toLowerCase() === normalized)
61
+ if (!normalized)
62
+ return undefined;
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;
69
+ }
70
+ return features.find((feature) => feature.shortId?.toLowerCase() === normalized)
71
+ ?? features.find((feature) => feature.id.toLowerCase() === normalized)
62
72
  ?? features.find((feature) => feature.name.toLowerCase() === normalized)
63
73
  ?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
64
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}` };
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. */
65
105
  function featureNumberOfPhase(phase, features) {
66
106
  return phase.featureId ? features.find((f) => f.id === phase.featureId)?.number : undefined;
67
107
  }
@@ -210,6 +250,33 @@ server.registerTool("planner-repair", {
210
250
  const report = await st.repair();
211
251
  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 });
212
252
  });
253
+ server.registerTool("planner-cleanup-orphan-phases", {
254
+ 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.",
255
+ inputSchema: {
256
+ confirm: z.boolean().optional().describe("Set true to actually delete the discovered orphan phase files. Default: false (dry-run/list only)."),
257
+ },
258
+ }, async ({ confirm }) => {
259
+ const st = await requireStore();
260
+ const found = await st.listOrphanPhases();
261
+ if (!confirm) {
262
+ if (found.length === 0)
263
+ return text("No orphan phases found.", { found: [] });
264
+ const lines = [
265
+ `Found ${found.length} orphan phase${found.length === 1 ? "" : "s"}.`,
266
+ ...found.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.reason})`),
267
+ "Rerun with confirm=true to delete these orphan phase files.",
268
+ ];
269
+ return text(lines.join("\n"), { found, confirmRequired: true });
270
+ }
271
+ const report = await st.cleanupOrphanPhases();
272
+ if (report.removed.length === 0)
273
+ return text("No orphan phases found.", report);
274
+ const lines = [
275
+ `Removed ${report.removed.length} orphan phase${report.removed.length === 1 ? "" : "s"}.`,
276
+ ...report.removed.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title}`),
277
+ ];
278
+ return text(lines.join("\n"), report);
279
+ });
213
280
  server.registerTool("planner-project-language", {
214
281
  description: "Persist preferred languages for plan content and chat.",
215
282
  inputSchema: {
@@ -371,13 +438,51 @@ server.registerTool("planner-feature-show", {
371
438
  }, async ({ feature: ref, full }) => {
372
439
  const st = await requireStore();
373
440
  const features = (await st.loadFeatures()).features;
374
- const feature = findFeatureByRef(features, ref);
375
- if (!feature)
376
- return text(`Feature not found: ${ref}`);
441
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
442
+ if (!resolvedFeature.ok)
443
+ return text(resolvedFeature.error);
444
+ const feature = resolvedFeature.feature;
377
445
  const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
378
446
  const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
379
447
  return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
380
448
  });
449
+ server.registerTool("planner-feature-discuss", {
450
+ description: "Persist feature discovery/governance fields and mark the feature context as ready.",
451
+ inputSchema: {
452
+ feature: z.string().min(1).describe("Feature ref. Accepts F00x, shortId, UUID, or title."),
453
+ description: z.string().optional().describe("Current implementation state, scope, and goals for this feature."),
454
+ workDone: z.string().optional().describe("What is already implemented / decided."),
455
+ workRemaining: z.string().optional().describe("What still needs to be done."),
456
+ dependencies: z.array(z.string()).optional().describe("Cross-feature or external dependencies."),
457
+ },
458
+ }, async ({ feature: ref, description, workDone, workRemaining, dependencies }) => {
459
+ const st = await requireStore();
460
+ const features = (await st.loadFeatures()).features;
461
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
462
+ if (!resolvedFeature.ok)
463
+ return text(resolvedFeature.error);
464
+ const feature = resolvedFeature.feature;
465
+ const updated = await st.updateFeatures((doc) => {
466
+ const target = doc.features.find((entry) => entry.id === feature.id);
467
+ if (!target)
468
+ return doc;
469
+ if (description !== undefined)
470
+ target.description = description.trim();
471
+ if (workDone !== undefined)
472
+ target.workDone = workDone.trim();
473
+ if (workRemaining !== undefined)
474
+ target.workRemaining = workRemaining.trim();
475
+ if (dependencies !== undefined)
476
+ target.dependsOn = dependencies.map((item) => item.trim()).filter(Boolean);
477
+ target.discussedAt = nowISO();
478
+ target.contextReady = true;
479
+ target.contextReadyReason = "Updated through planner-feature-discuss MCP tool.";
480
+ target.updatedAt = nowISO();
481
+ return doc;
482
+ });
483
+ const result = updated.features.find((entry) => entry.id === feature.id);
484
+ return writeAndSummarize(st, `✅ Feature discussed/updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}`);
485
+ });
381
486
  server.registerTool("planner-feature-update", {
382
487
  description: "Update a feature.",
383
488
  inputSchema: {
@@ -394,9 +499,10 @@ server.registerTool("planner-feature-update", {
394
499
  }, async ({ feature: ref, ...updates }) => {
395
500
  const st = await requireStore();
396
501
  const features = (await st.loadFeatures()).features;
397
- const feature = findFeatureByRef(features, ref);
398
- if (!feature)
399
- return text(`Feature not found: ${ref}`);
502
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
503
+ if (!resolvedFeature.ok)
504
+ return text(resolvedFeature.error);
505
+ const feature = resolvedFeature.feature;
400
506
  const updated = await st.updateFeatures((doc) => {
401
507
  const target = doc.features.find((entry) => entry.id === feature.id);
402
508
  if (!target)
@@ -432,9 +538,10 @@ server.registerTool("planner-feature-delete", {
432
538
  }, async ({ feature: ref, cascade }) => {
433
539
  const st = await requireStore();
434
540
  const features = (await st.loadFeatures()).features;
435
- const feature = findFeatureByRef(features, ref);
436
- if (!feature)
437
- return text(`Feature not found: ${ref}`);
541
+ const resolvedFeature = resolveFeatureRefStrict(features, ref);
542
+ if (!resolvedFeature.ok)
543
+ return text(resolvedFeature.error);
544
+ const feature = resolvedFeature.feature;
438
545
  const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
439
546
  await st.updateFeatures((doc) => {
440
547
  doc.features = doc.features.filter((entry) => entry.id !== feature.id);
@@ -463,9 +570,21 @@ server.registerTool("planner-phase-add", {
463
570
  }, async ({ title, feature: featureRef, summary, description }) => {
464
571
  const st = await requireStore();
465
572
  const featuresDoc = await st.loadFeatures();
466
- const feature = featureRef ? findFeatureByRef(featuresDoc.features, featureRef) : featuresDoc.features[0];
573
+ let feature;
574
+ if (featureRef) {
575
+ const resolvedFeature = resolveFeatureRefStrict(featuresDoc.features, featureRef);
576
+ if (!resolvedFeature.ok)
577
+ return text(resolvedFeature.error);
578
+ const featureValidation = await validateResolvedTarget("feature", resolvedFeature.feature.id, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === resolvedFeature.feature.id)).catch(() => undefined));
579
+ if (!featureValidation.ok)
580
+ return text(featureValidation.error);
581
+ feature = resolvedFeature.feature;
582
+ }
583
+ else {
584
+ feature = featuresDoc.features[0];
585
+ }
467
586
  if (featureRef && !feature)
468
- return text(`Feature not found: ${featureRef}`);
587
+ return text(`Resolved feature ${featureRef} no longer exists. Refusing to create phase.`);
469
588
  const lockKey = feature?.id ?? "__unscoped__";
470
589
  let phase;
471
590
  await withFeatureLock(lockKey, async () => {
@@ -524,7 +643,7 @@ server.registerTool("planner-phase-add", {
524
643
  return writeAndSummarize(st, `✅ Phase created: ${formatPhaseRef(phase.number, feature?.number)} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
525
644
  });
526
645
  server.registerTool("planner-phase-show", {
527
- description: "Show a phase by id or name.",
646
+ description: "Show a phase by id or name, including derived linked requirements in the full view.",
528
647
  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).") },
529
648
  }, async ({ phase: ref, full }) => {
530
649
  const st = await requireStore();
@@ -532,8 +651,13 @@ server.registerTool("planner-phase-show", {
532
651
  const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
533
652
  if (!phase)
534
653
  return text(`Phase not found: ${ref}`);
535
- const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks)`;
536
- return text(full ? `${summary}\n\n${phase.description || ""}` : summary);
654
+ const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
655
+ const reqCount = linkedRequirements.length;
656
+ 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"}` : ""})`;
657
+ const requirementsBlock = reqCount > 0
658
+ ? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
659
+ : "";
660
+ return text(full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary, { linkedRequirements });
537
661
  });
538
662
  server.registerTool("planner-phase-discuss", {
539
663
  description: "Persist phase discovery fields and mark phase planned.",
@@ -639,6 +763,12 @@ server.registerTool("planner-task-add", {
639
763
  const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
640
764
  if (!found)
641
765
  return text(`Phase not found: ${ref}`);
766
+ const phaseValidation = await validateResolvedTarget("phase", found.id, () => st.loadPhase(found.id).catch(() => undefined));
767
+ if (!phaseValidation.ok)
768
+ return text(phaseValidation.error);
769
+ const existingPhase = await st.loadPhase(found.id);
770
+ if (!existingPhase)
771
+ return text(`Resolved phase ${found.id} no longer exists. Refusing to create task.`);
642
772
  const timestamp = nowISO();
643
773
  const taskId = createTaskId();
644
774
  const shortId = createShortId(await st.assignedShortIds());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.19-next.16",
3
+ "version": "0.2.19-next.18",
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.16",
45
- "@agent-plan/server": "0.2.19-next.16"
44
+ "@agent-plan/core": "0.2.19-next.18",
45
+ "@agent-plan/server": "0.2.19-next.18"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.json",