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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA+pCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA2rCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ 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, findTaskByRef, buildRecap, migrateToGlobalSequence, addChecklistItem, removeChecklistItem, toggleChecklistItem } from "@agent-plan/core";
7
+ import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, migrateToGlobalSequence, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock } from "@agent-plan/core";
8
8
  import { serve } from "@agent-plan/server";
9
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"];
@@ -560,30 +560,26 @@ server.registerTool("planner-feature-delete", {
560
560
  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) });
561
561
  });
562
562
  server.registerTool("planner-phase-add", {
563
- 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.",
563
+ 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.",
564
564
  inputSchema: {
565
565
  title: z.string().min(1),
566
- feature: z.string().optional().describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
566
+ feature: z.string().min(1).optional().describe("Feature ref (required). Accepts F00x/P00x/T00x composite, bare F00x (global), 5-char shortId, UUID, or title."),
567
567
  summary: z.string().optional().describe("One-line summary of the phase"),
568
568
  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."),
569
569
  },
570
570
  }, async ({ title, feature: featureRef, summary, description }) => {
571
571
  const st = await requireStore();
572
+ if (!featureRef?.trim())
573
+ return text("feature is required: a phase must belong to a feature.");
572
574
  const featuresDoc = await st.loadFeatures();
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
- }
586
- if (featureRef && !feature)
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
+ const feature = resolvedFeature.feature;
582
+ if (!feature)
587
583
  return text(`Resolved feature ${featureRef} no longer exists. Refusing to create phase.`);
588
584
  const lockKey = feature?.id ?? "__unscoped__";
589
585
  let phase;
@@ -753,16 +749,31 @@ server.registerTool("planner-phase-delete", {
753
749
  server.registerTool("planner-task-add", {
754
750
  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.",
755
751
  inputSchema: {
752
+ 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."),
756
753
  phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
757
754
  title: z.string().min(1),
758
755
  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."),
759
756
  checklist: z.array(z.string()).optional(),
760
757
  },
761
- }, async ({ phase: ref, title, description, checklist }) => {
758
+ }, async ({ feature: featureRef, phase: ref, title, description, checklist }) => {
762
759
  const st = await requireStore();
763
- const found = findPhaseByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
760
+ const features = (await st.loadFeatures()).features;
761
+ if (!featureRef?.trim())
762
+ return text("feature is required: a task must belong to a feature.");
763
+ const resolvedFeature = resolveFeatureRefStrict(features, featureRef);
764
+ if (!resolvedFeature.ok)
765
+ return text(resolvedFeature.error);
766
+ const featureValidation = await validateResolvedTarget("feature", resolvedFeature.feature.id, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === resolvedFeature.feature.id)).catch(() => undefined));
767
+ if (!featureValidation.ok)
768
+ return text(featureValidation.error);
769
+ const feature = resolvedFeature.feature;
770
+ if (!feature)
771
+ return text(`Resolved feature ${featureRef} no longer exists. Refusing to create task.`);
772
+ const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
764
773
  if (!found)
765
774
  return text(`Phase not found: ${ref}`);
775
+ if (found.featureId !== feature.id)
776
+ return text(`Phase ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} does not belong to feature ${formatFeatureRef(feature.number)}. Refusing to create task.`);
766
777
  const phaseValidation = await validateResolvedTarget("phase", found.id, () => st.loadPhase(found.id).catch(() => undefined));
767
778
  if (!phaseValidation.ok)
768
779
  return text(phaseValidation.error);
@@ -801,8 +812,8 @@ server.registerTool("planner-task-add", {
801
812
  entry.updatedAt = timestamp;
802
813
  return entry;
803
814
  });
804
- const features = (await st.loadFeatures()).features;
805
- return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, features)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
815
+ const finalFeatures = (await st.loadFeatures()).features;
816
+ return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, finalFeatures)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
806
817
  });
807
818
  server.registerTool("planner-task-show", {
808
819
  description: "Show a task by id or name.",
@@ -1043,7 +1054,7 @@ server.registerTool("planner-task-start", {
1043
1054
  let handoffNotice = "";
1044
1055
  if ((found.phase.handoff ?? "") !== "") {
1045
1056
  await st.clearPhaseHandoff(found.phase.id, "task-started").catch(() => { });
1046
- handoffNotice = "📦 Archived handoff for this phase (task started) — recover via planner-handoff-list + .planner/handoff-archive/.\n";
1057
+ handoffNotice = "📦 Archived handoff for this phase (task started) — recover via planner-handoff-list + .planner/.local/handoff-archive/.\n";
1047
1058
  }
1048
1059
  const _otherHandoffs = allHandoffs.filter((h) => h.phaseId !== found.phase.id);
1049
1060
  if (_otherHandoffs.length > 0) {
@@ -1075,7 +1086,9 @@ server.registerTool("planner-task-start", {
1075
1086
  await st.syncTaskStatusRollup(found.phase.id);
1076
1087
  const features = (await st.loadFeatures()).features;
1077
1088
  const t = updatedTask ?? found.task;
1078
- return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}`);
1089
+ const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
1090
+ const phaseContext = buildPhaseContextBlock(found.phase, parentFeature);
1091
+ return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}`);
1079
1092
  });
1080
1093
  server.registerTool("planner-task-complete", {
1081
1094
  description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
@@ -1148,23 +1161,44 @@ server.registerTool("planner-handoff-show", {
1148
1161
  return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
1149
1162
  });
1150
1163
  server.registerTool("planner-handoff-write", {
1151
- 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.",
1164
+ description: "Write/refresh the entity-scoped handoff on a phase. Pass content (markdown) AND a meaningful `title` summarizing the work (e.g. 'P049 — featureId validation: tests + adapter wiring'). The title becomes the H1 / first line shown in handoff lists; generic titles like 'Handoff' or 'Canonical handoff' are REJECTED. phaseRef optional (default: current in-progress phase). Captures design context for a resuming agent on that phase.",
1152
1165
  inputSchema: {
1153
1166
  phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase."),
1167
+ 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'."),
1154
1168
  content: z.string().min(1).describe("Handoff text (markdown)."),
1155
1169
  },
1156
- }, async ({ phaseRef, content }) => {
1170
+ }, async ({ phaseRef, title, content }) => {
1157
1171
  const st = await requireStore();
1172
+ let body = content.trim();
1173
+ const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1174
+ const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
1175
+ const isGeneric = !firstHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(firstHeadingText);
1176
+ if (!title && isGeneric) {
1177
+ 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.");
1178
+ }
1179
+ if (title && title.trim()) {
1180
+ const t = title.trim();
1181
+ const lines = body.split(/\r?\n/);
1182
+ const firstIdx = lines.findIndex((l) => l.trim().length > 0);
1183
+ if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? "")) {
1184
+ lines[firstIdx] = `# ${t}`;
1185
+ body = lines.join("\n");
1186
+ }
1187
+ else {
1188
+ body = `# ${t}\n\n${body}`;
1189
+ }
1190
+ }
1158
1191
  const r = await resolvePhaseForHandoff(st, phaseRef);
1159
1192
  if (!r.ok)
1160
1193
  return text(`❌ ${r.error}`);
1161
- await st.setPhaseHandoff(r.phase.id, content);
1194
+ await st.setPhaseHandoff(r.phase.id, body);
1162
1195
  return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
1163
1196
  });
1164
1197
  server.registerTool("planner-handoff-prepare", {
1165
1198
  description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write (entity-scoped, phase.handoff).",
1166
1199
  }, async () => text([
1167
1200
  "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).",
1201
+ "Pass a meaningful `title` summarizing the work (e.g. 'P049 — featureId validation: tests + adapter wiring') — it becomes the H1 / first line in handoff lists. Generic titles like 'Handoff' or 'Canonical handoff' are REJECTED.",
1168
1202
  "Required sections: Created at, Updated at, Reason, Current focus, What was being done, How to resume, Files touched, Blockers, Next steps, Recent decisions, Reminder.",
1169
1203
  "Stored on the phase.handoff field (entity-scoped). .planner/HANDOFF.md is deprecated.",
1170
1204
  ].join("\n")));
@@ -1177,7 +1211,7 @@ server.registerTool("planner-handoff-clear", {
1177
1211
  if (!r.ok)
1178
1212
  return text(`❌ ${r.error}`);
1179
1213
  await st.clearPhaseHandoff(r.phase.id, "manual");
1180
- return text(`✅ Cleared handoff on ${r.compositeRef} (handoffUpdatedAt preserved as audit; archived to .planner/handoff-archive/).`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
1214
+ return text(`✅ Cleared handoff on ${r.compositeRef} (handoffUpdatedAt preserved as audit; archived to .planner/.local/handoff-archive/).`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
1181
1215
  });
1182
1216
  server.registerTool("planner-web", {
1183
1217
  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.18",
3
+ "version": "0.2.19-next.19",
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.18",
45
- "@agent-plan/server": "0.2.19-next.18"
46
+ "@agent-plan/core": "0.2.19-next.19",
47
+ "@agent-plan/server": "0.2.19-next.19"
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
+ }