@agent-plan/mcp 0.2.25 → 0.2.26
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 +681 -56
- 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":";AAi8EA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import * as z from "zod/v4";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
|
-
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureReadForSessionId, markPhaseReadForSessionId, markTaskReadForSessionId, contextReadEligibilityForSession, hasValidSessionAttestation, hasReadRequirementsForSession, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded } from "@agent-plan/core";
|
|
8
|
+
import { PlanStore, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureReadForSessionId, markPhaseReadForSessionId, markTaskReadForSessionId, contextReadEligibilityForSession, hasValidSessionAttestation, hasReadRequirementsForSession, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, MAX_HANDOFF_CONTENT_CHARS, HandoffContractError } from "@agent-plan/core";
|
|
9
9
|
import { serve } from "@agent-plan/server";
|
|
10
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
10
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createRequirementId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, formatIdeaRef, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
11
11
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
12
12
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
13
13
|
const fallbackPlannerSessionId = `mcp-process:${randomUUID()}`;
|
|
@@ -21,6 +21,35 @@ function plannerSessionIdFor(extra) {
|
|
|
21
21
|
function text(textValue, structuredContent) {
|
|
22
22
|
return structuredContent ? { content: [{ type: "text", text: textValue }], structuredContent } : { content: [{ type: "text", text: textValue }] };
|
|
23
23
|
}
|
|
24
|
+
function handoffContractFailure(error) {
|
|
25
|
+
return {
|
|
26
|
+
isError: true,
|
|
27
|
+
content: [{ type: "text", text: `❌ Handoff refresh denied [${error.code}]: ${error.message}` }],
|
|
28
|
+
structuredContent: { errorCode: error.code, ...error.details },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function boundedHandoffForTransport(content) {
|
|
32
|
+
if (content.length <= MAX_HANDOFF_CONTENT_CHARS)
|
|
33
|
+
return { content, fullLength: content.length, truncated: false };
|
|
34
|
+
const suffix = "\n\n[Legacy handoff truncated for transport safety. Move extended detail to a linked file under .planner/docs/ and refresh the handoff.]";
|
|
35
|
+
return {
|
|
36
|
+
content: `${content.slice(0, MAX_HANDOFF_CONTENT_CHARS - suffix.length)}${suffix}`,
|
|
37
|
+
fullLength: content.length,
|
|
38
|
+
truncated: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function boundedPage(items, page, pageSize) {
|
|
42
|
+
const size = Math.min(25, Math.max(1, Math.trunc(pageSize)));
|
|
43
|
+
const totalPages = Math.max(1, Math.ceil(items.length / size));
|
|
44
|
+
const currentPage = Math.min(totalPages, Math.max(1, Math.trunc(page)));
|
|
45
|
+
return {
|
|
46
|
+
items: items.slice((currentPage - 1) * size, currentPage * size),
|
|
47
|
+
page: currentPage,
|
|
48
|
+
pageSize: size,
|
|
49
|
+
total: items.length,
|
|
50
|
+
totalPages,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
24
53
|
function derivedStatusReadOnlyResult(entity, ref, attemptedStatus, effectiveStatus) {
|
|
25
54
|
const childKind = entity === "feature" ? "phases" : "tasks";
|
|
26
55
|
const retryAction = entity === "feature"
|
|
@@ -52,6 +81,23 @@ function derivedStatusReadOnlyResult(entity, ref, attemptedStatus, effectiveStat
|
|
|
52
81
|
},
|
|
53
82
|
};
|
|
54
83
|
}
|
|
84
|
+
function mutationNoFieldsFailure(input) {
|
|
85
|
+
const outcome = noMutableFieldsReceived(input);
|
|
86
|
+
return {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: [{ type: "text", text: outcome.message }],
|
|
89
|
+
structuredContent: { ...outcome },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function legacyContextMigrationSummary(preview) {
|
|
93
|
+
return [
|
|
94
|
+
`Legacy context present: ${preview.hasLegacyContext ? "yes" : "no"}`,
|
|
95
|
+
`Guideline additions: ${preview.guidelineAdditions.length} (${preview.skippedGuidelineDuplicates} duplicate(s) skipped)`,
|
|
96
|
+
`Accepted decision additions: ${preview.acceptedDecisionAdditions.length} (${preview.skippedDecisionDuplicates} duplicate(s) skipped)`,
|
|
97
|
+
`Legacy counts: globalRules=${preview.legacyCounts.globalRules}, workflowRules=${preview.legacyCounts.workflowRules}, decisions=${preview.legacyCounts.decisions}`,
|
|
98
|
+
`Fields cleared on apply: ${preview.fieldsClearedOnApply.join(", ") || "none"}`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
55
101
|
function taskStartError(outcome, structuredContent = {}) {
|
|
56
102
|
return {
|
|
57
103
|
isError: true,
|
|
@@ -66,14 +112,15 @@ function taskStartError(outcome, structuredContent = {}) {
|
|
|
66
112
|
structuredContent: { ...outcome, ...structuredContent },
|
|
67
113
|
};
|
|
68
114
|
}
|
|
69
|
-
function mcpContextReadActions(eligibility, refs, requirementReadRequired, retryAction) {
|
|
70
|
-
const actions =
|
|
115
|
+
function mcpContextReadActions(eligibility, refs, projectGuidelinesReadRequired, requirementReadRequired, retryAction) {
|
|
116
|
+
const actions = projectGuidelinesReadRequired ? ["planner-project-guidelines-show"] : [];
|
|
117
|
+
actions.push(...(eligibility.requiredReads ?? []).flatMap((read) => {
|
|
71
118
|
if (read.kind === "task")
|
|
72
119
|
return [`planner-task-show ${refs.task} with full=true`];
|
|
73
120
|
if (read.kind === "phase")
|
|
74
121
|
return [`planner-phase-show ${refs.phase} with full=true`];
|
|
75
122
|
return refs.feature ? [`planner-feature-show ${refs.feature} with full=true`] : [];
|
|
76
|
-
});
|
|
123
|
+
}));
|
|
77
124
|
if (requirementReadRequired)
|
|
78
125
|
actions.push("planner-requirement-list");
|
|
79
126
|
actions.push(retryAction);
|
|
@@ -194,6 +241,21 @@ const server = new McpServer({
|
|
|
194
241
|
name: "agent-plan-planner",
|
|
195
242
|
version: MCP_PACKAGE.version,
|
|
196
243
|
});
|
|
244
|
+
async function resolveRequirementPhaseRefs(st, refs) {
|
|
245
|
+
if (!refs || refs.length === 0)
|
|
246
|
+
return { ok: false, error: "linkedPhaseIds must contain at least one phase reference." };
|
|
247
|
+
const [phases, features] = await Promise.all([st.loadAllPhases(), st.loadFeatures()]);
|
|
248
|
+
const linkedPhaseIds = [];
|
|
249
|
+
for (const rawRef of refs) {
|
|
250
|
+
const ref = rawRef.trim();
|
|
251
|
+
const phase = ref ? findPhaseByRef(phases, features.features, ref) : undefined;
|
|
252
|
+
if (!phase)
|
|
253
|
+
return { ok: false, error: `Linked phase not found: ${rawRef}` };
|
|
254
|
+
if (!linkedPhaseIds.includes(phase.id))
|
|
255
|
+
linkedPhaseIds.push(phase.id);
|
|
256
|
+
}
|
|
257
|
+
return { ok: true, linkedPhaseIds };
|
|
258
|
+
}
|
|
197
259
|
async function resolvePhaseForHandoff(st, ref) {
|
|
198
260
|
const phases = await st.loadAllPhases();
|
|
199
261
|
const features = (await st.loadFeatures()).features;
|
|
@@ -294,6 +356,7 @@ server.registerTool("planner-show", {
|
|
|
294
356
|
const summary = [
|
|
295
357
|
`📋 ${project.name}`,
|
|
296
358
|
`Description: ${project.description || "(not set)"}`,
|
|
359
|
+
...(project.descriptionRef ? [`Description ref: ${project.descriptionRef}`] : []),
|
|
297
360
|
`Goal: ${project.goal || "(not set)"}`,
|
|
298
361
|
`Features: ${features.length} | Phases: ${phases.length} | Tasks: ${totalTasks}`,
|
|
299
362
|
`Updated: ${manifest.updatedAt || "(unknown)"}`,
|
|
@@ -303,7 +366,7 @@ server.registerTool("planner-show", {
|
|
|
303
366
|
].join("\n");
|
|
304
367
|
// Compact structured overview (no full plan: avoids 1M+ char token overflow).
|
|
305
368
|
const overview = {
|
|
306
|
-
project: { name: project.name, description: project.description || null, goal: project.goal || null, updatedAt: manifest.updatedAt || null },
|
|
369
|
+
project: { name: project.name, description: project.description || null, descriptionRef: project.descriptionRef || null, goal: project.goal || null, updatedAt: manifest.updatedAt || null },
|
|
307
370
|
counts: { features: features.length, phases: phases.length, tasks: totalTasks },
|
|
308
371
|
features: features.map((feature) => ({
|
|
309
372
|
ref: formatFeatureRef(feature.number),
|
|
@@ -369,6 +432,8 @@ server.registerTool("planner-project-language", {
|
|
|
369
432
|
server.registerTool("planner-project-discuss", {
|
|
370
433
|
description: "Record/update project-level discovery fields. This is the MCP equivalent of grouped project discuss without Pi UI prompts.",
|
|
371
434
|
inputSchema: {
|
|
435
|
+
description: z.string().optional(),
|
|
436
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full project description."),
|
|
372
437
|
goal: z.string().optional(),
|
|
373
438
|
scope: z.array(z.string()).optional(),
|
|
374
439
|
outOfScope: z.array(z.string()).optional(),
|
|
@@ -380,6 +445,32 @@ server.registerTool("planner-project-discuss", {
|
|
|
380
445
|
}, async (params) => {
|
|
381
446
|
const st = await requireStore();
|
|
382
447
|
const project = await st.loadProject();
|
|
448
|
+
const mutableFields = ["description", "descriptionRef", "goal", "scope", "outOfScope", "technologies", "tools", "globalRules", "decisions"];
|
|
449
|
+
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
450
|
+
if (receivedFields.length === 0) {
|
|
451
|
+
return mutationNoFieldsFailure({
|
|
452
|
+
entity: "project",
|
|
453
|
+
ref: project.name,
|
|
454
|
+
operation: "discuss",
|
|
455
|
+
mutableFields,
|
|
456
|
+
retryCommand: "planner-project-discuss with a concise description summary and the intended metadata",
|
|
457
|
+
readBackCommand: "planner-show",
|
|
458
|
+
descriptionFallback: {
|
|
459
|
+
inputField: "description",
|
|
460
|
+
storedField: "project description",
|
|
461
|
+
descriptionRefField: "descriptionRef",
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (params.description !== undefined)
|
|
466
|
+
project.description = params.description.trim();
|
|
467
|
+
if (params.descriptionRef !== undefined) {
|
|
468
|
+
const descriptionRef = normalizeDescriptionRef(params.descriptionRef);
|
|
469
|
+
if (descriptionRef)
|
|
470
|
+
project.descriptionRef = descriptionRef;
|
|
471
|
+
else
|
|
472
|
+
delete project.descriptionRef;
|
|
473
|
+
}
|
|
383
474
|
if (params.goal !== undefined)
|
|
384
475
|
project.goal = params.goal.trim();
|
|
385
476
|
if (params.scope !== undefined)
|
|
@@ -395,25 +486,83 @@ server.registerTool("planner-project-discuss", {
|
|
|
395
486
|
if (params.decisions !== undefined)
|
|
396
487
|
project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
|
|
397
488
|
await st.saveProject(project);
|
|
398
|
-
return writeAndSummarize(st, `Project discussed/updated: ${project.name}
|
|
489
|
+
return writeAndSummarize(st, `Project discussed/updated: ${project.name}. Fields saved: ${receivedFields.join(", ")}.`, { project, discussed: true, updatedFields: receivedFields });
|
|
490
|
+
});
|
|
491
|
+
server.registerTool("planner-project-guidelines-show", {
|
|
492
|
+
description: "Read the canonical Project Guidelines / Coding Standard section for this project. Call this when planner-task-start or planner-task-switch nextActions require it, and keep the guidance in working memory while doing task work.",
|
|
493
|
+
}, async (extra) => {
|
|
494
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
495
|
+
const st = await requireStore();
|
|
496
|
+
const project = await st.loadProject();
|
|
497
|
+
if (project.projectGuidelines.content.trim()) {
|
|
498
|
+
await st.recordProjectGuidelinesRead({ sessionId: plannerSessionId });
|
|
499
|
+
}
|
|
500
|
+
const refreshed = await st.loadProject();
|
|
501
|
+
const content = refreshed.projectGuidelines.content.trim();
|
|
502
|
+
return text(content
|
|
503
|
+
? ["## Project Guidelines", content].join("\n\n")
|
|
504
|
+
: "No project guidelines set.", { projectGuidelines: refreshed.projectGuidelines, readRecorded: Boolean(content) });
|
|
505
|
+
});
|
|
506
|
+
server.registerTool("planner-project-guidelines-update", {
|
|
507
|
+
description: "Replace the canonical Project Guidelines / Coding Standard text that agents must read on load and before starting or switching task work.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
content: z.string().describe("Full Project Guidelines markdown/text. Pass an empty string to clear the section."),
|
|
510
|
+
},
|
|
511
|
+
}, async ({ content }) => {
|
|
512
|
+
const st = await requireStore();
|
|
513
|
+
const project = await st.loadProject();
|
|
514
|
+
project.projectGuidelines = {
|
|
515
|
+
content: content.trim(),
|
|
516
|
+
updatedAt: project.projectGuidelines.updatedAt,
|
|
517
|
+
sessionInfo: project.projectGuidelines.sessionInfo,
|
|
518
|
+
};
|
|
519
|
+
await st.saveProject(project);
|
|
520
|
+
const refreshed = await st.loadProject();
|
|
521
|
+
return writeAndSummarize(st, refreshed.projectGuidelines.content
|
|
522
|
+
? `Project Guidelines updated. Updated at ${refreshed.projectGuidelines.updatedAt}.`
|
|
523
|
+
: "Project Guidelines cleared.", { updated: true, projectGuidelines: refreshed.projectGuidelines });
|
|
524
|
+
});
|
|
525
|
+
server.registerTool("planner-project-context-migrate", {
|
|
526
|
+
description: "Preview or explicitly apply the lossless migration of legacy project globalRules/workflowRules into Project Guidelines and legacy decisions into structured accepted decisions. Ordinary planner loads never run this migration.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
apply: z.boolean().default(false).describe("False previews without mutation. True explicitly applies the previewed migration and clears only the migrated legacy fields."),
|
|
529
|
+
},
|
|
530
|
+
}, async ({ apply }) => {
|
|
531
|
+
const st = await requireStore();
|
|
532
|
+
const preview = await st.previewLegacyProjectContextMigration();
|
|
533
|
+
if (!apply) {
|
|
534
|
+
return text([`Project context migration preview (no changes applied).`, legacyContextMigrationSummary(preview), preview.hasLegacyContext ? "Retry with apply=true to confirm." : "No migration is needed."].join("\n"), { applied: false, preview });
|
|
535
|
+
}
|
|
536
|
+
const result = await st.migrateLegacyProjectContext();
|
|
537
|
+
return writeAndSummarize(st, [result.applied ? "Legacy project context migration applied and verified." : "No legacy project context required migration.", legacyContextMigrationSummary(result.preview)].join("\n"), { ...result });
|
|
399
538
|
});
|
|
400
539
|
server.registerTool("planner-feature-list", {
|
|
401
|
-
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.",
|
|
540
|
+
description: "List features (compact: F00x · shortId — name (status; priority N; 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.",
|
|
402
541
|
inputSchema: { featureRef: z.string().optional().describe("Optional: filter to one feature (F00x/shortId/UUID/name).") },
|
|
403
542
|
}, async ({ featureRef }) => {
|
|
404
543
|
const st = await requireStore();
|
|
405
544
|
const allFeatures = (await st.loadFeatures()).features;
|
|
406
545
|
const features = featureRef ? allFeatures.filter((f) => f.id === findFeatureByRef(allFeatures, featureRef)?.id) : allFeatures;
|
|
407
546
|
const phases = await st.loadAllPhases();
|
|
408
|
-
const
|
|
547
|
+
const items = features.map((feature) => {
|
|
409
548
|
const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
|
|
410
549
|
const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
411
|
-
return
|
|
550
|
+
return {
|
|
551
|
+
id: feature.id,
|
|
552
|
+
ref: formatFeatureRef(feature.number),
|
|
553
|
+
shortId: feature.shortId,
|
|
554
|
+
name: feature.name,
|
|
555
|
+
status: feature.status,
|
|
556
|
+
priority: feature.priority,
|
|
557
|
+
phaseCount: featurePhases.length,
|
|
558
|
+
taskCount,
|
|
559
|
+
};
|
|
412
560
|
});
|
|
413
|
-
|
|
561
|
+
const lines = items.map((feature) => `- ${feature.ref}${feature.shortId ? ` · ${feature.shortId}` : ""} — ${feature.name} (${feature.status}${feature.priority > 0 ? `; priority ${feature.priority}` : ""}; ${feature.phaseCount} phases, ${feature.taskCount} tasks)`);
|
|
562
|
+
return text(lines.join("\n") || "No features", { features: items });
|
|
414
563
|
});
|
|
415
564
|
server.registerTool("planner-phase-list", {
|
|
416
|
-
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.",
|
|
565
|
+
description: "List phases (compact: F00x/P00x · shortId — title (status; priority N; N tasks) [F00x]). Filters: featureRef, status. Cheap discovery — do NOT read .planner/ files or planner-plan-get full=true.",
|
|
417
566
|
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.") },
|
|
418
567
|
}, async ({ featureRef, status }) => {
|
|
419
568
|
const st = await requireStore();
|
|
@@ -427,15 +576,27 @@ server.registerTool("planner-phase-list", {
|
|
|
427
576
|
}
|
|
428
577
|
if (status)
|
|
429
578
|
phases = phases.filter((p) => p.status === status);
|
|
430
|
-
const
|
|
579
|
+
const items = phases.map((phase) => {
|
|
431
580
|
const fNum = featureNumberOfPhase(phase, features);
|
|
432
|
-
|
|
433
|
-
|
|
581
|
+
return {
|
|
582
|
+
id: phase.id,
|
|
583
|
+
ref: formatPhaseRef(phase.number, fNum),
|
|
584
|
+
shortId: phase.shortId,
|
|
585
|
+
title: phase.title,
|
|
586
|
+
status: phase.status,
|
|
587
|
+
priority: phase.priority,
|
|
588
|
+
taskCount: phase.tasks.length,
|
|
589
|
+
featureNumber: fNum,
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
const lines = items.map((phase) => {
|
|
593
|
+
const fTag = phase.featureNumber !== undefined ? ` [F${String(phase.featureNumber).padStart(3, "0")}]` : "";
|
|
594
|
+
return `- ${phase.ref}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.status}${phase.priority > 0 ? `; priority ${phase.priority}` : ""}; ${phase.taskCount} tasks)${fTag}`;
|
|
434
595
|
});
|
|
435
|
-
return text(lines.join("\n") || "No phases");
|
|
596
|
+
return text(lines.join("\n") || "No phases", { phases: items });
|
|
436
597
|
});
|
|
437
598
|
server.registerTool("planner-task-list", {
|
|
438
|
-
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.",
|
|
599
|
+
description: "List tasks (compact: F00x/P00x/T00x · shortId — title (status; priority N)). Filters: featureRef, phaseRef, status. Cheap discovery — do NOT read .planner/ files or planner-plan-get full=true.",
|
|
439
600
|
inputSchema: { featureRef: z.string().optional(), phaseRef: z.string().optional(), status: z.string().optional().describe("Optional: filter by status name.") },
|
|
440
601
|
}, async ({ featureRef, phaseRef, status }) => {
|
|
441
602
|
const st = await requireStore();
|
|
@@ -453,15 +614,25 @@ server.registerTool("planner-task-list", {
|
|
|
453
614
|
return text(`Phase not found: ${phaseRef}`);
|
|
454
615
|
phases = [p];
|
|
455
616
|
}
|
|
456
|
-
const
|
|
617
|
+
const items = [];
|
|
457
618
|
for (const phase of phases) {
|
|
458
619
|
for (const task of phase.tasks) {
|
|
459
620
|
if (status && task.status !== status)
|
|
460
621
|
continue;
|
|
461
|
-
|
|
622
|
+
items.push({
|
|
623
|
+
id: task.id,
|
|
624
|
+
ref: taskCompositeRef(task, phase, features),
|
|
625
|
+
shortId: task.shortId,
|
|
626
|
+
title: task.title,
|
|
627
|
+
status: task.status,
|
|
628
|
+
priority: task.priority,
|
|
629
|
+
phaseId: phase.id,
|
|
630
|
+
...(phase.featureId ? { featureId: phase.featureId } : {}),
|
|
631
|
+
});
|
|
462
632
|
}
|
|
463
633
|
}
|
|
464
|
-
|
|
634
|
+
const out = items.map((task) => `- ${task.ref}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status}${task.priority > 0 ? `; priority ${task.priority}` : ""})`);
|
|
635
|
+
return text(out.join("\n") || "No tasks", { tasks: items });
|
|
465
636
|
});
|
|
466
637
|
server.registerTool("planner-feature-add", {
|
|
467
638
|
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.",
|
|
@@ -523,30 +694,58 @@ server.registerTool("planner-feature-show", {
|
|
|
523
694
|
markFeatureReadForSessionId(plannerSessionId, feature.id);
|
|
524
695
|
const phases = (await st.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
525
696
|
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
|
|
526
|
-
|
|
697
|
+
const descriptionRefBlock = feature.descriptionRef ? `\n\nDescription reference:\n- ${feature.descriptionRef}` : "";
|
|
698
|
+
return text(full ? `${summary}\n\n${feature.description || ""}${descriptionRefBlock}` : summary, full ? { feature: { ref: formatFeatureRef(feature.number), description: feature.description, descriptionRef: feature.descriptionRef || "" } } : undefined);
|
|
527
699
|
});
|
|
528
700
|
server.registerTool("planner-feature-discuss", {
|
|
529
701
|
description: "Persist feature discovery/governance fields and mark the feature context as ready.",
|
|
530
702
|
inputSchema: {
|
|
531
703
|
feature: z.string().min(1).describe("Feature ref. Accepts F00x, shortId, UUID, or title."),
|
|
532
704
|
description: z.string().optional().describe("Current implementation state, scope, and goals for this feature."),
|
|
705
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full feature description."),
|
|
533
706
|
workDone: z.string().optional().describe("What is already implemented / decided."),
|
|
534
707
|
workRemaining: z.string().optional().describe("What still needs to be done."),
|
|
535
708
|
dependencies: z.array(z.string()).optional().describe("Cross-feature or external dependencies."),
|
|
536
709
|
},
|
|
537
|
-
}, async ({ feature: ref, description, workDone, workRemaining, dependencies }) => {
|
|
710
|
+
}, async ({ feature: ref, description, descriptionRef: rawDescriptionRef, workDone, workRemaining, dependencies }) => {
|
|
538
711
|
const st = await requireStore();
|
|
539
712
|
const features = (await st.loadFeatures()).features;
|
|
540
713
|
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
541
714
|
if (!resolvedFeature.ok)
|
|
542
715
|
return text(resolvedFeature.error);
|
|
543
716
|
const feature = resolvedFeature.feature;
|
|
717
|
+
const mutableFields = ["description", "descriptionRef", "workDone", "workRemaining", "dependencies"];
|
|
718
|
+
const discussInputs = { description, descriptionRef: rawDescriptionRef, workDone, workRemaining, dependencies };
|
|
719
|
+
const receivedFields = mutableFields.filter((field) => discussInputs[field] !== undefined);
|
|
720
|
+
if (receivedFields.length === 0) {
|
|
721
|
+
return mutationNoFieldsFailure({
|
|
722
|
+
entity: "feature",
|
|
723
|
+
ref: formatFeatureRef(feature.number),
|
|
724
|
+
operation: "discuss",
|
|
725
|
+
mutableFields,
|
|
726
|
+
retryCommand: `planner-feature-discuss ${formatFeatureRef(feature.number)}`,
|
|
727
|
+
readBackCommand: `planner-feature-show ${formatFeatureRef(feature.number)} with full=true`,
|
|
728
|
+
descriptionFallback: {
|
|
729
|
+
entityId: feature.id,
|
|
730
|
+
inputField: "description",
|
|
731
|
+
storedField: "feature description",
|
|
732
|
+
descriptionRefField: "descriptionRef",
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
}
|
|
544
736
|
const updated = await st.updateFeatures((doc) => {
|
|
545
737
|
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
546
738
|
if (!target)
|
|
547
739
|
return doc;
|
|
548
740
|
if (description !== undefined)
|
|
549
741
|
target.description = description.trim();
|
|
742
|
+
if (rawDescriptionRef !== undefined) {
|
|
743
|
+
const descriptionRef = normalizeDescriptionRef(rawDescriptionRef);
|
|
744
|
+
if (descriptionRef)
|
|
745
|
+
target.descriptionRef = descriptionRef;
|
|
746
|
+
else
|
|
747
|
+
delete target.descriptionRef;
|
|
748
|
+
}
|
|
550
749
|
if (workDone !== undefined)
|
|
551
750
|
target.workDone = workDone.trim();
|
|
552
751
|
if (workRemaining !== undefined)
|
|
@@ -560,7 +759,7 @@ server.registerTool("planner-feature-discuss", {
|
|
|
560
759
|
return doc;
|
|
561
760
|
});
|
|
562
761
|
const result = updated.features.find((entry) => entry.id === feature.id);
|
|
563
|
-
return writeAndSummarize(st, `✅ Feature discussed/updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}
|
|
762
|
+
return writeAndSummarize(st, `✅ Feature discussed/updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.`, { feature: result, discussed: true, updatedFields: receivedFields });
|
|
564
763
|
});
|
|
565
764
|
server.registerTool("planner-feature-update", {
|
|
566
765
|
description: "Update a feature.",
|
|
@@ -568,6 +767,7 @@ server.registerTool("planner-feature-update", {
|
|
|
568
767
|
feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."),
|
|
569
768
|
name: z.string().optional(),
|
|
570
769
|
description: z.string().optional(),
|
|
770
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full feature description."),
|
|
571
771
|
status: z.enum(STATUS_VALUES).optional(),
|
|
572
772
|
workDone: z.string().optional(),
|
|
573
773
|
workRemaining: z.string().optional(),
|
|
@@ -585,6 +785,24 @@ server.registerTool("planner-feature-update", {
|
|
|
585
785
|
if (updates.status !== undefined) {
|
|
586
786
|
return derivedStatusReadOnlyResult("feature", formatFeatureRef(feature.number), updates.status, feature.status);
|
|
587
787
|
}
|
|
788
|
+
const mutableFields = ["name", "description", "descriptionRef", "workDone", "workRemaining", "startDate", "endDate", "priority"];
|
|
789
|
+
const receivedFields = mutableFields.filter((field) => updates[field] !== undefined);
|
|
790
|
+
if (receivedFields.length === 0) {
|
|
791
|
+
return mutationNoFieldsFailure({
|
|
792
|
+
entity: "feature",
|
|
793
|
+
ref: formatFeatureRef(feature.number),
|
|
794
|
+
operation: "update",
|
|
795
|
+
mutableFields,
|
|
796
|
+
retryCommand: `planner-feature-update ${formatFeatureRef(feature.number)}`,
|
|
797
|
+
readBackCommand: `planner-feature-show ${formatFeatureRef(feature.number)} with full=true`,
|
|
798
|
+
descriptionFallback: {
|
|
799
|
+
entityId: feature.id,
|
|
800
|
+
inputField: "description",
|
|
801
|
+
storedField: "feature description",
|
|
802
|
+
descriptionRefField: "descriptionRef",
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
}
|
|
588
806
|
const updated = await st.updateFeatures((doc) => {
|
|
589
807
|
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
590
808
|
if (!target)
|
|
@@ -593,6 +811,13 @@ server.registerTool("planner-feature-update", {
|
|
|
593
811
|
target.name = updates.name.trim();
|
|
594
812
|
if (updates.description !== undefined)
|
|
595
813
|
target.description = updates.description.trim();
|
|
814
|
+
if (updates.descriptionRef !== undefined) {
|
|
815
|
+
const descriptionRef = normalizeDescriptionRef(updates.descriptionRef);
|
|
816
|
+
if (descriptionRef)
|
|
817
|
+
target.descriptionRef = descriptionRef;
|
|
818
|
+
else
|
|
819
|
+
delete target.descriptionRef;
|
|
820
|
+
}
|
|
596
821
|
if (updates.workDone !== undefined)
|
|
597
822
|
target.workDone = updates.workDone.trim();
|
|
598
823
|
if (updates.workRemaining !== undefined)
|
|
@@ -607,7 +832,7 @@ server.registerTool("planner-feature-update", {
|
|
|
607
832
|
return doc;
|
|
608
833
|
});
|
|
609
834
|
const result = updated.features.find((entry) => entry.id === feature.id);
|
|
610
|
-
return writeAndSummarize(st, `✅ Feature updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}
|
|
835
|
+
return writeAndSummarize(st, `✅ Feature updated: ${formatFeatureRef(result.number)} — ${result.name}${result.shortId ? ` · ${result.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.`, { feature: result, updated: true, updatedFields: receivedFields });
|
|
611
836
|
});
|
|
612
837
|
server.registerTool("planner-feature-delete", {
|
|
613
838
|
description: "Delete a feature. By default, phases are unlinked rather than deleted.",
|
|
@@ -701,6 +926,7 @@ server.registerTool("planner-phase-add", {
|
|
|
701
926
|
updatedAt: timestamp,
|
|
702
927
|
handoff: "",
|
|
703
928
|
handoffUpdatedAt: "",
|
|
929
|
+
handoffAudit: null,
|
|
704
930
|
handoffReadAt: "",
|
|
705
931
|
handoffHistory: [],
|
|
706
932
|
statusLog: [],
|
|
@@ -736,10 +962,11 @@ server.registerTool("planner-phase-show", {
|
|
|
736
962
|
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
737
963
|
const reqCount = linkedRequirements.length;
|
|
738
964
|
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"}` : ""})`;
|
|
965
|
+
const descriptionRefBlock = phase.descriptionRef ? `\n\nDescription reference:\n- ${phase.descriptionRef}` : "";
|
|
739
966
|
const requirementsBlock = reqCount > 0
|
|
740
967
|
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
741
968
|
: "";
|
|
742
|
-
return text(full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary, {
|
|
969
|
+
return text(full ? `${summary}\n\n${phase.description || ""}${descriptionRefBlock}${requirementsBlock}` : summary, {
|
|
743
970
|
phase: {
|
|
744
971
|
ref: formatPhaseRef(phase.number, featureNumberOfPhase(phase, features)),
|
|
745
972
|
shortId: phase.shortId,
|
|
@@ -747,7 +974,7 @@ server.registerTool("planner-phase-show", {
|
|
|
747
974
|
summary: phase.summary,
|
|
748
975
|
status: phase.status,
|
|
749
976
|
taskCount: phase.tasks.length,
|
|
750
|
-
...(full ? { description: phase.description } : {}),
|
|
977
|
+
...(full ? { description: phase.description, descriptionRef: phase.descriptionRef || "" } : {}),
|
|
751
978
|
},
|
|
752
979
|
linkedRequirements,
|
|
753
980
|
});
|
|
@@ -759,6 +986,7 @@ server.registerTool("planner-phase-discuss", {
|
|
|
759
986
|
goal: z.string().optional(),
|
|
760
987
|
summary: z.string().optional(),
|
|
761
988
|
scope: z.string().optional(),
|
|
989
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full phase description."),
|
|
762
990
|
nonGoals: z.array(z.string()).optional(),
|
|
763
991
|
dependencies: z.array(z.string()).optional(),
|
|
764
992
|
risks: z.array(z.string()).optional(),
|
|
@@ -770,6 +998,24 @@ server.registerTool("planner-phase-discuss", {
|
|
|
770
998
|
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
771
999
|
if (!found)
|
|
772
1000
|
return text(`Phase not found: ${ref}`);
|
|
1001
|
+
const mutableFields = ["goal", "summary", "scope", "descriptionRef", "nonGoals", "dependencies", "risks", "completionCriteria"];
|
|
1002
|
+
const receivedFields = mutableFields.filter((field) => updates[field] !== undefined);
|
|
1003
|
+
if (receivedFields.length === 0) {
|
|
1004
|
+
return mutationNoFieldsFailure({
|
|
1005
|
+
entity: "phase",
|
|
1006
|
+
ref: formatPhaseRef(found.number, featureNumberOfPhase(found, features)),
|
|
1007
|
+
operation: "discuss",
|
|
1008
|
+
mutableFields,
|
|
1009
|
+
retryCommand: `planner-phase-discuss ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))}`,
|
|
1010
|
+
readBackCommand: `planner-phase-show ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} with full=true`,
|
|
1011
|
+
descriptionFallback: {
|
|
1012
|
+
entityId: found.id,
|
|
1013
|
+
inputField: "scope",
|
|
1014
|
+
storedField: "phase description",
|
|
1015
|
+
descriptionRefField: "descriptionRef",
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
773
1019
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
774
1020
|
if (updates.goal !== undefined)
|
|
775
1021
|
entry.goals = [updates.goal.trim()].filter(Boolean);
|
|
@@ -777,6 +1023,13 @@ server.registerTool("planner-phase-discuss", {
|
|
|
777
1023
|
entry.summary = updates.summary.trim();
|
|
778
1024
|
if (updates.scope !== undefined)
|
|
779
1025
|
entry.description = updates.scope.trim();
|
|
1026
|
+
if (updates.descriptionRef !== undefined) {
|
|
1027
|
+
const descriptionRef = normalizeDescriptionRef(updates.descriptionRef);
|
|
1028
|
+
if (descriptionRef)
|
|
1029
|
+
entry.descriptionRef = descriptionRef;
|
|
1030
|
+
else
|
|
1031
|
+
delete entry.descriptionRef;
|
|
1032
|
+
}
|
|
780
1033
|
if (updates.nonGoals !== undefined)
|
|
781
1034
|
entry.nonGoals = updates.nonGoals.map((item) => item.trim()).filter(Boolean);
|
|
782
1035
|
if (updates.dependencies !== undefined)
|
|
@@ -792,7 +1045,7 @@ server.registerTool("planner-phase-discuss", {
|
|
|
792
1045
|
entry.updatedAt = nowISO();
|
|
793
1046
|
return entry;
|
|
794
1047
|
});
|
|
795
|
-
return writeAndSummarize(st, `✅ Phase discussed/planned: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}
|
|
1048
|
+
return writeAndSummarize(st, `✅ Phase discussed/planned: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.`, { phase, discussed: true, updatedFields: receivedFields });
|
|
796
1049
|
});
|
|
797
1050
|
server.registerTool("planner-phase-update", {
|
|
798
1051
|
description: "Update phase fields.",
|
|
@@ -802,6 +1055,7 @@ server.registerTool("planner-phase-update", {
|
|
|
802
1055
|
status: z.enum(PHASE_STATUS_VALUES).optional(),
|
|
803
1056
|
summary: z.string().optional(),
|
|
804
1057
|
description: z.string().optional(),
|
|
1058
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full phase description."),
|
|
805
1059
|
priority: z.number().int().nonnegative().optional().describe("Display order within the feature (lower = higher)."),
|
|
806
1060
|
},
|
|
807
1061
|
}, async ({ phase: ref, ...updates }) => {
|
|
@@ -813,6 +1067,24 @@ server.registerTool("planner-phase-update", {
|
|
|
813
1067
|
if (updates.status !== undefined) {
|
|
814
1068
|
return derivedStatusReadOnlyResult("phase", formatPhaseRef(found.number, featureNumberOfPhase(found, features)), updates.status, found.status);
|
|
815
1069
|
}
|
|
1070
|
+
const mutableFields = ["title", "summary", "description", "descriptionRef", "priority"];
|
|
1071
|
+
const receivedFields = mutableFields.filter((field) => updates[field] !== undefined);
|
|
1072
|
+
if (receivedFields.length === 0) {
|
|
1073
|
+
return mutationNoFieldsFailure({
|
|
1074
|
+
entity: "phase",
|
|
1075
|
+
ref: formatPhaseRef(found.number, featureNumberOfPhase(found, features)),
|
|
1076
|
+
operation: "update",
|
|
1077
|
+
mutableFields,
|
|
1078
|
+
retryCommand: `planner-phase-update ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))}`,
|
|
1079
|
+
readBackCommand: `planner-phase-show ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} with full=true`,
|
|
1080
|
+
descriptionFallback: {
|
|
1081
|
+
entityId: found.id,
|
|
1082
|
+
inputField: "description",
|
|
1083
|
+
storedField: "phase description",
|
|
1084
|
+
descriptionRefField: "descriptionRef",
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
816
1088
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
817
1089
|
if (updates.title !== undefined)
|
|
818
1090
|
entry.title = updates.title.trim();
|
|
@@ -820,12 +1092,19 @@ server.registerTool("planner-phase-update", {
|
|
|
820
1092
|
entry.summary = updates.summary.trim();
|
|
821
1093
|
if (updates.description !== undefined)
|
|
822
1094
|
entry.description = updates.description.trim();
|
|
1095
|
+
if (updates.descriptionRef !== undefined) {
|
|
1096
|
+
const descriptionRef = normalizeDescriptionRef(updates.descriptionRef);
|
|
1097
|
+
if (descriptionRef)
|
|
1098
|
+
entry.descriptionRef = descriptionRef;
|
|
1099
|
+
else
|
|
1100
|
+
delete entry.descriptionRef;
|
|
1101
|
+
}
|
|
823
1102
|
if (updates.priority !== undefined)
|
|
824
1103
|
entry.priority = updates.priority;
|
|
825
1104
|
entry.updatedAt = nowISO();
|
|
826
1105
|
return entry;
|
|
827
1106
|
});
|
|
828
|
-
return writeAndSummarize(st, `✅ Phase updated: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}
|
|
1107
|
+
return writeAndSummarize(st, `✅ Phase updated: ${formatPhaseRef(found.number, featureNumberOfPhase(found, features))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.`, { phase, updated: true, updatedFields: receivedFields });
|
|
829
1108
|
});
|
|
830
1109
|
server.registerTool("planner-phase-delete", {
|
|
831
1110
|
description: "Delete a phase and unlink it from features.",
|
|
@@ -944,6 +1223,8 @@ server.registerTool("planner-task-show", {
|
|
|
944
1223
|
const sections = [summary];
|
|
945
1224
|
if (found.task.description?.trim())
|
|
946
1225
|
sections.push(found.task.description.trim());
|
|
1226
|
+
if (found.task.descriptionRef)
|
|
1227
|
+
sections.push(`Description reference:\n- ${found.task.descriptionRef}`);
|
|
947
1228
|
if (snapshot || pendingDeviation) {
|
|
948
1229
|
sections.push([
|
|
949
1230
|
pendingDeviation
|
|
@@ -957,20 +1238,40 @@ server.registerTool("planner-task-show", {
|
|
|
957
1238
|
}
|
|
958
1239
|
if (log)
|
|
959
1240
|
sections.push(`Status log:\n${log}`);
|
|
960
|
-
return text(sections.join("\n\n"));
|
|
1241
|
+
return text(sections.join("\n\n"), { task: { ref: taskCompositeRef(found.task, found.phase, features), description: found.task.description, descriptionRef: found.task.descriptionRef || "" } });
|
|
961
1242
|
});
|
|
962
1243
|
server.registerTool("planner-task-discuss", {
|
|
963
1244
|
description: "Persist task execution notes/checklist.",
|
|
964
1245
|
inputSchema: {
|
|
965
1246
|
task: z.string().min(1),
|
|
966
1247
|
description: z.string().optional(),
|
|
1248
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full task description."),
|
|
967
1249
|
checklist: z.array(z.string()).optional(),
|
|
968
1250
|
},
|
|
969
|
-
}, async ({ task: ref, description, checklist }) => {
|
|
1251
|
+
}, async ({ task: ref, description, descriptionRef: rawDescriptionRef, checklist }) => {
|
|
970
1252
|
const st = await requireStore();
|
|
971
1253
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
972
1254
|
if (!found)
|
|
973
1255
|
return text(`Task not found: ${ref}`);
|
|
1256
|
+
const mutableFields = ["description", "descriptionRef", "checklist"];
|
|
1257
|
+
const discussInputs = { description, descriptionRef: rawDescriptionRef, checklist };
|
|
1258
|
+
const receivedFields = mutableFields.filter((field) => discussInputs[field] !== undefined);
|
|
1259
|
+
if (receivedFields.length === 0) {
|
|
1260
|
+
return mutationNoFieldsFailure({
|
|
1261
|
+
entity: "task",
|
|
1262
|
+
ref: taskCompositeRef(found.task, found.phase, (await st.loadFeatures()).features),
|
|
1263
|
+
operation: "discuss",
|
|
1264
|
+
mutableFields,
|
|
1265
|
+
retryCommand: `planner-task-discuss ${taskCompositeRef(found.task, found.phase, (await st.loadFeatures()).features)}`,
|
|
1266
|
+
readBackCommand: `planner-task-show ${taskCompositeRef(found.task, found.phase, (await st.loadFeatures()).features)} with full=true`,
|
|
1267
|
+
descriptionFallback: {
|
|
1268
|
+
entityId: found.task.id,
|
|
1269
|
+
inputField: "description",
|
|
1270
|
+
storedField: "task description",
|
|
1271
|
+
descriptionRefField: "descriptionRef",
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
974
1275
|
let updatedTask;
|
|
975
1276
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
976
1277
|
const task = phase.tasks.find((entry) => entry.id === found.task.id);
|
|
@@ -978,8 +1279,13 @@ server.registerTool("planner-task-discuss", {
|
|
|
978
1279
|
return phase;
|
|
979
1280
|
if (description !== undefined)
|
|
980
1281
|
task.description = description.trim();
|
|
981
|
-
if (
|
|
982
|
-
|
|
1282
|
+
if (rawDescriptionRef !== undefined) {
|
|
1283
|
+
const descriptionRef = normalizeDescriptionRef(rawDescriptionRef);
|
|
1284
|
+
if (descriptionRef)
|
|
1285
|
+
task.descriptionRef = descriptionRef;
|
|
1286
|
+
else
|
|
1287
|
+
delete task.descriptionRef;
|
|
1288
|
+
}
|
|
983
1289
|
if (checklist !== undefined)
|
|
984
1290
|
task.checklist = checklist.map((item, index) => ({ id: createChecklistItemId(task.id, index + 1, item), number: index + 1, title: item, checked: false }));
|
|
985
1291
|
task.updatedAt = nowISO();
|
|
@@ -989,7 +1295,7 @@ server.registerTool("planner-task-discuss", {
|
|
|
989
1295
|
});
|
|
990
1296
|
const features = (await st.loadFeatures()).features;
|
|
991
1297
|
const t = updatedTask ?? found.task;
|
|
992
|
-
return writeAndSummarize(st, `✅ Task discussed/updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}
|
|
1298
|
+
return writeAndSummarize(st, `✅ Task discussed/updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.`, { task: t, discussed: true, updatedFields: receivedFields });
|
|
993
1299
|
});
|
|
994
1300
|
server.registerTool("planner-task-update", {
|
|
995
1301
|
description: "Update task fields. A motivation is REQUIRED when changing to blocked, canceled, deferred, rejected, waiting, or back to planned from another status.",
|
|
@@ -998,11 +1304,12 @@ server.registerTool("planner-task-update", {
|
|
|
998
1304
|
title: z.string().optional(),
|
|
999
1305
|
status: z.enum(STATUS_VALUES).optional(),
|
|
1000
1306
|
description: z.string().optional(),
|
|
1307
|
+
descriptionRef: z.string().optional().describe("Optional markdown reference under .planner/docs/ for the full task description."),
|
|
1001
1308
|
motivation: z.string().optional(),
|
|
1002
1309
|
priority: z.number().int().nonnegative().optional().describe("Display order within the phase (lower = higher)."),
|
|
1003
1310
|
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."),
|
|
1004
1311
|
},
|
|
1005
|
-
}, async ({ task: ref, title, status, description, motivation, priority, checklist }) => {
|
|
1312
|
+
}, async ({ task: ref, title, status, description, descriptionRef: rawDescriptionRef, motivation, priority, checklist }) => {
|
|
1006
1313
|
const st = await requireStore();
|
|
1007
1314
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
1008
1315
|
if (!found)
|
|
@@ -1020,6 +1327,25 @@ server.registerTool("planner-task-update", {
|
|
|
1020
1327
|
`Provide the \"motivation\" parameter with a detailed explanation of why this change is needed.`);
|
|
1021
1328
|
}
|
|
1022
1329
|
}
|
|
1330
|
+
const mutableFields = ["title", "status", "description", "descriptionRef", "motivation", "priority", "checklist"];
|
|
1331
|
+
const updateInputs = { title, status, description, descriptionRef: rawDescriptionRef, motivation, priority, checklist };
|
|
1332
|
+
const receivedFields = mutableFields.filter((field) => updateInputs[field] !== undefined);
|
|
1333
|
+
if (receivedFields.length === 0) {
|
|
1334
|
+
return mutationNoFieldsFailure({
|
|
1335
|
+
entity: "task",
|
|
1336
|
+
ref: taskCompositeRef(found.task, found.phase, (await st.loadFeatures()).features),
|
|
1337
|
+
operation: "update",
|
|
1338
|
+
mutableFields,
|
|
1339
|
+
retryCommand: `planner-task-update ${taskCompositeRef(found.task, found.phase, (await st.loadFeatures()).features)}`,
|
|
1340
|
+
readBackCommand: `planner-task-show ${taskCompositeRef(found.task, found.phase, (await st.loadFeatures()).features)} with full=true`,
|
|
1341
|
+
descriptionFallback: {
|
|
1342
|
+
entityId: found.task.id,
|
|
1343
|
+
inputField: "description",
|
|
1344
|
+
storedField: "task description",
|
|
1345
|
+
descriptionRefField: "descriptionRef",
|
|
1346
|
+
},
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1023
1349
|
let updatedTask;
|
|
1024
1350
|
const timestamp = nowISO();
|
|
1025
1351
|
await st.updatePhase(found.phase.id, (phase) => {
|
|
@@ -1032,6 +1358,13 @@ server.registerTool("planner-task-update", {
|
|
|
1032
1358
|
task.priority = priority;
|
|
1033
1359
|
if (description !== undefined)
|
|
1034
1360
|
task.description = description.trim();
|
|
1361
|
+
if (rawDescriptionRef !== undefined) {
|
|
1362
|
+
const descriptionRef = normalizeDescriptionRef(rawDescriptionRef);
|
|
1363
|
+
if (descriptionRef)
|
|
1364
|
+
task.descriptionRef = descriptionRef;
|
|
1365
|
+
else
|
|
1366
|
+
delete task.descriptionRef;
|
|
1367
|
+
}
|
|
1035
1368
|
if (status !== undefined && status !== task.status) {
|
|
1036
1369
|
// Record status change in the incremental statusLog.
|
|
1037
1370
|
const entry = {
|
|
@@ -1068,7 +1401,7 @@ server.registerTool("planner-task-update", {
|
|
|
1068
1401
|
}
|
|
1069
1402
|
}
|
|
1070
1403
|
}
|
|
1071
|
-
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}${resumeNotice}`, resumeRequired ? { resumeRequired } :
|
|
1404
|
+
return writeAndSummarize(st, `✅ Task updated: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (${t.status})${t.shortId ? ` · ${t.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.${resumeNotice}`, resumeRequired ? { task: t, updated: true, updatedFields: receivedFields, resumeRequired } : { task: t, updated: true, updatedFields: receivedFields });
|
|
1072
1405
|
});
|
|
1073
1406
|
server.registerTool("planner-task-checklist-toggle", {
|
|
1074
1407
|
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.",
|
|
@@ -1279,7 +1612,7 @@ server.registerTool("planner-task-pause", {
|
|
|
1279
1612
|
].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
|
|
1280
1613
|
});
|
|
1281
1614
|
server.registerTool("planner-task-switch", {
|
|
1282
|
-
description: "Atomically switch from active work to a temporary task, even outside normal priority order. Call this tool first so valid sessionInfo context can be reused; on denial, perform only the missing/stale reads listed in nextActions and retry. Pause the source with a mandatory snapshot, start the target, and push a durable LIFO return target.",
|
|
1615
|
+
description: "Atomically switch from active work to a temporary task, even outside normal priority order. Call this tool first so valid sessionInfo context can be reused; on denial, perform only the missing/stale reads listed in nextActions and retry. If nextActions includes planner-project-guidelines-show, read that section and keep it in working memory while doing task work. Pause the source with a mandatory snapshot, start the target, and push a durable LIFO return target.",
|
|
1283
1616
|
inputSchema: {
|
|
1284
1617
|
from_task: z.string().min(1),
|
|
1285
1618
|
to_task: z.string().min(1),
|
|
@@ -1324,11 +1657,17 @@ server.registerTool("planner-task-switch", {
|
|
|
1324
1657
|
};
|
|
1325
1658
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1326
1659
|
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
1660
|
+
const projectGuidelinesReadState = projectGuidelinesReadStateForSession(project, plannerSessionId);
|
|
1661
|
+
const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
|
|
1327
1662
|
const targetTaskRef = taskCompositeRef(target.task, target.phase, features);
|
|
1328
1663
|
const targetPhaseRef = formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features));
|
|
1329
1664
|
const targetFeatureRef = targetFeature ? formatFeatureRef(targetFeature.number) : "";
|
|
1665
|
+
if (!projectGuidelinesReady) {
|
|
1666
|
+
const nextActions = mcpContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, true, !requirementsReady, `Retry planner-task-switch to ${targetTaskRef}`);
|
|
1667
|
+
return text(`Task switch denied: read Project Guidelines before switching task work. Current state: ${projectGuidelinesReadState}.`, { projectGuidelinesReadState, contextEligibility, nextActions });
|
|
1668
|
+
}
|
|
1330
1669
|
if (!contextEligibility.eligible) {
|
|
1331
|
-
const nextActions = mcpContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, !requirementsReady, `Retry planner-task-switch to ${targetTaskRef}`);
|
|
1670
|
+
const nextActions = mcpContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, false, !requirementsReady, `Retry planner-task-switch to ${targetTaskRef}`);
|
|
1332
1671
|
return text(`Task switch denied: context reads required. ${contextEligibility.reason}`, { ...contextEligibility, nextActions });
|
|
1333
1672
|
}
|
|
1334
1673
|
if (!requirementsReady) {
|
|
@@ -1434,7 +1773,7 @@ server.registerTool("planner-task-switch", {
|
|
|
1434
1773
|
].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
|
|
1435
1774
|
});
|
|
1436
1775
|
server.registerTool("planner-task-start", {
|
|
1437
|
-
description: "Set a task to in-progress or resume checkpointed work. Call this tool before reading context so it can reuse valid sessionInfo attestations; if denied, perform only the missing/stale reads listed in nextActions, in order, and
|
|
1776
|
+
description: "Set a task to in-progress or resume checkpointed work. Call this tool before reading context so it can reuse valid sessionInfo attestations; if denied, perform only the missing/stale reads listed in nextActions, then retry. Reads may be performed in any order within the current session. If nextActions includes planner-project-guidelines-show, read that section and keep it in working memory while doing task work. Every denied start is an MCP isError result with started=false; NEVER claim work started unless structuredContent.started is true. Success is returned only after persisted in-progress state is verified.",
|
|
1438
1777
|
inputSchema: { task: z.string().min(1) },
|
|
1439
1778
|
}, async ({ task: ref }, extra) => {
|
|
1440
1779
|
const plannerSessionId = plannerSessionIdFor(extra);
|
|
@@ -1454,7 +1793,8 @@ server.registerTool("planner-task-start", {
|
|
|
1454
1793
|
const parentFeature = found.phase.featureId ? features.find((candidate) => candidate.id === found.phase.featureId) : undefined;
|
|
1455
1794
|
const featureRef = parentFeature ? formatFeatureRef(parentFeature.number) : "";
|
|
1456
1795
|
if (found.task.status === "done")
|
|
1457
|
-
return taskStartError(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use planner-task-update with status=planned and a motivation to reopen ${taskRef}.`, `
|
|
1796
|
+
return taskStartError(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use planner-task-update with status=planned and a motivation to reopen ${taskRef}.`, `Retry planner-task-start ${taskRef}. If it then reports read prerequisites, perform only the missing or stale nextActions before retrying again.`], { taskId: found.task.id }));
|
|
1797
|
+
const project = await st.loadProject();
|
|
1458
1798
|
const linkedRequirements = [
|
|
1459
1799
|
...(await st.linkedRequirementsForPhase(found.phase.id)),
|
|
1460
1800
|
...(found.phase.featureId ? await st.linkedRequirementsForFeature(found.phase.featureId) : []),
|
|
@@ -1473,8 +1813,12 @@ server.registerTool("planner-task-start", {
|
|
|
1473
1813
|
};
|
|
1474
1814
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1475
1815
|
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
1816
|
+
const projectGuidelinesReadState = projectGuidelinesReadStateForSession(project, plannerSessionId);
|
|
1817
|
+
const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
|
|
1818
|
+
if (!projectGuidelinesReady)
|
|
1819
|
+
return taskStartError(taskStartDenied("PROJECT_GUIDELINES_READ_REQUIRED", `Project Guidelines must be read before task work. Current state: ${projectGuidelinesReadState}.`, mcpContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, true, !requirementsReady, `Retry planner-task-start ${taskRef}`), { taskId: found.task.id }), { projectGuidelinesReadState, contextEligibility });
|
|
1476
1820
|
if (!contextEligibility.eligible)
|
|
1477
|
-
return taskStartError(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, mcpContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, !requirementsReady, `Retry planner-task-start ${taskRef}`), { taskId: found.task.id }), { contextEligibility });
|
|
1821
|
+
return taskStartError(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, mcpContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, false, !requirementsReady, `Retry planner-task-start ${taskRef}`), { taskId: found.task.id }), { contextEligibility });
|
|
1478
1822
|
if (!requirementsReady) {
|
|
1479
1823
|
return taskStartError(taskStartDenied("REQUIREMENTS_READ_REQUIRED", "Linked requirements were not read; the task remains unchanged.", ["planner-requirement-list", `Retry planner-task-start ${taskRef}`], { taskId: found.task.id, requirementIds: linkedRequirementIds }));
|
|
1480
1824
|
}
|
|
@@ -1485,7 +1829,7 @@ server.registerTool("planner-task-start", {
|
|
|
1485
1829
|
const outcome = taskStartSucceeded(found.task.id, true);
|
|
1486
1830
|
return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true`, { ...outcome, task: found.task });
|
|
1487
1831
|
}
|
|
1488
|
-
const [
|
|
1832
|
+
const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume()]);
|
|
1489
1833
|
const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
|
|
1490
1834
|
if (!eligibility.eligible)
|
|
1491
1835
|
return taskStartError(taskStartDenied("START_NOT_ALLOWED", eligibility.reason, ["Resolve the reported task readiness condition, then retry planner-task-start."], { taskId: found.task.id }), { eligibility });
|
|
@@ -1658,14 +2002,20 @@ server.registerTool("planner-task-complete", {
|
|
|
1658
2002
|
return writeAndSummarize(st, `✅ Task completed: ${taskCompositeRef(t, found.phase, features)} — ${t.title} (done)${t.shortId ? ` · ${t.shortId}` : ""}${clearedRef ? ` — phase handoff auto-cleared (${clearedRef})` : ""}${resumeNotice}`, resumeRequired ? { resumeRequired } : undefined);
|
|
1659
2003
|
});
|
|
1660
2004
|
server.registerTool("planner-handoff-list", {
|
|
1661
|
-
description: "List
|
|
1662
|
-
|
|
2005
|
+
description: "List active handoffs as compact summary-only pages. Full bodies are never embedded; call planner-handoff-show for one exact phase.",
|
|
2006
|
+
inputSchema: {
|
|
2007
|
+
page: z.number().int().min(1).default(1).describe("1-based page number."),
|
|
2008
|
+
pageSize: z.number().int().min(1).max(25).default(10).describe("Summaries per page; maximum 25."),
|
|
2009
|
+
},
|
|
2010
|
+
}, async ({ page, pageSize }) => {
|
|
1663
2011
|
const st = await requireStore();
|
|
1664
2012
|
const list = await st.listHandoffs();
|
|
1665
2013
|
if (list.length === 0)
|
|
1666
|
-
return text("No phase handoffs set.");
|
|
1667
|
-
const
|
|
1668
|
-
|
|
2014
|
+
return text("No phase handoffs set.", { count: 0, total: 0, page: 1, pageSize, totalPages: 1, handoffs: [] });
|
|
2015
|
+
const result = boundedPage(list, page, pageSize);
|
|
2016
|
+
const { items, ...pageInfo } = result;
|
|
2017
|
+
const lines = items.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt}; ${e.contentLength} chars)`);
|
|
2018
|
+
return text(`Phase handoffs — page ${pageInfo.page}/${pageInfo.totalPages} (${pageInfo.total} total):\n${lines.join("\n")}`, { count: items.length, ...pageInfo, handoffs: items });
|
|
1669
2019
|
});
|
|
1670
2020
|
server.registerTool("planner-handoff-show", {
|
|
1671
2021
|
description: "Read the entity-scoped handoff of a phase. phaseRef is required; never infer a phase from in-progress status or stale context.",
|
|
@@ -1675,10 +2025,11 @@ server.registerTool("planner-handoff-show", {
|
|
|
1675
2025
|
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
1676
2026
|
if (!r.ok)
|
|
1677
2027
|
return text(`❌ ${r.error}`);
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2028
|
+
const phase = await st.loadPhase(r.phase.id);
|
|
2029
|
+
const bounded = boundedHandoffForTransport(phase.handoff);
|
|
2030
|
+
if (!bounded.content.trim())
|
|
2031
|
+
return text(`No handoff set on ${r.compositeRef}.`, { phaseRef: r.compositeRef, phaseId: r.phase.id, content: "", empty: true, handoffAudit: phase.handoffAudit });
|
|
2032
|
+
return text(`Handoff for ${r.compositeRef}:\n\n${bounded.content}`, { phaseRef: r.compositeRef, phaseId: r.phase.id, ...bounded, handoffAudit: phase.handoffAudit });
|
|
1682
2033
|
});
|
|
1683
2034
|
server.registerTool("planner-requirement-list", {
|
|
1684
2035
|
description: "List all top-level requirements in requirements.json. Reading this list records the requirements as read for the start/resume read-enforcement (explicit requirement read, required alongside feature/phase reads).",
|
|
@@ -1690,8 +2041,228 @@ server.registerTool("planner-requirement-list", {
|
|
|
1690
2041
|
requirements.requirements.forEach((req) => markRequirementReadForSessionId(plannerSessionId, req.id));
|
|
1691
2042
|
return text(requirements.requirements.map((req) => `- ${req.id} — ${req.title} (${req.status})`).join("\n") || "No requirements", { requirements });
|
|
1692
2043
|
});
|
|
2044
|
+
server.registerTool("planner-requirement-create", {
|
|
2045
|
+
description: "Create a top-level requirement with optional nested macro tasks. Requirement and macro-task IDs/timestamps are assigned by the planner.",
|
|
2046
|
+
inputSchema: {
|
|
2047
|
+
title: z.string().min(1),
|
|
2048
|
+
description: z.string().optional(),
|
|
2049
|
+
status: z.enum(STATUS_VALUES).optional(),
|
|
2050
|
+
linkedPhaseIds: z.array(z.string().min(1)),
|
|
2051
|
+
macroTasks: z.array(z.object({
|
|
2052
|
+
title: z.string().min(1),
|
|
2053
|
+
description: z.string().optional(),
|
|
2054
|
+
status: z.enum(STATUS_VALUES).optional(),
|
|
2055
|
+
})).optional(),
|
|
2056
|
+
},
|
|
2057
|
+
}, async ({ title, description, status, linkedPhaseIds, macroTasks }) => {
|
|
2058
|
+
const st = await requireStore();
|
|
2059
|
+
const links = await resolveRequirementPhaseRefs(st, linkedPhaseIds);
|
|
2060
|
+
if (!links.ok)
|
|
2061
|
+
return text(`❌ ${links.error}`, { created: false, errorCode: "REQUIREMENT_PHASE_LINK_INVALID" });
|
|
2062
|
+
const now = nowISO();
|
|
2063
|
+
let normalizedMacroTasks;
|
|
2064
|
+
try {
|
|
2065
|
+
normalizedMacroTasks = reconcileRequirementMacroTasks([], (macroTasks ?? []).map((task) => ({
|
|
2066
|
+
title: task.title,
|
|
2067
|
+
...(task.description !== undefined ? { description: task.description } : {}),
|
|
2068
|
+
status: task.status ?? "planned",
|
|
2069
|
+
})), now);
|
|
2070
|
+
}
|
|
2071
|
+
catch (error) {
|
|
2072
|
+
const message = error instanceof RequirementMacroTaskError ? error.message : "Invalid macro tasks.";
|
|
2073
|
+
return text(`❌ ${message}`, { created: false, errorCode: "REQUIREMENT_MACRO_TASK_INVALID" });
|
|
2074
|
+
}
|
|
2075
|
+
const requirement = {
|
|
2076
|
+
id: createRequirementId(),
|
|
2077
|
+
title: title.trim(),
|
|
2078
|
+
description: description?.trim() ?? "",
|
|
2079
|
+
status: status ?? "planned",
|
|
2080
|
+
macroTasks: normalizedMacroTasks,
|
|
2081
|
+
linkedPhaseIds: links.linkedPhaseIds,
|
|
2082
|
+
createdAt: now,
|
|
2083
|
+
updatedAt: now,
|
|
2084
|
+
sessionInfo: [],
|
|
2085
|
+
};
|
|
2086
|
+
const requirements = await st.loadRequirements();
|
|
2087
|
+
await st.saveRequirements({ requirements: [...requirements.requirements, requirement] });
|
|
2088
|
+
await st.writeGenerated();
|
|
2089
|
+
return writeAndSummarize(st, `Requirement created: ${requirement.id}.`, { requirement, created: true });
|
|
2090
|
+
});
|
|
2091
|
+
server.registerTool("planner-requirement-update", {
|
|
2092
|
+
description: "Update a requirement’s human-owned title, description, status, linked phases, or complete ordered macro-task list. Existing macro-task IDs and timestamps remain planner-managed; omit an ID to add a task.",
|
|
2093
|
+
inputSchema: {
|
|
2094
|
+
requirementId: z.string().min(1),
|
|
2095
|
+
title: z.string().min(1).optional(),
|
|
2096
|
+
description: z.string().optional(),
|
|
2097
|
+
status: z.enum(STATUS_VALUES).optional(),
|
|
2098
|
+
linkedPhaseIds: z.array(z.string().min(1)).optional(),
|
|
2099
|
+
macroTasks: z.array(z.object({
|
|
2100
|
+
id: z.string().min(1).optional(),
|
|
2101
|
+
title: z.string().min(1),
|
|
2102
|
+
description: z.string().optional(),
|
|
2103
|
+
status: z.enum(STATUS_VALUES),
|
|
2104
|
+
})).optional(),
|
|
2105
|
+
},
|
|
2106
|
+
}, async ({ requirementId, title, description, status, linkedPhaseIds, macroTasks }) => {
|
|
2107
|
+
const st = await requireStore();
|
|
2108
|
+
const requirements = await st.loadRequirements();
|
|
2109
|
+
const current = requirements.requirements.find((requirement) => requirement.id === requirementId);
|
|
2110
|
+
if (!current)
|
|
2111
|
+
return text(`❌ Requirement not found: ${requirementId}`, { updated: false, errorCode: "NOT_FOUND" });
|
|
2112
|
+
const links = linkedPhaseIds === undefined ? { ok: true, linkedPhaseIds: current.linkedPhaseIds } : await resolveRequirementPhaseRefs(st, linkedPhaseIds);
|
|
2113
|
+
if (!links.ok)
|
|
2114
|
+
return text(`❌ ${links.error}`, { updated: false, errorCode: "REQUIREMENT_PHASE_LINK_INVALID" });
|
|
2115
|
+
const now = nowISO();
|
|
2116
|
+
let normalizedMacroTasks = current.macroTasks;
|
|
2117
|
+
if (macroTasks !== undefined) {
|
|
2118
|
+
try {
|
|
2119
|
+
normalizedMacroTasks = reconcileRequirementMacroTasks(current.macroTasks, macroTasks.map((task) => ({
|
|
2120
|
+
...(task.id !== undefined ? { id: task.id } : {}),
|
|
2121
|
+
title: task.title,
|
|
2122
|
+
...(task.description !== undefined ? { description: task.description } : {}),
|
|
2123
|
+
status: task.status,
|
|
2124
|
+
})), now);
|
|
2125
|
+
}
|
|
2126
|
+
catch (error) {
|
|
2127
|
+
const message = error instanceof RequirementMacroTaskError ? error.message : "Invalid macro tasks.";
|
|
2128
|
+
return text(`❌ ${message}`, { updated: false, errorCode: "REQUIREMENT_MACRO_TASK_INVALID" });
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const next = {
|
|
2132
|
+
...current,
|
|
2133
|
+
...(title !== undefined ? { title: title.trim() } : {}),
|
|
2134
|
+
...(description !== undefined ? { description: description.trim() } : {}),
|
|
2135
|
+
...(status !== undefined ? { status } : {}),
|
|
2136
|
+
linkedPhaseIds: links.linkedPhaseIds,
|
|
2137
|
+
macroTasks: normalizedMacroTasks,
|
|
2138
|
+
updatedAt: now,
|
|
2139
|
+
};
|
|
2140
|
+
await st.saveRequirements({ requirements: requirements.requirements.map((requirement) => requirement.id === current.id ? next : requirement) });
|
|
2141
|
+
await st.writeGenerated();
|
|
2142
|
+
return writeAndSummarize(st, `Requirement updated: ${next.id}.`, { requirement: next, updated: true });
|
|
2143
|
+
});
|
|
2144
|
+
server.registerTool("planner-requirement-delete", {
|
|
2145
|
+
description: "Permanently delete a top-level requirement. Confirm the intended requirement first.",
|
|
2146
|
+
inputSchema: { requirementId: z.string().min(1) },
|
|
2147
|
+
}, async ({ requirementId }) => {
|
|
2148
|
+
const st = await requireStore();
|
|
2149
|
+
const requirements = await st.loadRequirements();
|
|
2150
|
+
const next = requirements.requirements.filter((requirement) => requirement.id !== requirementId);
|
|
2151
|
+
if (next.length === requirements.requirements.length)
|
|
2152
|
+
return text(`❌ Requirement not found: ${requirementId}`, { deleted: false, errorCode: "NOT_FOUND" });
|
|
2153
|
+
await st.saveRequirements({ requirements: next });
|
|
2154
|
+
await st.writeGenerated();
|
|
2155
|
+
return writeAndSummarize(st, `Requirement deleted: ${requirementId}.`, { deleted: requirementId });
|
|
2156
|
+
});
|
|
2157
|
+
function ideaPromotionTarget(st, type, ref) {
|
|
2158
|
+
return Promise.all([st.loadFeatures(), st.loadAllPhases()]).then(([featuresDoc, phases]) => {
|
|
2159
|
+
const features = featuresDoc.features;
|
|
2160
|
+
if (type === "feature") {
|
|
2161
|
+
const feature = findFeatureByRef(features, ref);
|
|
2162
|
+
return feature ? { id: feature.id, ref: formatFeatureRef(feature.number) } : undefined;
|
|
2163
|
+
}
|
|
2164
|
+
if (type === "phase") {
|
|
2165
|
+
const phase = findPhaseByRef(phases, features, ref);
|
|
2166
|
+
return phase ? { id: phase.id, ref: formatPhaseRef(phase.number, features.find((item) => item.id === phase.featureId)?.number) } : undefined;
|
|
2167
|
+
}
|
|
2168
|
+
const found = findTaskByRef(phases, features, ref);
|
|
2169
|
+
return found ? { id: found.task.id, ref: `${formatPhaseRef(found.phase.number, features.find((item) => item.id === found.phase.featureId)?.number)}/T${String(found.task.number).padStart(3, "0")}` } : undefined;
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
server.registerTool("planner-idea-list", {
|
|
2173
|
+
description: "List top-level Ideas Inbox entries. Ideas never affect feature, phase, or task rollups.",
|
|
2174
|
+
inputSchema: {},
|
|
2175
|
+
}, async () => {
|
|
2176
|
+
const ideas = (await requireStore().then((st) => st.loadIdeas())).ideas;
|
|
2177
|
+
return text(ideas.map((idea) => `- ${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}${idea.promotion ? ` → ${idea.promotion.targetRef}` : ""}`).join("\n") || "No ideas", { ideas });
|
|
2178
|
+
});
|
|
2179
|
+
server.registerTool("planner-idea-show", {
|
|
2180
|
+
description: "Show one Idea Inbox entry by I-number, short ID, UUID, or title.",
|
|
2181
|
+
inputSchema: { idea: z.string().min(1) },
|
|
2182
|
+
}, async ({ idea: ref }) => {
|
|
2183
|
+
const st = await requireStore();
|
|
2184
|
+
const idea = findIdeaByRef((await st.loadIdeas()).ideas, ref);
|
|
2185
|
+
return idea ? text(`${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}\n\n${idea.description || "(no description)"}`, { idea }) : text(`Idea not found: ${ref}`, { errorCode: "NOT_FOUND" });
|
|
2186
|
+
});
|
|
2187
|
+
server.registerTool("planner-idea-create", {
|
|
2188
|
+
description: "Create a rollup-independent Ideas Inbox entry.",
|
|
2189
|
+
inputSchema: { title: z.string().min(1), description: z.string().optional() },
|
|
2190
|
+
}, async ({ title, description }) => {
|
|
2191
|
+
const st = await requireStore();
|
|
2192
|
+
const idea = await st.createIdea({ title, ...(description !== undefined ? { description } : {}) });
|
|
2193
|
+
await st.writeGenerated();
|
|
2194
|
+
return text(`Idea created: ${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}`, { idea, created: true });
|
|
2195
|
+
});
|
|
2196
|
+
server.registerTool("planner-idea-update", {
|
|
2197
|
+
description: "Update an unpromoted or promoted Idea Inbox entry title or description.",
|
|
2198
|
+
inputSchema: { idea: z.string().min(1), title: z.string().min(1).optional(), description: z.string().optional() },
|
|
2199
|
+
}, async ({ idea: ref, title, description }) => {
|
|
2200
|
+
const st = await requireStore();
|
|
2201
|
+
const current = findIdeaByRef((await st.loadIdeas()).ideas, ref);
|
|
2202
|
+
if (!current)
|
|
2203
|
+
return text(`Idea not found: ${ref}`, { errorCode: "NOT_FOUND", updated: false });
|
|
2204
|
+
if (title === undefined && description === undefined)
|
|
2205
|
+
return text("No mutable idea fields were received.", { errorCode: "NO_MUTABLE_FIELDS_RECEIVED", updated: false });
|
|
2206
|
+
const idea = await st.updateIdea(current.id, { ...(title !== undefined ? { title } : {}), ...(description !== undefined ? { description } : {}) });
|
|
2207
|
+
await st.writeGenerated();
|
|
2208
|
+
return text(`Idea updated: ${formatIdeaRef(idea.number)} — ${idea.title}`, { idea, updated: true });
|
|
2209
|
+
});
|
|
2210
|
+
server.registerTool("planner-idea-delete", {
|
|
2211
|
+
description: "Delete an Idea Inbox entry. Confirm the exact idea with the user before calling.",
|
|
2212
|
+
inputSchema: { idea: z.string().min(1), confirmed: z.boolean() },
|
|
2213
|
+
}, async ({ idea: ref, confirmed }) => {
|
|
2214
|
+
if (!confirmed)
|
|
2215
|
+
return text("Idea deletion requires confirmed=true after explicit user confirmation.", { errorCode: "CONFIRMATION_REQUIRED", deleted: false });
|
|
2216
|
+
const st = await requireStore();
|
|
2217
|
+
const current = findIdeaByRef((await st.loadIdeas()).ideas, ref);
|
|
2218
|
+
if (!current)
|
|
2219
|
+
return text(`Idea not found: ${ref}`, { errorCode: "NOT_FOUND", deleted: false });
|
|
2220
|
+
await st.deleteIdea(current.id);
|
|
2221
|
+
await st.writeGenerated();
|
|
2222
|
+
return text(`Idea deleted: ${formatIdeaRef(current.number)}.`, { deleted: true, ideaRef: formatIdeaRef(current.number) });
|
|
2223
|
+
});
|
|
2224
|
+
server.registerTool("planner-idea-promotion-begin", {
|
|
2225
|
+
description: "Begin an Idea promotion discussion without persisting a target or promotion. Returns the project-local grill-me instructions; follow them one question at a time, then use planner-idea-promotion-finalize only after target confirmation.",
|
|
2226
|
+
inputSchema: { idea: z.string().min(1), targetType: z.enum(["feature", "phase", "task"]) },
|
|
2227
|
+
}, async ({ idea: ref, targetType }) => {
|
|
2228
|
+
const st = await requireStore();
|
|
2229
|
+
const idea = findIdeaByRef((await st.loadIdeas()).ideas, ref);
|
|
2230
|
+
if (!idea)
|
|
2231
|
+
return text(`Idea not found: ${ref}`, { errorCode: "NOT_FOUND" });
|
|
2232
|
+
if (idea.promotion)
|
|
2233
|
+
return text(`${formatIdeaRef(idea.number)} is already promoted to ${idea.promotion.targetRef}.`, { errorCode: "ALREADY_PROMOTED", idea });
|
|
2234
|
+
const features = (await st.loadFeatures()).features;
|
|
2235
|
+
const phases = await st.loadAllPhases();
|
|
2236
|
+
const recommendedFeature = features[0];
|
|
2237
|
+
const recommendedPhase = recommendedFeature ? phases.find((phase) => phase.featureId === recommendedFeature.id) : phases[0];
|
|
2238
|
+
const recommendation = targetType === "feature"
|
|
2239
|
+
? "Create a new feature after the discussion."
|
|
2240
|
+
: targetType === "phase"
|
|
2241
|
+
? recommendedFeature ? `Recommended parent feature: ${formatFeatureRef(recommendedFeature.number)} — ${recommendedFeature.name}. Confirm or choose another feature before creating the phase.` : "Create and confirm a parent feature before promoting to a phase."
|
|
2242
|
+
: recommendedPhase
|
|
2243
|
+
? `Recommended destination: ${formatPhaseRef(recommendedPhase.number, features.find((feature) => feature.id === recommendedPhase.featureId)?.number)} — ${recommendedPhase.title}. Confirm its feature and phase before creating the task.`
|
|
2244
|
+
: "Create and confirm a parent feature and phase before promoting to a task.";
|
|
2245
|
+
return text(`Promotion discussion started for ${formatIdeaRef(idea.number)}. ${recommendation}`, { idea, targetType, recommendation, grillMeSkill: await st.ideaDiscussionSkill(), persisted: false, nextActions: ["Follow grill-me one question at a time.", "Obtain explicit parent and target confirmation.", "Create the agreed target through the normal feature/phase/task creation tool.", "Call planner-idea-promotion-finalize with discussionCompleted=true and confirmed=true."] });
|
|
2246
|
+
});
|
|
2247
|
+
server.registerTool("planner-idea-promotion-finalize", {
|
|
2248
|
+
description: "Record a promotion only after grill-me discussion, explicit target confirmation, and successful target creation. targetRef must be the already-created agreed target; no raw UUID is returned.",
|
|
2249
|
+
inputSchema: { idea: z.string().min(1), targetType: z.enum(["feature", "phase", "task"]), targetRef: z.string().min(1), discussionCompleted: z.boolean(), confirmed: z.boolean() },
|
|
2250
|
+
}, async ({ idea: ref, targetType, targetRef, discussionCompleted, confirmed }) => {
|
|
2251
|
+
if (!discussionCompleted || !confirmed)
|
|
2252
|
+
return text("Promotion requires discussionCompleted=true and confirmed=true; no target or promotion was persisted.", { errorCode: "IDEA_PROMOTION_CONFIRMATION_REQUIRED", promoted: false });
|
|
2253
|
+
const st = await requireStore();
|
|
2254
|
+
const idea = findIdeaByRef((await st.loadIdeas()).ideas, ref);
|
|
2255
|
+
if (!idea)
|
|
2256
|
+
return text(`Idea not found: ${ref}`, { errorCode: "NOT_FOUND", promoted: false });
|
|
2257
|
+
const target = await ideaPromotionTarget(st, targetType, targetRef);
|
|
2258
|
+
if (!target)
|
|
2259
|
+
return text(`Confirmed ${targetType} target not found: ${targetRef}; no promotion was persisted.`, { errorCode: "TARGET_NOT_FOUND", promoted: false });
|
|
2260
|
+
const promoted = await st.promoteIdea(idea.id, { targetType, targetId: target.id });
|
|
2261
|
+
await st.writeGenerated();
|
|
2262
|
+
return text(`Idea promoted: ${formatIdeaRef(promoted.number)} → ${promoted.promotion.targetRef}`, { idea: promoted, promoted: true, targetRef: promoted.promotion.targetRef });
|
|
2263
|
+
});
|
|
1693
2264
|
server.registerTool("planner-handoff-write", {
|
|
1694
|
-
description: "Reconcile
|
|
2265
|
+
description: "Reconcile one active handoff with durable context synchronization and a mandatory versioned completeness audit. Run planner-handoff-prepare first; link extended detail from committed .planner/docs/ Markdown.",
|
|
1695
2266
|
inputSchema: {
|
|
1696
2267
|
phaseRef: z.string().min(1).describe("Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title."),
|
|
1697
2268
|
title: z.string().min(3).optional().describe("Meaningful handoff title summarizing the work."),
|
|
@@ -1699,6 +2270,18 @@ server.registerTool("planner-handoff-write", {
|
|
|
1699
2270
|
content: z.string().min(1).describe("Full reconciled handoff text (markdown)."),
|
|
1700
2271
|
expectedHandoffUpdatedAt: z.string().optional().describe("Exact token returned by planner-handoff-prepare; empty when no handoff exists."),
|
|
1701
2272
|
reconciledExistingHandoff: z.boolean().optional().describe("Confirm that still-relevant existing handoff information was retained."),
|
|
2273
|
+
completenessAudit: z.object({
|
|
2274
|
+
version: z.literal(HANDOFF_COMPLETENESS_AUDIT_VERSION),
|
|
2275
|
+
entries: z.array(z.object({
|
|
2276
|
+
category: z.string().min(1).describe(`One required category id: ${HANDOFF_COMPLETENESS_CATEGORIES.map((entry) => entry.id).join(", ")}`),
|
|
2277
|
+
status: z.enum(["captured", "not-applicable"]),
|
|
2278
|
+
detail: z.string().min(1),
|
|
2279
|
+
})),
|
|
2280
|
+
}).optional().describe("Mandatory for confirmed writes; every category from planner-handoff-prepare must be present."),
|
|
2281
|
+
supportingDocuments: z.array(z.object({
|
|
2282
|
+
path: z.string().min(1).describe("Committed Markdown path under .planner/docs/."),
|
|
2283
|
+
description: z.string().min(1).describe("What the document contains and why the next agent needs it."),
|
|
2284
|
+
})).optional(),
|
|
1702
2285
|
taskUpdates: z.array(z.object({
|
|
1703
2286
|
taskRef: z.string().min(1),
|
|
1704
2287
|
completionSummary: z.string().min(1),
|
|
@@ -1716,7 +2299,7 @@ server.registerTool("planner-handoff-write", {
|
|
|
1716
2299
|
featureUpdate: z.object({ workDone: z.string().min(1), workRemaining: z.string().min(1) }).optional(),
|
|
1717
2300
|
featureNoUpdateReason: z.string().min(1).optional(),
|
|
1718
2301
|
},
|
|
1719
|
-
}, async ({ phaseRef, title, confirmed, content, expectedHandoffUpdatedAt, reconciledExistingHandoff, taskUpdates, phaseUpdate, phaseNoUpdateReason, featureUpdate, featureNoUpdateReason }) => {
|
|
2302
|
+
}, async ({ phaseRef, title, confirmed, content, expectedHandoffUpdatedAt, reconciledExistingHandoff, completenessAudit, supportingDocuments, taskUpdates, phaseUpdate, phaseNoUpdateReason, featureUpdate, featureNoUpdateReason }) => {
|
|
1720
2303
|
const st = await requireStore();
|
|
1721
2304
|
let body = content.trim();
|
|
1722
2305
|
const firstLine = body.split(/\r?\n/).find((line) => line.trim().length > 0) ?? "";
|
|
@@ -1762,6 +2345,8 @@ server.registerTool("planner-handoff-write", {
|
|
|
1762
2345
|
content: body,
|
|
1763
2346
|
expectedHandoffUpdatedAt,
|
|
1764
2347
|
reconciledExistingHandoff: reconciledExistingHandoff === true,
|
|
2348
|
+
...(completenessAudit ? { completenessAudit: completenessAudit } : {}),
|
|
2349
|
+
...(supportingDocuments ? { supportingDocuments } : {}),
|
|
1765
2350
|
contextSync: {
|
|
1766
2351
|
taskUpdates: resolvedTaskUpdates,
|
|
1767
2352
|
...(phaseUpdate ? { phaseUpdate: {
|
|
@@ -1775,15 +2360,23 @@ server.registerTool("planner-handoff-write", {
|
|
|
1775
2360
|
},
|
|
1776
2361
|
});
|
|
1777
2362
|
await st.writeGenerated();
|
|
1778
|
-
return text(`✅ Reconciled handoff and durable context on ${r.compositeRef}; updated ${result.updatedTaskIds.length} task(s).`, {
|
|
2363
|
+
return text(`✅ Reconciled handoff and durable context on ${r.compositeRef}; updated ${result.updatedTaskIds.length} task(s).`, {
|
|
2364
|
+
phaseRef: r.compositeRef,
|
|
2365
|
+
phaseId: r.phase.id,
|
|
2366
|
+
updatedTaskIds: result.updatedTaskIds,
|
|
2367
|
+
handoffUpdatedAt: result.handoffUpdatedAt,
|
|
2368
|
+
handoffAudit: result.handoffAudit,
|
|
2369
|
+
});
|
|
1779
2370
|
}
|
|
1780
2371
|
catch (error) {
|
|
2372
|
+
if (error instanceof HandoffContractError)
|
|
2373
|
+
return handoffContractFailure(error);
|
|
1781
2374
|
const message = error instanceof Error ? error.message : String(error);
|
|
1782
2375
|
return text(`❌ Handoff refresh denied: ${message}`, { error: message });
|
|
1783
2376
|
}
|
|
1784
2377
|
});
|
|
1785
2378
|
server.registerTool("planner-handoff-prepare", {
|
|
1786
|
-
description: "Identify or audit the exact handoff target.
|
|
2379
|
+
description: "Identify or audit the exact handoff target. With a confirmed phaseRef, returns the current token, missing task evidence, mandatory completeness categories, and the 24,000-character inline budget.",
|
|
1787
2380
|
inputSchema: { phaseRef: z.string().min(1).optional() },
|
|
1788
2381
|
}, async ({ phaseRef }) => {
|
|
1789
2382
|
if (!phaseRef)
|
|
@@ -1805,6 +2398,10 @@ server.registerTool("planner-handoff-prepare", {
|
|
|
1805
2398
|
"Done tasks missing durable completion/verification evidence:",
|
|
1806
2399
|
missing,
|
|
1807
2400
|
"",
|
|
2401
|
+
`Required completeness audit v${audit.completenessVersion} — every category must be captured or marked not-applicable with a substantive reason:`,
|
|
2402
|
+
...audit.completenessCategories.map((entry) => `- ${entry.id} — ${entry.label}`),
|
|
2403
|
+
`Maximum canonical inline content: ${audit.maxContentChars} characters. Link extended material from .planner/docs/.`,
|
|
2404
|
+
"",
|
|
1808
2405
|
"Existing active handoff (reconcile all still-relevant content):",
|
|
1809
2406
|
audit.handoff.trim() || "(none)",
|
|
1810
2407
|
].join("\n"), { phaseRef: r.compositeRef, ...audit });
|
|
@@ -1867,13 +2464,41 @@ server.registerTool("planner-web", {
|
|
|
1867
2464
|
});
|
|
1868
2465
|
server.registerTool("planner-load", {
|
|
1869
2466
|
description: "Load/refresh the planner on explicit user request (NOT automatic): starts the web dashboard on LAN and returns a consolidated recap (project state, active task, pending handoff, web URL). This is the MCP equivalent of Pi /planner load. Call it ONLY when the user runs /planner load or /planner recap (or asks to load the planner). Present the recap verbatim in that reply, including its final prominent Web UI line. A pending handoff is read-only context: NEVER call planner-handoff-show or planner-handoff-clear as part of load/recap. Archive it only when every phase task is done/canceled, when replacing it with a new handoff, or after an explicit user handoff-clear request. Do NOT start the planner/web or show the web URL unless the user explicitly asks (load/recap/web status).",
|
|
1870
|
-
}, async () => {
|
|
2467
|
+
}, async (extra) => {
|
|
1871
2468
|
const st = store();
|
|
1872
2469
|
if (!(await st.exists()))
|
|
1873
2470
|
return text("No .planner/ found at " + planRoot() + ". Run planner-init first.");
|
|
2471
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
2472
|
+
let preparation;
|
|
2473
|
+
try {
|
|
2474
|
+
preparation = await st.preparePlannerSession();
|
|
2475
|
+
}
|
|
2476
|
+
catch (error) {
|
|
2477
|
+
return text(`Planner load aborted: ${error instanceof Error ? error.message : String(error)}`, { errorCode: "PLANNER_SESSION_PREPARATION_FAILED", loaded: false });
|
|
2478
|
+
}
|
|
2479
|
+
const project = preparation.project;
|
|
2480
|
+
if (project.projectGuidelines.content.trim()) {
|
|
2481
|
+
await st.recordProjectGuidelinesRead({ sessionId: plannerSessionId });
|
|
2482
|
+
}
|
|
2483
|
+
const plannerSkill = await st.syncPlannerSkill();
|
|
2484
|
+
await st.syncGrillMeSkill();
|
|
1874
2485
|
const web = await ensureWebStarted();
|
|
1875
2486
|
const recap = await buildRecap(st, web, { harness: "mcp" });
|
|
1876
|
-
|
|
2487
|
+
const visibleRecap = preparation.changed ? `${preparation.legacyProjectContext.summary}\n\n${recap}` : recap;
|
|
2488
|
+
return {
|
|
2489
|
+
content: [{ type: "text", text: visibleRecap }],
|
|
2490
|
+
structuredContent: {
|
|
2491
|
+
preparation: preparation.legacyProjectContext,
|
|
2492
|
+
agentContext: {
|
|
2493
|
+
kind: "planner-usage-skill",
|
|
2494
|
+
content: plannerSkill.content,
|
|
2495
|
+
status: plannerSkill.status,
|
|
2496
|
+
customized: plannerSkill.customized,
|
|
2497
|
+
message: plannerSkill.message,
|
|
2498
|
+
instruction: "Read and retain this project-local planner usage skill. Do not quote it in the human-facing recap.",
|
|
2499
|
+
},
|
|
2500
|
+
},
|
|
2501
|
+
};
|
|
1877
2502
|
});
|
|
1878
2503
|
server.registerTool("planner-disable", {
|
|
1879
2504
|
description: "MCP no-op equivalent of Pi /planner disable. Stop the MCP process from the host to disable it.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
45
45
|
"zod": "^3.24.0",
|
|
46
|
-
"@agent-plan/core": "0.2.
|
|
47
|
-
"@agent-plan/server": "0.2.
|
|
46
|
+
"@agent-plan/core": "0.2.26",
|
|
47
|
+
"@agent-plan/server": "0.2.26"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|