@agent-plan/mcp 0.2.21 → 0.2.22-next.1

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.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  MCP server for Agent Plan.
4
4
 
5
- Exposes `planner-*` tools over stdio for hosts such as Claude Code.
5
+ Exposes `planner-*` tools over stdio for hosts such as Claude Code and Codex.
6
+
7
+ Use `planner-version` (or `/planner version` in the Claude plugin) to report the `@agent-plan/mcp` and `@agent-plan/core` versions actually loaded by the server process. The tool works without an initialized `.planner/` workspace.
6
8
 
7
9
  See the repository root README for full documentation:
8
10
  https://github.com/ovidius72/agent-planner#readme
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAm9CA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAmjDA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -4,14 +4,16 @@ 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, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask } from "@agent-plan/core";
7
+ import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureRead, markPhaseRead, markTaskRead, parentReadAdvisory, requirementReadAdvisory, markRequirementRead, invalidateReads } from "@agent-plan/core";
8
8
  import { serve } from "@agent-plan/server";
9
9
  import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, validateResolvedTarget } from "@agent-plan/core/naming";
10
- const STATUS_VALUES = ["planned", "in-progress", "paused", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
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) {
13
13
  return structuredContent ? { content: [{ type: "text", text: textValue }], structuredContent } : { content: [{ type: "text", text: textValue }] };
14
14
  }
15
+ const MCP_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/mcp");
16
+ const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
15
17
  function nowISO() {
16
18
  return new Date().toISOString();
17
19
  }
@@ -123,7 +125,7 @@ async function writeAndSummarize(st, message, structuredContent) {
123
125
  }
124
126
  const server = new McpServer({
125
127
  name: "agent-plan-planner",
126
- version: "0.1.0",
128
+ version: MCP_PACKAGE.version,
127
129
  });
128
130
  async function resolvePhaseForHandoff(st, ref) {
129
131
  const phases = await st.loadAllPhases();
@@ -143,6 +145,19 @@ async function resolvePhaseForHandoff(st, ref) {
143
145
  const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
144
146
  return { ok: true, phase, compositeRef: formatPhaseRef(phase.number, feat?.number) };
145
147
  }
148
+ server.registerTool("planner-version", {
149
+ description: "Report the versions of the Agent Plan MCP server and core package actually loaded by this process. Available without an initialized .planner/ workspace.",
150
+ }, async () => {
151
+ const versions = {
152
+ [MCP_PACKAGE.name]: MCP_PACKAGE.version,
153
+ [CORE_PACKAGE.name]: CORE_PACKAGE.version,
154
+ };
155
+ return text([
156
+ "Agent Plan runtime versions",
157
+ `${MCP_PACKAGE.name}: ${MCP_PACKAGE.version}`,
158
+ `${CORE_PACKAGE.name}: ${CORE_PACKAGE.version}`,
159
+ ].join("\n"), { versions });
160
+ });
146
161
  server.registerTool("planner-export", {
147
162
  description: "Export the project plan as a Markdown report. Supports a concise summary or full hierarchical detail.",
148
163
  inputSchema: {
@@ -435,6 +450,7 @@ server.registerTool("planner-feature-show", {
435
450
  if (!resolvedFeature.ok)
436
451
  return text(resolvedFeature.error);
437
452
  const feature = resolvedFeature.feature;
453
+ markFeatureRead(feature.id);
438
454
  const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
439
455
  const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
440
456
  return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
@@ -642,6 +658,7 @@ server.registerTool("planner-phase-show", {
642
658
  const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
643
659
  if (!phase)
644
660
  return text(`Phase not found: ${ref}`);
661
+ markPhaseRead(phase.id, phase.featureId);
645
662
  const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
646
663
  const reqCount = linkedRequirements.length;
647
664
  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"}` : ""})`;
@@ -826,18 +843,43 @@ server.registerTool("planner-task-add", {
826
843
  });
827
844
  server.registerTool("planner-task-show", {
828
845
  description: "Show a task by id or name.",
829
- 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).") },
846
+ 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, resume checkpoint/advisory, and statusLog. Default: compact identity only (saves tokens).") },
830
847
  }, async ({ task: ref, full }) => {
831
848
  const st = await requireStore();
832
849
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
833
850
  if (!found)
834
851
  return text(`Task not found: ${ref}`);
852
+ markTaskRead(found.task.id, found.phase.id, found.phase.featureId);
835
853
  const features = (await st.loadFeatures()).features;
836
854
  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))})`;
837
855
  if (!full)
838
856
  return text(summary);
857
+ const project = await st.loadProject();
858
+ const pendingDeviation = found.task.status === "done" || found.task.status === "canceled" || found.task.status === "rejected"
859
+ ? undefined
860
+ : project.workDeviations
861
+ .filter((deviation) => deviation.resumeTaskId === found.task.id
862
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
863
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
864
+ const snapshot = found.task.pauseSnapshot ?? pendingDeviation?.snapshot ?? null;
839
865
  const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
840
- return text(`${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}`);
866
+ const sections = [summary];
867
+ if (found.task.description?.trim())
868
+ sections.push(found.task.description.trim());
869
+ if (snapshot || pendingDeviation) {
870
+ sections.push([
871
+ pendingDeviation
872
+ ? "Resume advisory: this task has a saved checkpoint that should be evaluated before selecting other work."
873
+ : "Resume checkpoint:",
874
+ snapshot ? `Checkpoint reason: ${snapshot.reason}` : "",
875
+ snapshot ? `Work checkpoint: ${snapshot.whatWasBeingDone}` : "",
876
+ snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
877
+ snapshot ? `How to resume: ${snapshot.howToResume}` : "",
878
+ ].filter(Boolean).join("\n"));
879
+ }
880
+ if (log)
881
+ sections.push(`Status log:\n${log}`);
882
+ return text(sections.join("\n\n"));
841
883
  });
842
884
  server.registerTool("planner-task-discuss", {
843
885
  description: "Persist task execution notes/checklist.",
@@ -887,8 +929,8 @@ server.registerTool("planner-task-update", {
887
929
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
888
930
  if (!found)
889
931
  return text(`Task not found: ${ref}`);
890
- if (status === "paused") {
891
- return text("Use planner-task-pause or planner-task-switch to enter paused state; a structured resume checkpoint is mandatory.");
932
+ if (found.task.pauseSnapshot && status === "in-progress" && status !== found.task.status) {
933
+ return text("Resume checkpoint lifecycle transitions require planner-task-start so the checkpoint and return stack remain consistent.");
892
934
  }
893
935
  // Validate motivation requirement for status transitions.
894
936
  if (status !== undefined && needsMotivation(found.task.status, status)) {
@@ -1131,7 +1173,8 @@ server.registerTool("planner-task-pause", {
1131
1173
  resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
1132
1174
  pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
1133
1175
  };
1134
- const paused = await st.pauseTask(found.phase.id, found.task.id, snapshot);
1176
+ invalidateReads();
1177
+ const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
1135
1178
  await st.syncTaskStatusRollup(found.phase.id);
1136
1179
  const activeDeviation = (await st.loadProject()).workDeviations
1137
1180
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
@@ -1146,12 +1189,12 @@ server.registerTool("planner-task-pause", {
1146
1189
  ? `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`
1147
1190
  : "";
1148
1191
  return writeAndSummarize(st, [
1149
- `⏸️ Task paused: ${taskCompositeRef(paused, found.phase, features)} — ${paused.title}`,
1192
+ `💾 Resume checkpoint saved: ${taskCompositeRef(checkpointed, found.phase, features)} — ${checkpointed.title}`,
1150
1193
  `Why: ${snapshot.reason}`,
1151
1194
  `Checkpoint: ${snapshot.whatWasBeingDone}`,
1152
1195
  `Resume from: ${snapshot.resumeLocation}`,
1153
1196
  `How to resume: ${snapshot.howToResume}${resumeNotice}`,
1154
- ].join("\n"), { task: paused, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
1197
+ ].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
1155
1198
  });
1156
1199
  server.registerTool("planner-task-switch", {
1157
1200
  description: "Atomically switch from active work to a temporary task, even outside normal priority order. Pauses the source with a mandatory snapshot, starts the target, and pushes a durable LIFO return target.",
@@ -1176,8 +1219,8 @@ server.registerTool("planner-task-switch", {
1176
1219
  return text(`Task not found: ${to_task}`);
1177
1220
  if (source.task.id === target.task.id)
1178
1221
  return text("Source and temporary task must differ.");
1179
- if (source.task.status !== "in-progress" && source.task.status !== "paused") {
1180
- return text(`Task switch denied: source is ${source.task.status}; only in-progress or pending-resume paused work can be switched.`);
1222
+ if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
1223
+ return text(`Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.`);
1181
1224
  }
1182
1225
  const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
1183
1226
  if (!eligibility.eligible)
@@ -1207,7 +1250,7 @@ server.registerTool("planner-task-switch", {
1207
1250
  await st.updatePhase(source.phase.id, (phase) => {
1208
1251
  const task = phase.tasks.find((entry) => entry.id === source.task.id);
1209
1252
  if (!task)
1210
- throw new Error(`Paused source task disappeared: ${from_task}`);
1253
+ throw new Error(`Checkpointed source task disappeared: ${from_task}`);
1211
1254
  task.pauseSnapshot = snapshot;
1212
1255
  task.pauseHistory = [...task.pauseHistory, snapshot];
1213
1256
  task.updatedAt = timestamp;
@@ -1215,7 +1258,7 @@ server.registerTool("planner-task-switch", {
1215
1258
  });
1216
1259
  }
1217
1260
  sourceCheckpointed = true;
1218
- if (target.task.status === "paused") {
1261
+ if (target.task.pauseSnapshot) {
1219
1262
  await st.resumeTask(target.phase.id, target.task.id, timestamp);
1220
1263
  }
1221
1264
  else {
@@ -1237,7 +1280,7 @@ server.registerTool("planner-task-switch", {
1237
1280
  }
1238
1281
  targetStarted = true;
1239
1282
  await st.addWorkDeviation(record);
1240
- if (target.task.status === "paused") {
1283
+ if (target.task.pauseSnapshot) {
1241
1284
  const resumedDeviation = project.workDeviations
1242
1285
  .filter((deviation) => deviation.resumeTaskId === target.task.id
1243
1286
  && (deviation.state === "resume-required" || deviation.state === "resolved"))
@@ -1267,15 +1310,22 @@ server.registerTool("planner-task-switch", {
1267
1310
  await st.syncTaskStatusRollup(source.phase.id);
1268
1311
  if (target.phase.id !== source.phase.id)
1269
1312
  await st.syncTaskStatusRollup(target.phase.id);
1313
+ const targetReadAdvisory = parentReadAdvisory(target.phase.featureId, target.phase.id);
1314
+ const targetRequirementAdvisory = requirementReadAdvisory([
1315
+ ...(await st.linkedRequirementsForPhase(target.phase.id)).map((r) => r.id),
1316
+ ...(target.phase.featureId ? (await st.linkedRequirementsForFeature(target.phase.featureId)).map((r) => r.id) : []),
1317
+ ]);
1270
1318
  return writeAndSummarize(st, [
1271
1319
  `🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
1272
- `Paused snapshot: ${snapshot.whatWasBeingDone}`,
1320
+ `Resume checkpoint: ${snapshot.whatWasBeingDone}`,
1273
1321
  `Return target: ${taskCompositeRef(source.task, source.phase, features)} from ${snapshot.resumeLocation}`,
1274
1322
  `Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.`,
1275
- ].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
1323
+ ...(targetReadAdvisory ? [targetReadAdvisory.trimStart()] : []),
1324
+ ...(targetRequirementAdvisory ? [targetRequirementAdvisory.trimStart()] : []),
1325
+ ].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id, ...(targetReadAdvisory || targetRequirementAdvisory ? { readRequired: true } : {}) });
1276
1326
  });
1277
1327
  server.registerTool("planner-task-start", {
1278
- description: "Set a task to in-progress or resume a paused task. BEFORE calling, use planner-task-show with full=true and planner-phase-show with full=true for the parent phase, including linked requirements. A different active task must be suspended through planner-task-switch first. Otherwise follow active/pending-resume/feature → phase → task priority guidance.",
1328
+ description: "Set a task to in-progress or resume checkpointed work. BEFORE calling, use planner-task-show with full=true and planner-phase-show with full=true for the parent phase, including linked requirements. A different active task must be suspended through planner-task-switch first. Otherwise follow active/pending-resume/feature → phase → task priority guidance.",
1279
1329
  inputSchema: { task: z.string().min(1) },
1280
1330
  }, async ({ task: ref }) => {
1281
1331
  const st = await requireStore();
@@ -1301,8 +1351,26 @@ server.registerTool("planner-task-start", {
1301
1351
  if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
1302
1352
  return text(`Task start denied: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is active. Use planner-task-switch to snapshot and pause it before starting ${taskCompositeRef(found.task, found.phase, features)}.`);
1303
1353
  }
1304
- if (selection.kind === "resume" && selection.candidate?.task.id !== found.task.id) {
1305
- return text(`Task start denied: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is pending resume. Resume it now, or use planner-task-switch from that paused task to ${taskCompositeRef(found.task, found.phase, features)} with a new checkpoint and reason.`);
1354
+ let resumeProposal;
1355
+ if (selection.kind === "resume" && selection.candidate && selection.candidate.task.id !== found.task.id) {
1356
+ // Advisory only: a pending resume must be surfaced loudly, but it must
1357
+ // not hard-block an explicit start of a different task. Capture a
1358
+ // structured proposal so the resume-first guidance is explicit and
1359
+ // machine-readable for the host/agent.
1360
+ const resumeTask = selection.candidate;
1361
+ const resumeSnapshot = resumeTask.task.pauseSnapshot
1362
+ ?? project.workDeviations.find((deviation) => deviation.resumeTaskId === resumeTask.task.id
1363
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))?.snapshot
1364
+ ?? null;
1365
+ resumeProposal = buildResumeRequiredProposal({
1366
+ ref: taskCompositeRef(resumeTask.task, resumeTask.phase, features),
1367
+ title: resumeTask.task.title,
1368
+ taskId: resumeTask.task.id,
1369
+ phaseId: resumeTask.phase.id,
1370
+ snapshot: resumeSnapshot
1371
+ ? { reason: resumeSnapshot.reason, resumeLocation: resumeSnapshot.resumeLocation, howToResume: resumeSnapshot.howToResume }
1372
+ : null,
1373
+ });
1306
1374
  }
1307
1375
  // Assemble the mandatory parent context before changing task lifecycle state.
1308
1376
  const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
@@ -1315,11 +1383,17 @@ server.registerTool("planner-task-start", {
1315
1383
  // archived only when the phase completes (auto) or the user explicitly
1316
1384
  // clears it.
1317
1385
  const advisory = selection.candidate && selection.candidate.task.id !== found.task.id
1318
- ? `\n\n⚠️ ${selection.kind === "resume" ? "Resume" : "Priority"} advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
1386
+ ? selection.kind === "resume"
1387
+ ? `\n\n${(() => {
1388
+ if (resumeProposal)
1389
+ return resumeProposal.text;
1390
+ return `⚠️ Resume advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} has a saved checkpoint. Evaluate its resume context before continuing with this explicit task request. Explicit task request honored.`;
1391
+ })()}`
1392
+ : `\n\n⚠️ Priority advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
1319
1393
  : "";
1320
1394
  const timestamp = nowISO();
1321
1395
  let updatedTask;
1322
- if (found.task.status === "paused") {
1396
+ if (found.task.pauseSnapshot) {
1323
1397
  updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp);
1324
1398
  }
1325
1399
  else {
@@ -1355,7 +1429,15 @@ server.registerTool("planner-task-start", {
1355
1429
  if (resumedDeviation)
1356
1430
  await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
1357
1431
  const t = updatedTask ?? found.task;
1358
- return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}`);
1432
+ const readAdvisory = parentReadAdvisory(parentFeature?.id, found.phase.id);
1433
+ const requirementAdvisory = requirementReadAdvisory([
1434
+ ...(phaseWithRequirements.linkedRequirements ?? []).map((r) => r.id),
1435
+ ...featureRequirements.map((r) => r.id),
1436
+ ]);
1437
+ return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}${readAdvisory}${requirementAdvisory}`, {
1438
+ ...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
1439
+ ...(readAdvisory || requirementAdvisory ? { readRequired: true } : {}),
1440
+ });
1359
1441
  });
1360
1442
  server.registerTool("planner-task-complete", {
1361
1443
  description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
@@ -1369,8 +1451,8 @@ server.registerTool("planner-task-complete", {
1369
1451
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
1370
1452
  if (!found)
1371
1453
  return text(`Task not found: ${ref}`);
1372
- if (found.task.status === "paused")
1373
- return text("Task completion denied: resume paused work with planner-task-start before completing it, or cancel it explicitly with motivation.");
1454
+ if (found.task.pauseSnapshot)
1455
+ return text("Task completion denied: resume checkpointed work with planner-task-start before completing it, or cancel it explicitly with motivation.");
1374
1456
  const unchecked = found.task.checklist.filter((item) => !item.checked);
1375
1457
  if (unchecked.length > 0 && !force)
1376
1458
  return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
@@ -1417,7 +1499,7 @@ server.registerTool("planner-task-complete", {
1417
1499
  resumeNotice = [
1418
1500
  "",
1419
1501
  `↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`,
1420
- snapshot ? `Paused because: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
1502
+ snapshot ? `Checkpoint reason: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
1421
1503
  snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
1422
1504
  snapshot ? `How to resume: ${snapshot.howToResume}` : "",
1423
1505
  `Next action: planner-task-start ${taskCompositeRef(resume.task, resume.phase, features)}`,
@@ -1450,6 +1532,15 @@ server.registerTool("planner-handoff-show", {
1450
1532
  return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, empty: true });
1451
1533
  return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
1452
1534
  });
1535
+ server.registerTool("planner-requirement-list", {
1536
+ description: "List all top-level requirements in requirements.json. Reading this list records the requirements as read for the start/resume read-enforcement (explicit requirement read, required alongside feature/phase reads).",
1537
+ inputSchema: {},
1538
+ }, async () => {
1539
+ const st = await requireStore();
1540
+ const requirements = await st.loadRequirements();
1541
+ requirements.requirements.forEach((req) => markRequirementRead(req.id));
1542
+ return text(requirements.requirements.map((req) => `- ${req.id} — ${req.title} (${req.status})`).join("\n") || "No requirements", { requirements });
1543
+ });
1453
1544
  server.registerTool("planner-handoff-write", {
1454
1545
  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.",
1455
1546
  inputSchema: {
@@ -1463,8 +1554,10 @@ server.registerTool("planner-handoff-write", {
1463
1554
  let body = content.trim();
1464
1555
  const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1465
1556
  const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
1466
- const isGeneric = !firstHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(firstHeadingText);
1467
- if (!title && isGeneric) {
1557
+ const genericTitlePattern = /^(handoff|canonical handoff|session handoff)$/i;
1558
+ const effectiveHeadingText = title?.trim() ? title.trim() : firstHeadingText;
1559
+ const isGeneric = !effectiveHeadingText || genericTitlePattern.test(effectiveHeadingText);
1560
+ if (isGeneric) {
1468
1561
  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.");
1469
1562
  }
1470
1563
  if (title && title.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.21",
3
+ "version": "0.2.22-next.1",
4
4
  "private": false,
5
5
  "description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
6
6
  "license": "MIT",
@@ -43,8 +43,8 @@
43
43
  "dependencies": {
44
44
  "@modelcontextprotocol/sdk": "^1.17.0",
45
45
  "zod": "^3.24.0",
46
- "@agent-plan/core": "0.2.21",
47
- "@agent-plan/server": "0.2.21"
46
+ "@agent-plan/core": "0.2.22-next.1",
47
+ "@agent-plan/server": "0.2.22-next.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",