@agent-plan/pi-adapter 0.2.19-next.0 → 0.2.19-next.1
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 +89 -166
- package/package.json +3 -3
- package/web-ui-dist/assets/index-CZ27vQH6.js +211 -0
- package/web-ui-dist/assets/index-Jy8Yy6I4.css +1 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-DmQC9Ttj.css +0 -1
- package/web-ui-dist/assets/index-O9MpLyc3.js +0 -211
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AA0tBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA+6F9D"}
|
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, findPhaseByRef } from "@agent-plan/core";
|
|
13
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef } from "@agent-plan/core/naming";
|
|
12
|
+
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, withFeatureLock, needsMotivation, findPhaseByRef, buildRecap } from "@agent-plan/core";
|
|
13
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase } 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";
|
|
@@ -227,106 +227,6 @@ function phaseLabel(phase) {
|
|
|
227
227
|
function taskLabel(task) {
|
|
228
228
|
return `T${formatSequence(task.number)} — ${task.title}`;
|
|
229
229
|
}
|
|
230
|
-
async function buildStartupResumeSummary(st) {
|
|
231
|
-
const [plan, resume, handoffs] = await Promise.all([
|
|
232
|
-
st.loadAll(),
|
|
233
|
-
st.refreshResume(),
|
|
234
|
-
st.listHandoffs(),
|
|
235
|
-
]);
|
|
236
|
-
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
237
|
-
const orderedPhases = [
|
|
238
|
-
...plan.features.features.flatMap((feature) => {
|
|
239
|
-
const linked = feature.phaseIds.map((id) => phaseById.get(id)).filter((phase) => Boolean(phase));
|
|
240
|
-
const linkedIds = new Set(linked.map((phase) => phase.id));
|
|
241
|
-
const inferred = plan.phases
|
|
242
|
-
.filter((phase) => phase.featureId === feature.id && !linkedIds.has(phase.id))
|
|
243
|
-
.sort((left, right) => left.number - right.number || left.createdAt.localeCompare(right.createdAt));
|
|
244
|
-
return [...linked, ...inferred];
|
|
245
|
-
}),
|
|
246
|
-
...plan.phases
|
|
247
|
-
.filter((phase) => !phase.featureId)
|
|
248
|
-
.sort((left, right) => left.number - right.number || left.createdAt.localeCompare(right.createdAt)),
|
|
249
|
-
];
|
|
250
|
-
const allTasks = orderedPhases.flatMap((phase) => [...phase.tasks]
|
|
251
|
-
.sort((left, right) => left.number - right.number || left.createdAt.localeCompare(right.createdAt))
|
|
252
|
-
.map((task) => ({ phase, task })));
|
|
253
|
-
const totalFeatures = plan.features.features.length;
|
|
254
|
-
const doneFeatures = plan.features.features.filter((feature) => feature.status === "done").length;
|
|
255
|
-
const activeFeatures = plan.features.features.filter((feature) => feature.status === "in-progress").length;
|
|
256
|
-
const totalPhases = plan.phases.length;
|
|
257
|
-
const donePhases = plan.phases.filter((phase) => phase.status === "done").length;
|
|
258
|
-
const activePhases = plan.phases.filter((phase) => phase.status === "in-progress" || phase.status === "discovery").length;
|
|
259
|
-
const totalTasks = allTasks.length;
|
|
260
|
-
const doneTasks = allTasks.filter(({ task }) => task.status === "done").length;
|
|
261
|
-
const activeTasks = allTasks.filter(({ task }) => task.status === "in-progress").length;
|
|
262
|
-
const hasActiveWork = activeFeatures > 0 || activePhases > 0 || activeTasks > 0;
|
|
263
|
-
const currentPhase = hasActiveWork
|
|
264
|
-
? orderedPhases.find((phase) => phase.id === resume.currentPhaseId && (phase.status === "in-progress" || phase.tasks.some((task) => resume.inProgressTaskIds.includes(task.id))))
|
|
265
|
-
?? orderedPhases.find((phase) => phase.tasks.some((task) => resume.inProgressTaskIds.includes(task.id)))
|
|
266
|
-
?? orderedPhases.find((phase) => phase.status === "in-progress")
|
|
267
|
-
?? null
|
|
268
|
-
: null;
|
|
269
|
-
const currentTask = hasActiveWork
|
|
270
|
-
? ([...(currentPhase?.tasks ?? [])]
|
|
271
|
-
.sort((left, right) => left.number - right.number || left.createdAt.localeCompare(right.createdAt))
|
|
272
|
-
.find((task) => resume.inProgressTaskIds.includes(task.id))
|
|
273
|
-
?? [...(currentPhase?.tasks ?? [])]
|
|
274
|
-
.sort((left, right) => left.number - right.number || left.createdAt.localeCompare(right.createdAt))
|
|
275
|
-
.find((task) => task.status === "in-progress")
|
|
276
|
-
?? allTasks.find(({ task }) => task.status === "in-progress")?.task
|
|
277
|
-
?? null)
|
|
278
|
-
: null;
|
|
279
|
-
const currentFeature = hasActiveWork
|
|
280
|
-
? (currentPhase?.featureId
|
|
281
|
-
? plan.features.features.find((feature) => feature.id === currentPhase.featureId) ?? null
|
|
282
|
-
: plan.features.features.find((feature) => feature.status === "in-progress") ?? null)
|
|
283
|
-
: null;
|
|
284
|
-
const nextActivity = hasActiveWork
|
|
285
|
-
? (resume.nextSteps[0]
|
|
286
|
-
?? (currentTask
|
|
287
|
-
? `riprendere il task ${currentTask.id} — ${taskLabel(currentTask)}`
|
|
288
|
-
: currentPhase
|
|
289
|
-
? `riprendere la fase ${currentPhase.id} — ${phaseLabel(currentPhase)}`
|
|
290
|
-
: "riprendere il lavoro attivo corrente"))
|
|
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"
|
|
293
|
-
: "rivedere il piano e scegliere il prossimo task concreto");
|
|
294
|
-
const chatLanguage = (plan.project.chatLanguage || "").toLowerCase();
|
|
295
|
-
const italian = chatLanguage.includes("ital");
|
|
296
|
-
const localUrl = server?.localUrl ?? server?.url ?? "";
|
|
297
|
-
const lanUrl = server?.lanUrl ?? "";
|
|
298
|
-
const webUrl = lanUrl ? `${localUrl} (LAN: ${lanUrl})` : localUrl;
|
|
299
|
-
if (italian || !chatLanguage) {
|
|
300
|
-
return [
|
|
301
|
-
"### Summary di ripresa planner",
|
|
302
|
-
`- Progresso: ${doneFeatures}/${totalFeatures} feature completate (${activeFeatures} attive), ${donePhases}/${totalPhases} fasi completate (${activePhases} attive), ${doneTasks}/${totalTasks} task completati (${activeTasks} attivi).`,
|
|
303
|
-
currentFeature ? `- Focus feature: ${currentFeature.id} — ${featureLabel(currentFeature)} (${currentFeature.status}).` : "- Focus feature: nessuna feature attiva chiara.",
|
|
304
|
-
currentPhase ? `- Focus fase: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Focus fase: nessuna fase attiva chiara.",
|
|
305
|
-
currentTask ? `- Focus task: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Focus task: nessun task attivo in questo momento.",
|
|
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." : "",
|
|
308
|
-
`- Prossima attività consigliata: ${nextActivity}.`,
|
|
309
|
-
"",
|
|
310
|
-
"Vuoi che riprendiamo da qui?",
|
|
311
|
-
"",
|
|
312
|
-
webUrl ? `🌐 Web UI: ${webUrl}${lastKnownWebPort ? ` (port ${lastKnownWebPort})` : ""}` : "🌐 Web UI: not running — avvia con /planner load",
|
|
313
|
-
].filter(Boolean).join("\n");
|
|
314
|
-
}
|
|
315
|
-
return [
|
|
316
|
-
"### Planner resume summary",
|
|
317
|
-
`- Progress: ${doneFeatures}/${totalFeatures} features done (${activeFeatures} active), ${donePhases}/${totalPhases} phases done (${activePhases} active), ${doneTasks}/${totalTasks} tasks done (${activeTasks} active).`,
|
|
318
|
-
currentFeature ? `- Feature focus: ${currentFeature.id} — ${featureLabel(currentFeature)} (${currentFeature.status}).` : "- Feature focus: no clear active feature.",
|
|
319
|
-
currentPhase ? `- Phase focus: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Phase focus: no clear active phase.",
|
|
320
|
-
currentTask ? `- Task focus: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Task focus: no active task right now.",
|
|
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." : "",
|
|
323
|
-
`- Recommended next activity: ${nextActivity}.`,
|
|
324
|
-
"",
|
|
325
|
-
"Do you want to resume from here?",
|
|
326
|
-
"",
|
|
327
|
-
webUrl ? `🌐 Web UI: ${webUrl}${lastKnownWebPort ? ` (port ${lastKnownWebPort})` : ""}` : "🌐 Web UI: not running — start with /planner load",
|
|
328
|
-
].filter(Boolean).join("\n");
|
|
329
|
-
}
|
|
330
230
|
async function buildHandoffMarkdown(st, reason, overrides) {
|
|
331
231
|
const [plan, resume, activity] = await Promise.all([
|
|
332
232
|
st.loadAll(),
|
|
@@ -625,13 +525,23 @@ async function pickProjectPort(ctx, explicitPort) {
|
|
|
625
525
|
await st.saveProject(project);
|
|
626
526
|
return explicitPort;
|
|
627
527
|
}
|
|
628
|
-
if (
|
|
528
|
+
// Reuse the persisted port if it's still free (stable port across restarts,
|
|
529
|
+
// lets subagents notify the server via HTTP).
|
|
530
|
+
if (project.webPort > 0 && (await isPortAvailable(project.webPort))) {
|
|
629
531
|
return project.webPort;
|
|
630
532
|
}
|
|
533
|
+
// Persisted port busy (another planner instance on this project) or first run:
|
|
534
|
+
// scan for a free port. Persist ONLY on first run (canonical); when falling
|
|
535
|
+
// back from a busy persisted port, return transient so we don't hijack the
|
|
536
|
+
// primary session's canonical port (prevents port thrashing).
|
|
631
537
|
for (let port = 3030; port <= 3999; port += 1) {
|
|
538
|
+
if (port === project.webPort)
|
|
539
|
+
continue;
|
|
632
540
|
if (await isPortAvailable(port)) {
|
|
633
|
-
project.webPort
|
|
634
|
-
|
|
541
|
+
if (project.webPort <= 0) {
|
|
542
|
+
project.webPort = port;
|
|
543
|
+
await st.saveProject(project);
|
|
544
|
+
}
|
|
635
545
|
return port;
|
|
636
546
|
}
|
|
637
547
|
}
|
|
@@ -708,7 +618,7 @@ async function startServer(ctx, requestedPort, visibility = "lan") {
|
|
|
708
618
|
uiConfig: readUiConfig(ctx),
|
|
709
619
|
isBusy: () => planBusy,
|
|
710
620
|
});
|
|
711
|
-
ctx.ui.notify(`Planner web server:
|
|
621
|
+
ctx.ui.notify(`Planner web server: port ${port} busy (another planner instance on this project?), started on ${server?.url ?? "?"} instead.`, "info");
|
|
712
622
|
}
|
|
713
623
|
catch (e2) {
|
|
714
624
|
ctx.ui.notify(`Failed to start web server (also retry failed): ${e2 instanceof Error ? e2.message : String(e2)}`, "error");
|
|
@@ -736,8 +646,13 @@ async function startServer(ctx, requestedPort, visibility = "lan") {
|
|
|
736
646
|
const st = ensureStore(ctx);
|
|
737
647
|
const project = await st.loadProject().catch(() => null);
|
|
738
648
|
if (project) {
|
|
739
|
-
|
|
740
|
-
|
|
649
|
+
// Persist only the canonical port (first run, or realPort matches the
|
|
650
|
+
// existing canonical). Never persist a transient fallback port — that
|
|
651
|
+
// hijacks the primary session's port when two instances share a .planner.
|
|
652
|
+
if (project.webPort <= 0 || project.webPort === realPort) {
|
|
653
|
+
project.webPort = realPort;
|
|
654
|
+
await st.saveProject(project);
|
|
655
|
+
}
|
|
741
656
|
}
|
|
742
657
|
}
|
|
743
658
|
catch { }
|
|
@@ -1113,40 +1028,33 @@ export default function planPiExtension(pi) {
|
|
|
1113
1028
|
// Helper: pick a feature interactively
|
|
1114
1029
|
// ═══════════════════════════════════════════════════════════════
|
|
1115
1030
|
async function pickFeature() {
|
|
1116
|
-
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features
|
|
1031
|
+
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features
|
|
1032
|
+
.slice()
|
|
1033
|
+
.sort((a, b) => a.number - b.number);
|
|
1117
1034
|
if (features.length === 0)
|
|
1118
1035
|
return null;
|
|
1119
|
-
const
|
|
1120
|
-
ctx.ui.
|
|
1121
|
-
|
|
1122
|
-
if (!pick?.trim())
|
|
1123
|
-
return null;
|
|
1124
|
-
const idx = parseInt(pick.trim(), 10) - 1;
|
|
1125
|
-
if (isNaN(idx) || idx < 0 || idx >= features.length) {
|
|
1126
|
-
ctx.ui.notify("Invalid number", "error");
|
|
1036
|
+
const options = features.map((f) => ` ${formatFeatureRef(f.number)} ${statusIcon(f.status)} ${f.name}${f.shortId ? ` · ${f.shortId}` : ""}`);
|
|
1037
|
+
const chosen = await ctx.ui.select("Pick a feature", options);
|
|
1038
|
+
if (chosen == null)
|
|
1127
1039
|
return null;
|
|
1128
|
-
|
|
1129
|
-
return features[idx];
|
|
1040
|
+
const idx = options.indexOf(chosen);
|
|
1041
|
+
return idx >= 0 ? features[idx] : null;
|
|
1130
1042
|
}
|
|
1131
1043
|
// ═══════════════════════════════════════════════════════════════
|
|
1132
1044
|
// Helper: pick a phase interactively
|
|
1133
1045
|
// ═══════════════════════════════════════════════════════════════
|
|
1134
1046
|
async function pickPhase() {
|
|
1135
|
-
const phases = await st.loadAllPhases().catch(() => []);
|
|
1047
|
+
const phases = (await st.loadAllPhases().catch(() => [])).slice().sort((a, b) => a.number - b.number);
|
|
1136
1048
|
if (phases.length === 0) {
|
|
1137
1049
|
return null;
|
|
1138
1050
|
}
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
if (
|
|
1143
|
-
return null;
|
|
1144
|
-
const idx = parseInt(pick.trim(), 10) - 1;
|
|
1145
|
-
if (isNaN(idx) || idx < 0 || idx >= phases.length) {
|
|
1146
|
-
ctx.ui.notify("Invalid number", "error");
|
|
1051
|
+
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features;
|
|
1052
|
+
const options = phases.map((p) => ` ${formatPhaseRef(p.number, featureNumberOfPhase(p, features))} ${statusIcon(p.status)} ${p.title}${p.shortId ? ` · ${p.shortId}` : ""}`);
|
|
1053
|
+
const chosen = await ctx.ui.select("Pick a phase", options);
|
|
1054
|
+
if (chosen == null)
|
|
1147
1055
|
return null;
|
|
1148
|
-
|
|
1149
|
-
return phases[idx];
|
|
1056
|
+
const idx = options.indexOf(chosen);
|
|
1057
|
+
return idx >= 0 ? phases[idx] : null;
|
|
1150
1058
|
}
|
|
1151
1059
|
// ═══════════════════════════════════════════════════════════════
|
|
1152
1060
|
// Helper: pick a task interactively from a phase
|
|
@@ -1154,17 +1062,13 @@ export default function planPiExtension(pi) {
|
|
|
1154
1062
|
async function pickTask(phase) {
|
|
1155
1063
|
if (phase.tasks.length === 0)
|
|
1156
1064
|
return null;
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
if (
|
|
1161
|
-
return null;
|
|
1162
|
-
const idx = parseInt(pick.trim(), 10) - 1;
|
|
1163
|
-
if (isNaN(idx) || idx < 0 || idx >= phase.tasks.length) {
|
|
1164
|
-
ctx.ui.notify("Invalid number", "error");
|
|
1065
|
+
const tasks = phase.tasks.slice().sort((a, b) => a.number - b.number);
|
|
1066
|
+
const options = tasks.map((t) => ` T${String(t.number).padStart(3, "0")} ${statusIcon(t.status)} ${t.title}${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
1067
|
+
const chosen = await ctx.ui.select(`Pick a task from "${phase.title}"`, options);
|
|
1068
|
+
if (chosen == null)
|
|
1165
1069
|
return null;
|
|
1166
|
-
|
|
1167
|
-
return
|
|
1070
|
+
const idx = options.indexOf(chosen);
|
|
1071
|
+
return idx >= 0 ? tasks[idx] : null;
|
|
1168
1072
|
}
|
|
1169
1073
|
function parseMultilineList(value) {
|
|
1170
1074
|
return (value ?? "")
|
|
@@ -1183,7 +1087,15 @@ export default function planPiExtension(pi) {
|
|
|
1183
1087
|
const normalized = ref.trim().toLowerCase();
|
|
1184
1088
|
if (!normalized)
|
|
1185
1089
|
return null;
|
|
1186
|
-
|
|
1090
|
+
const fMatch = normalized.match(/^f(\d+)$/);
|
|
1091
|
+
if (fMatch) {
|
|
1092
|
+
const n = parseInt(fMatch[1], 10);
|
|
1093
|
+
const byNum = features.find((f) => f.number === n);
|
|
1094
|
+
if (byNum)
|
|
1095
|
+
return byNum;
|
|
1096
|
+
}
|
|
1097
|
+
return features.find((feature) => feature.shortId && feature.shortId.toLowerCase() === normalized)
|
|
1098
|
+
?? features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
1187
1099
|
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
1188
1100
|
?? features.find((feature) => feature.name.toLowerCase().includes(normalized))
|
|
1189
1101
|
?? null;
|
|
@@ -1192,7 +1104,15 @@ export default function planPiExtension(pi) {
|
|
|
1192
1104
|
const normalized = ref.trim().toLowerCase();
|
|
1193
1105
|
if (!normalized)
|
|
1194
1106
|
return null;
|
|
1195
|
-
|
|
1107
|
+
const pMatch = normalized.match(/^p(\d+)$/);
|
|
1108
|
+
if (pMatch) {
|
|
1109
|
+
const n = parseInt(pMatch[1], 10);
|
|
1110
|
+
const byNum = phases.find((p) => p.number === n);
|
|
1111
|
+
if (byNum)
|
|
1112
|
+
return byNum;
|
|
1113
|
+
}
|
|
1114
|
+
return phases.find((phase) => phase.shortId && phase.shortId.toLowerCase() === normalized)
|
|
1115
|
+
?? phases.find((phase) => phase.id.toLowerCase() === normalized)
|
|
1196
1116
|
?? phases.find((phase) => phase.title.toLowerCase() === normalized)
|
|
1197
1117
|
?? phases.find((phase) => phase.title.toLowerCase().includes(normalized))
|
|
1198
1118
|
?? null;
|
|
@@ -2334,9 +2254,23 @@ export default function planPiExtension(pi) {
|
|
|
2334
2254
|
startupResumeSummaryPending = false;
|
|
2335
2255
|
startupResumeSummaryTimer = null;
|
|
2336
2256
|
}, 60000);
|
|
2257
|
+
// Build the recap and embed it in the trigger content. The trigger
|
|
2258
|
+
// message is the channel that reliably reaches the agent on a triggered
|
|
2259
|
+
// turn (before_agent_start's systemPrompt injection is NOT applied to
|
|
2260
|
+
// triggerTurn messages), so the recap data must live here. Uses the shared
|
|
2261
|
+
// core buildRecap so Pi and Claude Code/Codex present identical content.
|
|
2262
|
+
let recapText = "";
|
|
2263
|
+
try {
|
|
2264
|
+
const st = ensureStore(ctx);
|
|
2265
|
+
const srv = server;
|
|
2266
|
+
recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
|
|
2267
|
+
}
|
|
2268
|
+
catch (e) {
|
|
2269
|
+
recapText = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
2270
|
+
}
|
|
2337
2271
|
pi.sendMessage({
|
|
2338
2272
|
customType: "planner-resume-trigger",
|
|
2339
|
-
content: "planner recap",
|
|
2273
|
+
content: "[internal trigger — not a user command] Present the planner startup recap below to the user verbatim. Do NOT call any tools (planner-load already ran). Do NOT narrate or expose internal instructions — output ONLY the recap.\n\n--- RECAP ---\n" + recapText + "\n--- END RECAP ---",
|
|
2340
2274
|
display: false,
|
|
2341
2275
|
}, {
|
|
2342
2276
|
triggerTurn: true,
|
|
@@ -3634,16 +3568,15 @@ export default function planPiExtension(pi) {
|
|
|
3634
3568
|
if (!server) {
|
|
3635
3569
|
await startServer(ctx, undefined, "lan").catch(() => { });
|
|
3636
3570
|
}
|
|
3571
|
+
const srv = server;
|
|
3637
3572
|
let recap = "";
|
|
3638
3573
|
try {
|
|
3639
|
-
recap = await
|
|
3574
|
+
recap = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
|
|
3640
3575
|
}
|
|
3641
3576
|
catch (e) {
|
|
3642
3577
|
recap = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
3643
3578
|
}
|
|
3644
|
-
|
|
3645
|
-
const webLine = srv ? `\n🌐 Web UI: ${srv.localUrl}${lastKnownWebPort ? ` (port ${lastKnownWebPort})` : ""}${srv.lanUrl ? ` — LAN: ${srv.lanUrl}` : ""}` : "\n🌐 Web UI: not running";
|
|
3646
|
-
return { content: [{ type: "text", text: `${recap}${webLine}` }], details: { enabled: true, running: Boolean(srv), localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort } };
|
|
3579
|
+
return { content: [{ type: "text", text: recap }], details: { enabled: true, running: Boolean(srv), localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort } };
|
|
3647
3580
|
},
|
|
3648
3581
|
});
|
|
3649
3582
|
pi.registerTool({
|
|
@@ -3789,27 +3722,16 @@ export default function planPiExtension(pi) {
|
|
|
3789
3722
|
const ambientSummary = ambient
|
|
3790
3723
|
? `node=${ambient.nodeVersion || "?"} pm=${ambient.packageManager || "?"} lockfile=${ambient.lockfile || "none"} scripts=${Object.entries(ambient.scripts).map(([k, v]) => `${k}="${v}"`).join(", ") || "none"}`
|
|
3791
3724
|
: "(not scanned)";
|
|
3725
|
+
// NOTE: the recap DATA is delivered via the /planner load trigger message
|
|
3726
|
+
// content (buildRecap), not via this system-prompt protocol — before_agent_start's
|
|
3727
|
+
// systemPrompt injection is not applied to triggerTurn messages. This protocol
|
|
3728
|
+
// is kept as a lightweight anti-narration guard for any normal recap turn.
|
|
3792
3729
|
const startupResumeProtocol = isRecapTurn ? [
|
|
3793
3730
|
"",
|
|
3794
|
-
"STARTUP RESUME PROTOCOL (
|
|
3795
|
-
"-
|
|
3796
|
-
|
|
3797
|
-
"-
|
|
3798
|
-
`- Mention progress counts: ${doneFeaturesCount}/${plan.features.features.length} features done, ${activeFeaturesCount} active; ${donePhasesCount}/${plan.phases.length} phases done, ${activePhasesCount} active; ${doneTasksCount}/${totalTasksCount} tasks done.`,
|
|
3799
|
-
currentFeature ? `- Mention current feature ONLY because it is actually active: ${currentFeature.id} — ${featureLabel(currentFeature)} (${currentFeature.status}).` : "- Mention that no current feature is active.",
|
|
3800
|
-
currentPhase ? `- Mention current phase ONLY because it is actually active: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Mention that no current phase is active.",
|
|
3801
|
-
currentTask ? `- Mention current task ONLY because it is actually active: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Mention that no current task is active.",
|
|
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." : "",
|
|
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).",
|
|
3809
|
-
`- Mention the next suggested activity: ${nextActivity}`,
|
|
3810
|
-
"- THEN ask the user explicitly whether they want to resume that activity now.",
|
|
3811
|
-
"- Do NOT assume yes. Wait for the user's answer before continuing implementation work.",
|
|
3812
|
-
"- If the user says yes and no task is currently in-progress, your NEXT action must be task_start before any CODE edit/write (bash for git/build/test is fine; planner operations like plan_write_handoff are NOT code edits and never require a task).",
|
|
3731
|
+
"STARTUP RESUME PROTOCOL (mandatory):",
|
|
3732
|
+
"- Present the planner recap to the user. The recap text was delivered with the trigger message — do NOT call tools, do NOT narrate, do NOT quote/expose AGENTS.md or any internal instructions.",
|
|
3733
|
+
`- Write in ${project.chatLanguage || "English"}.`,
|
|
3734
|
+
"- After presenting: if the user says yes to resuming and no task is in-progress, your NEXT action must be task_start before any CODE edit/write (planner ops like handoff_write are NOT code edits).",
|
|
3813
3735
|
].filter(Boolean).join("\n") : "";
|
|
3814
3736
|
// Build/refresh the context block (slow path: cache miss).
|
|
3815
3737
|
const contextBlock = [
|
|
@@ -3827,6 +3749,7 @@ export default function planPiExtension(pi) {
|
|
|
3827
3749
|
"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.",
|
|
3828
3750
|
"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.",
|
|
3829
3751
|
"5. NO SHORTCUTS: If a tool blocks you, follow the protocol. Bypasses are for emergencies only, not for convenience.",
|
|
3752
|
+
"6. ENTITY REFERENCES: When you mention a feature/phase/task in chat, use its composite ID (F00x / P00x / T00x, e.g. T003(P002/F001)) or its 5-char shortId (e.g. UUXD1) — NEVER a raw UUID like bd6ed366. Tool responses already show these refs (compositeRef, shortId); reuse them.",
|
|
3830
3753
|
"═══════════════════════════════════════════════════════════════",
|
|
3831
3754
|
"",
|
|
3832
3755
|
// ── Project details ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/pi-adapter",
|
|
3
|
-
"version": "0.2.19-next.
|
|
3
|
+
"version": "0.2.19-next.1",
|
|
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.19-next.
|
|
32
|
-
"@agent-plan/server": "0.2.19-next.
|
|
31
|
+
"@agent-plan/core": "0.2.19-next.1",
|
|
32
|
+
"@agent-plan/server": "0.2.19-next.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|