@agent-plan/pi-adapter 0.2.19 → 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,CAkhH9D"}
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
@@ -1479,6 +1479,7 @@ export default function planPiExtension(pi) {
1479
1479
  priority,
1480
1480
  name: nameInput.trim(),
1481
1481
  description: description?.trim() ?? "",
1482
+ descriptionUpdatedAt: now,
1482
1483
  status: status,
1483
1484
  discussedAt: "",
1484
1485
  contextReady: false,
@@ -1704,7 +1705,7 @@ export default function planPiExtension(pi) {
1704
1705
  const identity = await st.allocateEntityIdentity("phase", id);
1705
1706
  const priority = await st.nextPriority("phase", feature.id);
1706
1707
  phase = {
1707
- id, number: identity.number, shortId: identity.shortId, priority, slug, title, featureId: feature.id, status: "draft", discussedAt: "", contextReady: false, contextReadyReason: "", summary: "", description: "", notes: "",
1708
+ id, number: identity.number, shortId: identity.shortId, priority, slug, title, featureId: feature.id, status: "draft", discussedAt: "", contextReady: false, contextReadyReason: "", summary: "", description: "", descriptionUpdatedAt: "", notes: "",
1708
1709
  goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
1709
1710
  openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
1710
1711
  createdAt: nowISO(), updatedAt: nowISO(),
@@ -1949,12 +1950,14 @@ export default function planPiExtension(pi) {
1949
1950
  id: taskId, phaseId: phase.id, number: identity.number, shortId: identity.shortId, priority, shortName,
1950
1951
  title: title.trim(), status: "planned",
1951
1952
  description: "",
1953
+ descriptionUpdatedAt: "",
1952
1954
  notes: "",
1953
1955
  statusLog: [],
1954
1956
  decisions: [],
1955
1957
  acceptedDecisions: [],
1956
1958
  checklist: [], subtasks: [],
1957
1959
  dependsOn: [],
1960
+ pauseSnapshot: null, pauseHistory: [],
1958
1961
  startedAt: "", completedAt: "",
1959
1962
  createdAt: now, updatedAt: now,
1960
1963
  };
@@ -3209,6 +3212,7 @@ export default function planPiExtension(pi) {
3209
3212
  priority,
3210
3213
  name: params.name,
3211
3214
  description: params.description ?? "",
3215
+ descriptionUpdatedAt: now,
3212
3216
  status,
3213
3217
  discussedAt: "",
3214
3218
  contextReady: false,
@@ -3521,7 +3525,7 @@ export default function planPiExtension(pi) {
3521
3525
  discussedAt: "",
3522
3526
  contextReady: false,
3523
3527
  contextReadyReason: "",
3524
- summary: params.summary ?? "", description: params.description ?? "", notes: "",
3528
+ summary: params.summary ?? "", description: params.description ?? "", descriptionUpdatedAt: now, notes: "",
3525
3529
  goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
3526
3530
  openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
3527
3531
  createdAt: now, updatedAt: now,
@@ -3874,11 +3878,15 @@ export default function planPiExtension(pi) {
3874
3878
  const priority = await st.nextPriority("task", phaseId);
3875
3879
  const now = nowISO();
3876
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
+ }
3877
3884
  const task = {
3878
3885
  id: taskId, phaseId, number: identity.number, shortId: identity.shortId, priority, shortName,
3879
3886
  title: params.title,
3880
3887
  status: initialStatus,
3881
3888
  description: params.description ?? "",
3889
+ descriptionUpdatedAt: now,
3882
3890
  notes: "",
3883
3891
  statusLog: [],
3884
3892
  decisions: [],
@@ -3888,6 +3896,8 @@ export default function planPiExtension(pi) {
3888
3896
  : [],
3889
3897
  subtasks: [],
3890
3898
  dependsOn: [],
3899
+ pauseSnapshot: null,
3900
+ pauseHistory: [],
3891
3901
  startedAt: initialStatus === "in-progress" || initialStatus === "done" ? now : "",
3892
3902
  completedAt: initialStatus === "done" ? now : "",
3893
3903
  createdAt: now, updatedAt: now,
@@ -4081,7 +4091,12 @@ export default function planPiExtension(pi) {
4081
4091
  if (temporary.task.status !== "planned")
4082
4092
  return { content: [{ type: "text", text: `Temporary task is not startable: ${temporary.task.status}.` }], details: {} };
4083
4093
  const timestamp = nowISO();
4084
- 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
+ };
4085
4100
  await st.addWorkDeviation(record);
4086
4101
  await st.writeGenerated();
4087
4102
  const tempRef = `${formatPhaseRef(temporary.phase.number, featureNumberOfPhase(temporary.phase, features.features))}/T${String(temporary.task.number).padStart(3, "0")}`;
@@ -4089,10 +4104,198 @@ export default function planPiExtension(pi) {
4089
4104
  return { content: [{ type: "text", text: `✅ Approved deviation: ${tempRef} temporarily overrides ${resumeRef}. Resume target retained.` }], details: { deviation: record } };
4090
4105
  },
4091
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
+ });
4092
4295
  pi.registerTool({
4093
4296
  name: "task_start",
4094
4297
  label: "Task Start",
4095
- 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.",
4096
4299
  parameters: Type.Object({
4097
4300
  taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
4098
4301
  }),
@@ -4115,6 +4318,17 @@ export default function planPiExtension(pi) {
4115
4318
  if (!eligibility.eligible)
4116
4319
  return { content: [{ type: "text", text: `Task start denied: ${eligibility.reason}` }], details: eligibility };
4117
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
+ }
4118
4332
  // Assemble the mandatory parent context before changing task lifecycle state.
4119
4333
  // The output order is feature + its requirements, then phase + its requirements.
4120
4334
  const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
@@ -4123,29 +4337,42 @@ export default function planPiExtension(pi) {
4123
4337
  parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
4124
4338
  ]);
4125
4339
  const phaseContext = buildPhaseContextBlock(phaseWithRequirements, parentFeature, phaseWithRequirements.linkedRequirements ?? [], featureRequirements);
4126
- const advisory = selection.kind === "conflict"
4127
- ? `\n\n⚠️ ${selection.reason} Explicit task request honored; reconcile active work before relying on automatic recommendations.`
4128
- : selection.candidate && selection.candidate.task.id !== task.id
4129
- ? selection.kind === "active"
4130
- ? `\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.`
4131
- : `\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.`
4132
- : "";
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
+ : "";
4133
4343
  const now = nowISO();
4134
4344
  let startedTask;
4135
- await st.updatePhase(found.phase.id, (phase) => {
4136
- const t = phase.tasks.find((x) => x.id === found.task.id);
4137
- 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;
4138
4363
  return phase;
4139
- applyTaskLifecycleDates(t, "in-progress", now);
4140
- t.updatedAt = now;
4141
- phase.updatedAt = now;
4142
- startedTask = t;
4143
- return phase;
4144
- });
4364
+ });
4365
+ }
4145
4366
  await st.syncTaskStatusRollup(found.phase.id);
4146
4367
  const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === task.id && deviation.state === "approved");
4147
4368
  if (approvedDeviation)
4148
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);
4149
4376
  await st.writeGenerated();
4150
4377
  if (!startedTask)
4151
4378
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
@@ -4172,6 +4399,8 @@ export default function planPiExtension(pi) {
4172
4399
  const task = found.task;
4173
4400
  if (task.status === "done")
4174
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 };
4175
4404
  const unchecked = task.checklist.filter((item) => !item.checked);
4176
4405
  if (unchecked.length > 0 && !params.force) {
4177
4406
  return {
@@ -4185,7 +4414,13 @@ export default function planPiExtension(pi) {
4185
4414
  const t = phase.tasks.find((x) => x.id === found.task.id);
4186
4415
  if (!t)
4187
4416
  return phase;
4417
+ const previousStatus = t.status;
4188
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
+ }];
4189
4424
  if (params.description_update) {
4190
4425
  const sep = t.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
4191
4426
  t.description = t.description + sep + params.description_update;
@@ -4200,11 +4435,33 @@ export default function planPiExtension(pi) {
4200
4435
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === task.id)
4201
4436
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4202
4437
  if (completedDeviation)
4203
- await st.setWorkDeviationState(completedDeviation.id, "resolved", now);
4438
+ await st.setWorkDeviationState(completedDeviation.id, "resume-required", now);
4204
4439
  await st.writeGenerated();
4205
4440
  if (!completedTask)
4206
4441
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
4207
- 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
+ };
4208
4465
  },
4209
4466
  });
4210
4467
  pi.registerTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.2.19",
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/server": "0.2.19",
32
- "@agent-plan/core": "0.2.19"
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",