@agent-plan/pi-adapter 0.4.0 → 0.4.2

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;AAm0BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAy0K9D"}
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"}
package/dist/index.js CHANGED
@@ -52,6 +52,10 @@ let store = null;
52
52
  let server = null;
53
53
  let lastKnownWebPort = null;
54
54
  let plannerSessionId = "pi:uninitialized";
55
+ /** The planner session id of a Pi host context. One place, so every handler derives it the same way. */
56
+ function piSessionIdOf(ctx) {
57
+ return `pi:${ctx.sessionManager.getSessionId()}`;
58
+ }
55
59
  // True while the agent is mutating .planner/ files. The web server returns 503-busy
56
60
  // during this window so the UI doesn't render inconsistent data.
57
61
  let planBusy = false;
@@ -314,7 +318,7 @@ async function ensureProjectLanguagePreferences(st, persist = true) {
314
318
  async function buildHandoffMarkdown(st, reason, overrides) {
315
319
  const [plan, resume, activity] = await Promise.all([
316
320
  st.loadAll(),
317
- st.refreshResume(),
321
+ st.refreshResume(undefined, undefined, plannerSessionId),
318
322
  st.loadActivityLog(),
319
323
  ]);
320
324
  const createdAt = nowISO();
@@ -773,7 +777,7 @@ export default function planPiExtension(pi) {
773
777
  capturedPi = pi;
774
778
  // ── Restore on session start/reload ─────────────────────────────────
775
779
  pi.on("session_start", async (_event, ctx) => {
776
- plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
780
+ plannerSessionId = piSessionIdOf(ctx);
777
781
  invalidateReads(plannerSessionId);
778
782
  startReadSession(plannerSessionId);
779
783
  try {
@@ -2248,15 +2252,14 @@ export default function planPiExtension(pi) {
2248
2252
  // task_recommend tool, so a person at the command line and an agent
2249
2253
  // calling the tool see identical reasoning, just a compact few-line
2250
2254
  // notify instead of a structured payload.
2251
- const [features, phases, project, resume, handoffs, archivedHandoffs] = await Promise.all([
2255
+ const [features, phases, resume, handoffs, archivedHandoffs] = await Promise.all([
2252
2256
  st.loadFeatures(),
2253
2257
  st.loadAllPhases(),
2254
- st.loadProject(),
2255
- st.loadResume(),
2258
+ st.loadResume(plannerSessionId),
2256
2259
  st.listHandoffs(),
2257
2260
  st.listArchivedHandoffs(),
2258
2261
  ]);
2259
- const result = recommendNextWork(features.features, phases, project.workDeviations, resume?.currentPhaseId, "", {
2262
+ const result = recommendNextWork(features.features, phases, await st.listAllWorkDeviations(), resume?.currentPhaseId, plannerSessionId, {
2260
2263
  handoffs: handoffs.map((handoff) => ({
2261
2264
  compositeRef: handoff.compositeRef,
2262
2265
  firstLine: handoff.firstLine,
@@ -2738,7 +2741,7 @@ export default function planPiExtension(pi) {
2738
2741
  const plannerSkill = await st.syncPlannerSkill();
2739
2742
  await st.syncGrillMeSkill();
2740
2743
  const srv = server;
2741
- let recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
2744
+ let recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi", sessionId: plannerSessionId });
2742
2745
  if (preparation.changed)
2743
2746
  recapText = `${preparation.legacyProjectContext.summary}\n\n${recapText}`;
2744
2747
  // Explicit planner load must deliver the complete project-level
@@ -3101,7 +3104,7 @@ export default function planPiExtension(pi) {
3101
3104
  }),
3102
3105
  async execute(_id, params, _signal, _onUpdate, ctx) {
3103
3106
  const st = await requirePlan(ctx);
3104
- const plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
3107
+ const plannerSessionId = piSessionIdOf(ctx);
3105
3108
  if (!st)
3106
3109
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3107
3110
  const requirements = await st.loadRequirements();
@@ -3965,7 +3968,7 @@ export default function planPiExtension(pi) {
3965
3968
  }),
3966
3969
  async execute(_id, params, _signal, _onUpdate, ctx) {
3967
3970
  const st = await requirePlan(ctx);
3968
- const plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
3971
+ const plannerSessionId = piSessionIdOf(ctx);
3969
3972
  if (!st)
3970
3973
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3971
3974
  const features = (await st.loadFeatures()).features;
@@ -4324,7 +4327,7 @@ export default function planPiExtension(pi) {
4324
4327
  }),
4325
4328
  async execute(_id, params, _signal, _onUpdate, ctx) {
4326
4329
  const st = await requirePlan(ctx);
4327
- const plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
4330
+ const plannerSessionId = piSessionIdOf(ctx);
4328
4331
  if (!st)
4329
4332
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4330
4333
  const features = (await st.loadFeatures()).features;
@@ -4917,7 +4920,7 @@ export default function planPiExtension(pi) {
4917
4920
  }),
4918
4921
  async execute(_id, params, _signal, _onUpdate, ctx) {
4919
4922
  const st = await requirePlan(ctx);
4920
- const plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
4923
+ const plannerSessionId = piSessionIdOf(ctx);
4921
4924
  if (!st)
4922
4925
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4923
4926
  const features = (await st.loadFeatures()).features;
@@ -4928,7 +4931,7 @@ export default function planPiExtension(pi) {
4928
4931
  const summary = `${found.task.title} — ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)}${found.task.shortId ? ` · ${found.task.shortId}` : ""} (${found.task.status})`;
4929
4932
  if (!params.full)
4930
4933
  return { content: [{ type: "text", text: summary }], details: {} };
4931
- const project = await st.loadProject();
4934
+ const project = await st.loadProject(plannerSessionId);
4932
4935
  const pendingDeviation = found.task.status === "done" || found.task.status === "canceled" || found.task.status === "rejected"
4933
4936
  ? undefined
4934
4937
  : project.workDeviations
@@ -5415,18 +5418,18 @@ export default function planPiExtension(pi) {
5415
5418
  description: "Return the next-work chain: active task if present, otherwise the lowest-priority ready feature → phase → task, plus bounded typed claims. Only persisted resume-ready handoffs are actionable; Markdown prose and terminal archives are not authority. Never blocks an explicit user choice.",
5416
5419
  parameters: Type.Object({}),
5417
5420
  async execute(_id, _params, _signal, _onUpdate, ctx) {
5421
+ const plannerSessionId = piSessionIdOf(ctx);
5418
5422
  const st = await requirePlan(ctx);
5419
5423
  if (!st)
5420
5424
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
5421
- const [features, phases, project, resume, handoffs, archivedHandoffs] = await Promise.all([
5425
+ const [features, phases, resume, handoffs, archivedHandoffs] = await Promise.all([
5422
5426
  st.loadFeatures(),
5423
5427
  st.loadAllPhases(),
5424
- st.loadProject(),
5425
- st.loadResume(),
5428
+ st.loadResume(plannerSessionId),
5426
5429
  st.listHandoffs(),
5427
5430
  st.listArchivedHandoffs(),
5428
5431
  ]);
5429
- const result = recommendNextWork(features.features, phases, project.workDeviations, resume?.currentPhaseId, "", {
5432
+ const result = recommendNextWork(features.features, phases, await st.listAllWorkDeviations(), resume?.currentPhaseId, plannerSessionId, {
5430
5433
  handoffs: handoffs.map((handoff) => ({
5431
5434
  compositeRef: handoff.compositeRef,
5432
5435
  firstLine: handoff.firstLine,
@@ -5451,14 +5454,15 @@ export default function planPiExtension(pi) {
5451
5454
  reason: Type.String({ description: "Why this approved temporary deviation is needed" }),
5452
5455
  }),
5453
5456
  async execute(_id, params, _signal, _onUpdate, ctx) {
5457
+ const plannerSessionId = piSessionIdOf(ctx);
5454
5458
  const st = await requirePlan(ctx);
5455
5459
  if (!st)
5456
5460
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
5457
- const [features, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
5461
+ const [features, phases, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadResume(plannerSessionId)]);
5458
5462
  const temporary = findTaskByRef(phases, features.features, params.temporary_task.trim());
5459
5463
  if (!temporary)
5460
5464
  return { content: [{ type: "text", text: `Task not found: ${params.temporary_task}` }], details: {} };
5461
- const selected = recommendNextTask(features.features, phases, project.workDeviations, focus?.currentPhaseId);
5465
+ const selected = recommendNextTask(features.features, phases, await st.listAllWorkDeviations(), focus?.currentPhaseId, plannerSessionId);
5462
5466
  const resume = params.resume_task ? findTaskByRef(phases, features.features, params.resume_task.trim()) : selected.candidate;
5463
5467
  if (!resume)
5464
5468
  return { content: [{ type: "text", text: `No resume task is available. Provide resume_task explicitly. ${selected.reason}` }], details: selected };
@@ -5473,7 +5477,7 @@ export default function planPiExtension(pi) {
5473
5477
  requestedBy: "user", approvedBy: "user", state: "approved",
5474
5478
  createdAt: timestamp, activatedAt: "", resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
5475
5479
  };
5476
- await st.addWorkDeviation(record);
5480
+ await st.addWorkDeviation(record, plannerSessionId);
5477
5481
  await st.writeGenerated();
5478
5482
  const tempRef = `${formatPhaseRef(temporary.phase.number, featureNumberOfPhase(temporary.phase, features.features))}/T${String(temporary.task.number).padStart(3, "0")}`;
5479
5483
  const resumeRef = `${formatPhaseRef(resume.phase.number, featureNumberOfPhase(resume.phase, features.features))}/T${String(resume.task.number).padStart(3, "0")}`;
@@ -5493,6 +5497,7 @@ export default function planPiExtension(pi) {
5493
5497
  paused_by: Type.Optional(Type.String({ description: "Agent/session identifier" })),
5494
5498
  }),
5495
5499
  async execute(_id, params, _signal, _onUpdate, ctx) {
5500
+ const plannerSessionId = piSessionIdOf(ctx);
5496
5501
  const st = await requirePlan(ctx);
5497
5502
  if (!st)
5498
5503
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
@@ -5510,7 +5515,7 @@ export default function planPiExtension(pi) {
5510
5515
  invalidateReads(plannerSessionId);
5511
5516
  const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
5512
5517
  await st.syncTaskStatusRollup(found.phase.id);
5513
- const activeDeviation = (await st.loadProject()).workDeviations
5518
+ const activeDeviation = (await st.loadProject(plannerSessionId)).workDeviations
5514
5519
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
5515
5520
  && deviation.temporaryTaskId === found.task.id)
5516
5521
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
@@ -5552,10 +5557,10 @@ export default function planPiExtension(pi) {
5552
5557
  }),
5553
5558
  async execute(_id, params, _signal, _onUpdate, ctx) {
5554
5559
  const st = await requirePlan(ctx);
5555
- const plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
5560
+ const plannerSessionId = piSessionIdOf(ctx);
5556
5561
  if (!st)
5557
5562
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
5558
- const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
5563
+ const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(plannerSessionId), st.loadResume(plannerSessionId)]);
5559
5564
  const features = featuresDoc.features;
5560
5565
  const source = findTaskByRef(phases, features, params.from_task.trim());
5561
5566
  const target = findTaskByRef(phases, features, params.to_task.trim());
@@ -5622,7 +5627,7 @@ export default function planPiExtension(pi) {
5622
5627
  resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(),
5623
5628
  relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: params.switched_by?.trim() ?? "",
5624
5629
  };
5625
- const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId, plannerSessionId);
5630
+ const selection = recommendNextTask(features, phases, await st.listAllWorkDeviations(), focus?.currentPhaseId, plannerSessionId);
5626
5631
  const record = {
5627
5632
  id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
5628
5633
  temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason: snapshot.reason, snapshot,
@@ -5672,7 +5677,7 @@ export default function planPiExtension(pi) {
5672
5677
  });
5673
5678
  }
5674
5679
  targetStarted = true;
5675
- await st.addWorkDeviation(record);
5680
+ await st.addWorkDeviation(record, plannerSessionId);
5676
5681
  if (target.task.pauseSnapshot) {
5677
5682
  const resumedDeviation = project.workDeviations
5678
5683
  .filter((deviation) => deviation.resumeTaskId === target.task.id
@@ -5733,7 +5738,7 @@ export default function planPiExtension(pi) {
5733
5738
  if (!found)
5734
5739
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: { reopened: false, errorCode: "TASK_NOT_FOUND" }, isError: true };
5735
5740
  try {
5736
- const reopened = await st.reopenTask(found.phase.id, found.task.id, { confirmed: params.confirmed, ownerSessionId: `pi:${ctx.sessionManager.getSessionId()}` });
5741
+ const reopened = await st.reopenTask(found.phase.id, found.task.id, { confirmed: params.confirmed, ownerSessionId: piSessionIdOf(ctx) });
5737
5742
  await st.syncTaskStatusRollup(found.phase.id);
5738
5743
  await st.writeGenerated();
5739
5744
  return { content: [{ type: "text", text: `✅ Task reopened: ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(reopened.number)} — ${reopened.title} (in-progress)` }], details: { reopened: true, task: reopened } };
@@ -5753,7 +5758,7 @@ export default function planPiExtension(pi) {
5753
5758
  }),
5754
5759
  async execute(_id, params, _signal, _onUpdate, ctx) {
5755
5760
  const st = await requirePlan(ctx);
5756
- const plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
5761
+ const plannerSessionId = piSessionIdOf(ctx);
5757
5762
  if (!st)
5758
5763
  return taskStartFailure(taskStartDenied("PLAN_NOT_FOUND", "No .planner/ found.", ["Initialize or load the planner, then retry task_start."]));
5759
5764
  return st.runBatch(async () => {
@@ -5769,7 +5774,7 @@ export default function planPiExtension(pi) {
5769
5774
  const featureRef = feature ? formatFeatureRef(feature.number) : "";
5770
5775
  if (task.status === "done")
5771
5776
  return taskStartFailure(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use task_reopen with taskId=${taskRef} and confirmed=true after explicit user confirmation.`, `Retry task_start ${taskRef} only if reopening is not the intended operation.`], { taskId: task.id }));
5772
- const project = await st.loadProject();
5777
+ const project = await st.loadProject(plannerSessionId);
5773
5778
  // Targeted stale-context refresh (P102(F005)/T404): task_start delivery
5774
5779
  // stays scoped to feature/phase/task context, but a session that
5775
5780
  // already did a full planner-load gets a non-blocking advisory when
@@ -5822,11 +5827,11 @@ export default function planPiExtension(pi) {
5822
5827
  const phaseOrderAdvisory = higherPriorityOpenPhaseAdvisory(higherPriorityPhase);
5823
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 } };
5824
5829
  }
5825
- const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume()]);
5830
+ const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume(plannerSessionId)]);
5826
5831
  const eligibility = checkExplicitTaskStart(features, phases, task.id, project.workDeviations);
5827
5832
  if (!eligibility.eligible)
5828
5833
  return taskStartFailure(taskStartDenied("START_NOT_ALLOWED", eligibility.reason, ["Resolve the reported task readiness condition, then retry task_start."], { taskId: task.id }), { eligibility });
5829
- const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId, plannerSessionId);
5834
+ const selection = recommendNextTask(features, phases, await st.listAllWorkDeviations(), focus?.currentPhaseId, plannerSessionId);
5830
5835
  if (selection.kind === "conflict") {
5831
5836
  return taskStartFailure(taskStartDenied("ACTIVE_TASK_CONFLICT", selection.reason, ["Pause or reconcile the active tasks.", `Retry task_start ${taskRef}.`], { taskId: task.id }), { selection });
5832
5837
  }
@@ -5930,6 +5935,7 @@ export default function planPiExtension(pi) {
5930
5935
  description_update: Type.String({ minLength: 10, description: "Required evidence: shipped work, verification level including partial verification, remaining/unverified work, files, decisions, and updated code references." }),
5931
5936
  }),
5932
5937
  async execute(_id, params, _signal, _onUpdate, ctx) {
5938
+ const plannerSessionId = piSessionIdOf(ctx);
5933
5939
  const st = await requirePlan(ctx);
5934
5940
  if (!st)
5935
5941
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
@@ -5979,7 +5985,7 @@ export default function planPiExtension(pi) {
5979
5985
  return phase;
5980
5986
  });
5981
5987
  const clearedRef = await st.syncTaskStatusRollup(found.phase.id);
5982
- const completedDeviation = (await st.loadProject()).workDeviations
5988
+ const completedDeviation = (await st.loadProject(plannerSessionId)).workDeviations
5983
5989
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === task.id)
5984
5990
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
5985
5991
  if (completedDeviation)
@@ -6207,7 +6213,7 @@ export default function planPiExtension(pi) {
6207
6213
  const srv = server;
6208
6214
  let recap = "";
6209
6215
  try {
6210
- recap = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
6216
+ recap = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi", sessionId: plannerSessionId });
6211
6217
  }
6212
6218
  catch (e) {
6213
6219
  recap = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
@@ -6264,7 +6270,7 @@ export default function planPiExtension(pi) {
6264
6270
  // Session initialization must not mutate canonical planner entities.
6265
6271
  // Migrations/backfills/repairs are explicit tools; resume is `.local`.
6266
6272
  if (!plannerHeavyInitDone) {
6267
- await st.refreshResume();
6273
+ await st.refreshResume(undefined, undefined, plannerSessionId);
6268
6274
  plannerHeavyInitDone = true;
6269
6275
  }
6270
6276
  // Resume-flag lifecycle — MUST run every turn, including cache hits.
@@ -6291,7 +6297,7 @@ export default function planPiExtension(pi) {
6291
6297
  const plan = await st.loadAll();
6292
6298
  const project = plan.project;
6293
6299
  const profile = await st.loadCodebaseProfile();
6294
- const resume = await st.loadResume().catch(() => null) ?? await st.refreshResume();
6300
+ const resume = await st.loadResume(plannerSessionId).catch(() => null) ?? await st.refreshResume(undefined, undefined, plannerSessionId);
6295
6301
  const activity = await st.loadActivityLog();
6296
6302
  const recentActivity = activity.entries.slice(-3).reverse();
6297
6303
  const handoffs = await st.listHandoffs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "description": "Pi agent extension for Agent Plan — structured project planning in .planner/",
6
6
  "keywords": [
@@ -28,8 +28,8 @@
28
28
  "dependencies": {
29
29
  "typebox": "^1.1.24",
30
30
  "zod": "^3.24.0",
31
- "@agent-plan/core": "0.4.0",
32
- "@agent-plan/server": "0.4.0"
31
+ "@agent-plan/server": "0.4.2",
32
+ "@agent-plan/core": "0.4.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",