@agent-plan/mcp 0.2.22 → 0.2.23

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":";AAo+CA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAwjDA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -4,10 +4,10 @@ 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, packageVersionFromModule, resolvedPackageVersion } 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, contextReadEligibility, hasReadRequirements, 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 }] };
@@ -450,6 +450,8 @@ server.registerTool("planner-feature-show", {
450
450
  if (!resolvedFeature.ok)
451
451
  return text(resolvedFeature.error);
452
452
  const feature = resolvedFeature.feature;
453
+ if (full)
454
+ markFeatureRead(feature.id);
453
455
  const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
454
456
  const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
455
457
  return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
@@ -657,6 +659,8 @@ server.registerTool("planner-phase-show", {
657
659
  const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
658
660
  if (!phase)
659
661
  return text(`Phase not found: ${ref}`);
662
+ if (full)
663
+ markPhaseRead(phase.id, phase.featureId);
660
664
  const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
661
665
  const reqCount = linkedRequirements.length;
662
666
  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"}` : ""})`;
@@ -841,18 +845,44 @@ server.registerTool("planner-task-add", {
841
845
  });
842
846
  server.registerTool("planner-task-show", {
843
847
  description: "Show a task by id or name.",
844
- 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).") },
848
+ 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).") },
845
849
  }, async ({ task: ref, full }) => {
846
850
  const st = await requireStore();
847
851
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
848
852
  if (!found)
849
853
  return text(`Task not found: ${ref}`);
854
+ if (full)
855
+ markTaskRead(found.task.id, found.phase.id, found.phase.featureId);
850
856
  const features = (await st.loadFeatures()).features;
851
857
  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))})`;
852
858
  if (!full)
853
859
  return text(summary);
860
+ const project = await st.loadProject();
861
+ const pendingDeviation = found.task.status === "done" || found.task.status === "canceled" || found.task.status === "rejected"
862
+ ? undefined
863
+ : project.workDeviations
864
+ .filter((deviation) => deviation.resumeTaskId === found.task.id
865
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
866
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
867
+ const snapshot = found.task.pauseSnapshot ?? pendingDeviation?.snapshot ?? null;
854
868
  const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
855
- return text(`${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}`);
869
+ const sections = [summary];
870
+ if (found.task.description?.trim())
871
+ sections.push(found.task.description.trim());
872
+ if (snapshot || pendingDeviation) {
873
+ sections.push([
874
+ pendingDeviation
875
+ ? "Resume advisory: this task has a saved checkpoint that should be evaluated before selecting other work."
876
+ : "Resume checkpoint:",
877
+ snapshot ? `Checkpoint reason: ${snapshot.reason}` : "",
878
+ snapshot ? `Work checkpoint: ${snapshot.whatWasBeingDone}` : "",
879
+ snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
880
+ snapshot ? `How to resume: ${snapshot.howToResume}` : "",
881
+ ].filter(Boolean).join("\n"));
882
+ }
883
+ if (log)
884
+ sections.push(`Status log:\n${log}`);
885
+ return text(sections.join("\n\n"));
856
886
  });
857
887
  server.registerTool("planner-task-discuss", {
858
888
  description: "Persist task execution notes/checklist.",
@@ -902,8 +932,8 @@ server.registerTool("planner-task-update", {
902
932
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
903
933
  if (!found)
904
934
  return text(`Task not found: ${ref}`);
905
- if (status === "paused") {
906
- return text("Use planner-task-pause or planner-task-switch to enter paused state; a structured resume checkpoint is mandatory.");
935
+ if (status === "in-progress" && status !== found.task.status) {
936
+ return text("Task start/resume transitions require planner-task-start so lifecycle state, checkpoints, and return stacks remain consistent.");
907
937
  }
908
938
  // Validate motivation requirement for status transitions.
909
939
  if (status !== undefined && needsMotivation(found.task.status, status)) {
@@ -1082,12 +1112,12 @@ server.registerTool("planner-task-delete", {
1082
1112
  return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
1083
1113
  });
1084
1114
  server.registerTool("planner-task-recommend", {
1085
- 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.",
1115
+ description: "Return the harness-agnostic recommended task: continue one active task or the current phase, otherwise choose ready work by feature → phase → task priority. Reports dependency, availability, and approved deviation/resume context without starting or blocking work.",
1086
1116
  inputSchema: {},
1087
1117
  }, async () => {
1088
1118
  const st = await requireStore();
1089
- const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
1090
- const result = recommendNextTask(features.features, phases, project.workDeviations);
1119
+ const [features, phases, project, resume] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
1120
+ const result = recommendNextTask(features.features, phases, project.workDeviations, resume?.currentPhaseId);
1091
1121
  if (!result.candidate)
1092
1122
  return text(`No task recommendation: ${result.reason}`, { kind: result.kind, reason: result.reason, activeTaskIds: result.activeCandidates?.map((candidate) => candidate.task.id) ?? [] });
1093
1123
  const { candidate } = result;
@@ -1101,11 +1131,11 @@ server.registerTool("planner-task-deviation", {
1101
1131
  inputSchema: { temporary_task: z.string().min(1), resume_task: z.string().min(1).optional(), reason: z.string().min(1) },
1102
1132
  }, async ({ temporary_task, resume_task, reason }) => {
1103
1133
  const st = await requireStore();
1104
- const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
1134
+ const [features, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
1105
1135
  const temporary = findTaskByRef(phases, features.features, temporary_task);
1106
1136
  if (!temporary)
1107
1137
  return text(`Task not found: ${temporary_task}`);
1108
- const selected = recommendNextTask(features.features, phases, project.workDeviations);
1138
+ const selected = recommendNextTask(features.features, phases, project.workDeviations, focus?.currentPhaseId);
1109
1139
  const resume = resume_task ? findTaskByRef(phases, features.features, resume_task) : selected.candidate;
1110
1140
  if (!resume)
1111
1141
  return text(`No resume task is available. Provide resume_task explicitly. ${selected.reason}`);
@@ -1146,7 +1176,8 @@ server.registerTool("planner-task-pause", {
1146
1176
  resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
1147
1177
  pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
1148
1178
  };
1149
- const paused = await st.pauseTask(found.phase.id, found.task.id, snapshot);
1179
+ invalidateReads();
1180
+ const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
1150
1181
  await st.syncTaskStatusRollup(found.phase.id);
1151
1182
  const activeDeviation = (await st.loadProject()).workDeviations
1152
1183
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
@@ -1161,12 +1192,12 @@ server.registerTool("planner-task-pause", {
1161
1192
  ? `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`
1162
1193
  : "";
1163
1194
  return writeAndSummarize(st, [
1164
- `⏸️ Task paused: ${taskCompositeRef(paused, found.phase, features)} — ${paused.title}`,
1195
+ `💾 Resume checkpoint saved: ${taskCompositeRef(checkpointed, found.phase, features)} — ${checkpointed.title}`,
1165
1196
  `Why: ${snapshot.reason}`,
1166
1197
  `Checkpoint: ${snapshot.whatWasBeingDone}`,
1167
1198
  `Resume from: ${snapshot.resumeLocation}`,
1168
1199
  `How to resume: ${snapshot.howToResume}${resumeNotice}`,
1169
- ].join("\n"), { task: paused, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
1200
+ ].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
1170
1201
  });
1171
1202
  server.registerTool("planner-task-switch", {
1172
1203
  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.",
@@ -1181,7 +1212,7 @@ server.registerTool("planner-task-switch", {
1181
1212
  },
1182
1213
  }, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
1183
1214
  const st = await requireStore();
1184
- const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
1215
+ const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
1185
1216
  const features = featuresDoc.features;
1186
1217
  const source = findTaskByRef(phases, features, from_task);
1187
1218
  const target = findTaskByRef(phases, features, to_task);
@@ -1191,8 +1222,18 @@ server.registerTool("planner-task-switch", {
1191
1222
  return text(`Task not found: ${to_task}`);
1192
1223
  if (source.task.id === target.task.id)
1193
1224
  return text("Source and temporary task must differ.");
1194
- if (source.task.status !== "in-progress" && source.task.status !== "paused") {
1195
- return text(`Task switch denied: source is ${source.task.status}; only in-progress or pending-resume paused work can be switched.`);
1225
+ if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
1226
+ return text(`Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.`);
1227
+ }
1228
+ const contextEligibility = contextReadEligibility(target.task.id, target.phase.id, target.phase.featureId);
1229
+ if (!contextEligibility.eligible)
1230
+ return text(`Task switch denied: context reads required. ${contextEligibility.reason}`, contextEligibility);
1231
+ const linkedRequirementIds = [
1232
+ ...(await st.linkedRequirementsForPhase(target.phase.id)).map((requirement) => requirement.id),
1233
+ ...(target.phase.featureId ? (await st.linkedRequirementsForFeature(target.phase.featureId)).map((requirement) => requirement.id) : []),
1234
+ ];
1235
+ if (!hasReadRequirements(linkedRequirementIds)) {
1236
+ return text("Task switch denied: read the requirements linked to the target phase and feature before switching.", { requirementIds: linkedRequirementIds });
1196
1237
  }
1197
1238
  const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
1198
1239
  if (!eligibility.eligible)
@@ -1203,7 +1244,7 @@ server.registerTool("planner-task-switch", {
1203
1244
  resumeLocation: resume_location, howToResume: how_to_resume,
1204
1245
  relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: switched_by?.trim() ?? "",
1205
1246
  };
1206
- const selection = recommendNextTask(features, phases, project.workDeviations);
1247
+ const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
1207
1248
  const record = {
1208
1249
  id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
1209
1250
  temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason, snapshot,
@@ -1222,7 +1263,7 @@ server.registerTool("planner-task-switch", {
1222
1263
  await st.updatePhase(source.phase.id, (phase) => {
1223
1264
  const task = phase.tasks.find((entry) => entry.id === source.task.id);
1224
1265
  if (!task)
1225
- throw new Error(`Paused source task disappeared: ${from_task}`);
1266
+ throw new Error(`Checkpointed source task disappeared: ${from_task}`);
1226
1267
  task.pauseSnapshot = snapshot;
1227
1268
  task.pauseHistory = [...task.pauseHistory, snapshot];
1228
1269
  task.updatedAt = timestamp;
@@ -1230,7 +1271,7 @@ server.registerTool("planner-task-switch", {
1230
1271
  });
1231
1272
  }
1232
1273
  sourceCheckpointed = true;
1233
- if (target.task.status === "paused") {
1274
+ if (target.task.pauseSnapshot) {
1234
1275
  await st.resumeTask(target.phase.id, target.task.id, timestamp);
1235
1276
  }
1236
1277
  else {
@@ -1252,7 +1293,7 @@ server.registerTool("planner-task-switch", {
1252
1293
  }
1253
1294
  targetStarted = true;
1254
1295
  await st.addWorkDeviation(record);
1255
- if (target.task.status === "paused") {
1296
+ if (target.task.pauseSnapshot) {
1256
1297
  const resumedDeviation = project.workDeviations
1257
1298
  .filter((deviation) => deviation.resumeTaskId === target.task.id
1258
1299
  && (deviation.state === "resume-required" || deviation.state === "resolved"))
@@ -1282,15 +1323,16 @@ server.registerTool("planner-task-switch", {
1282
1323
  await st.syncTaskStatusRollup(source.phase.id);
1283
1324
  if (target.phase.id !== source.phase.id)
1284
1325
  await st.syncTaskStatusRollup(target.phase.id);
1326
+ invalidateReads();
1285
1327
  return writeAndSummarize(st, [
1286
1328
  `🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
1287
- `Paused snapshot: ${snapshot.whatWasBeingDone}`,
1329
+ `Resume checkpoint: ${snapshot.whatWasBeingDone}`,
1288
1330
  `Return target: ${taskCompositeRef(source.task, source.phase, features)} from ${snapshot.resumeLocation}`,
1289
1331
  `Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.`,
1290
1332
  ].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
1291
1333
  });
1292
1334
  server.registerTool("planner-task-start", {
1293
- 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.",
1335
+ description: "Set a task to in-progress or resume checkpointed work. BEFORE calling, use planner-task-show(full=true), then planner-phase-show(full=true), then planner-feature-show(full=true) for the same lineage, in that exact order. Read linked requirements with planner-requirement-list when present. A different active task must first be suspended through planner-task-switch.",
1294
1336
  inputSchema: { task: z.string().min(1) },
1295
1337
  }, async ({ task: ref }) => {
1296
1338
  const st = await requireStore();
@@ -1302,12 +1344,21 @@ server.registerTool("planner-task-start", {
1302
1344
  return text(`Task ${found.task.id} is already in-progress.`);
1303
1345
  if (found.task.status === "done")
1304
1346
  return text(`Task ${found.task.id} is done. Use planner-task-update to reopen.`);
1305
- const project = await st.loadProject();
1306
- const phases = await st.loadAllPhases();
1347
+ const contextEligibility = contextReadEligibility(found.task.id, found.phase.id, found.phase.featureId);
1348
+ if (!contextEligibility.eligible)
1349
+ return text(`Task start denied: context reads required. ${contextEligibility.reason}`, contextEligibility);
1350
+ const linkedRequirementIds = [
1351
+ ...(await st.linkedRequirementsForPhase(found.phase.id)).map((requirement) => requirement.id),
1352
+ ...(found.phase.featureId ? (await st.linkedRequirementsForFeature(found.phase.featureId)).map((requirement) => requirement.id) : []),
1353
+ ];
1354
+ if (!hasReadRequirements(linkedRequirementIds)) {
1355
+ return text("Task start denied: read the requirements linked to the task's phase and feature first.", { requirementIds: linkedRequirementIds });
1356
+ }
1357
+ const [project, phases, focus] = await Promise.all([st.loadProject(), st.loadAllPhases(), st.loadResume()]);
1307
1358
  const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
1308
1359
  if (!eligibility.eligible)
1309
1360
  return text(`Task start denied: ${eligibility.reason}`, { reason: eligibility.reason });
1310
- const selection = recommendNextTask(features, phases, project.workDeviations);
1361
+ const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
1311
1362
  if (selection.kind === "conflict") {
1312
1363
  return text(`${selection.reason} Pause/reconcile active tasks before starting another task.`, {
1313
1364
  activeTaskIds: selection.activeCandidates?.map((candidate) => candidate.task.id) ?? [],
@@ -1316,8 +1367,26 @@ server.registerTool("planner-task-start", {
1316
1367
  if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
1317
1368
  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)}.`);
1318
1369
  }
1319
- if (selection.kind === "resume" && selection.candidate?.task.id !== found.task.id) {
1320
- 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.`);
1370
+ let resumeProposal;
1371
+ if (selection.kind === "resume" && selection.candidate && selection.candidate.task.id !== found.task.id) {
1372
+ // Advisory only: a pending resume must be surfaced loudly, but it must
1373
+ // not hard-block an explicit start of a different task. Capture a
1374
+ // structured proposal so the resume-first guidance is explicit and
1375
+ // machine-readable for the host/agent.
1376
+ const resumeTask = selection.candidate;
1377
+ const resumeSnapshot = resumeTask.task.pauseSnapshot
1378
+ ?? project.workDeviations.find((deviation) => deviation.resumeTaskId === resumeTask.task.id
1379
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))?.snapshot
1380
+ ?? null;
1381
+ resumeProposal = buildResumeRequiredProposal({
1382
+ ref: taskCompositeRef(resumeTask.task, resumeTask.phase, features),
1383
+ title: resumeTask.task.title,
1384
+ taskId: resumeTask.task.id,
1385
+ phaseId: resumeTask.phase.id,
1386
+ snapshot: resumeSnapshot
1387
+ ? { reason: resumeSnapshot.reason, resumeLocation: resumeSnapshot.resumeLocation, howToResume: resumeSnapshot.howToResume }
1388
+ : null,
1389
+ });
1321
1390
  }
1322
1391
  // Assemble the mandatory parent context before changing task lifecycle state.
1323
1392
  const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
@@ -1330,11 +1399,17 @@ server.registerTool("planner-task-start", {
1330
1399
  // archived only when the phase completes (auto) or the user explicitly
1331
1400
  // clears it.
1332
1401
  const advisory = selection.candidate && selection.candidate.task.id !== found.task.id
1333
- ? `\n\n⚠️ ${selection.kind === "resume" ? "Resume" : "Priority"} advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
1402
+ ? selection.kind === "resume"
1403
+ ? `\n\n${(() => {
1404
+ if (resumeProposal)
1405
+ return resumeProposal.text;
1406
+ 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.`;
1407
+ })()}`
1408
+ : `\n\n⚠️ Priority advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
1334
1409
  : "";
1335
1410
  const timestamp = nowISO();
1336
1411
  let updatedTask;
1337
- if (found.task.status === "paused") {
1412
+ if (found.task.pauseSnapshot) {
1338
1413
  updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp);
1339
1414
  }
1340
1415
  else {
@@ -1370,7 +1445,9 @@ server.registerTool("planner-task-start", {
1370
1445
  if (resumedDeviation)
1371
1446
  await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
1372
1447
  const t = updatedTask ?? found.task;
1373
- return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}`);
1448
+ return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}`, {
1449
+ ...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
1450
+ });
1374
1451
  });
1375
1452
  server.registerTool("planner-task-complete", {
1376
1453
  description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
@@ -1384,8 +1461,8 @@ server.registerTool("planner-task-complete", {
1384
1461
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
1385
1462
  if (!found)
1386
1463
  return text(`Task not found: ${ref}`);
1387
- if (found.task.status === "paused")
1388
- return text("Task completion denied: resume paused work with planner-task-start before completing it, or cancel it explicitly with motivation.");
1464
+ if (found.task.pauseSnapshot)
1465
+ return text("Task completion denied: resume checkpointed work with planner-task-start before completing it, or cancel it explicitly with motivation.");
1389
1466
  const unchecked = found.task.checklist.filter((item) => !item.checked);
1390
1467
  if (unchecked.length > 0 && !force)
1391
1468
  return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
@@ -1432,7 +1509,7 @@ server.registerTool("planner-task-complete", {
1432
1509
  resumeNotice = [
1433
1510
  "",
1434
1511
  `↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`,
1435
- snapshot ? `Paused because: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
1512
+ snapshot ? `Checkpoint reason: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
1436
1513
  snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
1437
1514
  snapshot ? `How to resume: ${snapshot.howToResume}` : "",
1438
1515
  `Next action: planner-task-start ${taskCompositeRef(resume.task, resume.phase, features)}`,
@@ -1465,6 +1542,15 @@ server.registerTool("planner-handoff-show", {
1465
1542
  return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, empty: true });
1466
1543
  return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
1467
1544
  });
1545
+ server.registerTool("planner-requirement-list", {
1546
+ 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).",
1547
+ inputSchema: {},
1548
+ }, async () => {
1549
+ const st = await requireStore();
1550
+ const requirements = await st.loadRequirements();
1551
+ requirements.requirements.forEach((req) => markRequirementRead(req.id));
1552
+ return text(requirements.requirements.map((req) => `- ${req.id} — ${req.title} (${req.status})`).join("\n") || "No requirements", { requirements });
1553
+ });
1468
1554
  server.registerTool("planner-handoff-write", {
1469
1555
  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.",
1470
1556
  inputSchema: {
@@ -1478,8 +1564,10 @@ server.registerTool("planner-handoff-write", {
1478
1564
  let body = content.trim();
1479
1565
  const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
1480
1566
  const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
1481
- const isGeneric = !firstHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(firstHeadingText);
1482
- if (!title && isGeneric) {
1567
+ const genericTitlePattern = /^(handoff|canonical handoff|session handoff)$/i;
1568
+ const effectiveHeadingText = title?.trim() ? title.trim() : firstHeadingText;
1569
+ const isGeneric = !effectiveHeadingText || genericTitlePattern.test(effectiveHeadingText);
1570
+ if (isGeneric) {
1483
1571
  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.");
1484
1572
  }
1485
1573
  if (title && title.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
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.22",
47
- "@agent-plan/server": "0.2.22"
46
+ "@agent-plan/core": "0.2.23",
47
+ "@agent-plan/server": "0.2.23"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",