@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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +121 -28
- package/package.json +3 -3
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
|
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":";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", "
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
891
|
-
return text("
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
1180
|
-
return text(`Task switch denied: source is ${source.task.status}; only in-progress or
|
|
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(`
|
|
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.
|
|
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.
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1305
|
-
|
|
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
|
-
?
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1373
|
-
return text("Task completion denied: resume
|
|
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 ? `
|
|
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
|
|
1467
|
-
|
|
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.
|
|
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.
|
|
47
|
-
"@agent-plan/server": "0.2.
|
|
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",
|