@agent-plan/mcp 0.2.19-next.20 → 0.2.19-next.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +107 -39
- 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":";AAgwCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, migrateToGlobalSequence, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock } from "@agent-plan/core";
|
|
7
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, migrateToGlobalSequence, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask } from "@agent-plan/core";
|
|
8
8
|
import { serve } from "@agent-plan/server";
|
|
9
9
|
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
10
10
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
@@ -137,16 +137,12 @@ async function resolvePhaseForHandoff(st, ref) {
|
|
|
137
137
|
if (ref && ref.trim()) {
|
|
138
138
|
phase = findPhaseByRef(phases, features, ref.trim());
|
|
139
139
|
}
|
|
140
|
-
else {
|
|
141
|
-
phase = phases.find((p) => p.status === "in-progress")
|
|
142
|
-
?? phases.find((p) => p.tasks.some((t) => t.status === "in-progress"));
|
|
143
|
-
}
|
|
144
140
|
if (!phase) {
|
|
145
141
|
return {
|
|
146
142
|
ok: false,
|
|
147
143
|
error: ref && ref.trim()
|
|
148
144
|
? `Phase not found: "${ref.trim()}". Use P00x, P00x(F00x), UUID, or title.`
|
|
149
|
-
: "
|
|
145
|
+
: "A phaseRef is required. First identify the exact feature and phase with the user, then pass P00x or P00x(F00x).",
|
|
150
146
|
};
|
|
151
147
|
}
|
|
152
148
|
const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
|
|
@@ -248,7 +244,7 @@ server.registerTool("planner-repair", {
|
|
|
248
244
|
}, async () => {
|
|
249
245
|
const st = await requireStore();
|
|
250
246
|
const report = await st.repair();
|
|
251
|
-
return text(`Repair done: renamed ${report.migrated.renamed}, repaired ${report.migrated.repaired} refs, inferred ${report.migrated.inferred}. Containment: ${report.containment.changed} phase files rewritten (${report.containment.tasks} tasks scanned, ${report.containment.orphan} orphan). Integrity: ${report.integrity.duplicatePhaseIds.length} duplicate, ${report.integrity.danglingPhaseIds.length} dangling.`, { report });
|
|
247
|
+
return text(`Repair done: renamed ${report.migrated.renamed}, repaired ${report.migrated.repaired} refs, inferred ${report.migrated.inferred}. Containment: ${report.containment.changed} phase files rewritten (${report.containment.tasks} tasks scanned, ${report.containment.orphan} orphan). Handoffs: archived ${report.handoffs.archived} stale completed/canceled handoff(s). Integrity: ${report.integrity.duplicatePhaseIds.length} duplicate, ${report.integrity.danglingPhaseIds.length} dangling.`, { report });
|
|
252
248
|
});
|
|
253
249
|
server.registerTool("planner-cleanup-orphan-phases", {
|
|
254
250
|
description: "Discover phase files that no longer resolve to a valid owning feature, and optionally delete them. Run once with confirm=false (default) to inspect, then rerun with confirm=true to remove them.",
|
|
@@ -653,7 +649,18 @@ server.registerTool("planner-phase-show", {
|
|
|
653
649
|
const requirementsBlock = reqCount > 0
|
|
654
650
|
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
655
651
|
: "";
|
|
656
|
-
return text(full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary, {
|
|
652
|
+
return text(full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary, {
|
|
653
|
+
phase: {
|
|
654
|
+
ref: formatPhaseRef(phase.number, featureNumberOfPhase(phase, features)),
|
|
655
|
+
shortId: phase.shortId,
|
|
656
|
+
title: phase.title,
|
|
657
|
+
summary: phase.summary,
|
|
658
|
+
status: phase.status,
|
|
659
|
+
taskCount: phase.tasks.length,
|
|
660
|
+
...(full ? { description: phase.description } : {}),
|
|
661
|
+
},
|
|
662
|
+
linkedRequirements,
|
|
663
|
+
});
|
|
657
664
|
});
|
|
658
665
|
server.registerTool("planner-phase-discuss", {
|
|
659
666
|
description: "Persist phase discovery fields and mark phase planned.",
|
|
@@ -1039,27 +1046,79 @@ server.registerTool("planner-task-delete", {
|
|
|
1039
1046
|
const tdFeatures = (await st.loadFeatures()).features;
|
|
1040
1047
|
return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
|
|
1041
1048
|
});
|
|
1049
|
+
server.registerTool("planner-task-recommend", {
|
|
1050
|
+
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.",
|
|
1051
|
+
inputSchema: {},
|
|
1052
|
+
}, async () => {
|
|
1053
|
+
const st = await requireStore();
|
|
1054
|
+
const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1055
|
+
const result = recommendNextTask(features.features, phases, project.workDeviations);
|
|
1056
|
+
if (!result.candidate)
|
|
1057
|
+
return text(`No task recommendation: ${result.reason}`, { kind: result.kind, reason: result.reason, activeTaskIds: result.activeCandidates?.map((candidate) => candidate.task.id) ?? [] });
|
|
1058
|
+
const { candidate } = result;
|
|
1059
|
+
const ref = taskCompositeRef(candidate.task, candidate.phase, features.features);
|
|
1060
|
+
return text(`Recommended (${result.kind}): ${ref} — ${candidate.task.title}\n${result.reason}${result.deviation ? `\nDeviation: ${result.deviation.id}; resume target ${result.deviation.resumeTaskId}.` : ""}`, {
|
|
1061
|
+
kind: result.kind, taskId: candidate.task.id, phaseId: candidate.phase.id, featureId: candidate.feature?.id, deviation: result.deviation,
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
server.registerTool("planner-task-deviation", {
|
|
1065
|
+
description: "Record a user-approved temporary task deviation. It preserves the recommended/resume task and never starts, pauses, or blocks work; use normal lifecycle tools for those transitions.",
|
|
1066
|
+
inputSchema: { temporary_task: z.string().min(1), resume_task: z.string().min(1).optional(), reason: z.string().min(1) },
|
|
1067
|
+
}, async ({ temporary_task, resume_task, reason }) => {
|
|
1068
|
+
const st = await requireStore();
|
|
1069
|
+
const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1070
|
+
const temporary = findTaskByRef(phases, features.features, temporary_task);
|
|
1071
|
+
if (!temporary)
|
|
1072
|
+
return text(`Task not found: ${temporary_task}`);
|
|
1073
|
+
const selected = recommendNextTask(features.features, phases, project.workDeviations);
|
|
1074
|
+
const resume = resume_task ? findTaskByRef(phases, features.features, resume_task) : selected.candidate;
|
|
1075
|
+
if (!resume)
|
|
1076
|
+
return text(`No resume task is available. Provide resume_task explicitly. ${selected.reason}`);
|
|
1077
|
+
if (temporary.task.id === resume.task.id)
|
|
1078
|
+
return text("A temporary task must differ from its resume target.");
|
|
1079
|
+
if (temporary.task.status !== "planned")
|
|
1080
|
+
return text(`Temporary task is not startable: ${temporary.task.status}.`);
|
|
1081
|
+
const timestamp = nowISO();
|
|
1082
|
+
const record = { id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id, temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason, requestedBy: "user", approvedBy: "user", state: "approved", createdAt: timestamp, activatedAt: "", resolvedAt: "" };
|
|
1083
|
+
await st.addWorkDeviation(record);
|
|
1084
|
+
return writeAndSummarize(st, `✅ Approved deviation: ${taskCompositeRef(temporary.task, temporary.phase, features.features)} temporarily overrides ${taskCompositeRef(resume.task, resume.phase, features.features)}. Resume target retained.`, { deviation: record });
|
|
1085
|
+
});
|
|
1042
1086
|
server.registerTool("planner-task-start", {
|
|
1043
|
-
description: "Set a task to in-progress.",
|
|
1087
|
+
description: "Set a task to in-progress. BEFORE calling, use planner-task-show with full=true and planner-phase-show with full=true for the parent phase, including linked requirements. Continue an already in-progress task; otherwise select ready work by feature → phase → task priority (lower number first), respecting dependencies and blocked/waiting states. Returns a phase/requirements briefing.",
|
|
1044
1088
|
inputSchema: { task: z.string().min(1) },
|
|
1045
1089
|
}, async ({ task: ref }) => {
|
|
1046
1090
|
const st = await requireStore();
|
|
1047
|
-
const
|
|
1091
|
+
const features = (await st.loadFeatures()).features;
|
|
1092
|
+
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
1048
1093
|
if (!found)
|
|
1049
1094
|
return text(`Task not found: ${ref}`);
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1095
|
+
if (found.task.status === "in-progress")
|
|
1096
|
+
return text(`Task ${found.task.id} is already in-progress.`);
|
|
1097
|
+
if (found.task.status === "done")
|
|
1098
|
+
return text(`Task ${found.task.id} is done. Use planner-task-update to reopen.`);
|
|
1099
|
+
const project = await st.loadProject();
|
|
1100
|
+
const phases = await st.loadAllPhases();
|
|
1101
|
+
const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
|
|
1102
|
+
if (!eligibility.eligible)
|
|
1103
|
+
return text(`Task start denied: ${eligibility.reason}`, { reason: eligibility.reason });
|
|
1104
|
+
const selection = recommendNextTask(features, phases, project.workDeviations);
|
|
1105
|
+
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
1106
|
+
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
1107
|
+
const [phaseWithRequirements, featureRequirements] = await Promise.all([
|
|
1108
|
+
st.loadPhaseWithRequirements(found.phase.id),
|
|
1109
|
+
parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
|
|
1110
|
+
]);
|
|
1111
|
+
const phaseContext = buildPhaseContextBlock(phaseWithRequirements, parentFeature, phaseWithRequirements.linkedRequirements ?? [], featureRequirements);
|
|
1112
|
+
// A pending handoff is context, not a lock: task start RETAINS it. It is
|
|
1113
|
+
// archived only when the phase completes (auto) or the user explicitly
|
|
1114
|
+
// clears it.
|
|
1115
|
+
const advisory = selection.kind === "conflict"
|
|
1116
|
+
? `\n\n⚠️ ${selection.reason} Explicit task request honored; reconcile active work before relying on automatic recommendations.`
|
|
1117
|
+
: selection.candidate && selection.candidate.task.id !== found.task.id
|
|
1118
|
+
? selection.kind === "active"
|
|
1119
|
+
? `\n\n⚠️ Active-task advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is already in progress. Explicit task request honored.`
|
|
1120
|
+
: `\n\n⚠️ Priority advisory: ${taskCompositeRef(selection.candidate.task, selection.candidate.phase, features)} is the automatic recommendation. Explicit task request honored.`
|
|
1121
|
+
: "";
|
|
1063
1122
|
const timestamp = nowISO();
|
|
1064
1123
|
let updatedTask;
|
|
1065
1124
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -1084,11 +1143,11 @@ server.registerTool("planner-task-start", {
|
|
|
1084
1143
|
return phase;
|
|
1085
1144
|
});
|
|
1086
1145
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
1087
|
-
const
|
|
1146
|
+
const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === found.task.id && deviation.state === "approved");
|
|
1147
|
+
if (approvedDeviation)
|
|
1148
|
+
await st.setWorkDeviationState(approvedDeviation.id, "active", timestamp);
|
|
1088
1149
|
const t = updatedTask ?? found.task;
|
|
1089
|
-
|
|
1090
|
-
const phaseContext = buildPhaseContextBlock(found.phase, parentFeature);
|
|
1091
|
-
return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}`);
|
|
1150
|
+
return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}${advisory}`);
|
|
1092
1151
|
});
|
|
1093
1152
|
server.registerTool("planner-task-complete", {
|
|
1094
1153
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -1133,6 +1192,11 @@ server.registerTool("planner-task-complete", {
|
|
|
1133
1192
|
return phase;
|
|
1134
1193
|
});
|
|
1135
1194
|
const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
|
|
1195
|
+
const completedDeviation = (await st.loadProject()).workDeviations
|
|
1196
|
+
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === found.task.id)
|
|
1197
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1198
|
+
if (completedDeviation)
|
|
1199
|
+
await st.setWorkDeviationState(completedDeviation.id, "resolved", timestamp);
|
|
1136
1200
|
const features = (await st.loadFeatures()).features;
|
|
1137
1201
|
const t = updatedTask ?? found.task;
|
|
1138
1202
|
return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}`);
|
|
@@ -1148,8 +1212,8 @@ server.registerTool("planner-handoff-list", {
|
|
|
1148
1212
|
return text(`Phase handoffs (${list.length}):\n${lines.join("\n")}`, { count: list.length, handoffs: list });
|
|
1149
1213
|
});
|
|
1150
1214
|
server.registerTool("planner-handoff-show", {
|
|
1151
|
-
description: "Read the entity-scoped handoff of a phase. phaseRef
|
|
1152
|
-
inputSchema: { phaseRef: z.string().
|
|
1215
|
+
description: "Read the entity-scoped handoff of a phase. phaseRef is required; never infer a phase from in-progress status or stale context.",
|
|
1216
|
+
inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title. Ask the user when ambiguous.") },
|
|
1153
1217
|
}, async ({ phaseRef }) => {
|
|
1154
1218
|
const st = await requireStore();
|
|
1155
1219
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
@@ -1161,13 +1225,14 @@ server.registerTool("planner-handoff-show", {
|
|
|
1161
1225
|
return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
1162
1226
|
});
|
|
1163
1227
|
server.registerTool("planner-handoff-write", {
|
|
1164
|
-
description: "Write/refresh the entity-scoped handoff on a phase.
|
|
1228
|
+
description: "Write/refresh the entity-scoped handoff on a phase. phaseRef is required and must be the exact phase selected with the user. Never guess from in-progress status or stale context; completed/canceled phases reject new handoffs. Pass content (markdown) and a meaningful title.",
|
|
1165
1229
|
inputSchema: {
|
|
1166
|
-
phaseRef: z.string().
|
|
1230
|
+
phaseRef: z.string().min(1).describe("Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title."),
|
|
1167
1231
|
title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work (becomes the H1 / first line in lists). REQUIRED if your markdown's first heading is generic. Example: 'P049 — featureId validation: tests + adapter wiring'."),
|
|
1232
|
+
confirmed: z.boolean().describe("Set true only after the user explicitly confirms the proposed feature+phase target."),
|
|
1168
1233
|
content: z.string().min(1).describe("Handoff text (markdown)."),
|
|
1169
1234
|
},
|
|
1170
|
-
}, async ({ phaseRef, title, content }) => {
|
|
1235
|
+
}, async ({ phaseRef, title, confirmed, content }) => {
|
|
1171
1236
|
const st = await requireStore();
|
|
1172
1237
|
let body = content.trim();
|
|
1173
1238
|
const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
|
@@ -1191,20 +1256,23 @@ server.registerTool("planner-handoff-write", {
|
|
|
1191
1256
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1192
1257
|
if (!r.ok)
|
|
1193
1258
|
return text(`❌ ${r.error}`);
|
|
1259
|
+
if (!confirmed)
|
|
1260
|
+
return text(`Proposal only: I would write this handoff on ${r.compositeRef}. Ask the user to confirm this exact feature/phase, then retry with confirmed=true.`, { phaseRef: r.compositeRef, confirmationRequired: true });
|
|
1194
1261
|
await st.setPhaseHandoff(r.phase.id, body);
|
|
1195
1262
|
return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
1196
1263
|
});
|
|
1197
1264
|
server.registerTool("planner-handoff-prepare", {
|
|
1198
|
-
description: "
|
|
1265
|
+
description: "Prepare a canonical handoff proposal. Do not write until the agent has identified the exact feature+phase from the conversation and the user has confirmed that target.",
|
|
1199
1266
|
}, async () => text([
|
|
1200
|
-
"
|
|
1201
|
-
"
|
|
1202
|
-
"
|
|
1203
|
-
"
|
|
1267
|
+
"First identify the exact feature and phase actually discussed/worked on in this session. Do not use the first in-progress phase and do not target a phase that just became done.",
|
|
1268
|
+
"Before calling planner-handoff-write, tell the user: 'I propose writing this handoff on P00x(F00x) — <phase title>. Confirm?' Wait for explicit confirmation.",
|
|
1269
|
+
"After confirmation, call planner-handoff-write with that exact phaseRef. If the phase is done/canceled, do not write an operational handoff.",
|
|
1270
|
+
"Pass a meaningful title. Required sections: Created at, Updated at, Reason, Current focus, What was being done, How to resume, Files touched, Blockers, Next steps, Recent decisions, Reminder.",
|
|
1271
|
+
"Stored on phase.handoff; .planner/HANDOFF.md is deprecated.",
|
|
1204
1272
|
].join("\n")));
|
|
1205
1273
|
server.registerTool("planner-handoff-clear", {
|
|
1206
|
-
description: "Clear the entity-scoped handoff of a phase
|
|
1207
|
-
inputSchema: { phaseRef: z.string().
|
|
1274
|
+
description: "Clear/archive the entity-scoped handoff of a phase. phaseRef is required; never infer a phase automatically.",
|
|
1275
|
+
inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title.") },
|
|
1208
1276
|
}, async ({ phaseRef }) => {
|
|
1209
1277
|
const st = await requireStore();
|
|
1210
1278
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.19-next.
|
|
3
|
+
"version": "0.2.19-next.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
45
45
|
"zod": "^3.24.0",
|
|
46
|
-
"@agent-plan/core": "0.2.19-next.
|
|
47
|
-
"@agent-plan/server": "0.2.19-next.
|
|
46
|
+
"@agent-plan/core": "0.2.19-next.22",
|
|
47
|
+
"@agent-plan/server": "0.2.19-next.22"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|