@agent-plan/pi-adapter 0.2.20 → 0.2.21

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;AAgzBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAshH9D"}
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;AAgzBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiwH9D"}
package/dist/index.js CHANGED
@@ -1957,6 +1957,7 @@ export default function planPiExtension(pi) {
1957
1957
  acceptedDecisions: [],
1958
1958
  checklist: [], subtasks: [],
1959
1959
  dependsOn: [],
1960
+ pauseSnapshot: null, pauseHistory: [],
1960
1961
  startedAt: "", completedAt: "",
1961
1962
  createdAt: now, updatedAt: now,
1962
1963
  };
@@ -3877,6 +3878,9 @@ export default function planPiExtension(pi) {
3877
3878
  const priority = await st.nextPriority("task", phaseId);
3878
3879
  const now = nowISO();
3879
3880
  const initialStatus = params.status ?? "planned";
3881
+ if (initialStatus === "paused") {
3882
+ return { content: [{ type: "text", text: "A task cannot be created paused without an in-progress checkpoint. Create it planned, then use task_pause/task_switch." }], details: {} };
3883
+ }
3880
3884
  const task = {
3881
3885
  id: taskId, phaseId, number: identity.number, shortId: identity.shortId, priority, shortName,
3882
3886
  title: params.title,
@@ -3892,6 +3896,8 @@ export default function planPiExtension(pi) {
3892
3896
  : [],
3893
3897
  subtasks: [],
3894
3898
  dependsOn: [],
3899
+ pauseSnapshot: null,
3900
+ pauseHistory: [],
3895
3901
  startedAt: initialStatus === "in-progress" || initialStatus === "done" ? now : "",
3896
3902
  completedAt: initialStatus === "done" ? now : "",
3897
3903
  createdAt: now, updatedAt: now,
@@ -4085,7 +4091,12 @@ export default function planPiExtension(pi) {
4085
4091
  if (temporary.task.status !== "planned")
4086
4092
  return { content: [{ type: "text", text: `Temporary task is not startable: ${temporary.task.status}.` }], details: {} };
4087
4093
  const timestamp = nowISO();
4088
- const record = { id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id, temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason: params.reason, requestedBy: "user", approvedBy: "user", state: "approved", createdAt: timestamp, activatedAt: "", resolvedAt: "" };
4094
+ const record = {
4095
+ id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id,
4096
+ temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason: params.reason, snapshot: null,
4097
+ requestedBy: "user", approvedBy: "user", state: "approved",
4098
+ createdAt: timestamp, activatedAt: "", resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
4099
+ };
4089
4100
  await st.addWorkDeviation(record);
4090
4101
  await st.writeGenerated();
4091
4102
  const tempRef = `${formatPhaseRef(temporary.phase.number, featureNumberOfPhase(temporary.phase, features.features))}/T${String(temporary.task.number).padStart(3, "0")}`;
@@ -4093,10 +4104,198 @@ export default function planPiExtension(pi) {
4093
4104
  return { content: [{ type: "text", text: `✅ Approved deviation: ${tempRef} temporarily overrides ${resumeRef}. Resume target retained.` }], details: { deviation: record } };
4094
4105
  },
4095
4106
  });
4107
+ pi.registerTool({
4108
+ name: "task_pause",
4109
+ label: "Task Pause",
4110
+ description: "Pause an in-progress task with a mandatory durable checkpoint: why work stopped, what was underway, the exact resume location, and how to continue.",
4111
+ parameters: Type.Object({
4112
+ taskId: Type.String({ description: "Task ref to pause" }),
4113
+ reason: Type.String({ description: "Why work is being paused" }),
4114
+ what_was_being_done: Type.String({ description: "Concrete work underway at the checkpoint" }),
4115
+ resume_location: Type.String({ description: "Exact file, symbol, command, or location from which to continue" }),
4116
+ how_to_resume: Type.String({ description: "Actionable instructions for resuming" }),
4117
+ paused_by: Type.Optional(Type.String({ description: "Agent/session identifier" })),
4118
+ }),
4119
+ async execute(_id, params, _signal, _onUpdate, ctx) {
4120
+ const st = await requirePlan(ctx);
4121
+ if (!st)
4122
+ return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4123
+ const features = (await st.loadFeatures()).features;
4124
+ const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
4125
+ if (!found)
4126
+ return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
4127
+ if (found.task.status !== "in-progress")
4128
+ return { content: [{ type: "text", text: `Task pause denied: task is ${found.task.status}, not in-progress.` }], details: found.task };
4129
+ const snapshot = {
4130
+ id: crypto.randomUUID(), reason: params.reason.trim(), whatWasBeingDone: params.what_was_being_done.trim(),
4131
+ resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(), relatedTaskId: "",
4132
+ pausedAt: nowISO(), pausedBy: params.paused_by?.trim() ?? "",
4133
+ };
4134
+ const paused = await st.pauseTask(found.phase.id, found.task.id, snapshot);
4135
+ await st.syncTaskStatusRollup(found.phase.id);
4136
+ const activeDeviation = (await st.loadProject()).workDeviations
4137
+ .filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
4138
+ && deviation.temporaryTaskId === found.task.id)
4139
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4140
+ if (activeDeviation)
4141
+ await st.setWorkDeviationState(activeDeviation.id, "resume-required", snapshot.pausedAt);
4142
+ const resume = activeDeviation
4143
+ ? findTaskByRef(await st.loadAllPhases(), features, activeDeviation.resumeTaskId)
4144
+ : undefined;
4145
+ await st.writeGenerated();
4146
+ const reference = `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(paused.number).padStart(3, "0")}`;
4147
+ const resumeRef = resume
4148
+ ? `${formatPhaseRef(resume.phase.number, featureNumberOfPhase(resume.phase, features))}/T${String(resume.task.number).padStart(3, "0")}`
4149
+ : "";
4150
+ return {
4151
+ content: [{ type: "text", text: [
4152
+ `⏸️ Task paused: ${reference} — ${paused.title}`,
4153
+ `Why: ${snapshot.reason}`,
4154
+ `Checkpoint: ${snapshot.whatWasBeingDone}`,
4155
+ `Resume from: ${snapshot.resumeLocation}`,
4156
+ `How to resume: ${snapshot.howToResume}`,
4157
+ resume ? `↩️ RESUME REQUIRED: ${resumeRef} — ${resume.task.title}` : "",
4158
+ ].filter(Boolean).join("\n") }],
4159
+ details: { task: paused, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) },
4160
+ };
4161
+ },
4162
+ });
4163
+ pi.registerTool({
4164
+ name: "task_switch",
4165
+ label: "Task Switch",
4166
+ description: "Switch from active work to a temporary task, even outside priority order. Atomically snapshots/pauses the source, starts the target, and pushes a durable LIFO return target.",
4167
+ parameters: Type.Object({
4168
+ from_task: Type.String({ description: "Currently in-progress task ref" }),
4169
+ to_task: Type.String({ description: "Temporary task ref to start" }),
4170
+ reason: Type.String({ description: "Why this temporary switch is necessary" }),
4171
+ what_was_being_done: Type.String({ description: "Concrete work underway in the source task" }),
4172
+ resume_location: Type.String({ description: "Exact source resume location" }),
4173
+ how_to_resume: Type.String({ description: "Actionable source resume instructions" }),
4174
+ switched_by: Type.Optional(Type.String({ description: "Agent/session identifier" })),
4175
+ }),
4176
+ async execute(_id, params, _signal, _onUpdate, ctx) {
4177
+ const st = await requirePlan(ctx);
4178
+ if (!st)
4179
+ return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4180
+ const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
4181
+ const features = featuresDoc.features;
4182
+ const source = findTaskByRef(phases, features, params.from_task.trim());
4183
+ const target = findTaskByRef(phases, features, params.to_task.trim());
4184
+ if (!source)
4185
+ return { content: [{ type: "text", text: `Task not found: ${params.from_task}` }], details: {} };
4186
+ if (!target)
4187
+ return { content: [{ type: "text", text: `Task not found: ${params.to_task}` }], details: {} };
4188
+ if (source.task.id === target.task.id)
4189
+ return { content: [{ type: "text", text: "Source and temporary task must differ." }], details: {} };
4190
+ if (source.task.status !== "in-progress" && source.task.status !== "paused") {
4191
+ return { content: [{ type: "text", text: `Task switch denied: source is ${source.task.status}; only in-progress or pending-resume paused work can be switched.` }], details: {} };
4192
+ }
4193
+ const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
4194
+ if (!eligibility.eligible)
4195
+ return { content: [{ type: "text", text: `Task switch denied: ${eligibility.reason}` }], details: eligibility };
4196
+ const timestamp = nowISO();
4197
+ const snapshot = {
4198
+ id: crypto.randomUUID(), reason: params.reason.trim(), whatWasBeingDone: params.what_was_being_done.trim(),
4199
+ resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(),
4200
+ relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: params.switched_by?.trim() ?? "",
4201
+ };
4202
+ const selection = recommendNextTask(features, phases, project.workDeviations);
4203
+ const record = {
4204
+ id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
4205
+ temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason: snapshot.reason, snapshot,
4206
+ requestedBy: "agent", approvedBy: params.switched_by?.trim() || "explicit task_switch",
4207
+ state: "active", createdAt: timestamp, activatedAt: timestamp,
4208
+ resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
4209
+ };
4210
+ const sourceWasActive = source.task.status === "in-progress";
4211
+ let sourceCheckpointed = false;
4212
+ let targetStarted = false;
4213
+ try {
4214
+ if (sourceWasActive) {
4215
+ await st.pauseTask(source.phase.id, source.task.id, snapshot);
4216
+ }
4217
+ else {
4218
+ await st.updatePhase(source.phase.id, (phase) => {
4219
+ const task = phase.tasks.find((entry) => entry.id === source.task.id);
4220
+ if (!task)
4221
+ throw new Error(`Paused source task disappeared: ${params.from_task}`);
4222
+ task.pauseSnapshot = snapshot;
4223
+ task.pauseHistory = [...task.pauseHistory, snapshot];
4224
+ task.updatedAt = timestamp;
4225
+ return phase;
4226
+ });
4227
+ }
4228
+ sourceCheckpointed = true;
4229
+ if (target.task.status === "paused") {
4230
+ await st.resumeTask(target.phase.id, target.task.id, timestamp);
4231
+ }
4232
+ else {
4233
+ await st.updatePhase(target.phase.id, (phase) => {
4234
+ const task = phase.tasks.find((entry) => entry.id === target.task.id);
4235
+ if (!task)
4236
+ throw new Error(`Temporary task disappeared: ${params.to_task}`);
4237
+ const previousStatus = task.status;
4238
+ applyTaskLifecycleDates(task, "in-progress", timestamp);
4239
+ task.statusLog = [...task.statusLog, {
4240
+ id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
4241
+ date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
4242
+ title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
4243
+ }];
4244
+ task.updatedAt = timestamp;
4245
+ phase.updatedAt = timestamp;
4246
+ return phase;
4247
+ });
4248
+ }
4249
+ targetStarted = true;
4250
+ await st.addWorkDeviation(record);
4251
+ if (target.task.status === "paused") {
4252
+ const resumedDeviation = project.workDeviations
4253
+ .filter((deviation) => deviation.resumeTaskId === target.task.id
4254
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
4255
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4256
+ if (resumedDeviation)
4257
+ await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
4258
+ }
4259
+ }
4260
+ catch (error) {
4261
+ if (targetStarted) {
4262
+ await st.updatePhase(target.phase.id, (phase) => {
4263
+ phase.tasks = phase.tasks.map((task) => task.id === target.task.id ? target.task : task);
4264
+ return phase;
4265
+ }).catch(() => { });
4266
+ }
4267
+ if (sourceCheckpointed) {
4268
+ if (sourceWasActive)
4269
+ await st.resumeTask(source.phase.id, source.task.id, nowISO()).catch(() => { });
4270
+ else
4271
+ await st.updatePhase(source.phase.id, (phase) => {
4272
+ phase.tasks = phase.tasks.map((task) => task.id === source.task.id ? source.task : task);
4273
+ return phase;
4274
+ }).catch(() => { });
4275
+ }
4276
+ throw error;
4277
+ }
4278
+ await st.syncTaskStatusRollup(source.phase.id);
4279
+ if (target.phase.id !== source.phase.id)
4280
+ await st.syncTaskStatusRollup(target.phase.id);
4281
+ await st.writeGenerated();
4282
+ const sourceRef = `${formatPhaseRef(source.phase.number, featureNumberOfPhase(source.phase, features))}/T${String(source.task.number).padStart(3, "0")}`;
4283
+ const targetRef = `${formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features))}/T${String(target.task.number).padStart(3, "0")}`;
4284
+ return {
4285
+ content: [{ type: "text", text: [
4286
+ `🔀 Task switched: ${sourceRef} → ${targetRef}`,
4287
+ `Paused snapshot: ${snapshot.whatWasBeingDone}`,
4288
+ `Return target: ${sourceRef} from ${snapshot.resumeLocation}`,
4289
+ "Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.",
4290
+ ].join("\n") }],
4291
+ details: { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id },
4292
+ };
4293
+ },
4294
+ });
4096
4295
  pi.registerTool({
4097
4296
  name: "task_start",
4098
4297
  label: "Task Start",
4099
- description: "Set a task to in-progress. BEFORE calling, read feature_get(full=true) for the parent feature and its linked requirements, then phase_get(full=true) for the parent phase and its linked requirements, then task_get(full=true). Continue an already in-progress task; otherwise select ready work by feature → phase → task priority (lower number first), respecting dependencies and blocked/waiting states. Sets startedAt automatically and returns the ordered feature/phase requirements briefing.",
4298
+ description: "Set a task to in-progress or resume paused work. BEFORE calling, read feature_get(full=true), phase_get(full=true), then task_get(full=true). A different active task must first be suspended through task_switch. Otherwise follow active/pending-resume/feature → phase → task priority guidance.",
4100
4299
  parameters: Type.Object({
4101
4300
  taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
4102
4301
  }),
@@ -4119,6 +4318,17 @@ export default function planPiExtension(pi) {
4119
4318
  if (!eligibility.eligible)
4120
4319
  return { content: [{ type: "text", text: `Task start denied: ${eligibility.reason}` }], details: eligibility };
4121
4320
  const selection = recommendNextTask(features, phases, project.workDeviations);
4321
+ if (selection.kind === "conflict") {
4322
+ return { content: [{ type: "text", text: `${selection.reason} Pause/reconcile active tasks before starting another task.` }], details: selection };
4323
+ }
4324
+ if (selection.kind === "active" && selection.candidate?.task.id !== task.id) {
4325
+ const activeRef = `${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")}`;
4326
+ return { content: [{ type: "text", text: `Task start denied: ${activeRef} is active. Use task_switch to snapshot and pause it before starting another task.` }], details: selection };
4327
+ }
4328
+ if (selection.kind === "resume" && selection.candidate?.task.id !== task.id) {
4329
+ const resumeRef = `${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")}`;
4330
+ return { content: [{ type: "text", text: `Task start denied: ${resumeRef} is pending resume. Resume it now, or use task_switch from that paused task with a new checkpoint and reason.` }], details: selection };
4331
+ }
4122
4332
  // Assemble the mandatory parent context before changing task lifecycle state.
4123
4333
  // The output order is feature + its requirements, then phase + its requirements.
4124
4334
  const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
@@ -4127,29 +4337,42 @@ export default function planPiExtension(pi) {
4127
4337
  parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
4128
4338
  ]);
4129
4339
  const phaseContext = buildPhaseContextBlock(phaseWithRequirements, parentFeature, phaseWithRequirements.linkedRequirements ?? [], featureRequirements);
4130
- const advisory = selection.kind === "conflict"
4131
- ? `\n\n⚠️ ${selection.reason} Explicit task request honored; reconcile active work before relying on automatic recommendations.`
4132
- : selection.candidate && selection.candidate.task.id !== task.id
4133
- ? selection.kind === "active"
4134
- ? `\n\n⚠️ Active-task advisory: ${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")} is already in progress. Explicit task request honored.`
4135
- : `\n\n⚠️ Priority advisory: ${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")} is the automatic recommendation. Explicit task request honored.`
4136
- : "";
4340
+ const advisory = selection.candidate && selection.candidate.task.id !== task.id
4341
+ ? `\n\n⚠️ ${selection.kind === "resume" ? "Resume" : "Priority"} advisory: ${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")} is the automatic recommendation. Explicit task request honored.`
4342
+ : "";
4137
4343
  const now = nowISO();
4138
4344
  let startedTask;
4139
- await st.updatePhase(found.phase.id, (phase) => {
4140
- const t = phase.tasks.find((x) => x.id === found.task.id);
4141
- if (!t)
4345
+ if (task.status === "paused") {
4346
+ startedTask = await st.resumeTask(found.phase.id, found.task.id, now);
4347
+ }
4348
+ else {
4349
+ await st.updatePhase(found.phase.id, (phase) => {
4350
+ const t = phase.tasks.find((x) => x.id === found.task.id);
4351
+ if (!t)
4352
+ return phase;
4353
+ const previousStatus = t.status;
4354
+ applyTaskLifecycleDates(t, "in-progress", now);
4355
+ t.statusLog = [...t.statusLog, {
4356
+ id: createChecklistItemId(t.id, t.statusLog.length + 1, `${previousStatus}-in-progress`),
4357
+ date: now, fromStatus: previousStatus, toStatus: "in-progress",
4358
+ title: `${previousStatus} → in-progress`, description: "",
4359
+ }];
4360
+ t.updatedAt = now;
4361
+ phase.updatedAt = now;
4362
+ startedTask = t;
4142
4363
  return phase;
4143
- applyTaskLifecycleDates(t, "in-progress", now);
4144
- t.updatedAt = now;
4145
- phase.updatedAt = now;
4146
- startedTask = t;
4147
- return phase;
4148
- });
4364
+ });
4365
+ }
4149
4366
  await st.syncTaskStatusRollup(found.phase.id);
4150
4367
  const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === task.id && deviation.state === "approved");
4151
4368
  if (approvedDeviation)
4152
4369
  await st.setWorkDeviationState(approvedDeviation.id, "active", now);
4370
+ const resumedDeviation = project.workDeviations
4371
+ .filter((deviation) => deviation.resumeTaskId === task.id
4372
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
4373
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4374
+ if (resumedDeviation)
4375
+ await st.setWorkDeviationState(resumedDeviation.id, "resumed", now);
4153
4376
  await st.writeGenerated();
4154
4377
  if (!startedTask)
4155
4378
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
@@ -4176,6 +4399,8 @@ export default function planPiExtension(pi) {
4176
4399
  const task = found.task;
4177
4400
  if (task.status === "done")
4178
4401
  return { content: [{ type: "text", text: `Task ${task.id} is already done.` }], details: task };
4402
+ if (task.status === "paused")
4403
+ return { content: [{ type: "text", text: "Task completion denied: resume paused work with task_start before completing it, or cancel it explicitly with motivation." }], details: task };
4179
4404
  const unchecked = task.checklist.filter((item) => !item.checked);
4180
4405
  if (unchecked.length > 0 && !params.force) {
4181
4406
  return {
@@ -4189,7 +4414,13 @@ export default function planPiExtension(pi) {
4189
4414
  const t = phase.tasks.find((x) => x.id === found.task.id);
4190
4415
  if (!t)
4191
4416
  return phase;
4417
+ const previousStatus = t.status;
4192
4418
  applyTaskLifecycleDates(t, "done", now);
4419
+ t.statusLog = [...t.statusLog, {
4420
+ id: createChecklistItemId(t.id, t.statusLog.length + 1, `${previousStatus}-done`),
4421
+ date: now, fromStatus: previousStatus, toStatus: "done",
4422
+ title: `${previousStatus} → done`, description: "",
4423
+ }];
4193
4424
  if (params.description_update) {
4194
4425
  const sep = t.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
4195
4426
  t.description = t.description + sep + params.description_update;
@@ -4204,11 +4435,33 @@ export default function planPiExtension(pi) {
4204
4435
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === task.id)
4205
4436
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4206
4437
  if (completedDeviation)
4207
- await st.setWorkDeviationState(completedDeviation.id, "resolved", now);
4438
+ await st.setWorkDeviationState(completedDeviation.id, "resume-required", now);
4208
4439
  await st.writeGenerated();
4209
4440
  if (!completedTask)
4210
4441
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
4211
- return { content: [{ type: "text", text: `✅ Task completed: ${completedTask.id} — ${completedTask.title} (done)${clearedRef ? ` — phase handoff archived (${clearedRef})` : ""}` }], details: completedTask };
4442
+ let resumeNotice = "";
4443
+ let resumeRequired;
4444
+ if (completedDeviation) {
4445
+ const resume = findTaskByRef(await st.loadAllPhases(), features, completedDeviation.resumeTaskId);
4446
+ if (resume) {
4447
+ const snapshot = resume.task.pauseSnapshot ?? completedDeviation.snapshot;
4448
+ const resumeRef = `${formatPhaseRef(resume.phase.number, featureNumberOfPhase(resume.phase, features))}/T${String(resume.task.number).padStart(3, "0")}`;
4449
+ resumeNotice = [
4450
+ "",
4451
+ `↩️ RESUME REQUIRED: ${resumeRef} — ${resume.task.title}`,
4452
+ snapshot ? `Paused because: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
4453
+ snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
4454
+ snapshot ? `How to resume: ${snapshot.howToResume}` : "",
4455
+ `Next action: task_start ${resumeRef}`,
4456
+ ].filter(Boolean).join("\n");
4457
+ resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot };
4458
+ }
4459
+ }
4460
+ const completedRef = `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(completedTask.number).padStart(3, "0")}`;
4461
+ return {
4462
+ content: [{ type: "text", text: `✅ Task completed: ${completedRef} — ${completedTask.title} (done)${clearedRef ? ` — phase handoff archived (${clearedRef})` : ""}${resumeNotice}` }],
4463
+ details: resumeRequired ? { task: completedTask, resumeRequired } : completedTask,
4464
+ };
4212
4465
  },
4213
4466
  });
4214
4467
  pi.registerTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
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.2.20",
32
- "@agent-plan/server": "0.2.20"
31
+ "@agent-plan/core": "0.2.21",
32
+ "@agent-plan/server": "0.2.21"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",