@agent-plan/pi-adapter 0.2.20 → 0.2.22

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.
package/README.md CHANGED
@@ -10,10 +10,22 @@ pi install npm:@agent-plan/pi-adapter
10
10
 
11
11
  Then restart Pi in a project directory to enable planning.
12
12
 
13
+ Check the version actually loaded by Pi:
14
+
15
+ ```text
16
+ /planner version
17
+ ```
18
+
19
+ From a shell, inspect the installed package directly:
20
+
21
+ ```bash
22
+ npm --prefix "$HOME/.pi/agent/npm" list @agent-plan/pi-adapter --depth=0
23
+ ```
24
+
13
25
  ## Commands
14
26
 
15
27
  Once installed, use `/planner init` to initialize planning in a project,
16
- then `/planner feature add`, `/planner phase add`, `/planner task add`,
28
+ then `/planner version`, `/planner feature add`, `/planner phase add`, `/planner task add`,
17
29
  and the lifecycle commands `/planner task start`/`/planner task complete`.
18
30
 
19
31
  See the [root README](https://github.com/ovidius72/agent-planner#readme) for the full command reference.
@@ -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;AAozBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4wH9D"}
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - inject context into prompts
10
10
  */
11
11
  import { Type } from "typebox";
12
- import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask } from "@agent-plan/core";
12
+ import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, packageVersionFromModule, resolvedPackageVersion } from "@agent-plan/core";
13
13
  import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
14
14
  import { join, dirname } from "node:path";
15
15
  import { homedir } from "node:os";
@@ -21,6 +21,9 @@ import { serve } from "@agent-plan/server/serve";
21
21
  // ─── State (module-level) ───────────────────────────────────────────────
22
22
  const PI_CONFIG_DIR_NAME = ".pi";
23
23
  const PLAN_DIR_NAME = ".planner";
24
+ const PI_ADAPTER_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/pi-adapter");
25
+ const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
26
+ const SERVER_PACKAGE = resolvedPackageVersion("@agent-plan/server", import.meta.url);
24
27
  let capturedPi = null;
25
28
  let store = null;
26
29
  let server = null;
@@ -104,6 +107,7 @@ const healedStatusRoots = new Set();
104
107
  const PLANNER_COMMAND_COMPLETIONS = [
105
108
  { value: "init", label: "init", description: "Initialize planner in this project" },
106
109
  { value: "show", label: "show", description: "Show planner overview" },
110
+ { value: "version", label: "version", description: "Show loaded Agent Plan package versions" },
107
111
  { value: "repair", label: "repair", description: "Repair planner integrity" },
108
112
  { value: "cleanup-orphans", label: "cleanup-orphans", description: "List and remove orphan phase files" },
109
113
  { value: "project discuss", label: "project discuss", description: "Run project discovery" },
@@ -1041,6 +1045,7 @@ export default function planPiExtension(pi) {
1041
1045
  const PLANNER_MENU_ACTIONS = [
1042
1046
  "init",
1043
1047
  "show",
1048
+ "version",
1044
1049
  "repair",
1045
1050
  "cleanup-orphans",
1046
1051
  "project discuss",
@@ -1076,7 +1081,7 @@ export default function planPiExtension(pi) {
1076
1081
  "load",
1077
1082
  "stop",
1078
1083
  ];
1079
- const SUB_HELP = "Available: init, show, repair, cleanup-orphans, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
1084
+ const SUB_HELP = "Available: init, show, version, repair, cleanup-orphans, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
1080
1085
  "Try: /planner <TAB> | /planner feature list | /planner task start | /planner cleanup-orphans\n" +
1081
1086
  "Handoff actions: /planner handoff list | show P00x | write P00x | clear P00x | prepare (prepare proposes a target and asks for confirmation)";
1082
1087
  if (!a) {
@@ -1088,6 +1093,15 @@ export default function planPiExtension(pi) {
1088
1093
  await runPlanner(action, ctx);
1089
1094
  return;
1090
1095
  }
1096
+ if (a === "version") {
1097
+ ctx.ui.notify([
1098
+ "Agent Plan runtime versions",
1099
+ `${PI_ADAPTER_PACKAGE.name}: ${PI_ADAPTER_PACKAGE.version}`,
1100
+ `${CORE_PACKAGE.name}: ${CORE_PACKAGE.version}`,
1101
+ `${SERVER_PACKAGE.name}: ${SERVER_PACKAGE.version}`,
1102
+ ].join("\n"), "info");
1103
+ return;
1104
+ }
1091
1105
  const st = ensureStore(ctx);
1092
1106
  // ── init ──
1093
1107
  if (a === "init") {
@@ -1957,6 +1971,7 @@ export default function planPiExtension(pi) {
1957
1971
  acceptedDecisions: [],
1958
1972
  checklist: [], subtasks: [],
1959
1973
  dependsOn: [],
1974
+ pauseSnapshot: null, pauseHistory: [],
1960
1975
  startedAt: "", completedAt: "",
1961
1976
  createdAt: now, updatedAt: now,
1962
1977
  };
@@ -3877,6 +3892,9 @@ export default function planPiExtension(pi) {
3877
3892
  const priority = await st.nextPriority("task", phaseId);
3878
3893
  const now = nowISO();
3879
3894
  const initialStatus = params.status ?? "planned";
3895
+ if (initialStatus === "paused") {
3896
+ 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: {} };
3897
+ }
3880
3898
  const task = {
3881
3899
  id: taskId, phaseId, number: identity.number, shortId: identity.shortId, priority, shortName,
3882
3900
  title: params.title,
@@ -3892,6 +3910,8 @@ export default function planPiExtension(pi) {
3892
3910
  : [],
3893
3911
  subtasks: [],
3894
3912
  dependsOn: [],
3913
+ pauseSnapshot: null,
3914
+ pauseHistory: [],
3895
3915
  startedAt: initialStatus === "in-progress" || initialStatus === "done" ? now : "",
3896
3916
  completedAt: initialStatus === "done" ? now : "",
3897
3917
  createdAt: now, updatedAt: now,
@@ -4085,7 +4105,12 @@ export default function planPiExtension(pi) {
4085
4105
  if (temporary.task.status !== "planned")
4086
4106
  return { content: [{ type: "text", text: `Temporary task is not startable: ${temporary.task.status}.` }], details: {} };
4087
4107
  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: "" };
4108
+ const record = {
4109
+ id: crypto.randomUUID(), recommendedTaskId: selected.candidate?.task.id ?? resume.task.id,
4110
+ temporaryTaskId: temporary.task.id, resumeTaskId: resume.task.id, reason: params.reason, snapshot: null,
4111
+ requestedBy: "user", approvedBy: "user", state: "approved",
4112
+ createdAt: timestamp, activatedAt: "", resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
4113
+ };
4089
4114
  await st.addWorkDeviation(record);
4090
4115
  await st.writeGenerated();
4091
4116
  const tempRef = `${formatPhaseRef(temporary.phase.number, featureNumberOfPhase(temporary.phase, features.features))}/T${String(temporary.task.number).padStart(3, "0")}`;
@@ -4093,10 +4118,198 @@ export default function planPiExtension(pi) {
4093
4118
  return { content: [{ type: "text", text: `✅ Approved deviation: ${tempRef} temporarily overrides ${resumeRef}. Resume target retained.` }], details: { deviation: record } };
4094
4119
  },
4095
4120
  });
4121
+ pi.registerTool({
4122
+ name: "task_pause",
4123
+ label: "Task Pause",
4124
+ 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.",
4125
+ parameters: Type.Object({
4126
+ taskId: Type.String({ description: "Task ref to pause" }),
4127
+ reason: Type.String({ description: "Why work is being paused" }),
4128
+ what_was_being_done: Type.String({ description: "Concrete work underway at the checkpoint" }),
4129
+ resume_location: Type.String({ description: "Exact file, symbol, command, or location from which to continue" }),
4130
+ how_to_resume: Type.String({ description: "Actionable instructions for resuming" }),
4131
+ paused_by: Type.Optional(Type.String({ description: "Agent/session identifier" })),
4132
+ }),
4133
+ async execute(_id, params, _signal, _onUpdate, ctx) {
4134
+ const st = await requirePlan(ctx);
4135
+ if (!st)
4136
+ return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4137
+ const features = (await st.loadFeatures()).features;
4138
+ const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
4139
+ if (!found)
4140
+ return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
4141
+ if (found.task.status !== "in-progress")
4142
+ return { content: [{ type: "text", text: `Task pause denied: task is ${found.task.status}, not in-progress.` }], details: found.task };
4143
+ const snapshot = {
4144
+ id: crypto.randomUUID(), reason: params.reason.trim(), whatWasBeingDone: params.what_was_being_done.trim(),
4145
+ resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(), relatedTaskId: "",
4146
+ pausedAt: nowISO(), pausedBy: params.paused_by?.trim() ?? "",
4147
+ };
4148
+ const paused = await st.pauseTask(found.phase.id, found.task.id, snapshot);
4149
+ await st.syncTaskStatusRollup(found.phase.id);
4150
+ const activeDeviation = (await st.loadProject()).workDeviations
4151
+ .filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
4152
+ && deviation.temporaryTaskId === found.task.id)
4153
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4154
+ if (activeDeviation)
4155
+ await st.setWorkDeviationState(activeDeviation.id, "resume-required", snapshot.pausedAt);
4156
+ const resume = activeDeviation
4157
+ ? findTaskByRef(await st.loadAllPhases(), features, activeDeviation.resumeTaskId)
4158
+ : undefined;
4159
+ await st.writeGenerated();
4160
+ const reference = `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(paused.number).padStart(3, "0")}`;
4161
+ const resumeRef = resume
4162
+ ? `${formatPhaseRef(resume.phase.number, featureNumberOfPhase(resume.phase, features))}/T${String(resume.task.number).padStart(3, "0")}`
4163
+ : "";
4164
+ return {
4165
+ content: [{ type: "text", text: [
4166
+ `⏸️ Task paused: ${reference} — ${paused.title}`,
4167
+ `Why: ${snapshot.reason}`,
4168
+ `Checkpoint: ${snapshot.whatWasBeingDone}`,
4169
+ `Resume from: ${snapshot.resumeLocation}`,
4170
+ `How to resume: ${snapshot.howToResume}`,
4171
+ resume ? `↩️ RESUME REQUIRED: ${resumeRef} — ${resume.task.title}` : "",
4172
+ ].filter(Boolean).join("\n") }],
4173
+ details: { task: paused, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) },
4174
+ };
4175
+ },
4176
+ });
4177
+ pi.registerTool({
4178
+ name: "task_switch",
4179
+ label: "Task Switch",
4180
+ 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.",
4181
+ parameters: Type.Object({
4182
+ from_task: Type.String({ description: "Currently in-progress task ref" }),
4183
+ to_task: Type.String({ description: "Temporary task ref to start" }),
4184
+ reason: Type.String({ description: "Why this temporary switch is necessary" }),
4185
+ what_was_being_done: Type.String({ description: "Concrete work underway in the source task" }),
4186
+ resume_location: Type.String({ description: "Exact source resume location" }),
4187
+ how_to_resume: Type.String({ description: "Actionable source resume instructions" }),
4188
+ switched_by: Type.Optional(Type.String({ description: "Agent/session identifier" })),
4189
+ }),
4190
+ async execute(_id, params, _signal, _onUpdate, ctx) {
4191
+ const st = await requirePlan(ctx);
4192
+ if (!st)
4193
+ return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4194
+ const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
4195
+ const features = featuresDoc.features;
4196
+ const source = findTaskByRef(phases, features, params.from_task.trim());
4197
+ const target = findTaskByRef(phases, features, params.to_task.trim());
4198
+ if (!source)
4199
+ return { content: [{ type: "text", text: `Task not found: ${params.from_task}` }], details: {} };
4200
+ if (!target)
4201
+ return { content: [{ type: "text", text: `Task not found: ${params.to_task}` }], details: {} };
4202
+ if (source.task.id === target.task.id)
4203
+ return { content: [{ type: "text", text: "Source and temporary task must differ." }], details: {} };
4204
+ if (source.task.status !== "in-progress" && source.task.status !== "paused") {
4205
+ 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: {} };
4206
+ }
4207
+ const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
4208
+ if (!eligibility.eligible)
4209
+ return { content: [{ type: "text", text: `Task switch denied: ${eligibility.reason}` }], details: eligibility };
4210
+ const timestamp = nowISO();
4211
+ const snapshot = {
4212
+ id: crypto.randomUUID(), reason: params.reason.trim(), whatWasBeingDone: params.what_was_being_done.trim(),
4213
+ resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(),
4214
+ relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: params.switched_by?.trim() ?? "",
4215
+ };
4216
+ const selection = recommendNextTask(features, phases, project.workDeviations);
4217
+ const record = {
4218
+ id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
4219
+ temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason: snapshot.reason, snapshot,
4220
+ requestedBy: "agent", approvedBy: params.switched_by?.trim() || "explicit task_switch",
4221
+ state: "active", createdAt: timestamp, activatedAt: timestamp,
4222
+ resumeRequiredAt: "", resolvedAt: "", resumedAt: "",
4223
+ };
4224
+ const sourceWasActive = source.task.status === "in-progress";
4225
+ let sourceCheckpointed = false;
4226
+ let targetStarted = false;
4227
+ try {
4228
+ if (sourceWasActive) {
4229
+ await st.pauseTask(source.phase.id, source.task.id, snapshot);
4230
+ }
4231
+ else {
4232
+ await st.updatePhase(source.phase.id, (phase) => {
4233
+ const task = phase.tasks.find((entry) => entry.id === source.task.id);
4234
+ if (!task)
4235
+ throw new Error(`Paused source task disappeared: ${params.from_task}`);
4236
+ task.pauseSnapshot = snapshot;
4237
+ task.pauseHistory = [...task.pauseHistory, snapshot];
4238
+ task.updatedAt = timestamp;
4239
+ return phase;
4240
+ });
4241
+ }
4242
+ sourceCheckpointed = true;
4243
+ if (target.task.status === "paused") {
4244
+ await st.resumeTask(target.phase.id, target.task.id, timestamp);
4245
+ }
4246
+ else {
4247
+ await st.updatePhase(target.phase.id, (phase) => {
4248
+ const task = phase.tasks.find((entry) => entry.id === target.task.id);
4249
+ if (!task)
4250
+ throw new Error(`Temporary task disappeared: ${params.to_task}`);
4251
+ const previousStatus = task.status;
4252
+ applyTaskLifecycleDates(task, "in-progress", timestamp);
4253
+ task.statusLog = [...task.statusLog, {
4254
+ id: createChecklistItemId(task.id, task.statusLog.length + 1, `${previousStatus}-in-progress`),
4255
+ date: timestamp, fromStatus: previousStatus, toStatus: "in-progress",
4256
+ title: `${previousStatus} → in-progress`, description: `Temporary switch from ${source.task.title}.`,
4257
+ }];
4258
+ task.updatedAt = timestamp;
4259
+ phase.updatedAt = timestamp;
4260
+ return phase;
4261
+ });
4262
+ }
4263
+ targetStarted = true;
4264
+ await st.addWorkDeviation(record);
4265
+ if (target.task.status === "paused") {
4266
+ const resumedDeviation = project.workDeviations
4267
+ .filter((deviation) => deviation.resumeTaskId === target.task.id
4268
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
4269
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4270
+ if (resumedDeviation)
4271
+ await st.setWorkDeviationState(resumedDeviation.id, "resumed", timestamp);
4272
+ }
4273
+ }
4274
+ catch (error) {
4275
+ if (targetStarted) {
4276
+ await st.updatePhase(target.phase.id, (phase) => {
4277
+ phase.tasks = phase.tasks.map((task) => task.id === target.task.id ? target.task : task);
4278
+ return phase;
4279
+ }).catch(() => { });
4280
+ }
4281
+ if (sourceCheckpointed) {
4282
+ if (sourceWasActive)
4283
+ await st.resumeTask(source.phase.id, source.task.id, nowISO()).catch(() => { });
4284
+ else
4285
+ await st.updatePhase(source.phase.id, (phase) => {
4286
+ phase.tasks = phase.tasks.map((task) => task.id === source.task.id ? source.task : task);
4287
+ return phase;
4288
+ }).catch(() => { });
4289
+ }
4290
+ throw error;
4291
+ }
4292
+ await st.syncTaskStatusRollup(source.phase.id);
4293
+ if (target.phase.id !== source.phase.id)
4294
+ await st.syncTaskStatusRollup(target.phase.id);
4295
+ await st.writeGenerated();
4296
+ const sourceRef = `${formatPhaseRef(source.phase.number, featureNumberOfPhase(source.phase, features))}/T${String(source.task.number).padStart(3, "0")}`;
4297
+ const targetRef = `${formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features))}/T${String(target.task.number).padStart(3, "0")}`;
4298
+ return {
4299
+ content: [{ type: "text", text: [
4300
+ `🔀 Task switched: ${sourceRef} → ${targetRef}`,
4301
+ `Paused snapshot: ${snapshot.whatWasBeingDone}`,
4302
+ `Return target: ${sourceRef} from ${snapshot.resumeLocation}`,
4303
+ "Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.",
4304
+ ].join("\n") }],
4305
+ details: { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id },
4306
+ };
4307
+ },
4308
+ });
4096
4309
  pi.registerTool({
4097
4310
  name: "task_start",
4098
4311
  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.",
4312
+ 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
4313
  parameters: Type.Object({
4101
4314
  taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
4102
4315
  }),
@@ -4119,6 +4332,17 @@ export default function planPiExtension(pi) {
4119
4332
  if (!eligibility.eligible)
4120
4333
  return { content: [{ type: "text", text: `Task start denied: ${eligibility.reason}` }], details: eligibility };
4121
4334
  const selection = recommendNextTask(features, phases, project.workDeviations);
4335
+ if (selection.kind === "conflict") {
4336
+ return { content: [{ type: "text", text: `${selection.reason} Pause/reconcile active tasks before starting another task.` }], details: selection };
4337
+ }
4338
+ if (selection.kind === "active" && selection.candidate?.task.id !== task.id) {
4339
+ const activeRef = `${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")}`;
4340
+ 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 };
4341
+ }
4342
+ if (selection.kind === "resume" && selection.candidate?.task.id !== task.id) {
4343
+ const resumeRef = `${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")}`;
4344
+ 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 };
4345
+ }
4122
4346
  // Assemble the mandatory parent context before changing task lifecycle state.
4123
4347
  // The output order is feature + its requirements, then phase + its requirements.
4124
4348
  const parentFeature = found.phase.featureId ? features.find((f) => f.id === found.phase.featureId) : undefined;
@@ -4127,29 +4351,42 @@ export default function planPiExtension(pi) {
4127
4351
  parentFeature ? st.linkedRequirementsForFeature(parentFeature.id) : Promise.resolve([]),
4128
4352
  ]);
4129
4353
  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
- : "";
4354
+ const advisory = selection.candidate && selection.candidate.task.id !== task.id
4355
+ ? `\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.`
4356
+ : "";
4137
4357
  const now = nowISO();
4138
4358
  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)
4359
+ if (task.status === "paused") {
4360
+ startedTask = await st.resumeTask(found.phase.id, found.task.id, now);
4361
+ }
4362
+ else {
4363
+ await st.updatePhase(found.phase.id, (phase) => {
4364
+ const t = phase.tasks.find((x) => x.id === found.task.id);
4365
+ if (!t)
4366
+ return phase;
4367
+ const previousStatus = t.status;
4368
+ applyTaskLifecycleDates(t, "in-progress", now);
4369
+ t.statusLog = [...t.statusLog, {
4370
+ id: createChecklistItemId(t.id, t.statusLog.length + 1, `${previousStatus}-in-progress`),
4371
+ date: now, fromStatus: previousStatus, toStatus: "in-progress",
4372
+ title: `${previousStatus} → in-progress`, description: "",
4373
+ }];
4374
+ t.updatedAt = now;
4375
+ phase.updatedAt = now;
4376
+ startedTask = t;
4142
4377
  return phase;
4143
- applyTaskLifecycleDates(t, "in-progress", now);
4144
- t.updatedAt = now;
4145
- phase.updatedAt = now;
4146
- startedTask = t;
4147
- return phase;
4148
- });
4378
+ });
4379
+ }
4149
4380
  await st.syncTaskStatusRollup(found.phase.id);
4150
4381
  const approvedDeviation = project.workDeviations.find((deviation) => deviation.temporaryTaskId === task.id && deviation.state === "approved");
4151
4382
  if (approvedDeviation)
4152
4383
  await st.setWorkDeviationState(approvedDeviation.id, "active", now);
4384
+ const resumedDeviation = project.workDeviations
4385
+ .filter((deviation) => deviation.resumeTaskId === task.id
4386
+ && (deviation.state === "resume-required" || deviation.state === "resolved"))
4387
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4388
+ if (resumedDeviation)
4389
+ await st.setWorkDeviationState(resumedDeviation.id, "resumed", now);
4153
4390
  await st.writeGenerated();
4154
4391
  if (!startedTask)
4155
4392
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
@@ -4176,6 +4413,8 @@ export default function planPiExtension(pi) {
4176
4413
  const task = found.task;
4177
4414
  if (task.status === "done")
4178
4415
  return { content: [{ type: "text", text: `Task ${task.id} is already done.` }], details: task };
4416
+ if (task.status === "paused")
4417
+ 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
4418
  const unchecked = task.checklist.filter((item) => !item.checked);
4180
4419
  if (unchecked.length > 0 && !params.force) {
4181
4420
  return {
@@ -4189,7 +4428,13 @@ export default function planPiExtension(pi) {
4189
4428
  const t = phase.tasks.find((x) => x.id === found.task.id);
4190
4429
  if (!t)
4191
4430
  return phase;
4431
+ const previousStatus = t.status;
4192
4432
  applyTaskLifecycleDates(t, "done", now);
4433
+ t.statusLog = [...t.statusLog, {
4434
+ id: createChecklistItemId(t.id, t.statusLog.length + 1, `${previousStatus}-done`),
4435
+ date: now, fromStatus: previousStatus, toStatus: "done",
4436
+ title: `${previousStatus} → done`, description: "",
4437
+ }];
4193
4438
  if (params.description_update) {
4194
4439
  const sep = t.description ? "\n\n---\n**Completion summary:**\n" : "**Completion summary:**\n";
4195
4440
  t.description = t.description + sep + params.description_update;
@@ -4204,11 +4449,33 @@ export default function planPiExtension(pi) {
4204
4449
  .filter((deviation) => (deviation.state === "approved" || deviation.state === "active") && deviation.temporaryTaskId === task.id)
4205
4450
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
4206
4451
  if (completedDeviation)
4207
- await st.setWorkDeviationState(completedDeviation.id, "resolved", now);
4452
+ await st.setWorkDeviationState(completedDeviation.id, "resume-required", now);
4208
4453
  await st.writeGenerated();
4209
4454
  if (!completedTask)
4210
4455
  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 };
4456
+ let resumeNotice = "";
4457
+ let resumeRequired;
4458
+ if (completedDeviation) {
4459
+ const resume = findTaskByRef(await st.loadAllPhases(), features, completedDeviation.resumeTaskId);
4460
+ if (resume) {
4461
+ const snapshot = resume.task.pauseSnapshot ?? completedDeviation.snapshot;
4462
+ const resumeRef = `${formatPhaseRef(resume.phase.number, featureNumberOfPhase(resume.phase, features))}/T${String(resume.task.number).padStart(3, "0")}`;
4463
+ resumeNotice = [
4464
+ "",
4465
+ `↩️ RESUME REQUIRED: ${resumeRef} — ${resume.task.title}`,
4466
+ snapshot ? `Paused because: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
4467
+ snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
4468
+ snapshot ? `How to resume: ${snapshot.howToResume}` : "",
4469
+ `Next action: task_start ${resumeRef}`,
4470
+ ].filter(Boolean).join("\n");
4471
+ resumeRequired = { taskId: resume.task.id, phaseId: resume.phase.id, snapshot };
4472
+ }
4473
+ }
4474
+ const completedRef = `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(completedTask.number).padStart(3, "0")}`;
4475
+ return {
4476
+ content: [{ type: "text", text: `✅ Task completed: ${completedRef} — ${completedTask.title} (done)${clearedRef ? ` — phase handoff archived (${clearedRef})` : ""}${resumeNotice}` }],
4477
+ details: resumeRequired ? { task: completedTask, resumeRequired } : completedTask,
4478
+ };
4212
4479
  },
4213
4480
  });
4214
4481
  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.22",
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.22",
32
+ "@agent-plan/server": "0.2.22"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",