@agent-plan/mcp 0.2.20 → 0.2.22
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 +283 -42
- 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":";AAo+CA,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, packageVersionFromModule, resolvedPackageVersion } 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 }] };
|
|
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: {
|
|
@@ -808,6 +823,8 @@ server.registerTool("planner-task-add", {
|
|
|
808
823
|
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
|
|
809
824
|
subtasks: [],
|
|
810
825
|
dependsOn: [],
|
|
826
|
+
pauseSnapshot: null,
|
|
827
|
+
pauseHistory: [],
|
|
811
828
|
startedAt: "",
|
|
812
829
|
completedAt: "",
|
|
813
830
|
createdAt: timestamp,
|
|
@@ -885,6 +902,9 @@ server.registerTool("planner-task-update", {
|
|
|
885
902
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
886
903
|
if (!found)
|
|
887
904
|
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.");
|
|
907
|
+
}
|
|
888
908
|
// Validate motivation requirement for status transitions.
|
|
889
909
|
if (status !== undefined && needsMotivation(found.task.status, status)) {
|
|
890
910
|
if (!motivation || !motivation.trim()) {
|
|
@@ -925,7 +945,22 @@ server.registerTool("planner-task-update", {
|
|
|
925
945
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
926
946
|
const features = (await st.loadFeatures()).features;
|
|
927
947
|
const t = updatedTask ?? found.task;
|
|
928
|
-
|
|
948
|
+
let resumeNotice = "";
|
|
949
|
+
let resumeRequired;
|
|
950
|
+
if (status && status !== "in-progress" && status !== found.task.status) {
|
|
951
|
+
const deviation = (await st.loadProject()).workDeviations
|
|
952
|
+
.filter((entry) => (entry.state === "approved" || entry.state === "active") && entry.temporaryTaskId === found.task.id)
|
|
953
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
954
|
+
if (deviation) {
|
|
955
|
+
await st.setWorkDeviationState(deviation.id, "resume-required", timestamp);
|
|
956
|
+
const resume = findTaskByRef(await st.loadAllPhases(), features, deviation.resumeTaskId);
|
|
957
|
+
if (resume) {
|
|
958
|
+
resumeNotice = `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`;
|
|
959
|
+
resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot: resume.task.pauseSnapshot ?? deviation.snapshot };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}${resumeNotice}`, resumeRequired ? { resumeRequired } : undefined);
|
|
929
964
|
});
|
|
930
965
|
server.registerTool("planner-task-checklist-toggle", {
|
|
931
966
|
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.",
|
|
@@ -1079,12 +1114,183 @@ server.registerTool("planner-task-deviation", {
|
|
|
1079
1114
|
if (temporary.task.status !== "planned")
|
|
1080
1115
|
return text(`Temporary task is not startable: ${temporary.task.status}.`);
|
|
1081
1116
|
const timestamp = nowISO();
|
|
1082
|
-
const record = {
|
|
1117
|
+
const record = {
|
|
1118
|
+
id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id,
|
|
1119
|
+
temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason, snapshot: null,
|
|
1120
|
+
requestedBy: "user", approvedBy: "user", state: "approved",
|
|
1121
|
+
createdAt: timestamp, activatedAt: "", resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
|
|
1122
|
+
};
|
|
1083
1123
|
await st.addWorkDeviation(record);
|
|
1084
1124
|
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 });
|
|
1085
1125
|
});
|
|
1126
|
+
server.registerTool("planner-task-pause", {
|
|
1127
|
+
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.",
|
|
1128
|
+
inputSchema: {
|
|
1129
|
+
task: z.string().min(1),
|
|
1130
|
+
reason: z.string().min(1),
|
|
1131
|
+
what_was_being_done: z.string().min(1),
|
|
1132
|
+
resume_location: z.string().min(1),
|
|
1133
|
+
how_to_resume: z.string().min(1),
|
|
1134
|
+
paused_by: z.string().optional(),
|
|
1135
|
+
},
|
|
1136
|
+
}, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }) => {
|
|
1137
|
+
const st = await requireStore();
|
|
1138
|
+
const features = (await st.loadFeatures()).features;
|
|
1139
|
+
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
1140
|
+
if (!found)
|
|
1141
|
+
return text(`Task not found: ${ref}`);
|
|
1142
|
+
if (found.task.status !== "in-progress")
|
|
1143
|
+
return text(`Task pause denied: ${taskCompositeRef(found.task, found.phase, features)} is ${found.task.status}, not in-progress.`);
|
|
1144
|
+
const snapshot = {
|
|
1145
|
+
id: crypto.randomUUID(), reason, whatWasBeingDone: what_was_being_done,
|
|
1146
|
+
resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
|
|
1147
|
+
pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
|
|
1148
|
+
};
|
|
1149
|
+
const paused = await st.pauseTask(found.phase.id, found.task.id, snapshot);
|
|
1150
|
+
await st.syncTaskStatusRollup(found.phase.id);
|
|
1151
|
+
const activeDeviation = (await st.loadProject()).workDeviations
|
|
1152
|
+
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
|
|
1153
|
+
&& deviation.temporaryTaskId === found.task.id)
|
|
1154
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1155
|
+
if (activeDeviation)
|
|
1156
|
+
await st.setWorkDeviationState(activeDeviation.id, "resume-required", snapshot.pausedAt);
|
|
1157
|
+
const resume = activeDeviation
|
|
1158
|
+
? findTaskByRef(await st.loadAllPhases(), features, activeDeviation.resumeTaskId)
|
|
1159
|
+
: undefined;
|
|
1160
|
+
const resumeNotice = resume
|
|
1161
|
+
? `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`
|
|
1162
|
+
: "";
|
|
1163
|
+
return writeAndSummarize(st, [
|
|
1164
|
+
`⏸️ Task paused: ${taskCompositeRef(paused, found.phase, features)} — ${paused.title}`,
|
|
1165
|
+
`Why: ${snapshot.reason}`,
|
|
1166
|
+
`Checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
1167
|
+
`Resume from: ${snapshot.resumeLocation}`,
|
|
1168
|
+
`How to resume: ${snapshot.howToResume}${resumeNotice}`,
|
|
1169
|
+
].join("\n"), { task: paused, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
|
|
1170
|
+
});
|
|
1171
|
+
server.registerTool("planner-task-switch", {
|
|
1172
|
+
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.",
|
|
1173
|
+
inputSchema: {
|
|
1174
|
+
from_task: z.string().min(1),
|
|
1175
|
+
to_task: z.string().min(1),
|
|
1176
|
+
reason: z.string().min(1),
|
|
1177
|
+
what_was_being_done: z.string().min(1),
|
|
1178
|
+
resume_location: z.string().min(1),
|
|
1179
|
+
how_to_resume: z.string().min(1),
|
|
1180
|
+
switched_by: z.string().optional(),
|
|
1181
|
+
},
|
|
1182
|
+
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
|
|
1183
|
+
const st = await requireStore();
|
|
1184
|
+
const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1185
|
+
const features = featuresDoc.features;
|
|
1186
|
+
const source = findTaskByRef(phases, features, from_task);
|
|
1187
|
+
const target = findTaskByRef(phases, features, to_task);
|
|
1188
|
+
if (!source)
|
|
1189
|
+
return text(`Task not found: ${from_task}`);
|
|
1190
|
+
if (!target)
|
|
1191
|
+
return text(`Task not found: ${to_task}`);
|
|
1192
|
+
if (source.task.id === target.task.id)
|
|
1193
|
+
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.`);
|
|
1196
|
+
}
|
|
1197
|
+
const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
|
|
1198
|
+
if (!eligibility.eligible)
|
|
1199
|
+
return text(`Task switch denied: ${eligibility.reason}`);
|
|
1200
|
+
const timestamp = nowISO();
|
|
1201
|
+
const snapshot = {
|
|
1202
|
+
id: crypto.randomUUID(), reason, whatWasBeingDone: what_was_being_done,
|
|
1203
|
+
resumeLocation: resume_location, howToResume: how_to_resume,
|
|
1204
|
+
relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: switched_by?.trim() ?? "",
|
|
1205
|
+
};
|
|
1206
|
+
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1207
|
+
const record = {
|
|
1208
|
+
id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
|
|
1209
|
+
temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason, snapshot,
|
|
1210
|
+
requestedBy: "agent", approvedBy: switched_by?.trim() || "explicit task_switch",
|
|
1211
|
+
state: "active", createdAt: timestamp, activatedAt: timestamp,
|
|
1212
|
+
resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
|
|
1213
|
+
};
|
|
1214
|
+
const sourceWasActive = source.task.status === "in-progress";
|
|
1215
|
+
let sourceCheckpointed = false;
|
|
1216
|
+
let targetStarted = false;
|
|
1217
|
+
try {
|
|
1218
|
+
if (sourceWasActive) {
|
|
1219
|
+
await st.pauseTask(source.phase.id, source.task.id, snapshot);
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
await st.updatePhase(source.phase.id, (phase) => {
|
|
1223
|
+
const task = phase.tasks.find((entry) => entry.id === source.task.id);
|
|
1224
|
+
if (!task)
|
|
1225
|
+
throw new Error(`Paused source task disappeared: ${from_task}`);
|
|
1226
|
+
task.pauseSnapshot = snapshot;
|
|
1227
|
+
task.pauseHistory = [...task.pauseHistory, snapshot];
|
|
1228
|
+
task.updatedAt = timestamp;
|
|
1229
|
+
return phase;
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
sourceCheckpointed = true;
|
|
1233
|
+
if (target.task.status === "paused") {
|
|
1234
|
+
await st.resumeTask(target.phase.id, target.task.id, timestamp);
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
await st.updatePhase(target.phase.id, (phase) => {
|
|
1238
|
+
const task = phase.tasks.find((entry) => entry.id === target.task.id);
|
|
1239
|
+
if (!task)
|
|
1240
|
+
throw new Error(`Temporary task disappeared: ${to_task}`);
|
|
1241
|
+
const previousStatus = task.status;
|
|
1242
|
+
applyTaskLifecycleDates(task, "in-progress", timestamp);
|
|
1243
|
+
task.statusLog = [...task.statusLog, {
|
|
1244
|
+
id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
|
|
1245
|
+
date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
|
|
1246
|
+
title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
|
|
1247
|
+
}];
|
|
1248
|
+
task.updatedAt = timestamp;
|
|
1249
|
+
phase.updatedAt = timestamp;
|
|
1250
|
+
return phase;
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
targetStarted = true;
|
|
1254
|
+
await st.addWorkDeviation(record);
|
|
1255
|
+
if (target.task.status === "paused") {
|
|
1256
|
+
const resumedDeviation = project.workDeviations
|
|
1257
|
+
.filter((deviation) => deviation.resumeTaskId === target.task.id
|
|
1258
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
1259
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1260
|
+
if (resumedDeviation)
|
|
1261
|
+
await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
catch (error) {
|
|
1265
|
+
if (targetStarted) {
|
|
1266
|
+
await st.updatePhase(target.phase.id, (phase) => {
|
|
1267
|
+
phase.tasks = phase.tasks.map((task) => task.id === target.task.id ? target.task : task);
|
|
1268
|
+
return phase;
|
|
1269
|
+
}).catch(() => { });
|
|
1270
|
+
}
|
|
1271
|
+
if (sourceCheckpointed) {
|
|
1272
|
+
if (sourceWasActive)
|
|
1273
|
+
await st.resumeTask(source.phase.id, source.task.id, nowISO()).catch(() => { });
|
|
1274
|
+
else
|
|
1275
|
+
await st.updatePhase(source.phase.id, (phase) => {
|
|
1276
|
+
phase.tasks = phase.tasks.map((task) => task.id === source.task.id ? source.task : task);
|
|
1277
|
+
return phase;
|
|
1278
|
+
}).catch(() => { });
|
|
1279
|
+
}
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1282
|
+
await st.syncTaskStatusRollup(source.phase.id);
|
|
1283
|
+
if (target.phase.id !== source.phase.id)
|
|
1284
|
+
await st.syncTaskStatusRollup(target.phase.id);
|
|
1285
|
+
return writeAndSummarize(st, [
|
|
1286
|
+
`🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
|
|
1287
|
+
`Paused snapshot: ${snapshot.whatWasBeingDone}`,
|
|
1288
|
+
`Return target: ${taskCompositeRef(source.task, source.phase, features)} from ${snapshot.resumeLocation}`,
|
|
1289
|
+
`Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.`,
|
|
1290
|
+
].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
|
|
1291
|
+
});
|
|
1086
1292
|
server.registerTool("planner-task-start", {
|
|
1087
|
-
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.
|
|
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.",
|
|
1088
1294
|
inputSchema: { task: z.string().min(1) },
|
|
1089
1295
|
}, async ({ task: ref }) => {
|
|
1090
1296
|
const st = await requireStore();
|
|
@@ -1102,6 +1308,17 @@ server.registerTool("planner-task-start", {
|
|
|
1102
1308
|
if (!eligibility.eligible)
|
|
1103
1309
|
return text(`Task start denied: ${eligibility.reason}`, { reason: eligibility.reason });
|
|
1104
1310
|
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1311
|
+
if (selection.kind === "conflict") {
|
|
1312
|
+
return text(`${selection.reason} Pause/reconcile active tasks before starting another task.`, {
|
|
1313
|
+
activeTaskIds: selection.activeCandidates?.map((candidate) => candidate.task.id) ?? [],
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
|
|
1317
|
+
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
|
+
}
|
|
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.`);
|
|
1321
|
+
}
|
|
1105
1322
|
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
1106
1323
|
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
1107
1324
|
const [phaseWithRequirements, featureRequirements] = await Promise.all([
|
|
@@ -1112,40 +1329,46 @@ server.registerTool("planner-task-start", {
|
|
|
1112
1329
|
// A pending handoff is context, not a lock: task start RETAINS it. It is
|
|
1113
1330
|
// archived only when the phase completes (auto) or the user explicitly
|
|
1114
1331
|
// clears it.
|
|
1115
|
-
const advisory = selection.
|
|
1116
|
-
? `\n\n⚠️ ${selection.
|
|
1117
|
-
:
|
|
1118
|
-
? selection.kind === "active"
|
|
1119
|
-
? `\n\n⚠️ Active-task advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is already in progress. Explicit task request honored.`
|
|
1120
|
-
: `\n\n⚠️ Priority advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
|
|
1121
|
-
: "";
|
|
1332
|
+
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.`
|
|
1334
|
+
: "";
|
|
1122
1335
|
const timestamp = nowISO();
|
|
1123
1336
|
let updatedTask;
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1337
|
+
if (found.task.status === "paused") {
|
|
1338
|
+
updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp);
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
1342
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
1343
|
+
if (!task)
|
|
1344
|
+
return phase;
|
|
1345
|
+
const previousStatus = task.status;
|
|
1346
|
+
applyTaskLifecycleDates(task, "in-progress", timestamp);
|
|
1130
1347
|
const entry = {
|
|
1131
|
-
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${
|
|
1348
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-in-progress`),
|
|
1132
1349
|
date: timestamp,
|
|
1133
|
-
fromStatus:
|
|
1350
|
+
fromStatus: previousStatus,
|
|
1134
1351
|
toStatus: "in-progress",
|
|
1135
|
-
title:
|
|
1136
|
-
description:
|
|
1352
|
+
title: `${previousStatus} → in-progress`,
|
|
1353
|
+
description: "",
|
|
1137
1354
|
};
|
|
1138
1355
|
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1356
|
+
task.updatedAt = timestamp;
|
|
1357
|
+
phase.updatedAt = timestamp;
|
|
1358
|
+
updatedTask = task;
|
|
1359
|
+
return phase;
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1145
1362
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
1146
1363
|
const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === found.task.id && deviation.state === "approved");
|
|
1147
1364
|
if (approvedDeviation)
|
|
1148
1365
|
await st.setWorkDeviationState(approvedDeviation.id, "active", timestamp);
|
|
1366
|
+
const resumedDeviation = project.workDeviations
|
|
1367
|
+
.filter((deviation) => deviation.resumeTaskId === found.task.id
|
|
1368
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
1369
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1370
|
+
if (resumedDeviation)
|
|
1371
|
+
await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
|
|
1149
1372
|
const t = updatedTask ?? found.task;
|
|
1150
1373
|
return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}`);
|
|
1151
1374
|
});
|
|
@@ -1161,6 +1384,8 @@ server.registerTool("planner-task-complete", {
|
|
|
1161
1384
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
1162
1385
|
if (!found)
|
|
1163
1386
|
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.");
|
|
1164
1389
|
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
1165
1390
|
if (unchecked.length > 0 && !force)
|
|
1166
1391
|
return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
|
|
@@ -1170,18 +1395,17 @@ server.registerTool("planner-task-complete", {
|
|
|
1170
1395
|
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
1171
1396
|
if (!task)
|
|
1172
1397
|
return phase;
|
|
1398
|
+
const previousStatus = task.status;
|
|
1173
1399
|
applyTaskLifecycleDates(task, "done", timestamp);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1184
|
-
}
|
|
1400
|
+
const entry = {
|
|
1401
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-done`),
|
|
1402
|
+
date: timestamp,
|
|
1403
|
+
fromStatus: previousStatus,
|
|
1404
|
+
toStatus: "done",
|
|
1405
|
+
title: `${previousStatus} → done`,
|
|
1406
|
+
description: "",
|
|
1407
|
+
};
|
|
1408
|
+
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1185
1409
|
if (description_update) {
|
|
1186
1410
|
const sep = task.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
|
|
1187
1411
|
task.description = task.description + sep + description_update;
|
|
@@ -1196,10 +1420,27 @@ server.registerTool("planner-task-complete", {
|
|
|
1196
1420
|
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === found.task.id)
|
|
1197
1421
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1198
1422
|
if (completedDeviation)
|
|
1199
|
-
await st.setWorkDeviationState(completedDeviation.id, "
|
|
1423
|
+
await st.setWorkDeviationState(completedDeviation.id, "resume-required", timestamp);
|
|
1200
1424
|
const features = (await st.loadFeatures()).features;
|
|
1201
1425
|
const t = updatedTask ?? found.task;
|
|
1202
|
-
|
|
1426
|
+
let resumeNotice = "";
|
|
1427
|
+
let resumeRequired;
|
|
1428
|
+
if (completedDeviation) {
|
|
1429
|
+
const resume = findTaskByRef(await st.loadAllPhases(), features, completedDeviation.resumeTaskId);
|
|
1430
|
+
if (resume) {
|
|
1431
|
+
const snapshot = resume.task.pauseSnapshot ?? completedDeviation.snapshot;
|
|
1432
|
+
resumeNotice = [
|
|
1433
|
+
"",
|
|
1434
|
+
`↩️ 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.",
|
|
1436
|
+
snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
|
|
1437
|
+
snapshot ? `How to resume: ${snapshot.howToResume}` : "",
|
|
1438
|
+
`Next action: planner-task-start ${taskCompositeRef(resume.task, resume.phase, features)}`,
|
|
1439
|
+
].filter(Boolean).join("\n");
|
|
1440
|
+
resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot };
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
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);
|
|
1203
1444
|
});
|
|
1204
1445
|
server.registerTool("planner-handoff-list", {
|
|
1205
1446
|
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.
|
|
3
|
+
"version": "0.2.22",
|
|
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",
|
|
47
|
+
"@agent-plan/server": "0.2.22"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|