@agent-plan/pi-adapter 0.2.17 → 0.2.19-next.0
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 +271 -100
- package/package.json +3 -3
- package/web-ui-dist/assets/index-DmQC9Ttj.css +1 -0
- package/web-ui-dist/assets/index-O9MpLyc3.js +211 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-Tma57h5W.js +0 -191
- package/web-ui-dist/assets/index-U-dx9Eua.css +0 -1
package/dist/index.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - inject context into prompts
|
|
10
10
|
*/
|
|
11
11
|
import { Type } from "typebox";
|
|
12
|
-
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, withFeatureLock, needsMotivation } from "@agent-plan/core";
|
|
13
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug } from "@agent-plan/core/naming";
|
|
12
|
+
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, withFeatureLock, needsMotivation, findPhaseByRef } from "@agent-plan/core";
|
|
13
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef } from "@agent-plan/core/naming";
|
|
14
14
|
import { join, dirname } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
@@ -101,10 +101,11 @@ const PLANNER_COMMAND_COMPLETIONS = [
|
|
|
101
101
|
{ value: "task delete", label: "task delete", description: "Delete a task" },
|
|
102
102
|
{ value: "task start", label: "task start", description: "Mark a task in-progress" },
|
|
103
103
|
{ value: "task complete", label: "task complete", description: "Mark a task done" },
|
|
104
|
-
{ value: "handoff
|
|
105
|
-
{ value: "handoff
|
|
106
|
-
{ value: "handoff
|
|
107
|
-
{ value: "handoff
|
|
104
|
+
{ value: "handoff list", label: "handoff list", description: "List phases with a handoff" },
|
|
105
|
+
{ value: "handoff prepare", label: "handoff prepare", description: "Tell the agent to write the handoff (legacy file-based)" },
|
|
106
|
+
{ value: "handoff show", label: "handoff show [P00x]", description: "Show a phase handoff (default: in-progress phase)" },
|
|
107
|
+
{ value: "handoff write", label: "handoff write [P00x]", description: "Write a phase handoff (default: in-progress phase)" },
|
|
108
|
+
{ value: "handoff clear", label: "handoff clear [P00x]", description: "Clear a phase handoff (default: in-progress phase)" },
|
|
108
109
|
{ value: "web start", label: "web start", description: "Start the web UI" },
|
|
109
110
|
{ value: "web stop", label: "web stop", description: "Stop the web UI" },
|
|
110
111
|
{ value: "web status", label: "web status", description: "Show web UI status" },
|
|
@@ -157,11 +158,17 @@ function resetState() {
|
|
|
157
158
|
editedThisTurn = false;
|
|
158
159
|
taskCompleteReminderSaidThisTurn = false;
|
|
159
160
|
}
|
|
160
|
-
async function maybeHealStatuses(st) {
|
|
161
|
+
async function maybeHealStatuses(st, ctx) {
|
|
161
162
|
if (healedStatusRoots.has(st.root))
|
|
162
163
|
return;
|
|
163
|
-
await st.syncStatuses();
|
|
164
|
+
const cleared = await st.syncStatuses();
|
|
164
165
|
healedStatusRoots.add(st.root);
|
|
166
|
+
if (cleared.length > 0 && ctx?.ui?.notify) {
|
|
167
|
+
try {
|
|
168
|
+
ctx.ui.notify(`ℹ️ Handoff auto-cleared (phase completed): ${cleared.join(", ")}`, "info");
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
}
|
|
165
172
|
}
|
|
166
173
|
function normalizeLanguagePref(value) {
|
|
167
174
|
return value?.trim() ?? "";
|
|
@@ -221,10 +228,10 @@ function taskLabel(task) {
|
|
|
221
228
|
return `T${formatSequence(task.number)} — ${task.title}`;
|
|
222
229
|
}
|
|
223
230
|
async function buildStartupResumeSummary(st) {
|
|
224
|
-
const [plan, resume,
|
|
231
|
+
const [plan, resume, handoffs] = await Promise.all([
|
|
225
232
|
st.loadAll(),
|
|
226
233
|
st.refreshResume(),
|
|
227
|
-
st.
|
|
234
|
+
st.listHandoffs(),
|
|
228
235
|
]);
|
|
229
236
|
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
230
237
|
const orderedPhases = [
|
|
@@ -281,8 +288,8 @@ async function buildStartupResumeSummary(st) {
|
|
|
281
288
|
: currentPhase
|
|
282
289
|
? `riprendere la fase ${currentPhase.id} — ${phaseLabel(currentPhase)}`
|
|
283
290
|
: "riprendere il lavoro attivo corrente"))
|
|
284
|
-
: (
|
|
285
|
-
? "leggere l'handoff e
|
|
291
|
+
: (handoffs.length > 0
|
|
292
|
+
? "leggere l'handoff di fase più recente (handoff show <ref>) e verificarne ordine e stato rispetto al piano prima di scegliere il prossimo task"
|
|
286
293
|
: "rivedere il piano e scegliere il prossimo task concreto");
|
|
287
294
|
const chatLanguage = (plan.project.chatLanguage || "").toLowerCase();
|
|
288
295
|
const italian = chatLanguage.includes("ital");
|
|
@@ -296,8 +303,8 @@ async function buildStartupResumeSummary(st) {
|
|
|
296
303
|
currentFeature ? `- Focus feature: ${currentFeature.id} — ${featureLabel(currentFeature)} (${currentFeature.status}).` : "- Focus feature: nessuna feature attiva chiara.",
|
|
297
304
|
currentPhase ? `- Focus fase: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Focus fase: nessuna fase attiva chiara.",
|
|
298
305
|
currentTask ? `- Focus task: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Focus task: nessun task attivo in questo momento.",
|
|
299
|
-
|
|
300
|
-
!hasActiveWork &&
|
|
306
|
+
handoffs.length > 0 ? `- Handoff di fase pendenti (${handoffs.length}):\n${handoffs.map((h, i) => ` [${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`).join("\n")}` : "- Nessun handoff di fase pendente.",
|
|
307
|
+
!hasActiveWork && handoffs.length > 0 ? "- Nota: l'handoff di fase è un suggerimento della sessione precedente; con 0 task/fasi attivi va validato contro stato e dipendenze correnti prima di riprendere un task specifico." : "",
|
|
301
308
|
`- Prossima attività consigliata: ${nextActivity}.`,
|
|
302
309
|
"",
|
|
303
310
|
"Vuoi che riprendiamo da qui?",
|
|
@@ -311,8 +318,8 @@ async function buildStartupResumeSummary(st) {
|
|
|
311
318
|
currentFeature ? `- Feature focus: ${currentFeature.id} — ${featureLabel(currentFeature)} (${currentFeature.status}).` : "- Feature focus: no clear active feature.",
|
|
312
319
|
currentPhase ? `- Phase focus: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Phase focus: no clear active phase.",
|
|
313
320
|
currentTask ? `- Task focus: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Task focus: no active task right now.",
|
|
314
|
-
|
|
315
|
-
!hasActiveWork &&
|
|
321
|
+
handoffs.length > 0 ? `- Pending phase handoffs (${handoffs.length}):\n${handoffs.map((h, i) => ` [${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`).join("\n")}` : "- No pending phase handoffs.",
|
|
322
|
+
!hasActiveWork && handoffs.length > 0 ? "- Note: the phase handoff is only a previous-session hint; with 0 active tasks/phases it must be validated against current planner state and dependencies before resuming a specific task." : "",
|
|
316
323
|
`- Recommended next activity: ${nextActivity}.`,
|
|
317
324
|
"",
|
|
318
325
|
"Do you want to resume from here?",
|
|
@@ -321,13 +328,12 @@ async function buildStartupResumeSummary(st) {
|
|
|
321
328
|
].filter(Boolean).join("\n");
|
|
322
329
|
}
|
|
323
330
|
async function buildHandoffMarkdown(st, reason, overrides) {
|
|
324
|
-
const [plan, resume, activity
|
|
331
|
+
const [plan, resume, activity] = await Promise.all([
|
|
325
332
|
st.loadAll(),
|
|
326
333
|
st.refreshResume(),
|
|
327
334
|
st.loadActivityLog(),
|
|
328
|
-
st.loadHandoff(),
|
|
329
335
|
]);
|
|
330
|
-
const createdAt =
|
|
336
|
+
const createdAt = nowISO();
|
|
331
337
|
const updatedAt = nowISO();
|
|
332
338
|
const allTasks = plan.phases.flatMap((phase) => phase.tasks.map((task) => ({ phase, task })));
|
|
333
339
|
const nonTerminalTasks = allTasks.filter(({ task }) => task.status !== "done" && task.status !== "canceled");
|
|
@@ -412,7 +418,7 @@ async function buildHandoffMarkdown(st, reason, overrides) {
|
|
|
412
418
|
: currentPhase
|
|
413
419
|
? `1. Open phase ${currentPhase.id} (${currentPhase.title}).`
|
|
414
420
|
: "1. Open the planner dashboard and inspect the latest feature/phase state.",
|
|
415
|
-
"2.
|
|
421
|
+
"2. Run handoff show <ref> (or /planner handoff show <ref>) to read the phase handoff and compare it with the latest planner data.",
|
|
416
422
|
currentTask && currentTask.status !== "in-progress"
|
|
417
423
|
? `3. Before implementation work, run /planner task start ${currentTask.id} (or call task_start).`
|
|
418
424
|
: "3. Confirm whether the current task is already in-progress before doing implementation work.",
|
|
@@ -420,11 +426,10 @@ async function buildHandoffMarkdown(st, reason, overrides) {
|
|
|
420
426
|
].join("\n");
|
|
421
427
|
const filesTouched = [
|
|
422
428
|
".planner/project.json",
|
|
423
|
-
".planner/features
|
|
429
|
+
".planner/features/",
|
|
424
430
|
currentPhase ? `.planner/phases/${currentPhase.id}.json` : "",
|
|
425
431
|
latestPhaseUpdate && latestPhaseUpdate.id !== currentPhase?.id ? `.planner/phases/${latestPhaseUpdate.id}.json` : "",
|
|
426
432
|
".planner/resume.json",
|
|
427
|
-
".planner/HANDOFF.md",
|
|
428
433
|
".planner/generated/PLAN.md",
|
|
429
434
|
].filter(Boolean);
|
|
430
435
|
const recentActivityLines = recentActivity.length > 0
|
|
@@ -488,12 +493,19 @@ async function buildHandoffMarkdown(st, reason, overrides) {
|
|
|
488
493
|
...recentActivityLines,
|
|
489
494
|
"",
|
|
490
495
|
"## Reminder",
|
|
491
|
-
"- When work is fully resumed and this handoff is no longer needed,
|
|
496
|
+
"- When work is fully resumed and this handoff is no longer needed, clear the phase handoff with handoff clear <ref> (or /planner handoff clear).",
|
|
492
497
|
].join("\n");
|
|
493
498
|
}
|
|
494
499
|
async function writeProjectHandoff(st, reason, overrides) {
|
|
500
|
+
// Entity-scoped: write the auto-handoff on the current in-progress phase
|
|
501
|
+
// (not the deprecated .planner/HANDOFF.md file). If no in-progress phase,
|
|
502
|
+
// skip silently — the resume flow falls back to resume.json.
|
|
503
|
+
const phases = await st.loadAllPhases();
|
|
504
|
+
const phase = phases.find((p) => p.status === "in-progress") ?? null;
|
|
505
|
+
if (!phase)
|
|
506
|
+
return;
|
|
495
507
|
const markdown = await buildHandoffMarkdown(st, reason, overrides);
|
|
496
|
-
await st.
|
|
508
|
+
await st.setPhaseHandoff(phase.id, markdown);
|
|
497
509
|
}
|
|
498
510
|
function compactShortcut(spec) {
|
|
499
511
|
return {
|
|
@@ -802,11 +814,11 @@ export default function planPiExtension(pi) {
|
|
|
802
814
|
ctx.ui.notify("Planner detected in this project. Run '/planner load' to enable the planner and start the web UI (LAN).", "info");
|
|
803
815
|
}
|
|
804
816
|
plannerSessionEnabled = enablePlanner;
|
|
805
|
-
// Proactive
|
|
817
|
+
// Proactive review hint: surface pending phase handoffs (entity-scoped).
|
|
806
818
|
if (plannerSessionEnabled) {
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
809
|
-
ctx.ui.notify(
|
|
819
|
+
const handoffs = await st.listHandoffs().catch(() => []);
|
|
820
|
+
if (handoffs.length > 0) {
|
|
821
|
+
ctx.ui.notify(`ℹ️ ${handoffs.length} phase handoff(s) pending — review with /planner handoff list.`, "info");
|
|
810
822
|
}
|
|
811
823
|
}
|
|
812
824
|
// If the user declined enablement, skip EVERYTHING (no web prompt, no migration, no summary).
|
|
@@ -929,7 +941,7 @@ export default function planPiExtension(pi) {
|
|
|
929
941
|
const st = loadStore(ctx);
|
|
930
942
|
if (!(await st.exists().catch(() => false)))
|
|
931
943
|
return;
|
|
932
|
-
await maybeHealStatuses(st).catch(() => { });
|
|
944
|
+
await maybeHealStatuses(st, ctx).catch(() => { });
|
|
933
945
|
const guard = await getPlannerExecutionGuard(st).catch(() => null);
|
|
934
946
|
if (!guard || guard.totalTasks === 0)
|
|
935
947
|
return; // nothing to enforce yet
|
|
@@ -1018,7 +1030,7 @@ export default function planPiExtension(pi) {
|
|
|
1018
1030
|
];
|
|
1019
1031
|
const SUB_HELP = "Available: init, show, repair, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
|
|
1020
1032
|
"Try: /planner <TAB> | /planner feature list | /planner task start | /planner export-full\n" +
|
|
1021
|
-
"Handoff actions: /planner handoff
|
|
1033
|
+
"Handoff actions: /planner handoff list | show [P00x] | write [P00x] | clear [P00x] | prepare";
|
|
1022
1034
|
if (!a) {
|
|
1023
1035
|
const action = await ctx.ui.select("Planner action", PLANNER_MENU_ACTIONS);
|
|
1024
1036
|
if (!action) {
|
|
@@ -1403,9 +1415,13 @@ export default function planPiExtension(pi) {
|
|
|
1403
1415
|
}
|
|
1404
1416
|
const now = nowISO();
|
|
1405
1417
|
const featureNumber = (await st.loadFeatures()).features.length + 1;
|
|
1418
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
1419
|
+
const priority = await st.nextPriority("feature");
|
|
1406
1420
|
const feature = {
|
|
1407
1421
|
id: createFeatureId(),
|
|
1408
1422
|
number: featureNumber,
|
|
1423
|
+
shortId,
|
|
1424
|
+
priority,
|
|
1409
1425
|
name: nameInput.trim(),
|
|
1410
1426
|
description: description?.trim() ?? "",
|
|
1411
1427
|
status: status,
|
|
@@ -1588,11 +1604,14 @@ export default function planPiExtension(pi) {
|
|
|
1588
1604
|
const number = featurePhases.reduce((max, p) => Math.max(max, p.number), 0) + 1;
|
|
1589
1605
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1590
1606
|
const id = createPhaseId();
|
|
1607
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
1608
|
+
const priority = await st.nextPriority("phase", feature.id);
|
|
1591
1609
|
phase = {
|
|
1592
|
-
id, number, slug, title, featureId: feature.id, status: "draft", discussedAt: "", contextReady: false, contextReadyReason: "", summary: "", description: "", notes: "",
|
|
1610
|
+
id, number, shortId, priority, slug, title, featureId: feature.id, status: "draft", discussedAt: "", contextReady: false, contextReadyReason: "", summary: "", description: "", notes: "",
|
|
1593
1611
|
goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
|
|
1594
1612
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
1595
1613
|
createdAt: nowISO(), updatedAt: nowISO(),
|
|
1614
|
+
handoff: "", handoffUpdatedAt: "",
|
|
1596
1615
|
};
|
|
1597
1616
|
await st.savePhase(phase);
|
|
1598
1617
|
await st.updateFeatures((doc) => {
|
|
@@ -1825,9 +1844,11 @@ export default function planPiExtension(pi) {
|
|
|
1825
1844
|
const shortName = clampSlug(title, 30, `task-${Date.now().toString(36)}`);
|
|
1826
1845
|
const taskNum = phase.tasks.length + 1;
|
|
1827
1846
|
const taskId = createTaskId();
|
|
1847
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
1848
|
+
const priority = await st.nextPriority("task", phase.id);
|
|
1828
1849
|
const now = nowISO();
|
|
1829
1850
|
const task = {
|
|
1830
|
-
id: taskId, phaseId: phase.id, number: taskNum, shortName,
|
|
1851
|
+
id: taskId, phaseId: phase.id, number: taskNum, shortId, priority, shortName,
|
|
1831
1852
|
title: title.trim(), status: "planned",
|
|
1832
1853
|
description: "",
|
|
1833
1854
|
notes: "",
|
|
@@ -1994,8 +2015,8 @@ export default function planPiExtension(pi) {
|
|
|
1994
2015
|
ctx.ui.notify(`Invalid status. Use: ${valid.join(", ")}`, "error");
|
|
1995
2016
|
return;
|
|
1996
2017
|
}
|
|
1997
|
-
if (normalizedStatus === "in-progress" &&
|
|
1998
|
-
ctx.ui.notify("ℹ️
|
|
2018
|
+
if (normalizedStatus === "in-progress" && (await st.listHandoffs()).length > 0) {
|
|
2019
|
+
ctx.ui.notify("ℹ️ One or more phases have a pending handoff — if relevant to this task, read it with /planner handoff show <ref>, then clear with /planner handoff clear. Proceeding with the status change.", "info");
|
|
1999
2020
|
}
|
|
2000
2021
|
applyTaskLifecycleDates(task, normalizedStatus, now);
|
|
2001
2022
|
}
|
|
@@ -2040,8 +2061,8 @@ export default function planPiExtension(pi) {
|
|
|
2040
2061
|
// block task_start — the handoff is a captured-context artifact, not a
|
|
2041
2062
|
// lock (a small modification after writing a handoff should start without
|
|
2042
2063
|
// forcing deletion, which would lose context).
|
|
2043
|
-
if (
|
|
2044
|
-
ctx.ui.notify("ℹ️
|
|
2064
|
+
if ((await st.listHandoffs()).length > 0) {
|
|
2065
|
+
ctx.ui.notify("ℹ️ One or more phases have a pending handoff — if relevant to the task you're starting, read it with /planner handoff show <ref>, then clear with /planner handoff clear. Proceeding with task start.", "info");
|
|
2045
2066
|
}
|
|
2046
2067
|
const now = nowISO();
|
|
2047
2068
|
// Record status change in the incremental statusLog.
|
|
@@ -2129,60 +2150,80 @@ export default function planPiExtension(pi) {
|
|
|
2129
2150
|
// ── handoff ──
|
|
2130
2151
|
if (a === "handoff") {
|
|
2131
2152
|
const action = b || "show";
|
|
2153
|
+
const phaseRef = parts[2]?.trim();
|
|
2132
2154
|
if (!(await st.exists())) {
|
|
2133
2155
|
ctx.ui.notify("No .planner/ found.", "warning");
|
|
2134
2156
|
return;
|
|
2135
2157
|
}
|
|
2158
|
+
if (action === "list") {
|
|
2159
|
+
const list = await st.listHandoffs();
|
|
2160
|
+
if (list.length === 0) {
|
|
2161
|
+
ctx.ui.notify("No phase handoffs set.", "info");
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
ctx.ui.notify(list.map((e) => `• ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt})`).join("\n"), "info");
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2136
2167
|
if (action === "show") {
|
|
2137
|
-
const
|
|
2138
|
-
if (!
|
|
2139
|
-
ctx.ui.notify(
|
|
2168
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
2169
|
+
if (!r.ok) {
|
|
2170
|
+
ctx.ui.notify(r.error, "warning");
|
|
2140
2171
|
return;
|
|
2141
2172
|
}
|
|
2142
|
-
|
|
2173
|
+
const content = await st.getPhaseHandoff(r.phase.id);
|
|
2174
|
+
ctx.ui.notify(content.trim() ? `Handoff ${r.compositeRef}:\n${content}` : `No handoff set on ${r.compositeRef}.`, "info");
|
|
2143
2175
|
return;
|
|
2144
2176
|
}
|
|
2145
|
-
if (action === "
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
const handoff = await st.loadHandoff();
|
|
2152
|
-
await capturedPi.sendUserMessage("Prepare the canonical session handoff now and write it to `.planner/HANDOFF.md` using the `plan_write_handoff` tool.\n\n" +
|
|
2153
|
-
"The handoff MUST be a structured markdown document containing at minimum:\n" +
|
|
2154
|
-
"- `Created at:` and `Updated at:` lines (ISO timestamps)\n" +
|
|
2155
|
-
"- `Reason:` why this handoff is being written\n" +
|
|
2156
|
-
"- `## Current focus`: the current feature (id + name + status), phase (id + title + status) and task (id + title + status). Derive them from the plan data.\n" +
|
|
2157
|
-
"- `## What was being done`: a concrete 3–10 line narrative of the work in progress, based on our conversation and the current task notes/description.\n" +
|
|
2158
|
-
"- `## How to resume`: explicit, ordered steps to pick the work back up (next command / tool / file to open / build to run).\n" +
|
|
2159
|
-
"- `## Files touched`: explicit file paths modified during this session.\n" +
|
|
2160
|
-
"- `## Blockers`: any open blockers, or `None recorded`.\n" +
|
|
2161
|
-
"- `## Next steps`: the immediate next steps.\n" +
|
|
2162
|
-
"- `## Recent decisions`: accepted decisions made this session, if any.\n" +
|
|
2163
|
-
"- `## Reminder`: note that when the work is fully resumed, the agent must delete `.planner/HANDOFF.md` using `plan_delete_handoff`.\n\n" +
|
|
2164
|
-
"Do NOT write the handoff to `.pi/`. The canonical path is `.planner/HANDOFF.md`. " +
|
|
2165
|
-
(handoff?.content ? "A previous handoff exists; refresh its `Updated at` and merge/replace stale sections. " : "") +
|
|
2166
|
-
"Once written, confirm to me with a one-line summary.");
|
|
2167
|
-
ctx.ui.notify("Instructing the agent to prepare .planner/HANDOFF.md…", "info");
|
|
2177
|
+
if (action === "write") {
|
|
2178
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
2179
|
+
if (!r.ok) {
|
|
2180
|
+
ctx.ui.notify(r.error, "warning");
|
|
2168
2181
|
return;
|
|
2169
2182
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
});
|
|
2177
|
-
|
|
2183
|
+
const text = await ctx.ui.input(`Handoff text for ${r.compositeRef}`);
|
|
2184
|
+
if (!text?.trim()) {
|
|
2185
|
+
ctx.ui.notify("No text provided; nothing written.", "info");
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
await st.setPhaseHandoff(r.phase.id, text);
|
|
2189
|
+
ctx.ui.notify(`Wrote handoff on ${r.compositeRef}`, "info");
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (action === "prepare") {
|
|
2193
|
+
if (!capturedPi) {
|
|
2194
|
+
ctx.ui.notify("Agent bridge unavailable; use /planner handoff write for an auto-generated handoff.", "warning");
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
const handoffs = await st.listHandoffs();
|
|
2198
|
+
await capturedPi.sendUserMessage("Prepare the canonical session handoff now and write it on the current in-progress phase using the `handoff_write` tool (entity-scoped, stored on phase.handoff — .planner/HANDOFF.md is deprecated).\n\n" +
|
|
2199
|
+
"The handoff MUST be a structured markdown document containing at minimum:\n" +
|
|
2200
|
+
"- `Created at:` and `Updated at:` lines (ISO timestamps)\n" +
|
|
2201
|
+
"- `Reason:` why this handoff is being written\n" +
|
|
2202
|
+
"- `## Current focus`: the current feature (id + name + status), phase (id + title + status) and task (id + title + status). Derive them from the plan data.\n" +
|
|
2203
|
+
"- `## What was being done`: a concrete 3–10 line narrative of the work in progress, based on our conversation and the current task notes/description.\n" +
|
|
2204
|
+
"- `## How to resume`: explicit, ordered steps to pick the work back up (next command / tool / file to open / build to run).\n" +
|
|
2205
|
+
"- `## Files touched`: explicit file paths modified during this session.\n" +
|
|
2206
|
+
"- `## Blockers`: any open blockers, or `None recorded`.\n" +
|
|
2207
|
+
"- `## Next steps`: the immediate next steps.\n" +
|
|
2208
|
+
"- `## Recent decisions`: accepted decisions made this session, if any.\n" +
|
|
2209
|
+
"- `## Reminder`: note that when the work is fully resumed, the agent should clear the phase handoff using `handoff_clear`.\n\n" +
|
|
2210
|
+
"The handoff is stored on the phase.handoff field of the current in-progress phase (entity-scoped). " +
|
|
2211
|
+
(handoffs.length > 0 ? "A previous phase handoff exists; refresh its `Updated at` and merge/replace stale sections. " : "") +
|
|
2212
|
+
"Once written, confirm to me with a one-line summary.");
|
|
2213
|
+
ctx.ui.notify("Instructing the agent to prepare the phase handoff…", "info");
|
|
2178
2214
|
return;
|
|
2179
2215
|
}
|
|
2180
2216
|
if (action === "clear" || action === "delete") {
|
|
2181
|
-
await st
|
|
2182
|
-
|
|
2217
|
+
const r = await resolvePhaseForHandoff(st, phaseRef);
|
|
2218
|
+
if (!r.ok) {
|
|
2219
|
+
ctx.ui.notify(r.error, "warning");
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
await st.clearPhaseHandoff(r.phase.id);
|
|
2223
|
+
ctx.ui.notify(`Cleared handoff on ${r.compositeRef}`, "info");
|
|
2183
2224
|
return;
|
|
2184
2225
|
}
|
|
2185
|
-
ctx.ui.notify("handoff actions:
|
|
2226
|
+
ctx.ui.notify("handoff actions: list | show [phaseRef] | write [phaseRef] | clear [phaseRef] | prepare\nUse: /planner handoff list | show | write | clear | prepare", "info");
|
|
2186
2227
|
return;
|
|
2187
2228
|
}
|
|
2188
2229
|
// ── web ──
|
|
@@ -2338,7 +2379,7 @@ export default function planPiExtension(pi) {
|
|
|
2338
2379
|
const st = loadStore(ctx);
|
|
2339
2380
|
if (!(await st.exists()))
|
|
2340
2381
|
return null;
|
|
2341
|
-
await maybeHealStatuses(st);
|
|
2382
|
+
await maybeHealStatuses(st, ctx);
|
|
2342
2383
|
return st;
|
|
2343
2384
|
}
|
|
2344
2385
|
// ── plan lifecycle ──────────────────────────────────────────────────
|
|
@@ -2631,22 +2672,23 @@ export default function planPiExtension(pi) {
|
|
|
2631
2672
|
pi.registerTool({
|
|
2632
2673
|
name: "plan_get_handoff",
|
|
2633
2674
|
label: "Plan Get Handoff",
|
|
2634
|
-
description: "
|
|
2675
|
+
description: "DEPRECATED — redirects to the entity-scoped handoff (phase.handoff). Lists current phase handoffs. Prefer handoff_list / handoff_show.",
|
|
2635
2676
|
parameters: Type.Object({}),
|
|
2636
2677
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
2637
2678
|
const st = await requirePlan(ctx);
|
|
2638
2679
|
if (!st)
|
|
2639
2680
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2640
|
-
const
|
|
2641
|
-
if (
|
|
2642
|
-
return { content: [{ type: "text", text: "
|
|
2643
|
-
|
|
2681
|
+
const list = await st.listHandoffs();
|
|
2682
|
+
if (list.length === 0)
|
|
2683
|
+
return { content: [{ type: "text", text: "⚠️ plan_get_handoff is deprecated — no phase handoffs set. Use handoff_list / handoff_show (entity-scoped, phase.handoff field)." }], details: { deprecated: true, count: 0 } };
|
|
2684
|
+
const lines = list.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt})`);
|
|
2685
|
+
return { content: [{ type: "text", text: `⚠️ plan_get_handoff is deprecated — use handoff_list / handoff_show. Phase handoffs (${list.length}):\n${lines.join("\n")}` }], details: { deprecated: true, count: list.length, handoffs: list } };
|
|
2644
2686
|
},
|
|
2645
2687
|
});
|
|
2646
2688
|
pi.registerTool({
|
|
2647
2689
|
name: "plan_write_handoff",
|
|
2648
2690
|
label: "Plan Write Handoff",
|
|
2649
|
-
description: "
|
|
2691
|
+
description: "DEPRECATED — writes to the entity-scoped phase.handoff (not .planner/HANDOFF.md). Prefer handoff_write. Write or refresh the canonical phase handoff. Capture full design context, not just operational resume steps: pass extraSections with locked decisions, architecture (with file:line refs), mode/state flows, plugin/API contracts, data mappings, files touched, and known gaps so a resuming agent doesn't re-discover decisions already made.",
|
|
2650
2692
|
parameters: Type.Object({
|
|
2651
2693
|
reason: Type.Optional(Type.String({ description: "Why the handoff is being written" })),
|
|
2652
2694
|
whatWasBeingDone: Type.Optional(Type.String({ description: "Optional override for the current work summary" })),
|
|
@@ -2660,7 +2702,7 @@ export default function planPiExtension(pi) {
|
|
|
2660
2702
|
const st = await requirePlan(ctx);
|
|
2661
2703
|
if (!st)
|
|
2662
2704
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2663
|
-
await
|
|
2705
|
+
const markdown = await buildHandoffMarkdown(st, params.reason?.trim() || "manual tool handoff", {
|
|
2664
2706
|
...(params.whatWasBeingDone?.trim() ? { whatWasBeingDone: params.whatWasBeingDone.trim() } : {}),
|
|
2665
2707
|
...(params.howToResume?.trim() ? { howToResume: params.howToResume.trim() } : {}),
|
|
2666
2708
|
...(params.extraSections && params.extraSections.length > 0
|
|
@@ -2671,21 +2713,125 @@ export default function planPiExtension(pi) {
|
|
|
2671
2713
|
}
|
|
2672
2714
|
: {}),
|
|
2673
2715
|
});
|
|
2674
|
-
const
|
|
2675
|
-
|
|
2716
|
+
const r = await resolvePhaseForHandoff(st, undefined);
|
|
2717
|
+
if (!r.ok)
|
|
2718
|
+
return { content: [{ type: "text", text: `⚠️ plan_write_handoff is deprecated (writes to phase.handoff, not .planner/HANDOFF.md). ${r.error}` }], details: { deprecated: true, error: r.error } };
|
|
2719
|
+
await st.setPhaseHandoff(r.phase.id, markdown);
|
|
2720
|
+
return { content: [{ type: "text", text: `⚠️ plan_write_handoff is deprecated — wrote handoff on ${r.compositeRef} (phase.handoff field) instead of .planner/HANDOFF.md. Prefer the handoff_write tool.` }], details: { deprecated: true, phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2676
2721
|
},
|
|
2677
2722
|
});
|
|
2678
2723
|
pi.registerTool({
|
|
2679
2724
|
name: "plan_delete_handoff",
|
|
2680
2725
|
label: "Plan Delete Handoff",
|
|
2681
|
-
description: "
|
|
2726
|
+
description: "DEPRECATED — clears the entity-scoped handoff of the current in-progress phase. Prefer handoff_clear.",
|
|
2727
|
+
parameters: Type.Object({}),
|
|
2728
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
2729
|
+
const st = await requirePlan(ctx);
|
|
2730
|
+
if (!st)
|
|
2731
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2732
|
+
const r = await resolvePhaseForHandoff(st, undefined);
|
|
2733
|
+
if (!r.ok)
|
|
2734
|
+
return { content: [{ type: "text", text: `⚠️ plan_delete_handoff is deprecated — use handoff_clear. ${r.error}` }], details: { deprecated: true, error: r.error } };
|
|
2735
|
+
await st.clearPhaseHandoff(r.phase.id);
|
|
2736
|
+
return { content: [{ type: "text", text: `⚠️ plan_delete_handoff is deprecated — cleared handoff on ${r.compositeRef} (phase.handoff). Prefer handoff_clear.` }], details: { deprecated: true, phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2737
|
+
},
|
|
2738
|
+
});
|
|
2739
|
+
// Resolve a phase for entity-scoped handoff tools. ref = P00x | P00x(F00x) |
|
|
2740
|
+
// UUID | title; omitted -> current in-progress phase (phase.status, or the
|
|
2741
|
+
// phase containing an in-progress task). Returns the phase + its composite ref.
|
|
2742
|
+
async function resolvePhaseForHandoff(st, ref) {
|
|
2743
|
+
const phases = await st.loadAllPhases();
|
|
2744
|
+
const features = (await st.loadFeatures()).features;
|
|
2745
|
+
let phase;
|
|
2746
|
+
if (ref && ref.trim()) {
|
|
2747
|
+
phase = findPhaseByRef(phases, features, ref.trim());
|
|
2748
|
+
}
|
|
2749
|
+
else {
|
|
2750
|
+
phase = phases.find((p) => p.status === "in-progress")
|
|
2751
|
+
?? phases.find((p) => p.tasks.some((t) => t.status === "in-progress"));
|
|
2752
|
+
}
|
|
2753
|
+
if (!phase) {
|
|
2754
|
+
return {
|
|
2755
|
+
ok: false,
|
|
2756
|
+
error: ref && ref.trim()
|
|
2757
|
+
? `Phase not found: "${ref.trim()}". Use P00x, P00x(F00x), UUID, or title.`
|
|
2758
|
+
: "No in-progress phase. Specify a phaseRef (P00x or P00x(F00x)).",
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
|
|
2762
|
+
return { ok: true, phase, compositeRef: formatPhaseRef(phase.number, feat?.number) };
|
|
2763
|
+
}
|
|
2764
|
+
pi.registerTool({
|
|
2765
|
+
name: "handoff_list",
|
|
2766
|
+
label: "Handoff List",
|
|
2767
|
+
description: "List all phases with a non-empty entity-scoped handoff (phase.handoff field). Returns composite refs (P00x / P00x(F00x)) with the first line and last-updated time.",
|
|
2682
2768
|
parameters: Type.Object({}),
|
|
2683
2769
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
2684
2770
|
const st = await requirePlan(ctx);
|
|
2685
2771
|
if (!st)
|
|
2686
2772
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2687
|
-
await st.
|
|
2688
|
-
|
|
2773
|
+
const list = await st.listHandoffs();
|
|
2774
|
+
if (list.length === 0)
|
|
2775
|
+
return { content: [{ type: "text", text: "No phase handoffs set." }], details: { count: 0 } };
|
|
2776
|
+
const lines = list.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt})`);
|
|
2777
|
+
return { content: [{ type: "text", text: `Phase handoffs (${list.length}):\n${lines.join("\n")}` }], details: { count: list.length, handoffs: list } };
|
|
2778
|
+
},
|
|
2779
|
+
});
|
|
2780
|
+
pi.registerTool({
|
|
2781
|
+
name: "handoff_show",
|
|
2782
|
+
label: "Handoff Show",
|
|
2783
|
+
description: "Read the entity-scoped handoff of a phase. phaseRef accepts P00x, P00x(F00x), UUID, or title. Omit to target the current in-progress phase.",
|
|
2784
|
+
parameters: Type.Object({ phaseRef: Type.Optional(Type.String({ description: "Phase ref: P00x | P00x(F00x) | UUID | title. Default: current in-progress phase." })) }),
|
|
2785
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2786
|
+
const st = await requirePlan(ctx);
|
|
2787
|
+
if (!st)
|
|
2788
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2789
|
+
const r = await resolvePhaseForHandoff(st, params.phaseRef);
|
|
2790
|
+
if (!r.ok)
|
|
2791
|
+
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
2792
|
+
const content = await st.getPhaseHandoff(r.phase.id);
|
|
2793
|
+
if (!content.trim())
|
|
2794
|
+
return { content: [{ type: "text", text: `No handoff set on ${r.compositeRef}.` }], details: { phaseRef: r.compositeRef, empty: true } };
|
|
2795
|
+
return { content: [{ type: "text", text: `Handoff for ${r.compositeRef}:\n\n${content}` }], details: { phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2796
|
+
},
|
|
2797
|
+
});
|
|
2798
|
+
pi.registerTool({
|
|
2799
|
+
name: "handoff_write",
|
|
2800
|
+
label: "Handoff Write",
|
|
2801
|
+
description: "Write/refresh the entity-scoped handoff on a phase. Pass content (or markdown_content). phaseRef optional (default: current in-progress phase). Captures design context for a resuming agent on that phase.",
|
|
2802
|
+
parameters: Type.Object({
|
|
2803
|
+
phaseRef: Type.Optional(Type.String({ description: "Phase ref. Default: current in-progress phase." })),
|
|
2804
|
+
content: Type.Optional(Type.String({ description: "Handoff text (plain)." })),
|
|
2805
|
+
markdown_content: Type.Optional(Type.String({ description: "Handoff text (markdown). Preferred over content." })),
|
|
2806
|
+
}),
|
|
2807
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2808
|
+
const st = await requirePlan(ctx);
|
|
2809
|
+
if (!st)
|
|
2810
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2811
|
+
const body = (params.markdown_content ?? params.content ?? "").trim();
|
|
2812
|
+
if (!body)
|
|
2813
|
+
return { content: [{ type: "text", text: "❌ Provide the handoff text (content or markdown_content)." }], details: { error: "empty text" } };
|
|
2814
|
+
const r = await resolvePhaseForHandoff(st, params.phaseRef);
|
|
2815
|
+
if (!r.ok)
|
|
2816
|
+
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
2817
|
+
await st.setPhaseHandoff(r.phase.id, body);
|
|
2818
|
+
return { content: [{ type: "text", text: `✅ Wrote handoff on ${r.compositeRef}.` }], details: { phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2819
|
+
},
|
|
2820
|
+
});
|
|
2821
|
+
pi.registerTool({
|
|
2822
|
+
name: "handoff_clear",
|
|
2823
|
+
label: "Handoff Clear",
|
|
2824
|
+
description: "Clear the entity-scoped handoff of a phase (sets handoff to empty, keeps handoffUpdatedAt as an audit trail). phaseRef optional (default: current in-progress phase).",
|
|
2825
|
+
parameters: Type.Object({ phaseRef: Type.Optional(Type.String({ description: "Phase ref. Default: current in-progress phase." })) }),
|
|
2826
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2827
|
+
const st = await requirePlan(ctx);
|
|
2828
|
+
if (!st)
|
|
2829
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2830
|
+
const r = await resolvePhaseForHandoff(st, params.phaseRef);
|
|
2831
|
+
if (!r.ok)
|
|
2832
|
+
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
2833
|
+
await st.clearPhaseHandoff(r.phase.id);
|
|
2834
|
+
return { content: [{ type: "text", text: `✅ Cleared handoff on ${r.compositeRef}. (handoffUpdatedAt preserved as audit)` }], details: { phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2689
2835
|
},
|
|
2690
2836
|
});
|
|
2691
2837
|
pi.registerTool({
|
|
@@ -2771,9 +2917,13 @@ export default function planPiExtension(pi) {
|
|
|
2771
2917
|
const now = nowISO();
|
|
2772
2918
|
const status = params.status ?? "planned";
|
|
2773
2919
|
const currentFeatures = (await st.loadFeatures()).features;
|
|
2920
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
2921
|
+
const priority = await st.nextPriority("feature");
|
|
2774
2922
|
const feature = {
|
|
2775
2923
|
id: createFeatureId(),
|
|
2776
2924
|
number: currentFeatures.length + 1,
|
|
2925
|
+
shortId,
|
|
2926
|
+
priority,
|
|
2777
2927
|
name: params.name,
|
|
2778
2928
|
description: params.description ?? "",
|
|
2779
2929
|
status,
|
|
@@ -2813,6 +2963,7 @@ export default function planPiExtension(pi) {
|
|
|
2813
2963
|
endDate: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })),
|
|
2814
2964
|
workDone: Type.Optional(Type.String({ description: "Notes on work done" })),
|
|
2815
2965
|
workRemaining: Type.Optional(Type.String({ description: "Notes on remaining work" })),
|
|
2966
|
+
priority: Type.Optional(Type.Number({ description: "Display order within the project (lower = higher). Tiebreak by number then createdAt." })),
|
|
2816
2967
|
acceptedDecisions: Type.Optional(Type.Array(Type.Object({
|
|
2817
2968
|
id: Type.String(),
|
|
2818
2969
|
title: Type.String(),
|
|
@@ -2844,6 +2995,8 @@ export default function planPiExtension(pi) {
|
|
|
2844
2995
|
feature.startDate = params.startDate;
|
|
2845
2996
|
if (params.endDate !== undefined)
|
|
2846
2997
|
feature.endDate = params.endDate;
|
|
2998
|
+
if (params.priority !== undefined)
|
|
2999
|
+
feature.priority = params.priority;
|
|
2847
3000
|
if (params.acceptedDecisions !== undefined)
|
|
2848
3001
|
feature.acceptedDecisions = params.acceptedDecisions;
|
|
2849
3002
|
if (params.status !== undefined) {
|
|
@@ -2970,9 +3123,11 @@ export default function planPiExtension(pi) {
|
|
|
2970
3123
|
const featurePhases = phases.filter((p) => p.featureId === params.featureId);
|
|
2971
3124
|
const number = featurePhases.reduce((max, p) => Math.max(max, p.number), 0) + 1;
|
|
2972
3125
|
const id = createPhaseId();
|
|
3126
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
3127
|
+
const priority = await st.nextPriority("phase", params.featureId);
|
|
2973
3128
|
const now = nowISO();
|
|
2974
3129
|
phase = {
|
|
2975
|
-
id, number, slug: normalizeSlug(params.title), title: params.title,
|
|
3130
|
+
id, number, shortId, priority, slug: normalizeSlug(params.title), title: params.title,
|
|
2976
3131
|
featureId: params.featureId,
|
|
2977
3132
|
status: params.status ?? "draft",
|
|
2978
3133
|
discussedAt: "",
|
|
@@ -2982,6 +3137,7 @@ export default function planPiExtension(pi) {
|
|
|
2982
3137
|
goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
|
|
2983
3138
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
2984
3139
|
createdAt: now, updatedAt: now,
|
|
3140
|
+
handoff: "", handoffUpdatedAt: "",
|
|
2985
3141
|
};
|
|
2986
3142
|
await st.savePhase(phase);
|
|
2987
3143
|
// Atomic: serialize concurrent phase_create linking to the same feature.
|
|
@@ -3011,6 +3167,7 @@ export default function planPiExtension(pi) {
|
|
|
3011
3167
|
summary: Type.Optional(Type.String({ description: "New summary" })),
|
|
3012
3168
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
3013
3169
|
featureId: Type.Optional(Type.String({ description: "Link/unlink phase to a feature. Use empty string to unlink." })),
|
|
3170
|
+
priority: Type.Optional(Type.Number({ description: "Display order within the feature (lower = higher)" })),
|
|
3014
3171
|
goals: Type.Optional(Type.Array(Type.String(), { description: "Replace goals list" })),
|
|
3015
3172
|
nonGoals: Type.Optional(Type.Array(Type.String(), { description: "Replace non-goals list" })),
|
|
3016
3173
|
dependencies: Type.Optional(Type.Array(Type.String(), { description: "Replace dependencies list" })),
|
|
@@ -3049,6 +3206,8 @@ export default function planPiExtension(pi) {
|
|
|
3049
3206
|
phase.summary = params.summary;
|
|
3050
3207
|
if (params.description !== undefined)
|
|
3051
3208
|
phase.description = params.description;
|
|
3209
|
+
if (params.priority !== undefined)
|
|
3210
|
+
phase.priority = params.priority;
|
|
3052
3211
|
if (params.goals !== undefined)
|
|
3053
3212
|
phase.goals = params.goals;
|
|
3054
3213
|
if (params.nonGoals !== undefined)
|
|
@@ -3185,11 +3344,13 @@ export default function planPiExtension(pi) {
|
|
|
3185
3344
|
}
|
|
3186
3345
|
const shortName = clampSlug(params.shortName ?? params.title, 30, `task-${Date.now().toString(36)}`); // clamp+strip trailing dash; never empty
|
|
3187
3346
|
const taskId = createTaskId();
|
|
3347
|
+
const shortId = createShortId(await st.assignedShortIds());
|
|
3348
|
+
const priority = await st.nextPriority("task", params.phaseId);
|
|
3188
3349
|
const now = nowISO();
|
|
3189
3350
|
const initialStatus = params.status ?? "planned";
|
|
3190
3351
|
const existingPhase = await st.loadPhase(params.phaseId);
|
|
3191
3352
|
const task = {
|
|
3192
|
-
id: taskId, phaseId: params.phaseId, number: existingPhase.tasks.length + 1, shortName,
|
|
3353
|
+
id: taskId, phaseId: params.phaseId, number: existingPhase.tasks.length + 1, shortId, priority, shortName,
|
|
3193
3354
|
title: params.title,
|
|
3194
3355
|
status: initialStatus,
|
|
3195
3356
|
description: params.description ?? "",
|
|
@@ -3235,6 +3396,7 @@ export default function planPiExtension(pi) {
|
|
|
3235
3396
|
implementationNotes: Type.String(),
|
|
3236
3397
|
acceptedAt: Type.String(),
|
|
3237
3398
|
}), { description: "Replace accepted decisions list" })),
|
|
3399
|
+
priority: Type.Optional(Type.Number({ description: "Display order within the phase (lower = higher)" })),
|
|
3238
3400
|
checklist: Type.Optional(Type.Array(Type.String(), { description: "Replace checklist (plain strings). For interactive toggling use the web UI." })),
|
|
3239
3401
|
}),
|
|
3240
3402
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
@@ -3255,6 +3417,8 @@ export default function planPiExtension(pi) {
|
|
|
3255
3417
|
return phase;
|
|
3256
3418
|
if (params.title !== undefined)
|
|
3257
3419
|
task.title = params.title;
|
|
3420
|
+
if (params.priority !== undefined)
|
|
3421
|
+
task.priority = params.priority;
|
|
3258
3422
|
if (params.description !== undefined)
|
|
3259
3423
|
task.description = params.description;
|
|
3260
3424
|
if (params.notes !== undefined)
|
|
@@ -3510,8 +3674,9 @@ export default function planPiExtension(pi) {
|
|
|
3510
3674
|
// add seconds of latency on every turn if run unconditionally.
|
|
3511
3675
|
if (!plannerHeavyInitDone) {
|
|
3512
3676
|
await migrateToUuids(st);
|
|
3677
|
+
await st.ensureShortIdsAndPriority().catch(() => null);
|
|
3513
3678
|
await ensureProjectLanguagePreferences(st).catch(() => null);
|
|
3514
|
-
await maybeHealStatuses(st);
|
|
3679
|
+
await maybeHealStatuses(st, ctx);
|
|
3515
3680
|
await st.refreshResume();
|
|
3516
3681
|
plannerHeavyInitDone = true;
|
|
3517
3682
|
}
|
|
@@ -3542,7 +3707,7 @@ export default function planPiExtension(pi) {
|
|
|
3542
3707
|
const resume = await st.loadResume().catch(() => null) ?? await st.refreshResume();
|
|
3543
3708
|
const activity = await st.loadActivityLog();
|
|
3544
3709
|
const recentActivity = activity.entries.slice(-8).reverse();
|
|
3545
|
-
const
|
|
3710
|
+
const handoffs = await st.listHandoffs();
|
|
3546
3711
|
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
3547
3712
|
const orderedPhases = [
|
|
3548
3713
|
...plan.features.features.flatMap((feature) => {
|
|
@@ -3593,8 +3758,8 @@ export default function planPiExtension(pi) {
|
|
|
3593
3758
|
const nextActivity = hasActiveWork
|
|
3594
3759
|
? (resume.nextSteps[0]
|
|
3595
3760
|
?? (currentTask ? `Resume task ${currentTask.id} — ${taskLabel(currentTask)}` : currentPhase ? `Resume phase ${currentPhase.id} — ${phaseLabel(currentPhase)}` : "Resume the active work stream"))
|
|
3596
|
-
: (
|
|
3597
|
-
? "Read the handoff,
|
|
3761
|
+
: (handoffs.length > 0
|
|
3762
|
+
? "Read the most recent phase handoff (handoff show <ref>), validate it against current plan ordering and dependencies, then pick the next task."
|
|
3598
3763
|
: "Review the current plan and choose the next task.");
|
|
3599
3764
|
const webServer = server;
|
|
3600
3765
|
const webLocalUrl = webServer?.localUrl ?? webServer?.url ?? "";
|
|
@@ -3634,7 +3799,12 @@ export default function planPiExtension(pi) {
|
|
|
3634
3799
|
currentFeature ? `- Mention current feature ONLY because it is actually active: ${currentFeature.id} — ${featureLabel(currentFeature)} (${currentFeature.status}).` : "- Mention that no current feature is active.",
|
|
3635
3800
|
currentPhase ? `- Mention current phase ONLY because it is actually active: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Mention that no current phase is active.",
|
|
3636
3801
|
currentTask ? `- Mention current task ONLY because it is actually active: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Mention that no current task is active.",
|
|
3637
|
-
|
|
3802
|
+
handoffs.length > 0 ? `- PHASE HANDOFFS (${handoffs.length} pending):` : "",
|
|
3803
|
+
...handoffs.map((h, i) => ` [${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`),
|
|
3804
|
+
handoffs.length > 0 ? "- Present this numbered list to the user. Recommend the most recent (or the one aligned with the current resume focus)." : "",
|
|
3805
|
+
handoffs.length > 0 ? "- Ask the user to confirm which one to resume. Then run handoff show <ref> (or /planner handoff show <ref>) to read it." : "",
|
|
3806
|
+
handoffs.length > 0 ? "- After reading and confirming the resume, CLEAR that handoff with handoff clear <ref> (or /planner handoff clear) BEFORE starting implementation work — a resumed handoff must not go stale." : "",
|
|
3807
|
+
handoffs.length === 0 ? "- No pending phase handoffs: fall back to the resume.json focus (current in-progress task/phase) above." : "",
|
|
3638
3808
|
webUrl ? `- At the END of your summary, you MUST print a dedicated line with the web dashboard address and port, exactly: '🌐 Web UI: ${webLocalUrl}${webPort ? ` (port ${webPort})` : ""}${webLanUrl ? ` — LAN: ${webLanUrl}` : ""}'. This line is mandatory; do not omit it.` : "- Explicitly tell the user whether the web dashboard is active in this session (and how to start it with '/planner web start' if not).",
|
|
3639
3809
|
`- Mention the next suggested activity: ${nextActivity}`,
|
|
3640
3810
|
"- THEN ask the user explicitly whether they want to resume that activity now.",
|
|
@@ -3652,7 +3822,7 @@ export default function planPiExtension(pi) {
|
|
|
3652
3822
|
"═══════════════════════════════════════════════════════════════",
|
|
3653
3823
|
"MANDATORY OPERATIONAL PROTOCOL (violation = execution failure):",
|
|
3654
3824
|
"═══════════════════════════════════════════════════════════════",
|
|
3655
|
-
"1. HANDOFF HYGIENE: If
|
|
3825
|
+
"1. HANDOFF HYGIENE: If one or more phases have a pending handoff (entity-scoped, phase.handoff), present the list, read the relevant one with handoff show <ref>, then CLEAR it with handoff clear <ref> once consumed. Handoffs are non-blocking context artifacts, not locks — they never block task_start.",
|
|
3656
3826
|
"2. TASK LIFECYCLE: BEFORE coding ANY file: call task_start. AFTER finishing work on ANY task: call task_complete. No exceptions. No 'I'll do it later'. NOTE: Planner operations (plan_write_handoff / /planner handoff, task_start, task_complete, task_update, status reads) are NOT code edits. They are ALWAYS allowed regardless of task state — you may write/refresh the handoff even when no task is in-progress (e.g., all tasks done, or capturing state mid-flight). Never refuse to write a handoff because 'no task is open'; that is incorrect.",
|
|
3657
3827
|
"3. IMMEDIATE SYNC: Update task status AT THE EXACT MOMENT of transition. Start = task_start NOW. Done = task_complete NOW. Blocked = task_update with motivation NOW. Never batch status updates.",
|
|
3658
3828
|
"4. BLOCKED MOTIVATION: Transitions to blocked/canceled/rejected/deferred/waiting/planned(from non-planned) MUST include a detailed 'motivation' parameter. Write it as if the next person has zero context.",
|
|
@@ -3727,11 +3897,12 @@ export default function planPiExtension(pi) {
|
|
|
3727
3897
|
"- Before your final answer after implementation work, either call task_complete if the task is finished, or explicitly tell the user the task remains in-progress.",
|
|
3728
3898
|
"- Use task_complete with force=true only if you have a good reason to skip the checklist check.",
|
|
3729
3899
|
"- To reopen a completed task, use task_start (recommended) or task_update.",
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3900
|
+
handoffs.length > 0 ? `Pending phase handoffs (${handoffs.length}):` : "",
|
|
3901
|
+
...handoffs.map((h, i) => ` [${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`),
|
|
3902
|
+
handoffs.length > 0 ? "Read the relevant one with handoff show <ref> (or /planner handoff show <ref>) as previous-session context, but validate it against the current planner state, ordering, and dependencies before treating any target as the next task." : "",
|
|
3903
|
+
handoffs.length > 0 ? "If planner state shows no task/phase in-progress, do NOT present a handoff target as the current focus; present it only as a candidate to validate." : "",
|
|
3904
|
+
handoffs.length > 0 ? "Once a handoff is fully consumed and work is safely resumed, clear it with handoff clear <ref> (or /planner handoff clear)." : "",
|
|
3905
|
+
"Plan tools available: project_set_language_preferences, plan_init, project_update, requirement_list, requirement_create, requirement_update, requirement_delete, plan_get, feature_list, feature_get, feature_create, feature_update, feature_delete, phase_list, phase_get, phase_create, phase_update, phase_delete, task_list, task_get, task_create, task_update, task_delete, task_start, task_complete, plan_render, plan_get_handoff, plan_write_handoff, plan_delete_handoff, handoff_list, handoff_show, handoff_write, handoff_clear, plan_authorize_bypass, plan_clear_bypass",
|
|
3735
3906
|
].filter(Boolean).join("\n");
|
|
3736
3907
|
contextBlockCache = contextBlock;
|
|
3737
3908
|
contextBlockDirty = false;
|