@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 +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +289 -22
- package/package.json +3 -3
- package/web-ui-dist/assets/index-B97B4wii.js +298 -0
- package/web-ui-dist/assets/index-Bi9ehfxU.css +1 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-CTAfas5X.css +0 -1
- package/web-ui-dist/assets/index-CyrzGuWc.js +0 -293
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.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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 = {
|
|
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)
|
|
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.
|
|
4131
|
-
? `\n\n⚠️ ${selection.
|
|
4132
|
-
:
|
|
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
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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
|
-
|
|
4144
|
-
|
|
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, "
|
|
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
|
-
|
|
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.
|
|
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.
|
|
32
|
-
"@agent-plan/server": "0.2.
|
|
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",
|