@agent-plan/mcp 0.2.20 → 0.2.22-next.0
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 +365 -46
- 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,7 +4,7 @@ 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
10
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
@@ -12,6 +12,8 @@ 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"}` : ""})`;
|
|
@@ -808,6 +825,8 @@ server.registerTool("planner-task-add", {
|
|
|
808
825
|
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
|
|
809
826
|
subtasks: [],
|
|
810
827
|
dependsOn: [],
|
|
828
|
+
pauseSnapshot: null,
|
|
829
|
+
pauseHistory: [],
|
|
811
830
|
startedAt: "",
|
|
812
831
|
completedAt: "",
|
|
813
832
|
createdAt: timestamp,
|
|
@@ -824,18 +843,43 @@ server.registerTool("planner-task-add", {
|
|
|
824
843
|
});
|
|
825
844
|
server.registerTool("planner-task-show", {
|
|
826
845
|
description: "Show a task by id or name.",
|
|
827
|
-
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).") },
|
|
828
847
|
}, async ({ task: ref, full }) => {
|
|
829
848
|
const st = await requireStore();
|
|
830
849
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
831
850
|
if (!found)
|
|
832
851
|
return text(`Task not found: ${ref}`);
|
|
852
|
+
markTaskRead(found.task.id, found.phase.id, found.phase.featureId);
|
|
833
853
|
const features = (await st.loadFeatures()).features;
|
|
834
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))})`;
|
|
835
855
|
if (!full)
|
|
836
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;
|
|
837
865
|
const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
|
|
838
|
-
|
|
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"));
|
|
839
883
|
});
|
|
840
884
|
server.registerTool("planner-task-discuss", {
|
|
841
885
|
description: "Persist task execution notes/checklist.",
|
|
@@ -885,6 +929,9 @@ server.registerTool("planner-task-update", {
|
|
|
885
929
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
886
930
|
if (!found)
|
|
887
931
|
return text(`Task not found: ${ref}`);
|
|
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.");
|
|
934
|
+
}
|
|
888
935
|
// Validate motivation requirement for status transitions.
|
|
889
936
|
if (status !== undefined && needsMotivation(found.task.status, status)) {
|
|
890
937
|
if (!motivation || !motivation.trim()) {
|
|
@@ -925,7 +972,22 @@ server.registerTool("planner-task-update", {
|
|
|
925
972
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
926
973
|
const features = (await st.loadFeatures()).features;
|
|
927
974
|
const t = updatedTask ?? found.task;
|
|
928
|
-
|
|
975
|
+
let resumeNotice = "";
|
|
976
|
+
let resumeRequired;
|
|
977
|
+
if (status && status !== "in-progress" && status !== found.task.status) {
|
|
978
|
+
const deviation = (await st.loadProject()).workDeviations
|
|
979
|
+
.filter((entry) => (entry.state === "approved" || entry.state === "active") && entry.temporaryTaskId === found.task.id)
|
|
980
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
981
|
+
if (deviation) {
|
|
982
|
+
await st.setWorkDeviationState(deviation.id, "resume-required", timestamp);
|
|
983
|
+
const resume = findTaskByRef(await st.loadAllPhases(), features, deviation.resumeTaskId);
|
|
984
|
+
if (resume) {
|
|
985
|
+
resumeNotice = `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`;
|
|
986
|
+
resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot: resume.task.pauseSnapshot ?? deviation.snapshot };
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}${resumeNotice}`, resumeRequired ? { resumeRequired } : undefined);
|
|
929
991
|
});
|
|
930
992
|
server.registerTool("planner-task-checklist-toggle", {
|
|
931
993
|
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 +1141,191 @@ server.registerTool("planner-task-deviation", {
|
|
|
1079
1141
|
if (temporary.task.status !== "planned")
|
|
1080
1142
|
return text(`Temporary task is not startable: ${temporary.task.status}.`);
|
|
1081
1143
|
const timestamp = nowISO();
|
|
1082
|
-
const record = {
|
|
1144
|
+
const record = {
|
|
1145
|
+
id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id,
|
|
1146
|
+
temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason, snapshot: null,
|
|
1147
|
+
requestedBy: "user", approvedBy: "user", state: "approved",
|
|
1148
|
+
createdAt: timestamp, activatedAt: "", resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
|
|
1149
|
+
};
|
|
1083
1150
|
await st.addWorkDeviation(record);
|
|
1084
1151
|
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
1152
|
});
|
|
1153
|
+
server.registerTool("planner-task-pause", {
|
|
1154
|
+
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.",
|
|
1155
|
+
inputSchema: {
|
|
1156
|
+
task: z.string().min(1),
|
|
1157
|
+
reason: z.string().min(1),
|
|
1158
|
+
what_was_being_done: z.string().min(1),
|
|
1159
|
+
resume_location: z.string().min(1),
|
|
1160
|
+
how_to_resume: z.string().min(1),
|
|
1161
|
+
paused_by: z.string().optional(),
|
|
1162
|
+
},
|
|
1163
|
+
}, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }) => {
|
|
1164
|
+
const st = await requireStore();
|
|
1165
|
+
const features = (await st.loadFeatures()).features;
|
|
1166
|
+
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
1167
|
+
if (!found)
|
|
1168
|
+
return text(`Task not found: ${ref}`);
|
|
1169
|
+
if (found.task.status !== "in-progress")
|
|
1170
|
+
return text(`Task pause denied: ${taskCompositeRef(found.task, found.phase, features)} is ${found.task.status}, not in-progress.`);
|
|
1171
|
+
const snapshot = {
|
|
1172
|
+
id: crypto.randomUUID(), reason, whatWasBeingDone: what_was_being_done,
|
|
1173
|
+
resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
|
|
1174
|
+
pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
|
|
1175
|
+
};
|
|
1176
|
+
invalidateReads();
|
|
1177
|
+
const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
|
|
1178
|
+
await st.syncTaskStatusRollup(found.phase.id);
|
|
1179
|
+
const activeDeviation = (await st.loadProject()).workDeviations
|
|
1180
|
+
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
|
|
1181
|
+
&& deviation.temporaryTaskId === found.task.id)
|
|
1182
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1183
|
+
if (activeDeviation)
|
|
1184
|
+
await st.setWorkDeviationState(activeDeviation.id, "resume-required", snapshot.pausedAt);
|
|
1185
|
+
const resume = activeDeviation
|
|
1186
|
+
? findTaskByRef(await st.loadAllPhases(), features, activeDeviation.resumeTaskId)
|
|
1187
|
+
: undefined;
|
|
1188
|
+
const resumeNotice = resume
|
|
1189
|
+
? `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`
|
|
1190
|
+
: "";
|
|
1191
|
+
return writeAndSummarize(st, [
|
|
1192
|
+
`💾 Resume checkpoint saved: ${taskCompositeRef(checkpointed, found.phase, features)} — ${checkpointed.title}`,
|
|
1193
|
+
`Why: ${snapshot.reason}`,
|
|
1194
|
+
`Checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
1195
|
+
`Resume from: ${snapshot.resumeLocation}`,
|
|
1196
|
+
`How to resume: ${snapshot.howToResume}${resumeNotice}`,
|
|
1197
|
+
].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
|
|
1198
|
+
});
|
|
1199
|
+
server.registerTool("planner-task-switch", {
|
|
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.",
|
|
1201
|
+
inputSchema: {
|
|
1202
|
+
from_task: z.string().min(1),
|
|
1203
|
+
to_task: z.string().min(1),
|
|
1204
|
+
reason: z.string().min(1),
|
|
1205
|
+
what_was_being_done: z.string().min(1),
|
|
1206
|
+
resume_location: z.string().min(1),
|
|
1207
|
+
how_to_resume: z.string().min(1),
|
|
1208
|
+
switched_by: z.string().optional(),
|
|
1209
|
+
},
|
|
1210
|
+
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
|
|
1211
|
+
const st = await requireStore();
|
|
1212
|
+
const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1213
|
+
const features = featuresDoc.features;
|
|
1214
|
+
const source = findTaskByRef(phases, features, from_task);
|
|
1215
|
+
const target = findTaskByRef(phases, features, to_task);
|
|
1216
|
+
if (!source)
|
|
1217
|
+
return text(`Task not found: ${from_task}`);
|
|
1218
|
+
if (!target)
|
|
1219
|
+
return text(`Task not found: ${to_task}`);
|
|
1220
|
+
if (source.task.id === target.task.id)
|
|
1221
|
+
return text("Source and temporary task must differ.");
|
|
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.`);
|
|
1224
|
+
}
|
|
1225
|
+
const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
|
|
1226
|
+
if (!eligibility.eligible)
|
|
1227
|
+
return text(`Task switch denied: ${eligibility.reason}`);
|
|
1228
|
+
const timestamp = nowISO();
|
|
1229
|
+
const snapshot = {
|
|
1230
|
+
id: crypto.randomUUID(), reason, whatWasBeingDone: what_was_being_done,
|
|
1231
|
+
resumeLocation: resume_location, howToResume: how_to_resume,
|
|
1232
|
+
relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: switched_by?.trim() ?? "",
|
|
1233
|
+
};
|
|
1234
|
+
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1235
|
+
const record = {
|
|
1236
|
+
id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
|
|
1237
|
+
temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason, snapshot,
|
|
1238
|
+
requestedBy: "agent", approvedBy: switched_by?.trim() || "explicit task_switch",
|
|
1239
|
+
state: "active", createdAt: timestamp, activatedAt: timestamp,
|
|
1240
|
+
resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
|
|
1241
|
+
};
|
|
1242
|
+
const sourceWasActive = source.task.status === "in-progress";
|
|
1243
|
+
let sourceCheckpointed = false;
|
|
1244
|
+
let targetStarted = false;
|
|
1245
|
+
try {
|
|
1246
|
+
if (sourceWasActive) {
|
|
1247
|
+
await st.pauseTask(source.phase.id, source.task.id, snapshot);
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
await st.updatePhase(source.phase.id, (phase) => {
|
|
1251
|
+
const task = phase.tasks.find((entry) => entry.id === source.task.id);
|
|
1252
|
+
if (!task)
|
|
1253
|
+
throw new Error(`Checkpointed source task disappeared: ${from_task}`);
|
|
1254
|
+
task.pauseSnapshot = snapshot;
|
|
1255
|
+
task.pauseHistory = [...task.pauseHistory, snapshot];
|
|
1256
|
+
task.updatedAt = timestamp;
|
|
1257
|
+
return phase;
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
sourceCheckpointed = true;
|
|
1261
|
+
if (target.task.pauseSnapshot) {
|
|
1262
|
+
await st.resumeTask(target.phase.id, target.task.id, timestamp);
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
await st.updatePhase(target.phase.id, (phase) => {
|
|
1266
|
+
const task = phase.tasks.find((entry) => entry.id === target.task.id);
|
|
1267
|
+
if (!task)
|
|
1268
|
+
throw new Error(`Temporary task disappeared: ${to_task}`);
|
|
1269
|
+
const previousStatus = task.status;
|
|
1270
|
+
applyTaskLifecycleDates(task, "in-progress", timestamp);
|
|
1271
|
+
task.statusLog = [...task.statusLog, {
|
|
1272
|
+
id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
|
|
1273
|
+
date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
|
|
1274
|
+
title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
|
|
1275
|
+
}];
|
|
1276
|
+
task.updatedAt = timestamp;
|
|
1277
|
+
phase.updatedAt = timestamp;
|
|
1278
|
+
return phase;
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
targetStarted = true;
|
|
1282
|
+
await st.addWorkDeviation(record);
|
|
1283
|
+
if (target.task.pauseSnapshot) {
|
|
1284
|
+
const resumedDeviation = project.workDeviations
|
|
1285
|
+
.filter((deviation) => deviation.resumeTaskId === target.task.id
|
|
1286
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
1287
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1288
|
+
if (resumedDeviation)
|
|
1289
|
+
await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
catch (error) {
|
|
1293
|
+
if (targetStarted) {
|
|
1294
|
+
await st.updatePhase(target.phase.id, (phase) => {
|
|
1295
|
+
phase.tasks = phase.tasks.map((task) => task.id === target.task.id ? target.task : task);
|
|
1296
|
+
return phase;
|
|
1297
|
+
}).catch(() => { });
|
|
1298
|
+
}
|
|
1299
|
+
if (sourceCheckpointed) {
|
|
1300
|
+
if (sourceWasActive)
|
|
1301
|
+
await st.resumeTask(source.phase.id, source.task.id, nowISO()).catch(() => { });
|
|
1302
|
+
else
|
|
1303
|
+
await st.updatePhase(source.phase.id, (phase) => {
|
|
1304
|
+
phase.tasks = phase.tasks.map((task) => task.id === source.task.id ? source.task : task);
|
|
1305
|
+
return phase;
|
|
1306
|
+
}).catch(() => { });
|
|
1307
|
+
}
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
await st.syncTaskStatusRollup(source.phase.id);
|
|
1311
|
+
if (target.phase.id !== source.phase.id)
|
|
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
|
+
]);
|
|
1318
|
+
return writeAndSummarize(st, [
|
|
1319
|
+
`🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
|
|
1320
|
+
`Resume checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
1321
|
+
`Return target: ${taskCompositeRef(source.task, source.phase, features)} from ${snapshot.resumeLocation}`,
|
|
1322
|
+
`Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.`,
|
|
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 } : {}) });
|
|
1326
|
+
});
|
|
1086
1327
|
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.
|
|
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.",
|
|
1088
1329
|
inputSchema: { task: z.string().min(1) },
|
|
1089
1330
|
}, async ({ task: ref }) => {
|
|
1090
1331
|
const st = await requireStore();
|
|
@@ -1102,6 +1343,35 @@ server.registerTool("planner-task-start", {
|
|
|
1102
1343
|
if (!eligibility.eligible)
|
|
1103
1344
|
return text(`Task start denied: ${eligibility.reason}`, { reason: eligibility.reason });
|
|
1104
1345
|
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1346
|
+
if (selection.kind === "conflict") {
|
|
1347
|
+
return text(`${selection.reason} Pause/reconcile active tasks before starting another task.`, {
|
|
1348
|
+
activeTaskIds: selection.activeCandidates?.map((candidate) => candidate.task.id) ?? [],
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
|
|
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)}.`);
|
|
1353
|
+
}
|
|
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
|
+
});
|
|
1374
|
+
}
|
|
1105
1375
|
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
1106
1376
|
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
1107
1377
|
const [phaseWithRequirements, featureRequirements] = await Promise.all([
|
|
@@ -1112,42 +1382,62 @@ server.registerTool("planner-task-start", {
|
|
|
1112
1382
|
// A pending handoff is context, not a lock: task start RETAINS it. It is
|
|
1113
1383
|
// archived only when the phase completes (auto) or the user explicitly
|
|
1114
1384
|
// clears it.
|
|
1115
|
-
const advisory = selection.
|
|
1116
|
-
?
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1385
|
+
const advisory = selection.candidate && selection.candidate.task.id !== found.task.id
|
|
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.`
|
|
1393
|
+
: "";
|
|
1122
1394
|
const timestamp = nowISO();
|
|
1123
1395
|
let updatedTask;
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1396
|
+
if (found.task.pauseSnapshot) {
|
|
1397
|
+
updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp);
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
1401
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
1402
|
+
if (!task)
|
|
1403
|
+
return phase;
|
|
1404
|
+
const previousStatus = task.status;
|
|
1405
|
+
applyTaskLifecycleDates(task, "in-progress", timestamp);
|
|
1130
1406
|
const entry = {
|
|
1131
|
-
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${
|
|
1407
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-in-progress`),
|
|
1132
1408
|
date: timestamp,
|
|
1133
|
-
fromStatus:
|
|
1409
|
+
fromStatus: previousStatus,
|
|
1134
1410
|
toStatus: "in-progress",
|
|
1135
|
-
title:
|
|
1136
|
-
description:
|
|
1411
|
+
title: `${previousStatus} → in-progress`,
|
|
1412
|
+
description: "",
|
|
1137
1413
|
};
|
|
1138
1414
|
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1415
|
+
task.updatedAt = timestamp;
|
|
1416
|
+
phase.updatedAt = timestamp;
|
|
1417
|
+
updatedTask = task;
|
|
1418
|
+
return phase;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1145
1421
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
1146
1422
|
const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === found.task.id && deviation.state === "approved");
|
|
1147
1423
|
if (approvedDeviation)
|
|
1148
1424
|
await st.setWorkDeviationState(approvedDeviation.id, "active", timestamp);
|
|
1425
|
+
const resumedDeviation = project.workDeviations
|
|
1426
|
+
.filter((deviation) => deviation.resumeTaskId === found.task.id
|
|
1427
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
1428
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1429
|
+
if (resumedDeviation)
|
|
1430
|
+
await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
|
|
1149
1431
|
const t = updatedTask ?? found.task;
|
|
1150
|
-
|
|
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
|
+
});
|
|
1151
1441
|
});
|
|
1152
1442
|
server.registerTool("planner-task-complete", {
|
|
1153
1443
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -1161,6 +1451,8 @@ server.registerTool("planner-task-complete", {
|
|
|
1161
1451
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
1162
1452
|
if (!found)
|
|
1163
1453
|
return text(`Task not found: ${ref}`);
|
|
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.");
|
|
1164
1456
|
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
1165
1457
|
if (unchecked.length > 0 && !force)
|
|
1166
1458
|
return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
|
|
@@ -1170,18 +1462,17 @@ server.registerTool("planner-task-complete", {
|
|
|
1170
1462
|
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
1171
1463
|
if (!task)
|
|
1172
1464
|
return phase;
|
|
1465
|
+
const previousStatus = task.status;
|
|
1173
1466
|
applyTaskLifecycleDates(task, "done", timestamp);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1184
|
-
}
|
|
1467
|
+
const entry = {
|
|
1468
|
+
id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-done`),
|
|
1469
|
+
date: timestamp,
|
|
1470
|
+
fromStatus: previousStatus,
|
|
1471
|
+
toStatus: "done",
|
|
1472
|
+
title: `${previousStatus} → done`,
|
|
1473
|
+
description: "",
|
|
1474
|
+
};
|
|
1475
|
+
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1185
1476
|
if (description_update) {
|
|
1186
1477
|
const sep = task.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
|
|
1187
1478
|
task.description = task.description + sep + description_update;
|
|
@@ -1196,10 +1487,27 @@ server.registerTool("planner-task-complete", {
|
|
|
1196
1487
|
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === found.task.id)
|
|
1197
1488
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1198
1489
|
if (completedDeviation)
|
|
1199
|
-
await st.setWorkDeviationState(completedDeviation.id, "
|
|
1490
|
+
await st.setWorkDeviationState(completedDeviation.id, "resume-required", timestamp);
|
|
1200
1491
|
const features = (await st.loadFeatures()).features;
|
|
1201
1492
|
const t = updatedTask ?? found.task;
|
|
1202
|
-
|
|
1493
|
+
let resumeNotice = "";
|
|
1494
|
+
let resumeRequired;
|
|
1495
|
+
if (completedDeviation) {
|
|
1496
|
+
const resume = findTaskByRef(await st.loadAllPhases(), features, completedDeviation.resumeTaskId);
|
|
1497
|
+
if (resume) {
|
|
1498
|
+
const snapshot = resume.task.pauseSnapshot ?? completedDeviation.snapshot;
|
|
1499
|
+
resumeNotice = [
|
|
1500
|
+
"",
|
|
1501
|
+
`↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`,
|
|
1502
|
+
snapshot ? `Checkpoint reason: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
|
|
1503
|
+
snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
|
|
1504
|
+
snapshot ? `How to resume: ${snapshot.howToResume}` : "",
|
|
1505
|
+
`Next action: planner-task-start ${taskCompositeRef(resume.task, resume.phase, features)}`,
|
|
1506
|
+
].filter(Boolean).join("\n");
|
|
1507
|
+
resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot };
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
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
1511
|
});
|
|
1204
1512
|
server.registerTool("planner-handoff-list", {
|
|
1205
1513
|
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.",
|
|
@@ -1224,6 +1532,15 @@ server.registerTool("planner-handoff-show", {
|
|
|
1224
1532
|
return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, empty: true });
|
|
1225
1533
|
return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
1226
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
|
+
});
|
|
1227
1544
|
server.registerTool("planner-handoff-write", {
|
|
1228
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.",
|
|
1229
1546
|
inputSchema: {
|
|
@@ -1237,8 +1554,10 @@ server.registerTool("planner-handoff-write", {
|
|
|
1237
1554
|
let body = content.trim();
|
|
1238
1555
|
const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
|
1239
1556
|
const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
|
|
1240
|
-
const
|
|
1241
|
-
|
|
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) {
|
|
1242
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.");
|
|
1243
1562
|
}
|
|
1244
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.0",
|
|
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.0",
|
|
47
|
+
"@agent-plan/server": "0.2.22-next.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|