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