@agent-plan/mcp 0.2.23 → 0.2.25
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 +303 -88
- 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":";AAw0DA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,82 @@ 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, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion,
|
|
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
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 fallbackPlannerSessionId = `mcp-process:${randomUUID()}`;
|
|
14
|
+
startReadSession(fallbackPlannerSessionId);
|
|
15
|
+
function plannerSessionIdFor(extra) {
|
|
16
|
+
const transportSessionId = extra?.sessionId?.trim();
|
|
17
|
+
const sessionId = transportSessionId ? `mcp:${transportSessionId}` : fallbackPlannerSessionId;
|
|
18
|
+
startReadSession(sessionId);
|
|
19
|
+
return sessionId;
|
|
20
|
+
}
|
|
12
21
|
function text(textValue, structuredContent) {
|
|
13
22
|
return structuredContent ? { content: [{ type: "text", text: textValue }], structuredContent } : { content: [{ type: "text", text: textValue }] };
|
|
14
23
|
}
|
|
24
|
+
function derivedStatusReadOnlyResult(entity, ref, attemptedStatus, effectiveStatus) {
|
|
25
|
+
const childKind = entity === "feature" ? "phases" : "tasks";
|
|
26
|
+
const retryAction = entity === "feature"
|
|
27
|
+
? `Retry planner-feature-update ${ref} without the status field if you need to update metadata.`
|
|
28
|
+
: `Retry planner-phase-update ${ref} without the status field if you need to update metadata.`;
|
|
29
|
+
const nextActions = [
|
|
30
|
+
`${entity[0].toUpperCase()}${entity.slice(1)} status is derived from child ${childKind}; update the child ${childKind} instead of setting ${entity}.status.`,
|
|
31
|
+
retryAction,
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
isError: true,
|
|
35
|
+
content: [{ type: "text", text: [
|
|
36
|
+
`❌ ${entity.toUpperCase()} UPDATE FAILED [DERIVED_STATUS_READ_ONLY]`,
|
|
37
|
+
`${entity[0].toUpperCase()}${entity.slice(1)} status is derived from child ${childKind} and cannot be set directly.`,
|
|
38
|
+
`Attempted status: ${attemptedStatus}`,
|
|
39
|
+
`Effective derived status: ${effectiveStatus}`,
|
|
40
|
+
"No planner data was changed.",
|
|
41
|
+
"Next required actions:",
|
|
42
|
+
...nextActions.map((action, index) => `${index + 1}. ${action}`),
|
|
43
|
+
].join("\n") }],
|
|
44
|
+
structuredContent: {
|
|
45
|
+
updated: false,
|
|
46
|
+
errorCode: "DERIVED_STATUS_READ_ONLY",
|
|
47
|
+
entity,
|
|
48
|
+
ref,
|
|
49
|
+
attemptedStatus,
|
|
50
|
+
effectiveStatus,
|
|
51
|
+
nextActions,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function taskStartError(outcome, structuredContent = {}) {
|
|
56
|
+
return {
|
|
57
|
+
isError: true,
|
|
58
|
+
content: [{ type: "text", text: [
|
|
59
|
+
`❌ TASK START FAILED [${outcome.errorCode}]`,
|
|
60
|
+
"started: false",
|
|
61
|
+
outcome.message,
|
|
62
|
+
"No task lifecycle transition was applied.",
|
|
63
|
+
"Next required actions:",
|
|
64
|
+
...outcome.nextActions.map((action, index) => `${index + 1}. ${action}`),
|
|
65
|
+
].join("\n") }],
|
|
66
|
+
structuredContent: { ...outcome, ...structuredContent },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function mcpContextReadActions(eligibility, refs, requirementReadRequired, retryAction) {
|
|
70
|
+
const actions = (eligibility.requiredReads ?? []).flatMap((read) => {
|
|
71
|
+
if (read.kind === "task")
|
|
72
|
+
return [`planner-task-show ${refs.task} with full=true`];
|
|
73
|
+
if (read.kind === "phase")
|
|
74
|
+
return [`planner-phase-show ${refs.phase} with full=true`];
|
|
75
|
+
return refs.feature ? [`planner-feature-show ${refs.feature} with full=true`] : [];
|
|
76
|
+
});
|
|
77
|
+
if (requirementReadRequired)
|
|
78
|
+
actions.push("planner-requirement-list");
|
|
79
|
+
actions.push(retryAction);
|
|
80
|
+
return actions;
|
|
81
|
+
}
|
|
15
82
|
const MCP_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/mcp");
|
|
16
83
|
const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
|
|
17
84
|
function nowISO() {
|
|
@@ -431,6 +498,7 @@ server.registerTool("planner-feature-add", {
|
|
|
431
498
|
phaseIds: [],
|
|
432
499
|
dependsOn: [],
|
|
433
500
|
statusLog: [],
|
|
501
|
+
sessionInfo: [],
|
|
434
502
|
createdAt: timestamp,
|
|
435
503
|
updatedAt: timestamp,
|
|
436
504
|
};
|
|
@@ -443,7 +511,8 @@ server.registerTool("planner-feature-add", {
|
|
|
443
511
|
server.registerTool("planner-feature-show", {
|
|
444
512
|
description: "Show a feature by id or name.",
|
|
445
513
|
inputSchema: { feature: z.string().min(1).describe("Feature 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 feature description. Default: compact identity only (saves tokens).") },
|
|
446
|
-
}, async ({ feature: ref, full }) => {
|
|
514
|
+
}, async ({ feature: ref, full }, extra) => {
|
|
515
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
447
516
|
const st = await requireStore();
|
|
448
517
|
const features = (await st.loadFeatures()).features;
|
|
449
518
|
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
@@ -451,7 +520,7 @@ server.registerTool("planner-feature-show", {
|
|
|
451
520
|
return text(resolvedFeature.error);
|
|
452
521
|
const feature = resolvedFeature.feature;
|
|
453
522
|
if (full)
|
|
454
|
-
|
|
523
|
+
markFeatureReadForSessionId(plannerSessionId, feature.id);
|
|
455
524
|
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
456
525
|
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
|
|
457
526
|
return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
|
|
@@ -513,6 +582,9 @@ server.registerTool("planner-feature-update", {
|
|
|
513
582
|
if (!resolvedFeature.ok)
|
|
514
583
|
return text(resolvedFeature.error);
|
|
515
584
|
const feature = resolvedFeature.feature;
|
|
585
|
+
if (updates.status !== undefined) {
|
|
586
|
+
return derivedStatusReadOnlyResult("feature", formatFeatureRef(feature.number), updates.status, feature.status);
|
|
587
|
+
}
|
|
516
588
|
const updated = await st.updateFeatures((doc) => {
|
|
517
589
|
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
518
590
|
if (!target)
|
|
@@ -521,8 +593,6 @@ server.registerTool("planner-feature-update", {
|
|
|
521
593
|
target.name = updates.name.trim();
|
|
522
594
|
if (updates.description !== undefined)
|
|
523
595
|
target.description = updates.description.trim();
|
|
524
|
-
if (updates.status !== undefined)
|
|
525
|
-
target.status = updates.status;
|
|
526
596
|
if (updates.workDone !== undefined)
|
|
527
597
|
target.workDone = updates.workDone.trim();
|
|
528
598
|
if (updates.workRemaining !== undefined)
|
|
@@ -634,6 +704,7 @@ server.registerTool("planner-phase-add", {
|
|
|
634
704
|
handoffReadAt: "",
|
|
635
705
|
handoffHistory: [],
|
|
636
706
|
statusLog: [],
|
|
707
|
+
sessionInfo: [],
|
|
637
708
|
};
|
|
638
709
|
await st.savePhase(phase);
|
|
639
710
|
if (feature) {
|
|
@@ -653,14 +724,15 @@ server.registerTool("planner-phase-add", {
|
|
|
653
724
|
server.registerTool("planner-phase-show", {
|
|
654
725
|
description: "Show a phase by id or name, including derived linked requirements in the full view.",
|
|
655
726
|
inputSchema: { phase: z.string().min(1).describe("Phase 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 phase description. Default: compact identity only (saves tokens).") },
|
|
656
|
-
}, async ({ phase: ref, full }) => {
|
|
727
|
+
}, async ({ phase: ref, full }, extra) => {
|
|
728
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
657
729
|
const st = await requireStore();
|
|
658
730
|
const features = (await st.loadFeatures()).features;
|
|
659
731
|
const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
660
732
|
if (!phase)
|
|
661
733
|
return text(`Phase not found: ${ref}`);
|
|
662
734
|
if (full)
|
|
663
|
-
|
|
735
|
+
markPhaseReadForSessionId(plannerSessionId, phase.id);
|
|
664
736
|
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
665
737
|
const reqCount = linkedRequirements.length;
|
|
666
738
|
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"}` : ""})`;
|
|
@@ -738,11 +810,12 @@ server.registerTool("planner-phase-update", {
|
|
|
738
810
|
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
739
811
|
if (!found)
|
|
740
812
|
return text(`Phase not found: ${ref}`);
|
|
813
|
+
if (updates.status !== undefined) {
|
|
814
|
+
return derivedStatusReadOnlyResult("phase", formatPhaseRef(found.number, featureNumberOfPhase(found, features)), updates.status, found.status);
|
|
815
|
+
}
|
|
741
816
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
742
817
|
if (updates.title !== undefined)
|
|
743
818
|
entry.title = updates.title.trim();
|
|
744
|
-
if (updates.status !== undefined)
|
|
745
|
-
entry.status = updates.status;
|
|
746
819
|
if (updates.summary !== undefined)
|
|
747
820
|
entry.summary = updates.summary.trim();
|
|
748
821
|
if (updates.description !== undefined)
|
|
@@ -822,6 +895,7 @@ server.registerTool("planner-task-add", {
|
|
|
822
895
|
descriptionUpdatedAt: timestamp,
|
|
823
896
|
notes: "",
|
|
824
897
|
statusLog: [],
|
|
898
|
+
sessionInfo: [],
|
|
825
899
|
decisions: [],
|
|
826
900
|
acceptedDecisions: [],
|
|
827
901
|
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
|
|
@@ -846,13 +920,14 @@ server.registerTool("planner-task-add", {
|
|
|
846
920
|
server.registerTool("planner-task-show", {
|
|
847
921
|
description: "Show a task by id or name.",
|
|
848
922
|
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).") },
|
|
849
|
-
}, async ({ task: ref, full }) => {
|
|
923
|
+
}, async ({ task: ref, full }, extra) => {
|
|
924
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
850
925
|
const st = await requireStore();
|
|
851
926
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
852
927
|
if (!found)
|
|
853
928
|
return text(`Task not found: ${ref}`);
|
|
854
929
|
if (full)
|
|
855
|
-
|
|
930
|
+
markTaskReadForSessionId(plannerSessionId, found.task.id);
|
|
856
931
|
const features = (await st.loadFeatures()).features;
|
|
857
932
|
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))})`;
|
|
858
933
|
if (!full)
|
|
@@ -935,6 +1010,9 @@ server.registerTool("planner-task-update", {
|
|
|
935
1010
|
if (status === "in-progress" && status !== found.task.status) {
|
|
936
1011
|
return text("Task start/resume transitions require planner-task-start so lifecycle state, checkpoints, and return stacks remain consistent.");
|
|
937
1012
|
}
|
|
1013
|
+
if (status === "done" && status !== found.task.status) {
|
|
1014
|
+
return text("Task completion transitions require planner-task-complete with durable completion and verification evidence.");
|
|
1015
|
+
}
|
|
938
1016
|
// Validate motivation requirement for status transitions.
|
|
939
1017
|
if (status !== undefined && needsMotivation(found.task.status, status)) {
|
|
940
1018
|
if (!motivation || !motivation.trim()) {
|
|
@@ -1163,7 +1241,8 @@ server.registerTool("planner-task-pause", {
|
|
|
1163
1241
|
how_to_resume: z.string().min(1),
|
|
1164
1242
|
paused_by: z.string().optional(),
|
|
1165
1243
|
},
|
|
1166
|
-
}, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }) => {
|
|
1244
|
+
}, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }, extra) => {
|
|
1245
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1167
1246
|
const st = await requireStore();
|
|
1168
1247
|
const features = (await st.loadFeatures()).features;
|
|
1169
1248
|
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
@@ -1176,7 +1255,7 @@ server.registerTool("planner-task-pause", {
|
|
|
1176
1255
|
resumeLocation: resume_location, howToResume: how_to_resume, relatedTaskId: "",
|
|
1177
1256
|
pausedAt: nowISO(), pausedBy: paused_by?.trim() ?? "",
|
|
1178
1257
|
};
|
|
1179
|
-
invalidateReads();
|
|
1258
|
+
invalidateReads(plannerSessionId);
|
|
1180
1259
|
const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
|
|
1181
1260
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
1182
1261
|
const activeDeviation = (await st.loadProject()).workDeviations
|
|
@@ -1200,7 +1279,7 @@ server.registerTool("planner-task-pause", {
|
|
|
1200
1279
|
].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
|
|
1201
1280
|
});
|
|
1202
1281
|
server.registerTool("planner-task-switch", {
|
|
1203
|
-
description: "Atomically switch from active work to a temporary task, even outside normal priority order.
|
|
1282
|
+
description: "Atomically switch from active work to a temporary task, even outside normal priority order. Call this tool first so valid sessionInfo context can be reused; on denial, perform only the missing/stale reads listed in nextActions and retry. Pause the source with a mandatory snapshot, start the target, and push a durable LIFO return target.",
|
|
1204
1283
|
inputSchema: {
|
|
1205
1284
|
from_task: z.string().min(1),
|
|
1206
1285
|
to_task: z.string().min(1),
|
|
@@ -1210,7 +1289,8 @@ server.registerTool("planner-task-switch", {
|
|
|
1210
1289
|
how_to_resume: z.string().min(1),
|
|
1211
1290
|
switched_by: z.string().optional(),
|
|
1212
1291
|
},
|
|
1213
|
-
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
|
|
1292
|
+
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }, extra) => {
|
|
1293
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1214
1294
|
const st = await requireStore();
|
|
1215
1295
|
const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
|
|
1216
1296
|
const features = featuresDoc.features;
|
|
@@ -1225,15 +1305,37 @@ server.registerTool("planner-task-switch", {
|
|
|
1225
1305
|
if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
|
|
1226
1306
|
return text(`Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.`);
|
|
1227
1307
|
}
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1308
|
+
const targetFeature = target.phase.featureId ? features.find((feature) => feature.id === target.phase.featureId) : undefined;
|
|
1309
|
+
const linkedRequirements = [
|
|
1310
|
+
...(await st.linkedRequirementsForPhase(target.phase.id)),
|
|
1311
|
+
...(target.phase.featureId ? await st.linkedRequirementsForFeature(target.phase.featureId) : []),
|
|
1312
|
+
].filter((requirement, index, all) => all.findIndex((candidate) => candidate.id === requirement.id) === index);
|
|
1313
|
+
const linkedRequirementIds = linkedRequirements.map((requirement) => requirement.id);
|
|
1314
|
+
const contextInput = {
|
|
1315
|
+
sessionId: plannerSessionId,
|
|
1316
|
+
taskId: target.task.id,
|
|
1317
|
+
phaseId: target.phase.id,
|
|
1318
|
+
...(target.phase.featureId ? { featureId: target.phase.featureId } : {}),
|
|
1319
|
+
task: target.task,
|
|
1320
|
+
phase: target.phase,
|
|
1321
|
+
...(targetFeature ? { feature: targetFeature } : {}),
|
|
1322
|
+
requirements: linkedRequirements,
|
|
1323
|
+
requirementIds: linkedRequirementIds,
|
|
1324
|
+
};
|
|
1325
|
+
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1326
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
1327
|
+
const targetTaskRef = taskCompositeRef(target.task, target.phase, features);
|
|
1328
|
+
const targetPhaseRef = formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features));
|
|
1329
|
+
const targetFeatureRef = targetFeature ? formatFeatureRef(targetFeature.number) : "";
|
|
1330
|
+
if (!contextEligibility.eligible) {
|
|
1331
|
+
const nextActions = mcpContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, !requirementsReady, `Retry planner-task-switch to ${targetTaskRef}`);
|
|
1332
|
+
return text(`Task switch denied: context reads required. ${contextEligibility.reason}`, { ...contextEligibility, nextActions });
|
|
1333
|
+
}
|
|
1334
|
+
if (!requirementsReady) {
|
|
1335
|
+
return text("Task switch denied: read the requirements linked to the target phase and feature before switching.", { requirementIds: linkedRequirementIds, nextActions: ["planner-requirement-list", `Retry planner-task-switch to ${targetTaskRef}`] });
|
|
1336
|
+
}
|
|
1337
|
+
if (!hasValidSessionAttestation(contextInput)) {
|
|
1338
|
+
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: target.phase.id, taskId: target.task.id, ...(target.phase.featureId ? { featureId: target.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
1237
1339
|
}
|
|
1238
1340
|
const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
|
|
1239
1341
|
if (!eligibility.eligible)
|
|
@@ -1323,7 +1425,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1323
1425
|
await st.syncTaskStatusRollup(source.phase.id);
|
|
1324
1426
|
if (target.phase.id !== source.phase.id)
|
|
1325
1427
|
await st.syncTaskStatusRollup(target.phase.id);
|
|
1326
|
-
invalidateReads();
|
|
1428
|
+
invalidateReads(plannerSessionId);
|
|
1327
1429
|
return writeAndSummarize(st, [
|
|
1328
1430
|
`🔀 Task switched: ${taskCompositeRef(source.task, source.phase, features)} → ${taskCompositeRef(target.task, target.phase, features)}`,
|
|
1329
1431
|
`Resume checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
@@ -1332,40 +1434,68 @@ server.registerTool("planner-task-switch", {
|
|
|
1332
1434
|
].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
|
|
1333
1435
|
});
|
|
1334
1436
|
server.registerTool("planner-task-start", {
|
|
1335
|
-
description: "Set a task to in-progress or resume checkpointed work.
|
|
1437
|
+
description: "Set a task to in-progress or resume checkpointed work. Call this tool before reading context so it can reuse valid sessionInfo attestations; if denied, perform only the missing/stale reads listed in nextActions, in order, and retry. Every denied start is an MCP isError result with started=false; NEVER claim work started unless structuredContent.started is true. Success is returned only after persisted in-progress state is verified.",
|
|
1336
1438
|
inputSchema: { task: z.string().min(1) },
|
|
1337
|
-
}, async ({ task: ref }) => {
|
|
1338
|
-
const
|
|
1439
|
+
}, async ({ task: ref }, extra) => {
|
|
1440
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1441
|
+
let st;
|
|
1442
|
+
try {
|
|
1443
|
+
st = await requireStore();
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
return taskStartError(taskStartDenied("PLAN_NOT_FOUND", "No .planner/ found.", ["Initialize or load the planner, then retry planner-task-start."]));
|
|
1447
|
+
}
|
|
1339
1448
|
const features = (await st.loadFeatures()).features;
|
|
1340
1449
|
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
1341
1450
|
if (!found)
|
|
1342
|
-
return
|
|
1343
|
-
|
|
1344
|
-
|
|
1451
|
+
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."]));
|
|
1452
|
+
const taskRef = taskCompositeRef(found.task, found.phase, features);
|
|
1453
|
+
const phaseRef = formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features));
|
|
1454
|
+
const parentFeature = found.phase.featureId ? features.find((candidate) => candidate.id === found.phase.featureId) : undefined;
|
|
1455
|
+
const featureRef = parentFeature ? formatFeatureRef(parentFeature.number) : "";
|
|
1345
1456
|
if (found.task.status === "done")
|
|
1346
|
-
return
|
|
1347
|
-
const
|
|
1457
|
+
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 }));
|
|
1458
|
+
const linkedRequirements = [
|
|
1459
|
+
...(await st.linkedRequirementsForPhase(found.phase.id)),
|
|
1460
|
+
...(found.phase.featureId ? await st.linkedRequirementsForFeature(found.phase.featureId) : []),
|
|
1461
|
+
].filter((requirement, index, all) => all.findIndex((candidate) => candidate.id === requirement.id) === index);
|
|
1462
|
+
const linkedRequirementIds = linkedRequirements.map((requirement) => requirement.id);
|
|
1463
|
+
const contextInput = {
|
|
1464
|
+
sessionId: plannerSessionId,
|
|
1465
|
+
taskId: found.task.id,
|
|
1466
|
+
phaseId: found.phase.id,
|
|
1467
|
+
...(found.phase.featureId ? { featureId: found.phase.featureId } : {}),
|
|
1468
|
+
task: found.task,
|
|
1469
|
+
phase: found.phase,
|
|
1470
|
+
...(parentFeature ? { feature: parentFeature } : {}),
|
|
1471
|
+
requirements: linkedRequirements,
|
|
1472
|
+
requirementIds: linkedRequirementIds,
|
|
1473
|
+
};
|
|
1474
|
+
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1475
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
1348
1476
|
if (!contextEligibility.eligible)
|
|
1349
|
-
return
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1477
|
+
return taskStartError(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, mcpContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, !requirementsReady, `Retry planner-task-start ${taskRef}`), { taskId: found.task.id }), { contextEligibility });
|
|
1478
|
+
if (!requirementsReady) {
|
|
1479
|
+
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 }));
|
|
1480
|
+
}
|
|
1481
|
+
if (!hasValidSessionAttestation(contextInput)) {
|
|
1482
|
+
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: found.phase.id, taskId: found.task.id, ...(found.phase.featureId ? { featureId: found.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
1483
|
+
}
|
|
1484
|
+
if (found.task.status === "in-progress") {
|
|
1485
|
+
const outcome = taskStartSucceeded(found.task.id, true);
|
|
1486
|
+
return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true`, { ...outcome, task: found.task });
|
|
1356
1487
|
}
|
|
1357
1488
|
const [project, phases, focus] = await Promise.all([st.loadProject(), st.loadAllPhases(), st.loadResume()]);
|
|
1358
1489
|
const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
|
|
1359
1490
|
if (!eligibility.eligible)
|
|
1360
|
-
return
|
|
1491
|
+
return taskStartError(taskStartDenied("START_NOT_ALLOWED", eligibility.reason, ["Resolve the reported task readiness condition, then retry planner-task-start."], { taskId: found.task.id }), { eligibility });
|
|
1361
1492
|
const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
|
|
1362
1493
|
if (selection.kind === "conflict") {
|
|
1363
|
-
return
|
|
1364
|
-
activeTaskIds: selection.activeCandidates?.map((candidate) => candidate.task.id) ?? [],
|
|
1365
|
-
});
|
|
1494
|
+
return taskStartError(taskStartDenied("ACTIVE_TASK_CONFLICT", selection.reason, ["Pause or reconcile the active tasks.", `Retry planner-task-start ${taskRef}.`], { taskId: found.task.id }), { selection });
|
|
1366
1495
|
}
|
|
1367
1496
|
if (selection.kind === "active" && selection.candidate?.task.id !== found.task.id) {
|
|
1368
|
-
|
|
1497
|
+
const activeRef = taskCompositeRef(selection.candidate.task, selection.candidate.phase, features);
|
|
1498
|
+
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 });
|
|
1369
1499
|
}
|
|
1370
1500
|
let resumeProposal;
|
|
1371
1501
|
if (selection.kind === "resume" && selection.candidate && selection.candidate.task.id !== found.task.id) {
|
|
@@ -1389,7 +1519,6 @@ server.registerTool("planner-task-start", {
|
|
|
1389
1519
|
});
|
|
1390
1520
|
}
|
|
1391
1521
|
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
1392
|
-
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
1393
1522
|
const [phaseWithRequirements, featureRequirements] = await Promise.all([
|
|
1394
1523
|
st.loadPhaseWithRequirements(found.phase.id),
|
|
1395
1524
|
parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
|
|
@@ -1444,23 +1573,33 @@ server.registerTool("planner-task-start", {
|
|
|
1444
1573
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1445
1574
|
if (resumedDeviation)
|
|
1446
1575
|
await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
|
|
1447
|
-
|
|
1448
|
-
|
|
1576
|
+
await st.writeGenerated();
|
|
1577
|
+
const persistedTask = (await st.loadPhase(found.phase.id)).tasks.find((candidate) => candidate.id === found.task.id);
|
|
1578
|
+
if (!updatedTask || persistedTask?.status !== "in-progress") {
|
|
1579
|
+
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 }));
|
|
1580
|
+
}
|
|
1581
|
+
const outcome = taskStartSucceeded(persistedTask.id);
|
|
1582
|
+
return text(`✅ Task started: ${taskRef} — ${persistedTask.title} (in-progress)${persistedTask.shortId ? ` · ${persistedTask.shortId}` : ""}\nstarted: true${phaseContext}${advisory}`, {
|
|
1583
|
+
...outcome,
|
|
1584
|
+
task: persistedTask,
|
|
1449
1585
|
...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
|
|
1450
1586
|
});
|
|
1451
1587
|
});
|
|
1452
1588
|
server.registerTool("planner-task-complete", {
|
|
1453
|
-
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
1589
|
+
description: "Set a task to done with mandatory durable completion and verification evidence. Fails if checklist is incomplete unless force=true.",
|
|
1454
1590
|
inputSchema: {
|
|
1455
1591
|
task: z.string().min(1),
|
|
1456
1592
|
force: z.boolean().optional(),
|
|
1457
|
-
description_update: z.string().min(10).
|
|
1593
|
+
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."),
|
|
1458
1594
|
},
|
|
1459
1595
|
}, async ({ task: ref, force, description_update }) => {
|
|
1460
1596
|
const st = await requireStore();
|
|
1461
1597
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
1462
1598
|
if (!found)
|
|
1463
1599
|
return text(`Task not found: ${ref}`);
|
|
1600
|
+
const completionSummary = description_update.trim();
|
|
1601
|
+
if (completionSummary.length < 10)
|
|
1602
|
+
return text("Task completion denied: provide at least 10 characters of durable completion and verification evidence.");
|
|
1464
1603
|
if (found.task.pauseSnapshot)
|
|
1465
1604
|
return text("Task completion denied: resume checkpointed work with planner-task-start before completing it, or cancel it explicitly with motivation.");
|
|
1466
1605
|
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
@@ -1480,13 +1619,12 @@ server.registerTool("planner-task-complete", {
|
|
|
1480
1619
|
fromStatus: previousStatus,
|
|
1481
1620
|
toStatus: "done",
|
|
1482
1621
|
title: `${previousStatus} → done`,
|
|
1483
|
-
description:
|
|
1622
|
+
description: completionSummary,
|
|
1484
1623
|
};
|
|
1485
1624
|
task.statusLog = [...(task.statusLog ?? []), entry];
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
}
|
|
1625
|
+
const sep = task.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
|
|
1626
|
+
task.description = task.description + sep + completionSummary;
|
|
1627
|
+
task.descriptionUpdatedAt = timestamp;
|
|
1490
1628
|
task.updatedAt = timestamp;
|
|
1491
1629
|
phase.updatedAt = timestamp;
|
|
1492
1630
|
updatedTask = task;
|
|
@@ -1545,60 +1683,137 @@ server.registerTool("planner-handoff-show", {
|
|
|
1545
1683
|
server.registerTool("planner-requirement-list", {
|
|
1546
1684
|
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).",
|
|
1547
1685
|
inputSchema: {},
|
|
1548
|
-
}, async () => {
|
|
1686
|
+
}, async (_args, extra) => {
|
|
1687
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1549
1688
|
const st = await requireStore();
|
|
1550
1689
|
const requirements = await st.loadRequirements();
|
|
1551
|
-
requirements.requirements.forEach((req) =>
|
|
1690
|
+
requirements.requirements.forEach((req) => markRequirementReadForSessionId(plannerSessionId, req.id));
|
|
1552
1691
|
return text(requirements.requirements.map((req) => `- ${req.id} — ${req.title} (${req.status})`).join("\n") || "No requirements", { requirements });
|
|
1553
1692
|
});
|
|
1554
1693
|
server.registerTool("planner-handoff-write", {
|
|
1555
|
-
description: "
|
|
1694
|
+
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.",
|
|
1556
1695
|
inputSchema: {
|
|
1557
1696
|
phaseRef: z.string().min(1).describe("Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title."),
|
|
1558
|
-
title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work
|
|
1697
|
+
title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work."),
|
|
1559
1698
|
confirmed: z.boolean().describe("Set true only after the user explicitly confirms the proposed feature+phase target."),
|
|
1560
|
-
content: z.string().min(1).describe("
|
|
1699
|
+
content: z.string().min(1).describe("Full reconciled handoff text (markdown)."),
|
|
1700
|
+
expectedHandoffUpdatedAt: z.string().optional().describe("Exact token returned by planner-handoff-prepare; empty when no handoff exists."),
|
|
1701
|
+
reconciledExistingHandoff: z.boolean().optional().describe("Confirm that still-relevant existing handoff information was retained."),
|
|
1702
|
+
taskUpdates: z.array(z.object({
|
|
1703
|
+
taskRef: z.string().min(1),
|
|
1704
|
+
completionSummary: z.string().min(1),
|
|
1705
|
+
verification: z.string().min(1),
|
|
1706
|
+
remainingWork: z.string().min(1),
|
|
1707
|
+
filesTouched: z.array(z.string().min(1)).optional(),
|
|
1708
|
+
decisions: z.array(z.string().min(1)).optional(),
|
|
1709
|
+
})).optional(),
|
|
1710
|
+
phaseUpdate: z.object({
|
|
1711
|
+
progressSummary: z.string().min(1),
|
|
1712
|
+
remainingWork: z.string().min(1),
|
|
1713
|
+
decisions: z.array(z.string().min(1)).optional(),
|
|
1714
|
+
}).optional(),
|
|
1715
|
+
phaseNoUpdateReason: z.string().min(1).optional(),
|
|
1716
|
+
featureUpdate: z.object({ workDone: z.string().min(1), workRemaining: z.string().min(1) }).optional(),
|
|
1717
|
+
featureNoUpdateReason: z.string().min(1).optional(),
|
|
1561
1718
|
},
|
|
1562
|
-
}, async ({ phaseRef, title, confirmed, content }) => {
|
|
1719
|
+
}, async ({ phaseRef, title, confirmed, content, expectedHandoffUpdatedAt, reconciledExistingHandoff, taskUpdates, phaseUpdate, phaseNoUpdateReason, featureUpdate, featureNoUpdateReason }) => {
|
|
1563
1720
|
const st = await requireStore();
|
|
1564
1721
|
let body = content.trim();
|
|
1565
|
-
const firstLine = body.split(/\r?\n/).find((
|
|
1722
|
+
const firstLine = body.split(/\r?\n/).find((line) => line.trim().length > 0) ?? "";
|
|
1566
1723
|
const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
if (isGeneric) {
|
|
1571
|
-
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.");
|
|
1724
|
+
const effectiveHeadingText = title?.trim() || firstHeadingText;
|
|
1725
|
+
if (!effectiveHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(effectiveHeadingText)) {
|
|
1726
|
+
return text("❌ Generic handoff title. Provide a meaningful title summarizing the work.");
|
|
1572
1727
|
}
|
|
1573
|
-
if (title
|
|
1574
|
-
const t = title.trim();
|
|
1728
|
+
if (title?.trim()) {
|
|
1575
1729
|
const lines = body.split(/\r?\n/);
|
|
1576
|
-
const firstIdx = lines.findIndex((
|
|
1577
|
-
if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? ""))
|
|
1578
|
-
lines[firstIdx] = `# ${
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
body = `# ${t}\n\n${body}`;
|
|
1583
|
-
}
|
|
1730
|
+
const firstIdx = lines.findIndex((line) => line.trim().length > 0);
|
|
1731
|
+
if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? ""))
|
|
1732
|
+
lines[firstIdx] = `# ${title.trim()}`;
|
|
1733
|
+
else
|
|
1734
|
+
lines.unshift(`# ${title.trim()}`, "");
|
|
1735
|
+
body = lines.join("\n");
|
|
1584
1736
|
}
|
|
1585
1737
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1586
1738
|
if (!r.ok)
|
|
1587
1739
|
return text(`❌ ${r.error}`);
|
|
1588
1740
|
if (!confirmed)
|
|
1589
|
-
return text(`Proposal only: I would
|
|
1590
|
-
|
|
1591
|
-
|
|
1741
|
+
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 });
|
|
1742
|
+
if (expectedHandoffUpdatedAt === undefined)
|
|
1743
|
+
return text("❌ Run planner-handoff-prepare first and pass expectedHandoffUpdatedAt.");
|
|
1744
|
+
const phases = await st.loadAllPhases();
|
|
1745
|
+
const features = (await st.loadFeatures()).features;
|
|
1746
|
+
const resolvedTaskUpdates = [];
|
|
1747
|
+
for (const update of taskUpdates ?? []) {
|
|
1748
|
+
const found = findTaskByRef(phases, features, update.taskRef.trim());
|
|
1749
|
+
if (!found || found.phase.id !== r.phase.id)
|
|
1750
|
+
return text(`❌ Task ${update.taskRef} does not belong to ${r.compositeRef}.`);
|
|
1751
|
+
resolvedTaskUpdates.push({
|
|
1752
|
+
taskId: found.task.id,
|
|
1753
|
+
completionSummary: update.completionSummary,
|
|
1754
|
+
verification: update.verification,
|
|
1755
|
+
remainingWork: update.remainingWork,
|
|
1756
|
+
...(update.filesTouched ? { filesTouched: update.filesTouched } : {}),
|
|
1757
|
+
...(update.decisions ? { decisions: update.decisions } : {}),
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
try {
|
|
1761
|
+
const result = await st.refreshPhaseHandoff(r.phase.id, {
|
|
1762
|
+
content: body,
|
|
1763
|
+
expectedHandoffUpdatedAt,
|
|
1764
|
+
reconciledExistingHandoff: reconciledExistingHandoff === true,
|
|
1765
|
+
contextSync: {
|
|
1766
|
+
taskUpdates: resolvedTaskUpdates,
|
|
1767
|
+
...(phaseUpdate ? { phaseUpdate: {
|
|
1768
|
+
progressSummary: phaseUpdate.progressSummary,
|
|
1769
|
+
remainingWork: phaseUpdate.remainingWork,
|
|
1770
|
+
...(phaseUpdate.decisions ? { decisions: phaseUpdate.decisions } : {}),
|
|
1771
|
+
} } : {}),
|
|
1772
|
+
...(phaseNoUpdateReason ? { phaseNoUpdateReason } : {}),
|
|
1773
|
+
...(featureUpdate ? { featureUpdate } : {}),
|
|
1774
|
+
...(featureNoUpdateReason ? { featureNoUpdateReason } : {}),
|
|
1775
|
+
},
|
|
1776
|
+
});
|
|
1777
|
+
await st.writeGenerated();
|
|
1778
|
+
return text(`✅ Reconciled handoff and durable context on ${r.compositeRef}; updated ${result.updatedTaskIds.length} task(s).`, { phaseRef: r.compositeRef, phaseId: r.phase.id, ...result });
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1782
|
+
return text(`❌ Handoff refresh denied: ${message}`, { error: message });
|
|
1783
|
+
}
|
|
1592
1784
|
});
|
|
1593
1785
|
server.registerTool("planner-handoff-prepare", {
|
|
1594
|
-
description: "
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1786
|
+
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.",
|
|
1787
|
+
inputSchema: { phaseRef: z.string().min(1).optional() },
|
|
1788
|
+
}, async ({ phaseRef }) => {
|
|
1789
|
+
if (!phaseRef)
|
|
1790
|
+
return text([
|
|
1791
|
+
"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.",
|
|
1792
|
+
"Tell the user: 'I propose writing this handoff on P00x(F00x) — <phase title>. Confirm?' Wait for explicit confirmation.",
|
|
1793
|
+
"After confirmation, call planner-handoff-prepare again with that exact phaseRef, then reconcile its current handoff and missing task evidence through planner-handoff-write.",
|
|
1794
|
+
].join("\n"));
|
|
1795
|
+
const st = await requireStore();
|
|
1796
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1797
|
+
if (!r.ok)
|
|
1798
|
+
return text(`❌ ${r.error}`);
|
|
1799
|
+
try {
|
|
1800
|
+
const audit = await st.preparePhaseHandoff(r.phase.id);
|
|
1801
|
+
const missing = audit.missingCompletionTasks.map((task) => `- T${String(task.number).padStart(3, "0")} — ${task.title}`).join("\n") || "- None";
|
|
1802
|
+
return text([
|
|
1803
|
+
`Handoff preparation audit for ${r.compositeRef}`,
|
|
1804
|
+
`Base handoffUpdatedAt: ${audit.handoffUpdatedAt || "(empty)"}`,
|
|
1805
|
+
"Done tasks missing durable completion/verification evidence:",
|
|
1806
|
+
missing,
|
|
1807
|
+
"",
|
|
1808
|
+
"Existing active handoff (reconcile all still-relevant content):",
|
|
1809
|
+
audit.handoff.trim() || "(none)",
|
|
1810
|
+
].join("\n"), { phaseRef: r.compositeRef, ...audit });
|
|
1811
|
+
}
|
|
1812
|
+
catch (error) {
|
|
1813
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1814
|
+
return text(`❌ Handoff preparation failed: ${message}`, { error: message });
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1602
1817
|
server.registerTool("planner-handoff-clear", {
|
|
1603
1818
|
description: "Clear/archive the entity-scoped handoff of a phase. phaseRef is required; never infer a phase automatically.",
|
|
1604
1819
|
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.25",
|
|
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.25",
|
|
47
|
+
"@agent-plan/server": "0.2.25"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|