@agent-plan/pi-adapter 0.4.7 → 0.4.9

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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AAw0BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA20K9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AAw0BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAy2K9D"}
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { Type } from "typebox";
12
12
  import { paginatedSelect, paginatedNotify } from "./ui/paginate.js";
13
- import { ExportService, PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, classifyGuardedTool, loadNoTaskGuardState, noTaskWarning, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, 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_SOURCE_REVIEWS, HANDOFF_COLD_START_INVENTORY_CATEGORIES, handoffContentHash, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, buildPlannerLoadReply, buildScopedDecisionContext, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, ACCEPTED_DECISION_OWNERSHIP_RULE, ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildDecisionRecordRedirectReply, 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, statusIcon, moveTasks, buildTaskMoveReply } from "@agent-plan/core";
13
+ import { ExportService, PlanStore, PlanStoreError, taskOwnershipConflict, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, classifyGuardedTool, loadNoTaskGuardState, noTaskWarning, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, isStartableTaskStatus, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, 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_SOURCE_REVIEWS, HANDOFF_COLD_START_INVENTORY_CATEGORIES, handoffContentHash, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, buildPlannerLoadReply, buildScopedDecisionContext, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, ACCEPTED_DECISION_OWNERSHIP_RULE, ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildDecisionRecordRedirectReply, 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, statusIcon, moveTasks, buildTaskMoveReply } from "@agent-plan/core";
14
14
  import { createChecklistItemId, createFeatureId, createPhaseId, createRequirementId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, formatIdeaRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
15
15
  /** Stated on every long-prose input so a caller learns the limit before the
16
16
  * call rather than from the rejection. The number lives in the core
@@ -775,6 +775,37 @@ async function stopServer() {
775
775
  // ─── Extension factory ──────────────────────────────────────────────────
776
776
  export default function planPiExtension(pi) {
777
777
  capturedPi = pi;
778
+ const registerSessionTool = pi.registerTool.bind(pi);
779
+ const sessionTools = new Set(["planner-load", "task_start", "task_switch", "task_pause", "task_complete", "task_reopen", "task_update", "task_deviation", "session_recover"]);
780
+ const registeredSessions = new Set();
781
+ pi.registerTool = (definition) => {
782
+ if (!sessionTools.has(definition.name))
783
+ return registerSessionTool(definition);
784
+ const execute = definition.execute;
785
+ registerSessionTool({ ...definition, async execute(id, params, signal, onUpdate, ctx) {
786
+ const st = await requirePlan(ctx);
787
+ if (!st)
788
+ return execute(id, params, signal, onUpdate, ctx);
789
+ const sessionId = piSessionIdOf(ctx);
790
+ const prepare = async () => {
791
+ if (!registeredSessions.has(sessionId)) {
792
+ await st.registerSession(sessionId);
793
+ registeredSessions.add(sessionId);
794
+ }
795
+ };
796
+ if (definition.name === "planner-load") {
797
+ await st.runBatch(prepare);
798
+ return execute(id, params, signal, onUpdate, ctx);
799
+ }
800
+ return st.runBatch(async () => {
801
+ await prepare();
802
+ const result = await execute(id, params, signal, onUpdate, ctx);
803
+ if (!result.isError)
804
+ await st.refreshResume(undefined, undefined, sessionId);
805
+ return result;
806
+ });
807
+ } });
808
+ };
778
809
  // ── Restore on session start/reload ─────────────────────────────────
779
810
  pi.on("session_start", async (_event, ctx) => {
780
811
  plannerSessionId = piSessionIdOf(ctx);
@@ -5468,7 +5499,7 @@ export default function planPiExtension(pi) {
5468
5499
  return { content: [{ type: "text", text: `No resume task is available. Provide resume_task explicitly. ${selected.reason}` }], details: selected };
5469
5500
  if (temporary.task.id === resume.task.id)
5470
5501
  return { content: [{ type: "text", text: "A temporary task must differ from its resume target." }], details: {} };
5471
- if (temporary.task.status !== "planned")
5502
+ if (!isStartableTaskStatus(temporary.task.status))
5472
5503
  return { content: [{ type: "text", text: `Temporary task is not startable: ${temporary.task.status}.` }], details: {} };
5473
5504
  const timestamp = nowISO();
5474
5505
  const record = {
@@ -5570,6 +5601,10 @@ export default function planPiExtension(pi) {
5570
5601
  return { content: [{ type: "text", text: `Task not found: ${params.to_task}` }], details: {} };
5571
5602
  if (source.task.id === target.task.id)
5572
5603
  return { content: [{ type: "text", text: "Source and temporary task must differ." }], details: {} };
5604
+ const allDeviations = await st.listAllWorkDeviations();
5605
+ if ([source.task, target.task].some((task) => taskOwnershipConflict(task, allDeviations, plannerSessionId))) {
5606
+ return { content: [{ type: "text", text: "Task switch denied: source or target belongs to another session." }], details: { started: false, errorCode: "TASK_OWNED_BY_ANOTHER_SESSION" }, isError: true };
5607
+ }
5573
5608
  if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
5574
5609
  return { content: [{ type: "text", text: `Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.` }], details: {} };
5575
5610
  }
@@ -5655,27 +5690,7 @@ export default function planPiExtension(pi) {
5655
5690
  });
5656
5691
  }
5657
5692
  sourceCheckpointed = true;
5658
- if (target.task.pauseSnapshot) {
5659
- await st.resumeTask(target.phase.id, target.task.id, timestamp, plannerSessionId);
5660
- }
5661
- else {
5662
- await st.updatePhase(target.phase.id, (phase) => {
5663
- const task = phase.tasks.find((entry) => entry.id === target.task.id);
5664
- if (!task)
5665
- throw new Error(`Temporary task disappeared: ${params.to_task}`);
5666
- const previousStatus = task.status;
5667
- applyTaskLifecycleDates(task, "in-progress", timestamp);
5668
- task.activeOwnerSession = plannerSessionId;
5669
- task.statusLog = [...task.statusLog, {
5670
- id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
5671
- date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
5672
- title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
5673
- }];
5674
- task.updatedAt = timestamp;
5675
- phase.updatedAt = timestamp;
5676
- return phase;
5677
- });
5678
- }
5693
+ await st.claimTask(target.phase.id, target.task.id, plannerSessionId, timestamp);
5679
5694
  targetStarted = true;
5680
5695
  await st.addWorkDeviation(record, plannerSessionId);
5681
5696
  if (target.task.pauseSnapshot) {
@@ -5749,6 +5764,27 @@ export default function planPiExtension(pi) {
5749
5764
  }
5750
5765
  },
5751
5766
  });
5767
+ pi.registerTool({
5768
+ name: "session_recover",
5769
+ label: "Session Recover",
5770
+ description: "Explicitly recover a stopped caller's complete task ownership, return stack and focus into this fresh session. Confirm that the previous caller has stopped. Registered live processes and callers that already own work are refused. Then read context and use task_start normally.",
5771
+ parameters: Type.Object({ taskId: Type.String(), confirmed: Type.Boolean() }),
5772
+ async execute(_id, params, _signal, _onUpdate, ctx) {
5773
+ const st = await requirePlan(ctx);
5774
+ if (!st)
5775
+ return { content: [{ type: "text", text: "No planner found." }], details: { recovered: false, errorCode: "PLAN_NOT_FOUND" }, isError: true };
5776
+ const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, params.taskId);
5777
+ if (!found)
5778
+ return { content: [{ type: "text", text: "Task not found." }], details: { recovered: false, errorCode: "TASK_NOT_FOUND" }, isError: true };
5779
+ try {
5780
+ const recovered = await st.recoverSessionForTask(found.phase.id, found.task.id, piSessionIdOf(ctx), params.confirmed);
5781
+ return { content: [{ type: "text", text: "Session recovered; tasks, checkpoints and return stack are preserved. Read task context before continuing." }], details: { recovered: true, ...recovered } };
5782
+ }
5783
+ catch (error) {
5784
+ return { content: [{ type: "text", text: `Session recovery denied: ${error instanceof Error ? error.message : String(error)}` }], details: { recovered: false, errorCode: error instanceof PlanStoreError ? error.details?.errorCode ?? "SESSION_RECOVERY_FAILED" : "SESSION_RECOVERY_FAILED" }, isError: true };
5785
+ }
5786
+ },
5787
+ });
5752
5788
  pi.registerTool({
5753
5789
  name: "task_start",
5754
5790
  label: "Task Start",
@@ -5822,10 +5858,20 @@ export default function planPiExtension(pi) {
5822
5858
  });
5823
5859
  }
5824
5860
  if (task.status === "in-progress") {
5861
+ let claimed;
5862
+ try {
5863
+ claimed = await st.claimTask(found.phase.id, task.id, plannerSessionId);
5864
+ }
5865
+ catch (error) {
5866
+ if (!(error instanceof PlanStoreError))
5867
+ throw error;
5868
+ return taskStartFailure(taskStartDenied(error.details?.errorCode, error.message, ["Continue with the owning caller, or explicitly recover its session after it has stopped."], { taskId: task.id }));
5869
+ }
5870
+ await st.refreshResume(undefined, undefined, plannerSessionId);
5825
5871
  const outcome = taskStartSucceeded(task.id, true);
5826
5872
  const higherPriorityPhase = findHigherPriorityOpenPhase(initialPhases, features, found.phase.id, found.phase.priority ?? found.phase.number);
5827
5873
  const phaseOrderAdvisory = higherPriorityOpenPhaseAdvisory(higherPriorityPhase);
5828
- return { content: [{ type: "text", text: `✅ Task already started: ${taskRef} — ${task.title} (in-progress)\nstarted: true${projectContextAdvisory}${phaseOrderAdvisory}` }], details: { ...task, ...outcome, task, projectContextStale: projectContextReadState === "stale", higherPriorityOpenPhase: higherPriorityPhase } };
5874
+ return { content: [{ type: "text", text: `✅ Task already started: ${taskRef} — ${task.title} (in-progress)\nstarted: true${projectContextAdvisory}${phaseOrderAdvisory}` }], details: { ...claimed.task, ...outcome, task: claimed.task, projectContextStale: projectContextReadState === "stale", higherPriorityOpenPhase: higherPriorityPhase } };
5829
5875
  }
5830
5876
  const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume(plannerSessionId)]);
5831
5877
  const eligibility = checkExplicitTaskStart(features, phases, task.id, project.workDeviations);
@@ -5876,27 +5922,13 @@ export default function planPiExtension(pi) {
5876
5922
  : "";
5877
5923
  const now = nowISO();
5878
5924
  let startedTask;
5879
- if (task.pauseSnapshot) {
5880
- startedTask = await st.resumeTask(found.phase.id, found.task.id, now, plannerSessionId);
5925
+ try {
5926
+ startedTask = (await st.claimTask(found.phase.id, found.task.id, plannerSessionId, now)).task;
5881
5927
  }
5882
- else {
5883
- await st.updatePhase(found.phase.id, (phase) => {
5884
- const t = phase.tasks.find((x) => x.id === found.task.id);
5885
- if (!t)
5886
- return phase;
5887
- const previousStatus = t.status;
5888
- applyTaskLifecycleDates(t, "in-progress", now);
5889
- t.activeOwnerSession = plannerSessionId;
5890
- t.statusLog = [...t.statusLog, {
5891
- id: createChecklistItemId(t.id, t.statusLog.length + 1, `${previousStatus}-in-progress`),
5892
- date: now, fromStatus: previousStatus, toStatus: "in-progress",
5893
- title: `${previousStatus} → in-progress`, description: "",
5894
- }];
5895
- t.updatedAt = now;
5896
- phase.updatedAt = now;
5897
- startedTask = t;
5898
- return phase;
5899
- });
5928
+ catch (error) {
5929
+ if (!(error instanceof PlanStoreError))
5930
+ throw error;
5931
+ return taskStartFailure(taskStartDenied(error.details?.errorCode, error.message, ["Re-read the task and reconcile its owner before retrying."], { taskId: task.id }));
5900
5932
  }
5901
5933
  await st.syncTaskStatusRollup(found.phase.id);
5902
5934
  const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === task.id && deviation.state === "approved");
@@ -5910,10 +5942,11 @@ export default function planPiExtension(pi) {
5910
5942
  await st.setWorkDeviationState(resumedDeviation.id, "resumed", now);
5911
5943
  await st.writeGenerated();
5912
5944
  const persistedTask = (await st.loadPhase(found.phase.id)).tasks.find((candidate) => candidate.id === task.id);
5913
- if (!startedTask || persistedTask?.status !== "in-progress") {
5945
+ if (!startedTask || persistedTask?.status !== "in-progress" || persistedTask.activeOwnerSession !== plannerSessionId) {
5914
5946
  return taskStartFailure(taskStartDenied("PERSISTENCE_VERIFICATION_FAILED", `The lifecycle write could not be verified as in-progress for ${taskRef}.`, ["Inspect task_get for the persisted state and retry task_start only after resolving the persistence failure."], { taskId: task.id }));
5915
5947
  }
5916
5948
  const outcome = taskStartSucceeded(persistedTask.id);
5949
+ await st.refreshResume(undefined, undefined, plannerSessionId);
5917
5950
  const higherPriorityPhase = findHigherPriorityOpenPhase(phases, features, found.phase.id, found.phase.priority ?? found.phase.number);
5918
5951
  const phaseOrderAdvisory = higherPriorityOpenPhaseAdvisory(higherPriorityPhase);
5919
5952
  return {
@@ -6199,6 +6232,7 @@ export default function planPiExtension(pi) {
6199
6232
  catch (error) {
6200
6233
  return { content: [{ type: "text", text: `Planner load aborted: ${error instanceof Error ? error.message : String(error)}` }], details: { errorCode: "PLANNER_SESSION_PREPARATION_FAILED", enabled: false, running: false } };
6201
6234
  }
6235
+ await st.runBatch(() => st.refreshResume(undefined, undefined, piSessionIdOf(ctx)));
6202
6236
  enablePlannerSession();
6203
6237
  if (!server) {
6204
6238
  await startServer(ctx, undefined, "lan").catch(() => { });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "private": false,
5
5
  "description": "Pi agent extension for Agent Plan — structured project planning in .planner/",
6
6
  "keywords": [
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "zod": "^3.24.0",
30
- "@agent-plan/server": "0.4.7",
31
- "@agent-plan/core": "0.4.7"
30
+ "@agent-plan/core": "0.4.9",
31
+ "@agent-plan/server": "0.4.9"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "typebox": "*"