@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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +124 -36
- package/package.json +3 -3
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
906
|
-
return text("
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
1195
|
-
return text(`Task switch denied: source is ${source.task.status}; only in-progress or
|
|
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(`
|
|
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.
|
|
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.
|
|
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
|
-
`
|
|
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
|
|
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
|
|
1306
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
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
|
-
?
|
|
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.
|
|
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.
|
|
1388
|
-
return text("Task completion denied: resume
|
|
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 ? `
|
|
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
|
|
1482
|
-
|
|
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.
|
|
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.
|
|
47
|
-
"@agent-plan/server": "0.2.
|
|
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",
|