@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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +279 -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-rFM0JSW_.js +0 -293
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;AAgzBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,
|
|
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 = {
|
|
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)
|
|
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.
|
|
4127
|
-
? `\n\n⚠️ ${selection.
|
|
4128
|
-
:
|
|
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
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
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
|
-
|
|
4140
|
-
|
|
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, "
|
|
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
|
-
|
|
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.
|
|
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/
|
|
32
|
-
"@agent-plan/
|
|
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",
|