@agent-plan/mcp 0.2.19 → 0.2.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":";AA4vCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAm9CA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
7
7
  import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask } 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", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
10
+ const STATUS_VALUES = ["planned", "in-progress", "paused", "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 }] };
@@ -403,6 +403,7 @@ server.registerTool("planner-feature-add", {
403
403
  priority,
404
404
  name: name.trim(),
405
405
  description: description?.trim() ?? "",
406
+ descriptionUpdatedAt: timestamp,
406
407
  status: effectiveStatus,
407
408
  discussedAt: "",
408
409
  contextReady: false,
@@ -596,6 +597,7 @@ server.registerTool("planner-phase-add", {
596
597
  contextReadyReason: "",
597
598
  summary: summary?.trim() ?? "",
598
599
  description: description?.trim() ?? "",
600
+ descriptionUpdatedAt: timestamp,
599
601
  notes: "",
600
602
  goals: [],
601
603
  nonGoals: [],
@@ -798,6 +800,7 @@ server.registerTool("planner-task-add", {
798
800
  title: title.trim(),
799
801
  status: "planned",
800
802
  description: description?.trim() ?? "",
803
+ descriptionUpdatedAt: timestamp,
801
804
  notes: "",
802
805
  statusLog: [],
803
806
  decisions: [],
@@ -805,6 +808,8 @@ server.registerTool("planner-task-add", {
805
808
  checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
806
809
  subtasks: [],
807
810
  dependsOn: [],
811
+ pauseSnapshot: null,
812
+ pauseHistory: [],
808
813
  startedAt: "",
809
814
  completedAt: "",
810
815
  createdAt: timestamp,
@@ -882,6 +887,9 @@ server.registerTool("planner-task-update", {
882
887
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
883
888
  if (!found)
884
889
  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.");
892
+ }
885
893
  // Validate motivation requirement for status transitions.
886
894
  if (status !== undefined && needsMotivation(found.task.status, status)) {
887
895
  if (!motivation || !motivation.trim()) {
@@ -922,7 +930,22 @@ server.registerTool("planner-task-update", {
922
930
  await st.syncTaskStatusRollup(found.phase.id);
923
931
  const features = (await st.loadFeatures()).features;
924
932
  const t = updatedTask ?? found.task;
925
- return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
933
+ let resumeNotice = "";
934
+ let resumeRequired;
935
+ if (status && status !== "in-progress" && status !== found.task.status) {
936
+ const deviation = (await st.loadProject()).workDeviations
937
+ .filter((entry) => (entry.state === "approved" || entry.state === "active") && entry.temporaryTaskId === found.task.id)
938
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
939
+ if (deviation) {
940
+ await st.setWorkDeviationState(deviation.id, "resume-required", timestamp);
941
+ const resume = findTaskByRef(await st.loadAllPhases(), features, deviation.resumeTaskId);
942
+ if (resume) {
943
+ resumeNotice = `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`;
944
+ resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot: resume.task.pauseSnapshot ?? deviation.snapshot };
945
+ }
946
+ }
947
+ }
948
+ return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}${resumeNotice}`, resumeRequired ? { resumeRequired } : undefined);
926
949
  });
927
950
  server.registerTool("planner-task-checklist-toggle", {
928
951
  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.",
@@ -1076,12 +1099,183 @@ server.registerTool("planner-task-deviation", {
1076
1099
  if (temporary.task.status !== "planned")
1077
1100
  return text(`Temporary task is not startable: ${temporary.task.status}.`);
1078
1101
  const timestamp = nowISO();
1079
- 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: "" };
1102
+ const record = {
1103
+ id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id,
1104
+ temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason, snapshot: null,
1105
+ requestedBy: "user", approvedBy: "user", state: "approved",
1106
+ createdAt: timestamp, activatedAt: "", resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
1107
+ };
1080
1108
  await st.addWorkDeviation(record);
1081
1109
  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 });
1082
1110
  });
1111
+ server.registerTool("planner-task-pause", {
1112
+ description: "Pause an in-progress task with a mandatory durable checkpoint describing why work stopped, what was underway, the exact resume location, and how to continue.",
1113
+ inputSchema: {
1114
+ task: z.string().min(1),
1115
+ reason: z.string().min(1),
1116
+ what_was_being_done: z.string().min(1),
1117
+ resume_location: z.string().min(1),
1118
+ how_to_resume: z.string().min(1),
1119
+ paused_by: z.string().optional(),
1120
+ },
1121
+ }, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }) => {
1122
+ const st = await requireStore();
1123
+ const features = (await st.loadFeatures()).features;
1124
+ const found = findTaskByRef(await st.loadAllPhases(), features, ref);
1125
+ if (!found)
1126
+ return text(`Task not found: ${ref}`);
1127
+ if (found.task.status !== "in-progress")
1128
+ return text(`Task pause denied: ${taskCompositeRef(found.task, found.phase, features)} is ${found.task.status}, not in-progress.`);
1129
+ const snapshot = {
1130
+ id: crypto.randomUUID(), reason, whatWasBeingDone: what_was_being_done,
1131
+ resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
1132
+ pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
1133
+ };
1134
+ const paused = await st.pauseTask(found.phase.id, found.task.id, snapshot);
1135
+ await st.syncTaskStatusRollup(found.phase.id);
1136
+ const activeDeviation = (await st.loadProject()).workDeviations
1137
+ .filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
1138
+ && deviation.temporaryTaskId === found.task.id)
1139
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
1140
+ if (activeDeviation)
1141
+ await st.setWorkDeviationState(activeDeviation.id, "resume-required", snapshot.pausedAt);
1142
+ const resume = activeDeviation
1143
+ ? findTaskByRef(await st.loadAllPhases(), features, activeDeviation.resumeTaskId)
1144
+ : undefined;
1145
+ const resumeNotice = resume
1146
+ ? `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`
1147
+ : "";
1148
+ return writeAndSummarize(st, [
1149
+ `⏸️ Task paused: ${taskCompositeRef(paused, found.phase, features)} — ${paused.title}`,
1150
+ `Why: ${snapshot.reason}`,
1151
+ `Checkpoint: ${snapshot.whatWasBeingDone}`,
1152
+ `Resume from: ${snapshot.resumeLocation}`,
1153
+ `How to resume: ${snapshot.howToResume}${resumeNotice}`,
1154
+ ].join("\n"), { task: paused, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
1155
+ });
1156
+ server.registerTool("planner-task-switch", {
1157
+ 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.",
1158
+ inputSchema: {
1159
+ from_task: z.string().min(1),
1160
+ to_task: z.string().min(1),
1161
+ reason: z.string().min(1),
1162
+ what_was_being_done: z.string().min(1),
1163
+ resume_location: z.string().min(1),
1164
+ how_to_resume: z.string().min(1),
1165
+ switched_by: z.string().optional(),
1166
+ },
1167
+ }, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
1168
+ const st = await requireStore();
1169
+ const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
1170
+ const features = featuresDoc.features;
1171
+ const source = findTaskByRef(phases, features, from_task);
1172
+ const target = findTaskByRef(phases, features, to_task);
1173
+ if (!source)
1174
+ return text(`Task not found: ${from_task}`);
1175
+ if (!target)
1176
+ return text(`Task not found: ${to_task}`);
1177
+ if (source.task.id === target.task.id)
1178
+ 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.`);
1181
+ }
1182
+ const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
1183
+ if (!eligibility.eligible)
1184
+ return text(`Task switch denied: ${eligibility.reason}`);
1185
+ const timestamp = nowISO();
1186
+ const snapshot = {
1187
+ id: crypto.randomUUID(), reason, whatWasBeingDone: what_was_being_done,
1188
+ resumeLocation: resume_location, howToResume: how_to_resume,
1189
+ relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: switched_by?.trim() ?? "",
1190
+ };
1191
+ const selection = recommendNextTask(features, phases, project.workDeviations);
1192
+ const record = {
1193
+ id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
1194
+ temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason, snapshot,
1195
+ requestedBy: "agent", approvedBy: switched_by?.trim() || "explicit task_switch",
1196
+ state: "active", createdAt: timestamp, activatedAt: timestamp,
1197
+ resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
1198
+ };
1199
+ const sourceWasActive = source.task.status === "in-progress";
1200
+ let sourceCheckpointed = false;
1201
+ let targetStarted = false;
1202
+ try {
1203
+ if (sourceWasActive) {
1204
+ await st.pauseTask(source.phase.id, source.task.id, snapshot);
1205
+ }
1206
+ else {
1207
+ await st.updatePhase(source.phase.id, (phase) => {
1208
+ const task = phase.tasks.find((entry) => entry.id === source.task.id);
1209
+ if (!task)
1210
+ throw new Error(`Paused source task disappeared: ${from_task}`);
1211
+ task.pauseSnapshot = snapshot;
1212
+ task.pauseHistory = [...task.pauseHistory, snapshot];
1213
+ task.updatedAt = timestamp;
1214
+ return phase;
1215
+ });
1216
+ }
1217
+ sourceCheckpointed = true;
1218
+ if (target.task.status === "paused") {
1219
+ await st.resumeTask(target.phase.id, target.task.id, timestamp);
1220
+ }
1221
+ else {
1222
+ await st.updatePhase(target.phase.id, (phase) => {
1223
+ const task = phase.tasks.find((entry) => entry.id === target.task.id);
1224
+ if (!task)
1225
+ throw new Error(`Temporary task disappeared: ${to_task}`);
1226
+ const previousStatus = task.status;
1227
+ applyTaskLifecycleDates(task, "in-progress", timestamp);
1228
+ task.statusLog = [...task.statusLog, {
1229
+ id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
1230
+ date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
1231
+ title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
1232
+ }];
1233
+ task.updatedAt = timestamp;
1234
+ phase.updatedAt = timestamp;
1235
+ return phase;
1236
+ });
1237
+ }
1238
+ targetStarted = true;
1239
+ await st.addWorkDeviation(record);
1240
+ if (target.task.status === "paused") {
1241
+ const resumedDeviation = project.workDeviations
1242
+ .filter((deviation) => deviation.resumeTaskId === target.task.id
1243
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
1244
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
1245
+ if (resumedDeviation)
1246
+ await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
1247
+ }
1248
+ }
1249
+ catch (error) {
1250
+ if (targetStarted) {
1251
+ await st.updatePhase(target.phase.id, (phase) => {
1252
+ phase.tasks = phase.tasks.map((task) => task.id === target.task.id ? target.task : task);
1253
+ return phase;
1254
+ }).catch(() => { });
1255
+ }
1256
+ if (sourceCheckpointed) {
1257
+ if (sourceWasActive)
1258
+ await st.resumeTask(source.phase.id, source.task.id, nowISO()).catch(() => { });
1259
+ else
1260
+ await st.updatePhase(source.phase.id, (phase) => {
1261
+ phase.tasks = phase.tasks.map((task) => task.id === source.task.id ? source.task : task);
1262
+ return phase;
1263
+ }).catch(() => { });
1264
+ }
1265
+ throw error;
1266
+ }
1267
+ await st.syncTaskStatusRollup(source.phase.id);
1268
+ if (target.phase.id !== source.phase.id)
1269
+ await st.syncTaskStatusRollup(target.phase.id);
1270
+ return writeAndSummarize(st, [
1271
+ `🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
1272
+ `Paused snapshot: ${snapshot.whatWasBeingDone}`,
1273
+ `Return target: ${taskCompositeRef(source.task, source.phase, features)} from ${snapshot.resumeLocation}`,
1274
+ `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 });
1276
+ });
1083
1277
  server.registerTool("planner-task-start", {
1084
- 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.",
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.",
1085
1279
  inputSchema: { task: z.string().min(1) },
1086
1280
  }, async ({ task: ref }) => {
1087
1281
  const st = await requireStore();
@@ -1099,6 +1293,17 @@ server.registerTool("planner-task-start", {
1099
1293
  if (!eligibility.eligible)
1100
1294
  return text(`Task start denied: ${eligibility.reason}`, { reason: eligibility.reason });
1101
1295
  const selection = recommendNextTask(features, phases, project.workDeviations);
1296
+ if (selection.kind === "conflict") {
1297
+ return text(`${selection.reason} Pause/reconcile active tasks before starting another task.`, {
1298
+ activeTaskIds: selection.activeCandidates?.map((candidate) => candidate.task.id) ?? [],
1299
+ });
1300
+ }
1301
+ if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
1302
+ 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
+ }
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.`);
1306
+ }
1102
1307
  // Assemble the mandatory parent context before changing task lifecycle state.
1103
1308
  const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
1104
1309
  const [phaseWithRequirements, featureRequirements] = await Promise.all([
@@ -1109,40 +1314,46 @@ server.registerTool("planner-task-start", {
1109
1314
  // A pending handoff is context, not a lock: task start RETAINS it. It is
1110
1315
  // archived only when the phase completes (auto) or the user explicitly
1111
1316
  // clears it.
1112
- const advisory = selection.kind === "conflict"
1113
- ? `\n\n⚠️ ${selection.reason} Explicit task request honored; reconcile active work before relying on automatic recommendations.`
1114
- : selection.candidate && selection.candidate.task.id !== found.task.id
1115
- ? selection.kind === "active"
1116
- ? `\n\n⚠️ Active-task advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is already in progress. Explicit task request honored.`
1117
- : `\n\n⚠️ Priority advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
1118
- : "";
1317
+ 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.`
1319
+ : "";
1119
1320
  const timestamp = nowISO();
1120
1321
  let updatedTask;
1121
- await st.updatePhase(found.phase.id, (phase) => {
1122
- const task = phase.tasks.find((entry) => entry.id === found.task.id);
1123
- if (!task)
1124
- return phase;
1125
- applyTaskLifecycleDates(task, "in-progress", timestamp);
1126
- if (task.status !== "in-progress") {
1322
+ if (found.task.status === "paused") {
1323
+ updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp);
1324
+ }
1325
+ else {
1326
+ await st.updatePhase(found.phase.id, (phase) => {
1327
+ const task = phase.tasks.find((entry) => entry.id === found.task.id);
1328
+ if (!task)
1329
+ return phase;
1330
+ const previousStatus = task.status;
1331
+ applyTaskLifecycleDates(task, "in-progress", timestamp);
1127
1332
  const entry = {
1128
- id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${task.status}-in-progress`),
1333
+ id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-in-progress`),
1129
1334
  date: timestamp,
1130
- fromStatus: task.status,
1335
+ fromStatus: previousStatus,
1131
1336
  toStatus: "in-progress",
1132
- title: task.status === "done" ? "Reopened" : `→ in-progress`,
1133
- description: task.status === "done" ? "Task reopened from done status." : "",
1337
+ title: `${previousStatus} → in-progress`,
1338
+ description: "",
1134
1339
  };
1135
1340
  task.statusLog = [...(task.statusLog ?? []), entry];
1136
- }
1137
- task.updatedAt = timestamp;
1138
- phase.updatedAt = timestamp;
1139
- updatedTask = task;
1140
- return phase;
1141
- });
1341
+ task.updatedAt = timestamp;
1342
+ phase.updatedAt = timestamp;
1343
+ updatedTask = task;
1344
+ return phase;
1345
+ });
1346
+ }
1142
1347
  await st.syncTaskStatusRollup(found.phase.id);
1143
1348
  const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === found.task.id && deviation.state === "approved");
1144
1349
  if (approvedDeviation)
1145
1350
  await st.setWorkDeviationState(approvedDeviation.id, "active", timestamp);
1351
+ const resumedDeviation = project.workDeviations
1352
+ .filter((deviation) => deviation.resumeTaskId === found.task.id
1353
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
1354
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
1355
+ if (resumedDeviation)
1356
+ await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
1146
1357
  const t = updatedTask ?? found.task;
1147
1358
  return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}`);
1148
1359
  });
@@ -1158,6 +1369,8 @@ server.registerTool("planner-task-complete", {
1158
1369
  const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
1159
1370
  if (!found)
1160
1371
  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.");
1161
1374
  const unchecked = found.task.checklist.filter((item) => !item.checked);
1162
1375
  if (unchecked.length > 0 && !force)
1163
1376
  return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
@@ -1167,18 +1380,17 @@ server.registerTool("planner-task-complete", {
1167
1380
  const task = phase.tasks.find((entry) => entry.id === found.task.id);
1168
1381
  if (!task)
1169
1382
  return phase;
1383
+ const previousStatus = task.status;
1170
1384
  applyTaskLifecycleDates(task, "done", timestamp);
1171
- if (task.status !== "done") {
1172
- const entry = {
1173
- id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${task.status}-done`),
1174
- date: timestamp,
1175
- fromStatus: task.status,
1176
- toStatus: "done",
1177
- title: `→ done`,
1178
- description: "",
1179
- };
1180
- task.statusLog = [...(task.statusLog ?? []), entry];
1181
- }
1385
+ const entry = {
1386
+ id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-done`),
1387
+ date: timestamp,
1388
+ fromStatus: previousStatus,
1389
+ toStatus: "done",
1390
+ title: `${previousStatus} → done`,
1391
+ description: "",
1392
+ };
1393
+ task.statusLog = [...(task.statusLog ?? []), entry];
1182
1394
  if (description_update) {
1183
1395
  const sep = task.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
1184
1396
  task.description = task.description + sep + description_update;
@@ -1193,10 +1405,27 @@ server.registerTool("planner-task-complete", {
1193
1405
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === found.task.id)
1194
1406
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
1195
1407
  if (completedDeviation)
1196
- await st.setWorkDeviationState(completedDeviation.id, "resolved", timestamp);
1408
+ await st.setWorkDeviationState(completedDeviation.id, "resume-required", timestamp);
1197
1409
  const features = (await st.loadFeatures()).features;
1198
1410
  const t = updatedTask ?? found.task;
1199
- return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`);
1411
+ let resumeNotice = "";
1412
+ let resumeRequired;
1413
+ if (completedDeviation) {
1414
+ const resume = findTaskByRef(await st.loadAllPhases(), features, completedDeviation.resumeTaskId);
1415
+ if (resume) {
1416
+ const snapshot = resume.task.pauseSnapshot ?? completedDeviation.snapshot;
1417
+ resumeNotice = [
1418
+ "",
1419
+ `↩️ 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.",
1421
+ snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
1422
+ snapshot ? `How to resume: ${snapshot.howToResume}` : "",
1423
+ `Next action: planner-task-start ${taskCompositeRef(resume.task, resume.phase, features)}`,
1424
+ ].filter(Boolean).join("\n");
1425
+ resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot };
1426
+ }
1427
+ }
1428
+ return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}${resumeNotice}`, resumeRequired ? { resumeRequired } : undefined);
1200
1429
  });
1201
1430
  server.registerTool("planner-handoff-list", {
1202
1431
  description: "List all phases with a non-empty entity-scoped handoff (phase.handoff field). Returns composite refs (P00x / P00x(F00x)) with the first line and last-updated time.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
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/server": "0.2.19",
47
- "@agent-plan/core": "0.2.19"
46
+ "@agent-plan/core": "0.2.21",
47
+ "@agent-plan/server": "0.2.21"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",