@agent-plan/mcp 0.2.22 → 0.2.23-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/dist/index.d.ts.map +1 -1
- package/dist/index.js +326 -83
- 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":";AAmvDA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,32 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, packageVersionFromModule, resolvedPackageVersion } from "@agent-plan/core";
|
|
8
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureReadForSessionId, markPhaseReadForSessionId, markTaskReadForSessionId, contextReadEligibilityForSession, hasValidSessionAttestation, hasReadRequirementsForSession, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded } from "@agent-plan/core";
|
|
8
9
|
import { serve } from "@agent-plan/server";
|
|
9
10
|
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
10
|
-
const STATUS_VALUES = ["planned", "in-progress", "
|
|
11
|
+
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
11
12
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
13
|
+
const plannerSessionId = randomUUID();
|
|
14
|
+
startReadSession(plannerSessionId);
|
|
12
15
|
function text(textValue, structuredContent) {
|
|
13
16
|
return structuredContent ? { content: [{ type: "text", text: textValue }], structuredContent } : { content: [{ type: "text", text: textValue }] };
|
|
14
17
|
}
|
|
18
|
+
function taskStartError(outcome, structuredContent = {}) {
|
|
19
|
+
return {
|
|
20
|
+
isError: true,
|
|
21
|
+
content: [{ type: "text", text: [
|
|
22
|
+
`❌ TASK START FAILED [${outcome.errorCode}]`,
|
|
23
|
+
"started: false",
|
|
24
|
+
outcome.message,
|
|
25
|
+
"No task lifecycle transition was applied.",
|
|
26
|
+
"Next required actions:",
|
|
27
|
+
...outcome.nextActions.map((action, index) => `${index + 1}. ${action}`),
|
|
28
|
+
].join("\n") }],
|
|
29
|
+
structuredContent: { ...outcome, ...structuredContent },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
15
32
|
const MCP_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/mcp");
|
|
16
33
|
const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
|
|
17
34
|
function nowISO() {
|
|
@@ -431,6 +448,7 @@ server.registerTool("planner-feature-add", {
|
|
|
431
448
|
phaseIds: [],
|
|
432
449
|
dependsOn: [],
|
|
433
450
|
statusLog: [],
|
|
451
|
+
sessionInfo: [],
|
|
434
452
|
createdAt: timestamp,
|
|
435
453
|
updatedAt: timestamp,
|
|
436
454
|
};
|
|
@@ -450,6 +468,8 @@ server.registerTool("planner-feature-show", {
|
|
|
450
468
|
if (!resolvedFeature.ok)
|
|
451
469
|
return text(resolvedFeature.error);
|
|
452
470
|
const feature = resolvedFeature.feature;
|
|
471
|
+
if (full)
|
|
472
|
+
markFeatureReadForSessionId(plannerSessionId, feature.id);
|
|
453
473
|
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
454
474
|
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
|
|
455
475
|
return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
|
|
@@ -632,6 +652,7 @@ server.registerTool("planner-phase-add", {
|
|
|
632
652
|
handoffReadAt: "",
|
|
633
653
|
handoffHistory: [],
|
|
634
654
|
statusLog: [],
|
|
655
|
+
sessionInfo: [],
|
|
635
656
|
};
|
|
636
657
|
await st.savePhase(phase);
|
|
637
658
|
if (feature) {
|
|
@@ -657,6 +678,8 @@ server.registerTool("planner-phase-show", {
|
|
|
657
678
|
const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
658
679
|
if (!phase)
|
|
659
680
|
return text(`Phase not found: ${ref}`);
|
|
681
|
+
if (full)
|
|
682
|
+
markPhaseReadForSessionId(plannerSessionId, phase.id);
|
|
660
683
|
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
661
684
|
const reqCount = linkedRequirements.length;
|
|
662
685
|
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"}` : ""})`;
|
|
@@ -818,6 +841,7 @@ server.registerTool("planner-task-add", {
|
|
|
818
841
|
descriptionUpdatedAt: timestamp,
|
|
819
842
|
notes: "",
|
|
820
843
|
statusLog: [],
|
|
844
|
+
sessionInfo: [],
|
|
821
845
|
decisions: [],
|
|
822
846
|
acceptedDecisions: [],
|
|
823
847
|
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
|
|
@@ -841,18 +865,44 @@ server.registerTool("planner-task-add", {
|
|
|
841
865
|
});
|
|
842
866
|
server.registerTool("planner-task-show", {
|
|
843
867
|
description: "Show a task by id or name.",
|
|
844
|
-
inputSchema: { task: z.string().min(1).describe("Task ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the task description
|
|
868
|
+
inputSchema: { task: z.string().min(1).describe("Task ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the task description, resume checkpoint/advisory, and statusLog. Default: compact identity only (saves tokens).") },
|
|
845
869
|
}, async ({ task: ref, full }) => {
|
|
846
870
|
const st = await requireStore();
|
|
847
871
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
848
872
|
if (!found)
|
|
849
873
|
return text(`Task not found: ${ref}`);
|
|
874
|
+
if (full)
|
|
875
|
+
markTaskReadForSessionId(plannerSessionId, found.task.id);
|
|
850
876
|
const features = (await st.loadFeatures()).features;
|
|
851
877
|
const summary = `${found.task.title} — ${taskCompositeRef(found.task, found.phase, features)}${found.task.shortId ? ` · ${found.task.shortId}` : ""} (${found.task.status}; phase ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))})`;
|
|
852
878
|
if (!full)
|
|
853
879
|
return text(summary);
|
|
880
|
+
const project = await st.loadProject();
|
|
881
|
+
const pendingDeviation = found.task.status === "done" || found.task.status === "canceled" || found.task.status === "rejected"
|
|
882
|
+
? undefined
|
|
883
|
+
: project.workDeviations
|
|
884
|
+
.filter((deviation) => deviation.resumeTaskId === found.task.id
|
|
885
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
886
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
887
|
+
const snapshot = found.task.pauseSnapshot ?? pendingDeviation?.snapshot ?? null;
|
|
854
888
|
const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
|
|
855
|
-
|
|
889
|
+
const sections = [summary];
|
|
890
|
+
if (found.task.description?.trim())
|
|
891
|
+
sections.push(found.task.description.trim());
|
|
892
|
+
if (snapshot || pendingDeviation) {
|
|
893
|
+
sections.push([
|
|
894
|
+
pendingDeviation
|
|
895
|
+
? "Resume advisory: this task has a saved checkpoint that should be evaluated before selecting other work."
|
|
896
|
+
: "Resume checkpoint:",
|
|
897
|
+
snapshot ? `Checkpoint reason: ${snapshot.reason}` : "",
|
|
898
|
+
snapshot ? `Work checkpoint: ${snapshot.whatWasBeingDone}` : "",
|
|
899
|
+
snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
|
|
900
|
+
snapshot ? `How to resume: ${snapshot.howToResume}` : "",
|
|
901
|
+
].filter(Boolean).join("\n"));
|
|
902
|
+
}
|
|
903
|
+
if (log)
|
|
904
|
+
sections.push(`Status log:\n${log}`);
|
|
905
|
+
return text(sections.join("\n\n"));
|
|
856
906
|
});
|
|
857
907
|
server.registerTool("planner-task-discuss", {
|
|
858
908
|
description: "Persist task execution notes/checklist.",
|
|
@@ -902,8 +952,11 @@ server.registerTool("planner-task-update", {
|
|
|
902
952
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
903
953
|
if (!found)
|
|
904
954
|
return text(`Task not found: ${ref}`);
|
|
905
|
-
if (status === "
|
|
906
|
-
return text("
|
|
955
|
+
if (status === "in-progress" && status !== found.task.status) {
|
|
956
|
+
return text("Task start/resume transitions require planner-task-start so lifecycle state, checkpoints, and return stacks remain consistent.");
|
|
957
|
+
}
|
|
958
|
+
if (status === "done" && status !== found.task.status) {
|
|
959
|
+
return text("Task completion transitions require planner-task-complete with durable completion and verification evidence.");
|
|
907
960
|
}
|
|
908
961
|
// Validate motivation requirement for status transitions.
|
|
909
962
|
if (status !== undefined && needsMotivation(found.task.status, status)) {
|
|
@@ -1082,12 +1135,12 @@ server.registerTool("planner-task-delete", {
|
|
|
1082
1135
|
return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
|
|
1083
1136
|
});
|
|
1084
1137
|
server.registerTool("planner-task-recommend", {
|
|
1085
|
-
description: "Return the harness-agnostic recommended task: continue one active task, otherwise choose ready work by feature → phase → task priority. Reports dependency, availability, and approved deviation/resume context without starting or blocking work.",
|
|
1138
|
+
description: "Return the harness-agnostic recommended task: continue one active task or the current phase, otherwise choose ready work by feature → phase → task priority. Reports dependency, availability, and approved deviation/resume context without starting or blocking work.",
|
|
1086
1139
|
inputSchema: {},
|
|
1087
1140
|
}, async () => {
|
|
1088
1141
|
const st = await requireStore();
|
|
1089
|
-
const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1090
|
-
const result = recommendNextTask(features.features, phases, project.workDeviations);
|
|
1142
|
+
const [features, phases, project, resume] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
|
|
1143
|
+
const result = recommendNextTask(features.features, phases, project.workDeviations, resume?.currentPhaseId);
|
|
1091
1144
|
if (!result.candidate)
|
|
1092
1145
|
return text(`No task recommendation: ${result.reason}`, { kind: result.kind, reason: result.reason, activeTaskIds: result.activeCandidates?.map((candidate) => candidate.task.id) ?? [] });
|
|
1093
1146
|
const { candidate } = result;
|
|
@@ -1101,11 +1154,11 @@ server.registerTool("planner-task-deviation", {
|
|
|
1101
1154
|
inputSchema: { temporary_task: z.string().min(1), resume_task: z.string().min(1).optional(), reason: z.string().min(1) },
|
|
1102
1155
|
}, async ({ temporary_task, resume_task, reason }) => {
|
|
1103
1156
|
const st = await requireStore();
|
|
1104
|
-
const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1157
|
+
const [features, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
|
|
1105
1158
|
const temporary = findTaskByRef(phases, features.features, temporary_task);
|
|
1106
1159
|
if (!temporary)
|
|
1107
1160
|
return text(`Task not found: ${temporary_task}`);
|
|
1108
|
-
const selected = recommendNextTask(features.features, phases, project.workDeviations);
|
|
1161
|
+
const selected = recommendNextTask(features.features, phases, project.workDeviations, focus?.currentPhaseId);
|
|
1109
1162
|
const resume = resume_task ? findTaskByRef(phases, features.features, resume_task) : selected.candidate;
|
|
1110
1163
|
if (!resume)
|
|
1111
1164
|
return text(`No resume task is available. Provide resume_task explicitly. ${selected.reason}`);
|
|
@@ -1146,7 +1199,8 @@ server.registerTool("planner-task-pause", {
|
|
|
1146
1199
|
resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
|
|
1147
1200
|
pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
|
|
1148
1201
|
};
|
|
1149
|
-
|
|
1202
|
+
invalidateReads(plannerSessionId);
|
|
1203
|
+
const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
|
|
1150
1204
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
1151
1205
|
const activeDeviation = (await st.loadProject()).workDeviations
|
|
1152
1206
|
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
|
|
@@ -1161,15 +1215,15 @@ server.registerTool("planner-task-pause", {
|
|
|
1161
1215
|
? `\n↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`
|
|
1162
1216
|
: "";
|
|
1163
1217
|
return writeAndSummarize(st, [
|
|
1164
|
-
|
|
1218
|
+
`💾 Resume checkpoint saved: ${taskCompositeRef(checkpointed, found.phase, features)} — ${checkpointed.title}`,
|
|
1165
1219
|
`Why: ${snapshot.reason}`,
|
|
1166
1220
|
`Checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
1167
1221
|
`Resume from: ${snapshot.resumeLocation}`,
|
|
1168
1222
|
`How to resume: ${snapshot.howToResume}${resumeNotice}`,
|
|
1169
|
-
].join("\n"), { task:
|
|
1223
|
+
].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
|
|
1170
1224
|
});
|
|
1171
1225
|
server.registerTool("planner-task-switch", {
|
|
1172
|
-
description: "Atomically switch from active work to a temporary task, even outside normal priority order.
|
|
1226
|
+
description: "Atomically switch from active work to a temporary task, even outside normal priority order. Reuse the target's valid sessionInfo context or require the exact full tree when missing or stale. Pause the source with a mandatory snapshot, start the target, and push a durable LIFO return target.",
|
|
1173
1227
|
inputSchema: {
|
|
1174
1228
|
from_task: z.string().min(1),
|
|
1175
1229
|
to_task: z.string().min(1),
|
|
@@ -1181,7 +1235,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1181
1235
|
},
|
|
1182
1236
|
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
|
|
1183
1237
|
const st = await requireStore();
|
|
1184
|
-
const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1238
|
+
const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
|
|
1185
1239
|
const features = featuresDoc.features;
|
|
1186
1240
|
const source = findTaskByRef(phases, features, from_task);
|
|
1187
1241
|
const target = findTaskByRef(phases, features, to_task);
|
|
@@ -1191,8 +1245,34 @@ server.registerTool("planner-task-switch", {
|
|
|
1191
1245
|
return text(`Task not found: ${to_task}`);
|
|
1192
1246
|
if (source.task.id === target.task.id)
|
|
1193
1247
|
return text("Source and temporary task must differ.");
|
|
1194
|
-
if (source.task.status !== "in-progress" && source.task.
|
|
1195
|
-
return text(`Task switch denied: source is ${source.task.status}; only in-progress or
|
|
1248
|
+
if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
|
|
1249
|
+
return text(`Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.`);
|
|
1250
|
+
}
|
|
1251
|
+
const targetFeature = target.phase.featureId ? features.find((feature) => feature.id === target.phase.featureId) : undefined;
|
|
1252
|
+
const linkedRequirements = [
|
|
1253
|
+
...(await st.linkedRequirementsForPhase(target.phase.id)),
|
|
1254
|
+
...(target.phase.featureId ? await st.linkedRequirementsForFeature(target.phase.featureId) : []),
|
|
1255
|
+
].filter((requirement, index, all) => all.findIndex((candidate) => candidate.id === requirement.id) === index);
|
|
1256
|
+
const linkedRequirementIds = linkedRequirements.map((requirement) => requirement.id);
|
|
1257
|
+
const contextInput = {
|
|
1258
|
+
sessionId: plannerSessionId,
|
|
1259
|
+
taskId: target.task.id,
|
|
1260
|
+
phaseId: target.phase.id,
|
|
1261
|
+
...(target.phase.featureId ? { featureId: target.phase.featureId } : {}),
|
|
1262
|
+
task: target.task,
|
|
1263
|
+
phase: target.phase,
|
|
1264
|
+
...(targetFeature ? { feature: targetFeature } : {}),
|
|
1265
|
+
requirements: linkedRequirements,
|
|
1266
|
+
requirementIds: linkedRequirementIds,
|
|
1267
|
+
};
|
|
1268
|
+
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1269
|
+
if (!contextEligibility.eligible)
|
|
1270
|
+
return text(`Task switch denied: context reads required. ${contextEligibility.reason}`, contextEligibility);
|
|
1271
|
+
if (!hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements)) {
|
|
1272
|
+
return text("Task switch denied: read the requirements linked to the target phase and feature before switching.", { requirementIds: linkedRequirementIds });
|
|
1273
|
+
}
|
|
1274
|
+
if (!hasValidSessionAttestation(contextInput)) {
|
|
1275
|
+
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: target.phase.id, taskId: target.task.id, ...(target.phase.featureId ? { featureId: target.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
1196
1276
|
}
|
|
1197
1277
|
const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
|
|
1198
1278
|
if (!eligibility.eligible)
|
|
@@ -1203,7 +1283,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1203
1283
|
resumeLocation: resume_location, howToResume: how_to_resume,
|
|
1204
1284
|
relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: switched_by?.trim() ?? "",
|
|
1205
1285
|
};
|
|
1206
|
-
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1286
|
+
const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
|
|
1207
1287
|
const record = {
|
|
1208
1288
|
id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
|
|
1209
1289
|
temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason, snapshot,
|
|
@@ -1222,7 +1302,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1222
1302
|
await st.updatePhase(source.phase.id, (phase) => {
|
|
1223
1303
|
const task = phase.tasks.find((entry) => entry.id === source.task.id);
|
|
1224
1304
|
if (!task)
|
|
1225
|
-
throw new Error(`
|
|
1305
|
+
throw new Error(`Checkpointed source task disappeared: ${from_task}`);
|
|
1226
1306
|
task.pauseSnapshot = snapshot;
|
|
1227
1307
|
task.pauseHistory = [...task.pauseHistory, snapshot];
|
|
1228
1308
|
task.updatedAt = timestamp;
|
|
@@ -1230,7 +1310,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1230
1310
|
});
|
|
1231
1311
|
}
|
|
1232
1312
|
sourceCheckpointed = true;
|
|
1233
|
-
if (target.task.
|
|
1313
|
+
if (target.task.pauseSnapshot) {
|
|
1234
1314
|
await st.resumeTask(target.phase.id, target.task.id, timestamp);
|
|
1235
1315
|
}
|
|
1236
1316
|
else {
|
|
@@ -1252,7 +1332,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1252
1332
|
}
|
|
1253
1333
|
targetStarted = true;
|
|
1254
1334
|
await st.addWorkDeviation(record);
|
|
1255
|
-
if (target.task.
|
|
1335
|
+
if (target.task.pauseSnapshot) {
|
|
1256
1336
|
const resumedDeviation = project.workDeviations
|
|
1257
1337
|
.filter((deviation) => deviation.resumeTaskId === target.task.id
|
|
1258
1338
|
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
@@ -1282,45 +1362,104 @@ server.registerTool("planner-task-switch", {
|
|
|
1282
1362
|
await st.syncTaskStatusRollup(source.phase.id);
|
|
1283
1363
|
if (target.phase.id !== source.phase.id)
|
|
1284
1364
|
await st.syncTaskStatusRollup(target.phase.id);
|
|
1365
|
+
invalidateReads(plannerSessionId);
|
|
1285
1366
|
return writeAndSummarize(st, [
|
|
1286
1367
|
`🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
|
|
1287
|
-
`
|
|
1368
|
+
`Resume checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
1288
1369
|
`Return target: ${taskCompositeRef(source.task, source.phase, features)} from ${snapshot.resumeLocation}`,
|
|
1289
1370
|
`Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.`,
|
|
1290
1371
|
].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
|
|
1291
1372
|
});
|
|
1292
1373
|
server.registerTool("planner-task-start", {
|
|
1293
|
-
description: "Set a task to in-progress or resume
|
|
1374
|
+
description: "Set a task to in-progress or resume checkpointed work. Read the exact full task → phase → feature tree and linked requirements once per harness session; subsequent starts reuse persisted sessionInfo while entity updatedAt values remain unchanged. Every stale or denied start is an MCP isError result with started=false and exact retry actions; NEVER claim work started unless structuredContent.started is true. Success is returned only after persisted in-progress state is verified.",
|
|
1294
1375
|
inputSchema: { task: z.string().min(1) },
|
|
1295
1376
|
}, async ({ task: ref }) => {
|
|
1296
|
-
|
|
1377
|
+
let st;
|
|
1378
|
+
try {
|
|
1379
|
+
st = await requireStore();
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
return taskStartError(taskStartDenied("PLAN_NOT_FOUND", "No .planner/ found.", ["Initialize or load the planner, then retry planner-task-start."]));
|
|
1383
|
+
}
|
|
1297
1384
|
const features = (await st.loadFeatures()).features;
|
|
1298
1385
|
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
1299
1386
|
if (!found)
|
|
1300
|
-
return
|
|
1301
|
-
|
|
1302
|
-
|
|
1387
|
+
return taskStartError(taskStartDenied("TASK_NOT_FOUND", `Task not found: ${ref}`, ["Resolve the task with planner-task-list or planner-task-show, then retry planner-task-start."]));
|
|
1388
|
+
const taskRef = taskCompositeRef(found.task, found.phase, features);
|
|
1389
|
+
const phaseRef = formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features));
|
|
1390
|
+
const parentFeature = found.phase.featureId ? features.find((candidate) => candidate.id === found.phase.featureId) : undefined;
|
|
1391
|
+
const featureRef = parentFeature ? formatFeatureRef(parentFeature.number) : "";
|
|
1392
|
+
if (found.task.status === "in-progress") {
|
|
1393
|
+
const outcome = taskStartSucceeded(found.task.id, true);
|
|
1394
|
+
return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true`, { ...outcome, task: found.task });
|
|
1395
|
+
}
|
|
1303
1396
|
if (found.task.status === "done")
|
|
1304
|
-
return
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1397
|
+
return taskStartError(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use planner-task-update with status=planned and a motivation to reopen ${taskRef}.`, `Repeat the required full context reads, then retry planner-task-start ${taskRef}.`], { taskId: found.task.id }));
|
|
1398
|
+
const linkedRequirements = [
|
|
1399
|
+
...(await st.linkedRequirementsForPhase(found.phase.id)),
|
|
1400
|
+
...(found.phase.featureId ? await st.linkedRequirementsForFeature(found.phase.featureId) : []),
|
|
1401
|
+
].filter((requirement, index, all) => all.findIndex((candidate) => candidate.id === requirement.id) === index);
|
|
1402
|
+
const linkedRequirementIds = linkedRequirements.map((requirement) => requirement.id);
|
|
1403
|
+
const contextInput = {
|
|
1404
|
+
sessionId: plannerSessionId,
|
|
1405
|
+
taskId: found.task.id,
|
|
1406
|
+
phaseId: found.phase.id,
|
|
1407
|
+
...(found.phase.featureId ? { featureId: found.phase.featureId } : {}),
|
|
1408
|
+
task: found.task,
|
|
1409
|
+
phase: found.phase,
|
|
1410
|
+
...(parentFeature ? { feature: parentFeature } : {}),
|
|
1411
|
+
requirements: linkedRequirements,
|
|
1412
|
+
requirementIds: linkedRequirementIds,
|
|
1413
|
+
};
|
|
1414
|
+
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1415
|
+
if (!contextEligibility.eligible)
|
|
1416
|
+
return taskStartError(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, [
|
|
1417
|
+
`planner-task-show ${taskRef} with full=true`,
|
|
1418
|
+
`planner-phase-show ${phaseRef} with full=true`,
|
|
1419
|
+
...(featureRef ? [`planner-feature-show ${featureRef} with full=true`] : []),
|
|
1420
|
+
...(linkedRequirementIds.length > 0 ? ["planner-requirement-list"] : []),
|
|
1421
|
+
`Retry planner-task-start ${taskRef}`,
|
|
1422
|
+
], { taskId: found.task.id }), { contextEligibility });
|
|
1423
|
+
if (!hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements)) {
|
|
1424
|
+
return taskStartError(taskStartDenied("REQUIREMENTS_READ_REQUIRED", "Linked requirements were not read; the task remains unchanged.", ["planner-requirement-list", `Retry planner-task-start ${taskRef}`], { taskId: found.task.id, requirementIds: linkedRequirementIds }));
|
|
1425
|
+
}
|
|
1426
|
+
if (!hasValidSessionAttestation(contextInput)) {
|
|
1427
|
+
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: found.phase.id, taskId: found.task.id, ...(found.phase.featureId ? { featureId: found.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
1428
|
+
}
|
|
1429
|
+
const [project, phases, focus] = await Promise.all([st.loadProject(), st.loadAllPhases(), st.loadResume()]);
|
|
1307
1430
|
const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
|
|
1308
1431
|
if (!eligibility.eligible)
|
|
1309
|
-
return
|
|
1310
|
-
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1432
|
+
return taskStartError(taskStartDenied("START_NOT_ALLOWED", eligibility.reason, ["Resolve the reported task readiness condition, then retry planner-task-start."], { taskId: found.task.id }), { eligibility });
|
|
1433
|
+
const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
|
|
1311
1434
|
if (selection.kind === "conflict") {
|
|
1312
|
-
return
|
|
1313
|
-
activeTaskIds: selection.activeCandidates?.map((candidate) => candidate.task.id) ?? [],
|
|
1314
|
-
});
|
|
1435
|
+
return taskStartError(taskStartDenied("ACTIVE_TASK_CONFLICT", selection.reason, ["Pause or reconcile the active tasks.", `Retry planner-task-start ${taskRef}.`], { taskId: found.task.id }), { selection });
|
|
1315
1436
|
}
|
|
1316
1437
|
if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
|
|
1317
|
-
|
|
1438
|
+
const activeRef = taskCompositeRef(selection.candidate.task, selection.candidate.phase, features);
|
|
1439
|
+
return taskStartError(taskStartDenied("ACTIVE_TASK_CONFLICT", `${activeRef} is active; ${taskRef} was not started.`, [`Use planner-task-switch to snapshot and pause ${activeRef}.`, `Retry planner-task-start ${taskRef}.`], { taskId: found.task.id }), { selection });
|
|
1318
1440
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1441
|
+
let resumeProposal;
|
|
1442
|
+
if (selection.kind === "resume" && selection.candidate && selection.candidate.task.id !== found.task.id) {
|
|
1443
|
+
// Advisory only: a pending resume must be surfaced loudly, but it must
|
|
1444
|
+
// not hard-block an explicit start of a different task. Capture a
|
|
1445
|
+
// structured proposal so the resume-first guidance is explicit and
|
|
1446
|
+
// machine-readable for the host/agent.
|
|
1447
|
+
const resumeTask = selection.candidate;
|
|
1448
|
+
const resumeSnapshot = resumeTask.task.pauseSnapshot
|
|
1449
|
+
?? project.workDeviations.find((deviation) => deviation.resumeTaskId === resumeTask.task.id
|
|
1450
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))?.snapshot
|
|
1451
|
+
?? null;
|
|
1452
|
+
resumeProposal = buildResumeRequiredProposal({
|
|
1453
|
+
ref: taskCompositeRef(resumeTask.task, resumeTask.phase, features),
|
|
1454
|
+
title: resumeTask.task.title,
|
|
1455
|
+
taskId: resumeTask.task.id,
|
|
1456
|
+
phaseId: resumeTask.phase.id,
|
|
1457
|
+
snapshot: resumeSnapshot
|
|
1458
|
+
? { reason: resumeSnapshot.reason, resumeLocation: resumeSnapshot.resumeLocation, howToResume: resumeSnapshot.howToResume }
|
|
1459
|
+
: null,
|
|
1460
|
+
});
|
|
1321
1461
|
}
|
|
1322
1462
|
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
1323
|
-
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
1324
1463
|
const [phaseWithRequirements, featureRequirements] = await Promise.all([
|
|
1325
1464
|
st.loadPhaseWithRequirements(found.phase.id),
|
|
1326
1465
|
parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
|
|
@@ -1330,11 +1469,17 @@ server.registerTool("planner-task-start", {
|
|
|
1330
1469
|
// archived only when the phase completes (auto) or the user explicitly
|
|
1331
1470
|
// clears it.
|
|
1332
1471
|
const advisory = selection.candidate && selection.candidate.task.id !== found.task.id
|
|
1333
|
-
?
|
|
1472
|
+
? selection.kind === "resume"
|
|
1473
|
+
? `\n\n${(() => {
|
|
1474
|
+
if (resumeProposal)
|
|
1475
|
+
return resumeProposal.text;
|
|
1476
|
+
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.`;
|
|
1477
|
+
})()}`
|
|
1478
|
+
: `\n\n⚠️ Priority advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
|
|
1334
1479
|
: "";
|
|
1335
1480
|
const timestamp = nowISO();
|
|
1336
1481
|
let updatedTask;
|
|
1337
|
-
if (found.task.
|
|
1482
|
+
if (found.task.pauseSnapshot) {
|
|
1338
1483
|
updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp);
|
|
1339
1484
|
}
|
|
1340
1485
|
else {
|
|
@@ -1369,23 +1514,35 @@ server.registerTool("planner-task-start", {
|
|
|
1369
1514
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1370
1515
|
if (resumedDeviation)
|
|
1371
1516
|
await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
|
|
1372
|
-
|
|
1373
|
-
|
|
1517
|
+
await st.writeGenerated();
|
|
1518
|
+
const persistedTask = (await st.loadPhase(found.phase.id)).tasks.find((candidate) => candidate.id === found.task.id);
|
|
1519
|
+
if (!updatedTask || persistedTask?.status !== "in-progress") {
|
|
1520
|
+
return taskStartError(taskStartDenied("PERSISTENCE_VERIFICATION_FAILED", `The lifecycle write could not be verified as in-progress for ${taskRef}.`, ["Inspect planner-task-show for the persisted state and retry only after resolving the persistence failure."], { taskId: found.task.id }));
|
|
1521
|
+
}
|
|
1522
|
+
const outcome = taskStartSucceeded(persistedTask.id);
|
|
1523
|
+
return text(`✅ Task started: ${taskRef} — ${persistedTask.title} (in-progress)${persistedTask.shortId ? ` · ${persistedTask.shortId}` : ""}\nstarted: true${phaseContext}${advisory}`, {
|
|
1524
|
+
...outcome,
|
|
1525
|
+
task: persistedTask,
|
|
1526
|
+
...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
|
|
1527
|
+
});
|
|
1374
1528
|
});
|
|
1375
1529
|
server.registerTool("planner-task-complete", {
|
|
1376
|
-
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
1530
|
+
description: "Set a task to done with mandatory durable completion and verification evidence. Fails if checklist is incomplete unless force=true.",
|
|
1377
1531
|
inputSchema: {
|
|
1378
1532
|
task: z.string().min(1),
|
|
1379
1533
|
force: z.boolean().optional(),
|
|
1380
|
-
description_update: z.string().min(10).
|
|
1534
|
+
description_update: z.string().min(10).describe("Required evidence: shipped work, verification level including partial verification, remaining/unverified work, files, decisions, and updated code references."),
|
|
1381
1535
|
},
|
|
1382
1536
|
}, async ({ task: ref, force, description_update }) => {
|
|
1383
1537
|
const st = await requireStore();
|
|
1384
1538
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
1385
1539
|
if (!found)
|
|
1386
1540
|
return text(`Task not found: ${ref}`);
|
|
1387
|
-
|
|
1388
|
-
|
|
1541
|
+
const completionSummary = description_update.trim();
|
|
1542
|
+
if (completionSummary.length < 10)
|
|
1543
|
+
return text("Task completion denied: provide at least 10 characters of durable completion and verification evidence.");
|
|
1544
|
+
if (found.task.pauseSnapshot)
|
|
1545
|
+
return text("Task completion denied: resume checkpointed work with planner-task-start before completing it, or cancel it explicitly with motivation.");
|
|
1389
1546
|
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
1390
1547
|
if (unchecked.length > 0 && !force)
|
|
1391
1548
|
return text(`${unchecked.length} checklist item(s) not done. Re-run with force=true to complete anyway.`);
|
|
@@ -1403,13 +1560,12 @@ server.registerTool("planner-task-complete", {
|
|
|
1403
1560
|
fromStatus: previousStatus,
|
|
1404
1561
|
toStatus: "done",
|
|
1405
1562
|
title: `${previousStatus} → done`,
|
|
1406
|
-
description:
|
|
1563
|
+
description: completionSummary,
|
|
1407
1564
|
};
|
|
1408
1565
|
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1566
|
+
const sep = task.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
|
|
1567
|
+
task.description = task.description + sep + completionSummary;
|
|
1568
|
+
task.descriptionUpdatedAt = timestamp;
|
|
1413
1569
|
task.updatedAt = timestamp;
|
|
1414
1570
|
phase.updatedAt = timestamp;
|
|
1415
1571
|
updatedTask = task;
|
|
@@ -1432,7 +1588,7 @@ server.registerTool("planner-task-complete", {
|
|
|
1432
1588
|
resumeNotice = [
|
|
1433
1589
|
"",
|
|
1434
1590
|
`↩️ RESUME REQUIRED: ${taskCompositeRef(resume.task, resume.phase, features)} — ${resume.task.title}`,
|
|
1435
|
-
snapshot ? `
|
|
1591
|
+
snapshot ? `Checkpoint reason: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
|
|
1436
1592
|
snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
|
|
1437
1593
|
snapshot ? `How to resume: ${snapshot.howToResume}` : "",
|
|
1438
1594
|
`Next action: planner-task-start ${taskCompositeRef(resume.task, resume.phase, features)}`,
|
|
@@ -1465,52 +1621,139 @@ server.registerTool("planner-handoff-show", {
|
|
|
1465
1621
|
return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, empty: true });
|
|
1466
1622
|
return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
1467
1623
|
});
|
|
1624
|
+
server.registerTool("planner-requirement-list", {
|
|
1625
|
+
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).",
|
|
1626
|
+
inputSchema: {},
|
|
1627
|
+
}, async () => {
|
|
1628
|
+
const st = await requireStore();
|
|
1629
|
+
const requirements = await st.loadRequirements();
|
|
1630
|
+
requirements.requirements.forEach((req) => markRequirementReadForSessionId(plannerSessionId, req.id));
|
|
1631
|
+
return text(requirements.requirements.map((req) => `- ${req.id} — ${req.title} (${req.status})`).join("\n") || "No requirements", { requirements });
|
|
1632
|
+
});
|
|
1468
1633
|
server.registerTool("planner-handoff-write", {
|
|
1469
|
-
description: "
|
|
1634
|
+
description: "Reconcile and refresh the single active phase handoff while synchronizing durable task, phase, and feature context. Run planner-handoff-prepare with the confirmed phaseRef first and pass its exact handoffUpdatedAt token.",
|
|
1470
1635
|
inputSchema: {
|
|
1471
1636
|
phaseRef: z.string().min(1).describe("Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title."),
|
|
1472
|
-
title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work
|
|
1637
|
+
title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work."),
|
|
1473
1638
|
confirmed: z.boolean().describe("Set true only after the user explicitly confirms the proposed feature+phase target."),
|
|
1474
|
-
content: z.string().min(1).describe("
|
|
1639
|
+
content: z.string().min(1).describe("Full reconciled handoff text (markdown)."),
|
|
1640
|
+
expectedHandoffUpdatedAt: z.string().optional().describe("Exact token returned by planner-handoff-prepare; empty when no handoff exists."),
|
|
1641
|
+
reconciledExistingHandoff: z.boolean().optional().describe("Confirm that still-relevant existing handoff information was retained."),
|
|
1642
|
+
taskUpdates: z.array(z.object({
|
|
1643
|
+
taskRef: z.string().min(1),
|
|
1644
|
+
completionSummary: z.string().min(1),
|
|
1645
|
+
verification: z.string().min(1),
|
|
1646
|
+
remainingWork: z.string().min(1),
|
|
1647
|
+
filesTouched: z.array(z.string().min(1)).optional(),
|
|
1648
|
+
decisions: z.array(z.string().min(1)).optional(),
|
|
1649
|
+
})).optional(),
|
|
1650
|
+
phaseUpdate: z.object({
|
|
1651
|
+
progressSummary: z.string().min(1),
|
|
1652
|
+
remainingWork: z.string().min(1),
|
|
1653
|
+
decisions: z.array(z.string().min(1)).optional(),
|
|
1654
|
+
}).optional(),
|
|
1655
|
+
phaseNoUpdateReason: z.string().min(1).optional(),
|
|
1656
|
+
featureUpdate: z.object({ workDone: z.string().min(1), workRemaining: z.string().min(1) }).optional(),
|
|
1657
|
+
featureNoUpdateReason: z.string().min(1).optional(),
|
|
1475
1658
|
},
|
|
1476
|
-
}, async ({ phaseRef, title, confirmed, content }) => {
|
|
1659
|
+
}, async ({ phaseRef, title, confirmed, content, expectedHandoffUpdatedAt, reconciledExistingHandoff, taskUpdates, phaseUpdate, phaseNoUpdateReason, featureUpdate, featureNoUpdateReason }) => {
|
|
1477
1660
|
const st = await requireStore();
|
|
1478
1661
|
let body = content.trim();
|
|
1479
|
-
const firstLine = body.split(/\r?\n/).find((
|
|
1662
|
+
const firstLine = body.split(/\r?\n/).find((line) => line.trim().length > 0) ?? "";
|
|
1480
1663
|
const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
|
|
1481
|
-
const
|
|
1482
|
-
if (!
|
|
1483
|
-
return text("❌ Generic handoff title. Provide a meaningful
|
|
1664
|
+
const effectiveHeadingText = title?.trim() || firstHeadingText;
|
|
1665
|
+
if (!effectiveHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(effectiveHeadingText)) {
|
|
1666
|
+
return text("❌ Generic handoff title. Provide a meaningful title summarizing the work.");
|
|
1484
1667
|
}
|
|
1485
|
-
if (title
|
|
1486
|
-
const t = title.trim();
|
|
1668
|
+
if (title?.trim()) {
|
|
1487
1669
|
const lines = body.split(/\r?\n/);
|
|
1488
|
-
const firstIdx = lines.findIndex((
|
|
1489
|
-
if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? ""))
|
|
1490
|
-
lines[firstIdx] = `# ${
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
body = `# ${t}\n\n${body}`;
|
|
1495
|
-
}
|
|
1670
|
+
const firstIdx = lines.findIndex((line) => line.trim().length > 0);
|
|
1671
|
+
if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? ""))
|
|
1672
|
+
lines[firstIdx] = `# ${title.trim()}`;
|
|
1673
|
+
else
|
|
1674
|
+
lines.unshift(`# ${title.trim()}`, "");
|
|
1675
|
+
body = lines.join("\n");
|
|
1496
1676
|
}
|
|
1497
1677
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1498
1678
|
if (!r.ok)
|
|
1499
1679
|
return text(`❌ ${r.error}`);
|
|
1500
1680
|
if (!confirmed)
|
|
1501
|
-
return text(`Proposal only: I would
|
|
1502
|
-
|
|
1503
|
-
|
|
1681
|
+
return text(`Proposal only: I would refresh this handoff on ${r.compositeRef}. Ask the user to confirm, then run planner-handoff-prepare with the exact phaseRef.`, { phaseRef: r.compositeRef, confirmationRequired: true });
|
|
1682
|
+
if (expectedHandoffUpdatedAt === undefined)
|
|
1683
|
+
return text("❌ Run planner-handoff-prepare first and pass expectedHandoffUpdatedAt.");
|
|
1684
|
+
const phases = await st.loadAllPhases();
|
|
1685
|
+
const features = (await st.loadFeatures()).features;
|
|
1686
|
+
const resolvedTaskUpdates = [];
|
|
1687
|
+
for (const update of taskUpdates ?? []) {
|
|
1688
|
+
const found = findTaskByRef(phases, features, update.taskRef.trim());
|
|
1689
|
+
if (!found || found.phase.id !== r.phase.id)
|
|
1690
|
+
return text(`❌ Task ${update.taskRef} does not belong to ${r.compositeRef}.`);
|
|
1691
|
+
resolvedTaskUpdates.push({
|
|
1692
|
+
taskId: found.task.id,
|
|
1693
|
+
completionSummary: update.completionSummary,
|
|
1694
|
+
verification: update.verification,
|
|
1695
|
+
remainingWork: update.remainingWork,
|
|
1696
|
+
...(update.filesTouched ? { filesTouched: update.filesTouched } : {}),
|
|
1697
|
+
...(update.decisions ? { decisions: update.decisions } : {}),
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
try {
|
|
1701
|
+
const result = await st.refreshPhaseHandoff(r.phase.id, {
|
|
1702
|
+
content: body,
|
|
1703
|
+
expectedHandoffUpdatedAt,
|
|
1704
|
+
reconciledExistingHandoff: reconciledExistingHandoff === true,
|
|
1705
|
+
contextSync: {
|
|
1706
|
+
taskUpdates: resolvedTaskUpdates,
|
|
1707
|
+
...(phaseUpdate ? { phaseUpdate: {
|
|
1708
|
+
progressSummary: phaseUpdate.progressSummary,
|
|
1709
|
+
remainingWork: phaseUpdate.remainingWork,
|
|
1710
|
+
...(phaseUpdate.decisions ? { decisions: phaseUpdate.decisions } : {}),
|
|
1711
|
+
} } : {}),
|
|
1712
|
+
...(phaseNoUpdateReason ? { phaseNoUpdateReason } : {}),
|
|
1713
|
+
...(featureUpdate ? { featureUpdate } : {}),
|
|
1714
|
+
...(featureNoUpdateReason ? { featureNoUpdateReason } : {}),
|
|
1715
|
+
},
|
|
1716
|
+
});
|
|
1717
|
+
await st.writeGenerated();
|
|
1718
|
+
return text(`✅ Reconciled handoff and durable context on ${r.compositeRef}; updated ${result.updatedTaskIds.length} task(s).`, { phaseRef: r.compositeRef, phaseId: r.phase.id, ...result });
|
|
1719
|
+
}
|
|
1720
|
+
catch (error) {
|
|
1721
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1722
|
+
return text(`❌ Handoff refresh denied: ${message}`, { error: message });
|
|
1723
|
+
}
|
|
1504
1724
|
});
|
|
1505
1725
|
server.registerTool("planner-handoff-prepare", {
|
|
1506
|
-
description: "
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1726
|
+
description: "Identify or audit the exact handoff target. Without phaseRef, returns the confirmation workflow. With a confirmed phaseRef, returns the current handoff/version and done tasks missing durable completion evidence.",
|
|
1727
|
+
inputSchema: { phaseRef: z.string().min(1).optional() },
|
|
1728
|
+
}, async ({ phaseRef }) => {
|
|
1729
|
+
if (!phaseRef)
|
|
1730
|
+
return text([
|
|
1731
|
+
"First identify the exact feature and phase actually discussed/worked on in this session. Do not use a stale resume pointer or target a phase that just became done.",
|
|
1732
|
+
"Tell the user: 'I propose writing this handoff on P00x(F00x) — <phase title>. Confirm?' Wait for explicit confirmation.",
|
|
1733
|
+
"After confirmation, call planner-handoff-prepare again with that exact phaseRef, then reconcile its current handoff and missing task evidence through planner-handoff-write.",
|
|
1734
|
+
].join("\n"));
|
|
1735
|
+
const st = await requireStore();
|
|
1736
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1737
|
+
if (!r.ok)
|
|
1738
|
+
return text(`❌ ${r.error}`);
|
|
1739
|
+
try {
|
|
1740
|
+
const audit = await st.preparePhaseHandoff(r.phase.id);
|
|
1741
|
+
const missing = audit.missingCompletionTasks.map((task) => `- T${String(task.number).padStart(3, "0")} — ${task.title}`).join("\n") || "- None";
|
|
1742
|
+
return text([
|
|
1743
|
+
`Handoff preparation audit for ${r.compositeRef}`,
|
|
1744
|
+
`Base handoffUpdatedAt: ${audit.handoffUpdatedAt || "(empty)"}`,
|
|
1745
|
+
"Done tasks missing durable completion/verification evidence:",
|
|
1746
|
+
missing,
|
|
1747
|
+
"",
|
|
1748
|
+
"Existing active handoff (reconcile all still-relevant content):",
|
|
1749
|
+
audit.handoff.trim() || "(none)",
|
|
1750
|
+
].join("\n"), { phaseRef: r.compositeRef, ...audit });
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1754
|
+
return text(`❌ Handoff preparation failed: ${message}`, { error: message });
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1514
1757
|
server.registerTool("planner-handoff-clear", {
|
|
1515
1758
|
description: "Clear/archive the entity-scoped handoff of a phase. phaseRef is required; never infer a phase automatically.",
|
|
1516
1759
|
inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title.") },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23-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.23-next.0",
|
|
47
|
+
"@agent-plan/server": "0.2.23-next.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|