@agent-plan/mcp 0.2.19-next.2 → 0.2.19-next.20
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 +459 -119
- package/package.json +5 -3
- package/server.json +15 -0
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":";AA2rCA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@ 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, buildRecap } from "@agent-plan/core";
|
|
7
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, migrateToGlobalSequence, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock } from "@agent-plan/core";
|
|
8
8
|
import { serve } from "@agent-plan/server";
|
|
9
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef } from "@agent-plan/core/naming";
|
|
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"];
|
|
11
11
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
12
12
|
function text(textValue, structuredContent) {
|
|
@@ -31,6 +31,7 @@ async function requireStore() {
|
|
|
31
31
|
if (!planBackfillDone) {
|
|
32
32
|
planBackfillDone = true;
|
|
33
33
|
await st.ensureShortIdsAndPriority().catch(() => { });
|
|
34
|
+
await migrateToGlobalSequence(st).catch(() => { });
|
|
34
35
|
}
|
|
35
36
|
return st;
|
|
36
37
|
}
|
|
@@ -56,39 +57,51 @@ async function ensureWebStarted() {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
function findFeatureByRef(features, ref) {
|
|
59
|
-
const normalized = ref.trim().toLowerCase();
|
|
60
|
-
return features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
61
|
-
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
62
|
-
?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
|
|
63
|
-
}
|
|
64
|
-
function findTaskByRef(phases, ref) {
|
|
65
60
|
const normalized = ref.trim().toLowerCase();
|
|
66
61
|
if (!normalized)
|
|
67
62
|
return undefined;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const phase of phases) {
|
|
75
|
-
if (phase.number !== pNum)
|
|
76
|
-
continue;
|
|
77
|
-
const task = phase.tasks.find((t) => t.number === tNum);
|
|
78
|
-
if (task)
|
|
79
|
-
return { phase, task };
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
for (const phase of phases) {
|
|
83
|
-
const task = phase.tasks.find((entry) => entry.id.toLowerCase() === normalized
|
|
84
|
-
|| (entry.shortId && entry.shortId.toLowerCase() === normalized)
|
|
85
|
-
|| entry.title.toLowerCase() === normalized
|
|
86
|
-
|| entry.title.toLowerCase().includes(normalized));
|
|
87
|
-
if (task)
|
|
88
|
-
return { phase, task };
|
|
63
|
+
const fMatch = normalized.match(/^f(\d+)$/);
|
|
64
|
+
if (fMatch) {
|
|
65
|
+
const n = parseInt(fMatch[1], 10);
|
|
66
|
+
const byNum = features.find((feature) => feature.number === n);
|
|
67
|
+
if (byNum)
|
|
68
|
+
return byNum;
|
|
89
69
|
}
|
|
90
|
-
return
|
|
70
|
+
return features.find((feature) => feature.shortId?.toLowerCase() === normalized)
|
|
71
|
+
?? features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
72
|
+
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
73
|
+
?? features.find((feature) => feature.name.toLowerCase().includes(normalized));
|
|
74
|
+
}
|
|
75
|
+
function resolveFeatureRefStrict(features, ref) {
|
|
76
|
+
const raw = ref.trim();
|
|
77
|
+
if (!raw)
|
|
78
|
+
return { ok: false, error: "Feature ref is required." };
|
|
79
|
+
const normalized = raw.toLowerCase();
|
|
80
|
+
const byNumber = normalized.match(/^f(\d+)$/)
|
|
81
|
+
? features.find((feature) => feature.number === parseInt(normalized.slice(1), 10))
|
|
82
|
+
: undefined;
|
|
83
|
+
if (byNumber)
|
|
84
|
+
return { ok: true, feature: byNumber };
|
|
85
|
+
const byShortId = features.find((feature) => feature.shortId?.toLowerCase() === normalized);
|
|
86
|
+
if (byShortId)
|
|
87
|
+
return { ok: true, feature: byShortId };
|
|
88
|
+
const byId = features.find((feature) => feature.id.toLowerCase() === normalized);
|
|
89
|
+
if (byId)
|
|
90
|
+
return { ok: true, feature: byId };
|
|
91
|
+
const exactName = features.filter((feature) => feature.name.toLowerCase() === normalized);
|
|
92
|
+
if (exactName.length === 1)
|
|
93
|
+
return { ok: true, feature: exactName[0] };
|
|
94
|
+
if (exactName.length > 1)
|
|
95
|
+
return { ok: false, error: `Ambiguous feature ref: ${raw}. Multiple features have that exact name; use F00x, shortId, or UUID.` };
|
|
96
|
+
const partialName = features.filter((feature) => feature.name.toLowerCase().includes(normalized));
|
|
97
|
+
if (partialName.length === 1)
|
|
98
|
+
return { ok: true, feature: partialName[0] };
|
|
99
|
+
if (partialName.length > 1)
|
|
100
|
+
return { ok: false, error: `Ambiguous feature ref: ${raw}. Matches: ${partialName.map((feature) => formatFeatureRef(feature.number)).join(", ")}. Use a specific F00x, shortId, or UUID.` };
|
|
101
|
+
return { ok: false, error: `Feature not found: ${raw}` };
|
|
91
102
|
}
|
|
103
|
+
/** Belt-and-suspenders validation: a resolved ref must be a real UUID and the target
|
|
104
|
+
* must still exist in the store before we allocate numbers or write. */
|
|
92
105
|
function featureNumberOfPhase(phase, features) {
|
|
93
106
|
return phase.featureId ? features.find((f) => f.id === phase.featureId)?.number : undefined;
|
|
94
107
|
}
|
|
@@ -151,7 +164,7 @@ server.registerTool("planner-export", {
|
|
|
151
164
|
const markdown = exportService.exportToMarkdown(plan, full);
|
|
152
165
|
const fs = await import("node:fs/promises");
|
|
153
166
|
await fs.writeFile(join(st.root, "EXPORT.md"), markdown, "utf-8");
|
|
154
|
-
return text(`Project export generated. Summary results:\n\n${markdown.slice(0, 1000)}${markdown.length > 1000 ? "... (full report in .planner/EXPORT.md)" : ""}
|
|
167
|
+
return text(`Project export generated. Summary results:\n\n${markdown.slice(0, 1000)}${markdown.length > 1000 ? "... (full report in .planner/EXPORT.md)" : ""}`);
|
|
155
168
|
});
|
|
156
169
|
server.registerTool("planner-authorize-bypass", {
|
|
157
170
|
description: "Authorize a temporary guard bypass (default 15 minutes) so edit/write tools can proceed even when no task is in-progress. Use ONLY after the user explicitly authorizes proceeding without a task. Harness-agnostic: stored in resume.json so all adapters (Pi, Claude Code, Codex, ...) respect it.",
|
|
@@ -189,29 +202,80 @@ server.registerTool("planner-init", {
|
|
|
189
202
|
if (goal !== undefined)
|
|
190
203
|
project.goal = goal.trim();
|
|
191
204
|
await st.saveProject(project);
|
|
192
|
-
return writeAndSummarize(st, `.planner/ initialized for "${project.name}"
|
|
205
|
+
return writeAndSummarize(st, `.planner/ initialized for "${project.name}"`);
|
|
193
206
|
});
|
|
194
207
|
server.registerTool("planner-show", {
|
|
195
|
-
description: "Show
|
|
208
|
+
description: "Show a compact planner overview: project metadata, counts, and a one-line summary of each feature (F00x · shortId — name (status; N phases, M tasks)). Does NOT include full descriptions, phases, or tasks — use planner-feature-list / planner-phase-list / planner-task-list for discovery and planner-*-show for full detail of a single entity. Keeps the result small to avoid token overflow.",
|
|
196
209
|
}, async () => {
|
|
197
210
|
const st = await requireStore();
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
`
|
|
207
|
-
|
|
211
|
+
const features = (await st.loadFeatures()).features;
|
|
212
|
+
const phases = await st.loadAllPhases();
|
|
213
|
+
const totalTasks = phases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
214
|
+
const project = await st.loadProject();
|
|
215
|
+
const manifest = await st.loadManifest();
|
|
216
|
+
const featureLines = features.map((feature) => {
|
|
217
|
+
const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
|
|
218
|
+
const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
219
|
+
return `- ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
|
|
220
|
+
});
|
|
221
|
+
const summary = [
|
|
222
|
+
`📋 ${project.name}`,
|
|
223
|
+
`Description: ${project.description || "(not set)"}`,
|
|
224
|
+
`Goal: ${project.goal || "(not set)"}`,
|
|
225
|
+
`Features: ${features.length} | Phases: ${phases.length} | Tasks: ${totalTasks}`,
|
|
226
|
+
`Updated: ${manifest.updatedAt || "(unknown)"}`,
|
|
227
|
+
"",
|
|
228
|
+
"Features:",
|
|
229
|
+
featureLines.join("\n") || "(none)",
|
|
230
|
+
].join("\n");
|
|
231
|
+
// Compact structured overview (no full plan: avoids 1M+ char token overflow).
|
|
232
|
+
const overview = {
|
|
233
|
+
project: { name: project.name, description: project.description || null, goal: project.goal || null, updatedAt: manifest.updatedAt || null },
|
|
234
|
+
counts: { features: features.length, phases: phases.length, tasks: totalTasks },
|
|
235
|
+
features: features.map((feature) => ({
|
|
236
|
+
ref: formatFeatureRef(feature.number),
|
|
237
|
+
shortId: feature.shortId || null,
|
|
238
|
+
name: feature.name,
|
|
239
|
+
status: feature.status,
|
|
240
|
+
phases: phases.filter((p) => p.featureId === feature.id).length,
|
|
241
|
+
tasks: phases.filter((p) => p.featureId === feature.id).reduce((t, p) => t + p.tasks.length, 0),
|
|
242
|
+
})),
|
|
243
|
+
};
|
|
244
|
+
return text(summary, { overview });
|
|
208
245
|
});
|
|
209
246
|
server.registerTool("planner-repair", {
|
|
210
|
-
description: "Repair dangling feature→phase references and report integrity.",
|
|
247
|
+
description: "Repair dangling feature→phase references, rebuild phase containment from each task's own phaseId (heals the migrateToGlobalSequence task-shuffle bug where tasks land in the wrong phase file), and report integrity.",
|
|
211
248
|
}, async () => {
|
|
212
249
|
const st = await requireStore();
|
|
213
250
|
const report = await st.repair();
|
|
214
|
-
return text(`Repair done: renamed ${report.migrated.renamed}, repaired ${report.migrated.repaired} refs, inferred ${report.migrated.inferred}. Integrity: ${report.integrity.duplicatePhaseIds.length} duplicate, ${report.integrity.danglingPhaseIds.length} dangling.`, { report });
|
|
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 });
|
|
252
|
+
});
|
|
253
|
+
server.registerTool("planner-cleanup-orphan-phases", {
|
|
254
|
+
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.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
confirm: z.boolean().optional().describe("Set true to actually delete the discovered orphan phase files. Default: false (dry-run/list only)."),
|
|
257
|
+
},
|
|
258
|
+
}, async ({ confirm }) => {
|
|
259
|
+
const st = await requireStore();
|
|
260
|
+
const found = await st.listOrphanPhases();
|
|
261
|
+
if (!confirm) {
|
|
262
|
+
if (found.length === 0)
|
|
263
|
+
return text("No orphan phases found.", { found: [] });
|
|
264
|
+
const lines = [
|
|
265
|
+
`Found ${found.length} orphan phase${found.length === 1 ? "" : "s"}.`,
|
|
266
|
+
...found.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.reason})`),
|
|
267
|
+
"Rerun with confirm=true to delete these orphan phase files.",
|
|
268
|
+
];
|
|
269
|
+
return text(lines.join("\n"), { found, confirmRequired: true });
|
|
270
|
+
}
|
|
271
|
+
const report = await st.cleanupOrphanPhases();
|
|
272
|
+
if (report.removed.length === 0)
|
|
273
|
+
return text("No orphan phases found.", report);
|
|
274
|
+
const lines = [
|
|
275
|
+
`Removed ${report.removed.length} orphan phase${report.removed.length === 1 ? "" : "s"}.`,
|
|
276
|
+
...report.removed.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title}`),
|
|
277
|
+
];
|
|
278
|
+
return text(lines.join("\n"), report);
|
|
215
279
|
});
|
|
216
280
|
server.registerTool("planner-project-language", {
|
|
217
281
|
description: "Persist preferred languages for plan content and chat.",
|
|
@@ -227,7 +291,7 @@ server.registerTool("planner-project-language", {
|
|
|
227
291
|
if (chatLanguage !== undefined)
|
|
228
292
|
project.chatLanguage = chatLanguage.trim();
|
|
229
293
|
await st.saveProject(project);
|
|
230
|
-
return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}
|
|
294
|
+
return writeAndSummarize(st, `Saved language preferences: content=${project.contentLanguage || "(unset)"}, chat=${project.chatLanguage || "(unset)"}`);
|
|
231
295
|
});
|
|
232
296
|
server.registerTool("planner-project-discuss", {
|
|
233
297
|
description: "Record/update project-level discovery fields. This is the MCP equivalent of grouped project discuss without Pi UI prompts.",
|
|
@@ -258,20 +322,73 @@ server.registerTool("planner-project-discuss", {
|
|
|
258
322
|
if (params.decisions !== undefined)
|
|
259
323
|
project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
|
|
260
324
|
await st.saveProject(project);
|
|
261
|
-
return writeAndSummarize(st, `Project discussed/updated: ${project.name}
|
|
325
|
+
return writeAndSummarize(st, `Project discussed/updated: ${project.name}`);
|
|
262
326
|
});
|
|
263
327
|
server.registerTool("planner-feature-list", {
|
|
264
|
-
description: "List features
|
|
265
|
-
|
|
328
|
+
description: "List features (compact: F00x · shortId — name (status; N phases, M tasks)). Pass featureRef to filter. Use this to discover refs cheaply — do NOT read .planner/ files or planner-plan-get full=true to find entities.",
|
|
329
|
+
inputSchema: { featureRef: z.string().optional().describe("Optional: filter to one feature (F00x/shortId/UUID/name).") },
|
|
330
|
+
}, async ({ featureRef }) => {
|
|
266
331
|
const st = await requireStore();
|
|
267
|
-
const
|
|
332
|
+
const allFeatures = (await st.loadFeatures()).features;
|
|
333
|
+
const features = featureRef ? allFeatures.filter((f) => f.id === findFeatureByRef(allFeatures, featureRef)?.id) : allFeatures;
|
|
268
334
|
const phases = await st.loadAllPhases();
|
|
269
335
|
const lines = features.map((feature) => {
|
|
270
336
|
const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
|
|
271
337
|
const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
272
|
-
return `- ${feature.
|
|
338
|
+
return `- ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} — ${feature.name} (${feature.status}; ${featurePhases.length} phases, ${taskCount} tasks)`;
|
|
339
|
+
});
|
|
340
|
+
return text(lines.join("\n") || "No features");
|
|
341
|
+
});
|
|
342
|
+
server.registerTool("planner-phase-list", {
|
|
343
|
+
description: "List phases (compact: F00x/P00x · shortId — title (status; N tasks) [F00x]). Filters: featureRef, status. Cheap discovery — do NOT read .planner/ files or planner-plan-get full=true.",
|
|
344
|
+
inputSchema: { featureRef: z.string().optional().describe("Optional: filter to one feature (F00x/shortId/UUID/name)."), status: z.string().optional().describe("Optional: filter by status name.") },
|
|
345
|
+
}, async ({ featureRef, status }) => {
|
|
346
|
+
const st = await requireStore();
|
|
347
|
+
const features = (await st.loadFeatures()).features;
|
|
348
|
+
let phases = await st.loadAllPhases();
|
|
349
|
+
if (featureRef) {
|
|
350
|
+
const f = findFeatureByRef(features, featureRef);
|
|
351
|
+
if (!f)
|
|
352
|
+
return text(`Feature not found: ${featureRef}`);
|
|
353
|
+
phases = phases.filter((p) => p.featureId === f.id);
|
|
354
|
+
}
|
|
355
|
+
if (status)
|
|
356
|
+
phases = phases.filter((p) => p.status === status);
|
|
357
|
+
const lines = phases.map((phase) => {
|
|
358
|
+
const fNum = featureNumberOfPhase(phase, features);
|
|
359
|
+
const fTag = fNum !== undefined ? ` [F${String(fNum).padStart(3, "0")}]` : "";
|
|
360
|
+
return `- ${formatPhaseRef(phase.number, fNum)}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.status}; ${phase.tasks.length} tasks)${fTag}`;
|
|
273
361
|
});
|
|
274
|
-
return text(lines.join("\n") || "No
|
|
362
|
+
return text(lines.join("\n") || "No phases");
|
|
363
|
+
});
|
|
364
|
+
server.registerTool("planner-task-list", {
|
|
365
|
+
description: "List tasks (compact: F00x/P00x/T00x · shortId — title (status)). Filters: featureRef, phaseRef, status. Cheap discovery — do NOT read .planner/ files or planner-plan-get full=true.",
|
|
366
|
+
inputSchema: { featureRef: z.string().optional(), phaseRef: z.string().optional(), status: z.string().optional().describe("Optional: filter by status name.") },
|
|
367
|
+
}, async ({ featureRef, phaseRef, status }) => {
|
|
368
|
+
const st = await requireStore();
|
|
369
|
+
const features = (await st.loadFeatures()).features;
|
|
370
|
+
let phases = await st.loadAllPhases();
|
|
371
|
+
if (featureRef) {
|
|
372
|
+
const f = findFeatureByRef(features, featureRef);
|
|
373
|
+
if (!f)
|
|
374
|
+
return text(`Feature not found: ${featureRef}`);
|
|
375
|
+
phases = phases.filter((p) => p.featureId === f.id);
|
|
376
|
+
}
|
|
377
|
+
if (phaseRef) {
|
|
378
|
+
const p = findPhaseByRef(phases, features, phaseRef);
|
|
379
|
+
if (!p)
|
|
380
|
+
return text(`Phase not found: ${phaseRef}`);
|
|
381
|
+
phases = [p];
|
|
382
|
+
}
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const phase of phases) {
|
|
385
|
+
for (const task of phase.tasks) {
|
|
386
|
+
if (status && task.status !== status)
|
|
387
|
+
continue;
|
|
388
|
+
out.push(`- ${taskCompositeRef(task, phase, features)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status})`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return text(out.join("\n") || "No tasks");
|
|
275
392
|
});
|
|
276
393
|
server.registerTool("planner-feature-add", {
|
|
277
394
|
description: "Create a feature with a rich description. REQUIRED: description must include code references (file:line), current implementation state (what exists, what is unimplemented), systems/structs/traits involved, concrete goals, and behaviors to preserve. The description is the primary context for future agents resuming this feature; one-liners cause misalignment.",
|
|
@@ -289,7 +406,7 @@ server.registerTool("planner-feature-add", {
|
|
|
289
406
|
const priority = await st.nextPriority("feature");
|
|
290
407
|
const feature = {
|
|
291
408
|
id: createFeatureId(),
|
|
292
|
-
number:
|
|
409
|
+
number: await st.allocFeatureNumber(),
|
|
293
410
|
shortId,
|
|
294
411
|
priority,
|
|
295
412
|
name: name.trim(),
|
|
@@ -305,6 +422,7 @@ server.registerTool("planner-feature-add", {
|
|
|
305
422
|
acceptedDecisions: [],
|
|
306
423
|
phaseIds: [],
|
|
307
424
|
dependsOn: [],
|
|
425
|
+
statusLog: [],
|
|
308
426
|
createdAt: timestamp,
|
|
309
427
|
updatedAt: timestamp,
|
|
310
428
|
};
|
|
@@ -312,24 +430,63 @@ server.registerTool("planner-feature-add", {
|
|
|
312
430
|
doc.features.push(feature);
|
|
313
431
|
return doc;
|
|
314
432
|
});
|
|
315
|
-
return writeAndSummarize(st,
|
|
433
|
+
return writeAndSummarize(st, `✅ Feature created: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}`);
|
|
316
434
|
});
|
|
317
435
|
server.registerTool("planner-feature-show", {
|
|
318
436
|
description: "Show a feature by id or name.",
|
|
319
|
-
inputSchema: { feature: z.string().min(1).describe("Feature
|
|
320
|
-
}, async ({ feature: ref }) => {
|
|
437
|
+
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).") },
|
|
438
|
+
}, async ({ feature: ref, full }) => {
|
|
321
439
|
const st = await requireStore();
|
|
322
440
|
const features = (await st.loadFeatures()).features;
|
|
323
|
-
const
|
|
324
|
-
if (!
|
|
325
|
-
return text(
|
|
441
|
+
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
442
|
+
if (!resolvedFeature.ok)
|
|
443
|
+
return text(resolvedFeature.error);
|
|
444
|
+
const feature = resolvedFeature.feature;
|
|
326
445
|
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
327
|
-
|
|
446
|
+
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
|
|
447
|
+
return text(full ? `${summary}\n\n${feature.description || ""}` : summary);
|
|
448
|
+
});
|
|
449
|
+
server.registerTool("planner-feature-discuss", {
|
|
450
|
+
description: "Persist feature discovery/governance fields and mark the feature context as ready.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
feature: z.string().min(1).describe("Feature ref. Accepts F00x, shortId, UUID, or title."),
|
|
453
|
+
description: z.string().optional().describe("Current implementation state, scope, and goals for this feature."),
|
|
454
|
+
workDone: z.string().optional().describe("What is already implemented / decided."),
|
|
455
|
+
workRemaining: z.string().optional().describe("What still needs to be done."),
|
|
456
|
+
dependencies: z.array(z.string()).optional().describe("Cross-feature or external dependencies."),
|
|
457
|
+
},
|
|
458
|
+
}, async ({ feature: ref, description, workDone, workRemaining, dependencies }) => {
|
|
459
|
+
const st = await requireStore();
|
|
460
|
+
const features = (await st.loadFeatures()).features;
|
|
461
|
+
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
462
|
+
if (!resolvedFeature.ok)
|
|
463
|
+
return text(resolvedFeature.error);
|
|
464
|
+
const feature = resolvedFeature.feature;
|
|
465
|
+
const updated = await st.updateFeatures((doc) => {
|
|
466
|
+
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
467
|
+
if (!target)
|
|
468
|
+
return doc;
|
|
469
|
+
if (description !== undefined)
|
|
470
|
+
target.description = description.trim();
|
|
471
|
+
if (workDone !== undefined)
|
|
472
|
+
target.workDone = workDone.trim();
|
|
473
|
+
if (workRemaining !== undefined)
|
|
474
|
+
target.workRemaining = workRemaining.trim();
|
|
475
|
+
if (dependencies !== undefined)
|
|
476
|
+
target.dependsOn = dependencies.map((item) => item.trim()).filter(Boolean);
|
|
477
|
+
target.discussedAt = nowISO();
|
|
478
|
+
target.contextReady = true;
|
|
479
|
+
target.contextReadyReason = "Updated through planner-feature-discuss MCP tool.";
|
|
480
|
+
target.updatedAt = nowISO();
|
|
481
|
+
return doc;
|
|
482
|
+
});
|
|
483
|
+
const result = updated.features.find((entry) => entry.id === feature.id);
|
|
484
|
+
return writeAndSummarize(st, `✅ Feature discussed/updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}`);
|
|
328
485
|
});
|
|
329
486
|
server.registerTool("planner-feature-update", {
|
|
330
487
|
description: "Update a feature.",
|
|
331
488
|
inputSchema: {
|
|
332
|
-
feature: z.string().min(1).describe("Feature
|
|
489
|
+
feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
333
490
|
name: z.string().optional(),
|
|
334
491
|
description: z.string().optional(),
|
|
335
492
|
status: z.enum(STATUS_VALUES).optional(),
|
|
@@ -342,9 +499,10 @@ server.registerTool("planner-feature-update", {
|
|
|
342
499
|
}, async ({ feature: ref, ...updates }) => {
|
|
343
500
|
const st = await requireStore();
|
|
344
501
|
const features = (await st.loadFeatures()).features;
|
|
345
|
-
const
|
|
346
|
-
if (!
|
|
347
|
-
return text(
|
|
502
|
+
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
503
|
+
if (!resolvedFeature.ok)
|
|
504
|
+
return text(resolvedFeature.error);
|
|
505
|
+
const feature = resolvedFeature.feature;
|
|
348
506
|
const updated = await st.updateFeatures((doc) => {
|
|
349
507
|
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
350
508
|
if (!target)
|
|
@@ -369,20 +527,21 @@ server.registerTool("planner-feature-update", {
|
|
|
369
527
|
return doc;
|
|
370
528
|
});
|
|
371
529
|
const result = updated.features.find((entry) => entry.id === feature.id);
|
|
372
|
-
return writeAndSummarize(st,
|
|
530
|
+
return writeAndSummarize(st, `✅ Feature updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}`);
|
|
373
531
|
});
|
|
374
532
|
server.registerTool("planner-feature-delete", {
|
|
375
533
|
description: "Delete a feature. By default, phases are unlinked rather than deleted.",
|
|
376
534
|
inputSchema: {
|
|
377
|
-
feature: z.string().min(1).describe("Feature
|
|
535
|
+
feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
378
536
|
cascade: z.boolean().optional().describe("Also delete phases belonging to this feature"),
|
|
379
537
|
},
|
|
380
538
|
}, async ({ feature: ref, cascade }) => {
|
|
381
539
|
const st = await requireStore();
|
|
382
540
|
const features = (await st.loadFeatures()).features;
|
|
383
|
-
const
|
|
384
|
-
if (!
|
|
385
|
-
return text(
|
|
541
|
+
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
542
|
+
if (!resolvedFeature.ok)
|
|
543
|
+
return text(resolvedFeature.error);
|
|
544
|
+
const feature = resolvedFeature.feature;
|
|
386
545
|
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
387
546
|
await st.updateFeatures((doc) => {
|
|
388
547
|
doc.features = doc.features.filter((entry) => entry.id !== feature.id);
|
|
@@ -401,19 +560,27 @@ server.registerTool("planner-feature-delete", {
|
|
|
401
560
|
return writeAndSummarize(st, `Feature deleted: ${feature.id}${cascade ? `; deleted ${phases.length} phases` : `; unlinked ${phases.length} phases`}`, { deleted: feature.id, affectedPhases: phases.length, cascade: Boolean(cascade) });
|
|
402
561
|
});
|
|
403
562
|
server.registerTool("planner-phase-add", {
|
|
404
|
-
description: "Create a phase with a rich description. REQUIRED: description must include code references (file:line), current implementation state, dependencies, specific files/systems to modify, and behaviors to preserve. The description is the primary context for future agents; one-liners cause misalignment.",
|
|
563
|
+
description: "Create a phase linked to a feature with a rich description. REQUIRED: feature ref and description. The description must include code references (file:line), current implementation state, dependencies, specific files/systems to modify, and behaviors to preserve. The description is the primary context for future agents; one-liners cause misalignment.",
|
|
405
564
|
inputSchema: {
|
|
406
565
|
title: z.string().min(1),
|
|
407
|
-
feature: z.string().optional().describe("Feature
|
|
566
|
+
feature: z.string().min(1).optional().describe("Feature ref (required). Accepts F00x/P00x/T00x composite, bare F00x (global), 5-char shortId, UUID, or title."),
|
|
408
567
|
summary: z.string().optional().describe("One-line summary of the phase"),
|
|
409
568
|
description: z.string().min(50, "Description must be at least 50 characters — include code references (file:line), current state, structs/traits involved, concrete work items, behaviors to preserve. Prefix with 'design-only' for pre-implementation design tasks.").describe("Required code references (file:line), current state, structs/traits involved, concrete work items, behaviors to preserve. Not a one-liner."),
|
|
410
569
|
},
|
|
411
570
|
}, async ({ title, feature: featureRef, summary, description }) => {
|
|
412
571
|
const st = await requireStore();
|
|
572
|
+
if (!featureRef?.trim())
|
|
573
|
+
return text("feature is required: a phase must belong to a feature.");
|
|
413
574
|
const featuresDoc = await st.loadFeatures();
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
return text(
|
|
575
|
+
const resolvedFeature = resolveFeatureRefStrict(featuresDoc.features, featureRef);
|
|
576
|
+
if (!resolvedFeature.ok)
|
|
577
|
+
return text(resolvedFeature.error);
|
|
578
|
+
const featureValidation = await validateResolvedTarget("feature", resolvedFeature.feature.id, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === resolvedFeature.feature.id)).catch(() => undefined));
|
|
579
|
+
if (!featureValidation.ok)
|
|
580
|
+
return text(featureValidation.error);
|
|
581
|
+
const feature = resolvedFeature.feature;
|
|
582
|
+
if (!feature)
|
|
583
|
+
return text(`Resolved feature ${featureRef} no longer exists. Refusing to create phase.`);
|
|
417
584
|
const lockKey = feature?.id ?? "__unscoped__";
|
|
418
585
|
let phase;
|
|
419
586
|
await withFeatureLock(lockKey, async () => {
|
|
@@ -424,7 +591,7 @@ server.registerTool("planner-phase-add", {
|
|
|
424
591
|
const priority = await st.nextPriority("phase", feature?.id);
|
|
425
592
|
phase = {
|
|
426
593
|
id: createPhaseId(),
|
|
427
|
-
number:
|
|
594
|
+
number: await st.allocPhaseNumber(),
|
|
428
595
|
shortId,
|
|
429
596
|
priority,
|
|
430
597
|
slug: normalizeSlug(title),
|
|
@@ -452,6 +619,9 @@ server.registerTool("planner-phase-add", {
|
|
|
452
619
|
updatedAt: timestamp,
|
|
453
620
|
handoff: "",
|
|
454
621
|
handoffUpdatedAt: "",
|
|
622
|
+
handoffReadAt: "",
|
|
623
|
+
handoffHistory: [],
|
|
624
|
+
statusLog: [],
|
|
455
625
|
};
|
|
456
626
|
await st.savePhase(phase);
|
|
457
627
|
if (feature) {
|
|
@@ -466,22 +636,29 @@ server.registerTool("planner-phase-add", {
|
|
|
466
636
|
});
|
|
467
637
|
if (!phase)
|
|
468
638
|
return text("Phase creation failed.");
|
|
469
|
-
return writeAndSummarize(st,
|
|
639
|
+
return writeAndSummarize(st, `✅ Phase created: ${formatPhaseRef(phase.number, feature?.number)} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
|
|
470
640
|
});
|
|
471
641
|
server.registerTool("planner-phase-show", {
|
|
472
|
-
description: "Show a phase by id or name.",
|
|
473
|
-
inputSchema: { phase: z.string().min(1).describe("Phase
|
|
474
|
-
}, async ({ phase: ref }) => {
|
|
642
|
+
description: "Show a phase by id or name, including derived linked requirements in the full view.",
|
|
643
|
+
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).") },
|
|
644
|
+
}, async ({ phase: ref, full }) => {
|
|
475
645
|
const st = await requireStore();
|
|
476
|
-
const
|
|
646
|
+
const features = (await st.loadFeatures()).features;
|
|
647
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
477
648
|
if (!phase)
|
|
478
649
|
return text(`Phase not found: ${ref}`);
|
|
479
|
-
|
|
650
|
+
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
651
|
+
const reqCount = linkedRequirements.length;
|
|
652
|
+
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"}` : ""})`;
|
|
653
|
+
const requirementsBlock = reqCount > 0
|
|
654
|
+
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
655
|
+
: "";
|
|
656
|
+
return text(full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary, { linkedRequirements });
|
|
480
657
|
});
|
|
481
658
|
server.registerTool("planner-phase-discuss", {
|
|
482
659
|
description: "Persist phase discovery fields and mark phase planned.",
|
|
483
660
|
inputSchema: {
|
|
484
|
-
phase: z.string().min(1).describe("Phase
|
|
661
|
+
phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
485
662
|
goal: z.string().optional(),
|
|
486
663
|
summary: z.string().optional(),
|
|
487
664
|
scope: z.string().optional(),
|
|
@@ -492,7 +669,8 @@ server.registerTool("planner-phase-discuss", {
|
|
|
492
669
|
},
|
|
493
670
|
}, async ({ phase: ref, ...updates }) => {
|
|
494
671
|
const st = await requireStore();
|
|
495
|
-
const
|
|
672
|
+
const features = (await st.loadFeatures()).features;
|
|
673
|
+
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
496
674
|
if (!found)
|
|
497
675
|
return text(`Phase not found: ${ref}`);
|
|
498
676
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
@@ -517,7 +695,7 @@ server.registerTool("planner-phase-discuss", {
|
|
|
517
695
|
entry.updatedAt = nowISO();
|
|
518
696
|
return entry;
|
|
519
697
|
});
|
|
520
|
-
return writeAndSummarize(st,
|
|
698
|
+
return writeAndSummarize(st, `✅ Phase discussed/planned: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
|
|
521
699
|
});
|
|
522
700
|
server.registerTool("planner-phase-update", {
|
|
523
701
|
description: "Update phase fields.",
|
|
@@ -531,7 +709,8 @@ server.registerTool("planner-phase-update", {
|
|
|
531
709
|
},
|
|
532
710
|
}, async ({ phase: ref, ...updates }) => {
|
|
533
711
|
const st = await requireStore();
|
|
534
|
-
const
|
|
712
|
+
const features = (await st.loadFeatures()).features;
|
|
713
|
+
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
535
714
|
if (!found)
|
|
536
715
|
return text(`Phase not found: ${ref}`);
|
|
537
716
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
@@ -548,7 +727,7 @@ server.registerTool("planner-phase-update", {
|
|
|
548
727
|
entry.updatedAt = nowISO();
|
|
549
728
|
return entry;
|
|
550
729
|
});
|
|
551
|
-
return writeAndSummarize(st,
|
|
730
|
+
return writeAndSummarize(st, `✅ Phase updated: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}`);
|
|
552
731
|
});
|
|
553
732
|
server.registerTool("planner-phase-delete", {
|
|
554
733
|
description: "Delete a phase and unlink it from features.",
|
|
@@ -564,21 +743,43 @@ server.registerTool("planner-phase-delete", {
|
|
|
564
743
|
feature.phaseIds = feature.phaseIds.filter((id) => id !== phase.id);
|
|
565
744
|
return doc;
|
|
566
745
|
});
|
|
567
|
-
|
|
746
|
+
const dFeat = (await st.loadFeatures()).features.find((f) => f.id === phase.featureId);
|
|
747
|
+
return writeAndSummarize(st, `Phase deleted: ${formatPhaseRef(phase.number, dFeat?.number)}${phase.shortId ? ` · ${phase.shortId}` : ""}`, { deleted: phase.id });
|
|
568
748
|
});
|
|
569
749
|
server.registerTool("planner-task-add", {
|
|
570
750
|
description: "Create a task with a rich description. REQUIRED: description must include code references (file:line), what already exists vs what needs to be built, specific structs/traits/systems to modify, concrete implementation steps, and edge cases to handle. The description is the execution context for agents; one-liners cause misalignment.",
|
|
571
751
|
inputSchema: {
|
|
572
|
-
|
|
752
|
+
feature: z.string().min(1).optional().describe("Feature ref the task's phase belongs to. Accepts F00x/P00x/T00x composite, bare F00x (global), 5-char shortId, UUID, or title. REQUIRED."),
|
|
753
|
+
phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
573
754
|
title: z.string().min(1),
|
|
574
755
|
description: z.string().min(50, "Description must be at least 50 characters — include code references (file:line), current state vs desired state, structs/traits to modify, concrete implementation steps, edge cases. Prefix with 'design-only' for pre-implementation design tasks.").describe("Required code references (file:line), current state vs desired state, structs/traits to modify, concrete implementation steps, edge cases. Not a one-liner."),
|
|
575
756
|
checklist: z.array(z.string()).optional(),
|
|
576
757
|
},
|
|
577
|
-
}, async ({ phase: ref, title, description, checklist }) => {
|
|
758
|
+
}, async ({ feature: featureRef, phase: ref, title, description, checklist }) => {
|
|
578
759
|
const st = await requireStore();
|
|
579
|
-
const
|
|
760
|
+
const features = (await st.loadFeatures()).features;
|
|
761
|
+
if (!featureRef?.trim())
|
|
762
|
+
return text("feature is required: a task must belong to a feature.");
|
|
763
|
+
const resolvedFeature = resolveFeatureRefStrict(features, featureRef);
|
|
764
|
+
if (!resolvedFeature.ok)
|
|
765
|
+
return text(resolvedFeature.error);
|
|
766
|
+
const featureValidation = await validateResolvedTarget("feature", resolvedFeature.feature.id, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === resolvedFeature.feature.id)).catch(() => undefined));
|
|
767
|
+
if (!featureValidation.ok)
|
|
768
|
+
return text(featureValidation.error);
|
|
769
|
+
const feature = resolvedFeature.feature;
|
|
770
|
+
if (!feature)
|
|
771
|
+
return text(`Resolved feature ${featureRef} no longer exists. Refusing to create task.`);
|
|
772
|
+
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
580
773
|
if (!found)
|
|
581
774
|
return text(`Phase not found: ${ref}`);
|
|
775
|
+
if (found.featureId !== feature.id)
|
|
776
|
+
return text(`Phase ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} does not belong to feature ${formatFeatureRef(feature.number)}. Refusing to create task.`);
|
|
777
|
+
const phaseValidation = await validateResolvedTarget("phase", found.id, () => st.loadPhase(found.id).catch(() => undefined));
|
|
778
|
+
if (!phaseValidation.ok)
|
|
779
|
+
return text(phaseValidation.error);
|
|
780
|
+
const existingPhase = await st.loadPhase(found.id);
|
|
781
|
+
if (!existingPhase)
|
|
782
|
+
return text(`Resolved phase ${found.id} no longer exists. Refusing to create task.`);
|
|
582
783
|
const timestamp = nowISO();
|
|
583
784
|
const taskId = createTaskId();
|
|
584
785
|
const shortId = createShortId(await st.assignedShortIds());
|
|
@@ -586,7 +787,7 @@ server.registerTool("planner-task-add", {
|
|
|
586
787
|
const task = {
|
|
587
788
|
id: taskId,
|
|
588
789
|
phaseId: found.id,
|
|
589
|
-
number:
|
|
790
|
+
number: await st.allocTaskNumber(),
|
|
590
791
|
shortId,
|
|
591
792
|
priority,
|
|
592
793
|
shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
|
|
@@ -597,7 +798,7 @@ server.registerTool("planner-task-add", {
|
|
|
597
798
|
statusLog: [],
|
|
598
799
|
decisions: [],
|
|
599
800
|
acceptedDecisions: [],
|
|
600
|
-
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), title: item, checked: false })),
|
|
801
|
+
checklist: (checklist ?? []).map((item, index) => ({ id: createChecklistItemId(taskId, index + 1, item), number: index + 1, title: item, checked: false })),
|
|
601
802
|
subtasks: [],
|
|
602
803
|
dependsOn: [],
|
|
603
804
|
startedAt: "",
|
|
@@ -611,18 +812,23 @@ server.registerTool("planner-task-add", {
|
|
|
611
812
|
entry.updatedAt = timestamp;
|
|
612
813
|
return entry;
|
|
613
814
|
});
|
|
614
|
-
const
|
|
615
|
-
return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found,
|
|
815
|
+
const finalFeatures = (await st.loadFeatures()).features;
|
|
816
|
+
return writeAndSummarize(st, `✅ Task created: ${taskCompositeRef(task, found, finalFeatures)} — ${task.title} (planned)${task.shortId ? ` · ${task.shortId}` : ""}`);
|
|
616
817
|
});
|
|
617
818
|
server.registerTool("planner-task-show", {
|
|
618
819
|
description: "Show a task by id or name.",
|
|
619
|
-
inputSchema: { task: z.string().min(1).describe("Task
|
|
620
|
-
}, async ({ task: ref }) => {
|
|
820
|
+
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 + statusLog. Default: compact identity only (saves tokens).") },
|
|
821
|
+
}, async ({ task: ref, full }) => {
|
|
621
822
|
const st = await requireStore();
|
|
622
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
823
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
623
824
|
if (!found)
|
|
624
825
|
return text(`Task not found: ${ref}`);
|
|
625
|
-
|
|
826
|
+
const features = (await st.loadFeatures()).features;
|
|
827
|
+
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))})`;
|
|
828
|
+
if (!full)
|
|
829
|
+
return text(summary);
|
|
830
|
+
const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
|
|
831
|
+
return text(`${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}`);
|
|
626
832
|
});
|
|
627
833
|
server.registerTool("planner-task-discuss", {
|
|
628
834
|
description: "Persist task execution notes/checklist.",
|
|
@@ -633,7 +839,7 @@ server.registerTool("planner-task-discuss", {
|
|
|
633
839
|
},
|
|
634
840
|
}, async ({ task: ref, description, checklist }) => {
|
|
635
841
|
const st = await requireStore();
|
|
636
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
842
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
637
843
|
if (!found)
|
|
638
844
|
return text(`Task not found: ${ref}`);
|
|
639
845
|
let updatedTask;
|
|
@@ -644,7 +850,9 @@ server.registerTool("planner-task-discuss", {
|
|
|
644
850
|
if (description !== undefined)
|
|
645
851
|
task.description = description.trim();
|
|
646
852
|
if (checklist !== undefined)
|
|
647
|
-
task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), title: item, checked: false }));
|
|
853
|
+
task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), number: index + 1, title: item, checked: false }));
|
|
854
|
+
if (checklist !== undefined)
|
|
855
|
+
task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), number: index + 1, title: item, checked: false }));
|
|
648
856
|
task.updatedAt = nowISO();
|
|
649
857
|
phase.updatedAt = task.updatedAt;
|
|
650
858
|
updatedTask = task;
|
|
@@ -663,10 +871,11 @@ server.registerTool("planner-task-update", {
|
|
|
663
871
|
description: z.string().optional(),
|
|
664
872
|
motivation: z.string().optional(),
|
|
665
873
|
priority: z.number().int().nonnegative().optional().describe("Display order within the phase (lower = higher)."),
|
|
874
|
+
checklist: z.array(z.string()).optional().describe("Replace the task checklist (implementation steps, plain strings). Agents should tick steps via planner-task-checklist-toggle, not write DONE in titles."),
|
|
666
875
|
},
|
|
667
|
-
}, async ({ task: ref, title, status, description, motivation, priority }) => {
|
|
876
|
+
}, async ({ task: ref, title, status, description, motivation, priority, checklist }) => {
|
|
668
877
|
const st = await requireStore();
|
|
669
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
878
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
670
879
|
if (!found)
|
|
671
880
|
return text(`Task not found: ${ref}`);
|
|
672
881
|
// Validate motivation requirement for status transitions.
|
|
@@ -711,12 +920,113 @@ server.registerTool("planner-task-update", {
|
|
|
711
920
|
const t = updatedTask ?? found.task;
|
|
712
921
|
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
713
922
|
});
|
|
923
|
+
server.registerTool("planner-task-checklist-toggle", {
|
|
924
|
+
description: "Tick/untick a task checklist item (a 'step') without rewriting the whole list. Accepts the item as C{n} (e.g. C2), the item id, or the title (case-insensitive, first match). Pass checked=true/false to set explicitly, or omit to toggle. Use this instead of writing DONE in step titles. Items are numbered C1/C2… per task.",
|
|
925
|
+
inputSchema: {
|
|
926
|
+
task: z.string().min(1).describe("Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title"),
|
|
927
|
+
item: z.string().min(1).describe("Checklist item selector: C{n} (e.g. C2), item id, or title (case-insensitive)"),
|
|
928
|
+
checked: z.boolean().optional().describe("Set explicitly: true=done, false=open. Omit to toggle."),
|
|
929
|
+
},
|
|
930
|
+
}, async ({ task: ref, item, checked }) => {
|
|
931
|
+
const st = await requireStore();
|
|
932
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
933
|
+
if (!found)
|
|
934
|
+
return text(`Task not found: ${ref}`);
|
|
935
|
+
let result = "";
|
|
936
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
937
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
938
|
+
if (!task) {
|
|
939
|
+
result = `Task not found: ${ref}`;
|
|
940
|
+
return phase;
|
|
941
|
+
}
|
|
942
|
+
const ck = task.checklist ?? [];
|
|
943
|
+
if (ck.length === 0) {
|
|
944
|
+
result = `Task "${found.task.title}" has no checklist.`;
|
|
945
|
+
return phase;
|
|
946
|
+
}
|
|
947
|
+
const target = toggleChecklistItem(ck, item, checked);
|
|
948
|
+
if (!target) {
|
|
949
|
+
result = `No checklist item matching "${item}".`;
|
|
950
|
+
return phase;
|
|
951
|
+
}
|
|
952
|
+
task.updatedAt = nowISO();
|
|
953
|
+
phase.updatedAt = nowISO();
|
|
954
|
+
const doneCount = ck.filter((i) => i.checked).length;
|
|
955
|
+
result = `C${target.number} "${target.title}" → ${target.checked ? "done" : "open"} (${doneCount}/${ck.length} checked)`;
|
|
956
|
+
return phase;
|
|
957
|
+
});
|
|
958
|
+
return writeAndSummarize(st, `✅ ${result}`);
|
|
959
|
+
});
|
|
960
|
+
server.registerTool("planner-task-checklist-add", {
|
|
961
|
+
description: "Add a single checklist item (a 'step') to a task without rewriting the list. The new item is appended as C{n} (next progressive number, stable id, unchecked). Use this to subdivide a task into smaller steps INSTEAD of spawning sub-tasks — the checklist keeps description, notes, statusLog and steps concentrated in one task (sub-tasks disperse context). Also use for granular adds instead of replacing the whole checklist via planner-task-update.",
|
|
962
|
+
inputSchema: {
|
|
963
|
+
task: z.string().min(1).describe("Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title"),
|
|
964
|
+
title: z.string().min(1).describe("Checklist item text (a single step)"),
|
|
965
|
+
},
|
|
966
|
+
}, async ({ task: ref, title }) => {
|
|
967
|
+
const st = await requireStore();
|
|
968
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
969
|
+
if (!found)
|
|
970
|
+
return text(`Task not found: ${ref}`);
|
|
971
|
+
let result = "";
|
|
972
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
973
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
974
|
+
if (!task) {
|
|
975
|
+
result = `Task not found: ${ref}`;
|
|
976
|
+
return phase;
|
|
977
|
+
}
|
|
978
|
+
const ck = task.checklist ?? [];
|
|
979
|
+
const item = addChecklistItem(ck, task.id, title);
|
|
980
|
+
ck.push(item);
|
|
981
|
+
task.checklist = ck;
|
|
982
|
+
task.updatedAt = nowISO();
|
|
983
|
+
phase.updatedAt = nowISO();
|
|
984
|
+
result = `Added C${item.number} "${item.title}" (${ck.length} items)`;
|
|
985
|
+
return phase;
|
|
986
|
+
});
|
|
987
|
+
return writeAndSummarize(st, `✅ ${result}`);
|
|
988
|
+
});
|
|
989
|
+
server.registerTool("planner-task-checklist-remove", {
|
|
990
|
+
description: "Remove a single checklist item (a 'step') from a task by C{n} (e.g. C2), item id, or title (case-insensitive). Remaining items are renumbered C1..Cn for readability; their stable ids are preserved. Use this for granular removes instead of replacing the whole checklist via planner-task-update.",
|
|
991
|
+
inputSchema: {
|
|
992
|
+
task: z.string().min(1).describe("Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title"),
|
|
993
|
+
item: z.string().min(1).describe("Checklist item selector: C{n} (e.g. C2), item id, or title (case-insensitive)"),
|
|
994
|
+
},
|
|
995
|
+
}, async ({ task: ref, item }) => {
|
|
996
|
+
const st = await requireStore();
|
|
997
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
998
|
+
if (!found)
|
|
999
|
+
return text(`Task not found: ${ref}`);
|
|
1000
|
+
let result = "";
|
|
1001
|
+
await st.updatePhase(found.phase.id, (phase) => {
|
|
1002
|
+
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
1003
|
+
if (!task) {
|
|
1004
|
+
result = `Task not found: ${ref}`;
|
|
1005
|
+
return phase;
|
|
1006
|
+
}
|
|
1007
|
+
const ck = task.checklist ?? [];
|
|
1008
|
+
if (ck.length === 0) {
|
|
1009
|
+
result = `Task "${found.task.title}" has no checklist.`;
|
|
1010
|
+
return phase;
|
|
1011
|
+
}
|
|
1012
|
+
const removed = removeChecklistItem(ck, item);
|
|
1013
|
+
if (!removed) {
|
|
1014
|
+
result = `No checklist item matching "${item}".`;
|
|
1015
|
+
return phase;
|
|
1016
|
+
}
|
|
1017
|
+
task.updatedAt = nowISO();
|
|
1018
|
+
phase.updatedAt = nowISO();
|
|
1019
|
+
result = `Removed C${removed.number} "${removed.title}" (${ck.length} items left)`;
|
|
1020
|
+
return phase;
|
|
1021
|
+
});
|
|
1022
|
+
return writeAndSummarize(st, `✅ ${result}`);
|
|
1023
|
+
});
|
|
714
1024
|
server.registerTool("planner-task-delete", {
|
|
715
1025
|
description: "Delete a task.",
|
|
716
1026
|
inputSchema: { task: z.string().min(1) },
|
|
717
1027
|
}, async ({ task: ref }) => {
|
|
718
1028
|
const st = await requireStore();
|
|
719
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
1029
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
720
1030
|
if (!found)
|
|
721
1031
|
return text(`Task not found: ${ref}`);
|
|
722
1032
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -726,23 +1036,30 @@ server.registerTool("planner-task-delete", {
|
|
|
726
1036
|
return phase;
|
|
727
1037
|
});
|
|
728
1038
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
729
|
-
|
|
1039
|
+
const tdFeatures = (await st.loadFeatures()).features;
|
|
1040
|
+
return writeAndSummarize(st, `Task deleted: ${taskCompositeRef(found.task, found.phase, tdFeatures)}${found.task.shortId ? ` · ${found.task.shortId}` : ""}`, { deleted: found.task.id });
|
|
730
1041
|
});
|
|
731
1042
|
server.registerTool("planner-task-start", {
|
|
732
1043
|
description: "Set a task to in-progress.",
|
|
733
1044
|
inputSchema: { task: z.string().min(1) },
|
|
734
1045
|
}, async ({ task: ref }) => {
|
|
735
1046
|
const st = await requireStore();
|
|
736
|
-
|
|
737
|
-
// task_start — the handoff is a captured-context artifact, not a lock (e.g. a
|
|
738
|
-
// small modification after writing a handoff should start without forcing
|
|
739
|
-
// deletion, which would lose context).
|
|
740
|
-
const handoffNotice = (await st.listHandoffs()).length > 0
|
|
741
|
-
? "ℹ️ One or more phases have a pending handoff — if relevant to the task you're starting, read it with planner-handoff-show <ref>, then clear with planner-handoff-clear. Proceeding with task start.\n"
|
|
742
|
-
: "";
|
|
743
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
1047
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
744
1048
|
if (!found)
|
|
745
1049
|
return text(`Task not found: ${ref}`);
|
|
1050
|
+
// Auto-archive the handoff on THIS task's phase (reason: task-started) — the
|
|
1051
|
+
// agent is now actively working, so the captured context is consumed. Other
|
|
1052
|
+
// phases' handoffs are left untouched (informational, non-blocking).
|
|
1053
|
+
const allHandoffs = await st.listHandoffs();
|
|
1054
|
+
let handoffNotice = "";
|
|
1055
|
+
if ((found.phase.handoff ?? "") !== "") {
|
|
1056
|
+
await st.clearPhaseHandoff(found.phase.id, "task-started").catch(() => { });
|
|
1057
|
+
handoffNotice = "📦 Archived handoff for this phase (task started) — recover via planner-handoff-list + .planner/.local/handoff-archive/.\n";
|
|
1058
|
+
}
|
|
1059
|
+
const _otherHandoffs = allHandoffs.filter((h) => h.phaseId !== found.phase.id);
|
|
1060
|
+
if (_otherHandoffs.length > 0) {
|
|
1061
|
+
handoffNotice += `ℹ️ ${_otherHandoffs.length} other phase handoff(s) pending — review with planner-handoff-list if relevant.\n`;
|
|
1062
|
+
}
|
|
746
1063
|
const timestamp = nowISO();
|
|
747
1064
|
let updatedTask;
|
|
748
1065
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -769,7 +1086,9 @@ server.registerTool("planner-task-start", {
|
|
|
769
1086
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
770
1087
|
const features = (await st.loadFeatures()).features;
|
|
771
1088
|
const t = updatedTask ?? found.task;
|
|
772
|
-
|
|
1089
|
+
const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
|
|
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}`);
|
|
773
1092
|
});
|
|
774
1093
|
server.registerTool("planner-task-complete", {
|
|
775
1094
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -780,7 +1099,7 @@ server.registerTool("planner-task-complete", {
|
|
|
780
1099
|
},
|
|
781
1100
|
}, async ({ task: ref, force, description_update }) => {
|
|
782
1101
|
const st = await requireStore();
|
|
783
|
-
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
1102
|
+
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
784
1103
|
if (!found)
|
|
785
1104
|
return text(`Task not found: ${ref}`);
|
|
786
1105
|
const unchecked = found.task.checklist.filter((item) => !item.checked);
|
|
@@ -842,23 +1161,44 @@ server.registerTool("planner-handoff-show", {
|
|
|
842
1161
|
return text(`Handoff for ${r.compositeRef}:\n\n${content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
843
1162
|
});
|
|
844
1163
|
server.registerTool("planner-handoff-write", {
|
|
845
|
-
description: "Write/refresh the entity-scoped handoff on a phase. Pass content (markdown). phaseRef optional (default: current in-progress phase). Captures design context for a resuming agent on that phase.",
|
|
1164
|
+
description: "Write/refresh the entity-scoped handoff on a phase. Pass content (markdown) AND a meaningful `title` summarizing the work (e.g. 'P049 — featureId validation: tests + adapter wiring'). The title becomes the H1 / first line shown in handoff lists; generic titles like 'Handoff' or 'Canonical handoff' are REJECTED. phaseRef optional (default: current in-progress phase). Captures design context for a resuming agent on that phase.",
|
|
846
1165
|
inputSchema: {
|
|
847
1166
|
phaseRef: z.string().optional().describe("Phase ref. Default: current in-progress phase."),
|
|
1167
|
+
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'."),
|
|
848
1168
|
content: z.string().min(1).describe("Handoff text (markdown)."),
|
|
849
1169
|
},
|
|
850
|
-
}, async ({ phaseRef, content }) => {
|
|
1170
|
+
}, async ({ phaseRef, title, content }) => {
|
|
851
1171
|
const st = await requireStore();
|
|
1172
|
+
let body = content.trim();
|
|
1173
|
+
const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
|
1174
|
+
const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
|
|
1175
|
+
const isGeneric = !firstHeadingText || /^(handoff|canonical handoff|session handoff)$/i.test(firstHeadingText);
|
|
1176
|
+
if (!title && isGeneric) {
|
|
1177
|
+
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.");
|
|
1178
|
+
}
|
|
1179
|
+
if (title && title.trim()) {
|
|
1180
|
+
const t = title.trim();
|
|
1181
|
+
const lines = body.split(/\r?\n/);
|
|
1182
|
+
const firstIdx = lines.findIndex((l) => l.trim().length > 0);
|
|
1183
|
+
if (firstIdx !== -1 && /^#+\s/.test(lines[firstIdx] ?? "")) {
|
|
1184
|
+
lines[firstIdx] = `# ${t}`;
|
|
1185
|
+
body = lines.join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
body = `# ${t}\n\n${body}`;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
852
1191
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
853
1192
|
if (!r.ok)
|
|
854
1193
|
return text(`❌ ${r.error}`);
|
|
855
|
-
await st.setPhaseHandoff(r.phase.id,
|
|
1194
|
+
await st.setPhaseHandoff(r.phase.id, body);
|
|
856
1195
|
return text(`✅ Wrote handoff on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
857
1196
|
});
|
|
858
1197
|
server.registerTool("planner-handoff-prepare", {
|
|
859
1198
|
description: "Return instructions for the agent to prepare a canonical handoff, then call planner-handoff-write (entity-scoped, phase.handoff).",
|
|
860
1199
|
}, async () => text([
|
|
861
1200
|
"Prepare the canonical session handoff and write it on the current in-progress phase with planner-handoff-write (omit phaseRef to target the in-progress phase).",
|
|
1201
|
+
"Pass a meaningful `title` summarizing the work (e.g. 'P049 — featureId validation: tests + adapter wiring') — it becomes the H1 / first line in handoff lists. Generic titles like 'Handoff' or 'Canonical handoff' are REJECTED.",
|
|
862
1202
|
"Required sections: Created at, Updated at, Reason, Current focus, What was being done, How to resume, Files touched, Blockers, Next steps, Recent decisions, Reminder.",
|
|
863
1203
|
"Stored on the phase.handoff field (entity-scoped). .planner/HANDOFF.md is deprecated.",
|
|
864
1204
|
].join("\n")));
|
|
@@ -870,8 +1210,8 @@ server.registerTool("planner-handoff-clear", {
|
|
|
870
1210
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
871
1211
|
if (!r.ok)
|
|
872
1212
|
return text(`❌ ${r.error}`);
|
|
873
|
-
await st.clearPhaseHandoff(r.phase.id);
|
|
874
|
-
return text(`✅ Cleared handoff on ${r.compositeRef}
|
|
1213
|
+
await st.clearPhaseHandoff(r.phase.id, "manual");
|
|
1214
|
+
return text(`✅ Cleared handoff on ${r.compositeRef} (handoffUpdatedAt preserved as audit; archived to .planner/.local/handoff-archive/).`, { phaseRef: r.compositeRef, phaseId: r.phase.id });
|
|
875
1215
|
});
|
|
876
1216
|
server.registerTool("planner-web", {
|
|
877
1217
|
description: "Manage the planner web dashboard (start/status/stop) from MCP stdio. Runs an in-process HTTP+WS server bound to LAN; it lives as long as this MCP process. plan-root comes from AGENT_PLAN_ROOT or cwd()/.planner.",
|
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.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
"dist/**/*.js",
|
|
18
18
|
"dist/**/*.d.ts",
|
|
19
19
|
"dist/**/*.d.ts.map",
|
|
20
|
+
"server.json",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
24
|
+
"mcpName": "io.github.ovidius72/agent-plan",
|
|
23
25
|
"publishConfig": {
|
|
24
26
|
"access": "public"
|
|
25
27
|
},
|
|
@@ -41,8 +43,8 @@
|
|
|
41
43
|
"dependencies": {
|
|
42
44
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
43
45
|
"zod": "^3.24.0",
|
|
44
|
-
"@agent-plan/core": "0.2.19-next.
|
|
45
|
-
"@agent-plan/server": "0.2.19-next.
|
|
46
|
+
"@agent-plan/core": "0.2.19-next.20",
|
|
47
|
+
"@agent-plan/server": "0.2.19-next.20"
|
|
46
48
|
},
|
|
47
49
|
"scripts": {
|
|
48
50
|
"build": "tsc -p tsconfig.json",
|
package/server.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "io.github.ovidius72/agent-plan",
|
|
3
|
+
"version": "0.2.19-next.18",
|
|
4
|
+
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ovidius72/agent-planner.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ovidius72/agent-planner#readme",
|
|
11
|
+
"package": {
|
|
12
|
+
"type": "npm",
|
|
13
|
+
"name": "@agent-plan/mcp"
|
|
14
|
+
}
|
|
15
|
+
}
|