@agent-plan/mcp 0.4.6 → 0.4.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA4BA,KAAK,UAAU,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA2LrI;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAUzE;AAi4FD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA4BA,KAAK,UAAU,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA2LrI;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAUzE;AAk5FD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ 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, PlanStoreError, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, createStaleProcessCheck, runtimeCapabilities, runtimePackagesDiagnostic, markCanonicalFullReadForSessionId, contextReadEligibilityForSession, requirementReadEligibilityForSession, hasValidSessionAttestation, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, taskStartGateState, taskStartGateStateLine, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, HANDOFF_COLD_START_INVENTORY_VERSION, HANDOFF_COLD_START_INVENTORY_CATEGORIES, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, buildPlannerLoadReply, buildScopedDecisionContext, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildMutationReply, longTextFieldLimitNotice, buildRecommendationReply, buildTaskOrderContext, taskOrderContextLine, taskCreatedPriorityFragment, findHigherPriorityOpenPhase, higherPriorityOpenPhaseAdvisory, listOpenPhaseWork, openPhaseWorkLines, boundedOpenPhaseWork, deleteFeatureCascade, evaluateTaskCompletionGate, taskCompletionChecklistRefusal, taskCompletionForceMotivationRequired, taskCompletionOverrideNote, taskCompletionMismatchLine, boundedPage, nowISO, resolveFeatureRefStrict, resolveAcceptedDecisionTarget, applyTaskLifecycleDates, handoffContractFailureReply, derivedStatusReadOnlyReply, acceptedDecisionMutationFailureReply, legacyContextMigrationSummary, moveTasks, buildTaskMoveReply } from "@agent-plan/core";
8
+ import { PlanStore, PlanStoreError, taskOwnershipConflict, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, createStaleProcessCheck, runtimeCapabilities, runtimePackagesDiagnostic, markCanonicalFullReadForSessionId, contextReadEligibilityForSession, requirementReadEligibilityForSession, hasValidSessionAttestation, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, taskStartGateState, taskStartGateStateLine, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, HANDOFF_COLD_START_INVENTORY_VERSION, HANDOFF_COLD_START_INVENTORY_CATEGORIES, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, buildPlannerLoadReply, buildScopedDecisionContext, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildMutationReply, longTextFieldLimitNotice, buildRecommendationReply, buildTaskOrderContext, taskOrderContextLine, taskCreatedPriorityFragment, findHigherPriorityOpenPhase, higherPriorityOpenPhaseAdvisory, listOpenPhaseWork, openPhaseWorkLines, boundedOpenPhaseWork, deleteFeatureCascade, evaluateTaskCompletionGate, taskCompletionChecklistRefusal, taskCompletionForceMotivationRequired, taskCompletionOverrideNote, taskCompletionMismatchLine, boundedPage, nowISO, resolveFeatureRefStrict, resolveAcceptedDecisionTarget, applyTaskLifecycleDates, handoffContractFailureReply, derivedStatusReadOnlyReply, acceptedDecisionMutationFailureReply, legacyContextMigrationSummary, moveTasks, buildTaskMoveReply } from "@agent-plan/core";
9
9
  import { serve } from "@agent-plan/server";
10
10
  import { createChecklistItemId, createFeatureId, createPhaseId, createRequirementId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, formatIdeaRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
11
11
  const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
@@ -201,10 +201,38 @@ function withStaleServerNotice(result) {
201
201
  return { ...result, content: [...result.content, { type: "text", text: notice }] };
202
202
  }
203
203
  {
204
+ const sessionOperations = new Set(["planner-load", "planner-task-start", "planner-task-switch", "planner-task-pause", "planner-task-complete", "planner-task-reopen", "planner-task-update", "planner-task-deviation", "planner-session-recover"]);
205
+ const registeredSessions = new Set();
204
206
  const registerToolUnwrapped = server.registerTool.bind(server);
205
207
  server.registerTool = (...args) => {
206
208
  const handler = args[args.length - 1];
207
- const wrapped = async (...handlerArgs) => foldTextIntoStructured(withStaleServerNotice(await handler(...handlerArgs)));
209
+ const wrapped = async (...handlerArgs) => {
210
+ const invoke = async () => foldTextIntoStructured(withStaleServerNotice(await handler(...handlerArgs)));
211
+ if (!sessionOperations.has(args[0]))
212
+ return invoke();
213
+ const st = store();
214
+ if (!(await st.exists()))
215
+ return invoke();
216
+ const sessionId = plannerSessionIdFor(handlerArgs[1]);
217
+ const prepare = async () => {
218
+ if (!registeredSessions.has(sessionId)) {
219
+ await st.registerSession(sessionId);
220
+ registeredSessions.add(sessionId);
221
+ }
222
+ };
223
+ // Server/watchers must be created outside a write transaction's async context.
224
+ if (args[0] === "planner-load") {
225
+ await st.runBatch(prepare);
226
+ return invoke();
227
+ }
228
+ return st.runBatch(async () => {
229
+ await prepare();
230
+ const result = await invoke();
231
+ if (!result.isError)
232
+ await st.refreshResume(undefined, undefined, sessionId);
233
+ return result;
234
+ });
235
+ };
208
236
  return registerToolUnwrapped(...args.slice(0, -1), wrapped);
209
237
  };
210
238
  }
@@ -2120,6 +2148,10 @@ server.registerTool("planner-task-switch", {
2120
2148
  return text(`Task not found: ${to_task}`);
2121
2149
  if (source.task.id === target.task.id)
2122
2150
  return text("Source and temporary task must differ.");
2151
+ const allDeviations = await st.listAllWorkDeviations();
2152
+ if ([source.task, target.task].some((task) => taskOwnershipConflict(task, allDeviations, plannerSessionId))) {
2153
+ return { ...text("Task switch denied: source or target belongs to another session.", { started: false, errorCode: "TASK_OWNED_BY_ANOTHER_SESSION" }), isError: true };
2154
+ }
2123
2155
  if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
2124
2156
  return text(`Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.`);
2125
2157
  }
@@ -2202,27 +2234,7 @@ server.registerTool("planner-task-switch", {
2202
2234
  });
2203
2235
  }
2204
2236
  sourceCheckpointed = true;
2205
- if (target.task.pauseSnapshot) {
2206
- await st.resumeTask(target.phase.id, target.task.id, timestamp, plannerSessionId);
2207
- }
2208
- else {
2209
- await st.updatePhase(target.phase.id, (phase) => {
2210
- const task = phase.tasks.find((entry) => entry.id === target.task.id);
2211
- if (!task)
2212
- throw new Error(`Temporary task disappeared: ${to_task}`);
2213
- const previousStatus = task.status;
2214
- applyTaskLifecycleDates(task, "in-progress", timestamp);
2215
- task.activeOwnerSession = plannerSessionId;
2216
- task.statusLog = [...task.statusLog, {
2217
- id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
2218
- date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
2219
- title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
2220
- }];
2221
- task.updatedAt = timestamp;
2222
- phase.updatedAt = timestamp;
2223
- return phase;
2224
- });
2225
- }
2237
+ await st.claimTask(target.phase.id, target.task.id, plannerSessionId, timestamp);
2226
2238
  targetStarted = true;
2227
2239
  await st.addWorkDeviation(record, plannerSessionId);
2228
2240
  if (target.task.pauseSnapshot) {
@@ -2284,6 +2296,22 @@ server.registerTool("planner-task-reopen", {
2284
2296
  return { ...text(`Task reopen denied: ${error instanceof Error ? error.message : "unknown error"}`, { reopened: false, errorCode: details?.errorCode ?? "TASK_REOPEN_FAILED" }), isError: true };
2285
2297
  }
2286
2298
  });
2299
+ server.registerTool("planner-session-recover", {
2300
+ description: "Explicitly recover a stopped caller's task ownership, complete return stack and resume focus into this fresh connection. Identify the old session by an active task or a pending return task, without client-side session-ID mapping. Requires confirmed=true after confirming the previous caller has stopped; refuses a registered live process and a caller that already owns work. Task statuses, checkpoints and context-read gates are preserved. Then read the task context and use planner-task-start normally.",
2301
+ inputSchema: { task: z.string().min(1), confirmed: z.boolean() },
2302
+ }, async ({ task: ref, confirmed }, extra) => {
2303
+ const st = await requireStore();
2304
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
2305
+ if (!found)
2306
+ return { ...text(`Task not found: ${ref}`, { recovered: false, errorCode: "TASK_NOT_FOUND" }), isError: true };
2307
+ try {
2308
+ const result = await st.recoverSessionForTask(found.phase.id, found.task.id, plannerSessionIdFor(extra), confirmed);
2309
+ return text("Session recovered. Existing tasks, checkpoints and return stack are preserved; read the task context before continuing.", { recovered: true, ...result });
2310
+ }
2311
+ catch (error) {
2312
+ return { ...text(`Session recovery denied: ${error instanceof Error ? error.message : String(error)}`, { recovered: false, errorCode: error instanceof PlanStoreError ? error.details?.errorCode ?? "SESSION_RECOVERY_FAILED" : "SESSION_RECOVERY_FAILED" }), isError: true };
2313
+ }
2314
+ });
2287
2315
  server.registerTool("planner-task-start", {
2288
2316
  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.",
2289
2317
  inputSchema: { task: z.string().min(1) },
@@ -2347,10 +2375,19 @@ server.registerTool("planner-task-start", {
2347
2375
  await st.recordContextRead({ sessionId: plannerSessionId, phaseId: found.phase.id, taskId: found.task.id, ...(found.phase.featureId ? { featureId: found.phase.featureId } : {}), requirementIds: linkedRequirementIds });
2348
2376
  }
2349
2377
  if (found.task.status === "in-progress") {
2378
+ let claimed;
2379
+ try {
2380
+ claimed = await st.claimTask(found.phase.id, found.task.id, plannerSessionId);
2381
+ }
2382
+ catch (error) {
2383
+ if (!(error instanceof PlanStoreError))
2384
+ throw error;
2385
+ return taskStartError(taskStartDenied(error.details?.errorCode, error.message, ["Continue with the owning caller, or use planner-session-recover after confirming it has stopped."], { taskId: found.task.id }));
2386
+ }
2350
2387
  const outcome = taskStartSucceeded(found.task.id, true);
2351
2388
  const higherPriorityPhase = findHigherPriorityOpenPhase(initialPhases, features, found.phase.id, found.phase.priority ?? found.phase.number);
2352
2389
  const phaseOrderAdvisory = higherPriorityOpenPhaseAdvisory(higherPriorityPhase);
2353
- return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true${projectContextAdvisory}${phaseOrderAdvisory}`, { ...outcome, task: found.task, projectContextStale: projectContextReadState === "stale", higherPriorityOpenPhase: higherPriorityPhase });
2390
+ return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true${projectContextAdvisory}${phaseOrderAdvisory}`, { ...outcome, task: claimed.task, projectContextStale: projectContextReadState === "stale", higherPriorityOpenPhase: higherPriorityPhase });
2354
2391
  }
2355
2392
  const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume(plannerSessionId)]);
2356
2393
  const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
@@ -2405,31 +2442,13 @@ server.registerTool("planner-task-start", {
2405
2442
  : "";
2406
2443
  const timestamp = nowISO();
2407
2444
  let updatedTask;
2408
- if (found.task.pauseSnapshot) {
2409
- updatedTask = await st.resumeTask(found.phase.id, found.task.id, timestamp, plannerSessionId);
2445
+ try {
2446
+ updatedTask = (await st.claimTask(found.phase.id, found.task.id, plannerSessionId, timestamp)).task;
2410
2447
  }
2411
- else {
2412
- await st.updatePhase(found.phase.id, (phase) => {
2413
- const task = phase.tasks.find((entry) => entry.id === found.task.id);
2414
- if (!task)
2415
- return phase;
2416
- const previousStatus = task.status;
2417
- applyTaskLifecycleDates(task, "in-progress", timestamp);
2418
- const entry = {
2419
- id: createChecklistItemId(task.id, (task.statusLog?.length ?? 0) + 1, `${previousStatus}-in-progress`),
2420
- date: timestamp,
2421
- fromStatus: previousStatus,
2422
- toStatus: "in-progress",
2423
- title: `${previousStatus} → in-progress`,
2424
- description: "",
2425
- };
2426
- task.activeOwnerSession = plannerSessionId;
2427
- task.statusLog = [...(task.statusLog ?? []), entry];
2428
- task.updatedAt = timestamp;
2429
- phase.updatedAt = timestamp;
2430
- updatedTask = task;
2431
- return phase;
2432
- });
2448
+ catch (error) {
2449
+ if (!(error instanceof PlanStoreError))
2450
+ throw error;
2451
+ return taskStartError(taskStartDenied(error.details?.errorCode, error.message, ["Re-read the task and reconcile its owner before retrying."], { taskId: found.task.id }));
2433
2452
  }
2434
2453
  await st.syncTaskStatusRollup(found.phase.id);
2435
2454
  const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === found.task.id && deviation.state === "approved");
@@ -2443,7 +2462,7 @@ server.registerTool("planner-task-start", {
2443
2462
  await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
2444
2463
  await st.writeGenerated();
2445
2464
  const persistedTask = (await st.loadPhase(found.phase.id)).tasks.find((candidate) => candidate.id === found.task.id);
2446
- if (!updatedTask || persistedTask?.status !== "in-progress") {
2465
+ if (!updatedTask || persistedTask?.status !== "in-progress" || persistedTask.activeOwnerSession !== plannerSessionId) {
2447
2466
  return taskStartError(taskStartDenied("PERSISTENCE_VERIFICATION_FAILED", `The lifecycle write could not be verified as in-progress for ${taskRef}.`, ["Inspect planner-task-show for the persisted state and retry only after resolving the persistence failure."], { taskId: found.task.id }));
2448
2467
  }
2449
2468
  const outcome = taskStartSucceeded(persistedTask.id);
@@ -3164,6 +3183,7 @@ server.registerTool("planner-load", {
3164
3183
  return text(`Planner load aborted: ${error instanceof Error ? error.message : String(error)}`, { errorCode: "PLANNER_SESSION_PREPARATION_FAILED", loaded: false });
3165
3184
  }
3166
3185
  const project = preparation.project;
3186
+ await st.runBatch(() => st.refreshResume(undefined, undefined, plannerSessionId));
3167
3187
  if (project.projectGuidelines.content.trim()) {
3168
3188
  await st.recordProjectGuidelinesRead({ sessionId: plannerSessionId });
3169
3189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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.4.6",
47
- "@agent-plan/server": "0.4.6"
46
+ "@agent-plan/server": "0.4.8",
47
+ "@agent-plan/core": "0.4.8"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",