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