@agent-plan/pi-adapter 0.2.19-next.0 → 0.2.19-next.2
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 +127 -179
- 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;AA6uBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAg7F9D"}
|
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";
|
|
@@ -68,6 +68,29 @@ setWriteNotifyHook(() => {
|
|
|
68
68
|
let previousContextPercent = 0;
|
|
69
69
|
let autoHandoffTriggered = false;
|
|
70
70
|
let plannerSessionEnabled = false;
|
|
71
|
+
// In-process guard bypass (per session, NOT persisted/shared). The edit/write
|
|
72
|
+
// guard is advisory in Pi (warns "NO ACTIVE TASK" but does not block). When a
|
|
73
|
+
// bypass is active the warning is silenced for the window. Lives in memory only:
|
|
74
|
+
// not on disk, not in git, not shared across agents/sessions on the same folder.
|
|
75
|
+
let guardBypassUntil = "";
|
|
76
|
+
function authorizeGuardBypass(durationMinutes = 15) {
|
|
77
|
+
const until = new Date(Date.now() + durationMinutes * 60_000).toISOString();
|
|
78
|
+
guardBypassUntil = until;
|
|
79
|
+
return until;
|
|
80
|
+
}
|
|
81
|
+
function clearGuardBypass() { guardBypassUntil = ""; }
|
|
82
|
+
function isGuardBypassed() {
|
|
83
|
+
if (!guardBypassUntil)
|
|
84
|
+
return false;
|
|
85
|
+
const until = Date.parse(guardBypassUntil);
|
|
86
|
+
if (!Number.isFinite(until))
|
|
87
|
+
return false;
|
|
88
|
+
if (until <= Date.now()) {
|
|
89
|
+
guardBypassUntil = "";
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
71
94
|
let startupResumePromptPending = false;
|
|
72
95
|
let startupResumeSummaryPending = false;
|
|
73
96
|
let plannerHeavyInitDone = false; // runs migrate/heal/refreshResume once per session, not every turn
|
|
@@ -227,106 +250,6 @@ function phaseLabel(phase) {
|
|
|
227
250
|
function taskLabel(task) {
|
|
228
251
|
return `T${formatSequence(task.number)} — ${task.title}`;
|
|
229
252
|
}
|
|
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
253
|
async function buildHandoffMarkdown(st, reason, overrides) {
|
|
331
254
|
const [plan, resume, activity] = await Promise.all([
|
|
332
255
|
st.loadAll(),
|
|
@@ -625,13 +548,23 @@ async function pickProjectPort(ctx, explicitPort) {
|
|
|
625
548
|
await st.saveProject(project);
|
|
626
549
|
return explicitPort;
|
|
627
550
|
}
|
|
628
|
-
if (
|
|
551
|
+
// Reuse the persisted port if it's still free (stable port across restarts,
|
|
552
|
+
// lets subagents notify the server via HTTP).
|
|
553
|
+
if (project.webPort > 0 && (await isPortAvailable(project.webPort))) {
|
|
629
554
|
return project.webPort;
|
|
630
555
|
}
|
|
556
|
+
// Persisted port busy (another planner instance on this project) or first run:
|
|
557
|
+
// scan for a free port. Persist ONLY on first run (canonical); when falling
|
|
558
|
+
// back from a busy persisted port, return transient so we don't hijack the
|
|
559
|
+
// primary session's canonical port (prevents port thrashing).
|
|
631
560
|
for (let port = 3030; port <= 3999; port += 1) {
|
|
561
|
+
if (port === project.webPort)
|
|
562
|
+
continue;
|
|
632
563
|
if (await isPortAvailable(port)) {
|
|
633
|
-
project.webPort
|
|
634
|
-
|
|
564
|
+
if (project.webPort <= 0) {
|
|
565
|
+
project.webPort = port;
|
|
566
|
+
await st.saveProject(project);
|
|
567
|
+
}
|
|
635
568
|
return port;
|
|
636
569
|
}
|
|
637
570
|
}
|
|
@@ -708,7 +641,7 @@ async function startServer(ctx, requestedPort, visibility = "lan") {
|
|
|
708
641
|
uiConfig: readUiConfig(ctx),
|
|
709
642
|
isBusy: () => planBusy,
|
|
710
643
|
});
|
|
711
|
-
ctx.ui.notify(`Planner web server:
|
|
644
|
+
ctx.ui.notify(`Planner web server: port ${port} busy (another planner instance on this project?), started on ${server?.url ?? "?"} instead.`, "info");
|
|
712
645
|
}
|
|
713
646
|
catch (e2) {
|
|
714
647
|
ctx.ui.notify(`Failed to start web server (also retry failed): ${e2 instanceof Error ? e2.message : String(e2)}`, "error");
|
|
@@ -736,8 +669,13 @@ async function startServer(ctx, requestedPort, visibility = "lan") {
|
|
|
736
669
|
const st = ensureStore(ctx);
|
|
737
670
|
const project = await st.loadProject().catch(() => null);
|
|
738
671
|
if (project) {
|
|
739
|
-
|
|
740
|
-
|
|
672
|
+
// Persist only the canonical port (first run, or realPort matches the
|
|
673
|
+
// existing canonical). Never persist a transient fallback port — that
|
|
674
|
+
// hijacks the primary session's port when two instances share a .planner.
|
|
675
|
+
if (project.webPort <= 0 || project.webPort === realPort) {
|
|
676
|
+
project.webPort = realPort;
|
|
677
|
+
await st.saveProject(project);
|
|
678
|
+
}
|
|
741
679
|
}
|
|
742
680
|
}
|
|
743
681
|
catch { }
|
|
@@ -947,6 +885,8 @@ export default function planPiExtension(pi) {
|
|
|
947
885
|
return; // nothing to enforce yet
|
|
948
886
|
if (guard.inProgressTaskIds.length > 0)
|
|
949
887
|
return; // a task is open → we're good
|
|
888
|
+
if (isGuardBypassed())
|
|
889
|
+
return; // user authorized proceeding without a task
|
|
950
890
|
const focusHint = guard.focusTaskId
|
|
951
891
|
? `Il task più probabile è ${guard.focusTaskId} — ${guard.focusTaskTitle}. Avvialo con: \`/planner task start ${guard.focusTaskId}\``
|
|
952
892
|
: `Scegli un task dal piano e avvialo con \`/planner task start <taskId>\`.`;
|
|
@@ -1113,40 +1053,33 @@ export default function planPiExtension(pi) {
|
|
|
1113
1053
|
// Helper: pick a feature interactively
|
|
1114
1054
|
// ═══════════════════════════════════════════════════════════════
|
|
1115
1055
|
async function pickFeature() {
|
|
1116
|
-
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features
|
|
1056
|
+
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features
|
|
1057
|
+
.slice()
|
|
1058
|
+
.sort((a, b) => a.number - b.number);
|
|
1117
1059
|
if (features.length === 0)
|
|
1118
1060
|
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");
|
|
1061
|
+
const options = features.map((f) => ` ${formatFeatureRef(f.number)} ${statusIcon(f.status)} ${f.name}${f.shortId ? ` · ${f.shortId}` : ""}`);
|
|
1062
|
+
const chosen = await ctx.ui.select("Pick a feature", options);
|
|
1063
|
+
if (chosen == null)
|
|
1127
1064
|
return null;
|
|
1128
|
-
|
|
1129
|
-
return features[idx];
|
|
1065
|
+
const idx = options.indexOf(chosen);
|
|
1066
|
+
return idx >= 0 ? features[idx] : null;
|
|
1130
1067
|
}
|
|
1131
1068
|
// ═══════════════════════════════════════════════════════════════
|
|
1132
1069
|
// Helper: pick a phase interactively
|
|
1133
1070
|
// ═══════════════════════════════════════════════════════════════
|
|
1134
1071
|
async function pickPhase() {
|
|
1135
|
-
const phases = await st.loadAllPhases().catch(() => []);
|
|
1072
|
+
const phases = (await st.loadAllPhases().catch(() => [])).slice().sort((a, b) => a.number - b.number);
|
|
1136
1073
|
if (phases.length === 0) {
|
|
1137
1074
|
return null;
|
|
1138
1075
|
}
|
|
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");
|
|
1076
|
+
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features;
|
|
1077
|
+
const options = phases.map((p) => ` ${formatPhaseRef(p.number, featureNumberOfPhase(p, features))} ${statusIcon(p.status)} ${p.title}${p.shortId ? ` · ${p.shortId}` : ""}`);
|
|
1078
|
+
const chosen = await ctx.ui.select("Pick a phase", options);
|
|
1079
|
+
if (chosen == null)
|
|
1147
1080
|
return null;
|
|
1148
|
-
|
|
1149
|
-
return phases[idx];
|
|
1081
|
+
const idx = options.indexOf(chosen);
|
|
1082
|
+
return idx >= 0 ? phases[idx] : null;
|
|
1150
1083
|
}
|
|
1151
1084
|
// ═══════════════════════════════════════════════════════════════
|
|
1152
1085
|
// Helper: pick a task interactively from a phase
|
|
@@ -1154,17 +1087,13 @@ export default function planPiExtension(pi) {
|
|
|
1154
1087
|
async function pickTask(phase) {
|
|
1155
1088
|
if (phase.tasks.length === 0)
|
|
1156
1089
|
return null;
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
if (
|
|
1090
|
+
const tasks = phase.tasks.slice().sort((a, b) => a.number - b.number);
|
|
1091
|
+
const options = tasks.map((t) => ` T${String(t.number).padStart(3, "0")} ${statusIcon(t.status)} ${t.title}${t.shortId ? ` · ${t.shortId}` : ""}`);
|
|
1092
|
+
const chosen = await ctx.ui.select(`Pick a task from "${phase.title}"`, options);
|
|
1093
|
+
if (chosen == null)
|
|
1161
1094
|
return null;
|
|
1162
|
-
const idx =
|
|
1163
|
-
|
|
1164
|
-
ctx.ui.notify("Invalid number", "error");
|
|
1165
|
-
return null;
|
|
1166
|
-
}
|
|
1167
|
-
return phase.tasks[idx];
|
|
1095
|
+
const idx = options.indexOf(chosen);
|
|
1096
|
+
return idx >= 0 ? tasks[idx] : null;
|
|
1168
1097
|
}
|
|
1169
1098
|
function parseMultilineList(value) {
|
|
1170
1099
|
return (value ?? "")
|
|
@@ -1183,7 +1112,15 @@ export default function planPiExtension(pi) {
|
|
|
1183
1112
|
const normalized = ref.trim().toLowerCase();
|
|
1184
1113
|
if (!normalized)
|
|
1185
1114
|
return null;
|
|
1186
|
-
|
|
1115
|
+
const fMatch = normalized.match(/^f(\d+)$/);
|
|
1116
|
+
if (fMatch) {
|
|
1117
|
+
const n = parseInt(fMatch[1], 10);
|
|
1118
|
+
const byNum = features.find((f) => f.number === n);
|
|
1119
|
+
if (byNum)
|
|
1120
|
+
return byNum;
|
|
1121
|
+
}
|
|
1122
|
+
return features.find((feature) => feature.shortId && feature.shortId.toLowerCase() === normalized)
|
|
1123
|
+
?? features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
1187
1124
|
?? features.find((feature) => feature.name.toLowerCase() === normalized)
|
|
1188
1125
|
?? features.find((feature) => feature.name.toLowerCase().includes(normalized))
|
|
1189
1126
|
?? null;
|
|
@@ -1192,7 +1129,15 @@ export default function planPiExtension(pi) {
|
|
|
1192
1129
|
const normalized = ref.trim().toLowerCase();
|
|
1193
1130
|
if (!normalized)
|
|
1194
1131
|
return null;
|
|
1195
|
-
|
|
1132
|
+
const pMatch = normalized.match(/^p(\d+)$/);
|
|
1133
|
+
if (pMatch) {
|
|
1134
|
+
const n = parseInt(pMatch[1], 10);
|
|
1135
|
+
const byNum = phases.find((p) => p.number === n);
|
|
1136
|
+
if (byNum)
|
|
1137
|
+
return byNum;
|
|
1138
|
+
}
|
|
1139
|
+
return phases.find((phase) => phase.shortId && phase.shortId.toLowerCase() === normalized)
|
|
1140
|
+
?? phases.find((phase) => phase.id.toLowerCase() === normalized)
|
|
1196
1141
|
?? phases.find((phase) => phase.title.toLowerCase() === normalized)
|
|
1197
1142
|
?? phases.find((phase) => phase.title.toLowerCase().includes(normalized))
|
|
1198
1143
|
?? null;
|
|
@@ -2301,12 +2246,12 @@ export default function planPiExtension(pi) {
|
|
|
2301
2246
|
try {
|
|
2302
2247
|
if (a === "bypass") {
|
|
2303
2248
|
const mins = parseInt(subArgs.trim(), 10);
|
|
2304
|
-
const until =
|
|
2305
|
-
ctx.ui.notify(`Guard bypass authorized until ${until}. edit/write
|
|
2249
|
+
const until = authorizeGuardBypass(Number.isFinite(mins) && mins > 0 ? mins : 15);
|
|
2250
|
+
ctx.ui.notify(`Guard bypass authorized until ${until}. edit/write advisory warnings silenced for that window (per-session, not shared).`, "info");
|
|
2306
2251
|
}
|
|
2307
2252
|
else {
|
|
2308
|
-
|
|
2309
|
-
ctx.ui.notify("Guard bypass revoked. edit/write
|
|
2253
|
+
clearGuardBypass();
|
|
2254
|
+
ctx.ui.notify("Guard bypass revoked. edit/write advisory warnings re-enabled.", "info");
|
|
2310
2255
|
}
|
|
2311
2256
|
}
|
|
2312
2257
|
catch (e) {
|
|
@@ -2334,9 +2279,23 @@ export default function planPiExtension(pi) {
|
|
|
2334
2279
|
startupResumeSummaryPending = false;
|
|
2335
2280
|
startupResumeSummaryTimer = null;
|
|
2336
2281
|
}, 60000);
|
|
2282
|
+
// Build the recap and embed it in the trigger content. The trigger
|
|
2283
|
+
// message is the channel that reliably reaches the agent on a triggered
|
|
2284
|
+
// turn (before_agent_start's systemPrompt injection is NOT applied to
|
|
2285
|
+
// triggerTurn messages), so the recap data must live here. Uses the shared
|
|
2286
|
+
// core buildRecap so Pi and Claude Code/Codex present identical content.
|
|
2287
|
+
let recapText = "";
|
|
2288
|
+
try {
|
|
2289
|
+
const st = ensureStore(ctx);
|
|
2290
|
+
const srv = server;
|
|
2291
|
+
recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
|
|
2292
|
+
}
|
|
2293
|
+
catch (e) {
|
|
2294
|
+
recapText = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
2295
|
+
}
|
|
2337
2296
|
pi.sendMessage({
|
|
2338
2297
|
customType: "planner-resume-trigger",
|
|
2339
|
-
content: "planner recap",
|
|
2298
|
+
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
2299
|
display: false,
|
|
2341
2300
|
}, {
|
|
2342
2301
|
triggerTurn: true,
|
|
@@ -2837,29 +2796,29 @@ export default function planPiExtension(pi) {
|
|
|
2837
2796
|
pi.registerTool({
|
|
2838
2797
|
name: "plan_authorize_bypass",
|
|
2839
2798
|
label: "Plan Authorize Guard Bypass",
|
|
2840
|
-
description: "Authorize a temporary guard bypass (default 15 minutes) so edit/write
|
|
2799
|
+
description: "Authorize a temporary guard bypass (default 15 minutes) so the edit/write advisory guard stops warning when no task is in-progress. Per-session in-memory: not persisted, not shared across agents. Use ONLY after the user explicitly authorizes proceeding without a task.",
|
|
2841
2800
|
parameters: Type.Object({
|
|
2842
2801
|
durationMinutes: Type.Optional(Type.Number({ description: "Bypass window in minutes. Default 15." })),
|
|
2843
2802
|
}),
|
|
2844
2803
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2845
|
-
const
|
|
2846
|
-
if (!
|
|
2804
|
+
const ok = await requirePlan(ctx);
|
|
2805
|
+
if (!ok)
|
|
2847
2806
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2848
2807
|
const mins = params.durationMinutes ?? 15;
|
|
2849
|
-
const until =
|
|
2850
|
-
return { content: [{ type: "text", text: `Guard bypass authorized until ${until}. edit/write
|
|
2808
|
+
const until = authorizeGuardBypass(mins);
|
|
2809
|
+
return { content: [{ type: "text", text: `Guard bypass authorized until ${until}. Advisory edit/write warnings silenced for ${mins} minutes (per-session, not shared).` }], details: { until } };
|
|
2851
2810
|
},
|
|
2852
2811
|
});
|
|
2853
2812
|
pi.registerTool({
|
|
2854
2813
|
name: "plan_clear_bypass",
|
|
2855
2814
|
label: "Plan Clear Guard Bypass",
|
|
2856
|
-
description: "Revoke
|
|
2815
|
+
description: "Revoke the per-session guard bypass so the edit/write advisory warnings resume.",
|
|
2857
2816
|
parameters: Type.Object({}),
|
|
2858
2817
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
2859
|
-
const
|
|
2860
|
-
if (!
|
|
2818
|
+
const ok = await requirePlan(ctx);
|
|
2819
|
+
if (!ok)
|
|
2861
2820
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2862
|
-
|
|
2821
|
+
clearGuardBypass();
|
|
2863
2822
|
return { content: [{ type: "text", text: "Guard bypass revoked." }], details: { cleared: true } };
|
|
2864
2823
|
},
|
|
2865
2824
|
});
|
|
@@ -3634,16 +3593,15 @@ export default function planPiExtension(pi) {
|
|
|
3634
3593
|
if (!server) {
|
|
3635
3594
|
await startServer(ctx, undefined, "lan").catch(() => { });
|
|
3636
3595
|
}
|
|
3596
|
+
const srv = server;
|
|
3637
3597
|
let recap = "";
|
|
3638
3598
|
try {
|
|
3639
|
-
recap = await
|
|
3599
|
+
recap = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
|
|
3640
3600
|
}
|
|
3641
3601
|
catch (e) {
|
|
3642
3602
|
recap = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
3643
3603
|
}
|
|
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 } };
|
|
3604
|
+
return { content: [{ type: "text", text: recap }], details: { enabled: true, running: Boolean(srv), localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort } };
|
|
3647
3605
|
},
|
|
3648
3606
|
});
|
|
3649
3607
|
pi.registerTool({
|
|
@@ -3789,27 +3747,16 @@ export default function planPiExtension(pi) {
|
|
|
3789
3747
|
const ambientSummary = ambient
|
|
3790
3748
|
? `node=${ambient.nodeVersion || "?"} pm=${ambient.packageManager || "?"} lockfile=${ambient.lockfile || "none"} scripts=${Object.entries(ambient.scripts).map(([k, v]) => `${k}="${v}"`).join(", ") || "none"}`
|
|
3791
3749
|
: "(not scanned)";
|
|
3750
|
+
// NOTE: the recap DATA is delivered via the /planner load trigger message
|
|
3751
|
+
// content (buildRecap), not via this system-prompt protocol — before_agent_start's
|
|
3752
|
+
// systemPrompt injection is not applied to triggerTurn messages. This protocol
|
|
3753
|
+
// is kept as a lightweight anti-narration guard for any normal recap turn.
|
|
3792
3754
|
const startupResumeProtocol = isRecapTurn ? [
|
|
3793
3755
|
"",
|
|
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).",
|
|
3756
|
+
"STARTUP RESUME PROTOCOL (mandatory):",
|
|
3757
|
+
"- 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.",
|
|
3758
|
+
`- Write in ${project.chatLanguage || "English"}.`,
|
|
3759
|
+
"- 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
3760
|
].filter(Boolean).join("\n") : "";
|
|
3814
3761
|
// Build/refresh the context block (slow path: cache miss).
|
|
3815
3762
|
const contextBlock = [
|
|
@@ -3827,6 +3774,7 @@ export default function planPiExtension(pi) {
|
|
|
3827
3774
|
"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
3775
|
"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
3776
|
"5. NO SHORTCUTS: If a tool blocks you, follow the protocol. Bypasses are for emergencies only, not for convenience.",
|
|
3777
|
+
"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
3778
|
"═══════════════════════════════════════════════════════════════",
|
|
3831
3779
|
"",
|
|
3832
3780
|
// ── 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.2",
|
|
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.2",
|
|
32
|
+
"@agent-plan/server": "0.2.19-next.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|