@agent-plan/mcp 0.2.19-next.20 → 0.2.19-next.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -37
- 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":";AAkvCA,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, 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.",
|
|
@@ -1039,27 +1035,76 @@ server.registerTool("planner-task-delete", {
|
|
|
1039
1035
|
const tdFeatures = (await st.loadFeatures()).features;
|
|
1040
1036
|
return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
|
|
1041
1037
|
});
|
|
1038
|
+
server.registerTool("planner-task-recommend", {
|
|
1039
|
+
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.",
|
|
1040
|
+
inputSchema: {},
|
|
1041
|
+
}, async () => {
|
|
1042
|
+
const st = await requireStore();
|
|
1043
|
+
const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1044
|
+
const result = recommendNextTask(features.features, phases, project.workDeviations);
|
|
1045
|
+
if (!result.candidate)
|
|
1046
|
+
return text(`No task recommendation: ${result.reason}`, { kind: result.kind, reason: result.reason, activeTaskIds: result.activeCandidates?.map((candidate) => candidate.task.id) ?? [] });
|
|
1047
|
+
const { candidate } = result;
|
|
1048
|
+
const ref = taskCompositeRef(candidate.task, candidate.phase, features.features);
|
|
1049
|
+
return text(`Recommended (${result.kind}): ${ref} — ${candidate.task.title}\n${result.reason}${result.deviation ? `\nDeviation: ${result.deviation.id}; resume target ${result.deviation.resumeTaskId}.` : ""}`, {
|
|
1050
|
+
kind: result.kind, taskId: candidate.task.id, phaseId: candidate.phase.id, featureId: candidate.feature?.id, deviation: result.deviation,
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
server.registerTool("planner-task-deviation", {
|
|
1054
|
+
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.",
|
|
1055
|
+
inputSchema: { temporary_task: z.string().min(1), resume_task: z.string().min(1).optional(), reason: z.string().min(1) },
|
|
1056
|
+
}, async ({ temporary_task, resume_task, reason }) => {
|
|
1057
|
+
const st = await requireStore();
|
|
1058
|
+
const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
|
|
1059
|
+
const temporary = findTaskByRef(phases, features.features, temporary_task);
|
|
1060
|
+
if (!temporary)
|
|
1061
|
+
return text(`Task not found: ${temporary_task}`);
|
|
1062
|
+
const selected = recommendNextTask(features.features, phases, project.workDeviations);
|
|
1063
|
+
const resume = resume_task ? findTaskByRef(phases, features.features, resume_task) : selected.candidate;
|
|
1064
|
+
if (!resume)
|
|
1065
|
+
return text(`No resume task is available. Provide resume_task explicitly. ${selected.reason}`);
|
|
1066
|
+
if (temporary.task.id === resume.task.id)
|
|
1067
|
+
return text("A temporary task must differ from its resume target.");
|
|
1068
|
+
if (temporary.task.status !== "planned")
|
|
1069
|
+
return text(`Temporary task is not startable: ${temporary.task.status}.`);
|
|
1070
|
+
const timestamp = nowISO();
|
|
1071
|
+
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: "" };
|
|
1072
|
+
await st.addWorkDeviation(record);
|
|
1073
|
+
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 });
|
|
1074
|
+
});
|
|
1042
1075
|
server.registerTool("planner-task-start", {
|
|
1043
|
-
description: "Set a task to in-progress.",
|
|
1076
|
+
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
1077
|
inputSchema: { task: z.string().min(1) },
|
|
1045
1078
|
}, async ({ task: ref }) => {
|
|
1046
1079
|
const st = await requireStore();
|
|
1047
|
-
const
|
|
1080
|
+
const features = (await st.loadFeatures()).features;
|
|
1081
|
+
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
1048
1082
|
if (!found)
|
|
1049
1083
|
return text(`Task not found: ${ref}`);
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1084
|
+
if (found.task.status === "in-progress")
|
|
1085
|
+
return text(`Task ${found.task.id} is already in-progress.`);
|
|
1086
|
+
if (found.task.status === "done")
|
|
1087
|
+
return text(`Task ${found.task.id} is done. Use planner-task-update to reopen.`);
|
|
1088
|
+
const project = await st.loadProject();
|
|
1089
|
+
const selection = recommendNextTask(features, await st.loadAllPhases(), project.workDeviations);
|
|
1090
|
+
if (!selection.candidate || selection.candidate.task.id !== found.task.id) {
|
|
1091
|
+
const recommended = selection.candidate ? ` Start ${selection.candidate.task.id} instead, or record a user-approved planner-task-deviation first.` : "";
|
|
1092
|
+
return text(`Task start denied: ${selection.reason}.${recommended}`, {
|
|
1093
|
+
kind: selection.kind,
|
|
1094
|
+
reason: selection.reason,
|
|
1095
|
+
recommendedTaskId: selection.candidate?.task.id,
|
|
1096
|
+
});
|
|
1062
1097
|
}
|
|
1098
|
+
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
1099
|
+
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
1100
|
+
const [phaseWithRequirements, featureRequirements] = await Promise.all([
|
|
1101
|
+
st.loadPhaseWithRequirements(found.phase.id),
|
|
1102
|
+
parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
|
|
1103
|
+
]);
|
|
1104
|
+
const phaseContext = buildPhaseContextBlock(phaseWithRequirements, parentFeature, phaseWithRequirements.linkedRequirements ?? [], featureRequirements);
|
|
1105
|
+
// A pending handoff is context, not a lock: task start RETAINS it. It is
|
|
1106
|
+
// archived only when the phase completes (auto) or the user explicitly
|
|
1107
|
+
// clears it.
|
|
1063
1108
|
const timestamp = nowISO();
|
|
1064
1109
|
let updatedTask;
|
|
1065
1110
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -1084,11 +1129,11 @@ server.registerTool("planner-task-start", {
|
|
|
1084
1129
|
return phase;
|
|
1085
1130
|
});
|
|
1086
1131
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
1087
|
-
|
|
1132
|
+
if (selection.deviation?.temporaryTaskId === found.task.id && selection.deviation.state === "approved") {
|
|
1133
|
+
await st.setWorkDeviationState(selection.deviation.id, "active", timestamp);
|
|
1134
|
+
}
|
|
1088
1135
|
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}`);
|
|
1136
|
+
return writeAndSummarize(st, `✅ Task started: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (in-progress)${t.shortId ? ` · ${t.shortId}` : ""}${phaseContext}`);
|
|
1092
1137
|
});
|
|
1093
1138
|
server.registerTool("planner-task-complete", {
|
|
1094
1139
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -1133,6 +1178,11 @@ server.registerTool("planner-task-complete", {
|
|
|
1133
1178
|
return phase;
|
|
1134
1179
|
});
|
|
1135
1180
|
const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
|
|
1181
|
+
const completedDeviation = (await st.loadProject()).workDeviations
|
|
1182
|
+
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === found.task.id)
|
|
1183
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
1184
|
+
if (completedDeviation)
|
|
1185
|
+
await st.setWorkDeviationState(completedDeviation.id, "resolved", timestamp);
|
|
1136
1186
|
const features = (await st.loadFeatures()).features;
|
|
1137
1187
|
const t = updatedTask ?? found.task;
|
|
1138
1188
|
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 +1198,8 @@ server.registerTool("planner-handoff-list", {
|
|
|
1148
1198
|
return text(`Phase handoffs (${list.length}):\n${lines.join("\n")}`, { count: list.length, handoffs: list });
|
|
1149
1199
|
});
|
|
1150
1200
|
server.registerTool("planner-handoff-show", {
|
|
1151
|
-
description: "Read the entity-scoped handoff of a phase. phaseRef
|
|
1152
|
-
inputSchema: { phaseRef: z.string().
|
|
1201
|
+
description: "Read the entity-scoped handoff of a phase. phaseRef is required; never infer a phase from in-progress status or stale context.",
|
|
1202
|
+
inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title. Ask the user when ambiguous.") },
|
|
1153
1203
|
}, async ({ phaseRef }) => {
|
|
1154
1204
|
const st = await requireStore();
|
|
1155
1205
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
@@ -1161,13 +1211,14 @@ server.registerTool("planner-handoff-show", {
|
|
|
1161
1211
|
return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
1162
1212
|
});
|
|
1163
1213
|
server.registerTool("planner-handoff-write", {
|
|
1164
|
-
description: "Write/refresh the entity-scoped handoff on a phase.
|
|
1214
|
+
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
1215
|
inputSchema: {
|
|
1166
|
-
phaseRef: z.string().
|
|
1216
|
+
phaseRef: z.string().min(1).describe("Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title."),
|
|
1167
1217
|
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'."),
|
|
1218
|
+
confirmed: z.boolean().describe("Set true only after the user explicitly confirms the proposed feature+phase target."),
|
|
1168
1219
|
content: z.string().min(1).describe("Handoff text (markdown)."),
|
|
1169
1220
|
},
|
|
1170
|
-
}, async ({ phaseRef, title, content }) => {
|
|
1221
|
+
}, async ({ phaseRef, title, confirmed, content }) => {
|
|
1171
1222
|
const st = await requireStore();
|
|
1172
1223
|
let body = content.trim();
|
|
1173
1224
|
const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
|
@@ -1191,20 +1242,23 @@ server.registerTool("planner-handoff-write", {
|
|
|
1191
1242
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1192
1243
|
if (!r.ok)
|
|
1193
1244
|
return text(`❌ ${r.error}`);
|
|
1245
|
+
if (!confirmed)
|
|
1246
|
+
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
1247
|
await st.setPhaseHandoff(r.phase.id, body);
|
|
1195
1248
|
return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
1196
1249
|
});
|
|
1197
1250
|
server.registerTool("planner-handoff-prepare", {
|
|
1198
|
-
description: "
|
|
1251
|
+
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
1252
|
}, async () => text([
|
|
1200
|
-
"
|
|
1201
|
-
"
|
|
1202
|
-
"
|
|
1203
|
-
"
|
|
1253
|
+
"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.",
|
|
1254
|
+
"Before calling planner-handoff-write, tell the user: 'I propose writing this handoff on P00x(F00x) — <phase title>. Confirm?' Wait for explicit confirmation.",
|
|
1255
|
+
"After confirmation, call planner-handoff-write with that exact phaseRef. If the phase is done/canceled, do not write an operational handoff.",
|
|
1256
|
+
"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.",
|
|
1257
|
+
"Stored on phase.handoff; .planner/HANDOFF.md is deprecated.",
|
|
1204
1258
|
].join("\n")));
|
|
1205
1259
|
server.registerTool("planner-handoff-clear", {
|
|
1206
|
-
description: "Clear the entity-scoped handoff of a phase
|
|
1207
|
-
inputSchema: { phaseRef: z.string().
|
|
1260
|
+
description: "Clear/archive the entity-scoped handoff of a phase. phaseRef is required; never infer a phase automatically.",
|
|
1261
|
+
inputSchema: { phaseRef: z.string().min(1).describe("Exact phase ref: P00x | P00x(F00x) | UUID | title.") },
|
|
1208
1262
|
}, async ({ phaseRef }) => {
|
|
1209
1263
|
const st = await requireStore();
|
|
1210
1264
|
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.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
45
45
|
"zod": "^3.24.0",
|
|
46
|
-
"@agent-plan/
|
|
47
|
-
"@agent-plan/
|
|
46
|
+
"@agent-plan/server": "0.2.19-next.21",
|
|
47
|
+
"@agent-plan/core": "0.2.19-next.21"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|